diff --git a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift index 69d8de4..4c5ab10 100644 --- a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift +++ b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift @@ -171,10 +171,20 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega } public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { - var data: [String: Any] = calls[action.callUUID] ?? [:] - data["callUUID"] = action.callUUID.uuidString - notifyListeners("answerCall", data: data) + let uuid = action.callUUID + var data: [String: Any] = calls[uuid] ?? [:] + data["callUUID"] = uuid.uuidString action.fulfill() + // RING-ONLY MODEL: CallKit was used purely for the incoming ring. End the CallKit call NOW so iOS + // RELEASES the microphone — an active CallKit call reserves the mic and the WebView's WebRTC would get + // a dead mic. Then hand the call to the WebView, giving iOS a beat to tear down the CallKit audio + // session before the WebView grabs the mic. Background audio is preserved by the 'voip' background mode. + provider.reportCall(with: uuid, endedAt: Date(), reason: .remoteEnded) + calls.removeValue(forKey: uuid) + endedCalls.insert(uuid) + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in + self?.notifyListeners("answerCall", data: data) + } } public func provider(_ provider: CXProvider, perform action: CXEndCallAction) { @@ -196,11 +206,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega action.fulfill() } - // CallKit hands us the call audio session; configure it for a voice call. The WebView's WebRTC audio - // uses this session, and because it's a CallKit call the app keeps running in the background. + // RING-ONLY MODEL: the call audio runs in the WebView, not through CallKit's session — so we do NOT + // reconfigure the session here (that would fight WebKit's WebRTC). Just report the state. (The CallKit + // call is ended right after answer anyway, so this session is short-lived.) public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { - try? audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .allowBluetoothA2DP]) - try? audioSession.setActive(true) notifyListeners("audioActivated", data: ["ok": true]) } diff --git a/server/public/home.html b/server/public/home.html index 9e16071..761b409 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -2546,7 +2546,7 @@ function refreshGroupRowTick(gid){ } // Shared group call: start it (or join the live one — the server returns the existing room). async function startOrJoinGroupCall(group){ - try{ const r=await postJSON('/api/groups/call/start',{ group }); if(r&&r.room){ meetReturn={kind:'group',id:group}; const _g=rowFor('group',group); if(nativeCallOn()) callkitReportOutgoing(r.uuid, r.room, 'group', (_g&&_g.name)||'Group call', true); switchTab('meeting'); enterMeeting(r.room); } } + try{ const r=await postJSON('/api/groups/call/start',{ group }); if(r&&r.room){ meetReturn={kind:'group',id:group}; switchTab('meeting'); enterMeeting(r.room); /* Outgoing stays on the WebView (no CallKit) so the mic works. */ } } catch(e){ toast(e.message||'Could not start the call'); } } function updateCallBtn(active){ const cc=document.getElementById('convoCall'); if(!cc) return; cc.classList.toggle('joinable',active); cc.title=active?'Join call':'Start call'; cc.innerHTML=ic(active?'video':'phone',18)+(active?'Join':''); } @@ -2562,7 +2562,7 @@ function onGroupCall(d){ function dismissCallInvite(room){ if(!room) return; const el=document.getElementById('ci-'+room); if(el){ try{ el.remove(); }catch(_){} stopRing(); } } // 1:1 call: start/join from the DM header; live state updates the button + shows an incoming invite. async function startOrJoinDmCall(otherId){ - try{ const r=await postJSON('/api/calls/dm/start',{ to:otherId }); if(r&&r.room){ const _r=rowFor('dm',otherId); _dmCallWaiting={ name:(_r&&_r.name)||'Contact', avatar:(_r&&_r.avatar)||null }; meetReturn={kind:'dm',id:otherId}; if(nativeCallOn()) callkitReportOutgoing(r.uuid, r.room, 'dm', (_r&&_r.name)||'Call', false); switchTab('meeting'); enterMeeting(r.room); startRingback(); /* #17: ring until they answer */ } } + try{ const r=await postJSON('/api/calls/dm/start',{ to:otherId }); if(r&&r.room){ const _r=rowFor('dm',otherId); _dmCallWaiting={ name:(_r&&_r.name)||'Contact', avatar:(_r&&_r.avatar)||null }; meetReturn={kind:'dm',id:otherId}; switchTab('meeting'); enterMeeting(r.room); startRingback(); /* #17: ring until they answer. Outgoing stays on the WebView (no CallKit) so the mic works. */ } } catch(e){ toast(e.message||'Could not start the call'); } } // Live presence: a contact came online/offline or entered/left a call — update their dot + the