@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.
package/dist/index.js CHANGED
@@ -1364,18 +1364,9 @@ var RenderExecutionContext = class {
1364
1364
  };
1365
1365
 
1366
1366
  // src/services/render/artifactTransaction.ts
1367
- import {
1368
- closeSync as closeSync2,
1369
- existsSync as existsSync5,
1370
- fstatSync as fstatSync2,
1371
- openSync as openSync2,
1372
- readSync,
1373
- readdirSync as readdirSync2,
1374
- renameSync,
1375
- rmSync as rmSync3
1376
- } from "node:fs";
1367
+ import { closeSync as closeSync2, existsSync as existsSync5, fstatSync as fstatSync2, openSync as openSync2, readSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
1377
1368
  import { basename as basename3, dirname as dirname4, extname as extname2, join as join5, resolve as resolve4 } from "node:path";
1378
- import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST } from "@hyperframes/engine";
1369
+ import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, renameWithRetry } from "@hyperframes/engine";
1379
1370
 
1380
1371
  // src/utils/ffprobe.ts
1381
1372
  import {
@@ -1495,7 +1486,7 @@ function createRenderWorkDir(prefix, outputDir) {
1495
1486
  // src/services/render/artifactTransaction.ts
1496
1487
  var defaultFileSystem = {
1497
1488
  existsSync: existsSync5,
1498
- renameSync,
1489
+ renameSync: (source, destination) => renameWithRetry(source, destination),
1499
1490
  rmSync: rmSync3
1500
1491
  };
1501
1492
  async function defaultArtifactDurationProbe(path) {
@@ -1573,8 +1564,11 @@ function assertDurationWithinTolerance(expectedSeconds, probedSeconds, tolerance
1573
1564
  `Render artifact is truncated: expected ${expectedSeconds.toFixed(3)}s, probed ${probedSeconds.toFixed(3)}s (deficit ${deficit.toFixed(3)}s exceeds tolerance ${toleranceSeconds.toFixed(3)}s). Artifact: ${stagingPath}`
1574
1565
  );
1575
1566
  }
1567
+ function hasUsableFrameCount(frames) {
1568
+ return frames !== void 0 && Number.isFinite(frames) && frames > 0;
1569
+ }
1576
1570
  function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath, toleranceFrames = 1) {
1577
- if (probedFrames === void 0 || !Number.isFinite(probedFrames) || probedFrames <= 0) {
1571
+ if (!hasUsableFrameCount(probedFrames)) {
1578
1572
  return;
1579
1573
  }
1580
1574
  const shortfall = expectedFrames - probedFrames;
@@ -1594,9 +1588,10 @@ function assertSizeMatches(expected, probed, stagingPath) {
1594
1588
  );
1595
1589
  }
1596
1590
  var ArtifactTransaction = class {
1597
- constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe) {
1591
+ constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe, logger = defaultLogger) {
1598
1592
  this.kind = kind;
1599
1593
  this.fileSystem = fileSystem;
1594
+ this.logger = logger;
1600
1595
  this.destinationPath = resolve4(destinationPath);
1601
1596
  this.transactionDirectory = createSiblingTransactionDirectory(this.destinationPath);
1602
1597
  this.stagingPath = join5(this.transactionDirectory, basename3(this.destinationPath));
@@ -1679,6 +1674,19 @@ var ArtifactTransaction = class {
1679
1674
  probePath
1680
1675
  );
1681
1676
  if (expected.expectedFrames !== void 0) {
1677
+ if (!hasUsableFrameCount(probed.frames)) {
1678
+ try {
1679
+ this.logger.debug(
1680
+ "[Render] Frame-count validation skipped: no usable frame count was reported.",
1681
+ {
1682
+ event: "artifact_frame_count_unavailable",
1683
+ artifact: this.destinationPath,
1684
+ expectedFrames: expected.expectedFrames
1685
+ }
1686
+ );
1687
+ } catch {
1688
+ }
1689
+ }
1682
1690
  assertFrameCountWithinTolerance(expected.expectedFrames, probed.frames, probePath);
1683
1691
  }
1684
1692
  assertSizeMatches(expected, probed, probePath);
@@ -1894,58 +1902,10 @@ import {
1894
1902
 
1895
1903
  // src/services/render/captureStageError.ts
1896
1904
  import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
1897
- var CaptureStageError = class extends CaptureFailure {
1898
- browserConsole;
1899
- constructor(input) {
1900
- const classified = classifyCaptureFailure(input.cause);
1901
- super({
1902
- kind: classified.kind,
1903
- message: normalizeErrorMessage(input.cause),
1904
- cause: input.cause,
1905
- workerDiagnostics: classified.workerDiagnostics
1906
- });
1907
- this.name = "CaptureStageError";
1908
- this.browserConsole = input.browserConsole.slice();
1909
- if (input.cause instanceof Error && input.cause.stack) {
1910
- this.stack = input.cause.stack;
1911
- }
1912
- }
1913
- };
1914
- function wrapCaptureStageError(error, browserConsole) {
1915
- if (error instanceof CaptureStageError || error instanceof EncoderInterruptedError) return error;
1916
- return new CaptureStageError({ cause: error, browserConsole });
1917
- }
1918
- function getCaptureStageBrowserConsole(error) {
1919
- if (error instanceof CaptureStageError) return error.browserConsole;
1920
- return [];
1921
- }
1922
-
1923
- // src/services/render/stages/captureHdrFrameShared.ts
1924
- import {
1925
- applyDomLayerMask as applyDomLayerMask2,
1926
- blitRgba8OverRgb48le as blitRgba8OverRgb48le2,
1927
- captureAlphaPng as captureAlphaPng2,
1928
- decodePng as decodePng2,
1929
- queryElementStacking as queryElementStacking2,
1930
- removeDomLayerMask as removeDomLayerMask2
1931
- } from "@hyperframes/engine";
1932
1905
 
1933
- // src/services/hdrCompositor.ts
1934
- import { readSync as readSync2, closeSync as closeSync3 } from "fs";
1935
- import { join as join6 } from "path";
1936
- import {
1937
- captureAlphaPng,
1938
- applyDomLayerMask,
1939
- removeDomLayerMask,
1940
- decodePng,
1941
- blitRgba8OverRgb48le,
1942
- blitRgb48leRegion,
1943
- groupIntoLayers,
1944
- blitRgb48leAffine,
1945
- parseTransformMatrix,
1946
- convertTransfer,
1947
- isVideoHiddenBeforeStreamStart
1948
- } from "@hyperframes/engine";
1906
+ // src/services/render/perfSummary.ts
1907
+ import { arch, cpus, platform, totalmem } from "node:os";
1908
+ import { fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1949
1909
 
1950
1910
  // src/services/render/hdrPerf.ts
1951
1911
  function createHdrPerfCollector() {
@@ -2051,7 +2011,280 @@ function finalizeHdrPerf(perf) {
2051
2011
  };
2052
2012
  }
2053
2013
 
2014
+ // src/services/render/perfSummary.ts
2015
+ function worstSubTimelineWaitOutcome(perfs) {
2016
+ const outcomes = perfs.map((p) => p.subTimelineWaitOutcome).filter((o) => !!o);
2017
+ if (outcomes.length === 0) return void 0;
2018
+ if (outcomes.includes("script_failure")) return "script_failure";
2019
+ if (outcomes.includes("timeout")) return "timeout";
2020
+ return "ready";
2021
+ }
2022
+ function pushWorkerDedupPerfs(results, sink) {
2023
+ for (const r of results) {
2024
+ if (r.perf) sink.push(r.perf);
2025
+ }
2026
+ }
2027
+ function pushWorkerPageErrors(results, sink) {
2028
+ for (const result of results) {
2029
+ for (const line of result.pageErrorLines ?? []) if (!sink.includes(line)) sink.push(line);
2030
+ }
2031
+ }
2032
+ function withWorkerPageErrors(consoleLines, workerLines) {
2033
+ return [...consoleLines, ...workerLines.filter((line) => !consoleLines.includes(line))];
2034
+ }
2035
+ function roundDb(value) {
2036
+ return value === void 0 ? void 0 : Math.round(Math.min(value, 999) * 10) / 10;
2037
+ }
2038
+ function aggregateDrawElement(perfs, de) {
2039
+ if (perfs.length === 0) return void 0;
2040
+ const modes = [...new Set(perfs.map((p) => p.captureMode).filter(Boolean))].sort();
2041
+ const gateReasons = [
2042
+ ...new Set(
2043
+ [de.probeGateReason, ...perfs.map((p) => p.deGateReason)].filter((r) => !!r)
2044
+ )
2045
+ ].sort();
2046
+ const gpuRenderers = [
2047
+ ...new Set(perfs.map((p) => p.gpuRenderer).filter((r) => !!r))
2048
+ ].sort();
2049
+ const drain = de.drainStats;
2050
+ return {
2051
+ mode: modes.join("|") || "unknown",
2052
+ compileGate: de.compileGate,
2053
+ clampReason: de.clampReason,
2054
+ workerInversion: de.workerInversion ?? "none",
2055
+ preInversionWorkers: de.preInversionWorkers,
2056
+ compositionElementCount: de.compositionElementCount,
2057
+ compositionElementCountSource: de.compositionElementCountSource,
2058
+ compositionElementTags: de.compositionElementTags,
2059
+ arollVideoCount: de.arollVideoCount,
2060
+ heygenVideoCount: de.heygenVideoCount,
2061
+ adaptersUsed: de.adaptersUsed,
2062
+ audioCount: de.audioCount,
2063
+ imageCount: de.imageCount,
2064
+ subCompositionCount: de.subCompositionCount,
2065
+ audioGroupCount: de.audioGroupCount,
2066
+ colorGradingCount: de.colorGradingCount,
2067
+ hasLut: de.hasLut,
2068
+ rootBodyMismatch: de.rootBodyMismatch,
2069
+ rootBodyDeltaPxBucket: de.rootBodyDeltaPxBucket,
2070
+ vfxHostCount: de.vfxHostCount,
2071
+ vfxCapture: de.vfxCapture,
2072
+ vfxTypes: de.vfxTypes,
2073
+ shortBand: de.shortBand,
2074
+ parallelRouter: de.parallelRouter ?? "none",
2075
+ preRouterWorkers: de.preRouterWorkers,
2076
+ gateReason: gateReasons.length > 0 ? gateReasons.join("|") : void 0,
2077
+ gpuRenderer: gpuRenderers.length > 0 ? gpuRenderers.join("|") : void 0,
2078
+ workerEncode: perfs.some((p) => p.deWorkerEncode),
2079
+ verifyArmed: perfs.reduce((sum2, p) => sum2 + (p.deVerifyArmed ?? 0), 0),
2080
+ verifyChecked: drain?.verifyChecked ?? 0,
2081
+ verifyMinDb: roundDb(drain?.verifyMinDb),
2082
+ verifyInitMs: perfs.reduce((sum2, p) => sum2 + (p.deVerifyInitMs ?? 0), 0),
2083
+ selfVerifyFallback: de.selfVerifyFallback,
2084
+ fallbackReason: de.fallbackReason,
2085
+ fallbackFailedDb: roundDb(de.fallbackFailedDb),
2086
+ fallbackFrameIndex: de.fallbackFrameIndex,
2087
+ fallbackThresholdDb: roundDb(de.fallbackThresholdDb),
2088
+ blankSuspects: drain?.blankSuspects ?? 0,
2089
+ blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
2090
+ blankRecaptures: drain?.blankRecaptures ?? 0,
2091
+ boundaryFrames: perfs.reduce((sum2, p) => sum2 + (p.deBoundaryFrames ?? 0), 0),
2092
+ ncprFallbacks: perfs.reduce((sum2, p) => sum2 + (p.deNcprFallbacks ?? 0), 0),
2093
+ frameTimeouts: perfs.reduce((sum2, p) => sum2 + (p.deFrameTimeouts ?? 0), 0)
2094
+ };
2095
+ }
2096
+ function maxDefined(values) {
2097
+ const present = values.filter((v) => typeof v === "number");
2098
+ return present.length > 0 ? Math.max(...present) : void 0;
2099
+ }
2100
+ function aggregateChromeMemory(perfs) {
2101
+ const sampled = perfs.filter((p) => (p.chromeMemorySamples ?? 0) > 0);
2102
+ if (sampled.length === 0) return void 0;
2103
+ return {
2104
+ browserRssPeakMb: maxDefined(sampled.map((p) => p.chromeBrowserRssPeakMb)),
2105
+ rendererRssPeakMb: maxDefined(sampled.map((p) => p.chromeRendererRssPeakMb)),
2106
+ // Last samples are per session; summing approximates the whole fleet's
2107
+ // footprint at the end of capture (workers run concurrently).
2108
+ rssLastMb: sampled.reduce((sum2, p) => sum2 + (p.chromeRssLastMb ?? 0), 0),
2109
+ gpuProcessSeenLastSample: sampled.some((p) => p.chromeGpuProcessSeenLastSample === true),
2110
+ samples: sampled.reduce((sum2, p) => sum2 + (p.chromeMemorySamples ?? 0), 0)
2111
+ };
2112
+ }
2113
+ function aggregateDedup(perfs) {
2114
+ if (perfs.length === 0) return void 0;
2115
+ const armed = perfs.some((p) => p.staticDedupArmed);
2116
+ const skipReasons = armed ? [] : [
2117
+ ...new Set(perfs.map((p) => p.staticDedupSkipReason).filter((r) => !!r))
2118
+ ].sort();
2119
+ const verificationPerfs = perfs.filter((perf) => perf.staticDedupVerificationOutcome);
2120
+ const verificationOutcomes = [
2121
+ ...new Set(
2122
+ verificationPerfs.map((perf) => perf.staticDedupVerificationOutcome).filter((outcome) => outcome != null)
2123
+ )
2124
+ ].sort();
2125
+ return {
2126
+ enabled: perfs.some((p) => p.staticDedupEnabled),
2127
+ armed,
2128
+ predictedFrames: perfs.reduce((sum2, p) => sum2 + (p.staticDedupPredicted ?? 0), 0),
2129
+ reusedFrames: perfs.reduce((sum2, p) => sum2 + (p.staticDedupReused ?? 0), 0),
2130
+ skipReason: skipReasons.length > 0 ? skipReasons.join("|") : void 0,
2131
+ ...verificationPerfs.length === 0 ? {} : {
2132
+ verifiedFrames: verificationPerfs.reduce(
2133
+ (sum2, perf) => sum2 + (perf.staticDedupVerified ?? 0),
2134
+ 0
2135
+ ),
2136
+ verificationOutcomes,
2137
+ plannedRuns: verificationPerfs.reduce(
2138
+ (sum2, perf) => sum2 + (perf.staticDedupVerificationPlannedRuns ?? 0),
2139
+ 0
2140
+ ),
2141
+ completedRuns: verificationPerfs.reduce(
2142
+ (sum2, perf) => sum2 + (perf.staticDedupVerificationCompletedRuns ?? 0),
2143
+ 0
2144
+ ),
2145
+ screenshots: verificationPerfs.reduce(
2146
+ (sum2, perf) => sum2 + (perf.staticDedupVerificationScreenshots ?? 0),
2147
+ 0
2148
+ ),
2149
+ seeks: verificationPerfs.reduce(
2150
+ (sum2, perf) => sum2 + (perf.staticDedupVerificationSeeks ?? 0),
2151
+ 0
2152
+ ),
2153
+ comparisons: verificationPerfs.reduce(
2154
+ (sum2, perf) => sum2 + (perf.staticDedupVerificationComparisons ?? 0),
2155
+ 0
2156
+ ),
2157
+ verificationElapsedMs: verificationPerfs.reduce(
2158
+ (sum2, perf) => sum2 + (perf.staticDedupVerificationElapsedMs ?? 0),
2159
+ 0
2160
+ )
2161
+ }
2162
+ };
2163
+ }
2164
+ function aggregateBeginFrameReuse(perfs) {
2165
+ const noDamageFrames = perfs.reduce((sum2, p) => sum2 + (p.beginFrameNoDamage ?? 0), 0);
2166
+ const hasDamageFrames = perfs.reduce((sum2, p) => sum2 + (p.beginFrameHasDamage ?? 0), 0);
2167
+ if (noDamageFrames + hasDamageFrames === 0) return void 0;
2168
+ return { noDamageFrames, hasDamageFrames };
2169
+ }
2170
+ function buildRenderPerfSummary(input) {
2171
+ return {
2172
+ renderId: input.job.id,
2173
+ totalElapsedMs: input.totalElapsedMs,
2174
+ // RenderPerfSummary surfaces fps as a decimal because it lands in JSON
2175
+ // payloads (CLI telemetry, regression-harness reports) where a single
2176
+ // number is friendlier than `{num,den}`. Callers needing the rational
2177
+ // back can read `job.config.fps`.
2178
+ fps: fpsToNumber3(input.job.config.fps),
2179
+ quality: input.job.config.quality,
2180
+ workers: input.workerCount,
2181
+ workerSizing: input.workerSizing,
2182
+ chunkedEncode: input.enableChunkedEncode,
2183
+ chunkSizeFrames: input.enableChunkedEncode ? input.chunkedEncodeSize : null,
2184
+ compositionDurationSeconds: input.compositionDurationSeconds,
2185
+ totalFrames: input.totalFrames,
2186
+ resolution: { width: input.outputWidth, height: input.outputHeight },
2187
+ videoCount: input.videoCount,
2188
+ audioCount: input.audioCount,
2189
+ stages: input.perfStages,
2190
+ videoExtractBreakdown: input.videoExtractBreakdown,
2191
+ tmpPeakBytes: input.tmpPeakBytes,
2192
+ captureCalibration: input.captureCalibration ? {
2193
+ sampledFrames: input.captureCalibration.samples.map((sample) => sample.frameIndex),
2194
+ p95Ms: input.captureCalibration.estimate.p95Ms,
2195
+ multiplier: input.captureCalibration.estimate.multiplier,
2196
+ reasons: input.captureCalibration.estimate.reasons
2197
+ } : void 0,
2198
+ captureAttempts: input.captureAttempts.length > 0 ? input.captureAttempts : void 0,
2199
+ hdrDiagnostics: input.hdrDiagnostics.videoExtractionFailures > 0 || input.hdrDiagnostics.imageDecodeFailures > 0 ? { ...input.hdrDiagnostics } : void 0,
2200
+ hdrPerf: input.hdrPerf ? finalizeHdrPerf(input.hdrPerf) : void 0,
2201
+ observability: input.observability,
2202
+ captureAvgMs: input.totalFrames > 0 ? Math.round(
2203
+ (input.perfStages.captureFrameMs ?? input.perfStages.captureMs ?? 0) / input.totalFrames
2204
+ ) : void 0,
2205
+ subTimelineWait: worstSubTimelineWaitOutcome(input.dedupPerfs),
2206
+ captureP50Ms: (() => {
2207
+ const withSamples = input.dedupPerfs.filter((p) => (p.p50TotalMs ?? 0) > 0);
2208
+ if (withSamples.length === 0) return void 0;
2209
+ return withSamples.reduce((a, b) => b.frames > a.frames ? b : a).p50TotalMs;
2210
+ })(),
2211
+ peakRssMb: Math.round(input.peakRssBytes / (1024 * 1024)),
2212
+ peakHeapUsedMb: Math.round(input.peakHeapUsedBytes / (1024 * 1024)),
2213
+ staticDedup: aggregateDedup(input.dedupPerfs),
2214
+ chromeMemory: aggregateChromeMemory(input.dedupPerfs),
2215
+ beginFrameReuse: aggregateBeginFrameReuse(input.dedupPerfs),
2216
+ drawElement: aggregateDrawElement(
2217
+ input.dedupPerfs,
2218
+ input.drawElement ?? { selfVerifyFallback: false }
2219
+ ),
2220
+ host: {
2221
+ platform: platform(),
2222
+ arch: arch(),
2223
+ cpuCount: cpus().length,
2224
+ totalMemMb: Math.round(totalmem() / (1024 * 1024)),
2225
+ nodeVersion: process.version,
2226
+ gpuDisabled: input.gpuDisabled
2227
+ }
2228
+ };
2229
+ }
2230
+
2231
+ // src/services/render/captureStageError.ts
2232
+ var CaptureStageError = class extends CaptureFailure {
2233
+ browserConsole;
2234
+ constructor(input) {
2235
+ const classified = classifyCaptureFailure(input.cause);
2236
+ super({
2237
+ kind: classified.kind,
2238
+ message: normalizeErrorMessage(input.cause),
2239
+ cause: input.cause,
2240
+ workerDiagnostics: classified.workerDiagnostics,
2241
+ pageErrorLines: classified.pageErrorLines
2242
+ });
2243
+ this.name = "CaptureStageError";
2244
+ this.browserConsole = withWorkerPageErrors(input.browserConsole, [
2245
+ ...classified.pageErrorLines
2246
+ ]);
2247
+ if (input.cause instanceof Error && input.cause.stack) {
2248
+ this.stack = input.cause.stack;
2249
+ }
2250
+ }
2251
+ };
2252
+ function wrapCaptureStageError(error, browserConsole) {
2253
+ if (error instanceof CaptureStageError || error instanceof EncoderInterruptedError) return error;
2254
+ return new CaptureStageError({ cause: error, browserConsole });
2255
+ }
2256
+ function getCaptureStageBrowserConsole(error) {
2257
+ if (error instanceof CaptureStageError) return error.browserConsole;
2258
+ if (error instanceof CaptureFailure) return [...error.pageErrorLines];
2259
+ return [];
2260
+ }
2261
+
2262
+ // src/services/render/stages/captureHdrFrameShared.ts
2263
+ import {
2264
+ applyDomLayerMask as applyDomLayerMask2,
2265
+ blitRgba8OverRgb48le as blitRgba8OverRgb48le2,
2266
+ captureAlphaPng as captureAlphaPng2,
2267
+ decodePng as decodePng2,
2268
+ queryElementStacking as queryElementStacking2,
2269
+ removeDomLayerMask as removeDomLayerMask2
2270
+ } from "@hyperframes/engine";
2271
+
2054
2272
  // src/services/hdrCompositor.ts
2273
+ import { readSync as readSync2, closeSync as closeSync3 } from "fs";
2274
+ import { join as join6 } from "path";
2275
+ import {
2276
+ captureAlphaPng,
2277
+ applyDomLayerMask,
2278
+ removeDomLayerMask,
2279
+ decodePng,
2280
+ blitRgba8OverRgb48le,
2281
+ blitRgb48leRegion,
2282
+ groupIntoLayers,
2283
+ blitRgb48leAffine,
2284
+ parseTransformMatrix,
2285
+ convertTransfer,
2286
+ isVideoHiddenBeforeStreamStart
2287
+ } from "@hyperframes/engine";
2055
2288
  function countNonZeroAlpha(rgba) {
2056
2289
  let n = 0;
2057
2290
  for (let p = 3; p < rgba.length; p += 4) {
@@ -2518,7 +2751,7 @@ import {
2518
2751
  resolveVideoExtractionWindow,
2519
2752
  runFfmpeg
2520
2753
  } from "@hyperframes/engine";
2521
- import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
2754
+ import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
2522
2755
  var NO_FOLLOW_FLAG = constants2.O_NOFOLLOW ?? 0;
2523
2756
  function tempDirSafePrefix(id) {
2524
2757
  const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
@@ -2754,7 +2987,7 @@ async function extractHdrVideoFrames(args) {
2754
2987
  const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
2755
2988
  framesDir,
2756
2989
  plannedVideos,
2757
- fpsToNumber3(job.config.fps),
2990
+ fpsToNumber4(job.config.fps),
2758
2991
  log
2759
2992
  );
2760
2993
  const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
@@ -3147,282 +3380,72 @@ function planSegments(totalFrames, segmentFrames) {
3147
3380
  slices.push({
3148
3381
  index,
3149
3382
  startFrame: start,
3150
- endFrame: Math.min(totalFrames, start + segmentFrames)
3151
- });
3152
- }
3153
- return slices;
3154
- }
3155
- function resolveSegmentFrames(env) {
3156
- const raw = env.HF_SEGMENT_FRAMES;
3157
- if (raw === void 0 || raw.trim() === "") return DEFAULT_SEGMENT_FRAMES;
3158
- const parsed = Number(raw);
3159
- if (!Number.isInteger(parsed)) return DEFAULT_SEGMENT_FRAMES;
3160
- return Math.max(MIN_SEGMENT_FRAMES, parsed);
3161
- }
3162
-
3163
- // src/services/render/segmentQueue.ts
3164
- function createSegmentQueue(slices) {
3165
- let cursor = 0;
3166
- return {
3167
- next: () => slices[cursor++],
3168
- remaining: () => Math.max(0, slices.length - cursor)
3169
- };
3170
- }
3171
-
3172
- // src/services/render/segmentRecycle.ts
3173
- var DEFAULT_SEGMENT_BROWSER_RECYCLE = 3;
3174
- function resolveSegmentBrowserRecycle(env) {
3175
- const raw = env.HF_SEGMENT_BROWSER_RECYCLE;
3176
- if (raw === void 0 || raw.trim() === "") return DEFAULT_SEGMENT_BROWSER_RECYCLE;
3177
- const parsed = Number(raw);
3178
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : DEFAULT_SEGMENT_BROWSER_RECYCLE;
3179
- }
3180
- var TARGET_LOSS_PATTERNS = [/target closed/i, /detached frame/i, /session closed/i];
3181
- function isTargetLossError(error) {
3182
- if (!(error instanceof Error)) return false;
3183
- return TARGET_LOSS_PATTERNS.some((re) => re.test(error.message));
3184
- }
3185
-
3186
- // src/services/render/stages/captureStreamingStage.ts
3187
- import { mkdtemp, writeFile } from "node:fs/promises";
3188
- import { tmpdir } from "node:os";
3189
- import { join as join8 } from "node:path";
3190
- import {
3191
- DrawElementVerificationError,
3192
- captureFrameToBuffer,
3193
- captureFrameToBufferPipelined,
3194
- captureFramesBatchPipelined,
3195
- closeCaptureSession,
3196
- createCaptureSession,
3197
- createFrameReorderBuffer,
3198
- distributeFrames,
3199
- distributeFramesInterleaved,
3200
- executeParallelCapture,
3201
- getCapturePerfSummary,
3202
- psnrDb,
3203
- resolveDeVerifyMinDb,
3204
- recaptureDrawElementFrameForVerify,
3205
- completeDeferredDrawElementInit,
3206
- initializeSession,
3207
- prepareCaptureSessionForReuse,
3208
- spawnStreamingEncoder
3209
- } from "@hyperframes/engine";
3210
-
3211
- // src/services/render/perfSummary.ts
3212
- import { arch, cpus, platform, totalmem } from "node:os";
3213
- import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
3214
- function worstSubTimelineWaitOutcome(perfs) {
3215
- const outcomes = perfs.map((p) => p.subTimelineWaitOutcome).filter((o) => !!o);
3216
- if (outcomes.length === 0) return void 0;
3217
- if (outcomes.includes("script_failure")) return "script_failure";
3218
- if (outcomes.includes("timeout")) return "timeout";
3219
- return "ready";
3220
- }
3221
- function pushWorkerDedupPerfs(results, sink) {
3222
- for (const r of results) {
3223
- if (r.perf) sink.push(r.perf);
3224
- }
3225
- }
3226
- function roundDb(value) {
3227
- return value === void 0 ? void 0 : Math.round(Math.min(value, 999) * 10) / 10;
3228
- }
3229
- function aggregateDrawElement(perfs, de) {
3230
- if (perfs.length === 0) return void 0;
3231
- const modes = [...new Set(perfs.map((p) => p.captureMode).filter(Boolean))].sort();
3232
- const gateReasons = [
3233
- ...new Set(
3234
- [de.probeGateReason, ...perfs.map((p) => p.deGateReason)].filter((r) => !!r)
3235
- )
3236
- ].sort();
3237
- const gpuRenderers = [
3238
- ...new Set(perfs.map((p) => p.gpuRenderer).filter((r) => !!r))
3239
- ].sort();
3240
- const drain = de.drainStats;
3241
- return {
3242
- mode: modes.join("|") || "unknown",
3243
- compileGate: de.compileGate,
3244
- clampReason: de.clampReason,
3245
- workerInversion: de.workerInversion ?? "none",
3246
- preInversionWorkers: de.preInversionWorkers,
3247
- compositionElementCount: de.compositionElementCount,
3248
- compositionElementCountSource: de.compositionElementCountSource,
3249
- compositionElementTags: de.compositionElementTags,
3250
- arollVideoCount: de.arollVideoCount,
3251
- heygenVideoCount: de.heygenVideoCount,
3252
- adaptersUsed: de.adaptersUsed,
3253
- audioCount: de.audioCount,
3254
- imageCount: de.imageCount,
3255
- subCompositionCount: de.subCompositionCount,
3256
- audioGroupCount: de.audioGroupCount,
3257
- colorGradingCount: de.colorGradingCount,
3258
- hasLut: de.hasLut,
3259
- rootBodyMismatch: de.rootBodyMismatch,
3260
- rootBodyDeltaPxBucket: de.rootBodyDeltaPxBucket,
3261
- vfxHostCount: de.vfxHostCount,
3262
- vfxCapture: de.vfxCapture,
3263
- vfxTypes: de.vfxTypes,
3264
- shortBand: de.shortBand,
3265
- parallelRouter: de.parallelRouter ?? "none",
3266
- preRouterWorkers: de.preRouterWorkers,
3267
- gateReason: gateReasons.length > 0 ? gateReasons.join("|") : void 0,
3268
- gpuRenderer: gpuRenderers.length > 0 ? gpuRenderers.join("|") : void 0,
3269
- workerEncode: perfs.some((p) => p.deWorkerEncode),
3270
- verifyArmed: perfs.reduce((sum2, p) => sum2 + (p.deVerifyArmed ?? 0), 0),
3271
- verifyChecked: drain?.verifyChecked ?? 0,
3272
- verifyMinDb: roundDb(drain?.verifyMinDb),
3273
- verifyInitMs: perfs.reduce((sum2, p) => sum2 + (p.deVerifyInitMs ?? 0), 0),
3274
- selfVerifyFallback: de.selfVerifyFallback,
3275
- fallbackReason: de.fallbackReason,
3276
- fallbackFailedDb: roundDb(de.fallbackFailedDb),
3277
- fallbackFrameIndex: de.fallbackFrameIndex,
3278
- fallbackThresholdDb: roundDb(de.fallbackThresholdDb),
3279
- blankSuspects: drain?.blankSuspects ?? 0,
3280
- blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
3281
- blankRecaptures: drain?.blankRecaptures ?? 0,
3282
- boundaryFrames: perfs.reduce((sum2, p) => sum2 + (p.deBoundaryFrames ?? 0), 0),
3283
- ncprFallbacks: perfs.reduce((sum2, p) => sum2 + (p.deNcprFallbacks ?? 0), 0),
3284
- frameTimeouts: perfs.reduce((sum2, p) => sum2 + (p.deFrameTimeouts ?? 0), 0)
3285
- };
3286
- }
3287
- function maxDefined(values) {
3288
- const present = values.filter((v) => typeof v === "number");
3289
- return present.length > 0 ? Math.max(...present) : void 0;
3290
- }
3291
- function aggregateChromeMemory(perfs) {
3292
- const sampled = perfs.filter((p) => (p.chromeMemorySamples ?? 0) > 0);
3293
- if (sampled.length === 0) return void 0;
3294
- return {
3295
- browserRssPeakMb: maxDefined(sampled.map((p) => p.chromeBrowserRssPeakMb)),
3296
- rendererRssPeakMb: maxDefined(sampled.map((p) => p.chromeRendererRssPeakMb)),
3297
- // Last samples are per session; summing approximates the whole fleet's
3298
- // footprint at the end of capture (workers run concurrently).
3299
- rssLastMb: sampled.reduce((sum2, p) => sum2 + (p.chromeRssLastMb ?? 0), 0),
3300
- gpuProcessSeenLastSample: sampled.some((p) => p.chromeGpuProcessSeenLastSample === true),
3301
- samples: sampled.reduce((sum2, p) => sum2 + (p.chromeMemorySamples ?? 0), 0)
3302
- };
3303
- }
3304
- function aggregateDedup(perfs) {
3305
- if (perfs.length === 0) return void 0;
3306
- const armed = perfs.some((p) => p.staticDedupArmed);
3307
- const skipReasons = armed ? [] : [
3308
- ...new Set(perfs.map((p) => p.staticDedupSkipReason).filter((r) => !!r))
3309
- ].sort();
3310
- const verificationPerfs = perfs.filter((perf) => perf.staticDedupVerificationOutcome);
3311
- const verificationOutcomes = [
3312
- ...new Set(
3313
- verificationPerfs.map((perf) => perf.staticDedupVerificationOutcome).filter((outcome) => outcome != null)
3314
- )
3315
- ].sort();
3316
- return {
3317
- enabled: perfs.some((p) => p.staticDedupEnabled),
3318
- armed,
3319
- predictedFrames: perfs.reduce((sum2, p) => sum2 + (p.staticDedupPredicted ?? 0), 0),
3320
- reusedFrames: perfs.reduce((sum2, p) => sum2 + (p.staticDedupReused ?? 0), 0),
3321
- skipReason: skipReasons.length > 0 ? skipReasons.join("|") : void 0,
3322
- ...verificationPerfs.length === 0 ? {} : {
3323
- verifiedFrames: verificationPerfs.reduce(
3324
- (sum2, perf) => sum2 + (perf.staticDedupVerified ?? 0),
3325
- 0
3326
- ),
3327
- verificationOutcomes,
3328
- plannedRuns: verificationPerfs.reduce(
3329
- (sum2, perf) => sum2 + (perf.staticDedupVerificationPlannedRuns ?? 0),
3330
- 0
3331
- ),
3332
- completedRuns: verificationPerfs.reduce(
3333
- (sum2, perf) => sum2 + (perf.staticDedupVerificationCompletedRuns ?? 0),
3334
- 0
3335
- ),
3336
- screenshots: verificationPerfs.reduce(
3337
- (sum2, perf) => sum2 + (perf.staticDedupVerificationScreenshots ?? 0),
3338
- 0
3339
- ),
3340
- seeks: verificationPerfs.reduce(
3341
- (sum2, perf) => sum2 + (perf.staticDedupVerificationSeeks ?? 0),
3342
- 0
3343
- ),
3344
- comparisons: verificationPerfs.reduce(
3345
- (sum2, perf) => sum2 + (perf.staticDedupVerificationComparisons ?? 0),
3346
- 0
3347
- ),
3348
- verificationElapsedMs: verificationPerfs.reduce(
3349
- (sum2, perf) => sum2 + (perf.staticDedupVerificationElapsedMs ?? 0),
3350
- 0
3351
- )
3352
- }
3353
- };
3383
+ endFrame: Math.min(totalFrames, start + segmentFrames)
3384
+ });
3385
+ }
3386
+ return slices;
3354
3387
  }
3355
- function aggregateBeginFrameReuse(perfs) {
3356
- const noDamageFrames = perfs.reduce((sum2, p) => sum2 + (p.beginFrameNoDamage ?? 0), 0);
3357
- const hasDamageFrames = perfs.reduce((sum2, p) => sum2 + (p.beginFrameHasDamage ?? 0), 0);
3358
- if (noDamageFrames + hasDamageFrames === 0) return void 0;
3359
- return { noDamageFrames, hasDamageFrames };
3388
+ function resolveSegmentFrames(env) {
3389
+ const raw = env.HF_SEGMENT_FRAMES;
3390
+ if (raw === void 0 || raw.trim() === "") return DEFAULT_SEGMENT_FRAMES;
3391
+ const parsed = Number(raw);
3392
+ if (!Number.isInteger(parsed)) return DEFAULT_SEGMENT_FRAMES;
3393
+ return Math.max(MIN_SEGMENT_FRAMES, parsed);
3360
3394
  }
3361
- function buildRenderPerfSummary(input) {
3395
+
3396
+ // src/services/render/segmentQueue.ts
3397
+ function createSegmentQueue(slices) {
3398
+ let cursor = 0;
3362
3399
  return {
3363
- renderId: input.job.id,
3364
- totalElapsedMs: input.totalElapsedMs,
3365
- // RenderPerfSummary surfaces fps as a decimal because it lands in JSON
3366
- // payloads (CLI telemetry, regression-harness reports) where a single
3367
- // number is friendlier than `{num,den}`. Callers needing the rational
3368
- // back can read `job.config.fps`.
3369
- fps: fpsToNumber4(input.job.config.fps),
3370
- quality: input.job.config.quality,
3371
- workers: input.workerCount,
3372
- workerSizing: input.workerSizing,
3373
- chunkedEncode: input.enableChunkedEncode,
3374
- chunkSizeFrames: input.enableChunkedEncode ? input.chunkedEncodeSize : null,
3375
- compositionDurationSeconds: input.compositionDurationSeconds,
3376
- totalFrames: input.totalFrames,
3377
- resolution: { width: input.outputWidth, height: input.outputHeight },
3378
- videoCount: input.videoCount,
3379
- audioCount: input.audioCount,
3380
- stages: input.perfStages,
3381
- videoExtractBreakdown: input.videoExtractBreakdown,
3382
- tmpPeakBytes: input.tmpPeakBytes,
3383
- captureCalibration: input.captureCalibration ? {
3384
- sampledFrames: input.captureCalibration.samples.map((sample) => sample.frameIndex),
3385
- p95Ms: input.captureCalibration.estimate.p95Ms,
3386
- multiplier: input.captureCalibration.estimate.multiplier,
3387
- reasons: input.captureCalibration.estimate.reasons
3388
- } : void 0,
3389
- captureAttempts: input.captureAttempts.length > 0 ? input.captureAttempts : void 0,
3390
- hdrDiagnostics: input.hdrDiagnostics.videoExtractionFailures > 0 || input.hdrDiagnostics.imageDecodeFailures > 0 ? { ...input.hdrDiagnostics } : void 0,
3391
- hdrPerf: input.hdrPerf ? finalizeHdrPerf(input.hdrPerf) : void 0,
3392
- observability: input.observability,
3393
- captureAvgMs: input.totalFrames > 0 ? Math.round(
3394
- (input.perfStages.captureFrameMs ?? input.perfStages.captureMs ?? 0) / input.totalFrames
3395
- ) : void 0,
3396
- subTimelineWait: worstSubTimelineWaitOutcome(input.dedupPerfs),
3397
- captureP50Ms: (() => {
3398
- const withSamples = input.dedupPerfs.filter((p) => (p.p50TotalMs ?? 0) > 0);
3399
- if (withSamples.length === 0) return void 0;
3400
- return withSamples.reduce((a, b) => b.frames > a.frames ? b : a).p50TotalMs;
3401
- })(),
3402
- peakRssMb: Math.round(input.peakRssBytes / (1024 * 1024)),
3403
- peakHeapUsedMb: Math.round(input.peakHeapUsedBytes / (1024 * 1024)),
3404
- staticDedup: aggregateDedup(input.dedupPerfs),
3405
- chromeMemory: aggregateChromeMemory(input.dedupPerfs),
3406
- beginFrameReuse: aggregateBeginFrameReuse(input.dedupPerfs),
3407
- drawElement: aggregateDrawElement(
3408
- input.dedupPerfs,
3409
- input.drawElement ?? { selfVerifyFallback: false }
3410
- ),
3411
- host: {
3412
- platform: platform(),
3413
- arch: arch(),
3414
- cpuCount: cpus().length,
3415
- totalMemMb: Math.round(totalmem() / (1024 * 1024)),
3416
- nodeVersion: process.version,
3417
- gpuDisabled: input.gpuDisabled
3418
- }
3400
+ next: () => slices[cursor++],
3401
+ remaining: () => Math.max(0, slices.length - cursor)
3419
3402
  };
3420
3403
  }
3421
3404
 
3405
+ // src/services/render/segmentRecycle.ts
3406
+ var DEFAULT_SEGMENT_BROWSER_RECYCLE = 3;
3407
+ function resolveSegmentBrowserRecycle(env) {
3408
+ const raw = env.HF_SEGMENT_BROWSER_RECYCLE;
3409
+ if (raw === void 0 || raw.trim() === "") return DEFAULT_SEGMENT_BROWSER_RECYCLE;
3410
+ const parsed = Number(raw);
3411
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : DEFAULT_SEGMENT_BROWSER_RECYCLE;
3412
+ }
3413
+ var TARGET_LOSS_PATTERNS = [/target closed/i, /detached frame/i, /session closed/i];
3414
+ function isTargetLossError(error) {
3415
+ if (!(error instanceof Error)) return false;
3416
+ return TARGET_LOSS_PATTERNS.some((re) => re.test(error.message));
3417
+ }
3418
+
3422
3419
  // src/services/render/stages/captureStreamingStage.ts
3420
+ import { mkdtemp, writeFile } from "node:fs/promises";
3421
+ import { tmpdir } from "node:os";
3422
+ import { join as join8 } from "node:path";
3423
+ import {
3424
+ DrawElementVerificationError,
3425
+ captureFrameToBuffer,
3426
+ captureFrameToBufferPipelined,
3427
+ captureFramesBatchPipelined,
3428
+ closeCaptureSession,
3429
+ createCaptureSession,
3430
+ createFrameReorderBuffer,
3431
+ distributeFrames,
3432
+ distributeFramesInterleaved,
3433
+ executeParallelCapture,
3434
+ getCapturePerfSummary,
3435
+ psnrDb,
3436
+ resolveDeVerifyMinDb,
3437
+ recaptureDrawElementFrameForVerify,
3438
+ completeDeferredDrawElementInit,
3439
+ initializeSession,
3440
+ prepareCaptureSessionForReuse,
3441
+ spawnStreamingEncoder
3442
+ } from "@hyperframes/engine";
3423
3443
  var DEFAULT_CAPTURE_STALL_MS = 6e4;
3424
3444
  var DE_STALL_POLL_MS = 5e3;
3425
3445
  var STALL_ADVICE = "Chrome stopped answering. If another app is keeping the GPU or CPU busy (a 3D render, a game, another export), let it finish or render one job at a time, then retry.";
3446
+ function poolFailure(error, probeSession) {
3447
+ return wrapCaptureStageError(error, probeSession?.browserConsoleBuffer ?? []);
3448
+ }
3426
3449
  var ParallelCaptureStallError = class extends Error {
3427
3450
  variant;
3428
3451
  constructor(args) {
@@ -3869,7 +3892,7 @@ async function runCaptureStreamingStage(input) {
3869
3892
  }
3870
3893
  );
3871
3894
  } catch (err) {
3872
- if (parallelDrainError) throw parallelDrainError;
3895
+ if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
3873
3896
  if (stalled && abortSignal?.aborted !== true) {
3874
3897
  throw new ParallelCaptureStallError({
3875
3898
  variant: "pool",
@@ -3880,13 +3903,15 @@ async function runCaptureStreamingStage(input) {
3880
3903
  phaseSummary: phaseSummary()
3881
3904
  });
3882
3905
  }
3883
- throw err;
3906
+ throw poolFailure(err, probeSession);
3884
3907
  } finally {
3885
3908
  clearInterval(stallTimer);
3886
3909
  abortSignal?.removeEventListener("abort", forwardParentAbort);
3887
3910
  }
3888
- if (parallelDrainError) throw parallelDrainError;
3911
+ if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
3889
3912
  pushWorkerDedupPerfs(workerResults, dedupPerfs);
3913
+ const workerPageErrors = [];
3914
+ pushWorkerPageErrors(workerResults, workerPageErrors);
3890
3915
  if (parallelGuardRan) {
3891
3916
  deDrainStats = parallelStats;
3892
3917
  }
@@ -3896,6 +3921,7 @@ async function runCaptureStreamingStage(input) {
3896
3921
  await closeCaptureSession(probeSession);
3897
3922
  probeSession = null;
3898
3923
  }
3924
+ lastBrowserConsole = withWorkerPageErrors(lastBrowserConsole, workerPageErrors);
3899
3925
  } else {
3900
3926
  const videoInjector = createRenderVideoFrameInjector();
3901
3927
  const session = probeSession ?? await createCaptureSession(
@@ -4342,8 +4368,9 @@ async function runCaptureSegmentedStage(input) {
4342
4368
 
4343
4369
  // src/services/render/segmentManifest.ts
4344
4370
  import { createHash as createHash2 } from "node:crypto";
4345
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
4371
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
4346
4372
  import { join as join10 } from "node:path";
4373
+ import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
4347
4374
  var MANIFEST_FILENAME = "segments.json";
4348
4375
  function computeSegmentPlanHash(input) {
4349
4376
  const ordered = [
@@ -4399,7 +4426,7 @@ function writeSegmentManifest(segmentDir, manifest) {
4399
4426
  mkdirSync4(segmentDir, { recursive: true });
4400
4427
  const tmp = join10(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4401
4428
  writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
4402
- renameSync2(tmp, join10(segmentDir, MANIFEST_FILENAME));
4429
+ renameWithRetry2(tmp, join10(segmentDir, MANIFEST_FILENAME));
4403
4430
  }
4404
4431
  async function probeSegmentFrameCount(path, probe) {
4405
4432
  try {
@@ -5247,6 +5274,8 @@ import {
5247
5274
  rewriteInlineStyleAssetUrls
5248
5275
  } from "@hyperframes/core";
5249
5276
  import { MAX_AUDIO_GAIN } from "@hyperframes/core/audio-gain";
5277
+ import { readPlaybackRate } from "@hyperframes/parsers/media-duration";
5278
+ import { resolveRateSpec } from "@hyperframes/core/speed-ramp";
5250
5279
  import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
5251
5280
  import {
5252
5281
  assignBundledRuntimeCompositionIds,
@@ -5451,19 +5480,11 @@ import {
5451
5480
 
5452
5481
  // src/services/animatedGifPrep.ts
5453
5482
  import { createHash as createHash4 } from "node:crypto";
5454
- import {
5455
- copyFileSync as copyFileSync2,
5456
- existsSync as existsSync8,
5457
- mkdirSync as mkdirSync5,
5458
- readFileSync as readFileSync5,
5459
- renameSync as renameSync3,
5460
- rmSync as rmSync6,
5461
- statSync as statSync4
5462
- } from "node:fs";
5483
+ import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync6, statSync as statSync4 } from "node:fs";
5463
5484
  import { dirname as dirname5, isAbsolute as isAbsolute2, join as join12, resolve as resolve5 } from "node:path";
5464
5485
  import { parseHTML as parseHTML3 } from "linkedom";
5465
5486
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
5466
- import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2 } from "@hyperframes/engine";
5487
+ import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
5467
5488
  var PREPARED_GIF_SUBDIR = "_animated_gif";
5468
5489
  var CACHE_SCHEMA = "hfgif-v1";
5469
5490
  function splitUrlSuffix(src) {
@@ -5642,7 +5663,7 @@ async function ensurePreparedWebm(input) {
5642
5663
  throw new Error("Animated GIF transcode produced an empty output");
5643
5664
  }
5644
5665
  if (!isUsableFile(input.cachePath)) {
5645
- renameSync3(tmpPath, input.cachePath);
5666
+ renameWithRetry3(tmpPath, input.cachePath);
5646
5667
  } else {
5647
5668
  rmSync6(tmpPath, { force: true });
5648
5669
  }
@@ -7230,6 +7251,9 @@ async function discoverMediaFromBrowser(page) {
7230
7251
  const intrinsicDuration = isImage ? 0 : htmlEl.duration;
7231
7252
  const playbackStartRaw = htmlEl.getAttribute("data-playback-start");
7232
7253
  const mediaStartRaw = htmlEl.getAttribute("data-media-start");
7254
+ const playbackRateRaw = htmlEl.getAttribute("data-playback-rate");
7255
+ const automationRaw = htmlEl.getAttribute("data-automation");
7256
+ const defaultPlaybackRate = isImage ? 1 : htmlEl.defaultPlaybackRate;
7233
7257
  const loop = htmlEl.hasAttribute("loop");
7234
7258
  const hasAudio = htmlEl.getAttribute("data-has-audio") === "true";
7235
7259
  const volume = parseFloat(htmlEl.getAttribute("data-volume") || "1");
@@ -7245,6 +7269,9 @@ async function discoverMediaFromBrowser(page) {
7245
7269
  durationInferred,
7246
7270
  playbackStartRaw,
7247
7271
  mediaStartRaw,
7272
+ playbackRateRaw,
7273
+ automationRaw,
7274
+ defaultPlaybackRate,
7248
7275
  loop,
7249
7276
  hasAudio,
7250
7277
  volume,
@@ -7254,7 +7281,17 @@ async function discoverMediaFromBrowser(page) {
7254
7281
  return results;
7255
7282
  });
7256
7283
  return elements.map(
7257
- ({ endRaw, durationRaw, intrinsicDuration, playbackStartRaw, mediaStartRaw, ...element }) => ({
7284
+ ({
7285
+ endRaw,
7286
+ durationRaw,
7287
+ intrinsicDuration,
7288
+ playbackStartRaw,
7289
+ mediaStartRaw,
7290
+ playbackRateRaw,
7291
+ automationRaw,
7292
+ defaultPlaybackRate,
7293
+ ...element
7294
+ }) => ({
7258
7295
  ...element,
7259
7296
  end: parseStrictFiniteTimingNumber(endRaw) ?? 0,
7260
7297
  duration: element.durationInferred && Number.isFinite(intrinsicDuration) && intrinsicDuration > 0 ? intrinsicDuration : parseStrictFiniteTimingNumber(durationRaw) ?? 0,
@@ -7264,7 +7301,14 @@ async function discoverMediaFromBrowser(page) {
7264
7301
  if (name === "data-media-start") return mediaStartRaw;
7265
7302
  return null;
7266
7303
  }
7267
- })
7304
+ }),
7305
+ playbackRate: resolveRateSpec(
7306
+ automationRaw,
7307
+ readPlaybackRate(
7308
+ (name) => name === "data-playback-rate" ? playbackRateRaw : null,
7309
+ defaultPlaybackRate
7310
+ )
7311
+ )
7268
7312
  })
7269
7313
  );
7270
7314
  }
@@ -7684,9 +7728,18 @@ import {
7684
7728
  getCompositionDuration,
7685
7729
  initializeSession as initializeSession4,
7686
7730
  isTransientBrowserError,
7687
- probeBeginFrameLiveness
7731
+ probeBeginFrameLiveness,
7732
+ videoAudioId
7688
7733
  } from "@hyperframes/engine";
7689
- import { MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2, fpsToNumber as fpsToNumber7, readMediaStart as readMediaStart2 } from "@hyperframes/core";
7734
+ import {
7735
+ MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
7736
+ fpsToNumber as fpsToNumber7,
7737
+ normalizeRateSpec as normalizeRateSpec3,
7738
+ readMediaStart as readMediaStart2,
7739
+ shiftRateLane as shiftRateLane2,
7740
+ sourceTimeAt as sourceTimeAt3,
7741
+ timeAtSourceTime
7742
+ } from "@hyperframes/core";
7690
7743
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
7691
7744
 
7692
7745
  // src/services/render/stages/probeFailures.ts
@@ -7745,16 +7798,28 @@ function authoredMediaStarts(html) {
7745
7798
  }
7746
7799
  return starts;
7747
7800
  }
7748
- function adoptRuntimeMediaStart(composition, el, authored) {
7801
+ function isVideoAudio(audio, videoId) {
7802
+ return audio.type === "video" && audio.id === videoAudioId(videoId);
7803
+ }
7804
+ function adoptRuntimeMediaTiming(composition, el, authored) {
7749
7805
  const shift = el.mediaStart - authored;
7750
- if (el.mediaStart <= 0 || Math.abs(shift) <= BROWSER_MEDIA_EPSILON) return;
7751
7806
  const entries = el.tagName === "video" ? [
7752
7807
  ...composition.videos.filter((video) => video.id === el.id),
7753
- ...composition.audios.filter(
7754
- (audio) => audio.type === "video" && audio.id === `${el.id}-audio`
7755
- )
7808
+ ...composition.audios.filter((audio) => isVideoAudio(audio, el.id))
7756
7809
  ] : composition.audios.filter((audio) => audio.type !== "video" && audio.id === el.id);
7757
- for (const entry of entries) entry.mediaStart += shift;
7810
+ for (const entry of entries) {
7811
+ if (typeof entry.playbackRate !== "object") {
7812
+ if (typeof el.playbackRate === "object") {
7813
+ const consumed = Math.max(0, entry.mediaStart - authored);
7814
+ const head = timeAtSourceTime(normalizeRateSpec3(entry.playbackRate), consumed);
7815
+ entry.mediaStart += sourceTimeAt3(el.playbackRate, head) - consumed;
7816
+ entry.playbackRate = shiftRateLane2(el.playbackRate, head);
7817
+ } else {
7818
+ entry.playbackRate = el.playbackRate;
7819
+ }
7820
+ }
7821
+ if (el.mediaStart > 0 && Math.abs(shift) > BROWSER_MEDIA_EPSILON) entry.mediaStart += shift;
7822
+ }
7758
7823
  }
7759
7824
  function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, durationInferred) {
7760
7825
  if (projectedEnd <= 0) return existingEnd;
@@ -8038,7 +8103,7 @@ async function runProbeStage(input) {
8038
8103
  sourceChanged,
8039
8104
  el.durationInferred
8040
8105
  );
8041
- adoptRuntimeMediaStart(
8106
+ adoptRuntimeMediaTiming(
8042
8107
  composition,
8043
8108
  el,
8044
8109
  authoredStarts.get(el.id) ?? existing.mediaStart
@@ -8061,6 +8126,7 @@ async function runProbeStage(input) {
8061
8126
  start: el.start,
8062
8127
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
8063
8128
  mediaStart: el.mediaStart,
8129
+ playbackRate: el.playbackRate,
8064
8130
  loop: el.loop,
8065
8131
  hasAudio: el.hasAudio && !el.muted
8066
8132
  });
@@ -8085,7 +8151,7 @@ async function runProbeStage(input) {
8085
8151
  sourceChanged,
8086
8152
  el.durationInferred
8087
8153
  );
8088
- adoptRuntimeMediaStart(
8154
+ adoptRuntimeMediaTiming(
8089
8155
  composition,
8090
8156
  el,
8091
8157
  authoredStarts.get(el.id) ?? existing.mediaStart
@@ -8103,6 +8169,7 @@ async function runProbeStage(input) {
8103
8169
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
8104
8170
  mediaStart: el.mediaStart,
8105
8171
  loop: el.loop,
8172
+ playbackRate: el.playbackRate,
8106
8173
  layer: 0,
8107
8174
  volume: el.volume,
8108
8175
  type: "audio"
@@ -8363,7 +8430,8 @@ import {
8363
8430
  isHttpUrl as isHttpUrl2,
8364
8431
  resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
8365
8432
  runVideoExtractionWithRetry,
8366
- safeVideoExtractionSourceIdentity
8433
+ safeVideoExtractionSourceIdentity,
8434
+ videoAudioId as videoAudioId2
8367
8435
  } from "@hyperframes/engine";
8368
8436
 
8369
8437
  // src/services/render/extractionFailureMetadata.ts
@@ -8721,7 +8789,7 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
8721
8789
  const video = composition.videos.find((v) => v.id === ext.videoId);
8722
8790
  if (!video || !video.hasAudio || video.hidden || existingAudioSrcs.has(video.src)) continue;
8723
8791
  composition.audios.push({
8724
- id: `${video.id}-audio`,
8792
+ id: videoAudioId2(video.id),
8725
8793
  src: video.src,
8726
8794
  start: video.start,
8727
8795
  end: video.end,
@@ -8747,7 +8815,7 @@ import {
8747
8815
 
8748
8816
  // src/services/render/audioPadTrim.ts
8749
8817
  import { spawn } from "node:child_process";
8750
- import { mkdtempSync as mkdtempSync3, renameSync as renameSync4, rmSync as rmSync7 } from "node:fs";
8818
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync7 } from "node:fs";
8751
8819
  import { dirname as dirname7, join as join17 } from "node:path";
8752
8820
  import {
8753
8821
  DEFAULT_CONFIG,
@@ -8756,6 +8824,7 @@ import {
8756
8824
  extractAudioMetadata as extractAudioMetadata3,
8757
8825
  getFfprobeBinary,
8758
8826
  ManagedChildProcess,
8827
+ renameWithRetry as renameWithRetry4,
8759
8828
  runFfmpeg as runFfmpeg4,
8760
8829
  trackChildProcess
8761
8830
  } from "@hyperframes/engine";
@@ -9011,7 +9080,7 @@ async function enforceAacTruePeak(input) {
9011
9080
  }
9012
9081
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
9013
9082
  if (measuredPath !== correctedPath) return { success: true };
9014
- renameSync4(correctedPath, input.audioPath);
9083
+ renameWithRetry4(correctedPath, input.audioPath);
9015
9084
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
9016
9085
  }
9017
9086
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -9201,6 +9270,7 @@ async function runAudioStage(input) {
9201
9270
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
9202
9271
  let audioLoweredDb;
9203
9272
  if (result.hasAudio) {
9273
+ const guardStart = Date.now();
9204
9274
  const peak = await constrainAacTruePeak({
9205
9275
  audioPath: result.audioOutputPath,
9206
9276
  sourcePath: pcmSourcePath,
@@ -9210,6 +9280,7 @@ async function runAudioStage(input) {
9210
9280
  });
9211
9281
  if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
9212
9282
  audioLoweredDb = peak.loweredDb;
9283
+ input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
9213
9284
  }
9214
9285
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9215
9286
  } finally {
@@ -9235,6 +9306,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9235
9306
  let hasAudio = false;
9236
9307
  let audioError;
9237
9308
  let audioFailures;
9309
+ let audioPhaseMs;
9238
9310
  if (audios.length > 0) {
9239
9311
  let audioResult;
9240
9312
  try {
@@ -9284,6 +9356,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9284
9356
  assertAudioNotInterrupted(audioResult.failures);
9285
9357
  hasAudio = audioResult.success;
9286
9358
  audioFailures = audioResult.failures;
9359
+ audioPhaseMs = audioResult.phaseMs;
9287
9360
  if (!hasAudio) {
9288
9361
  audioError = audioResult.error ?? "audio mix failed for an unknown reason";
9289
9362
  for (const failure of audioResult.failures ?? []) {
@@ -9302,7 +9375,8 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9302
9375
  audioOutputPath,
9303
9376
  hasAudio,
9304
9377
  audioError,
9305
- audioFailures
9378
+ audioFailures,
9379
+ audioPhaseMs
9306
9380
  };
9307
9381
  }
9308
9382
 
@@ -9431,6 +9505,7 @@ async function runCaptureStage(input) {
9431
9505
  let { workerCount } = plan2;
9432
9506
  const { forceScreenshot } = plan2;
9433
9507
  let lastBrowserConsole = [];
9508
+ const workerPageErrors = [];
9434
9509
  let captureBeyondViewport = probeSession?.options.captureBeyondViewport;
9435
9510
  const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
9436
9511
  if (frameRange !== void 0) {
@@ -9468,6 +9543,7 @@ async function runCaptureStage(input) {
9468
9543
  abortSignal,
9469
9544
  frameRangeStart: frameRange?.startFrame,
9470
9545
  dedupPerfs,
9546
+ pageErrorLines: workerPageErrors,
9471
9547
  onProgress: (progress) => {
9472
9548
  if (progress.latestWorkerPhase) {
9473
9549
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -9488,6 +9564,11 @@ async function runCaptureStage(input) {
9488
9564
  },
9489
9565
  cfg: captureCfg,
9490
9566
  log
9567
+ }).catch((error) => {
9568
+ throw wrapCaptureStageError(
9569
+ error,
9570
+ withWorkerPageErrors(probeSession?.browserConsoleBuffer ?? [], workerPageErrors)
9571
+ );
9491
9572
  });
9492
9573
  captureAttempts.push(...attempts);
9493
9574
  const lastAttempt = attempts[attempts.length - 1];
@@ -9500,6 +9581,7 @@ async function runCaptureStage(input) {
9500
9581
  await closeCaptureSession5(probeSession);
9501
9582
  probeSession = null;
9502
9583
  }
9584
+ lastBrowserConsole = withWorkerPageErrors(lastBrowserConsole, workerPageErrors);
9503
9585
  } else {
9504
9586
  const sequential = await runSequentialDiskCapture(
9505
9587
  input,
@@ -10815,7 +10897,7 @@ function videoEncoderOptions(input, engineCfg) {
10815
10897
  };
10816
10898
  }
10817
10899
  function canEncodeWhileCapturing(input, workerCount, cpuCount) {
10818
- 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" && input.lockGopForChunkConcat !== true;
10900
+ 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";
10819
10901
  }
10820
10902
  async function readLandedFrame(workerPath, mergedPath) {
10821
10903
  if (workerPath !== void 0) {
@@ -11470,6 +11552,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11470
11552
  options.cfg
11471
11553
  );
11472
11554
  pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
11555
+ if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
11473
11556
  } finally {
11474
11557
  await mergeWorkerFrames(attemptWorkDir, tasks, options.framesDir);
11475
11558
  }
@@ -11732,13 +11815,36 @@ function colorGradingValueHasLut(rawAttributeValue) {
11732
11815
  return false;
11733
11816
  }
11734
11817
  }
11735
- async function resolveCompositionElementCount(probeSession, html) {
11818
+ var PROBE_READ_TIMEOUT_MS = 5e3;
11819
+ async function evaluateWithinBound(page, fn, what, log) {
11820
+ let timer;
11821
+ const timedOut = new Promise((_, reject) => {
11822
+ timer = setTimeout(
11823
+ () => reject(new Error(`no answer within ${PROBE_READ_TIMEOUT_MS} ms (page busy)`)),
11824
+ PROBE_READ_TIMEOUT_MS
11825
+ );
11826
+ });
11827
+ try {
11828
+ return await Promise.race([page.evaluate(fn), timedOut]);
11829
+ } catch (err) {
11830
+ log.warn(`[Render] live ${what} read skipped; using the static answer`, {
11831
+ error: err instanceof Error ? err.message : String(err)
11832
+ });
11833
+ throw err;
11834
+ } finally {
11835
+ clearTimeout(timer);
11836
+ }
11837
+ }
11838
+ async function resolveCompositionElementCount(probeSession, html, log = defaultLogger) {
11736
11839
  if (probeSession?.isInitialized) {
11737
11840
  try {
11738
- const liveCount = await probeSession.page.evaluate(
11841
+ const liveCount = await evaluateWithinBound(
11842
+ probeSession.page,
11739
11843
  // Live HTMLCollection length — avoids materializing a static NodeList
11740
11844
  // on the large-DOM comps this gate exists to catch (review nit).
11741
- () => document.getElementsByTagName("*").length
11845
+ () => document.getElementsByTagName("*").length,
11846
+ "element count",
11847
+ log
11742
11848
  );
11743
11849
  if (typeof liveCount === "number" && Number.isFinite(liveCount)) {
11744
11850
  return { count: liveCount, source: "live" };
@@ -11826,11 +11932,11 @@ function probeAdaptersUsed() {
11826
11932
  }
11827
11933
  return used;
11828
11934
  }
11829
- async function resolveAdaptersUsed(probeSession, html) {
11935
+ async function resolveAdaptersUsed(probeSession, html, log = defaultLogger) {
11830
11936
  const staticAdapters = detectAdaptersStatic(html);
11831
11937
  if (!probeSession?.isInitialized) return staticAdapters;
11832
11938
  try {
11833
- const live = await probeSession.page.evaluate(probeAdaptersUsed);
11939
+ const live = await evaluateWithinBound(probeSession.page, probeAdaptersUsed, "adapters", log);
11834
11940
  if (!Array.isArray(live)) return staticAdapters;
11835
11941
  const detected = /* @__PURE__ */ new Set([...staticAdapters, ...live]);
11836
11942
  return KNOWN_RUNTIME_ADAPTERS.filter((name) => detected.has(name));
@@ -12088,7 +12194,10 @@ async function executeRenderPipeline(input) {
12088
12194
  );
12089
12195
  const artifactTransaction = new ArtifactTransaction(
12090
12196
  outputPath,
12091
- isPngSequence || isHls ? "directory" : "file"
12197
+ isPngSequence || isHls ? "directory" : "file",
12198
+ void 0,
12199
+ void 0,
12200
+ log
12092
12201
  );
12093
12202
  const stagedOutputPath = artifactTransaction.stagingPath;
12094
12203
  const needsAlpha = outputNeedsAlpha(outputFormat);
@@ -12587,8 +12696,8 @@ async function executeRenderPipeline(input) {
12587
12696
  vfxHostCount,
12588
12697
  vfxCapture,
12589
12698
  vfxTypes
12590
- } = await resolveCompositionElementCount(probeSession, compiled.html);
12591
- const adaptersUsed = await resolveAdaptersUsed(probeSession, compiled.html);
12699
+ } = await resolveCompositionElementCount(probeSession, compiled.html, log);
12700
+ const adaptersUsed = await resolveAdaptersUsed(probeSession, compiled.html, log);
12592
12701
  const deShortBandEnabled = deShortBandMaxElements > 0;
12593
12702
  const deShortBandOpen = deShortBandEnabled && deShortBandMinFrames > 0 && compositionElementCountSource === "live" && compositionElementCount <= deShortBandMaxElements;
12594
12703
  const deShortBandRoute = process.env.HF_DE_SHORT_BAND_ROUTE === "true";
@@ -13868,7 +13977,6 @@ import {
13868
13977
  existsSync as existsSync21,
13869
13978
  linkSync,
13870
13979
  mkdirSync as mkdirSync13,
13871
- renameSync as renameSync6,
13872
13980
  rmSync as rmSync12,
13873
13981
  writeFileSync as writeFileSync8
13874
13982
  } from "node:fs";
@@ -13881,7 +13989,9 @@ import {
13881
13989
  getEncoderPreset as getEncoderPreset4,
13882
13990
  normalizeVp9CpuUsed,
13883
13991
  resolveConfig as resolveConfig3,
13884
- declareContentAddressedUrl
13992
+ declareContentAddressedUrl,
13993
+ renameWithRetry as renameWithRetry6,
13994
+ declareProbeFactsUrl
13885
13995
  } from "@hyperframes/engine";
13886
13996
 
13887
13997
  // src/services/distributed/planFrameCost.ts
@@ -14822,15 +14932,9 @@ import { join as join31 } from "node:path";
14822
14932
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
14823
14933
 
14824
14934
  // src/services/distributed/mp4Stitch.ts
14825
- import {
14826
- createReadStream as createReadStream4,
14827
- createWriteStream,
14828
- mkdirSync as mkdirSync11,
14829
- mkdtempSync as mkdtempSync4,
14830
- renameSync as renameSync5,
14831
- rmSync as rmSync11
14832
- } from "node:fs";
14935
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync4, rmSync as rmSync11 } from "node:fs";
14833
14936
  import { open } from "node:fs/promises";
14937
+ import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
14834
14938
  import { dirname as dirname12, join as join30 } from "node:path";
14835
14939
  import { finished, pipeline } from "node:stream/promises";
14836
14940
  function boxAt(src, off, at, limit) {
@@ -15203,7 +15307,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
15203
15307
  for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
15204
15308
  out.end();
15205
15309
  await finished(out);
15206
- renameSync5(join30(workDir, "out.mp4"), outputPath);
15310
+ renameWithRetry5(join30(workDir, "out.mp4"), outputPath);
15207
15311
  } finally {
15208
15312
  out.destroy();
15209
15313
  rmSync11(workDir, { recursive: true, force: true });
@@ -15929,7 +16033,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
15929
16033
  const needsAlpha = outputNeedsAlpha(config.format);
15930
16034
  const remoteFiles = Object.entries(options.remoteFiles ?? {});
15931
16035
  const remoteLocalPath = (rel) => join33(projectDir, ...rel.split("/"));
15932
- for (const [, file] of remoteFiles) declareContentAddressedUrl(file.url, file.sha256);
16036
+ for (const [, file] of remoteFiles) {
16037
+ declareContentAddressedUrl(file.url, file.sha256);
16038
+ if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
16039
+ }
15933
16040
  const urlByAbsentLocalPath = new Map(
15934
16041
  remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
15935
16042
  );
@@ -16051,7 +16158,8 @@ function mixPlanAudio(c, compiledDir) {
16051
16158
  audios: c.composition.audios,
16052
16159
  masterFxChain: c.composition.masterFxChain,
16053
16160
  abortSignal: c.abortSignal,
16054
- assertNotAborted: c.assertNotAborted
16161
+ assertNotAborted: c.assertNotAborted,
16162
+ log: c.log
16055
16163
  });
16056
16164
  }
16057
16165
  async function extractPlanVideos(c, deferRangeExtraction) {
@@ -16070,6 +16178,7 @@ async function extractPlanVideos(c, deferRangeExtraction) {
16070
16178
  if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
16071
16179
  if (extractResult.extractionResult) {
16072
16180
  assertVideoExtractionSucceeded(extractResult.extractionResult);
16181
+ c.log.info("[plan] extract phases", { ...extractResult.extractionResult.phaseBreakdown });
16073
16182
  }
16074
16183
  return extractResult;
16075
16184
  }
@@ -16115,12 +16224,12 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16115
16224
  const videoSourcesDir = join33(planDir, "video-sources");
16116
16225
  rmSync12(videoSourcesDir, { recursive: true, force: true });
16117
16226
  if (existsSync21(stagedVideoFrames)) {
16118
- renameSync6(stagedVideoFrames, videoFramesDst);
16227
+ renameWithRetry6(stagedVideoFrames, videoFramesDst);
16119
16228
  } else {
16120
16229
  mkdirSync13(videoFramesDst, { recursive: true });
16121
16230
  }
16122
16231
  if (existsSync21(finalCompiledDir)) rmSync12(finalCompiledDir, { recursive: true, force: true });
16123
- renameSync6(compiledDir, finalCompiledDir);
16232
+ renameWithRetry6(compiledDir, finalCompiledDir);
16124
16233
  const shipVideoSource = createVideoSourceShipper({
16125
16234
  planDir,
16126
16235
  projectDir,
@@ -16223,7 +16332,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16223
16332
  }
16224
16333
  const planAudioPath = join33(planDir, PLAN_AUDIO_RELATIVE_PATH);
16225
16334
  if (audioResult?.hasAudio && existsSync21(audioResult.audioOutputPath)) {
16226
- renameSync6(audioResult.audioOutputPath, planAudioPath);
16335
+ renameWithRetry6(audioResult.audioOutputPath, planAudioPath);
16227
16336
  }
16228
16337
  if (chunksCarryAudio && existsSync21(planAudioPath)) {
16229
16338
  if (options.sliceChunkAudio) {
@@ -17740,7 +17849,6 @@ import {
17740
17849
  mkdtempSync as mkdtempSync7,
17741
17850
  readFileSync as readFileSync13,
17742
17851
  readdirSync as readdirSync9,
17743
- renameSync as renameSync8,
17744
17852
  rmSync as rmSync15,
17745
17853
  statSync as statSync10,
17746
17854
  writeFileSync as writeFileSync11
@@ -17748,7 +17856,7 @@ import {
17748
17856
  import { tmpdir as tmpdir4 } from "node:os";
17749
17857
  import { dirname as dirname16, isAbsolute as isAbsolute5, join as join39, relative as relative7, resolve as resolve11, sep as sep4 } from "node:path";
17750
17858
  import { DISTRIBUTED_FPS as DISTRIBUTED_FPS2, isDistributedFps as isDistributedFps3 } from "@hyperframes/core";
17751
- import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
17859
+ import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
17752
17860
 
17753
17861
  // src/services/distributed/planV2Publisher.ts
17754
17862
  import {
@@ -17757,11 +17865,11 @@ import {
17757
17865
  linkSync as linkSync2,
17758
17866
  mkdirSync as mkdirSync15,
17759
17867
  mkdtempSync as mkdtempSync6,
17760
- renameSync as renameSync7,
17761
17868
  rmSync as rmSync14,
17762
17869
  statSync as statSync9,
17763
17870
  writeFileSync as writeFileSync10
17764
17871
  } from "node:fs";
17872
+ import { renameWithRetry as renameWithRetry7 } from "@hyperframes/engine";
17765
17873
  import { dirname as dirname15, join as join38 } from "node:path";
17766
17874
 
17767
17875
  // src/services/distributed/planV2Errors.ts
@@ -17850,7 +17958,7 @@ var LocalPlanV2ArtifactPublisher = class {
17850
17958
  if (!canFallbackToCopy(error)) throw error;
17851
17959
  copyFileSync6(blob.sourcePath, temporaryPath);
17852
17960
  }
17853
- renameSync7(temporaryPath, destinationPath);
17961
+ renameWithRetry7(temporaryPath, destinationPath);
17854
17962
  } finally {
17855
17963
  rmSync14(stagingDir, { recursive: true, force: true });
17856
17964
  }
@@ -17864,7 +17972,7 @@ var LocalPlanV2ArtifactPublisher = class {
17864
17972
  }
17865
17973
  }
17866
17974
  writeFileSync10(join38(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
17867
- renameSync7(this.temporaryDir, this.destinationDir);
17975
+ renameWithRetry7(this.temporaryDir, this.destinationDir);
17868
17976
  this.#committed = true;
17869
17977
  }
17870
17978
  async abort() {
@@ -18131,7 +18239,7 @@ function writeBlob(sourcePath, destinationPath) {
18131
18239
  const temporaryPath = join39(temporaryDir, "blob");
18132
18240
  try {
18133
18241
  copyFileSync7(sourcePath, temporaryPath);
18134
- renameSync8(temporaryPath, destinationPath);
18242
+ renameWithRetry8(temporaryPath, destinationPath);
18135
18243
  } finally {
18136
18244
  rmSync15(temporaryDir, { recursive: true, force: true });
18137
18245
  }
@@ -18263,7 +18371,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
18263
18371
  canonicalJsonStringify(publication.manifest),
18264
18372
  "utf-8"
18265
18373
  );
18266
- renameSync8(tempDir, planV2Dir);
18374
+ renameWithRetry8(tempDir, planV2Dir);
18267
18375
  return resultFromManifest(planV2Dir, publication.manifest);
18268
18376
  } catch (error) {
18269
18377
  rmSync15(tempDir, { recursive: true, force: true });
@@ -18548,7 +18656,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
18548
18656
  }),
18549
18657
  "utf-8"
18550
18658
  );
18551
- renameSync8(tempDir, destinationDir);
18659
+ renameWithRetry8(tempDir, destinationDir);
18552
18660
  } catch (error) {
18553
18661
  rmSync15(tempDir, { recursive: true, force: true });
18554
18662
  throw error;
@@ -18626,7 +18734,6 @@ import {
18626
18734
  mkdtempSync as mkdtempSync8,
18627
18735
  readFileSync as readFileSync14,
18628
18736
  readdirSync as readdirSync10,
18629
- renameSync as renameSync9,
18630
18737
  rmSync as rmSync16,
18631
18738
  statSync as statSync11,
18632
18739
  writeFileSync as writeFileSync12
@@ -18640,7 +18747,8 @@ import {
18640
18747
  muxVideoWithAudio as muxVideoWithAudio2,
18641
18748
  resolveConfig as resolveConfig6,
18642
18749
  runFfmpeg as runFfmpeg7,
18643
- appendSoftwareEncoderThreadsArg
18750
+ appendSoftwareEncoderThreadsArg,
18751
+ renameWithRetry as renameWithRetry9
18644
18752
  } from "@hyperframes/engine";
18645
18753
  import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
18646
18754
  function cfrRateControlArgs(locked) {
@@ -18697,7 +18805,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18697
18805
  const finalPath = join40(workDir, `output.${format}`);
18698
18806
  const audio = audioPath !== null && existsSync27(audioPath) ? audioPath : null;
18699
18807
  await writeJoined(ctx, videoSources, audio, finalPath);
18700
- renameSync9(finalPath, outputPath);
18808
+ renameWithRetry9(finalPath, outputPath);
18701
18809
  log.info("[assemble] stage timings", {
18702
18810
  ...stageMs,
18703
18811
  chunks: chunkPaths.length,
@@ -18949,6 +19057,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
18949
19057
  // src/services/distributed/renderChunk.ts
18950
19058
  import { randomBytes } from "node:crypto";
18951
19059
  import { existsSync as existsSync28, mkdirSync as mkdirSync18, readFileSync as readFileSync15, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "node:fs";
19060
+ import { cpus as cpus3 } from "node:os";
18952
19061
  import { join as join41 } from "node:path";
18953
19062
  import {
18954
19063
  assertSwiftShader,
@@ -19253,6 +19362,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19253
19362
  let forceScreenshotForChunk = encoder.forceScreenshot;
19254
19363
  let shownBoxes;
19255
19364
  let extraction;
19365
+ const overlap = { current: null };
19256
19366
  try {
19257
19367
  if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
19258
19368
  const bootStarted = Date.now();
@@ -19340,6 +19450,59 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19340
19450
  videoFrameLookup,
19341
19451
  frameCacheOptions(cfg, chunkWorkerCount)
19342
19452
  );
19453
+ const isPngSequence = plan2.dimensions.format === "png-sequence";
19454
+ outputKind = isPngSequence ? "frame-dir" : "file";
19455
+ const presetFormat = isPngSequence ? "mp4" : plan2.dimensions.format;
19456
+ const basePreset = getEncoderPreset6(job.config.quality, presetFormat, void 0);
19457
+ const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
19458
+ const effectiveQuality = encoder.crf ?? preset.quality;
19459
+ const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
19460
+ const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
19461
+ if (isPngSequence) {
19462
+ if (!existsSync28(outputChunkPath)) mkdirSync18(outputChunkPath, { recursive: true });
19463
+ } else {
19464
+ const outDir = join41(outputChunkPath, "..");
19465
+ if (!existsSync28(outDir)) mkdirSync18(outDir, { recursive: true });
19466
+ }
19467
+ const encodeInput = {
19468
+ job,
19469
+ log,
19470
+ outputPath: videoOnlyPath,
19471
+ framesDir,
19472
+ videoOnlyPath,
19473
+ width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
19474
+ height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
19475
+ needsAlpha: plan2.dimensions.format !== "mp4",
19476
+ captureImageFormat: captureOptions.format ?? "jpeg",
19477
+ // The encoder writes video only; audio joins at assemble time or in the
19478
+ // chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
19479
+ // sidecar AND the mp4 audio mux.
19480
+ hasAudio: false,
19481
+ isPngSequence,
19482
+ // `DistributedFormat` has no "gif" member — distributed chunks are
19483
+ // always video segments (gif renders in-process only).
19484
+ isGif: false,
19485
+ preset,
19486
+ effectiveQuality,
19487
+ effectiveBitrate,
19488
+ engineConfig: {
19489
+ ffmpegEncodeTimeout: cfg.ffmpegEncodeTimeout,
19490
+ vp9CpuUsed: resolveLockedVp9CpuUsed(encoder) ?? cfg.vp9CpuUsed
19491
+ },
19492
+ // Distributed chunks emit a single ffmpeg call per chunk; the
19493
+ // in-process per-chunk-within-chunk path would re-split our
19494
+ // already-chunked work.
19495
+ enableChunkedEncode: false,
19496
+ chunkedEncodeSize: framesInChunk,
19497
+ abortSignal: void 0,
19498
+ assertNotAborted: () => {
19499
+ },
19500
+ // GOP === framesInChunk + force-keyframe at frame 0 → the chunk's
19501
+ // first frame is an IDR keyframe and concat-copy at assemble time
19502
+ // round-trips losslessly.
19503
+ lockGopForChunkConcat: !isPngSequence,
19504
+ gopSize: framesInChunk
19505
+ };
19343
19506
  const captureStarted = Date.now();
19344
19507
  captureMode = await runCaptureWithScreenshotFallback({
19345
19508
  forceScreenshot: forceScreenshotForChunk,
@@ -19372,6 +19535,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19372
19535
  }
19373
19536
  const captureSession = session;
19374
19537
  session = null;
19538
+ overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus3().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
19375
19539
  await runCaptureStage({
19376
19540
  fileServer,
19377
19541
  workDir,
@@ -19391,7 +19555,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19391
19555
  abortSignal: void 0,
19392
19556
  assertNotAborted: () => {
19393
19557
  },
19394
- frameRange: range
19558
+ frameRange: range,
19559
+ onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
19560
+ }).catch(async (captureError) => {
19561
+ await overlap.current?.abort();
19562
+ overlap.current = null;
19563
+ throw captureError;
19395
19564
  });
19396
19565
  return observedChunkCaptureMode(capturePerfs);
19397
19566
  },
@@ -19423,60 +19592,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19423
19592
  log
19424
19593
  );
19425
19594
  framesEncoded = framesInChunk;
19426
- const isPngSequence = plan2.dimensions.format === "png-sequence";
19427
- outputKind = isPngSequence ? "frame-dir" : "file";
19428
- const presetFormat = isPngSequence ? "mp4" : plan2.dimensions.format;
19429
- const basePreset = getEncoderPreset6(job.config.quality, presetFormat, void 0);
19430
- const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
19431
- const effectiveQuality = encoder.crf ?? preset.quality;
19432
- const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
19433
- const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
19434
- if (isPngSequence) {
19435
- if (!existsSync28(outputChunkPath)) mkdirSync18(outputChunkPath, { recursive: true });
19436
- } else {
19437
- const outDir = join41(outputChunkPath, "..");
19438
- if (!existsSync28(outDir)) mkdirSync18(outDir, { recursive: true });
19439
- }
19440
19595
  const encodeStarted = Date.now();
19441
- await runEncodeStage({
19442
- job,
19443
- log,
19444
- outputPath: videoOnlyPath,
19445
- framesDir,
19446
- videoOnlyPath,
19447
- width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
19448
- height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
19449
- needsAlpha: plan2.dimensions.format !== "mp4",
19450
- captureImageFormat: captureOptions.format ?? "jpeg",
19451
- // The encoder writes video only; audio joins at assemble time or in the
19452
- // chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
19453
- // sidecar AND the mp4 audio mux.
19454
- hasAudio: false,
19455
- isPngSequence,
19456
- // `DistributedFormat` has no "gif" member — distributed chunks are
19457
- // always video segments (gif renders in-process only).
19458
- isGif: false,
19459
- preset,
19460
- effectiveQuality,
19461
- effectiveBitrate,
19462
- engineConfig: {
19463
- ffmpegEncodeTimeout: cfg.ffmpegEncodeTimeout,
19464
- vp9CpuUsed: resolveLockedVp9CpuUsed(encoder) ?? cfg.vp9CpuUsed
19465
- },
19466
- // Distributed chunks emit a single ffmpeg call per chunk; the
19467
- // in-process per-chunk-within-chunk path would re-split our
19468
- // already-chunked work.
19469
- enableChunkedEncode: false,
19470
- chunkedEncodeSize: framesInChunk,
19471
- abortSignal: void 0,
19472
- assertNotAborted: () => {
19473
- },
19474
- // GOP === framesInChunk + force-keyframe at frame 0 → the chunk's
19475
- // first frame is an IDR keyframe and concat-copy at assemble time
19476
- // round-trips losslessly.
19477
- lockGopForChunkConcat: !isPngSequence,
19478
- gopSize: framesInChunk
19479
- });
19596
+ const overlapped = overlap.current;
19597
+ overlap.current = null;
19598
+ await (overlapped ? overlapped.finish() : runEncodeStage(encodeInput));
19480
19599
  if (encoder.chunksCarryAudio) {
19481
19600
  const audioScratchPath = `${outputChunkPath}.audio.m4a`;
19482
19601
  const audioChunks = options?.frameRange ? chunksWithSubRange(chunks, chunkIndex, range) : { chunks, index: chunkIndex };
@@ -19494,6 +19613,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19494
19613
  }
19495
19614
  encodeStageMs = Date.now() - encodeStarted;
19496
19615
  } finally {
19616
+ await overlap.current?.abort().catch(() => {
19617
+ });
19497
19618
  await extraction?.catch(() => {
19498
19619
  });
19499
19620
  if (session) {
@@ -19602,6 +19723,9 @@ var READ_CHUNK_BYTES = 256 * 1024;
19602
19723
  // src/services/distributed/audioSidecar.ts
19603
19724
  import { extractAudioMetadata as extractAudioMetadata4, runFfmpeg as runFfmpeg8 } from "@hyperframes/engine";
19604
19725
  var AUDIO_SIDECAR_MIN_BYTES = 32 * 1024 * 1024;
19726
+
19727
+ // src/services/distributed/probeFacts.ts
19728
+ import { buildProbeFactsText } from "@hyperframes/engine";
19605
19729
  export {
19606
19730
  CURRENT_PLAN_PROTOCOL,
19607
19731
  DEFAULT_CONFIG2 as DEFAULT_CONFIG,