@hyperframes/engine 0.8.209 → 0.8.210
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/__fixtures__/loopback.js +7 -2
- package/dist/services/chunkEncoder.d.ts +12 -2
- package/dist/services/chunkEncoder.js +20 -10
- package/dist/services/videoFrameExtractor.js +36 -7
- package/dist/utils/ffprobe.d.ts +2 -0
- package/dist/utils/ffprobe.js +9 -0
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export { CaptureFailure, classifyCaptureFailure, isFatalCaptureFailure, type Cap
|
|
|
43
43
|
export { createChromeMemorySampler, type ChromeMemoryStats, } from "./services/chromeMemorySampler.js";
|
|
44
44
|
export { installMediaRenderIdBridge } from "./services/mediaRenderIdBridge.js";
|
|
45
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
|
-
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, appendRateCapArgs, keyframeIntervalToGopSize, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, type GpuEncoder, type PackageHlsOptions, } from "./services/chunkEncoder.js";
|
|
46
|
+
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, 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";
|
|
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, sdrCanvasColourFilters, } from "./services/videoFrameExtractor.js";
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ export { createChromeMemorySampler, } from "./services/chromeMemorySampler.js";
|
|
|
47
47
|
export { installMediaRenderIdBridge } from "./services/mediaRenderIdBridge.js";
|
|
48
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
|
-
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, appendRateCapArgs, keyframeIntervalToGopSize, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, } from "./services/chunkEncoder.js";
|
|
50
|
+
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, PRORES_PROFILES, PRORES_PROFILE_NAMES, isProresProfile, appendRateCapArgs, keyframeIntervalToGopSize, 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, sdrCanvasColourFilters, } from "./services/videoFrameExtractor.js";
|
|
@@ -7,12 +7,17 @@ export async function listenOnLoopback(server) {
|
|
|
7
7
|
throw new Error("No test server address");
|
|
8
8
|
return `http://127.0.0.1:${address.port}`;
|
|
9
9
|
}
|
|
10
|
-
export function launchTestChrome(executablePath) {
|
|
11
|
-
|
|
10
|
+
export async function launchTestChrome(executablePath) {
|
|
11
|
+
const startedAt = Date.now();
|
|
12
|
+
const browser = await puppeteer.launch({
|
|
12
13
|
executablePath,
|
|
13
14
|
headless: true,
|
|
14
15
|
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
|
15
16
|
});
|
|
17
|
+
const launchedAt = Date.now();
|
|
18
|
+
await (await browser.newPage()).close();
|
|
19
|
+
console.info(`[engine-test-chrome] launch ${launchedAt - startedAt} ms, first page ${Date.now() - launchedAt} ms`);
|
|
20
|
+
return browser;
|
|
16
21
|
}
|
|
17
22
|
/** Closes a test Chrome, killing it when close hangs (seen on Windows runners) so afterAll never times out. */
|
|
18
23
|
export async function closeTestChrome(browser, timeoutMs = 5_000) {
|
|
@@ -54,14 +54,24 @@ export interface MuxVideoWithAudioOptions extends Partial<Pick<EngineConfig, "ff
|
|
|
54
54
|
*/
|
|
55
55
|
preserveAudioPrimingEditList?: boolean;
|
|
56
56
|
}
|
|
57
|
+
/** ProRes profile names in the order users are told about them. */
|
|
58
|
+
export declare const PRORES_PROFILE_NAMES: readonly ["4444", "hq", "422", "lt", "proxy"];
|
|
59
|
+
export type ProresProfile = (typeof PRORES_PROFILE_NAMES)[number];
|
|
60
|
+
/** The one table for `-profile:v` and pixel format; all but 4444 are 4:2:2 without alpha. */
|
|
61
|
+
export declare const PRORES_PROFILES: Record<ProresProfile, {
|
|
62
|
+
profile: string;
|
|
63
|
+
pixelFormat: string;
|
|
64
|
+
alpha: boolean;
|
|
65
|
+
}>;
|
|
66
|
+
export declare function isProresProfile(value: unknown): value is ProresProfile;
|
|
57
67
|
/**
|
|
58
68
|
* Get encoder preset for a given quality and output format.
|
|
59
69
|
* WebM uses VP9 with alpha-capable pixel format; MP4 uses h264 (or h265 for HDR);
|
|
60
|
-
* MOV uses ProRes 4444 with alpha for editor-compatible transparency.
|
|
70
|
+
* MOV uses ProRes 4444 with alpha for editor-compatible transparency, or a 422 profile without alpha.
|
|
61
71
|
*/
|
|
62
72
|
export declare function getEncoderPreset(quality: "draft" | "standard" | "high", format?: "mp4" | "webm" | "mov", hdr?: {
|
|
63
73
|
transfer: HdrTransfer;
|
|
64
|
-
}): EncoderPreset;
|
|
74
|
+
}, proresProfile?: ProresProfile): EncoderPreset;
|
|
65
75
|
export { detectGpuEncoder, type GpuEncoder } from "../utils/gpuEncoder.js";
|
|
66
76
|
/** The `lockGopForChunkConcat` / `gopSize` pair, shared by every encoder entry point. */
|
|
67
77
|
export interface LockedGopOptions {
|
|
@@ -59,12 +59,25 @@ async function shouldCopyAacSidecar(audioPath, options) {
|
|
|
59
59
|
return false;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
/** ProRes profile names in the order users are told about them. */
|
|
63
|
+
export const PRORES_PROFILE_NAMES = ["4444", "hq", "422", "lt", "proxy"];
|
|
64
|
+
/** The one table for `-profile:v` and pixel format; all but 4444 are 4:2:2 without alpha. */
|
|
65
|
+
export const PRORES_PROFILES = {
|
|
66
|
+
"4444": { profile: "4444", pixelFormat: "yuva444p10le", alpha: true },
|
|
67
|
+
hq: { profile: "3", pixelFormat: "yuv422p10le", alpha: false },
|
|
68
|
+
"422": { profile: "2", pixelFormat: "yuv422p10le", alpha: false },
|
|
69
|
+
lt: { profile: "1", pixelFormat: "yuv422p10le", alpha: false },
|
|
70
|
+
proxy: { profile: "0", pixelFormat: "yuv422p10le", alpha: false },
|
|
71
|
+
};
|
|
72
|
+
export function isProresProfile(value) {
|
|
73
|
+
return typeof value === "string" && PRORES_PROFILE_NAMES.includes(value);
|
|
74
|
+
}
|
|
62
75
|
/**
|
|
63
76
|
* Get encoder preset for a given quality and output format.
|
|
64
77
|
* WebM uses VP9 with alpha-capable pixel format; MP4 uses h264 (or h265 for HDR);
|
|
65
|
-
* MOV uses ProRes 4444 with alpha for editor-compatible transparency.
|
|
78
|
+
* MOV uses ProRes 4444 with alpha for editor-compatible transparency, or a 422 profile without alpha.
|
|
66
79
|
*/
|
|
67
|
-
export function getEncoderPreset(quality, format = "mp4", hdr) {
|
|
80
|
+
export function getEncoderPreset(quality, format = "mp4", hdr, proresProfile = "4444") {
|
|
68
81
|
const base = ENCODER_PRESETS[quality];
|
|
69
82
|
if (format === "webm") {
|
|
70
83
|
return {
|
|
@@ -75,12 +88,8 @@ export function getEncoderPreset(quality, format = "mp4", hdr) {
|
|
|
75
88
|
};
|
|
76
89
|
}
|
|
77
90
|
if (format === "mov") {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
quality: base.quality,
|
|
81
|
-
codec: "prores",
|
|
82
|
-
pixelFormat: "yuva444p10le",
|
|
83
|
-
};
|
|
91
|
+
const { profile, pixelFormat } = PRORES_PROFILES[proresProfile];
|
|
92
|
+
return { preset: profile, quality: base.quality, codec: "prores", pixelFormat };
|
|
84
93
|
}
|
|
85
94
|
if (hdr) {
|
|
86
95
|
return {
|
|
@@ -635,8 +644,9 @@ export async function muxVideoWithAudio(videoPath, audioPath, outputPath, signal
|
|
|
635
644
|
const outputDir = dirname(outputPath);
|
|
636
645
|
if (!existsSync(outputDir))
|
|
637
646
|
mkdirSync(outputDir, { recursive: true });
|
|
638
|
-
const
|
|
639
|
-
const
|
|
647
|
+
const outputExtension = extname(outputPath).toLowerCase();
|
|
648
|
+
const isWebm = outputExtension === ".webm";
|
|
649
|
+
const isMov = outputExtension === ".mov";
|
|
640
650
|
const shouldCopyAudio = isWebm ? false : await shouldCopyAacSidecar(audioPath, config);
|
|
641
651
|
const args = [...(config?.videoInputArgs ?? ["-i", videoPath]), "-i", audioPath, "-c:v", "copy"];
|
|
642
652
|
if (isWebm) {
|
|
@@ -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, mediaFileIdentity, readMetadataCache, readStoredFinalFrame, resolvePlayableVideoDuration, writeMetadataCache, } from "../utils/ffprobe.js";
|
|
20
|
+
import { extractFinalVideoFrameTimestamp, extractMediaMetadata, keyframeGapSeconds, mediaFileIdentity, readMetadataCache, readStoredFinalFrame, resolvePlayableVideoDuration, 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";
|
|
@@ -508,6 +508,22 @@ export function sdrCanvasColourFilters(metadata, toneMappedToSdr = false) {
|
|
|
508
508
|
* frames are byte-identical to it.
|
|
509
509
|
*/
|
|
510
510
|
const EXTRACTION_SEGMENT_SECONDS = 120;
|
|
511
|
+
/** Slowest decode rate the stall clock allows while ffmpeg discards frames from a keyframe up to the seek point. */
|
|
512
|
+
const SKIP_DECODE_FPS_FLOOR = 30;
|
|
513
|
+
async function leadInSecondsUnlessAborted(videoPath, seek, signal) {
|
|
514
|
+
try {
|
|
515
|
+
return await keyframeGapSeconds(videoPath, seek, signal);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
// Aborted: let runFfmpeg report the abort, so a segment stopped by another's failure reads as stopped.
|
|
519
|
+
if (signal?.aborted)
|
|
520
|
+
return 0;
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function stallTimeoutMs(baseMs, skippedSeconds, sourceFps) {
|
|
525
|
+
return baseMs + Math.ceil((skippedSeconds * sourceFps * 1000) / SKIP_DECODE_FPS_FLOOR);
|
|
526
|
+
}
|
|
511
527
|
/** Frame times since file start are `PTS*TB + seek`; snapping them to `lead + k/FR` gives every process one grid. */
|
|
512
528
|
function undoTickRoundingOnTheSourceFrameGrid(seek, lead) {
|
|
513
529
|
const sinceFileStart = `(PTS*TB+${seek}-${lead})`;
|
|
@@ -535,7 +551,7 @@ function splitFrameRanges(ranges, segmentFrames) {
|
|
|
535
551
|
return segments;
|
|
536
552
|
}
|
|
537
553
|
async function runSegmentedExtraction(job) {
|
|
538
|
-
const { decodeArgs, filterAndEncodeArgs, videoPath, startTime, fps, segments, gate } = job;
|
|
554
|
+
const { decodeArgs, filterAndEncodeArgs, videoPath, startTime, fps, sourceFps, segments, gate } = job;
|
|
539
555
|
const segmentCount = segments.length;
|
|
540
556
|
// One failed segment fails the range, so stop the others instead of finishing them.
|
|
541
557
|
const failed = new AbortController();
|
|
@@ -549,7 +565,7 @@ async function runSegmentedExtraction(job) {
|
|
|
549
565
|
const index = next++;
|
|
550
566
|
const { firstFrame, frames } = segments[index];
|
|
551
567
|
const seek = startTime + firstFrame / fps;
|
|
552
|
-
const result = await gate(() => runFfmpeg([
|
|
568
|
+
const result = await gate(async () => runFfmpeg([
|
|
553
569
|
...decodeArgs,
|
|
554
570
|
"-noaccurate_seek",
|
|
555
571
|
"-ss",
|
|
@@ -564,14 +580,21 @@ async function runSegmentedExtraction(job) {
|
|
|
564
580
|
"-start_number",
|
|
565
581
|
String(firstFrame + 1),
|
|
566
582
|
...filterAndEncodeArgs(seek),
|
|
567
|
-
], {
|
|
583
|
+
], {
|
|
584
|
+
...job.runOptions,
|
|
585
|
+
signal,
|
|
586
|
+
timeout: stallTimeoutMs(job.runOptions.timeout, await leadInSecondsUnlessAborted(videoPath, seek, signal), sourceFps),
|
|
587
|
+
}), signal);
|
|
568
588
|
results[index] = result;
|
|
569
589
|
if (!result.success)
|
|
570
590
|
failed.abort();
|
|
571
591
|
}
|
|
572
592
|
};
|
|
573
593
|
const workers = Math.min(segmentCount, EXTRACTION_SLOTS);
|
|
574
|
-
await Promise.all(Array.from({ length: workers }, worker))
|
|
594
|
+
await Promise.all(Array.from({ length: workers }, worker)).catch((error) => {
|
|
595
|
+
failed.abort();
|
|
596
|
+
throw error;
|
|
597
|
+
});
|
|
575
598
|
const ran = results.filter(Boolean);
|
|
576
599
|
// Report the segment that failed, not the ones stopped because of it.
|
|
577
600
|
const failure = ran.find((r) => !r.success && r.terminationReason !== "abort") ?? ran.find((r) => !r.success);
|
|
@@ -731,11 +754,17 @@ outputDirOverride) {
|
|
|
731
754
|
videoPath,
|
|
732
755
|
startTime,
|
|
733
756
|
fps,
|
|
757
|
+
sourceFps: metadata.fps,
|
|
734
758
|
gate: options.tailProbe ? inTailProbeSlot : inExtractionSlot,
|
|
735
759
|
segments: splitFrameRanges(options.frameRanges ?? [{ firstFrame: 0, frames: totalFrames }], segmentFrames),
|
|
736
760
|
runOptions,
|
|
737
761
|
})
|
|
738
|
-
: inExtractionSlot(() => runFfmpeg([...hwaccel, ...args, ...filterArgsFor(startTime), ...encodeArgs],
|
|
762
|
+
: inExtractionSlot(async () => runFfmpeg([...hwaccel, ...args, ...filterArgsFor(startTime), ...encodeArgs], {
|
|
763
|
+
...runOptions,
|
|
764
|
+
timeout: stallTimeoutMs(ffmpegProcessTimeout, options.finalFrameOnly
|
|
765
|
+
? startTime
|
|
766
|
+
: await leadInSecondsUnlessAborted(videoPath, startTime, signal), metadata.fps),
|
|
767
|
+
}), signal);
|
|
739
768
|
let processResult = await extract(decodeWithVideoToolbox ? ["-hwaccel", "videotoolbox"] : []);
|
|
740
769
|
const ffmpegExitedFailing = processResult.terminationReason === "exit" &&
|
|
741
770
|
!processResult.success &&
|
|
@@ -1349,7 +1378,7 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
|
|
|
1349
1378
|
// Phase 3 still calls `extractVideoFramesRange` on the same past-EOF
|
|
1350
1379
|
// mediaStart, surfacing a second raw FFmpeg error for the same clip.
|
|
1351
1380
|
const hdrSkippedIndices = new Set();
|
|
1352
|
-
if (hdrInfo.hasHdr && hdrInfo.dominantTransfer) {
|
|
1381
|
+
if (!options.toneMapHdrToSdr && hdrInfo.hasHdr && hdrInfo.dominantTransfer) {
|
|
1353
1382
|
// dominantTransfer is "majority wins" — if a composition mixes PQ and HLG
|
|
1354
1383
|
// sources (rare but legal), the minority transfer's videos get converted
|
|
1355
1384
|
// with the wrong curve. We treat this as caller-error: a single composition
|
package/dist/utils/ffprobe.d.ts
CHANGED
|
@@ -219,4 +219,6 @@ export interface KeyframeAnalysis {
|
|
|
219
219
|
* Pass the stream's own duration/start (not the container's), which can overstate a single-keyframe GOP.
|
|
220
220
|
*/
|
|
221
221
|
export declare function analyzeKeyframeIntervals(filePath: string, metadata: Pick<VideoMetadata, "videoStreamDurationSeconds" | "videoStreamStartSeconds">): Promise<KeyframeAnalysis>;
|
|
222
|
+
/** Seconds ffmpeg decodes without output after a seek to `seekSeconds`: back to the keyframe it lands on. */
|
|
223
|
+
export declare function keyframeGapSeconds(filePath: string, seekSeconds: number, signal?: AbortSignal): Promise<number>;
|
|
222
224
|
export {};
|
package/dist/utils/ffprobe.js
CHANGED
|
@@ -1084,6 +1084,15 @@ export async function analyzeKeyframeIntervals(filePath, metadata) {
|
|
|
1084
1084
|
});
|
|
1085
1085
|
return promise;
|
|
1086
1086
|
}
|
|
1087
|
+
/** Seconds ffmpeg decodes without output after a seek to `seekSeconds`: back to the keyframe it lands on. */
|
|
1088
|
+
export async function keyframeGapSeconds(filePath, seekSeconds, signal) {
|
|
1089
|
+
const stdout = await runFfprobe(filePath, [
|
|
1090
|
+
...["-select_streams", "v:0", "-show_entries", "packet=pts_time", "-of", "csv=p=0"],
|
|
1091
|
+
...["-read_intervals", `${seekSeconds}%+#1`],
|
|
1092
|
+
], signal);
|
|
1093
|
+
const keyframe = parseFloat(stdout.split("\n")[0] ?? "");
|
|
1094
|
+
return Number.isFinite(keyframe) ? Math.max(0, seekSeconds - keyframe) : 0;
|
|
1095
|
+
}
|
|
1087
1096
|
async function analyzeKeyframeIntervalsUncached(filePath, videoStreamStartSeconds, videoStreamDurationSeconds) {
|
|
1088
1097
|
const stdout = await runFfprobe(filePath, [
|
|
1089
1098
|
"-select_streams",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.210",
|
|
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.8.0",
|
|
56
56
|
"puppeteer-core": "^25.8.0",
|
|
57
|
-
"@hyperframes/
|
|
58
|
-
"@hyperframes/
|
|
57
|
+
"@hyperframes/core": "^0.8.210",
|
|
58
|
+
"@hyperframes/parsers": "^0.8.210"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^25.0.10",
|