Files
ytplayer/tests/audio-continuity.spec.js
2026-10-04 02:29:10 +08:00

47 lines
3.5 KiB
JavaScript

const { test, expect } = require('@playwright/test');
const { openClassic, showPlayingFixture } = require('./helpers/classic-fixture');
// A small local PCM fixture exercises actual media elements without YouTube.
function wav() {
const rate = 8000, samples = rate * 12, bytes = Buffer.alloc(44 + samples * 2);
bytes.write('RIFF'); bytes.writeUInt32LE(bytes.length - 8, 4); bytes.write('WAVEfmt ', 8);
bytes.writeUInt32LE(16, 16); bytes.writeUInt16LE(1, 20); bytes.writeUInt16LE(1, 22);
bytes.writeUInt32LE(rate, 24); bytes.writeUInt32LE(rate * 2, 28); bytes.writeUInt16LE(2, 32); bytes.writeUInt16LE(16, 34);
bytes.write('data', 36); bytes.writeUInt32LE(samples * 2, 40);
for (let i = 0; i < samples; i++) bytes.writeInt16LE(Math.sin(i * 2 * Math.PI * 220 / rate) * 2000, 44 + i * 2);
return bytes;
}
for (const source of ['saved', 'streamed']) test(`${source}: iPhone keeps the same playing audio across repeated lock/unlock events`, async ({ page }) => {
await page.addInitScript(() => Object.defineProperty(navigator, 'userAgent', { value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15' }));
await page.route('**/continuity.wav', route => route.fulfill({ contentType: 'audio/wav', body: wav() }));
await openClassic(page, { populated: true }); await showPlayingFixture(page);
await page.evaluate(source => {
current.localUrl = source === 'saved' ? '/continuity.wav' : '';
current.audioUrl = '/continuity.wav';
Player.attach(source === 'streamed' ? { url: '/continuity.wav', hasAudio: true } : null);
}, source);
await expect.poll(() => page.evaluate(() => els.audio.readyState)).toBeGreaterThanOrEqual(2);
await page.evaluate(() => Player.play());
await expect.poll(() => page.evaluate(() => !els.audio.paused && els.audio.currentTime > 0)).toBe(true);
const result = await page.evaluate(async () => {
const A = els.audio, V = els.video;
const initial = A.currentTime, src = A.src;
let playCalls = 0, audioSeeks = 0, loads = 0, pauses = 0;
const play = A.play.bind(A), load = A.load.bind(A), pause = A.pause.bind(A);
A.play = () => { playCalls++; return play(); }; A.load = () => { loads++; load(); }; A.pause = () => { pauses++; pause(); };
A.addEventListener('seeking', () => audioSeeks++);
Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'hidden' });
document.dispatchEvent(new Event('visibilitychange')); window.dispatchEvent(new Event('pagehide'));
window.dispatchEvent(new Event('focus')); // focus arriving while hidden must not restore
const promoted = Player.master === A && Player._bgHandoff === 'dual';
V.pause();
await new Promise(resolve => setTimeout(resolve, 350));
const advanced = A.currentTime > initial;
Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'visible' });
document.dispatchEvent(new Event('visibilitychange')); window.dispatchEvent(new Event('pageshow')); window.dispatchEvent(new Event('focus'));
await new Promise(resolve => setTimeout(resolve, 200));
const answer = { mode: Player.mode, promoted, advanced, sameSource: A.src === src, playing: !A.paused, playCalls, audioSeeks, loads, pauses, restored: Player.master === V && Player.secondary === A };
Player.pause(); Player.stopDrift(); return answer;
});
expect(result).toEqual({ mode: 'dual', promoted: true, advanced: true, sameSource: true, playing: true, playCalls: 0, audioSeeks: 0, loads: 0, pauses: 0, restored: true });
});