@hyperframes/engine 0.8.212 → 0.8.214
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 +1 -1
- package/dist/index.js +1 -1
- package/dist/services/audioMixer.js +7 -6
- package/dist/services/videoFrameExtractor.d.ts +3 -3
- package/dist/services/videoFrameExtractor.js +15 -13
- package/dist/services/videoFrameInjector.d.ts +1 -1
- package/dist/services/videoFrameInjector.js +10 -5
- package/dist/utils/ffprobe.d.ts +4 -0
- package/dist/utils/ffprobe.js +15 -1
- package/package.json +3 -3
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
|
|
583
|
-
const
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
1870
|
-
return ((
|
|
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
|
|
1897
|
+
if (isVideoHiddenBeforeStreamStart(videoStartOnMediaTimeline(extracted.metadata), mediaStart + sourceTime)) {
|
|
1896
1898
|
return null;
|
|
1897
1899
|
}
|
|
1898
1900
|
if (frameIndex >= extracted.totalFrames) {
|
|
@@ -89,7 +89,7 @@ export interface ShownBox {
|
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* Seeks to each of `times`; returns each video's largest drawn box in device pixels. Videos that are
|
|
92
|
-
* graded, sampled by a canvas, `object-fit: none | scale-down` or never laid out get none (full size).
|
|
92
|
+
* graded (directly or through a grade group), sampled by a canvas, `object-fit: none | scale-down` or never laid out get none (full size).
|
|
93
93
|
*/
|
|
94
94
|
export declare function measureVideoShownBoxes(page: Page, videoIds: readonly string[], times: readonly number[]): Promise<Map<string, ShownBox>>;
|
|
95
95
|
/**
|
|
@@ -10,7 +10,7 @@ import { promises as fs } from "fs";
|
|
|
10
10
|
import { touchCacheDir } from "./extractionCache.js";
|
|
11
11
|
import { injectVideoFramesBatch, syncVideoFrameVisibility } from "./screenshotService.js";
|
|
12
12
|
import { DEFAULT_CONFIG } from "../config.js";
|
|
13
|
-
import { HF_COLOR_GRADING_ATTR, HF_COLOR_GRADING_CANVAS_ID_PREFIX, RENDER_FRAME_ID_PREFIX, RENDER_FRAME_ID_SUFFIX, renderFrameIdForRenderId, } from "@hyperframes/core";
|
|
13
|
+
import { HF_COLOR_GRADING_ATTR, HF_COLOR_GRADING_CANVAS_ID_PREFIX, HF_COLOR_GROUP_ATTR, HF_COLOR_GROUP_TAG, RENDER_FRAME_ID_PREFIX, RENDER_FRAME_ID_SUFFIX, renderFrameIdForRenderId, } from "@hyperframes/core";
|
|
14
14
|
/**
|
|
15
15
|
* Two-bound LRU keyed by frame path. Either bound triggers eviction of the
|
|
16
16
|
* oldest entry — entry count protects against pathological many-tiny-frames
|
|
@@ -255,12 +255,12 @@ async function setVideoElementsVisibility(page, videoIds, visible) {
|
|
|
255
255
|
}
|
|
256
256
|
/**
|
|
257
257
|
* Seeks to each of `times`; returns each video's largest drawn box in device pixels. Videos that are
|
|
258
|
-
* graded, sampled by a canvas, `object-fit: none | scale-down` or never laid out get none (full size).
|
|
258
|
+
* graded (directly or through a grade group), sampled by a canvas, `object-fit: none | scale-down` or never laid out get none (full size).
|
|
259
259
|
*/
|
|
260
260
|
export async function measureVideoShownBoxes(page, videoIds, times) {
|
|
261
261
|
if (videoIds.length === 0 || times.length === 0)
|
|
262
262
|
return new Map();
|
|
263
|
-
const boxes = await page.evaluate(async (ids, seekTimes, gradingAttr, gradingCanvasPrefix) => {
|
|
263
|
+
const boxes = await page.evaluate(async (ids, seekTimes, gradingAttr, gradingCanvasPrefix, groupAttr, groupTag) => {
|
|
264
264
|
const hf = window.__hf;
|
|
265
265
|
if (typeof hf?.seek !== "function")
|
|
266
266
|
return [];
|
|
@@ -268,8 +268,13 @@ export async function measureVideoShownBoxes(page, videoIds, times) {
|
|
|
268
268
|
.__hfWaitForSeekCompletion;
|
|
269
269
|
const largest = new Map();
|
|
270
270
|
const fullSize = new Set();
|
|
271
|
+
const gradedThroughGroup = (video) => {
|
|
272
|
+
const groupId = video.getAttribute(groupAttr);
|
|
273
|
+
const group = groupId ? document.getElementById(groupId) : null;
|
|
274
|
+
return group?.localName === groupTag && group.hasAttribute(gradingAttr);
|
|
275
|
+
};
|
|
271
276
|
const keepsFullSize = (video) => {
|
|
272
|
-
if (video.hasAttribute(gradingAttr))
|
|
277
|
+
if (video.hasAttribute(gradingAttr) || gradedThroughGroup(video))
|
|
273
278
|
return true;
|
|
274
279
|
const fit = window.getComputedStyle(video).objectFit;
|
|
275
280
|
return fit === "none" || fit === "scale-down";
|
|
@@ -297,7 +302,7 @@ export async function measureVideoShownBoxes(page, videoIds, times) {
|
|
|
297
302
|
measure(id);
|
|
298
303
|
}
|
|
299
304
|
return Array.from(largest).filter(([id, box]) => !fullSize.has(id) && box.width > 0 && box.height > 0);
|
|
300
|
-
}, [...videoIds], [...times], HF_COLOR_GRADING_ATTR, HF_COLOR_GRADING_CANVAS_ID_PREFIX);
|
|
305
|
+
}, [...videoIds], [...times], HF_COLOR_GRADING_ATTR, HF_COLOR_GRADING_CANVAS_ID_PREFIX, HF_COLOR_GROUP_ATTR, HF_COLOR_GROUP_TAG);
|
|
301
306
|
return new Map(boxes);
|
|
302
307
|
}
|
|
303
308
|
/**
|
package/dist/utils/ffprobe.d.ts
CHANGED
|
@@ -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.
|
package/dist/utils/ffprobe.js
CHANGED
|
@@ -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
|
-
/**
|
|
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.
|
|
3
|
+
"version": "0.8.214",
|
|
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/
|
|
58
|
-
"@hyperframes/
|
|
57
|
+
"@hyperframes/parsers": "^0.8.214",
|
|
58
|
+
"@hyperframes/core": "^0.8.214"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^25.0.10",
|