2424 * never have to handle them.
2525 */
2626
27- import { execFile as execFileCallback } from "node:child_process" ;
28- import { existsSync , mkdirSync , readFileSync , renameSync , rmSync } from "node:fs" ;
29- import { dirname , join } from "node:path" ;
30- import { fileURLToPath } from "node:url" ;
31- import { promisify } from "node:util" ;
32- import { type CanvasResolution , type Fps } from "@hyperframes/core" ;
27+ import { existsSync , mkdirSync , renameSync , rmSync } from "node:fs" ;
28+ import { join } from "node:path" ;
29+ import { type CanvasResolution } from "@hyperframes/core" ;
3330import { type EngineConfig , resolveConfig } from "@hyperframes/engine" ;
3431import { defaultLogger , type ProducerLogger } from "../../logger.js" ;
35- import { type RenderConfig , type RenderJob , createRenderJob } from "../renderOrchestrator.js" ;
3632import { runAudioStage } from "../render/stages/audioStage.js" ;
3733import { runCompileStage } from "../render/stages/compileStage.js" ;
3834import { runExtractVideosStage } from "../render/stages/extractVideosStage.js" ;
@@ -50,8 +46,7 @@ import {
5046} from "../render/stages/planHash.js" ;
5147import { validateNoGpuEncode , validateNoSystemFonts } from "../render/planValidation.js" ;
5248import { snapshotRuntimeEnv } from "../render/runtimeEnvSnapshot.js" ;
53-
54- const execFile = promisify ( execFileCallback ) ;
49+ import { buildSyntheticRenderJob , readFfmpegVersion , readProducerVersion } from "./shared.js" ;
5550
5651/**
5752 * Caller-supplied configuration for a distributed render. `fps`, `width`,
@@ -189,81 +184,6 @@ export function buildChunkSlices(
189184 return slices ;
190185}
191186
192- /**
193- * Map a `DistributedRenderConfig` onto the in-process `RenderConfig` shape
194- * the stage functions consume. Distributed plan() is the first caller of
195- * the staged renderer that operates without a full RenderJob — we synthesize
196- * one from the distributed config so the existing stage interfaces don't
197- * need a parallel "distributed mode" overload.
198- */
199- function buildSyntheticRenderJob ( config : DistributedRenderConfig ) : RenderJob {
200- const renderConfig : RenderConfig = {
201- fps : { num : config . fps , den : 1 } satisfies Fps ,
202- quality : config . quality ?? "standard" ,
203- format : config . format ,
204- crf : config . crf ,
205- videoBitrate : config . bitrate ,
206- outputResolution : config . outputResolution ,
207- // Distributed mode hard-pins to software GPU. The plan-time validator
208- // (see validateNoGpuEncode) refuses to fan out otherwise.
209- useGpu : false ,
210- debug : false ,
211- entryFile : config . entryFile ?? "index.html" ,
212- logger : config . logger ?? defaultLogger ,
213- // HDR is banned in distributed mode. force-sdr keeps the
214- // extract / encoder paths off the HDR branches entirely.
215- hdrMode : config . hdrMode ?? "force-sdr" ,
216- producerConfig : config . producerConfig ,
217- } ;
218- return createRenderJob ( renderConfig ) ;
219- }
220-
221- /**
222- * Resolve the producer package version by walking up from the calling module
223- * until a `package.json` whose `name === "@hyperframes/producer"` is found.
224- * Works for both the bundled `dist/index.js` (1 level up) and the unbundled
225- * source tree (`src/services/distributed/plan.ts` → 4 levels up).
226- */
227- function readProducerVersion ( ) : string {
228- const startDir = dirname ( fileURLToPath ( import . meta. url ) ) ;
229- let current = startDir ;
230- for ( let i = 0 ; i < 10 ; i ++ ) {
231- const candidate = join ( current , "package.json" ) ;
232- if ( existsSync ( candidate ) ) {
233- try {
234- const pkg = JSON . parse ( readFileSync ( candidate , "utf-8" ) ) as {
235- name ?: string ;
236- version ?: string ;
237- } ;
238- if ( pkg . name === "@hyperframes/producer" && typeof pkg . version === "string" ) {
239- return pkg . version ;
240- }
241- } catch {
242- // Fall through to the next ancestor.
243- }
244- }
245- const parent = dirname ( current ) ;
246- if ( parent === current ) break ;
247- current = parent ;
248- }
249- return "0.0.0-unknown" ;
250- }
251-
252- /**
253- * Spawn `ffmpeg -version` and return the first line (e.g. `"ffmpeg version 6.1.1"`).
254- * The string is opaque — `planHash` mixes it in verbatim, so any drift across
255- * worker hosts trips a `FFMPEG_VERSION_MISMATCH` rather than producing pixels
256- * that subtly disagree with the plan's baked-in encoder args.
257- */
258- async function readFfmpegVersion ( ) : Promise < string > {
259- const { stdout } = await execFile ( "ffmpeg" , [ "-version" ] , { maxBuffer : 1024 * 1024 } ) ;
260- const firstLine = stdout . split ( / \r ? \n / ) [ 0 ] ?. trim ( ) ?? "" ;
261- if ( ! firstLine ) {
262- throw new Error ( "[plan] ffmpeg -version returned empty output" ) ;
263- }
264- return firstLine ;
265- }
266-
267187/**
268188 * Hash the deterministic-font bundle that ships inside `@hyperframes/producer`.
269189 * The compiled HTML already inlines per-family `@font-face` data URIs, so the
@@ -391,7 +311,20 @@ export async function plan(
391311 forceScreenshot : false ,
392312 } ;
393313
394- const job = buildSyntheticRenderJob ( config ) ;
314+ const job = buildSyntheticRenderJob ( {
315+ fps : { num : config . fps , den : 1 } ,
316+ quality : config . quality ?? "standard" ,
317+ format : config . format ,
318+ crf : config . crf ,
319+ bitrate : config . bitrate ,
320+ outputResolution : config . outputResolution ,
321+ // HDR is banned in distributed mode. force-sdr keeps the
322+ // extract / encoder paths off the HDR branches entirely.
323+ hdrMode : config . hdrMode ?? "force-sdr" ,
324+ entryFile : config . entryFile ?? "index.html" ,
325+ logger : config . logger ,
326+ producerConfig : config . producerConfig ,
327+ } ) ;
395328 const entryFile = config . entryFile ?? "index.html" ;
396329 const htmlPath = join ( projectDir , entryFile ) ;
397330 if ( ! existsSync ( htmlPath ) ) {
0 commit comments