@hyperframes/producer 0.8.205 → 0.8.207

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.
@@ -3,14 +3,14 @@
3
3
  // src/server.ts
4
4
  import {
5
5
  existsSync as existsSync19,
6
- mkdirSync as mkdirSync10,
7
- statSync as statSync9,
6
+ mkdirSync as mkdirSync11,
7
+ statSync as statSync10,
8
8
  mkdtempSync as mkdtempSync4,
9
- writeFileSync as writeFileSync6,
10
- rmSync as rmSync12,
9
+ writeFileSync as writeFileSync7,
10
+ rmSync as rmSync11,
11
11
  createReadStream as createReadStream3
12
12
  } from "node:fs";
13
- import { resolve as resolve9, dirname as dirname12, join as join30 } from "node:path";
13
+ import { resolve as resolve10, dirname as dirname13, join as join31 } from "node:path";
14
14
  import { tmpdir as tmpdir3 } from "node:os";
15
15
  import { parseArgs } from "node:util";
16
16
  import crypto from "node:crypto";
@@ -23,19 +23,19 @@ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engi
23
23
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
24
24
  import {
25
25
  existsSync as existsSync16,
26
- mkdirSync as mkdirSync9,
27
- readFileSync as readFileSync8,
26
+ mkdirSync as mkdirSync10,
27
+ readFileSync as readFileSync9,
28
28
  readdirSync as readdirSync5,
29
- rmSync as rmSync11,
30
- statSync as statSync7,
31
- writeFileSync as writeFileSync5,
32
- copyFileSync as copyFileSync4,
29
+ rmSync as rmSync10,
30
+ statSync as statSync8,
31
+ writeFileSync as writeFileSync6,
32
+ copyFileSync as copyFileSync5,
33
33
  appendFileSync
34
34
  } from "fs";
35
35
  import { tmpdir as tmpdir2 } from "node:os";
36
36
  import {
37
37
  chainCapture,
38
- fpsToNumber as fpsToNumber9,
38
+ fpsToNumber as fpsToNumber8,
39
39
  HF_COLOR_GRADING_ATTR,
40
40
  HF_VFX_ATTR,
41
41
  parseVfxChain,
@@ -77,7 +77,7 @@ import {
77
77
  augmentProtocolTimeoutError,
78
78
  augmentPageNavigationTimeoutError
79
79
  } from "@hyperframes/engine";
80
- import { join as join26, dirname as dirname10, resolve as resolve7 } from "path";
80
+ import { join as join27, dirname as dirname11, resolve as resolve8 } from "path";
81
81
  import { totalmem as totalmem2 } from "node:os";
82
82
  import { randomUUID } from "crypto";
83
83
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -101,6 +101,7 @@ import { Readable } from "node:stream";
101
101
  import { join, extname, resolve as resolve2, sep } from "node:path";
102
102
  import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler";
103
103
  import { fpsToNumber } from "@hyperframes/core";
104
+ import { stripSdrPngColourTags } from "@hyperframes/core/compiler/png-colour-tags";
104
105
 
105
106
  // src/services/hyperframeRuntimeLoader.ts
106
107
  import { createHash } from "node:crypto";
@@ -217,11 +218,11 @@ var defaultLogger = createConsoleLogger("info");
217
218
 
218
219
  // src/services/fileServer.ts
219
220
  var readFileAsync = promisify(readFile);
220
- function isPathInside(child, parent, options = {}) {
221
+ function isPathInside(child2, parent, options = {}) {
221
222
  const { resolveSymlinks = false, pathModule } = options;
222
223
  const resolveFn = pathModule?.resolve ?? resolve2;
223
224
  const separator = pathModule?.sep ?? sep;
224
- const resolvedChild = resolveFn(child);
225
+ const resolvedChild = resolveFn(child2);
225
226
  const resolvedParent = resolveFn(parent);
226
227
  const normalizedChild = resolveSymlinks && existsSync2(resolvedChild) ? realpathSync.native(resolvedChild) : resolvedChild;
227
228
  const normalizedParent = resolveSymlinks && existsSync2(resolvedParent) ? realpathSync.native(resolvedParent) : resolvedParent;
@@ -741,6 +742,18 @@ function createFileServer(options) {
741
742
  html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
742
743
  return c.text(html, 200, { "Content-Type": contentType });
743
744
  }
745
+ if (ext === ".png") {
746
+ const untagged = stripSdrPngColourTags(await readFileAsync(fd));
747
+ if (untagged) {
748
+ const headers = {
749
+ "Content-Type": contentType,
750
+ "Content-Length": String(untagged.length)
751
+ };
752
+ return new Response(c.req.method === "HEAD" ? null : new Uint8Array(untagged), {
753
+ headers
754
+ });
755
+ }
756
+ }
744
757
  const totalSize = stat.size;
745
758
  const rangeHeader = c.req.header("range");
746
759
  const rangeRequest = parseRangeHeader(rangeHeader, totalSize);
@@ -768,7 +781,8 @@ function createFileServer(options) {
768
781
  const stream = createReadStream(filePath, {
769
782
  fd,
770
783
  autoClose: true,
771
- ...range ? { start: range.start, end: range.end } : {}
784
+ start: range?.start ?? 0,
785
+ ...range ? { end: range.end } : {}
772
786
  });
773
787
  streamOwnsFd = true;
774
788
  try {
@@ -782,10 +796,10 @@ function createFileServer(options) {
782
796
  if (!streamOwnsFd) closeSync(fd);
783
797
  }
784
798
  });
785
- return new Promise((resolve10) => {
799
+ return new Promise((resolve11) => {
786
800
  const connections = /* @__PURE__ */ new Set();
787
801
  const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => {
788
- resolve10({
802
+ resolve11({
789
803
  url: `http://localhost:${info.port}`,
790
804
  port: info.port,
791
805
  addPreHeadScript: (script) => {
@@ -817,7 +831,7 @@ function outputUsesH264Pipeline(format) {
817
831
  }
818
832
 
819
833
  // src/services/render/hlsConfig.ts
820
- import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
834
+ import { keyframeIntervalToGopSize } from "@hyperframes/engine";
821
835
  var DEFAULT_HLS_SEGMENT_SECONDS = 4;
822
836
  function validateHlsRenderConfig(config) {
823
837
  if (config.format !== "hls") return;
@@ -842,7 +856,7 @@ function resolveHlsSegmentSeconds(config) {
842
856
  return config.hlsSegmentSeconds ?? DEFAULT_HLS_SEGMENT_SECONDS;
843
857
  }
844
858
  function resolveHlsGopSize(fps, segmentSeconds) {
845
- return Math.max(1, Math.round(fpsToNumber2(fps) * segmentSeconds));
859
+ return keyframeIntervalToGopSize(fps, segmentSeconds);
846
860
  }
847
861
  function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
848
862
  if (outputFormat !== "hls") return { lockGopForChunkConcat: false, gopSize: void 0 };
@@ -852,6 +866,38 @@ function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
852
866
  };
853
867
  }
854
868
 
869
+ // src/services/render/deliveryEncoder.ts
870
+ import { appendRateCapArgs, keyframeIntervalToGopSize as keyframeIntervalToGopSize2 } from "@hyperframes/engine";
871
+ function validateDeliveryEncoderConfig(config) {
872
+ const { keyframeIntervalSeconds, maxBitrate, bufferSize, format } = config;
873
+ if (keyframeIntervalSeconds === void 0 && maxBitrate === void 0 && bufferSize === void 0)
874
+ return;
875
+ if (keyframeIntervalSeconds !== void 0) {
876
+ if (!Number.isFinite(keyframeIntervalSeconds) || keyframeIntervalSeconds <= 0) {
877
+ throw new Error(
878
+ `keyframeIntervalSeconds must be a positive number of seconds; got ${keyframeIntervalSeconds}`
879
+ );
880
+ }
881
+ if (format === "hls") {
882
+ throw new Error(
883
+ "keyframeIntervalSeconds does not apply to hls; hlsSegmentSeconds sets its keyframes"
884
+ );
885
+ }
886
+ }
887
+ if (format !== void 0 && format !== "mp4") {
888
+ throw new Error(`keyframeIntervalSeconds and maxBitrate apply to mp4 renders, not ${format}`);
889
+ }
890
+ appendRateCapArgs([], { maxBitrate, bufferSize });
891
+ }
892
+ function resolveDeliveryEncoderOptions(config) {
893
+ const { maxBitrate, bufferSize, keyframeIntervalSeconds } = config;
894
+ const gop = keyframeIntervalSeconds === void 0 ? resolveHlsEncoderGopLock(config.format ?? "mp4", config.fps, config.hlsSegmentSeconds) : {
895
+ lockGopForChunkConcat: true,
896
+ gopSize: keyframeIntervalToGopSize2(config.fps, keyframeIntervalSeconds)
897
+ };
898
+ return { maxBitrate, bufferSize, ...gop };
899
+ }
900
+
855
901
  // src/services/render/shared.ts
856
902
  import {
857
903
  copyFileSync,
@@ -1292,8 +1338,8 @@ var OrderedRenderEventPublisher = class {
1292
1338
  let timer;
1293
1339
  const drained = await Promise.race([
1294
1340
  this.tail.then(() => true),
1295
- new Promise((resolve10) => {
1296
- timer = setTimeout(() => resolve10(false), FLUSH_TIMEOUT_MS);
1341
+ new Promise((resolve11) => {
1342
+ timer = setTimeout(() => resolve11(false), FLUSH_TIMEOUT_MS);
1297
1343
  })
1298
1344
  ]);
1299
1345
  clearTimeout(timer);
@@ -1870,7 +1916,7 @@ function replanSegmentedAfterFailure(plan, failure) {
1870
1916
  const keepSegmenting = failure.kind === "draw_element_verification" || failure.kind === "draw_element_capture";
1871
1917
  return createCapturePlan({
1872
1918
  ...plan,
1873
- forceScreenshot: keepSegmenting || failure.kind === "capture_failure",
1919
+ forceScreenshot: plan.forceScreenshot || keepSegmenting || failure.kind === "capture_failure",
1874
1920
  useStreamingEncode: true,
1875
1921
  useLayeredComposite: false,
1876
1922
  useSegmentedCapture: keepSegmenting,
@@ -1940,7 +1986,7 @@ import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
1940
1986
 
1941
1987
  // src/services/render/perfSummary.ts
1942
1988
  import { arch, cpus, platform, totalmem } from "node:os";
1943
- import { fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1989
+ import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
1944
1990
 
1945
1991
  // src/services/render/hdrPerf.ts
1946
1992
  function createHdrPerfCollector() {
@@ -2211,7 +2257,7 @@ function buildRenderPerfSummary(input) {
2211
2257
  // payloads (CLI telemetry, regression-harness reports) where a single
2212
2258
  // number is friendlier than `{num,den}`. Callers needing the rational
2213
2259
  // back can read `job.config.fps`.
2214
- fps: fpsToNumber3(input.job.config.fps),
2260
+ fps: fpsToNumber2(input.job.config.fps),
2215
2261
  quality: input.job.config.quality,
2216
2262
  workers: input.workerCount,
2217
2263
  workerSizing: input.workerSizing,
@@ -2796,7 +2842,7 @@ import {
2796
2842
  resolveVideoExtractionWindow,
2797
2843
  runFfmpeg
2798
2844
  } from "@hyperframes/engine";
2799
- import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
2845
+ import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
2800
2846
 
2801
2847
  // src/services/hdrFrameWindows.ts
2802
2848
  import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
@@ -3099,7 +3145,7 @@ async function extractHdrVideoFrames(args) {
3099
3145
  const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
3100
3146
  const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
3101
3147
  const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
3102
- const fps = fpsToNumber4(job.config.fps);
3148
+ const fps = fpsToNumber3(job.config.fps);
3103
3149
  const out = /* @__PURE__ */ new Map();
3104
3150
  mkdirSync2(framesDir, { recursive: true });
3105
3151
  let scratchBytes = 0;
@@ -3636,7 +3682,7 @@ var SequentialCaptureStallError = class extends Error {
3636
3682
  }
3637
3683
  };
3638
3684
  function raceAgainstStall(promise, deadlineMs, input, signal) {
3639
- return new Promise((resolve10, reject) => {
3685
+ return new Promise((resolve11, reject) => {
3640
3686
  const timer = setTimeout(
3641
3687
  () => {
3642
3688
  reject(
@@ -3655,7 +3701,7 @@ function raceAgainstStall(promise, deadlineMs, input, signal) {
3655
3701
  promise.then(
3656
3702
  (value) => {
3657
3703
  clearTimeout(timer);
3658
- resolve10(value);
3704
+ resolve11(value);
3659
3705
  },
3660
3706
  (error) => {
3661
3707
  clearTimeout(timer);
@@ -3937,7 +3983,6 @@ async function runCaptureStreamingStage(input) {
3937
3983
  const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
3938
3984
  if (workerCount > 1) {
3939
3985
  const deParallelStream = forceParallelStream === true || process.env.HF_DE_PARALLEL_STREAM === "true";
3940
- const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames, workerCount, workDir) : distributeFrames(totalFrames, workerCount, workDir);
3941
3986
  const parallelStats = {
3942
3987
  verifyChecked: 0,
3943
3988
  blankSuspects: 0,
@@ -3950,143 +3995,158 @@ async function runCaptureStreamingStage(input) {
3950
3995
  frameTime: (i) => i * job.config.fps.den / job.config.fps.num
3951
3996
  });
3952
3997
  let parallelGuardRan = false;
3953
- let parallelDrainError = null;
3954
- const onFrameBuffer = async (frameIndex, buffer, workerSession) => {
3955
- try {
3956
- if (deParallelStream && workerSession.captureMode === "drawelement") {
3957
- parallelGuardRan = true;
3958
- buffer = await parallelGuard(workerSession, frameIndex, buffer);
3959
- }
3960
- await reorderBuffer.waitForFrame(frameIndex);
3961
- ensureFrameWritten(await currentEncoder.writeFrame(buffer), frameIndex, currentEncoder);
3962
- reorderBuffer.advanceTo(frameIndex + 1);
3963
- } catch (err) {
3964
- const e = err instanceof Error ? err : new Error(String(err));
3965
- if (!parallelDrainError) {
3966
- parallelDrainError = e;
3967
- reorderBuffer.abort(e);
3998
+ let writtenFrames = 0;
3999
+ let resumeFrom = 0;
4000
+ let resumedAfterStall = false;
4001
+ let workerResults;
4002
+ for (; ; ) {
4003
+ const parallelReorder = createFrameReorderBuffer(resumeFrom, totalFrames);
4004
+ const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames - resumeFrom, workerCount, workDir, resumeFrom) : distributeFrames(totalFrames - resumeFrom, workerCount, workDir, resumeFrom);
4005
+ let parallelDrainError = null;
4006
+ const onFrameBuffer = async (frameIndex, buffer, workerSession) => {
4007
+ try {
4008
+ if (deParallelStream && workerSession.captureMode === "drawelement") {
4009
+ parallelGuardRan = true;
4010
+ buffer = await parallelGuard(workerSession, frameIndex, buffer);
4011
+ }
4012
+ await parallelReorder.waitForFrame(frameIndex);
4013
+ ensureFrameWritten(await currentEncoder.writeFrame(buffer), frameIndex, currentEncoder);
4014
+ parallelReorder.advanceTo(frameIndex + 1);
4015
+ writtenFrames = frameIndex + 1;
4016
+ } catch (err) {
4017
+ const e = err instanceof Error ? err : new Error(String(err));
4018
+ if (!parallelDrainError) {
4019
+ parallelDrainError = e;
4020
+ parallelReorder.abort(e);
4021
+ }
4022
+ throw e;
3968
4023
  }
3969
- throw e;
4024
+ };
4025
+ const captureLabel = input.parallelCaptureLabel ?? "worker";
4026
+ let watchdogArmed = false;
4027
+ const stallController = new AbortController();
4028
+ const forwardParentAbort = () => stallController.abort();
4029
+ if (abortSignal) {
4030
+ if (abortSignal.aborted) stallController.abort();
4031
+ else abortSignal.addEventListener("abort", forwardParentAbort, { once: true });
3970
4032
  }
3971
- };
3972
- const captureLabel = input.parallelCaptureLabel ?? "worker";
3973
- let watchdogArmed = false;
3974
- const stallController = new AbortController();
3975
- const forwardParentAbort = () => stallController.abort();
3976
- if (abortSignal) {
3977
- if (abortSignal.aborted) stallController.abort();
3978
- else abortSignal.addEventListener("abort", forwardParentAbort, { once: true });
3979
- }
3980
- const stallTimeoutMs = resolveCaptureStallTimeoutMs();
3981
- let lastCapturedFrames = 0;
3982
- let lastProgressAt = Date.now();
3983
- const workerPhases = /* @__PURE__ */ new Map();
3984
- const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
3985
- let stalled = false;
3986
- const stallTimer = setInterval(
3987
- () => {
3988
- if (!watchdogArmed || Date.now() - lastProgressAt <= stallTimeoutMs) return;
3989
- stalled = true;
3990
- const stallErr = new ParallelCaptureStallError({
3991
- variant: "drain",
3992
- captureLabel,
3993
- stallTimeoutMs,
3994
- lastCapturedFrames,
3995
- totalFrames,
3996
- phaseSummary: phaseSummary()
3997
- });
3998
- reorderBuffer.abort(stallErr);
3999
- stallController.abort();
4000
- },
4001
- Math.min(stallTimeoutMs, DE_STALL_POLL_MS)
4002
- );
4003
- let workerResults;
4004
- try {
4005
- workerResults = await executeParallelCapture(
4006
- fileServer.url,
4007
- workDir,
4008
- tasks,
4009
- buildCaptureOptions(),
4010
- createRenderVideoFrameInjector,
4011
- stallController.signal,
4012
- (progress) => {
4013
- if (progress.latestWorkerPhase) {
4014
- const phase = progress.latestWorkerPhase;
4015
- const detail = `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
4016
- workerPhases.set(phase.workerId, detail);
4017
- if (phase.phase === "frame_capture" && !watchdogArmed) {
4018
- watchdogArmed = true;
4033
+ const stallTimeoutMs = resolveCaptureStallTimeoutMs();
4034
+ let lastCapturedFrames = 0;
4035
+ let lastProgressAt = Date.now();
4036
+ const workerPhases = /* @__PURE__ */ new Map();
4037
+ const phaseDetail = (phase) => `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} step=${phase.readStep?.() ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
4038
+ const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
4039
+ let stalled = false;
4040
+ const stallTimer = setInterval(
4041
+ () => {
4042
+ if (!watchdogArmed || Date.now() - lastProgressAt <= stallTimeoutMs) return;
4043
+ stalled = true;
4044
+ const stallErr = new ParallelCaptureStallError({
4045
+ variant: "drain",
4046
+ captureLabel,
4047
+ stallTimeoutMs,
4048
+ lastCapturedFrames: resumeFrom + lastCapturedFrames,
4049
+ totalFrames,
4050
+ phaseSummary: phaseSummary()
4051
+ });
4052
+ parallelReorder.abort(stallErr);
4053
+ stallController.abort();
4054
+ },
4055
+ Math.min(stallTimeoutMs, DE_STALL_POLL_MS)
4056
+ );
4057
+ try {
4058
+ workerResults = await executeParallelCapture(
4059
+ fileServer.url,
4060
+ workDir,
4061
+ tasks,
4062
+ buildCaptureOptions(),
4063
+ createRenderVideoFrameInjector,
4064
+ stallController.signal,
4065
+ (progress) => {
4066
+ if (progress.latestWorkerPhase) {
4067
+ const phase = progress.latestWorkerPhase;
4068
+ workerPhases.set(phase.workerId, phaseDetail(phase));
4069
+ if (phase.phase === "frame_capture" && !watchdogArmed) {
4070
+ watchdogArmed = true;
4071
+ lastProgressAt = Date.now();
4072
+ }
4073
+ log.info("[Render] Parallel capture worker phase", {
4074
+ workerId: phase.workerId,
4075
+ phase: phase.phase,
4076
+ frameIndex: phase.frameIndex,
4077
+ browserExecutable: phase.browserExecutable,
4078
+ browserVersion: phase.browserVersion,
4079
+ canvasDrawElement: phase.canvasDrawElement,
4080
+ gpuBackend: phase.gpuBackend
4081
+ });
4082
+ if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
4083
+ return;
4084
+ }
4085
+ if (progress.capturedFrames > lastCapturedFrames) {
4086
+ lastCapturedFrames = progress.capturedFrames;
4019
4087
  lastProgressAt = Date.now();
4088
+ watchdogArmed = true;
4089
+ }
4090
+ const doneFrames = resumeFrom + progress.capturedFrames;
4091
+ job.framesRendered = doneFrames;
4092
+ const frameProgress = doneFrames / totalFrames;
4093
+ reportFrameProgress(
4094
+ job,
4095
+ `Streaming frame ${doneFrames}/${totalFrames} (${workerCount} workers)`,
4096
+ Math.round(25 + frameProgress * 55),
4097
+ onProgress,
4098
+ doneFrames,
4099
+ totalFrames
4100
+ );
4101
+ },
4102
+ onFrameBuffer,
4103
+ // Interleaved DE workers each get their own browser process: co-tenant pages starve
4104
+ // each other's paint events.
4105
+ deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
4106
+ {
4107
+ // A worker died: release parked peers with its original failure, not the stall the
4108
+ // watchdog raises later.
4109
+ onWorkerOperation: (phase) => workerPhases.set(phase.workerId, phaseDetail(phase)),
4110
+ onWorkerFailure: (failure) => {
4111
+ if (parallelDrainError) return;
4112
+ parallelDrainError = failure;
4113
+ parallelReorder.abort(failure);
4020
4114
  }
4021
- log.info("[Render] Parallel capture worker phase", {
4022
- workerId: phase.workerId,
4023
- phase: phase.phase,
4024
- frameIndex: phase.frameIndex,
4025
- browserExecutable: phase.browserExecutable,
4026
- browserVersion: phase.browserVersion,
4027
- canvasDrawElement: phase.canvasDrawElement,
4028
- gpuBackend: phase.gpuBackend
4029
- });
4030
- if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
4031
- return;
4032
- }
4033
- if (progress.capturedFrames > lastCapturedFrames) {
4034
- lastCapturedFrames = progress.capturedFrames;
4035
- lastProgressAt = Date.now();
4036
- watchdogArmed = true;
4037
4115
  }
4038
- job.framesRendered = progress.capturedFrames;
4039
- const frameProgress = progress.capturedFrames / progress.totalFrames;
4040
- reportFrameProgress(
4041
- job,
4042
- `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
4043
- Math.round(25 + frameProgress * 55),
4044
- onProgress,
4045
- progress.capturedFrames,
4046
- progress.totalFrames
4116
+ );
4117
+ } catch (err) {
4118
+ if (stalled && abortSignal?.aborted !== true && captureLabel === "BeginFrame" && !resumedAfterStall && writtenFrames > resumeFrom) {
4119
+ resumedAfterStall = true;
4120
+ log.warn(
4121
+ "[Render] Parallel BeginFrame capture stalled; resuming from the first unwritten frame",
4122
+ {
4123
+ resumeFrom: writtenFrames,
4124
+ totalFrames,
4125
+ workerPhases: phaseSummary()
4126
+ }
4047
4127
  );
4048
- },
4049
- onFrameBuffer,
4050
- // Interleaved DE workers each need their own browser PROCESS:
4051
- // pages co-tenant in one browser share one compositor, whose
4052
- // internal frame scheduling deprioritizes non-active pages — their
4053
- // canvas `paint` events starve and the drawElement paint-wait slows
4054
- // ~3x (measured: 86s vs 30s on a 3,245-frame rAF comp). This is
4055
- // Chromium's internal frame-production scheduling on ALL platforms,
4056
- // NOT the Linux-only HeadlessExperimental.beginFrame capture mode.
4057
- deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
4058
- {
4059
- // A worker died (Target closed, Page crashed, launch failure).
4060
- // Its frames are gone and peers are parked in waitForFrame on the
4061
- // first of them; release them with the ORIGINAL failure so the
4062
- // orchestrator sees a transient browser error it knows how to
4063
- // retry, not the stall the watchdog would have raised a minute
4064
- // later. First failure wins, same as the drain guard above.
4065
- onWorkerFailure: (failure) => {
4066
- if (parallelDrainError) return;
4067
- parallelDrainError = failure;
4068
- reorderBuffer.abort(failure);
4069
- }
4128
+ resumeFrom = writtenFrames;
4129
+ continue;
4070
4130
  }
4071
- );
4072
- } catch (err) {
4073
- if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
4074
- if (stalled && abortSignal?.aborted !== true) {
4075
- throw new ParallelCaptureStallError({
4076
- variant: "pool",
4077
- captureLabel,
4078
- stallTimeoutMs,
4079
- lastCapturedFrames,
4080
- totalFrames,
4081
- phaseSummary: phaseSummary()
4082
- });
4131
+ if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
4132
+ if (stalled && abortSignal?.aborted !== true) {
4133
+ throw new ParallelCaptureStallError({
4134
+ variant: "pool",
4135
+ captureLabel,
4136
+ stallTimeoutMs,
4137
+ lastCapturedFrames: resumeFrom + lastCapturedFrames,
4138
+ totalFrames,
4139
+ phaseSummary: phaseSummary()
4140
+ });
4141
+ }
4142
+ throw poolFailure(err, probeSession);
4143
+ } finally {
4144
+ clearInterval(stallTimer);
4145
+ abortSignal?.removeEventListener("abort", forwardParentAbort);
4083
4146
  }
4084
- throw poolFailure(err, probeSession);
4085
- } finally {
4086
- clearInterval(stallTimer);
4087
- abortSignal?.removeEventListener("abort", forwardParentAbort);
4147
+ if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
4148
+ break;
4088
4149
  }
4089
- if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
4090
4150
  pushWorkerDedupPerfs(workerResults, dedupPerfs);
4091
4151
  const workerPageErrors = [];
4092
4152
  pushWorkerPageErrors(workerResults, workerPageErrors);
@@ -4247,7 +4307,7 @@ function segmentEncoderOptions(base, segment) {
4247
4307
  return {
4248
4308
  ...base,
4249
4309
  lockGopForChunkConcat: true,
4250
- gopSize: segment.endFrame - segment.startFrame
4310
+ gopSize: Math.min(base.gopSize ?? Infinity, segment.endFrame - segment.startFrame)
4251
4311
  };
4252
4312
  }
4253
4313
  var SegmentEncoderSpawnError = class extends Error {
@@ -4341,6 +4401,12 @@ function defaultSessionFactory(input) {
4341
4401
  let probeSession = input.probeSession;
4342
4402
  return {
4343
4403
  create: async () => {
4404
+ const probeCannotCaptureEveryFrame = Boolean(probeSession?.deRiskFrames?.size);
4405
+ if (probeSession && probeCannotCaptureEveryFrame) {
4406
+ await closeCaptureSession2(probeSession).catch(() => {
4407
+ });
4408
+ probeSession = null;
4409
+ }
4344
4410
  const session = probeSession ?? await openSegmentedSession(input);
4345
4411
  if (probeSession) {
4346
4412
  openSegmentedSessionReuse(input, probeSession);
@@ -4671,8 +4737,10 @@ function computeSegmentPlanHash(input) {
4671
4737
  input.motionBlur,
4672
4738
  input.pageMotionBlur ?? "",
4673
4739
  input.captureQuality === void 0 ? "" : String(input.captureQuality)
4674
- ].join(" ");
4675
- return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
4740
+ ];
4741
+ const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
4742
+ if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
4743
+ return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
4676
4744
  }
4677
4745
  function segmentDirFor(projectRendersDir, planHash) {
4678
4746
  return join11(projectRendersDir, ".hf-segments", planHash);
@@ -4819,7 +4887,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
4819
4887
 
4820
4888
  // src/services/render/captureCost.ts
4821
4889
  import { join as join12 } from "node:path";
4822
- import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
4890
+ import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
4823
4891
  import {
4824
4892
  calculateOptimalWorkers,
4825
4893
  captureFrameToBuffer as captureFrameToBuffer3,
@@ -5023,7 +5091,7 @@ async function runCaptureCalibration(input) {
5023
5091
  let probeSession = input.probeSession;
5024
5092
  let forceScreenshot = input.forceScreenshot;
5025
5093
  let lastBrowserConsole = [];
5026
- const fps = fpsToNumber5(job.config.fps);
5094
+ const fps = fpsToNumber4(job.config.fps);
5027
5095
  const sessionRef = { current: null };
5028
5096
  const runOneCalibration = async (sessionDir, sessionCfg) => {
5029
5097
  log.info("Launching browser for capture calibration...");
@@ -5483,7 +5551,7 @@ function emitFallbackCaptureProfile(recorder, perfSummaries, env = process.env)
5483
5551
  // src/services/render/videoFrameCoverage.ts
5484
5552
  import { parseHTML } from "linkedom";
5485
5553
  import {
5486
- fpsToNumber as fpsToNumber6,
5554
+ fpsToNumber as fpsToNumber5,
5487
5555
  normalizeRateSpec,
5488
5556
  sourceTimeAt,
5489
5557
  toFps
@@ -5518,7 +5586,7 @@ function resolveVideoCoverageThreshold(envValue = process.env.HF_VIDEO_COVERAGE_
5518
5586
  return parsed;
5519
5587
  }
5520
5588
  function expectedFramesForClip(start, end, fps, rounding = "ceil") {
5521
- const fpsValue = fpsToNumber6(toFps(fps));
5589
+ const fpsValue = fpsToNumber5(toFps(fps));
5522
5590
  if (!Number.isFinite(start) || !Number.isFinite(end) || !Number.isFinite(fpsValue)) return 0;
5523
5591
  if (fpsValue <= 0) return 0;
5524
5592
  const duration = Math.max(0, end - start);
@@ -5583,15 +5651,15 @@ function countAuthoredTimedClips(html) {
5583
5651
  }
5584
5652
 
5585
5653
  // src/services/render/stages/compileStage.ts
5586
- import { join as join15 } from "node:path";
5654
+ import { join as join16 } from "node:path";
5587
5655
  import { suggestMatchingPreset } from "@hyperframes/core";
5588
5656
 
5589
5657
  // src/services/htmlCompiler.ts
5590
5658
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5591
- import { createHash as createHash5 } from "node:crypto";
5592
- import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync6 } from "fs";
5593
- import { join as join14, dirname as dirname6, resolve as resolve6, basename as basename4, relative as relative2 } from "path";
5594
- import { parseHTML as parseHTML4 } from "linkedom";
5659
+ import { createHash as createHash6 } from "node:crypto";
5660
+ import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
5661
+ import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
5662
+ import { parseHTML as parseHTML5 } from "linkedom";
5595
5663
  import {
5596
5664
  COMPOSITION_SOURCE_URL,
5597
5665
  compileTimingAttrs,
@@ -5867,7 +5935,7 @@ function readLoopOverride(el) {
5867
5935
  }
5868
5936
  return null;
5869
5937
  }
5870
- function resolveGifSourcePath(src, options) {
5938
+ function resolveLocalImageSourcePath(src, options) {
5871
5939
  const trimmed = src.trim();
5872
5940
  if (!trimmed || trimmed.startsWith("data:")) return null;
5873
5941
  const { basePath } = splitUrlSuffix(trimmed);
@@ -6090,7 +6158,7 @@ async function prepareAnimatedGifInputs(html, options) {
6090
6158
  const img = images[i];
6091
6159
  const src = (img.getAttribute("src") || "").trim();
6092
6160
  if (!hasGifExtension(src)) continue;
6093
- const sourcePath = resolveGifSourcePath(src, options);
6161
+ const sourcePath = resolveLocalImageSourcePath(src, options);
6094
6162
  if (!sourcePath) continue;
6095
6163
  const bytes = readFileSync5(sourcePath);
6096
6164
  const metadata = parseAnimatedGifMetadata(bytes);
@@ -6138,19 +6206,74 @@ async function prepareAnimatedGifInputs(html, options) {
6138
6206
  };
6139
6207
  }
6140
6208
 
6141
- // src/services/htmlCompiler.ts
6142
- import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-server/manual-edits-render-script";
6143
- import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
6144
-
6145
- // src/services/assetMediaType.ts
6146
- import { existsSync as existsSync9 } from "node:fs";
6209
+ // src/services/heifImagePrep.ts
6210
+ import { createHash as createHash5 } from "node:crypto";
6147
6211
  import {
6148
- fingerprintElementId,
6149
- isNotMediaPayload,
6150
- NotMediaPayloadError,
6151
- probeMediaProfile,
6152
- resolveProjectRelativeSrc as resolveProjectRelativeSrc2
6153
- } from "@hyperframes/engine";
6212
+ closeSync as closeSync4,
6213
+ copyFileSync as copyFileSync3,
6214
+ mkdirSync as mkdirSync6,
6215
+ openSync as openSync4,
6216
+ readFileSync as readFileSync6,
6217
+ readSync as readSync3,
6218
+ rmSync as rmSync8,
6219
+ statSync as statSync6,
6220
+ writeFileSync as writeFileSync4
6221
+ } from "node:fs";
6222
+ import { dirname as dirname6, join as join14, resolve as resolve6 } from "node:path";
6223
+ import { parseHTML as parseHTML4 } from "linkedom";
6224
+ import { renameWithRetry as renameWithRetry4, runFfmpeg as runFfmpeg3, runFfprobe } from "@hyperframes/engine";
6225
+
6226
+ // src/services/heifRotation.ts
6227
+ function* boxes(buf, start, end) {
6228
+ let at = start;
6229
+ while (at + 8 <= end) {
6230
+ let size = buf.readUInt32BE(at);
6231
+ let header = 8;
6232
+ if (size === 1 && at + 16 <= end) {
6233
+ size = Number(buf.readBigUInt64BE(at + 8));
6234
+ header = 16;
6235
+ } else if (size === 0) {
6236
+ size = end - at;
6237
+ }
6238
+ if (size < header || at + size > end) return;
6239
+ yield { type: buf.toString("latin1", at + 4, at + 8), start: at + header, end: at + size };
6240
+ at += size;
6241
+ }
6242
+ }
6243
+ function child(buf, parent, type, skip = 0) {
6244
+ return [...boxes(buf, parent.start + skip, parent.end)].filter((box) => box.type === type);
6245
+ }
6246
+ function readHeifPrimaryRotation(buf) {
6247
+ try {
6248
+ const root = { type: "", start: 0, end: buf.length };
6249
+ const meta = child(buf, root, "meta")[0];
6250
+ const pitm = meta && child(buf, meta, "pitm", 4)[0];
6251
+ const iprp = meta && child(buf, meta, "iprp", 4)[0];
6252
+ const ipco = iprp && child(buf, iprp, "ipco")[0];
6253
+ if (!pitm || !ipco || !iprp) return 0;
6254
+ const primaryId = buf[pitm.start] === 0 ? buf.readUInt16BE(pitm.start + 4) : buf.readUInt32BE(pitm.start + 4);
6255
+ const props = [...boxes(buf, ipco.start, ipco.end)];
6256
+ for (const ipma of child(buf, iprp, "ipma")) {
6257
+ const version = buf[ipma.start];
6258
+ const wide = (buf[ipma.start + 3] & 1) === 1;
6259
+ let at = ipma.start + 8;
6260
+ for (let n = buf.readUInt32BE(ipma.start + 4); n > 0 && at < ipma.end; n--) {
6261
+ const id = version < 1 ? buf.readUInt16BE(at) : buf.readUInt32BE(at);
6262
+ at += version < 1 ? 2 : 4;
6263
+ const count = buf[at++];
6264
+ for (let i = 0; i < count; i++) {
6265
+ const index = wide ? buf.readUInt16BE(at) & 32767 : buf[at] & 127;
6266
+ at += wide ? 2 : 1;
6267
+ const prop = props[index - 1];
6268
+ if (id === primaryId && prop?.type === "irot")
6269
+ return (buf[prop.start] & 3) * 90;
6270
+ }
6271
+ }
6272
+ }
6273
+ } catch {
6274
+ }
6275
+ return 0;
6276
+ }
6154
6277
 
6155
6278
  // src/utils/semaphore.ts
6156
6279
  var Semaphore = class {
@@ -6165,10 +6288,10 @@ var Semaphore = class {
6165
6288
  this.active++;
6166
6289
  return () => this.release();
6167
6290
  }
6168
- return new Promise((resolve10) => {
6291
+ return new Promise((resolve11) => {
6169
6292
  this.queue.push(() => {
6170
6293
  this.active++;
6171
- resolve10(() => this.release());
6294
+ resolve11(() => this.release());
6172
6295
  });
6173
6296
  });
6174
6297
  }
@@ -6199,7 +6322,229 @@ async function withMediaProbeSlot(operation) {
6199
6322
  }
6200
6323
  }
6201
6324
 
6325
+ // src/services/heifImagePrep.ts
6326
+ var PREPARED_HEIF_SUBDIR = "_heif_images";
6327
+ var CACHE_SCHEMA2 = "hfheif-v1";
6328
+ var DEFAULT_TIMEOUT_MS = 12e4;
6329
+ var HEIF_IMAGE_CONVERSION_FAILED = "HEIF_IMAGE_CONVERSION_FAILED";
6330
+ var HEIF_TOOL_UNAVAILABLE = "HEIF_TOOL_UNAVAILABLE";
6331
+ var HEIF_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "heim", "heis", "mif1", "msf1"]);
6332
+ var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
6333
+ var DATA_URL_RE = /^data:image\/(?:heic|heif)(?:-sequence)?;base64,(.*)$/is;
6334
+ var HeifImageConversionError = class extends Error {
6335
+ code = HEIF_IMAGE_CONVERSION_FAILED;
6336
+ owner = "user";
6337
+ retryable = false;
6338
+ constructor(label, reason) {
6339
+ super(
6340
+ `Could not convert HEIC/HEIF image ${label} to JPEG: ${reason}. Re-export the photo as JPEG or PNG and update its src.`
6341
+ );
6342
+ this.name = "HeifImageConversionError";
6343
+ }
6344
+ };
6345
+ var HeifToolUnavailableError = class extends Error {
6346
+ code = HEIF_TOOL_UNAVAILABLE;
6347
+ owner = "system";
6348
+ retryable = false;
6349
+ constructor(label, reason) {
6350
+ super(
6351
+ `Could not convert HEIC/HEIF image ${label}: ${reason}. Converting HEIC needs ffmpeg and ffprobe 7.0 or newer with the hevc decoder.`
6352
+ );
6353
+ this.name = "HeifToolUnavailableError";
6354
+ }
6355
+ };
6356
+ var TOOL_FAILURE_RE = /(?:ffmpeg|ffprobe) not found|ENOENT|Option not found|Unrecognized option|Unknown decoder|Decoder \S+ not found/i;
6357
+ function isHeifBytes(head) {
6358
+ const bytes = Buffer.from(head.buffer, head.byteOffset, head.byteLength);
6359
+ if (bytes.length < 16 || bytes.toString("ascii", 4, 8) !== "ftyp") return false;
6360
+ const boxEnd = Math.min(bytes.readUInt32BE(0), bytes.length);
6361
+ if (boxEnd < 16) return false;
6362
+ const brands = [bytes.toString("ascii", 8, 12)];
6363
+ for (let offset = 16; offset + 4 <= boxEnd; offset += 4) {
6364
+ brands.push(bytes.toString("ascii", offset, offset + 4));
6365
+ }
6366
+ return !brands.some((b) => AVIF_BRANDS.has(b)) && brands.some((b) => HEIF_BRANDS.has(b));
6367
+ }
6368
+ function readHead(path) {
6369
+ let fd;
6370
+ try {
6371
+ fd = openSync4(path, "r");
6372
+ const buf = Buffer.alloc(4096);
6373
+ return buf.subarray(0, readSync3(fd, buf, 0, buf.length, 0));
6374
+ } catch {
6375
+ return null;
6376
+ } finally {
6377
+ if (fd !== void 0) closeSync4(fd);
6378
+ }
6379
+ }
6380
+ async function probeJson(path, withGroups) {
6381
+ const stdout = await runFfprobe(
6382
+ path,
6383
+ withGroups ? ["-show_stream_groups", "-show_streams", "-of", "json"] : ["-show_streams", "-of", "json"]
6384
+ );
6385
+ return JSON.parse(stdout);
6386
+ }
6387
+ var ROTATION_FILTER = {
6388
+ 0: "",
6389
+ 90: "transpose=2",
6390
+ 180: "hflip,vflip",
6391
+ 270: "transpose=1"
6392
+ };
6393
+ function primaryGrid(probed) {
6394
+ const groups = probed.stream_groups ?? [];
6395
+ return (groups.find((g) => g.disposition?.default === 1) ?? groups[0])?.components?.[0];
6396
+ }
6397
+ async function convertHeifToJpeg(request) {
6398
+ const probed = await probeJson(request.inputPath, true);
6399
+ const grid = primaryGrid(probed);
6400
+ const rotation = readHeifPrimaryRotation(readFileSync6(request.inputPath));
6401
+ const filters = [];
6402
+ let inputs = [];
6403
+ let size = probed.streams?.[0];
6404
+ if (grid?.subcomponents?.length) {
6405
+ size = grid;
6406
+ const layout = grid.subcomponents.map(
6407
+ (t) => `${t.tile_horizontal_offset}_${t.tile_vertical_offset}`
6408
+ );
6409
+ inputs = grid.subcomponents.map((t) => `[0:${t.stream_index}]`);
6410
+ filters.push(
6411
+ `${inputs.join("")}xstack=inputs=${inputs.length}:layout=${layout.join("|")}`,
6412
+ `crop=${grid.width}:${grid.height}:${grid.horizontal_offset ?? 0}:${grid.vertical_offset ?? 0}`
6413
+ );
6414
+ }
6415
+ if (!size?.width || !size.height) throw new Error("no declared image size");
6416
+ if (rotation && grid) filters.push(ROTATION_FILTER[rotation]);
6417
+ const mapArgs = filters.length > 0 ? ["-filter_complex", `${filters.join(",")}[v]`, "-map", "[v]"] : [];
6418
+ const result = await runFfmpeg3(
6419
+ [
6420
+ "-hide_banner",
6421
+ "-loglevel",
6422
+ "error",
6423
+ "-i",
6424
+ request.inputPath,
6425
+ ...mapArgs,
6426
+ "-frames:v",
6427
+ "1",
6428
+ "-q:v",
6429
+ "2",
6430
+ "-f",
6431
+ "mjpeg",
6432
+ "-y",
6433
+ request.outputPath
6434
+ ],
6435
+ { timeout: request.timeoutMs ?? DEFAULT_TIMEOUT_MS }
6436
+ );
6437
+ if (!result.success) {
6438
+ throw result.error ?? new Error(`ffmpeg exited ${result.exitCode}: ${result.stderr.slice(-300)}`);
6439
+ }
6440
+ const out = (await probeJson(request.outputPath, false)).streams?.[0];
6441
+ const same = out?.width === size.width && out.height === size.height;
6442
+ const swapped = out?.width === size.height && out.height === size.width;
6443
+ if (!same && !swapped) {
6444
+ throw new Error(
6445
+ `wrong size: decoded ${out?.width}x${out?.height}, file declares ${size.width}x${size.height}`
6446
+ );
6447
+ }
6448
+ }
6449
+ function resolveImageSource(src, options) {
6450
+ const data = DATA_URL_RE.exec(src);
6451
+ if (data) {
6452
+ const bytes = Buffer.from(data[1], "base64");
6453
+ return isHeifBytes(bytes.subarray(0, 4096)) ? { path: null, bytes, label: '"inline image"' } : null;
6454
+ }
6455
+ const path = resolveLocalImageSourcePath(src, options);
6456
+ const head = path ? readHead(path) : null;
6457
+ if (!path || !head || !isHeifBytes(head)) return null;
6458
+ return { path, bytes: readFileSync6(path), label: `"${splitUrlSuffix(src).basePath}"` };
6459
+ }
6460
+ function isUsableFile2(path) {
6461
+ try {
6462
+ return statSync6(path).size > 0;
6463
+ } catch {
6464
+ return false;
6465
+ }
6466
+ }
6467
+ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
6468
+ mkdirSync6(dirname6(cachePath), { recursive: true });
6469
+ mkdirSync6(dirname6(outputPath), { recursive: true });
6470
+ if (!isUsableFile2(cachePath)) {
6471
+ const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
6472
+ const inlinePath = `${tmpPath}.heic`;
6473
+ const inputPath = source.path ?? inlinePath;
6474
+ try {
6475
+ if (!source.path) writeFileSync4(inlinePath, source.bytes);
6476
+ await (options.transcode ?? convertHeifToJpeg)({
6477
+ inputPath,
6478
+ outputPath: tmpPath,
6479
+ timeoutMs: options.timeoutMs
6480
+ });
6481
+ if (!isUsableFile2(tmpPath)) throw new Error("conversion produced an empty file");
6482
+ renameWithRetry4(tmpPath, cachePath);
6483
+ } catch (error) {
6484
+ const reason = error instanceof Error ? error.message : String(error);
6485
+ throw TOOL_FAILURE_RE.test(reason) ? new HeifToolUnavailableError(source.label, reason) : new HeifImageConversionError(source.label, reason);
6486
+ } finally {
6487
+ rmSync8(tmpPath, { force: true });
6488
+ rmSync8(inlinePath, { force: true });
6489
+ }
6490
+ }
6491
+ if (resolve6(outputPath) !== resolve6(cachePath) && !isUsableFile2(outputPath)) {
6492
+ copyFileSync3(cachePath, outputPath);
6493
+ }
6494
+ }
6495
+ async function prepareHeifImageInputs(html, options) {
6496
+ const outputDir = options.outputDir ?? join14(options.downloadDir, PREPARED_HEIF_SUBDIR);
6497
+ const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_HEIF_SUBDIR);
6498
+ const cacheDir = options.cacheDir ?? outputDir;
6499
+ const preparedAssets = /* @__PURE__ */ new Map();
6500
+ const preparedImages = [];
6501
+ if (!/<img\b/i.test(html)) return { html, preparedAssets, preparedImages };
6502
+ const { document: document2 } = parseHTML4(html);
6503
+ const found = [];
6504
+ for (const img of Array.from(document2.querySelectorAll("img[src]"))) {
6505
+ const src = (img.getAttribute("src") || "").trim();
6506
+ const source = src ? resolveImageSource(src, options) : null;
6507
+ if (!source) continue;
6508
+ const hash = createHash5("sha256").update(CACHE_SCHEMA2).update("\0").update(source.bytes).digest("hex");
6509
+ found.push({ img, src, source, filename: `${CACHE_SCHEMA2}-${hash.slice(0, 24)}.jpg` });
6510
+ }
6511
+ if (found.length === 0) return { html, preparedAssets, preparedImages };
6512
+ const unique = new Map(found.map((item) => [item.filename, item.source]));
6513
+ const failed = /* @__PURE__ */ new Set();
6514
+ await Promise.all(
6515
+ [...unique].map(
6516
+ ([filename, source]) => withMediaProbeSlot(
6517
+ () => ensurePreparedJpeg(source, join14(cacheDir, filename), join14(outputDir, filename), options)
6518
+ ).catch((error) => {
6519
+ if (!options.onFailure) throw error;
6520
+ failed.add(filename);
6521
+ options.onFailure(error);
6522
+ })
6523
+ )
6524
+ );
6525
+ for (const { img, src, filename } of found) {
6526
+ if (failed.has(filename)) continue;
6527
+ const outputSrc = `${outputSrcPrefix}/${filename}`;
6528
+ img.setAttribute("src", outputSrc);
6529
+ preparedAssets.set(outputSrc, join14(outputDir, filename));
6530
+ preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join14(outputDir, filename) });
6531
+ }
6532
+ return { html: document2.toString(), preparedAssets, preparedImages };
6533
+ }
6534
+
6535
+ // src/services/htmlCompiler.ts
6536
+ import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-server/manual-edits-render-script";
6537
+ import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
6538
+
6202
6539
  // src/services/assetMediaType.ts
6540
+ import { existsSync as existsSync9 } from "node:fs";
6541
+ import {
6542
+ fingerprintElementId,
6543
+ isNotMediaPayload,
6544
+ NotMediaPayloadError,
6545
+ probeMediaProfile,
6546
+ resolveProjectRelativeSrc as resolveProjectRelativeSrc2
6547
+ } from "@hyperframes/engine";
6203
6548
  var ASSET_MEDIA_TYPE_MISMATCH = "ASSET_MEDIA_TYPE_MISMATCH";
6204
6549
  var AssetMediaTypeMismatchError = class extends Error {
6205
6550
  code = ASSET_MEDIA_TYPE_MISMATCH;
@@ -6309,7 +6654,7 @@ function logRemoteDownloadTelemetry(event) {
6309
6654
  }
6310
6655
  var INFERRED_MEDIA_DURATION_ATTR = "data-hf-inferred-duration";
6311
6656
  function parseSubCompHtmlForValidity(html) {
6312
- return parseHTML4(html).document;
6657
+ return parseHTML5(html).document;
6313
6658
  }
6314
6659
  function endsWithSourceUrl(code) {
6315
6660
  const trimmed = code.trimEnd();
@@ -6317,7 +6662,7 @@ function endsWithSourceUrl(code) {
6317
6662
  return lastLine.startsWith("//# sourceURL=") || lastLine.startsWith("//@ sourceURL=");
6318
6663
  }
6319
6664
  function prepareCompositionScripts(html) {
6320
- const { document: document2 } = parseHTML4(html);
6665
+ const { document: document2 } = parseHTML5(html);
6321
6666
  for (const el of document2.querySelectorAll("script:not([src])")) {
6322
6667
  const isModule = (el.getAttribute("type") || "").trim().toLowerCase() === "module";
6323
6668
  const runsAsScript = isModule || isJavaScriptType(el);
@@ -6354,14 +6699,14 @@ Check that each file referenced by data-composition-src contains valid HTML with
6354
6699
  }
6355
6700
  };
6356
6701
  function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */ new Set()) {
6357
- const { document: document2 } = parseHTML4(html);
6702
+ const { document: document2 } = parseHTML5(html);
6358
6703
  const hosts = [...document2.querySelectorAll("[data-composition-src]")];
6359
6704
  const problems = [];
6360
6705
  for (const el of hosts) {
6361
6706
  const srcPath = el.getAttribute("data-composition-src");
6362
6707
  if (!srcPath) continue;
6363
6708
  if (isUnresolvedAssetPlaceholder(srcPath)) continue;
6364
- const filePath = resolve6(projectDir, srcPath);
6709
+ const filePath = resolve7(projectDir, srcPath);
6365
6710
  if (visited.has(filePath)) continue;
6366
6711
  const read = readProjectFile(filePath);
6367
6712
  if (read.kind === "missing") {
@@ -6417,7 +6762,7 @@ function runtimeRoot(document2) {
6417
6762
  }
6418
6763
  function detectRenderModeHints(html, pageMotionBlur) {
6419
6764
  const reasons = [];
6420
- const { document: document2 } = parseHTML4(html);
6765
+ const { document: document2 } = parseHTML5(html);
6421
6766
  if (document2.querySelector("canvas[layoutsubtree]")) {
6422
6767
  reasons.push({
6423
6768
  code: "htmlInCanvas",
@@ -6459,7 +6804,7 @@ function detectThreeDTransformUsage(html) {
6459
6804
  }
6460
6805
  var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
6461
6806
  function detectAncestorBackgroundImage(html) {
6462
- const { document: document2 } = parseHTML4(html);
6807
+ const { document: document2 } = parseHTML5(html);
6463
6808
  const root = document2.querySelector("[data-composition-id]");
6464
6809
  if (!root) return false;
6465
6810
  const ancestors = [];
@@ -6503,10 +6848,10 @@ function detectShaderTransitionUsage(html) {
6503
6848
  }
6504
6849
  return false;
6505
6850
  }
6506
- async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
6851
+ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
6507
6852
  let filePath = src;
6508
6853
  if (isHttpUrl(src)) {
6509
- if (!existsSync10(downloadDir)) mkdirSync6(downloadDir, { recursive: true });
6854
+ if (!existsSync10(downloadDir)) mkdirSync7(downloadDir, { recursive: true });
6510
6855
  try {
6511
6856
  filePath = await downloadToTemp(src, downloadDir, void 0, void 0, void 0, {
6512
6857
  onTelemetry: logRemoteDownloadTelemetry
@@ -6516,7 +6861,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
6516
6861
  }
6517
6862
  } else {
6518
6863
  filePath = decodedUrlPath(src);
6519
- if (!filePath.startsWith("/")) filePath = join14(baseDir, filePath);
6864
+ if (!filePath.startsWith("/")) filePath = join15(baseDir, filePath);
6520
6865
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
6521
6866
  }
6522
6867
  if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
@@ -6530,50 +6875,57 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
6530
6875
  if (error instanceof Error && error.stack) wrapped.stack = error.stack;
6531
6876
  return wrapped;
6532
6877
  };
6533
- return withMediaProbeSlot(async () => {
6534
- let profile;
6878
+ const probe = () => withMediaProbeSlot(async () => {
6535
6879
  try {
6536
6880
  await assertMediaPayload(filePath, elementIdentity);
6537
- profile = await probeMediaProfile2(filePath);
6538
- } catch (error) {
6539
- if (tagName !== "video") {
6540
- if (error instanceof NotMediaPayloadError2) {
6541
- log?.warn(
6542
- `[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
6543
- );
6544
- }
6545
- return { duration: null, resolvedPath: filePath };
6881
+ const profile = await probeMediaProfile2(filePath);
6882
+ if (tagName === "video") {
6883
+ return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
6546
6884
  }
6547
- throw withSrcContext(error);
6548
- }
6549
- assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);
6550
- let metadata;
6551
- if (tagName === "video") {
6552
6885
  try {
6553
- metadata = await extractMediaMetadata(filePath);
6554
- } catch (error) {
6555
- throw withSrcContext(error);
6556
- }
6557
- } else {
6558
- try {
6559
- metadata = await extractAudioMetadata(filePath);
6886
+ return { profile, fileDuration: (await extractAudioMetadata(filePath)).durationSeconds };
6560
6887
  } catch {
6561
- return { duration: null, resolvedPath: filePath };
6888
+ return { profile, fileDuration: null };
6562
6889
  }
6890
+ } catch (error) {
6891
+ return { error };
6563
6892
  }
6564
- const fileDuration = metadata.durationSeconds;
6565
- const duration = resolveNaturalMediaTimelineDurationFromValues(
6566
- fileDuration,
6567
- mediaStart,
6568
- playbackRate
6569
- );
6570
- return { duration, resolvedPath: filePath };
6571
6893
  });
6894
+ const key = `${tagName}\0${filePath}`;
6895
+ let probed = probes?.get(key);
6896
+ if (!probed) {
6897
+ probed = probe();
6898
+ probes?.set(key, probed);
6899
+ }
6900
+ const outcome = await probed;
6901
+ if ("error" in outcome) {
6902
+ if (tagName !== "video") {
6903
+ if (outcome.error instanceof NotMediaPayloadError2) {
6904
+ log?.warn(
6905
+ `[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
6906
+ );
6907
+ }
6908
+ return { duration: null, resolvedPath: filePath };
6909
+ }
6910
+ throw withSrcContext(outcome.error);
6911
+ }
6912
+ assertAssetMediaTypeProfile(
6913
+ tagName === "video" ? "video" : "audio",
6914
+ outcome.profile,
6915
+ elementIdentity
6916
+ );
6917
+ if (outcome.fileDuration === null) return { duration: null, resolvedPath: filePath };
6918
+ const duration = resolveNaturalMediaTimelineDurationFromValues(
6919
+ outcome.fileDuration,
6920
+ mediaStart,
6921
+ playbackRate
6922
+ );
6923
+ return { duration, resolvedPath: filePath };
6572
6924
  }
6573
6925
  function markInferredVariableMediaDurations(html, unresolvedMedia) {
6574
6926
  const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
6575
6927
  if (unresolvedIds.size === 0) return html;
6576
- const { document: document2 } = parseHTML4(html);
6928
+ const { document: document2 } = parseHTML5(html);
6577
6929
  let changed = false;
6578
6930
  for (const element of document2.querySelectorAll("video[data-var-src], audio[data-var-src]")) {
6579
6931
  if (!unresolvedIds.has(element.id)) continue;
@@ -6582,7 +6934,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
6582
6934
  }
6583
6935
  return changed ? document2.toString() : html;
6584
6936
  }
6585
- async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
6937
+ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath, probes) {
6586
6938
  const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
6587
6939
  const mediaUnresolved = unresolved.filter(
6588
6940
  (el) => (el.tagName === "video" || el.tagName === "audio") && el.src
@@ -6599,7 +6951,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6599
6951
  el.tagName,
6600
6952
  el.id,
6601
6953
  log,
6602
- urlByAbsentLocalPath
6954
+ urlByAbsentLocalPath,
6955
+ probes
6603
6956
  ).then(({ duration }) => ({ id: el.id, duration }))
6604
6957
  )
6605
6958
  );
@@ -6620,7 +6973,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6620
6973
  downloadDir,
6621
6974
  el.tagName,
6622
6975
  el.id,
6623
- log
6976
+ log,
6977
+ void 0,
6978
+ probes
6624
6979
  );
6625
6980
  return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
6626
6981
  })
@@ -6642,15 +6997,15 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6642
6997
  compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
6643
6998
  return { html: compiledHtml, unresolvedCompositions };
6644
6999
  }
6645
- async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
7000
+ async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath, probes) {
6646
7001
  const subCompositions = /* @__PURE__ */ new Map();
6647
- const { document: document2 } = parseHTML4(html);
7002
+ const { document: document2 } = parseHTML5(html);
6648
7003
  const compEls = document2.querySelectorAll("[data-composition-src]");
6649
7004
  const workItems = [];
6650
7005
  for (const el of compEls) {
6651
7006
  const srcPath = el.getAttribute("data-composition-src");
6652
7007
  if (!srcPath) continue;
6653
- const filePath = resolve6(projectDir, srcPath);
7008
+ const filePath = resolve7(projectDir, srcPath);
6654
7009
  if (visited.has(filePath)) {
6655
7010
  continue;
6656
7011
  }
@@ -6667,17 +7022,19 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
6667
7022
  workItems.map(async (item) => {
6668
7023
  const { html: compiledSub } = await compileHtmlFile(
6669
7024
  rewriteUnresolvableGsapToCdn(item.rawSubHtml, projectDir, item.srcPath),
6670
- dirname6(item.filePath),
7025
+ dirname7(item.filePath),
6671
7026
  downloadDir,
6672
7027
  void 0,
6673
- urlByAbsentLocalPath
7028
+ urlByAbsentLocalPath,
7029
+ probes
6674
7030
  );
6675
7031
  const nested = await parseSubCompositions(
6676
7032
  compiledSub,
6677
7033
  projectDir,
6678
7034
  downloadDir,
6679
7035
  item.nestedVisited,
6680
- urlByAbsentLocalPath
7036
+ urlByAbsentLocalPath,
7037
+ probes
6681
7038
  );
6682
7039
  return {
6683
7040
  srcPath: item.srcPath,
@@ -6695,7 +7052,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
6695
7052
  return { subCompositions };
6696
7053
  }
6697
7054
  function promoteCssImportsToLinkTags(html) {
6698
- const { document: document2 } = parseHTML4(html);
7055
+ const { document: document2 } = parseHTML5(html);
6699
7056
  const head = document2.querySelector("head");
6700
7057
  if (!head) return html;
6701
7058
  const importRe = /@import\s+url\(\s*['"]?([^'")\s]+)['"]?\s*\)\s*;?/gi;
@@ -6764,7 +7121,7 @@ var ProducerHostIdentityMap = class extends Map {
6764
7121
  }
6765
7122
  };
6766
7123
  function coalesceHeadStylesAndBodyScripts(html) {
6767
- const { document: document2 } = parseHTML4(html);
7124
+ const { document: document2 } = parseHTML5(html);
6768
7125
  const head = document2.querySelector("head");
6769
7126
  const body = document2.querySelector("body");
6770
7127
  if (!head && !body) return html;
@@ -6815,7 +7172,7 @@ function coalesceHeadStylesAndBodyScripts(html) {
6815
7172
  return document2.toString();
6816
7173
  }
6817
7174
  function inlineSubCompositions(html, subCompositions, projectDir, variableOverrides = {}) {
6818
- const { document: document2 } = parseHTML4(html);
7175
+ const { document: document2 } = parseHTML5(html);
6819
7176
  const head = document2.querySelector("head");
6820
7177
  const body = document2.querySelector("body");
6821
7178
  const hosts = Array.from(document2.querySelectorAll("[data-composition-src]"));
@@ -6844,17 +7201,17 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
6844
7201
  resolveHtml: (srcPath) => {
6845
7202
  let compHtml = subCompositions.get(srcPath) || null;
6846
7203
  if (!compHtml) {
6847
- const read = readProjectFile(resolve6(projectDir, srcPath));
7204
+ const read = readProjectFile(resolve7(projectDir, srcPath));
6848
7205
  if (read.kind === "file") {
6849
7206
  compHtml = rewriteUnresolvableGsapToCdn(read.text, projectDir, srcPath);
6850
7207
  }
6851
7208
  }
6852
7209
  return compHtml;
6853
7210
  },
6854
- parseHtml: (htmlStr) => parseHTML4(htmlStr).document,
7211
+ parseHtml: (htmlStr) => parseHTML5(htmlStr).document,
6855
7212
  // Mirrors the preview bundler: a sub-composition's SIBLING assets resolve
6856
7213
  // against its own directory, project-root refs stay as authored.
6857
- assetExists: (path) => existsSync10(resolve6(projectDir, path)),
7214
+ assetExists: (path) => existsSync10(resolve7(projectDir, path)),
6858
7215
  // Preserve the authored root wrapper as a child of the host, matching
6859
7216
  // the preview bundler's shape (htmlBundler.ts's prepareFlattenedInnerRoot,
6860
7217
  // which the runtime compositionLoader mirrors with its own copy for the
@@ -6955,7 +7312,7 @@ ${html}
6955
7312
  </html>`;
6956
7313
  }
6957
7314
  function injectTextRenderingRule(html) {
6958
- const { document: document2 } = parseHTML4(html);
7315
+ const { document: document2 } = parseHTML5(html);
6959
7316
  const head = document2.querySelector("head");
6960
7317
  if (!head) return html;
6961
7318
  if (document2.querySelector("style[data-hyperframes-text-rendering]")) {
@@ -6979,7 +7336,7 @@ function matchesScriptIntegrity(bytes, metadata) {
6979
7336
  (alg) => hashes.some((hash) => hash[1]?.toLowerCase() === alg)
6980
7337
  );
6981
7338
  if (!strongest) return true;
6982
- const actual = createHash5(strongest).update(bytes).digest();
7339
+ const actual = createHash6(strongest).update(bytes).digest();
6983
7340
  return hashes.some(
6984
7341
  (hash) => hash[1]?.toLowerCase() === strongest && actual.equals(Buffer.from(hash[2], "base64"))
6985
7342
  );
@@ -6987,7 +7344,7 @@ function matchesScriptIntegrity(bytes, metadata) {
6987
7344
  async function inlineExternalScripts(html) {
6988
7345
  const fullHtml = ensureFullDocument(html);
6989
7346
  const wrappedFragment = fullHtml !== html;
6990
- const { document: document2 } = parseHTML4(fullHtml);
7347
+ const { document: document2 } = parseHTML5(fullHtml);
6991
7348
  const scripts = document2.querySelectorAll("script[src]");
6992
7349
  const externalScripts = [];
6993
7350
  for (const el of scripts) {
@@ -7036,12 +7393,12 @@ ${safeText}
7036
7393
  return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
7037
7394
  }
7038
7395
  function collectExternalAssets(html, projectDir) {
7039
- const absProjectDir = resolve6(projectDir);
7396
+ const absProjectDir = resolve7(projectDir);
7040
7397
  const externalAssets = /* @__PURE__ */ new Map();
7041
7398
  function processPath(rawPath) {
7042
7399
  const trimmed = rawPath.trim();
7043
7400
  if (isNonRelativeUrl(trimmed)) return null;
7044
- const absPath = resolve6(absProjectDir, trimmed);
7401
+ const absPath = resolve7(absProjectDir, trimmed);
7045
7402
  if (isPathInside2(absPath, absProjectDir)) {
7046
7403
  return null;
7047
7404
  }
@@ -7050,7 +7407,7 @@ function collectExternalAssets(html, projectDir) {
7050
7407
  externalAssets.set(safeKey, absPath);
7051
7408
  return safeKey;
7052
7409
  }
7053
- const { document: document2 } = parseHTML4(html);
7410
+ const { document: document2 } = parseHTML5(html);
7054
7411
  for (const el of document2.querySelectorAll("[src], [href]")) {
7055
7412
  for (const attr of ["src", "href"]) {
7056
7413
  const val = (el.getAttribute(attr) || "").trim();
@@ -7095,7 +7452,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
7095
7452
  var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
7096
7453
  async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
7097
7454
  if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
7098
- if (!existsSync10(remoteDir)) mkdirSync6(remoteDir, { recursive: true });
7455
+ if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
7099
7456
  const urlToLocal = /* @__PURE__ */ new Map();
7100
7457
  await Promise.all(
7101
7458
  [...urlSet].map(async (url) => {
@@ -7142,7 +7499,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
7142
7499
  return downloadAndRewriteUrls(
7143
7500
  urlSet,
7144
7501
  html,
7145
- join14(downloadDir, REMOTE_MEDIA_SUBDIR),
7502
+ join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7146
7503
  "Remote media download failed",
7147
7504
  "Localized remote media source(s)"
7148
7505
  );
@@ -7157,7 +7514,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
7157
7514
  return downloadAndRewriteUrls(
7158
7515
  urlSet,
7159
7516
  html,
7160
- join14(downloadDir, REMOTE_MEDIA_SUBDIR),
7517
+ join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7161
7518
  "Remote image download failed",
7162
7519
  "Localized remote image source(s)"
7163
7520
  );
@@ -7173,7 +7530,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
7173
7530
  return downloadAndRewriteUrls(
7174
7531
  urlSet,
7175
7532
  html,
7176
- join14(downloadDir, REMOTE_MEDIA_SUBDIR),
7533
+ join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7177
7534
  "Remote background-image download failed",
7178
7535
  "Localized remote background-image(s)",
7179
7536
  // Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
@@ -7308,7 +7665,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
7308
7665
  return downloadAndRewriteUrls(
7309
7666
  urlSet,
7310
7667
  processed,
7311
- join14(downloadDir, REMOTE_MEDIA_SUBDIR),
7668
+ join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7312
7669
  "Remote font download failed",
7313
7670
  "Localized remote font face(s)",
7314
7671
  (h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
@@ -7353,7 +7710,7 @@ async function embedLocalFontFaces(html, projectDir) {
7353
7710
  while ((urlMatch = urlRe.exec(block)) !== null) {
7354
7711
  const localPath = urlMatch[1];
7355
7712
  if (!localPath || embeddedPaths.has(localPath)) continue;
7356
- const absPath = localPath.startsWith("/") ? localPath : resolve6(projectDir, localPath);
7713
+ const absPath = localPath.startsWith("/") ? localPath : resolve7(projectDir, localPath);
7357
7714
  if (!isPathInside2(absPath, projectDir)) continue;
7358
7715
  const ext = absPath.match(/\.(woff2?|ttf|otf|ttc)$/i)?.[1]?.toLowerCase() ?? "ttf";
7359
7716
  try {
@@ -7402,7 +7759,7 @@ function renderGsapDist() {
7402
7759
  }
7403
7760
  function rewriteUnresolvableGsapToCdn(html, projectDir, compSrcPath) {
7404
7761
  let gsapDist;
7405
- const exists = (path) => existsSync10(resolve6(projectDir, path));
7762
+ const exists = (path) => existsSync10(resolve7(projectDir, path));
7406
7763
  return html.replace(
7407
7764
  /(<script\b[^>]*\bsrc=["'])([^"']*gsap[^"']*\/dist\/([^"']+))(["'][^>]*>)/gi,
7408
7765
  (full, prefix, src, file, suffix) => {
@@ -7418,8 +7775,8 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
7418
7775
  if (!isPathInside2(htmlPath, projectDir)) return html;
7419
7776
  const entryPath = relative2(projectDir, htmlPath).replace(/\\/g, "/");
7420
7777
  if (!entryPath.includes("/")) return html;
7421
- const { document: document2 } = parseHTML4(html);
7422
- const assetExists = (path) => existsSync10(resolve6(projectDir, path));
7778
+ const { document: document2 } = parseHTML5(html);
7779
+ const assetExists = (path) => existsSync10(resolve7(projectDir, path));
7423
7780
  rewriteAssetPaths(
7424
7781
  document2.querySelectorAll("[src], [href]"),
7425
7782
  entryPath,
@@ -7441,25 +7798,28 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
7441
7798
  }
7442
7799
  async function compileForRender(projectDir, htmlPath, downloadDir, options = {}) {
7443
7800
  const entryHtml = rebaseDirectEntryAssetPaths(
7444
- readFileSync6(htmlPath, "utf-8"),
7801
+ readFileSync7(htmlPath, "utf-8"),
7445
7802
  projectDir,
7446
7803
  htmlPath
7447
7804
  );
7448
7805
  const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
7449
7806
  assertSubCompositionsUsable(rawHtml, projectDir);
7807
+ const probes = /* @__PURE__ */ new Map();
7450
7808
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
7451
7809
  rawHtml,
7452
7810
  projectDir,
7453
7811
  downloadDir,
7454
7812
  options.log,
7455
- options.urlByAbsentLocalPath
7813
+ options.urlByAbsentLocalPath,
7814
+ probes
7456
7815
  );
7457
7816
  const { subCompositions } = await parseSubCompositions(
7458
7817
  compiledHtml,
7459
7818
  projectDir,
7460
7819
  downloadDir,
7461
7820
  void 0,
7462
- options.urlByAbsentLocalPath
7821
+ options.urlByAbsentLocalPath,
7822
+ probes
7463
7823
  );
7464
7824
  const fullHtml = ensureFullDocument(compiledHtml);
7465
7825
  const inlinedHtml = inlineSubCompositions(
@@ -7511,12 +7871,26 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7511
7871
  const { html: htmlWithLocalImages, remoteMediaAssets: remoteImageAssets } = await localizeRemoteImageSources(htmlWithLocalMedia, downloadDir);
7512
7872
  const { html: htmlWithLocalBg, remoteMediaAssets: remoteBgAssets } = await localizeRemoteBackgroundImages(htmlWithLocalImages, downloadDir);
7513
7873
  const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeRemoteFontFaces(htmlWithLocalBg, downloadDir);
7874
+ const {
7875
+ html: htmlWithJpegs,
7876
+ preparedAssets: preparedHeifAssets,
7877
+ preparedImages: preparedHeifImages
7878
+ } = await prepareHeifImageInputs(htmlWithLocalizedFonts, {
7879
+ projectDir,
7880
+ downloadDir,
7881
+ cacheDir: options.animatedGifCacheDir,
7882
+ sourceAssets: remoteImageAssets,
7883
+ timeoutMs: options.ffmpegProcessTimeout
7884
+ });
7885
+ if (preparedHeifImages.length > 0) {
7886
+ defaultLogger.info(`[Compiler] Converted ${preparedHeifImages.length} HEIC image(s) to JPEG`);
7887
+ }
7514
7888
  const gifSourceAssets = new Map(remoteImageAssets);
7515
7889
  const {
7516
7890
  html: htmlWithPreparedGifs,
7517
7891
  preparedAssets: preparedGifAssets,
7518
7892
  preparedGifs
7519
- } = await prepareAnimatedGifInputs(htmlWithLocalizedFonts, {
7893
+ } = await prepareAnimatedGifInputs(htmlWithJpegs, {
7520
7894
  projectDir,
7521
7895
  downloadDir,
7522
7896
  cacheDir: options.animatedGifCacheDir,
@@ -7543,11 +7917,17 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7543
7917
  for (const [relPath, absPath] of preparedGifAssets) {
7544
7918
  externalAssets.set(relPath, absPath);
7545
7919
  }
7920
+ for (const [relPath, absPath] of preparedHeifAssets) {
7921
+ externalAssets.set(relPath, absPath);
7922
+ }
7546
7923
  const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
7547
7924
  const masterFxChain = parseMasterFxChain(html);
7925
+ const advised = /* @__PURE__ */ new Set();
7548
7926
  for (const video of videos) {
7549
7927
  if (isHttpUrl(video.src)) continue;
7550
- const videoPath = resolve6(projectDir, video.src);
7928
+ const videoPath = resolve7(projectDir, video.src);
7929
+ if (advised.has(videoPath)) continue;
7930
+ advised.add(videoPath);
7551
7931
  const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
7552
7932
  (async () => {
7553
7933
  const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
@@ -7567,7 +7947,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7567
7947
  })().catch(() => {
7568
7948
  });
7569
7949
  }
7570
- const { document: document2 } = parseHTML4(html);
7950
+ const { document: document2 } = parseHTML5(html);
7571
7951
  const rootEl = document2.querySelector("[data-composition-id]");
7572
7952
  const width = rootEl ? parseInt(rootEl.getAttribute("data-width") || "1080", 10) : 1080;
7573
7953
  const height = rootEl ? parseInt(rootEl.getAttribute("data-height") || "1920", 10) : 1920;
@@ -7974,7 +8354,7 @@ async function runCompileStage(input) {
7974
8354
  abortSignal
7975
8355
  } = input;
7976
8356
  const compileStart = Date.now();
7977
- const compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"), {
8357
+ const compiled = await compileForRender(projectDir, htmlPath, join16(workDir, "downloads"), {
7978
8358
  log,
7979
8359
  failClosedFontFetch: failClosedFontFetch === true,
7980
8360
  allowSystemFontCapture,
@@ -7982,7 +8362,7 @@ async function runCompileStage(input) {
7982
8362
  variables: input.variables,
7983
8363
  urlByAbsentLocalPath: input.urlByAbsentLocalPath,
7984
8364
  pageMotionBlur: input.pageMotionBlur,
7985
- animatedGifCacheDir: cfg.extractCacheDir ? join15(cfg.extractCacheDir, "animated-gif") : void 0,
8365
+ animatedGifCacheDir: cfg.extractCacheDir ? join16(cfg.extractCacheDir, "animated-gif") : void 0,
7986
8366
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
7987
8367
  });
7988
8368
  assertNotAborted();
@@ -8038,7 +8418,7 @@ async function runCompileStage(input) {
8038
8418
  };
8039
8419
  await preflightCompositionAssetMediaTypes({
8040
8420
  projectDir,
8041
- compiledDir: join15(workDir, "compiled"),
8421
+ compiledDir: join16(workDir, "compiled"),
8042
8422
  composition,
8043
8423
  signal: abortSignal
8044
8424
  });
@@ -8083,8 +8463,8 @@ async function runCompileStage(input) {
8083
8463
  }
8084
8464
 
8085
8465
  // src/services/render/stages/probeStage.ts
8086
- import { join as join16 } from "node:path";
8087
- import { parseHTML as parseHTML5 } from "linkedom";
8466
+ import { join as join17 } from "node:path";
8467
+ import { parseHTML as parseHTML6 } from "linkedom";
8088
8468
  import {
8089
8469
  closeCaptureSession as closeCaptureSession4,
8090
8470
  compositionRequiresWebGpu,
@@ -8098,13 +8478,14 @@ import {
8098
8478
  } from "@hyperframes/engine";
8099
8479
  import {
8100
8480
  MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
8101
- fpsToNumber as fpsToNumber7,
8481
+ fpsToNumber as fpsToNumber6,
8102
8482
  normalizeRateSpec as normalizeRateSpec3,
8103
8483
  readMediaStart as readMediaStart2,
8104
8484
  shiftRateLane as shiftRateLane2,
8105
8485
  sourceTimeAt as sourceTimeAt3,
8106
8486
  timeAtSourceTime
8107
8487
  } from "@hyperframes/core";
8488
+ import { parseHostVariableValues as parseHostVariableValues2, readDeclaredDefaults as readDeclaredDefaults2 } from "@hyperframes/core/compiler";
8108
8489
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
8109
8490
 
8110
8491
  // src/services/render/stages/probeFailures.ts
@@ -8131,7 +8512,7 @@ function durationToFrameCount(duration, fps) {
8131
8512
  }
8132
8513
  function hasScriptedAudioVolumeAutomation(html, audioCount) {
8133
8514
  if (audioCount <= 0) return false;
8134
- const { document: document2 } = parseHTML5(html);
8515
+ const { document: document2 } = parseHTML6(html);
8135
8516
  const scriptBodies = [...document2.querySelectorAll("script")].map((script) => script.textContent ?? "").join("\n");
8136
8517
  if (!scriptBodies) return false;
8137
8518
  return /\.\s*volume\s*=/i.test(scriptBodies) || /\b(?:gsap|tl|timeline|tween)\s*\.\s*(?:to|fromTo|set)\s*\([\s\S]{0,2000}\bvolume\s*:/i.test(
@@ -8139,23 +8520,30 @@ function hasScriptedAudioVolumeAutomation(html, audioCount) {
8139
8520
  );
8140
8521
  }
8141
8522
  function hasAutoStartVideos(html) {
8142
- const { document: document2 } = parseHTML5(html);
8523
+ const { document: document2 } = parseHTML6(html);
8143
8524
  return document2.querySelector("video[data-hf-auto-start]") !== null;
8144
8525
  }
8526
+ function variableHasValue(element, variableId, variables) {
8527
+ if (variables && Object.hasOwn(variables, variableId)) return true;
8528
+ for (let scope = element.parentElement; scope; scope = scope.parentElement) {
8529
+ if (Object.hasOwn(parseHostVariableValues2(scope), variableId)) return true;
8530
+ if (Object.hasOwn(readDeclaredDefaults2(scope), variableId)) return true;
8531
+ }
8532
+ return Object.hasOwn(readDeclaredDefaults2(element.ownerDocument.documentElement), variableId);
8533
+ }
8145
8534
  function hasVariableBoundMedia(html, variables) {
8146
- if (!variables || Object.keys(variables).length === 0) return false;
8147
- const { document: document2 } = parseHTML5(html);
8535
+ const { document: document2 } = parseHTML6(html);
8148
8536
  return Array.from(
8149
8537
  document2.querySelectorAll(
8150
8538
  "img[data-var-src], audio[data-var-src], video[data-var-src], source[data-var-src]"
8151
8539
  )
8152
8540
  ).some((element) => {
8153
8541
  const variableId = element.getAttribute("data-var-src")?.trim();
8154
- return Boolean(variableId && Object.hasOwn(variables, variableId));
8542
+ return Boolean(variableId && variableHasValue(element, variableId, variables));
8155
8543
  });
8156
8544
  }
8157
8545
  function authoredMediaStarts(html) {
8158
- const { document: document2 } = parseHTML5(html);
8546
+ const { document: document2 } = parseHTML6(html);
8159
8547
  const starts = /* @__PURE__ */ new Map();
8160
8548
  for (const media of document2.querySelectorAll("video, audio")) {
8161
8549
  const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
@@ -8192,7 +8580,7 @@ function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, dura
8192
8580
  return existingEnd <= 0 ? projectedEnd : Math.min(existingEnd, projectedEnd);
8193
8581
  }
8194
8582
  function hasRuntimeMediaChanges(html) {
8195
- const { document: document2 } = parseHTML5(html);
8583
+ const { document: document2 } = parseHTML6(html);
8196
8584
  const scriptBodies = [...document2.querySelectorAll("script")].map(
8197
8585
  (script) => script.textContent ?? ""
8198
8586
  );
@@ -8275,7 +8663,7 @@ async function runProbeStage(input) {
8275
8663
  });
8276
8664
  fileServer = await createFileServer({
8277
8665
  projectDir,
8278
- compiledDir: join16(workDir, "compiled"),
8666
+ compiledDir: join17(workDir, "compiled"),
8279
8667
  port: 0,
8280
8668
  preHeadScripts: [VIRTUAL_TIME_SHIM],
8281
8669
  fps: job.config.fps
@@ -8304,7 +8692,7 @@ async function runProbeStage(input) {
8304
8692
  log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
8305
8693
  probeSession = await createCaptureSession4(
8306
8694
  fileServer.url,
8307
- join16(workDir, "probe"),
8695
+ join17(workDir, "probe"),
8308
8696
  captureOpts,
8309
8697
  null,
8310
8698
  probeCfg
@@ -8389,7 +8777,7 @@ async function runProbeStage(input) {
8389
8777
  });
8390
8778
  probeSession = await createCaptureSession4(
8391
8779
  fileServer.url,
8392
- join16(workDir, "probe-screenshot"),
8780
+ join17(workDir, "probe-screenshot"),
8393
8781
  captureOpts,
8394
8782
  null,
8395
8783
  { ...probeCfg, forceScreenshot: true }
@@ -8425,7 +8813,7 @@ async function runProbeStage(input) {
8425
8813
  compiled,
8426
8814
  resolutions,
8427
8815
  projectDir,
8428
- join16(workDir, "downloads")
8816
+ join17(workDir, "downloads")
8429
8817
  );
8430
8818
  assertNotAborted();
8431
8819
  composition.videos = compiled.videos;
@@ -8577,7 +8965,7 @@ async function runProbeStage(input) {
8577
8965
  session.page,
8578
8966
  composition.audios.map((audio) => audio.id),
8579
8967
  composition.duration,
8580
- fpsToNumber7(job.config.fps)
8968
+ fpsToNumber6(job.config.fps)
8581
8969
  );
8582
8970
  assertNotAborted();
8583
8971
  if (automation.length > 0) {
@@ -8616,7 +9004,7 @@ async function runProbeStage(input) {
8616
9004
  }
8617
9005
  await preflightCompositionAssetMediaTypes({
8618
9006
  projectDir,
8619
- compiledDir: join16(workDir, "compiled"),
9007
+ compiledDir: join17(workDir, "compiled"),
8620
9008
  composition,
8621
9009
  signal: abortSignal
8622
9010
  });
@@ -8640,7 +9028,7 @@ async function runProbeStage(input) {
8640
9028
  }
8641
9029
  const browserProbeMs = Date.now() - probeStart;
8642
9030
  const duration = composition.duration;
8643
- const totalFrames = durationToFrameCount(duration, fpsToNumber7(job.config.fps));
9031
+ const totalFrames = durationToFrameCount(duration, fpsToNumber6(job.config.fps));
8644
9032
  if (duration <= 0) {
8645
9033
  const diagnostics = [];
8646
9034
  try {
@@ -8750,7 +9138,7 @@ function validateRenderDuration(input) {
8750
9138
 
8751
9139
  // src/services/render/stages/extractVideosStage.ts
8752
9140
  import { existsSync as existsSync11 } from "node:fs";
8753
- import { join as join17 } from "node:path";
9141
+ import { join as join18 } from "node:path";
8754
9142
  import {
8755
9143
  classifyVideoExtractionError,
8756
9144
  createFrameLookupTable,
@@ -9065,14 +9453,14 @@ async function runExtractVideosStage(input) {
9065
9453
  // because short boundary counts can differ by one frame.
9066
9454
  {
9067
9455
  fps: job.config.fps,
9068
- outputDir: join17(compiledDir, "__hyperframes_video_frames"),
9456
+ outputDir: join18(compiledDir, "__hyperframes_video_frames"),
9069
9457
  format: job.config.videoFrameFormat ?? "auto",
9070
9458
  toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
9071
9459
  timelineEnd: composition.duration,
9072
9460
  maxTransientRetries: extractionPolicy.maxTransientRetries,
9073
9461
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
9074
9462
  deferRangeExtraction,
9075
- contentKeyedDirs: [join17(compiledDir, REMOTE_MEDIA_SUBDIR)],
9463
+ contentKeyedDirs: [join18(compiledDir, REMOTE_MEDIA_SUBDIR)],
9076
9464
  urlByAbsentLocalPath
9077
9465
  },
9078
9466
  abortSignal,
@@ -9139,18 +9527,19 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
9139
9527
  }
9140
9528
 
9141
9529
  // src/services/render/stages/audioStage.ts
9142
- import { join as join19 } from "node:path";
9143
- import { rmSync as rmSync9 } from "node:fs";
9530
+ import { join as join20 } from "node:path";
9144
9531
  import {
9145
9532
  MIXED_AUDIO_FILENAME,
9146
9533
  audioFailureDiagnostics,
9147
- processCompositionAudio
9534
+ processCompositionAudio,
9535
+ removeScratch as removeScratch2,
9536
+ collectScratchLeftovers
9148
9537
  } from "@hyperframes/engine";
9149
9538
 
9150
9539
  // src/services/render/audioPadTrim.ts
9151
9540
  import { spawn } from "node:child_process";
9152
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
9153
- import { dirname as dirname7, join as join18 } from "node:path";
9541
+ import { mkdtempSync as mkdtempSync3 } from "node:fs";
9542
+ import { dirname as dirname8, join as join19 } from "node:path";
9154
9543
  import {
9155
9544
  DEFAULT_CONFIG,
9156
9545
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -9158,8 +9547,9 @@ import {
9158
9547
  extractAudioMetadata as extractAudioMetadata3,
9159
9548
  getFfprobeBinary,
9160
9549
  ManagedChildProcess,
9161
- renameWithRetry as renameWithRetry4,
9162
- runFfmpeg as runFfmpeg4,
9550
+ removeScratch,
9551
+ renameWithRetry as renameWithRetry5,
9552
+ runFfmpeg as runFfmpeg5,
9163
9553
  trackChildProcess
9164
9554
  } from "@hyperframes/engine";
9165
9555
  import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
@@ -9169,7 +9559,7 @@ import {
9169
9559
  aacSegmentCount,
9170
9560
  describeFfmpegFailure,
9171
9561
  extractAudioMetadata as extractAudioMetadata2,
9172
- runFfmpeg as runFfmpeg3
9562
+ runFfmpeg as runFfmpeg4
9173
9563
  } from "@hyperframes/engine";
9174
9564
  var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
9175
9565
  function checkedDbfs(value) {
@@ -9193,7 +9583,7 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
9193
9583
  else laterBlocks = Math.max(laterBlocks, block);
9194
9584
  }
9195
9585
  };
9196
- const result = await runFfmpeg3(
9586
+ const result = await runFfmpeg4(
9197
9587
  ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
9198
9588
  { signal, timeout: timeoutMs, onStderr: readLines }
9199
9589
  );
@@ -9353,7 +9743,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
9353
9743
  error: `audioPadTrim: failed to materialize ${plan.operation}: ${err instanceof Error ? err.message : String(err)}`
9354
9744
  };
9355
9745
  } finally {
9356
- for (const path of plan.cleanupPaths) rmSync8(path, { force: true });
9746
+ for (const path of plan.cleanupPaths) removeScratch(path);
9357
9747
  }
9358
9748
  let loweredDb = 0;
9359
9749
  if (probeTruePeak) {
@@ -9400,8 +9790,8 @@ async function constrainAacTruePeak(input) {
9400
9790
  async function enforceAacTruePeak(input) {
9401
9791
  let scratchDir;
9402
9792
  try {
9403
- scratchDir = mkdtempSync3(join18(dirname7(input.audioPath), ".true-peak-"));
9404
- const correctedPath = join18(scratchDir, "audio.m4a");
9793
+ scratchDir = mkdtempSync3(join19(dirname8(input.audioPath), ".true-peak-"));
9794
+ const correctedPath = join19(scratchDir, "audio.m4a");
9405
9795
  const measurements = [];
9406
9796
  let attenuationDb = 0;
9407
9797
  let sourcePeakDbfs;
@@ -9416,7 +9806,7 @@ async function enforceAacTruePeak(input) {
9416
9806
  }
9417
9807
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
9418
9808
  if (measuredPath !== correctedPath) return { success: true };
9419
- renameWithRetry4(correctedPath, input.audioPath);
9809
+ renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
9420
9810
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
9421
9811
  }
9422
9812
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -9457,7 +9847,7 @@ async function enforceAacTruePeak(input) {
9457
9847
  )}`
9458
9848
  };
9459
9849
  } finally {
9460
- if (scratchDir) rmSync8(scratchDir, { recursive: true, force: true });
9850
+ if (scratchDir) removeScratch(scratchDir);
9461
9851
  }
9462
9852
  }
9463
9853
  function failResult(outputPath, target, source, error) {
@@ -9534,7 +9924,7 @@ async function defaultProbeAudioInfo(audioPath, signal) {
9534
9924
  };
9535
9925
  }
9536
9926
  async function defaultRunFfmpeg(args, timeoutMs, signal) {
9537
- return toStepResult(await runFfmpeg4(args, { signal, timeout: timeoutMs }), timeoutMs);
9927
+ return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
9538
9928
  }
9539
9929
  function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
9540
9930
  return async (inputPath, outputPath, durationSeconds, filter) => {
@@ -9602,9 +9992,17 @@ function assertAudioNotInterrupted(failures) {
9602
9992
  ])
9603
9993
  });
9604
9994
  }
9605
- async function runAudioStage(input) {
9995
+ function runAudioStage(input) {
9996
+ return collectScratchLeftovers(
9997
+ () => runAudioStageScoped(input),
9998
+ (left) => {
9999
+ if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
10000
+ }
10001
+ );
10002
+ }
10003
+ async function runAudioStageScoped(input) {
9606
10004
  const stageStart = Date.now();
9607
- const pcmSourcePath = join19(input.workDir, "audio-source.wav");
10005
+ const pcmSourcePath = join20(input.workDir, "audio-source.wav");
9608
10006
  const duration = input.totalFrames * input.fps.den / input.fps.num;
9609
10007
  try {
9610
10008
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
@@ -9624,7 +10022,7 @@ async function runAudioStage(input) {
9624
10022
  }
9625
10023
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9626
10024
  } finally {
9627
- rmSync9(pcmSourcePath, { force: true });
10025
+ removeScratch2(pcmSourcePath);
9628
10026
  }
9629
10027
  }
9630
10028
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -9642,7 +10040,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9642
10040
  assertNotAborted,
9643
10041
  log
9644
10042
  } = input;
9645
- const audioOutputPath = join19(workDir, MIXED_AUDIO_FILENAME);
10043
+ const audioOutputPath = join20(workDir, MIXED_AUDIO_FILENAME);
9646
10044
  let hasAudio = false;
9647
10045
  let audioError;
9648
10046
  let audioFailures;
@@ -9653,7 +10051,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9653
10051
  audioResult = await processCompositionAudio(
9654
10052
  audios,
9655
10053
  projectDir,
9656
- join19(workDir, "audio-work"),
10054
+ join20(workDir, "audio-work"),
9657
10055
  audioOutputPath,
9658
10056
  duration,
9659
10057
  abortSignal,
@@ -9722,7 +10120,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9722
10120
 
9723
10121
  // src/services/render/stages/captureStage.ts
9724
10122
  import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
9725
- import { join as join20 } from "node:path";
10123
+ import { join as join21 } from "node:path";
9726
10124
  import {
9727
10125
  classifyCaptureFailure as classifyCaptureFailure2,
9728
10126
  frameFileExtension,
@@ -9795,13 +10193,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
9795
10193
  if (!existsSync12(workDir)) return total;
9796
10194
  for (const attempt of readdirSync3(workDir)) {
9797
10195
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
9798
- total += sampleDirectoryBytes(join20(workDir, attempt));
10196
+ total += sampleDirectoryBytes(join21(workDir, attempt));
9799
10197
  continue;
9800
10198
  }
9801
10199
  if (!/^capture-attempt-\d+$/.test(attempt)) continue;
9802
- const attemptDir = join20(workDir, attempt);
10200
+ const attemptDir = join21(workDir, attempt);
9803
10201
  for (const name of readdirSync3(attemptDir)) {
9804
- if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join20(attemptDir, name));
10202
+ if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join21(attemptDir, name));
9805
10203
  }
9806
10204
  }
9807
10205
  return total;
@@ -10094,8 +10492,8 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
10094
10492
  }
10095
10493
 
10096
10494
  // src/services/render/stages/captureHdrStage.ts
10097
- import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
10098
- import { join as join24 } from "node:path";
10495
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "node:fs";
10496
+ import { join as join25 } from "node:path";
10099
10497
  import {
10100
10498
  closeCaptureSession as closeCaptureSession7,
10101
10499
  cloneCaptureWarnings as cloneCaptureWarnings2,
@@ -10104,7 +10502,7 @@ import {
10104
10502
  initializeSession as initializeSession7,
10105
10503
  spawnStreamingEncoder as spawnStreamingEncoder3
10106
10504
  } from "@hyperframes/engine";
10107
- import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
10505
+ import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
10108
10506
 
10109
10507
  // src/services/hdrImageTransferCache.ts
10110
10508
  import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
@@ -10167,7 +10565,7 @@ function createHdrImageTransferCache(options = {}) {
10167
10565
  }
10168
10566
 
10169
10567
  // src/services/render/stages/captureHdrSequentialLoop.ts
10170
- import { join as join21 } from "node:path";
10568
+ import { join as join22 } from "node:path";
10171
10569
  import {
10172
10570
  TRANSITIONS,
10173
10571
  crossfade
@@ -10292,7 +10690,7 @@ async function runSequentialLayeredFrameLoop(input) {
10292
10690
  );
10293
10691
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
10294
10692
  writeFileExclusiveSync(
10295
- join21(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10693
+ join22(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10296
10694
  normalCanvas
10297
10695
  );
10298
10696
  }
@@ -10324,7 +10722,7 @@ async function runSequentialLayeredFrameLoop(input) {
10324
10722
  }
10325
10723
 
10326
10724
  // src/services/render/stages/captureHdrHybridLoop.ts
10327
- import { join as join23 } from "node:path";
10725
+ import { join as join24 } from "node:path";
10328
10726
  import {
10329
10727
  TRANSITIONS as TRANSITIONS2,
10330
10728
  closeCaptureSession as closeCaptureSession6,
@@ -10338,7 +10736,7 @@ import {
10338
10736
  // src/services/shaderTransitionWorkerPool.ts
10339
10737
  import { Worker } from "node:worker_threads";
10340
10738
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
10341
- import { dirname as dirname8, join as join22 } from "node:path";
10739
+ import { dirname as dirname9, join as join23 } from "node:path";
10342
10740
  import { createRequire } from "node:module";
10343
10741
  import { existsSync as existsSync13 } from "node:fs";
10344
10742
  import { cpus as cpus2 } from "node:os";
@@ -10351,10 +10749,10 @@ function resolveWorkerEntry(explicit) {
10351
10749
  const isTs = override.endsWith(".ts");
10352
10750
  return { path: override, isTs };
10353
10751
  }
10354
- const moduleDir = dirname8(fileURLToPath3(import.meta.url));
10355
- const jsPath = join22(moduleDir, "shaderTransitionWorker.js");
10752
+ const moduleDir = dirname9(fileURLToPath3(import.meta.url));
10753
+ const jsPath = join23(moduleDir, "shaderTransitionWorker.js");
10356
10754
  if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
10357
- const tsPath = join22(moduleDir, "shaderTransitionWorker.ts");
10755
+ const tsPath = join23(moduleDir, "shaderTransitionWorker.ts");
10358
10756
  return { path: tsPath, isTs: true };
10359
10757
  }
10360
10758
  function buildExecArgv(entryIsTs) {
@@ -10497,8 +10895,8 @@ async function createShaderTransitionWorkerPool(opts) {
10497
10895
  if (terminated) {
10498
10896
  throw new Error("shader-blend pool already terminated");
10499
10897
  }
10500
- return new Promise((resolve10, reject) => {
10501
- const task = traceEnabled ? { req, resolve: resolve10, reject, enqueuedAtMs: Date.now(), traceId: ++nextTaskId } : { req, resolve: resolve10, reject };
10898
+ return new Promise((resolve11, reject) => {
10899
+ const task = traceEnabled ? { req, resolve: resolve11, reject, enqueuedAtMs: Date.now(), traceId: ++nextTaskId } : { req, resolve: resolve11, reject };
10502
10900
  const idle = slots.find((s) => !s.busy && !s.dead);
10503
10901
  if (idle) {
10504
10902
  queue.unshift(task);
@@ -10722,7 +11120,7 @@ async function runHybridLayeredFrameLoop(input) {
10722
11120
  );
10723
11121
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
10724
11122
  writeFileExclusiveSync(
10725
- join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
11123
+ join24(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10726
11124
  canvas
10727
11125
  );
10728
11126
  }
@@ -10778,6 +11176,7 @@ async function runCaptureHdrStage(input) {
10778
11176
  preset,
10779
11177
  effectiveQuality,
10780
11178
  effectiveBitrate,
11179
+ deliveryEncoder,
10781
11180
  fileServer,
10782
11181
  buildCaptureOptions,
10783
11182
  createRenderVideoFrameInjector,
@@ -10839,7 +11238,7 @@ async function runCaptureHdrStage(input) {
10839
11238
  }
10840
11239
  return map;
10841
11240
  });
10842
- const fpsDecimal = fpsToNumber8(job.config.fps);
11241
+ const fpsDecimal = fpsToNumber7(job.config.fps);
10843
11242
  const transitionRanges = transitionMeta.map((t) => ({
10844
11243
  ...t,
10845
11244
  startFrame: Math.floor(t.time * fpsDecimal),
@@ -10867,6 +11266,7 @@ async function runCaptureHdrStage(input) {
10867
11266
  preset: preset.preset,
10868
11267
  quality: effectiveQuality,
10869
11268
  bitrate: effectiveBitrate,
11269
+ ...deliveryEncoder,
10870
11270
  pixelFormat: preset.pixelFormat,
10871
11271
  hdr: preset.hdr,
10872
11272
  rawInputFormat: "rgb48le"
@@ -10910,9 +11310,9 @@ async function runCaptureHdrStage(input) {
10910
11310
  if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
10911
11311
  }
10912
11312
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
10913
- const debugDumpDir = debugDumpEnabled ? join24(framesDir, "debug-composite") : null;
11313
+ const debugDumpDir = debugDumpEnabled ? join25(framesDir, "debug-composite") : null;
10914
11314
  if (debugDumpDir && !existsSync14(debugDumpDir)) {
10915
- mkdirSync7(debugDumpDir, { recursive: true });
11315
+ mkdirSync8(debugDumpDir, { recursive: true });
10916
11316
  }
10917
11317
  const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
10918
11318
  const hdrTargetTransfer = compositeTransfer === "srgb" ? void 0 : compositeTransfer;
@@ -10926,7 +11326,7 @@ async function runCaptureHdrStage(input) {
10926
11326
  beforeCaptureHook: domSession.onBeforeCapture,
10927
11327
  width,
10928
11328
  height,
10929
- fps: fpsToNumber8(job.config.fps),
11329
+ fps: fpsToNumber7(job.config.fps),
10930
11330
  compositeTransfer,
10931
11331
  nativeHdrImageIds,
10932
11332
  hdrImageBuffers,
@@ -11065,24 +11465,24 @@ async function runCaptureHdrStage(input) {
11065
11465
 
11066
11466
  // src/services/render/stages/encodeStage.ts
11067
11467
  import {
11068
- copyFileSync as copyFileSync3,
11468
+ copyFileSync as copyFileSync4,
11069
11469
  existsSync as existsSync15,
11070
- mkdirSync as mkdirSync8,
11470
+ mkdirSync as mkdirSync9,
11071
11471
  readdirSync as readdirSync4,
11072
- readFileSync as readFileSync7,
11073
- rmSync as rmSync10,
11074
- statSync as statSync6,
11075
- writeFileSync as writeFileSync4
11472
+ readFileSync as readFileSync8,
11473
+ rmSync as rmSync9,
11474
+ statSync as statSync7,
11475
+ writeFileSync as writeFileSync5
11076
11476
  } from "node:fs";
11077
11477
  import { readFile as readFile2 } from "node:fs/promises";
11078
- import { dirname as dirname9, join as join25 } from "node:path";
11478
+ import { dirname as dirname10, join as join26 } from "node:path";
11079
11479
  import {
11080
11480
  encodeFramesChunkedConcat,
11081
11481
  encodeFramesFromDir,
11082
11482
  formatFfmpegError,
11083
11483
  MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
11084
11484
  resolveConfig,
11085
- runFfmpeg as runFfmpeg5,
11485
+ runFfmpeg as runFfmpeg6,
11086
11486
  spawnStreamingEncoder as spawnStreamingEncoder4
11087
11487
  } from "@hyperframes/engine";
11088
11488
  import {
@@ -11158,17 +11558,17 @@ function gifEncodeFailure(startTime, outputPath, error, failureReason) {
11158
11558
  }
11159
11559
  async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
11160
11560
  const { outputPath } = argsInput;
11161
- const result = await runFfmpeg5(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
11561
+ const result = await runFfmpeg6(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
11162
11562
  if (!result.success) {
11163
11563
  const error = formatFfmpegError(result.exitCode, result.stderr);
11164
11564
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
11165
11565
  }
11166
- const gif = readFileSync7(outputPath);
11566
+ const gif = readFileSync8(outputPath);
11167
11567
  if (!clearGifFramesBeforeNext(gif)) {
11168
11568
  const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
11169
11569
  return gifEncodeFailure(startTime, outputPath, error);
11170
11570
  }
11171
- writeFileSync4(outputPath, gif);
11571
+ writeFileSync5(outputPath, gif);
11172
11572
  return null;
11173
11573
  }
11174
11574
  async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
@@ -11190,17 +11590,17 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
11190
11590
  try {
11191
11591
  const run = { signal: input.signal, timeout: input.timeout };
11192
11592
  for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
11193
- const result = await runFfmpeg5(args, run);
11593
+ const result = await runFfmpeg6(args, run);
11194
11594
  if (!result.success) {
11195
11595
  const error = formatFfmpegError(result.exitCode, result.stderr);
11196
11596
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
11197
11597
  }
11198
11598
  }
11199
- if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync7(outputPath)) !== false) {
11599
+ if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync8(outputPath)) !== false) {
11200
11600
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
11201
11601
  if (failure) return failure;
11202
11602
  }
11203
- const fileSize = existsSync15(outputPath) ? statSync6(outputPath).size : 0;
11603
+ const fileSize = existsSync15(outputPath) ? statSync7(outputPath).size : 0;
11204
11604
  return {
11205
11605
  success: true,
11206
11606
  outputPath,
@@ -11209,7 +11609,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
11209
11609
  fileSize
11210
11610
  };
11211
11611
  } finally {
11212
- rmSync10(input.palettePath, { force: true });
11612
+ rmSync9(input.palettePath, { force: true });
11213
11613
  }
11214
11614
  }
11215
11615
  function startEncodeProgress(job, stage, onProgress) {
@@ -11233,6 +11633,8 @@ function videoEncoderOptions(input, engineCfg) {
11233
11633
  preset: preset.preset,
11234
11634
  quality: input.effectiveQuality,
11235
11635
  bitrate: input.effectiveBitrate,
11636
+ maxBitrate: input.maxBitrate,
11637
+ bufferSize: input.bufferSize,
11236
11638
  pixelFormat: preset.pixelFormat,
11237
11639
  vp9CpuUsed: engineCfg.vp9CpuUsed,
11238
11640
  useGpu: job.config.useGpu,
@@ -11281,10 +11683,10 @@ function encodeWhileCapturing(input, totalFrames) {
11281
11683
  const stream = await encoder;
11282
11684
  for (let next = 0; next < totalFrames; next++) {
11283
11685
  while (!landed.has(next) && !captureDone && !signal.aborted) {
11284
- await new Promise((resolve10) => wake = resolve10);
11686
+ await new Promise((resolve11) => wake = resolve11);
11285
11687
  }
11286
11688
  if (signal.aborted) return;
11287
- const mergedPath = join25(input.framesDir, formatCaptureFrameName(next, "jpg"));
11689
+ const mergedPath = join26(input.framesDir, formatCaptureFrameName(next, "jpg"));
11288
11690
  const frame = await readLandedFrame(landed.get(next), mergedPath);
11289
11691
  landed.delete(next);
11290
11692
  if (!await stream.writeFrame(frame)) {
@@ -11359,7 +11761,7 @@ async function runEncodeStage(input) {
11359
11761
  const stage5Start = Date.now();
11360
11762
  if (isPngSequence) {
11361
11763
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
11362
- if (!existsSync15(outputPath)) mkdirSync8(outputPath, { recursive: true });
11764
+ if (!existsSync15(outputPath)) mkdirSync9(outputPath, { recursive: true });
11363
11765
  const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
11364
11766
  if (captured.length === 0) {
11365
11767
  throw new Error(
@@ -11367,11 +11769,11 @@ async function runEncodeStage(input) {
11367
11769
  );
11368
11770
  }
11369
11771
  captured.forEach((name, i) => {
11370
- const dst = join25(outputPath, formatExportFrameName(i, "png"));
11371
- copyFileSync3(join25(framesDir, name), dst);
11772
+ const dst = join26(outputPath, formatExportFrameName(i, "png"));
11773
+ copyFileSync4(join26(framesDir, name), dst);
11372
11774
  });
11373
11775
  if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
11374
- copyFileSync3(audioOutputPath, join25(outputPath, MIXED_AUDIO_FILENAME2));
11776
+ copyFileSync4(audioOutputPath, join26(outputPath, MIXED_AUDIO_FILENAME2));
11375
11777
  log.info(
11376
11778
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
11377
11779
  );
@@ -11391,7 +11793,7 @@ async function runEncodeStage(input) {
11391
11793
  const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
11392
11794
  fps: job.config.fps,
11393
11795
  loop,
11394
- palettePath: join25(dirname9(videoOnlyPath), "gif-palette.png"),
11796
+ palettePath: join26(dirname10(videoOnlyPath), "gif-palette.png"),
11395
11797
  preserveAlpha: needsAlpha,
11396
11798
  signal: abortSignal,
11397
11799
  timeout: engineCfg.ffmpegEncodeTimeout
@@ -11601,9 +12003,9 @@ function sampleDirectoryBytes(dir) {
11601
12003
  continue;
11602
12004
  }
11603
12005
  for (const name of entries) {
11604
- const full = join26(current, name);
12006
+ const full = join27(current, name);
11605
12007
  try {
11606
- const st = statSync7(full);
12008
+ const st = statSync8(full);
11607
12009
  if (st.isDirectory()) {
11608
12010
  stack.push(full);
11609
12011
  } else if (st.isFile()) {
@@ -11750,8 +12152,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11750
12152
  const ranges = [];
11751
12153
  let rangeStart = null;
11752
12154
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11753
- const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11754
- const missing = !existsSync16(framePath) || statSync7(framePath).size <= 8;
12155
+ const framePath = join27(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12156
+ const missing = !existsSync16(framePath) || statSync8(framePath).size <= 8;
11755
12157
  if (missing && rangeStart === null) {
11756
12158
  rangeStart = frameIndex;
11757
12159
  } else if (!missing && rangeStart !== null) {
@@ -11767,7 +12169,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11767
12169
  function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
11768
12170
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
11769
12171
  const batches = [];
11770
- const outputDirFor = (batchIndex, workerId) => join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
12172
+ const outputDirFor = (batchIndex, workerId) => join27(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
11771
12173
  if (stride > 1) {
11772
12174
  const spans = /* @__PURE__ */ new Map();
11773
12175
  for (const range of ranges) {
@@ -11827,8 +12229,8 @@ function getNextRetryWorkerCount(currentWorkers) {
11827
12229
  return Math.max(1, Math.floor(currentWorkers / 2));
11828
12230
  }
11829
12231
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
11830
- if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11831
- return join26(dirname10(outputPath), `work-${jobId}-`);
12232
+ if (platform2 === "win32" || platform2 === "darwin") return join27(systemTempDir, "hf-render-");
12233
+ return join27(dirname11(outputPath), `work-${jobId}-`);
11832
12234
  }
11833
12235
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
11834
12236
  function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
@@ -11870,7 +12272,7 @@ The composition is too large for the available memory. To reduce memory pressure
11870
12272
  function countCapturedFrames(totalFrames, framesDir, frameExt) {
11871
12273
  let captured = 0;
11872
12274
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11873
- const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12275
+ const framePath = join27(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11874
12276
  if (existsSync16(framePath)) captured++;
11875
12277
  }
11876
12278
  return captured;
@@ -11896,7 +12298,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11896
12298
  reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
11897
12299
  });
11898
12300
  pendingTransientRetry = false;
11899
- const attemptWorkDir = join26(options.workDir, `capture-attempt-${attempt}`);
12301
+ const attemptWorkDir = join27(options.workDir, `capture-attempt-${attempt}`);
11900
12302
  const batches = missingRanges ? buildMissingFrameRetryBatches(
11901
12303
  missingRanges,
11902
12304
  currentWorkers,
@@ -12502,18 +12904,19 @@ function deVerifyFallbackTelemetry(err) {
12502
12904
  };
12503
12905
  }
12504
12906
  function resolveRenderDebugDir() {
12505
- const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve7(process.env.PRODUCER_RENDERS_DIR, "..") : resolve7(dirname10(fileURLToPath4(import.meta.url)), "../..");
12506
- return join26(producerRoot, ".debug");
12907
+ const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(dirname11(fileURLToPath4(import.meta.url)), "../..");
12908
+ return join27(producerRoot, ".debug");
12507
12909
  }
12508
12910
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
12509
12911
  validateHlsRenderConfig(job.config);
12912
+ validateDeliveryEncoderConfig(job.config);
12510
12913
  const debugDir = resolveRenderDebugDir();
12511
- const outputDir = dirname10(outputPath);
12512
- if (!existsSync16(outputDir)) mkdirSync9(outputDir, { recursive: true });
12513
- const workDir = job.config.debug ? join26(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12914
+ const outputDir = dirname11(outputPath);
12915
+ if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
12916
+ const workDir = job.config.debug ? join27(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12514
12917
  const pipelineStart = Date.now();
12515
12918
  const baseLog = job.config.logger ?? defaultLogger;
12516
- const logPath = job.config.debug ? join26(workDir, "render.log") : null;
12919
+ const logPath = job.config.debug ? join27(workDir, "render.log") : null;
12517
12920
  const execution = new RenderExecutionContext({
12518
12921
  request: { renderJobId: job.id, projectDir, outputPath },
12519
12922
  logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
@@ -12527,7 +12930,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
12527
12930
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
12528
12931
  return;
12529
12932
  }
12530
- rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12933
+ rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12531
12934
  });
12532
12935
  try {
12533
12936
  await executeRenderPipeline({
@@ -12573,7 +12976,7 @@ async function executeRenderPipeline(input) {
12573
12976
  imageDecodeFailures: 0
12574
12977
  };
12575
12978
  let hdrPerf;
12576
- const perfOutputPath = join26(workDir, "perf-summary.json");
12979
+ const perfOutputPath = join27(workDir, "perf-summary.json");
12577
12980
  const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
12578
12981
  const observability = new RenderObservabilityRecorder({
12579
12982
  pipelineStartMs: pipelineStart,
@@ -12585,11 +12988,14 @@ async function executeRenderPipeline(input) {
12585
12988
  const isGif = outputFormat === "gif";
12586
12989
  const isHls = outputFormat === "hls";
12587
12990
  const hlsSegmentSeconds = isHls ? resolveHlsSegmentSeconds(job.config) : void 0;
12588
- const hlsEncoderGopLock = resolveHlsEncoderGopLock(
12589
- outputFormat,
12590
- job.config.fps,
12591
- job.config.hlsSegmentSeconds
12592
- );
12991
+ const deliveryEncoder = resolveDeliveryEncoderOptions({
12992
+ format: outputFormat,
12993
+ fps: job.config.fps,
12994
+ hlsSegmentSeconds: job.config.hlsSegmentSeconds,
12995
+ keyframeIntervalSeconds: job.config.keyframeIntervalSeconds,
12996
+ maxBitrate: job.config.maxBitrate,
12997
+ bufferSize: job.config.bufferSize
12998
+ });
12593
12999
  const artifactTransaction = new ArtifactTransaction(
12594
13000
  outputPath,
12595
13001
  isPngSequence || isHls ? "directory" : "file",
@@ -12646,7 +13052,7 @@ async function executeRenderPipeline(input) {
12646
13052
  job.startedAt = /* @__PURE__ */ new Date();
12647
13053
  assertNotAborted();
12648
13054
  assertConfiguredFfmpegBinariesExist();
12649
- if (!existsSync16(workDir)) mkdirSync9(workDir, { recursive: true });
13055
+ if (!existsSync16(workDir)) mkdirSync10(workDir, { recursive: true });
12650
13056
  if (job.config.debug) {
12651
13057
  log.info("[Render] Debug artifacts enabled", { workDir, logPath });
12652
13058
  }
@@ -12675,22 +13081,22 @@ async function executeRenderPipeline(input) {
12675
13081
  requestedWorkers: job.config.workers ?? "auto"
12676
13082
  });
12677
13083
  const entryFile = job.config.entryFile || "index.html";
12678
- let htmlPath = join26(projectDir, entryFile);
13084
+ let htmlPath = join27(projectDir, entryFile);
12679
13085
  if (!existsSync16(htmlPath)) {
12680
13086
  throw new Error(`Entry file not found: ${htmlPath}`);
12681
13087
  }
12682
13088
  assertNotAborted();
12683
- const rawEntry = readFileSync8(htmlPath, "utf-8");
13089
+ const rawEntry = readFileSync9(htmlPath, "utf-8");
12684
13090
  if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
12685
- const wrapperPath = join26(workDir, "standalone-entry.html");
12686
- const projectIndexPath = join26(projectDir, "index.html");
13091
+ const wrapperPath = join27(workDir, "standalone-entry.html");
13092
+ const projectIndexPath = join27(projectDir, "index.html");
12687
13093
  if (!existsSync16(projectIndexPath)) {
12688
13094
  throw new Error(
12689
13095
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
12690
13096
  );
12691
13097
  }
12692
13098
  const standaloneHtml = extractStandaloneEntryFromIndex(
12693
- readFileSync8(projectIndexPath, "utf-8"),
13099
+ readFileSync9(projectIndexPath, "utf-8"),
12694
13100
  entryFile,
12695
13101
  rawEntry
12696
13102
  );
@@ -12699,7 +13105,7 @@ async function executeRenderPipeline(input) {
12699
13105
  `Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
12700
13106
  );
12701
13107
  }
12702
- writeFileSync5(wrapperPath, standaloneHtml, "utf-8");
13108
+ writeFileSync6(wrapperPath, standaloneHtml, "utf-8");
12703
13109
  htmlPath = wrapperPath;
12704
13110
  log.info("Extracted standalone entry from index.html host context", {
12705
13111
  entryFile
@@ -12829,7 +13235,7 @@ async function executeRenderPipeline(input) {
12829
13235
  validateRenderDuration({
12830
13236
  duration: probeResult.duration,
12831
13237
  totalFrames,
12832
- fps: fpsToNumber9(job.config.fps)
13238
+ fps: fpsToNumber8(job.config.fps)
12833
13239
  });
12834
13240
  perfStages.browserProbeMs = probeResult.browserProbeMs;
12835
13241
  perfStages.compileMs = Date.now() - stage1Start;
@@ -12846,7 +13252,7 @@ async function executeRenderPipeline(input) {
12846
13252
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
12847
13253
  code: "extract_video"
12848
13254
  });
12849
- const compiledDir = join26(workDir, "compiled");
13255
+ const compiledDir = join27(workDir, "compiled");
12850
13256
  const extractResult = await observeRenderStage(
12851
13257
  observability,
12852
13258
  "video_extract",
@@ -12980,7 +13386,7 @@ async function executeRenderPipeline(input) {
12980
13386
  try {
12981
13387
  fileServer = await createFileServer({
12982
13388
  projectDir,
12983
- compiledDir: join26(workDir, "compiled"),
13389
+ compiledDir: join27(workDir, "compiled"),
12984
13390
  port: 0,
12985
13391
  preHeadScripts: [VIRTUAL_TIME_SHIM],
12986
13392
  fps: job.config.fps
@@ -12998,8 +13404,8 @@ async function executeRenderPipeline(input) {
12998
13404
  if (!activeFileServer) {
12999
13405
  throw new Error("File server failed to initialize before frame capture");
13000
13406
  }
13001
- const framesDir = join26(workDir, "captured-frames");
13002
- if (!existsSync16(framesDir)) mkdirSync9(framesDir, { recursive: true });
13407
+ const framesDir = join27(workDir, "captured-frames");
13408
+ if (!existsSync16(framesDir)) mkdirSync10(framesDir, { recursive: true });
13003
13409
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
13004
13410
  code: "start_browsers"
13005
13411
  });
@@ -13470,7 +13876,7 @@ async function executeRenderPipeline(input) {
13470
13876
  hls: ".mp4"
13471
13877
  };
13472
13878
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
13473
- const videoOnlyPath = join26(workDir, `video-only${videoExt}`);
13879
+ const videoOnlyPath = join27(workDir, `video-only${videoExt}`);
13474
13880
  const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
13475
13881
  if (usePageSideCompositingForTransitions) {
13476
13882
  activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
@@ -13672,6 +14078,7 @@ async function executeRenderPipeline(input) {
13672
14078
  preset,
13673
14079
  effectiveQuality,
13674
14080
  effectiveBitrate,
14081
+ deliveryEncoder,
13675
14082
  fileServer: activeFileServer,
13676
14083
  buildCaptureOptions,
13677
14084
  createRenderVideoFrameInjector,
@@ -13696,9 +14103,7 @@ async function executeRenderPipeline(input) {
13696
14103
  } else {
13697
14104
  let streamingHandled = false;
13698
14105
  if (capturePlan.kind === "sdr_segmented") {
13699
- const segmentedPlan = capturePlan;
13700
14106
  const captureFrameStart = Date.now();
13701
- resetCaptureAttemptProgress(job);
13702
14107
  const segmentFrames = resolveSegmentFrames(process.env);
13703
14108
  const segmentImageFormat = captureOptions.format || "jpeg";
13704
14109
  const segmentPlanHash = computeSegmentPlanHash({
@@ -13713,6 +14118,9 @@ async function executeRenderPipeline(input) {
13713
14118
  preset: preset.preset,
13714
14119
  quality: effectiveQuality,
13715
14120
  bitrate: effectiveBitrate,
14121
+ maxBitrate: deliveryEncoder.maxBitrate,
14122
+ bufferSize: deliveryEncoder.bufferSize,
14123
+ gopSize: deliveryEncoder.gopSize,
13716
14124
  pixelFormat: preset.pixelFormat,
13717
14125
  imageFormat: segmentImageFormat,
13718
14126
  useGpu: job.config.useGpu === true,
@@ -13723,7 +14131,7 @@ async function executeRenderPipeline(input) {
13723
14131
  pageMotionBlur: job.config.pageMotionBlur,
13724
14132
  captureQuality: captureOptions.quality
13725
14133
  });
13726
- const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
14134
+ const segmentDir = segmentDirFor(join27(projectDir, "renders"), segmentPlanHash);
13727
14135
  let completedSegments = /* @__PURE__ */ new Set();
13728
14136
  if (job.config.resumeSegments === true) {
13729
14137
  const existing = readSegmentManifest(segmentDir);
@@ -13738,71 +14146,129 @@ async function executeRenderPipeline(input) {
13738
14146
  });
13739
14147
  }
13740
14148
  if (completedSegments.size === 0) {
13741
- rmSync11(segmentDir, { recursive: true, force: true });
14149
+ rmSync10(segmentDir, { recursive: true, force: true });
13742
14150
  }
13743
14151
  } else {
13744
- rmSync11(segmentDir, { recursive: true, force: true });
14152
+ rmSync10(segmentDir, { recursive: true, force: true });
13745
14153
  }
13746
- const segmentManifest = readSegmentManifest(segmentDir) ?? {
14154
+ const emptySegmentManifest = () => ({
13747
14155
  version: 1,
13748
14156
  planHash: segmentPlanHash,
13749
14157
  totalFrames,
13750
14158
  segmentFrames,
13751
14159
  completed: []
14160
+ });
14161
+ let segmentManifest = readSegmentManifest(segmentDir) ?? emptySegmentManifest();
14162
+ const invokeSegmented = () => {
14163
+ if (capturePlan.kind !== "sdr_segmented") {
14164
+ throw new Error(`Cannot invoke segmented stage with ${capturePlan.kind} plan`);
14165
+ }
14166
+ const segmentedPlan = capturePlan;
14167
+ resetCaptureAttemptProgress(job);
14168
+ return observeRenderStage(
14169
+ observability,
14170
+ "capture_segmented",
14171
+ captureStageObservationData(),
14172
+ () => runCaptureSegmentedStage({
14173
+ fileServer: activeFileServer,
14174
+ workDir,
14175
+ framesDir,
14176
+ videoOnlyPath,
14177
+ job,
14178
+ totalFrames,
14179
+ cfg,
14180
+ plan: segmentedPlan,
14181
+ log,
14182
+ probeSession,
14183
+ outputFormat,
14184
+ streamingEncoderOptions: {
14185
+ fps: job.config.fps,
14186
+ width: captureCompositionWidth ?? width,
14187
+ height: captureCompositionHeight ?? height,
14188
+ codec: preset.codec,
14189
+ preset: preset.preset,
14190
+ quality: effectiveQuality,
14191
+ bitrate: effectiveBitrate,
14192
+ pixelFormat: preset.pixelFormat,
14193
+ vp9CpuUsed: cfg.vp9CpuUsed,
14194
+ useGpu: job.config.useGpu,
14195
+ imageFormat: segmentImageFormat,
14196
+ hdr: preset.hdr,
14197
+ // Each segment sets its own GOP (at most the interval); hls never reaches this route.
14198
+ ...deliveryEncoder
14199
+ },
14200
+ buildCaptureOptions,
14201
+ createRenderVideoFrameInjector,
14202
+ abortSignal: executionSignal,
14203
+ assertNotAborted,
14204
+ onProgress,
14205
+ dedupPerfs,
14206
+ segmentFrames,
14207
+ segmentDir,
14208
+ workerCount: segmentedPlan.workerCount,
14209
+ browserRecycleEverySegments: resolveSegmentBrowserRecycle(process.env),
14210
+ completedSegments,
14211
+ onSegmentComplete: (entry) => {
14212
+ segmentManifest.completed = [
14213
+ ...segmentManifest.completed.filter((e) => e.index !== entry.index),
14214
+ { ...entry, completedAt: (/* @__PURE__ */ new Date()).toISOString() }
14215
+ ];
14216
+ writeSegmentManifest(segmentDir, segmentManifest);
14217
+ },
14218
+ updateCaptureObservability
14219
+ })
14220
+ );
13752
14221
  };
13753
- const segmentedRes = await observeRenderStage(
13754
- observability,
13755
- "capture_segmented",
13756
- captureStageObservationData(),
13757
- () => runCaptureSegmentedStage({
13758
- fileServer: activeFileServer,
13759
- workDir,
13760
- framesDir,
13761
- videoOnlyPath,
13762
- job,
13763
- totalFrames,
13764
- cfg,
13765
- plan: segmentedPlan,
13766
- log,
13767
- probeSession,
13768
- outputFormat,
13769
- streamingEncoderOptions: {
13770
- fps: job.config.fps,
13771
- width: captureCompositionWidth ?? width,
13772
- height: captureCompositionHeight ?? height,
13773
- codec: preset.codec,
13774
- preset: preset.preset,
13775
- quality: effectiveQuality,
13776
- bitrate: effectiveBitrate,
13777
- pixelFormat: preset.pixelFormat,
13778
- vp9CpuUsed: cfg.vp9CpuUsed,
13779
- useGpu: job.config.useGpu,
13780
- imageFormat: segmentImageFormat,
13781
- hdr: preset.hdr
13782
- // No hlsEncoderGopLock here: each segment sets its own GOP to
13783
- // its own length, and hls never reaches this route.
13784
- },
13785
- buildCaptureOptions,
13786
- createRenderVideoFrameInjector,
13787
- abortSignal: executionSignal,
13788
- assertNotAborted,
13789
- onProgress,
13790
- dedupPerfs,
13791
- segmentFrames,
13792
- segmentDir,
13793
- workerCount: segmentedPlan.workerCount,
13794
- browserRecycleEverySegments: resolveSegmentBrowserRecycle(process.env),
13795
- completedSegments,
13796
- onSegmentComplete: (entry) => {
13797
- segmentManifest.completed = [
13798
- ...segmentManifest.completed.filter((e) => e.index !== entry.index),
13799
- { ...entry, completedAt: (/* @__PURE__ */ new Date()).toISOString() }
13800
- ];
13801
- writeSegmentManifest(segmentDir, segmentManifest);
13802
- },
13803
- updateCaptureObservability
13804
- })
13805
- );
14222
+ let segmentedRes;
14223
+ try {
14224
+ segmentedRes = await invokeSegmented();
14225
+ } catch (err) {
14226
+ const isVerifyError = isDrawElementVerificationError(err);
14227
+ const isDeStall = isDeRendererStallError(err);
14228
+ if (!isVerifyError && !isDrawElementCaptureError(err) && !isDeStall || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
14229
+ throw err;
14230
+ }
14231
+ deSelfVerifyFallback = isVerifyError;
14232
+ const t = deVerifyFallbackTelemetry(err);
14233
+ deFallbackReason = isVerifyError ? t.reason : isDeStall ? "de_renderer_stall" : "capture_error";
14234
+ deFallbackFailedDb = t.failedDb;
14235
+ deFallbackFrameIndex = t.frameIndex;
14236
+ deFallbackThresholdDb = t.thresholdDb;
14237
+ log.warn(
14238
+ "[Render] drawElement capture failed on the segmented path; re-rendering via screenshot",
14239
+ {
14240
+ error: err instanceof Error ? err.message : String(err)
14241
+ }
14242
+ );
14243
+ observability.checkpoint(
14244
+ "capture_segmented",
14245
+ "drawElement capture failed; retrying with a fresh screenshot session"
14246
+ );
14247
+ if (probeSession) {
14248
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
14249
+ const orphaned = probeSession;
14250
+ probeSession = null;
14251
+ await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
14252
+ }
14253
+ rmSync10(segmentDir, { recursive: true, force: true });
14254
+ completedSegments = /* @__PURE__ */ new Set();
14255
+ segmentManifest = emptySegmentManifest();
14256
+ dedupPerfs.length = 0;
14257
+ capturePlan = replanAfterFailure(capturePlan, {
14258
+ kind: isVerifyError ? "draw_element_verification" : "draw_element_capture"
14259
+ });
14260
+ syncCapturePlan();
14261
+ updateCaptureObservability({
14262
+ forceScreenshot: capturePlan.forceScreenshot,
14263
+ deSelfVerifyFallback,
14264
+ deFallbackReason,
14265
+ deFallbackFailedDb,
14266
+ deFallbackFrameIndex,
14267
+ deFallbackThresholdDb
14268
+ });
14269
+ segmentedRes = await invokeSegmented();
14270
+ observability.clearFailure("capture_segmented");
14271
+ }
13806
14272
  if (segmentedRes.success) {
13807
14273
  streamingHandled = true;
13808
14274
  workerCount = segmentedRes.workerCount;
@@ -13822,7 +14288,7 @@ async function executeRenderPipeline(input) {
13822
14288
  );
13823
14289
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
13824
14290
  if (job.config.keepSegments !== true) {
13825
- rmSync11(segmentDir, { recursive: true, force: true });
14291
+ rmSync10(segmentDir, { recursive: true, force: true });
13826
14292
  }
13827
14293
  } else {
13828
14294
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
@@ -13875,7 +14341,7 @@ async function executeRenderPipeline(input) {
13875
14341
  // assemble stage's `-c copy` segmentation cuts exactly on
13876
14342
  // the segment boundary. Every other format leaves the
13877
14343
  // encoder's open-GOP output untouched.
13878
- ...hlsEncoderGopLock
14344
+ ...deliveryEncoder
13879
14345
  },
13880
14346
  buildCaptureOptions,
13881
14347
  createRenderVideoFrameInjector,
@@ -14038,7 +14504,7 @@ async function executeRenderPipeline(input) {
14038
14504
  engineConfig: cfg,
14039
14505
  // Same value the streaming encoder above spreads — the disk and
14040
14506
  // chunked-concat encoders need an identical GOP lock for HLS.
14041
- ...hlsEncoderGopLock,
14507
+ ...deliveryEncoder,
14042
14508
  abortSignal: executionSignal,
14043
14509
  assertNotAborted,
14044
14510
  onProgress
@@ -14094,8 +14560,8 @@ async function executeRenderPipeline(input) {
14094
14560
  "capture_disk",
14095
14561
  "drawElement capture failed; retrying with a fresh screenshot session"
14096
14562
  );
14097
- rmSync11(framesDir, { recursive: true, force: true });
14098
- mkdirSync9(framesDir, { recursive: true });
14563
+ rmSync10(framesDir, { recursive: true, force: true });
14564
+ mkdirSync10(framesDir, { recursive: true });
14099
14565
  resetCaptureAttemptProgress(job);
14100
14566
  dedupPerfs.length = 0;
14101
14567
  cfg.useDrawElement = false;
@@ -14192,7 +14658,7 @@ async function executeRenderPipeline(input) {
14192
14658
  buildArtifactExpectation({
14193
14659
  outputFormat,
14194
14660
  durationSeconds: job.duration,
14195
- fps: fpsToNumber9(job.config.fps),
14661
+ fps: fpsToNumber8(job.config.fps),
14196
14662
  expectedFrames: captureTotalFrames,
14197
14663
  width: outputWidth,
14198
14664
  height: outputHeight
@@ -14271,7 +14737,7 @@ async function executeRenderPipeline(input) {
14271
14737
  job.perfSummary = perfSummary;
14272
14738
  if (job.config.debug) {
14273
14739
  try {
14274
- writeFileSync5(perfOutputPath, JSON.stringify(perfSummary, null, 2), "utf-8");
14740
+ writeFileSync6(perfOutputPath, JSON.stringify(perfSummary, null, 2), "utf-8");
14275
14741
  } catch (err) {
14276
14742
  log.debug("Failed to write perf summary", {
14277
14743
  perfOutputPath,
@@ -14281,8 +14747,8 @@ async function executeRenderPipeline(input) {
14281
14747
  }
14282
14748
  if (job.config.debug) {
14283
14749
  if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
14284
- const debugOutput = join26(workDir, `output${videoExt}`);
14285
- copyFileSync4(stagedOutputPath, debugOutput);
14750
+ const debugOutput = join27(workDir, `output${videoExt}`);
14751
+ copyFileSync5(stagedOutputPath, debugOutput);
14286
14752
  }
14287
14753
  }
14288
14754
  await commitArtifactTransaction(artifactTransaction, () => {
@@ -14387,14 +14853,14 @@ async function executeRenderPipeline(input) {
14387
14853
  // src/services/hyperframeLint.ts
14388
14854
  import {
14389
14855
  existsSync as existsSync17,
14390
- readFileSync as readFileSync9,
14391
- statSync as statSync8,
14392
- openSync as openSync4,
14856
+ readFileSync as readFileSync10,
14857
+ statSync as statSync9,
14858
+ openSync as openSync5,
14393
14859
  fstatSync as fstatSync3,
14394
- closeSync as closeSync4,
14860
+ closeSync as closeSync5,
14395
14861
  constants as constants3
14396
14862
  } from "node:fs";
14397
- import { resolve as resolve8, join as join27, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
14863
+ import { resolve as resolve9, join as join28, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
14398
14864
  import { lintHyperframeHtml } from "@hyperframes/lint";
14399
14865
  function isStringRecord(value) {
14400
14866
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -14424,30 +14890,30 @@ function pickEntryFile(files, preferredEntryFile) {
14424
14890
  function readEntryHtml(filePath) {
14425
14891
  let fd;
14426
14892
  try {
14427
- fd = openSync4(filePath, constants3.O_RDONLY | constants3.O_NONBLOCK);
14893
+ fd = openSync5(filePath, constants3.O_RDONLY | constants3.O_NONBLOCK);
14428
14894
  } catch (error) {
14429
14895
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
14430
14896
  return null;
14431
- if (!statSync8(filePath, { throwIfNoEntry: false })?.isFile()) return null;
14897
+ if (!statSync9(filePath, { throwIfNoEntry: false })?.isFile()) return null;
14432
14898
  throw error;
14433
14899
  }
14434
14900
  try {
14435
14901
  if (!fstatSync3(fd).isFile()) return null;
14436
- return readFileSync9(fd, "utf-8");
14902
+ return readFileSync10(fd, "utf-8");
14437
14903
  } finally {
14438
- closeSync4(fd);
14904
+ closeSync5(fd);
14439
14905
  }
14440
14906
  }
14441
14907
  function readProjectEntryFile(projectDir, preferredEntryFile) {
14442
- const absProjectDir = resolve8(projectDir);
14443
- if (!existsSync17(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
14908
+ const absProjectDir = resolve9(projectDir);
14909
+ if (!existsSync17(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
14444
14910
  return { error: `Project directory not found: ${absProjectDir}` };
14445
14911
  }
14446
14912
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
14447
14913
  (value) => typeof value === "string" && value.trim().length > 0
14448
14914
  );
14449
14915
  for (const entryFile of entryCandidates) {
14450
- const absoluteEntryPath = resolve8(absProjectDir, entryFile);
14916
+ const absoluteEntryPath = resolve9(absProjectDir, entryFile);
14451
14917
  const entryRelativePath = relative3(absProjectDir, absoluteEntryPath);
14452
14918
  if (entryRelativePath === ".." || entryRelativePath.startsWith(`..${sep2}`) || isAbsolute3(entryRelativePath)) {
14453
14919
  return { error: `Entry file must stay inside project directory: ${entryFile}` };
@@ -14462,7 +14928,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
14462
14928
  }
14463
14929
  }
14464
14930
  return {
14465
- error: `No HTML entry file found in project directory: ${join27(absProjectDir, preferredEntryFile || "index.html")}`
14931
+ error: `No HTML entry file found in project directory: ${join28(absProjectDir, preferredEntryFile || "index.html")}`
14466
14932
  };
14467
14933
  }
14468
14934
  function prepareHyperframeLintBody(body) {
@@ -14508,7 +14974,7 @@ async function runHyperframeLint(prepared) {
14508
14974
  // src/services/healthWorker.ts
14509
14975
  import { Worker as Worker2 } from "node:worker_threads";
14510
14976
  import { fileURLToPath as fileURLToPath5 } from "node:url";
14511
- import { dirname as dirname11, join as join28 } from "node:path";
14977
+ import { dirname as dirname12, join as join29 } from "node:path";
14512
14978
  import { existsSync as existsSync18 } from "node:fs";
14513
14979
  var DEFAULT_HEALTH_PORT = 9848;
14514
14980
  async function startHealthWorker(options = {}) {
@@ -14527,12 +14993,12 @@ async function startHealthWorker(options = {}) {
14527
14993
  stdout: false,
14528
14994
  stderr: false
14529
14995
  });
14530
- await new Promise((resolve10, reject) => {
14996
+ await new Promise((resolve11, reject) => {
14531
14997
  const onMessage = (msg) => {
14532
14998
  if (msg?.type === "listening") {
14533
14999
  worker.off("error", onError);
14534
15000
  worker.off("message", onMessage);
14535
- resolve10();
15001
+ resolve11();
14536
15002
  } else if (msg?.type === "listen-error") {
14537
15003
  worker.off("error", onError);
14538
15004
  worker.off("message", onMessage);
@@ -14583,8 +15049,8 @@ function defaultLogger2() {
14583
15049
  };
14584
15050
  }
14585
15051
  function resolveWorkerEntry2() {
14586
- const here = dirname11(fileURLToPath5(import.meta.url));
14587
- const candidates = [join28(here, "healthWorkerThread.js"), join28(here, "healthWorkerThread.ts")];
15052
+ const here = dirname12(fileURLToPath5(import.meta.url));
15053
+ const candidates = [join29(here, "healthWorkerThread.js"), join29(here, "healthWorkerThread.ts")];
14588
15054
  for (const candidate of candidates) {
14589
15055
  if (existsSync18(candidate)) return candidate;
14590
15056
  }
@@ -14620,17 +15086,18 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
14620
15086
 
14621
15087
  // src/services/distributed/plan.ts
14622
15088
  import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
14623
- import { extname as extname4, isAbsolute as isAbsolute4, join as join29, relative as relative4, sep as sep3 } from "node:path";
15089
+ import { extname as extname4, isAbsolute as isAbsolute4, join as join30, relative as relative4, sep as sep3 } from "node:path";
14624
15090
  import {
14625
- fpsToNumber as fpsToNumber10,
15091
+ fpsToNumber as fpsToNumber9,
14626
15092
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
14627
15093
  } from "@hyperframes/core";
14628
15094
  import {
14629
15095
  getEncoderPreset as getEncoderPreset4,
15096
+ keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
14630
15097
  normalizeVp9CpuUsed,
14631
15098
  resolveConfig as resolveConfig3,
14632
15099
  declareContentAddressedUrl,
14633
- renameWithRetry as renameWithRetry6,
15100
+ renameWithRetry as renameWithRetry7,
14634
15101
  declareProbeFactsUrl
14635
15102
  } from "@hyperframes/engine";
14636
15103
 
@@ -14701,10 +15168,10 @@ var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
14701
15168
  var FIELD_DELIMITER = Buffer.from([0]);
14702
15169
 
14703
15170
  // src/services/distributed/chunkAudio.ts
14704
- import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
15171
+ import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
14705
15172
 
14706
15173
  // src/services/distributed/mp4Stitch.ts
14707
- import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
15174
+ import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
14708
15175
  var SCAN_BYTES = 64 * 1024;
14709
15176
 
14710
15177
  // src/services/distributed/chunkAudio.ts
@@ -14724,9 +15191,9 @@ var PLAN_ROOT_CATEGORY = {
14724
15191
  var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
14725
15192
  var FREEZE_OWNED_PLAN_FILES = [
14726
15193
  "plan.json",
14727
- join29("meta", "composition.json"),
14728
- join29("meta", "encoder.json"),
14729
- join29("meta", "chunks.json")
15194
+ join30("meta", "composition.json"),
15195
+ join30("meta", "encoder.json"),
15196
+ join30("meta", "chunks.json")
14730
15197
  ];
14731
15198
 
14732
15199
  // src/services/distributed/renderConfigValidation.ts
@@ -14822,6 +15289,12 @@ function assertOptionalInteger(options, field, min = 0) {
14822
15289
  throw new Error(`Render request ${field} must be an integer >= ${min}`);
14823
15290
  }
14824
15291
  }
15292
+ function assertOptionalPositiveNumber(options, field) {
15293
+ const value = options[field];
15294
+ if (value !== void 0 && !(typeof value === "number" && value > 0)) {
15295
+ throw new Error(`Render request ${field} must be a positive number`);
15296
+ }
15297
+ }
14825
15298
  function assertOptionalString(options, field) {
14826
15299
  if (options[field] !== void 0 && typeof options[field] !== "string") {
14827
15300
  throw new Error(`Render request ${field} must be a string`);
@@ -14869,10 +15342,11 @@ function assertRequestOptionScalars(options) {
14869
15342
  assertOptionalInteger(options, "workers", 1);
14870
15343
  assertOptionalInteger(options, "maxRenderMs");
14871
15344
  assertOptionalInteger(options, "crf");
15345
+ assertOptionalPositiveNumber(options, "keyframeIntervalSeconds");
14872
15346
  for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
14873
15347
  assertOptionalBoolean(options, field);
14874
15348
  }
14875
- for (const field of ["entryFile", "videoBitrate"]) {
15349
+ for (const field of ["entryFile", "videoBitrate", "maxBitrate", "bufferSize"]) {
14876
15350
  assertOptionalString(options, field);
14877
15351
  }
14878
15352
  assertOptionalEnum(options, "strictness", ["strict", "best-effort"]);
@@ -14957,6 +15431,8 @@ var DEFAULT_SERVER_FPS = { num: 30, den: 1 };
14957
15431
  var SSE_KEEPALIVE_MS = 3e4;
14958
15432
  var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
14959
15433
  "ASSET_MEDIA_TYPE_MISMATCH",
15434
+ "HEIF_IMAGE_CONVERSION_FAILED",
15435
+ "HEIF_TOOL_UNAVAILABLE",
14960
15436
  "NOT_MEDIA_PAYLOAD",
14961
15437
  "INVALID_VIDEO_METADATA",
14962
15438
  "VIDEO_SOURCE_UNRENDERABLE",
@@ -15031,7 +15507,7 @@ function parseRenderOptions(body) {
15031
15507
  const format = parseServerFormat(body.format);
15032
15508
  const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
15033
15509
  const encoder = parseEncoderOptions(body, format ?? "mp4");
15034
- const { crf, videoBitrate, videoFrameFormat } = encoder.ok ? encoder.options : {};
15510
+ const { crf, videoBitrate, videoFrameFormat, keyframeIntervalSeconds, maxBitrate, bufferSize } = encoder.ok ? encoder.options : {};
15035
15511
  const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
15036
15512
  return {
15037
15513
  outputPath,
@@ -15041,6 +15517,9 @@ function parseRenderOptions(body) {
15041
15517
  maxRenderMs,
15042
15518
  crf,
15043
15519
  videoBitrate,
15520
+ keyframeIntervalSeconds,
15521
+ maxBitrate,
15522
+ bufferSize,
15044
15523
  useGpu,
15045
15524
  debug,
15046
15525
  strictness,
@@ -15075,6 +15554,9 @@ function buildRenderJobConfig(input, outputPath, log) {
15075
15554
  maxRenderMs: input.maxRenderMs,
15076
15555
  crf: input.crf,
15077
15556
  videoBitrate: input.videoBitrate,
15557
+ keyframeIntervalSeconds: input.keyframeIntervalSeconds,
15558
+ maxBitrate: input.maxBitrate,
15559
+ bufferSize: input.bufferSize,
15078
15560
  useGpu: input.useGpu,
15079
15561
  debug: input.debug,
15080
15562
  strictness: input.strictness,
@@ -15092,8 +15574,8 @@ function buildRenderJobConfig(input, outputPath, log) {
15092
15574
  function resolvePreparedRenderOutput(prepared, rendersDir, log) {
15093
15575
  const { input, cleanupProjectDir } = prepared;
15094
15576
  const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
15095
- const outputDir = dirname12(absoluteOutputPath);
15096
- if (!existsSync19(outputDir)) mkdirSync10(outputDir, { recursive: true });
15577
+ const outputDir = dirname13(absoluteOutputPath);
15578
+ if (!existsSync19(outputDir)) mkdirSync11(outputDir, { recursive: true });
15097
15579
  return { input, cleanupProjectDir, absoluteOutputPath };
15098
15580
  }
15099
15581
  function validateRenderOverrides(body) {
@@ -15133,12 +15615,12 @@ var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
15133
15615
  function prepareProjectDirectory(projectDir, options) {
15134
15616
  const candidate = nonEmptyString(projectDir);
15135
15617
  if (!candidate) return null;
15136
- const absProjectDir = resolve9(candidate);
15137
- if (!existsSync19(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
15618
+ const absProjectDir = resolve10(candidate);
15619
+ if (!existsSync19(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
15138
15620
  return { error: `Project directory not found: ${absProjectDir}` };
15139
15621
  }
15140
15622
  const entry = options.entryFile || "index.html";
15141
- if (!existsSync19(resolve9(absProjectDir, entry))) {
15623
+ if (!existsSync19(resolve10(absProjectDir, entry))) {
15142
15624
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
15143
15625
  }
15144
15626
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -15159,8 +15641,8 @@ async function resolveInlineRenderHtml(body) {
15159
15641
  }
15160
15642
  function materializeInlineProject(html, options) {
15161
15643
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
15162
- const tempProjectDir = mkdtempSync4(join30(tempRoot, "producer-project-"));
15163
- writeFileSync6(join30(tempProjectDir, "index.html"), html, "utf-8");
15644
+ const tempProjectDir = mkdtempSync4(join31(tempRoot, "producer-project-"));
15645
+ writeFileSync7(join31(tempProjectDir, "index.html"), html, "utf-8");
15164
15646
  return {
15165
15647
  prepared: {
15166
15648
  input: { projectDir: tempProjectDir, ...options },
@@ -15185,7 +15667,7 @@ function resolveOutputPath(projectDir, outputCandidate, rendersDir, log) {
15185
15667
  try {
15186
15668
  return resolveRenderPaths(projectDir, outputCandidate, rendersDir).absoluteOutputPath;
15187
15669
  } catch (error) {
15188
- const fallbackPath = resolve9(rendersDir, `producer-fallback-${Date.now()}.mp4`);
15670
+ const fallbackPath = resolve10(rendersDir, `producer-fallback-${Date.now()}.mp4`);
15189
15671
  log.warn("Failed to resolve output path, using fallback", {
15190
15672
  fallback: fallbackPath,
15191
15673
  error: error instanceof Error ? error.message : String(error)
@@ -15221,7 +15703,7 @@ function createArtifactStore(ttlMs) {
15221
15703
  function cleanupTempDir(dir, log) {
15222
15704
  if (!dir) return;
15223
15705
  try {
15224
- rmSync12(dir, { recursive: true, force: true });
15706
+ rmSync11(dir, { recursive: true, force: true });
15225
15707
  } catch (error) {
15226
15708
  log.warn("Failed to cleanup temp project dir", {
15227
15709
  cleanupProjectDir: dir,
@@ -15230,7 +15712,7 @@ function cleanupTempDir(dir, log) {
15230
15712
  }
15231
15713
  }
15232
15714
  function outputFileSize(path) {
15233
- return existsSync19(path) ? statSync9(path).size : 0;
15715
+ return existsSync19(path) ? statSync10(path).size : 0;
15234
15716
  }
15235
15717
  function createBlockingProgressReporter(log, requestId) {
15236
15718
  let lastLoggedPct = -10;
@@ -15536,7 +16018,7 @@ function createRenderHandlers(options = {}) {
15536
16018
  store.delete(token);
15537
16019
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
15538
16020
  }
15539
- const stats = statSync9(artifact.path);
16021
+ const stats = statSync10(artifact.path);
15540
16022
  return new Response(createReadStream3(artifact.path), {
15541
16023
  headers: {
15542
16024
  "content-type": "video/mp4",
@@ -15603,7 +16085,7 @@ function startServer(options = {}) {
15603
16085
  process.on("SIGINT", () => shutdown("SIGINT"));
15604
16086
  return server;
15605
16087
  }
15606
- var entryScript = process.argv[1] ? resolve9(process.argv[1]) : "";
16088
+ var entryScript = process.argv[1] ? resolve10(process.argv[1]) : "";
15607
16089
  var isPublicServerEntry = entryScript.endsWith("/public-server.js") || entryScript.endsWith("/src/server.ts");
15608
16090
  if (isPublicServerEntry) {
15609
16091
  const { values } = parseArgs({