@hyperframes/producer 0.8.200 → 0.8.202
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 +63 -12
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +65 -64
- package/dist/index.js +66 -13
- package/dist/public-server.js +64 -12
- package/dist/regression-harness.d.ts +53 -0
- package/dist/services/render/stages/captureStage.d.ts +1 -0
- package/dist/services/renderMediaCollector.d.ts +2 -0
- package/dist/services/renderOrchestrator.d.ts +35 -44
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
createVideoFrameInjector,
|
|
34
34
|
getEncoderPreset as getEncoderPreset3,
|
|
35
35
|
distributeFrames as distributeFrames2,
|
|
36
|
+
distributeFramesInterleaved as distributeFramesInterleaved2,
|
|
36
37
|
executeParallelCapture as executeParallelCapture2,
|
|
37
38
|
mergeWorkerFrames,
|
|
38
39
|
getSystemTotalMb,
|
|
@@ -5579,6 +5580,7 @@ import {
|
|
|
5579
5580
|
parseVideoElements,
|
|
5580
5581
|
parseImageElements,
|
|
5581
5582
|
parseAudioElements,
|
|
5583
|
+
isSelfOrAncestorHidden,
|
|
5582
5584
|
resolveReferencedStart
|
|
5583
5585
|
} from "@hyperframes/engine";
|
|
5584
5586
|
function resolveHostEnd(host, hostStart) {
|
|
@@ -5639,6 +5641,15 @@ function collectHostWindows(html) {
|
|
|
5639
5641
|
}
|
|
5640
5642
|
return windows;
|
|
5641
5643
|
}
|
|
5644
|
+
function collectHiddenVideoIds(html) {
|
|
5645
|
+
const { document: document2 } = parseHTML2(html);
|
|
5646
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
5647
|
+
for (const element of document2.querySelectorAll("video")) {
|
|
5648
|
+
const id = element.getAttribute(MEDIA_RENDER_ID_ATTR) || element.getAttribute("id");
|
|
5649
|
+
if (id && isSelfOrAncestorHidden(element)) hidden.add(id);
|
|
5650
|
+
}
|
|
5651
|
+
return hidden;
|
|
5652
|
+
}
|
|
5642
5653
|
function toAbsoluteWindow(start, end, window2) {
|
|
5643
5654
|
const absoluteStart = resolveAbsoluteMediaStartSeconds({
|
|
5644
5655
|
authoredStart: start,
|
|
@@ -5672,8 +5683,13 @@ function collectRenderMedia(html) {
|
|
|
5672
5683
|
const windows = collectHostWindows(html);
|
|
5673
5684
|
const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
|
|
5674
5685
|
const droppedMediaIds = /* @__PURE__ */ new Set();
|
|
5686
|
+
const hiddenIds = collectHiddenVideoIds(html);
|
|
5675
5687
|
const videos = [];
|
|
5676
5688
|
for (const video of parseVideoElements(html)) {
|
|
5689
|
+
if (hiddenIds.has(video.id)) {
|
|
5690
|
+
droppedMediaIds.add(video.id);
|
|
5691
|
+
continue;
|
|
5692
|
+
}
|
|
5677
5693
|
const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
|
|
5678
5694
|
if (!absolute) {
|
|
5679
5695
|
droppedMediaIds.add(video.id);
|
|
@@ -5883,7 +5899,7 @@ async function runAnimatedGifTranscode(request) {
|
|
|
5883
5899
|
failureReason: result.failureReason
|
|
5884
5900
|
});
|
|
5885
5901
|
}
|
|
5886
|
-
if (result.terminationReason === "
|
|
5902
|
+
if (result.terminationReason === "inactivity") {
|
|
5887
5903
|
throw new Error(`Animated GIF transcode timed out after ${timeout}ms`);
|
|
5888
5904
|
}
|
|
5889
5905
|
throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
|
|
@@ -6502,8 +6518,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6502
6518
|
const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
|
|
6503
6519
|
let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
|
|
6504
6520
|
const preResolved = extractResolvedMedia(compiledHtml);
|
|
6521
|
+
const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
|
|
6505
6522
|
const clampResults = await Promise.all(
|
|
6506
|
-
preResolved.filter((el) => !!el.src && !el.loop).map(async (el) => {
|
|
6523
|
+
preResolved.filter((el) => !!el.src && !el.loop && !hiddenVideoIds.has(el.id)).map(async (el) => {
|
|
6507
6524
|
const { duration: maxDuration } = await resolveMediaDuration(
|
|
6508
6525
|
el.src,
|
|
6509
6526
|
el.mediaStart,
|
|
@@ -8174,6 +8191,7 @@ async function runProbeStage(input) {
|
|
|
8174
8191
|
height,
|
|
8175
8192
|
fps: job.config.fps,
|
|
8176
8193
|
format: captureFormat,
|
|
8194
|
+
transparent: needsAlpha,
|
|
8177
8195
|
quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
|
|
8178
8196
|
variables: job.config.variables,
|
|
8179
8197
|
deviceScaleFactor,
|
|
@@ -9803,6 +9821,7 @@ async function runCaptureStage(input) {
|
|
|
9803
9821
|
createBeforeCaptureHook: createRenderVideoFrameInjector,
|
|
9804
9822
|
abortSignal,
|
|
9805
9823
|
frameRangeStart: frameRange?.startFrame,
|
|
9824
|
+
interleaveFrames: input.interleaveFrames,
|
|
9806
9825
|
dedupPerfs,
|
|
9807
9826
|
pageErrorLines: workerPageErrors,
|
|
9808
9827
|
onProgress: (progress) => {
|
|
@@ -11318,9 +11337,6 @@ async function runEncodeStage(input) {
|
|
|
11318
11337
|
return { encodeMs: Date.now() - stage5Start };
|
|
11319
11338
|
}
|
|
11320
11339
|
const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
|
|
11321
|
-
const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
|
|
11322
|
-
const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
|
|
11323
|
-
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
11324
11340
|
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
11325
11341
|
const framePattern = `frame_%06d.${frameExt}`;
|
|
11326
11342
|
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
@@ -11331,7 +11347,7 @@ async function runEncodeStage(input) {
|
|
|
11331
11347
|
encoderOpts,
|
|
11332
11348
|
chunkedEncodeSize,
|
|
11333
11349
|
abortSignal,
|
|
11334
|
-
|
|
11350
|
+
engineCfg,
|
|
11335
11351
|
onFramesEncoded
|
|
11336
11352
|
) : await encodeFramesFromDir(
|
|
11337
11353
|
framesDir,
|
|
@@ -11339,7 +11355,7 @@ async function runEncodeStage(input) {
|
|
|
11339
11355
|
videoOnlyPath,
|
|
11340
11356
|
encoderOpts,
|
|
11341
11357
|
abortSignal,
|
|
11342
|
-
|
|
11358
|
+
engineCfg,
|
|
11343
11359
|
onFramesEncoded
|
|
11344
11360
|
);
|
|
11345
11361
|
assertNotAborted();
|
|
@@ -11681,16 +11697,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11681
11697
|
}
|
|
11682
11698
|
return ranges;
|
|
11683
11699
|
}
|
|
11684
|
-
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
|
|
11700
|
+
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
11685
11701
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
11686
11702
|
const batches = [];
|
|
11703
|
+
const outputDirFor = (batchIndex, workerId) => join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
|
|
11704
|
+
if (stride > 1) {
|
|
11705
|
+
const spans = /* @__PURE__ */ new Map();
|
|
11706
|
+
for (const range of ranges) {
|
|
11707
|
+
for (let frame = range.startFrame; frame < range.endFrame; frame++) {
|
|
11708
|
+
const span = spans.get(frame % stride);
|
|
11709
|
+
if (span) span.last = frame;
|
|
11710
|
+
else spans.set(frame % stride, { first: frame, last: frame });
|
|
11711
|
+
}
|
|
11712
|
+
}
|
|
11713
|
+
const strided = [...spans.values()];
|
|
11714
|
+
for (let i = 0; i < strided.length; i += workersPerBatch) {
|
|
11715
|
+
const batchIndex = batches.length;
|
|
11716
|
+
batches.push(
|
|
11717
|
+
strided.slice(i, i + workersPerBatch).map(({ first, last }, workerId) => ({
|
|
11718
|
+
workerId,
|
|
11719
|
+
startFrame: rangeStart + first,
|
|
11720
|
+
endFrame: rangeStart + last + 1,
|
|
11721
|
+
frameStride: stride,
|
|
11722
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
11723
|
+
outputFrameOffset: rangeStart
|
|
11724
|
+
}))
|
|
11725
|
+
);
|
|
11726
|
+
}
|
|
11727
|
+
return batches;
|
|
11728
|
+
}
|
|
11687
11729
|
for (let i = 0; i < ranges.length; i += workersPerBatch) {
|
|
11688
11730
|
const batchIndex = batches.length;
|
|
11689
11731
|
const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
|
|
11690
11732
|
workerId,
|
|
11691
11733
|
startFrame: rangeStart + range.startFrame,
|
|
11692
11734
|
endFrame: rangeStart + range.endFrame,
|
|
11693
|
-
outputDir:
|
|
11735
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
11694
11736
|
outputFrameOffset: rangeStart
|
|
11695
11737
|
}));
|
|
11696
11738
|
batches.push(batch);
|
|
@@ -11783,8 +11825,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11783
11825
|
currentWorkers,
|
|
11784
11826
|
attemptWorkDir,
|
|
11785
11827
|
attempt,
|
|
11786
|
-
rangeStart
|
|
11787
|
-
|
|
11828
|
+
rangeStart,
|
|
11829
|
+
options.interleaveFrames ? options.initialWorkerCount : 1
|
|
11830
|
+
) : [
|
|
11831
|
+
(options.interleaveFrames ? distributeFramesInterleaved2 : distributeFrames2)(
|
|
11832
|
+
options.totalFrames,
|
|
11833
|
+
currentWorkers,
|
|
11834
|
+
attemptWorkDir,
|
|
11835
|
+
rangeStart
|
|
11836
|
+
)
|
|
11837
|
+
];
|
|
11788
11838
|
options.dedupPerfs.length = 0;
|
|
11789
11839
|
try {
|
|
11790
11840
|
for (const tasks of batches) {
|
|
@@ -12896,6 +12946,7 @@ async function executeRenderPipeline(input) {
|
|
|
12896
12946
|
height,
|
|
12897
12947
|
fps: job.config.fps,
|
|
12898
12948
|
format: captureImageFormat,
|
|
12949
|
+
transparent: needsAlpha,
|
|
12899
12950
|
quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
|
|
12900
12951
|
variables: job.config.variables,
|
|
12901
12952
|
deviceScaleFactor,
|
|
@@ -13928,7 +13979,8 @@ async function executeRenderPipeline(input) {
|
|
|
13928
13979
|
abortSignal: executionSignal,
|
|
13929
13980
|
assertNotAborted,
|
|
13930
13981
|
onProgress,
|
|
13931
|
-
onFrameLanded: (frame) => overlap?.onFrameLanded(frame)
|
|
13982
|
+
onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
|
|
13983
|
+
interleaveFrames: overlap !== null
|
|
13932
13984
|
})
|
|
13933
13985
|
);
|
|
13934
13986
|
let captureRes;
|
|
@@ -19979,7 +20031,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19979
20031
|
assertNotAborted: () => {
|
|
19980
20032
|
},
|
|
19981
20033
|
frameRange: range,
|
|
19982
|
-
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
|
|
20034
|
+
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
|
|
20035
|
+
interleaveFrames: overlap.current !== null
|
|
19983
20036
|
}).catch(async (captureError) => {
|
|
19984
20037
|
await overlap.current?.abort();
|
|
19985
20038
|
overlap.current = null;
|
package/dist/public-server.js
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
createVideoFrameInjector,
|
|
55
55
|
getEncoderPreset as getEncoderPreset3,
|
|
56
56
|
distributeFrames as distributeFrames2,
|
|
57
|
+
distributeFramesInterleaved as distributeFramesInterleaved2,
|
|
57
58
|
executeParallelCapture as executeParallelCapture2,
|
|
58
59
|
mergeWorkerFrames,
|
|
59
60
|
getSystemTotalMb,
|
|
@@ -5599,6 +5600,7 @@ import {
|
|
|
5599
5600
|
parseVideoElements,
|
|
5600
5601
|
parseImageElements,
|
|
5601
5602
|
parseAudioElements,
|
|
5603
|
+
isSelfOrAncestorHidden,
|
|
5602
5604
|
resolveReferencedStart
|
|
5603
5605
|
} from "@hyperframes/engine";
|
|
5604
5606
|
function resolveHostEnd(host, hostStart) {
|
|
@@ -5659,6 +5661,15 @@ function collectHostWindows(html) {
|
|
|
5659
5661
|
}
|
|
5660
5662
|
return windows;
|
|
5661
5663
|
}
|
|
5664
|
+
function collectHiddenVideoIds(html) {
|
|
5665
|
+
const { document: document2 } = parseHTML2(html);
|
|
5666
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
5667
|
+
for (const element of document2.querySelectorAll("video")) {
|
|
5668
|
+
const id = element.getAttribute(MEDIA_RENDER_ID_ATTR) || element.getAttribute("id");
|
|
5669
|
+
if (id && isSelfOrAncestorHidden(element)) hidden.add(id);
|
|
5670
|
+
}
|
|
5671
|
+
return hidden;
|
|
5672
|
+
}
|
|
5662
5673
|
function toAbsoluteWindow(start, end, window2) {
|
|
5663
5674
|
const absoluteStart = resolveAbsoluteMediaStartSeconds({
|
|
5664
5675
|
authoredStart: start,
|
|
@@ -5692,8 +5703,13 @@ function collectRenderMedia(html) {
|
|
|
5692
5703
|
const windows = collectHostWindows(html);
|
|
5693
5704
|
const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
|
|
5694
5705
|
const droppedMediaIds = /* @__PURE__ */ new Set();
|
|
5706
|
+
const hiddenIds = collectHiddenVideoIds(html);
|
|
5695
5707
|
const videos = [];
|
|
5696
5708
|
for (const video of parseVideoElements(html)) {
|
|
5709
|
+
if (hiddenIds.has(video.id)) {
|
|
5710
|
+
droppedMediaIds.add(video.id);
|
|
5711
|
+
continue;
|
|
5712
|
+
}
|
|
5697
5713
|
const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
|
|
5698
5714
|
if (!absolute) {
|
|
5699
5715
|
droppedMediaIds.add(video.id);
|
|
@@ -5903,7 +5919,7 @@ async function runAnimatedGifTranscode(request) {
|
|
|
5903
5919
|
failureReason: result.failureReason
|
|
5904
5920
|
});
|
|
5905
5921
|
}
|
|
5906
|
-
if (result.terminationReason === "
|
|
5922
|
+
if (result.terminationReason === "inactivity") {
|
|
5907
5923
|
throw new Error(`Animated GIF transcode timed out after ${timeout}ms`);
|
|
5908
5924
|
}
|
|
5909
5925
|
throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
|
|
@@ -6522,8 +6538,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6522
6538
|
const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
|
|
6523
6539
|
let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
|
|
6524
6540
|
const preResolved = extractResolvedMedia(compiledHtml);
|
|
6541
|
+
const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
|
|
6525
6542
|
const clampResults = await Promise.all(
|
|
6526
|
-
preResolved.filter((el) => !!el.src && !el.loop).map(async (el) => {
|
|
6543
|
+
preResolved.filter((el) => !!el.src && !el.loop && !hiddenVideoIds.has(el.id)).map(async (el) => {
|
|
6527
6544
|
const { duration: maxDuration } = await resolveMediaDuration(
|
|
6528
6545
|
el.src,
|
|
6529
6546
|
el.mediaStart,
|
|
@@ -8194,6 +8211,7 @@ async function runProbeStage(input) {
|
|
|
8194
8211
|
height,
|
|
8195
8212
|
fps: job.config.fps,
|
|
8196
8213
|
format: captureFormat,
|
|
8214
|
+
transparent: needsAlpha,
|
|
8197
8215
|
quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
|
|
8198
8216
|
variables: job.config.variables,
|
|
8199
8217
|
deviceScaleFactor,
|
|
@@ -9787,6 +9805,7 @@ async function runCaptureStage(input) {
|
|
|
9787
9805
|
createBeforeCaptureHook: createRenderVideoFrameInjector,
|
|
9788
9806
|
abortSignal,
|
|
9789
9807
|
frameRangeStart: frameRange?.startFrame,
|
|
9808
|
+
interleaveFrames: input.interleaveFrames,
|
|
9790
9809
|
dedupPerfs,
|
|
9791
9810
|
pageErrorLines: workerPageErrors,
|
|
9792
9811
|
onProgress: (progress) => {
|
|
@@ -11302,9 +11321,6 @@ async function runEncodeStage(input) {
|
|
|
11302
11321
|
return { encodeMs: Date.now() - stage5Start };
|
|
11303
11322
|
}
|
|
11304
11323
|
const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
|
|
11305
|
-
const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
|
|
11306
|
-
const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
|
|
11307
|
-
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
11308
11324
|
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
11309
11325
|
const framePattern = `frame_%06d.${frameExt}`;
|
|
11310
11326
|
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
@@ -11315,7 +11331,7 @@ async function runEncodeStage(input) {
|
|
|
11315
11331
|
encoderOpts,
|
|
11316
11332
|
chunkedEncodeSize,
|
|
11317
11333
|
abortSignal,
|
|
11318
|
-
|
|
11334
|
+
engineCfg,
|
|
11319
11335
|
onFramesEncoded
|
|
11320
11336
|
) : await encodeFramesFromDir(
|
|
11321
11337
|
framesDir,
|
|
@@ -11323,7 +11339,7 @@ async function runEncodeStage(input) {
|
|
|
11323
11339
|
videoOnlyPath,
|
|
11324
11340
|
encoderOpts,
|
|
11325
11341
|
abortSignal,
|
|
11326
|
-
|
|
11342
|
+
engineCfg,
|
|
11327
11343
|
onFramesEncoded
|
|
11328
11344
|
);
|
|
11329
11345
|
assertNotAborted();
|
|
@@ -11665,16 +11681,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11665
11681
|
}
|
|
11666
11682
|
return ranges;
|
|
11667
11683
|
}
|
|
11668
|
-
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
|
|
11684
|
+
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
11669
11685
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
11670
11686
|
const batches = [];
|
|
11687
|
+
const outputDirFor = (batchIndex, workerId) => join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
|
|
11688
|
+
if (stride > 1) {
|
|
11689
|
+
const spans = /* @__PURE__ */ new Map();
|
|
11690
|
+
for (const range of ranges) {
|
|
11691
|
+
for (let frame = range.startFrame; frame < range.endFrame; frame++) {
|
|
11692
|
+
const span = spans.get(frame % stride);
|
|
11693
|
+
if (span) span.last = frame;
|
|
11694
|
+
else spans.set(frame % stride, { first: frame, last: frame });
|
|
11695
|
+
}
|
|
11696
|
+
}
|
|
11697
|
+
const strided = [...spans.values()];
|
|
11698
|
+
for (let i = 0; i < strided.length; i += workersPerBatch) {
|
|
11699
|
+
const batchIndex = batches.length;
|
|
11700
|
+
batches.push(
|
|
11701
|
+
strided.slice(i, i + workersPerBatch).map(({ first, last }, workerId) => ({
|
|
11702
|
+
workerId,
|
|
11703
|
+
startFrame: rangeStart + first,
|
|
11704
|
+
endFrame: rangeStart + last + 1,
|
|
11705
|
+
frameStride: stride,
|
|
11706
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
11707
|
+
outputFrameOffset: rangeStart
|
|
11708
|
+
}))
|
|
11709
|
+
);
|
|
11710
|
+
}
|
|
11711
|
+
return batches;
|
|
11712
|
+
}
|
|
11671
11713
|
for (let i = 0; i < ranges.length; i += workersPerBatch) {
|
|
11672
11714
|
const batchIndex = batches.length;
|
|
11673
11715
|
const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
|
|
11674
11716
|
workerId,
|
|
11675
11717
|
startFrame: rangeStart + range.startFrame,
|
|
11676
11718
|
endFrame: rangeStart + range.endFrame,
|
|
11677
|
-
outputDir:
|
|
11719
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
11678
11720
|
outputFrameOffset: rangeStart
|
|
11679
11721
|
}));
|
|
11680
11722
|
batches.push(batch);
|
|
@@ -11767,8 +11809,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11767
11809
|
currentWorkers,
|
|
11768
11810
|
attemptWorkDir,
|
|
11769
11811
|
attempt,
|
|
11770
|
-
rangeStart
|
|
11771
|
-
|
|
11812
|
+
rangeStart,
|
|
11813
|
+
options.interleaveFrames ? options.initialWorkerCount : 1
|
|
11814
|
+
) : [
|
|
11815
|
+
(options.interleaveFrames ? distributeFramesInterleaved2 : distributeFrames2)(
|
|
11816
|
+
options.totalFrames,
|
|
11817
|
+
currentWorkers,
|
|
11818
|
+
attemptWorkDir,
|
|
11819
|
+
rangeStart
|
|
11820
|
+
)
|
|
11821
|
+
];
|
|
11772
11822
|
options.dedupPerfs.length = 0;
|
|
11773
11823
|
try {
|
|
11774
11824
|
for (const tasks of batches) {
|
|
@@ -12880,6 +12930,7 @@ async function executeRenderPipeline(input) {
|
|
|
12880
12930
|
height,
|
|
12881
12931
|
fps: job.config.fps,
|
|
12882
12932
|
format: captureImageFormat,
|
|
12933
|
+
transparent: needsAlpha,
|
|
12883
12934
|
quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
|
|
12884
12935
|
variables: job.config.variables,
|
|
12885
12936
|
deviceScaleFactor,
|
|
@@ -13912,7 +13963,8 @@ async function executeRenderPipeline(input) {
|
|
|
13912
13963
|
abortSignal: executionSignal,
|
|
13913
13964
|
assertNotAborted,
|
|
13914
13965
|
onProgress,
|
|
13915
|
-
onFrameLanded: (frame) => overlap?.onFrameLanded(frame)
|
|
13966
|
+
onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
|
|
13967
|
+
interleaveFrames: overlap !== null
|
|
13916
13968
|
})
|
|
13917
13969
|
);
|
|
13918
13970
|
let captureRes;
|
|
@@ -105,6 +105,54 @@ type CliOptions = {
|
|
|
105
105
|
*/
|
|
106
106
|
mode: HarnessMode;
|
|
107
107
|
};
|
|
108
|
+
type TestResult = {
|
|
109
|
+
suite: TestSuite;
|
|
110
|
+
passed: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Set when `--mode=distributed-simulated` skips a fixture that the
|
|
113
|
+
* distributed pipeline can't run (HDR, NTSC fps, fps outside DISTRIBUTED_FPS).
|
|
114
|
+
* `passed` is `true` for skipped fixtures — skipping is a clean outcome,
|
|
115
|
+
* not a failure — but the summary distinguishes them.
|
|
116
|
+
*/
|
|
117
|
+
skipped?: {
|
|
118
|
+
reason: string;
|
|
119
|
+
};
|
|
120
|
+
compilation?: {
|
|
121
|
+
passed: boolean;
|
|
122
|
+
errors: string[];
|
|
123
|
+
warnings: string[];
|
|
124
|
+
};
|
|
125
|
+
visual?: {
|
|
126
|
+
passed: boolean;
|
|
127
|
+
failedFrames: number;
|
|
128
|
+
checkpoints: Array<{
|
|
129
|
+
time: number;
|
|
130
|
+
psnr: number;
|
|
131
|
+
passed: boolean;
|
|
132
|
+
}>;
|
|
133
|
+
};
|
|
134
|
+
audio?: {
|
|
135
|
+
passed: boolean;
|
|
136
|
+
correlation: number;
|
|
137
|
+
lagWindows: number;
|
|
138
|
+
/**
|
|
139
|
+
* Residual Overall RMS (dBFS) of `rendered - snapshot`. Present only
|
|
140
|
+
* when the fixture opts in via `meta.maxAudioResidualRmsDb`.
|
|
141
|
+
* `Number.NEGATIVE_INFINITY` ⇒ perfect cancellation. `NaN` ⇒ residual
|
|
142
|
+
* check could not run (missing ffmpeg, duration mismatch, ...); see
|
|
143
|
+
* `audio.residualError` for the reason.
|
|
144
|
+
*/
|
|
145
|
+
residualRmsDb?: number;
|
|
146
|
+
residualError?: string;
|
|
147
|
+
};
|
|
148
|
+
streamDurationParity?: {
|
|
149
|
+
passed: boolean;
|
|
150
|
+
videoDurationSeconds: number;
|
|
151
|
+
audioDurationSeconds: number;
|
|
152
|
+
driftSeconds: number;
|
|
153
|
+
};
|
|
154
|
+
renderedOutputPath?: string;
|
|
155
|
+
};
|
|
108
156
|
export declare function parseArgs(argv: string[]): CliOptions;
|
|
109
157
|
export declare function discoverTestSuites(testsDir: string, filterNames: string[], excludeTags?: string[]): TestSuite[];
|
|
110
158
|
/** The frame a checkpoint time samples. Shared so selection and lookup agree. */
|
|
@@ -154,4 +202,9 @@ export type StreamDurationParity = {
|
|
|
154
202
|
};
|
|
155
203
|
export declare function checkStreamDurationParity(videoPath: string): Promise<StreamDurationParity | null>;
|
|
156
204
|
export declare function createRegressionTempRoot(suiteId: string, parent?: string): string;
|
|
205
|
+
export declare function runTestSuite(suite: TestSuite, options: {
|
|
206
|
+
update: boolean;
|
|
207
|
+
keepTemp: boolean;
|
|
208
|
+
mode: HarnessMode;
|
|
209
|
+
}): Promise<TestResult>;
|
|
157
210
|
export {};
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* and placed by the composition hosts it sits inside.
|
|
7
7
|
*/
|
|
8
8
|
import { type VideoElement, type ImageElement, type AudioElement } from "@hyperframes/engine";
|
|
9
|
+
/** Ids, as the parsers report them, of videos that are `data-hidden` or sit inside such an element. */
|
|
10
|
+
export declare function collectHiddenVideoIds(html: string): Set<string>;
|
|
9
11
|
export interface RenderMedia {
|
|
10
12
|
videos: VideoElement[];
|
|
11
13
|
audios: AudioElement[];
|
|
@@ -29,51 +29,40 @@ export interface RenderConfig {
|
|
|
29
29
|
/** Segmented capture: keep renders/.hf-segments/<planHash> after a successful render. */
|
|
30
30
|
keepSegments?: boolean;
|
|
31
31
|
/**
|
|
32
|
-
* Output container format. Defaults to `"mp4"`;
|
|
33
|
-
* unaffected unless this field is set explicitly.
|
|
32
|
+
* Output container format. Defaults to `"mp4"`; set explicitly for other formats.
|
|
34
33
|
*
|
|
35
|
-
* - `"mp4"`: H.264
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* - `"
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* - `"
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* segments: `master.m3u8`, `video.m3u8` + `video_%05d.ts`, and
|
|
62
|
-
* `audio.m3u8` + `audio_%05d.ts` when the composition has audio.
|
|
63
|
-
* Segments are fixed-length (see {@link RenderConfig.hlsSegmentSeconds})
|
|
64
|
-
* and every one starts on an IDR frame, because the encoder's GOP is
|
|
65
|
-
* locked to `hlsSegmentSeconds × fps`. Like `"png-sequence"`,
|
|
66
|
-
* `outputPath` is treated as a directory. SDR only, software encoder
|
|
67
|
-
* only, and not available in distributed / Lambda / Cloud Run mode.
|
|
34
|
+
* - `"mp4"`: opaque H.264, or H.265 + HDR10 with HDR auto-detection or
|
|
35
|
+
* `hdrMode: "force-hdr"`. For streaming/social delivery; faststart puts the
|
|
36
|
+
* `moov` atom first so playback can begin during a partial download.
|
|
37
|
+
* - `"webm"`: VP9 + `yuva420p` with true alpha (no chroma key) and Opus audio.
|
|
38
|
+
* For `<video>` overlays in Chrome, Edge and Firefox; Safari's alpha-WebM
|
|
39
|
+
* support is incomplete.
|
|
40
|
+
* - `"mov"`: ProRes 4444 + `yuva444p10le` with true alpha and AAC audio.
|
|
41
|
+
* Chrome captures 8-bit RGBA; encoding it as 10-bit ProRes does not add
|
|
42
|
+
* capture precision. For editor ingest (Premiere, Final Cut Pro, DaVinci
|
|
43
|
+
* Resolve), rather than direct web playback.
|
|
44
|
+
* - `"gif"`: animated GIF from RGBA captures, using FFmpeg's two-pass
|
|
45
|
+
* `palettegen` + `paletteuse`. For inline autoplay in PRs, READMEs and docs
|
|
46
|
+
* when file size matters less. No audio; transparency is binary, not partial.
|
|
47
|
+
* - `"png-sequence"`: zero-padded RGBA PNGs (`frame_000001.png` ...), with
|
|
48
|
+
* lossless alpha and the largest disk footprint. No muxed audio; an
|
|
49
|
+
* `audio.m4a` sidecar is written when the composition contains audio.
|
|
50
|
+
* For After Effects / Nuke / Fusion or post-processing before encoding.
|
|
51
|
+
* `outputPath` is a directory, created if it does not exist.
|
|
52
|
+
* - `"hls"`: HLS VOD using the opaque H.264 + AAC encode from `"mp4"`,
|
|
53
|
+
* stream-copied without re-encoding into MPEG-TS in the `outputPath` directory:
|
|
54
|
+
* `master.m3u8`, `video.m3u8` + `video_%05d.ts`, and `audio.m3u8` +
|
|
55
|
+
* `audio_%05d.ts` when the composition contains audio. Segment length is
|
|
56
|
+
* {@link RenderConfig.hlsSegmentSeconds}; the encoder's GOP is locked to
|
|
57
|
+
* that length in frames, so each segment starts on an IDR frame.
|
|
58
|
+
* SDR and software encoders only; unavailable in distributed rendering,
|
|
59
|
+
* including Lambda and Cloud Run.
|
|
68
60
|
*
|
|
69
|
-
* Alpha
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* the engine's `initTransparentBackground` helper, so authors should
|
|
75
|
-
* not paint a fullscreen `body` / `#root` background in their
|
|
76
|
-
* compositions when targeting alpha output.
|
|
61
|
+
* Alpha formats (`"webm"`, `"mov"`, `"png-sequence"`, `"gif"`) force screenshot
|
|
62
|
+
* capture because BeginFrame does not preserve alpha on Linux headless-shell.
|
|
63
|
+
* HDR + alpha is unsupported: a warning is logged and output falls back to SDR.
|
|
64
|
+
* The engine's `initTransparentBackground` injects transparent-background CSS;
|
|
65
|
+
* avoid painting a fullscreen `body` / `#root` background for alpha output.
|
|
77
66
|
*/
|
|
78
67
|
format?: RenderOutputFormat;
|
|
79
68
|
/** GIF Netscape loop count. 0 means infinite looping. Only used with `format: "gif"`. */
|
|
@@ -481,7 +470,7 @@ interface ExtractedVideoReadinessInput {
|
|
|
481
470
|
}
|
|
482
471
|
export declare function collectVideoMetadataHints(extractedVideos: readonly ExtractedVideoReadinessInput[]): CaptureVideoMetadataHint[];
|
|
483
472
|
export declare function findMissingFrameRanges(totalFrames: number, framesDir: string, frameExt: "jpg" | "png"): FrameRange[];
|
|
484
|
-
export declare function buildMissingFrameRetryBatches(ranges: FrameRange[], maxWorkers: number, workDir: string, attempt: number, rangeStart?: number): WorkerTask[][];
|
|
473
|
+
export declare function buildMissingFrameRetryBatches(ranges: FrameRange[], maxWorkers: number, workDir: string, attempt: number, rangeStart?: number, stride?: number): WorkerTask[][];
|
|
485
474
|
/**
|
|
486
475
|
* The capture mode this render will REPORT, pre-capture.
|
|
487
476
|
*
|
|
@@ -596,6 +585,8 @@ export declare function executeDiskCaptureWithAdaptiveRetry(options: {
|
|
|
596
585
|
createBeforeCaptureHook: () => BeforeCaptureHook | null;
|
|
597
586
|
abortSignal?: AbortSignal;
|
|
598
587
|
onProgress?: (progress: ParallelProgress) => void;
|
|
588
|
+
/** Workers take frames round-robin so an in-order consumer (the overlapped encoder) is never starved. */
|
|
589
|
+
interleaveFrames?: boolean;
|
|
599
590
|
cfg: EngineConfig;
|
|
600
591
|
log: ProducerLogger;
|
|
601
592
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.202",
|
|
4
4
|
"description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"postcss": "^8.4.0",
|
|
36
36
|
"puppeteer": "^25.2.1",
|
|
37
37
|
"puppeteer-core": "^25.2.1",
|
|
38
|
-
"@hyperframes/core": "0.8.
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/lint": "0.8.
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/parsers": "0.8.
|
|
38
|
+
"@hyperframes/core": "0.8.202",
|
|
39
|
+
"@hyperframes/engine": "0.8.202",
|
|
40
|
+
"@hyperframes/lint": "0.8.202",
|
|
41
|
+
"@hyperframes/studio-server": "0.8.202",
|
|
42
|
+
"@hyperframes/parsers": "0.8.202"
|
|
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.202"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|