mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-16 07:13:12 -06:00
ROOT CAUSE (hypothesis A, pre-existing — NOT a beta7 regression; server/player/index.html is untouched since v1.9.2-beta6): handlePlaylistUpdate's "Playlist unchanged" branch blindly returned. The media re-attach (renderContent) lives ONLY in the content-changed branch, so if the <video> surface was lost (element detached from the DOM while still decoding — video gone, audio still playing) a no-new-content refresh never re-attached it. New-content refreshes were fine because they re-render. FIX (make the refresh idempotent for the media surface, no flicker on the healthy path): - server/lib/player-media-health.js (new, UMD + unit-testable, mirrors schedule-eval.js): needsReattach(state) — re-attach ONLY when playback should be happening but the current item's surface is actually lost (video null / detached / ended / errored; non-video: no mounted surface). A healthy attached+live video returns false, so a routine poll stays a no-op (no re-render, no flicker). Served at /player/player-media-health.js from the single source; loaded by the player. - index.html no-change branch: extract the current item's DOM facts and, iff PlayerMediaHealth.needsReattach, call playCurrentItem() to re-render the current item. Wrapped so the health check can never break a refresh. - teardownCurrentMedia: also release currentVideoEl even when it was DETACHED from the container — a detached-but-playing <video> keeps emitting audio and the container-scoped querySelectorAll can't find it. This kills the "ghost audio" on re-attach. - sw.js cache bumped v9 -> v10 so players pick up the new index.html + module. Tests: test/player-media-health.test.js (6) exercises the branch selection — healthy video -> no re-attach; detached/null/ended/errored -> re-attach; idle -> never; non-video by surface presence. Inline player JS syntax-checked; module served + referenced verified on a booted server. Suite 316/316. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
43 lines
2.2 KiB
JavaScript
43 lines
2.2 KiB
JavaScript
'use strict';
|
|
|
|
// #146 web-player fix — the no-change-refresh branch-selection decision. This is the unit
|
|
// the refresh handler consults to decide whether to re-attach the media surface. It is the
|
|
// smallest testable piece of the "no new content lost the video but not the audio" fix.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { needsReattach } = require('../lib/player-media-health');
|
|
|
|
const video = (o) => ({ isPlaying: true, hasCurrentItem: true, itemKind: 'video', videoEl: o, surfaceAttached: true });
|
|
|
|
test('healthy attached+live video on a no-change refresh -> NO re-attach (no flicker)', () => {
|
|
assert.equal(needsReattach(video({ attached: true, ended: false, errored: false })), false);
|
|
});
|
|
|
|
test('THE BUG: a detached-but-playing <video> (audio persists, surface gone) -> re-attach', () => {
|
|
assert.equal(needsReattach(video({ attached: false, ended: false, errored: false })), true);
|
|
});
|
|
|
|
test('video element gone entirely -> re-attach', () => {
|
|
assert.equal(needsReattach({ isPlaying: true, hasCurrentItem: true, itemKind: 'video', videoEl: null }), true);
|
|
});
|
|
|
|
test('ended or errored video -> re-attach', () => {
|
|
assert.equal(needsReattach(video({ attached: true, ended: true, errored: false })), true);
|
|
assert.equal(needsReattach(video({ attached: true, ended: false, errored: true })), true);
|
|
});
|
|
|
|
test('idle / no current item -> never re-attach (leave the waiting screen)', () => {
|
|
assert.equal(needsReattach({ isPlaying: false, hasCurrentItem: true, itemKind: 'video', videoEl: null }), false);
|
|
assert.equal(needsReattach({ isPlaying: true, hasCurrentItem: false, itemKind: 'video', videoEl: null }), false);
|
|
assert.equal(needsReattach(undefined), false);
|
|
});
|
|
|
|
test('non-video surface (image/youtube/widget): re-attach only when the surface is missing', () => {
|
|
const base = { isPlaying: true, hasCurrentItem: true };
|
|
assert.equal(needsReattach({ ...base, itemKind: 'image', surfaceAttached: true }), false);
|
|
assert.equal(needsReattach({ ...base, itemKind: 'image', surfaceAttached: false }), true);
|
|
assert.equal(needsReattach({ ...base, itemKind: 'youtube', surfaceAttached: false }), true);
|
|
assert.equal(needsReattach({ ...base, itemKind: 'widget', surfaceAttached: true }), false);
|
|
});
|