import { expect, test } from "@playwright/test"; import { defaultVideoCanvasAudioSeparationConfig, getVideoAudioSplitResultCopy, hasMp4AudioTrack, hasVideoAudioTrack, hasWebmAudioTrack, normalizeVideoCanvasAudioSeparationConfig, separateVideoAudioChannels, } from "../../lib/video-canvas/video-audio-separation"; function mp4Box(type: string, payload: Uint8Array) { const output = new Uint8Array(8 + payload.length); new DataView(output.buffer).setUint32(0, output.length); Array.from(type).forEach((character, index) => { output[4 + index] = character.charCodeAt(0); }); output.set(payload, 8); return output; } function joinBytes(...parts: Uint8Array[]) { const output = new Uint8Array(parts.reduce((total, part) => total + part.length, 0)); let offset = 0; for (const part of parts) { output.set(part, offset); offset += part.length; } return output; } function ebmlElement(id: number[], payload: Uint8Array) { if (payload.length >= 127) throw new Error("Test EBML payload is too large"); return joinBytes(new Uint8Array(id), new Uint8Array([0x80 | payload.length]), payload); } test.describe("video audio separation tools", () => { test("normalizes configurable local separation limits", () => { expect(normalizeVideoCanvasAudioSeparationConfig({ backgroundGain: 9, frameRate: 100, model: " center-channel-custom ", voiceGain: -1, })).toMatchObject({ backgroundGain: 4, frameRate: 60, model: "center-channel-custom", voiceGain: 0, }); }); test("extracts center and side stereo signals for local fallback modes", () => { const left = new Float32Array([1, 0.5]); const right = new Float32Array([1, -0.5]); const voice = separateVideoAudioChannels( [left, right], "voice", defaultVideoCanvasAudioSeparationConfig, ); const background = separateVideoAudioChannels( [left, right], "background", defaultVideoCanvasAudioSeparationConfig, ); expect(Array.from(voice[0])).toEqual([1, 0]); expect(Array.from(voice[1])).toEqual([1, 0]); expect(background[0][0]).toBe(0); expect(background[0][1]).toBeCloseTo(0.6); expect(background[1][1]).toBeCloseTo(-0.6); }); test("matches competitor result suffixes", () => { expect(getVideoAudioSplitResultCopy("audio-video").audioSuffix).toBe("音轨"); expect(getVideoAudioSplitResultCopy("voice").audioSuffix).toBe("人声"); expect(getVideoAudioSplitResultCopy("background").audioSuffix).toBe("背景音"); }); test("reads the structured MP4 audio handler instead of guessing by filename", () => { const handlerPayload = new Uint8Array(12); Array.from("soun").forEach((character, index) => { handlerPayload[8 + index] = character.charCodeAt(0); }); const audioFile = mp4Box("moov", mp4Box("trak", mp4Box("mdia", mp4Box("hdlr", handlerPayload)))); const videoHandler = new Uint8Array(handlerPayload); Array.from("vide").forEach((character, index) => { videoHandler[8 + index] = character.charCodeAt(0); }); const videoOnlyFile = mp4Box("moov", mp4Box("trak", mp4Box("mdia", mp4Box("hdlr", videoHandler)))); expect(hasMp4AudioTrack(audioFile.buffer)).toBe(true); expect(hasMp4AudioTrack(videoOnlyFile.buffer)).toBe(false); }); test("reads structured WebM TrackType elements for generated video results", () => { const audioEntry = ebmlElement( [0xae], ebmlElement([0x83], new Uint8Array([2])), ); const videoEntry = ebmlElement( [0xae], ebmlElement([0x83], new Uint8Array([1])), ); const audioWebm = ebmlElement( [0x18, 0x53, 0x80, 0x67], ebmlElement([0x16, 0x54, 0xae, 0x6b], audioEntry), ); const videoOnlyWebm = ebmlElement( [0x18, 0x53, 0x80, 0x67], ebmlElement([0x16, 0x54, 0xae, 0x6b], videoEntry), ); expect(hasWebmAudioTrack(audioWebm.buffer)).toBe(true); expect(hasWebmAudioTrack(videoOnlyWebm.buffer)).toBe(false); expect(hasVideoAudioTrack(audioWebm.buffer)).toBe(true); }); });