fix: additional WebRTC call hardening
- CallActivity.onDestroy: use standalone CoroutineScope instead of lifecycleScope which is cancelled during super.onDestroy(), ensuring hangup/reject signaling events are reliably published - disposePeerSession: remove videoSenders entry for the departing peer to prevent stale RtpSender references leaking after PeerConnection disposal - initiateGroupCall: detect when all PeerConnection creations fail and hang up immediately instead of leaving the call in Offering state until the 60-second timeout https://claude.ai/code/session_017HrFJNxD6zrGwiZ3s69xTh
This commit is contained in:
@@ -220,6 +220,7 @@ class CallController(
|
||||
|
||||
callManager.beginOffering(callId, peerPubKeys, callType)
|
||||
|
||||
var successCount = 0
|
||||
for (peerPubKey in peerPubKeys) {
|
||||
try {
|
||||
val webRtcSession = withContext(Dispatchers.IO) { createWebRtcSession(peerPubKey) }
|
||||
@@ -230,10 +231,16 @@ class CallController(
|
||||
callManager.publishOfferToPeer(peerPubKey, peerPubKeys, callType, callId, sdp.description)
|
||||
}
|
||||
}
|
||||
successCount++
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to create PeerConnection for ${peerPubKey.take(8)}", e)
|
||||
}
|
||||
}
|
||||
if (successCount == 0) {
|
||||
Log.e(TAG, "All PeerConnection creations failed, hanging up")
|
||||
_errorMessage.value = "Failed to start call: could not create any connections"
|
||||
callManager.hangup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,6 +583,7 @@ class CallController(
|
||||
// ---- Per-peer cleanup ----
|
||||
|
||||
fun disposePeerSession(peerPubKey: HexKey) {
|
||||
videoSenders.remove(peerPubKey)
|
||||
val entry = peerSessionMgr.removeSession(peerPubKey)
|
||||
if (entry != null) {
|
||||
try {
|
||||
|
||||
@@ -46,7 +46,10 @@ import com.vitorpamplona.amethyst.ui.StringResSetup
|
||||
import com.vitorpamplona.amethyst.ui.screen.ManageRelayServices
|
||||
import com.vitorpamplona.amethyst.ui.screen.ManageWebOkHttp
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -213,13 +216,13 @@ class CallActivity : AppCompatActivity() {
|
||||
|
||||
// Safety net: if the Activity is destroyed while a call is still
|
||||
// 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.
|
||||
// Use a standalone CoroutineScope because lifecycleScope is cancelled
|
||||
// during super.onDestroy() and may drop suspend work.
|
||||
val manager = CallSessionBridge.callManager
|
||||
when (manager?.state?.value) {
|
||||
is CallState.IncomingCall -> {
|
||||
lifecycleScope.launch {
|
||||
withContext(NonCancellable) { manager.rejectCall() }
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate).launch {
|
||||
manager.rejectCall()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,8 +230,8 @@ class CallActivity : AppCompatActivity() {
|
||||
is CallState.Connecting,
|
||||
is CallState.Connected,
|
||||
-> {
|
||||
lifecycleScope.launch {
|
||||
withContext(NonCancellable) { manager.hangup() }
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate).launch {
|
||||
manager.hangup()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user