@hyperframes/engine 0.8.183 → 0.8.185
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/services/browserManager.js +1 -5
- package/dist/services/frameCapture.js +2 -2
- package/dist/services/parallelCoordinator.d.ts +7 -0
- package/dist/services/parallelCoordinator.js +6 -3
- package/dist/services/videoFrameExtractor.js +3 -7
- package/dist/utils/ffprobe.d.ts +0 -14
- package/dist/utils/ffprobe.js +2 -26
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export { MIXED_AUDIO_FILENAME, parseAudioElements, parseMasterFxChain, processCo
|
|
|
53
53
|
export { audioFailureDiagnostics } from "./services/audioFailureDiagnostic.js";
|
|
54
54
|
export { cloneCaptureWarning, cloneCaptureWarnings } from "./services/captureWarning.js";
|
|
55
55
|
export type { AudioElement, AudioFailureReason, AudioFailureStage, AudioProcessingFailure, AudioFailureDiagnostic, AudioSubprocessEvidence, AudioTrack, AudioVolumeKeyframe, MixResult, } from "./services/audioMixer.types.js";
|
|
56
|
-
export { calculateOptimalWorkers, computeWorkerSizing, selectVerifySampleIndicesForTask, verifyDiskDrawElementSamples, distributeFrames, distributeFramesInterleaved, executeParallelCapture, mergeWorkerFrames, getSystemResources, shouldDisableBrowserPoolForParallelWorker, type WorkerTask, type WorkerResult, type WorkerSizing, type WorkerSizingBound, type ParallelProgress, } from "./services/parallelCoordinator.js";
|
|
56
|
+
export { calculateOptimalWorkers, computeWorkerSizing, selectVerifySampleIndicesForTask, verifyDiskDrawElementSamples, distributeFrames, distributeFramesInterleaved, executeParallelCapture, mergeWorkerFrames, getSystemResources, shouldDisableBrowserPoolForParallelWorker, type WorkerTask, type WorkerResult, type WorkerSizing, type WorkerSizingBound, type LandedFrame, type ParallelProgress, } from "./services/parallelCoordinator.js";
|
|
57
57
|
export { createFileServer, type FileServerOptions, type FileServerHandle, } from "./services/fileServer.js";
|
|
58
58
|
export { quantizeTimeToFrame, MEDIA_VISUAL_STYLE_PROPERTIES } from "@hyperframes/core";
|
|
59
59
|
export { frameFileExtension } from "./services/frameCapture.js";
|
|
@@ -364,11 +364,7 @@ async function probeBeginFrameSupport(browser, timeoutMs = BEGINFRAME_PROBE_TIME
|
|
|
364
364
|
};
|
|
365
365
|
}
|
|
366
366
|
if (page && !(await settleWithin(page.close(), BEGINFRAME_PROBE_CLEANUP_TIMEOUT_MS))) {
|
|
367
|
-
return {
|
|
368
|
-
supported: false,
|
|
369
|
-
detail: `${result.detail}; probe page cleanup timed out`,
|
|
370
|
-
durationMs: Date.now() - started,
|
|
371
|
-
};
|
|
367
|
+
return { ...result, detail: `${result.detail}; probe page cleanup timed out` };
|
|
372
368
|
}
|
|
373
369
|
return result;
|
|
374
370
|
}
|
|
@@ -1498,8 +1498,8 @@ export function buildLiveMapWarning(libraries) {
|
|
|
1498
1498
|
message: `Live map viewport(s) detected in the composition (${libraries.join(", ")}). ` +
|
|
1499
1499
|
`Map tiles load over the network at render time, which the deterministic-render ` +
|
|
1500
1500
|
`contract forbids: frames captured before tiles arrive ship a blank or partial map ` +
|
|
1501
|
-
`with no error. Bake the map to a video first (
|
|
1502
|
-
`bake-basemap.mjs) and use the baked file as the imagery layer.`,
|
|
1501
|
+
`with no error. Bake the map to a video first (the hyperframes skill's ` +
|
|
1502
|
+
`scripts/bake-basemap.mjs, see references/film.md) and use the baked file as the imagery layer.`,
|
|
1503
1503
|
details: { sources: [...libraries] },
|
|
1504
1504
|
};
|
|
1505
1505
|
}
|
|
@@ -55,6 +55,13 @@ export interface ParallelProgress {
|
|
|
55
55
|
workerProgress: Map<number, number>;
|
|
56
56
|
/** Latest lifecycle transition; absent on ordinary completed-frame updates. */
|
|
57
57
|
latestWorkerPhase?: ParallelWorkerPhaseDiagnostic;
|
|
58
|
+
/** Disk capture only: the frame file this update finished writing, readable from now on. */
|
|
59
|
+
landedFrame?: LandedFrame;
|
|
60
|
+
}
|
|
61
|
+
/** A captured frame file: `index` is its file index (`frame_<index>`), `path` where it was written. */
|
|
62
|
+
export interface LandedFrame {
|
|
63
|
+
index: number;
|
|
64
|
+
path: string;
|
|
58
65
|
}
|
|
59
66
|
export type ParallelWorkerPhase = "browser_launch" | "browser_probe" | "session_init" | "frame_capture" | "frame_encode";
|
|
60
67
|
export interface ParallelWorkerPhaseDiagnostic {
|
|
@@ -400,16 +400,18 @@ async function captureFrameRange(session, task, captureOptions, signal, onFrameC
|
|
|
400
400
|
throw new Error("Parallel worker cancelled");
|
|
401
401
|
const time = (i * captureOptions.fps.den) / captureOptions.fps.num;
|
|
402
402
|
const fileFrameIdx = i - outputOffset;
|
|
403
|
+
let landedFrame;
|
|
403
404
|
if (onFrameBuffer) {
|
|
404
405
|
const { buffer } = await runCaptureOperation("frame_capture", i, () => captureFrameToBuffer(session, fileFrameIdx, time));
|
|
405
406
|
await onFrameBuffer(i, buffer, session);
|
|
406
407
|
}
|
|
407
408
|
else {
|
|
408
|
-
await runCaptureOperation("frame_capture", i, () => captureFrame(session, fileFrameIdx, time));
|
|
409
|
+
const { path } = await runCaptureOperation("frame_capture", i, () => captureFrame(session, fileFrameIdx, time));
|
|
410
|
+
landedFrame = { index: fileFrameIdx, path };
|
|
409
411
|
}
|
|
410
412
|
framesCaptured++;
|
|
411
413
|
if (onFrameCaptured)
|
|
412
|
-
onFrameCaptured(task.workerId, i);
|
|
414
|
+
onFrameCaptured(task.workerId, i, landedFrame);
|
|
413
415
|
}
|
|
414
416
|
return framesCaptured;
|
|
415
417
|
}
|
|
@@ -722,7 +724,7 @@ export async function executeParallelCapture(serverUrl, workDir, tasks, captureO
|
|
|
722
724
|
const workerPhases = new Map();
|
|
723
725
|
for (const task of tasks)
|
|
724
726
|
workerProgress.set(task.workerId, 0);
|
|
725
|
-
const onFrameCaptured = (workerId, _frameIndex) => {
|
|
727
|
+
const onFrameCaptured = (workerId, _frameIndex, landedFrame) => {
|
|
726
728
|
const current = workerProgress.get(workerId) || 0;
|
|
727
729
|
workerProgress.set(workerId, current + 1);
|
|
728
730
|
if (onProgress) {
|
|
@@ -732,6 +734,7 @@ export async function executeParallelCapture(serverUrl, workDir, tasks, captureO
|
|
|
732
734
|
capturedFrames,
|
|
733
735
|
activeWorkers: tasks.length,
|
|
734
736
|
workerProgress: new Map(workerProgress),
|
|
737
|
+
...(landedFrame && { landedFrame }),
|
|
735
738
|
});
|
|
736
739
|
}
|
|
737
740
|
};
|
|
@@ -13,6 +13,7 @@ import { createConcurrencyLimit } from "../utils/concurrencyLimit.js";
|
|
|
13
13
|
import { parseHTML } from "linkedom";
|
|
14
14
|
import { isPathInside } from "@hyperframes/parsers/asset-paths";
|
|
15
15
|
import { resolveProjectRelativeSrc } from "@hyperframes/parsers/asset-resolution";
|
|
16
|
+
import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
|
|
16
17
|
import { MEDIA_RENDER_ID_ATTR, isAudibleVideoElement, fpsToFfmpegArg, fpsToNumber, MEDIA_DURATION_CLAMP_EPSILON_SECONDS, normalizeRateSpec, readElementRateSpec, shiftRateLane, sourceTimeAt, timeAtSourceTime, exportClipWindow, hasClipStarted, isInClipWindow, parseStrictFiniteTimingNumber, readMediaStart, toFps, firstFrameColourArgs, hdrToSdrToneMapFilter, parseFirstFrameColour, } from "@hyperframes/core";
|
|
17
18
|
import { resolveReferencedStart } from "./referenceResolver.js";
|
|
18
19
|
import { isKnownInactiveTimelineWindow } from "./mediaTimelineWindow.js";
|
|
@@ -376,7 +377,7 @@ export function parseVideoElements(html) {
|
|
|
376
377
|
mediaStart: readMediaStart(el),
|
|
377
378
|
playbackRate: readElementRateSpec(el),
|
|
378
379
|
loop: el.hasAttribute("loop"),
|
|
379
|
-
hasAudio: hasAudioAttr === "true",
|
|
380
|
+
hasAudio: hasAudioAttr === "true" && isAudibleVideoElement(el),
|
|
380
381
|
...(isSelfOrAncestorHidden(el) ||
|
|
381
382
|
(isAudibleVideoElement(el) && isMemberGroupHidden(groupsById, el))
|
|
382
383
|
? { hidden: true }
|
|
@@ -958,12 +959,7 @@ export function codecMayHaveAlpha(codec) {
|
|
|
958
959
|
return ALPHA_CAPABLE_CODECS.has((codec ?? "").toLowerCase());
|
|
959
960
|
}
|
|
960
961
|
export function decoderForCodec(codec) {
|
|
961
|
-
|
|
962
|
-
if (c === "vp9")
|
|
963
|
-
return "libvpx-vp9";
|
|
964
|
-
if (c === "vp8")
|
|
965
|
-
return "libvpx";
|
|
966
|
-
return c;
|
|
962
|
+
return alphaDecoderFor(codec) ?? (codec ?? "").toLowerCase();
|
|
967
963
|
}
|
|
968
964
|
export function resolveFrameFormat(metadata, requested) {
|
|
969
965
|
if (metadata.hasAlpha || codecMayHaveAlpha(metadata.videoCodec))
|
package/dist/utils/ffprobe.d.ts
CHANGED
|
@@ -110,20 +110,6 @@ export declare function probeMediaProfile(filePath: string, options?: {
|
|
|
110
110
|
signal?: AbortSignal;
|
|
111
111
|
}): Promise<MediaProbeProfile>;
|
|
112
112
|
export declare function extractPngMetadataFromBuffer(buf: Buffer): StillImageMetadata | null;
|
|
113
|
-
/**
|
|
114
|
-
* Does this pix_fmt carry an alpha channel?
|
|
115
|
-
*
|
|
116
|
-
* Exported so the test asserts the shipped predicate rather than a copy of
|
|
117
|
-
* the pattern. Anchored at the start, matching studio-server's
|
|
118
|
-
* mediaMetadata.ts: the previous inline pattern bound its `(^|[^a-z])` anchor
|
|
119
|
-
* to the first alternative only — `|` is looser than concatenation — so the
|
|
120
|
-
* guard was decorative for every other name. It also missed abgr, ya8/ya16
|
|
121
|
-
* and ayuv64, and its `gray[a-z0-9]*a` branch matched only gray8a/gray16a,
|
|
122
|
-
* names FFmpeg renamed to ya8/ya16 in 2013. A `ya8` grayscale-plus-alpha PNG
|
|
123
|
-
* reported hasAlpha:false, resolveFrameFormat picked jpg, and the overlay
|
|
124
|
-
* flattened to an opaque rectangle.
|
|
125
|
-
*/
|
|
126
|
-
export declare function pixelFormatHasAlpha(pixelFormat: string): boolean;
|
|
127
113
|
/**
|
|
128
114
|
* Read an ffprobe tag case-insensitively. ffmpeg/libavformat versions disagree
|
|
129
115
|
* on tag casing — VP9 alpha is `alpha_mode` in older builds and `ALPHA_MODE`
|
package/dist/utils/ffprobe.js
CHANGED
|
@@ -5,6 +5,7 @@ import { chunkCrc32 } from "./crc32.js";
|
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
6
6
|
import { basename } from "path";
|
|
7
7
|
import { redactTelemetryString } from "@hyperframes/core";
|
|
8
|
+
import { videoStreamHasAlpha } from "@hyperframes/parsers/media-alpha";
|
|
8
9
|
import { urlInputArgs } from "./urlInput.js";
|
|
9
10
|
import { FFPROBE_PATH_ENV, getFfprobeBinary } from "./ffmpegBinaries.js";
|
|
10
11
|
import { ManagedChildProcess } from "./managedChildProcess.js";
|
|
@@ -437,22 +438,6 @@ export function extractPngMetadataFromBuffer(buf) {
|
|
|
437
438
|
}
|
|
438
439
|
return width > 0 && height > 0 ? { width, height, colorSpace: colorSpaceFromCicp } : null;
|
|
439
440
|
}
|
|
440
|
-
/**
|
|
441
|
-
* Does this pix_fmt carry an alpha channel?
|
|
442
|
-
*
|
|
443
|
-
* Exported so the test asserts the shipped predicate rather than a copy of
|
|
444
|
-
* the pattern. Anchored at the start, matching studio-server's
|
|
445
|
-
* mediaMetadata.ts: the previous inline pattern bound its `(^|[^a-z])` anchor
|
|
446
|
-
* to the first alternative only — `|` is looser than concatenation — so the
|
|
447
|
-
* guard was decorative for every other name. It also missed abgr, ya8/ya16
|
|
448
|
-
* and ayuv64, and its `gray[a-z0-9]*a` branch matched only gray8a/gray16a,
|
|
449
|
-
* names FFmpeg renamed to ya8/ya16 in 2013. A `ya8` grayscale-plus-alpha PNG
|
|
450
|
-
* reported hasAlpha:false, resolveFrameFormat picked jpg, and the overlay
|
|
451
|
-
* flattened to an opaque rectangle.
|
|
452
|
-
*/
|
|
453
|
-
export function pixelFormatHasAlpha(pixelFormat) {
|
|
454
|
-
return /^(?:yuva|rgba|argb|bgra|abgr|gbrap|ya|ayuv)/i.test(pixelFormat);
|
|
455
|
-
}
|
|
456
441
|
function extractStillImageMetadata(filePath) {
|
|
457
442
|
try {
|
|
458
443
|
return extractPngMetadataFromBuffer(readFileSync(filePath));
|
|
@@ -613,16 +598,7 @@ export async function extractMediaMetadata(filePath) {
|
|
|
613
598
|
};
|
|
614
599
|
const colorSpace = merged.colorTransfer || merged.colorPrimaries || merged.colorSpace ? merged : null;
|
|
615
600
|
const pixelFormat = videoStream.pix_fmt || "";
|
|
616
|
-
const
|
|
617
|
-
const hasAlpha = pixelFormatHasAlpha(pixelFormat) || alphaMode === "1";
|
|
618
|
-
// Anchored at the start, matching studio-server's mediaMetadata.ts. The
|
|
619
|
-
// previous pattern bound its `(^|[^a-z])` anchor to the FIRST alternative
|
|
620
|
-
// only — `|` is looser than concatenation — so the guard was decorative
|
|
621
|
-
// for every other name. It also missed abgr, ya8/ya16 and ayuv64, and its
|
|
622
|
-
// `gray[a-z0-9]*a` branch matched only gray8a/gray16a, names FFmpeg
|
|
623
|
-
// renamed to ya8/ya16 in 2013. A `ya8` grayscale-plus-alpha PNG reported
|
|
624
|
-
// hasAlpha:false, resolveFrameFormat picked jpg, and the overlay
|
|
625
|
-
// flattened to an opaque rectangle.
|
|
601
|
+
const hasAlpha = videoStreamHasAlpha(videoStream);
|
|
626
602
|
const containerDuration = output?.format.duration ? parseFloat(output.format.duration) : 0;
|
|
627
603
|
const streamDuration = videoStream.duration ? parseFloat(videoStream.duration) : 0;
|
|
628
604
|
const parsedStreamStart = videoStream.start_time ? parseFloat(videoStream.start_time) : 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.185",
|
|
4
4
|
"description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"linkedom": "^0.18.12",
|
|
51
51
|
"puppeteer": "^25.8.0",
|
|
52
52
|
"puppeteer-core": "^25.8.0",
|
|
53
|
-
"@hyperframes/
|
|
54
|
-
"@hyperframes/
|
|
53
|
+
"@hyperframes/core": "^0.8.185",
|
|
54
|
+
"@hyperframes/parsers": "^0.8.185"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^25.0.10",
|