// A clip whose source is SOUND ALONE -- here a `src` mp3 beside the manifest -- // builds a segment with a picture: a poster at the picture box's size, the // waveform moving along its foot, and a stream the concat cannot tell from any // other segment's (size, fps, pixel format, SAR, audio layout). // // Real encodes, kept tiny: a three-second lavfi mp3 and a 320x180 cut. // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { audioPosterParts, buildVideo, deckFraming, posterWaveGeometry } from "./build-video.mjs"; import { MONO_FONT_FILE } from "./brand.mjs"; const have = ["ffmpeg", "ffprobe", "magick"].every((b) => spawnSync(b, ["-version"]).status === 0); const PALETTE = { bg: "#15121c", fg: "#ece8f4", muted: "#9a93ad", accent: "#7c5cff", amber: "#f2b84b" }; const RENDER = { width: 320, height: 180, fps: 30, audioRate: 48000, audioChannels: 2, preset: "ultrafast", transition: 0, headerHeight: 24, palette: PALETTE, fontRegular: MONO_FONT_FILE, }; function probe(file) { const r = spawnSync("ffprobe", ["-v", "error", "-show_streams", "-show_format", "-of", "json", file]); assert.equal(r.status, 0, String(r.stderr)); const doc = JSON.parse(String(r.stdout)); const v = doc.streams.find((s) => s.codec_type === "video"); const a = doc.streams.find((s) => s.codec_type === "audio"); return { v, a, duration: Number(doc.format.duration) }; } /** What a stream-copy concat needs to be equal between two segments. */ const concatKey = ({ v, a }) => ({ vcodec: v.codec_name, width: v.width, height: v.height, pix_fmt: v.pix_fmt, fps: v.r_frame_rate, sar: v.sample_aspect_ratio ?? "1:1", tb: v.time_base, acodec: a.codec_name, rate: a.sample_rate, channels: a.channels, layout: a.channel_layout, }); async function project(render = RENDER) { const root = await mkdtemp(path.join(os.tmpdir(), "rtv-audio-poster-")); await mkdir(path.join(root, "media"), { recursive: true }); // Three seconds of a tone whose loudness swells: a waveform that moves. const mp3 = path.join(root, "media", "episode.mp3"); const r = spawnSync("ffmpeg", [ "-v", "error", "-y", "-f", "lavfi", "-i", "sine=f=220:r=44100:d=3", "-af", "volume='0.2+0.8*t/3':eval=frame", "-c:a", "libmp3lame", "-b:a", "64k", mp3, ]); assert.equal(r.status, 0, String(r.stderr)); await writeFile(path.join(root, "media", "episode.json"), JSON.stringify({ duration_seconds: 3, chunk_data: [{ start_time: 0.2, end_time: 2.8, text: "A tone, held." }], })); const manifestPath = path.join(root, "video.manifest.json"); await writeFile(manifestPath, JSON.stringify({ slug: "demo", title: "Demo", render, timeline: [ { id: "t0", type: "card", style: "chapter", heading: "A card", seconds: 1 }, { id: "p1", type: "clip", src: "media/episode.mp3", cues: "media/episode.json", start: 0.5, end: 2.5, title: "Episode twelve", channelTitle: "Demo Feed", date: "2026-01-02", }, ], })); return { root, manifestPath }; } test("posterWaveGeometry: even, inside the card's margins, along the foot", () => { const g = posterWaveGeometry(1574, 886); assert.equal(g.width % 2, 0); assert.equal(g.height % 2, 0); assert.ok(g.x > 0 && g.x + g.width <= 1574); assert.ok(g.y > 886 / 2 && g.y + g.height < 886); }); test("audioPosterParts: the still alone, or the sound's waveform over it", () => { const box = { width: 320, height: 156 }; assert.deepEqual( audioPosterParts({ posterIdx: 3, box, render: { ...RENDER, audioPoster: { waveform: false } } }), ["[3:v]null[pic]"], ); const parts = audioPosterParts({ posterIdx: 1, box, render: RENDER }); const g = posterWaveGeometry(320, 156); assert.equal(parts.length, 2); assert.match(parts[0], /^\[0:a\]aformat=channel_layouts=mono,showwaves=s=\d+x\d+:mode=cline:draw=full:rate=30:colors=#7c5cff\[wave\]$/); assert.equal(parts[1], `[1:v][wave]overlay=x=${g.x}:y=${g.y}:eof_action=pass[pic]`); }); test("an audio-only src clip builds a poster segment the concat cannot tell from a card's", { skip: !have && "needs ffmpeg, ffprobe and magick" }, async () => { const { root, manifestPath } = await project(); try { const clip = await buildVideo({ manifestPath, only: "p1", opts: { noNetwork: true } }); assert.equal(clip.failures.length, 0); const card = await buildVideo({ manifestPath, only: "t0", opts: { noNetwork: true } }); const c = probe(clip.out); assert.ok(c.v, "the segment has a picture"); assert.equal(c.v.width, 320); assert.equal(c.v.height, 180); assert.equal(c.v.r_frame_rate, "30/1"); assert.equal(c.v.pix_fmt, "yuv420p"); assert.equal(c.a.channels, 2); assert.equal(c.a.sample_rate, "48000"); assert.deepEqual(concatKey(c), concatKey(probe(card.out))); // About the played window, snapped (start 0.5 → 2.5 of a 3 s file), and the // picture is no longer than the sound. assert.ok(c.duration > 1 && c.duration <= 3.1, `duration ${c.duration}`); assert.ok(Number(c.v.duration) <= Number(c.a.duration) + 1 / 30 + 0.03, `${c.v.duration} vs ${c.a.duration}`); // The poster was drawn at the picture box's size: the frame less the header. const still = probe(path.join(root, "out", "sourced", "cards", "p1.poster.png")); assert.equal(still.v.width, 320); assert.equal(still.v.height, 180 - 24); // The waveform moves: the frames are not one still. const md5 = spawnSync("ffmpeg", ["-v", "error", "-i", clip.out, "-map", "0:v", "-f", "framemd5", "-"]); assert.equal(md5.status, 0, String(md5.stderr)); const hashes = new Set(String(md5.stdout).split("\n").filter((l) => l && !l.startsWith("#")).map((l) => l.split(",").pop().trim())); assert.ok(hashes.size > 10, `${hashes.size} distinct frames`); } finally { await rm(root, { recursive: true, force: true }); } }); test("under the deck, the poster is framed into the footage box like any footage", { skip: !have && "needs ffmpeg, ffprobe and magick" }, async () => { const { root, manifestPath } = await project({ ...RENDER, width: 1920, height: 1080, headerHeight: undefined, chrome: { engine: "hyperframes", layout: "deck", deck: {} }, audioPoster: { waveform: false }, }); try { const clip = await buildVideo({ manifestPath, only: "p1", opts: { noNetwork: true } }); const c = probe(clip.out); assert.equal(c.v.width, 1920); assert.equal(c.v.height, 1080); assert.equal(c.v.r_frame_rate, "30/1"); const still = probe(path.join(root, "out", "sourced", "cards", "p1.poster.png")); const box = deckFraming({ ...RENDER, width: 1920, height: 1080, chrome: { engine: "hyperframes", layout: "deck", deck: {} } }).box; assert.equal(still.v.width, box.width); assert.equal(still.v.height, box.height); } finally { await rm(root, { recursive: true, force: true }); } }); test("--fetch-only on a src clip has nothing to fetch", async () => { if (!have) return; const { root, manifestPath } = await project(); try { const r = await buildVideo({ manifestPath, fetchOnly: "p1", opts: {} }); assert.equal(r.out, null); } finally { await rm(root, { recursive: true, force: true }); } });