feat(ios calls): drop unreachable earpiece from route toggle; Speaker <-> BT/headset
Proven via device telemetry: inside a WKWebView, WebKit owns the WebRTC audio unit and forces the loudspeaker; overrideOutputAudioPort(.none) is a no-op (route settles on Speaker 1.2s later), so the built-in earpiece cannot be selected. Present only what actually works on iOS: Speaker, and Bluetooth/wired headset when connected. bzApplyRoute now maps only 'speaker' to the loudspeaker override; bt/headset use the default port. Coerce any stale 'earpiece' pref to speaker on iOS. Also strips the route debug telemetry. Other platforms keep the earpiece option. build batch147. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+11
-6
@@ -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-batch146';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
<script>window.__BUILD='2026-07-21-batch147';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
|
||||||
@@ -4370,10 +4370,12 @@ function isMobileUA(){ return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.
|
|||||||
function nativeAudioRoute(){ try{ var C=window.Capacitor; return (C && C.getPlatform && C.getPlatform()==='ios' && C.Plugins && C.Plugins.AudioRoute) ? C.Plugins.AudioRoute : null; }catch(_){ return null; } }
|
function nativeAudioRoute(){ try{ var C=window.Capacitor; return (C && C.getPlatform && C.getPlatform()==='ios' && C.Plugins && C.Plugins.AudioRoute) ? C.Plugins.AudioRoute : null; }catch(_){ return null; } }
|
||||||
function bzApplyRoute(route){
|
function bzApplyRoute(route){
|
||||||
var ar=nativeAudioRoute();
|
var ar=nativeAudioRoute();
|
||||||
// TEMP diagnostic: report whether the native plugin registered and what setSpeaker does, so we can see on
|
// iOS native only: 'speaker' forces the loudspeaker; anything else (bt / wired headset) uses the default
|
||||||
// device WHY the toggle isn't switching (plugin missing? setSpeaker rejecting? route not holding?).
|
// output port, which reverts to a connected accessory. The built-in EARPIECE is not reachable inside a
|
||||||
if(window.bzDbg){ try{ var C=window.Capacitor; window.bzDbg('route',{ stage:'apply', route:route, has:ar?1:0, plat:(C&&C.getPlatform&&C.getPlatform())||'', plugins:(C&&C.Plugins)?Object.keys(C.Plugins).slice(0,30).join(','):'none' }); }catch(_){} }
|
// WKWebView — WebKit owns the WebRTC audio unit and forces the speaker (proven: overrideOutputAudioPort(.none)
|
||||||
if(ar){ try{ var pr=ar.setSpeaker({ on: route!=='earpiece' }); if(pr&&pr.then){ pr.then(function(r){ if(window.bzDbg) window.bzDbg('route',{stage:'ok', route:route, r:JSON.stringify(r||{})}); }).catch(function(e){ if(window.bzDbg) window.bzDbg('route',{stage:'reject', route:route, err:String((e&&e.message)||e)}); }); } }catch(e){ if(window.bzDbg) window.bzDbg('route',{stage:'threw', err:String((e&&e.message)||e)}); } } // speaker/bt -> loudspeaker override; earpiece -> default route (BT auto-selected if connected)
|
// is a no-op there), so the iOS route cycle never offers 'earpiece'. Native-grade earpiece would require a
|
||||||
|
// native WebRTC stack (e.g. the LiveKit iOS SDK) instead of the webview.
|
||||||
|
if(ar){ try{ ar.setSpeaker({ on: route==='speaker' }); }catch(_){} }
|
||||||
}
|
}
|
||||||
// The speaker toggle is usable if the browser supports output switching (setSinkId) OR the native iOS route
|
// The speaker toggle is usable if the browser supports output switching (setSinkId) OR the native iOS route
|
||||||
// plugin is present.
|
// plugin is present.
|
||||||
@@ -4425,6 +4427,7 @@ function spkIcon(){ return meetRoute==='bt' ? 'bluetooth' : (meetRoute==='speake
|
|||||||
function routeLabel(){ return meetRoute==='bt' ? 'Bluetooth / headset' : (meetRoute==='speaker' ? 'Speaker' : 'Earpiece'); }
|
function routeLabel(){ return meetRoute==='bt' ? 'Bluetooth / headset' : (meetRoute==='speaker' ? 'Speaker' : 'Earpiece'); }
|
||||||
function refreshSpkBtn(){
|
function refreshSpkBtn(){
|
||||||
const b=document.getElementById('meetSpkBtn'); if(!b) return;
|
const b=document.getElementById('meetSpkBtn'); if(!b) return;
|
||||||
|
if(meetRoute==='earpiece' && nativeAudioRoute()) meetRoute='speaker'; // iOS: earpiece is unreachable in the WebView; coerce stale prefs
|
||||||
meetSpeakerOn=(meetRoute==='speaker');
|
meetSpeakerOn=(meetRoute==='speaker');
|
||||||
b.classList.toggle('off', meetRoute!=='speaker');
|
b.classList.toggle('off', meetRoute!=='speaker');
|
||||||
b.innerHTML=ic(spkIcon(),20);
|
b.innerHTML=ic(spkIcon(),20);
|
||||||
@@ -4529,7 +4532,9 @@ async function openAudioMenu(anchor){
|
|||||||
async function toggleSpeakerphone(){
|
async function toggleSpeakerphone(){
|
||||||
let outs=[]; try{ outs=(await navigator.mediaDevices.enumerateDevices()).filter(x=>x.kind==='audiooutput'); }catch(_){}
|
let outs=[]; try{ outs=(await navigator.mediaDevices.enumerateDevices()).filter(x=>x.kind==='audiooutput'); }catch(_){}
|
||||||
const bt=outs.find(d=>isBtLabel(d.label)); _btConnected=!!bt;
|
const bt=outs.find(d=>isBtLabel(d.label)); _btConnected=!!bt;
|
||||||
const routes=['speaker','earpiece'].concat(bt?['bt']:[]);
|
// iOS WKWebView cannot route call audio to the built-in earpiece (WebKit forces the speaker), so drop it from
|
||||||
|
// the cycle there: Speaker <-> Bluetooth/headset only. Other platforms keep the earpiece option.
|
||||||
|
const routes=['speaker'].concat(nativeAudioRoute()?[]:['earpiece']).concat(bt?['bt']:[]);
|
||||||
const i=routes.indexOf(meetRoute);
|
const i=routes.indexOf(meetRoute);
|
||||||
meetRoute=routes[(i<0?0:(i+1)%routes.length)];
|
meetRoute=routes[(i<0?0:(i+1)%routes.length)];
|
||||||
try{ localStorage.setItem('bzc_route', meetRoute); }catch(_){}
|
try{ localStorage.setItem('bzc_route', meetRoute); }catch(_){}
|
||||||
|
|||||||
Reference in New Issue
Block a user