@hyperframes/engine 0.8.204 → 0.8.206
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 +2 -2
- package/dist/index.js +2 -2
- package/dist/services/browserManager.d.ts +2 -1
- package/dist/services/browserManager.js +3 -3
- package/dist/services/frameCapture.d.ts +4 -1
- package/dist/services/frameCapture.js +104 -12
- package/dist/services/hdrCapture.js +2 -2
- package/dist/services/screenshotService.d.ts +3 -1
- package/dist/services/screenshotService.js +20 -11
- package/dist/services/videoFrameExtractor.d.ts +1 -0
- package/dist/services/videoFrameExtractor.js +11 -5
- package/dist/services/videoFrameInjector.js +8 -3
- package/dist/utils/ffprobe.d.ts +5 -0
- package/dist/utils/ffprobe.js +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFra
|
|
|
46
46
|
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, type GpuEncoder, type PackageHlsOptions, } from "./services/chunkEncoder.js";
|
|
47
47
|
export type { EncoderOptions, EncodeResult, MuxResult } from "./services/chunkEncoder.types.js";
|
|
48
48
|
export { spawnStreamingEncoder, createFrameReorderBuffer, type StreamingEncoder, type StreamingEncoderOptions, type StreamingEncoderResult, type FrameReorderBuffer, } from "./services/streamingEncoder.js";
|
|
49
|
-
export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, type VideoElement, type ImageElement, type ExtractedFrames, type DeferredFrameRange, type FrameRange, type ExtractionOptions, type ExtractionResult, type ExtractionPhaseBreakdown, type TimelineExtractionWindow, type VideoExtractionFailure, type VideoExtractionFailureKind, type VideoExtractionFailureGroupDetails, type VideoExtractionFailureRetry, type VideoExtractionFailureStatusClass, type SafeVideoExtractionSourceIdentity, type VideoFrameFormat, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, } from "./services/videoFrameExtractor.js";
|
|
49
|
+
export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, type VideoElement, type ImageElement, type ExtractedFrames, type DeferredFrameRange, type FrameRange, type ExtractionOptions, type ExtractionResult, type ExtractionPhaseBreakdown, type TimelineExtractionWindow, type VideoExtractionFailure, type VideoExtractionFailureKind, type VideoExtractionFailureGroupDetails, type VideoExtractionFailureRetry, type VideoExtractionFailureStatusClass, type SafeVideoExtractionSourceIdentity, type VideoFrameFormat, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, sdrCanvasColourFilters, } from "./services/videoFrameExtractor.js";
|
|
50
50
|
export { isSelfOrAncestorHidden } from "./services/mediaHidden.js";
|
|
51
51
|
export { directorySizeBytes, gcExtractionCache } from "./services/extractionCache.js";
|
|
52
52
|
export { resolveReferencedStart, resolveReferencedDuration, type RefResolverEl, type RefResolverDoc, } from "./services/referenceResolver.js";
|
|
@@ -65,7 +65,7 @@ export type { MotionBlurOptions, MotionBlurBlendSpace } from "./services/motionB
|
|
|
65
65
|
export type { PageMotionBlurOff } from "./types.js";
|
|
66
66
|
export { assertSwiftShader, readWebGlVendorInfo, SwiftShaderAssertionError, BROWSER_GPU_NOT_SOFTWARE, } from "./utils/assertSwiftShader.js";
|
|
67
67
|
export { readWebGlVendorInfoFromCanvas } from "./utils/readWebGlVendorInfoFromCanvas.js";
|
|
68
|
-
export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, analyzeKeyframeIntervals, type VideoMetadata, type AudioMetadata, type MediaProbeProfile, type KeyframeAnalysis, } from "./utils/ffprobe.js";
|
|
68
|
+
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";
|
|
69
69
|
export { NOT_MEDIA_PAYLOAD, NotMediaPayloadError, assertMediaPayload, fingerprintElementId, isNotMediaPayload, } from "./utils/notMediaPayload.js";
|
|
70
70
|
export { assertPublicHttpsUrl, isBlockedNetworkHost, downloadToTemp, fetchPublicHttpsText, isHttpUrl, safeDownloadUrlIdentity, writeUrlDownloadTelemetry, type SafeDownloadUrlIdentity, type UrlDownloadOptions, type UrlDownloadTelemetry, type PublicHttpsTextOptions, } from "./utils/urlDownloader.js";
|
|
71
71
|
export { runFfmpeg, formatFfmpegError, describeFfmpegFailure, isExternalFfmpegInterruption, type RunFfmpegOptions, type RunFfmpegResult, } from "./utils/runFfmpeg.js";
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFra
|
|
|
50
50
|
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, } from "./services/chunkEncoder.js";
|
|
51
51
|
export { spawnStreamingEncoder, createFrameReorderBuffer, } from "./services/streamingEncoder.js";
|
|
52
52
|
// ── Media processing ───────────────────────────────────────────────────────────
|
|
53
|
-
export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, } from "./services/videoFrameExtractor.js";
|
|
53
|
+
export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, sdrCanvasColourFilters, } from "./services/videoFrameExtractor.js";
|
|
54
54
|
export { isSelfOrAncestorHidden } from "./services/mediaHidden.js";
|
|
55
55
|
export { directorySizeBytes, gcExtractionCache } from "./services/extractionCache.js";
|
|
56
56
|
export { resolveReferencedStart, resolveReferencedDuration, } from "./services/referenceResolver.js";
|
|
@@ -69,7 +69,7 @@ export { appendSoftwareEncoderThreadsArg } from "./services/softwareEncoderThrea
|
|
|
69
69
|
export { frameSequencePath } from "./services/extractedFrameIndex.js";
|
|
70
70
|
export { assertSwiftShader, readWebGlVendorInfo, SwiftShaderAssertionError, BROWSER_GPU_NOT_SOFTWARE, } from "./utils/assertSwiftShader.js";
|
|
71
71
|
export { readWebGlVendorInfoFromCanvas } from "./utils/readWebGlVendorInfoFromCanvas.js";
|
|
72
|
-
export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, analyzeKeyframeIntervals, } from "./utils/ffprobe.js";
|
|
72
|
+
export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, runFfprobe, analyzeKeyframeIntervals, } from "./utils/ffprobe.js";
|
|
73
73
|
export { NOT_MEDIA_PAYLOAD, NotMediaPayloadError, assertMediaPayload, fingerprintElementId, isNotMediaPayload, } from "./utils/notMediaPayload.js";
|
|
74
74
|
export { assertPublicHttpsUrl, isBlockedNetworkHost, downloadToTemp, fetchPublicHttpsText, isHttpUrl, safeDownloadUrlIdentity, writeUrlDownloadTelemetry, } from "./utils/urlDownloader.js";
|
|
75
75
|
export { runFfmpeg, formatFfmpegError, describeFfmpegFailure, isExternalFfmpegInterruption, } from "./utils/runFfmpeg.js";
|
|
@@ -12,8 +12,9 @@ export { BrowserLeasePool } from "./browserLeasePool.js";
|
|
|
12
12
|
export type { BrowserLaunchFingerprint, BrowserLease, BrowserPoolState, CaptureMode, } from "./browserLeasePool.js";
|
|
13
13
|
/** Set while the host cancels renders on Ctrl+C; Puppeteer's own handler would exit before any cleanup ran. */
|
|
14
14
|
export declare function setHostHandlesSigint(owned: boolean): void;
|
|
15
|
-
export declare const
|
|
15
|
+
export declare const ownedLaunchOptions: () => {
|
|
16
16
|
handleSIGINT: boolean;
|
|
17
|
+
pipe: boolean;
|
|
17
18
|
};
|
|
18
19
|
export interface GpuProbeLifecycle {
|
|
19
20
|
launch(puppeteer: PuppeteerNode, options: LaunchOptions): Promise<Browser>;
|
|
@@ -23,7 +23,7 @@ let hostHandlesSigint = false;
|
|
|
23
23
|
export function setHostHandlesSigint(owned) {
|
|
24
24
|
hostHandlesSigint = owned;
|
|
25
25
|
}
|
|
26
|
-
export const
|
|
26
|
+
export const ownedLaunchOptions = () => ({ handleSIGINT: !hostHandlesSigint, pipe: true });
|
|
27
27
|
function isSoftwareWebGlRenderer(rendererInfo) {
|
|
28
28
|
const renderer = rendererInfo.trim().toLowerCase();
|
|
29
29
|
return (renderer.includes("swiftshader") ||
|
|
@@ -54,7 +54,7 @@ async function probeHardwareWebGlInfo(ppt, options) {
|
|
|
54
54
|
let probeBrowser;
|
|
55
55
|
try {
|
|
56
56
|
const launchOptions = {
|
|
57
|
-
...
|
|
57
|
+
...ownedLaunchOptions(),
|
|
58
58
|
headless: true,
|
|
59
59
|
args: withHyperframesBrowserMarker(options.args),
|
|
60
60
|
defaultViewport: { width: 64, height: 64 },
|
|
@@ -659,7 +659,7 @@ async function launchBrowser(fingerprint) {
|
|
|
659
659
|
let captureMode = fingerprint.requestedCaptureMode;
|
|
660
660
|
let browser;
|
|
661
661
|
const launch = (args) => ppt.launch({
|
|
662
|
-
...
|
|
662
|
+
...ownedLaunchOptions(),
|
|
663
663
|
headless: true,
|
|
664
664
|
args: withHyperframesBrowserMarker(args),
|
|
665
665
|
defaultViewport: null,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the page to expose window.__hf, then captures frames deterministically
|
|
7
7
|
* via Chrome's BeginFrame API or Page.captureScreenshot fallback.
|
|
8
8
|
*/
|
|
9
|
-
import { type Browser, type Page, type Protocol } from "puppeteer-core";
|
|
9
|
+
import { type Browser, type CDPSession, type Page, type Protocol } from "puppeteer-core";
|
|
10
10
|
import { type RawAuthoredTiming } from "@hyperframes/core";
|
|
11
11
|
import { type MotionBlurPlan } from "./motionBlur.js";
|
|
12
12
|
import { type BrowserLease, type CaptureMode } from "./browserManager.js";
|
|
@@ -429,6 +429,7 @@ export interface IsolatedComposition {
|
|
|
429
429
|
compositionId: string;
|
|
430
430
|
elementId: string | null;
|
|
431
431
|
}
|
|
432
|
+
export declare function authorScriptsMayRegisterTimelines(runtimeAttr: string, authoredBegin: string, authoredEnd: string): Promise<boolean>;
|
|
432
433
|
export interface SubTimelinePollOptions {
|
|
433
434
|
intervalMs?: number;
|
|
434
435
|
getScriptLoadFailures?: () => readonly string[];
|
|
@@ -467,6 +468,8 @@ export declare function classifyConsoleScriptError(type: string, text: string):
|
|
|
467
468
|
export declare function classifyConsoleScriptFailure(type: string, text: string): string | null;
|
|
468
469
|
/** `runtime-error:<first line>` when a script served with the composition threw; null if not attributable to one. */
|
|
469
470
|
export declare function classifyPageError(details: Protocol.Runtime.ExceptionDetails, projectScripts: ReadonlySet<string>): string | null;
|
|
471
|
+
/** ` [thrown at: <url>:<line>:<col>, <lookup>]` for a throw's authored frame, so the author can find the element. */
|
|
472
|
+
export declare function describeThrowSite(client: CDPSession, details: Protocol.Runtime.ExceptionDetails, projectScripts: ReadonlySet<string>): Promise<string>;
|
|
470
473
|
export declare function initializeSession(session: CaptureSession): Promise<void>;
|
|
471
474
|
/**
|
|
472
475
|
* Internal helper: seek timeline and inject video frames.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
|
-
import { COMPOSITION_SOURCE_URL, MEDIA_RENDER_ID_ATTR, quantizeSeekTime, quantizeTimeToFrame, fpsToNumber, resolveAuthoredTimingWindow, SCENE_CALLBACK_ERROR_PREFIX, } from "@hyperframes/core";
|
|
12
|
+
import { COMPOSITION_SOURCE_URL, MEDIA_RENDER_ID_ATTR, quantizeSeekTime, quantizeTimeToFrame, fpsToNumber, resolveAuthoredTimingWindow, AUTHORED_SOURCE_BEGIN, AUTHORED_SOURCE_END, RUNTIME_BOOTSTRAP_ATTR, SCENE_CALLBACK_ERROR_PREFIX, } from "@hyperframes/core";
|
|
13
13
|
import { holdRenderFrameVideos } from "./renderFrameVideoHold.js";
|
|
14
14
|
import { DrawElementCaptureError } from "./drawElementCaptureError.js";
|
|
15
15
|
import { encodePng } from "../utils/alphaBlit.js";
|
|
@@ -1361,6 +1361,50 @@ export async function waitForSubCompositionTimelines(session, page, timeoutMs, o
|
|
|
1361
1361
|
memo.unregisteredIds = session.pendingTimelineIds ?? [];
|
|
1362
1362
|
}
|
|
1363
1363
|
}
|
|
1364
|
+
// Runs in the page. False once every inline, template and same-origin script is read and none
|
|
1365
|
+
// mentions __timelines or imports code; an unreadable or module script, or a page still loading, answers true.
|
|
1366
|
+
export async function authorScriptsMayRegisterTimelines(runtimeAttr, authoredBegin, authoredEnd) {
|
|
1367
|
+
if (document.readyState !== "complete")
|
|
1368
|
+
return true;
|
|
1369
|
+
const memo = globalThis;
|
|
1370
|
+
memo.__hfAuthorScan ??= await (async () => {
|
|
1371
|
+
const scripts = [
|
|
1372
|
+
...Array.from(document.querySelectorAll("script")),
|
|
1373
|
+
...Array.from(document.querySelectorAll("template")).flatMap((t) => Array.from(t.content.querySelectorAll("script"))),
|
|
1374
|
+
].filter((script) => !script.hasAttribute(runtimeAttr));
|
|
1375
|
+
// Code that imports another file may register through it, which is not read here.
|
|
1376
|
+
const mayRegister = (code) => code.includes("__timelines") || /\bimport\s*\(|^\s*import\s*[\w{*"']/m.test(code);
|
|
1377
|
+
for (const script of scripts) {
|
|
1378
|
+
if (script.type === "module")
|
|
1379
|
+
return true;
|
|
1380
|
+
if (!script.src) {
|
|
1381
|
+
const text = script.textContent ?? "";
|
|
1382
|
+
const authored = text.includes(authoredBegin)
|
|
1383
|
+
? text
|
|
1384
|
+
.split(authoredBegin)
|
|
1385
|
+
.slice(1)
|
|
1386
|
+
.map((region) => region.split(authoredEnd)[0])
|
|
1387
|
+
.join("\n")
|
|
1388
|
+
: text;
|
|
1389
|
+
if (mayRegister(authored))
|
|
1390
|
+
return true;
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
const url = new URL(script.src, location.href);
|
|
1394
|
+
if (url.origin !== location.origin)
|
|
1395
|
+
return true;
|
|
1396
|
+
try {
|
|
1397
|
+
if (mayRegister(await (await fetch(url)).text()))
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
catch {
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return false;
|
|
1405
|
+
})();
|
|
1406
|
+
return memo.__hfAuthorScan;
|
|
1407
|
+
}
|
|
1364
1408
|
export async function pollSubCompositionTimelines(page, timeoutMs, { intervalMs = 150, getScriptLoadFailures, scriptFailureGraceMs = 2_000, getPageErrors, scriptErrorGraceMs = 5_000, onPending, shouldStop, knownUnregisteredIds = [], } = {}) {
|
|
1365
1409
|
// Hosts may opt out of the timeline wait with `data-no-timeline` —
|
|
1366
1410
|
// compositions driven purely by CSS animations / rAF (the render-compat
|
|
@@ -1386,15 +1430,19 @@ export async function pollSubCompositionTimelines(page, timeoutMs, { intervalMs
|
|
|
1386
1430
|
// A page error is given its grace from when it first appeared, not from when the poll began.
|
|
1387
1431
|
let errorSeenAt;
|
|
1388
1432
|
for (;;) {
|
|
1389
|
-
|
|
1433
|
+
const failures = getScriptLoadFailures?.() ?? [];
|
|
1434
|
+
const errors = getPageErrors?.() ?? [];
|
|
1435
|
+
const failed = failures.length > 0 ? failures : errors;
|
|
1436
|
+
// A failed script keeps the wait, so the failure is still named; only a quiet page is ready early.
|
|
1437
|
+
ready =
|
|
1438
|
+
Boolean(await page.evaluate(expression)) ||
|
|
1439
|
+
(failed.length === 0 &&
|
|
1440
|
+
!(await page.evaluate(authorScriptsMayRegisterTimelines, RUNTIME_BOOTSTRAP_ATTR, AUTHORED_SOURCE_BEGIN, AUTHORED_SOURCE_END)));
|
|
1390
1441
|
if (ready || shouldStop?.())
|
|
1391
1442
|
break;
|
|
1392
1443
|
const now = Date.now();
|
|
1393
1444
|
if (now >= deadline)
|
|
1394
1445
|
break;
|
|
1395
|
-
const failures = getScriptLoadFailures?.() ?? [];
|
|
1396
|
-
const errors = getPageErrors?.() ?? [];
|
|
1397
|
-
const failed = failures.length > 0 ? failures : errors;
|
|
1398
1446
|
const graceMs = failures.length > 0 ? scriptFailureGraceMs : scriptErrorGraceMs;
|
|
1399
1447
|
if (failed.length > 0)
|
|
1400
1448
|
errorSeenAt ??= now;
|
|
@@ -1839,10 +1887,41 @@ export function classifyPageError(details, projectScripts) {
|
|
|
1839
1887
|
if (frames.length === 0 && details.text.startsWith("Uncaught (in promise)"))
|
|
1840
1888
|
return null;
|
|
1841
1889
|
const urls = frames.length > 0 ? frames : [details.url];
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1890
|
+
return urls.some((url) => isOwnedScriptUrl(url, projectScripts))
|
|
1891
|
+
? `runtime-error:${firstLine}`
|
|
1892
|
+
: null;
|
|
1893
|
+
}
|
|
1894
|
+
// The render compiler names the composition's inline code.
|
|
1895
|
+
// Inline handlers report the document, which a widget can move.
|
|
1896
|
+
function isOwnedScriptUrl(url, projectScripts) {
|
|
1897
|
+
return url !== undefined && (projectScripts.has(url) || url.startsWith(COMPOSITION_SOURCE_URL));
|
|
1898
|
+
}
|
|
1899
|
+
const LOOKUP_CALL = /\b(?:getElementById|querySelectorAll|querySelector|getElementsByClassName|getElementsByTagName)\(\s*(["'`])((?:(?!\1).)*)\1/g;
|
|
1900
|
+
/** ` [thrown at: <url>:<line>:<col>, <lookup>]` for a throw's authored frame, so the author can find the element. */
|
|
1901
|
+
export async function describeThrowSite(client, details, projectScripts) {
|
|
1902
|
+
const frame = (details.stackTrace?.callFrames ?? []).find((f) => isOwnedScriptUrl(f.url, projectScripts));
|
|
1903
|
+
if (!frame)
|
|
1904
|
+
return "";
|
|
1905
|
+
let where = `${frame.url}:${frame.lineNumber + 1}:${frame.columnNumber + 1}`;
|
|
1906
|
+
try {
|
|
1907
|
+
// Source is only fetchable with the debugger on; enabling after the fact keeps script ids.
|
|
1908
|
+
await client.send("Debugger.enable");
|
|
1909
|
+
const { scriptSource } = await client.send("Debugger.getScriptSource", {
|
|
1910
|
+
scriptId: frame.scriptId,
|
|
1911
|
+
});
|
|
1912
|
+
const line = scriptSource.split("\n")[frame.lineNumber] ?? "";
|
|
1913
|
+
const lookups = [...line.matchAll(LOOKUP_CALL)];
|
|
1914
|
+
// Only a lone lookup is certain to be the one that returned null; with several the position alone is given.
|
|
1915
|
+
if (lookups.length === 1)
|
|
1916
|
+
where += `, ${lookups[0]?.[0]})`;
|
|
1917
|
+
}
|
|
1918
|
+
catch {
|
|
1919
|
+
// Source unavailable: line:col alone still locates the throw.
|
|
1920
|
+
}
|
|
1921
|
+
finally {
|
|
1922
|
+
await client.send("Debugger.disable").catch(() => undefined);
|
|
1923
|
+
}
|
|
1924
|
+
return ` [thrown at: ${where}]`;
|
|
1846
1925
|
}
|
|
1847
1926
|
export async function initializeSession(session) {
|
|
1848
1927
|
const { page, serverUrl } = session;
|
|
@@ -1913,7 +1992,20 @@ export async function initializeSession(session) {
|
|
|
1913
1992
|
// Unlike the pageerror Error, this keeps the script URL of syntax errors and thrown non-errors.
|
|
1914
1993
|
const runtimeClient = await getCdpSession(page);
|
|
1915
1994
|
runtimeClient.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
|
|
1916
|
-
|
|
1995
|
+
const error = classifyPageError(exceptionDetails, projectScripts);
|
|
1996
|
+
recordPageError(session, error);
|
|
1997
|
+
if (!error)
|
|
1998
|
+
return;
|
|
1999
|
+
// Recorded first so fail-fast sees it; the throw site is appended once the source is fetched.
|
|
2000
|
+
void describeThrowSite(runtimeClient, exceptionDetails, projectScripts).then((site) => {
|
|
2001
|
+
const i = session.pageErrors.indexOf(error);
|
|
2002
|
+
if (!site || i < 0)
|
|
2003
|
+
return;
|
|
2004
|
+
if (session.pageErrors.includes(`${error}${site}`))
|
|
2005
|
+
session.pageErrors.splice(i, 1);
|
|
2006
|
+
else
|
|
2007
|
+
session.pageErrors[i] = `${error}${site}`;
|
|
2008
|
+
});
|
|
1917
2009
|
});
|
|
1918
2010
|
await runtimeClient.send("Runtime.enable");
|
|
1919
2011
|
const pageNavigationTimeout = session.config?.pageNavigationTimeout ?? DEFAULT_CONFIG.pageNavigationTimeout;
|
|
@@ -3337,8 +3429,8 @@ async function captureAccumulatedFrame(session, frameIndex, absFrameIndex, plan)
|
|
|
3337
3429
|
await seekPageTimeline(session.page, frameTime, undefined);
|
|
3338
3430
|
const totals = { seekMs: Date.now() - eventfulSeekStart, beforeCaptureMs: 0, screenshotMs: 0 };
|
|
3339
3431
|
if (session.onBeforeCapture) {
|
|
3340
|
-
//
|
|
3341
|
-
//
|
|
3432
|
+
// Sub-samples leave a video's style alone: inject at frameTime first so the held frame
|
|
3433
|
+
// carries frameTime's style, not the first sample's.
|
|
3342
3434
|
const injectStart = Date.now();
|
|
3343
3435
|
session.captureStep = "injecting video frames";
|
|
3344
3436
|
await session.onBeforeCapture(session.page, frameTime, frameTime);
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Performance: ~6 fps at 1080x1920 via base64 transfer.
|
|
20
20
|
*/
|
|
21
21
|
import { withHyperframesBrowserMarker } from "@hyperframes/core/browser-marker";
|
|
22
|
-
import { resolveFullChromePath,
|
|
22
|
+
import { resolveFullChromePath, ownedLaunchOptions } from "./browserManager.js";
|
|
23
23
|
// ── PQ (SMPTE 2084) OETF ─────────────────────────────────────────────────────
|
|
24
24
|
const PQ_M1 = 0.1593017578125;
|
|
25
25
|
const PQ_M2 = 78.84375;
|
|
@@ -181,7 +181,7 @@ export async function launchHdrBrowser(width, height) {
|
|
|
181
181
|
throw new Error("[HDR] No Chrome binary found. Install: npx @puppeteer/browsers install chrome@stable");
|
|
182
182
|
}
|
|
183
183
|
const browser = await ppt.launch({
|
|
184
|
-
...
|
|
184
|
+
...ownedLaunchOptions(),
|
|
185
185
|
headless: false,
|
|
186
186
|
executablePath: chromePath,
|
|
187
187
|
args: withHyperframesBrowserMarker(buildHdrChromeArgs(width, height)),
|
|
@@ -194,9 +194,11 @@ export declare function ensureRenderFrameSiblings(page: Page): Promise<void>;
|
|
|
194
194
|
* cache entry for a frame that never reached the page, which would otherwise
|
|
195
195
|
* short-circuit the next inject at the same frameIndex and leave the host's
|
|
196
196
|
* first visible frame blank.
|
|
197
|
+
*
|
|
198
|
+
* `restyleIds` keep their current picture and only re-copy the video's style.
|
|
197
199
|
*/
|
|
198
200
|
export declare function injectVideoFramesBatch(page: Page, updates: Array<{
|
|
199
201
|
videoId: string;
|
|
200
202
|
dataUri: string;
|
|
201
|
-
}
|
|
203
|
+
}>, restyleIds?: readonly string[]): Promise<string[]>;
|
|
202
204
|
export declare function syncVideoFrameVisibility(page: Page, activeVideoIds: string[]): Promise<void>;
|
|
@@ -548,13 +548,21 @@ export async function ensureRenderFrameSiblings(page) {
|
|
|
548
548
|
* cache entry for a frame that never reached the page, which would otherwise
|
|
549
549
|
* short-circuit the next inject at the same frameIndex and leave the host's
|
|
550
550
|
* first visible frame blank.
|
|
551
|
+
*
|
|
552
|
+
* `restyleIds` keep their current picture and only re-copy the video's style.
|
|
551
553
|
*/
|
|
552
|
-
export async function injectVideoFramesBatch(page, updates) {
|
|
553
|
-
if (updates.length === 0)
|
|
554
|
+
export async function injectVideoFramesBatch(page, updates, restyleIds = []) {
|
|
555
|
+
if (updates.length === 0 && restyleIds.length === 0)
|
|
554
556
|
return [];
|
|
555
|
-
return withVideoFrameSources(page, updates, (resolved) => injectResolvedVideoFramesBatch(page, resolved));
|
|
557
|
+
return withVideoFrameSources(page, updates, (resolved) => injectResolvedVideoFramesBatch(page, resolved, restyleIds));
|
|
556
558
|
}
|
|
557
|
-
async function injectResolvedVideoFramesBatch(page, updates) {
|
|
559
|
+
async function injectResolvedVideoFramesBatch(page, updates, restyleIds) {
|
|
560
|
+
// Build the sibling id with core's function rather than a template here,
|
|
561
|
+
// so the id the readers look up has exactly one definition.
|
|
562
|
+
const items = [
|
|
563
|
+
...updates,
|
|
564
|
+
...restyleIds.map((videoId) => ({ videoId })),
|
|
565
|
+
].map((item) => ({ ...item, frameId: renderFrameIdForRenderId(item.videoId) }));
|
|
558
566
|
return await page.evaluate(
|
|
559
567
|
// fallow-ignore-next-line complexity
|
|
560
568
|
async (items, visualProperties, colorGradingSourceHiddenAttr) => {
|
|
@@ -615,6 +623,9 @@ async function injectResolvedVideoFramesBatch(page, updates) {
|
|
|
615
623
|
continue;
|
|
616
624
|
let img = video.nextElementSibling;
|
|
617
625
|
const hasImg = img !== null && img.classList.contains("__render_frame__");
|
|
626
|
+
const restyleOnly = item.dataUri === undefined;
|
|
627
|
+
if (restyleOnly && !hasImg)
|
|
628
|
+
continue;
|
|
618
629
|
if (isVisualAncestorHidden(video)) {
|
|
619
630
|
// Don't paint a frame over a hidden host — if an existing replacement
|
|
620
631
|
// <img> is still around from when the host was visible, hide it so it
|
|
@@ -700,7 +711,7 @@ async function injectResolvedVideoFramesBatch(page, updates) {
|
|
|
700
711
|
// Publish after full decode so raster cannot race it with a reduced JPEG decode.
|
|
701
712
|
img.style.setProperty("visibility", "hidden", "important");
|
|
702
713
|
img.decoding = "sync";
|
|
703
|
-
if (img.getAttribute("src") !== item.dataUri) {
|
|
714
|
+
if (item.dataUri !== undefined && img.getAttribute("src") !== item.dataUri) {
|
|
704
715
|
if (item.crossOrigin)
|
|
705
716
|
img.crossOrigin = item.crossOrigin;
|
|
706
717
|
else
|
|
@@ -718,23 +729,21 @@ async function injectResolvedVideoFramesBatch(page, updates) {
|
|
|
718
729
|
// GSAP-controlled value.
|
|
719
730
|
video.style.setProperty("visibility", "hidden", "important");
|
|
720
731
|
video.style.setProperty("pointer-events", "none", "important");
|
|
721
|
-
|
|
732
|
+
if (!restyleOnly)
|
|
733
|
+
injectedIds.push(item.videoId);
|
|
722
734
|
}
|
|
723
735
|
if (pendingDecodes.length > 0) {
|
|
724
736
|
await Promise.all(pendingDecodes);
|
|
725
737
|
}
|
|
726
738
|
for (const img of imagesToPublish)
|
|
727
739
|
img.style.visibility = "visible";
|
|
728
|
-
if (
|
|
740
|
+
if (imagesToPublish.length > 0) {
|
|
729
741
|
const redraw = window
|
|
730
742
|
.__hf?.colorGrading?.redraw;
|
|
731
743
|
redraw?.();
|
|
732
744
|
}
|
|
733
745
|
return injectedIds;
|
|
734
|
-
},
|
|
735
|
-
// Build the sibling id with core's function rather than a template here,
|
|
736
|
-
// so the id the readers look up has exactly one definition.
|
|
737
|
-
updates.map((update) => ({ ...update, frameId: renderFrameIdForRenderId(update.videoId) })), [...MEDIA_VISUAL_STYLE_PROPERTIES], COLOR_GRADING_SOURCE_HIDDEN_ATTR);
|
|
746
|
+
}, items, [...MEDIA_VISUAL_STYLE_PROPERTIES], COLOR_GRADING_SOURCE_HIDDEN_ATTR);
|
|
738
747
|
}
|
|
739
748
|
export async function syncVideoFrameVisibility(page, activeVideoIds) {
|
|
740
749
|
await page.evaluate(
|
|
@@ -258,6 +258,7 @@ export interface ImageElement {
|
|
|
258
258
|
end: number;
|
|
259
259
|
}
|
|
260
260
|
export declare function parseImageElements(html: string): ImageElement[];
|
|
261
|
+
export declare function sdrCanvasColourFilters(metadata: VideoMetadata, toneMappedToSdr?: boolean): string[];
|
|
261
262
|
export declare function extractVideoFramesRange(...args: Parameters<typeof extractVideoFramesRangeUnredacted>): Promise<ExtractedFrames>;
|
|
262
263
|
declare function extractVideoFramesRangeUnredacted(videoPath: string, videoId: string, startTime: number, duration: number, options: ExtractionOptions, signal?: AbortSignal, config?: Partial<Pick<EngineConfig, "ffmpegProcessTimeout">>,
|
|
263
264
|
/** Frames go straight here, with no per-videoId subdir (the cache layer's keyed entry). */
|
|
@@ -14,7 +14,7 @@ import { parseHTML } from "linkedom";
|
|
|
14
14
|
import { isPathInside } from "@hyperframes/parsers/asset-paths";
|
|
15
15
|
import { resolveProjectRelativeSrc } from "@hyperframes/parsers/asset-resolution";
|
|
16
16
|
import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
|
|
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
|
+
import { MEDIA_RENDER_ID_ATTR, isAudibleVideoElement, fpsToFfmpegArg, fpsToNumber, MEDIA_DURATION_CLAMP_EPSILON_SECONDS, normalizeRateSpec, readElementRateSpec, shiftRateLane, sourceTimeAt, timeAtSourceTime, exportClipWindow, hasClipStarted, isInClipWindow, mediaElementTime, parseStrictFiniteTimingNumber, readMediaStart, toFps, firstFrameColourArgs, hdrToSdrToneMapFilter, parseFirstFrameColour, } from "@hyperframes/core";
|
|
18
18
|
import { resolveReferencedStart } from "./referenceResolver.js";
|
|
19
19
|
import { isKnownInactiveTimelineWindow } from "./mediaTimelineWindow.js";
|
|
20
20
|
import { extractFinalVideoFrameTimestamp, extractMediaMetadata, mediaFileIdentity, readMetadataCache, readStoredFinalFrame, resolvePlayableVideoDuration, writeMetadataCache, } from "../utils/ffprobe.js";
|
|
@@ -477,6 +477,12 @@ function chromeGuessForUntaggedMatrix(metadata) {
|
|
|
477
477
|
const hd = metadata.height >= 720 && !CHROME_BT601_UNTAGGED_CODECS.has(metadata.videoCodec);
|
|
478
478
|
return [`setparams=colorspace=${hd ? "bt709" : "smpte170m"}`];
|
|
479
479
|
}
|
|
480
|
+
export function sdrCanvasColourFilters(metadata, toneMappedToSdr = false) {
|
|
481
|
+
if (!isHdrColorSpaceUtil(metadata.colorSpace)) {
|
|
482
|
+
return [...chromeGuessForUntaggedMatrix(metadata), SDR_CANVAS_PASSTHROUGH_FILTER];
|
|
483
|
+
}
|
|
484
|
+
return toneMappedToSdr ? [SDR_CANVAS_PASSTHROUGH_FILTER] : [];
|
|
485
|
+
}
|
|
480
486
|
/**
|
|
481
487
|
* Longest stretch of output one ffmpeg process extracts; longer ranges split into segments
|
|
482
488
|
* that run side by side. Segments sample the same absolute times as a single pass, so their
|
|
@@ -678,7 +684,7 @@ outputDirOverride) {
|
|
|
678
684
|
vfFilters.push(hdrToSdrToneMapFilter(metadata.colorSpace ?? {}, frameColour));
|
|
679
685
|
}
|
|
680
686
|
if (toneMappedToSdr || (!isHdr && !options.sdrToHdrTransfer)) {
|
|
681
|
-
vfFilters.push(...(
|
|
687
|
+
vfFilters.push(...sdrCanvasColourFilters(metadata, toneMappedToSdr));
|
|
682
688
|
if (format === "jpg") {
|
|
683
689
|
if (options.shownBox)
|
|
684
690
|
vfFilters.push(halveToCoverScaleFilter(options.shownBox));
|
|
@@ -1832,9 +1838,9 @@ function getFrameIndexAtTime(extracted, globalTime, videoStart, loop = false, me
|
|
|
1832
1838
|
if (cycle > 0 && sourceTime >= cycle)
|
|
1833
1839
|
sourceTime %= cycle;
|
|
1834
1840
|
}
|
|
1835
|
-
//
|
|
1836
|
-
|
|
1837
|
-
const frameIndex = Math.floor(
|
|
1841
|
+
// The frame the preview's <video> shows at this source time, so render and preview pick the same one.
|
|
1842
|
+
const shownTime = mediaElementTime(mediaStart + sourceTime) - mediaStart;
|
|
1843
|
+
const frameIndex = Math.floor(shownTime * extracted.fps);
|
|
1838
1844
|
if (frameIndex < 0 || extracted.totalFrames <= 0)
|
|
1839
1845
|
return null;
|
|
1840
1846
|
if (isVideoHiddenBeforeStreamStart(extracted.metadata.videoStreamLeadSeconds, mediaStart + sourceTime)) {
|
|
@@ -159,14 +159,19 @@ export function createVideoFrameInjector(frameLookup, config) {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
const updates = [];
|
|
162
|
+
const restyleIds = [];
|
|
162
163
|
const activeIds = new Set();
|
|
164
|
+
const isBlurSubSample = heldVideoTime !== undefined && heldVideoTime !== time;
|
|
163
165
|
if (activePayloads.size > 0) {
|
|
164
166
|
const pendingReads = [];
|
|
165
167
|
for (const [videoId, payload] of activePayloads) {
|
|
166
168
|
activeIds.add(videoId);
|
|
167
169
|
const lastFrameIndex = lastInjectedFrameByVideo.get(videoId);
|
|
168
|
-
if (lastFrameIndex === payload.frameIndex)
|
|
170
|
+
if (lastFrameIndex === payload.frameIndex) {
|
|
171
|
+
if (!isBlurSubSample)
|
|
172
|
+
restyleIds.push(videoId);
|
|
169
173
|
continue;
|
|
174
|
+
}
|
|
170
175
|
pendingReads.push(frameCache
|
|
171
176
|
.get(payload.framePath)
|
|
172
177
|
.then((dataUri) => ({ videoId, dataUri, frameIndex: payload.frameIndex })));
|
|
@@ -179,14 +184,14 @@ export function createVideoFrameInjector(frameLookup, config) {
|
|
|
179
184
|
}
|
|
180
185
|
}
|
|
181
186
|
await syncVideoFrameVisibility(page, Array.from(activeIds));
|
|
182
|
-
if (updates.length > 0) {
|
|
187
|
+
if (updates.length > 0 || restyleIds.length > 0) {
|
|
183
188
|
// Only record cache entries for videos the page actually painted.
|
|
184
189
|
// injectVideoFramesBatch skips any video whose visual ancestor is
|
|
185
190
|
// hidden (sub-comp host out-of-window) and returns the subset of ids
|
|
186
191
|
// it really wrote — recording the rest would short-circuit the next
|
|
187
192
|
// call at the same frameIndex and leave the host's first visible
|
|
188
193
|
// frame blank.
|
|
189
|
-
const injectedIds = new Set(await injectVideoFramesBatch(page, updates.map((u) => ({ videoId: u.videoId, dataUri: u.dataUri }))));
|
|
194
|
+
const injectedIds = new Set(await injectVideoFramesBatch(page, updates.map((u) => ({ videoId: u.videoId, dataUri: u.dataUri })), restyleIds));
|
|
190
195
|
for (const update of updates) {
|
|
191
196
|
if (injectedIds.has(update.videoId)) {
|
|
192
197
|
lastInjectedFrameByVideo.set(update.videoId, update.frameIndex);
|
package/dist/utils/ffprobe.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Spawn ffprobe with given args, return stdout. Throws on non-zero exit or missing binary. */
|
|
2
|
+
export declare function runFfprobe(filePath: string, argsWithoutInput: string[], signal?: AbortSignal, stdoutOptions?: {
|
|
3
|
+
retainTail?: boolean;
|
|
4
|
+
maxChars?: number;
|
|
5
|
+
}): Promise<string>;
|
|
1
6
|
export interface MediaProbeCacheEntry<T = FFProbeOutput> {
|
|
2
7
|
identity: string | null;
|
|
3
8
|
promise: Promise<T>;
|
package/dist/utils/ffprobe.js
CHANGED
|
@@ -49,7 +49,7 @@ function sanitizeFfprobeDiagnostic(stderr, filePath) {
|
|
|
49
49
|
return `…${redacted.slice(-(FFPROBE_ERROR_MAX_CHARS - 1))}`;
|
|
50
50
|
}
|
|
51
51
|
/** Spawn ffprobe with given args, return stdout. Throws on non-zero exit or missing binary. */
|
|
52
|
-
async function runFfprobe(filePath, argsWithoutInput, signal, stdoutOptions) {
|
|
52
|
+
export async function runFfprobe(filePath, argsWithoutInput, signal, stdoutOptions) {
|
|
53
53
|
// `--` stops option parsing so a path like "-intro.mp4" is a filename, but
|
|
54
54
|
// it does NOT cover a path of exactly "-": ffprobe rewrites that to `fd:`
|
|
55
55
|
// AFTER option parsing and then reads stdin. Since stdin here is a pipe the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.206",
|
|
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.206",
|
|
54
|
+
"@hyperframes/parsers": "^0.8.206"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^25.0.10",
|