@hyperframes/producer 0.8.191 → 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";
@@ -18,7 +17,10 @@ import {
18
17
  import {
19
18
  getEncoderPreset as getEncoderPreset4,
20
19
  normalizeVp9CpuUsed,
21
- resolveConfig as resolveConfig3
20
+ resolveConfig as resolveConfig3,
21
+ declareContentAddressedUrl,
22
+ renameWithRetry as renameWithRetry6,
23
+ declareProbeFactsUrl
22
24
  } from "@hyperframes/engine";
23
25
 
24
26
  // src/logger.ts
@@ -1149,7 +1151,7 @@ function encoderFailureError(prefix, result) {
1149
1151
  }
1150
1152
 
1151
1153
  // src/services/render/artifactTransaction.ts
1152
- import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST } from "@hyperframes/engine";
1154
+ import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, renameWithRetry } from "@hyperframes/engine";
1153
1155
 
1154
1156
  // src/utils/ffprobe.ts
1155
1157
  import {
@@ -1226,6 +1228,24 @@ import {
1226
1228
 
1227
1229
  // src/services/render/captureStageError.ts
1228
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
1229
1249
  var CaptureStageError = class extends CaptureFailure {
1230
1250
  browserConsole;
1231
1251
  constructor(input) {
@@ -1234,10 +1254,13 @@ var CaptureStageError = class extends CaptureFailure {
1234
1254
  kind: classified.kind,
1235
1255
  message: normalizeErrorMessage(input.cause),
1236
1256
  cause: input.cause,
1237
- workerDiagnostics: classified.workerDiagnostics
1257
+ workerDiagnostics: classified.workerDiagnostics,
1258
+ pageErrorLines: classified.pageErrorLines
1238
1259
  });
1239
1260
  this.name = "CaptureStageError";
1240
- this.browserConsole = input.browserConsole.slice();
1261
+ this.browserConsole = withWorkerPageErrors(input.browserConsole, [
1262
+ ...classified.pageErrorLines
1263
+ ]);
1241
1264
  if (input.cause instanceof Error && input.cause.stack) {
1242
1265
  this.stack = input.cause.stack;
1243
1266
  }
@@ -1297,7 +1320,7 @@ import {
1297
1320
  resolveVideoExtractionWindow,
1298
1321
  runFfmpeg
1299
1322
  } from "@hyperframes/engine";
1300
- import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1323
+ import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
1301
1324
  var NO_FOLLOW_FLAG = constants2.O_NOFOLLOW ?? 0;
1302
1325
  var BYTES_PER_MIB = 1024 * 1024;
1303
1326
 
@@ -1323,13 +1346,8 @@ import {
1323
1346
  spawnStreamingEncoder
1324
1347
  } from "@hyperframes/engine";
1325
1348
 
1326
- // src/services/render/perfSummary.ts
1327
- import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
1328
- function pushWorkerDedupPerfs(results, sink) {
1329
- for (const r of results) {
1330
- if (r.perf) sink.push(r.perf);
1331
- }
1332
- }
1349
+ // src/services/render/segmentManifest.ts
1350
+ import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
1333
1351
 
1334
1352
  // src/services/render/hdrMode.ts
1335
1353
  import {
@@ -1683,19 +1701,11 @@ import {
1683
1701
 
1684
1702
  // src/services/animatedGifPrep.ts
1685
1703
  import { createHash as createHash2 } from "node:crypto";
1686
- import {
1687
- copyFileSync as copyFileSync2,
1688
- existsSync as existsSync4,
1689
- mkdirSync as mkdirSync3,
1690
- readFileSync as readFileSync3,
1691
- renameSync,
1692
- rmSync as rmSync3,
1693
- statSync as statSync2
1694
- } 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";
1695
1705
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve4 } from "node:path";
1696
1706
  import { parseHTML as parseHTML3 } from "linkedom";
1697
1707
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
1698
- 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";
1699
1709
  var PREPARED_GIF_SUBDIR = "_animated_gif";
1700
1710
  var CACHE_SCHEMA = "hfgif-v1";
1701
1711
  function splitUrlSuffix(src) {
@@ -1874,7 +1884,7 @@ async function ensurePreparedWebm(input) {
1874
1884
  throw new Error("Animated GIF transcode produced an empty output");
1875
1885
  }
1876
1886
  if (!isUsableFile(input.cachePath)) {
1877
- renameSync(tmpPath, input.cachePath);
1887
+ renameWithRetry3(tmpPath, input.cachePath);
1878
1888
  } else {
1879
1889
  rmSync3(tmpPath, { force: true });
1880
1890
  }
@@ -4281,6 +4291,10 @@ async function runProbeStage(input) {
4281
4291
  if (el.loop && !existing.loop) {
4282
4292
  existing.loop = true;
4283
4293
  }
4294
+ const audio = composition.audios.find(
4295
+ (audio2) => audio2.type === "video" && audio2.id === `${el.id}-audio`
4296
+ );
4297
+ if (audio) audio.loop = existing.loop;
4284
4298
  }
4285
4299
  } else if (!compiled.droppedMediaIds.has(el.id)) {
4286
4300
  composition.videos.push({
@@ -4318,6 +4332,7 @@ async function runProbeStage(input) {
4318
4332
  el,
4319
4333
  authoredStarts.get(el.id) ?? existing.mediaStart
4320
4334
  );
4335
+ existing.loop = el.loop;
4321
4336
  if (el.volume > 0 && Math.abs((existing.volume ?? 1) - el.volume) > BROWSER_MEDIA_EPSILON) {
4322
4337
  existing.volume = el.volume;
4323
4338
  }
@@ -4329,6 +4344,7 @@ async function runProbeStage(input) {
4329
4344
  start: el.start,
4330
4345
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
4331
4346
  mediaStart: el.mediaStart,
4347
+ loop: el.loop,
4332
4348
  layer: 0,
4333
4349
  volume: el.volume,
4334
4350
  type: "audio"
@@ -4583,6 +4599,10 @@ import {
4583
4599
  extractMediaMetadata as extractMediaMetadata3,
4584
4600
  isVideoPastTimelineEnd,
4585
4601
  isHdrColorSpace as isHdrColorSpace2,
4602
+ rebaseVideoToWindow,
4603
+ resolveFinalFrameExtractionWindow as resolveFinalFrameExtractionWindow2,
4604
+ resolveVideoExtractionWindow as resolveVideoExtractionWindow2,
4605
+ isHttpUrl as isHttpUrl2,
4586
4606
  resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
4587
4607
  runVideoExtractionWithRetry,
4588
4608
  safeVideoExtractionSourceIdentity
@@ -4948,6 +4968,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
4948
4968
  start: video.start,
4949
4969
  end: video.end,
4950
4970
  mediaStart: video.mediaStart,
4971
+ playbackRate: video.playbackRate,
4972
+ loop: video.loop,
4951
4973
  layer: 0,
4952
4974
  volume: 1,
4953
4975
  type: "video"
@@ -4955,6 +4977,33 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
4955
4977
  existingAudioSrcs.add(video.src);
4956
4978
  }
4957
4979
  }
4980
+ async function appendVideoAudioFromProbe(input) {
4981
+ const { projectDir, compiledDir, composition, urlByAbsentLocalPath } = input;
4982
+ const videos = composition.videos.filter(
4983
+ (video) => !isVideoPastTimelineEnd(video, composition.duration)
4984
+ );
4985
+ const probed = await Promise.all(
4986
+ videos.map(async (video) => {
4987
+ const path = isHttpUrl2(video.src) ? video.src : resolveProjectRelativeSrc3(video.src, projectDir, compiledDir);
4988
+ const readFrom = urlByAbsentLocalPath?.get(path) ?? path;
4989
+ if (!isHttpUrl2(readFrom) && !existsSync7(readFrom)) return null;
4990
+ const metadata = await extractMediaMetadata3(readFrom);
4991
+ const window2 = await resolveFinalFrameExtractionWindow2(
4992
+ readFrom,
4993
+ video,
4994
+ metadata,
4995
+ resolveVideoExtractionWindow2(video, metadata, composition.duration)
4996
+ );
4997
+ if (window2.durationSeconds <= 0) return null;
4998
+ rebaseVideoToWindow(video, window2);
4999
+ return { videoId: video.id, metadata };
5000
+ })
5001
+ );
5002
+ appendAutoDetectedVideoAudio(
5003
+ composition,
5004
+ probed.filter((entry) => entry !== null)
5005
+ );
5006
+ }
4958
5007
 
4959
5008
  // src/services/render/stages/audioStage.ts
4960
5009
  import { join as join10 } from "node:path";
@@ -4967,7 +5016,7 @@ import {
4967
5016
 
4968
5017
  // src/services/render/audioPadTrim.ts
4969
5018
  import { spawn } from "node:child_process";
4970
- import { mkdtempSync as mkdtempSync2, renameSync as renameSync2, rmSync as rmSync4 } from "node:fs";
5019
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync4 } from "node:fs";
4971
5020
  import { dirname as dirname5, join as join9 } from "node:path";
4972
5021
  import {
4973
5022
  DEFAULT_CONFIG,
@@ -4976,6 +5025,7 @@ import {
4976
5025
  extractAudioMetadata as extractAudioMetadata3,
4977
5026
  getFfprobeBinary,
4978
5027
  ManagedChildProcess,
5028
+ renameWithRetry as renameWithRetry4,
4979
5029
  runFfmpeg as runFfmpeg4,
4980
5030
  trackChildProcess
4981
5031
  } from "@hyperframes/engine";
@@ -5231,7 +5281,7 @@ async function enforceAacTruePeak(input) {
5231
5281
  }
5232
5282
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
5233
5283
  if (measuredPath !== correctedPath) return { success: true };
5234
- renameSync2(correctedPath, input.audioPath);
5284
+ renameWithRetry4(correctedPath, input.audioPath);
5235
5285
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
5236
5286
  }
5237
5287
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -5421,6 +5471,7 @@ async function runAudioStage(input) {
5421
5471
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
5422
5472
  let audioLoweredDb;
5423
5473
  if (result.hasAudio) {
5474
+ const guardStart = Date.now();
5424
5475
  const peak = await constrainAacTruePeak({
5425
5476
  audioPath: result.audioOutputPath,
5426
5477
  sourcePath: pcmSourcePath,
@@ -5430,6 +5481,7 @@ async function runAudioStage(input) {
5430
5481
  });
5431
5482
  if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
5432
5483
  audioLoweredDb = peak.loweredDb;
5484
+ input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
5433
5485
  }
5434
5486
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
5435
5487
  } finally {
@@ -5455,6 +5507,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5455
5507
  let hasAudio = false;
5456
5508
  let audioError;
5457
5509
  let audioFailures;
5510
+ let audioPhaseMs;
5458
5511
  if (audios.length > 0) {
5459
5512
  let audioResult;
5460
5513
  try {
@@ -5504,6 +5557,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5504
5557
  assertAudioNotInterrupted(audioResult.failures);
5505
5558
  hasAudio = audioResult.success;
5506
5559
  audioFailures = audioResult.failures;
5560
+ audioPhaseMs = audioResult.phaseMs;
5507
5561
  if (!hasAudio) {
5508
5562
  audioError = audioResult.error ?? "audio mix failed for an unknown reason";
5509
5563
  for (const failure of audioResult.failures ?? []) {
@@ -5522,7 +5576,8 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5522
5576
  audioOutputPath,
5523
5577
  hasAudio,
5524
5578
  audioError,
5525
- audioFailures
5579
+ audioFailures,
5580
+ audioPhaseMs
5526
5581
  };
5527
5582
  }
5528
5583
 
@@ -5632,6 +5687,7 @@ async function runCaptureStage(input) {
5632
5687
  let { workerCount } = plan2;
5633
5688
  const { forceScreenshot } = plan2;
5634
5689
  let lastBrowserConsole = [];
5690
+ const workerPageErrors = [];
5635
5691
  let captureBeyondViewport = probeSession?.options.captureBeyondViewport;
5636
5692
  const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
5637
5693
  if (frameRange !== void 0) {
@@ -5669,6 +5725,7 @@ async function runCaptureStage(input) {
5669
5725
  abortSignal,
5670
5726
  frameRangeStart: frameRange?.startFrame,
5671
5727
  dedupPerfs,
5728
+ pageErrorLines: workerPageErrors,
5672
5729
  onProgress: (progress) => {
5673
5730
  if (progress.latestWorkerPhase) {
5674
5731
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -5689,6 +5746,11 @@ async function runCaptureStage(input) {
5689
5746
  },
5690
5747
  cfg: captureCfg,
5691
5748
  log
5749
+ }).catch((error) => {
5750
+ throw wrapCaptureStageError(
5751
+ error,
5752
+ withWorkerPageErrors(probeSession?.browserConsoleBuffer ?? [], workerPageErrors)
5753
+ );
5692
5754
  });
5693
5755
  captureAttempts.push(...attempts);
5694
5756
  const lastAttempt = attempts[attempts.length - 1];
@@ -5701,6 +5763,7 @@ async function runCaptureStage(input) {
5701
5763
  await closeCaptureSession5(probeSession);
5702
5764
  probeSession = null;
5703
5765
  }
5766
+ lastBrowserConsole = withWorkerPageErrors(lastBrowserConsole, workerPageErrors);
5704
5767
  } else {
5705
5768
  const sequential = await runSequentialDiskCapture(
5706
5769
  input,
@@ -5908,6 +5971,7 @@ import {
5908
5971
  statSync as statSync3,
5909
5972
  writeFileSync as writeFileSync2
5910
5973
  } from "node:fs";
5974
+ import { readFile as readFile2 } from "node:fs/promises";
5911
5975
  import { dirname as dirname6, join as join12 } from "node:path";
5912
5976
  import {
5913
5977
  encodeFramesChunkedConcat,
@@ -6077,6 +6141,100 @@ function videoEncoderOptions(input, engineCfg) {
6077
6141
  gopSize: input.gopSize
6078
6142
  };
6079
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
+ }
6080
6238
  async function runEncodeStage(input) {
6081
6239
  const {
6082
6240
  job,
@@ -6390,6 +6548,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
6390
6548
  options.cfg
6391
6549
  );
6392
6550
  pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
6551
+ if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
6393
6552
  } finally {
6394
6553
  await mergeWorkerFrames(attemptWorkDir, tasks, options.framesDir);
6395
6554
  }
@@ -7468,15 +7627,9 @@ import { join as join19 } from "node:path";
7468
7627
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
7469
7628
 
7470
7629
  // src/services/distributed/mp4Stitch.ts
7471
- import {
7472
- createReadStream as createReadStream4,
7473
- createWriteStream,
7474
- mkdirSync as mkdirSync8,
7475
- mkdtempSync as mkdtempSync3,
7476
- renameSync as renameSync3,
7477
- rmSync as rmSync8
7478
- } from "node:fs";
7630
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
7479
7631
  import { open } from "node:fs/promises";
7632
+ import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
7480
7633
  import { dirname as dirname9, join as join18 } from "node:path";
7481
7634
  import { finished, pipeline } from "node:stream/promises";
7482
7635
  function boxAt(src, off, at, limit) {
@@ -7849,7 +8002,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
7849
8002
  for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
7850
8003
  out.end();
7851
8004
  await finished(out);
7852
- renameSync3(join18(workDir, "out.mp4"), outputPath);
8005
+ renameWithRetry5(join18(workDir, "out.mp4"), outputPath);
7853
8006
  } finally {
7854
8007
  out.destroy();
7855
8008
  rmSync8(workDir, { recursive: true, force: true });
@@ -8579,6 +8732,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
8579
8732
  const needsAlpha = outputNeedsAlpha(config.format);
8580
8733
  const remoteFiles = Object.entries(options.remoteFiles ?? {});
8581
8734
  const remoteLocalPath = (rel) => join21(projectDir, ...rel.split("/"));
8735
+ for (const [, file] of remoteFiles) {
8736
+ declareContentAddressedUrl(file.url, file.sha256);
8737
+ if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
8738
+ }
8582
8739
  const urlByAbsentLocalPath = new Map(
8583
8740
  remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
8584
8741
  );
@@ -8700,7 +8857,8 @@ function mixPlanAudio(c, compiledDir) {
8700
8857
  audios: c.composition.audios,
8701
8858
  masterFxChain: c.composition.masterFxChain,
8702
8859
  abortSignal: c.abortSignal,
8703
- assertNotAborted: c.assertNotAborted
8860
+ assertNotAborted: c.assertNotAborted,
8861
+ log: c.log
8704
8862
  });
8705
8863
  }
8706
8864
  async function extractPlanVideos(c, deferRangeExtraction) {
@@ -8719,6 +8877,7 @@ async function extractPlanVideos(c, deferRangeExtraction) {
8719
8877
  if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
8720
8878
  if (extractResult.extractionResult) {
8721
8879
  assertVideoExtractionSucceeded(extractResult.extractionResult);
8880
+ c.log.info("[plan] extract phases", { ...extractResult.extractionResult.phaseBreakdown });
8722
8881
  }
8723
8882
  return extractResult;
8724
8883
  }
@@ -8764,12 +8923,12 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
8764
8923
  const videoSourcesDir = join21(planDir, "video-sources");
8765
8924
  rmSync9(videoSourcesDir, { recursive: true, force: true });
8766
8925
  if (existsSync15(stagedVideoFrames)) {
8767
- renameSync4(stagedVideoFrames, videoFramesDst);
8926
+ renameWithRetry6(stagedVideoFrames, videoFramesDst);
8768
8927
  } else {
8769
8928
  mkdirSync10(videoFramesDst, { recursive: true });
8770
8929
  }
8771
8930
  if (existsSync15(finalCompiledDir)) rmSync9(finalCompiledDir, { recursive: true, force: true });
8772
- renameSync4(compiledDir, finalCompiledDir);
8931
+ renameWithRetry6(compiledDir, finalCompiledDir);
8773
8932
  const shipVideoSource = createVideoSourceShipper({
8774
8933
  planDir,
8775
8934
  projectDir,
@@ -8872,7 +9031,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
8872
9031
  }
8873
9032
  const planAudioPath = join21(planDir, PLAN_AUDIO_RELATIVE_PATH);
8874
9033
  if (audioResult?.hasAudio && existsSync15(audioResult.audioOutputPath)) {
8875
- renameSync4(audioResult.audioOutputPath, planAudioPath);
9034
+ renameWithRetry6(audioResult.audioOutputPath, planAudioPath);
8876
9035
  }
8877
9036
  if (chunksCarryAudio && existsSync15(planAudioPath)) {
8878
9037
  if (options.sliceChunkAudio) {
@@ -8977,7 +9136,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
8977
9136
  });
8978
9137
  try {
8979
9138
  let stageStarted = Date.now();
8980
- await extractPlanVideos(c, true);
9139
+ await appendVideoAudioFromProbe(c);
8981
9140
  logPlanStage(c.log, "extract", stageStarted);
8982
9141
  stageStarted = Date.now();
8983
9142
  const mixed = await mixPlanAudio(c, c.compiledDir);
@@ -8986,7 +9145,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
8986
9145
  applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
8987
9146
  }
8988
9147
  if (!mixed.hasAudio || !existsSync15(mixed.audioOutputPath)) return { hasAudio: false };
8989
- renameSync4(mixed.audioOutputPath, outputPath);
9148
+ renameWithRetry6(mixed.audioOutputPath, outputPath);
8990
9149
  return { hasAudio: true };
8991
9150
  } finally {
8992
9151
  rmSync9(c.workDir, { recursive: true, force: true });
@@ -9018,7 +9177,6 @@ import {
9018
9177
  mkdtempSync as mkdtempSync5,
9019
9178
  readFileSync as readFileSync10,
9020
9179
  readdirSync as readdirSync7,
9021
- renameSync as renameSync6,
9022
9180
  rmSync as rmSync11,
9023
9181
  statSync as statSync6,
9024
9182
  writeFileSync as writeFileSync8
@@ -9026,7 +9184,7 @@ import {
9026
9184
  import { tmpdir } from "node:os";
9027
9185
  import { dirname as dirname11, isAbsolute as isAbsolute4, join as join24, relative as relative6, resolve as resolve8, sep as sep3 } from "node:path";
9028
9186
  import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
9029
- import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
9187
+ import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
9030
9188
 
9031
9189
  // src/services/distributed/planV2Publisher.ts
9032
9190
  import {
@@ -9035,11 +9193,11 @@ import {
9035
9193
  linkSync as linkSync2,
9036
9194
  mkdirSync as mkdirSync11,
9037
9195
  mkdtempSync as mkdtempSync4,
9038
- renameSync as renameSync5,
9039
9196
  rmSync as rmSync10,
9040
9197
  statSync as statSync5,
9041
9198
  writeFileSync as writeFileSync7
9042
9199
  } from "node:fs";
9200
+ import { renameWithRetry as renameWithRetry7 } from "@hyperframes/engine";
9043
9201
  import { dirname as dirname10, join as join23 } from "node:path";
9044
9202
 
9045
9203
  // src/services/distributed/planV2Errors.ts
@@ -9128,7 +9286,7 @@ var LocalPlanV2ArtifactPublisher = class {
9128
9286
  if (!canFallbackToCopy(error)) throw error;
9129
9287
  copyFileSync6(blob.sourcePath, temporaryPath);
9130
9288
  }
9131
- renameSync5(temporaryPath, destinationPath);
9289
+ renameWithRetry7(temporaryPath, destinationPath);
9132
9290
  } finally {
9133
9291
  rmSync10(stagingDir, { recursive: true, force: true });
9134
9292
  }
@@ -9142,7 +9300,7 @@ var LocalPlanV2ArtifactPublisher = class {
9142
9300
  }
9143
9301
  }
9144
9302
  writeFileSync7(join23(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
9145
- renameSync5(this.temporaryDir, this.destinationDir);
9303
+ renameWithRetry7(this.temporaryDir, this.destinationDir);
9146
9304
  this.#committed = true;
9147
9305
  }
9148
9306
  async abort() {
@@ -9409,7 +9567,7 @@ function writeBlob(sourcePath, destinationPath) {
9409
9567
  const temporaryPath = join24(temporaryDir, "blob");
9410
9568
  try {
9411
9569
  copyFileSync7(sourcePath, temporaryPath);
9412
- renameSync6(temporaryPath, destinationPath);
9570
+ renameWithRetry8(temporaryPath, destinationPath);
9413
9571
  } finally {
9414
9572
  rmSync11(temporaryDir, { recursive: true, force: true });
9415
9573
  }
@@ -9541,7 +9699,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
9541
9699
  canonicalJsonStringify(publication.manifest),
9542
9700
  "utf-8"
9543
9701
  );
9544
- renameSync6(tempDir, planV2Dir);
9702
+ renameWithRetry8(tempDir, planV2Dir);
9545
9703
  return resultFromManifest(planV2Dir, publication.manifest);
9546
9704
  } catch (error) {
9547
9705
  rmSync11(tempDir, { recursive: true, force: true });
@@ -9826,7 +9984,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
9826
9984
  }),
9827
9985
  "utf-8"
9828
9986
  );
9829
- renameSync6(tempDir, destinationDir);
9987
+ renameWithRetry8(tempDir, destinationDir);
9830
9988
  } catch (error) {
9831
9989
  rmSync11(tempDir, { recursive: true, force: true });
9832
9990
  throw error;
@@ -9904,7 +10062,6 @@ import {
9904
10062
  mkdtempSync as mkdtempSync6,
9905
10063
  readFileSync as readFileSync11,
9906
10064
  readdirSync as readdirSync8,
9907
- renameSync as renameSync7,
9908
10065
  rmSync as rmSync12,
9909
10066
  statSync as statSync7,
9910
10067
  writeFileSync as writeFileSync9
@@ -9918,7 +10075,8 @@ import {
9918
10075
  muxVideoWithAudio as muxVideoWithAudio2,
9919
10076
  resolveConfig as resolveConfig4,
9920
10077
  runFfmpeg as runFfmpeg7,
9921
- appendSoftwareEncoderThreadsArg
10078
+ appendSoftwareEncoderThreadsArg,
10079
+ renameWithRetry as renameWithRetry9
9922
10080
  } from "@hyperframes/engine";
9923
10081
  import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
9924
10082
  function cfrRateControlArgs(locked) {
@@ -9975,7 +10133,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
9975
10133
  const finalPath = join25(workDir, `output.${format}`);
9976
10134
  const audio = audioPath !== null && existsSync18(audioPath) ? audioPath : null;
9977
10135
  await writeJoined(ctx, videoSources, audio, finalPath);
9978
- renameSync7(finalPath, outputPath);
10136
+ renameWithRetry9(finalPath, outputPath);
9979
10137
  log.info("[assemble] stage timings", {
9980
10138
  ...stageMs,
9981
10139
  chunks: chunkPaths.length,
@@ -10045,7 +10203,7 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
10045
10203
  "-r",
10046
10204
  fpsArg
10047
10205
  ];
10048
- appendSoftwareEncoderThreadsArg(args);
10206
+ appendSoftwareEncoderThreadsArg(args, "libx264");
10049
10207
  return args;
10050
10208
  }
10051
10209
  async function reencodeCfr(ctx, chunkPaths, encoder) {
@@ -10227,6 +10385,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
10227
10385
  // src/services/distributed/renderChunk.ts
10228
10386
  import { randomBytes } from "node:crypto";
10229
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";
10230
10389
  import { join as join26 } from "node:path";
10231
10390
  import {
10232
10391
  assertSwiftShader,
@@ -10531,6 +10690,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10531
10690
  let forceScreenshotForChunk = encoder.forceScreenshot;
10532
10691
  let shownBoxes;
10533
10692
  let extraction;
10693
+ const overlap = { current: null };
10534
10694
  try {
10535
10695
  if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
10536
10696
  const bootStarted = Date.now();
@@ -10618,6 +10778,59 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10618
10778
  videoFrameLookup,
10619
10779
  frameCacheOptions(cfg, chunkWorkerCount)
10620
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
+ };
10621
10834
  const captureStarted = Date.now();
10622
10835
  captureMode = await runCaptureWithScreenshotFallback({
10623
10836
  forceScreenshot: forceScreenshotForChunk,
@@ -10650,6 +10863,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10650
10863
  }
10651
10864
  const captureSession = session;
10652
10865
  session = null;
10866
+ overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
10653
10867
  await runCaptureStage({
10654
10868
  fileServer,
10655
10869
  workDir,
@@ -10669,7 +10883,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10669
10883
  abortSignal: void 0,
10670
10884
  assertNotAborted: () => {
10671
10885
  },
10672
- 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;
10673
10892
  });
10674
10893
  return observedChunkCaptureMode(capturePerfs);
10675
10894
  },
@@ -10701,60 +10920,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10701
10920
  log
10702
10921
  );
10703
10922
  framesEncoded = framesInChunk;
10704
- const isPngSequence = plan2.dimensions.format === "png-sequence";
10705
- outputKind = isPngSequence ? "frame-dir" : "file";
10706
- const presetFormat = isPngSequence ? "mp4" : plan2.dimensions.format;
10707
- const basePreset = getEncoderPreset6(job.config.quality, presetFormat, void 0);
10708
- const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
10709
- const effectiveQuality = encoder.crf ?? preset.quality;
10710
- const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
10711
- const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
10712
- if (isPngSequence) {
10713
- if (!existsSync19(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
10714
- } else {
10715
- const outDir = join26(outputChunkPath, "..");
10716
- if (!existsSync19(outDir)) mkdirSync14(outDir, { recursive: true });
10717
- }
10718
10923
  const encodeStarted = Date.now();
10719
- await runEncodeStage({
10720
- job,
10721
- log,
10722
- outputPath: videoOnlyPath,
10723
- framesDir,
10724
- videoOnlyPath,
10725
- width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
10726
- height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
10727
- needsAlpha: plan2.dimensions.format !== "mp4",
10728
- captureImageFormat: captureOptions.format ?? "jpeg",
10729
- // The encoder writes video only; audio joins at assemble time or in the
10730
- // chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
10731
- // sidecar AND the mp4 audio mux.
10732
- hasAudio: false,
10733
- isPngSequence,
10734
- // `DistributedFormat` has no "gif" member — distributed chunks are
10735
- // always video segments (gif renders in-process only).
10736
- isGif: false,
10737
- preset,
10738
- effectiveQuality,
10739
- effectiveBitrate,
10740
- engineConfig: {
10741
- ffmpegEncodeTimeout: cfg.ffmpegEncodeTimeout,
10742
- vp9CpuUsed: resolveLockedVp9CpuUsed(encoder) ?? cfg.vp9CpuUsed
10743
- },
10744
- // Distributed chunks emit a single ffmpeg call per chunk; the
10745
- // in-process per-chunk-within-chunk path would re-split our
10746
- // already-chunked work.
10747
- enableChunkedEncode: false,
10748
- chunkedEncodeSize: framesInChunk,
10749
- abortSignal: void 0,
10750
- assertNotAborted: () => {
10751
- },
10752
- // GOP === framesInChunk + force-keyframe at frame 0 → the chunk's
10753
- // first frame is an IDR keyframe and concat-copy at assemble time
10754
- // round-trips losslessly.
10755
- lockGopForChunkConcat: !isPngSequence,
10756
- gopSize: framesInChunk
10757
- });
10924
+ const overlapped = overlap.current;
10925
+ overlap.current = null;
10926
+ await (overlapped ? overlapped.finish() : runEncodeStage(encodeInput));
10758
10927
  if (encoder.chunksCarryAudio) {
10759
10928
  const audioScratchPath = `${outputChunkPath}.audio.m4a`;
10760
10929
  const audioChunks = options?.frameRange ? chunksWithSubRange(chunks, chunkIndex, range) : { chunks, index: chunkIndex };
@@ -10772,6 +10941,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10772
10941
  }
10773
10942
  encodeStageMs = Date.now() - encodeStarted;
10774
10943
  } finally {
10944
+ await overlap.current?.abort().catch(() => {
10945
+ });
10775
10946
  await extraction?.catch(() => {
10776
10947
  });
10777
10948
  if (session) {
@@ -11201,6 +11372,24 @@ async function buildAudioSidecar(inputPath, outputPath, signal) {
11201
11372
  }
11202
11373
  var isMissing = (result) => result.error?.code === "ENOENT";
11203
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
+ }
11204
11393
  export {
11205
11394
  AUDIO_SIDECAR_MIN_BYTES,
11206
11395
  CHUNK_EXCEEDS_WORKER_TIMEOUT,
@@ -11248,6 +11437,7 @@ export {
11248
11437
  buildAudioSidecar,
11249
11438
  buildChunkSlices,
11250
11439
  buildPlanAudio,
11440
+ buildProbeFacts,
11251
11441
  buildStitchedHeader,
11252
11442
  chunksAreStitchedMp4,
11253
11443
  createPlanV2FromExecutionPlan,
@@ -11282,5 +11472,6 @@ export {
11282
11472
  validateDistributedRenderConfig,
11283
11473
  validatePlanV2MaterializedTarget,
11284
11474
  validateVariablesPayload,
11285
- wantsAudioSidecar
11475
+ wantsAudioSidecar,
11476
+ wantsProbeFacts
11286
11477
  };