import type { ClipTiming } from './edl-time.js'; /** Format seconds/ratios for filtergraph text without float noise. */ export declare function fmtNum(value: number): string; /** One rasterized HUD overlay and the output window it is live for. */ export interface UiOverlay { filePath: string; /** Which EDL clip produced it; windows only ever chain within one clip. */ clipIndex: number; startSec: number; endSec: number; } /** An output-time window during which HUD overlays must not draw. */ export interface BlockedWindow { startSec: number; endSec: number; } /** * Width/height straight out of a PNG's IHDR chunk, so the filtergraph only * pays for a scale filter when the overlay really is the wrong size. */ export declare function readPngSize(filePath: string): { width: number; height: number; } | null; /** One keyframe's window, as `foldUiOverlays` needs it. */ export interface UiKeyframeWindow { clipIndex: number; outputStartSec: number; outputEndSec: number; } /** Overlays plus the count of windows left with no UI at all. */ export interface FoldedOverlays { overlays: UiOverlay[]; bare: number; } /** * Turn rasterized paths (with null for the ones that failed) into overlay * windows, holding the previous overlay across a failure. * * The hold is deliberately limited to the SAME clip. A clip's last window ends * at exactly the next clip's outputStart — for cuts and crossfades alike — so * comparing times alone matches across the boundary too, and a failed first * keyframe would stretch the previous SHOT's HUD over new footage: the wrong * score over the wrong scene, which is worse than no HUD at all. A window with * no in-clip predecessor goes bare instead. * * Pure and exported so this is testable: render.ts reaches Playwright through * hud-raster.ts, which jest cannot load. */ export declare function foldUiOverlays(paths: ReadonlyArray, keyframes: readonly UiKeyframeWindow[]): FoldedOverlays; /** A gap-free PNG sequence, one entry per output frame, ready for image2. */ export interface UiSequence { /** Holds ui_%06d.png from index 0, plus the shared transparent filler. */ dir: string; frameCount: number; /** Entries resolved to the filler: no overlay owned them, or a card blocked them. */ blankFrames: number; /** Overlay windows too short to contain a frame time. */ unusedOverlays: number; } /** * A fully transparent PNG, written without shelling out. * * `ffmpeg -f lavfi -i color=black@0.0 -pix_fmt rgba` looks like it produces * this and does not — every alpha byte comes back 255, which would black out * the whole frame. The chunk/CRC primitives are the QR encoder's. */ export declare function writeTransparentPng(filePath: string, width: number, height: number): void; /** * Densify the overlay windows into one gap-free image2 sequence in OUTPUT time. * * Why not one ffmpeg input per keyframe (what this replaced): that shape costs * ~23 MB of RSS and one serial filter node per overlay, so it was the real * ceiling on how often the HUD could change — measured 4.31 s / 3.8 GB for 126 * overlays, 32 s / 17.2 GB at one per output frame, and above ~400 inputs * ffmpeg starts failing scaler allocations, drops the HUD from the rest of the * video, and still exits 0. One sequence is flat in keyframe count: 2.83 s / * 0.94 GB whether it carries 126 distinct rasters or 902. * * Output time, not per-clip source time: the windows are already computed in * output time (so exactly one HUD is live across a crossfade), and title-card * blocked windows are output time too. A per-clip overlay applied before the * xfade would cross-dissolve two HUDs against each other. * * The sequence MUST be gap-free: a missing index truncates the image2 input, * ffmpeg exits 0, and the HUD silently disappears for the rest of the video. * Running long is free, so the frame count deliberately rounds up and adds one. */ export declare function buildUiSequence(overlays: readonly UiOverlay[], fps: number, totalDurationSec: number, seqDir: string, blocked?: readonly BlockedWindow[]): UiSequence; /** * Chain the UI sequence onto the finished video chain. Runs in yuv444p so the * alpha blend keeps HUD text edges crisp (yuv420 would subsample them), and * converts back for libx264 at the end. * * `eof_action=pass`, never `repeat`: if the sequence ever does come up short, * passing drops the HUD from that point, while repeating would freeze the last * raster to the end AND punch it through every title card. */ export declare function appendUiSequenceChain(lines: string[], sequence: UiSequence, uiIndex: number, width: number, height: number, endLabel: string): void; export interface LogoPlacement { /** ffmpeg input index of the logo image. */ inputIndex: number; opacity: number; /** Logo height as a fraction of video height. */ heightFrac: number; /** Edge margin as a fraction of video width. */ marginFrac: number; corner: 'br' | 'bl' | 'tr' | 'tl'; } /** * Watermark chain, applied last so the logo sits above the HUD overlay too. * * The brand mark is white, and gameplay footage is frequently bright (sunlit * sand measures luma 120-185 in the corner), so a plain translucent white logo * washes out. A blurred black copy of the same alpha is laid underneath as a * soft drop shadow — the standard broadcast treatment — which keeps the mark * readable over light and dark backgrounds without altering the brand colour. * * Composited in yuv444 so the mark's edges stay crisp, converted to yuv420p at * the very end for libx264. */ export declare function buildLogoChain(inLabel: string, outLabel: string, placement: LogoPlacement, width: number, height: number): string[]; /** * Build the per-clip prep + chaining lines. * * @param chainEnd label the finished video chain must land on (`vout`, or * `vbase` when a UI overlay chain will be appended after it). */ export declare function buildVideoChain(timings: ClipTiming[], fps: number, chainEnd: string): string[];