import assert from "node:assert/strict"; import test from "node:test"; import { parseProductionSpec } from "../production"; import { buildCompositorArgs } from "./command-builder"; import { planScene, programScene } from "./scene-plan"; const id = (n: number) => `00000000-0000-4000-8000-${String(n).padStart(12, "0")}`; const sha = (c: string) => c.repeat(64); const spec = parseProductionSpec({ sources: [ { id: id(1), label: "Cam A", kind: "hls", url: "https://a.example/live.m3u8" }, { id: id(2), label: "Cam B", kind: "hls", url: "https://b.example/live.m3u8" }, { id: id(3), label: "Loop", kind: "playlist", assetIds: [id(9)], loop: true }, ], scenes: [ { id: id(10), label: "Grid", layout: "2x2", slots: [ { index: 0, sourceId: id(1), rect: null, fit: "cover", audioMode: "primary", gainDb: 0 }, { index: 1, sourceId: id(2), rect: null, fit: "contain", audioMode: "on", gainDb: -6 }, { index: 2, sourceId: id(3), rect: null, fit: "contain", audioMode: "on", gainDb: 0 }, { index: 3, sourceId: id(1), rect: null, fit: "crop", audioMode: "on", gainDb: -12 }, ] }, { id: id(11), label: "PIP", layout: "pip", slots: [ { index: 0, sourceId: id(2), rect: null, fit: "cover", audioMode: "primary", gainDb: -3 }, { index: 1, sourceId: id(1), rect: { x: 6500, y: 6500, w: 3000, h: 3000 }, fit: "contain", audioMode: "off", gainDb: 0 }, ] }, ], buses: { preview: id(11), program: id(10) }, overlays: [ { id: id(20), sceneId: id(10), kind: "chyron", enabled: true, layer: 2, position: { x: 0, y: 9000 }, revisionId: id(30), assetId: sha("c"), chyron: { speedPxPerSec: 120, stripWidthPx: 4000 }, clock: null }, { id: id(21), sceneId: id(10), kind: "lower_third", enabled: true, layer: 1, position: { x: 500, y: 7500 }, revisionId: id(31), assetId: sha("a"), chyron: null, clock: null }, { id: id(22), sceneId: id(10), kind: "logo", enabled: false, layer: 3, position: { x: 9000, y: 500 }, revisionId: id(32), assetId: sha("b"), chyron: null, clock: null }, { id: id(23), sceneId: id(10), kind: "clock", enabled: true, layer: 4, position: { x: 9000, y: 500 }, revisionId: null, assetId: null, chyron: null, clock: { format: "HH:MM" } }, { id: id(24), sceneId: id(11), kind: "lower_third", enabled: true, layer: 0, position: { x: 0, y: 0 }, revisionId: id(33), assetId: sha("d"), chyron: null, clock: null }, ], transition: { type: "cut" }, audio: { masterGainDb: -3 }, }); const canvas = { width: 1920, height: 1080, fps: 30 }; const hls = (sourceId: string) => spec.sources.find((s) => s.id === sourceId)?.kind === "hls"; test("program scene → typed graph: grid cells, shared source, playlist black, one mix input per source", () => { const plan = planScene(spec, programScene(spec), canvas, hls); assert.deepEqual(plan.sourceIds, [id(1), id(2)]); assert.deepEqual(plan.audio, [true, true]); assert.deepEqual(plan.graph.tiles.map((t) => t.source), [0, 1, null, 0]); assert.deepEqual(plan.graph.tiles[3]?.rect, { x: 960, y: 540, w: 960, h: 540 }); // Cam A is primary at 0 dB and "on" at -12 dB in slot 3: one input at the louder gain. assert.deepEqual(plan.graph.audio, { inputs: [{ source: 0, gainDb: 0 }, { source: 1, gainDb: -6 }], masterGainDb: -3 }); // Layer order (lower third under chyron); disabled logo and S2a-unsupported clock skipped. assert.deepEqual(plan.assetIds, [sha("a"), sha("c")]); assert.deepEqual(plan.graph.overlays, [{ kind: "image", asset: 0, x: 96, y: 810 }, { kind: "chyron", asset: 1, y: 972, speedPxPerSec: 120 }]); assert.equal(plan.skipped.length, 2); const args = buildCompositorArgs({ sources: plan.sourceIds.map((_, i) => ({ video: { kind: "rawPipe" as const, fd: 4 + 2 * i }, audio: { kind: "rawPipe" as const, fd: 5 + 2 * i } })), assets: plan.assetIds.map((a) => `/scratch/assets/${a}.png`), graph: plan.graph, output: { kind: "gate", videoFd: 1, audioFd: 3 }, }); assert.ok(!args.join(" ").includes("Cam A"), "labels never reach argv"); assert.ok(!args.join(" ").includes("example"), "source URLs never reach the compositor"); }); test("PIP scene: inset rect from basis points, audio off slot not mixed", () => { const plan = planScene(spec, spec.scenes[1]!, canvas, hls); assert.deepEqual(plan.sourceIds, [id(2), id(1)]); assert.deepEqual(plan.audio, [true, false]); assert.deepEqual(plan.graph.tiles[1]?.rect, { x: 1248, y: 702, w: 576, h: 324 }); assert.deepEqual(plan.graph.audio.inputs, [{ source: 0, gainDb: -3 }]); });