Merge 05eee80741
into 7fe40ea9f1
This commit is contained in:
commit
47f2c9f9be
|
@ -126,6 +126,12 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
}
|
||||
|
||||
binding.floatingRendererContainer.setOnClickListener {
|
||||
val swapVideoViewIntent =
|
||||
WebRtcCallService.swapVideoViews(this, viewModel.videoViewSwapped)
|
||||
startService(swapVideoViewIntent)
|
||||
}
|
||||
|
||||
binding.microphoneButton.setOnClickListener {
|
||||
val audioEnabledIntent =
|
||||
WebRtcCallService.microphoneIntent(this, !viewModel.microphoneEnabled)
|
||||
|
@ -330,28 +336,57 @@ 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
|
||||
binding.floatingRendererContainer.isVisible = binding.localFloatingRenderer.isVisible
|
||||
binding.videocamOffIcon.isVisible = !binding.localFloatingRenderer.isVisible
|
||||
binding.remoteRecipient.isVisible = !(binding.remoteRenderer.isVisible || binding.localRenderer.isVisible)
|
||||
binding.swapViewIcon.bringToFront()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
binding.videocamOffIcon.isVisible = !binding.remoteFloatingRenderer.isVisible
|
||||
binding.floatingRendererContainer.isVisible = binding.remoteFloatingRenderer.isVisible
|
||||
binding.remoteRecipient.isVisible = !(binding.remoteRenderer.isVisible || binding.localRenderer.isVisible)
|
||||
binding.swapViewIcon.bringToFront()
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.videoViewSwappedState.collect{ isSwapped ->
|
||||
binding.remoteRenderer.isVisible = !isSwapped && viewModel.remoteVideoEnabled
|
||||
binding.remoteFloatingRenderer.isVisible = isSwapped && viewModel.remoteVideoEnabled
|
||||
binding.localFloatingRenderer.isVisible = !isSwapped && viewModel.videoEnabled
|
||||
binding.localRenderer.isVisible = isSwapped && viewModel.videoEnabled
|
||||
binding.floatingRendererContainer.isVisible = binding.localFloatingRenderer.isVisible || binding.remoteFloatingRenderer.isVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -366,6 +401,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||
override fun onStop() {
|
||||
super.onStop()
|
||||
uiJob?.cancel()
|
||||
binding.remoteFloatingRenderer.removeAllViews()
|
||||
binding.remoteRenderer.removeAllViews()
|
||||
binding.localRenderer.removeAllViews()
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||
const val ACTION_LOCAL_HANGUP = "LOCAL_HANGUP"
|
||||
const val ACTION_SET_MUTE_AUDIO = "SET_MUTE_AUDIO"
|
||||
const val ACTION_SET_MUTE_VIDEO = "SET_MUTE_VIDEO"
|
||||
const val ACTION_SWAP_VIDEO_VIEW = "SWAP_VIDEO_VIEW"
|
||||
const val ACTION_FLIP_CAMERA = "FLIP_CAMERA"
|
||||
const val ACTION_UPDATE_AUDIO = "UPDATE_AUDIO"
|
||||
const val ACTION_WIRED_HEADSET_CHANGE = "WIRED_HEADSET_CHANGE"
|
||||
|
@ -81,6 +82,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||
const val EXTRA_RECIPIENT_ADDRESS = "RECIPIENT_ID"
|
||||
const val EXTRA_ENABLED = "ENABLED"
|
||||
const val EXTRA_AUDIO_COMMAND = "AUDIO_COMMAND"
|
||||
const val EXTRA_SWAPPED = "is_video_swapped"
|
||||
const val EXTRA_MUTE = "mute_value"
|
||||
const val EXTRA_AVAILABLE = "enabled_value"
|
||||
const val EXTRA_REMOTE_DESCRIPTION = "remote_description"
|
||||
|
@ -108,6 +110,11 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||
fun acceptCallIntent(context: Context) = Intent(context, WebRtcCallService::class.java)
|
||||
.setAction(ACTION_ANSWER_CALL)
|
||||
|
||||
fun swapVideoViews(context: Context, swapped: Boolean) =
|
||||
Intent(context, WebRtcCallService::class.java)
|
||||
.setAction(ACTION_SWAP_VIDEO_VIEW)
|
||||
.putExtra(EXTRA_SWAPPED, swapped)
|
||||
|
||||
fun microphoneIntent(context: Context, enabled: Boolean) =
|
||||
Intent(context, WebRtcCallService::class.java)
|
||||
.setAction(ACTION_SET_MUTE_AUDIO)
|
||||
|
@ -294,6 +301,7 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||
action == ACTION_DENY_CALL -> handleDenyCall(intent)
|
||||
action == ACTION_LOCAL_HANGUP -> handleLocalHangup(intent)
|
||||
action == ACTION_REMOTE_HANGUP -> handleRemoteHangup(intent)
|
||||
action == ACTION_SWAP_VIDEO_VIEW ->handleSwapVideoView(intent)
|
||||
action == ACTION_SET_MUTE_AUDIO -> handleSetMuteAudio(intent)
|
||||
action == ACTION_SET_MUTE_VIDEO -> handleSetMuteVideo(intent)
|
||||
action == ACTION_FLIP_CAMERA -> handleSetCameraFlip(intent)
|
||||
|
@ -598,6 +606,11 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
|
|||
onHangup()
|
||||
}
|
||||
|
||||
private fun handleSwapVideoView(intent: Intent) {
|
||||
val swapped = intent.getBooleanExtra(EXTRA_SWAPPED, false)
|
||||
callManager.handleSwapVideoView(swapped)
|
||||
}
|
||||
|
||||
private fun handleSetMuteAudio(intent: Intent) {
|
||||
val muted = intent.getBooleanExtra(EXTRA_MUTE, false)
|
||||
callManager.handleSetMuteAudio(muted)
|
||||
|
|
|
@ -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
|
||||
|
@ -29,6 +32,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
|
||||
|
@ -61,6 +65,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()
|
||||
|
@ -103,6 +108,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)
|
||||
|
||||
|
@ -146,8 +153,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() {
|
||||
|
@ -214,15 +223,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)
|
||||
|
@ -377,12 +397,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
|
||||
|
||||
|
@ -395,6 +419,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()
|
||||
}
|
||||
|
@ -460,7 +485,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,
|
||||
|
@ -505,7 +530,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"))
|
||||
|
||||
|
@ -595,6 +620,17 @@ 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) {
|
||||
_audioEvents.value = AudioEnabled(!muted)
|
||||
peerConnection?.setAudioEnabled(!muted)
|
||||
|
|
|
@ -32,14 +32,30 @@ 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
|
||||
get() = _videoEnabled
|
||||
|
||||
private var _remoteVideoEnabled: Boolean = false
|
||||
|
||||
val remoteVideoEnabled: Boolean
|
||||
get() = _remoteVideoEnabled
|
||||
|
||||
private var _videoViewSwapped: Boolean = false
|
||||
|
||||
val videoViewSwapped: Boolean
|
||||
get() = _videoViewSwapped
|
||||
|
||||
private var _microphoneEnabled: Boolean = true
|
||||
|
||||
val microphoneEnabled: Boolean
|
||||
|
@ -65,7 +81,14 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
|
|||
.onEach { _videoEnabled = it }
|
||||
|
||||
val remoteVideoEnabledState
|
||||
get() = callManager.remoteVideoEvents.map { it.isEnabled }
|
||||
get() = callManager.remoteVideoEvents
|
||||
.map { it.isEnabled }
|
||||
.onEach { _remoteVideoEnabled = it }
|
||||
|
||||
val videoViewSwappedState
|
||||
get() = callManager.videoViewSwappedEvents
|
||||
.map { it.isSwapped }
|
||||
.onEach { _videoViewSwapped = it }
|
||||
|
||||
var deviceRotation: Int = 0
|
||||
set(value) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,7.59l5,-5c0.78,-0.78 2.05,-0.78 2.83,0L20.24,11h-2.83L10.4,4L5.41,9H8v2H2V5h2V7.59zM20,19h2v-6h-6v2h2.59l-4.99,5l-7.01,-7H3.76l8.41,8.41c0.78,0.78 2.05,0.78 2.83,0l5,-5V19z"/>
|
||||
</vector>
|
|
@ -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,6 +117,7 @@
|
|||
android:layout_height="wrap_content"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/floating_renderer_container"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintDimensionRatio="h,9:16"
|
||||
|
@ -118,10 +125,23 @@
|
|||
android:layout_marginVertical="@dimen/massive_spacing"
|
||||
app:layout_constraintWidth_percent="0.2"
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="0dp">
|
||||
android:layout_width="0dp"
|
||||
android:visibility="invisible"
|
||||
android:background="@color/black">
|
||||
<ImageView
|
||||
android:id="@+id/videocam_off_icon"
|
||||
android:src="@drawable/ic_baseline_videocam_off_24"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
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
|
||||
|
@ -133,6 +153,16 @@
|
|||
android:layout_gravity="center"
|
||||
tools:visibility="visible"
|
||||
android:visibility="gone" />
|
||||
<ImageView
|
||||
android:id="@+id/swap_view_icon"
|
||||
android:src="@drawable/ic_baseline_screen_rotation_alt_24"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginTop="@dimen/very_small_spacing"
|
||||
android:layout_marginEnd="@dimen/very_small_spacing"
|
||||
android:layout_gravity="end"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"/>
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
|
|
Loading…
Reference in New Issue