@hyperframes/engine 0.8.172 → 0.8.173

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
@@ -404,6 +404,8 @@ export declare function explainDrawElementDisabled(args: {
404
404
  workerEncode: boolean;
405
405
  chromeCeiling?: number;
406
406
  }): "unsupported_platform" | "old_chrome" | "software_gpu" | "worker_encode_off" | "disabled";
407
+ /** `pageNavigationTimeout` alone, for callers that need the page-load budget without the rest of the config. */
408
+ export declare function resolvePageNavigationTimeout(): number;
407
409
  export declare function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig;
408
410
  /**
409
411
  * Runtime-resolved companion to the software-GPU screenshot clamp in
package/dist/config.js CHANGED
@@ -444,6 +444,12 @@ export function explainDrawElementDisabled(args) {
444
444
  return "worker_encode_off";
445
445
  return "disabled";
446
446
  }
447
+ /** `pageNavigationTimeout` alone, for callers that need the page-load budget without the rest of the config. */
448
+ export function resolvePageNavigationTimeout() {
449
+ const raw = process.env.PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS;
450
+ const parsed = raw === undefined || raw === "" ? NaN : Number(raw);
451
+ return Number.isFinite(parsed) ? parsed : DEFAULT_CONFIG.pageNavigationTimeout;
452
+ }
447
453
  export function resolveConfig(overrides) {
448
454
  const env = (key) => process.env[key];
449
455
  const envNum = (key, fallback) => {
@@ -528,7 +534,7 @@ export function resolveConfig(overrides) {
528
534
  frameDataUriCacheBytesLimitMb: Math.max(64, envNum("PRODUCER_FRAME_DATA_URI_CACHE_BYTES_MB", memoryAdaptiveCacheBytesMb())),
529
535
  playerReadyTimeout: envNum("PRODUCER_PLAYER_READY_TIMEOUT_MS", DEFAULT_CONFIG.playerReadyTimeout),
530
536
  renderReadyTimeout: envNum("PRODUCER_RENDER_READY_TIMEOUT_MS", DEFAULT_CONFIG.renderReadyTimeout),
531
- pageNavigationTimeout: envNum("PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS", DEFAULT_CONFIG.pageNavigationTimeout),
537
+ pageNavigationTimeout: resolvePageNavigationTimeout(),
532
538
  verifyRuntime: env("PRODUCER_VERIFY_HYPERFRAME_RUNTIME") !== "false",
533
539
  runtimeManifestPath: env("PRODUCER_HYPERFRAME_MANIFEST_PATH"),
534
540
  extractCacheDir: resolveExtractCacheDirFromEnv(env),
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@
30
30
  *
31
31
  */
32
32
  export type { HfProtocol, HfMediaElement, HfTransitionMeta, CaptureOptions, CaptureVideoMetadataHint, CaptureResult, CaptureBufferResult, CapturePerfSummary, CaptureWarning, CaptureWarningCode, SubTimelineWaitMemo, SubTimelineWaitOutcome, } from "./types.js";
33
- export { resolveConfig, validateEngineConfigSnapshot, DEFAULT_CONFIG, scaleProtocolTimeoutForComposition, shouldClampToScreenshotForConcreteGpu, applyConcreteGpuScreenshotClamp, explainDrawElementDisabled, resolveExtractCacheDir, defaultExtractCacheDir, EXTRACT_CACHE_DIR_DISABLED_ALIASES, type EngineConfig, type ExtractCacheDirResolution, } from "./config.js";
33
+ export { resolveConfig, resolvePageNavigationTimeout, validateEngineConfigSnapshot, DEFAULT_CONFIG, scaleProtocolTimeoutForComposition, shouldClampToScreenshotForConcreteGpu, applyConcreteGpuScreenshotClamp, explainDrawElementDisabled, resolveExtractCacheDir, defaultExtractCacheDir, EXTRACT_CACHE_DIR_DISABLED_ALIASES, type EngineConfig, type ExtractCacheDirResolution, } from "./config.js";
34
34
  export { SDR_RGB_TO_BT709_FILTER, SDR_RGB_TO_TAGGED_BT709_FILTER, } from "./utils/sdrCaptureColor.js";
35
35
  export { DEFAULT_VP9_CPU_USED, MAX_VP9_CPU_USED, MIN_VP9_CPU_USED, normalizeVp9CpuUsed, } from "./services/vp9Options.js";
36
36
  export { getCgroupMemoryLimitMb, getSystemTotalMb, isLowMemorySystem, LOW_MEMORY_TOTAL_MB_THRESHOLD, } from "./services/systemMemory.js";
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@
30
30
  *
31
31
  */
32
32
  // ── Configuration ──────────────────────────────────────────────────────────────
33
- export { resolveConfig, validateEngineConfigSnapshot, DEFAULT_CONFIG, scaleProtocolTimeoutForComposition, shouldClampToScreenshotForConcreteGpu, applyConcreteGpuScreenshotClamp, explainDrawElementDisabled, resolveExtractCacheDir, defaultExtractCacheDir, EXTRACT_CACHE_DIR_DISABLED_ALIASES, } from "./config.js";
33
+ export { resolveConfig, resolvePageNavigationTimeout, validateEngineConfigSnapshot, DEFAULT_CONFIG, scaleProtocolTimeoutForComposition, shouldClampToScreenshotForConcreteGpu, applyConcreteGpuScreenshotClamp, explainDrawElementDisabled, resolveExtractCacheDir, defaultExtractCacheDir, EXTRACT_CACHE_DIR_DISABLED_ALIASES, } from "./config.js";
34
34
  export { SDR_RGB_TO_BT709_FILTER, SDR_RGB_TO_TAGGED_BT709_FILTER, } from "./utils/sdrCaptureColor.js";
35
35
  export { DEFAULT_VP9_CPU_USED, MAX_VP9_CPU_USED, MIN_VP9_CPU_USED, normalizeVp9CpuUsed, } from "./services/vp9Options.js";
36
36
  export { getCgroupMemoryLimitMb, getSystemTotalMb, isLowMemorySystem, LOW_MEMORY_TOTAL_MB_THRESHOLD, } from "./services/systemMemory.js";
@@ -42,7 +42,7 @@ export declare const GC_MARKER = ".hf-last-gc";
42
42
  * Current schema version. Bump it whenever extraction writes different frames for the same key,
43
43
  * or warm entries keep serving the old frames across a deploy. Each bump's commit says why.
44
44
  */
45
- export declare const SCHEMA_PREFIX = "hfcache-v7-";
45
+ export declare const SCHEMA_PREFIX = "hfcache-v8-";
46
46
  export type CacheFrameFormat = "jpg" | "png";
47
47
  export interface CacheKeyInput {
48
48
  /** Absolute path to the source video file. Part of the key so moved files
@@ -45,7 +45,7 @@ export const GC_MARKER = ".hf-last-gc";
45
45
  * Current schema version. Bump it whenever extraction writes different frames for the same key,
46
46
  * or warm entries keep serving the old frames across a deploy. Each bump's commit says why.
47
47
  */
48
- export const SCHEMA_PREFIX = "hfcache-v7-";
48
+ export const SCHEMA_PREFIX = "hfcache-v8-";
49
49
  /** Truncated hex chars of SHA-256 used for the entry directory name. */
50
50
  const KEY_HEX_CHARS = 16;
51
51
  export async function readContentSha256(path, signal) {
@@ -437,6 +437,14 @@ function chromeGuessForUntaggedMatrix(metadata) {
437
437
  * same absolute times as a single pass, so their frames are byte-identical to it.
438
438
  */
439
439
  const EXTRACTION_SEGMENT_SECONDS = 120;
440
+ /** Frame times since file start are `PTS*TB + seek`; snapping them to `lead + k/FR` gives every process one grid. */
441
+ function undoTickRoundingOnTheSourceFrameGrid(seek, lead) {
442
+ const sinceFileStart = `(PTS*TB+${seek}-${lead})`;
443
+ return [
444
+ "settb=AVTB",
445
+ `setpts=if(isnan(FR)\\,PTS\\,(${lead}+round(${sinceFileStart}*FR)/FR-${seek})/TB)`,
446
+ ];
447
+ }
440
448
  // Caps extraction ffmpeg runs across all clips; uncapped, dozens of clips starved each other into timeouts.
441
449
  const EXTRACTION_SLOTS = Math.max(1, Math.floor(cpus().length / 2));
442
450
  const inExtractionSlot = createConcurrencyLimit(EXTRACTION_SLOTS);
@@ -467,11 +475,12 @@ async function runSegmentedExtraction(job) {
467
475
  while (next < segmentCount && !signal.aborted) {
468
476
  const index = next++;
469
477
  const { firstFrame, frames } = segments[index];
478
+ const seek = startTime + firstFrame / fps;
470
479
  const result = await inExtractionSlot(() => runFfmpeg([
471
480
  ...decodeArgs,
472
481
  "-noaccurate_seek",
473
482
  "-ss",
474
- String(startTime + firstFrame / fps),
483
+ String(seek),
475
484
  "-i",
476
485
  videoPath,
477
486
  // Read one frame past the segment and keep exactly its own frames.
@@ -481,7 +490,7 @@ async function runSegmentedExtraction(job) {
481
490
  String(frames),
482
491
  "-start_number",
483
492
  String(firstFrame + 1),
484
- ...filterAndEncodeArgs,
493
+ ...filterAndEncodeArgs(seek),
485
494
  ], { ...job.runOptions, signal }), signal);
486
495
  results[index] = result;
487
496
  if (!result.success)
@@ -580,11 +589,19 @@ outputDirOverride) {
580
589
  if (decodeWithVideoToolbox) {
581
590
  vfFilters.push("format=nv12");
582
591
  }
583
- if (sampleAtOutputFps) {
584
- // Each slot takes the last frame starting at or before its time. Ticks of 2 ms or finer
585
- // first shift half a tick, absorbing container rounding (WebM/MKV use whole ms).
586
- vfFilters.push("settb=intb/2", "setpts=PTS-lte(TB\\,0.001)", `fps=${ffmpegFps}:start_time=0:round=up`);
587
- }
592
+ const samplingAt = vfFilters.length;
593
+ // Each slot takes the last frame starting at or before its time. Ticks of 2 ms or finer
594
+ // first shift half a tick, absorbing container rounding (WebM/MKV use whole ms).
595
+ const samplingFilters = (seek) => sampleAtOutputFps
596
+ ? [
597
+ ...(metadata.isVFR
598
+ ? []
599
+ : undoTickRoundingOnTheSourceFrameGrid(seek, metadata.videoStreamLeadSeconds ?? 0)),
600
+ "settb=intb/2",
601
+ "setpts=PTS-lte(TB\\,0.001)",
602
+ `fps=${ffmpegFps}:start_time=0:round=up`,
603
+ ]
604
+ : [];
588
605
  if (options.sdrToHdrTransfer) {
589
606
  // Ordering intent: fps sampling runs BEFORE the colorspace remap so only
590
607
  // kept frames are converted. The remap is pointwise per-frame, so the
@@ -610,7 +627,11 @@ outputDirOverride) {
610
627
  encodeArgs.push("-compression_level", "1");
611
628
  encodeArgs.push("-y", outputPattern);
612
629
  const runOptions = { signal, timeout: ffmpegProcessTimeout };
613
- const filterArgs = vfFilters.length > 0 ? ["-vf", vfFilters.join(",")] : [];
630
+ const filterArgsFor = (seek) => {
631
+ const filters = [...vfFilters];
632
+ filters.splice(samplingAt, 0, ...samplingFilters(seek));
633
+ return filters.length > 0 ? ["-vf", filters.join(",")] : [];
634
+ };
614
635
  const segmentFrames = Math.round(EXTRACTION_SEGMENT_SECONDS * fps);
615
636
  const totalFrames = extractionFrameCountForDuration(duration, normalizedFps, metadata.isVFR);
616
637
  // A VFR window rounds its frame count up, and only the segments' `-frames:v` bound
@@ -620,14 +641,14 @@ outputDirOverride) {
620
641
  const processResult = sampleAtOutputFps && (options.frameRanges || boundVfrFrameCount || totalFrames > segmentFrames)
621
642
  ? await runSegmentedExtraction({
622
643
  decodeArgs,
623
- filterAndEncodeArgs: [...filterArgs, ...encodeArgs],
644
+ filterAndEncodeArgs: (seek) => [...filterArgsFor(seek), ...encodeArgs],
624
645
  videoPath,
625
646
  startTime,
626
647
  fps,
627
648
  segments: splitFrameRanges(options.frameRanges ?? [{ firstFrame: 0, frames: totalFrames }], segmentFrames),
628
649
  runOptions,
629
650
  })
630
- : await inExtractionSlot(() => runFfmpeg([...args, ...filterArgs, ...encodeArgs], runOptions), signal);
651
+ : await inExtractionSlot(() => runFfmpeg([...args, ...filterArgsFor(startTime), ...encodeArgs], runOptions), signal);
631
652
  if (processResult.failureReason === "external_interruption") {
632
653
  throw new VideoSourceExtractionError("external_interruption", true, "Video frame extraction interrupted by host lifecycle", `FFmpeg exited with code ${processResult.exitCode}: ${processResult.stderr.slice(-500)}`);
633
654
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.172",
3
+ "version": "0.8.173",
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/core": "^0.8.172",
54
- "@hyperframes/parsers": "^0.8.172"
53
+ "@hyperframes/parsers": "^0.8.173",
54
+ "@hyperframes/core": "^0.8.173"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.0.10",