Keep a recording going behind another browser tab
Chrome stops calling requestAnimationFrame for a hidden tab, and Pixi's ticker hangs on it. The run, the drawing and the recording all stood still as soon as the user switched to another tab. A plain run keeps standing still there, so a forgotten game does not play on unheard. - backgroundBeat.ts ticks the editor from a worker at the recording's fps while a recording goes and the tab is hidden, or the page has not drawn a frame of its own for 250 ms. Once the page draws again, the beat keeps out of the way. - The worker looks every 4 ms whether a tick is due. A 16 ms interval came in bursts behind a busy page, and the recording threw away the second tick of each burst. - backgroundTimers.ts and timerWorker.ts give the short waits of a recording timers in a worker. Chrome slows the page's own timers to once a second in a hidden tab. This covers the waits while saving and the wait for a video's picture after a seek. - Behind a tab Chrome does not call requestVideoFrameCallback, but the picture is there at seeked. The wait there is 10 ms instead of 150 ms while the tab is hidden. - The beat runs for Start recording, the exported game and Start recording... in the main menu. Tests: - tsc, eslint, Prettier clean - tests/viewport: 374 green, 3 new in background-beat.test.ts - e2e 68k (3 checks) on melektaus-atx with the tab really hidden: not in real time all 120 frames, colour changes every 30 frames, the blip 8 ms from green; in real time 97 of 120 frames (73 with the tab in front) - Counter-check without the beat: all three 68k checks red, 3 frames in 60 s not in real time, 7 frames in real time - Gate on melektaus-atx: green, 311 files, 4821 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
87b2ee9402
commit
abb8fa51d8
10 files changed
+474
-94
No files matched your search
@@ -0,0 +1,83 @@
|
||||
import type { Ticker } from 'pixi.js';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { BackgroundBeat, beatDue, STALL_MS } from '../../src/viewport/backgroundBeat';
|
||||
|
||||
const page = { hidden: false };
|
||||
|
||||
/** A ticker that runs its listeners on `update`, as Pixi's does; `frame` is one the page drew itself. */
|
||||
function fakeTicker() {
|
||||
const listeners = new Set<() => void>();
|
||||
const ticker = {
|
||||
updates: 0,
|
||||
add: (fn: () => void) => listeners.add(fn),
|
||||
remove: (fn: () => void) => listeners.delete(fn),
|
||||
update: () => {
|
||||
ticker.updates += 1;
|
||||
for (const fn of listeners) fn();
|
||||
},
|
||||
frame: () => {
|
||||
for (const fn of listeners) fn();
|
||||
},
|
||||
};
|
||||
return ticker;
|
||||
}
|
||||
|
||||
describe('beatDue', () => {
|
||||
it('ticks a hidden page, and a page that has drawn no frame of its own for a while', () => {
|
||||
expect(beatDue(true, 0)).toBe(true);
|
||||
expect(beatDue(false, 16)).toBe(false);
|
||||
expect(beatDue(false, STALL_MS)).toBe(false);
|
||||
expect(beatDue(false, STALL_MS + 1)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BackgroundBeat', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ['setInterval', 'clearInterval', 'performance'] });
|
||||
page.hidden = false;
|
||||
vi.stubGlobal('document', {
|
||||
get hidden() {
|
||||
return page.hidden;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('stands aside while the page draws, and ticks at the rate asked for once it is hidden', () => {
|
||||
const ticker = fakeTicker();
|
||||
const beat = new BackgroundBeat(ticker as unknown as Ticker);
|
||||
beat.start(60);
|
||||
for (let i = 0; i < 30; i++) {
|
||||
vi.advanceTimersByTime(16);
|
||||
ticker.frame();
|
||||
}
|
||||
expect(ticker.updates).toBe(0);
|
||||
page.hidden = true;
|
||||
vi.advanceTimersByTime(1600);
|
||||
expect(ticker.updates).toBe(96);
|
||||
expect(beat.ticks).toBe(96);
|
||||
beat.stop();
|
||||
vi.advanceTimersByTime(1600);
|
||||
expect(ticker.updates).toBe(96);
|
||||
});
|
||||
|
||||
it('takes over a page that stopped drawing without saying it is hidden, until it draws again', () => {
|
||||
const ticker = fakeTicker();
|
||||
const beat = new BackgroundBeat(ticker as unknown as Ticker);
|
||||
beat.start(30);
|
||||
vi.advanceTimersByTime(STALL_MS - 10);
|
||||
expect(ticker.updates).toBe(0);
|
||||
// Its own ticks do not count as the page's frames: it goes on at every beat.
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(ticker.updates).toBe(30);
|
||||
const given = ticker.updates;
|
||||
ticker.frame();
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(ticker.updates).toBe(given);
|
||||
beat.stop();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user