@hyperframes/producer 0.8.192 → 0.8.193

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,6 @@ import {
6
6
  existsSync as existsSync15,
7
7
  linkSync,
8
8
  mkdirSync as mkdirSync10,
9
- renameSync as renameSync4,
10
9
  rmSync as rmSync9,
11
10
  writeFileSync as writeFileSync6
12
11
  } from "node:fs";
@@ -19,7 +18,9 @@ import {
19
18
  getEncoderPreset as getEncoderPreset4,
20
19
  normalizeVp9CpuUsed,
21
20
  resolveConfig as resolveConfig3,
22
- declareContentAddressedUrl
21
+ declareContentAddressedUrl,
22
+ renameWithRetry as renameWithRetry6,
23
+ declareProbeFactsUrl
23
24
  } from "@hyperframes/engine";
24
25
 
25
26
  // src/logger.ts
@@ -1150,7 +1151,7 @@ function encoderFailureError(prefix, result) {
1150
1151
  }
1151
1152
 
1152
1153
  // src/services/render/artifactTransaction.ts
1153
- import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST } from "@hyperframes/engine";
1154
+ import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, renameWithRetry } from "@hyperframes/engine";
1154
1155
 
1155
1156
  // src/utils/ffprobe.ts
1156
1157
  import {
@@ -1227,6 +1228,24 @@ import {
1227
1228
 
1228
1229
  // src/services/render/captureStageError.ts
1229
1230
  import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
1231
+
1232
+ // src/services/render/perfSummary.ts
1233
+ import { fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1234
+ function pushWorkerDedupPerfs(results, sink) {
1235
+ for (const r of results) {
1236
+ if (r.perf) sink.push(r.perf);
1237
+ }
1238
+ }
1239
+ function pushWorkerPageErrors(results, sink) {
1240
+ for (const result of results) {
1241
+ for (const line of result.pageErrorLines ?? []) if (!sink.includes(line)) sink.push(line);
1242
+ }
1243
+ }
1244
+ function withWorkerPageErrors(consoleLines, workerLines) {
1245
+ return [...consoleLines, ...workerLines.filter((line) => !consoleLines.includes(line))];
1246
+ }
1247
+
1248
+ // src/services/render/captureStageError.ts
1230
1249
  var CaptureStageError = class extends CaptureFailure {
1231
1250
  browserConsole;
1232
1251
  constructor(input) {
@@ -1235,10 +1254,13 @@ var CaptureStageError = class extends CaptureFailure {
1235
1254
  kind: classified.kind,
1236
1255
  message: normalizeErrorMessage(input.cause),
1237
1256
  cause: input.cause,
1238
- workerDiagnostics: classified.workerDiagnostics
1257
+ workerDiagnostics: classified.workerDiagnostics,
1258
+ pageErrorLines: classified.pageErrorLines
1239
1259
  });
1240
1260
  this.name = "CaptureStageError";
1241
- this.browserConsole = input.browserConsole.slice();
1261
+ this.browserConsole = withWorkerPageErrors(input.browserConsole, [
1262
+ ...classified.pageErrorLines
1263
+ ]);
1242
1264
  if (input.cause instanceof Error && input.cause.stack) {
1243
1265
  this.stack = input.cause.stack;
1244
1266
  }
@@ -1298,7 +1320,7 @@ import {
1298
1320
  resolveVideoExtractionWindow,
1299
1321
  runFfmpeg
1300
1322
  } from "@hyperframes/engine";
1301
- import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1323
+ import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
1302
1324
  var NO_FOLLOW_FLAG = constants2.O_NOFOLLOW ?? 0;
1303
1325
  var BYTES_PER_MIB = 1024 * 1024;
1304
1326
 
@@ -1324,13 +1346,8 @@ import {
1324
1346
  spawnStreamingEncoder
1325
1347
  } from "@hyperframes/engine";
1326
1348
 
1327
- // src/services/render/perfSummary.ts
1328
- import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
1329
- function pushWorkerDedupPerfs(results, sink) {
1330
- for (const r of results) {
1331
- if (r.perf) sink.push(r.perf);
1332
- }
1333
- }
1349
+ // src/services/render/segmentManifest.ts
1350
+ import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
1334
1351
 
1335
1352
  // src/services/render/hdrMode.ts
1336
1353
  import {
@@ -1684,19 +1701,11 @@ import {
1684
1701
 
1685
1702
  // src/services/animatedGifPrep.ts
1686
1703
  import { createHash as createHash2 } from "node:crypto";
1687
- import {
1688
- copyFileSync as copyFileSync2,
1689
- existsSync as existsSync4,
1690
- mkdirSync as mkdirSync3,
1691
- readFileSync as readFileSync3,
1692
- renameSync,
1693
- rmSync as rmSync3,
1694
- statSync as statSync2
1695
- } from "node:fs";
1704
+ import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync2 } from "node:fs";
1696
1705
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve4 } from "node:path";
1697
1706
  import { parseHTML as parseHTML3 } from "linkedom";
1698
1707
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
1699
- import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2 } from "@hyperframes/engine";
1708
+ import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
1700
1709
  var PREPARED_GIF_SUBDIR = "_animated_gif";
1701
1710
  var CACHE_SCHEMA = "hfgif-v1";
1702
1711
  function splitUrlSuffix(src) {
@@ -1875,7 +1884,7 @@ async function ensurePreparedWebm(input) {
1875
1884
  throw new Error("Animated GIF transcode produced an empty output");
1876
1885
  }
1877
1886
  if (!isUsableFile(input.cachePath)) {
1878
- renameSync(tmpPath, input.cachePath);
1887
+ renameWithRetry3(tmpPath, input.cachePath);
1879
1888
  } else {
1880
1889
  rmSync3(tmpPath, { force: true });
1881
1890
  }
@@ -5007,7 +5016,7 @@ import {
5007
5016
 
5008
5017
  // src/services/render/audioPadTrim.ts
5009
5018
  import { spawn } from "node:child_process";
5010
- import { mkdtempSync as mkdtempSync2, renameSync as renameSync2, rmSync as rmSync4 } from "node:fs";
5019
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync4 } from "node:fs";
5011
5020
  import { dirname as dirname5, join as join9 } from "node:path";
5012
5021
  import {
5013
5022
  DEFAULT_CONFIG,
@@ -5016,6 +5025,7 @@ import {
5016
5025
  extractAudioMetadata as extractAudioMetadata3,
5017
5026
  getFfprobeBinary,
5018
5027
  ManagedChildProcess,
5028
+ renameWithRetry as renameWithRetry4,
5019
5029
  runFfmpeg as runFfmpeg4,
5020
5030
  trackChildProcess
5021
5031
  } from "@hyperframes/engine";
@@ -5271,7 +5281,7 @@ async function enforceAacTruePeak(input) {
5271
5281
  }
5272
5282
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
5273
5283
  if (measuredPath !== correctedPath) return { success: true };
5274
- renameSync2(correctedPath, input.audioPath);
5284
+ renameWithRetry4(correctedPath, input.audioPath);
5275
5285
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
5276
5286
  }
5277
5287
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -5461,6 +5471,7 @@ async function runAudioStage(input) {
5461
5471
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
5462
5472
  let audioLoweredDb;
5463
5473
  if (result.hasAudio) {
5474
+ const guardStart = Date.now();
5464
5475
  const peak = await constrainAacTruePeak({
5465
5476
  audioPath: result.audioOutputPath,
5466
5477
  sourcePath: pcmSourcePath,
@@ -5470,6 +5481,7 @@ async function runAudioStage(input) {
5470
5481
  });
5471
5482
  if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
5472
5483
  audioLoweredDb = peak.loweredDb;
5484
+ input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
5473
5485
  }
5474
5486
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
5475
5487
  } finally {
@@ -5495,6 +5507,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5495
5507
  let hasAudio = false;
5496
5508
  let audioError;
5497
5509
  let audioFailures;
5510
+ let audioPhaseMs;
5498
5511
  if (audios.length > 0) {
5499
5512
  let audioResult;
5500
5513
  try {
@@ -5544,6 +5557,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5544
5557
  assertAudioNotInterrupted(audioResult.failures);
5545
5558
  hasAudio = audioResult.success;
5546
5559
  audioFailures = audioResult.failures;
5560
+ audioPhaseMs = audioResult.phaseMs;
5547
5561
  if (!hasAudio) {
5548
5562
  audioError = audioResult.error ?? "audio mix failed for an unknown reason";
5549
5563
  for (const failure of audioResult.failures ?? []) {
@@ -5562,7 +5576,8 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5562
5576
  audioOutputPath,
5563
5577
  hasAudio,
5564
5578
  audioError,
5565
- audioFailures
5579
+ audioFailures,
5580
+ audioPhaseMs
5566
5581
  };
5567
5582
  }
5568
5583
 
@@ -5672,6 +5687,7 @@ async function runCaptureStage(input) {
5672
5687
  let { workerCount } = plan2;
5673
5688
  const { forceScreenshot } = plan2;
5674
5689
  let lastBrowserConsole = [];
5690
+ const workerPageErrors = [];
5675
5691
  let captureBeyondViewport = probeSession?.options.captureBeyondViewport;
5676
5692
  const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
5677
5693
  if (frameRange !== void 0) {
@@ -5709,6 +5725,7 @@ async function runCaptureStage(input) {
5709
5725
  abortSignal,
5710
5726
  frameRangeStart: frameRange?.startFrame,
5711
5727
  dedupPerfs,
5728
+ pageErrorLines: workerPageErrors,
5712
5729
  onProgress: (progress) => {
5713
5730
  if (progress.latestWorkerPhase) {
5714
5731
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -5729,6 +5746,11 @@ async function runCaptureStage(input) {
5729
5746
  },
5730
5747
  cfg: captureCfg,
5731
5748
  log
5749
+ }).catch((error) => {
5750
+ throw wrapCaptureStageError(
5751
+ error,
5752
+ withWorkerPageErrors(probeSession?.browserConsoleBuffer ?? [], workerPageErrors)
5753
+ );
5732
5754
  });
5733
5755
  captureAttempts.push(...attempts);
5734
5756
  const lastAttempt = attempts[attempts.length - 1];
@@ -5741,6 +5763,7 @@ async function runCaptureStage(input) {
5741
5763
  await closeCaptureSession5(probeSession);
5742
5764
  probeSession = null;
5743
5765
  }
5766
+ lastBrowserConsole = withWorkerPageErrors(lastBrowserConsole, workerPageErrors);
5744
5767
  } else {
5745
5768
  const sequential = await runSequentialDiskCapture(
5746
5769
  input,
@@ -5948,6 +5971,7 @@ import {
5948
5971
  statSync as statSync3,
5949
5972
  writeFileSync as writeFileSync2
5950
5973
  } from "node:fs";
5974
+ import { readFile as readFile2 } from "node:fs/promises";
5951
5975
  import { dirname as dirname6, join as join12 } from "node:path";
5952
5976
  import {
5953
5977
  encodeFramesChunkedConcat,
@@ -6117,6 +6141,100 @@ function videoEncoderOptions(input, engineCfg) {
6117
6141
  gopSize: input.gopSize
6118
6142
  };
6119
6143
  }
6144
+ function canEncodeWhileCapturing(input, workerCount, cpuCount) {
6145
+ return workerCount > 1 && workerCount < cpuCount && !input.isPngSequence && !input.isGif && !input.enableChunkedEncode && input.preset.codec === "h264" && !input.preset.hdr && input.job.config.useGpu !== true && input.captureImageFormat === "jpeg";
6146
+ }
6147
+ async function readLandedFrame(workerPath, mergedPath) {
6148
+ if (workerPath !== void 0) {
6149
+ try {
6150
+ return await readFile2(workerPath);
6151
+ } catch (error) {
6152
+ if (error.code !== "ENOENT") throw error;
6153
+ }
6154
+ }
6155
+ return readFile2(mergedPath);
6156
+ }
6157
+ function encodeWhileCapturing(input, totalFrames) {
6158
+ const engineCfg = resolveEngineConfig(input);
6159
+ const stop = new AbortController();
6160
+ const signal = input.abortSignal ? AbortSignal.any([input.abortSignal, stop.signal]) : stop.signal;
6161
+ const landed = /* @__PURE__ */ new Map();
6162
+ let captureDone = false;
6163
+ let framesFed = 0;
6164
+ let wake = null;
6165
+ const nudge = () => {
6166
+ wake?.();
6167
+ wake = null;
6168
+ };
6169
+ signal.addEventListener("abort", nudge, { once: true });
6170
+ let reportFed = null;
6171
+ const encoder = spawnStreamingEncoder4(
6172
+ input.videoOnlyPath,
6173
+ { ...videoEncoderOptions(input, engineCfg), imageFormat: "jpeg" },
6174
+ signal,
6175
+ engineCfg
6176
+ );
6177
+ const fed = (async () => {
6178
+ const stream = await encoder;
6179
+ for (let next = 0; next < totalFrames; next++) {
6180
+ while (!landed.has(next) && !captureDone && !signal.aborted) {
6181
+ await new Promise((resolve9) => wake = resolve9);
6182
+ }
6183
+ if (signal.aborted) return;
6184
+ const mergedPath = join12(input.framesDir, formatCaptureFrameName(next, "jpg"));
6185
+ const frame = await readLandedFrame(landed.get(next), mergedPath);
6186
+ landed.delete(next);
6187
+ if (!await stream.writeFrame(frame)) {
6188
+ throw new Error(stream.getExitError() ?? "encoder exited before the last frame");
6189
+ }
6190
+ framesFed++;
6191
+ reportFed?.(framesFed);
6192
+ }
6193
+ })().then(
6194
+ () => void 0,
6195
+ (error) => error ?? new Error("encode failed")
6196
+ );
6197
+ const closeEncoder = async () => (await encoder.catch(() => null))?.close();
6198
+ return {
6199
+ onFrameLanded: (frame) => {
6200
+ landed.set(frame.index, frame.path);
6201
+ nudge();
6202
+ },
6203
+ finish: async () => {
6204
+ const start = Date.now();
6205
+ captureDone = true;
6206
+ input.log.info("[Render] Encoded frames while capturing", {
6207
+ framesFed,
6208
+ totalFrames
6209
+ });
6210
+ const encoded = startEncodeProgress(input.job, "Encoding video", input.onProgress);
6211
+ reportFed = encoded;
6212
+ nudge();
6213
+ const failure = await fed;
6214
+ const result = await closeEncoder();
6215
+ input.assertNotAborted();
6216
+ if (failure === void 0 && result?.success) {
6217
+ encoded();
6218
+ return { encodeMs: Date.now() - start };
6219
+ }
6220
+ input.log.warn(
6221
+ "[Render] Encoding while capturing failed; encoding the frames after capture",
6222
+ {
6223
+ error: String(failure ?? result?.error)
6224
+ }
6225
+ );
6226
+ return runEncodeStage(input);
6227
+ },
6228
+ abort: async () => {
6229
+ stop.abort();
6230
+ await fed;
6231
+ await closeEncoder();
6232
+ },
6233
+ get framesFed() {
6234
+ return framesFed;
6235
+ }
6236
+ };
6237
+ }
6120
6238
  async function runEncodeStage(input) {
6121
6239
  const {
6122
6240
  job,
@@ -6430,6 +6548,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
6430
6548
  options.cfg
6431
6549
  );
6432
6550
  pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
6551
+ if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
6433
6552
  } finally {
6434
6553
  await mergeWorkerFrames(attemptWorkDir, tasks, options.framesDir);
6435
6554
  }
@@ -7508,15 +7627,9 @@ import { join as join19 } from "node:path";
7508
7627
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
7509
7628
 
7510
7629
  // src/services/distributed/mp4Stitch.ts
7511
- import {
7512
- createReadStream as createReadStream4,
7513
- createWriteStream,
7514
- mkdirSync as mkdirSync8,
7515
- mkdtempSync as mkdtempSync3,
7516
- renameSync as renameSync3,
7517
- rmSync as rmSync8
7518
- } from "node:fs";
7630
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
7519
7631
  import { open } from "node:fs/promises";
7632
+ import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
7520
7633
  import { dirname as dirname9, join as join18 } from "node:path";
7521
7634
  import { finished, pipeline } from "node:stream/promises";
7522
7635
  function boxAt(src, off, at, limit) {
@@ -7889,7 +8002,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
7889
8002
  for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
7890
8003
  out.end();
7891
8004
  await finished(out);
7892
- renameSync3(join18(workDir, "out.mp4"), outputPath);
8005
+ renameWithRetry5(join18(workDir, "out.mp4"), outputPath);
7893
8006
  } finally {
7894
8007
  out.destroy();
7895
8008
  rmSync8(workDir, { recursive: true, force: true });
@@ -8619,7 +8732,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
8619
8732
  const needsAlpha = outputNeedsAlpha(config.format);
8620
8733
  const remoteFiles = Object.entries(options.remoteFiles ?? {});
8621
8734
  const remoteLocalPath = (rel) => join21(projectDir, ...rel.split("/"));
8622
- for (const [, file] of remoteFiles) declareContentAddressedUrl(file.url, file.sha256);
8735
+ for (const [, file] of remoteFiles) {
8736
+ declareContentAddressedUrl(file.url, file.sha256);
8737
+ if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
8738
+ }
8623
8739
  const urlByAbsentLocalPath = new Map(
8624
8740
  remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
8625
8741
  );
@@ -8741,7 +8857,8 @@ function mixPlanAudio(c, compiledDir) {
8741
8857
  audios: c.composition.audios,
8742
8858
  masterFxChain: c.composition.masterFxChain,
8743
8859
  abortSignal: c.abortSignal,
8744
- assertNotAborted: c.assertNotAborted
8860
+ assertNotAborted: c.assertNotAborted,
8861
+ log: c.log
8745
8862
  });
8746
8863
  }
8747
8864
  async function extractPlanVideos(c, deferRangeExtraction) {
@@ -8760,6 +8877,7 @@ async function extractPlanVideos(c, deferRangeExtraction) {
8760
8877
  if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
8761
8878
  if (extractResult.extractionResult) {
8762
8879
  assertVideoExtractionSucceeded(extractResult.extractionResult);
8880
+ c.log.info("[plan] extract phases", { ...extractResult.extractionResult.phaseBreakdown });
8763
8881
  }
8764
8882
  return extractResult;
8765
8883
  }
@@ -8805,12 +8923,12 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
8805
8923
  const videoSourcesDir = join21(planDir, "video-sources");
8806
8924
  rmSync9(videoSourcesDir, { recursive: true, force: true });
8807
8925
  if (existsSync15(stagedVideoFrames)) {
8808
- renameSync4(stagedVideoFrames, videoFramesDst);
8926
+ renameWithRetry6(stagedVideoFrames, videoFramesDst);
8809
8927
  } else {
8810
8928
  mkdirSync10(videoFramesDst, { recursive: true });
8811
8929
  }
8812
8930
  if (existsSync15(finalCompiledDir)) rmSync9(finalCompiledDir, { recursive: true, force: true });
8813
- renameSync4(compiledDir, finalCompiledDir);
8931
+ renameWithRetry6(compiledDir, finalCompiledDir);
8814
8932
  const shipVideoSource = createVideoSourceShipper({
8815
8933
  planDir,
8816
8934
  projectDir,
@@ -8913,7 +9031,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
8913
9031
  }
8914
9032
  const planAudioPath = join21(planDir, PLAN_AUDIO_RELATIVE_PATH);
8915
9033
  if (audioResult?.hasAudio && existsSync15(audioResult.audioOutputPath)) {
8916
- renameSync4(audioResult.audioOutputPath, planAudioPath);
9034
+ renameWithRetry6(audioResult.audioOutputPath, planAudioPath);
8917
9035
  }
8918
9036
  if (chunksCarryAudio && existsSync15(planAudioPath)) {
8919
9037
  if (options.sliceChunkAudio) {
@@ -9027,7 +9145,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
9027
9145
  applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
9028
9146
  }
9029
9147
  if (!mixed.hasAudio || !existsSync15(mixed.audioOutputPath)) return { hasAudio: false };
9030
- renameSync4(mixed.audioOutputPath, outputPath);
9148
+ renameWithRetry6(mixed.audioOutputPath, outputPath);
9031
9149
  return { hasAudio: true };
9032
9150
  } finally {
9033
9151
  rmSync9(c.workDir, { recursive: true, force: true });
@@ -9059,7 +9177,6 @@ import {
9059
9177
  mkdtempSync as mkdtempSync5,
9060
9178
  readFileSync as readFileSync10,
9061
9179
  readdirSync as readdirSync7,
9062
- renameSync as renameSync6,
9063
9180
  rmSync as rmSync11,
9064
9181
  statSync as statSync6,
9065
9182
  writeFileSync as writeFileSync8
@@ -9067,7 +9184,7 @@ import {
9067
9184
  import { tmpdir } from "node:os";
9068
9185
  import { dirname as dirname11, isAbsolute as isAbsolute4, join as join24, relative as relative6, resolve as resolve8, sep as sep3 } from "node:path";
9069
9186
  import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
9070
- import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
9187
+ import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
9071
9188
 
9072
9189
  // src/services/distributed/planV2Publisher.ts
9073
9190
  import {
@@ -9076,11 +9193,11 @@ import {
9076
9193
  linkSync as linkSync2,
9077
9194
  mkdirSync as mkdirSync11,
9078
9195
  mkdtempSync as mkdtempSync4,
9079
- renameSync as renameSync5,
9080
9196
  rmSync as rmSync10,
9081
9197
  statSync as statSync5,
9082
9198
  writeFileSync as writeFileSync7
9083
9199
  } from "node:fs";
9200
+ import { renameWithRetry as renameWithRetry7 } from "@hyperframes/engine";
9084
9201
  import { dirname as dirname10, join as join23 } from "node:path";
9085
9202
 
9086
9203
  // src/services/distributed/planV2Errors.ts
@@ -9169,7 +9286,7 @@ var LocalPlanV2ArtifactPublisher = class {
9169
9286
  if (!canFallbackToCopy(error)) throw error;
9170
9287
  copyFileSync6(blob.sourcePath, temporaryPath);
9171
9288
  }
9172
- renameSync5(temporaryPath, destinationPath);
9289
+ renameWithRetry7(temporaryPath, destinationPath);
9173
9290
  } finally {
9174
9291
  rmSync10(stagingDir, { recursive: true, force: true });
9175
9292
  }
@@ -9183,7 +9300,7 @@ var LocalPlanV2ArtifactPublisher = class {
9183
9300
  }
9184
9301
  }
9185
9302
  writeFileSync7(join23(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
9186
- renameSync5(this.temporaryDir, this.destinationDir);
9303
+ renameWithRetry7(this.temporaryDir, this.destinationDir);
9187
9304
  this.#committed = true;
9188
9305
  }
9189
9306
  async abort() {
@@ -9450,7 +9567,7 @@ function writeBlob(sourcePath, destinationPath) {
9450
9567
  const temporaryPath = join24(temporaryDir, "blob");
9451
9568
  try {
9452
9569
  copyFileSync7(sourcePath, temporaryPath);
9453
- renameSync6(temporaryPath, destinationPath);
9570
+ renameWithRetry8(temporaryPath, destinationPath);
9454
9571
  } finally {
9455
9572
  rmSync11(temporaryDir, { recursive: true, force: true });
9456
9573
  }
@@ -9582,7 +9699,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
9582
9699
  canonicalJsonStringify(publication.manifest),
9583
9700
  "utf-8"
9584
9701
  );
9585
- renameSync6(tempDir, planV2Dir);
9702
+ renameWithRetry8(tempDir, planV2Dir);
9586
9703
  return resultFromManifest(planV2Dir, publication.manifest);
9587
9704
  } catch (error) {
9588
9705
  rmSync11(tempDir, { recursive: true, force: true });
@@ -9867,7 +9984,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
9867
9984
  }),
9868
9985
  "utf-8"
9869
9986
  );
9870
- renameSync6(tempDir, destinationDir);
9987
+ renameWithRetry8(tempDir, destinationDir);
9871
9988
  } catch (error) {
9872
9989
  rmSync11(tempDir, { recursive: true, force: true });
9873
9990
  throw error;
@@ -9945,7 +10062,6 @@ import {
9945
10062
  mkdtempSync as mkdtempSync6,
9946
10063
  readFileSync as readFileSync11,
9947
10064
  readdirSync as readdirSync8,
9948
- renameSync as renameSync7,
9949
10065
  rmSync as rmSync12,
9950
10066
  statSync as statSync7,
9951
10067
  writeFileSync as writeFileSync9
@@ -9959,7 +10075,8 @@ import {
9959
10075
  muxVideoWithAudio as muxVideoWithAudio2,
9960
10076
  resolveConfig as resolveConfig4,
9961
10077
  runFfmpeg as runFfmpeg7,
9962
- appendSoftwareEncoderThreadsArg
10078
+ appendSoftwareEncoderThreadsArg,
10079
+ renameWithRetry as renameWithRetry9
9963
10080
  } from "@hyperframes/engine";
9964
10081
  import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
9965
10082
  function cfrRateControlArgs(locked) {
@@ -10016,7 +10133,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
10016
10133
  const finalPath = join25(workDir, `output.${format}`);
10017
10134
  const audio = audioPath !== null && existsSync18(audioPath) ? audioPath : null;
10018
10135
  await writeJoined(ctx, videoSources, audio, finalPath);
10019
- renameSync7(finalPath, outputPath);
10136
+ renameWithRetry9(finalPath, outputPath);
10020
10137
  log.info("[assemble] stage timings", {
10021
10138
  ...stageMs,
10022
10139
  chunks: chunkPaths.length,
@@ -10268,6 +10385,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
10268
10385
  // src/services/distributed/renderChunk.ts
10269
10386
  import { randomBytes } from "node:crypto";
10270
10387
  import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync13, writeFileSync as writeFileSync10 } from "node:fs";
10388
+ import { cpus } from "node:os";
10271
10389
  import { join as join26 } from "node:path";
10272
10390
  import {
10273
10391
  assertSwiftShader,
@@ -10572,6 +10690,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10572
10690
  let forceScreenshotForChunk = encoder.forceScreenshot;
10573
10691
  let shownBoxes;
10574
10692
  let extraction;
10693
+ const overlap = { current: null };
10575
10694
  try {
10576
10695
  if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
10577
10696
  const bootStarted = Date.now();
@@ -10659,6 +10778,59 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10659
10778
  videoFrameLookup,
10660
10779
  frameCacheOptions(cfg, chunkWorkerCount)
10661
10780
  );
10781
+ const isPngSequence = plan2.dimensions.format === "png-sequence";
10782
+ outputKind = isPngSequence ? "frame-dir" : "file";
10783
+ const presetFormat = isPngSequence ? "mp4" : plan2.dimensions.format;
10784
+ const basePreset = getEncoderPreset6(job.config.quality, presetFormat, void 0);
10785
+ const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
10786
+ const effectiveQuality = encoder.crf ?? preset.quality;
10787
+ const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
10788
+ const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
10789
+ if (isPngSequence) {
10790
+ if (!existsSync19(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
10791
+ } else {
10792
+ const outDir = join26(outputChunkPath, "..");
10793
+ if (!existsSync19(outDir)) mkdirSync14(outDir, { recursive: true });
10794
+ }
10795
+ const encodeInput = {
10796
+ job,
10797
+ log,
10798
+ outputPath: videoOnlyPath,
10799
+ framesDir,
10800
+ videoOnlyPath,
10801
+ width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
10802
+ height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
10803
+ needsAlpha: plan2.dimensions.format !== "mp4",
10804
+ captureImageFormat: captureOptions.format ?? "jpeg",
10805
+ // The encoder writes video only; audio joins at assemble time or in the
10806
+ // chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
10807
+ // sidecar AND the mp4 audio mux.
10808
+ hasAudio: false,
10809
+ isPngSequence,
10810
+ // `DistributedFormat` has no "gif" member — distributed chunks are
10811
+ // always video segments (gif renders in-process only).
10812
+ isGif: false,
10813
+ preset,
10814
+ effectiveQuality,
10815
+ effectiveBitrate,
10816
+ engineConfig: {
10817
+ ffmpegEncodeTimeout: cfg.ffmpegEncodeTimeout,
10818
+ vp9CpuUsed: resolveLockedVp9CpuUsed(encoder) ?? cfg.vp9CpuUsed
10819
+ },
10820
+ // Distributed chunks emit a single ffmpeg call per chunk; the
10821
+ // in-process per-chunk-within-chunk path would re-split our
10822
+ // already-chunked work.
10823
+ enableChunkedEncode: false,
10824
+ chunkedEncodeSize: framesInChunk,
10825
+ abortSignal: void 0,
10826
+ assertNotAborted: () => {
10827
+ },
10828
+ // GOP === framesInChunk + force-keyframe at frame 0 → the chunk's
10829
+ // first frame is an IDR keyframe and concat-copy at assemble time
10830
+ // round-trips losslessly.
10831
+ lockGopForChunkConcat: !isPngSequence,
10832
+ gopSize: framesInChunk
10833
+ };
10662
10834
  const captureStarted = Date.now();
10663
10835
  captureMode = await runCaptureWithScreenshotFallback({
10664
10836
  forceScreenshot: forceScreenshotForChunk,
@@ -10691,6 +10863,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10691
10863
  }
10692
10864
  const captureSession = session;
10693
10865
  session = null;
10866
+ overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
10694
10867
  await runCaptureStage({
10695
10868
  fileServer,
10696
10869
  workDir,
@@ -10710,7 +10883,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10710
10883
  abortSignal: void 0,
10711
10884
  assertNotAborted: () => {
10712
10885
  },
10713
- frameRange: range
10886
+ frameRange: range,
10887
+ onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
10888
+ }).catch(async (captureError) => {
10889
+ await overlap.current?.abort();
10890
+ overlap.current = null;
10891
+ throw captureError;
10714
10892
  });
10715
10893
  return observedChunkCaptureMode(capturePerfs);
10716
10894
  },
@@ -10742,60 +10920,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10742
10920
  log
10743
10921
  );
10744
10922
  framesEncoded = framesInChunk;
10745
- const isPngSequence = plan2.dimensions.format === "png-sequence";
10746
- outputKind = isPngSequence ? "frame-dir" : "file";
10747
- const presetFormat = isPngSequence ? "mp4" : plan2.dimensions.format;
10748
- const basePreset = getEncoderPreset6(job.config.quality, presetFormat, void 0);
10749
- const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
10750
- const effectiveQuality = encoder.crf ?? preset.quality;
10751
- const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
10752
- const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
10753
- if (isPngSequence) {
10754
- if (!existsSync19(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
10755
- } else {
10756
- const outDir = join26(outputChunkPath, "..");
10757
- if (!existsSync19(outDir)) mkdirSync14(outDir, { recursive: true });
10758
- }
10759
10923
  const encodeStarted = Date.now();
10760
- await runEncodeStage({
10761
- job,
10762
- log,
10763
- outputPath: videoOnlyPath,
10764
- framesDir,
10765
- videoOnlyPath,
10766
- width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
10767
- height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
10768
- needsAlpha: plan2.dimensions.format !== "mp4",
10769
- captureImageFormat: captureOptions.format ?? "jpeg",
10770
- // The encoder writes video only; audio joins at assemble time or in the
10771
- // chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
10772
- // sidecar AND the mp4 audio mux.
10773
- hasAudio: false,
10774
- isPngSequence,
10775
- // `DistributedFormat` has no "gif" member — distributed chunks are
10776
- // always video segments (gif renders in-process only).
10777
- isGif: false,
10778
- preset,
10779
- effectiveQuality,
10780
- effectiveBitrate,
10781
- engineConfig: {
10782
- ffmpegEncodeTimeout: cfg.ffmpegEncodeTimeout,
10783
- vp9CpuUsed: resolveLockedVp9CpuUsed(encoder) ?? cfg.vp9CpuUsed
10784
- },
10785
- // Distributed chunks emit a single ffmpeg call per chunk; the
10786
- // in-process per-chunk-within-chunk path would re-split our
10787
- // already-chunked work.
10788
- enableChunkedEncode: false,
10789
- chunkedEncodeSize: framesInChunk,
10790
- abortSignal: void 0,
10791
- assertNotAborted: () => {
10792
- },
10793
- // GOP === framesInChunk + force-keyframe at frame 0 → the chunk's
10794
- // first frame is an IDR keyframe and concat-copy at assemble time
10795
- // round-trips losslessly.
10796
- lockGopForChunkConcat: !isPngSequence,
10797
- gopSize: framesInChunk
10798
- });
10924
+ const overlapped = overlap.current;
10925
+ overlap.current = null;
10926
+ await (overlapped ? overlapped.finish() : runEncodeStage(encodeInput));
10799
10927
  if (encoder.chunksCarryAudio) {
10800
10928
  const audioScratchPath = `${outputChunkPath}.audio.m4a`;
10801
10929
  const audioChunks = options?.frameRange ? chunksWithSubRange(chunks, chunkIndex, range) : { chunks, index: chunkIndex };
@@ -10813,6 +10941,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10813
10941
  }
10814
10942
  encodeStageMs = Date.now() - encodeStarted;
10815
10943
  } finally {
10944
+ await overlap.current?.abort().catch(() => {
10945
+ });
10816
10946
  await extraction?.catch(() => {
10817
10947
  });
10818
10948
  if (session) {
@@ -11242,6 +11372,24 @@ async function buildAudioSidecar(inputPath, outputPath, signal) {
11242
11372
  }
11243
11373
  var isMissing = (result) => result.error?.code === "ENOENT";
11244
11374
  var ffmpegIsMissing = async (signal) => isMissing(await runFfmpeg8(["-version"], { signal }));
11375
+
11376
+ // src/services/distributed/probeFacts.ts
11377
+ import { writeFileSync as writeFileSync11 } from "node:fs";
11378
+ import { extname as extname6 } from "node:path";
11379
+ import { buildProbeFactsText } from "@hyperframes/engine";
11380
+ var PROBE_FACTS_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v", ".webm", ".mkv"]);
11381
+ function wantsProbeFacts(path) {
11382
+ return PROBE_FACTS_EXTENSIONS.has(extname6(path).toLowerCase());
11383
+ }
11384
+ async function buildProbeFacts(inputPath, outputPath, signal) {
11385
+ try {
11386
+ writeFileSync11(outputPath, await buildProbeFactsText(inputPath, signal));
11387
+ return "built";
11388
+ } catch (error) {
11389
+ if (signal?.aborted) throw error;
11390
+ return /ffprobe not found/i.test(String(error.message)) ? "unavailable" : "none";
11391
+ }
11392
+ }
11245
11393
  export {
11246
11394
  AUDIO_SIDECAR_MIN_BYTES,
11247
11395
  CHUNK_EXCEEDS_WORKER_TIMEOUT,
@@ -11289,6 +11437,7 @@ export {
11289
11437
  buildAudioSidecar,
11290
11438
  buildChunkSlices,
11291
11439
  buildPlanAudio,
11440
+ buildProbeFacts,
11292
11441
  buildStitchedHeader,
11293
11442
  chunksAreStitchedMp4,
11294
11443
  createPlanV2FromExecutionPlan,
@@ -11323,5 +11472,6 @@ export {
11323
11472
  validateDistributedRenderConfig,
11324
11473
  validatePlanV2MaterializedTarget,
11325
11474
  validateVariablesPayload,
11326
- wantsAudioSidecar
11475
+ wantsAudioSidecar,
11476
+ wantsProbeFacts
11327
11477
  };