Files
mrleo1nid b522d257c5
CI / frontend (push) Failing after 21s
CI / backend (push) Failing after 42s
Fix duplicate movie audio during scene transitions
2026-10-27 16:25:27 +03:00

91 lines
4.2 KiB
JavaScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
import ts from "typescript";
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, 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]);
applyMovieMask(pixels, new Uint8ClampedArray([0, 255, 255, 255, 128, 0, 0, 255]));
assert.deepEqual([...pixels], [200, 10, 20, 0, 30, 40, 50, 64]);
});
test("masked movie composites frames, synchronizes the mask and stops when removed", () => {
const { document } = parseHTML("<!doctype html><html><body></body></html>");
globalThis.document = document;
const create = document.createElement.bind(document);
const videos = [], frames = [], painted = [];
let canvases = 0;
globalThis.requestAnimationFrame = (callback) => frames.push(callback);
document.createElement = (name) => {
const element = create(name);
if (name === "video") {
Object.assign(element, { readyState: 2, videoWidth: 2, videoHeight: 1, currentTime: 0, ended: false,
play: () => Promise.resolve(), pause: () => { element.paused = true; } });
videos.push(element);
}
if (name === "canvas") {
const mask = canvases++ > 0;
element.getContext = () => ({ clearRect() {}, drawImage() {},
getImageData: () => ({ data: new Uint8ClampedArray(mask
? [0, 0, 0, 255, 128, 128, 128, 255] : [200, 10, 20, 255, 30, 40, 50, 255]) }),
putImageData: (image) => painted.push([...image.data]),
});
}
return element;
};
const canvas = movieDisplayable("/movie.webm", "/mask.webm", false);
document.body.append(canvas);
videos[1].currentTime = 2;
frames.shift()();
assert.deepEqual(painted[0], [200, 10, 20, 0, 30, 40, 50, 128]);
assert.equal(videos[1].currentTime, 0);
canvas.remove();
frames.shift()();
assert.ok(videos.every((video) => video.paused));
assert.equal(frames.length, 0);
});