Keep a playing video when the window comes back to the front (#402)
Chrome draws no frames for a hidden page, so the scene stopped asking
for its videos. The sweeper of the VideoLibrary kept running and
dropped every player after 2 s. Back in the window, the video started
over, and Pixi warned that a texture source was destroyed while still
bound to a shader.
- The sweeper skips a hidden page. The time hidden does not count as
idle, the 2 s start once the page is shown again.
- A dropped player only unloads its video source instead of destroying
it. A shader that still holds the source no longer warns.
- CI serves videos through a new router for php -S. The built-in
server answers no Range requests, so Chrome could not seek. e2e 66
had 4 red checks in the suite job of d48f174 because of it. nginx on
stein2 hid this.
Tests:
- Unit video-library.test.ts (new): 3 tests
- e2e 66 (atx): ALL PASSED, 18 checks
- Router checked with curl: 206 with the right bytes
- Counter-check: the old sweep fails both hidden-page tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d48f174caf
commit
c815d2c35f
4 files changed
+154
-8
No files matched your search
@@ -36,6 +36,15 @@ export class VideoLibrary {
|
||||
private heard = false;
|
||||
private paused = false;
|
||||
private sweeper: ReturnType<typeof setInterval> | null = null;
|
||||
/** When the page was last shown, by `performance.now()`; see `sweep`. */
|
||||
private shown = 0;
|
||||
|
||||
constructor() {
|
||||
if (typeof document === 'undefined') return;
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) this.shown = performance.now();
|
||||
});
|
||||
}
|
||||
|
||||
/** Runs `fn` whenever a player has its first picture; returns the unsubscribe. */
|
||||
onChange(fn: () => void): () => void {
|
||||
@@ -85,12 +94,16 @@ export class VideoLibrary {
|
||||
this.stopSweeping();
|
||||
}
|
||||
|
||||
/** Drops the players nobody asked for lately; a paused run asks for nothing and keeps them. */
|
||||
/**
|
||||
* Drops the players nobody asked for lately; a paused run asks for nothing and keeps them. So
|
||||
* does a hidden page: Chrome draws no frames for it while its timers go on, and a sprite that
|
||||
* still wears a video would lose it. The time hidden does not count as idle.
|
||||
*/
|
||||
private sweep(): void {
|
||||
if (this.paused) return;
|
||||
if (this.paused || document.hidden) return;
|
||||
const now = performance.now();
|
||||
for (const [key, player] of this.players)
|
||||
if (now - player.asked > IDLE_MS) {
|
||||
if (now - Math.max(player.asked, this.shown) > IDLE_MS) {
|
||||
this.drop(player);
|
||||
this.players.delete(key);
|
||||
}
|
||||
@@ -181,12 +194,14 @@ export class VideoLibrary {
|
||||
player.element.load();
|
||||
// The texture itself stays whole: a wearer that has not synced since still draws it, and a
|
||||
// destroyed one has no source and threw in the batcher. It shows nothing until the wearer asks
|
||||
// again; only the video behind it is let go.
|
||||
// again. The video source is only unloaded, not destroyed: a shader may still hold it until
|
||||
// its wearer syncs, and Pixi warns about every destroyed source still bound to one.
|
||||
const texture = player.texture;
|
||||
if (!texture) return;
|
||||
const video = texture.source;
|
||||
const video = texture.source as VideoSource;
|
||||
texture.source = Texture.EMPTY.source;
|
||||
video.destroy();
|
||||
video.autoUpdate = false;
|
||||
video.unload();
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
|
||||
Reference in new issue
Block a user