@hyperframes/engine 0.8.199 → 0.8.201

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/config.d.ts CHANGED
@@ -163,9 +163,9 @@ export interface EngineConfig {
163
163
  * renders stream. Env: PRODUCER_STREAMING_ENCODE_DURATION_CAP_ENABLED.
164
164
  */
165
165
  streamingEncodeDurationCapEnabled: boolean;
166
- /** Timeout for FFmpeg frame encoding (ms). Default: 600_000 */
166
+ /** How long a disk-frame encode may go without a new frame before it is stopped (ms). Default: 600_000 */
167
167
  ffmpegEncodeTimeout: number;
168
- /** FFmpeg mux/faststart timeout (ms); extraction waits this long for each new frame. Default: 300_000 */
168
+ /** How long any other FFmpeg step (audio, mux, extraction) may go without progress (ms). Default: 300_000 */
169
169
  ffmpegProcessTimeout: number;
170
170
  /**
171
171
  * Inactivity timeout for FFmpeg streaming encode (ms). The timer resets on
package/dist/index.d.ts CHANGED
@@ -42,11 +42,12 @@ export { createCaptureSession, initializeSession, closeCaptureSession, captureFr
42
42
  export { CaptureFailure, classifyCaptureFailure, isFatalCaptureFailure, type CaptureFailureKind, type CaptureWorkerDiagnostic, } from "./services/captureFailure.js";
43
43
  export { createChromeMemorySampler, type ChromeMemoryStats, } from "./services/chromeMemorySampler.js";
44
44
  export { installMediaRenderIdBridge } from "./services/mediaRenderIdBridge.js";
45
- export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFramesBatch, syncVideoFrameVisibility, cdpSessionCache, probeBeginFrameLiveness, initTransparentBackground, captureAlphaPng, applyDomLayerMask, removeDomLayerMask, DOM_LAYER_MASK_STYLE_ID, type BeginFrameResult, } from "./services/screenshotService.js";
45
+ 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
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
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";
50
+ export { isSelfOrAncestorHidden } from "./services/mediaHidden.js";
50
51
  export { directorySizeBytes, gcExtractionCache } from "./services/extractionCache.js";
51
52
  export { resolveReferencedStart, resolveReferencedDuration, type RefResolverEl, type RefResolverDoc, } from "./services/referenceResolver.js";
52
53
  export { createVideoFrameInjector } from "./services/videoFrameInjector.js";
package/dist/index.js CHANGED
@@ -45,12 +45,13 @@ export { CaptureFailure, classifyCaptureFailure, isFatalCaptureFailure, } from "
45
45
  export { createChromeMemorySampler, } from "./services/chromeMemorySampler.js";
46
46
  // ── Screenshot (BeginFrame) ─────────────────────────────────────────────────────
47
47
  export { installMediaRenderIdBridge } from "./services/mediaRenderIdBridge.js";
48
- export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFramesBatch, syncVideoFrameVisibility, cdpSessionCache, probeBeginFrameLiveness, initTransparentBackground, captureAlphaPng, applyDomLayerMask, removeDomLayerMask, DOM_LAYER_MASK_STYLE_ID, } from "./services/screenshotService.js";
48
+ export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFramesBatch, syncVideoFrameVisibility, cdpSessionCache, probeBeginFrameLiveness, initTransparentBackground, capturesAlpha, captureAlphaPng, applyDomLayerMask, removeDomLayerMask, DOM_LAYER_MASK_STYLE_ID, } from "./services/screenshotService.js";
49
49
  // ── Encoding ───────────────────────────────────────────────────────────────────
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
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";
54
+ export { isSelfOrAncestorHidden } from "./services/mediaHidden.js";
54
55
  export { directorySizeBytes, gcExtractionCache } from "./services/extractionCache.js";
55
56
  export { resolveReferencedStart, resolveReferencedDuration, } from "./services/referenceResolver.js";
56
57
  export { createVideoFrameInjector } from "./services/videoFrameInjector.js";
@@ -339,7 +339,7 @@ function ffmpegFailure(stage, result, timeoutMs, elementId) {
339
339
  reason = "cancelled";
340
340
  owner = "user";
341
341
  }
342
- else if (result.terminationReason === "deadline" || result.terminationReason === "inactivity") {
342
+ else if (result.terminationReason === "inactivity") {
343
343
  reason = "ffmpeg_timeout";
344
344
  retryable = true;
345
345
  }
@@ -692,14 +692,8 @@ async function generateSilence(outputPath, duration, signal, config) {
692
692
  failure,
693
693
  };
694
694
  }
695
- /** A mix graph's work grows with the film length (about 11 ms per film second at 96 inputs); so does its deadline. */
696
- const MIX_TIMEOUT_MS_PER_FILM_SECOND = 100;
697
- function mixProcessTimeout(config, totalDuration) {
698
- const configured = config?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
699
- return Math.max(configured, Math.ceil(totalDuration * MIX_TIMEOUT_MS_PER_FILM_SECOND));
700
- }
701
695
  async function mixAudioTracksFlat(tracks, outputPath, totalDuration, signal, config) {
702
- const ffmpegProcessTimeout = mixProcessTimeout(config, totalDuration);
696
+ const ffmpegProcessTimeout = config?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
703
697
  const masterOutputGain = config?.audioGain ?? DEFAULT_CONFIG.audioGain;
704
698
  if (tracks.length === 0) {
705
699
  const result = await generateSilence(outputPath, totalDuration, signal, config);
@@ -876,7 +870,7 @@ function groupNormalizeOptionUnsupported(stderr) {
876
870
  * only runs when this ffmpeg build's `amix` rejects the option.
877
871
  */
878
872
  async function mixGroupMembersFlat(memberTracks, outputPath, totalDuration, signal, config) {
879
- const ffmpegProcessTimeout = mixProcessTimeout(config, totalDuration);
873
+ const ffmpegProcessTimeout = config?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
880
874
  const outputDir = dirname(outputPath);
881
875
  if (!existsSync(outputDir))
882
876
  mkdirSync(outputDir, { recursive: true });
@@ -124,13 +124,13 @@ export declare function usesSoftwareWebGpu(requiresWebGpu: boolean | undefined,
124
124
  export declare function buildChromeArgs(options: BuildChromeArgsOptions, config?: GpuConfig): string[];
125
125
  /** Does the root, or a mounted root (data-hf-inner-root once bundled), declare `data-requires-webgpu`? */
126
126
  export declare function compositionRequiresWebGpu(html: string): boolean;
127
- /** No hardware WebGPU adapter on this host; distinct from a browser or navigation failure. */
127
+ /** No usable WebGPU device on this launch; distinct from a browser or navigation failure. */
128
128
  export declare class WebGpuUnavailableError extends Error {
129
129
  constructor();
130
130
  }
131
131
  /**
132
- * Confirms the already-navigated page can obtain a hardware WebGPU adapter, or any adapter when the launch runs
133
- * software WebGPU ({@link usesSoftwareWebGpu}); no-op otherwise.
132
+ * Confirms the already-navigated page can create a device on a hardware WebGPU adapter, or any adapter when
133
+ * the launch runs software WebGPU ({@link usesSoftwareWebGpu}); no-op otherwise.
134
134
  * `page` must be on a secure-context origin, or navigator.gpu always reads absent.
135
135
  */
136
136
  export declare function assertWebGpuAdapterAvailable(page: Page, requiresWebGpu: boolean, softwareWebGpu?: boolean): Promise<void>;
@@ -666,7 +666,7 @@ async function launchBrowser(fingerprint) {
666
666
  });
667
667
  const browserVersion = await browser.version().catch(() => "unknown");
668
668
  const gpuFlags = fingerprint.args.filter((a) => a.startsWith("--use-gl=") || a.startsWith("--use-angle="));
669
- console.log(`[BrowserManager] Browser launched (${browserVersion}, ${captureMode}, gl=${gpuFlags.join(" ") || "default"}, headlessShell=${!!fingerprint.executablePath}, platform=${process.platform})`);
669
+ console.error(`[BrowserManager] Browser launched (${browserVersion}, ${captureMode}, gl=${gpuFlags.join(" ") || "default"}, headlessShell=${!!fingerprint.executablePath}, platform=${process.platform})`);
670
670
  if (captureMode === "beginframe") {
671
671
  const probe = await probeBeginFrameSupport(browser).catch((error) => ({
672
672
  supported: true,
@@ -909,43 +909,63 @@ export function compositionRequiresWebGpu(html) {
909
909
  }
910
910
  return false;
911
911
  }
912
- /** No hardware WebGPU adapter on this host; distinct from a browser or navigation failure. */
912
+ /** No usable WebGPU device on this launch; distinct from a browser or navigation failure. */
913
913
  export class WebGpuUnavailableError extends Error {
914
914
  constructor() {
915
- super("This composition declares data-requires-webgpu, but no hardware WebGPU adapter could be " +
916
- "obtained on this browser launch. Run on a host with a GPU, set " +
915
+ super("This composition declares data-requires-webgpu, but a usable WebGPU device could not be " +
916
+ "created on this browser launch. Run on a host with a GPU, set " +
917
917
  "PRODUCER_ALLOW_SOFTWARE_WEBGPU=true to render it on SwiftShader (slow), or remove " +
918
918
  "data-requires-webgpu.");
919
919
  this.name = "WebGpuUnavailableError";
920
920
  }
921
921
  }
922
- const WEBGPU_ADAPTER_PROBE_TIMEOUT_MS = 10_000;
922
+ const WEBGPU_DEVICE_PROBE_TIMEOUT_MS = 10_000;
923
923
  /**
924
- * Confirms the already-navigated page can obtain a hardware WebGPU adapter, or any adapter when the launch runs
925
- * software WebGPU ({@link usesSoftwareWebGpu}); no-op otherwise.
924
+ * Confirms the already-navigated page can create a device on a hardware WebGPU adapter, or any adapter when
925
+ * the launch runs software WebGPU ({@link usesSoftwareWebGpu}); no-op otherwise.
926
926
  * `page` must be on a secure-context origin, or navigator.gpu always reads absent.
927
927
  */
928
928
  export async function assertWebGpuAdapterAvailable(page, requiresWebGpu, softwareWebGpu = false) {
929
929
  if (!requiresWebGpu)
930
930
  return;
931
- // requestAdapter() has no native timeout; a broken driver can hang it
932
- // indefinitely. Race it in-page so a stuck adapter reads as "unavailable"
933
- // instead of hanging the caller.
934
- const hasUsableAdapter = await page.evaluate(async (timeoutMs, acceptFallback) => {
931
+ // An adapter can exist even when missing driver libraries make device creation fail.
932
+ // Both requests can hang; share one deadline and release late devices as well.
933
+ const hasUsableDevice = await page.evaluate(async (timeoutMs, acceptFallback) => {
935
934
  if (typeof navigator === "undefined" || !navigator.gpu)
936
935
  return false;
936
+ let expired = false;
937
+ let timer;
937
938
  try {
938
- const adapter = await Promise.race([
939
- navigator.gpu.requestAdapter(),
940
- new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs)),
939
+ const probe = async () => {
940
+ const adapter = await navigator.gpu.requestAdapter();
941
+ if (!adapter || expired || (!acceptFallback && adapter.info.isFallbackAdapter))
942
+ return false;
943
+ const device = await adapter.requestDevice();
944
+ try {
945
+ return !expired;
946
+ }
947
+ finally {
948
+ device.destroy();
949
+ }
950
+ };
951
+ return await Promise.race([
952
+ probe(),
953
+ new Promise((resolve) => {
954
+ timer = setTimeout(() => {
955
+ expired = true;
956
+ resolve(false);
957
+ }, timeoutMs);
958
+ }),
941
959
  ]);
942
- return !!adapter && (acceptFallback || !adapter.info.isFallbackAdapter);
943
960
  }
944
961
  catch {
945
962
  return false;
946
963
  }
947
- }, WEBGPU_ADAPTER_PROBE_TIMEOUT_MS, softwareWebGpu);
948
- if (!hasUsableAdapter)
964
+ finally {
965
+ clearTimeout(timer);
966
+ }
967
+ }, WEBGPU_DEVICE_PROBE_TIMEOUT_MS, softwareWebGpu);
968
+ if (!hasUsableDevice)
949
969
  throw new WebGpuUnavailableError();
950
970
  }
951
971
  function getBrowserGpuArgs(mode, platform) {
@@ -27,15 +27,9 @@ export const ENCODER_PRESETS = {
27
27
  function appendEncodeTimeoutMessage(error, timedOut, timeoutMs) {
28
28
  if (!timedOut)
29
29
  return error;
30
- // Two independent reports of this exact timeout, both resolved by env vars
31
- // that already exist but aren't named anywhere the user would see them at
32
- // the point of failure — they had to go find FFMPEG_ENCODE_TIMEOUT_MS and
33
- // PRODUCER_ENABLE_CHUNKED_ENCODE themselves. Name both here instead of
34
- // just stating what happened.
35
- return (`${error}\nFFmpeg killed after exceeding ffmpegEncodeTimeout (${timeoutMs} ms). ` +
36
- "Long or high-frame-count renders may need more time: set FFMPEG_ENCODE_TIMEOUT_MS " +
37
- "to a higher value (ms), or set PRODUCER_ENABLE_CHUNKED_ENCODE=true to encode in " +
38
- "smaller chunks instead of one long-running ffmpeg process.");
30
+ return (`${error}\nFFmpeg killed after writing no new frame for ffmpegEncodeTimeout (${timeoutMs} ms). ` +
31
+ "An encode that is still writing frames is never stopped; if this encoder legitimately " +
32
+ "pauses longer between frames, raise FFMPEG_ENCODE_TIMEOUT_MS (ms).");
39
33
  }
40
34
  function isAacSidecar(audioPath) {
41
35
  return extname(audioPath).toLowerCase() === ".aac";
@@ -430,7 +424,7 @@ export async function encodeFramesFromDir(framesDir, framePattern, outputPath, o
430
424
  framesEncoded: 0,
431
425
  fileSize: 0,
432
426
  error: appendEncodeTimeoutMessage(frameReadFailure(result.stderr, framePattern, 0, framesRead, frameCount) +
433
- formatFfmpegError(result.exitCode, result.stderr), result.terminationReason === "deadline", encodeTimeout),
427
+ formatFfmpegError(result.exitCode, result.stderr), result.terminationReason === "inactivity", encodeTimeout),
434
428
  failureReason: isExternalFfmpegInterruption(result) ? "external_interruption" : undefined,
435
429
  };
436
430
  }
@@ -492,7 +486,7 @@ export async function concatVideoFiles(inputPaths, outputPath, signal, config) {
492
486
  return { success: true };
493
487
  return {
494
488
  success: false,
495
- error: appendEncodeTimeoutMessage(`Chunk concat failed: ${result.stderr.slice(-400)}`, result.terminationReason === "deadline", encodeTimeout),
489
+ error: appendEncodeTimeoutMessage(`Chunk concat failed: ${result.stderr.slice(-400)}`, result.terminationReason === "inactivity", encodeTimeout),
496
490
  externalInterruption: isExternalFfmpegInterruption(result),
497
491
  };
498
492
  }
@@ -566,7 +560,7 @@ export async function encodeFramesChunkedConcat(framesDir, framePattern, outputP
566
560
  success: processResult.success,
567
561
  error: processResult.success
568
562
  ? undefined
569
- : appendEncodeTimeoutMessage(frameReadFailure(processResult.stderr, framePattern, startNumber, framesRead, framesInChunk) + `Chunk ${i} encode failed: ${processResult.stderr.slice(-400)}`, processResult.terminationReason === "deadline", encodeTimeout),
563
+ : appendEncodeTimeoutMessage(frameReadFailure(processResult.stderr, framePattern, startNumber, framesRead, framesInChunk) + `Chunk ${i} encode failed: ${processResult.stderr.slice(-400)}`, processResult.terminationReason === "inactivity", encodeTimeout),
570
564
  };
571
565
  if (!chunkResult.success) {
572
566
  return {
@@ -19,7 +19,10 @@ export type { CaptureOptions, CaptureResult, CaptureBufferResult, CapturePerfSum
19
19
  /** Called after seeking, before screenshot. Use for video frame injection or other pre-capture work.
20
20
  * `heldVideoTime` (motion-blur samples only) is the output frame's time: a video on screen at
21
21
  * both times shows its frame for `heldVideoTime`, so footage does not smear. */
22
- export type BeforeCaptureHook = (page: Page, time: number, heldVideoTime?: number) => Promise<void>;
22
+ export type BeforeCaptureHook = ((page: Page, time: number, heldVideoTime?: number) => Promise<void>) & {
23
+ /** A hook with state of its own, for a second page; absent when the hook cannot be copied. */
24
+ fresh?: () => BeforeCaptureHook | null;
25
+ };
23
26
  /** The per-frame wait a capture is in, worded for the stall error a user reads. */
24
27
  type CaptureStep = "seeking the timeline" | "decoding CSS background images" | "injecting video frames" | "waiting for page seek handlers" | "loading color-grading LUTs" | "compositing the page" | "capturing the frame";
25
28
  export interface CaptureSession {
@@ -161,6 +164,13 @@ export interface CaptureSession {
161
164
  * Empty/undefined when the predictor produces no frames.
162
165
  */
163
166
  clipBoundaryFrames?: Set<number>;
167
+ /**
168
+ * Frames drawElement cannot reproduce, for the caller to capture with a screenshot session.
169
+ * Set only when the caller can (worker-encode pipeline); drawElement capture throws on these.
170
+ */
171
+ deRiskFrames?: Set<number>;
172
+ /** The effects behind {@link deRiskFrames}, for the capture log. */
173
+ deRiskReason?: string;
164
174
  /** Rolling drawElement frame byte-sizes (last ~60), for silent-blank-drop detection:
165
175
  * drawElement intermittently returns an anomalously small (blank) frame with no
166
176
  * throw; a frame far below the running median is re-captured via screenshot. */