fix(ios): stop VoIP-push crash; hand mic to WebView after answer; missed-call banner

CRASH: iOS terminates an app that receives a VoIP push without calling
reportNewIncomingCall. My re-ring 'fix' made the cancel path call completion() without
reporting -> crash. Always report then immediately end on cancel (a tiny ring blip is
unavoidable; a crash is worse).

MIC + earpiece: an ACTIVE CallKit call reserves the mic (WebView WebRTC gets a dead mic
+ earpiece routing). So on answer keep CallKit active only long enough to foreground the
app, then end it and fire 'callHandoff'; the WebView forces the loudspeaker and
re-acquires the mic (sfuSetMic off/on, retried while it finishes joining).

MISSED CALL: endDmCallByRoom now sends a plain missed-call banner to the callee when the
call ends unanswered (timeout / caller hung up before pickup); skipped on decline.

Server (missed banner) deploys now; plugin + web handoff need a Codemagic build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 22:48:24 +05:30
parent 9dc253143e
commit e09d17f1a7
3 changed files with 40 additions and 21 deletions
+11
View File
@@ -3806,6 +3806,17 @@ async function setupNativeCall(){
if(room && (typeof meetRoom!=='undefined') && meetRoom===room){ leaveMeeting(); } // already in it → leave
else if(room){ postJSON('/api/calls/decline',{room}).catch(()=>{}); dismissCallInvite(room); } // ringing, not joined → decline
}catch(_){} });
// After answer, CallKit releases the mic + audio session back to us (an active CallKit call reserves the
// mic → dead mic + earpiece). Force the loudspeaker (CallKit used the earpiece), then RE-ACQUIRE the mic
// (it was captured dead while CallKit held it). Retry the mic toggle a few times in case the WebView is
// still joining the room when the handoff fires.
NC.addListener('callHandoff', async ()=>{
try{ const ar=nativeAudioRoute(); if(ar) await ar.setSpeaker({on:true}); }catch(_){}
for(let i=0;i<5;i++){
try{ const lp=(typeof SFU!=='undefined'&&SFU.room&&SFU.room.localParticipant)||null; if(lp && typeof sfuSetMic==='function'){ await sfuSetMic(false); await sfuSetMic(true); break; } }catch(_){}
await new Promise(r=>setTimeout(r,700));
}
});
console.log('[callkit] native calling ready');
}
// Register an OUTGOING call with CallKit so it's a system call too (→ background audio for outgoing calls).