@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/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)}`);
|
|
@@ -2463,8 +2479,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
2463
2479
|
const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
|
|
2464
2480
|
let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
|
|
2465
2481
|
const preResolved = extractResolvedMedia(compiledHtml);
|
|
2482
|
+
const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
|
|
2466
2483
|
const clampResults = await Promise.all(
|
|
2467
|
-
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) => {
|
|
2468
2485
|
const { duration: maxDuration } = await resolveMediaDuration(
|
|
2469
2486
|
el.src,
|
|
2470
2487
|
el.mediaStart,
|
|
@@ -4135,6 +4152,7 @@ async function runProbeStage(input) {
|
|
|
4135
4152
|
height,
|
|
4136
4153
|
fps: job.config.fps,
|
|
4137
4154
|
format: captureFormat,
|
|
4155
|
+
transparent: needsAlpha,
|
|
4138
4156
|
quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
|
|
4139
4157
|
variables: job.config.variables,
|
|
4140
4158
|
deviceScaleFactor,
|
|
@@ -5772,6 +5790,7 @@ async function runCaptureStage(input) {
|
|
|
5772
5790
|
createBeforeCaptureHook: createRenderVideoFrameInjector,
|
|
5773
5791
|
abortSignal,
|
|
5774
5792
|
frameRangeStart: frameRange?.startFrame,
|
|
5793
|
+
interleaveFrames: input.interleaveFrames,
|
|
5775
5794
|
dedupPerfs,
|
|
5776
5795
|
pageErrorLines: workerPageErrors,
|
|
5777
5796
|
onProgress: (progress) => {
|
|
@@ -6349,9 +6368,6 @@ async function runEncodeStage(input) {
|
|
|
6349
6368
|
return { encodeMs: Date.now() - stage5Start };
|
|
6350
6369
|
}
|
|
6351
6370
|
const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
|
|
6352
|
-
const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
|
|
6353
|
-
const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
|
|
6354
|
-
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
6355
6371
|
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
6356
6372
|
const framePattern = `frame_%06d.${frameExt}`;
|
|
6357
6373
|
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
@@ -6362,7 +6378,7 @@ async function runEncodeStage(input) {
|
|
|
6362
6378
|
encoderOpts,
|
|
6363
6379
|
chunkedEncodeSize,
|
|
6364
6380
|
abortSignal,
|
|
6365
|
-
|
|
6381
|
+
engineCfg,
|
|
6366
6382
|
onFramesEncoded
|
|
6367
6383
|
) : await encodeFramesFromDir(
|
|
6368
6384
|
framesDir,
|
|
@@ -6370,7 +6386,7 @@ async function runEncodeStage(input) {
|
|
|
6370
6386
|
videoOnlyPath,
|
|
6371
6387
|
encoderOpts,
|
|
6372
6388
|
abortSignal,
|
|
6373
|
-
|
|
6389
|
+
engineCfg,
|
|
6374
6390
|
onFramesEncoded
|
|
6375
6391
|
);
|
|
6376
6392
|
assertNotAborted();
|
|
@@ -6498,16 +6514,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
6498
6514
|
}
|
|
6499
6515
|
return ranges;
|
|
6500
6516
|
}
|
|
6501
|
-
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
|
|
6517
|
+
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
6502
6518
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
6503
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
|
+
}
|
|
6504
6546
|
for (let i = 0; i < ranges.length; i += workersPerBatch) {
|
|
6505
6547
|
const batchIndex = batches.length;
|
|
6506
6548
|
const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
|
|
6507
6549
|
workerId,
|
|
6508
6550
|
startFrame: rangeStart + range.startFrame,
|
|
6509
6551
|
endFrame: rangeStart + range.endFrame,
|
|
6510
|
-
outputDir:
|
|
6552
|
+
outputDir: outputDirFor(batchIndex, workerId),
|
|
6511
6553
|
outputFrameOffset: rangeStart
|
|
6512
6554
|
}));
|
|
6513
6555
|
batches.push(batch);
|
|
@@ -6565,8 +6607,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
6565
6607
|
currentWorkers,
|
|
6566
6608
|
attemptWorkDir,
|
|
6567
6609
|
attempt,
|
|
6568
|
-
rangeStart
|
|
6569
|
-
|
|
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
|
+
];
|
|
6570
6620
|
options.dedupPerfs.length = 0;
|
|
6571
6621
|
try {
|
|
6572
6622
|
for (const tasks of batches) {
|
|
@@ -11058,7 +11108,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11058
11108
|
assertNotAborted: () => {
|
|
11059
11109
|
},
|
|
11060
11110
|
frameRange: range,
|
|
11061
|
-
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
|
|
11111
|
+
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
|
|
11112
|
+
interleaveFrames: overlap.current !== null
|
|
11062
11113
|
}).catch(async (captureError) => {
|
|
11063
11114
|
await overlap.current?.abort();
|
|
11064
11115
|
overlap.current = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.1.0",
|
|
3
3
|
"buildId": "dev",
|
|
4
|
-
"sha256": "
|
|
4
|
+
"sha256": "58749315991d8ab9f1dc5f8d6968335e10241f89793d85fc83f8aada8dba5a19",
|
|
5
5
|
"artifacts": {
|
|
6
6
|
"iife": "hyperframe.runtime.iife.js",
|
|
7
7
|
"esm": "hyperframe.runtime.mjs"
|