The Tizen .wgt player black-flashed between IMAGE items on slow decode HW
(Samsung OM55B / SSSP). Root cause: playCurrent() called clearStage() before
renderImage() set img.src, so the stage was empty (black) until the new image
decoded. Images had no decode-gated double-buffer — video gained one in #167
(938a43a), which made the always-present image flash conspicuous by contrast.
renderImage has been byte-identical since the first Tizen commit, so there was
nothing to revert; the buffer had to be ADDED, mirroring the video path.
- preloadImage()/_takePreloadImage()/_releasePreloadImage(): one-ahead,
image-only decode-gated buffer modeled on preloadVideo()/_takePreload().
Detached <img>, src set, warmed via HTMLImageElement.decode() (feature-
detected — onload/complete fallback for Tizen 5.0 / SSSP6). Warmed when the
current image begins its dwell and from the group-sync boundary tick.
- renderImage now SWAPS: take the pre-decoded <img> (or decode a fresh one) and
only THEN clearStage()+append, in one synchronous block — the compositor
never sees an empty stage. Never clear-then-load on the image path.
- Scoped to images only: playCurrent() skips the up-front clearStage() solely
for image targets (same branch order as the dispatch); video/youtube/widget
keep their pre-dispatch clear untouched. onerror and decode() rejection route
to skipSoon(); stale-index guard blocks mounting a stale decode over the
current item after next()/gotoIndex/load(); one-ahead with stale release on
index move, load(), stop(), and group-sync exit. #A1 single-item heal intact.
Tests:
- server/test/tizen-image-blackflash.test.js (new, 4/4): loads the real
player.js in a vm context with a test-controllable decode() Promise and proves
the invariant — across image->image the #stage is NEVER without a mounted
<img> (old element held until the new image's decode()/onload resolves, then
swap). Covers decode() supported, decode() absent (onload fallback), and
broken-image (decode-reject / onerror) -> skipSoon. Proven to VIOLATE on the
old clear-then-load ordering and HOLD on the fix.
- server/test/pip-overlay.test.js: the decode-gate makes image mount async,
which broke its older shim (no decode()/onload/complete). Teach the shim
element complete/naturalWidth so renderImage takes its synchronous
complete-fallback branch and mounts. Test-only.
Full server suite 486/486. node -c clean. Headless proves the DOM ordering
invariant (the flash's precondition); final black-frame sign-off needs a real
OM55B panel (manual steps in the test-file header).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>