Silence a free video once its sprite is destroyed (#420)

A sprite whose Video clock is free played its video on after it was
destroyed, heard in a game, until the player was let go 2 to 4 s
later. A driven one was already held and quiet after 100 ms.

- The library holds and quiets a free player nothing asked for in
  HUSH_MS, as it did a driven one. That covers Destroy, a scene load,
  the end of a run and a sprite switched off.
- Asked for again before it is let go, it plays on from where it was
  held, heard only in a run. A hidden page leaves it alone, as the
  sweep does.
- A sprite only out of view keeps its free video playing and heard.
  Whether a video is heard should not hang on the camera. Before, it
  ran 2 to 4 s and was then let go. Now it plays on, which opens its
  player even where it was never in view.

Tests:
- Unit: video-library 1 new. A free player losing its wearer is held
  and quiet 200 ms later, stays so through a new run, plays on heard
  when asked again with the same element, and runs on on a hidden
  page.
- e2e 66 on melektaus-atx: ALL PASSED, 36 checks, 66g-ii new. Heard
  31 frames before Destroy, held and muted in every frame read after.
- Counter-probe without hushing free players: the unit test and only
  66g-ii fail, the player plays on heard (paused false, muted false).
- Gate on melektaus-atx: 309 files, 4797 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-14 09:49:12 +02:00
1 parent acd12326dc
commit 875c76026a
5 files changed
+166 -19

No files matched your search

+58
View File
@@ -258,6 +258,64 @@ describe('VideoLibrary driven players (#404)', () => {
vi.useRealTimers();
});
it('holds a free player quiet once no sprite wears it, and plays it on when one does (#420)', async () => {
vi.useFakeTimers({ toFake: ['setInterval', 'clearInterval', 'performance'] });
const page = { hidden: false };
const events = new Map<string, () => void>();
let frames: (() => void)[] = [];
const element = {
...fakeVideo(),
play() {
this.paused = false;
return Promise.resolve();
},
pause() {
this.paused = true;
},
addEventListener: (type: string, listener: () => void) => events.set(type, listener),
requestVideoFrameCallback: (callback: () => void) => frames.push(callback),
};
vi.stubGlobal('document', {
get hidden() {
return page.hidden;
},
addEventListener: () => {},
createElement: () => element,
});
const { soundLibrary } = await import('../../src/viewport/SoundLibrary');
vi.spyOn(soundLibrary, 'mediaInput').mockReturnValue(null);
const videos = await freshLibrary();
const drawFor = (ms: number) => {
for (let at = 0; at < ms; at += 1000 / 60) {
videos.get(KEY);
vi.advanceTimersByTime(1000 / 60);
}
};
videos.get(KEY);
events.get('loadeddata')!();
[frames] = [[], ...frames.map((frame) => frame())];
videos.startRun();
drawFor(1000);
const state = () => [element.paused, element.muted];
expect(state()).toEqual([false, false]);
// Its sprite is destroyed: held and quiet at once, not heard until it is let go 2-4 s later.
vi.advanceTimersByTime(200);
expect(state()).toEqual([true, true]);
// A run started meanwhile takes it back to its start, still quiet.
videos.startRun();
vi.advanceTimersByTime(200);
expect(state()).toEqual([true, true]);
// Worn again before it is let go: it plays on, heard, the same video.
videosMade = 0;
drawFor(500);
expect([...state(), videosMade]).toEqual([false, false, 0]);
// A hidden page draws nothing, and asks for nothing: it plays on as before.
page.hidden = true;
vi.advanceTimersByTime(1000);
expect(state()).toEqual([false, false]);
vi.useRealTimers();
});
it('tells a view whether one of its driven players still waits for its first picture', async () => {
const videos = await freshLibrary();
videos.get(KEY);