zebra-spaces: rebuild <video> on MSID supplant — autoplay grant resets
Live-room defect (2026-06-03): Will refreshed his browser; the supplant fired a SECOND ontrack for kind=camera pub=Will at +16s. The page swapped srcObject on the existing <video>, called play(), got 'fetching process for the media resource was aborted by the user agent at the user's request' — browser refused to start a new playback session on the same element after its autoplay grant had already been consumed. Tile sat black on the moderator's screen. Fix: swapFreshVideoElement() — on supplant, replace the <video> with a freshly-built one carrying the same attrs. A brand-new <video> is eligible for muted-autoplay even when the prior one had its play() rejected, so the supplant lands cleanly without needing a tap. Applied to both the thumbnail and the spotlight tile. Mesh test harness extracts the helper so renderVideoTile still runs end-to-end under the sandbox.
This commit is contained in:
parent
3ca9042e54
commit
fb7228b289
2 changed files with 44 additions and 3 deletions
|
|
@ -51,6 +51,7 @@ function extractConst(name){
|
||||||
|
|
||||||
const watchSrc = extractFn(/function watchVideoTrackForRemoval\(/);
|
const watchSrc = extractFn(/function watchVideoTrackForRemoval\(/);
|
||||||
const firstFrameSrc = extractFn(/function watchFirstFrame\(/);
|
const firstFrameSrc = extractFn(/function watchFirstFrame\(/);
|
||||||
|
const swapVideoSrc = extractFn(/function swapFreshVideoElement\(/);
|
||||||
const renderTileSrc = extractFn(/function renderVideoTile\(/);
|
const renderTileSrc = extractFn(/function renderVideoTile\(/);
|
||||||
const removeTileSrc = extractFn(/function removeVideoTile\(/);
|
const removeTileSrc = extractFn(/function removeVideoTile\(/);
|
||||||
const renderScreenShim = extractFn(/function renderScreenTile\(/);
|
const renderScreenShim = extractFn(/function renderScreenTile\(/);
|
||||||
|
|
@ -231,6 +232,7 @@ function makeBrowser(name, pubKeyHex){
|
||||||
tileKindsSrc + '\n' +
|
tileKindsSrc + '\n' +
|
||||||
watchSrc + '\n' +
|
watchSrc + '\n' +
|
||||||
firstFrameSrc + '\n' +
|
firstFrameSrc + '\n' +
|
||||||
|
swapVideoSrc + '\n' +
|
||||||
renderTileSrc + '\n' +
|
renderTileSrc + '\n' +
|
||||||
removeTileSrc + '\n' +
|
removeTileSrc + '\n' +
|
||||||
renderScreenShim + '\n' +
|
renderScreenShim + '\n' +
|
||||||
|
|
|
||||||
|
|
@ -1869,6 +1869,26 @@ function updateContainerVisibility(){
|
||||||
$('tiles-h2').classList.toggle('hidden', !anyThumb);
|
$('tiles-h2').classList.toggle('hidden', !anyThumb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Replace a <video> with a freshly-built one carrying the same
|
||||||
|
* autoplay/playsinline/muted attributes, in the same DOM slot. Returns
|
||||||
|
* the new element so the caller can update its reference. Browsers
|
||||||
|
* treat a brand-new <video> as eligible for muted-autoplay even when
|
||||||
|
* the old element had its play() rejected — so use this on any
|
||||||
|
* supplant where srcObject would otherwise be swapped mid-life. */
|
||||||
|
function swapFreshVideoElement(oldVideo){
|
||||||
|
if (!oldVideo) return oldVideo;
|
||||||
|
const fresh = document.createElement('video');
|
||||||
|
fresh.setAttribute('autoplay', '');
|
||||||
|
fresh.setAttribute('playsinline', '');
|
||||||
|
fresh.setAttribute('muted', '');
|
||||||
|
fresh.autoplay = true; fresh.playsInline = true; fresh.muted = true;
|
||||||
|
if (oldVideo.parentNode){
|
||||||
|
try { oldVideo.srcObject = null; } catch(_){}
|
||||||
|
oldVideo.parentNode.replaceChild(fresh, oldVideo);
|
||||||
|
}
|
||||||
|
return fresh;
|
||||||
|
}
|
||||||
|
|
||||||
/* build a tile DOM. opts.thumb = true for thumb-style (no fullscreen
|
/* build a tile DOM. opts.thumb = true for thumb-style (no fullscreen
|
||||||
* button, gets .tile-thumb class). Returns { tile, video, fsBtn }. */
|
* button, gets .tile-thumb class). Returns { tile, video, fsBtn }. */
|
||||||
function buildTile(kind, pubHex, label, opts){
|
function buildTile(kind, pubHex, label, opts){
|
||||||
|
|
@ -2036,6 +2056,19 @@ function renderVideoTile(kind, pubHex, stream, opts){
|
||||||
$(k.thumbContainer).appendChild(built.tile);
|
$(k.thumbContainer).appendChild(built.tile);
|
||||||
entry = { tile: built.tile, video: built.video, label };
|
entry = { tile: built.tile, video: built.video, label };
|
||||||
k.store.set(pubHex, entry);
|
k.store.set(pubHex, entry);
|
||||||
|
} else {
|
||||||
|
/* MSID-supplant retarget: when the same pubHex+kind republishes
|
||||||
|
* (publisher refreshed / sfu PC rebuilt), reassigning srcObject on
|
||||||
|
* the existing <video> hits the browser's autoplay policy as a
|
||||||
|
* mid-life srcObject swap and a fresh play() rejects with
|
||||||
|
* "fetching process for the media resource was aborted by the
|
||||||
|
* user agent at the user's request" — the tile stays paused on a
|
||||||
|
* black frame. Building a fresh <video> element resets that
|
||||||
|
* decision: muted-video autoplay applies again because the
|
||||||
|
* element has no prior gesture-consumed play() to defend against.
|
||||||
|
* Fox 2026-06-03 ("still black share camera from will" after a
|
||||||
|
* 16s supplant). */
|
||||||
|
entry.video = swapFreshVideoElement(entry.video);
|
||||||
}
|
}
|
||||||
entry.video.srcObject = stream;
|
entry.video.srcObject = stream;
|
||||||
try {
|
try {
|
||||||
|
|
@ -2051,9 +2084,15 @@ function renderVideoTile(kind, pubHex, stream, opts){
|
||||||
logLine('err','play threw: '+e.message);
|
logLine('err','play threw: '+e.message);
|
||||||
entry.tile.classList.add('needs-tap');
|
entry.tile.classList.add('needs-tap');
|
||||||
}
|
}
|
||||||
/* if the same tile is currently spotlit, mirror the stream into the big tile */
|
/* if the same tile is currently spotlit, mirror the stream into the big
|
||||||
|
* tile — same supplant-vs-autoplay trap, same fix. */
|
||||||
if (spotlight && spotlight.kind === kind && spotlight.pubHex === pubHex && spotlight.video){
|
if (spotlight && spotlight.kind === kind && spotlight.pubHex === pubHex && spotlight.video){
|
||||||
|
spotlight.video = swapFreshVideoElement(spotlight.video);
|
||||||
spotlight.video.srcObject = stream;
|
spotlight.video.srcObject = stream;
|
||||||
|
try {
|
||||||
|
const p = spotlight.video.play();
|
||||||
|
if (p && p.catch) p.catch(()=>{ spotlight.tile.classList.add('needs-tap'); });
|
||||||
|
} catch(_){}
|
||||||
}
|
}
|
||||||
/* spotlight promotion rules:
|
/* spotlight promotion rules:
|
||||||
* - no spotlight yet → first tile auto-promotes (anyone)
|
* - no spotlight yet → first tile auto-promotes (anyone)
|
||||||
|
|
@ -4207,8 +4246,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">
|
<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-03</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-03</span><br>
|
||||||
md5 <span class="stamp-md5">c874915d93ed4b044f8a9fd30e542d7c</span><br>
|
md5 <span class="stamp-md5">86b88dea62be004921fe4e116bba90eb</span><br>
|
||||||
sha256 <span class="stamp-sha">e13bfc8b82cd58097e12ac00591b4f2a324f589aaae10f9b488bbe78e6cce82d</span><br>
|
sha256 <span class="stamp-sha">3686823398c477768b1af4f1dbb86f19cf69923e58d1ad94d8ecb7669e613e00</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">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>
|
<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>
|
</footer>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue