mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
screen_off must tear the video down — a DOM overlay cannot cover a hardware plane
Blanking the screen took three attempts on real hardware, and each failure was
the same lesson from a different angle:
1. black overlay -> the video played straight THROUGH it. With hwz
enabled the widget decodes onto a hardware plane and
the graphics plane sits behind it; z-index is
irrelevant across planes.
2. pause + hide element -> playback stopped and the LAST DECODED FRAME stayed
on screen. Hiding a DOM element does nothing to the
plane, which is not part of the DOM.
3. pause + remove src -> releases the plane. Black.
+ load()
Coming back re-mounts through nextItem(), because a torn-down element cannot be
resurrected. The playlist keeps advancing while the screen is off, so each newly
started item is torn down as well — caught on 'play' in the capture phase, or the
next video lights the panel back up a few seconds later.
CEC is now explicitly not load-bearing. Our XT245 logs "failed to get cec clock"
and does not respond to it at all, which is precisely why blanking cannot depend
on a cooperative display: plenty ignore broadcast CEC or need direct addressing.
displayPower() stays as opportunistic best-effort alongside the teardown.
Verified on hardware: not black, then frozen frame, then black.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
parent
c743aa4b81
commit
141deb97a5
|
|
@ -160,9 +160,29 @@ and the platform APIs:
|
|||
| `set_volume` | — | applied to current and future media |
|
||||
| `refresh` | `location.reload()` | widget rebuilt by the host (reload is unreliable here) |
|
||||
|
||||
`displayPower()` is best effort and returns false when CEC is unavailable, so the overlay is
|
||||
applied either way and something visible always happens — some displays ignore broadcast CEC and
|
||||
need direct addressing. Volume is re-applied on every `play` event in the capture phase, because
|
||||
### ⚠️ Nothing in the DOM can cover video
|
||||
|
||||
With `hwz_default: "on"` the widget decodes video onto a **hardware plane**, and the graphics plane
|
||||
— everything in the DOM — sits behind it. Blanking the screen took three attempts on real hardware,
|
||||
and each failure taught the same lesson from a different angle:
|
||||
|
||||
1. **Black overlay** → the video played straight *through* it. A `z-index: 9999` div cannot cover a
|
||||
hardware plane.
|
||||
2. **Pause + hide the element** → playback stopped, but the **last decoded frame stayed on screen**.
|
||||
Hiding a DOM element does nothing to the plane; the plane is not part of the DOM.
|
||||
3. **Pause + `removeAttribute('src')` + `load()`** → releases the plane. Black at last.
|
||||
|
||||
Coming back out re-mounts through `nextItem()`, because a torn-down element cannot be resurrected.
|
||||
The playlist keeps advancing while the screen is off, so each newly started item is torn down too,
|
||||
caught on the `play` event in the capture phase — otherwise the next video lights the panel back up.
|
||||
|
||||
Any feature that assumes an overlay can hide video needs rethinking here: screen blanking, masking,
|
||||
fades over video.
|
||||
|
||||
`displayPower()` (CEC) is best effort and deliberately **not** load-bearing — it returns false when
|
||||
CEC is unavailable and the media teardown does the real work. Our XT245 reports
|
||||
`failed to get cec clock` in the kernel log and does not respond to CEC at all, which is exactly why
|
||||
blanking must not depend on it. Plenty of displays ignore broadcast CEC or need direct addressing. Volume is re-applied on every `play` event in the capture phase, because
|
||||
media elements are created per item across several code paths and setting it once would otherwise
|
||||
last only until the playlist advanced.
|
||||
|
||||
|
|
|
|||
|
|
@ -1463,9 +1463,9 @@
|
|||
socket.on('device:command', (data) => {
|
||||
console.log('Command:', data.type);
|
||||
if (data.type === 'refresh') restartPlayer('operator refresh');
|
||||
if (data.type === 'launch') { document.getElementById('screenOffOverlay')?.remove(); setDisplayPower(true); }
|
||||
if (data.type === 'launch') { document.getElementById('screenOffOverlay')?.remove(); screenIsOff = false; suppressMedia(false); setDisplayPower(true); }
|
||||
if (data.type === 'screen_off') toggleScreenOff();
|
||||
if (data.type === 'screen_on') { document.getElementById('screenOffOverlay')?.remove(); setDisplayPower(true); }
|
||||
if (data.type === 'screen_on') { document.getElementById('screenOffOverlay')?.remove(); screenIsOff = false; suppressMedia(false); setDisplayPower(true); }
|
||||
// A browser tab cannot reboot its host, so the web player has always ignored this and the
|
||||
// dashboard button did nothing on it. A BrightSign can, through the host script.
|
||||
if (data.type === 'reboot') {
|
||||
|
|
@ -3506,16 +3506,55 @@
|
|||
// in the CAPTURE phase applies it to every element that ever starts, from one place. Media
|
||||
// events do not bubble, which is why capture is required rather than a plain listener.
|
||||
document.addEventListener('play', (e) => {
|
||||
if (mediaVolume == null) return;
|
||||
const el = e.target;
|
||||
if (!el || (el.tagName !== 'VIDEO' && el.tagName !== 'AUDIO')) return;
|
||||
// The playlist advances while the screen is off; hold each new item down as it starts,
|
||||
// or the next video appears on the hardware plane and the screen lights back up.
|
||||
if (screenIsOff) {
|
||||
try { el.pause(); el.removeAttribute('src'); el.load(); el.style.visibility = 'hidden'; } catch (err) {}
|
||||
return;
|
||||
}
|
||||
if (mediaVolume == null) return;
|
||||
try { if (typeof isWallFollower === 'function' && isWallFollower()) return; } catch (err) { /* not a wall */ }
|
||||
try { el.volume = mediaVolume; el.muted = mediaVolume === 0; } catch (err) { /* gone */ }
|
||||
}, true);
|
||||
|
||||
// Screen-off state, tracked because the playlist keeps advancing while the screen is "off"
|
||||
// and every newly mounted item has to be held down too.
|
||||
let screenIsOff = false;
|
||||
|
||||
// Stop media rather than cover it. On BrightSign the widget runs with hardware z-order
|
||||
// (hwz), so video decodes onto a HARDWARE PLANE and a DOM overlay — which lives in the
|
||||
// graphics plane — cannot hide it. The overlay goes up and the video plays straight through
|
||||
// it. Pausing and hiding the element is what actually blanks the screen there, and it is
|
||||
// harmless everywhere else.
|
||||
function suppressMedia(off) {
|
||||
if (!off) {
|
||||
// Coming back: re-mount current content rather than trying to resurrect a torn-down
|
||||
// element. nextItem() rebuilds from scratch, which is the same path a normal advance uses.
|
||||
document.querySelectorAll('video, audio').forEach((el) => { try { el.style.visibility = ''; } catch (e) {} });
|
||||
try { if (typeof nextItem === 'function') nextItem(); } catch (e) { /* fall back to whatever is on screen */ }
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll('video, audio').forEach((el) => {
|
||||
try {
|
||||
el.pause();
|
||||
// Pausing is not enough on a hardware plane: the last decoded frame STAYS on screen,
|
||||
// which is why the panel showed a frozen image rather than going black. Tearing the
|
||||
// source down releases the plane. Hiding the element does nothing to it either — the
|
||||
// plane is not part of the DOM.
|
||||
el.removeAttribute('src');
|
||||
el.load();
|
||||
el.style.visibility = 'hidden';
|
||||
} catch (e) { /* element torn down mid-call */ }
|
||||
});
|
||||
}
|
||||
|
||||
function toggleScreenOff() {
|
||||
let overlay = document.getElementById('screenOffOverlay');
|
||||
if (overlay) { overlay.remove(); setDisplayPower(true); return; }
|
||||
if (overlay) { overlay.remove(); screenIsOff = false; suppressMedia(false); setDisplayPower(true); return; }
|
||||
screenIsOff = true;
|
||||
suppressMedia(true);
|
||||
setDisplayPower(false);
|
||||
overlay = document.createElement('div');
|
||||
overlay.id = 'screenOffOverlay';
|
||||
|
|
|
|||
Loading…
Reference in a new issue