feat: swap video views

This commit is contained in:
Ryan Zhao
2023-02-20 16:58:27 +11:00
parent c474414b03
commit 39b798055c
5 changed files with 86 additions and 11 deletions

View File

@@ -126,7 +126,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(false)
}
binding.localRendererContainer.setOnClickListener {
binding.floatingRendererContainer.setOnClickListener {
val swapVideoViewIntent =
WebRtcCallService.swapVideoViews(this, viewModel.videoViewSwapped)
startService(swapVideoViewIntent)
@@ -341,14 +341,20 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
launch {
viewModel.localVideoEnabledState.collect { isEnabled ->
binding.localFloatingRenderer.removeAllViews()
binding.localRenderer.removeAllViews()
if (isEnabled) {
viewModel.localRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
binding.localRenderer.addView(surfaceView)
}
viewModel.localFloatingRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
binding.localFloatingRenderer.addView(surfaceView)
}
}
binding.localRenderer.isVisible = isEnabled
binding.localFloatingRenderer.isVisible = isEnabled && !viewModel.videoViewSwapped
binding.localRenderer.isVisible = isEnabled && viewModel.videoViewSwapped
binding.enableCameraButton.isSelected = isEnabled
}
}
@@ -356,13 +362,27 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
launch {
viewModel.remoteVideoEnabledState.collect { isEnabled ->
binding.remoteRenderer.removeAllViews()
binding.remoteFloatingRenderer.removeAllViews()
if (isEnabled) {
viewModel.remoteRenderer?.let { surfaceView ->
binding.remoteRenderer.addView(surfaceView)
}
viewModel.remoteFloatingRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
binding.remoteFloatingRenderer.addView(surfaceView)
}
}
binding.remoteRenderer.isVisible = isEnabled
binding.remoteRecipient.isVisible = !isEnabled
binding.remoteRenderer.isVisible = isEnabled && !viewModel.videoViewSwapped
binding.remoteFloatingRenderer.isVisible = isEnabled && viewModel.videoViewSwapped
}
}
launch {
viewModel.videoViewSwappedState.collect{ isSwapped ->
binding.remoteRenderer.isVisible = !isSwapped
binding.remoteFloatingRenderer.isVisible = isSwapped
binding.localFloatingRenderer.isVisible = !isSwapped
binding.localRenderer.isVisible = isSwapped
}
}
}
@@ -377,6 +397,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
override fun onStop() {
super.onStop()
uiJob?.cancel()
binding.remoteFloatingRenderer.removeAllViews()
binding.remoteRenderer.removeAllViews()
binding.localRenderer.removeAllViews()
}

View File

@@ -3,7 +3,10 @@ package org.thoughtcrime.securesms.webrtc
import android.content.Context
import android.content.pm.PackageManager
import android.telephony.TelephonyManager
import android.view.SurfaceView
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.serialization.json.Json
@@ -28,6 +31,7 @@ import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioDeviceUpdat
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioEnabled
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.RecipientUpdate
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.VideoEnabled
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.VideoSwapped
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
@@ -60,6 +64,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
SignalAudioManager.EventListener, CameraEventListener, DataChannel.Observer {
sealed class StateEvent {
data class VideoSwapped(val isSwapped: Boolean): StateEvent()
data class AudioEnabled(val isEnabled: Boolean): StateEvent()
data class VideoEnabled(val isEnabled: Boolean): StateEvent()
data class CallStateUpdate(val state: CallState): StateEvent()
@@ -98,6 +103,8 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
val videoEvents = _videoEvents.asSharedFlow()
private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false))
val remoteVideoEvents = _remoteVideoEvents.asSharedFlow()
private val _videoViewSwappedEvents = MutableStateFlow(VideoSwapped(false))
val videoViewSwappedEvents = _videoViewSwappedEvents.asSharedFlow()
private val stateProcessor = StateProcessor(CallState.Idle)
@@ -141,8 +148,10 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
private val outgoingIceDebouncer = Debouncer(200L)
var localRenderer: SurfaceViewRenderer? = null
var localFloatingRenderer: SurfaceViewRenderer? = null
var remoteRotationSink: RemoteRotationVideoProxySink? = null
var remoteRenderer: SurfaceViewRenderer? = null
var remoteFloatingRenderer: SurfaceViewRenderer? = null
private var peerConnectionFactory: PeerConnectionFactory? = null
fun clearPendingIceUpdates() {
@@ -209,15 +218,26 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
// setScalingType(SCALE_ASPECT_FIT)
}
localFloatingRenderer = SurfaceViewRenderer(context).apply {
// setScalingType(SCALE_ASPECT_FIT)
}
remoteRenderer = SurfaceViewRenderer(context).apply {
// setScalingType(SCALE_ASPECT_FIT)
}
remoteFloatingRenderer = SurfaceViewRenderer(context).apply {
// setScalingType(SCALE_ASPECT_FIT)
}
remoteRotationSink = RemoteRotationVideoProxySink()
localRenderer?.init(base.eglBaseContext, null)
localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT)
localFloatingRenderer?.init(base.eglBaseContext, null)
localFloatingRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT)
remoteRenderer?.init(base.eglBaseContext, null)
remoteFloatingRenderer?.init(base.eglBaseContext, null)
remoteRotationSink!!.setSink(remoteRenderer!!)
val encoderFactory = DefaultVideoEncoderFactory(base.eglBaseContext, true, true)
@@ -372,12 +392,16 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
peerConnection?.dispose()
peerConnection = null
localFloatingRenderer?.release()
localRenderer?.release()
remoteRotationSink?.release()
remoteFloatingRenderer?.release()
remoteRenderer?.release()
eglBase?.release()
localFloatingRenderer = null
localRenderer = null
remoteFloatingRenderer = null
remoteRenderer = null
eglBase = null
@@ -390,6 +414,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
_audioEvents.value = AudioEnabled(false)
_videoEvents.value = VideoEnabled(false)
_remoteVideoEvents.value = VideoEnabled(false)
_videoViewSwappedEvents.value = VideoSwapped(false)
pendingOutgoingIceUpdates.clear()
pendingIncomingIceUpdates.clear()
}
@@ -455,7 +480,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
val recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null"))
val offer = pendingOffer ?: return Promise.ofFail(NullPointerException("pendingOffer is null"))
val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null"))
val local = localRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null"))
val local = localFloatingRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null"))
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null"))
val connection = PeerConnectionWrapper(
context,
@@ -500,7 +525,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
?: return Promise.ofFail(NullPointerException("recipient is null"))
val factory = peerConnectionFactory
?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null"))
val local = localRenderer
val local = localFloatingRenderer
?: return Promise.ofFail(NullPointerException("localRenderer is null"))
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null"))
@@ -591,7 +616,14 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
}
fun handleSwapVideoView(swapped: Boolean) {
_videoViewSwappedEvents.value = VideoSwapped(!swapped)
if (!swapped) {
peerConnection?.rotationVideoSink?.setSink(localRenderer)
remoteRotationSink?.setSink(remoteFloatingRenderer!!)
} else {
peerConnection?.rotationVideoSink?.setSink(localFloatingRenderer)
remoteRotationSink?.setSink(remoteRenderer!!)
}
}
fun handleSetMuteAudio(muted: Boolean) {

View File

@@ -32,9 +32,15 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
val localRenderer: SurfaceViewRenderer?
get() = callManager.localRenderer
val localFloatingRenderer: SurfaceViewRenderer?
get() = callManager.localFloatingRenderer
val remoteRenderer: SurfaceViewRenderer?
get() = callManager.remoteRenderer
val remoteFloatingRenderer: SurfaceViewRenderer?
get() = callManager.remoteFloatingRenderer
private var _videoEnabled: Boolean = false
val videoEnabled: Boolean
@@ -72,6 +78,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
val remoteVideoEnabledState
get() = callManager.remoteVideoEvents.map { it.isEnabled }
val videoViewSwappedState
get() = callManager.videoViewSwappedEvents
.map { it.isSwapped }
.onEach { _videoViewSwapped = it }
var deviceRotation: Int = 0
set(value) {
field = value

View File

@@ -41,7 +41,7 @@ class PeerConnectionWrapper(private val context: Context,
private val mediaStream: MediaStream
private val videoSource: VideoSource?
private val videoTrack: VideoTrack?
private val rotationVideoSink = RotationVideoSink()
public val rotationVideoSink = RotationVideoSink()
val readyForIce
get() = peerConnection?.localDescription != null && peerConnection?.remoteDescription != null

View File

@@ -22,6 +22,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
<FrameLayout
android:id="@+id/local_renderer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
</FrameLayout>
<ImageView
android:id="@+id/remote_recipient"
@@ -111,7 +117,7 @@
android:layout_height="wrap_content"/>
<FrameLayout
android:id="@+id/local_renderer_container"
android:id="@+id/floating_renderer_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintDimensionRatio="h,9:16"
@@ -127,7 +133,12 @@
android:layout_gravity="center"/>
<FrameLayout
android:elevation="8dp"
android:id="@+id/local_renderer"
android:id="@+id/local_floating_renderer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<FrameLayout
android:elevation="8dp"
android:id="@+id/remote_floating_renderer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.github.ybq.android.spinkit.SpinKitView