@hyperframes/engine 0.8.223 → 0.8.225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -47,7 +47,7 @@ export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFra
47
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";
48
48
  export type { EncoderOptions, EncodeResult, MuxResult } from "./services/chunkEncoder.types.js";
49
49
  export { spawnStreamingEncoder, createFrameReorderBuffer, type StreamingEncoder, type StreamingEncoderOptions, type StreamingEncoderResult, type FrameReorderBuffer, } from "./services/streamingEncoder.js";
50
- export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, videoStartOnMediaTimeline, ffmpegSeekSeconds, 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
+ export { parseVideoElements, hasGradedCanvas, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, videoStartOnMediaTimeline, ffmpegSeekSeconds, 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";
51
51
  export { isSelfOrAncestorHidden } from "./services/mediaHidden.js";
52
52
  export { directorySizeBytes, gcExtractionCache } from "./services/extractionCache.js";
53
53
  export { resolveReferencedStart, resolveReferencedDuration, type RefResolverEl, type RefResolverDoc, } from "./services/referenceResolver.js";
@@ -86,7 +86,7 @@ export { type TransitionFn, TRANSITIONS, crossfade, sampleRgb48le, hdrToLinear,
86
86
  export { initHdrReadback, uploadAndReadbackHdrFrame, float16ToPqRgb, buildHdrChromeArgs, launchHdrBrowser, } from "./services/hdrCapture.js";
87
87
  export { captureScreenshotWithAlpha } from "./services/screenshotService.js";
88
88
  export { hideVideoElements, showVideoElements, queryVideoElementBounds, queryElementStacking, measureVideoShownBoxes, type ShownBox, type VideoElementBounds, type ElementStackingInfo, } from "./services/videoFrameInjector.js";
89
- export { isHdrColorSpace, detectTransfer, getHdrEncoderColorParams, analyzeCompositionHdr, findHdrAutoPromotion, formatHdrAutoPromotionWarning, sanitizeHdrAutoPromotionAsset, DEFAULT_HDR10_MASTERING, HDR_AUTO_PROMOTION_PIPELINE, type HdrTransfer, type HdrEncoderColorParams, type HdrAutoPromotion, type CompositionHdrInfo, type HdrMasteringMetadata, } from "./utils/hdr.js";
89
+ export { isHdrColorSpace, detectTransfer, getHdrEncoderColorParams, analyzeCompositionHdr, findHdrAutoPromotion, gradingKeepsSdr, formatHdrAutoPromotionWarning, sanitizeHdrAutoPromotionAsset, DEFAULT_HDR10_MASTERING, HDR_AUTO_PROMOTION_PIPELINE, type HdrTransfer, type HdrEncoderColorParams, type HdrAutoPromotion, type CompositionHdrInfo, type HdrMasteringMetadata, } from "./utils/hdr.js";
90
90
  export type { VideoColorSpace } from "./utils/ffprobe.js";
91
91
  export { renderProvenanceArgs, appendRenderProvenanceArgs, readRenderProvenance, PROVENANCE_RENDERER_TAG, PROVENANCE_VERSION_TAG, PROVENANCE_RENDERER_NAME, PROVENANCE_VERSION, type RenderProvenance, } from "./utils/renderProvenance.js";
92
92
  export { DrawElementCaptureError, isDrawElementCaptureError, } from "./services/drawElementCaptureError.js";
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFra
51
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";
52
52
  export { spawnStreamingEncoder, createFrameReorderBuffer, } from "./services/streamingEncoder.js";
53
53
  // ── Media processing ───────────────────────────────────────────────────────────
54
- export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, videoStartOnMediaTimeline, ffmpegSeekSeconds, 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
+ export { parseVideoElements, hasGradedCanvas, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, videoStartOnMediaTimeline, ffmpegSeekSeconds, 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";
55
55
  export { isSelfOrAncestorHidden } from "./services/mediaHidden.js";
56
56
  export { directorySizeBytes, gcExtractionCache } from "./services/extractionCache.js";
57
57
  export { resolveReferencedStart, resolveReferencedDuration, } from "./services/referenceResolver.js";
@@ -93,7 +93,7 @@ export { TRANSITIONS, crossfade, sampleRgb48le, hdrToLinear, linearToHdr, conver
93
93
  export { initHdrReadback, uploadAndReadbackHdrFrame, float16ToPqRgb, buildHdrChromeArgs, launchHdrBrowser, } from "./services/hdrCapture.js";
94
94
  export { captureScreenshotWithAlpha } from "./services/screenshotService.js";
95
95
  export { hideVideoElements, showVideoElements, queryVideoElementBounds, queryElementStacking, measureVideoShownBoxes, } from "./services/videoFrameInjector.js";
96
- export { isHdrColorSpace, detectTransfer, getHdrEncoderColorParams, analyzeCompositionHdr, findHdrAutoPromotion, formatHdrAutoPromotionWarning, sanitizeHdrAutoPromotionAsset, DEFAULT_HDR10_MASTERING, HDR_AUTO_PROMOTION_PIPELINE, } from "./utils/hdr.js";
96
+ export { isHdrColorSpace, detectTransfer, getHdrEncoderColorParams, analyzeCompositionHdr, findHdrAutoPromotion, gradingKeepsSdr, formatHdrAutoPromotionWarning, sanitizeHdrAutoPromotionAsset, DEFAULT_HDR10_MASTERING, HDR_AUTO_PROMOTION_PIPELINE, } from "./utils/hdr.js";
97
97
  export { renderProvenanceArgs, appendRenderProvenanceArgs, readRenderProvenance, PROVENANCE_RENDERER_TAG, PROVENANCE_VERSION_TAG, PROVENANCE_RENDERER_NAME, PROVENANCE_VERSION, } from "./utils/renderProvenance.js";
98
98
  export { DrawElementCaptureError, isDrawElementCaptureError, } from "./services/drawElementCaptureError.js";
99
99
  export { inAgentSandbox, isOneProcessOverPipe, machServiceDenied, oneProcessOverPipe, pipeLaunchClosedOnMac, sandboxDeniedRemediation, } from "./services/sandboxLaunch.js";
@@ -5,7 +5,7 @@ import { renameWithRetry } from "@hyperframes/core/rename-retry";
5
5
  import { removeScratch } from "@hyperframes/core/remove-scratch";
6
6
  import { join, dirname, isAbsolute, relative } from "path";
7
7
  import { parseHTML } from "linkedom";
8
- import { extractAudioMetadata, ffmpegSeekSeconds } from "../utils/ffprobe.js";
8
+ import { extractAudioMetadata, ffmpegSeekSeconds, formatFfmpegSeconds, } from "../utils/ffprobe.js";
9
9
  import { isNotMediaPayload } from "../utils/notMediaPayload.js";
10
10
  import { urlInputArgs } from "../utils/urlInput.js";
11
11
  import { clampAudioGain, clipGain } from "@hyperframes/core/audio-gain";
@@ -550,7 +550,7 @@ async function extractAudioSegment(srcPath, outputPath, stage, mediaStart, durat
550
550
  "-stream_loop",
551
551
  "-1",
552
552
  "-t",
553
- String(sourceDuration),
553
+ formatFfmpegSeconds(sourceDuration),
554
554
  "-i",
555
555
  cyclePath,
556
556
  "-vn",
@@ -560,7 +560,7 @@ async function extractAudioSegment(srcPath, outputPath, stage, mediaStart, durat
560
560
  "48000",
561
561
  ...outputArgs,
562
562
  "-t",
563
- String(duration),
563
+ formatFfmpegSeconds(duration),
564
564
  "-y",
565
565
  outputPath,
566
566
  ], { signal, timeout: ffmpegProcessTimeout });
@@ -598,14 +598,19 @@ async function extractAudioSegment(srcPath, outputPath, stage, mediaStart, durat
598
598
  ...(readsUrl ? FAIL_ON_READ_ERROR_INSTEAD_OF_ENDING_THERE : []),
599
599
  ...urlInputArgs(readFrom),
600
600
  ...(inputSeek
601
- ? ["-ss", String(ffmpegStart - warmUp), "-t", String(sourceDuration + warmUp)]
601
+ ? [
602
+ "-ss",
603
+ formatFfmpegSeconds(ffmpegStart - warmUp),
604
+ "-t",
605
+ formatFfmpegSeconds(sourceDuration + warmUp),
606
+ ]
602
607
  : []),
603
608
  "-i",
604
609
  readFrom,
605
610
  ];
606
611
  args.push("-vn", "-acodec", "pcm_s16le", "-ar", "48000", ...outputArgs);
607
612
  if (normalizedPlaybackRate !== 1 || options?.padToDuration)
608
- args.push("-t", String(duration));
613
+ args.push("-t", formatFfmpegSeconds(duration));
609
614
  args.push("-y", outputPath);
610
615
  const result = await runFfmpeg(args, { signal, timeout: ffmpegProcessTimeout });
611
616
  if (signal?.aborted) {
@@ -660,7 +665,7 @@ async function generateSilence(outputPath, duration, signal, config) {
660
665
  "-i",
661
666
  "anullsrc=r=48000:cl=stereo",
662
667
  "-t",
663
- String(duration),
668
+ formatFfmpegSeconds(duration),
664
669
  "-acodec",
665
670
  "pcm_s16le",
666
671
  "-y",
@@ -761,7 +766,7 @@ async function mixAudioTracksFlat(tracks, outputPath, totalDuration, signal, con
761
766
  "-ar",
762
767
  "48000",
763
768
  "-t",
764
- String(totalDuration),
769
+ formatFfmpegSeconds(totalDuration),
765
770
  "-y",
766
771
  outputPath,
767
772
  ];
@@ -923,7 +928,7 @@ async function mixGroupMembersFlat(memberTracks, outputPath, totalDuration, sign
923
928
  "-ar",
924
929
  "48000",
925
930
  "-t",
926
- String(totalDuration),
931
+ formatFfmpegSeconds(totalDuration),
927
932
  "-y",
928
933
  outputPath,
929
934
  ];
@@ -21,6 +21,7 @@ export interface VideoElement {
21
21
  loop: boolean;
22
22
  hasAudio: boolean;
23
23
  hidden?: boolean;
24
+ graded?: boolean;
24
25
  }
25
26
  export interface ExtractedFrames {
26
27
  videoId: string;
@@ -263,7 +264,9 @@ export interface ImageElement {
263
264
  src: string;
264
265
  start: number;
265
266
  end: number;
267
+ graded?: boolean;
266
268
  }
269
+ export declare function hasGradedCanvas(html: string): boolean;
267
270
  export declare function parseImageElements(html: string): ImageElement[];
268
271
  export declare function sdrCanvasColourFilters(metadata: VideoMetadata, toneMappedToSdr?: boolean): string[];
269
272
  export declare function extractVideoFramesRange(...args: Parameters<typeof extractVideoFramesRangeUnredacted>): Promise<ExtractedFrames>;
@@ -310,6 +313,7 @@ export interface TimelineExtractionWindow {
310
313
  }
311
314
  type TimelineWindowVideo = Pick<VideoElement, "start" | "end" | "mediaStart"> & Partial<Pick<VideoElement, "playbackRate">> & Partial<Pick<VideoElement, "loop">>;
312
315
  export declare function isVideoPastTimelineEnd(video: Pick<VideoElement, "start">, timelineEnd: number): boolean;
316
+ export declare function alignClipStartToFirstOutputFrame(video: VideoElement, fps: number): void;
313
317
  /** Move a clip to its extraction window; a rate lane is shifted so it keeps integrating from the new origin. */
314
318
  export declare function rebaseVideoToWindow(video: VideoElement, window: TimelineExtractionWindow): void;
315
319
  /**
@@ -11,13 +11,14 @@ import { basename, join } from "path";
11
11
  import { cpus } from "os";
12
12
  import { createConcurrencyLimit } from "../utils/concurrencyLimit.js";
13
13
  import { parseHTML } from "linkedom";
14
+ import { HF_COLOR_GRADING_ATTR } from "@hyperframes/core";
14
15
  import { isPathInside } from "@hyperframes/parsers/asset-paths";
15
16
  import { resolveProjectRelativeSrc } from "@hyperframes/parsers/asset-resolution";
16
17
  import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
17
18
  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
19
  import { resolveReferencedStart } from "./referenceResolver.js";
19
20
  import { isKnownInactiveTimelineWindow } from "./mediaTimelineWindow.js";
20
- import { extractFinalVideoFrameTimestamp, extractMediaMetadata, ffmpegSeekSeconds, keyframeGapSeconds, mediaFileIdentity, readMetadataCache, readStoredFinalFrame, resolvePlayableVideoDuration, videoStartOnMediaTimeline, writeMetadataCache, } from "../utils/ffprobe.js";
21
+ import { extractFinalVideoFrameTimestamp, extractMediaMetadata, ffmpegSeekSeconds, formatFfmpegSeconds, keyframeGapSeconds, mediaFileIdentity, readMetadataCache, readStoredFinalFrame, resolvePlayableVideoDuration, videoStartOnMediaTimeline, writeMetadataCache, } from "../utils/ffprobe.js";
21
22
  import { inputAlphaOpaqueWarning, probeInputAlphaPlane } from "../utils/alphaPlaneProbe.js";
22
23
  import { redactUrlQueriesInError, urlInputArgs } from "../utils/urlInput.js";
23
24
  import { analyzeCompositionHdr, isHdrColorSpace as isHdrColorSpaceUtil, } from "../utils/hdr.js";
@@ -427,10 +428,15 @@ export function parseVideoElements(html) {
427
428
  (isAudibleVideoElement(el) && isMemberGroupHidden(groupsById, el))
428
429
  ? { hidden: true }
429
430
  : {}),
431
+ ...(el.hasAttribute(HF_COLOR_GRADING_ATTR) ? { graded: true } : {}),
430
432
  });
431
433
  }
432
434
  return videos;
433
435
  }
436
+ export function hasGradedCanvas(html) {
437
+ return (parseHTML(unwrapTemplate(html)).document.querySelector(`canvas[${HF_COLOR_GRADING_ATTR}]`) !==
438
+ null);
439
+ }
434
440
  export function parseImageElements(html) {
435
441
  const images = [];
436
442
  const { document } = parseHTML(unwrapTemplate(html));
@@ -465,7 +471,13 @@ export function parseImageElements(html) {
465
471
  else {
466
472
  end = Infinity;
467
473
  }
468
- images.push({ id, src, start, end });
474
+ images.push({
475
+ id,
476
+ src,
477
+ start,
478
+ end,
479
+ ...(el.hasAttribute(HF_COLOR_GRADING_ATTR) ? { graded: true } : {}),
480
+ });
469
481
  }
470
482
  return images;
471
483
  }
@@ -581,12 +593,12 @@ async function runSegmentedExtraction(job) {
581
593
  ...decodeArgs,
582
594
  "-noaccurate_seek",
583
595
  "-ss",
584
- String(job.ffmpegSeek(seek)),
596
+ formatFfmpegSeconds(job.ffmpegSeek(seek)),
585
597
  "-i",
586
598
  videoPath,
587
599
  // Read one frame past the segment and keep exactly its own frames.
588
600
  "-t",
589
- String((frames + 1) / fps),
601
+ formatFfmpegSeconds((frames + 1) / fps),
590
602
  "-frames:v",
591
603
  String(frames),
592
604
  "-start_number",
@@ -697,13 +709,13 @@ outputDirOverride) {
697
709
  // sample. This is intentionally reserved for the one-frame path: input
698
710
  // seeking is faster, but valid unindexed transports (notably MPEG-TS with
699
711
  // a negative timestamp base) can seek to EOF and emit zero frames.
700
- args.push("-i", videoPath, "-ss", String(Math.max(0, ffmpegSeek(startTime))), "-frames:v", "1");
712
+ args.push("-i", videoPath, "-ss", formatFfmpegSeconds(Math.max(0, ffmpegSeek(startTime))), "-frames:v", "1");
701
713
  }
702
714
  else {
703
715
  // An accurate seek drops the frame on screen at startTime; the fps sampling below needs it.
704
716
  if (sampleAtOutputFps)
705
717
  args.push("-noaccurate_seek");
706
- args.push("-ss", String(ffmpegSeek(startTime)), "-i", videoPath, "-t", String(duration));
718
+ args.push("-ss", formatFfmpegSeconds(ffmpegSeek(startTime)), "-i", videoPath, "-t", formatFfmpegSeconds(duration));
707
719
  }
708
720
  const vfFilters = [];
709
721
  if (keepsHdrForChrome) {
@@ -905,14 +917,17 @@ const FINAL_FRAME_LOGICAL_DURATION_SECONDS = 1e-6;
905
917
  export function isVideoPastTimelineEnd(video, timelineEnd) {
906
918
  return video.start >= timelineEnd;
907
919
  }
908
- function alignClipStartToFirstOutputFrame(video, fps) {
920
+ export function alignClipStartToFirstOutputFrame(video, fps) {
909
921
  if (video.loop || !Number.isFinite(video.start) || video.start < 0 || !(fps > 0))
910
922
  return;
911
923
  const position = exportClipWindow(video.start, Infinity, fps).start * fps;
912
924
  const firstFrame = Math.abs(position - Math.round(position)) < 1e-6 ? Math.round(position) : Math.ceil(position);
913
925
  const shift = firstFrame / fps - video.start;
914
- if (shift === 0)
926
+ // Float dust (a sum landing a hair off the grid) snaps the start; it never reaches mediaStart.
927
+ if (Math.abs(shift) < 1e-9) {
928
+ video.start = firstFrame / fps;
915
929
  return;
930
+ }
916
931
  const playbackRate = normalizeRateSpec(video.playbackRate);
917
932
  video.mediaStart += sourceTimeAt(playbackRate, shift);
918
933
  video.playbackRate = shiftRateLane(playbackRate, shift);
@@ -128,6 +128,7 @@ export declare function readProbeJson(filePath: string, signal?: AbortSignal): P
128
128
  /** What a site stores beside a video: its ffprobe JSON and the final-frame facts a plan would otherwise probe for. */
129
129
  export declare function buildProbeFactsText(filePath: string, signal?: AbortSignal): Promise<string>;
130
130
  export declare function videoStartOnMediaTimeline(metadata: Pick<VideoMetadata, "videoStreamLeadSeconds" | "mediaTimeOffsetSeconds">): number;
131
+ export declare function formatFfmpegSeconds(seconds: number): string;
131
132
  export declare function ffmpegSeekSeconds(metadata: Pick<VideoMetadata, "mediaTimeOffsetSeconds">, mediaTime: number): number;
132
133
  export declare function resolvePlayableVideoDuration(metadata: VideoMetadata): number;
133
134
  /**
@@ -336,6 +336,10 @@ export async function buildProbeFactsText(filePath, signal) {
336
336
  export function videoStartOnMediaTimeline(metadata) {
337
337
  return (metadata.videoStreamLeadSeconds ?? 0) + (metadata.mediaTimeOffsetSeconds ?? 0);
338
338
  }
339
+ export function formatFfmpegSeconds(seconds) {
340
+ const text = String(seconds);
341
+ return text.includes("e") ? String(+seconds.toFixed(6)) : text;
342
+ }
339
343
  export function ffmpegSeekSeconds(metadata, mediaTime) {
340
344
  return mediaTime - (metadata.mediaTimeOffsetSeconds ?? 0);
341
345
  }
@@ -860,7 +864,7 @@ async function extractFinalPacketDuration(filePath, window, signal) {
860
864
  "-select_streams",
861
865
  "v:0",
862
866
  "-read_intervals",
863
- `${intervalStart}%${streamEnd}`,
867
+ `${formatFfmpegSeconds(intervalStart)}%${formatFfmpegSeconds(streamEnd)}`,
864
868
  "-show_entries",
865
869
  "packet=pts_time,duration_time",
866
870
  "-of",
@@ -939,7 +943,8 @@ export async function extractFinalVideoFrameTimestamp(filePath, metadata, signal
939
943
  });
940
944
  return parseFinalTimestamp(stdout);
941
945
  };
942
- const timestamp = (await probe(`${intervalStart}%${streamEnd}`)) ?? (await probe( /* full scan */));
946
+ const timestamp = (await probe(`${formatFfmpegSeconds(intervalStart)}%${formatFfmpegSeconds(streamEnd)}`)) ??
947
+ (await probe( /* full scan */));
943
948
  if (timestamp === undefined) {
944
949
  throw new Error("[FFmpeg] ffprobe found no decodable final video frame");
945
950
  }
@@ -1114,7 +1119,7 @@ export async function analyzeKeyframeIntervals(filePath, metadata) {
1114
1119
  export async function keyframeGapSeconds(filePath, seekSeconds, signal) {
1115
1120
  const stdout = await runFfprobe(filePath, [
1116
1121
  ...["-select_streams", "v:0", "-show_entries", "packet=pts_time", "-of", "csv=p=0"],
1117
- ...["-read_intervals", `${seekSeconds}%+#1`],
1122
+ ...["-read_intervals", `${formatFfmpegSeconds(seekSeconds)}%+#1`],
1118
1123
  ], signal);
1119
1124
  const keyframe = parseFloat(stdout.split("\n")[0] ?? "");
1120
1125
  return Number.isFinite(keyframe) ? Math.max(0, seekSeconds - keyframe) : 0;
@@ -88,6 +88,16 @@ export declare function findHdrAutoPromotion(sources: readonly {
88
88
  asset: string;
89
89
  colorSpace: VideoColorSpace | null;
90
90
  }[]): HdrAutoPromotion | null;
91
+ /** Grading is SDR-only, so in auto mode a composition with graded media renders SDR, as `--sdr` does. */
92
+ export declare function gradingKeepsSdr(hdrMode: "auto" | "force-hdr" | "force-sdr" | undefined, media: {
93
+ videos: readonly {
94
+ graded?: boolean;
95
+ }[];
96
+ images: readonly {
97
+ graded?: boolean;
98
+ }[];
99
+ gradedCanvas?: boolean;
100
+ }): boolean;
91
101
  export declare function formatHdrAutoPromotionWarning(promotion: HdrAutoPromotion): string;
92
102
  /**
93
103
  * Analyze a set of video color spaces to determine if the composition
package/dist/utils/hdr.js CHANGED
@@ -84,6 +84,12 @@ export function findHdrAutoPromotion(sources) {
84
84
  }
85
85
  : null;
86
86
  }
87
+ /** Grading is SDR-only, so in auto mode a composition with graded media renders SDR, as `--sdr` does. */
88
+ export function gradingKeepsSdr(hdrMode, media) {
89
+ return ((hdrMode ?? "auto") === "auto" &&
90
+ (media.gradedCanvas === true ||
91
+ [...media.videos, ...media.images].some(({ graded }) => graded === true)));
92
+ }
87
93
  export function formatHdrAutoPromotionWarning(promotion) {
88
94
  return `[Render] HDR auto-promotion triggered by "${promotion.triggeringAsset}", output: ${promotion.output.colorSpace} / ${promotion.output.codec}`;
89
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.223",
3
+ "version": "0.8.225",
4
4
  "description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -59,8 +59,8 @@
59
59
  "puppeteer": "~25.11.0",
60
60
  "puppeteer-core": "^25.11.0",
61
61
  "undici": "7.25.0",
62
- "@hyperframes/core": "^0.8.223",
63
- "@hyperframes/parsers": "^0.8.223"
62
+ "@hyperframes/core": "^0.8.225",
63
+ "@hyperframes/parsers": "^0.8.225"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "^25.0.10",