@hyperframes/producer 0.8.192 → 0.8.194

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 {
@@ -1480,6 +1497,8 @@ import {
1480
1497
  rewriteInlineStyleAssetUrls
1481
1498
  } from "@hyperframes/core";
1482
1499
  import { MAX_AUDIO_GAIN } from "@hyperframes/core/audio-gain";
1500
+ import { readPlaybackRate } from "@hyperframes/parsers/media-duration";
1501
+ import { resolveRateSpec } from "@hyperframes/core/speed-ramp";
1483
1502
  import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
1484
1503
  import {
1485
1504
  assignBundledRuntimeCompositionIds,
@@ -1684,19 +1703,11 @@ import {
1684
1703
 
1685
1704
  // src/services/animatedGifPrep.ts
1686
1705
  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";
1706
+ import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync2 } from "node:fs";
1696
1707
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve4 } from "node:path";
1697
1708
  import { parseHTML as parseHTML3 } from "linkedom";
1698
1709
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
1699
- import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2 } from "@hyperframes/engine";
1710
+ import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
1700
1711
  var PREPARED_GIF_SUBDIR = "_animated_gif";
1701
1712
  var CACHE_SCHEMA = "hfgif-v1";
1702
1713
  function splitUrlSuffix(src) {
@@ -1875,7 +1886,7 @@ async function ensurePreparedWebm(input) {
1875
1886
  throw new Error("Animated GIF transcode produced an empty output");
1876
1887
  }
1877
1888
  if (!isUsableFile(input.cachePath)) {
1878
- renameSync(tmpPath, input.cachePath);
1889
+ renameWithRetry3(tmpPath, input.cachePath);
1879
1890
  } else {
1880
1891
  rmSync3(tmpPath, { force: true });
1881
1892
  }
@@ -3463,6 +3474,9 @@ async function discoverMediaFromBrowser(page) {
3463
3474
  const intrinsicDuration = isImage ? 0 : htmlEl.duration;
3464
3475
  const playbackStartRaw = htmlEl.getAttribute("data-playback-start");
3465
3476
  const mediaStartRaw = htmlEl.getAttribute("data-media-start");
3477
+ const playbackRateRaw = htmlEl.getAttribute("data-playback-rate");
3478
+ const automationRaw = htmlEl.getAttribute("data-automation");
3479
+ const defaultPlaybackRate = isImage ? 1 : htmlEl.defaultPlaybackRate;
3466
3480
  const loop = htmlEl.hasAttribute("loop");
3467
3481
  const hasAudio = htmlEl.getAttribute("data-has-audio") === "true";
3468
3482
  const volume = parseFloat(htmlEl.getAttribute("data-volume") || "1");
@@ -3478,6 +3492,9 @@ async function discoverMediaFromBrowser(page) {
3478
3492
  durationInferred,
3479
3493
  playbackStartRaw,
3480
3494
  mediaStartRaw,
3495
+ playbackRateRaw,
3496
+ automationRaw,
3497
+ defaultPlaybackRate,
3481
3498
  loop,
3482
3499
  hasAudio,
3483
3500
  volume,
@@ -3487,7 +3504,17 @@ async function discoverMediaFromBrowser(page) {
3487
3504
  return results;
3488
3505
  });
3489
3506
  return elements.map(
3490
- ({ endRaw, durationRaw, intrinsicDuration, playbackStartRaw, mediaStartRaw, ...element }) => ({
3507
+ ({
3508
+ endRaw,
3509
+ durationRaw,
3510
+ intrinsicDuration,
3511
+ playbackStartRaw,
3512
+ mediaStartRaw,
3513
+ playbackRateRaw,
3514
+ automationRaw,
3515
+ defaultPlaybackRate,
3516
+ ...element
3517
+ }) => ({
3491
3518
  ...element,
3492
3519
  end: parseStrictFiniteTimingNumber(endRaw) ?? 0,
3493
3520
  duration: element.durationInferred && Number.isFinite(intrinsicDuration) && intrinsicDuration > 0 ? intrinsicDuration : parseStrictFiniteTimingNumber(durationRaw) ?? 0,
@@ -3497,7 +3524,14 @@ async function discoverMediaFromBrowser(page) {
3497
3524
  if (name === "data-media-start") return mediaStartRaw;
3498
3525
  return null;
3499
3526
  }
3500
- })
3527
+ }),
3528
+ playbackRate: resolveRateSpec(
3529
+ automationRaw,
3530
+ readPlaybackRate(
3531
+ (name) => name === "data-playback-rate" ? playbackRateRaw : null,
3532
+ defaultPlaybackRate
3533
+ )
3534
+ )
3501
3535
  })
3502
3536
  );
3503
3537
  }
@@ -3917,9 +3951,18 @@ import {
3917
3951
  getCompositionDuration,
3918
3952
  initializeSession as initializeSession4,
3919
3953
  isTransientBrowserError,
3920
- probeBeginFrameLiveness
3954
+ probeBeginFrameLiveness,
3955
+ videoAudioId
3921
3956
  } from "@hyperframes/engine";
3922
- import { MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2, fpsToNumber as fpsToNumber7, readMediaStart as readMediaStart2 } from "@hyperframes/core";
3957
+ import {
3958
+ MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
3959
+ fpsToNumber as fpsToNumber7,
3960
+ normalizeRateSpec as normalizeRateSpec3,
3961
+ readMediaStart as readMediaStart2,
3962
+ shiftRateLane as shiftRateLane2,
3963
+ sourceTimeAt as sourceTimeAt3,
3964
+ timeAtSourceTime
3965
+ } from "@hyperframes/core";
3923
3966
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
3924
3967
 
3925
3968
  // src/services/render/stages/probeFailures.ts
@@ -3978,16 +4021,28 @@ function authoredMediaStarts(html) {
3978
4021
  }
3979
4022
  return starts;
3980
4023
  }
3981
- function adoptRuntimeMediaStart(composition, el, authored) {
4024
+ function isVideoAudio(audio, videoId) {
4025
+ return audio.type === "video" && audio.id === videoAudioId(videoId);
4026
+ }
4027
+ function adoptRuntimeMediaTiming(composition, el, authored) {
3982
4028
  const shift = el.mediaStart - authored;
3983
- if (el.mediaStart <= 0 || Math.abs(shift) <= BROWSER_MEDIA_EPSILON) return;
3984
4029
  const entries = el.tagName === "video" ? [
3985
4030
  ...composition.videos.filter((video) => video.id === el.id),
3986
- ...composition.audios.filter(
3987
- (audio) => audio.type === "video" && audio.id === `${el.id}-audio`
3988
- )
4031
+ ...composition.audios.filter((audio) => isVideoAudio(audio, el.id))
3989
4032
  ] : composition.audios.filter((audio) => audio.type !== "video" && audio.id === el.id);
3990
- for (const entry of entries) entry.mediaStart += shift;
4033
+ for (const entry of entries) {
4034
+ if (typeof entry.playbackRate !== "object") {
4035
+ if (typeof el.playbackRate === "object") {
4036
+ const consumed = Math.max(0, entry.mediaStart - authored);
4037
+ const head = timeAtSourceTime(normalizeRateSpec3(entry.playbackRate), consumed);
4038
+ entry.mediaStart += sourceTimeAt3(el.playbackRate, head) - consumed;
4039
+ entry.playbackRate = shiftRateLane2(el.playbackRate, head);
4040
+ } else {
4041
+ entry.playbackRate = el.playbackRate;
4042
+ }
4043
+ }
4044
+ if (el.mediaStart > 0 && Math.abs(shift) > BROWSER_MEDIA_EPSILON) entry.mediaStart += shift;
4045
+ }
3991
4046
  }
3992
4047
  function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, durationInferred) {
3993
4048
  if (projectedEnd <= 0) return existingEnd;
@@ -4271,7 +4326,7 @@ async function runProbeStage(input) {
4271
4326
  sourceChanged,
4272
4327
  el.durationInferred
4273
4328
  );
4274
- adoptRuntimeMediaStart(
4329
+ adoptRuntimeMediaTiming(
4275
4330
  composition,
4276
4331
  el,
4277
4332
  authoredStarts.get(el.id) ?? existing.mediaStart
@@ -4294,6 +4349,7 @@ async function runProbeStage(input) {
4294
4349
  start: el.start,
4295
4350
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
4296
4351
  mediaStart: el.mediaStart,
4352
+ playbackRate: el.playbackRate,
4297
4353
  loop: el.loop,
4298
4354
  hasAudio: el.hasAudio && !el.muted
4299
4355
  });
@@ -4318,7 +4374,7 @@ async function runProbeStage(input) {
4318
4374
  sourceChanged,
4319
4375
  el.durationInferred
4320
4376
  );
4321
- adoptRuntimeMediaStart(
4377
+ adoptRuntimeMediaTiming(
4322
4378
  composition,
4323
4379
  el,
4324
4380
  authoredStarts.get(el.id) ?? existing.mediaStart
@@ -4336,6 +4392,7 @@ async function runProbeStage(input) {
4336
4392
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
4337
4393
  mediaStart: el.mediaStart,
4338
4394
  loop: el.loop,
4395
+ playbackRate: el.playbackRate,
4339
4396
  layer: 0,
4340
4397
  volume: el.volume,
4341
4398
  type: "audio"
@@ -4596,7 +4653,8 @@ import {
4596
4653
  isHttpUrl as isHttpUrl2,
4597
4654
  resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
4598
4655
  runVideoExtractionWithRetry,
4599
- safeVideoExtractionSourceIdentity
4656
+ safeVideoExtractionSourceIdentity,
4657
+ videoAudioId as videoAudioId2
4600
4658
  } from "@hyperframes/engine";
4601
4659
 
4602
4660
  // src/services/render/extractionFailureMetadata.ts
@@ -4954,7 +5012,7 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
4954
5012
  const video = composition.videos.find((v) => v.id === ext.videoId);
4955
5013
  if (!video || !video.hasAudio || video.hidden || existingAudioSrcs.has(video.src)) continue;
4956
5014
  composition.audios.push({
4957
- id: `${video.id}-audio`,
5015
+ id: videoAudioId2(video.id),
4958
5016
  src: video.src,
4959
5017
  start: video.start,
4960
5018
  end: video.end,
@@ -5007,7 +5065,7 @@ import {
5007
5065
 
5008
5066
  // src/services/render/audioPadTrim.ts
5009
5067
  import { spawn } from "node:child_process";
5010
- import { mkdtempSync as mkdtempSync2, renameSync as renameSync2, rmSync as rmSync4 } from "node:fs";
5068
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync4 } from "node:fs";
5011
5069
  import { dirname as dirname5, join as join9 } from "node:path";
5012
5070
  import {
5013
5071
  DEFAULT_CONFIG,
@@ -5016,6 +5074,7 @@ import {
5016
5074
  extractAudioMetadata as extractAudioMetadata3,
5017
5075
  getFfprobeBinary,
5018
5076
  ManagedChildProcess,
5077
+ renameWithRetry as renameWithRetry4,
5019
5078
  runFfmpeg as runFfmpeg4,
5020
5079
  trackChildProcess
5021
5080
  } from "@hyperframes/engine";
@@ -5271,7 +5330,7 @@ async function enforceAacTruePeak(input) {
5271
5330
  }
5272
5331
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
5273
5332
  if (measuredPath !== correctedPath) return { success: true };
5274
- renameSync2(correctedPath, input.audioPath);
5333
+ renameWithRetry4(correctedPath, input.audioPath);
5275
5334
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
5276
5335
  }
5277
5336
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -5461,6 +5520,7 @@ async function runAudioStage(input) {
5461
5520
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
5462
5521
  let audioLoweredDb;
5463
5522
  if (result.hasAudio) {
5523
+ const guardStart = Date.now();
5464
5524
  const peak = await constrainAacTruePeak({
5465
5525
  audioPath: result.audioOutputPath,
5466
5526
  sourcePath: pcmSourcePath,
@@ -5470,6 +5530,7 @@ async function runAudioStage(input) {
5470
5530
  });
5471
5531
  if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
5472
5532
  audioLoweredDb = peak.loweredDb;
5533
+ input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
5473
5534
  }
5474
5535
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
5475
5536
  } finally {
@@ -5495,6 +5556,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5495
5556
  let hasAudio = false;
5496
5557
  let audioError;
5497
5558
  let audioFailures;
5559
+ let audioPhaseMs;
5498
5560
  if (audios.length > 0) {
5499
5561
  let audioResult;
5500
5562
  try {
@@ -5544,6 +5606,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5544
5606
  assertAudioNotInterrupted(audioResult.failures);
5545
5607
  hasAudio = audioResult.success;
5546
5608
  audioFailures = audioResult.failures;
5609
+ audioPhaseMs = audioResult.phaseMs;
5547
5610
  if (!hasAudio) {
5548
5611
  audioError = audioResult.error ?? "audio mix failed for an unknown reason";
5549
5612
  for (const failure of audioResult.failures ?? []) {
@@ -5562,7 +5625,8 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5562
5625
  audioOutputPath,
5563
5626
  hasAudio,
5564
5627
  audioError,
5565
- audioFailures
5628
+ audioFailures,
5629
+ audioPhaseMs
5566
5630
  };
5567
5631
  }
5568
5632
 
@@ -5672,6 +5736,7 @@ async function runCaptureStage(input) {
5672
5736
  let { workerCount } = plan2;
5673
5737
  const { forceScreenshot } = plan2;
5674
5738
  let lastBrowserConsole = [];
5739
+ const workerPageErrors = [];
5675
5740
  let captureBeyondViewport = probeSession?.options.captureBeyondViewport;
5676
5741
  const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
5677
5742
  if (frameRange !== void 0) {
@@ -5709,6 +5774,7 @@ async function runCaptureStage(input) {
5709
5774
  abortSignal,
5710
5775
  frameRangeStart: frameRange?.startFrame,
5711
5776
  dedupPerfs,
5777
+ pageErrorLines: workerPageErrors,
5712
5778
  onProgress: (progress) => {
5713
5779
  if (progress.latestWorkerPhase) {
5714
5780
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -5729,6 +5795,11 @@ async function runCaptureStage(input) {
5729
5795
  },
5730
5796
  cfg: captureCfg,
5731
5797
  log
5798
+ }).catch((error) => {
5799
+ throw wrapCaptureStageError(
5800
+ error,
5801
+ withWorkerPageErrors(probeSession?.browserConsoleBuffer ?? [], workerPageErrors)
5802
+ );
5732
5803
  });
5733
5804
  captureAttempts.push(...attempts);
5734
5805
  const lastAttempt = attempts[attempts.length - 1];
@@ -5741,6 +5812,7 @@ async function runCaptureStage(input) {
5741
5812
  await closeCaptureSession5(probeSession);
5742
5813
  probeSession = null;
5743
5814
  }
5815
+ lastBrowserConsole = withWorkerPageErrors(lastBrowserConsole, workerPageErrors);
5744
5816
  } else {
5745
5817
  const sequential = await runSequentialDiskCapture(
5746
5818
  input,
@@ -5948,6 +6020,7 @@ import {
5948
6020
  statSync as statSync3,
5949
6021
  writeFileSync as writeFileSync2
5950
6022
  } from "node:fs";
6023
+ import { readFile as readFile2 } from "node:fs/promises";
5951
6024
  import { dirname as dirname6, join as join12 } from "node:path";
5952
6025
  import {
5953
6026
  encodeFramesChunkedConcat,
@@ -6117,6 +6190,100 @@ function videoEncoderOptions(input, engineCfg) {
6117
6190
  gopSize: input.gopSize
6118
6191
  };
6119
6192
  }
6193
+ function canEncodeWhileCapturing(input, workerCount, cpuCount) {
6194
+ 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";
6195
+ }
6196
+ async function readLandedFrame(workerPath, mergedPath) {
6197
+ if (workerPath !== void 0) {
6198
+ try {
6199
+ return await readFile2(workerPath);
6200
+ } catch (error) {
6201
+ if (error.code !== "ENOENT") throw error;
6202
+ }
6203
+ }
6204
+ return readFile2(mergedPath);
6205
+ }
6206
+ function encodeWhileCapturing(input, totalFrames) {
6207
+ const engineCfg = resolveEngineConfig(input);
6208
+ const stop = new AbortController();
6209
+ const signal = input.abortSignal ? AbortSignal.any([input.abortSignal, stop.signal]) : stop.signal;
6210
+ const landed = /* @__PURE__ */ new Map();
6211
+ let captureDone = false;
6212
+ let framesFed = 0;
6213
+ let wake = null;
6214
+ const nudge = () => {
6215
+ wake?.();
6216
+ wake = null;
6217
+ };
6218
+ signal.addEventListener("abort", nudge, { once: true });
6219
+ let reportFed = null;
6220
+ const encoder = spawnStreamingEncoder4(
6221
+ input.videoOnlyPath,
6222
+ { ...videoEncoderOptions(input, engineCfg), imageFormat: "jpeg" },
6223
+ signal,
6224
+ engineCfg
6225
+ );
6226
+ const fed = (async () => {
6227
+ const stream = await encoder;
6228
+ for (let next = 0; next < totalFrames; next++) {
6229
+ while (!landed.has(next) && !captureDone && !signal.aborted) {
6230
+ await new Promise((resolve9) => wake = resolve9);
6231
+ }
6232
+ if (signal.aborted) return;
6233
+ const mergedPath = join12(input.framesDir, formatCaptureFrameName(next, "jpg"));
6234
+ const frame = await readLandedFrame(landed.get(next), mergedPath);
6235
+ landed.delete(next);
6236
+ if (!await stream.writeFrame(frame)) {
6237
+ throw new Error(stream.getExitError() ?? "encoder exited before the last frame");
6238
+ }
6239
+ framesFed++;
6240
+ reportFed?.(framesFed);
6241
+ }
6242
+ })().then(
6243
+ () => void 0,
6244
+ (error) => error ?? new Error("encode failed")
6245
+ );
6246
+ const closeEncoder = async () => (await encoder.catch(() => null))?.close();
6247
+ return {
6248
+ onFrameLanded: (frame) => {
6249
+ landed.set(frame.index, frame.path);
6250
+ nudge();
6251
+ },
6252
+ finish: async () => {
6253
+ const start = Date.now();
6254
+ captureDone = true;
6255
+ input.log.info("[Render] Encoded frames while capturing", {
6256
+ framesFed,
6257
+ totalFrames
6258
+ });
6259
+ const encoded = startEncodeProgress(input.job, "Encoding video", input.onProgress);
6260
+ reportFed = encoded;
6261
+ nudge();
6262
+ const failure = await fed;
6263
+ const result = await closeEncoder();
6264
+ input.assertNotAborted();
6265
+ if (failure === void 0 && result?.success) {
6266
+ encoded();
6267
+ return { encodeMs: Date.now() - start };
6268
+ }
6269
+ input.log.warn(
6270
+ "[Render] Encoding while capturing failed; encoding the frames after capture",
6271
+ {
6272
+ error: String(failure ?? result?.error)
6273
+ }
6274
+ );
6275
+ return runEncodeStage(input);
6276
+ },
6277
+ abort: async () => {
6278
+ stop.abort();
6279
+ await fed;
6280
+ await closeEncoder();
6281
+ },
6282
+ get framesFed() {
6283
+ return framesFed;
6284
+ }
6285
+ };
6286
+ }
6120
6287
  async function runEncodeStage(input) {
6121
6288
  const {
6122
6289
  job,
@@ -6430,6 +6597,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
6430
6597
  options.cfg
6431
6598
  );
6432
6599
  pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
6600
+ if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
6433
6601
  } finally {
6434
6602
  await mergeWorkerFrames(attemptWorkDir, tasks, options.framesDir);
6435
6603
  }
@@ -7508,15 +7676,9 @@ import { join as join19 } from "node:path";
7508
7676
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
7509
7677
 
7510
7678
  // 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";
7679
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
7519
7680
  import { open } from "node:fs/promises";
7681
+ import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
7520
7682
  import { dirname as dirname9, join as join18 } from "node:path";
7521
7683
  import { finished, pipeline } from "node:stream/promises";
7522
7684
  function boxAt(src, off, at, limit) {
@@ -7889,7 +8051,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
7889
8051
  for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
7890
8052
  out.end();
7891
8053
  await finished(out);
7892
- renameSync3(join18(workDir, "out.mp4"), outputPath);
8054
+ renameWithRetry5(join18(workDir, "out.mp4"), outputPath);
7893
8055
  } finally {
7894
8056
  out.destroy();
7895
8057
  rmSync8(workDir, { recursive: true, force: true });
@@ -8619,7 +8781,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
8619
8781
  const needsAlpha = outputNeedsAlpha(config.format);
8620
8782
  const remoteFiles = Object.entries(options.remoteFiles ?? {});
8621
8783
  const remoteLocalPath = (rel) => join21(projectDir, ...rel.split("/"));
8622
- for (const [, file] of remoteFiles) declareContentAddressedUrl(file.url, file.sha256);
8784
+ for (const [, file] of remoteFiles) {
8785
+ declareContentAddressedUrl(file.url, file.sha256);
8786
+ if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
8787
+ }
8623
8788
  const urlByAbsentLocalPath = new Map(
8624
8789
  remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
8625
8790
  );
@@ -8741,7 +8906,8 @@ function mixPlanAudio(c, compiledDir) {
8741
8906
  audios: c.composition.audios,
8742
8907
  masterFxChain: c.composition.masterFxChain,
8743
8908
  abortSignal: c.abortSignal,
8744
- assertNotAborted: c.assertNotAborted
8909
+ assertNotAborted: c.assertNotAborted,
8910
+ log: c.log
8745
8911
  });
8746
8912
  }
8747
8913
  async function extractPlanVideos(c, deferRangeExtraction) {
@@ -8760,6 +8926,7 @@ async function extractPlanVideos(c, deferRangeExtraction) {
8760
8926
  if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
8761
8927
  if (extractResult.extractionResult) {
8762
8928
  assertVideoExtractionSucceeded(extractResult.extractionResult);
8929
+ c.log.info("[plan] extract phases", { ...extractResult.extractionResult.phaseBreakdown });
8763
8930
  }
8764
8931
  return extractResult;
8765
8932
  }
@@ -8805,12 +8972,12 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
8805
8972
  const videoSourcesDir = join21(planDir, "video-sources");
8806
8973
  rmSync9(videoSourcesDir, { recursive: true, force: true });
8807
8974
  if (existsSync15(stagedVideoFrames)) {
8808
- renameSync4(stagedVideoFrames, videoFramesDst);
8975
+ renameWithRetry6(stagedVideoFrames, videoFramesDst);
8809
8976
  } else {
8810
8977
  mkdirSync10(videoFramesDst, { recursive: true });
8811
8978
  }
8812
8979
  if (existsSync15(finalCompiledDir)) rmSync9(finalCompiledDir, { recursive: true, force: true });
8813
- renameSync4(compiledDir, finalCompiledDir);
8980
+ renameWithRetry6(compiledDir, finalCompiledDir);
8814
8981
  const shipVideoSource = createVideoSourceShipper({
8815
8982
  planDir,
8816
8983
  projectDir,
@@ -8913,7 +9080,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
8913
9080
  }
8914
9081
  const planAudioPath = join21(planDir, PLAN_AUDIO_RELATIVE_PATH);
8915
9082
  if (audioResult?.hasAudio && existsSync15(audioResult.audioOutputPath)) {
8916
- renameSync4(audioResult.audioOutputPath, planAudioPath);
9083
+ renameWithRetry6(audioResult.audioOutputPath, planAudioPath);
8917
9084
  }
8918
9085
  if (chunksCarryAudio && existsSync15(planAudioPath)) {
8919
9086
  if (options.sliceChunkAudio) {
@@ -9027,7 +9194,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
9027
9194
  applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
9028
9195
  }
9029
9196
  if (!mixed.hasAudio || !existsSync15(mixed.audioOutputPath)) return { hasAudio: false };
9030
- renameSync4(mixed.audioOutputPath, outputPath);
9197
+ renameWithRetry6(mixed.audioOutputPath, outputPath);
9031
9198
  return { hasAudio: true };
9032
9199
  } finally {
9033
9200
  rmSync9(c.workDir, { recursive: true, force: true });
@@ -9059,7 +9226,6 @@ import {
9059
9226
  mkdtempSync as mkdtempSync5,
9060
9227
  readFileSync as readFileSync10,
9061
9228
  readdirSync as readdirSync7,
9062
- renameSync as renameSync6,
9063
9229
  rmSync as rmSync11,
9064
9230
  statSync as statSync6,
9065
9231
  writeFileSync as writeFileSync8
@@ -9067,7 +9233,7 @@ import {
9067
9233
  import { tmpdir } from "node:os";
9068
9234
  import { dirname as dirname11, isAbsolute as isAbsolute4, join as join24, relative as relative6, resolve as resolve8, sep as sep3 } from "node:path";
9069
9235
  import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
9070
- import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
9236
+ import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
9071
9237
 
9072
9238
  // src/services/distributed/planV2Publisher.ts
9073
9239
  import {
@@ -9076,11 +9242,11 @@ import {
9076
9242
  linkSync as linkSync2,
9077
9243
  mkdirSync as mkdirSync11,
9078
9244
  mkdtempSync as mkdtempSync4,
9079
- renameSync as renameSync5,
9080
9245
  rmSync as rmSync10,
9081
9246
  statSync as statSync5,
9082
9247
  writeFileSync as writeFileSync7
9083
9248
  } from "node:fs";
9249
+ import { renameWithRetry as renameWithRetry7 } from "@hyperframes/engine";
9084
9250
  import { dirname as dirname10, join as join23 } from "node:path";
9085
9251
 
9086
9252
  // src/services/distributed/planV2Errors.ts
@@ -9169,7 +9335,7 @@ var LocalPlanV2ArtifactPublisher = class {
9169
9335
  if (!canFallbackToCopy(error)) throw error;
9170
9336
  copyFileSync6(blob.sourcePath, temporaryPath);
9171
9337
  }
9172
- renameSync5(temporaryPath, destinationPath);
9338
+ renameWithRetry7(temporaryPath, destinationPath);
9173
9339
  } finally {
9174
9340
  rmSync10(stagingDir, { recursive: true, force: true });
9175
9341
  }
@@ -9183,7 +9349,7 @@ var LocalPlanV2ArtifactPublisher = class {
9183
9349
  }
9184
9350
  }
9185
9351
  writeFileSync7(join23(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
9186
- renameSync5(this.temporaryDir, this.destinationDir);
9352
+ renameWithRetry7(this.temporaryDir, this.destinationDir);
9187
9353
  this.#committed = true;
9188
9354
  }
9189
9355
  async abort() {
@@ -9450,7 +9616,7 @@ function writeBlob(sourcePath, destinationPath) {
9450
9616
  const temporaryPath = join24(temporaryDir, "blob");
9451
9617
  try {
9452
9618
  copyFileSync7(sourcePath, temporaryPath);
9453
- renameSync6(temporaryPath, destinationPath);
9619
+ renameWithRetry8(temporaryPath, destinationPath);
9454
9620
  } finally {
9455
9621
  rmSync11(temporaryDir, { recursive: true, force: true });
9456
9622
  }
@@ -9582,7 +9748,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
9582
9748
  canonicalJsonStringify(publication.manifest),
9583
9749
  "utf-8"
9584
9750
  );
9585
- renameSync6(tempDir, planV2Dir);
9751
+ renameWithRetry8(tempDir, planV2Dir);
9586
9752
  return resultFromManifest(planV2Dir, publication.manifest);
9587
9753
  } catch (error) {
9588
9754
  rmSync11(tempDir, { recursive: true, force: true });
@@ -9867,7 +10033,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
9867
10033
  }),
9868
10034
  "utf-8"
9869
10035
  );
9870
- renameSync6(tempDir, destinationDir);
10036
+ renameWithRetry8(tempDir, destinationDir);
9871
10037
  } catch (error) {
9872
10038
  rmSync11(tempDir, { recursive: true, force: true });
9873
10039
  throw error;
@@ -9945,7 +10111,6 @@ import {
9945
10111
  mkdtempSync as mkdtempSync6,
9946
10112
  readFileSync as readFileSync11,
9947
10113
  readdirSync as readdirSync8,
9948
- renameSync as renameSync7,
9949
10114
  rmSync as rmSync12,
9950
10115
  statSync as statSync7,
9951
10116
  writeFileSync as writeFileSync9
@@ -9959,7 +10124,8 @@ import {
9959
10124
  muxVideoWithAudio as muxVideoWithAudio2,
9960
10125
  resolveConfig as resolveConfig4,
9961
10126
  runFfmpeg as runFfmpeg7,
9962
- appendSoftwareEncoderThreadsArg
10127
+ appendSoftwareEncoderThreadsArg,
10128
+ renameWithRetry as renameWithRetry9
9963
10129
  } from "@hyperframes/engine";
9964
10130
  import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
9965
10131
  function cfrRateControlArgs(locked) {
@@ -10016,7 +10182,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
10016
10182
  const finalPath = join25(workDir, `output.${format}`);
10017
10183
  const audio = audioPath !== null && existsSync18(audioPath) ? audioPath : null;
10018
10184
  await writeJoined(ctx, videoSources, audio, finalPath);
10019
- renameSync7(finalPath, outputPath);
10185
+ renameWithRetry9(finalPath, outputPath);
10020
10186
  log.info("[assemble] stage timings", {
10021
10187
  ...stageMs,
10022
10188
  chunks: chunkPaths.length,
@@ -10268,6 +10434,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
10268
10434
  // src/services/distributed/renderChunk.ts
10269
10435
  import { randomBytes } from "node:crypto";
10270
10436
  import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync13, writeFileSync as writeFileSync10 } from "node:fs";
10437
+ import { cpus } from "node:os";
10271
10438
  import { join as join26 } from "node:path";
10272
10439
  import {
10273
10440
  assertSwiftShader,
@@ -10572,6 +10739,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10572
10739
  let forceScreenshotForChunk = encoder.forceScreenshot;
10573
10740
  let shownBoxes;
10574
10741
  let extraction;
10742
+ const overlap = { current: null };
10575
10743
  try {
10576
10744
  if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
10577
10745
  const bootStarted = Date.now();
@@ -10659,6 +10827,59 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10659
10827
  videoFrameLookup,
10660
10828
  frameCacheOptions(cfg, chunkWorkerCount)
10661
10829
  );
10830
+ const isPngSequence = plan2.dimensions.format === "png-sequence";
10831
+ outputKind = isPngSequence ? "frame-dir" : "file";
10832
+ const presetFormat = isPngSequence ? "mp4" : plan2.dimensions.format;
10833
+ const basePreset = getEncoderPreset6(job.config.quality, presetFormat, void 0);
10834
+ const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
10835
+ const effectiveQuality = encoder.crf ?? preset.quality;
10836
+ const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
10837
+ const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
10838
+ if (isPngSequence) {
10839
+ if (!existsSync19(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
10840
+ } else {
10841
+ const outDir = join26(outputChunkPath, "..");
10842
+ if (!existsSync19(outDir)) mkdirSync14(outDir, { recursive: true });
10843
+ }
10844
+ const encodeInput = {
10845
+ job,
10846
+ log,
10847
+ outputPath: videoOnlyPath,
10848
+ framesDir,
10849
+ videoOnlyPath,
10850
+ width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
10851
+ height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
10852
+ needsAlpha: plan2.dimensions.format !== "mp4",
10853
+ captureImageFormat: captureOptions.format ?? "jpeg",
10854
+ // The encoder writes video only; audio joins at assemble time or in the
10855
+ // chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
10856
+ // sidecar AND the mp4 audio mux.
10857
+ hasAudio: false,
10858
+ isPngSequence,
10859
+ // `DistributedFormat` has no "gif" member — distributed chunks are
10860
+ // always video segments (gif renders in-process only).
10861
+ isGif: false,
10862
+ preset,
10863
+ effectiveQuality,
10864
+ effectiveBitrate,
10865
+ engineConfig: {
10866
+ ffmpegEncodeTimeout: cfg.ffmpegEncodeTimeout,
10867
+ vp9CpuUsed: resolveLockedVp9CpuUsed(encoder) ?? cfg.vp9CpuUsed
10868
+ },
10869
+ // Distributed chunks emit a single ffmpeg call per chunk; the
10870
+ // in-process per-chunk-within-chunk path would re-split our
10871
+ // already-chunked work.
10872
+ enableChunkedEncode: false,
10873
+ chunkedEncodeSize: framesInChunk,
10874
+ abortSignal: void 0,
10875
+ assertNotAborted: () => {
10876
+ },
10877
+ // GOP === framesInChunk + force-keyframe at frame 0 → the chunk's
10878
+ // first frame is an IDR keyframe and concat-copy at assemble time
10879
+ // round-trips losslessly.
10880
+ lockGopForChunkConcat: !isPngSequence,
10881
+ gopSize: framesInChunk
10882
+ };
10662
10883
  const captureStarted = Date.now();
10663
10884
  captureMode = await runCaptureWithScreenshotFallback({
10664
10885
  forceScreenshot: forceScreenshotForChunk,
@@ -10691,6 +10912,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10691
10912
  }
10692
10913
  const captureSession = session;
10693
10914
  session = null;
10915
+ overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
10694
10916
  await runCaptureStage({
10695
10917
  fileServer,
10696
10918
  workDir,
@@ -10710,7 +10932,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10710
10932
  abortSignal: void 0,
10711
10933
  assertNotAborted: () => {
10712
10934
  },
10713
- frameRange: range
10935
+ frameRange: range,
10936
+ onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
10937
+ }).catch(async (captureError) => {
10938
+ await overlap.current?.abort();
10939
+ overlap.current = null;
10940
+ throw captureError;
10714
10941
  });
10715
10942
  return observedChunkCaptureMode(capturePerfs);
10716
10943
  },
@@ -10742,60 +10969,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10742
10969
  log
10743
10970
  );
10744
10971
  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
10972
  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
- });
10973
+ const overlapped = overlap.current;
10974
+ overlap.current = null;
10975
+ await (overlapped ? overlapped.finish() : runEncodeStage(encodeInput));
10799
10976
  if (encoder.chunksCarryAudio) {
10800
10977
  const audioScratchPath = `${outputChunkPath}.audio.m4a`;
10801
10978
  const audioChunks = options?.frameRange ? chunksWithSubRange(chunks, chunkIndex, range) : { chunks, index: chunkIndex };
@@ -10813,6 +10990,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10813
10990
  }
10814
10991
  encodeStageMs = Date.now() - encodeStarted;
10815
10992
  } finally {
10993
+ await overlap.current?.abort().catch(() => {
10994
+ });
10816
10995
  await extraction?.catch(() => {
10817
10996
  });
10818
10997
  if (session) {
@@ -11242,6 +11421,24 @@ async function buildAudioSidecar(inputPath, outputPath, signal) {
11242
11421
  }
11243
11422
  var isMissing = (result) => result.error?.code === "ENOENT";
11244
11423
  var ffmpegIsMissing = async (signal) => isMissing(await runFfmpeg8(["-version"], { signal }));
11424
+
11425
+ // src/services/distributed/probeFacts.ts
11426
+ import { writeFileSync as writeFileSync11 } from "node:fs";
11427
+ import { extname as extname6 } from "node:path";
11428
+ import { buildProbeFactsText } from "@hyperframes/engine";
11429
+ var PROBE_FACTS_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v", ".webm", ".mkv"]);
11430
+ function wantsProbeFacts(path) {
11431
+ return PROBE_FACTS_EXTENSIONS.has(extname6(path).toLowerCase());
11432
+ }
11433
+ async function buildProbeFacts(inputPath, outputPath, signal) {
11434
+ try {
11435
+ writeFileSync11(outputPath, await buildProbeFactsText(inputPath, signal));
11436
+ return "built";
11437
+ } catch (error) {
11438
+ if (signal?.aborted) throw error;
11439
+ return /ffprobe not found/i.test(String(error.message)) ? "unavailable" : "none";
11440
+ }
11441
+ }
11245
11442
  export {
11246
11443
  AUDIO_SIDECAR_MIN_BYTES,
11247
11444
  CHUNK_EXCEEDS_WORKER_TIMEOUT,
@@ -11289,6 +11486,7 @@ export {
11289
11486
  buildAudioSidecar,
11290
11487
  buildChunkSlices,
11291
11488
  buildPlanAudio,
11489
+ buildProbeFacts,
11292
11490
  buildStitchedHeader,
11293
11491
  chunksAreStitchedMp4,
11294
11492
  createPlanV2FromExecutionPlan,
@@ -11323,5 +11521,6 @@ export {
11323
11521
  validateDistributedRenderConfig,
11324
11522
  validatePlanV2MaterializedTarget,
11325
11523
  validateVariablesPayload,
11326
- wantsAudioSidecar
11524
+ wantsAudioSidecar,
11525
+ wantsProbeFacts
11327
11526
  };