meeting: unlock iOS media playback on Join so remote call audio isn't silent until you mute/unmute (batch143)

This commit is contained in:
2026-07-20 00:08:25 +05:30
parent 4e78448743
commit 9c2ba847b0
+15 -2
View File
@@ -1158,7 +1158,7 @@
</head> </head>
<body> <body>
<script src="/icons.js?v=6"></script> <script src="/icons.js?v=6"></script>
<script>window.__BUILD='2026-07-20-batch142';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD); <script>window.__BUILD='2026-07-20-batch143';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network. // Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
// //
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from // We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
@@ -4535,7 +4535,7 @@ function addTile(id, stream, label, muted){
if(!tile){ tile=document.createElement('div'); tile.className='meet-tile'; tile.id='meet-tile-'+id; if(!tile){ tile=document.createElement('div'); tile.className='meet-tile'; tile.id='meet-tile-'+id;
const av=(id==='__local')?((ME&&ME.avatarUrl)||null):(meetAvatars.get(id)||null); // profile pic on the tile const av=(id==='__local')?((ME&&ME.avatarUrl)||null):(meetAvatars.get(id)||null); // profile pic on the tile
tile.innerHTML='<video autoplay playsinline'+(muted?' muted':'')+'></video><div class="meet-av" style="background:'+avColor(label||'?')+'">'+pEsc(initials(label||'?'))+(av?'<img src="'+pEsc(av)+'" alt="" onerror="this.remove()">':'')+'</div><div class="meet-mute" style="display:none">'+ic('micOff',14)+'</div><span class="nm">'+pEsc(label||'')+'</span>'; grid.appendChild(tile); } tile.innerHTML='<video autoplay playsinline'+(muted?' muted':'')+'></video><div class="meet-av" style="background:'+avColor(label||'?')+'">'+pEsc(initials(label||'?'))+(av?'<img src="'+pEsc(av)+'" alt="" onerror="this.remove()">':'')+'</div><div class="meet-mute" style="display:none">'+ic('micOff',14)+'</div><span class="nm">'+pEsc(label||'')+'</span>'; grid.appendChild(tile); }
const v=tile.querySelector('video'); if(v && stream && v.srcObject!==stream){ v.srcObject=stream; applySink(v); } // #5: route audio to the chosen speaker const v=tile.querySelector('video'); if(v && stream && v.srcObject!==stream){ v.srcObject=stream; applySink(v); try{ v.play().catch(function(){}); }catch(_){} } // #5: route audio to the chosen speaker + kick playback (iOS won't autostart WebRTC audio)
const hasVid=!!(stream && stream.getVideoTracks && stream.getVideoTracks().some(t=>t.enabled && t.readyState!=='ended')); const hasVid=!!(stream && stream.getVideoTracks && stream.getVideoTracks().some(t=>t.enabled && t.readyState!=='ended'));
tile.classList.toggle('novid', !hasVid || (meetCamOff.get(id)===true && !meetSharers.has(id))); // camOff → avatar, UNLESS they're sharing a screen (#9: screen must show even with camera off) tile.classList.toggle('novid', !hasVid || (meetCamOff.get(id)===true && !meetSharers.has(id))); // camOff → avatar, UNLESS they're sharing a screen (#9: screen must show even with camera off)
if(meetMuted.has(id)) setTileMute(id, meetMuted.get(id)); // apply any known mute state if(meetMuted.has(id)) setTileMute(id, meetMuted.get(id)); // apply any known mute state
@@ -4732,7 +4732,20 @@ function startSR(){ if(meetSR) return; const SR=window.SpeechRecognition||window
function stopSR(){ if(meetSR){ try{ meetSR.onend=null; meetSR.stop(); }catch(_){} meetSR=null; } } function stopSR(){ if(meetSR){ try{ meetSR.onend=null; meetSR.stop(); }catch(_){} meetSR=null; } }
function updateTransBtn(){ const b=document.getElementById('meetTransBtn'); if(!b) return; b.classList.toggle('on', meetTranscribe); b.title=meetTranscribe?'Stop my transcript':'Transcribe (your private copy)'; } function updateTransBtn(){ const b=document.getElementById('meetTransBtn'); if(!b) return; b.classList.toggle('on', meetTranscribe); b.title=meetTranscribe?'Stop my transcript':'Transcribe (your private copy)'; }
function transcribeNotice(on){ let el=document.getElementById('txNotice'); if(on){ if(!el){ el=document.createElement('div'); el.id='txNotice'; el.className='tx-notice'; el.innerHTML=ic('fileText',12)+' Transcribing'; document.body.appendChild(el); } } else if(el){ el.remove(); } } function transcribeNotice(on){ let el=document.getElementById('txNotice'); if(on){ if(!el){ el=document.createElement('div'); el.id='txNotice'; el.className='tx-notice'; el.innerHTML=ic('fileText',12)+' Transcribing'; document.body.appendChild(el); } } else if(el){ el.remove(); } }
// iOS blocks media/WebRTC audio playback until a user gesture, so remote call audio stays SILENT until you
// tap something (e.g. mute/unmute — that tap is the gesture). Unlock it: within the Join tap we play a short
// silent clip (grants the page media-playback activation) and (re)start every meeting <video>. A tap fallback
// re-runs it in case the activation lapsed before a remote stream arrived. Web fix — helps native app + PWA.
let _bzSilent=null;
function bzUnlockAudio(){
try{ if(_audioCtx && _audioCtx.state==='suspended') _audioCtx.resume(); }catch(_){}
try{ if(!_bzSilent){ _bzSilent=new Audio('data:audio/wav;base64,UklGRjIAAABXQVZFZm10IBAAAAABAAEAgLsAAAB3AQACABAAZGF0YQ4AAAAAAAAAAAAAAAAAAAAAAAA='); _bzSilent.setAttribute('playsinline',''); } _bzSilent.play().catch(function(){}); }catch(_){}
try{ document.querySelectorAll('#meetGrid video').forEach(function(v){ if(v && v.paused) v.play().catch(function(){}); }); }catch(_){}
}
document.addEventListener('touchend', function(){ if(meetState==='call') bzUnlockAudio(); }, {passive:true}); // fallback: any tap during a call restarts silent remote audio
document.addEventListener('click', function(){ if(meetState==='call') bzUnlockAudio(); }, {passive:true});
async function enterMeeting(code, audioOnly){ async function enterMeeting(code, audioOnly){
bzUnlockAudio(); // runs inside the Join tap → unlock playback so remote audio isn't silent until you tap
if(meetState==='call'){ switchTab('meeting'); return; } // already in a call — ignore double-join if(meetState==='call'){ switchTab('meeting'); return; } // already in a call — ignore double-join
// Joining this room → clear any lingering incoming-call invite popup for it (and stop its ring). // Joining this room → clear any lingering incoming-call invite popup for it (and stop its ring).
// Fixes: joining via the header "Join" button left the Join/Decline popup on screen. Belt-and-braces // Fixes: joining via the header "Join" button left the Join/Decline popup on screen. Belt-and-braces