diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html index 2b158d5..01a726b 100644 --- a/zebra-report/zebra-spaces.html +++ b/zebra-report/zebra-spaces.html @@ -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');