@hyperframes/producer 0.8.199 → 0.8.201
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/README.md +8 -8
- package/dist/distributed.js +105 -33
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +66 -62
- package/dist/index.js +175 -43
- package/dist/public-server.js +141 -38
- package/dist/regression-harness.d.ts +53 -0
- package/dist/services/distributed/chunkWeights.d.ts +2 -0
- package/dist/services/htmlCompiler.d.ts +0 -2
- package/dist/services/render/audioPadTrim.d.ts +2 -0
- package/dist/services/render/stages/captureStage.d.ts +1 -0
- package/dist/services/render/stages/compileStage.d.ts +1 -1
- package/dist/services/render/stages/freezePlan.d.ts +1 -0
- package/dist/services/renderMediaCollector.d.ts +2 -0
- package/dist/services/renderOrchestrator.d.ts +37 -45
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -61,13 +61,13 @@ are rejected before a render starts. This is separate from engine sub-frame
|
|
|
61
61
|
|
|
62
62
|
The producer can render HTML compositions to formats that carry a **true alpha channel** — not chroma key. The same composition that renders an opaque MP4 renders a layerable overlay when you set `format`.
|
|
63
63
|
|
|
64
|
-
| `format` | Codec / pixel format | Alpha
|
|
65
|
-
| ----------------- | --------------------------------- |
|
|
66
|
-
| `"mp4"` (default) | H.264 (yuv420p) or H.265 + HDR10 | No
|
|
67
|
-
| `"webm"` | VP9 + yuva420p | **True alpha**
|
|
68
|
-
| `"mov"` | ProRes 4444 + yuva444p10le | **True alpha
|
|
69
|
-
| `"png-sequence"` | Numbered RGBA PNGs in a directory | **Lossless alpha**
|
|
70
|
-
| `"hls"` | H.264 (yuv420p) in MPEG-TS | No
|
|
64
|
+
| `format` | Codec / pixel format | Alpha | Audio | Use case |
|
|
65
|
+
| ----------------- | --------------------------------- | ------------------ | ------------------- | --------------------------------------------------------------------------------------- |
|
|
66
|
+
| `"mp4"` (default) | H.264 (yuv420p) or H.265 + HDR10 | No | AAC | Streaming, social, default deliverable |
|
|
67
|
+
| `"webm"` | VP9 + yuva420p | **True alpha** | Opus | Web playback as overlay (`<video>` over background); supported in Chrome, Edge, Firefox |
|
|
68
|
+
| `"mov"` | ProRes 4444 + yuva444p10le | **True alpha** | AAC | Editor ingest (Premiere, Final Cut Pro, DaVinci Resolve) |
|
|
69
|
+
| `"png-sequence"` | Numbered RGBA PNGs in a directory | **Lossless alpha** | Sidecar `audio.m4a` | After Effects / Nuke / Fusion, or pipelines that post-process frames before encoding |
|
|
70
|
+
| `"hls"` | H.264 (yuv420p) in MPEG-TS | No | AAC rendition | VOD streaming — see [HLS Output](#hls-output) |
|
|
71
71
|
|
|
72
72
|
### Example
|
|
73
73
|
|
|
@@ -88,7 +88,7 @@ await executeRenderJob(job, "./my-project", "./output.webm");
|
|
|
88
88
|
The producer captures Chrome screenshots with the page background forced transparent (`html, body, [data-composition-id] { background: transparent !important }`) and the CDP default background override set to RGBA 0,0,0,0. The captured PNGs carry a real alpha channel and that channel is preserved end-to-end:
|
|
89
89
|
|
|
90
90
|
- VP9 (`webm`) is encoded with `-pix_fmt yuva420p`, `-auto-alt-ref 0`, `-cpu-used 4` by default, and `alpha_mode=1` metadata. Tune the speed/quality tradeoff with `PRODUCER_VP9_CPU_USED` (`-8` to `8`) or local CLI `--vp9-cpu-used`.
|
|
91
|
-
- ProRes 4444 (`mov`) is
|
|
91
|
+
- ProRes 4444 (`mov`) uses `-pix_fmt yuva444p10le` and carries a separate alpha plane. Chrome's RGBA capture, including alpha, is 8 bits per channel; the 10-bit pixel format adds no capture precision.
|
|
92
92
|
- PNG sequences are written without re-encoding (zero-padded `frame_NNNNNN.png`).
|
|
93
93
|
|
|
94
94
|
This is not chroma keying. There is no green/blue background to remove and no "key" tolerance to tune — pixels that were transparent in the browser are transparent in the output.
|
package/dist/distributed.js
CHANGED
|
@@ -96,6 +96,7 @@ import {
|
|
|
96
96
|
createVideoFrameInjector,
|
|
97
97
|
getEncoderPreset as getEncoderPreset3,
|
|
98
98
|
distributeFrames as distributeFrames2,
|
|
99
|
+
distributeFramesInterleaved as distributeFramesInterleaved2,
|
|
99
100
|
executeParallelCapture as executeParallelCapture2,
|
|
100
101
|
mergeWorkerFrames,
|
|
101
102
|
getSystemTotalMb,
|
|
@@ -1540,6 +1541,7 @@ import {
|
|
|
1540
1541
|
parseVideoElements,
|
|
1541
1542
|
parseImageElements,
|
|
1542
1543
|
parseAudioElements,
|
|
1544
|
+
isSelfOrAncestorHidden,
|
|
1543
1545
|
resolveReferencedStart
|
|
1544
1546
|
} from "@hyperframes/engine";
|
|
1545
1547
|
function resolveHostEnd(host, hostStart) {
|
|
@@ -1600,6 +1602,15 @@ function collectHostWindows(html) {
|
|
|
1600
1602
|
}
|
|
1601
1603
|
return windows;
|
|
1602
1604
|
}
|
|
1605
|
+
function collectHiddenVideoIds(html) {
|
|
1606
|
+
const { document: document2 } = parseHTML2(html);
|
|
1607
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
1608
|
+
for (const element of document2.querySelectorAll("video")) {
|
|
1609
|
+
const id = element.getAttribute(MEDIA_RENDER_ID_ATTR) || element.getAttribute("id");
|
|
1610
|
+
if (id && isSelfOrAncestorHidden(element)) hidden.add(id);
|
|
1611
|
+
}
|
|
1612
|
+
return hidden;
|
|
1613
|
+
}
|
|
1603
1614
|
function toAbsoluteWindow(start, end, window2) {
|
|
1604
1615
|
const absoluteStart = resolveAbsoluteMediaStartSeconds({
|
|
1605
1616
|
authoredStart: start,
|
|
@@ -1633,8 +1644,13 @@ function collectRenderMedia(html) {
|
|
|
1633
1644
|
const windows = collectHostWindows(html);
|
|
1634
1645
|
const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
|
|
1635
1646
|
const droppedMediaIds = /* @__PURE__ */ new Set();
|
|
1647
|
+
const hiddenIds = collectHiddenVideoIds(html);
|
|
1636
1648
|
const videos = [];
|
|
1637
1649
|
for (const video of parseVideoElements(html)) {
|
|
1650
|
+
if (hiddenIds.has(video.id)) {
|
|
1651
|
+
droppedMediaIds.add(video.id);
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1638
1654
|
const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
|
|
1639
1655
|
if (!absolute) {
|
|
1640
1656
|
droppedMediaIds.add(video.id);
|
|
@@ -1844,7 +1860,7 @@ async function runAnimatedGifTranscode(request) {
|
|
|
1844
1860
|
failureReason: result.failureReason
|
|
1845
1861
|
});
|
|
1846
1862
|
}
|
|
1847
|
-
if (result.terminationReason === "
|
|
1863
|
+
if (result.terminationReason === "inactivity") {
|
|
1848
1864
|
throw new Error(`Animated GIF transcode timed out after ${timeout}ms`);
|
|
1849
1865
|
}
|
|
1850
1866
|
throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
|
|
@@ -2311,10 +2327,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
|
|
|
2311
2327
|
function detectThreeDTransformUsage(html) {
|
|
2312
2328
|
return THREE_D_CONTEXT_PATTERN.test(html);
|
|
2313
2329
|
}
|
|
2314
|
-
var MIX_BLEND_MODE_PATTERN = /mix-blend-mode\s*:/i;
|
|
2315
|
-
function detectMixBlendModeUsage(html) {
|
|
2316
|
-
return MIX_BLEND_MODE_PATTERN.test(html);
|
|
2317
|
-
}
|
|
2318
2330
|
var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
|
|
2319
2331
|
function detectAncestorBackgroundImage(html) {
|
|
2320
2332
|
const { document: document2 } = parseHTML4(html);
|
|
@@ -2467,8 +2479,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
2467
2479
|
const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
|
|
2468
2480
|
let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
|
|
2469
2481
|
const preResolved = extractResolvedMedia(compiledHtml);
|
|
2482
|
+
const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
|
|
2470
2483
|
const clampResults = await Promise.all(
|
|
2471
|
-
preResolved.filter((el) => !!el.src && !el.loop).map(async (el) => {
|
|
2484
|
+
preResolved.filter((el) => !!el.src && !el.loop && !hiddenVideoIds.has(el.id)).map(async (el) => {
|
|
2472
2485
|
const { duration: maxDuration } = await resolveMediaDuration(
|
|
2473
2486
|
el.src,
|
|
2474
2487
|
el.mediaStart,
|
|
@@ -3327,7 +3340,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
3327
3340
|
const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
|
|
3328
3341
|
const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
|
|
3329
3342
|
const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
|
|
3330
|
-
const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
|
|
3331
3343
|
const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
|
|
3332
3344
|
const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
|
|
3333
3345
|
injectTextRenderingRule(
|
|
@@ -3441,7 +3453,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
3441
3453
|
renderModeHints,
|
|
3442
3454
|
hasShaderTransitions,
|
|
3443
3455
|
usesThreeDTransforms,
|
|
3444
|
-
usesMixBlendMode,
|
|
3445
3456
|
hasAncestorBackgroundImage
|
|
3446
3457
|
};
|
|
3447
3458
|
}
|
|
@@ -3848,13 +3859,6 @@ async function runCompileStage(input) {
|
|
|
3848
3859
|
"[Render] Fast capture: composition uses a CSS 3D rendering context (perspective / preserve-3d / backface-visibility), so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
3849
3860
|
);
|
|
3850
3861
|
}
|
|
3851
|
-
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
|
|
3852
|
-
cfg.useDrawElement = false;
|
|
3853
|
-
deCompileGate = "mix_blend_mode";
|
|
3854
|
-
log.info(
|
|
3855
|
-
"[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
3856
|
-
);
|
|
3857
|
-
}
|
|
3858
3862
|
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
|
|
3859
3863
|
cfg.useDrawElement = false;
|
|
3860
3864
|
deCompileGate = "ancestor_background_image";
|
|
@@ -4148,6 +4152,7 @@ async function runProbeStage(input) {
|
|
|
4148
4152
|
height,
|
|
4149
4153
|
fps: job.config.fps,
|
|
4150
4154
|
format: captureFormat,
|
|
4155
|
+
transparent: needsAlpha,
|
|
4151
4156
|
quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
|
|
4152
4157
|
variables: job.config.variables,
|
|
4153
4158
|
deviceScaleFactor,
|
|
@@ -5312,10 +5317,11 @@ async function constrainAacTruePeak(input) {
|
|
|
5312
5317
|
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
5313
5318
|
return enforceAacTruePeak({
|
|
5314
5319
|
audioPath: input.audioPath,
|
|
5320
|
+
sourcePath: input.sourcePath,
|
|
5315
5321
|
targetDurationSeconds: input.targetDurationSeconds,
|
|
5316
5322
|
signal: input.signal,
|
|
5317
|
-
probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
|
|
5318
|
-
encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
5323
|
+
probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
|
|
5324
|
+
encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
5319
5325
|
});
|
|
5320
5326
|
}
|
|
5321
5327
|
async function enforceAacTruePeak(input) {
|
|
@@ -5325,6 +5331,7 @@ async function enforceAacTruePeak(input) {
|
|
|
5325
5331
|
const correctedPath = join9(scratchDir, "audio.m4a");
|
|
5326
5332
|
const measurements = [];
|
|
5327
5333
|
let attenuationDb = 0;
|
|
5334
|
+
let sourcePeakDbfs;
|
|
5328
5335
|
let measuredPath = input.audioPath;
|
|
5329
5336
|
for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
|
|
5330
5337
|
const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
|
|
@@ -5341,7 +5348,11 @@ async function enforceAacTruePeak(input) {
|
|
|
5341
5348
|
}
|
|
5342
5349
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
5343
5350
|
const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
|
|
5344
|
-
|
|
5351
|
+
if (input.sourcePath && sourcePeakDbfs === void 0) {
|
|
5352
|
+
sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
|
|
5353
|
+
}
|
|
5354
|
+
const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
|
|
5355
|
+
attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
|
|
5345
5356
|
const result = await input.encode(
|
|
5346
5357
|
input.audioPath,
|
|
5347
5358
|
correctedPath,
|
|
@@ -5758,7 +5769,7 @@ async function runCaptureStage(input) {
|
|
|
5758
5769
|
);
|
|
5759
5770
|
}
|
|
5760
5771
|
}
|
|
5761
|
-
const captureOptions = buildCaptureOptions();
|
|
5772
|
+
const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
|
|
5762
5773
|
const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
|
|
5763
5774
|
const checkDiskProjection = createDiskCaptureProjection({
|
|
5764
5775
|
framesDir,
|
|
@@ -5779,6 +5790,7 @@ async function runCaptureStage(input) {
|
|
|
5779
5790
|
createBeforeCaptureHook: createRenderVideoFrameInjector,
|
|
5780
5791
|
abortSignal,
|
|
5781
5792
|
frameRangeStart: frameRange?.startFrame,
|
|
5793
|
+
interleaveFrames: input.interleaveFrames,
|
|
5782
5794
|
dedupPerfs,
|
|
5783
5795
|
pageErrorLines: workerPageErrors,
|
|
5784
5796
|
onProgress: (progress) => {
|
|
@@ -6356,9 +6368,6 @@ async function runEncodeStage(input) {
|
|
|
6356
6368
|
return { encodeMs: Date.now() - stage5Start };
|
|
6357
6369
|
}
|
|
6358
6370
|
const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
|
|
6359
|
-
const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
|
|
6360
|
-
const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
|
|
6361
|
-
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
6362
6371
|
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
6363
6372
|
const framePattern = `frame_%06d.${frameExt}`;
|
|
6364
6373
|
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
@@ -6369,7 +6378,7 @@ async function runEncodeStage(input) {
|
|
|
6369
6378
|
encoderOpts,
|
|
6370
6379
|
chunkedEncodeSize,
|
|
6371
6380
|
abortSignal,
|
|
6372
|
-
|
|
6381
|
+
engineCfg,
|
|
6373
6382
|
onFramesEncoded
|
|
6374
6383
|
) : await encodeFramesFromDir(
|
|
6375
6384
|
framesDir,
|
|
@@ -6377,7 +6386,7 @@ async function runEncodeStage(input) {
|
|
|
6377
6386
|
videoOnlyPath,
|
|
6378
6387
|
encoderOpts,
|
|
6379
6388
|
abortSignal,
|
|
6380
|
-
|
|
6389
|
+
engineCfg,
|
|
6381
6390
|
onFramesEncoded
|
|
6382
6391
|
);
|
|
6383
6392
|
assertNotAborted();
|
|
@@ -6505,16 +6514,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
6505
6514
|
}
|
|
6506
6515
|
return ranges;
|
|
6507
6516
|
}
|
|
6508
|
-
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
|
|
6517
|
+
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
6509
6518
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
6510
6519
|
const batches = [];
|
|
6520
|
+
const outputDirFor = (batchIndex, workerId) => join13(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
|
|
6521
|
+
if (stride > 1) {
|
|
6522
|
+
const spans = /* @__PURE__ */ new Map();
|
|
6523
|
+
for (const range of ranges) {
|
|
6524
|
+
for (let frame = range.startFrame; frame < range.endFrame; frame++) {
|
|
6525
|
+
const span = spans.get(frame % stride);
|
|
6526
|
+
if (span) span.last = frame;
|
|
6527
|
+
else spans.set(frame % stride, { first: frame, last: frame });
|
|
6528
|
+
}
|
|
6529
|
+
}
|
|
6530
|
+
const strided = [...spans.values()];
|
|
6531
|
+
for (let i = 0; i < strided.length; i += workersPerBatch) {
|
|
6532
|
+
const batchIndex = batches.length;
|
|
6533
|
+
batches.push(
|
|
6534
|
+
strided.slice(i, i + workersPerBatch).map(({ first, last }, workerId) => ({
|
|
6535
|
+
workerId,
|
|
6536
|
+
startFrame: rangeStart + first,
|
|
6537
|
+
endFrame: rangeStart + last + 1,
|
|
6538
|
+
frameStride: stride,
|
|
6539
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
6540
|
+
outputFrameOffset: rangeStart
|
|
6541
|
+
}))
|
|
6542
|
+
);
|
|
6543
|
+
}
|
|
6544
|
+
return batches;
|
|
6545
|
+
}
|
|
6511
6546
|
for (let i = 0; i < ranges.length; i += workersPerBatch) {
|
|
6512
6547
|
const batchIndex = batches.length;
|
|
6513
6548
|
const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
|
|
6514
6549
|
workerId,
|
|
6515
6550
|
startFrame: rangeStart + range.startFrame,
|
|
6516
6551
|
endFrame: rangeStart + range.endFrame,
|
|
6517
|
-
outputDir:
|
|
6552
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
6518
6553
|
outputFrameOffset: rangeStart
|
|
6519
6554
|
}));
|
|
6520
6555
|
batches.push(batch);
|
|
@@ -6572,8 +6607,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
6572
6607
|
currentWorkers,
|
|
6573
6608
|
attemptWorkDir,
|
|
6574
6609
|
attempt,
|
|
6575
|
-
rangeStart
|
|
6576
|
-
|
|
6610
|
+
rangeStart,
|
|
6611
|
+
options.interleaveFrames ? options.initialWorkerCount : 1
|
|
6612
|
+
) : [
|
|
6613
|
+
(options.interleaveFrames ? distributeFramesInterleaved2 : distributeFrames2)(
|
|
6614
|
+
options.totalFrames,
|
|
6615
|
+
currentWorkers,
|
|
6616
|
+
attemptWorkDir,
|
|
6617
|
+
rangeStart
|
|
6618
|
+
)
|
|
6619
|
+
];
|
|
6577
6620
|
options.dedupPerfs.length = 0;
|
|
6578
6621
|
try {
|
|
6579
6622
|
for (const tasks of batches) {
|
|
@@ -7336,6 +7379,18 @@ function planFrameWeights(input) {
|
|
|
7336
7379
|
}
|
|
7337
7380
|
return weights;
|
|
7338
7381
|
}
|
|
7382
|
+
function withChunkCaptureCost(slices, weights) {
|
|
7383
|
+
const lightest = weights.reduce((least, weight) => Math.min(least, weight), Infinity);
|
|
7384
|
+
return slices.map((slice) => {
|
|
7385
|
+
let sum2 = 0;
|
|
7386
|
+
for (let frame = slice.startFrame; frame < slice.endFrame; frame++) sum2 += weights[frame];
|
|
7387
|
+
const cost = Math.round(sum2 / (slice.endFrame - slice.startFrame) / lightest * 100) / 100;
|
|
7388
|
+
return cost > 1 ? { ...slice, captureCost: cost } : slice;
|
|
7389
|
+
});
|
|
7390
|
+
}
|
|
7391
|
+
function effectiveChunkFrames(frames, captureCost = 1) {
|
|
7392
|
+
return Math.round(frames * captureCost);
|
|
7393
|
+
}
|
|
7339
7394
|
function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
|
|
7340
7395
|
if (isUniformFrameCost(weights)) return null;
|
|
7341
7396
|
const totalFrames = weights.length;
|
|
@@ -8660,7 +8715,10 @@ function buildPlanChunkSlices(frameWeights, config, chunkCount, effectiveChunkSi
|
|
|
8660
8715
|
MIN_CHUNK_SIZE,
|
|
8661
8716
|
Math.min(config.targetChunkFrames ?? MAX_CHUNK_SIZE, MAX_CHUNK_SIZE, maxFramesPerChunk)
|
|
8662
8717
|
) : null;
|
|
8663
|
-
return
|
|
8718
|
+
return withChunkCaptureCost(
|
|
8719
|
+
weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize),
|
|
8720
|
+
frameWeights
|
|
8721
|
+
);
|
|
8664
8722
|
}
|
|
8665
8723
|
async function readFontSnapshotSha() {
|
|
8666
8724
|
const module = await import("@hyperframes/core/fonts/embedded-data");
|
|
@@ -9651,7 +9709,12 @@ function parseChunkSlices(value) {
|
|
|
9651
9709
|
throw new PlanV2IntegrityError(`meta/chunks.json contains duplicate chunk index ${index}`);
|
|
9652
9710
|
}
|
|
9653
9711
|
indexes.add(index);
|
|
9654
|
-
return { index, startFrame, endFrame };
|
|
9712
|
+
if (chunk.captureCost === void 0) return { index, startFrame, endFrame };
|
|
9713
|
+
const captureCost = chunk.captureCost;
|
|
9714
|
+
if (typeof captureCost !== "number" || !Number.isFinite(captureCost) || captureCost <= 1) {
|
|
9715
|
+
throw new PlanV2IntegrityError(`${field}.captureCost must be a number above 1`);
|
|
9716
|
+
}
|
|
9717
|
+
return { index, startFrame, endFrame, captureCost };
|
|
9655
9718
|
});
|
|
9656
9719
|
}
|
|
9657
9720
|
function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
@@ -10828,15 +10891,17 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10828
10891
|
deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
|
|
10829
10892
|
)
|
|
10830
10893
|
});
|
|
10894
|
+
const captureFrames = effectiveChunkFrames(framesInChunk, slice.captureCost);
|
|
10831
10895
|
const chunkWorkerCount = resolveChunkCaptureWorkers(
|
|
10832
|
-
|
|
10833
|
-
calculateOptimalWorkers2(
|
|
10896
|
+
captureFrames,
|
|
10897
|
+
calculateOptimalWorkers2(captureFrames, void 0, cfg),
|
|
10834
10898
|
options?.maxCaptureWorkers
|
|
10835
10899
|
);
|
|
10836
10900
|
let session = null;
|
|
10837
10901
|
let outputKind;
|
|
10838
10902
|
let framesEncoded = 0;
|
|
10839
10903
|
let sessionBootMs = 0;
|
|
10904
|
+
let videoExtractMs = 0;
|
|
10840
10905
|
let captureStageMs = 0;
|
|
10841
10906
|
let encodeStageMs = 0;
|
|
10842
10907
|
let captureMode;
|
|
@@ -10902,6 +10967,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10902
10967
|
session = null;
|
|
10903
10968
|
}
|
|
10904
10969
|
}
|
|
10970
|
+
const extractStarted = Date.now();
|
|
10905
10971
|
const deferredVideoFrames = planVideos ? extractDeferredVideoFramesForChunk({
|
|
10906
10972
|
planDir,
|
|
10907
10973
|
planVideos,
|
|
@@ -10911,6 +10977,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10911
10977
|
cfg,
|
|
10912
10978
|
remoteSources,
|
|
10913
10979
|
shownBoxes
|
|
10980
|
+
}).then((frames) => {
|
|
10981
|
+
videoExtractMs = Date.now() - extractStarted;
|
|
10982
|
+
return frames;
|
|
10914
10983
|
}) : Promise.resolve([]);
|
|
10915
10984
|
extraction = deferredVideoFrames;
|
|
10916
10985
|
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
|
|
@@ -11039,7 +11108,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11039
11108
|
assertNotAborted: () => {
|
|
11040
11109
|
},
|
|
11041
11110
|
frameRange: range,
|
|
11042
|
-
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
|
|
11111
|
+
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
|
|
11112
|
+
interleaveFrames: overlap.current !== null
|
|
11043
11113
|
}).catch(async (captureError) => {
|
|
11044
11114
|
await overlap.current?.abort();
|
|
11045
11115
|
overlap.current = null;
|
|
@@ -11128,6 +11198,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11128
11198
|
sessionBootMs,
|
|
11129
11199
|
captureStageMs,
|
|
11130
11200
|
encodeStageMs,
|
|
11201
|
+
// Wall time of the chunk's own video frame extraction; it overlaps captureStageMs, so do not add them.
|
|
11202
|
+
videoExtractMs,
|
|
11131
11203
|
workers: chunkWorkerCount,
|
|
11132
11204
|
captureMode,
|
|
11133
11205
|
// Device-pixel box each video's frames were extracted to cover; a video not listed kept full size.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.1.0",
|
|
3
3
|
"buildId": "dev",
|
|
4
|
-
"sha256": "
|
|
4
|
+
"sha256": "74fe41f512dcc9c39366e4f48c8d24e6011f5180f122ab1525d210000618830f",
|
|
5
5
|
"artifacts": {
|
|
6
6
|
"iife": "hyperframe.runtime.iife.js",
|
|
7
7
|
"esm": "hyperframe.runtime.mjs"
|