// fallow-ignore-file 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, readdirSync, rmSync, statSync, writeFileSync, copyFileSync, appendFileSync, } from "fs"; import { tmpdir } from "node:os"; import { parseHTML } from "linkedom"; import { type CanvasResolution, type Fps, type FpsInput, fpsToNumber, toFps, } from "@hyperframes/core"; import { type EngineConfig, resolveConfig, type ExtractionResult, type ExtractionPhaseBreakdown, type VideoFrameFormat, closeCaptureSession, type CaptureOptions, type CaptureVideoMetadataHint, type CaptureSession, type BeforeCaptureHook, createVideoFrameInjector, getEncoderPreset, distributeFrames, executeParallelCapture, mergeWorkerFrames, type ParallelProgress, type WorkerTask, getSystemTotalMb, LOW_MEMORY_TOTAL_MB_THRESHOLD, assertConfiguredFfmpegBinariesExist, type CapturePerfSummary, type CaptureWarning, type SubTimelineWaitOutcome, type WorkerSizing, resolveBrowserGpuMode, resolveHeadlessShellPath, applyConcreteGpuScreenshotClamp, explainDrawElementDisabled, scaleProtocolTimeoutForComposition, classifyCaptureFailure, cloneCaptureWarning, isMemoryExhaustionError, isDrawElementVerificationError, getDrawElementVerificationDetails, augmentProtocolTimeoutError, augmentPageNavigationTimeoutError, } from "@hyperframes/engine"; import { join, dirname, resolve } from "path"; import { totalmem } from "node:os"; import { randomUUID } from "crypto"; import { fileURLToPath } from "url"; import { closeFileServerSafely, createFileServer, type FileServerHandle, HF_PAGE_SIDE_COMPOSITING_STUB, VIRTUAL_TIME_SHIM, } from "./fileServer.js"; import { defaultLogger, type ProducerLogger } from "../logger.js"; import { outputNeedsAlpha, outputSupportsPageSideShaderCompositing, type RenderOutputFormat, } from "./render/renderFormat.js"; import { createMemorySampler, type MemorySampler, updateJobStatus } from "./render/shared.js"; import { buildRenderErrorDetails } from "./render/cleanup.js"; import { publishRenderFailure } from "./render/renderEventPublisher.js"; import { RenderExecutionContext } from "./render/renderExecutionContext.js"; import { ArtifactTransaction } from "./render/artifactTransaction.js"; import { createCapturePlan, replanAfterFailure, type CapturePlan, type SdrDiskCapturePlan, type CaptureRouting, } from "./render/capturePlan.js"; import { normalizeErrorMessage } from "../utils/errorMessage.js"; import { formatCaptureFrameName } from "../utils/paths.js"; import { resolveEffectiveHdrMode } from "./render/hdrMode.js"; import { buildRenderPerfSummary, pushWorkerDedupPerfs, roundDb, worstSubTimelineWaitOutcome, } from "./render/perfSummary.js"; import { getCaptureStageBrowserConsole } from "./render/captureStageError.js"; import { resolveVideoCaptureBeyondViewport } from "./render/captureBeyondViewport.js"; import { type CaptureCalibrationSample, type CaptureCostEstimate, buildHeapAdvisoryWarning, resolveRenderWorkerCount, runCaptureCalibration, } from "./render/captureCost.js"; import { computeCompositionObservabilityHash, RenderObservabilityRecorder, observeRenderStage, type RenderCaptureObservability, type RenderExtractionObservability, type RenderObservationData, type RenderObservabilitySummary, } from "./render/observability.js"; import { emitFallbackCaptureProfile } from "./render/fallbackCaptureProfile.js"; import { type HdrPerfCollector, type HdrPerfSummary } from "./render/hdrPerf.js"; import { assertVideoFrameCoverage, computeVideoFrameCoverage, countAuthoredTimedClips, resolveVideoCoverageThreshold, type VideoFrameCoverageReport, } from "./render/videoFrameCoverage.js"; import { runCompileStage } from "./render/stages/compileStage.js"; import { runProbeStage } from "./render/stages/probeStage.js"; import { validateRenderDuration } from "./render/planValidation.js"; import { runExtractVideosStage, shouldCopyExtractedFrames, } from "./render/stages/extractVideosStage.js"; import { runAudioStage } from "./render/stages/audioStage.js"; import { runCaptureStage } from "./render/stages/captureStage.js"; import { type CaptureStreamingStageResult, 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"; import { shouldUseLayeredComposite } from "./hdrCompositor.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, coverageReports?: readonly VideoFrameCoverageReport[], authoredTimedClipCount?: 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; // Only surface the coverage gauges when we actually ran the gate — a // no-video render must not emit a spurious `minVideoFrameCoverageRatio` // that dashboards interpret as "coverage measured, was 0/0=1". const coverageGauges = coverageReports && coverageReports.length > 0 ? { minVideoFrameCoverageRatio: coverageReports.reduce( (min, r) => Math.min(min, r.ratio), Number.POSITIVE_INFINITY, ), } : {}; 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, transientRetries: phaseBreakdown?.transientRetries, ...coverageGauges, authoredTimedClipCount, }; } export type RenderStatus = | "queued" | "preprocessing" | "rendering" | "encoding" | "assembling" | "complete" | "failed" | "cancelled"; export type RenderOutcome = "completed" | "completed_with_warnings" | "failed" | "cancelled"; export type RenderStrictness = "strict" | "best-effort"; export interface RenderWarning extends CaptureWarning { stage: "capture-readiness"; } 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 `