zebra-report: deploy label-based mic/speaker device persistence
This commit is contained in:
parent
9288555d8e
commit
6c214a19fb
1 changed files with 65 additions and 7 deletions
|
|
@ -1331,7 +1331,9 @@ const ID_KEY = 'zebra-spaces-id-v1';
|
|||
const HANDLE_KEY = 'zebra-spaces-handle-v1';
|
||||
const MUSIC_MODE_KEY = 'zebra-spaces-music-mode-v1';
|
||||
const MIC_DEV_KEY = 'zebra-spaces-mic-device-v1';
|
||||
const MIC_LABEL_KEY = 'zebra-spaces-mic-label-v1';
|
||||
const SPK_DEV_KEY = 'zebra-spaces-spk-device-v1';
|
||||
const SPK_LABEL_KEY = 'zebra-spaces-spk-label-v1';
|
||||
const CAM_DEV_KEY = 'zebra-spaces-cam-device-v1';
|
||||
const THEME_KEY = 'zebra-theme-v1';
|
||||
/* sessionStorage (per-tab) — tracks which call this tab is in so a
|
||||
|
|
@ -1568,10 +1570,12 @@ $('handle').value = myHandle;
|
|||
* last-picked mic + camera deviceIds. Declared up here so the restore
|
||||
* runs before their downstream `let` would put them in the temporal
|
||||
* dead zone; downstream code now reads from these existing bindings. */
|
||||
let musicMode = false, micDeviceId = '', speakerDeviceId = '', cameraDeviceId = '';
|
||||
let musicMode = false, micDeviceId = '', micDeviceLabel = '', speakerDeviceId = '', speakerDeviceLabel = '', cameraDeviceId = '';
|
||||
try { musicMode = localStorage.getItem(MUSIC_MODE_KEY) === '1'; } catch(_){}
|
||||
try { micDeviceId = localStorage.getItem(MIC_DEV_KEY) || ''; } catch(_){}
|
||||
try { micDeviceLabel = localStorage.getItem(MIC_LABEL_KEY) || ''; } catch(_){}
|
||||
try { speakerDeviceId = localStorage.getItem(SPK_DEV_KEY) || ''; } catch(_){}
|
||||
try { speakerDeviceLabel = localStorage.getItem(SPK_LABEL_KEY) || ''; } catch(_){}
|
||||
try { cameraDeviceId = localStorage.getItem(CAM_DEV_KEY) || ''; } catch(_){}
|
||||
if ($('music-mode')) $('music-mode').checked = musicMode;
|
||||
renderIdentity();
|
||||
|
|
@ -2956,8 +2960,37 @@ async function refreshMicList(){
|
|||
o.value = m.deviceId; o.textContent = 'input ' + (m.label || ('microphone '+(i+1)));
|
||||
sel.appendChild(o);
|
||||
});
|
||||
if (micDeviceId && mics.some(m=>m.deviceId===micDeviceId)) sel.value = micDeviceId;
|
||||
else micDeviceId = sel.value;
|
||||
/* Pre-permission Firefox returns empty deviceIds for every device; the
|
||||
* old `else: micDeviceId = sel.value` clobbered our saved selection to
|
||||
* '' the moment we ran this on page load (before any gUM grant). After
|
||||
* that, getMic() picked the default mic instead of the saved monitor,
|
||||
* and the host had to re-pick the input every refresh.
|
||||
*
|
||||
* Detection: if every enumerated device has deviceId === '', we're
|
||||
* still pre-permission — DON'T touch the saved micDeviceId. Wait until
|
||||
* a real enumerate (post-getMic) before fixing up sel.value.
|
||||
*
|
||||
* Also fall back to LABEL-match when the saved deviceId doesn't match
|
||||
* but the saved label does (Chrome rotates deviceIds across sessions,
|
||||
* label is more stable). When a label match wins, refresh micDeviceId
|
||||
* to the current session's deviceId so micConstraints() works. */
|
||||
const allEmpty = mics.every(m => !m.deviceId);
|
||||
if (allEmpty) return; /* pre-permission — preserve saved selection */
|
||||
if (micDeviceId && mics.some(m=>m.deviceId===micDeviceId)){
|
||||
sel.value = micDeviceId;
|
||||
return;
|
||||
}
|
||||
if (micDeviceLabel){
|
||||
const byLabel = mics.find(m => m.label === micDeviceLabel);
|
||||
if (byLabel){
|
||||
micDeviceId = byLabel.deviceId;
|
||||
try { localStorage.setItem(MIC_DEV_KEY, micDeviceId); } catch(_){}
|
||||
sel.value = micDeviceId;
|
||||
logLine('', 'mic input restored by label match: '+micDeviceLabel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
micDeviceId = sel.value;
|
||||
} catch(e){ logLine('err','could not list inputs: '+e.message); }
|
||||
}
|
||||
/* Speaker output picker — routes peer audio to a specific sink (studio
|
||||
|
|
@ -2987,8 +3020,27 @@ async function refreshSpeakerList(){
|
|||
opt.value = o.deviceId; opt.textContent = 'output ' + (o.label || ('speaker '+(i+1)));
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
if (speakerDeviceId && outs.some(o=>o.deviceId===speakerDeviceId)) sel.value = speakerDeviceId;
|
||||
else speakerDeviceId = sel.value;
|
||||
/* same restore pattern as refreshMicList: don't clobber the saved
|
||||
* speaker selection while we're pre-permission (all deviceIds empty),
|
||||
* and fall back to label-match when deviceId rotation invalidates the
|
||||
* saved id. */
|
||||
const allEmpty = outs.every(o => !o.deviceId);
|
||||
if (allEmpty) return;
|
||||
if (speakerDeviceId && outs.some(o=>o.deviceId===speakerDeviceId)){
|
||||
sel.value = speakerDeviceId;
|
||||
return;
|
||||
}
|
||||
if (speakerDeviceLabel){
|
||||
const byLabel = outs.find(o => o.label === speakerDeviceLabel);
|
||||
if (byLabel){
|
||||
speakerDeviceId = byLabel.deviceId;
|
||||
try { localStorage.setItem(SPK_DEV_KEY, speakerDeviceId); } catch(_){}
|
||||
sel.value = speakerDeviceId;
|
||||
logLine('', 'output restored by label match: '+speakerDeviceLabel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
speakerDeviceId = sel.value;
|
||||
} catch(e){ logLine('err','could not list outputs: '+e.message); }
|
||||
}
|
||||
/* Apply current speakerDeviceId to a single <audio> element. Safe to call
|
||||
|
|
@ -5045,13 +5097,19 @@ $('btn-mute').addEventListener('click', () => {
|
|||
});
|
||||
$('mic-select').addEventListener('change', async (e) => {
|
||||
micDeviceId = e.target.value;
|
||||
/* Also persist the human-readable label so we can re-resolve the
|
||||
* same device across Chrome's per-session deviceId rotation. */
|
||||
micDeviceLabel = (e.target.selectedOptions[0] && e.target.selectedOptions[0].textContent || '').replace(/^input\s+/, '');
|
||||
try { localStorage.setItem(MIC_DEV_KEY, micDeviceId); } catch(_){}
|
||||
try { localStorage.setItem(MIC_LABEL_KEY, micDeviceLabel); } catch(_){}
|
||||
if (micStream){ try { await applyMicMode(); } catch(err){ logLine('err','input switch failed: '+err.message); await refreshMicList(); } }
|
||||
});
|
||||
if ($('speaker-select')){
|
||||
$('speaker-select').addEventListener('change', async (e) => {
|
||||
speakerDeviceId = e.target.value;
|
||||
speakerDeviceLabel = (e.target.selectedOptions[0] && e.target.selectedOptions[0].textContent || '').replace(/^output\s+/, '');
|
||||
try { localStorage.setItem(SPK_DEV_KEY, speakerDeviceId); } catch(_){}
|
||||
try { localStorage.setItem(SPK_LABEL_KEY, speakerDeviceLabel); } catch(_){}
|
||||
await applySinkToAll();
|
||||
logLine('', 'output: '+(e.target.selectedOptions[0]?.textContent || 'default'));
|
||||
});
|
||||
|
|
@ -5375,8 +5433,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
|
|||
|
||||
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
|
||||
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> · built <span class="stamp-date">2026-06-04</span><br>
|
||||
md5 <span class="stamp-md5">55fa6a71aa44a14506015e88e724c10b</span><br>
|
||||
sha256 <span class="stamp-sha">13041569d8641958fa844222165b9b6c2bd7af8e0750ee26af4545b74de1d5ba</span><br>
|
||||
md5 <span class="stamp-md5">bd33f463261d6f66f7d1db7743c84b2d</span><br>
|
||||
sha256 <span class="stamp-sha">bf69db896f0f32a8bc22838551c0403ead65d36e0e64abcc0f5b66d58e14f6bc</span><br>
|
||||
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
|
||||
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
|
||||
</footer>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue