Files
2026-08-28 01:01:12 +02:00

143 lines
5.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { LocalFilmstripMedia, sampleLocalVideo } from '../../web/ui/local_media.js';
function fakeVideo(duration) {
const listeners = new Map();
let currentTime = 0;
const fire = (name) => queueMicrotask(() => {
const list = listeners.get(name) || [];
listeners.set(name, []);
for (const cb of list) cb();
});
return {
seeks: [],
duration,
videoWidth: 1920,
videoHeight: 1080,
set src(value) { this._src = value; fire('loadedmetadata'); },
get src() { return this._src; },
get currentTime() { return currentTime; },
set currentTime(value) { currentTime = value; this.seeks.push(value); fire('seeked'); },
addEventListener(name, cb) { listeners.set(name, [...(listeners.get(name) || []), cb]); },
removeEventListener(name, cb) { listeners.set(name, (listeners.get(name) || []).filter((item) => item !== cb)); },
removeAttribute() {},
load() {},
};
}
function installFakeDom(video) {
const canvas = {
width: 0,
height: 0,
getContext: () => ({ drawImage: () => {} }),
toDataURL: () => 'data:image/jpeg;base64,fake',
};
globalThis.document = { createElement: (tag) => (tag === 'video' ? video : canvas) };
return () => { delete globalThis.document; };
}
test('an untrimmed 10s clip samples every frame at the 24 fps midpoint', async () => {
const video = fakeVideo(10);
const restore = installFakeDom(video);
try {
const result = await sampleLocalVideo('/api/view?filename=clip.mp4', { fps: 24 });
assert.equal(result.frameCount, 240);
assert.equal(result.frames.length, 240);
assert.ok(Math.abs(video.seeks[0] - 0.5 / 24) < 1e-9);
assert.ok(Math.abs(video.seeks[1] - 1.5 / 24) < 1e-9);
} finally {
restore();
}
});
test('skip_first_frames and select_every_nth shift and stretch the sampled times', async () => {
const video = fakeVideo(60);
const restore = installFakeDom(video);
try {
const result = await sampleLocalVideo('/api/view?filename=clip.mp4', {
fps: 24,
startFrame: 100,
stride: 2,
frameCountHint: 48,
});
assert.equal(result.frameCount, 48, 'the load cap bounds the output stream');
// output frame 0 -> source frame 100, output frame 1 -> source frame 102
assert.ok(Math.abs(video.seeks[0] - 100.5 / 24) < 1e-9);
assert.ok(Math.abs(video.seeks[1] - 102.5 / 24) < 1e-9);
assert.ok(Math.abs(video.seeks.at(-1) - (100 + 47 * 2 + 0.5) / 24) < 1e-9);
} finally {
restore();
}
});
test('without a cap the available output length accounts for skip and stride', async () => {
const video = fakeVideo(10);
const restore = installFakeDom(video);
try {
const result = await sampleLocalVideo('/api/view?filename=clip.mp4', { fps: 24, startFrame: 40, stride: 2 });
assert.equal(result.frameCount, 100, '(240 - 40) / 2');
} finally {
restore();
}
});
test('thumbnails stay inside the 160x90 budget', async () => {
const video = fakeVideo(1);
const restore = installFakeDom(video);
try {
const result = await sampleLocalVideo('/api/view?filename=clip.mp4', { fps: 24 });
assert.equal(result.frames.length, 24);
assert.match(result.frames[0].url, /^data:image\/jpeg/);
} finally {
restore();
}
});
test('onProgress reports one call per sampled frame, in order, ending at the total', async () => {
const video = fakeVideo(2);
const restore = installFakeDom(video);
try {
const calls = [];
const result = await sampleLocalVideo('/api/view?filename=clip.mp4', {
fps: 24,
onProgress: (done, total) => calls.push([done, total]),
});
assert.equal(calls.length, result.frameCount);
assert.deepEqual(calls[0], [1, result.frameCount]);
assert.deepEqual(calls.at(-1), [result.frameCount, result.frameCount]);
for (let index = 1; index < calls.length; index += 1) assert.equal(calls[index][0], calls[index - 1][0] + 1);
} finally {
restore();
}
});
test('sampling without onProgress is unaffected', async () => {
const video = fakeVideo(1);
const restore = installFakeDom(video);
try {
const result = await sampleLocalVideo('/api/view?filename=clip.mp4', { fps: 24 });
assert.equal(result.frames.length, 24);
} finally {
restore();
}
});
test('a newer local media load cannot be overwritten by an older pending load', async () => {
const pending = [];
const media = new LocalFilmstripMedia({
urls: { createObjectURL: (file) => `blob:${file.name}`, revokeObjectURL() {} },
sampleVideo: (url) => new Promise((resolve) => pending.push({ url, resolve })),
});
const first = media.loadFiles([{ name: 'first.mp4', type: 'video/mp4' }]);
const second = media.loadFiles([{ name: 'second.mp4', type: 'video/mp4' }]);
pending[1].resolve({ frameCount: 2, frames: [{ frame: 0, url: 'second-frame' }] });
await second;
pending[0].resolve({ frameCount: 1, frames: [{ frame: 0, url: 'first-frame' }] });
await first;
assert.equal(media.snapshot().label, 'second.mp4');
});