mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-30 13:35:18 +00:00
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:
parent
72b919089a
commit
95dc1d9f54
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package org.thoughtcrime.securesms.webrtc
|
||||||
|
|
||||||
|
enum class Orientation {
|
||||||
|
PORTRAIT,
|
||||||
|
LANDSCAPE,
|
||||||
|
REVERSED_LANDSCAPE,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
||||||
|
@ -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?) {
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user