zebra-spaces: session fingerprint + sharper camera-error breadcrumbs
Telemetry for tickets 0001 (fedora chrome silence) and 0002 (fedora
firefox cannot share camera). Every logLine call already ships to
zebra-spaces-signal via client-log; these two additions thicken the
diagnostic payload at the points each ticket needs.
1. ONE-SHOT SESSION FINGERPRINT on entry. After the 'joined as ...'
line we now emit:
session: ua=chrome/desktop ctx={sr=48000 baseLat=0.0107
sinkSupp=1 sink=default} devs={out=4 in=3 cam=2}
picked={mic=abc12345 cam=def67890 spk=}
- ua family + mobile/desktop bucket (the bug shape often falls
cleanly along browser-family lines: fxhp-phone-firefox vs
fedora-chrome are different problem classes)
- audioCtx sampleRate, baseLatency, setSinkId support, current
sinkId — directly answers the 0001 hypothesis: if sink ≠ the
picked spk= shortprefix, we know the audioCtx isn't routed to
the user's chosen device even though the dropdown says so.
- device counts (audiooutput / audioinput / videoinput) — tells
us if a saved-deviceId is pointed at a device that's gone (0002).
- 8-char prefix of the saved mic/cam/spk deviceIds — enough to
correlate with the picker selection without leaking the full id.
Wrapped + caught so a hostile UA can't break entry. Empty fields
shown as 'n/a' / ''.
2. CAMERA-OPEN ERROR now logs e.name + constraints summary instead
of bare e.message. Firefox returns NotFoundError vs
OverconstrainedError vs NotAllowedError vs NotReadableError with
nearly identical .message strings — name disambiguates the actual
failure mode (device gone vs constraint mismatch vs permission
denied vs camera held by another app). Constraints summary
includes the resolution we asked for and the first 8 chars of the
saved deviceId so a saved-id mismatch is obvious on inspection.
AUTO-RECOVERY for OverconstrainedError+saved-deviceId: retry once
without the deviceId constraint so the user gets default camera
instead of a permanent fail. The most likely 0002 cause is a
saved cameraDeviceId pointing at a removed/renamed device, and
making the user re-pick from the dropdown after every system
reboot would be hostile.
This commit is contained in:
parent
e321a7cee1
commit
1ed5263fbc
1 changed files with 79 additions and 4 deletions
|
|
@ -4560,7 +4560,36 @@ async function sfuPublishCamera(){
|
|||
* speaker can choose camera-on while still using a different audio
|
||||
* input (monitor source, music mode, etc) */
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints, audio: false });
|
||||
} catch(e){ logLine('err','camera open cancelled: '+e.message); fsm.send('FAILED', { error: e.message }); fsm.send('DONE'); return; }
|
||||
} catch(e){
|
||||
/* surface e.name + the constraints we tried so signal-server telemetry
|
||||
* can disambiguate NotFoundError (device gone) vs OverconstrainedError
|
||||
* (saved deviceId or 720p/16:9 not supported) vs NotAllowedError (user
|
||||
* declined permission) vs NotReadableError (camera held by another app).
|
||||
* Defect #0002 fedora firefox: e.message alone wasn't enough to pick
|
||||
* the right branch. */
|
||||
const exact = videoConstraints.deviceId && videoConstraints.deviceId.exact;
|
||||
logLine('err',
|
||||
'camera open cancelled: '+(e.name||'Error')+' — '+e.message+
|
||||
' (constraints: 1280x720@30 16:9'+(exact?' deviceId='+String(exact).slice(0,8):'')+')'
|
||||
);
|
||||
/* OverconstrainedError + a saved deviceId is the canonical "saved
|
||||
* device gone or renamed" case — retry once without deviceId so the
|
||||
* user gets default camera rather than a permanent fail. */
|
||||
if (e.name === 'OverconstrainedError' && exact){
|
||||
logLine('', 'camera open retry without saved deviceId');
|
||||
try {
|
||||
const fallbackConstraints = Object.assign({}, videoConstraints);
|
||||
delete fallbackConstraints.deviceId;
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: fallbackConstraints, audio: false });
|
||||
logLine('', 'camera open recovered with default device');
|
||||
} catch(e2){
|
||||
logLine('err','camera open retry failed: '+(e2.name||'Error')+' — '+e2.message);
|
||||
fsm.send('FAILED', { error: e2.message }); fsm.send('DONE'); return;
|
||||
}
|
||||
} else {
|
||||
fsm.send('FAILED', { error: e.message }); fsm.send('DONE'); return;
|
||||
}
|
||||
}
|
||||
fsm.send('ACQUIRED', { stream });
|
||||
sfuCameraStream = stream;
|
||||
const pc = new RTCPeerConnection(rtcConfig);
|
||||
|
|
@ -6029,6 +6058,52 @@ async function handleSignal(raw){
|
|||
broadcastSpotlight();
|
||||
reorderTiles();
|
||||
logLine('', 'joined as '+myRole+' — uuid '+myUUID);
|
||||
/* one-shot session fingerprint for telemetry (see docs/tickets/
|
||||
* 0001 + 0002): captures the browser + audio/video device state
|
||||
* at entry so a reporter's signal-server log carries enough
|
||||
* context to disambiguate browser-specific defects. Mobile + UA
|
||||
* fields land first because the bug shape often correlates with
|
||||
* a single UA family (fxhp-phone-firefox, fedora-chrome, etc).
|
||||
* Best-effort — wrapped + caught so a hostile UA doesn't break
|
||||
* entry. */
|
||||
(async () => {
|
||||
try {
|
||||
const ua = navigator.userAgent || '';
|
||||
const isMobile = /Mobi|Android|iPhone|iPad|iPod/.test(ua);
|
||||
const browser =
|
||||
/Firefox\//.test(ua) ? 'firefox' :
|
||||
/Edg\//.test(ua) ? 'edge' :
|
||||
/Chrome\//.test(ua) ? 'chrome' :
|
||||
/Safari\//.test(ua) ? 'safari' : 'other';
|
||||
let sr = 'n/a', bl = 'n/a', sinkSupp = false, ctxSink = 'n/a';
|
||||
try {
|
||||
if (audioCtx){
|
||||
sr = audioCtx.sampleRate;
|
||||
bl = (audioCtx.baseLatency != null) ? audioCtx.baseLatency.toFixed(4) : 'n/a';
|
||||
sinkSupp = typeof audioCtx.setSinkId === 'function';
|
||||
ctxSink = (audioCtx.sinkId === undefined) ? 'n/a' : (audioCtx.sinkId || 'default');
|
||||
}
|
||||
} catch(_){}
|
||||
let outCount = 0, inCount = 0, camCount = 0;
|
||||
try {
|
||||
const devs = await navigator.mediaDevices.enumerateDevices();
|
||||
for (const d of devs){
|
||||
if (d.kind === 'audiooutput') outCount++;
|
||||
else if (d.kind === 'audioinput') inCount++;
|
||||
else if (d.kind === 'videoinput') camCount++;
|
||||
}
|
||||
} catch(_){}
|
||||
const pickShort = id => id ? String(id).slice(0,8) : '';
|
||||
logLine('',
|
||||
'session: ua='+browser+(isMobile?'/mobile':'/desktop')+
|
||||
' ctx={sr='+sr+' baseLat='+bl+' sinkSupp='+(sinkSupp?1:0)+' sink='+ctxSink+'}'+
|
||||
' devs={out='+outCount+' in='+inCount+' cam='+camCount+'}'+
|
||||
' picked={mic='+pickShort(micDeviceId)+
|
||||
' cam='+pickShort(cameraDeviceId)+
|
||||
' spk='+pickShort(speakerDeviceId)+'}'
|
||||
);
|
||||
} catch(_){}
|
||||
})();
|
||||
/* dot, leave/mute button visibility, sec-room reveal, entry-row
|
||||
* hide and status text are all driven by applyCallStateUI()
|
||||
* from the 'joined' state. Welcome already dispatched WELCOME
|
||||
|
|
@ -8008,9 +8083,9 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
|
|||
</script>
|
||||
|
||||
<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-06</span><br>
|
||||
md5 <span class="stamp-md5">43f585be4e478d110d4250eff68b9d68</span><br>
|
||||
sha256 <span class="stamp-sha">6677c5b69c23976a7c9d35104fdc324fe69fa80d24120e1f32aaab23ebdd0606</span><br>
|
||||
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> · built <span class="stamp-date">2026-06-07</span><br>
|
||||
md5 <span class="stamp-md5">14af06bd5893feb70229dded7e6c5497</span><br>
|
||||
sha256 <span class="stamp-sha">e603642628f264fa919f55be33e6578d77d2d42858566c2d708beefce091aed5</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