@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/distributed.js +22 -16
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +64 -64
- package/dist/index.js +92 -48
- package/dist/public-server.js +92 -48
- package/dist/services/render/captureCost.d.ts +2 -2
- package/dist/services/render/stages/encodeStage.d.ts +2 -2
- package/dist/services/renderOrchestrator.d.ts +19 -5
- package/package.json +9 -9
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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 !== "" && !
|
|
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
|
-
|
|
6876
|
+
const remoteDir = join15(downloadDir, REMOTE_MEDIA_SUBDIR);
|
|
6877
|
+
if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
|
|
6872
6878
|
try {
|
|
6873
|
-
filePath = await downloadToTemp(src,
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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";
|
package/dist/public-server.js
CHANGED
|
@@ -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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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 !== "" && !
|
|
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
|
-
|
|
6893
|
+
const remoteDir = join15(downloadDir, REMOTE_MEDIA_SUBDIR);
|
|
6894
|
+
if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
|
|
6889
6895
|
try {
|
|
6890
|
-
filePath = await downloadToTemp(src,
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
108
|
-
* arguments
|
|
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
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
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.
|
|
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": "
|
|
37
|
-
"puppeteer-core": "^25.
|
|
38
|
-
"@hyperframes/
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/
|
|
41
|
-
"@hyperframes/studio-server": "0.8.
|
|
42
|
-
"@hyperframes/
|
|
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.
|
|
51
|
+
"@hyperframes/aws-lambda": "0.8.212"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|