@hyperframes/engine 0.8.210 → 0.8.212
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 +4 -2
- package/dist/index.js +3 -1
- package/dist/services/aacEncode.js +1 -1
- package/dist/services/chromeHostCeiling.js +6 -2
- package/dist/services/chromeStartup.js +8 -1
- package/dist/services/chunkEncoder.d.ts +1 -1
- package/dist/services/chunkEncoder.js +1 -1
- package/dist/services/drawElementService.d.ts +6 -3
- package/dist/services/drawElementService.js +16 -7
- package/dist/services/frameCapture.js +2 -1
- package/dist/services/parallelCoordinator.d.ts +2 -0
- package/dist/services/parallelCoordinator.js +14 -3
- package/dist/utils/browserSafePort.d.ts +8 -0
- package/dist/utils/browserSafePort.js +38 -0
- package/dist/utils/ffprobe.js +4 -1
- package/dist/utils/gpuEncoder.d.ts +1 -0
- package/dist/utils/gpuEncoder.js +5 -4
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -38,12 +38,13 @@ export { chromeMajorCeiling } from "./services/chromeHostCeiling.js";
|
|
|
38
38
|
export { acquireBrowser, setHostHandlesSigint, releaseBrowser, drainBrowserPool, closeBrowserPool, resolveHeadlessShellPath, resolveBrowserGpuMode, buildChromeArgs, compositionRequiresWebGpu, assertWebGpuAdapterAvailable, usesSoftwareWebGpu, WebGpuUnavailableError, ENABLE_BROWSER_POOL, BrowserLeasePool, type GpuProbeLifecycle, type BuildChromeArgsOptions, type BrowserLaunchFingerprint, type BrowserLease, type BrowserPoolState, type CaptureMode, type AcquiredBrowser, } from "./services/browserManager.js";
|
|
39
39
|
export { augmentProtocolTimeoutError, isProtocolTimeoutError, } from "./services/protocolTimeoutErrorHint.js";
|
|
40
40
|
export { augmentPageNavigationTimeoutError, isPageNavigationTimeoutError, type NavigationTimeoutHintContext, } from "./services/pageNavigationTimeoutErrorHint.js";
|
|
41
|
+
export { forcePreservedDrawingBuffer } from "./services/drawElementService.js";
|
|
41
42
|
export { createCaptureSession, initializeSession, closeCaptureSession, captureFrame, captureFrameToBuffer, captureFrameToBufferPipelined, captureFramesBatchPipelined, DrawElementVerificationError, isDrawElementVerificationError, getDrawElementVerificationDetails, type DrawElementVerificationDetails, recaptureDrawElementFrameForVerify, completeDeferredDrawElementInit, writeCapturedFrame, discardWarmupCapture, getCompositionDuration, getCapturePerfSummary, percentileOf, prepareCaptureSessionForReuse, deriveBeginFrameProbeTimeTicks, type CaptureSession, isTransientBrowserError, isMemoryExhaustionError, type BeforeCaptureHook, type DiscardWarmupInnerCapture, type StaticVerificationOutcome, VfxFailureError, waitForImagesReady, evaluateHfDiagnostic, buildZeroDurationDiagnostic, } from "./services/frameCapture.js";
|
|
42
43
|
export { CaptureFailure, classifyCaptureFailure, isFatalCaptureFailure, type CaptureFailureKind, type CaptureWorkerDiagnostic, } from "./services/captureFailure.js";
|
|
43
44
|
export { createChromeMemorySampler, type ChromeMemoryStats, } from "./services/chromeMemorySampler.js";
|
|
44
45
|
export { installMediaRenderIdBridge } from "./services/mediaRenderIdBridge.js";
|
|
45
46
|
export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFramesBatch, syncVideoFrameVisibility, cdpSessionCache, probeBeginFrameLiveness, initTransparentBackground, capturesAlpha, captureAlphaPng, applyDomLayerMask, removeDomLayerMask, DOM_LAYER_MASK_STYLE_ID, type BeginFrameResult, } from "./services/screenshotService.js";
|
|
46
|
-
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, PRORES_PROFILES, PRORES_PROFILE_NAMES, isProresProfile, type ProresProfile, appendRateCapArgs, keyframeIntervalToGopSize, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, type GpuEncoder, type PackageHlsOptions, } from "./services/chunkEncoder.js";
|
|
47
|
+
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, getCachedGpuEncoder, ENCODER_PRESETS, getEncoderPreset, PRORES_PROFILES, PRORES_PROFILE_NAMES, isProresProfile, type ProresProfile, appendRateCapArgs, keyframeIntervalToGopSize, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, type GpuEncoder, type PackageHlsOptions, } from "./services/chunkEncoder.js";
|
|
47
48
|
export type { EncoderOptions, EncodeResult, MuxResult } from "./services/chunkEncoder.types.js";
|
|
48
49
|
export { spawnStreamingEncoder, createFrameReorderBuffer, type StreamingEncoder, type StreamingEncoderOptions, type StreamingEncoderResult, type FrameReorderBuffer, } from "./services/streamingEncoder.js";
|
|
49
50
|
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";
|
|
@@ -55,7 +56,7 @@ export { MIXED_AUDIO_FILENAME, parseAudioElements, parseMasterFxChain, processCo
|
|
|
55
56
|
export { audioFailureDiagnostics } from "./services/audioFailureDiagnostic.js";
|
|
56
57
|
export { cloneCaptureWarning, cloneCaptureWarnings } from "./services/captureWarning.js";
|
|
57
58
|
export type { AudioElement, AudioFailureReason, AudioFailureStage, AudioProcessingFailure, AudioFailureDiagnostic, AudioSubprocessEvidence, AudioTrack, AudioVolumeKeyframe, MixResult, } from "./services/audioMixer.types.js";
|
|
58
|
-
export { calculateOptimalWorkers, computeWorkerSizing, selectVerifySampleIndicesForTask, verifyDiskDrawElementSamples, distributeFrames, distributeFramesInterleaved, executeParallelCapture, mergeWorkerFrames, getSystemResources, shouldDisableBrowserPoolForParallelWorker, type WorkerTask, type WorkerResult, type WorkerSizing, type WorkerSizingBound, type LandedFrame, type ParallelProgress, type ParallelWorkerPhaseDiagnostic, } from "./services/parallelCoordinator.js";
|
|
59
|
+
export { calculateOptimalWorkers, computeWorkerSizing, selectVerifySampleIndicesForTask, verifyDiskDrawElementSamples, distributeFrames, distributeFramesInterleaved, executeParallelCapture, mergeWorkerFrames, getSystemResources, shouldDisableBrowserPoolForParallelWorker, type WorkerTask, type WorkerResult, type WorkerSizing, type WorkerSizingBound, type WorkerSizingConfig, type LandedFrame, type ParallelProgress, type ParallelWorkerPhaseDiagnostic, } from "./services/parallelCoordinator.js";
|
|
59
60
|
export { createFileServer, type FileServerOptions, type FileServerHandle, } from "./services/fileServer.js";
|
|
60
61
|
export { quantizeTimeToFrame, MEDIA_VISUAL_STYLE_PROPERTIES } from "@hyperframes/core";
|
|
61
62
|
export { frameFileExtension } from "./services/frameCapture.js";
|
|
@@ -64,6 +65,7 @@ export { frameSequencePath } from "./services/extractedFrameIndex.js";
|
|
|
64
65
|
export type { MotionBlurOptions, MotionBlurBlendSpace } from "./services/motionBlur.js";
|
|
65
66
|
export type { PageMotionBlurOff } from "./types.js";
|
|
66
67
|
export { assertSwiftShader, readWebGlVendorInfo, SwiftShaderAssertionError, BROWSER_GPU_NOT_SOFTWARE, } from "./utils/assertSwiftShader.js";
|
|
68
|
+
export { isBrowserSafePort, listenOnBrowserSafePort, settleOnBrowserSafePort, } from "./utils/browserSafePort.js";
|
|
67
69
|
export { readWebGlVendorInfoFromCanvas } from "./utils/readWebGlVendorInfoFromCanvas.js";
|
|
68
70
|
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
71
|
export { NOT_MEDIA_PAYLOAD, NotMediaPayloadError, assertMediaPayload, fingerprintElementId, isNotMediaPayload, } from "./utils/notMediaPayload.js";
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,7 @@ export { acquireBrowser, setHostHandlesSigint, releaseBrowser, drainBrowserPool,
|
|
|
40
40
|
export { augmentProtocolTimeoutError, isProtocolTimeoutError, } from "./services/protocolTimeoutErrorHint.js";
|
|
41
41
|
export { augmentPageNavigationTimeoutError, isPageNavigationTimeoutError, } from "./services/pageNavigationTimeoutErrorHint.js";
|
|
42
42
|
// ── Frame capture pipeline ──────────────────────────────────────────────────────
|
|
43
|
+
export { forcePreservedDrawingBuffer } from "./services/drawElementService.js";
|
|
43
44
|
export { createCaptureSession, initializeSession, closeCaptureSession, captureFrame, captureFrameToBuffer, captureFrameToBufferPipelined, captureFramesBatchPipelined, DrawElementVerificationError, isDrawElementVerificationError, getDrawElementVerificationDetails, recaptureDrawElementFrameForVerify, completeDeferredDrawElementInit, writeCapturedFrame, discardWarmupCapture, getCompositionDuration, getCapturePerfSummary, percentileOf, prepareCaptureSessionForReuse, deriveBeginFrameProbeTimeTicks, isTransientBrowserError, isMemoryExhaustionError, VfxFailureError, waitForImagesReady, evaluateHfDiagnostic, buildZeroDurationDiagnostic, } from "./services/frameCapture.js";
|
|
44
45
|
export { CaptureFailure, classifyCaptureFailure, isFatalCaptureFailure, } from "./services/captureFailure.js";
|
|
45
46
|
export { createChromeMemorySampler, } from "./services/chromeMemorySampler.js";
|
|
@@ -47,7 +48,7 @@ export { createChromeMemorySampler, } from "./services/chromeMemorySampler.js";
|
|
|
47
48
|
export { installMediaRenderIdBridge } from "./services/mediaRenderIdBridge.js";
|
|
48
49
|
export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFramesBatch, syncVideoFrameVisibility, cdpSessionCache, probeBeginFrameLiveness, initTransparentBackground, capturesAlpha, captureAlphaPng, applyDomLayerMask, removeDomLayerMask, DOM_LAYER_MASK_STYLE_ID, } from "./services/screenshotService.js";
|
|
49
50
|
// ── Encoding ───────────────────────────────────────────────────────────────────
|
|
50
|
-
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, PRORES_PROFILES, PRORES_PROFILE_NAMES, isProresProfile, appendRateCapArgs, keyframeIntervalToGopSize, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, } from "./services/chunkEncoder.js";
|
|
51
|
+
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, getCachedGpuEncoder, ENCODER_PRESETS, getEncoderPreset, PRORES_PROFILES, PRORES_PROFILE_NAMES, isProresProfile, appendRateCapArgs, keyframeIntervalToGopSize, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, } from "./services/chunkEncoder.js";
|
|
51
52
|
export { spawnStreamingEncoder, createFrameReorderBuffer, } from "./services/streamingEncoder.js";
|
|
52
53
|
// ── Media processing ───────────────────────────────────────────────────────────
|
|
53
54
|
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";
|
|
@@ -68,6 +69,7 @@ export { frameFileExtension } from "./services/frameCapture.js";
|
|
|
68
69
|
export { appendSoftwareEncoderThreadsArg } from "./services/softwareEncoderThreads.js";
|
|
69
70
|
export { frameSequencePath } from "./services/extractedFrameIndex.js";
|
|
70
71
|
export { assertSwiftShader, readWebGlVendorInfo, SwiftShaderAssertionError, BROWSER_GPU_NOT_SOFTWARE, } from "./utils/assertSwiftShader.js";
|
|
72
|
+
export { isBrowserSafePort, listenOnBrowserSafePort, settleOnBrowserSafePort, } from "./utils/browserSafePort.js";
|
|
71
73
|
export { readWebGlVendorInfoFromCanvas } from "./utils/readWebGlVendorInfoFromCanvas.js";
|
|
72
74
|
export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, runFfprobe, analyzeKeyframeIntervals, } from "./utils/ffprobe.js";
|
|
73
75
|
export { NOT_MEDIA_PAYLOAD, NotMediaPayloadError, assertMediaPayload, fingerprintElementId, isNotMediaPayload, } from "./utils/notMediaPayload.js";
|
|
@@ -57,7 +57,7 @@ function supportsSettsDuration(binary = getFfmpegBinary()) {
|
|
|
57
57
|
let supported = settsDurationSupport.get(binary);
|
|
58
58
|
if (!supported) {
|
|
59
59
|
supported = new Promise((resolve) => {
|
|
60
|
-
execFile(binary, ["-hide_banner", "-h", "bsf=setts"], (error, stdout) => {
|
|
60
|
+
execFile(binary, ["-hide_banner", "-h", "bsf=setts"], { windowsHide: true }, (error, stdout) => {
|
|
61
61
|
// A binary that cannot even print help fails the encode with its own error, not this one.
|
|
62
62
|
resolve(error !== null || stdout.includes("duration"));
|
|
63
63
|
});
|
|
@@ -57,8 +57,12 @@ export function shellStarts(binary, timeoutMs = 10_000) {
|
|
|
57
57
|
if (answer === undefined) {
|
|
58
58
|
const probe = ["--no-sandbox", "--dump-dom", "about:blank"];
|
|
59
59
|
answer =
|
|
60
|
-
spawnSync(binary, probe, {
|
|
61
|
-
|
|
60
|
+
spawnSync(binary, probe, {
|
|
61
|
+
stdio: "ignore",
|
|
62
|
+
timeout: timeoutMs,
|
|
63
|
+
killSignal: "SIGKILL",
|
|
64
|
+
windowsHide: true,
|
|
65
|
+
})?.status === 0;
|
|
62
66
|
if (!answer)
|
|
63
67
|
debug("skipping %s: it did not start", binary);
|
|
64
68
|
starts.set(binary, answer);
|
|
@@ -78,7 +78,14 @@ export async function diagnoseChromeStartup(executablePath, args, options = {})
|
|
|
78
78
|
}
|
|
79
79
|
/** The launch error, with the startup diagnosis appended; the original stays as `cause`. */
|
|
80
80
|
export async function explainChromeLaunchFailure(error, executablePath, args, signal) {
|
|
81
|
-
if (!
|
|
81
|
+
if (!(error instanceof Error))
|
|
82
|
+
return error;
|
|
83
|
+
const { code, errno, syscall } = error;
|
|
84
|
+
if (code === "UNKNOWN" && syscall === "spawn")
|
|
85
|
+
return new Error(`Windows refused to start the browser${executablePath ? ` at ${executablePath}` : ""} (error ${errno ?? "unknown"}, spawn UNKNOWN). ` +
|
|
86
|
+
"Security software may be blocking it, or the computer may be out of memory. " +
|
|
87
|
+
"Close other apps and retry; if it persists, run `hyperframes browser ensure --force` to download it again.", { cause: error });
|
|
88
|
+
if (!executablePath)
|
|
82
89
|
return error;
|
|
83
90
|
const diagnosis = await diagnoseChromeStartup(executablePath, args ?? [], { signal });
|
|
84
91
|
return new Error(`${error.message}\n${diagnosis}`, { cause: error });
|
|
@@ -72,7 +72,7 @@ export declare function isProresProfile(value: unknown): value is ProresProfile;
|
|
|
72
72
|
export declare function getEncoderPreset(quality: "draft" | "standard" | "high", format?: "mp4" | "webm" | "mov", hdr?: {
|
|
73
73
|
transfer: HdrTransfer;
|
|
74
74
|
}, proresProfile?: ProresProfile): EncoderPreset;
|
|
75
|
-
export { detectGpuEncoder, type GpuEncoder } from "../utils/gpuEncoder.js";
|
|
75
|
+
export { detectGpuEncoder, getCachedGpuEncoder, type GpuEncoder } from "../utils/gpuEncoder.js";
|
|
76
76
|
/** The `lockGopForChunkConcat` / `gopSize` pair, shared by every encoder entry point. */
|
|
77
77
|
export interface LockedGopOptions {
|
|
78
78
|
lockGopForChunkConcat?: boolean;
|
|
@@ -103,7 +103,7 @@ export function getEncoderPreset(quality, format = "mp4", hdr, proresProfile = "
|
|
|
103
103
|
return { ...base, pixelFormat: "yuv420p" };
|
|
104
104
|
}
|
|
105
105
|
// Re-export GPU utilities so existing consumers that import from chunkEncoder still work.
|
|
106
|
-
export { detectGpuEncoder } from "../utils/gpuEncoder.js";
|
|
106
|
+
export { detectGpuEncoder, getCachedGpuEncoder } from "../utils/gpuEncoder.js";
|
|
107
107
|
/**
|
|
108
108
|
* Integer GOP length, or `null` when no lock was requested. Throws on a lock
|
|
109
109
|
* with an invalid size — a silent fallback ships open-GOP output that only
|
|
@@ -71,13 +71,16 @@ export declare function resolveDrawElementCaptureMode(isSwiftShader: boolean, tr
|
|
|
71
71
|
* captureDrawElementFrame can hide them from paint records and drawImage
|
|
72
72
|
* their live content underneath the drawElementImage output.
|
|
73
73
|
*
|
|
74
|
-
*
|
|
75
|
-
* without it the drawing buffer is cleared after each compositor present and
|
|
76
|
-
* drawImage(glCanvas) reads blank.
|
|
74
|
+
* Register forcePreservedDrawingBuffer first: this wrapper only records.
|
|
77
75
|
*
|
|
78
76
|
* Must be registered via page.evaluateOnNewDocument BEFORE navigation.
|
|
79
77
|
*/
|
|
80
78
|
export declare function instrumentAcceleratedCanvases(): void;
|
|
79
|
+
/**
|
|
80
|
+
* WebGL contexts keep their drawing buffer after a frame is presented, so drawImage(glCanvas) reads the frame
|
|
81
|
+
* instead of blank. Must be registered via page.evaluateOnNewDocument BEFORE navigation.
|
|
82
|
+
*/
|
|
83
|
+
export declare function forcePreservedDrawingBuffer(): void;
|
|
81
84
|
export interface GpuBackendInfo {
|
|
82
85
|
/** SwiftShader (software rasterizer) — e.g. Docker headless-shell. */
|
|
83
86
|
isSwiftShader: boolean;
|
|
@@ -79,9 +79,7 @@ export function resolveDrawElementCaptureMode(isSwiftShader, transparent) {
|
|
|
79
79
|
* captureDrawElementFrame can hide them from paint records and drawImage
|
|
80
80
|
* their live content underneath the drawElementImage output.
|
|
81
81
|
*
|
|
82
|
-
*
|
|
83
|
-
* without it the drawing buffer is cleared after each compositor present and
|
|
84
|
-
* drawImage(glCanvas) reads blank.
|
|
82
|
+
* Register forcePreservedDrawingBuffer first: this wrapper only records.
|
|
85
83
|
*
|
|
86
84
|
* Must be registered via page.evaluateOnNewDocument BEFORE navigation.
|
|
87
85
|
*/
|
|
@@ -92,11 +90,9 @@ export function instrumentAcceleratedCanvases() {
|
|
|
92
90
|
const orig = HTMLCanvasElement.prototype.getContext;
|
|
93
91
|
// oxlint-disable-next-line no-explicit-any
|
|
94
92
|
HTMLCanvasElement.prototype.getContext = function (type, attrs) {
|
|
95
|
-
const
|
|
96
|
-
const isAccel = isGl || type === "webgpu";
|
|
97
|
-
const finalAttrs = isGl ? { ...attrs, preserveDrawingBuffer: true } : attrs;
|
|
93
|
+
const isAccel = type === "webgl" || type === "webgl2" || type === "experimental-webgl" || type === "webgpu";
|
|
98
94
|
// oxlint-disable-next-line no-explicit-any
|
|
99
|
-
const ctx = orig.call(this, type,
|
|
95
|
+
const ctx = orig.call(this, type, attrs);
|
|
100
96
|
if (ctx && isAccel) {
|
|
101
97
|
const list = w.__hf_accel_canvases ?? [];
|
|
102
98
|
if (!list.includes(this))
|
|
@@ -117,6 +113,19 @@ export function instrumentAcceleratedCanvases() {
|
|
|
117
113
|
return ctx;
|
|
118
114
|
};
|
|
119
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* WebGL contexts keep their drawing buffer after a frame is presented, so drawImage(glCanvas) reads the frame
|
|
118
|
+
* instead of blank. Must be registered via page.evaluateOnNewDocument BEFORE navigation.
|
|
119
|
+
*/
|
|
120
|
+
export function forcePreservedDrawingBuffer() {
|
|
121
|
+
const orig = HTMLCanvasElement.prototype.getContext;
|
|
122
|
+
// oxlint-disable-next-line no-explicit-any
|
|
123
|
+
HTMLCanvasElement.prototype.getContext = function (type, attrs) {
|
|
124
|
+
const isGl = type === "webgl" || type === "webgl2" || type === "experimental-webgl";
|
|
125
|
+
// oxlint-disable-next-line no-explicit-any
|
|
126
|
+
return orig.call(this, type, isGl ? { ...attrs, preserveDrawingBuffer: true } : attrs);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
120
129
|
/**
|
|
121
130
|
* Low-cardinality bucket for a raw WebGL renderer string: `<backend>/<vendor>`
|
|
122
131
|
* (e.g. `metal/apple`, `d3d11/nvidia`, `swiftshader/other`).
|
|
@@ -18,7 +18,7 @@ import { describePageMotionBlur, describePageMotionBlurAtEnd, installPageMotionB
|
|
|
18
18
|
// ── Extracted modules ───────────────────────────────────────────────────────
|
|
19
19
|
import { acquireBrowser, releaseBrowser, forceReleaseBrowser, buildChromeArgs, resolveBrowserGpuMode, captureWithoutWaylandDisplay, resolveHeadlessShellPath, compositionRequiresWebGpu, assertWebGpuAdapterAvailable, usesSoftwareWebGpu, } from "./browserManager.js";
|
|
20
20
|
import { beginFrameCapture, ensureRenderFrameSiblings, getCdpSession, pageContentExceedsCaptureHeight, pageScreenshotCapture, initTransparentBackground, capturesAlpha, shouldDefaultCaptureBeyondViewport, } from "./screenshotService.js";
|
|
21
|
-
import { classifyGpuRenderer, detectGpuBackend, injectDrawElementCanvas, captureDrawElementFrame, resolveDrawElementCaptureMode, instrumentAcceleratedCanvases, initDrawElementWorkerEncode, cleanupDrawElementWorkerEncode, produceDrawElementFrame, produceDrawElementFrameBatch, DE_CANVAS_NOT_INITIALIZED_CODE, } from "./drawElementService.js";
|
|
21
|
+
import { classifyGpuRenderer, detectGpuBackend, injectDrawElementCanvas, captureDrawElementFrame, resolveDrawElementCaptureMode, forcePreservedDrawingBuffer, instrumentAcceleratedCanvases, initDrawElementWorkerEncode, cleanupDrawElementWorkerEncode, produceDrawElementFrame, produceDrawElementFrameBatch, DE_CANVAS_NOT_INITIALIZED_CODE, } from "./drawElementService.js";
|
|
22
22
|
import { initThreeDProjection, scanCssEffects } from "./threeDProjection.js";
|
|
23
23
|
import { planRiskFrames, scanCssEffectLiveFrames, riskSessionFits, withFramesAfterCallback, MAX_RISK_FRAME_FRACTION, } from "./riskFrames.js";
|
|
24
24
|
import { getAvailableMemoryMb } from "./systemMemory.js";
|
|
@@ -1006,6 +1006,7 @@ async function constructCaptureSession(input) {
|
|
|
1006
1006
|
// composites their live content via drawImage instead (see
|
|
1007
1007
|
// instrumentAcceleratedCanvases). Must be registered before navigation.
|
|
1008
1008
|
if (useDrawElement) {
|
|
1009
|
+
await page.evaluateOnNewDocument(forcePreservedDrawingBuffer);
|
|
1009
1010
|
await page.evaluateOnNewDocument(instrumentAcceleratedCanvases);
|
|
1010
1011
|
}
|
|
1011
1012
|
// The opacity → autoAlpha tween rewrite was RETIRED with the requestPaint
|
|
@@ -83,6 +83,8 @@ export interface WorkerSizingConfig extends Partial<Pick<EngineConfig, "concurre
|
|
|
83
83
|
* than a plain DOM screenshot. Explicit --workers requests ignore this hint.
|
|
84
84
|
*/
|
|
85
85
|
captureCostMultiplier?: number;
|
|
86
|
+
/** macOS, hardware GPU, screenshot frames written to disk (not streamed through this process). */
|
|
87
|
+
appleGpuDiskCapture?: boolean;
|
|
86
88
|
}
|
|
87
89
|
type WorkerBrowserPoolDecision = {
|
|
88
90
|
parallel?: boolean;
|
|
@@ -32,6 +32,10 @@ const HEAP_RESERVED_MB = 1024;
|
|
|
32
32
|
// default heap ⇒ >~500MB/worker + base. Caps auto sizing; an explicit
|
|
33
33
|
// `--workers N` is still the operator's call.
|
|
34
34
|
const HEAP_PER_WORKER_MB = 640;
|
|
35
|
+
// Apple-GPU disk capture measured ~15-25 MB parent heap per worker; 14 cores kept gaining to 8.
|
|
36
|
+
const APPLE_DISK_HEAP_PER_WORKER_MB = 128;
|
|
37
|
+
const APPLE_DISK_CORES_PER_WORKER = 1.75;
|
|
38
|
+
const APPLE_DISK_MAX_WORKERS = 8;
|
|
35
39
|
const MIN_WORKERS = 1;
|
|
36
40
|
const MAX_WORKER_DIAGNOSTIC_LINES = 8;
|
|
37
41
|
// Hard ceiling on explicit `--workers N` requests. Above this, the cost of
|
|
@@ -133,7 +137,9 @@ export function computeWorkerSizing(totalFrames, requested, config) {
|
|
|
133
137
|
// is immediately reclaimable.
|
|
134
138
|
const totalMemoryMb = getSystemTotalMb();
|
|
135
139
|
const heapLimitMb = Math.round(getHeapStatistics().heap_size_limit / (1024 * 1024));
|
|
136
|
-
const
|
|
140
|
+
const appleDisk = config?.appleGpuDiskCapture === true;
|
|
141
|
+
const heapBasedWorkers = Math.max(1, Math.floor((heapLimitMb - HEAP_RESERVED_MB) /
|
|
142
|
+
(appleDisk ? APPLE_DISK_HEAP_PER_WORKER_MB : HEAP_PER_WORKER_MB)));
|
|
137
143
|
const cpuBasedWorkers = Math.max(1, cpuCount - 2);
|
|
138
144
|
const memoryBasedWorkers = Math.max(1, Math.floor((totalMemoryMb * 0.5) / MEMORY_PER_WORKER_MB));
|
|
139
145
|
const frameBasedWorkers = Math.floor(totalFrames / MIN_FRAMES_PER_WORKER);
|
|
@@ -164,9 +170,14 @@ export function computeWorkerSizing(totalFrames, requested, config) {
|
|
|
164
170
|
if (concurrency !== "auto") {
|
|
165
171
|
return Math.max(MIN_WORKERS, Math.min(ABSOLUTE_MAX_WORKERS, Math.floor(concurrency)));
|
|
166
172
|
}
|
|
167
|
-
return
|
|
173
|
+
return appleDisk
|
|
174
|
+
? Math.max(defaultSafeMaxWorkers(), APPLE_DISK_MAX_WORKERS)
|
|
175
|
+
: defaultSafeMaxWorkers();
|
|
168
176
|
})();
|
|
169
|
-
const
|
|
177
|
+
const configuredCoresPerWorker = config?.coresPerWorker ?? DEFAULT_CONFIG.coresPerWorker;
|
|
178
|
+
const effectiveCoresPerWorker = appleDisk && configuredCoresPerWorker === DEFAULT_CONFIG.coresPerWorker
|
|
179
|
+
? APPLE_DISK_CORES_PER_WORKER
|
|
180
|
+
: configuredCoresPerWorker;
|
|
170
181
|
const effectiveMinParallelFrames = config?.minParallelFrames ?? DEFAULT_CONFIG.minParallelFrames;
|
|
171
182
|
const effectiveLargeRenderThreshold = config?.largeRenderThreshold ?? DEFAULT_CONFIG.largeRenderThreshold;
|
|
172
183
|
if (totalFrames < MIN_FRAMES_PER_WORKER * 2) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Server } from "node:net";
|
|
2
|
+
export declare const isBrowserSafePort: (port: number) => boolean;
|
|
3
|
+
type Bindable = Pick<Server, "address" | "close" | "listen" | "once" | "off">;
|
|
4
|
+
/** Rebinds an already-listening server until the OS hands it a port a browser will load from. */
|
|
5
|
+
export declare function settleOnBrowserSafePort(server: Bindable, host: string): Promise<number>;
|
|
6
|
+
/** `server.listen(0, host)` that never lands on a port Chromium blocks; resolves to the bound port. */
|
|
7
|
+
export declare function listenOnBrowserSafePort(server: Bindable, host: string): Promise<number>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Chromium refuses to load from these ports (net::ERR_UNSAFE_PORT); net/base/port_util.cc kRestrictedPorts.
|
|
2
|
+
const RESTRICTED_PORTS = new Set([
|
|
3
|
+
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102,
|
|
4
|
+
103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465,
|
|
5
|
+
512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993,
|
|
6
|
+
995, 1719, 1720, 1723, 2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669,
|
|
7
|
+
6697, 10080,
|
|
8
|
+
]);
|
|
9
|
+
const MAX_ATTEMPTS = 32;
|
|
10
|
+
export const isBrowserSafePort = (port) => !RESTRICTED_PORTS.has(port);
|
|
11
|
+
function listenOnce(server, host) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
server.once("error", reject);
|
|
14
|
+
server.listen(0, host, () => {
|
|
15
|
+
server.off("error", reject);
|
|
16
|
+
resolve(server.address().port);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function closeServer(server) {
|
|
21
|
+
return new Promise((resolve) => server.close(() => resolve()));
|
|
22
|
+
}
|
|
23
|
+
/** Rebinds an already-listening server until the OS hands it a port a browser will load from. */
|
|
24
|
+
export async function settleOnBrowserSafePort(server, host) {
|
|
25
|
+
let port = server.address().port;
|
|
26
|
+
for (let attempt = 1; !isBrowserSafePort(port); attempt++) {
|
|
27
|
+
if (attempt >= MAX_ATTEMPTS)
|
|
28
|
+
throw new Error(`The OS kept offering browser-restricted ports (last: ${port}).`);
|
|
29
|
+
await closeServer(server);
|
|
30
|
+
port = await listenOnce(server, host);
|
|
31
|
+
}
|
|
32
|
+
return port;
|
|
33
|
+
}
|
|
34
|
+
/** `server.listen(0, host)` that never lands on a port Chromium blocks; resolves to the bound port. */
|
|
35
|
+
export async function listenOnBrowserSafePort(server, host) {
|
|
36
|
+
await listenOnce(server, host);
|
|
37
|
+
return settleOnBrowserSafePort(server, host);
|
|
38
|
+
}
|
package/dist/utils/ffprobe.js
CHANGED
|
@@ -296,7 +296,10 @@ function ffprobeMajor() {
|
|
|
296
296
|
runtimeFfprobeMajor ??= new Promise((resolve) => {
|
|
297
297
|
let out = "";
|
|
298
298
|
const binary = getFfprobeBinary();
|
|
299
|
-
const proc = spawn(binary, ["-version"], {
|
|
299
|
+
const proc = spawn(binary, ["-version"], {
|
|
300
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
301
|
+
windowsHide: true,
|
|
302
|
+
});
|
|
300
303
|
proc.stdout.on("data", (chunk) => (out += String(chunk)));
|
|
301
304
|
proc.on("error", () => resolve(undefined));
|
|
302
305
|
proc.on("close", () => resolve(Number(/version n?(\d+)/.exec(out)?.[1]) || undefined));
|
|
@@ -10,6 +10,7 @@ export declare function getCompiledGpuEncoders(ffmpegEncodersStdout: string): Co
|
|
|
10
10
|
export declare function selectUsableGpuEncoder(candidates: readonly ConcreteGpuEncoder[], isUsable: (encoder: ConcreteGpuEncoder) => Promise<boolean>): Promise<GpuEncoder>;
|
|
11
11
|
export declare function detectGpuEncoder(): Promise<GpuEncoder>;
|
|
12
12
|
export declare function getCachedGpuEncoder(): Promise<GpuEncoder>;
|
|
13
|
+
export declare function resetCachedGpuEncoder(): void;
|
|
13
14
|
export declare function getGpuEncoderName(encoder: GpuEncoder, codec: "h264" | "h265"): string;
|
|
14
15
|
export declare function buildVideoToolboxRateControlArgs(input: {
|
|
15
16
|
bitrate?: string;
|
package/dist/utils/gpuEncoder.js
CHANGED
|
@@ -64,12 +64,13 @@ export async function detectGpuEncoder() {
|
|
|
64
64
|
return selectUsableGpuEncoder(candidates, canUseGpuEncoder).catch(() => null);
|
|
65
65
|
}
|
|
66
66
|
let cachedGpuEncoder = undefined;
|
|
67
|
-
export
|
|
68
|
-
|
|
69
|
-
cachedGpuEncoder = await detectGpuEncoder();
|
|
70
|
-
}
|
|
67
|
+
export function getCachedGpuEncoder() {
|
|
68
|
+
cachedGpuEncoder ??= detectGpuEncoder();
|
|
71
69
|
return cachedGpuEncoder;
|
|
72
70
|
}
|
|
71
|
+
export function resetCachedGpuEncoder() {
|
|
72
|
+
cachedGpuEncoder = undefined;
|
|
73
|
+
}
|
|
73
74
|
export function getGpuEncoderName(encoder, codec) {
|
|
74
75
|
if (!encoder)
|
|
75
76
|
return codec === "h264" ? "libx264" : "libx265";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.212",
|
|
4
4
|
"description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"@hono/node-server": "^2.0.5",
|
|
53
53
|
"hono": "^4.6.0",
|
|
54
54
|
"linkedom": "^0.18.12",
|
|
55
|
-
"puppeteer": "
|
|
56
|
-
"puppeteer-core": "^25.
|
|
57
|
-
"@hyperframes/core": "^0.8.
|
|
58
|
-
"@hyperframes/parsers": "^0.8.
|
|
55
|
+
"puppeteer": "~25.11.0",
|
|
56
|
+
"puppeteer-core": "^25.11.0",
|
|
57
|
+
"@hyperframes/core": "^0.8.212",
|
|
58
|
+
"@hyperframes/parsers": "^0.8.212"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^25.0.10",
|