@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.
@@ -1384,18 +1384,9 @@ var RenderExecutionContext = class {
1384
1384
  };
1385
1385
 
1386
1386
  // src/services/render/artifactTransaction.ts
1387
- import {
1388
- closeSync as closeSync2,
1389
- existsSync as existsSync5,
1390
- fstatSync as fstatSync2,
1391
- openSync as openSync2,
1392
- readSync,
1393
- readdirSync as readdirSync2,
1394
- renameSync,
1395
- rmSync as rmSync3
1396
- } from "node:fs";
1387
+ import { closeSync as closeSync2, existsSync as existsSync5, fstatSync as fstatSync2, openSync as openSync2, readSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
1397
1388
  import { basename as basename3, dirname as dirname4, extname as extname2, join as join5, resolve as resolve4 } from "node:path";
1398
- import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST } from "@hyperframes/engine";
1389
+ import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, renameWithRetry } from "@hyperframes/engine";
1399
1390
 
1400
1391
  // src/utils/ffprobe.ts
1401
1392
  import {
@@ -1514,7 +1505,7 @@ function createRenderWorkDir(prefix, outputDir) {
1514
1505
  // src/services/render/artifactTransaction.ts
1515
1506
  var defaultFileSystem = {
1516
1507
  existsSync: existsSync5,
1517
- renameSync,
1508
+ renameSync: (source, destination) => renameWithRetry(source, destination),
1518
1509
  rmSync: rmSync3
1519
1510
  };
1520
1511
  async function defaultArtifactDurationProbe(path) {
@@ -1592,8 +1583,11 @@ function assertDurationWithinTolerance(expectedSeconds, probedSeconds, tolerance
1592
1583
  `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}`
1593
1584
  );
1594
1585
  }
1586
+ function hasUsableFrameCount(frames) {
1587
+ return frames !== void 0 && Number.isFinite(frames) && frames > 0;
1588
+ }
1595
1589
  function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath, toleranceFrames = 1) {
1596
- if (probedFrames === void 0 || !Number.isFinite(probedFrames) || probedFrames <= 0) {
1590
+ if (!hasUsableFrameCount(probedFrames)) {
1597
1591
  return;
1598
1592
  }
1599
1593
  const shortfall = expectedFrames - probedFrames;
@@ -1613,9 +1607,10 @@ function assertSizeMatches(expected, probed, stagingPath) {
1613
1607
  );
1614
1608
  }
1615
1609
  var ArtifactTransaction = class {
1616
- constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe) {
1610
+ constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe, logger = defaultLogger) {
1617
1611
  this.kind = kind;
1618
1612
  this.fileSystem = fileSystem;
1613
+ this.logger = logger;
1619
1614
  this.destinationPath = resolve4(destinationPath);
1620
1615
  this.transactionDirectory = createSiblingTransactionDirectory(this.destinationPath);
1621
1616
  this.stagingPath = join5(this.transactionDirectory, basename3(this.destinationPath));
@@ -1698,6 +1693,19 @@ var ArtifactTransaction = class {
1698
1693
  probePath
1699
1694
  );
1700
1695
  if (expected.expectedFrames !== void 0) {
1696
+ if (!hasUsableFrameCount(probed.frames)) {
1697
+ try {
1698
+ this.logger.debug(
1699
+ "[Render] Frame-count validation skipped: no usable frame count was reported.",
1700
+ {
1701
+ event: "artifact_frame_count_unavailable",
1702
+ artifact: this.destinationPath,
1703
+ expectedFrames: expected.expectedFrames
1704
+ }
1705
+ );
1706
+ } catch {
1707
+ }
1708
+ }
1701
1709
  assertFrameCountWithinTolerance(expected.expectedFrames, probed.frames, probePath);
1702
1710
  }
1703
1711
  assertSizeMatches(expected, probed, probePath);
@@ -1913,58 +1921,10 @@ import {
1913
1921
 
1914
1922
  // src/services/render/captureStageError.ts
1915
1923
  import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
1916
- var CaptureStageError = class extends CaptureFailure {
1917
- browserConsole;
1918
- constructor(input) {
1919
- const classified = classifyCaptureFailure(input.cause);
1920
- super({
1921
- kind: classified.kind,
1922
- message: normalizeErrorMessage(input.cause),
1923
- cause: input.cause,
1924
- workerDiagnostics: classified.workerDiagnostics
1925
- });
1926
- this.name = "CaptureStageError";
1927
- this.browserConsole = input.browserConsole.slice();
1928
- if (input.cause instanceof Error && input.cause.stack) {
1929
- this.stack = input.cause.stack;
1930
- }
1931
- }
1932
- };
1933
- function wrapCaptureStageError(error, browserConsole) {
1934
- if (error instanceof CaptureStageError || error instanceof EncoderInterruptedError) return error;
1935
- return new CaptureStageError({ cause: error, browserConsole });
1936
- }
1937
- function getCaptureStageBrowserConsole(error) {
1938
- if (error instanceof CaptureStageError) return error.browserConsole;
1939
- return [];
1940
- }
1941
1924
 
1942
- // src/services/render/stages/captureHdrFrameShared.ts
1943
- import {
1944
- applyDomLayerMask as applyDomLayerMask2,
1945
- blitRgba8OverRgb48le as blitRgba8OverRgb48le2,
1946
- captureAlphaPng as captureAlphaPng2,
1947
- decodePng as decodePng2,
1948
- queryElementStacking as queryElementStacking2,
1949
- removeDomLayerMask as removeDomLayerMask2
1950
- } from "@hyperframes/engine";
1951
-
1952
- // src/services/hdrCompositor.ts
1953
- import { readSync as readSync2, closeSync as closeSync3 } from "fs";
1954
- import { join as join6 } from "path";
1955
- import {
1956
- captureAlphaPng,
1957
- applyDomLayerMask,
1958
- removeDomLayerMask,
1959
- decodePng,
1960
- blitRgba8OverRgb48le,
1961
- blitRgb48leRegion,
1962
- groupIntoLayers,
1963
- blitRgb48leAffine,
1964
- parseTransformMatrix,
1965
- convertTransfer,
1966
- isVideoHiddenBeforeStreamStart
1967
- } from "@hyperframes/engine";
1925
+ // src/services/render/perfSummary.ts
1926
+ import { arch, cpus, platform, totalmem } from "node:os";
1927
+ import { fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1968
1928
 
1969
1929
  // src/services/render/hdrPerf.ts
1970
1930
  function createHdrPerfCollector() {
@@ -2070,7 +2030,280 @@ function finalizeHdrPerf(perf) {
2070
2030
  };
2071
2031
  }
2072
2032
 
2033
+ // src/services/render/perfSummary.ts
2034
+ function worstSubTimelineWaitOutcome(perfs) {
2035
+ const outcomes = perfs.map((p) => p.subTimelineWaitOutcome).filter((o) => !!o);
2036
+ if (outcomes.length === 0) return void 0;
2037
+ if (outcomes.includes("script_failure")) return "script_failure";
2038
+ if (outcomes.includes("timeout")) return "timeout";
2039
+ return "ready";
2040
+ }
2041
+ function pushWorkerDedupPerfs(results, sink) {
2042
+ for (const r of results) {
2043
+ if (r.perf) sink.push(r.perf);
2044
+ }
2045
+ }
2046
+ function pushWorkerPageErrors(results, sink) {
2047
+ for (const result of results) {
2048
+ for (const line of result.pageErrorLines ?? []) if (!sink.includes(line)) sink.push(line);
2049
+ }
2050
+ }
2051
+ function withWorkerPageErrors(consoleLines, workerLines) {
2052
+ return [...consoleLines, ...workerLines.filter((line) => !consoleLines.includes(line))];
2053
+ }
2054
+ function roundDb(value) {
2055
+ return value === void 0 ? void 0 : Math.round(Math.min(value, 999) * 10) / 10;
2056
+ }
2057
+ function aggregateDrawElement(perfs, de) {
2058
+ if (perfs.length === 0) return void 0;
2059
+ const modes = [...new Set(perfs.map((p) => p.captureMode).filter(Boolean))].sort();
2060
+ const gateReasons = [
2061
+ ...new Set(
2062
+ [de.probeGateReason, ...perfs.map((p) => p.deGateReason)].filter((r) => !!r)
2063
+ )
2064
+ ].sort();
2065
+ const gpuRenderers = [
2066
+ ...new Set(perfs.map((p) => p.gpuRenderer).filter((r) => !!r))
2067
+ ].sort();
2068
+ const drain = de.drainStats;
2069
+ return {
2070
+ mode: modes.join("|") || "unknown",
2071
+ compileGate: de.compileGate,
2072
+ clampReason: de.clampReason,
2073
+ workerInversion: de.workerInversion ?? "none",
2074
+ preInversionWorkers: de.preInversionWorkers,
2075
+ compositionElementCount: de.compositionElementCount,
2076
+ compositionElementCountSource: de.compositionElementCountSource,
2077
+ compositionElementTags: de.compositionElementTags,
2078
+ arollVideoCount: de.arollVideoCount,
2079
+ heygenVideoCount: de.heygenVideoCount,
2080
+ adaptersUsed: de.adaptersUsed,
2081
+ audioCount: de.audioCount,
2082
+ imageCount: de.imageCount,
2083
+ subCompositionCount: de.subCompositionCount,
2084
+ audioGroupCount: de.audioGroupCount,
2085
+ colorGradingCount: de.colorGradingCount,
2086
+ hasLut: de.hasLut,
2087
+ rootBodyMismatch: de.rootBodyMismatch,
2088
+ rootBodyDeltaPxBucket: de.rootBodyDeltaPxBucket,
2089
+ vfxHostCount: de.vfxHostCount,
2090
+ vfxCapture: de.vfxCapture,
2091
+ vfxTypes: de.vfxTypes,
2092
+ shortBand: de.shortBand,
2093
+ parallelRouter: de.parallelRouter ?? "none",
2094
+ preRouterWorkers: de.preRouterWorkers,
2095
+ gateReason: gateReasons.length > 0 ? gateReasons.join("|") : void 0,
2096
+ gpuRenderer: gpuRenderers.length > 0 ? gpuRenderers.join("|") : void 0,
2097
+ workerEncode: perfs.some((p) => p.deWorkerEncode),
2098
+ verifyArmed: perfs.reduce((sum, p) => sum + (p.deVerifyArmed ?? 0), 0),
2099
+ verifyChecked: drain?.verifyChecked ?? 0,
2100
+ verifyMinDb: roundDb(drain?.verifyMinDb),
2101
+ verifyInitMs: perfs.reduce((sum, p) => sum + (p.deVerifyInitMs ?? 0), 0),
2102
+ selfVerifyFallback: de.selfVerifyFallback,
2103
+ fallbackReason: de.fallbackReason,
2104
+ fallbackFailedDb: roundDb(de.fallbackFailedDb),
2105
+ fallbackFrameIndex: de.fallbackFrameIndex,
2106
+ fallbackThresholdDb: roundDb(de.fallbackThresholdDb),
2107
+ blankSuspects: drain?.blankSuspects ?? 0,
2108
+ blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
2109
+ blankRecaptures: drain?.blankRecaptures ?? 0,
2110
+ boundaryFrames: perfs.reduce((sum, p) => sum + (p.deBoundaryFrames ?? 0), 0),
2111
+ ncprFallbacks: perfs.reduce((sum, p) => sum + (p.deNcprFallbacks ?? 0), 0),
2112
+ frameTimeouts: perfs.reduce((sum, p) => sum + (p.deFrameTimeouts ?? 0), 0)
2113
+ };
2114
+ }
2115
+ function maxDefined(values) {
2116
+ const present = values.filter((v) => typeof v === "number");
2117
+ return present.length > 0 ? Math.max(...present) : void 0;
2118
+ }
2119
+ function aggregateChromeMemory(perfs) {
2120
+ const sampled = perfs.filter((p) => (p.chromeMemorySamples ?? 0) > 0);
2121
+ if (sampled.length === 0) return void 0;
2122
+ return {
2123
+ browserRssPeakMb: maxDefined(sampled.map((p) => p.chromeBrowserRssPeakMb)),
2124
+ rendererRssPeakMb: maxDefined(sampled.map((p) => p.chromeRendererRssPeakMb)),
2125
+ // Last samples are per session; summing approximates the whole fleet's
2126
+ // footprint at the end of capture (workers run concurrently).
2127
+ rssLastMb: sampled.reduce((sum, p) => sum + (p.chromeRssLastMb ?? 0), 0),
2128
+ gpuProcessSeenLastSample: sampled.some((p) => p.chromeGpuProcessSeenLastSample === true),
2129
+ samples: sampled.reduce((sum, p) => sum + (p.chromeMemorySamples ?? 0), 0)
2130
+ };
2131
+ }
2132
+ function aggregateDedup(perfs) {
2133
+ if (perfs.length === 0) return void 0;
2134
+ const armed = perfs.some((p) => p.staticDedupArmed);
2135
+ const skipReasons = armed ? [] : [
2136
+ ...new Set(perfs.map((p) => p.staticDedupSkipReason).filter((r) => !!r))
2137
+ ].sort();
2138
+ const verificationPerfs = perfs.filter((perf) => perf.staticDedupVerificationOutcome);
2139
+ const verificationOutcomes = [
2140
+ ...new Set(
2141
+ verificationPerfs.map((perf) => perf.staticDedupVerificationOutcome).filter((outcome) => outcome != null)
2142
+ )
2143
+ ].sort();
2144
+ return {
2145
+ enabled: perfs.some((p) => p.staticDedupEnabled),
2146
+ armed,
2147
+ predictedFrames: perfs.reduce((sum, p) => sum + (p.staticDedupPredicted ?? 0), 0),
2148
+ reusedFrames: perfs.reduce((sum, p) => sum + (p.staticDedupReused ?? 0), 0),
2149
+ skipReason: skipReasons.length > 0 ? skipReasons.join("|") : void 0,
2150
+ ...verificationPerfs.length === 0 ? {} : {
2151
+ verifiedFrames: verificationPerfs.reduce(
2152
+ (sum, perf) => sum + (perf.staticDedupVerified ?? 0),
2153
+ 0
2154
+ ),
2155
+ verificationOutcomes,
2156
+ plannedRuns: verificationPerfs.reduce(
2157
+ (sum, perf) => sum + (perf.staticDedupVerificationPlannedRuns ?? 0),
2158
+ 0
2159
+ ),
2160
+ completedRuns: verificationPerfs.reduce(
2161
+ (sum, perf) => sum + (perf.staticDedupVerificationCompletedRuns ?? 0),
2162
+ 0
2163
+ ),
2164
+ screenshots: verificationPerfs.reduce(
2165
+ (sum, perf) => sum + (perf.staticDedupVerificationScreenshots ?? 0),
2166
+ 0
2167
+ ),
2168
+ seeks: verificationPerfs.reduce(
2169
+ (sum, perf) => sum + (perf.staticDedupVerificationSeeks ?? 0),
2170
+ 0
2171
+ ),
2172
+ comparisons: verificationPerfs.reduce(
2173
+ (sum, perf) => sum + (perf.staticDedupVerificationComparisons ?? 0),
2174
+ 0
2175
+ ),
2176
+ verificationElapsedMs: verificationPerfs.reduce(
2177
+ (sum, perf) => sum + (perf.staticDedupVerificationElapsedMs ?? 0),
2178
+ 0
2179
+ )
2180
+ }
2181
+ };
2182
+ }
2183
+ function aggregateBeginFrameReuse(perfs) {
2184
+ const noDamageFrames = perfs.reduce((sum, p) => sum + (p.beginFrameNoDamage ?? 0), 0);
2185
+ const hasDamageFrames = perfs.reduce((sum, p) => sum + (p.beginFrameHasDamage ?? 0), 0);
2186
+ if (noDamageFrames + hasDamageFrames === 0) return void 0;
2187
+ return { noDamageFrames, hasDamageFrames };
2188
+ }
2189
+ function buildRenderPerfSummary(input) {
2190
+ return {
2191
+ renderId: input.job.id,
2192
+ totalElapsedMs: input.totalElapsedMs,
2193
+ // RenderPerfSummary surfaces fps as a decimal because it lands in JSON
2194
+ // payloads (CLI telemetry, regression-harness reports) where a single
2195
+ // number is friendlier than `{num,den}`. Callers needing the rational
2196
+ // back can read `job.config.fps`.
2197
+ fps: fpsToNumber3(input.job.config.fps),
2198
+ quality: input.job.config.quality,
2199
+ workers: input.workerCount,
2200
+ workerSizing: input.workerSizing,
2201
+ chunkedEncode: input.enableChunkedEncode,
2202
+ chunkSizeFrames: input.enableChunkedEncode ? input.chunkedEncodeSize : null,
2203
+ compositionDurationSeconds: input.compositionDurationSeconds,
2204
+ totalFrames: input.totalFrames,
2205
+ resolution: { width: input.outputWidth, height: input.outputHeight },
2206
+ videoCount: input.videoCount,
2207
+ audioCount: input.audioCount,
2208
+ stages: input.perfStages,
2209
+ videoExtractBreakdown: input.videoExtractBreakdown,
2210
+ tmpPeakBytes: input.tmpPeakBytes,
2211
+ captureCalibration: input.captureCalibration ? {
2212
+ sampledFrames: input.captureCalibration.samples.map((sample) => sample.frameIndex),
2213
+ p95Ms: input.captureCalibration.estimate.p95Ms,
2214
+ multiplier: input.captureCalibration.estimate.multiplier,
2215
+ reasons: input.captureCalibration.estimate.reasons
2216
+ } : void 0,
2217
+ captureAttempts: input.captureAttempts.length > 0 ? input.captureAttempts : void 0,
2218
+ hdrDiagnostics: input.hdrDiagnostics.videoExtractionFailures > 0 || input.hdrDiagnostics.imageDecodeFailures > 0 ? { ...input.hdrDiagnostics } : void 0,
2219
+ hdrPerf: input.hdrPerf ? finalizeHdrPerf(input.hdrPerf) : void 0,
2220
+ observability: input.observability,
2221
+ captureAvgMs: input.totalFrames > 0 ? Math.round(
2222
+ (input.perfStages.captureFrameMs ?? input.perfStages.captureMs ?? 0) / input.totalFrames
2223
+ ) : void 0,
2224
+ subTimelineWait: worstSubTimelineWaitOutcome(input.dedupPerfs),
2225
+ captureP50Ms: (() => {
2226
+ const withSamples = input.dedupPerfs.filter((p) => (p.p50TotalMs ?? 0) > 0);
2227
+ if (withSamples.length === 0) return void 0;
2228
+ return withSamples.reduce((a, b) => b.frames > a.frames ? b : a).p50TotalMs;
2229
+ })(),
2230
+ peakRssMb: Math.round(input.peakRssBytes / (1024 * 1024)),
2231
+ peakHeapUsedMb: Math.round(input.peakHeapUsedBytes / (1024 * 1024)),
2232
+ staticDedup: aggregateDedup(input.dedupPerfs),
2233
+ chromeMemory: aggregateChromeMemory(input.dedupPerfs),
2234
+ beginFrameReuse: aggregateBeginFrameReuse(input.dedupPerfs),
2235
+ drawElement: aggregateDrawElement(
2236
+ input.dedupPerfs,
2237
+ input.drawElement ?? { selfVerifyFallback: false }
2238
+ ),
2239
+ host: {
2240
+ platform: platform(),
2241
+ arch: arch(),
2242
+ cpuCount: cpus().length,
2243
+ totalMemMb: Math.round(totalmem() / (1024 * 1024)),
2244
+ nodeVersion: process.version,
2245
+ gpuDisabled: input.gpuDisabled
2246
+ }
2247
+ };
2248
+ }
2249
+
2250
+ // src/services/render/captureStageError.ts
2251
+ var CaptureStageError = class extends CaptureFailure {
2252
+ browserConsole;
2253
+ constructor(input) {
2254
+ const classified = classifyCaptureFailure(input.cause);
2255
+ super({
2256
+ kind: classified.kind,
2257
+ message: normalizeErrorMessage(input.cause),
2258
+ cause: input.cause,
2259
+ workerDiagnostics: classified.workerDiagnostics,
2260
+ pageErrorLines: classified.pageErrorLines
2261
+ });
2262
+ this.name = "CaptureStageError";
2263
+ this.browserConsole = withWorkerPageErrors(input.browserConsole, [
2264
+ ...classified.pageErrorLines
2265
+ ]);
2266
+ if (input.cause instanceof Error && input.cause.stack) {
2267
+ this.stack = input.cause.stack;
2268
+ }
2269
+ }
2270
+ };
2271
+ function wrapCaptureStageError(error, browserConsole) {
2272
+ if (error instanceof CaptureStageError || error instanceof EncoderInterruptedError) return error;
2273
+ return new CaptureStageError({ cause: error, browserConsole });
2274
+ }
2275
+ function getCaptureStageBrowserConsole(error) {
2276
+ if (error instanceof CaptureStageError) return error.browserConsole;
2277
+ if (error instanceof CaptureFailure) return [...error.pageErrorLines];
2278
+ return [];
2279
+ }
2280
+
2281
+ // src/services/render/stages/captureHdrFrameShared.ts
2282
+ import {
2283
+ applyDomLayerMask as applyDomLayerMask2,
2284
+ blitRgba8OverRgb48le as blitRgba8OverRgb48le2,
2285
+ captureAlphaPng as captureAlphaPng2,
2286
+ decodePng as decodePng2,
2287
+ queryElementStacking as queryElementStacking2,
2288
+ removeDomLayerMask as removeDomLayerMask2
2289
+ } from "@hyperframes/engine";
2290
+
2073
2291
  // src/services/hdrCompositor.ts
2292
+ import { readSync as readSync2, closeSync as closeSync3 } from "fs";
2293
+ import { join as join6 } from "path";
2294
+ import {
2295
+ captureAlphaPng,
2296
+ applyDomLayerMask,
2297
+ removeDomLayerMask,
2298
+ decodePng,
2299
+ blitRgba8OverRgb48le,
2300
+ blitRgb48leRegion,
2301
+ groupIntoLayers,
2302
+ blitRgb48leAffine,
2303
+ parseTransformMatrix,
2304
+ convertTransfer,
2305
+ isVideoHiddenBeforeStreamStart
2306
+ } from "@hyperframes/engine";
2074
2307
  function countNonZeroAlpha(rgba) {
2075
2308
  let n = 0;
2076
2309
  for (let p = 3; p < rgba.length; p += 4) {
@@ -2537,7 +2770,7 @@ import {
2537
2770
  resolveVideoExtractionWindow,
2538
2771
  runFfmpeg
2539
2772
  } from "@hyperframes/engine";
2540
- import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
2773
+ import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
2541
2774
  var NO_FOLLOW_FLAG = constants2.O_NOFOLLOW ?? 0;
2542
2775
  function tempDirSafePrefix(id) {
2543
2776
  const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
@@ -2773,7 +3006,7 @@ async function extractHdrVideoFrames(args) {
2773
3006
  const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
2774
3007
  framesDir,
2775
3008
  plannedVideos,
2776
- fpsToNumber3(job.config.fps),
3009
+ fpsToNumber4(job.config.fps),
2777
3010
  log
2778
3011
  );
2779
3012
  const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
@@ -3226,222 +3459,12 @@ import {
3226
3459
  prepareCaptureSessionForReuse,
3227
3460
  spawnStreamingEncoder
3228
3461
  } from "@hyperframes/engine";
3229
-
3230
- // src/services/render/perfSummary.ts
3231
- import { arch, cpus, platform, totalmem } from "node:os";
3232
- import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
3233
- function worstSubTimelineWaitOutcome(perfs) {
3234
- const outcomes = perfs.map((p) => p.subTimelineWaitOutcome).filter((o) => !!o);
3235
- if (outcomes.length === 0) return void 0;
3236
- if (outcomes.includes("script_failure")) return "script_failure";
3237
- if (outcomes.includes("timeout")) return "timeout";
3238
- return "ready";
3239
- }
3240
- function pushWorkerDedupPerfs(results, sink) {
3241
- for (const r of results) {
3242
- if (r.perf) sink.push(r.perf);
3243
- }
3244
- }
3245
- function roundDb(value) {
3246
- return value === void 0 ? void 0 : Math.round(Math.min(value, 999) * 10) / 10;
3247
- }
3248
- function aggregateDrawElement(perfs, de) {
3249
- if (perfs.length === 0) return void 0;
3250
- const modes = [...new Set(perfs.map((p) => p.captureMode).filter(Boolean))].sort();
3251
- const gateReasons = [
3252
- ...new Set(
3253
- [de.probeGateReason, ...perfs.map((p) => p.deGateReason)].filter((r) => !!r)
3254
- )
3255
- ].sort();
3256
- const gpuRenderers = [
3257
- ...new Set(perfs.map((p) => p.gpuRenderer).filter((r) => !!r))
3258
- ].sort();
3259
- const drain = de.drainStats;
3260
- return {
3261
- mode: modes.join("|") || "unknown",
3262
- compileGate: de.compileGate,
3263
- clampReason: de.clampReason,
3264
- workerInversion: de.workerInversion ?? "none",
3265
- preInversionWorkers: de.preInversionWorkers,
3266
- compositionElementCount: de.compositionElementCount,
3267
- compositionElementCountSource: de.compositionElementCountSource,
3268
- compositionElementTags: de.compositionElementTags,
3269
- arollVideoCount: de.arollVideoCount,
3270
- heygenVideoCount: de.heygenVideoCount,
3271
- adaptersUsed: de.adaptersUsed,
3272
- audioCount: de.audioCount,
3273
- imageCount: de.imageCount,
3274
- subCompositionCount: de.subCompositionCount,
3275
- audioGroupCount: de.audioGroupCount,
3276
- colorGradingCount: de.colorGradingCount,
3277
- hasLut: de.hasLut,
3278
- rootBodyMismatch: de.rootBodyMismatch,
3279
- rootBodyDeltaPxBucket: de.rootBodyDeltaPxBucket,
3280
- vfxHostCount: de.vfxHostCount,
3281
- vfxCapture: de.vfxCapture,
3282
- vfxTypes: de.vfxTypes,
3283
- shortBand: de.shortBand,
3284
- parallelRouter: de.parallelRouter ?? "none",
3285
- preRouterWorkers: de.preRouterWorkers,
3286
- gateReason: gateReasons.length > 0 ? gateReasons.join("|") : void 0,
3287
- gpuRenderer: gpuRenderers.length > 0 ? gpuRenderers.join("|") : void 0,
3288
- workerEncode: perfs.some((p) => p.deWorkerEncode),
3289
- verifyArmed: perfs.reduce((sum, p) => sum + (p.deVerifyArmed ?? 0), 0),
3290
- verifyChecked: drain?.verifyChecked ?? 0,
3291
- verifyMinDb: roundDb(drain?.verifyMinDb),
3292
- verifyInitMs: perfs.reduce((sum, p) => sum + (p.deVerifyInitMs ?? 0), 0),
3293
- selfVerifyFallback: de.selfVerifyFallback,
3294
- fallbackReason: de.fallbackReason,
3295
- fallbackFailedDb: roundDb(de.fallbackFailedDb),
3296
- fallbackFrameIndex: de.fallbackFrameIndex,
3297
- fallbackThresholdDb: roundDb(de.fallbackThresholdDb),
3298
- blankSuspects: drain?.blankSuspects ?? 0,
3299
- blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
3300
- blankRecaptures: drain?.blankRecaptures ?? 0,
3301
- boundaryFrames: perfs.reduce((sum, p) => sum + (p.deBoundaryFrames ?? 0), 0),
3302
- ncprFallbacks: perfs.reduce((sum, p) => sum + (p.deNcprFallbacks ?? 0), 0),
3303
- frameTimeouts: perfs.reduce((sum, p) => sum + (p.deFrameTimeouts ?? 0), 0)
3304
- };
3305
- }
3306
- function maxDefined(values) {
3307
- const present = values.filter((v) => typeof v === "number");
3308
- return present.length > 0 ? Math.max(...present) : void 0;
3309
- }
3310
- function aggregateChromeMemory(perfs) {
3311
- const sampled = perfs.filter((p) => (p.chromeMemorySamples ?? 0) > 0);
3312
- if (sampled.length === 0) return void 0;
3313
- return {
3314
- browserRssPeakMb: maxDefined(sampled.map((p) => p.chromeBrowserRssPeakMb)),
3315
- rendererRssPeakMb: maxDefined(sampled.map((p) => p.chromeRendererRssPeakMb)),
3316
- // Last samples are per session; summing approximates the whole fleet's
3317
- // footprint at the end of capture (workers run concurrently).
3318
- rssLastMb: sampled.reduce((sum, p) => sum + (p.chromeRssLastMb ?? 0), 0),
3319
- gpuProcessSeenLastSample: sampled.some((p) => p.chromeGpuProcessSeenLastSample === true),
3320
- samples: sampled.reduce((sum, p) => sum + (p.chromeMemorySamples ?? 0), 0)
3321
- };
3322
- }
3323
- function aggregateDedup(perfs) {
3324
- if (perfs.length === 0) return void 0;
3325
- const armed = perfs.some((p) => p.staticDedupArmed);
3326
- const skipReasons = armed ? [] : [
3327
- ...new Set(perfs.map((p) => p.staticDedupSkipReason).filter((r) => !!r))
3328
- ].sort();
3329
- const verificationPerfs = perfs.filter((perf) => perf.staticDedupVerificationOutcome);
3330
- const verificationOutcomes = [
3331
- ...new Set(
3332
- verificationPerfs.map((perf) => perf.staticDedupVerificationOutcome).filter((outcome) => outcome != null)
3333
- )
3334
- ].sort();
3335
- return {
3336
- enabled: perfs.some((p) => p.staticDedupEnabled),
3337
- armed,
3338
- predictedFrames: perfs.reduce((sum, p) => sum + (p.staticDedupPredicted ?? 0), 0),
3339
- reusedFrames: perfs.reduce((sum, p) => sum + (p.staticDedupReused ?? 0), 0),
3340
- skipReason: skipReasons.length > 0 ? skipReasons.join("|") : void 0,
3341
- ...verificationPerfs.length === 0 ? {} : {
3342
- verifiedFrames: verificationPerfs.reduce(
3343
- (sum, perf) => sum + (perf.staticDedupVerified ?? 0),
3344
- 0
3345
- ),
3346
- verificationOutcomes,
3347
- plannedRuns: verificationPerfs.reduce(
3348
- (sum, perf) => sum + (perf.staticDedupVerificationPlannedRuns ?? 0),
3349
- 0
3350
- ),
3351
- completedRuns: verificationPerfs.reduce(
3352
- (sum, perf) => sum + (perf.staticDedupVerificationCompletedRuns ?? 0),
3353
- 0
3354
- ),
3355
- screenshots: verificationPerfs.reduce(
3356
- (sum, perf) => sum + (perf.staticDedupVerificationScreenshots ?? 0),
3357
- 0
3358
- ),
3359
- seeks: verificationPerfs.reduce(
3360
- (sum, perf) => sum + (perf.staticDedupVerificationSeeks ?? 0),
3361
- 0
3362
- ),
3363
- comparisons: verificationPerfs.reduce(
3364
- (sum, perf) => sum + (perf.staticDedupVerificationComparisons ?? 0),
3365
- 0
3366
- ),
3367
- verificationElapsedMs: verificationPerfs.reduce(
3368
- (sum, perf) => sum + (perf.staticDedupVerificationElapsedMs ?? 0),
3369
- 0
3370
- )
3371
- }
3372
- };
3373
- }
3374
- function aggregateBeginFrameReuse(perfs) {
3375
- const noDamageFrames = perfs.reduce((sum, p) => sum + (p.beginFrameNoDamage ?? 0), 0);
3376
- const hasDamageFrames = perfs.reduce((sum, p) => sum + (p.beginFrameHasDamage ?? 0), 0);
3377
- if (noDamageFrames + hasDamageFrames === 0) return void 0;
3378
- return { noDamageFrames, hasDamageFrames };
3379
- }
3380
- function buildRenderPerfSummary(input) {
3381
- return {
3382
- renderId: input.job.id,
3383
- totalElapsedMs: input.totalElapsedMs,
3384
- // RenderPerfSummary surfaces fps as a decimal because it lands in JSON
3385
- // payloads (CLI telemetry, regression-harness reports) where a single
3386
- // number is friendlier than `{num,den}`. Callers needing the rational
3387
- // back can read `job.config.fps`.
3388
- fps: fpsToNumber4(input.job.config.fps),
3389
- quality: input.job.config.quality,
3390
- workers: input.workerCount,
3391
- workerSizing: input.workerSizing,
3392
- chunkedEncode: input.enableChunkedEncode,
3393
- chunkSizeFrames: input.enableChunkedEncode ? input.chunkedEncodeSize : null,
3394
- compositionDurationSeconds: input.compositionDurationSeconds,
3395
- totalFrames: input.totalFrames,
3396
- resolution: { width: input.outputWidth, height: input.outputHeight },
3397
- videoCount: input.videoCount,
3398
- audioCount: input.audioCount,
3399
- stages: input.perfStages,
3400
- videoExtractBreakdown: input.videoExtractBreakdown,
3401
- tmpPeakBytes: input.tmpPeakBytes,
3402
- captureCalibration: input.captureCalibration ? {
3403
- sampledFrames: input.captureCalibration.samples.map((sample) => sample.frameIndex),
3404
- p95Ms: input.captureCalibration.estimate.p95Ms,
3405
- multiplier: input.captureCalibration.estimate.multiplier,
3406
- reasons: input.captureCalibration.estimate.reasons
3407
- } : void 0,
3408
- captureAttempts: input.captureAttempts.length > 0 ? input.captureAttempts : void 0,
3409
- hdrDiagnostics: input.hdrDiagnostics.videoExtractionFailures > 0 || input.hdrDiagnostics.imageDecodeFailures > 0 ? { ...input.hdrDiagnostics } : void 0,
3410
- hdrPerf: input.hdrPerf ? finalizeHdrPerf(input.hdrPerf) : void 0,
3411
- observability: input.observability,
3412
- captureAvgMs: input.totalFrames > 0 ? Math.round(
3413
- (input.perfStages.captureFrameMs ?? input.perfStages.captureMs ?? 0) / input.totalFrames
3414
- ) : void 0,
3415
- subTimelineWait: worstSubTimelineWaitOutcome(input.dedupPerfs),
3416
- captureP50Ms: (() => {
3417
- const withSamples = input.dedupPerfs.filter((p) => (p.p50TotalMs ?? 0) > 0);
3418
- if (withSamples.length === 0) return void 0;
3419
- return withSamples.reduce((a, b) => b.frames > a.frames ? b : a).p50TotalMs;
3420
- })(),
3421
- peakRssMb: Math.round(input.peakRssBytes / (1024 * 1024)),
3422
- peakHeapUsedMb: Math.round(input.peakHeapUsedBytes / (1024 * 1024)),
3423
- staticDedup: aggregateDedup(input.dedupPerfs),
3424
- chromeMemory: aggregateChromeMemory(input.dedupPerfs),
3425
- beginFrameReuse: aggregateBeginFrameReuse(input.dedupPerfs),
3426
- drawElement: aggregateDrawElement(
3427
- input.dedupPerfs,
3428
- input.drawElement ?? { selfVerifyFallback: false }
3429
- ),
3430
- host: {
3431
- platform: platform(),
3432
- arch: arch(),
3433
- cpuCount: cpus().length,
3434
- totalMemMb: Math.round(totalmem() / (1024 * 1024)),
3435
- nodeVersion: process.version,
3436
- gpuDisabled: input.gpuDisabled
3437
- }
3438
- };
3439
- }
3440
-
3441
- // src/services/render/stages/captureStreamingStage.ts
3442
3462
  var DEFAULT_CAPTURE_STALL_MS = 6e4;
3443
3463
  var DE_STALL_POLL_MS = 5e3;
3444
3464
  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.";
3465
+ function poolFailure(error, probeSession) {
3466
+ return wrapCaptureStageError(error, probeSession?.browserConsoleBuffer ?? []);
3467
+ }
3445
3468
  var ParallelCaptureStallError = class extends Error {
3446
3469
  variant;
3447
3470
  constructor(args) {
@@ -3888,7 +3911,7 @@ async function runCaptureStreamingStage(input) {
3888
3911
  }
3889
3912
  );
3890
3913
  } catch (err) {
3891
- if (parallelDrainError) throw parallelDrainError;
3914
+ if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
3892
3915
  if (stalled && abortSignal?.aborted !== true) {
3893
3916
  throw new ParallelCaptureStallError({
3894
3917
  variant: "pool",
@@ -3899,13 +3922,15 @@ async function runCaptureStreamingStage(input) {
3899
3922
  phaseSummary: phaseSummary()
3900
3923
  });
3901
3924
  }
3902
- throw err;
3925
+ throw poolFailure(err, probeSession);
3903
3926
  } finally {
3904
3927
  clearInterval(stallTimer);
3905
3928
  abortSignal?.removeEventListener("abort", forwardParentAbort);
3906
3929
  }
3907
- if (parallelDrainError) throw parallelDrainError;
3930
+ if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
3908
3931
  pushWorkerDedupPerfs(workerResults, dedupPerfs);
3932
+ const workerPageErrors = [];
3933
+ pushWorkerPageErrors(workerResults, workerPageErrors);
3909
3934
  if (parallelGuardRan) {
3910
3935
  deDrainStats = parallelStats;
3911
3936
  }
@@ -3915,6 +3940,7 @@ async function runCaptureStreamingStage(input) {
3915
3940
  await closeCaptureSession(probeSession);
3916
3941
  probeSession = null;
3917
3942
  }
3943
+ lastBrowserConsole = withWorkerPageErrors(lastBrowserConsole, workerPageErrors);
3918
3944
  } else {
3919
3945
  const videoInjector = createRenderVideoFrameInjector();
3920
3946
  const session = probeSession ?? await createCaptureSession(
@@ -4361,8 +4387,9 @@ async function runCaptureSegmentedStage(input) {
4361
4387
 
4362
4388
  // src/services/render/segmentManifest.ts
4363
4389
  import { createHash as createHash2 } from "node:crypto";
4364
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
4390
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
4365
4391
  import { join as join10 } from "node:path";
4392
+ import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
4366
4393
  var MANIFEST_FILENAME = "segments.json";
4367
4394
  function computeSegmentPlanHash(input) {
4368
4395
  const ordered = [
@@ -4418,7 +4445,7 @@ function writeSegmentManifest(segmentDir, manifest) {
4418
4445
  mkdirSync4(segmentDir, { recursive: true });
4419
4446
  const tmp = join10(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4420
4447
  writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
4421
- renameSync2(tmp, join10(segmentDir, MANIFEST_FILENAME));
4448
+ renameWithRetry2(tmp, join10(segmentDir, MANIFEST_FILENAME));
4422
4449
  }
4423
4450
  async function probeSegmentFrameCount(path, probe) {
4424
4451
  try {
@@ -5266,6 +5293,8 @@ import {
5266
5293
  rewriteInlineStyleAssetUrls
5267
5294
  } from "@hyperframes/core";
5268
5295
  import { MAX_AUDIO_GAIN } from "@hyperframes/core/audio-gain";
5296
+ import { readPlaybackRate } from "@hyperframes/parsers/media-duration";
5297
+ import { resolveRateSpec } from "@hyperframes/core/speed-ramp";
5269
5298
  import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
5270
5299
  import {
5271
5300
  assignBundledRuntimeCompositionIds,
@@ -5470,19 +5499,11 @@ import {
5470
5499
 
5471
5500
  // src/services/animatedGifPrep.ts
5472
5501
  import { createHash as createHash4 } from "node:crypto";
5473
- import {
5474
- copyFileSync as copyFileSync2,
5475
- existsSync as existsSync8,
5476
- mkdirSync as mkdirSync5,
5477
- readFileSync as readFileSync5,
5478
- renameSync as renameSync3,
5479
- rmSync as rmSync6,
5480
- statSync as statSync4
5481
- } from "node:fs";
5502
+ import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync6, statSync as statSync4 } from "node:fs";
5482
5503
  import { dirname as dirname5, isAbsolute as isAbsolute2, join as join12, resolve as resolve5 } from "node:path";
5483
5504
  import { parseHTML as parseHTML3 } from "linkedom";
5484
5505
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
5485
- import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2 } from "@hyperframes/engine";
5506
+ import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
5486
5507
  var PREPARED_GIF_SUBDIR = "_animated_gif";
5487
5508
  var CACHE_SCHEMA = "hfgif-v1";
5488
5509
  function splitUrlSuffix(src) {
@@ -5661,7 +5682,7 @@ async function ensurePreparedWebm(input) {
5661
5682
  throw new Error("Animated GIF transcode produced an empty output");
5662
5683
  }
5663
5684
  if (!isUsableFile(input.cachePath)) {
5664
- renameSync3(tmpPath, input.cachePath);
5685
+ renameWithRetry3(tmpPath, input.cachePath);
5665
5686
  } else {
5666
5687
  rmSync6(tmpPath, { force: true });
5667
5688
  }
@@ -7249,6 +7270,9 @@ async function discoverMediaFromBrowser(page) {
7249
7270
  const intrinsicDuration = isImage ? 0 : htmlEl.duration;
7250
7271
  const playbackStartRaw = htmlEl.getAttribute("data-playback-start");
7251
7272
  const mediaStartRaw = htmlEl.getAttribute("data-media-start");
7273
+ const playbackRateRaw = htmlEl.getAttribute("data-playback-rate");
7274
+ const automationRaw = htmlEl.getAttribute("data-automation");
7275
+ const defaultPlaybackRate = isImage ? 1 : htmlEl.defaultPlaybackRate;
7252
7276
  const loop = htmlEl.hasAttribute("loop");
7253
7277
  const hasAudio = htmlEl.getAttribute("data-has-audio") === "true";
7254
7278
  const volume = parseFloat(htmlEl.getAttribute("data-volume") || "1");
@@ -7264,6 +7288,9 @@ async function discoverMediaFromBrowser(page) {
7264
7288
  durationInferred,
7265
7289
  playbackStartRaw,
7266
7290
  mediaStartRaw,
7291
+ playbackRateRaw,
7292
+ automationRaw,
7293
+ defaultPlaybackRate,
7267
7294
  loop,
7268
7295
  hasAudio,
7269
7296
  volume,
@@ -7273,7 +7300,17 @@ async function discoverMediaFromBrowser(page) {
7273
7300
  return results;
7274
7301
  });
7275
7302
  return elements.map(
7276
- ({ endRaw, durationRaw, intrinsicDuration, playbackStartRaw, mediaStartRaw, ...element }) => ({
7303
+ ({
7304
+ endRaw,
7305
+ durationRaw,
7306
+ intrinsicDuration,
7307
+ playbackStartRaw,
7308
+ mediaStartRaw,
7309
+ playbackRateRaw,
7310
+ automationRaw,
7311
+ defaultPlaybackRate,
7312
+ ...element
7313
+ }) => ({
7277
7314
  ...element,
7278
7315
  end: parseStrictFiniteTimingNumber(endRaw) ?? 0,
7279
7316
  duration: element.durationInferred && Number.isFinite(intrinsicDuration) && intrinsicDuration > 0 ? intrinsicDuration : parseStrictFiniteTimingNumber(durationRaw) ?? 0,
@@ -7283,7 +7320,14 @@ async function discoverMediaFromBrowser(page) {
7283
7320
  if (name === "data-media-start") return mediaStartRaw;
7284
7321
  return null;
7285
7322
  }
7286
- })
7323
+ }),
7324
+ playbackRate: resolveRateSpec(
7325
+ automationRaw,
7326
+ readPlaybackRate(
7327
+ (name) => name === "data-playback-rate" ? playbackRateRaw : null,
7328
+ defaultPlaybackRate
7329
+ )
7330
+ )
7287
7331
  })
7288
7332
  );
7289
7333
  }
@@ -7703,9 +7747,18 @@ import {
7703
7747
  getCompositionDuration,
7704
7748
  initializeSession as initializeSession4,
7705
7749
  isTransientBrowserError,
7706
- probeBeginFrameLiveness
7750
+ probeBeginFrameLiveness,
7751
+ videoAudioId
7707
7752
  } from "@hyperframes/engine";
7708
- import { MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2, fpsToNumber as fpsToNumber7, readMediaStart as readMediaStart2 } from "@hyperframes/core";
7753
+ import {
7754
+ MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
7755
+ fpsToNumber as fpsToNumber7,
7756
+ normalizeRateSpec as normalizeRateSpec3,
7757
+ readMediaStart as readMediaStart2,
7758
+ shiftRateLane as shiftRateLane2,
7759
+ sourceTimeAt as sourceTimeAt3,
7760
+ timeAtSourceTime
7761
+ } from "@hyperframes/core";
7709
7762
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
7710
7763
 
7711
7764
  // src/services/render/stages/probeFailures.ts
@@ -7764,16 +7817,28 @@ function authoredMediaStarts(html) {
7764
7817
  }
7765
7818
  return starts;
7766
7819
  }
7767
- function adoptRuntimeMediaStart(composition, el, authored) {
7820
+ function isVideoAudio(audio, videoId) {
7821
+ return audio.type === "video" && audio.id === videoAudioId(videoId);
7822
+ }
7823
+ function adoptRuntimeMediaTiming(composition, el, authored) {
7768
7824
  const shift = el.mediaStart - authored;
7769
- if (el.mediaStart <= 0 || Math.abs(shift) <= BROWSER_MEDIA_EPSILON) return;
7770
7825
  const entries = el.tagName === "video" ? [
7771
7826
  ...composition.videos.filter((video) => video.id === el.id),
7772
- ...composition.audios.filter(
7773
- (audio) => audio.type === "video" && audio.id === `${el.id}-audio`
7774
- )
7827
+ ...composition.audios.filter((audio) => isVideoAudio(audio, el.id))
7775
7828
  ] : composition.audios.filter((audio) => audio.type !== "video" && audio.id === el.id);
7776
- for (const entry of entries) entry.mediaStart += shift;
7829
+ for (const entry of entries) {
7830
+ if (typeof entry.playbackRate !== "object") {
7831
+ if (typeof el.playbackRate === "object") {
7832
+ const consumed = Math.max(0, entry.mediaStart - authored);
7833
+ const head = timeAtSourceTime(normalizeRateSpec3(entry.playbackRate), consumed);
7834
+ entry.mediaStart += sourceTimeAt3(el.playbackRate, head) - consumed;
7835
+ entry.playbackRate = shiftRateLane2(el.playbackRate, head);
7836
+ } else {
7837
+ entry.playbackRate = el.playbackRate;
7838
+ }
7839
+ }
7840
+ if (el.mediaStart > 0 && Math.abs(shift) > BROWSER_MEDIA_EPSILON) entry.mediaStart += shift;
7841
+ }
7777
7842
  }
7778
7843
  function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, durationInferred) {
7779
7844
  if (projectedEnd <= 0) return existingEnd;
@@ -8057,7 +8122,7 @@ async function runProbeStage(input) {
8057
8122
  sourceChanged,
8058
8123
  el.durationInferred
8059
8124
  );
8060
- adoptRuntimeMediaStart(
8125
+ adoptRuntimeMediaTiming(
8061
8126
  composition,
8062
8127
  el,
8063
8128
  authoredStarts.get(el.id) ?? existing.mediaStart
@@ -8080,6 +8145,7 @@ async function runProbeStage(input) {
8080
8145
  start: el.start,
8081
8146
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
8082
8147
  mediaStart: el.mediaStart,
8148
+ playbackRate: el.playbackRate,
8083
8149
  loop: el.loop,
8084
8150
  hasAudio: el.hasAudio && !el.muted
8085
8151
  });
@@ -8104,7 +8170,7 @@ async function runProbeStage(input) {
8104
8170
  sourceChanged,
8105
8171
  el.durationInferred
8106
8172
  );
8107
- adoptRuntimeMediaStart(
8173
+ adoptRuntimeMediaTiming(
8108
8174
  composition,
8109
8175
  el,
8110
8176
  authoredStarts.get(el.id) ?? existing.mediaStart
@@ -8122,6 +8188,7 @@ async function runProbeStage(input) {
8122
8188
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
8123
8189
  mediaStart: el.mediaStart,
8124
8190
  loop: el.loop,
8191
+ playbackRate: el.playbackRate,
8125
8192
  layer: 0,
8126
8193
  volume: el.volume,
8127
8194
  type: "audio"
@@ -8351,7 +8418,8 @@ import {
8351
8418
  isHttpUrl as isHttpUrl2,
8352
8419
  resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
8353
8420
  runVideoExtractionWithRetry,
8354
- safeVideoExtractionSourceIdentity
8421
+ safeVideoExtractionSourceIdentity,
8422
+ videoAudioId as videoAudioId2
8355
8423
  } from "@hyperframes/engine";
8356
8424
 
8357
8425
  // src/services/render/extractionFailureMetadata.ts
@@ -8704,7 +8772,7 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
8704
8772
  const video = composition.videos.find((v) => v.id === ext.videoId);
8705
8773
  if (!video || !video.hasAudio || video.hidden || existingAudioSrcs.has(video.src)) continue;
8706
8774
  composition.audios.push({
8707
- id: `${video.id}-audio`,
8775
+ id: videoAudioId2(video.id),
8708
8776
  src: video.src,
8709
8777
  start: video.start,
8710
8778
  end: video.end,
@@ -8730,7 +8798,7 @@ import {
8730
8798
 
8731
8799
  // src/services/render/audioPadTrim.ts
8732
8800
  import { spawn } from "node:child_process";
8733
- import { mkdtempSync as mkdtempSync3, renameSync as renameSync4, rmSync as rmSync7 } from "node:fs";
8801
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync7 } from "node:fs";
8734
8802
  import { dirname as dirname7, join as join17 } from "node:path";
8735
8803
  import {
8736
8804
  DEFAULT_CONFIG,
@@ -8739,6 +8807,7 @@ import {
8739
8807
  extractAudioMetadata as extractAudioMetadata3,
8740
8808
  getFfprobeBinary,
8741
8809
  ManagedChildProcess,
8810
+ renameWithRetry as renameWithRetry4,
8742
8811
  runFfmpeg as runFfmpeg4,
8743
8812
  trackChildProcess
8744
8813
  } from "@hyperframes/engine";
@@ -8994,7 +9063,7 @@ async function enforceAacTruePeak(input) {
8994
9063
  }
8995
9064
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
8996
9065
  if (measuredPath !== correctedPath) return { success: true };
8997
- renameSync4(correctedPath, input.audioPath);
9066
+ renameWithRetry4(correctedPath, input.audioPath);
8998
9067
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
8999
9068
  }
9000
9069
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -9184,6 +9253,7 @@ async function runAudioStage(input) {
9184
9253
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
9185
9254
  let audioLoweredDb;
9186
9255
  if (result.hasAudio) {
9256
+ const guardStart = Date.now();
9187
9257
  const peak = await constrainAacTruePeak({
9188
9258
  audioPath: result.audioOutputPath,
9189
9259
  sourcePath: pcmSourcePath,
@@ -9193,6 +9263,7 @@ async function runAudioStage(input) {
9193
9263
  });
9194
9264
  if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
9195
9265
  audioLoweredDb = peak.loweredDb;
9266
+ input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
9196
9267
  }
9197
9268
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9198
9269
  } finally {
@@ -9218,6 +9289,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9218
9289
  let hasAudio = false;
9219
9290
  let audioError;
9220
9291
  let audioFailures;
9292
+ let audioPhaseMs;
9221
9293
  if (audios.length > 0) {
9222
9294
  let audioResult;
9223
9295
  try {
@@ -9267,6 +9339,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9267
9339
  assertAudioNotInterrupted(audioResult.failures);
9268
9340
  hasAudio = audioResult.success;
9269
9341
  audioFailures = audioResult.failures;
9342
+ audioPhaseMs = audioResult.phaseMs;
9270
9343
  if (!hasAudio) {
9271
9344
  audioError = audioResult.error ?? "audio mix failed for an unknown reason";
9272
9345
  for (const failure of audioResult.failures ?? []) {
@@ -9285,7 +9358,8 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9285
9358
  audioOutputPath,
9286
9359
  hasAudio,
9287
9360
  audioError,
9288
- audioFailures
9361
+ audioFailures,
9362
+ audioPhaseMs
9289
9363
  };
9290
9364
  }
9291
9365
 
@@ -9414,6 +9488,7 @@ async function runCaptureStage(input) {
9414
9488
  let { workerCount } = plan;
9415
9489
  const { forceScreenshot } = plan;
9416
9490
  let lastBrowserConsole = [];
9491
+ const workerPageErrors = [];
9417
9492
  let captureBeyondViewport = probeSession?.options.captureBeyondViewport;
9418
9493
  const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
9419
9494
  if (frameRange !== void 0) {
@@ -9451,6 +9526,7 @@ async function runCaptureStage(input) {
9451
9526
  abortSignal,
9452
9527
  frameRangeStart: frameRange?.startFrame,
9453
9528
  dedupPerfs,
9529
+ pageErrorLines: workerPageErrors,
9454
9530
  onProgress: (progress) => {
9455
9531
  if (progress.latestWorkerPhase) {
9456
9532
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -9471,6 +9547,11 @@ async function runCaptureStage(input) {
9471
9547
  },
9472
9548
  cfg: captureCfg,
9473
9549
  log
9550
+ }).catch((error) => {
9551
+ throw wrapCaptureStageError(
9552
+ error,
9553
+ withWorkerPageErrors(probeSession?.browserConsoleBuffer ?? [], workerPageErrors)
9554
+ );
9474
9555
  });
9475
9556
  captureAttempts.push(...attempts);
9476
9557
  const lastAttempt = attempts[attempts.length - 1];
@@ -9483,6 +9564,7 @@ async function runCaptureStage(input) {
9483
9564
  await closeCaptureSession5(probeSession);
9484
9565
  probeSession = null;
9485
9566
  }
9567
+ lastBrowserConsole = withWorkerPageErrors(lastBrowserConsole, workerPageErrors);
9486
9568
  } else {
9487
9569
  const sequential = await runSequentialDiskCapture(
9488
9570
  input,
@@ -10798,7 +10880,7 @@ function videoEncoderOptions(input, engineCfg) {
10798
10880
  };
10799
10881
  }
10800
10882
  function canEncodeWhileCapturing(input, workerCount, cpuCount) {
10801
- 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;
10883
+ 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";
10802
10884
  }
10803
10885
  async function readLandedFrame(workerPath, mergedPath) {
10804
10886
  if (workerPath !== void 0) {
@@ -11453,6 +11535,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11453
11535
  options.cfg
11454
11536
  );
11455
11537
  pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
11538
+ if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
11456
11539
  } finally {
11457
11540
  await mergeWorkerFrames(attemptWorkDir, tasks, options.framesDir);
11458
11541
  }
@@ -11715,13 +11798,36 @@ function colorGradingValueHasLut(rawAttributeValue) {
11715
11798
  return false;
11716
11799
  }
11717
11800
  }
11718
- async function resolveCompositionElementCount(probeSession, html) {
11801
+ var PROBE_READ_TIMEOUT_MS = 5e3;
11802
+ async function evaluateWithinBound(page, fn, what, log) {
11803
+ let timer;
11804
+ const timedOut = new Promise((_, reject) => {
11805
+ timer = setTimeout(
11806
+ () => reject(new Error(`no answer within ${PROBE_READ_TIMEOUT_MS} ms (page busy)`)),
11807
+ PROBE_READ_TIMEOUT_MS
11808
+ );
11809
+ });
11810
+ try {
11811
+ return await Promise.race([page.evaluate(fn), timedOut]);
11812
+ } catch (err) {
11813
+ log.warn(`[Render] live ${what} read skipped; using the static answer`, {
11814
+ error: err instanceof Error ? err.message : String(err)
11815
+ });
11816
+ throw err;
11817
+ } finally {
11818
+ clearTimeout(timer);
11819
+ }
11820
+ }
11821
+ async function resolveCompositionElementCount(probeSession, html, log = defaultLogger) {
11719
11822
  if (probeSession?.isInitialized) {
11720
11823
  try {
11721
- const liveCount = await probeSession.page.evaluate(
11824
+ const liveCount = await evaluateWithinBound(
11825
+ probeSession.page,
11722
11826
  // Live HTMLCollection length — avoids materializing a static NodeList
11723
11827
  // on the large-DOM comps this gate exists to catch (review nit).
11724
- () => document.getElementsByTagName("*").length
11828
+ () => document.getElementsByTagName("*").length,
11829
+ "element count",
11830
+ log
11725
11831
  );
11726
11832
  if (typeof liveCount === "number" && Number.isFinite(liveCount)) {
11727
11833
  return { count: liveCount, source: "live" };
@@ -11809,11 +11915,11 @@ function probeAdaptersUsed() {
11809
11915
  }
11810
11916
  return used;
11811
11917
  }
11812
- async function resolveAdaptersUsed(probeSession, html) {
11918
+ async function resolveAdaptersUsed(probeSession, html, log = defaultLogger) {
11813
11919
  const staticAdapters = detectAdaptersStatic(html);
11814
11920
  if (!probeSession?.isInitialized) return staticAdapters;
11815
11921
  try {
11816
- const live = await probeSession.page.evaluate(probeAdaptersUsed);
11922
+ const live = await evaluateWithinBound(probeSession.page, probeAdaptersUsed, "adapters", log);
11817
11923
  if (!Array.isArray(live)) return staticAdapters;
11818
11924
  const detected = /* @__PURE__ */ new Set([...staticAdapters, ...live]);
11819
11925
  return KNOWN_RUNTIME_ADAPTERS.filter((name) => detected.has(name));
@@ -12071,7 +12177,10 @@ async function executeRenderPipeline(input) {
12071
12177
  );
12072
12178
  const artifactTransaction = new ArtifactTransaction(
12073
12179
  outputPath,
12074
- isPngSequence || isHls ? "directory" : "file"
12180
+ isPngSequence || isHls ? "directory" : "file",
12181
+ void 0,
12182
+ void 0,
12183
+ log
12075
12184
  );
12076
12185
  const stagedOutputPath = artifactTransaction.stagingPath;
12077
12186
  const needsAlpha = outputNeedsAlpha(outputFormat);
@@ -12570,8 +12679,8 @@ async function executeRenderPipeline(input) {
12570
12679
  vfxHostCount,
12571
12680
  vfxCapture,
12572
12681
  vfxTypes
12573
- } = await resolveCompositionElementCount(probeSession, compiled.html);
12574
- const adaptersUsed = await resolveAdaptersUsed(probeSession, compiled.html);
12682
+ } = await resolveCompositionElementCount(probeSession, compiled.html, log);
12683
+ const adaptersUsed = await resolveAdaptersUsed(probeSession, compiled.html, log);
12575
12684
  const deShortBandEnabled = deShortBandMaxElements > 0;
12576
12685
  const deShortBandOpen = deShortBandEnabled && deShortBandMinFrames > 0 && compositionElementCountSource === "live" && compositionElementCount <= deShortBandMaxElements;
12577
12686
  const deShortBandRoute = process.env.HF_DE_SHORT_BAND_ROUTE === "true";
@@ -14069,7 +14178,9 @@ import {
14069
14178
  getEncoderPreset as getEncoderPreset4,
14070
14179
  normalizeVp9CpuUsed,
14071
14180
  resolveConfig as resolveConfig3,
14072
- declareContentAddressedUrl
14181
+ declareContentAddressedUrl,
14182
+ renameWithRetry as renameWithRetry6,
14183
+ declareProbeFactsUrl
14073
14184
  } from "@hyperframes/engine";
14074
14185
 
14075
14186
  // src/services/distributed/planFrameCost.ts
@@ -14139,6 +14250,7 @@ var FIELD_DELIMITER = Buffer.from([0]);
14139
14250
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
14140
14251
 
14141
14252
  // src/services/distributed/mp4Stitch.ts
14253
+ import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
14142
14254
  var SCAN_BYTES = 64 * 1024;
14143
14255
 
14144
14256
  // src/services/distributed/chunkAudio.ts