Merge pull request #735 from heygen-com/05-12-refactor_producer_document_executerenderjob_as_a_thin_sequencer

refactor(producer): document executeRenderJob as a thin sequencer
This commit is contained in:
James Russo
2026-05-12 06:59:02 -04:00
committed by GitHub
5 changed files with 43 additions and 30 deletions
@@ -17,7 +17,6 @@
import { join } from "node:path";
import { processCompositionAudio } from "@hyperframes/engine";
import type { RenderJob } from "../../renderOrchestrator.js";
import type { CompositionMetadata } from "../shared.js";
export interface AudioStageInput {
@@ -25,7 +24,6 @@ export interface AudioStageInput {
workDir: string;
/** `join(workDir, "compiled")`; passed through to the audio mixer for asset resolution. */
compiledDir: string;
job: RenderJob;
/** Composition duration (post-probe). Must be > 0 — probeStage guarantees this. */
duration: number;
/** Read-only view of `composition.audios`. */
@@ -1,8 +1,7 @@
/**
* captureHdrStage — Z-ordered HDR / shader-transition layered composite.
*
* Lifted verbatim from `executeRenderJob`'s `if (useLayeredComposite)`
* branch. The most complex capture path:
* The most complex capture path:
* - Spawns a dedicated `domSession` for transparent-background screenshots.
* - Spawns an `hdrEncoder` (`spawnStreamingEncoder` with
* `rawInputFormat: "rgb48le"`) accepting pre-composited HDR frames.
@@ -34,9 +34,10 @@
* `streamingEncoderClosed` so it's idempotent.
*
* Known follow-up (same as captureStage): this stage imports
* `updateJobStatus` from `renderOrchestrator.ts`, re-introducing the
* cycle PR 1.3.5 broke. A subsequent PR will consolidate capture
* helpers into a shared module.
* `updateJobStatus` from `renderOrchestrator.ts`, forming a runtime
* cycle with the orchestrator's import of `runCaptureStreamingStage`.
* Safe at runtime; a subsequent change will move the capture helpers
* into a shared module so the stages can import without reaching back.
*/
import {
@@ -102,8 +103,6 @@ export type CaptureStreamingStageResult =
| {
/** Streaming path ran successfully — sequencer should skip the disk path AND Stage 5 encode. */
success: true;
/** Wall-clock ms for the capture phase (`Date.now() - stage4Start` is the sequencer's job). */
captureDurationMs: number;
/** Wall-clock ms for the encode phase (overlapped with capture; from the encoder's own report). */
encodeMs: number;
probeSession: CaptureSession | null;
@@ -165,7 +164,6 @@ export async function runCaptureStreamingStage(
return { success: false };
}
const streamStart = Date.now();
const currentEncoder: StreamingEncoder = streamingEncoder;
try {
@@ -280,7 +278,6 @@ export async function runCaptureStreamingStage(
return {
success: true,
captureDurationMs: Date.now() - streamStart,
encodeMs: encodeResult.durationMs,
probeSession,
lastBrowserConsole,
@@ -33,7 +33,6 @@ import {
encodeFramesFromDir,
getEncoderPreset,
} from "@hyperframes/engine";
import type { Fps } from "@hyperframes/core";
import type { ProducerLogger } from "../../../logger.js";
import {
updateJobStatus,
@@ -53,7 +52,6 @@ export interface EncodeStageInput {
/** Output dimensions (post-deviceScaleFactor). */
width: number;
height: number;
fps: Fps;
/** True when the output format requires an alpha channel; selects frame extension. */
needsAlpha: boolean;
/** True iff the composition has audio. Drives the sidecar copy. */
@@ -66,7 +64,6 @@ export interface EncodeStageInput {
preset: ReturnType<typeof getEncoderPreset>;
effectiveQuality: number;
effectiveBitrate: string | undefined;
useGpu: boolean | undefined;
/** Producer config — enables the chunked-concat encoder when on. */
enableChunkedEncode: boolean;
chunkedEncodeSize: number;
@@ -89,7 +86,6 @@ export async function runEncodeStage(input: EncodeStageInput): Promise<EncodeSta
videoOnlyPath,
width,
height,
fps,
needsAlpha,
hasAudio,
audioOutputPath,
@@ -97,7 +93,6 @@ export async function runEncodeStage(input: EncodeStageInput): Promise<EncodeSta
preset,
effectiveQuality,
effectiveBitrate,
useGpu,
enableChunkedEncode,
chunkedEncodeSize,
abortSignal,
@@ -143,7 +138,7 @@ export async function runEncodeStage(input: EncodeStageInput): Promise<EncodeSta
const frameExt = needsAlpha ? "png" : "jpg";
const framePattern = `frame_%06d.${frameExt}`;
const encoderOpts = {
fps,
fps: job.config.fps,
width,
height,
codec: preset.codec,
@@ -151,7 +146,7 @@ export async function runEncodeStage(input: EncodeStageInput): Promise<EncodeSta
quality: effectiveQuality,
bitrate: effectiveBitrate,
pixelFormat: preset.pixelFormat,
useGpu,
useGpu: job.config.useGpu,
hdr: preset.hdr,
};
const encodeResult = enableChunkedEncode
@@ -1,16 +1,33 @@
/**
* Render Orchestrator Service
*
* Coordinates the entire video rendering pipeline:
* 1. Parse composition metadata
* 2. Pre-extract video frames
* 3. Pre-process audio tracks
* 4. Parallel frame capture
* 5. Video encoding
* 6. Final assembly (audio mux + faststart)
* `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.
*
* Heavy observability: every stage logs timing, errors include
* full context, and failures produce a diagnostic summary.
* 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 {
@@ -1812,6 +1829,16 @@ export function extractStandaloneEntryFromIndex(
return document.toString();
}
/**
* Render a `RenderJob` end-to-end: compile probe extract videos
* audio capture encode assemble. The function body is a thin
* sequencer over the eight stage modules in `./render/stages/`; the
* orchestrator owns shared resources (work dir, file server, probe
* session, browser console buffer, perf counters, peak-memory sampler)
* and the `try/finally` cleanup. Returns once the final output exists at
* `outputPath`; throws on cancellation, encoder failure, or a stage
* error (with a diagnostic summary written to `perf-summary.json`).
*/
export async function executeRenderJob(
job: RenderJob,
projectDir: string,
@@ -2069,7 +2096,6 @@ export async function executeRenderJob(
projectDir,
workDir,
compiledDir,
job,
duration: job.duration,
audios: composition.audios,
abortSignal,
@@ -2442,7 +2468,6 @@ export async function executeRenderJob(
videoOnlyPath,
width,
height,
fps: job.config.fps,
needsAlpha,
hasAudio,
audioOutputPath,
@@ -2450,7 +2475,6 @@ export async function executeRenderJob(
preset,
effectiveQuality,
effectiveBitrate,
useGpu: job.config.useGpu,
enableChunkedEncode,
chunkedEncodeSize,
abortSignal,