zebra-spaces: tap-to-play overlay + HTML muted attribute — fixes Firefox Android autoplay
Firefox Android (and other strict mobile browsers) check the HTML 'muted' attribute, not the IDL .muted property, when deciding whether MediaStream <video> is autoplay-eligible. Setting only the property left the video ineligible, so play() rejected silently and the user saw a black tile. Three changes: - set the autoplay/playsinline/muted attributes alongside the properties so every UA's autoplay heuristic agrees the element is eligible - log play() rejections instead of swallowing them — silent failures hid this from us until now - when play() does reject, show a 'tap to play' overlay that covers the video area (but not the meta bar); tapping counts as the gesture and the retry succeeds
This commit is contained in:
parent
5affebead5
commit
6fbeade12d
1 changed files with 43 additions and 10 deletions
|
|
@ -63,7 +63,7 @@
|
|||
#screens { display: grid; grid-template-columns: 1fr; gap: 0.75rem; }
|
||||
.screen-tile {
|
||||
border: 1px solid #000; background: #000; padding: 0;
|
||||
display: flex; flex-direction: column;
|
||||
display: flex; flex-direction: column; position: relative;
|
||||
}
|
||||
.screen-tile video {
|
||||
width: 100%; height: auto; max-height: 92vh; display: block; background: #000;
|
||||
|
|
@ -72,6 +72,17 @@
|
|||
background: #111; color: #ddd; font-size: 0.7rem; padding: 0.3rem 0.5rem;
|
||||
display: flex; justify-content: space-between;
|
||||
}
|
||||
/* Firefox Android rejects MediaStream <video> autoplay even when muted.
|
||||
* When play() rejects we surface a tap-to-play overlay over the video
|
||||
* area only (the meta bar stays clickable); the click counts as the
|
||||
* gesture so the retry play() succeeds. */
|
||||
.screen-tile .tap-play {
|
||||
position: absolute; left: 0; right: 0; top: 0; bottom: 1.6rem;
|
||||
display: none; align-items: center; justify-content: center;
|
||||
background: rgba(0,0,0,0.65); color: #fff; cursor: pointer;
|
||||
font-family: monospace; font-size: 1rem; user-select: none;
|
||||
}
|
||||
.screen-tile.needs-tap .tap-play { display: flex; }
|
||||
/* sec-screens visible → push everything else in the timeline column down or hide */
|
||||
.timeline:has(> #sec-screens:not(.hidden)) > .game-tabs,
|
||||
.timeline:has(> #sec-screens:not(.hidden)) > #game-frame { display: none; }
|
||||
|
|
@ -613,11 +624,20 @@ function renderScreenTile(pubHex, stream, opts){
|
|||
if (!entry){
|
||||
const tile = document.createElement('div'); tile.className = 'screen-tile';
|
||||
const video = document.createElement('video');
|
||||
/* mobile autoplay policy refuses an unmuted <video> with audio without
|
||||
* a user gesture — the whole element stays paused, no pixels render.
|
||||
* Start muted on remote tiles too; an explicit unmute button below
|
||||
* lets the user opt into audio with a click (counts as the gesture). */
|
||||
/* set BOTH the HTML attributes and the IDL properties — Firefox Android
|
||||
* checks the attribute when deciding autoplay eligibility, not just the
|
||||
* .muted property. Without the attribute, MediaStream-backed video
|
||||
* stays paused even though it's logically muted. */
|
||||
video.setAttribute('autoplay', '');
|
||||
video.setAttribute('playsinline', '');
|
||||
video.setAttribute('muted', '');
|
||||
video.autoplay = true; video.playsInline = true; video.muted = true;
|
||||
const tap = document.createElement('div'); tap.className = 'tap-play';
|
||||
tap.textContent = 'tap to play';
|
||||
tap.onclick = () => {
|
||||
try { video.play().then(() => { tile.classList.remove('needs-tap'); })
|
||||
.catch(e => logLine('err','play after tap: '+e.message)); } catch(_){}
|
||||
};
|
||||
const meta = document.createElement('div'); meta.className = 'screen-meta';
|
||||
const who = document.createElement('span'); who.textContent = 'screen: '+label;
|
||||
const ctl = document.createElement('span');
|
||||
|
|
@ -629,8 +649,9 @@ function renderScreenTile(pubHex, stream, opts){
|
|||
unmute.onclick = () => {
|
||||
const turnOn = video.muted;
|
||||
video.muted = !turnOn;
|
||||
if (video.muted) video.setAttribute('muted', ''); else video.removeAttribute('muted');
|
||||
unmute.textContent = video.muted ? 'unmute audio' : 'mute audio';
|
||||
try { video.play().catch(()=>{}); } catch(_){}
|
||||
try { video.play().catch(e => logLine('err','play after unmute: '+e.message)); } catch(_){}
|
||||
};
|
||||
ctl.appendChild(unmute);
|
||||
}
|
||||
|
|
@ -638,13 +659,25 @@ function renderScreenTile(pubHex, stream, opts){
|
|||
pop.textContent = 'fullscreen'; pop.onclick = () => video.requestFullscreen && video.requestFullscreen().catch(()=>{});
|
||||
ctl.appendChild(pop);
|
||||
meta.appendChild(who); meta.appendChild(ctl);
|
||||
tile.appendChild(video); tile.appendChild(meta);
|
||||
tile.appendChild(video); tile.appendChild(tap); tile.appendChild(meta);
|
||||
$('screens').appendChild(tile);
|
||||
entry = { tile, video, label };
|
||||
screenVideos.set(pubHex, entry);
|
||||
}
|
||||
entry.video.srcObject = stream;
|
||||
try { const p = entry.video.play(); if (p && p.catch) p.catch(()=>{}); } catch(_){}
|
||||
try {
|
||||
const p = entry.video.play();
|
||||
if (p && p.then){
|
||||
p.then(() => { entry.tile.classList.remove('needs-tap'); })
|
||||
.catch(e => {
|
||||
logLine('err','autoplay blocked: '+e.message+' — tap the tile to play');
|
||||
entry.tile.classList.add('needs-tap');
|
||||
});
|
||||
}
|
||||
} catch(e){
|
||||
logLine('err','play threw: '+e.message);
|
||||
entry.tile.classList.add('needs-tap');
|
||||
}
|
||||
$('sec-screens').classList.remove('hidden');
|
||||
logLine('', 'screen share: '+(local?'local preview ':'receiving ')+label);
|
||||
}
|
||||
|
|
@ -1660,8 +1693,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-02</span><br>
|
||||
md5 <span class="stamp-md5">0f70c2982b75f949b39a25acdbeed454</span><br>
|
||||
sha256 <span class="stamp-sha">d746c549cf08f71203763b4e4845b2e1589acfc730185819ff848415d1cce059</span><br>
|
||||
md5 <span class="stamp-md5">dcf58a2cc3ae320f3d73da16ea79471b</span><br>
|
||||
sha256 <span class="stamp-sha">5bc64c4b97bf6a3426ca2b682da978c8950689e198b8b9c3fab8f554ede05733</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