zebra-report: broadcast-quality mic + screen — stereo Opus 256k, 1080p30 @ 6Mbps

Mirror source 5affebe — high-fidelity audio + video. Needs SFU
proxy.unturf.com#26913a8 deployed for end-to-end stereo (subscriber
decoders need stereo=1 in the SFU's outgoing fmtp).
This commit is contained in:
russell@unturf.com 2026-06-01 20:18:32 -04:00
parent 4ad776b9d5
commit 9b3504b163
No known key found for this signature in database

View file

@ -667,7 +667,9 @@ async function sfuPublish(){
const pc = new RTCPeerConnection(rtcConfig);
for (const tr of micStream.getTracks()){ tagTrack(tr); pc.addTrack(tr, micStream); }
setSenderBitrate(pc.getSenders().find(s=>s.track && s.track.kind==='audio'));
await pc.setLocalDescription(await pc.createOffer());
const offer = await pc.createOffer();
offer.sdp = preferStereoOpus(offer.sdp, musicMode ? 256000 : 40000);
await pc.setLocalDescription(offer);
await waitForIceGathering(pc);
const res = await fetch(SFU_BASE + '/publish?room=' + encodeURIComponent(roomID) + '&pub=' + myKeys.pubHex, {
method:'POST', headers:{'Content-Type':'application/json'},
@ -686,15 +688,30 @@ async function sfuPublishScreen(){
if (sfuScreenPC || !myKeys || !roomID) return;
let stream;
try {
stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
/* broadcast-quality capture: 1080p30 video, raw stereo 48kHz audio.
* Browsers treat these as 'ideal' — if a window is smaller it downscales
* gracefully; nothing is rejected. The constraint matters for the audio
* side: without channelCount:2 + sampleRate:48000, getDisplayMedia on
* Chrome can hand back mono 16kHz, which kills music quality. */
stream = await navigator.mediaDevices.getDisplayMedia({
video: { width:{ideal:1920}, height:{ideal:1080}, frameRate:{ideal:30} },
audio: { echoCancellation:false, noiseSuppression:false, autoGainControl:false,
channelCount:2, sampleRate:48000 }
});
} catch(e){ logLine('err','screen share cancelled: '+e.message); return; }
sfuScreenStream = stream;
const pc = new RTCPeerConnection(rtcConfig);
for (const tr of stream.getTracks()) pc.addTrack(tr, stream);
for (const tr of stream.getTracks()){
if (tr.kind === 'video') tr.contentHint = 'detail'; /* favour pixel fidelity over framerate */
if (tr.kind === 'audio') tr.contentHint = 'music';
pc.addTrack(tr, stream);
}
/* the user can stop the share from the browser's native "stop sharing"
* bar — propagate that into a clean unpublish */
stream.getVideoTracks()[0].addEventListener('ended', () => { sfuUnpublishScreen(); });
await pc.setLocalDescription(await pc.createOffer());
const offer = await pc.createOffer();
offer.sdp = preferStereoOpus(offer.sdp, 256000);
await pc.setLocalDescription(offer);
await waitForIceGathering(pc);
const url = SFU_BASE + '/publish?room=' + encodeURIComponent(roomID)
+ '&pub=' + myKeys.pubHex + '&kind=screen';
@ -707,6 +724,14 @@ async function sfuPublishScreen(){
const ans = await res.json();
await pc.setRemoteDescription({ type:'answer', sdp: ans.sdp });
sfuScreenPC = pc; sfuScreenPeerID = ans.peer_id;
/* raise the RTP-level caps: 6 Mbps for video (high-detail 1080p screen),
* 256 kbps for audio (transparent stereo Opus). The codec-level cap was
* already raised via preferStereoOpus(). */
for (const s of pc.getSenders()){
if (!s.track) continue;
if (s.track.kind === 'video') setSenderMaxBitrate(s, 6000000);
if (s.track.kind === 'audio') setSenderMaxBitrate(s, 256000);
}
logLine('', 'sfu: sharing screen as '+shortHex(sfuScreenPeerID));
/* render a muted local preview so the publisher sees what they're
* sharing — SFU does not echo the publisher's own stream back */
@ -847,8 +872,12 @@ async function refreshTurnCred(){
* ================================================================== */
let micStream = null, audioCtx = null, musicMode = false, micDeviceId = '';
function micConstraints(){
/* music mode = high-fidelity broadcast: stereo, raw, 48kHz so we can
* push 256kbps Opus and let Opus's stereo modes carry music properly.
* voice mode stays mono + the three cleanups so speech is intelligible. */
const base = musicMode
? { echoCancellation:false, noiseSuppression:false, autoGainControl:false }
? { echoCancellation:false, noiseSuppression:false, autoGainControl:false,
channelCount:2, sampleRate:48000, sampleSize:16 }
: { echoCancellation:true, noiseSuppression:true, autoGainControl:true };
if (micDeviceId) base.deviceId = { exact: micDeviceId };
return base;
@ -881,10 +910,41 @@ async function setSenderBitrate(sender){
try {
const p = sender.getParameters();
if (!p.encodings || !p.encodings.length) p.encodings = [{}];
p.encodings[0].maxBitrate = musicMode ? 160000 : 40000;
/* 256 kbps stereo Opus is roughly transparent for music; 40 kbps mono is
* plenty for speech */
p.encodings[0].maxBitrate = musicMode ? 256000 : 40000;
await sender.setParameters(p);
} catch(_){}
}
/* explicit bitrate setter for non-mic senders (screen video, screen audio).
* setSenderBitrate above is locked to the mic's musicMode value. */
async function setSenderMaxBitrate(sender, bps){
if (!sender) return;
try {
const p = sender.getParameters();
if (!p.encodings || !p.encodings.length) p.encodings = [{}];
p.encodings[0].maxBitrate = bps;
await sender.setParameters(p);
} catch(_){}
}
/* munge the offer SDP so Opus negotiates stereo + a high maxaveragebitrate.
* Browsers omit stereo=1 unless they're sure the track is stereo, and the
* codec-level maxaveragebitrate cap (separate from RTP-level maxBitrate)
* has to be raised explicitly for music to actually use the headroom. */
function preferStereoOpus(sdp, maxAvgBps){
return sdp.replace(/a=fmtp:(\d+) ([^\r\n]*minptime=10[^\r\n]*)/g, (m, pt, fmtp) => {
const want = { 'stereo': '1', 'sprop-stereo': '1', 'maxaveragebitrate': String(maxAvgBps) };
const parts = fmtp.split(';').map(s => s.trim()).filter(Boolean);
const seen = new Set();
for (let i = 0; i < parts.length; i++){
const k = parts[i].split('=')[0];
seen.add(k);
if (want[k] !== undefined) parts[i] = k + '=' + want[k];
}
for (const k of Object.keys(want)) if (!seen.has(k)) parts.push(k + '=' + want[k]);
return 'a=fmtp:' + pt + ' ' + parts.join(';');
});
}
async function applyMicMode(){
/* re-acquire mic with new constraints, hot-swap onto every live sender
* (mesh peers + the SFU publish PC) */
@ -896,9 +956,18 @@ async function applyMicMode(){
if (sender){ try { await sender.replaceTrack(nt); } catch(_){} setSenderBitrate(sender); }
}
for (const [_, pc] of peers) await swap(pc);
if (sfuPubPC) await swap(sfuPubPC);
/* SFU PC needs a full renegotiation — replaceTrack alone doesn't change
* the negotiated Opus fmtp (stereo/maxaveragebitrate), so a mono publish
* keeps emitting mono even after we swap in a stereo track. Tear down
* and re-publish so the new SDP carries the music-mode codec params. */
if (sfuPubPC){
await sfuUnpublish();
}
if (micStream) micStream.getTracks().forEach(t=>t.stop());
micStream = ns;
if (myRole && canSpeak(myRole)){
sfuPublish().catch(e => logLine('err','sfu re-publish: '+e.message));
}
/* old analyser is now dead — rewire local meter against the fresh stream */
if (myUUID){ stopMeter(myUUID); startMeter(myUUID, micStream); }
}
@ -1284,7 +1353,9 @@ async function connectToPeer(uuid, weOffer){
}
};
if (weOffer){
await pc.setLocalDescription(await pc.createOffer());
const offer = await pc.createOffer();
offer.sdp = preferStereoOpus(offer.sdp, musicMode ? 256000 : 40000);
await pc.setLocalDescription(offer);
await waitForIceGathering(pc);
await sendEncSDP(uuid, 'offer', pc.localDescription);
}
@ -1589,8 +1660,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> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">051db7e4a0ad45f749f2de691bb1f7c7</span><br>
sha256 <span class="stamp-sha">38861760202e2d6af7bc264bdf5243e2d25fa211af18c9bbd443f8867f9dd7a5</span><br>
md5 <span class="stamp-md5">0f70c2982b75f949b39a25acdbeed454</span><br>
sha256 <span class="stamp-sha">d746c549cf08f71203763b4e4845b2e1589acfc730185819ff848415d1cce059</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>