Merge pull request #2203 from vitorpamplona/claude/review-webrtc-calls-a7ZCq
Improve WebRTC call reliability and notification handling
This commit is contained in:
@@ -184,7 +184,7 @@ class CallAudioManager(
|
|||||||
PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
||||||
"amethyst:call_proximity",
|
"amethyst:call_proximity",
|
||||||
)
|
)
|
||||||
proximityWakeLock?.acquire(60 * 60 * 1000L)
|
proximityWakeLock?.acquire(10 * 60 * 1000L)
|
||||||
registerProximitySensor()
|
registerProximitySensor()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,10 +320,11 @@ class CallAudioManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Suppress("DEPRECATION")
|
ContextCompat.registerReceiver(
|
||||||
context.registerReceiver(
|
context,
|
||||||
scoReceiver,
|
scoReceiver,
|
||||||
IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED),
|
IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED),
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -417,14 +417,16 @@ class CallController(
|
|||||||
onIceCandidate = { candidate -> onLocalIceCandidate(peerPubKey, candidate) },
|
onIceCandidate = { candidate -> onLocalIceCandidate(peerPubKey, candidate) },
|
||||||
onPeerConnected = {
|
onPeerConnected = {
|
||||||
Log.d(TAG) { "Peer ${peerPubKey.take(8)} connected!" }
|
Log.d(TAG) { "Peer ${peerPubKey.take(8)} connected!" }
|
||||||
scope.launch { callManager.onPeerConnected() }
|
scope.launch {
|
||||||
if (!foregroundServiceStarted) {
|
callManager.onPeerConnected()
|
||||||
foregroundServiceStarted = true
|
if (!foregroundServiceStarted) {
|
||||||
startForegroundService()
|
foregroundServiceStarted = true
|
||||||
|
startForegroundService()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRemoteVideoTrack = { track -> videoMonitor.onRemoteVideoTrack(peerPubKey, track) },
|
onRemoteVideoTrack = { track -> videoMonitor.onRemoteVideoTrack(peerPubKey, track) },
|
||||||
onDisconnected = { onPeerDisconnected(peerPubKey) },
|
onDisconnected = { scope.launch { onPeerDisconnected(peerPubKey) } },
|
||||||
onError = { error -> _errorMessage.value = error },
|
onError = { error -> _errorMessage.value = error },
|
||||||
onRenegotiationNeeded = { performRenegotiation(peerPubKey) },
|
onRenegotiationNeeded = { performRenegotiation(peerPubKey) },
|
||||||
)
|
)
|
||||||
|
|||||||
+28
-2
@@ -24,6 +24,7 @@ import android.Manifest
|
|||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
@@ -34,6 +35,7 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.call.CallActivity
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
|
|
||||||
private const val TAG = "CallForegroundService"
|
private const val TAG = "CallForegroundService"
|
||||||
@@ -46,6 +48,7 @@ class CallForegroundService : Service() {
|
|||||||
const val ACTION_STOP = "com.vitorpamplona.amethyst.CALL_STOP"
|
const val ACTION_STOP = "com.vitorpamplona.amethyst.CALL_STOP"
|
||||||
const val EXTRA_PEER_NAME = "peer_name"
|
const val EXTRA_PEER_NAME = "peer_name"
|
||||||
const val EXTRA_IS_VIDEO = "is_video"
|
const val EXTRA_IS_VIDEO = "is_video"
|
||||||
|
private const val HANGUP_REQUEST_CODE = 0x70001
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
@@ -118,12 +121,35 @@ class CallForegroundService : Service() {
|
|||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNotification(peerName: String): Notification =
|
private fun buildNotification(peerName: String): Notification {
|
||||||
NotificationCompat
|
val openCallIntent =
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
Intent(this, CallActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
val hangupIntent =
|
||||||
|
PendingIntent.getBroadcast(
|
||||||
|
this,
|
||||||
|
HANGUP_REQUEST_CODE,
|
||||||
|
Intent(this, CallNotificationReceiver::class.java).apply {
|
||||||
|
action = CallNotificationReceiver.ACTION_HANGUP_CALL
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
return NotificationCompat
|
||||||
.Builder(this, CHANNEL_ID)
|
.Builder(this, CHANNEL_ID)
|
||||||
.setContentTitle(getString(R.string.app_name))
|
.setContentTitle(getString(R.string.app_name))
|
||||||
.setContentText(getString(R.string.call_with, peerName))
|
.setContentText(getString(R.string.call_with, peerName))
|
||||||
.setSmallIcon(R.drawable.amethyst)
|
.setSmallIcon(R.drawable.amethyst)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
.setContentIntent(openCallIntent)
|
||||||
|
.addAction(R.drawable.ic_call_end, getString(R.string.call_hangup), hangupIntent)
|
||||||
.build()
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-13
@@ -77,21 +77,33 @@ class CallMediaManager(
|
|||||||
fun initialize(callType: CallType) {
|
fun initialize(callType: CallType) {
|
||||||
if (peerConnectionFactory != null) return
|
if (peerConnectionFactory != null) return
|
||||||
|
|
||||||
sharedEglBase = EglBase.create()
|
var egl: EglBase? = null
|
||||||
|
try {
|
||||||
|
egl = EglBase.create()
|
||||||
|
|
||||||
PeerConnectionFactory.initialize(
|
PeerConnectionFactory.initialize(
|
||||||
PeerConnectionFactory
|
PeerConnectionFactory
|
||||||
.InitializationOptions
|
.InitializationOptions
|
||||||
.builder(context)
|
.builder(context)
|
||||||
.createInitializationOptions(),
|
.createInitializationOptions(),
|
||||||
)
|
)
|
||||||
|
|
||||||
peerConnectionFactory =
|
peerConnectionFactory =
|
||||||
PeerConnectionFactory
|
PeerConnectionFactory
|
||||||
.builder()
|
.builder()
|
||||||
.setVideoDecoderFactory(DefaultVideoDecoderFactory(sharedEglBase!!.eglBaseContext))
|
.setVideoDecoderFactory(DefaultVideoDecoderFactory(egl.eglBaseContext))
|
||||||
.setVideoEncoderFactory(DefaultVideoEncoderFactory(sharedEglBase!!.eglBaseContext, true, true))
|
.setVideoEncoderFactory(DefaultVideoEncoderFactory(egl.eglBaseContext, true, true))
|
||||||
.createPeerConnectionFactory()
|
.createPeerConnectionFactory()
|
||||||
|
|
||||||
|
sharedEglBase = egl
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Clean up partially-created resources to avoid leaking EglBase
|
||||||
|
egl?.release()
|
||||||
|
peerConnectionFactory?.dispose()
|
||||||
|
peerConnectionFactory = null
|
||||||
|
sharedEglBase = null
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
localAudioSource = peerConnectionFactory?.createAudioSource(MediaConstraints())
|
localAudioSource = peerConnectionFactory?.createAudioSource(MediaConstraints())
|
||||||
localAudioTrack = peerConnectionFactory?.createAudioTrack("audio0", localAudioSource)
|
localAudioTrack = peerConnectionFactory?.createAudioTrack("audio0", localAudioSource)
|
||||||
@@ -145,6 +157,7 @@ class CallMediaManager(
|
|||||||
localVideoTrack?.setEnabled(true)
|
localVideoTrack?.setEnabled(true)
|
||||||
_isVideoEnabled.value = true
|
_isVideoEnabled.value = true
|
||||||
_localVideoTrackFlow.value = localVideoTrack
|
_localVideoTrackFlow.value = localVideoTrack
|
||||||
|
startCamera()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disableVideo() {
|
fun disableVideo() {
|
||||||
|
|||||||
+15
-9
@@ -24,8 +24,6 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,24 +34,32 @@ import kotlinx.coroutines.launch
|
|||||||
* notification trampoline restrictions.
|
* notification trampoline restrictions.
|
||||||
*/
|
*/
|
||||||
class CallNotificationReceiver : BroadcastReceiver() {
|
class CallNotificationReceiver : BroadcastReceiver() {
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
override fun onReceive(
|
override fun onReceive(
|
||||||
context: Context,
|
context: Context,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
) {
|
) {
|
||||||
when (intent.action) {
|
val callManager = CallSessionBridge.callManager ?: return
|
||||||
ACTION_REJECT_CALL -> {
|
val pendingResult = goAsync()
|
||||||
NotificationUtils.cancelCallNotification(context)
|
kotlinx.coroutines.MainScope().launch {
|
||||||
|
try {
|
||||||
|
when (intent.action) {
|
||||||
|
ACTION_REJECT_CALL -> {
|
||||||
|
NotificationUtils.cancelCallNotification(context)
|
||||||
|
callManager.rejectCall()
|
||||||
|
}
|
||||||
|
|
||||||
val callManager = CallSessionBridge.callManager ?: return
|
ACTION_HANGUP_CALL -> {
|
||||||
GlobalScope.launch {
|
callManager.hangup()
|
||||||
callManager.rejectCall()
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
pendingResult.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ACTION_REJECT_CALL = "com.vitorpamplona.amethyst.REJECT_CALL"
|
const val ACTION_REJECT_CALL = "com.vitorpamplona.amethyst.REJECT_CALL"
|
||||||
|
const val ACTION_HANGUP_CALL = "com.vitorpamplona.amethyst.HANGUP_CALL"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-3
@@ -55,6 +55,7 @@ class WebRtcCallSession(
|
|||||||
private val onRenegotiationNeeded: () -> Unit = {},
|
private val onRenegotiationNeeded: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
private var peerConnection: PeerConnection? = null
|
private var peerConnection: PeerConnection? = null
|
||||||
|
private var iceRestartAttempted = false
|
||||||
|
|
||||||
fun createPeerConnection() {
|
fun createPeerConnection() {
|
||||||
val rtcConfig =
|
val rtcConfig =
|
||||||
@@ -90,6 +91,7 @@ class WebRtcCallSession(
|
|||||||
|
|
||||||
PeerConnection.IceConnectionState.CONNECTED -> {
|
PeerConnection.IceConnectionState.CONNECTED -> {
|
||||||
Log.d(TAG) { "ICE: CONNECTED - peer connection established!" }
|
Log.d(TAG) { "ICE: CONNECTED - peer connection established!" }
|
||||||
|
iceRestartAttempted = false
|
||||||
onPeerConnected()
|
onPeerConnected()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +100,15 @@ class WebRtcCallSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
PeerConnection.IceConnectionState.FAILED -> {
|
PeerConnection.IceConnectionState.FAILED -> {
|
||||||
Log.e(TAG, "ICE: FAILED - could not establish connection")
|
if (!iceRestartAttempted) {
|
||||||
onError("Connection failed - check network")
|
iceRestartAttempted = true
|
||||||
onDisconnected()
|
Log.d(TAG) { "ICE: FAILED - attempting ICE restart" }
|
||||||
|
restartIce()
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "ICE: FAILED after restart - giving up")
|
||||||
|
onError("Connection failed - check network")
|
||||||
|
onDisconnected()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PeerConnection.IceConnectionState.DISCONNECTED -> {
|
PeerConnection.IceConnectionState.DISCONNECTED -> {
|
||||||
@@ -255,6 +263,42 @@ class WebRtcCallSession(
|
|||||||
Log.d(TAG) { "addIceCandidate result=$added sdpMid=${candidate.sdpMid} sdp=${candidate.sdp.take(60)}" }
|
Log.d(TAG) { "addIceCandidate result=$added sdpMid=${candidate.sdpMid} sdp=${candidate.sdp.take(60)}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers an ICE restart by creating a new offer with iceRestart=true.
|
||||||
|
* This re-gathers candidates and attempts to establish connectivity
|
||||||
|
* without tearing down the PeerConnection.
|
||||||
|
*/
|
||||||
|
private fun restartIce() {
|
||||||
|
val constraints =
|
||||||
|
MediaConstraints().apply {
|
||||||
|
mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true"))
|
||||||
|
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||||
|
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
|
||||||
|
}
|
||||||
|
peerConnection?.createOffer(
|
||||||
|
object : SdpObserver {
|
||||||
|
override fun onCreateSuccess(sdp: SessionDescription?) {
|
||||||
|
sdp?.let {
|
||||||
|
Log.d(TAG) { "ICE restart offer created, setting local description" }
|
||||||
|
peerConnection?.setLocalDescription(loggingSdpObserver("setLocalDescription(ICE_RESTART)"), it)
|
||||||
|
onRenegotiationNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateFailure(error: String?) {
|
||||||
|
Log.e(TAG, "ICE restart offer creation failed: $error")
|
||||||
|
onError("Connection failed - check network")
|
||||||
|
onDisconnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetSuccess() {}
|
||||||
|
|
||||||
|
override fun onSetFailure(error: String?) {}
|
||||||
|
},
|
||||||
|
constraints,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun getSignalingState(): PeerConnection.SignalingState? = peerConnection?.signalingState()
|
fun getSignalingState(): PeerConnection.SignalingState? = peerConnection?.signalingState()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ import com.vitorpamplona.amethyst.ui.StringResSetup
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.ManageRelayServices
|
import com.vitorpamplona.amethyst.ui.screen.ManageRelayServices
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ManageWebOkHttp
|
import com.vitorpamplona.amethyst.ui.screen.ManageWebOkHttp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class CallActivity : AppCompatActivity() {
|
class CallActivity : AppCompatActivity() {
|
||||||
val isInPipMode = mutableStateOf(false)
|
val isInPipMode = mutableStateOf(false)
|
||||||
@@ -69,14 +69,13 @@ class CallActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private val pipActionReceiver =
|
private val pipActionReceiver =
|
||||||
object : BroadcastReceiver() {
|
object : BroadcastReceiver() {
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
override fun onReceive(
|
override fun onReceive(
|
||||||
context: Context,
|
context: Context,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
) {
|
) {
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
ACTION_PIP_HANGUP -> {
|
ACTION_PIP_HANGUP -> {
|
||||||
GlobalScope.launch { CallSessionBridge.callManager?.hangup() }
|
lifecycleScope.launch { CallSessionBridge.callManager?.hangup() }
|
||||||
}
|
}
|
||||||
|
|
||||||
ACTION_PIP_TOGGLE_MUTE -> {
|
ACTION_PIP_TOGGLE_MUTE -> {
|
||||||
@@ -191,7 +190,6 @@ class CallActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
// Only hang up if the user dismissed PiP (swiped it away).
|
// Only hang up if the user dismissed PiP (swiped it away).
|
||||||
@@ -202,29 +200,36 @@ class CallActivity : AppCompatActivity() {
|
|||||||
if (wasInPipMode && !isInPictureInPictureMode) {
|
if (wasInPipMode && !isInPictureInPictureMode) {
|
||||||
val state = CallSessionBridge.callManager?.state?.value
|
val state = CallSessionBridge.callManager?.state?.value
|
||||||
if (state is CallState.Connected || state is CallState.Connecting || state is CallState.Offering) {
|
if (state is CallState.Connected || state is CallState.Connecting || state is CallState.Offering) {
|
||||||
GlobalScope.launch { CallSessionBridge.callManager?.hangup() }
|
lifecycleScope.launch {
|
||||||
|
withContext(NonCancellable) { CallSessionBridge.callManager?.hangup() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
unregisterPipReceiver()
|
unregisterPipReceiver()
|
||||||
|
|
||||||
// Safety net: if the Activity is destroyed while a call is still
|
// Safety net: if the Activity is destroyed while a call is still
|
||||||
// ringing/offering, ensure the call is hung up so audio stops.
|
// ringing/offering, ensure the call is hung up so audio stops.
|
||||||
|
// Use NonCancellable so the signaling event is published even
|
||||||
|
// though the lifecycle scope is being cancelled.
|
||||||
val manager = CallSessionBridge.callManager
|
val manager = CallSessionBridge.callManager
|
||||||
when (manager?.state?.value) {
|
when (manager?.state?.value) {
|
||||||
is CallState.IncomingCall -> {
|
is CallState.IncomingCall -> {
|
||||||
GlobalScope.launch { manager.rejectCall() }
|
lifecycleScope.launch {
|
||||||
|
withContext(NonCancellable) { manager.rejectCall() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is CallState.Offering,
|
is CallState.Offering,
|
||||||
is CallState.Connecting,
|
is CallState.Connecting,
|
||||||
is CallState.Connected,
|
is CallState.Connected,
|
||||||
-> {
|
-> {
|
||||||
GlobalScope.launch { manager.hangup() }
|
lifecycleScope.launch {
|
||||||
|
withContext(NonCancellable) { manager.hangup() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -63,6 +64,10 @@ fun VideoRenderer(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
mirror: Boolean = false,
|
mirror: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
// Track the current track so the update block can swap sinks when the
|
||||||
|
// track reference changes (e.g. after renegotiation).
|
||||||
|
val currentTrack = remember { mutableStateOf(videoTrack) }
|
||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
@@ -73,8 +78,21 @@ fun VideoRenderer(
|
|||||||
videoTrack.addSink(this)
|
videoTrack.addSink(this)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
update = { renderer ->
|
||||||
|
if (currentTrack.value !== videoTrack) {
|
||||||
|
try {
|
||||||
|
currentTrack.value.removeSink(renderer)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
videoTrack.addSink(renderer)
|
||||||
|
currentTrack.value = videoTrack
|
||||||
|
}
|
||||||
|
},
|
||||||
onRelease = { renderer ->
|
onRelease = { renderer ->
|
||||||
videoTrack.removeSink(renderer)
|
try {
|
||||||
|
currentTrack.value.removeSink(renderer)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
renderer.release()
|
renderer.release()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user