@hyperframes/engine 0.9.15 → 0.9.16
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
CHANGED
|
@@ -72,7 +72,7 @@ export type { PageMotionBlurOff } from "./types.js";
|
|
|
72
72
|
export { assertSwiftShader, readWebGlVendorInfo, SwiftShaderAssertionError, BROWSER_GPU_NOT_SOFTWARE, } from "./utils/assertSwiftShader.js";
|
|
73
73
|
export { isBrowserSafePort, listenOnBrowserSafePort, settleOnBrowserSafePort, } from "./utils/browserSafePort.js";
|
|
74
74
|
export { readWebGlVendorInfoFromCanvas } from "./utils/readWebGlVendorInfoFromCanvas.js";
|
|
75
|
-
export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, runFfprobe, analyzeKeyframeIntervals, type VideoMetadata, type AudioMetadata, type MediaProbeProfile, type KeyframeAnalysis, } from "./utils/ffprobe.js";
|
|
75
|
+
export { extractMediaMetadata, displaySize, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, runFfprobe, analyzeKeyframeIntervals, type VideoMetadata, type AudioMetadata, type MediaProbeProfile, type KeyframeAnalysis, } from "./utils/ffprobe.js";
|
|
76
76
|
export { NOT_MEDIA_PAYLOAD, NotMediaPayloadError, assertMediaPayload, fingerprintElementId, isNotMediaPayload, } from "./utils/notMediaPayload.js";
|
|
77
77
|
export { _setDownloadFetchForTests, assertPublicHttpsUrl, downloadToTemp, fetchPublicHttpsText, isHttpUrl, safeDownloadUrlIdentity, writeUrlDownloadTelemetry, type SafeDownloadUrlIdentity, type UrlDownloadOptions, type UrlDownloadTelemetry, type PublicHttpsTextOptions, } from "./utils/urlDownloader.js";
|
|
78
78
|
export { isBlockedNetworkHost } from "./utils/networkPolicy.js";
|
package/dist/index.js
CHANGED
|
@@ -76,7 +76,7 @@ export { frameSequencePath } from "./services/extractedFrameIndex.js";
|
|
|
76
76
|
export { assertSwiftShader, readWebGlVendorInfo, SwiftShaderAssertionError, BROWSER_GPU_NOT_SOFTWARE, } from "./utils/assertSwiftShader.js";
|
|
77
77
|
export { isBrowserSafePort, listenOnBrowserSafePort, settleOnBrowserSafePort, } from "./utils/browserSafePort.js";
|
|
78
78
|
export { readWebGlVendorInfoFromCanvas } from "./utils/readWebGlVendorInfoFromCanvas.js";
|
|
79
|
-
export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, runFfprobe, analyzeKeyframeIntervals, } from "./utils/ffprobe.js";
|
|
79
|
+
export { extractMediaMetadata, displaySize, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, runFfprobe, analyzeKeyframeIntervals, } from "./utils/ffprobe.js";
|
|
80
80
|
export { NOT_MEDIA_PAYLOAD, NotMediaPayloadError, assertMediaPayload, fingerprintElementId, isNotMediaPayload, } from "./utils/notMediaPayload.js";
|
|
81
81
|
export { _setDownloadFetchForTests, assertPublicHttpsUrl, downloadToTemp, fetchPublicHttpsText, isHttpUrl, safeDownloadUrlIdentity, writeUrlDownloadTelemetry, } from "./utils/urlDownloader.js";
|
|
82
82
|
export { isBlockedNetworkHost } from "./utils/networkPolicy.js";
|
|
@@ -27,6 +27,7 @@ const TRANSIENT_BROWSER_ERROR_PATTERNS = [
|
|
|
27
27
|
/ECONNREFUSED/i,
|
|
28
28
|
/net::ERR_NETWORK_CHANGED/i,
|
|
29
29
|
/Composition has zero duration[\s\S]*Runtime ready: false/,
|
|
30
|
+
/Composition setup did not finish/,
|
|
30
31
|
// CDP can refuse a capture call outright with this exact wording; timed-out
|
|
31
32
|
// variants of the same call hit PROTOCOL_TIMEOUT_PATTERNS, checked first.
|
|
32
33
|
// Anchored to this literal reason (not just the CDP method) so an unrelated,
|
|
@@ -427,7 +427,7 @@ export interface HfDiagnostic {
|
|
|
427
427
|
pendingBuildReadyKeys: string[];
|
|
428
428
|
rejectedBuildReadyKeys: string[];
|
|
429
429
|
}
|
|
430
|
-
export declare function buildZeroDurationDiagnostic(diag: HfDiagnostic): string;
|
|
430
|
+
export declare function buildZeroDurationDiagnostic(diag: HfDiagnostic, timeoutMs?: number): string;
|
|
431
431
|
export declare function evaluateHfDiagnostic(page: Page): Promise<HfDiagnostic>;
|
|
432
432
|
export declare function pollHfReady(page: Page, timeoutMs: number, intervalMs?: number): Promise<void>;
|
|
433
433
|
export declare function waitForSubCompositionTimelines(session: CaptureSession, page: Page, timeoutMs: number, options?: {
|
|
@@ -1217,12 +1217,16 @@ export const HF_READY_DIAGNOSTIC_EXPR = `(async function() {
|
|
|
1217
1217
|
};
|
|
1218
1218
|
})()`;
|
|
1219
1219
|
// fallow-ignore-next-line complexity
|
|
1220
|
-
export function buildZeroDurationDiagnostic(diag) {
|
|
1220
|
+
export function buildZeroDurationDiagnostic(diag, timeoutMs) {
|
|
1221
1221
|
const hints = [];
|
|
1222
1222
|
if (diag.pendingBuildReadyKeys.length > 0) {
|
|
1223
1223
|
hints.push(`window.__hf.buildReady never resolved for: ${diag.pendingBuildReadyKeys.join(", ")}. ` +
|
|
1224
1224
|
"The runtime holds render-ready until every registered buildReady promise settles. " +
|
|
1225
|
-
"Find where the composition registers that key and confirm it actually resolves."
|
|
1225
|
+
"Find where the composition registers that key and confirm it actually resolves. " +
|
|
1226
|
+
"During a render, Date and performance.now stay at 0 and requestAnimationFrame " +
|
|
1227
|
+
"callbacks wait until the first frame, so setup that awaits an animation frame or " +
|
|
1228
|
+
"the clock never finishes. Await the library's own readiness promise instead (for " +
|
|
1229
|
+
"Three.js, renderer.compileAsync).");
|
|
1226
1230
|
}
|
|
1227
1231
|
if (diag.rejectedBuildReadyKeys.length > 0) {
|
|
1228
1232
|
hints.push(`window.__hf.buildReady rejected for: ${diag.rejectedBuildReadyKeys.join(", ")}. ` +
|
|
@@ -1231,7 +1235,7 @@ export function buildZeroDurationDiagnostic(diag) {
|
|
|
1231
1235
|
if (!diag.hasPlayer) {
|
|
1232
1236
|
hints.push("window.__player was never set: the HyperFrames runtime did not initialize.");
|
|
1233
1237
|
}
|
|
1234
|
-
if (!diag.hasTimeline) {
|
|
1238
|
+
if (diag.renderReady && !diag.hasTimeline) {
|
|
1235
1239
|
hints.push("No GSAP timeline registered (window.__timelines is empty). " +
|
|
1236
1240
|
"CSS/WAAPI/Lottie animations are usually auto-detected (the runtime infers " +
|
|
1237
1241
|
"duration from the longest running animation), but this composition's duration " +
|
|
@@ -1245,7 +1249,11 @@ export function buildZeroDurationDiagnostic(diag) {
|
|
|
1245
1249
|
if (diag.hasSeek && diag.duration === 0 && diag.renderReady) {
|
|
1246
1250
|
hints.push("The runtime finished initializing but reported zero duration. This is permanent.");
|
|
1247
1251
|
}
|
|
1248
|
-
|
|
1252
|
+
const within = timeoutMs === undefined ? "" : ` within ${timeoutMs}ms`;
|
|
1253
|
+
const headline = diag.renderReady
|
|
1254
|
+
? "Composition has zero duration."
|
|
1255
|
+
: `Composition setup did not finish${within}.`;
|
|
1256
|
+
return (`[FrameCapture] ${headline}\n` +
|
|
1249
1257
|
` Runtime ready: ${diag.renderReady}, __player: ${diag.hasPlayer}, ` +
|
|
1250
1258
|
`__hf.seek: ${diag.hasSeek}, GSAP timeline: ${diag.hasTimeline}, ` +
|
|
1251
1259
|
`data-duration: ${diag.declaredDuration > 0 ? diag.declaredDuration + "s" : "not set"}\n` +
|
|
@@ -1301,7 +1309,7 @@ export async function pollHfReady(page, timeoutMs, intervalMs = 100) {
|
|
|
1301
1309
|
}
|
|
1302
1310
|
const diag = await evaluateHfDiagnostic(page);
|
|
1303
1311
|
if (diag.hasSeek && diag.duration === 0) {
|
|
1304
|
-
throw new Error(buildZeroDurationDiagnostic(diag));
|
|
1312
|
+
throw new Error(buildZeroDurationDiagnostic(diag, timeoutMs));
|
|
1305
1313
|
}
|
|
1306
1314
|
throw new Error(`[FrameCapture] window.__hf not ready after ${timeoutMs}ms. ` +
|
|
1307
1315
|
`Page must expose window.__hf = { duration, seek }.\n` +
|
package/dist/utils/ffprobe.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export interface VideoMetadata {
|
|
|
43
43
|
colorRange?: string;
|
|
44
44
|
pixelFormat?: string;
|
|
45
45
|
sampleAspectRatio?: number;
|
|
46
|
+
rotation?: number;
|
|
46
47
|
/** Decoded frame count from the video stream's `nb_frames`. Omitted when the
|
|
47
48
|
* container does not surface a reliable count (still images, malformed
|
|
48
49
|
* streams, or muxes that require `-count_packets` to populate). Callers
|
|
@@ -90,6 +91,9 @@ interface FFProbeStream {
|
|
|
90
91
|
color_primaries?: string;
|
|
91
92
|
color_space?: string;
|
|
92
93
|
tags?: Record<string, string>;
|
|
94
|
+
side_data_list?: {
|
|
95
|
+
rotation?: number;
|
|
96
|
+
}[];
|
|
93
97
|
disposition?: {
|
|
94
98
|
attached_pic?: number;
|
|
95
99
|
};
|
|
@@ -172,6 +176,10 @@ export declare function readTagCI(tags: Record<string, string | undefined> | und
|
|
|
172
176
|
* smallest representable 2dp rate instead.
|
|
173
177
|
*/
|
|
174
178
|
export declare function parseFrameRate(frameRateStr: string | undefined): number;
|
|
179
|
+
export declare function displaySize(metadata: Pick<VideoMetadata, "width" | "height" | "sampleAspectRatio" | "rotation">): {
|
|
180
|
+
width: number;
|
|
181
|
+
height: number;
|
|
182
|
+
};
|
|
175
183
|
export declare function parseSampleAspectRatio(value: string | undefined): number | undefined;
|
|
176
184
|
/**
|
|
177
185
|
* Whether the first frame decoded with `decoder` (alpha-aware) has an alpha pixel format; undefined on any
|
package/dist/utils/ffprobe.js
CHANGED
|
@@ -637,6 +637,18 @@ export function parseFrameRate(frameRateStr) {
|
|
|
637
637
|
// caller's 30fps default.
|
|
638
638
|
return rounded > 0 ? rounded : 0.01;
|
|
639
639
|
}
|
|
640
|
+
function displayRotation(stream) {
|
|
641
|
+
const turn = Number(stream.side_data_list?.find((data) => data.rotation !== undefined)?.rotation ??
|
|
642
|
+
stream.tags?.rotate ??
|
|
643
|
+
0);
|
|
644
|
+
return Number.isFinite(turn) && turn % 360 !== 0 ? { rotation: turn } : {};
|
|
645
|
+
}
|
|
646
|
+
export function displaySize(metadata) {
|
|
647
|
+
const width = Math.round(metadata.width * (metadata.sampleAspectRatio ?? 1));
|
|
648
|
+
return Math.abs(metadata.rotation ?? 0) % 180 === 90
|
|
649
|
+
? { width: metadata.height, height: width }
|
|
650
|
+
: { width, height: metadata.height };
|
|
651
|
+
}
|
|
640
652
|
export function parseSampleAspectRatio(value) {
|
|
641
653
|
const [num, den] = (value ?? "").split(":").map(Number);
|
|
642
654
|
if (!num || !den || !Number.isFinite(num) || !Number.isFinite(den))
|
|
@@ -777,8 +789,12 @@ export async function extractMediaMetadata(filePath) {
|
|
|
777
789
|
const alphaPlaneDecoded = alphaDecoder
|
|
778
790
|
? await probeDecodedAlphaPlane(filePath, alphaDecoder)
|
|
779
791
|
: undefined;
|
|
780
|
-
|
|
792
|
+
let containerDuration = output?.format.duration ? parseFloat(output.format.duration) : 0;
|
|
781
793
|
const streamDuration = videoStream.duration ? parseFloat(videoStream.duration) : 0;
|
|
794
|
+
// Recorder output (WebM/Matroska written to a pipe) carries no duration header: read it off the packets.
|
|
795
|
+
if (!(containerDuration > 0) && !(streamDuration > 0) && !isPng) {
|
|
796
|
+
containerDuration = (await scanPacketDuration(filePath, fps)) ?? 0;
|
|
797
|
+
}
|
|
782
798
|
const parsedStreamStart = videoStream.start_time ? parseFloat(videoStream.start_time) : 0;
|
|
783
799
|
const streamStart = Number.isFinite(parsedStreamStart) ? parsedStreamStart : 0;
|
|
784
800
|
// `nb_frames` is populated by the container demuxer; a muxer that requires
|
|
@@ -825,6 +841,7 @@ export async function extractMediaMetadata(filePath) {
|
|
|
825
841
|
colorRange: videoStream.color_range,
|
|
826
842
|
pixelFormat,
|
|
827
843
|
...(sampleAspectRatio === undefined ? {} : { sampleAspectRatio }),
|
|
844
|
+
...displayRotation(videoStream),
|
|
828
845
|
frames,
|
|
829
846
|
};
|
|
830
847
|
})();
|
|
@@ -857,6 +874,30 @@ export function readStoredFinalFrame(filePath, metadata) {
|
|
|
857
874
|
: undefined;
|
|
858
875
|
});
|
|
859
876
|
}
|
|
877
|
+
const PACKET_TAIL_CHARS = 64 * 1024;
|
|
878
|
+
/** The video stream's length from its packets (last end time), for a file whose headers state none. */
|
|
879
|
+
async function scanPacketDuration(filePath, fps) {
|
|
880
|
+
try {
|
|
881
|
+
const stdout = await runFfprobe(filePath, [
|
|
882
|
+
...["-select_streams", "v:0", "-show_entries", "packet=pts_time,duration_time"],
|
|
883
|
+
...["-of", "csv=p=0"],
|
|
884
|
+
], undefined, { retainTail: true, maxChars: PACKET_TAIL_CHARS });
|
|
885
|
+
const lines = stdout.split("\n");
|
|
886
|
+
// A tail cut at the cap starts partway through a line, and its number would read as a huge time.
|
|
887
|
+
if (stdout.length >= PACKET_TAIL_CHARS)
|
|
888
|
+
lines.shift();
|
|
889
|
+
const frame = fps > 0 ? 1 / fps : 0;
|
|
890
|
+
const ends = lines
|
|
891
|
+
.map((line) => line.trim().split(",").map(Number))
|
|
892
|
+
.filter(([pts]) => Number.isFinite(pts))
|
|
893
|
+
.map(([pts, duration]) => pts + (Number.isFinite(duration) ? duration : frame));
|
|
894
|
+
return ends.length ? Math.max(...ends) : undefined;
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
// expected: a file ffprobe cannot scan keeps its "no duration" verdict.
|
|
898
|
+
return undefined;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
860
901
|
/**
|
|
861
902
|
* `duration_time` of the stream's last packet (by presentation time), read from the same final
|
|
862
903
|
* second the final-frame probe reads; undefined when ffprobe reports none.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.16",
|
|
4
4
|
"description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"puppeteer": "~25.11.0",
|
|
62
62
|
"puppeteer-core": "^25.11.0",
|
|
63
63
|
"undici": "7.25.0",
|
|
64
|
-
"@hyperframes/core": "^0.9.
|
|
65
|
-
"@hyperframes/parsers": "^0.9.
|
|
64
|
+
"@hyperframes/core": "^0.9.16",
|
|
65
|
+
"@hyperframes/parsers": "^0.9.16"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^25.0.10",
|