// fallow-ignore-file unused-export unused-type circular-dependency code-duplication complexity /** * Render Orchestrator Service * * `executeRenderJob` is the in-process entry point that composes the * pipeline's six stages. Each stage lives in its own module under * `./render/stages/` so the pure-function primitives can be reused by * the distributed render path without dragging the orchestrator's * cleanup and observability scaffolding with them. * * Stage 1 compile → services/render/stages/compileStage.ts * Stage 1b probe → services/render/stages/probeStage.ts * (browser-driven duration discovery + media reconciliation; * grouped with Stage 1 in the perf summary) * Stage 2 extract videos → services/render/stages/extractVideosStage.ts * Stage 3 audio → services/render/stages/audioStage.ts * Stage 4 capture → services/render/stages/captureStage.ts * services/render/stages/captureStreamingStage.ts * services/render/stages/captureHdrStage.ts * Stage 5 encode → services/render/stages/encodeStage.ts * Stage 6 assemble → services/render/stages/assembleStage.ts * * Resources spawned by stages (file server, capture sessions, streaming * encoders, raw HDR frame files) are tracked in the orchestrator's * `try/finally` so a stage throwing mid-pipeline doesn't leak Chrome * processes or ffmpeg subprocesses. * * Heavy observability: every stage records timing into `perfStages`, * errors carry full context, and failures produce a diagnostic summary * (browser console tail, memory peaks, capture attempts, HDR * diagnostics). */ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readSync, closeSync, readdirSync, rmSync, statSync, writeFileSync, copyFileSync, appendFileSync, } from "fs"; import { parseHTML } from "linkedom"; import { type CanvasResolution, type Fps, type FpsInput, toFps } from "@hyperframes/core"; import { type EngineConfig, resolveConfig, type ExtractionResult, type ExtractionPhaseBreakdown, type HdrTransfer, closeCaptureSession, type CaptureOptions, type CaptureVideoMetadataHint, type CaptureSession, type BeforeCaptureHook, createVideoFrameInjector, getEncoderPreset, distributeFrames, executeParallelCapture, mergeWorkerFrames, type ParallelProgress, type WorkerTask, captureAlphaPng, applyDomLayerMask, removeDomLayerMask, decodePng, blitRgba8OverRgb48le, blitRgb48leRegion, groupIntoLayers, blitRgb48leAffine, parseTransformMatrix, convertTransfer, type ElementStackingInfo, type HfTransitionMeta, getSystemTotalMb, LOW_MEMORY_TOTAL_MB_THRESHOLD, assertConfiguredFfmpegBinariesExist, } from "@hyperframes/engine"; import { join, dirname, resolve } from "path"; import { randomUUID } from "crypto"; import { fileURLToPath } from "url"; import { createFileServer, type FileServerHandle, HF_PAGE_SIDE_COMPOSITING_STUB, VIRTUAL_TIME_SHIM, } from "./fileServer.js"; import { defaultLogger, type ProducerLogger } from "../logger.js"; import { type HdrImageTransferCache } from "./hdrImageTransferCache.js"; import { createCompiledFrameSrcResolver, createMemorySampler, type MemorySampler, updateJobStatus, writeFileExclusiveSync, } from "./render/shared.js"; import { buildRenderErrorDetails, cleanupRenderResources, safeCleanup } from "./render/cleanup.js"; import { normalizeErrorMessage } from "../utils/errorMessage.js"; import { resolveEffectiveHdrMode } from "./render/hdrMode.js"; import { buildRenderPerfSummary } from "./render/perfSummary.js"; import { getCaptureStageBrowserConsole } from "./render/captureStageError.js"; import { type CaptureCalibrationSample, type CaptureCostEstimate, resolveRenderWorkerCount, runCaptureCalibration, } from "./render/captureCost.js"; import { computeCompositionObservabilityHash, RenderObservabilityRecorder, observeRenderStage, type RenderCaptureObservability, type RenderExtractionObservability, type RenderObservabilitySummary, } from "./render/observability.js"; import { type HdrPerfCollector, type HdrPerfSummary, addHdrTiming } from "./render/hdrPerf.js"; import { runCompileStage } from "./render/stages/compileStage.js"; import { runProbeStage } from "./render/stages/probeStage.js"; import { runExtractVideosStage } from "./render/stages/extractVideosStage.js"; import { runAudioStage } from "./render/stages/audioStage.js"; import { runCaptureStage } from "./render/stages/captureStage.js"; import { runCaptureStreamingStage } from "./render/stages/captureStreamingStage.js"; import { runCaptureHdrStage } from "./render/stages/captureHdrStage.js"; import { runEncodeStage } from "./render/stages/encodeStage.js"; import { runAssembleStage } from "./render/stages/assembleStage.js"; function sampleDirectoryBytes(dir: string): number { let total = 0; const stack: string[] = [dir]; while (stack.length > 0) { const current = stack.pop(); if (!current) continue; let entries: string[] = []; try { entries = readdirSync(current); } catch { continue; } for (const name of entries) { const full = join(current, name); try { const st = statSync(full); if (st.isDirectory()) { stack.push(full); } else if (st.isFile()) { total += st.size; } } catch { // ignore } } } return total; } // fallow-ignore-next-line complexity function summarizeExtractionObservability( extractionResult: ExtractionResult | null, videoCount: number, ): RenderExtractionObservability { const extracted = extractionResult?.extracted ?? []; const totalFramesExtracted = extractionResult?.totalFramesExtracted ?? 0; const maxFramesPerVideo = extracted.reduce((max, item) => Math.max(max, item.totalFrames), 0); const phaseBreakdown = extractionResult?.phaseBreakdown; return { videoCount, extractedVideoCount: extracted.length, totalFramesExtracted, maxFramesPerVideo, avgFramesPerExtractedVideo: extracted.length > 0 ? Math.round(totalFramesExtracted / extracted.length) : undefined, vfrProbeMs: phaseBreakdown?.vfrProbeMs, vfrPreflightMs: phaseBreakdown?.vfrPreflightMs, vfrPreflightCount: phaseBreakdown?.vfrPreflightCount, cacheHits: phaseBreakdown?.cacheHits, cacheMisses: phaseBreakdown?.cacheMisses, }; } // Diagnostic helpers used by the HDR layered compositor when KEEP_TEMP=1 // is set. They are pure (capture no state), so we keep them at module scope // to avoid re-creating closures per frame and to make them callable from // any future composite path that needs to log non-zero pixel counts. function countNonZeroAlpha(rgba: Uint8Array): number { let n = 0; for (let p = 3; p < rgba.length; p += 4) { if (rgba[p] !== 0) n++; } return n; } function countNonZeroRgb48(buf: Uint8Array): number { let n = 0; for (let p = 0; p < buf.length; p += 6) { if ( buf[p] !== 0 || buf[p + 1] !== 0 || buf[p + 2] !== 0 || buf[p + 3] !== 0 || buf[p + 4] !== 0 || buf[p + 5] !== 0 ) n++; } return n; } /** * Metadata for a shader transition between two scenes, extracted from * `window.__hf.transitions`. Re-exported from the engine so the producer * shares the contract with composition runtime code. */ export type HdrTransitionMeta = HfTransitionMeta; /** Pre-computed frame range for an active transition. */ export interface TransitionRange extends HdrTransitionMeta { startFrame: number; endFrame: number; } export type RenderStatus = | "queued" | "preprocessing" | "rendering" | "encoding" | "assembling" | "complete" | "failed" | "cancelled"; export interface RenderConfig { /** * Frame rate as an exact rational. Integer fps is `{ num: 30, den: 1 }`; * NTSC is `{ num: 30000, den: 1001 }`. This shape lets the orchestrator * pass the exact rational through to FFmpeg's `-r` / `-framerate` flags * without a decimal round-trip — see `fpsToFfmpegArg` in @hyperframes/core. * * Use `fpsToNumber(config.fps)` at any site that needs a `number` for * arithmetic (frame-index → time, telemetry, frame-interval ms). Decimal * precision at our scales is more than sufficient. */ fps: Fps; quality: "draft" | "standard" | "high"; /** * Output container format. Defaults to `"mp4"`; existing renders are * unaffected unless this field is set explicitly. * * - `"mp4"`: H.264 by default, or H.265 + HDR10 when HDR auto-detect * engages or `hdrMode: "force-hdr"` is set. Opaque. The * default streaming/social deliverable. Faststart is applied so the * `moov` atom sits at the file start and the file plays from a * partial download. * - `"webm"`: VP9 + `yuva420p` pixel format → **true alpha channel**, no * chroma key. Plays in Chrome, Edge, and Firefox; Safari support for * alpha-WebM is incomplete. Use this when the output should drop * straight into a `