Files
ytplayer/frontend/jamesdsp-controller.test.js
2026-10-09 20:00:49 +08:00

99 lines
5.0 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const { createController } = require('./vendor/jamesdsp/jamesdsp-feature.js');
function fixture({ loadNode } = {}) {
const routed = [], errors = [], created = [], configured = [], bypass = [];
const graph = { routeAll: (elements, destination) => routed.push([elements.map(x => x.id), destination]) };
const gainParam = { value: 1, cancelScheduledValues() {}, setValueAtTime(v) { this.value = v; }, linearRampToValueAtTime(v) { this.value = v; } };
const context = { sampleRate: 48000, state: 'running', currentTime: 0, destination: 'destination', createGain: () => ({ gain: gainParam, connect() {}, disconnect() {} }), resume: async () => {} };
const standardInput = 'standard-input', elements = [{ id: 'video' }, { id: 'audio' }];
const prepareGraph = async () => ({ context, standardInput, elements });
const defaultLoad = async () => {
const listeners = new Map();
const instance = {
node: { connect() {}, disconnect() {} }, destroyed: false,
addEventListener(type, fn) { listeners.set(type, fn); },
removeEventListener(type, fn) { if (listeners.get(type) === fn) listeners.delete(type); },
async setBypass(v) { bypass.push(v); },
async applyPreset(params) { configured.push({ ...params }); },
async setParam(name, value) { configured.push({ [name]: value }); },
destroy() { this.destroyed = true; },
fail(error) { listeners.get('error')?.({ detail: error }); },
};
created.push(instance); return instance;
};
const controller = createController({ graph, prepareGraph, loadNode: loadNode || defaultLoad, onError: error => errors.push(error) });
return { controller, routed, errors, created, configured, bypass, context, elements, standardInput };
}
test('enable routes one initialized JamesDSP node and disable restores Standard before destroying it', async () => {
const f = fixture();
assert.equal(await f.controller.enable({ preset: 'bass', params: { gain: -2 } }, 3), true);
assert.equal(f.controller.active, true);
assert.deepEqual(f.routed, [[['video', 'audio'], f.created[0].node]]);
assert.equal(f.created.length, 1);
assert.equal(f.controller.state().liveHeaps, 1);
assert.equal(f.controller.state().maxLiveHeaps, 1);
const standard = f.controller.disable();
assert.deepEqual(f.routed.at(-1), [['video', 'audio'], f.standardInput]);
await standard;
assert.equal(f.created[0].destroyed, true);
assert.equal(f.controller.state().liveHeaps, 0);
assert.equal(f.controller.state().nodesDestroyed, 1);
assert.equal(f.errors.length, 0);
});
test('a rejected or unsupported engine leaves the Standard route active', async () => {
const f = fixture({ loadNode: async () => { throw new Error('corrupt wasm'); } });
await assert.rejects(f.controller.enable({ preset: 'flat' }), /corrupt wasm/);
assert.deepEqual(f.routed, [[['video', 'audio'], f.standardInput]]);
assert.equal(f.controller.active, false);
assert.equal(f.controller.state().liveHeaps, 0);
assert.equal(f.errors.length, 1);
const wrongRate = fixture();
wrongRate.context.sampleRate = 32000;
await assert.rejects(wrongRate.controller.enable({ preset: 'flat' }), /44\.1 or 48 kHz/);
assert.equal(wrongRate.created.length, 0);
assert.equal(wrongRate.controller.state().liveHeaps, 0);
});
test('a runtime worklet failure switches sources to Standard before destroying the heap', async () => {
const f = fixture();
await f.controller.enable({ preset: 'flat' });
const instance = f.created[0];
instance.fail(new Error('processor fault'));
assert.deepEqual(f.routed.at(-1), [['video', 'audio'], f.standardInput]);
assert.equal(instance.destroyed, true);
assert.equal(f.controller.active, false);
assert.equal(f.controller.state().liveHeaps, 0);
assert.match(f.errors[0].message, /processor fault/);
});
test('disable during async node loading prevents a late DSP route', async () => {
let finish;
const f = fixture({ loadNode: () => new Promise(resolve => { finish = resolve; }) });
const enabling = f.controller.enable({ preset: 'flat' });
await new Promise(resolve => setImmediate(resolve));
const disabling = f.controller.disable();
const neverRouted = {
node: { connect() {}, disconnect() {} },
addEventListener() {}, removeEventListener() {}, setBypass: async () => {}, applyPreset: async () => {}, setParam: async () => {}, destroy() { this.destroyed = true; },
};
finish(neverRouted);
await enabling; await disabling;
assert.equal(f.routed.some(([, destination]) => destination === neverRouted.node), false);
assert.equal(neverRouted.destroyed, true);
assert.equal(f.controller.state().maxLiveHeaps, 0);
});
test('preset gain and Level gain combine within engine bounds without pausing playback', async () => {
const f = fixture();
await f.controller.enable({ preset: 'loudness', params: { gain: 8 } }, 9);
assert.ok(f.configured.some(entry => entry.gain === 12));
assert.equal(f.created.length, 1);
assert.equal(f.controller.state().maxLiveHeaps, 1);
assert.equal(f.controller.active, true);
});