import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { crc32, deflateSync } from "node:zlib";
const ROOT_DURATION = 180;
const CLIP_DURATION = 2;
const SUB_COMPOSITION_DURATION = 120;
const SUB_COMPOSITION_PATH = "compositions/load-details.html";
const CLIP_KINDS = ["load-card", "load-caption", "load-accent"];
const MEDIA_CLIP_DURATION = 1;
/** Lane offsets past the caller's tracks, one per media kind. */
const MEDIA_KINDS = ["audio", "video", "image"];
const MEDIA_EXTENSIONS = { audio: "wav", video: "mp4", image: "png" };
// Real projects are image-heavy: the measured 273-clip project carried 263
// images against 23 audio and 77 video files. This ratio reproduces that mix.
const MEDIA_SOURCE_KIND_PATTERN = [
"audio",
"image",
"video",
"image",
"image",
"video",
"image",
"image",
];
/**
* Every source must be reused at least this many times. This is the property
* the fixture exists for: a project where each clip has its own source cannot
* reproduce the request amplification (the measured project fetched one
* `track.mp3` seven-plus times), so a gate built on it would measure nothing.
*/
const MIN_REFERENCES_PER_SOURCE = 7;
const MAX_MEDIA_CLIPS = Math.floor(ROOT_DURATION / MEDIA_CLIP_DURATION) - 1;
/**
* The one asset that cannot be synthesized in a few lines: a real H.264 file a
* browser will actually decode. 1,684 bytes, 128x72, one second of solid colour.
*/
const SEED_VIDEO = readFileSync(
fileURLToPath(new URL("fixtures/media-seed/solid-colour.mp4", import.meta.url)),
);
const PROJECT_CONFIG = `${JSON.stringify(
{
$schema: "https://hyperframes.heygen.com/schema/hyperframes.json",
paths: {
blocks: "compositions",
components: "compositions/components",
assets: "assets",
},
},
null,
2,
)}\n`;
const OPTION_MINIMUMS = { clipCount: 0, trackCount: 1, mediaSourceCount: 0, mediaClipCount: 0 };
function validateOptions(options) {
for (const [name, minimum] of Object.entries(OPTION_MINIMUMS)) {
const value = options[name];
if (!Number.isInteger(value) || value < minimum) {
throw new RangeError(`${name} must be an integer of at least ${minimum}`);
}
}
const { mediaSourceCount, mediaClipCount } = options;
if (mediaClipCount > MAX_MEDIA_CLIPS) {
throw new RangeError(`mediaClipCount must be at most ${MAX_MEDIA_CLIPS}`);
}
if (mediaClipCount > 0 && mediaSourceCount < 1) {
throw new RangeError("mediaClipCount above zero requires at least one media source");
}
if (mediaSourceCount > 0 && mediaClipCount < mediaSourceCount * MIN_REFERENCES_PER_SOURCE) {
throw new RangeError(
`mediaClipCount must be at least ${MIN_REFERENCES_PER_SOURCE} times mediaSourceCount so every source is reused`,
);
}
}
function pngChunk(type, data) {
const header = Buffer.alloc(8);
header.writeUInt32BE(data.length, 0);
header.write(type, 4, "ascii");
const checksum = Buffer.alloc(4);
checksum.writeUInt32BE(crc32(Buffer.concat([header.subarray(4), data])), 0);
return Buffer.concat([header, data, checksum]);
}
/** A 32x32 solid-colour PNG whose colour is derived from the source index. */
function buildImageSource(sourceIndex) {
const size = 32;
const stride = 1 + size * 3;
const raw = Buffer.alloc(size * stride);
for (let y = 0; y < size; y += 1) {
for (let x = 0; x < size; x += 1) {
const pixel = y * stride + 1 + x * 3;
raw[pixel] = (sourceIndex * 61) % 256;
raw[pixel + 1] = (sourceIndex * 113) % 256;
raw[pixel + 2] = (sourceIndex * 173) % 256;
}
}
const header = Buffer.alloc(13);
header.writeUInt32BE(size, 0);
header.writeUInt32BE(size, 4);
header[8] = 8; // bit depth
header[9] = 2; // truecolour, no alpha
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
pngChunk("IHDR", header),
pngChunk("IDAT", deflateSync(raw, { level: 9 })),
pngChunk("IEND", Buffer.alloc(0)),
]);
}
/** One second of 8 kHz 16-bit mono silence. */
function buildAudioSource(sourceIndex) {
const sampleRate = 8_000;
const dataBytes = sampleRate * 2;
const wav = Buffer.alloc(44 + dataBytes);
wav.write("RIFF", 0, "ascii");
wav.writeUInt32LE(36 + dataBytes, 4);
wav.write("WAVEfmt ", 8, "ascii");
wav.writeUInt32LE(16, 16); // fmt chunk length
wav.writeUInt16LE(1, 20); // PCM
wav.writeUInt16LE(1, 22); // mono
wav.writeUInt32LE(sampleRate, 24);
wav.writeUInt32LE(sampleRate * 2, 28); // byte rate
wav.writeUInt16LE(2, 32); // block align
wav.writeUInt16LE(16, 34); // bits per sample
wav.write("data", 36, "ascii");
wav.writeUInt32LE(dataBytes, 40);
// Silence, except one inaudible final sample carrying the source index. No two
// sources may be byte-identical, or a content-addressed cache anywhere in the
// stack could collapse them and hide the very duplication being measured.
wav.writeInt16LE(sourceIndex, wav.length - 2);
return wav;
}
/** The seed clip plus a `free` box — ignored by demuxers, distinct on disk. */
function buildVideoSource(sourceIndex) {
const freeBox = Buffer.alloc(12);
freeBox.writeUInt32BE(12, 0);
freeBox.write("free", 4, "ascii");
freeBox.writeUInt32BE(sourceIndex, 8);
return Buffer.concat([SEED_VIDEO, freeBox]);
}
const MEDIA_BUILDERS = {
audio: buildAudioSource,
video: buildVideoSource,
image: buildImageSource,
};
function mediaSource(sourceIndex) {
const kind = MEDIA_SOURCE_KIND_PATTERN[sourceIndex % MEDIA_SOURCE_KIND_PATTERN.length];
const path = `assets/${kind}-${String(sourceIndex).padStart(3, "0")}.${MEDIA_EXTENSIONS[kind]}`;
return { kind, path };
}
function renderMediaClip(index, { mediaSourceCount, trackCount }) {
// Round-robin over the sources: distinct-source count and clip-instance count
// are then independently controllable, and every source is reused.
const { kind, path } = mediaSource(index % mediaSourceCount);
const track = trackCount + MEDIA_KINDS.indexOf(kind);
// One clip per second keeps every lane free of overlaps without any packing.
const start = index * MEDIA_CLIP_DURATION;
const timing = `data-start="${start}" data-duration="${MEDIA_CLIP_DURATION}" data-track-index="${track}"`;
const layout = `style="left: ${(index * 53) % 1820}px; top: ${(index * 29) % 980}px"`;
const id = `load-media-${index}`;
if (kind === "image") {
return ` `;
}
const muted = kind === "video" ? " muted" : "";
return ` <${kind} id="${id}" class="clip" src="${path}"${muted} ${timing} ${layout}>${kind}>`;
}
function renderClip(index, start, track, indent) {
const authoredId = index % 5 === 0 ? `${indent} id="load-clip-${index}"\n` : "";
const kind = CLIP_KINDS[index % CLIP_KINDS.length];
const left = (index * 37) % 1820;
const top = (index * 17) % 980;
return `${indent}