diff --git a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift index 6efce5c..7b2d1ab 100644 --- a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift +++ b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift @@ -66,7 +66,10 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega await old?.disconnect() // drop any previous/stale connection so we never leave DUPLICATE participants do { try await r.connect(url: url, token: token) - try await r.localParticipant.setMicrophone(enabled: true) + // House rule: answer MUTED. Not enabling the mic here means nothing is captured/published + // until the user taps Mic in the meeting UI (→ setMuted(false) → setMicrophone(true)), which + // is also when iOS asks for mic permission. Playback (hearing others) is unaffected. + try await r.localParticipant.setMicrophone(enabled: false) self?.notifyListeners("callConnected", data: ["ok": true]) } catch { self?.notifyListeners("callError", data: ["error": String(describing: error)]) diff --git a/server/public/home.html b/server/public/home.html index bc8d4c8..b832262 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -3810,7 +3810,9 @@ async function setupNativeCall(){ meetReturn = isGroup ? { kind:'group', id:d.groupId } : { kind:'dm', id:d.callerId }; switchTab('meeting'); enterMeeting(d.room, false, { native:true, uuid:d.callUUID }); }catch(e){ pdbg('nc-answer-err', { err:String((e&&e.message)||e) }); } }); - NC.addListener('callConnected', ()=>{ pdbg('nc-connected'); }); + // Enforce the muted-by-default rule against the plugin's actual mic once the room connects (belt-and-braces + // for builds whose plugin still connects the mic live). meetMic is the source of truth for the mic button. + NC.addListener('callConnected', ()=>{ pdbg('nc-connected'); if(meetNative){ try{ NC.setMuted({ muted:!meetMic }); }catch(_){} } }); NC.addListener('callError', (e)=>{ pdbg('nc-error', { err:(e&&e.error)||'' }); }); // Muted from the CallKit system UI → reflect it in the meeting window's mic button. NC.addListener('setMuted', (e)=>{ if(!e||meetState!=='call'||!meetNative) return; meetMic=!e.muted; try{ updateMicBtn(); setTileMute('__local', !meetMic); meetSend({type:'meeting-state', muted:!meetMic, camOff:!meetCam}); }catch(_){} }); @@ -5307,7 +5309,7 @@ async function enterMeeting(code, audioOnly, opts){ // Start with NO media — mic & cam OFF by default (no permission prompt until the user // turns one on). Tracks are acquired on demand by toggleMic / toggleCam. meetLocalStream=new MediaStream(); - meetMic=meetNative; meetCam=false; meetIsHost=false; meetHostId=null; // native: the plugin connects with the mic LIVE → show unmuted + meetMic=false; meetCam=false; meetIsHost=false; meetHostId=null; // answer MUTED by default (house rule) — tap Mic to speak meetScreen=false; meetScreenStream=null; meetSharers.clear(); meetMultiShare=false; meetRec=null; meetTranscribe=false; meetRoomTx=false; meetSR=null; _addPool=null; meetStageId=null; try{ const c=await fetch('/api/ice').then(r=>r.json()); if(c&&c.iceServers) MEET_ICE=c; }catch(_){}