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)
|
callManager.beginOffering(callId, peerPubKeys, callType)
|
||||||
|
|
||||||
|
var successCount = 0
|
||||||
for (peerPubKey in peerPubKeys) {
|
for (peerPubKey in peerPubKeys) {
|
||||||
try {
|
try {
|
||||||
val webRtcSession = withContext(Dispatchers.IO) { createWebRtcSession(peerPubKey) }
|
val webRtcSession = withContext(Dispatchers.IO) { createWebRtcSession(peerPubKey) }
|
||||||
@@ -230,10 +231,16 @@ class CallController(
|
|||||||
callManager.publishOfferToPeer(peerPubKey, peerPubKeys, callType, callId, sdp.description)
|
callManager.publishOfferToPeer(peerPubKey, peerPubKeys, callType, callId, sdp.description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
successCount++
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to create PeerConnection for ${peerPubKey.take(8)}", e)
|
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 ----
|
// ---- Per-peer cleanup ----
|
||||||
|
|
||||||
fun disposePeerSession(peerPubKey: HexKey) {
|
fun disposePeerSession(peerPubKey: HexKey) {
|
||||||
|
videoSenders.remove(peerPubKey)
|
||||||
val entry = peerSessionMgr.removeSession(peerPubKey)
|
val entry = peerSessionMgr.removeSession(peerPubKey)
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ 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.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -213,13 +216,13 @@ class CallActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// 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
|
// Use a standalone CoroutineScope because lifecycleScope is cancelled
|
||||||
// though the lifecycle scope is being cancelled.
|
// during super.onDestroy() and may drop suspend work.
|
||||||
val manager = CallSessionBridge.callManager
|
val manager = CallSessionBridge.callManager
|
||||||
when (manager?.state?.value) {
|
when (manager?.state?.value) {
|
||||||
is CallState.IncomingCall -> {
|
is CallState.IncomingCall -> {
|
||||||
lifecycleScope.launch {
|
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate).launch {
|
||||||
withContext(NonCancellable) { manager.rejectCall() }
|
manager.rejectCall()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,8 +230,8 @@ class CallActivity : AppCompatActivity() {
|
|||||||
is CallState.Connecting,
|
is CallState.Connecting,
|
||||||
is CallState.Connected,
|
is CallState.Connected,
|
||||||
-> {
|
-> {
|
||||||
lifecycleScope.launch {
|
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate).launch {
|
||||||
withContext(NonCancellable) { manager.hangup() }
|
manager.hangup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user