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.Intent
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.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.MenuItem
import android.view.OrientationEventListener
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
@ -21,7 +25,6 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import android.provider.Settings
import kotlinx.coroutines.launch
import network.loki.messenger.R
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_RECONNECTING
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.SPEAKER_PHONE
import kotlin.math.asin
@AndroidEntryPoint
class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
class WebRtcCallActivity : PassphraseRequiredActionBarActivity(), SensorEventListener {
companion object {
const val ACTION_PRE_OFFER = "pre-offer"
@ -71,16 +76,9 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
}
private var hangupReceiver: BroadcastReceiver? = null
private val rotationListener by lazy {
object : OrientationEventListener(this) {
override fun onOrientationChanged(orientation: Int) {
if ((orientation + 15) % 90 < 30) {
viewModel.deviceRotation = orientation
// updateControlsRotation(orientation.quadrantRotation() * -1)
}
}
}
}
private lateinit var sensorManager: SensorManager
private var rotationVectorSensor: Sensor? = null
private var lastOrientation = Orientation.UNKNOWN
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
@ -104,9 +102,11 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
// Only enable auto-rotate if system auto-rotate is enabled
if (isAutoRotateOn()) {
rotationListener.enable()
} else {
rotationListener.disable()
// Initialize the SensorManager
sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
// Initialize the sensors
rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
}
binding = ActivityWebrtcBinding.inflate(layoutInflater)
@ -138,7 +138,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
binding.floatingRendererContainer.setOnClickListener {
val swapVideoViewIntent =
WebRtcCallService.swapVideoViews(this, viewModel.videoViewSwapped)
WebRtcCallService.swapVideoViews(this, viewModel.toggleVideoSwap())
startService(swapVideoViewIntent)
}
@ -207,12 +207,54 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
) == 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() {
super.onDestroy()
hangupReceiver?.let { receiver ->
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
}
rotationListener.disable()
rotationVectorSensor = null
}
private fun answerCall() {
@ -354,62 +396,31 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
launch {
viewModel.localVideoEnabledState.collect { isEnabled ->
binding.localFloatingRenderer.removeAllViews()
binding.localRenderer.removeAllViews()
binding.floatingRenderer.removeAllViews()
if (isEnabled) {
viewModel.localRenderer?.let { surfaceView ->
viewModel.floatingRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
// 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.floatingRenderer.addView(surfaceView)
}
}
binding.localFloatingRenderer.isVisible = isEnabled && !viewModel.videoViewSwapped
binding.localRenderer.isVisible = isEnabled && viewModel.videoViewSwapped
binding.floatingRenderer.isVisible = isEnabled
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()
//binding.swapViewIcon.bringToFront()
}
}
launch {
viewModel.remoteVideoEnabledState.collect { isEnabled ->
binding.remoteRenderer.removeAllViews()
binding.remoteFloatingRenderer.removeAllViews()
binding.fullscreenRenderer.removeAllViews()
if (isEnabled) {
viewModel.remoteRenderer?.let { surfaceView ->
binding.remoteRenderer.addView(surfaceView)
}
viewModel.remoteFloatingRenderer?.let { surfaceView ->
surfaceView.setZOrderOnTop(true)
binding.remoteFloatingRenderer.addView(surfaceView)
viewModel.fullscreenRenderer?.let { surfaceView ->
binding.fullscreenRenderer.addView(surfaceView)
}
}
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
binding.fullscreenRenderer.isVisible = isEnabled
binding.remoteRecipient.isVisible = !isEnabled
//binding.swapViewIcon.bringToFront()
}
}
}
@ -424,8 +435,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
override fun onStop() {
super.onStop()
uiJob?.cancel()
binding.remoteFloatingRenderer.removeAllViews()
binding.remoteRenderer.removeAllViews()
binding.localRenderer.removeAllViews()
binding.fullscreenRenderer.removeAllViews()
binding.floatingRenderer.removeAllViews()
}
}

View File

@ -3,10 +3,7 @@ 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
@ -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.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
@ -114,8 +110,6 @@ class CallManager(
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)
@ -158,13 +152,14 @@ class CallManager(
private val outgoingIceDebouncer = Debouncer(200L)
var localRenderer: SurfaceViewRenderer? = null
var localFloatingRenderer: SurfaceViewRenderer? = null
var floatingRenderer: SurfaceViewRenderer? = null
var remoteRotationSink: RemoteRotationVideoProxySink? = null
var remoteRenderer: SurfaceViewRenderer? = null
var remoteFloatingRenderer: SurfaceViewRenderer? = null
var fullscreenRenderer: SurfaceViewRenderer? = 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() {
pendingOutgoingIceUpdates.clear()
pendingIncomingIceUpdates.clear()
@ -225,31 +220,16 @@ class CallManager(
Util.runOnMainSync {
val base = EglBase.create()
eglBase = base
localRenderer = SurfaceViewRenderer(context).apply {
// setScalingType(SCALE_ASPECT_FIT)
}
floatingRenderer = SurfaceViewRenderer(context)
localFloatingRenderer = SurfaceViewRenderer(context).apply {
// setScalingType(SCALE_ASPECT_FIT)
}
remoteRenderer = SurfaceViewRenderer(context).apply {
// setScalingType(SCALE_ASPECT_FIT)
}
remoteFloatingRenderer = SurfaceViewRenderer(context).apply {
// setScalingType(SCALE_ASPECT_FIT)
}
fullscreenRenderer = SurfaceViewRenderer(context)
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!!)
floatingRenderer?.init(base.eglBaseContext, null)
fullscreenRenderer?.init(base.eglBaseContext, null)
remoteRotationSink!!.setSink(fullscreenRenderer!!)
val encoderFactory = DefaultVideoEncoderFactory(base.eglBaseContext, true, true)
val decoderFactory = DefaultVideoDecoderFactory(base.eglBaseContext)
@ -403,17 +383,13 @@ class CallManager(
peerConnection?.dispose()
peerConnection = null
localFloatingRenderer?.release()
localRenderer?.release()
floatingRenderer?.release()
remoteRotationSink?.release()
remoteFloatingRenderer?.release()
remoteRenderer?.release()
fullscreenRenderer?.release()
eglBase?.release()
localFloatingRenderer = null
localRenderer = null
remoteFloatingRenderer = null
remoteRenderer = null
floatingRenderer = null
fullscreenRenderer = null
eglBase = null
localCameraState = CameraState.UNKNOWN
@ -425,7 +401,6 @@ class CallManager(
_audioEvents.value = AudioEnabled(false)
_videoEvents.value = VideoEnabled(false)
_remoteVideoEvents.value = VideoEnabled(false)
_videoViewSwappedEvents.value = VideoSwapped(false)
pendingOutgoingIceUpdates.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
// 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) {
@ -494,7 +469,7 @@ class CallManager(
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 = 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 connection = PeerConnectionWrapper(
context,
@ -540,7 +515,7 @@ class CallManager(
?: return Promise.ofFail(NullPointerException("recipient is null"))
val factory = peerConnectionFactory
?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null"))
val local = localFloatingRenderer
val local = floatingRenderer
?: return Promise.ofFail(NullPointerException("localRenderer is null"))
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null"))
@ -635,13 +610,16 @@ class CallManager(
}
fun handleSwapVideoView(swapped: Boolean) {
_videoViewSwappedEvents.value = VideoSwapped(!swapped)
videoSwapped = swapped
if (!swapped) {
peerConnection?.rotationVideoSink?.setSink(localRenderer)
remoteRotationSink?.setSink(remoteFloatingRenderer!!)
peerConnection?.rotationVideoSink?.apply {
setSink(floatingRenderer)
}
fullscreenRenderer?.let{ remoteRotationSink?.setSink(it) }
} else {
peerConnection?.rotationVideoSink?.setSink(localFloatingRenderer)
remoteRotationSink?.setSink(remoteRenderer!!)
peerConnection?.rotationVideoSink?.setSink(fullscreenRenderer)
floatingRenderer?.let{remoteRotationSink?.setSink(it) }
}
}
@ -650,8 +628,24 @@ class CallManager(
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) {
_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
connection.setVideoEnabled(!muted)
dataChannel?.let { channel ->
@ -687,9 +681,19 @@ class CallManager(
}
}
fun setDeviceRotation(newRotation: Int) {
peerConnection?.setDeviceRotation(newRotation)
remoteRotationSink?.rotation = newRotation
fun setDeviceOrientation(orientation: Orientation) {
// set rotation to the video based on the device's orientation and the camera facing direction
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) {
@ -786,6 +790,8 @@ class CallManager(
fun isInitiator(): Boolean = peerConnection?.isInitiator() == true
fun isCameraFrontFacing() = localCameraState.activeDirection == CameraState.Direction.FRONT
interface WebRtcListener: PeerConnection.Observer {
fun onHangup()
}

View File

@ -29,17 +29,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
UNTRUSTED_IDENTITY,
}
val localRenderer: SurfaceViewRenderer?
get() = callManager.localRenderer
val floatingRenderer: SurfaceViewRenderer?
get() = callManager.floatingRenderer
val localFloatingRenderer: SurfaceViewRenderer?
get() = callManager.localFloatingRenderer
val remoteRenderer: SurfaceViewRenderer?
get() = callManager.remoteRenderer
val remoteFloatingRenderer: SurfaceViewRenderer?
get() = callManager.remoteFloatingRenderer
val fullscreenRenderer: SurfaceViewRenderer?
get() = callManager.fullscreenRenderer
private var _videoEnabled: Boolean = false
@ -48,14 +42,8 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
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
@ -85,15 +73,10 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
.map { it.isEnabled }
.onEach { _remoteVideoEnabled = it }
val videoViewSwappedState
get() = callManager.videoViewSwappedEvents
.map { it.isSwapped }
.onEach { _videoViewSwapped = it }
var deviceRotation: Int = 0
var deviceOrientation: Orientation = Orientation.UNKNOWN
set(value) {
field = value
callManager.setDeviceRotation(value)
callManager.setDeviceOrientation(value)
}
val currentCallState
@ -108,4 +91,11 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
val callStartTime: Long
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,
rotationVideoSink
)
rotationVideoSink.mirrored = newCamera.activeDirection == CameraState.Direction.FRONT
rotationVideoSink.setSink(localRenderer)
newVideoTrack.setEnabled(false)
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
import org.thoughtcrime.securesms.webrtc.data.quadrantRotation
import org.webrtc.VideoFrame
import org.webrtc.VideoSink
@ -14,8 +14,7 @@ class RemoteRotationVideoProxySink: VideoSink {
val thisSink = targetSink ?: return
val thisFrame = frame ?: return
val quadrantRotation = rotation.quadrantRotation()
val modifiedRotation = thisFrame.rotation - quadrantRotation
val modifiedRotation = thisFrame.rotation - rotation
val newFrame = VideoFrame(thisFrame.buffer, modifiedRotation, thisFrame.timestampNs)
thisSink.onFrame(newFrame)

View File

@ -1,7 +1,6 @@
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.VideoFrame
import org.webrtc.VideoProcessor
@ -12,7 +11,6 @@ import java.util.concurrent.atomic.AtomicBoolean
class RotationVideoSink: CapturerObserver, VideoProcessor {
var rotation: Int = 0
var mirrored = false
private val capturing = AtomicBoolean(false)
private var capturerObserver = SoftReference<CapturerObserver>(null)
@ -31,13 +29,14 @@ class RotationVideoSink: CapturerObserver, VideoProcessor {
val observer = capturerObserver.get()
if (videoFrame == null || observer == null || !capturing.get()) return
val quadrantRotation = rotation.quadrantRotation()
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)
// 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)
// the frame we are sending to our contact needs to cater for rotation
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?) {

View File

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