@hyperframes/producer 0.8.210 → 0.8.212

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -64,6 +64,7 @@ import { fileURLToPath as fileURLToPath4 } from "url";
64
64
  // src/services/fileServer.ts
65
65
  import { Hono } from "hono";
66
66
  import { serve } from "@hono/node-server";
67
+ import { settleOnBrowserSafePort } from "@hyperframes/engine";
67
68
  import {
68
69
  existsSync as existsSync2,
69
70
  realpathSync,
@@ -778,18 +779,21 @@ function createFileServer(options) {
778
779
  return new Promise((resolve13, reject) => {
779
780
  const connections = /* @__PURE__ */ new Set();
780
781
  const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => {
781
- resolve13({
782
- url: `http://localhost:${info.port}`,
783
- port: info.port,
784
- addPreHeadScript: (script) => {
785
- preHeadScripts.push(script);
786
- },
787
- close: () => {
788
- for (const socket of connections) socket.destroy();
789
- connections.clear();
790
- server.close();
791
- }
792
- });
782
+ const bound = port === 0 ? settleOnBrowserSafePort(server, "127.0.0.1") : Promise.resolve(info.port);
783
+ bound.then((boundPort) => {
784
+ resolve13({
785
+ url: `http://localhost:${boundPort}`,
786
+ port: boundPort,
787
+ addPreHeadScript: (script) => {
788
+ preHeadScripts.push(script);
789
+ },
790
+ close: () => {
791
+ for (const socket of connections) socket.destroy();
792
+ connections.clear();
793
+ server.close();
794
+ }
795
+ });
796
+ }, reject);
793
797
  });
794
798
  server.on("connection", (socket) => {
795
799
  connections.add(socket);
@@ -926,6 +930,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
926
930
  var DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? // fileURLToPath (not URL.pathname): on Windows .pathname is "/D:/..." which
927
931
  // resolves to a bogus renders dir.
928
932
  nodeResolve(fileURLToPath2(import.meta.url), "../../..", "renders");
933
+ var LEADING_PARENT_SEGMENT_RE = /^\.\.(?:[\\/]|$)/;
929
934
  function isPathInside2(childPath, parentPath, options = {}) {
930
935
  const resolvePath = options.pathModule?.resolve ?? nodeResolve;
931
936
  const relativePath = options.pathModule?.relative ?? nodeRelative;
@@ -934,7 +939,7 @@ function isPathInside2(childPath, parentPath, options = {}) {
934
939
  const absParent = resolvePath(parentPath);
935
940
  if (absChild === absParent) return true;
936
941
  const rel = relativePath(absParent, absChild);
937
- return rel !== "" && !rel.startsWith("..") && !isPathAbsolute(rel);
942
+ return rel !== "" && !LEADING_PARENT_SEGMENT_RE.test(rel) && !isPathAbsolute(rel);
938
943
  }
939
944
  function toExternalAssetKey(absPath) {
940
945
  if (absPath.startsWith("hf-ext/")) return absPath;
@@ -6868,9 +6873,10 @@ function detectShaderTransitionUsage(html) {
6868
6873
  async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
6869
6874
  let filePath = src;
6870
6875
  if (isHttpUrl(src)) {
6871
- if (!existsSync10(downloadDir)) mkdirSync7(downloadDir, { recursive: true });
6876
+ const remoteDir = join15(downloadDir, REMOTE_MEDIA_SUBDIR);
6877
+ if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
6872
6878
  try {
6873
- filePath = await downloadToTemp(src, downloadDir, void 0, void 0, void 0, {
6879
+ filePath = await downloadToTemp(src, remoteDir, void 0, void 0, void 0, {
6874
6880
  onTelemetry: logRemoteDownloadTelemetry
6875
6881
  });
6876
6882
  } catch {
@@ -11703,7 +11709,7 @@ function videoEncoderOptions(input, engineCfg) {
11703
11709
  };
11704
11710
  }
11705
11711
  function canEncodeWhileCapturing(input, workerCount, cpuCount) {
11706
- return workerCount > 1 && workerCount < cpuCount && !input.isPngSequence && !input.isGif && !input.enableChunkedEncode && input.preset.codec === "h264" && !input.preset.hdr && input.job.config.useGpu !== true && input.captureImageFormat === "jpeg";
11712
+ return workerCount > 1 && workerCount < cpuCount && !input.isPngSequence && !input.isGif && !input.enableChunkedEncode && input.preset.codec === "h264" && !input.preset.hdr && input.captureImageFormat === "jpeg";
11707
11713
  }
11708
11714
  async function readLandedFrame(workerPath, mergedPath) {
11709
11715
  if (workerPath !== void 0) {
@@ -12822,6 +12828,11 @@ function resolveDeShortBand(args) {
12822
12828
  if (args.elementCountSource !== "live") return "unmeasured";
12823
12829
  return args.bandOpen ? "applied" : "skipped_elements";
12824
12830
  }
12831
+ function resolveDeSingleMinFrames(env) {
12832
+ const raw = env.HF_DE_SINGLE_MIN_FRAMES;
12833
+ const value = raw === void 0 || raw.trim() === "" ? 0 : Number(raw);
12834
+ return Number.isFinite(value) ? value : 0;
12835
+ }
12825
12836
  function shouldPreferSingleWorkerDrawElement(args) {
12826
12837
  return args.workerCount > 1 && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
12827
12838
  }
@@ -12939,12 +12950,25 @@ function shouldSegmentCapture(args) {
12939
12950
  if (raw === "true") return true;
12940
12951
  return args.durationSeconds >= resolveSegmentedMinSeconds(args.env);
12941
12952
  }
12953
+ function isAppleGpuDiskCapture(args) {
12954
+ return args.platform === "darwin" && args.browserGpuMode === "hardware" && !args.layered && !args.parallelStream;
12955
+ }
12942
12956
  function shouldStreamParallelCapture(args) {
12943
12957
  return args.routerEnabled && args.workerCount > 1 && !args.useDrawElement && outputUsesH264Pipeline(args.outputFormat) && args.streamingOk && !args.layeredOrEffectRoute;
12944
12958
  }
12945
12959
  function shouldClampDefaultDrawElement(args) {
12946
12960
  return args.useDrawElement && !args.fastCaptureExplicitOptIn && (!args.useStreamingEncode || args.workerCount > 1) && !args.deParallelStreamVerified;
12947
12961
  }
12962
+ function calibrationCaptureConfig(cfg, env = process.env) {
12963
+ const parallelKeepsDrawElement = !shouldClampDefaultDrawElement({
12964
+ useDrawElement: cfg.useDrawElement,
12965
+ fastCaptureExplicitOptIn: env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
12966
+ useStreamingEncode: true,
12967
+ workerCount: 2,
12968
+ deParallelStreamVerified: env.HF_DE_PARALLEL_STREAM === "true"
12969
+ });
12970
+ return parallelKeepsDrawElement ? cfg : { ...cfg, useDrawElement: false };
12971
+ }
12948
12972
  function resolveCaptureForceScreenshotForPageSideCompositing(args) {
12949
12973
  return args.usePageSideCompositing ? true : args.forceScreenshot;
12950
12974
  }
@@ -13028,6 +13052,7 @@ async function executeRenderPipeline(input) {
13028
13052
  let captureCompositionHeight;
13029
13053
  let captureTotalFrames;
13030
13054
  const perfStages = {};
13055
+ let pendingAudio;
13031
13056
  const hdrDiagnostics = {
13032
13057
  videoExtractionFailures: 0,
13033
13058
  imageDecodeFailures: 0
@@ -13389,7 +13414,7 @@ async function executeRenderPipeline(input) {
13389
13414
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress, {
13390
13415
  code: "process_audio"
13391
13416
  });
13392
- const audioResult = await observeRenderStage(
13417
+ const audioStage = observeRenderStage(
13393
13418
  observability,
13394
13419
  "audio_process",
13395
13420
  { audioCount: composition.audios.length },
@@ -13408,32 +13433,7 @@ async function executeRenderPipeline(input) {
13408
13433
  log
13409
13434
  })
13410
13435
  );
13411
- const { audioOutputPath, hasAudio } = audioResult;
13412
- perfStages.audioProcessMs = audioResult.audioProcessMs;
13413
- recordLimiterAttenuation(job, audioResult.audioLoweredDb);
13414
- if (audioResult.audioError) {
13415
- const audioFailures = audioResult.audioFailures ?? [];
13416
- const failureOwner = audioFailures.length === 0 ? void 0 : audioFailures.some((failure) => failure.owner === "system") ? "system" : "user";
13417
- const retryable = audioFailures.length === 0 ? void 0 : audioFailures.every((failure) => failure.retryable);
13418
- applyRenderWarningPolicy(
13419
- job,
13420
- [
13421
- {
13422
- code: "audio_processing_failed",
13423
- message: `Audio mix failed; output would be video-only: ${audioResult.audioError}`,
13424
- details: {
13425
- mediaType: "audio",
13426
- audioFailures: audioFailureDiagnostics2(audioFailures),
13427
- failureReasons: [...new Set(audioFailures.map((failure) => failure.reason))],
13428
- failureStages: [...new Set(audioFailures.map((failure) => failure.stage))],
13429
- failureOwner,
13430
- retryable
13431
- }
13432
- }
13433
- ],
13434
- log
13435
- );
13436
- }
13436
+ pendingAudio = audioStage.catch(() => void 0);
13437
13437
  const stage4Start = Date.now();
13438
13438
  updateJobStatus(job, "rendering", "Starting frame capture", 25, onProgress, {
13439
13439
  code: "start_browsers"
@@ -13537,9 +13537,7 @@ async function executeRenderPipeline(input) {
13537
13537
  const hasHdrContent = Boolean(effectiveHdr && nativeHdrIds.size > 0);
13538
13538
  if (hasHdrContent) assertOutputScaleSupported(deviceScaleFactor, "hdr_layered");
13539
13539
  routePageMotionBlur = pageMotionBlurForRoute({ hasHdrContent, useLayeredComposite: false });
13540
- const deSingleMinFramesRaw = process.env.HF_DE_SINGLE_MIN_FRAMES;
13541
- const deSingleMinFramesNum = deSingleMinFramesRaw === void 0 || deSingleMinFramesRaw.trim() === "" ? 900 : Number(deSingleMinFramesRaw);
13542
- const deSingleMinFrames = Number.isFinite(deSingleMinFramesNum) ? deSingleMinFramesNum : 900;
13540
+ const deSingleMinFrames = resolveDeSingleMinFrames(process.env);
13543
13541
  const deShortBandMinFrames = envInt("HF_DE_SHORT_MIN_FRAMES", 250);
13544
13542
  const deShortBandMaxElements = envInt("HF_DE_SHORT_MAX_ELEMENTS", 2500);
13545
13543
  const {
@@ -13708,7 +13706,7 @@ async function executeRenderPipeline(input) {
13708
13706
  stagePhase: "calibrating"
13709
13707
  }),
13710
13708
  () => runCaptureCalibration({
13711
- cfg,
13709
+ cfg: calibrationCaptureConfig(cfg),
13712
13710
  fileServer: activeFileServer,
13713
13711
  workDir,
13714
13712
  log,
@@ -13744,10 +13742,28 @@ async function executeRenderPipeline(input) {
13744
13742
  deParallelRouterEligible
13745
13743
  });
13746
13744
  }
13745
+ const appleGpuDiskCapture = isAppleGpuDiskCapture({
13746
+ platform: process.platform,
13747
+ browserGpuMode: resolvedBrowserGpuMode,
13748
+ layered: shouldUseLayeredComposite({
13749
+ hasHdrContent,
13750
+ hasShaderTransitions: compiled.hasShaderTransitions && !isGif,
13751
+ isPngSequence
13752
+ }),
13753
+ parallelStream: isCaptureParallelStreamRouterEnabled(
13754
+ process.env,
13755
+ resolveParallelCaptureMode({
13756
+ platform: process.platform,
13757
+ headlessShellPath: resolveHeadlessShellPath(cfg),
13758
+ forceScreenshot: captureForceScreenshot,
13759
+ deviceScaleFactor: captureOptions.deviceScaleFactor
13760
+ })
13761
+ )
13762
+ });
13747
13763
  workerCount = resolveRenderWorkerCount(
13748
13764
  totalFrames,
13749
13765
  job.config.workers,
13750
- cfg,
13766
+ { ...cfg, appleGpuDiskCapture },
13751
13767
  compiled,
13752
13768
  log,
13753
13769
  captureCalibration?.estimate,
@@ -14107,6 +14123,33 @@ async function executeRenderPipeline(input) {
14107
14123
  }
14108
14124
  const effectiveQuality = job.config.crf ?? preset.quality;
14109
14125
  const effectiveBitrate = job.config.crf != null ? void 0 : job.config.videoBitrate;
14126
+ const audioResult = await audioStage;
14127
+ const { audioOutputPath, hasAudio } = audioResult;
14128
+ perfStages.audioProcessMs = audioResult.audioProcessMs;
14129
+ recordLimiterAttenuation(job, audioResult.audioLoweredDb);
14130
+ if (audioResult.audioError) {
14131
+ const audioFailures = audioResult.audioFailures ?? [];
14132
+ const failureOwner = audioFailures.length === 0 ? void 0 : audioFailures.some((failure) => failure.owner === "system") ? "system" : "user";
14133
+ const retryable = audioFailures.length === 0 ? void 0 : audioFailures.every((failure) => failure.retryable);
14134
+ applyRenderWarningPolicy(
14135
+ job,
14136
+ [
14137
+ {
14138
+ code: "audio_processing_failed",
14139
+ message: `Audio mix failed; output would be video-only: ${audioResult.audioError}`,
14140
+ details: {
14141
+ mediaType: "audio",
14142
+ audioFailures: audioFailureDiagnostics2(audioFailures),
14143
+ failureReasons: [...new Set(audioFailures.map((failure) => failure.reason))],
14144
+ failureStages: [...new Set(audioFailures.map((failure) => failure.stage))],
14145
+ failureOwner,
14146
+ retryable
14147
+ }
14148
+ }
14149
+ ],
14150
+ log
14151
+ );
14152
+ }
14110
14153
  resetCaptureAttemptProgress(job);
14111
14154
  if (capturePlan.kind === "hdr_layered") {
14112
14155
  const layeredPlan = capturePlan;
@@ -14821,6 +14864,7 @@ async function executeRenderPipeline(input) {
14821
14864
  updateJobStatus(job, "complete", "Render complete", 100, onProgress);
14822
14865
  await eventPublisher.flush();
14823
14866
  } catch (caught) {
14867
+ await pendingAudio;
14824
14868
  const error = tooSlowError ?? caught;
14825
14869
  if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
14826
14870
  job.error = error instanceof Error ? error.message : "render_cancelled";
@@ -85,6 +85,7 @@ import { fileURLToPath as fileURLToPath4 } from "url";
85
85
  // src/services/fileServer.ts
86
86
  import { Hono } from "hono";
87
87
  import { serve } from "@hono/node-server";
88
+ import { settleOnBrowserSafePort } from "@hyperframes/engine";
88
89
  import {
89
90
  existsSync as existsSync2,
90
91
  realpathSync,
@@ -799,18 +800,21 @@ function createFileServer(options) {
799
800
  return new Promise((resolve11, reject) => {
800
801
  const connections = /* @__PURE__ */ new Set();
801
802
  const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => {
802
- resolve11({
803
- url: `http://localhost:${info.port}`,
804
- port: info.port,
805
- addPreHeadScript: (script) => {
806
- preHeadScripts.push(script);
807
- },
808
- close: () => {
809
- for (const socket of connections) socket.destroy();
810
- connections.clear();
811
- server.close();
812
- }
813
- });
803
+ const bound = port === 0 ? settleOnBrowserSafePort(server, "127.0.0.1") : Promise.resolve(info.port);
804
+ bound.then((boundPort) => {
805
+ resolve11({
806
+ url: `http://localhost:${boundPort}`,
807
+ port: boundPort,
808
+ addPreHeadScript: (script) => {
809
+ preHeadScripts.push(script);
810
+ },
811
+ close: () => {
812
+ for (const socket of connections) socket.destroy();
813
+ connections.clear();
814
+ server.close();
815
+ }
816
+ });
817
+ }, reject);
814
818
  });
815
819
  server.on("connection", (socket) => {
816
820
  connections.add(socket);
@@ -944,6 +948,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
944
948
  var DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? // fileURLToPath (not URL.pathname): on Windows .pathname is "/D:/..." which
945
949
  // resolves to a bogus renders dir.
946
950
  nodeResolve(fileURLToPath2(import.meta.url), "../../..", "renders");
951
+ var LEADING_PARENT_SEGMENT_RE = /^\.\.(?:[\\/]|$)/;
947
952
  function isPathInside2(childPath, parentPath, options = {}) {
948
953
  const resolvePath = options.pathModule?.resolve ?? nodeResolve;
949
954
  const relativePath = options.pathModule?.relative ?? nodeRelative;
@@ -952,7 +957,7 @@ function isPathInside2(childPath, parentPath, options = {}) {
952
957
  const absParent = resolvePath(parentPath);
953
958
  if (absChild === absParent) return true;
954
959
  const rel = relativePath(absParent, absChild);
955
- return rel !== "" && !rel.startsWith("..") && !isPathAbsolute(rel);
960
+ return rel !== "" && !LEADING_PARENT_SEGMENT_RE.test(rel) && !isPathAbsolute(rel);
956
961
  }
957
962
  function toExternalAssetKey(absPath) {
958
963
  if (absPath.startsWith("hf-ext/")) return absPath;
@@ -6885,9 +6890,10 @@ function detectShaderTransitionUsage(html) {
6885
6890
  async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
6886
6891
  let filePath = src;
6887
6892
  if (isHttpUrl(src)) {
6888
- if (!existsSync10(downloadDir)) mkdirSync7(downloadDir, { recursive: true });
6893
+ const remoteDir = join15(downloadDir, REMOTE_MEDIA_SUBDIR);
6894
+ if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
6889
6895
  try {
6890
- filePath = await downloadToTemp(src, downloadDir, void 0, void 0, void 0, {
6896
+ filePath = await downloadToTemp(src, remoteDir, void 0, void 0, void 0, {
6891
6897
  onTelemetry: logRemoteDownloadTelemetry
6892
6898
  });
6893
6899
  } catch {
@@ -11684,7 +11690,7 @@ function videoEncoderOptions(input, engineCfg) {
11684
11690
  };
11685
11691
  }
11686
11692
  function canEncodeWhileCapturing(input, workerCount, cpuCount) {
11687
- return workerCount > 1 && workerCount < cpuCount && !input.isPngSequence && !input.isGif && !input.enableChunkedEncode && input.preset.codec === "h264" && !input.preset.hdr && input.job.config.useGpu !== true && input.captureImageFormat === "jpeg";
11693
+ return workerCount > 1 && workerCount < cpuCount && !input.isPngSequence && !input.isGif && !input.enableChunkedEncode && input.preset.codec === "h264" && !input.preset.hdr && input.captureImageFormat === "jpeg";
11688
11694
  }
11689
11695
  async function readLandedFrame(workerPath, mergedPath) {
11690
11696
  if (workerPath !== void 0) {
@@ -12803,6 +12809,11 @@ function resolveDeShortBand(args) {
12803
12809
  if (args.elementCountSource !== "live") return "unmeasured";
12804
12810
  return args.bandOpen ? "applied" : "skipped_elements";
12805
12811
  }
12812
+ function resolveDeSingleMinFrames(env) {
12813
+ const raw = env.HF_DE_SINGLE_MIN_FRAMES;
12814
+ const value = raw === void 0 || raw.trim() === "" ? 0 : Number(raw);
12815
+ return Number.isFinite(value) ? value : 0;
12816
+ }
12806
12817
  function shouldPreferSingleWorkerDrawElement(args) {
12807
12818
  return args.workerCount > 1 && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
12808
12819
  }
@@ -12920,12 +12931,25 @@ function shouldSegmentCapture(args) {
12920
12931
  if (raw === "true") return true;
12921
12932
  return args.durationSeconds >= resolveSegmentedMinSeconds(args.env);
12922
12933
  }
12934
+ function isAppleGpuDiskCapture(args) {
12935
+ return args.platform === "darwin" && args.browserGpuMode === "hardware" && !args.layered && !args.parallelStream;
12936
+ }
12923
12937
  function shouldStreamParallelCapture(args) {
12924
12938
  return args.routerEnabled && args.workerCount > 1 && !args.useDrawElement && outputUsesH264Pipeline(args.outputFormat) && args.streamingOk && !args.layeredOrEffectRoute;
12925
12939
  }
12926
12940
  function shouldClampDefaultDrawElement(args) {
12927
12941
  return args.useDrawElement && !args.fastCaptureExplicitOptIn && (!args.useStreamingEncode || args.workerCount > 1) && !args.deParallelStreamVerified;
12928
12942
  }
12943
+ function calibrationCaptureConfig(cfg, env = process.env) {
12944
+ const parallelKeepsDrawElement = !shouldClampDefaultDrawElement({
12945
+ useDrawElement: cfg.useDrawElement,
12946
+ fastCaptureExplicitOptIn: env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
12947
+ useStreamingEncode: true,
12948
+ workerCount: 2,
12949
+ deParallelStreamVerified: env.HF_DE_PARALLEL_STREAM === "true"
12950
+ });
12951
+ return parallelKeepsDrawElement ? cfg : { ...cfg, useDrawElement: false };
12952
+ }
12929
12953
  function resolveCaptureForceScreenshotForPageSideCompositing(args) {
12930
12954
  return args.usePageSideCompositing ? true : args.forceScreenshot;
12931
12955
  }
@@ -13009,6 +13033,7 @@ async function executeRenderPipeline(input) {
13009
13033
  let captureCompositionHeight;
13010
13034
  let captureTotalFrames;
13011
13035
  const perfStages = {};
13036
+ let pendingAudio;
13012
13037
  const hdrDiagnostics = {
13013
13038
  videoExtractionFailures: 0,
13014
13039
  imageDecodeFailures: 0
@@ -13370,7 +13395,7 @@ async function executeRenderPipeline(input) {
13370
13395
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress, {
13371
13396
  code: "process_audio"
13372
13397
  });
13373
- const audioResult = await observeRenderStage(
13398
+ const audioStage = observeRenderStage(
13374
13399
  observability,
13375
13400
  "audio_process",
13376
13401
  { audioCount: composition.audios.length },
@@ -13389,32 +13414,7 @@ async function executeRenderPipeline(input) {
13389
13414
  log
13390
13415
  })
13391
13416
  );
13392
- const { audioOutputPath, hasAudio } = audioResult;
13393
- perfStages.audioProcessMs = audioResult.audioProcessMs;
13394
- recordLimiterAttenuation(job, audioResult.audioLoweredDb);
13395
- if (audioResult.audioError) {
13396
- const audioFailures = audioResult.audioFailures ?? [];
13397
- const failureOwner = audioFailures.length === 0 ? void 0 : audioFailures.some((failure) => failure.owner === "system") ? "system" : "user";
13398
- const retryable = audioFailures.length === 0 ? void 0 : audioFailures.every((failure) => failure.retryable);
13399
- applyRenderWarningPolicy(
13400
- job,
13401
- [
13402
- {
13403
- code: "audio_processing_failed",
13404
- message: `Audio mix failed; output would be video-only: ${audioResult.audioError}`,
13405
- details: {
13406
- mediaType: "audio",
13407
- audioFailures: audioFailureDiagnostics2(audioFailures),
13408
- failureReasons: [...new Set(audioFailures.map((failure) => failure.reason))],
13409
- failureStages: [...new Set(audioFailures.map((failure) => failure.stage))],
13410
- failureOwner,
13411
- retryable
13412
- }
13413
- }
13414
- ],
13415
- log
13416
- );
13417
- }
13417
+ pendingAudio = audioStage.catch(() => void 0);
13418
13418
  const stage4Start = Date.now();
13419
13419
  updateJobStatus(job, "rendering", "Starting frame capture", 25, onProgress, {
13420
13420
  code: "start_browsers"
@@ -13518,9 +13518,7 @@ async function executeRenderPipeline(input) {
13518
13518
  const hasHdrContent = Boolean(effectiveHdr && nativeHdrIds.size > 0);
13519
13519
  if (hasHdrContent) assertOutputScaleSupported(deviceScaleFactor, "hdr_layered");
13520
13520
  routePageMotionBlur = pageMotionBlurForRoute({ hasHdrContent, useLayeredComposite: false });
13521
- const deSingleMinFramesRaw = process.env.HF_DE_SINGLE_MIN_FRAMES;
13522
- const deSingleMinFramesNum = deSingleMinFramesRaw === void 0 || deSingleMinFramesRaw.trim() === "" ? 900 : Number(deSingleMinFramesRaw);
13523
- const deSingleMinFrames = Number.isFinite(deSingleMinFramesNum) ? deSingleMinFramesNum : 900;
13521
+ const deSingleMinFrames = resolveDeSingleMinFrames(process.env);
13524
13522
  const deShortBandMinFrames = envInt("HF_DE_SHORT_MIN_FRAMES", 250);
13525
13523
  const deShortBandMaxElements = envInt("HF_DE_SHORT_MAX_ELEMENTS", 2500);
13526
13524
  const {
@@ -13689,7 +13687,7 @@ async function executeRenderPipeline(input) {
13689
13687
  stagePhase: "calibrating"
13690
13688
  }),
13691
13689
  () => runCaptureCalibration({
13692
- cfg,
13690
+ cfg: calibrationCaptureConfig(cfg),
13693
13691
  fileServer: activeFileServer,
13694
13692
  workDir,
13695
13693
  log,
@@ -13725,10 +13723,28 @@ async function executeRenderPipeline(input) {
13725
13723
  deParallelRouterEligible
13726
13724
  });
13727
13725
  }
13726
+ const appleGpuDiskCapture = isAppleGpuDiskCapture({
13727
+ platform: process.platform,
13728
+ browserGpuMode: resolvedBrowserGpuMode,
13729
+ layered: shouldUseLayeredComposite({
13730
+ hasHdrContent,
13731
+ hasShaderTransitions: compiled.hasShaderTransitions && !isGif,
13732
+ isPngSequence
13733
+ }),
13734
+ parallelStream: isCaptureParallelStreamRouterEnabled(
13735
+ process.env,
13736
+ resolveParallelCaptureMode({
13737
+ platform: process.platform,
13738
+ headlessShellPath: resolveHeadlessShellPath(cfg),
13739
+ forceScreenshot: captureForceScreenshot,
13740
+ deviceScaleFactor: captureOptions.deviceScaleFactor
13741
+ })
13742
+ )
13743
+ });
13728
13744
  workerCount = resolveRenderWorkerCount(
13729
13745
  totalFrames,
13730
13746
  job.config.workers,
13731
- cfg,
13747
+ { ...cfg, appleGpuDiskCapture },
13732
13748
  compiled,
13733
13749
  log,
13734
13750
  captureCalibration?.estimate,
@@ -14088,6 +14104,33 @@ async function executeRenderPipeline(input) {
14088
14104
  }
14089
14105
  const effectiveQuality = job.config.crf ?? preset.quality;
14090
14106
  const effectiveBitrate = job.config.crf != null ? void 0 : job.config.videoBitrate;
14107
+ const audioResult = await audioStage;
14108
+ const { audioOutputPath, hasAudio } = audioResult;
14109
+ perfStages.audioProcessMs = audioResult.audioProcessMs;
14110
+ recordLimiterAttenuation(job, audioResult.audioLoweredDb);
14111
+ if (audioResult.audioError) {
14112
+ const audioFailures = audioResult.audioFailures ?? [];
14113
+ const failureOwner = audioFailures.length === 0 ? void 0 : audioFailures.some((failure) => failure.owner === "system") ? "system" : "user";
14114
+ const retryable = audioFailures.length === 0 ? void 0 : audioFailures.every((failure) => failure.retryable);
14115
+ applyRenderWarningPolicy(
14116
+ job,
14117
+ [
14118
+ {
14119
+ code: "audio_processing_failed",
14120
+ message: `Audio mix failed; output would be video-only: ${audioResult.audioError}`,
14121
+ details: {
14122
+ mediaType: "audio",
14123
+ audioFailures: audioFailureDiagnostics2(audioFailures),
14124
+ failureReasons: [...new Set(audioFailures.map((failure) => failure.reason))],
14125
+ failureStages: [...new Set(audioFailures.map((failure) => failure.stage))],
14126
+ failureOwner,
14127
+ retryable
14128
+ }
14129
+ }
14130
+ ],
14131
+ log
14132
+ );
14133
+ }
14091
14134
  resetCaptureAttemptProgress(job);
14092
14135
  if (capturePlan.kind === "hdr_layered") {
14093
14136
  const layeredPlan = capturePlan;
@@ -14802,6 +14845,7 @@ async function executeRenderPipeline(input) {
14802
14845
  updateJobStatus(job, "complete", "Render complete", 100, onProgress);
14803
14846
  await eventPublisher.flush();
14804
14847
  } catch (caught) {
14848
+ await pendingAudio;
14805
14849
  const error = tooSlowError ?? caught;
14806
14850
  if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
14807
14851
  job.error = error instanceof Error ? error.message : "render_cancelled";
@@ -8,7 +8,7 @@
8
8
  * (`CAPTURE_CALIBRATION_TARGET_MS`) are tunable knobs — they pin the
9
9
  * relationship between observed capture time and worker count.
10
10
  */
11
- import { type BeforeCaptureHook, type CaptureOptions, type CaptureSession, type EngineConfig, type WorkerSizing } from "@hyperframes/engine";
11
+ import { type BeforeCaptureHook, type CaptureOptions, type CaptureSession, type EngineConfig, type WorkerSizing, type WorkerSizingConfig } from "@hyperframes/engine";
12
12
  import type { CompiledComposition } from "../htmlCompiler.js";
13
13
  import type { FileServerHandle } from "../fileServer.js";
14
14
  import { type ProducerLogger } from "../../logger.js";
@@ -56,7 +56,7 @@ export declare function estimateCaptureCostMultiplier(compiled: Pick<CompiledCom
56
56
  * synthetic `WorkerSizing` (the real one depends on the host's heap).
57
57
  */
58
58
  export declare function buildHeapAdvisoryWarning(sizing: WorkerSizing, requestedWorkers: number | undefined): string | undefined;
59
- export declare function resolveRenderWorkerCount(totalFrames: number, requestedWorkers: number | undefined, cfg: EngineConfig, compiled: Pick<CompiledComposition, "hasShaderTransitions" | "renderModeHints">, log?: ProducerLogger, measuredCaptureCost?: CaptureCostEstimate,
59
+ export declare function resolveRenderWorkerCount(totalFrames: number, requestedWorkers: number | undefined, cfg: EngineConfig & Pick<WorkerSizingConfig, "appleGpuDiskCapture">, compiled: Pick<CompiledComposition, "hasShaderTransitions" | "renderModeHints">, log?: ProducerLogger, measuredCaptureCost?: CaptureCostEstimate,
60
60
  /** Sink for the sizing provenance so the orchestrator can thread it into telemetry. */
61
61
  onSizing?: (sizing: WorkerSizing) => void): number;
62
62
  export declare function createCaptureCalibrationConfig(cfg: EngineConfig): EngineConfig;
@@ -104,8 +104,8 @@ export interface EncodeStageResult {
104
104
  encodeMs: number;
105
105
  }
106
106
  /**
107
- * Software libx264 from JPEG only: the streaming encoder then runs the disk encoder's
108
- * arguments (same bytes). Needs a core the capture workers leave free.
107
+ * h264 from JPEG only, software or GPU: the streaming encoder then runs the disk encoder's
108
+ * arguments. Needs a core the capture workers leave free.
109
109
  */
110
110
  export declare function canEncodeWhileCapturing(input: EncodeStageInput, workerCount: number, cpuCount: number): boolean;
111
111
  export interface EncodeWhileCapturing {
@@ -829,15 +829,17 @@ export declare function resolveDeShortBand(args: {
829
829
  bandOpen: boolean;
830
830
  elementCountSource: "live" | "static";
831
831
  }): "applied" | "skipped_elements" | "unmeasured" | undefined;
832
+ /** Single-worker drawElement inversion floor; off (0) by default: parallel screenshot capture was
833
+ * ~25% faster wherever it fired. HF_DE_SINGLE_MIN_FRAMES=<n> turns it back on. */
834
+ export declare function resolveDeSingleMinFrames(env: NodeJS.ProcessEnv): number;
832
835
  /**
833
836
  * DE priority inversion predicate: should a multi-worker render drop to
834
837
  * single-worker verified drawElement streaming?
835
838
  *
836
- * Benchmarked 2026-07-08: above ~900 frames DE-single beats screenshot-parallel
837
- * at every worker count (2,380f: 66s vs 109–127s at W2–W5); below it DE's fixed
838
- * init cost (verify + dedup arming) loses by a small margin. Only fires for the
839
- * exact benchmarked configuration: default-on DE, mp4, streaming-eligible,
840
- * no compile gate, no forced screenshot.
839
+ * Opt-in (see resolveDeSingleMinFrames): re-measured on hardware-GL Macs, one
840
+ * drawElement worker was ~25% slower than 4 screenshot workers at 1,271 and 9,000
841
+ * frames. Only fires for default-on DE, mp4, streaming-eligible, no compile gate,
842
+ * no forced screenshot.
841
843
  */
842
844
  export declare function shouldPreferSingleWorkerDrawElement(args: {
843
845
  workerCount: number;
@@ -1226,6 +1228,13 @@ export declare function shouldSegmentCapture(args: {
1226
1228
  /** shouldUseStreamingEncode(cfg, format, 1, duration) at the call site. */
1227
1229
  streamingOk: boolean;
1228
1230
  }): boolean;
1231
+ /** Worker sizing's Apple-GPU budget fits only frames written to disk by screenshot workers. */
1232
+ export declare function isAppleGpuDiskCapture(args: {
1233
+ platform: NodeJS.Platform;
1234
+ browserGpuMode: "software" | "hardware";
1235
+ layered: boolean;
1236
+ parallelStream: boolean;
1237
+ }): boolean;
1229
1238
  export declare function shouldStreamParallelCapture(args: {
1230
1239
  /** Router switch for this render — see isCaptureParallelStreamRouterEnabled. */
1231
1240
  routerEnabled: boolean;
@@ -1260,6 +1269,11 @@ export declare function shouldClampDefaultDrawElement(args: {
1260
1269
  * self-verification net is present. */
1261
1270
  deParallelStreamVerified: boolean;
1262
1271
  }): boolean;
1272
+ /**
1273
+ * Calibration sizes a parallel render, and parallel workers capture by screenshot
1274
+ * unless drawElement was opted into: measure that mode, without a drawElement init.
1275
+ */
1276
+ export declare function calibrationCaptureConfig(cfg: EngineConfig, env?: NodeJS.ProcessEnv): EngineConfig;
1263
1277
  export declare function resolveCaptureForceScreenshotForPageSideCompositing(args: {
1264
1278
  forceScreenshot: boolean;
1265
1279
  usePageSideCompositing: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.8.210",
3
+ "version": "0.8.212",
4
4
  "description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,13 +33,13 @@
33
33
  "hono": "^4.6.0",
34
34
  "linkedom": "^0.18.12",
35
35
  "postcss": "^8.4.0",
36
- "puppeteer": "^25.2.1",
37
- "puppeteer-core": "^25.2.1",
38
- "@hyperframes/core": "0.8.210",
39
- "@hyperframes/engine": "0.8.210",
40
- "@hyperframes/lint": "0.8.210",
41
- "@hyperframes/studio-server": "0.8.210",
42
- "@hyperframes/parsers": "0.8.210"
36
+ "puppeteer": "~25.11.0",
37
+ "puppeteer-core": "^25.11.0",
38
+ "@hyperframes/engine": "0.8.212",
39
+ "@hyperframes/lint": "0.8.212",
40
+ "@hyperframes/parsers": "0.8.212",
41
+ "@hyperframes/studio-server": "0.8.212",
42
+ "@hyperframes/core": "0.8.212"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^25.0.10",
@@ -48,7 +48,7 @@
48
48
  "tsx": "^4.21.0",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^4.1.11",
51
- "@hyperframes/aws-lambda": "0.8.210"
51
+ "@hyperframes/aws-lambda": "0.8.212"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"