WebRTC rework

Only using two sinks and swapping between them
Reworked the device rotation logic as it didn't work well with pitch ( you could tip the device front to back and the rotation went out of whack, so had to resort to more robust calculation for the device orientation.
Had to use a deprecated sensor setting but it's the only one I could use that works.
This commit is contained in:
ThomasSession 2024-07-05 19:09:40 +10:00
parent 72b919089a
commit 95dc1d9f54
9 changed files with 172 additions and 181 deletions

View File

@ -5,11 +5,15 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.MenuItem import android.view.MenuItem
import android.view.OrientationEventListener
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -21,7 +25,6 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import android.provider.Settings
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityWebrtcBinding import network.loki.messenger.databinding.ActivityWebrtcBinding
@ -43,11 +46,13 @@ import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_OUTGOING
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_PRE_INIT import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_PRE_INIT
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RECONNECTING import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RECONNECTING
import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RINGING import org.thoughtcrime.securesms.webrtc.CallViewModel.State.CALL_RINGING
import org.thoughtcrime.securesms.webrtc.Orientation
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.EARPIECE import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.EARPIECE
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice.SPEAKER_PHONE
import kotlin.math.asin
@AndroidEntryPoint @AndroidEntryPoint
class WebRtcCallActivity : PassphraseRequiredActionBarActivity() { class WebRtcCallActivity : PassphraseRequiredActionBarActivity(), SensorEventListener {
companion object { companion object {
const val ACTION_PRE_OFFER = "pre-offer" const val ACTION_PRE_OFFER = "pre-offer"
@ -71,16 +76,9 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
} }
private var hangupReceiver: BroadcastReceiver? = null private var hangupReceiver: BroadcastReceiver? = null
private val rotationListener by lazy { private lateinit var sensorManager: SensorManager
object : OrientationEventListener(this) { private var rotationVectorSensor: Sensor? = null
override fun onOrientationChanged(orientation: Int) { private var lastOrientation = Orientation.UNKNOWN
if ((orientation + 15) % 90 < 30) {
viewModel.deviceRotation = orientation
// updateControlsRotation(orientation.quadrantRotation() * -1)
}
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) { if (item.itemId == android.R.id.home) {
@ -104,9 +102,11 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
// Only enable auto-rotate if system auto-rotate is enabled // Only enable auto-rotate if system auto-rotate is enabled
if (isAutoRotateOn()) { if (isAutoRotateOn()) {
rotationListener.enable() // Initialize the SensorManager
} else { sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
rotationListener.disable()
// Initialize the sensors
rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
} }
binding = ActivityWebrtcBinding.inflate(layoutInflater) binding = ActivityWebrtcBinding.inflate(layoutInflater)
@ -138,7 +138,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
binding.floatingRendererContainer.setOnClickListener { binding.floatingRendererContainer.setOnClickListener {
val swapVideoViewIntent = val swapVideoViewIntent =
WebRtcCallService.swapVideoViews(this, viewModel.videoViewSwapped) WebRtcCallService.swapVideoViews(this, viewModel.toggleVideoSwap())
startService(swapVideoViewIntent) startService(swapVideoViewIntent)
} }
@ -207,12 +207,54 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
) == 1 ) == 1
} }
override fun onResume() {
super.onResume()
rotationVectorSensor?.also { sensor ->
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI)
}
}
override fun onPause() {
super.onPause()
sensorManager.unregisterListener(this)
}
override fun onSensorChanged(event: SensorEvent) {
if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
// Get the quaternion from the rotation vector sensor
val quaternion = FloatArray(4)
SensorManager.getQuaternionFromVector(quaternion, event.values)
// Calculate Euler angles from the quaternion
val pitch = asin(2.0 * (quaternion[0] * quaternion[2] - quaternion[3] * quaternion[1]))
// Convert radians to degrees
val pitchDegrees = Math.toDegrees(pitch).toFloat()
// Determine the device's orientation based on the pitch and roll values
val currentOrientation = when {
pitchDegrees > 45 -> Orientation.LANDSCAPE
pitchDegrees < -45 -> Orientation.REVERSED_LANDSCAPE
else -> Orientation.PORTRAIT
}
if (currentOrientation != lastOrientation) {
lastOrientation = currentOrientation
Log.d("", "*********** orientation: $currentOrientation")
viewModel.deviceOrientation = currentOrientation
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
hangupReceiver?.let { receiver -> hangupReceiver?.let { receiver ->
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
} }
rotationListener.disable()
rotationVectorSensor = null
} }
private fun answerCall() { private fun answerCall() {
@ -354,62 +396,31 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
launch { launch {
viewModel.localVideoEnabledState.collect { isEnabled -> viewModel.localVideoEnabledState.collect { isEnabled ->
binding.localFloatingRenderer.removeAllViews() binding.floatingRenderer.removeAllViews()
binding.localRenderer.removeAllViews()
if (isEnabled) { if (isEnabled) {
viewModel.localRenderer?.let { surfaceView -> viewModel.floatingRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true) surfaceView.setZOrderOnTop(true)
binding.floatingRenderer.addView(surfaceView)
// Mirror the video preview of the person making the call to prevent disorienting them
surfaceView.setMirror(true)
binding.localRenderer.addView(surfaceView)
}
viewModel.localFloatingRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
binding.localFloatingRenderer.addView(surfaceView)
} }
} }
binding.localFloatingRenderer.isVisible = isEnabled && !viewModel.videoViewSwapped
binding.localRenderer.isVisible = isEnabled && viewModel.videoViewSwapped binding.floatingRenderer.isVisible = isEnabled
binding.enableCameraButton.isSelected = isEnabled binding.enableCameraButton.isSelected = isEnabled
binding.floatingRendererContainer.isVisible = binding.localFloatingRenderer.isVisible //binding.swapViewIcon.bringToFront()
binding.videocamOffIcon.isVisible = !binding.localFloatingRenderer.isVisible
binding.remoteRecipient.isVisible = !(binding.remoteRenderer.isVisible || binding.localRenderer.isVisible)
binding.swapViewIcon.bringToFront()
} }
} }
launch { launch {
viewModel.remoteVideoEnabledState.collect { isEnabled -> viewModel.remoteVideoEnabledState.collect { isEnabled ->
binding.remoteRenderer.removeAllViews() binding.fullscreenRenderer.removeAllViews()
binding.remoteFloatingRenderer.removeAllViews()
if (isEnabled) { if (isEnabled) {
viewModel.remoteRenderer?.let { surfaceView -> viewModel.fullscreenRenderer?.let { surfaceView ->
binding.remoteRenderer.addView(surfaceView) binding.fullscreenRenderer.addView(surfaceView)
}
viewModel.remoteFloatingRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
binding.remoteFloatingRenderer.addView(surfaceView)
} }
} }
binding.remoteRenderer.isVisible = isEnabled && !viewModel.videoViewSwapped binding.fullscreenRenderer.isVisible = isEnabled
binding.remoteFloatingRenderer.isVisible = isEnabled && viewModel.videoViewSwapped binding.remoteRecipient.isVisible = !isEnabled
binding.videocamOffIcon.isVisible = !binding.remoteFloatingRenderer.isVisible //binding.swapViewIcon.bringToFront()
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
} }
} }
} }
@ -424,8 +435,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
uiJob?.cancel() uiJob?.cancel()
binding.remoteFloatingRenderer.removeAllViews() binding.fullscreenRenderer.removeAllViews()
binding.remoteRenderer.removeAllViews() binding.floatingRenderer.removeAllViews()
binding.localRenderer.removeAllViews()
} }
} }

View File

@ -3,10 +3,7 @@ package org.thoughtcrime.securesms.webrtc
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.view.SurfaceView
import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -34,7 +31,6 @@ import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioDeviceUpdat
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioEnabled import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioEnabled
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.RecipientUpdate import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.RecipientUpdate
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.VideoEnabled 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.AudioManagerCompat
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
@ -114,8 +110,6 @@ class CallManager(
val videoEvents = _videoEvents.asSharedFlow() val videoEvents = _videoEvents.asSharedFlow()
private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false)) private val _remoteVideoEvents = MutableStateFlow(VideoEnabled(false))
val remoteVideoEvents = _remoteVideoEvents.asSharedFlow() val remoteVideoEvents = _remoteVideoEvents.asSharedFlow()
private val _videoViewSwappedEvents = MutableStateFlow(VideoSwapped(false))
val videoViewSwappedEvents = _videoViewSwappedEvents.asSharedFlow()
private val stateProcessor = StateProcessor(CallState.Idle) private val stateProcessor = StateProcessor(CallState.Idle)
@ -158,13 +152,14 @@ class CallManager(
private val outgoingIceDebouncer = Debouncer(200L) private val outgoingIceDebouncer = Debouncer(200L)
var localRenderer: SurfaceViewRenderer? = null var floatingRenderer: SurfaceViewRenderer? = null
var localFloatingRenderer: SurfaceViewRenderer? = null
var remoteRotationSink: RemoteRotationVideoProxySink? = null var remoteRotationSink: RemoteRotationVideoProxySink? = null
var remoteRenderer: SurfaceViewRenderer? = null var fullscreenRenderer: SurfaceViewRenderer? = null
var remoteFloatingRenderer: SurfaceViewRenderer? = null
private var peerConnectionFactory: PeerConnectionFactory? = null private var peerConnectionFactory: PeerConnectionFactory? = null
// false when the user's video is in the floating render and true when it's in fullscreen
private var videoSwapped: Boolean = false
fun clearPendingIceUpdates() { fun clearPendingIceUpdates() {
pendingOutgoingIceUpdates.clear() pendingOutgoingIceUpdates.clear()
pendingIncomingIceUpdates.clear() pendingIncomingIceUpdates.clear()
@ -225,31 +220,16 @@ class CallManager(
Util.runOnMainSync { Util.runOnMainSync {
val base = EglBase.create() val base = EglBase.create()
eglBase = base eglBase = base
localRenderer = SurfaceViewRenderer(context).apply { floatingRenderer = SurfaceViewRenderer(context)
// setScalingType(SCALE_ASPECT_FIT)
}
localFloatingRenderer = SurfaceViewRenderer(context).apply { fullscreenRenderer = SurfaceViewRenderer(context)
// setScalingType(SCALE_ASPECT_FIT)
}
remoteRenderer = SurfaceViewRenderer(context).apply {
// setScalingType(SCALE_ASPECT_FIT)
}
remoteFloatingRenderer = SurfaceViewRenderer(context).apply {
// setScalingType(SCALE_ASPECT_FIT)
}
remoteRotationSink = RemoteRotationVideoProxySink() remoteRotationSink = RemoteRotationVideoProxySink()
localRenderer?.init(base.eglBaseContext, null) floatingRenderer?.init(base.eglBaseContext, null)
localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT) fullscreenRenderer?.init(base.eglBaseContext, null)
localFloatingRenderer?.init(base.eglBaseContext, null) remoteRotationSink!!.setSink(fullscreenRenderer!!)
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) val encoderFactory = DefaultVideoEncoderFactory(base.eglBaseContext, true, true)
val decoderFactory = DefaultVideoDecoderFactory(base.eglBaseContext) val decoderFactory = DefaultVideoDecoderFactory(base.eglBaseContext)
@ -403,17 +383,13 @@ class CallManager(
peerConnection?.dispose() peerConnection?.dispose()
peerConnection = null peerConnection = null
localFloatingRenderer?.release() floatingRenderer?.release()
localRenderer?.release()
remoteRotationSink?.release() remoteRotationSink?.release()
remoteFloatingRenderer?.release() fullscreenRenderer?.release()
remoteRenderer?.release()
eglBase?.release() eglBase?.release()
localFloatingRenderer = null floatingRenderer = null
localRenderer = null fullscreenRenderer = null
remoteFloatingRenderer = null
remoteRenderer = null
eglBase = null eglBase = null
localCameraState = CameraState.UNKNOWN localCameraState = CameraState.UNKNOWN
@ -425,7 +401,6 @@ class CallManager(
_audioEvents.value = AudioEnabled(false) _audioEvents.value = AudioEnabled(false)
_videoEvents.value = VideoEnabled(false) _videoEvents.value = VideoEnabled(false)
_remoteVideoEvents.value = VideoEnabled(false) _remoteVideoEvents.value = VideoEnabled(false)
_videoViewSwappedEvents.value = VideoSwapped(false)
pendingOutgoingIceUpdates.clear() pendingOutgoingIceUpdates.clear()
pendingIncomingIceUpdates.clear() pendingIncomingIceUpdates.clear()
} }
@ -436,7 +411,7 @@ class CallManager(
// If the camera we've switched to is the front one then mirror it to match what someone // If the camera we've switched to is the front one then mirror it to match what someone
// would see when looking in the mirror rather than the left<-->right flipped version. // would see when looking in the mirror rather than the left<-->right flipped version.
localRenderer?.setMirror(localCameraState.activeDirection == CameraState.Direction.FRONT) handleUserMirroring()
} }
fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) { fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) {
@ -494,7 +469,7 @@ class CallManager(
val recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null")) val recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null"))
val offer = pendingOffer ?: return Promise.ofFail(NullPointerException("pendingOffer is null")) val offer = pendingOffer ?: return Promise.ofFail(NullPointerException("pendingOffer is null"))
val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null"))
val local = localFloatingRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null")) val local = floatingRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null"))
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null")) val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null"))
val connection = PeerConnectionWrapper( val connection = PeerConnectionWrapper(
context, context,
@ -540,7 +515,7 @@ class CallManager(
?: return Promise.ofFail(NullPointerException("recipient is null")) ?: return Promise.ofFail(NullPointerException("recipient is null"))
val factory = peerConnectionFactory val factory = peerConnectionFactory
?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null"))
val local = localFloatingRenderer val local = floatingRenderer
?: return Promise.ofFail(NullPointerException("localRenderer is null")) ?: return Promise.ofFail(NullPointerException("localRenderer is null"))
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null")) val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null"))
@ -635,13 +610,16 @@ class CallManager(
} }
fun handleSwapVideoView(swapped: Boolean) { fun handleSwapVideoView(swapped: Boolean) {
_videoViewSwappedEvents.value = VideoSwapped(!swapped) videoSwapped = swapped
if (!swapped) { if (!swapped) {
peerConnection?.rotationVideoSink?.setSink(localRenderer) peerConnection?.rotationVideoSink?.apply {
remoteRotationSink?.setSink(remoteFloatingRenderer!!) setSink(floatingRenderer)
}
fullscreenRenderer?.let{ remoteRotationSink?.setSink(it) }
} else { } else {
peerConnection?.rotationVideoSink?.setSink(localFloatingRenderer) peerConnection?.rotationVideoSink?.setSink(fullscreenRenderer)
remoteRotationSink?.setSink(remoteRenderer!!) floatingRenderer?.let{remoteRotationSink?.setSink(it) }
} }
} }
@ -650,8 +628,24 @@ class CallManager(
peerConnection?.setAudioEnabled(!muted) peerConnection?.setAudioEnabled(!muted)
} }
/**
* Returns the renderer currently showing the user's video, not the contact's
*/
private fun getUserRenderer() = if(videoSwapped) fullscreenRenderer else floatingRenderer
/**
* Makes sure the user's renderer applies mirroring if necessary
*/
private fun handleUserMirroring() = getUserRenderer()?.setMirror(isCameraFrontFacing())
fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) { fun handleSetMuteVideo(muted: Boolean, lockManager: LockManager) {
_videoEvents.value = VideoEnabled(!muted) _videoEvents.value = VideoEnabled(!muted)
// if we have video and the camera is not front facing, make sure to mirror stream
if(!muted){
handleUserMirroring()
}
val connection = peerConnection ?: return val connection = peerConnection ?: return
connection.setVideoEnabled(!muted) connection.setVideoEnabled(!muted)
dataChannel?.let { channel -> dataChannel?.let { channel ->
@ -687,9 +681,19 @@ class CallManager(
} }
} }
fun setDeviceRotation(newRotation: Int) { fun setDeviceOrientation(orientation: Orientation) {
peerConnection?.setDeviceRotation(newRotation) // set rotation to the video based on the device's orientation and the camera facing direction
remoteRotationSink?.rotation = newRotation val rotation = when{
orientation == Orientation.PORTRAIT -> 0
orientation == Orientation.LANDSCAPE && isCameraFrontFacing() -> 90
orientation == Orientation.LANDSCAPE && !isCameraFrontFacing() -> -90
orientation == Orientation.REVERSED_LANDSCAPE -> 270
else -> 0
}
// apply the rotation to the streams
peerConnection?.setDeviceRotation(rotation)
remoteRotationSink?.rotation = rotation
} }
fun handleWiredHeadsetChanged(present: Boolean) { fun handleWiredHeadsetChanged(present: Boolean) {
@ -786,6 +790,8 @@ class CallManager(
fun isInitiator(): Boolean = peerConnection?.isInitiator() == true fun isInitiator(): Boolean = peerConnection?.isInitiator() == true
fun isCameraFrontFacing() = localCameraState.activeDirection == CameraState.Direction.FRONT
interface WebRtcListener: PeerConnection.Observer { interface WebRtcListener: PeerConnection.Observer {
fun onHangup() fun onHangup()
} }

View File

@ -29,17 +29,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
UNTRUSTED_IDENTITY, UNTRUSTED_IDENTITY,
} }
val localRenderer: SurfaceViewRenderer? val floatingRenderer: SurfaceViewRenderer?
get() = callManager.localRenderer get() = callManager.floatingRenderer
val localFloatingRenderer: SurfaceViewRenderer? val fullscreenRenderer: SurfaceViewRenderer?
get() = callManager.localFloatingRenderer get() = callManager.fullscreenRenderer
val remoteRenderer: SurfaceViewRenderer?
get() = callManager.remoteRenderer
val remoteFloatingRenderer: SurfaceViewRenderer?
get() = callManager.remoteFloatingRenderer
private var _videoEnabled: Boolean = false private var _videoEnabled: Boolean = false
@ -48,14 +42,8 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
private var _remoteVideoEnabled: Boolean = false private var _remoteVideoEnabled: Boolean = false
val remoteVideoEnabled: Boolean
get() = _remoteVideoEnabled
private var _videoViewSwapped: Boolean = false private var _videoViewSwapped: Boolean = false
val videoViewSwapped: Boolean
get() = _videoViewSwapped
private var _microphoneEnabled: Boolean = true private var _microphoneEnabled: Boolean = true
val microphoneEnabled: Boolean val microphoneEnabled: Boolean
@ -85,15 +73,10 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
.map { it.isEnabled } .map { it.isEnabled }
.onEach { _remoteVideoEnabled = it } .onEach { _remoteVideoEnabled = it }
val videoViewSwappedState var deviceOrientation: Orientation = Orientation.UNKNOWN
get() = callManager.videoViewSwappedEvents
.map { it.isSwapped }
.onEach { _videoViewSwapped = it }
var deviceRotation: Int = 0
set(value) { set(value) {
field = value field = value
callManager.setDeviceRotation(value) callManager.setDeviceOrientation(value)
} }
val currentCallState val currentCallState
@ -108,4 +91,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
val callStartTime: Long val callStartTime: Long
get() = callManager.callStartTime get() = callManager.callStartTime
/**
* Toggles the video swapped state, and return the value post toggle
*/
fun toggleVideoSwap(): Boolean {
_videoViewSwapped = !_videoViewSwapped
return _videoViewSwapped
}
} }

View File

@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.webrtc
enum class Orientation {
PORTRAIT,
LANDSCAPE,
REVERSED_LANDSCAPE,
UNKNOWN
}

View File

@ -103,7 +103,7 @@ class PeerConnectionWrapper(private val context: Context,
context, context,
rotationVideoSink rotationVideoSink
) )
rotationVideoSink.mirrored = newCamera.activeDirection == CameraState.Direction.FRONT
rotationVideoSink.setSink(localRenderer) rotationVideoSink.setSink(localRenderer)
newVideoTrack.setEnabled(false) newVideoTrack.setEnabled(false)
mediaStream.addTrack(newVideoTrack) mediaStream.addTrack(newVideoTrack)

View File

@ -1,11 +0,0 @@
package org.thoughtcrime.securesms.webrtc.data
// get the video rotation from a specific rotation, locked into 90 degree
// chunks offset by 45 degrees
fun Int.quadrantRotation() = when (this % 360) {
in 315 .. 360,
in 0 until 45 -> 0
in 45 until 135 -> 90
in 135 until 225 -> 180
else -> 270
}

View File

@ -1,6 +1,6 @@
package org.thoughtcrime.securesms.webrtc.video package org.thoughtcrime.securesms.webrtc.video
import org.thoughtcrime.securesms.webrtc.data.quadrantRotation
import org.webrtc.VideoFrame import org.webrtc.VideoFrame
import org.webrtc.VideoSink import org.webrtc.VideoSink
@ -14,8 +14,7 @@ class RemoteRotationVideoProxySink: VideoSink {
val thisSink = targetSink ?: return val thisSink = targetSink ?: return
val thisFrame = frame ?: return val thisFrame = frame ?: return
val quadrantRotation = rotation.quadrantRotation() val modifiedRotation = thisFrame.rotation - rotation
val modifiedRotation = thisFrame.rotation - quadrantRotation
val newFrame = VideoFrame(thisFrame.buffer, modifiedRotation, thisFrame.timestampNs) val newFrame = VideoFrame(thisFrame.buffer, modifiedRotation, thisFrame.timestampNs)
thisSink.onFrame(newFrame) thisSink.onFrame(newFrame)

View File

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.webrtc.video package org.thoughtcrime.securesms.webrtc.video
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.webrtc.data.quadrantRotation
import org.webrtc.CapturerObserver import org.webrtc.CapturerObserver
import org.webrtc.VideoFrame import org.webrtc.VideoFrame
import org.webrtc.VideoProcessor import org.webrtc.VideoProcessor
@ -12,7 +11,6 @@ import java.util.concurrent.atomic.AtomicBoolean
class RotationVideoSink: CapturerObserver, VideoProcessor { class RotationVideoSink: CapturerObserver, VideoProcessor {
var rotation: Int = 0 var rotation: Int = 0
var mirrored = false
private val capturing = AtomicBoolean(false) private val capturing = AtomicBoolean(false)
private var capturerObserver = SoftReference<CapturerObserver>(null) private var capturerObserver = SoftReference<CapturerObserver>(null)
@ -31,13 +29,14 @@ class RotationVideoSink: CapturerObserver, VideoProcessor {
val observer = capturerObserver.get() val observer = capturerObserver.get()
if (videoFrame == null || observer == null || !capturing.get()) return if (videoFrame == null || observer == null || !capturing.get()) return
val quadrantRotation = rotation.quadrantRotation() // cater for frame rotation so that the video is always facing up as we rotate pas a certain point
val newFrame = VideoFrame(videoFrame.buffer, videoFrame.rotation - rotation, videoFrame.timestampNs)
val newFrame = VideoFrame(videoFrame.buffer, (videoFrame.rotation + quadrantRotation * if (mirrored && quadrantRotation in listOf(90,270)) -1 else 1) % 360, videoFrame.timestampNs)
val localFrame = VideoFrame(videoFrame.buffer, videoFrame.rotation * if (mirrored && quadrantRotation in listOf(90,270)) -1 else 1, videoFrame.timestampNs)
// the frame we are sending to our contact needs to cater for rotation
observer.onFrameCaptured(newFrame) observer.onFrameCaptured(newFrame)
sink.get()?.onFrame(localFrame)
// the frame we see on the user's phone doesn't require changes
sink.get()?.onFrame(videoFrame)
} }
override fun setSink(sink: VideoSink?) { override fun setSink(sink: VideoSink?) {

View File

@ -8,7 +8,7 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<FrameLayout <FrameLayout
android:id="@+id/remote_parent" android:id="@+id/fullscreen_renderer_container"
android:background="@color/black" android:background="@color/black"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -18,23 +18,17 @@
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<FrameLayout <FrameLayout
android:id="@+id/remote_renderer" android:id="@+id/fullscreen_renderer"
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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center"/> android:layout_gravity="center"/>
</FrameLayout> </FrameLayout>
<ImageView <ImageView
android:id="@+id/remote_recipient" android:id="@+id/remote_recipient"
app:layout_constraintStart_toStartOf="@id/remote_parent" app:layout_constraintStart_toStartOf="@id/fullscreen_renderer_container"
app:layout_constraintEnd_toEndOf="@id/remote_parent" app:layout_constraintEnd_toEndOf="@id/fullscreen_renderer_container"
app:layout_constraintTop_toTopOf="@id/remote_parent" app:layout_constraintTop_toTopOf="@id/fullscreen_renderer_container"
app:layout_constraintBottom_toBottomOf="@id/remote_parent" app:layout_constraintBottom_toBottomOf="@id/fullscreen_renderer_container"
app:layout_constraintVertical_bias="0.4" app:layout_constraintVertical_bias="0.4"
android:layout_width="@dimen/extra_large_profile_picture_size" android:layout_width="@dimen/extra_large_profile_picture_size"
android:layout_height="@dimen/extra_large_profile_picture_size"/> android:layout_height="@dimen/extra_large_profile_picture_size"/>
@ -126,22 +120,17 @@
app:layout_constraintWidth_percent="0.2" app:layout_constraintWidth_percent="0.2"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_width="0dp" android:layout_width="0dp"
android:visibility="invisible" android:background="?backgroundSecondary">
android:background="@color/black">
<ImageView <ImageView
android:id="@+id/videocam_off_icon" android:id="@+id/videocam_off_icon"
android:src="@drawable/ic_baseline_videocam_off_24" android:src="@drawable/ic_baseline_videocam_off_24"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:layout_gravity="center"/> android:layout_gravity="center"
app:tint="?android:textColorPrimary"/>
<FrameLayout <FrameLayout
android:elevation="8dp" android:elevation="8dp"
android:id="@+id/local_floating_renderer" android:id="@+id/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_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
<com.github.ybq.android.spinkit.SpinKitView <com.github.ybq.android.spinkit.SpinKitView
@ -158,6 +147,7 @@
android:src="@drawable/ic_baseline_screen_rotation_alt_24" android:src="@drawable/ic_baseline_screen_rotation_alt_24"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:tint="?android:textColorPrimary"
android:layout_marginTop="@dimen/very_small_spacing" android:layout_marginTop="@dimen/very_small_spacing"
android:layout_marginEnd="@dimen/very_small_spacing" android:layout_marginEnd="@dimen/very_small_spacing"
android:layout_gravity="end" android:layout_gravity="end"