Fix duplicate movie audio during scene transitions
CI / frontend (push) Failing after 21s
CI / backend (push) Failing after 42s

This commit is contained in:
mrleo1nid committed 2026-10-27 16:25:27 +03:00
1 parent 4dc660f786
commit b522d257c5
5 files changed
+119 -11

No files matched your search

@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
import { test } from "node:test";
import { build } from "esbuild";
test("MIST gallery scene accepts Skip and menu clicks, keeps video centered and bounds overlapping music", {
test("MIST gallery plays normal transitions without extra audio and supports Skip, video layout and exit", {
skip: !process.env.PGAMES_PLAYWRIGHT_PATH || !process.env.PGAMES_MIST_RUNTIME || !process.env.PGAMES_MIST_MANIFEST || !process.env.PGAMES_MIST_BLOBS,
}, async () => {
const { chromium } = await import(pathToFileURL(process.env.PGAMES_PLAYWRIGHT_PATH + "/index.mjs").href);
@@ -22,7 +22,7 @@ test("MIST gallery scene accepts Skip and menu clicks, keeps video centered and
{list: async () => [], get: async () => null, put: async () => {}, remove: async () => {}},
'mist-playback-browser', () => {}, tr, async () => tr);
e.settings.cps = 0;
e.renderer.transitionsOff = true;
e.renderer.transitionsOff = false;
e.audio.setVolumes({music: 0, sound: 0, voice: 0});
e.audio.setMixer('ambiant', 0);
const play = e.audio.play.bind(e.audio);
@@ -68,8 +68,25 @@ test("MIST gallery scene accepts Skip and menu clicks, keeps video centered and
await page.waitForFunction(() => [...document.querySelectorAll('.pg-simgbtn img')].every(el => el.complete && el.naturalWidth > 0));
await page.locator('[data-test="robgallery"] .pg-simgbtn').nth(1).click();
await page.waitForFunction(() => !!window.engine.onAdvance && !window.engine.screens.shown.size);
// Ordinary viewing must not start extra media players when a movie is
// captured as the outgoing picture of the next transition.
for (let line = 0; line < 12; line++) {
if (await page.locator('video.pg-image').count()) break;
const before = await page.evaluate(() => JSON.stringify(window.engine.cur));
await page.mouse.click(480, 270);
await page.waitForFunction(before => JSON.stringify(window.engine.cur) !== before && !!window.engine.onAdvance, before);
}
await page.waitForFunction(() => document.querySelector('video.pg-image')?.readyState >= 2);
const snapshot = await page.evaluate(() => {
window.firstSceneVideo = document.querySelector('video.pg-image');
window.engine.renderer.commit();
return window.engine.renderer.committed.querySelectorAll('video').length;
});
assert.equal(snapshot, 0, 'transition snapshots must not contain additional movie players');
await page.locator('[data-act="skip"]').click();
await page.getByRole('button', { name: 'End Scene.', exact: true }).waitFor();
assert.equal(await page.evaluate(() => window.firstSceneVideo.paused && !window.firstSceneVideo.hasAttribute('src')), true,
'the previous scene movie is stopped and unloaded even when a transition snapshot exists');
await page.getByRole('button', { name: 'Keep Looking.', exact: true }).click();
await page.waitForFunction(() => document.querySelector('.pg-image video, video.pg-image')?.videoWidth > 0);
const state = await page.evaluate(() => {
+40 -1
View File
@@ -6,7 +6,46 @@ import { parseHTML } from "linkedom";
const source = readFileSync(new URL("../src/player/movie.ts", import.meta.url), "utf8");
const { outputText } = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } });
const { applyMovieMask, movieDisplayable } = await import(`data:text/javascript;base64,${Buffer.from(outputText).toString("base64")}`);
const { applyMovieMask, movieDisplayable, disposeMovies, snapshotDisplayables } = await import(`data:text/javascript;base64,${Buffer.from(outputText).toString("base64")}`);
test("transition snapshots copy video and canvas pixels without cloning active players", () => {
const { document } = parseHTML('<html><body><div><video class="pg-image" style="left: 10px"></video><canvas width="4" height="2"></canvas></div></body></html>');
globalThis.document = document;
const create = document.createElement.bind(document);
const drawn = [];
const root = document.querySelector('div');
const video = root.querySelector('video');
const canvas = root.querySelector('canvas');
Object.assign(video, { readyState: 2, videoWidth: 4, videoHeight: 2, muted: true, autoplay: true });
const canvasProto = Object.getPrototypeOf(canvas);
const previous = canvasProto.getContext;
canvasProto.getContext = function () { return { drawImage: source => drawn.push(source) }; };
try {
const copy = snapshotDisplayables(root);
assert.equal(copy.querySelectorAll('video').length, 0);
assert.equal(copy.querySelectorAll('canvas').length, 2);
assert.deepEqual(drawn, [video, canvas]);
assert.equal(copy.querySelector('.pg-image').style.left, '10px');
assert.equal(root.querySelector('video'), video, 'snapshot leaves the live movie in place');
} finally { canvasProto.getContext = previous; document.createElement = create; }
});
test("disposing a removed movie stops and unloads its player immediately", () => {
const { document } = parseHTML('<html><body></body></html>');
globalThis.document = document;
const video = movieDisplayable('/movie.webm', null);
let paused = false, unloaded = false;
video.pause = () => { paused = true; };
video.load = () => { unloaded = true; };
assert.equal(video.defaultMuted, true);
document.body.append(video);
video.remove();
disposeMovies(video);
assert.equal(paused, true);
assert.equal(unloaded, true);
assert.equal(video.autoplay, false);
assert.equal(video.hasAttribute('src'), false);
});
test("video opacity uses the mask's red channel and retains existing transparency", () => {
const pixels = new Uint8ClampedArray([200, 10, 20, 255, 30, 40, 50, 128]);