@hyperframes/engine 0.8.212 → 0.8.213

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, 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, 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";
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, 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, 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";
@@ -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 } from "../utils/ffprobe.js";
8
+ import { extractAudioMetadata, ffmpegSeekSeconds } 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";
@@ -579,13 +579,14 @@ async function extractAudioSegment(srcPath, outputPath, stage, mediaStart, durat
579
579
  removeScratch(cycleDir);
580
580
  }
581
581
  }
582
- const inputSeek = mediaStart >= (metadata?.latestStreamLeadSeconds ?? 0);
583
- const start = formatFilterNumber(mediaStart);
582
+ const ffmpegStart = ffmpegSeekSeconds(metadata ?? {}, mediaStart);
583
+ const inputSeek = ffmpegStart >= (metadata?.latestStreamLeadSeconds ?? 0);
584
+ const start = formatFilterNumber(ffmpegStart);
584
585
  const timestampTrim = inputSeek
585
586
  ? null
586
- : `atrim=start=${start}:end=${formatFilterNumber(mediaStart + sourceDuration)},asetpts=PTS-${start}/TB,aresample=async=1:first_pts=0`;
587
+ : `atrim=start=${start}:end=${formatFilterNumber(ffmpegStart + sourceDuration)},asetpts=PTS-${start}/TB,aresample=async=1:first_pts=0`;
587
588
  const fromSidecar = audioSidecarUrl !== undefined && inputSeek && normalizedPlaybackRate === 1;
588
- const warmUp = fromSidecar ? Math.min(SIDECAR_WARM_UP_SECONDS, mediaStart) : 0;
589
+ const warmUp = fromSidecar ? Math.min(SIDECAR_WARM_UP_SECONDS, ffmpegStart) : 0;
589
590
  const outputArgs = preparedAudioOutputArgs(metadata, normalizedPlaybackRate, duration, [
590
591
  warmUp > 0 ? `atrim=start=${warmUp},asetpts=PTS-STARTPTS` : timestampTrim,
591
592
  ...(options?.padToDuration ? ["apad"] : []),
@@ -597,7 +598,7 @@ async function extractAudioSegment(srcPath, outputPath, stage, mediaStart, durat
597
598
  ...(readsUrl ? FAIL_ON_READ_ERROR_INSTEAD_OF_ENDING_THERE : []),
598
599
  ...urlInputArgs(readFrom),
599
600
  ...(inputSeek
600
- ? ["-ss", String(mediaStart - warmUp), "-t", String(sourceDuration + warmUp)]
601
+ ? ["-ss", String(ffmpegStart - warmUp), "-t", String(sourceDuration + warmUp)]
601
602
  : []),
602
603
  "-i",
603
604
  readFrom,
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { resolveProjectRelativeSrc } from "@hyperframes/parsers/asset-resolution";
8
8
  import { type RateSpec, type FpsInput } from "@hyperframes/core";
9
- import { resolvePlayableVideoDuration, type VideoMetadata } from "../utils/ffprobe.js";
9
+ import { ffmpegSeekSeconds, resolvePlayableVideoDuration, videoStartOnMediaTimeline, type VideoMetadata } from "../utils/ffprobe.js";
10
10
  import { type HdrTransfer } from "../utils/hdr.js";
11
11
  import { type EngineConfig } from "../config.js";
12
12
  import { type CacheFrameFormat } from "./extractionCache.js";
@@ -276,7 +276,7 @@ export declare function classifyFfmpegSpawnError(error: unknown, stderr?: string
276
276
  * the container duration when ffprobe omits the stream duration; keep the
277
277
  * explicit fallback here for callers supplying older/manual metadata.
278
278
  */
279
- export { resolvePlayableVideoDuration };
279
+ export { ffmpegSeekSeconds, resolvePlayableVideoDuration, videoStartOnMediaTimeline };
280
280
  export interface TimelineExtractionWindow {
281
281
  compositionStart: number;
282
282
  mediaStart: number;
@@ -332,7 +332,7 @@ export declare function codecMayHaveAlpha(codec: string | undefined): boolean;
332
332
  export declare function decoderForCodec(codec: string | undefined): string;
333
333
  export declare function resolveFrameFormat(metadata: VideoMetadata, requested?: VideoFrameFormat): CacheFrameFormat;
334
334
  export declare function extractAllVideoFrames(videos: VideoElement[], baseDir: string, options: ExtractionOptions, signal?: AbortSignal, config?: Partial<Pick<EngineConfig, "ffmpegProcessTimeout" | "extractCacheDir" | "extractCacheMaxBytes">>, compiledDir?: string): Promise<ExtractionResult>;
335
- export declare function isVideoHiddenBeforeStreamStart(videoStreamLeadSeconds: number | undefined, mediaTime: number): boolean;
335
+ export declare function isVideoHiddenBeforeStreamStart(videoStartSeconds: number | undefined, mediaTime: number): boolean;
336
336
  export declare function getFrameAtTime(extracted: ExtractedFrames, globalTime: number, videoStart: number, loop?: boolean, mediaStart?: number): string | null;
337
337
  /**
338
338
  * Whether a media source is shorter than its `data-duration` slot by more than
@@ -17,7 +17,7 @@ import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
17
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
- import { extractFinalVideoFrameTimestamp, extractMediaMetadata, keyframeGapSeconds, mediaFileIdentity, readMetadataCache, readStoredFinalFrame, resolvePlayableVideoDuration, writeMetadataCache, } from "../utils/ffprobe.js";
20
+ import { extractFinalVideoFrameTimestamp, extractMediaMetadata, ffmpegSeekSeconds, keyframeGapSeconds, mediaFileIdentity, readMetadataCache, readStoredFinalFrame, resolvePlayableVideoDuration, videoStartOnMediaTimeline, writeMetadataCache, } from "../utils/ffprobe.js";
21
21
  import { inputAlphaOpaqueWarning, probeInputAlphaPlane } from "../utils/alphaPlaneProbe.js";
22
22
  import { redactUrlQueriesInError, urlInputArgs } from "../utils/urlInput.js";
23
23
  import { analyzeCompositionHdr, isHdrColorSpace as isHdrColorSpaceUtil, } from "../utils/hdr.js";
@@ -569,7 +569,7 @@ async function runSegmentedExtraction(job) {
569
569
  ...decodeArgs,
570
570
  "-noaccurate_seek",
571
571
  "-ss",
572
- String(seek),
572
+ String(job.ffmpegSeek(seek)),
573
573
  "-i",
574
574
  videoPath,
575
575
  // Read one frame past the segment and keep exactly its own frames.
@@ -678,18 +678,19 @@ outputDirOverride) {
678
678
  args.push("-c:v", decoderForCodec(metadata.videoCodec));
679
679
  }
680
680
  const decodeArgs = [...args];
681
+ const ffmpegSeek = (mediaTime) => ffmpegSeekSeconds(metadata, mediaTime);
681
682
  if (options.finalFrameOnly) {
682
683
  // Output-side seek decodes from the start before selecting the final
683
684
  // sample. This is intentionally reserved for the one-frame path: input
684
685
  // seeking is faster, but valid unindexed transports (notably MPEG-TS with
685
686
  // a negative timestamp base) can seek to EOF and emit zero frames.
686
- args.push("-i", videoPath, "-ss", String(startTime), "-frames:v", "1");
687
+ args.push("-i", videoPath, "-ss", String(Math.max(0, ffmpegSeek(startTime))), "-frames:v", "1");
687
688
  }
688
689
  else {
689
690
  // An accurate seek drops the frame on screen at startTime; the fps sampling below needs it.
690
691
  if (sampleAtOutputFps)
691
692
  args.push("-noaccurate_seek");
692
- args.push("-ss", String(startTime), "-i", videoPath, "-t", String(duration));
693
+ args.push("-ss", String(ffmpegSeek(startTime)), "-i", videoPath, "-t", String(duration));
693
694
  }
694
695
  const vfFilters = [];
695
696
  if (keepsHdrForChrome) {
@@ -702,7 +703,7 @@ outputDirOverride) {
702
703
  ? [
703
704
  ...(metadata.isVFR
704
705
  ? []
705
- : undoTickRoundingOnTheSourceFrameGrid(seek, metadata.videoStreamLeadSeconds ?? 0)),
706
+ : undoTickRoundingOnTheSourceFrameGrid(seek, videoStartOnMediaTimeline(metadata))),
706
707
  "settb=intb/2",
707
708
  "setpts=PTS-lte(TB\\,0.001)",
708
709
  `fps=${ffmpegFps}:start_time=0:round=up`,
@@ -751,6 +752,7 @@ outputDirOverride) {
751
752
  ? runSegmentedExtraction({
752
753
  decodeArgs: [...hwaccel, ...decodeArgs],
753
754
  filterAndEncodeArgs: (seek) => [...filterArgsFor(seek), ...encodeArgs],
755
+ ffmpegSeek,
754
756
  videoPath,
755
757
  startTime,
756
758
  fps,
@@ -868,7 +870,7 @@ function resolveSegmentDuration(requested, mediaStart, sourceDuration) {
868
870
  * the container duration when ffprobe omits the stream duration; keep the
869
871
  * explicit fallback here for callers supplying older/manual metadata.
870
872
  */
871
- export { resolvePlayableVideoDuration };
873
+ export { ffmpegSeekSeconds, resolvePlayableVideoDuration, videoStartOnMediaTimeline };
872
874
  function canHoldFinalFramePastEof(video) {
873
875
  const timelineDuration = video.end - video.start;
874
876
  return !video.loop && Number.isFinite(timelineDuration) && timelineDuration > 0;
@@ -996,11 +998,12 @@ export async function resolveFinalFrameExtractionWindow(videoPath, video, metada
996
998
  return window;
997
999
  const playableDuration = resolvePlayableVideoDuration(metadata);
998
1000
  const videoStreamLead = metadata.videoStreamLeadSeconds ?? 0;
999
- const finalFrameTimestamp = videoStreamLead +
1001
+ const lastInstantTheBrowserPlays = playableDuration - FINAL_FRAME_LOGICAL_DURATION_SECONDS;
1002
+ const finalFrameTimestamp = Math.min(lastInstantTheBrowserPlays, videoStartOnMediaTimeline(metadata) +
1000
1003
  (await extractFinalVideoFrameTimestamp(videoPath, {
1001
1004
  videoStreamDurationSeconds: playableDuration - videoStreamLead,
1002
1005
  videoStreamStartSeconds: metadata.videoStreamStartSeconds,
1003
- }, signal));
1006
+ }, signal)));
1004
1007
  if (window.mediaStart < finalFrameTimestamp - 1e-9)
1005
1008
  return window;
1006
1009
  const sourceRemaining = playableDuration - video.mediaStart;
@@ -1624,7 +1627,7 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
1624
1627
  sourceFps: work.metadata.fps,
1625
1628
  finalFrameSeconds: storedFinalFrame.seconds,
1626
1629
  lastPacketSeconds: storedFinalFrame.lastPacketSeconds,
1627
- streamLeadSeconds: work.metadata.videoStreamLeadSeconds ?? 0,
1630
+ streamLeadSeconds: videoStartOnMediaTimeline(work.metadata),
1628
1631
  });
1629
1632
  if (storedTailFrames !== null) {
1630
1633
  const totalFrames = firstTailFrame + storedTailFrames;
@@ -1866,9 +1869,8 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
1866
1869
  };
1867
1870
  }
1868
1871
  const BROWSER_SHOWS_UPCOMING_FIRST_FRAME_WITHIN_SECONDS = 1;
1869
- export function isVideoHiddenBeforeStreamStart(videoStreamLeadSeconds, mediaTime) {
1870
- return ((videoStreamLeadSeconds ?? 0) - mediaTime >=
1871
- BROWSER_SHOWS_UPCOMING_FIRST_FRAME_WITHIN_SECONDS - 1e-6);
1872
+ export function isVideoHiddenBeforeStreamStart(videoStartSeconds, mediaTime) {
1873
+ return ((videoStartSeconds ?? 0) - mediaTime >= BROWSER_SHOWS_UPCOMING_FIRST_FRAME_WITHIN_SECONDS - 1e-6);
1872
1874
  }
1873
1875
  function getFrameIndexAtTime(extracted, globalTime, videoStart, loop = false, mediaStart = 0, holdLastFrame = false, playbackRate = 1) {
1874
1876
  let localTime = Math.max(0, globalTime - videoStart);
@@ -1892,7 +1894,7 @@ function getFrameIndexAtTime(extracted, globalTime, videoStart, loop = false, me
1892
1894
  const frameIndex = Math.floor(shownTime * extracted.fps);
1893
1895
  if (frameIndex < 0 || extracted.totalFrames <= 0)
1894
1896
  return null;
1895
- if (isVideoHiddenBeforeStreamStart(extracted.metadata.videoStreamLeadSeconds, mediaStart + sourceTime)) {
1897
+ if (isVideoHiddenBeforeStreamStart(videoStartOnMediaTimeline(extracted.metadata), mediaStart + sourceTime)) {
1896
1898
  return null;
1897
1899
  }
1898
1900
  if (frameIndex >= extracted.totalFrames) {
@@ -25,6 +25,7 @@ export interface VideoMetadata {
25
25
  videoStreamStartSeconds?: number;
26
26
  /** Seconds after the file's media time 0 (its earliest audio/video stream start) at which video begins. */
27
27
  videoStreamLeadSeconds?: number;
28
+ mediaTimeOffsetSeconds?: number;
28
29
  width: number;
29
30
  height: number;
30
31
  fps: number;
@@ -60,6 +61,7 @@ export interface AudioMetadata {
60
61
  /** Seconds after the file's media time 0 at which its last audio or video stream starts. */
61
62
  latestStreamLeadSeconds?: number;
62
63
  audioStreamLeadSeconds?: number;
64
+ mediaTimeOffsetSeconds?: number;
63
65
  audioStreamCount?: number;
64
66
  /** The track read for these facts and for the mix, by its place among the audio streams (decodableAudioStream). */
65
67
  audioStream: number;
@@ -123,6 +125,8 @@ export declare const PROBE_FACTS_VERSION = "v2";
123
125
  export declare function readProbeJson(filePath: string, signal?: AbortSignal): Promise<string>;
124
126
  /** What a site stores beside a video: its ffprobe JSON and the final-frame facts a plan would otherwise probe for. */
125
127
  export declare function buildProbeFactsText(filePath: string, signal?: AbortSignal): Promise<string>;
128
+ export declare function videoStartOnMediaTimeline(metadata: Pick<VideoMetadata, "videoStreamLeadSeconds" | "mediaTimeOffsetSeconds">): number;
129
+ export declare function ffmpegSeekSeconds(metadata: Pick<VideoMetadata, "mediaTimeOffsetSeconds">, mediaTime: number): number;
126
130
  export declare function resolvePlayableVideoDuration(metadata: VideoMetadata): number;
127
131
  /**
128
132
  * Probe stream capabilities without assuming the caller's element type.
@@ -333,6 +333,12 @@ export async function buildProbeFactsText(filePath, signal) {
333
333
  }
334
334
  return JSON.stringify(facts);
335
335
  }
336
+ export function videoStartOnMediaTimeline(metadata) {
337
+ return (metadata.videoStreamLeadSeconds ?? 0) + (metadata.mediaTimeOffsetSeconds ?? 0);
338
+ }
339
+ export function ffmpegSeekSeconds(metadata, mediaTime) {
340
+ return mediaTime - (metadata.mediaTimeOffsetSeconds ?? 0);
341
+ }
336
342
  export function resolvePlayableVideoDuration(metadata) {
337
343
  return Number.isFinite(metadata.videoStreamDurationSeconds) &&
338
344
  metadata.videoStreamDurationSeconds > 0
@@ -790,6 +796,7 @@ export async function extractMediaMetadata(filePath) {
790
796
  videoStreamDurationSeconds: effectiveStreamDuration,
791
797
  videoStreamStartSeconds: streamStart,
792
798
  videoStreamLeadSeconds: streamLeadSeconds(output, videoStream),
799
+ mediaTimeOffsetSeconds: mediaTimeOffsetSeconds(output),
793
800
  width: videoStream.width || stillImage()?.width || 0,
794
801
  height: videoStream.height || stillImage()?.height || 0,
795
802
  fps,
@@ -986,11 +993,12 @@ async function probeAudioMetadata(filePath, signal) {
986
993
  bitrate: output.format.bit_rate ? parseInt(output.format.bit_rate) : undefined,
987
994
  latestStreamLeadSeconds: latestStreamLeadSeconds(output),
988
995
  audioStreamLeadSeconds: streamLeadSeconds(output, audioStream),
996
+ mediaTimeOffsetSeconds: mediaTimeOffsetSeconds(output),
989
997
  audioStreamCount: audioStreams.length,
990
998
  audioStream: audioStreamIndex,
991
999
  };
992
1000
  }
993
- /** Chrome's media time 0 is the earliest of these; a stream starting later plays that much in. */
1001
+ /** A stream starting after the earliest of these plays that much after it. */
994
1002
  function audioVideoStreamStarts(output) {
995
1003
  return (output?.streams ?? [])
996
1004
  .filter((s) => (s.codec_type === "audio" || s.codec_type === "video") && s.disposition?.attached_pic !== 1)
@@ -1002,6 +1010,12 @@ function streamLeadSeconds(output, stream) {
1002
1010
  const start = parseFloat(stream.start_time ?? "");
1003
1011
  return Number.isFinite(start) && starts.length > 0 ? Math.max(0, start - Math.min(...starts)) : 0;
1004
1012
  }
1013
+ const BROWSER_PLAYS_FROM_TIMESTAMP_ZERO = /\b(mp4|webm)\b/;
1014
+ function mediaTimeOffsetSeconds(output) {
1015
+ const starts = audioVideoStreamStarts(output);
1016
+ const fromZero = BROWSER_PLAYS_FROM_TIMESTAMP_ZERO.test(output?.format.format_name ?? "");
1017
+ return fromZero && starts.length > 0 ? Math.max(0, Math.min(...starts)) : 0;
1018
+ }
1005
1019
  function latestStreamLeadSeconds(output) {
1006
1020
  const starts = audioVideoStreamStarts(output);
1007
1021
  return starts.length > 0 ? Math.max(...starts) - Math.min(...starts) : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.212",
3
+ "version": "0.8.213",
4
4
  "description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -54,8 +54,8 @@
54
54
  "linkedom": "^0.18.12",
55
55
  "puppeteer": "~25.11.0",
56
56
  "puppeteer-core": "^25.11.0",
57
- "@hyperframes/core": "^0.8.212",
58
- "@hyperframes/parsers": "^0.8.212"
57
+ "@hyperframes/core": "^0.8.213",
58
+ "@hyperframes/parsers": "^0.8.213"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/node": "^25.0.10",