@hyperframes/engine 0.9.28-alpha.0 → 0.9.28
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.
|
@@ -42,7 +42,7 @@ export declare const GC_MARKER = ".hf-last-gc";
|
|
|
42
42
|
* Current schema version. Bump it whenever extraction writes different frames for the same key,
|
|
43
43
|
* or warm entries keep serving the old frames across a deploy. Each bump's commit says why.
|
|
44
44
|
*/
|
|
45
|
-
export declare const SCHEMA_PREFIX = "hfcache-
|
|
45
|
+
export declare const SCHEMA_PREFIX = "hfcache-v9-";
|
|
46
46
|
export type CacheFrameFormat = "jpg" | "png";
|
|
47
47
|
export interface CacheKeyInput {
|
|
48
48
|
/** Absolute path to the source video file. Part of the key so moved files
|
|
@@ -46,7 +46,7 @@ export const GC_MARKER = ".hf-last-gc";
|
|
|
46
46
|
* Current schema version. Bump it whenever extraction writes different frames for the same key,
|
|
47
47
|
* or warm entries keep serving the old frames across a deploy. Each bump's commit says why.
|
|
48
48
|
*/
|
|
49
|
-
export const SCHEMA_PREFIX = "hfcache-
|
|
49
|
+
export const SCHEMA_PREFIX = "hfcache-v9-";
|
|
50
50
|
/** Truncated hex chars of SHA-256 used for the entry directory name. */
|
|
51
51
|
const KEY_HEX_CHARS = 16;
|
|
52
52
|
export async function readContentSha256(path, signal) {
|
|
@@ -584,10 +584,12 @@ async function leadInSecondsUnlessAborted(videoPath, seek, signal) {
|
|
|
584
584
|
function stallTimeoutMs(baseMs, skippedSeconds, sourceFps) {
|
|
585
585
|
return baseMs + Math.ceil((skippedSeconds * sourceFps * 1000) / SKIP_DECODE_FPS_FLOOR);
|
|
586
586
|
}
|
|
587
|
+
const KEEP_END_PAST_A_ONE_TICK_FINAL_FRAME_THROUGH_THE_SNAP = "tpad=stop_mode=clone:stop=1";
|
|
587
588
|
/** Frame times since file start are `PTS*TB + seek`; snapping them to `lead + k/FR` gives every process one grid. */
|
|
588
|
-
function undoTickRoundingOnTheSourceFrameGrid(seek, lead) {
|
|
589
|
+
function undoTickRoundingOnTheSourceFrameGrid(seek, lead, sourceFps) {
|
|
589
590
|
const sinceFileStart = `(PTS*TB+${seek}-${lead})`;
|
|
590
591
|
return [
|
|
592
|
+
...(sourceFps > 0 ? [KEEP_END_PAST_A_ONE_TICK_FINAL_FRAME_THROUGH_THE_SNAP] : []),
|
|
591
593
|
"settb=AVTB",
|
|
592
594
|
`setpts=if(isnan(FR)\\,PTS\\,(${lead}+round(${sinceFileStart}*FR)/FR-${seek})/TB)`,
|
|
593
595
|
];
|
|
@@ -610,13 +612,14 @@ function framesWrittenReader(onProgress) {
|
|
|
610
612
|
return (chunk) => read(chunk);
|
|
611
613
|
}
|
|
612
614
|
/** Split runs so no single ffmpeg process extracts more than `segmentFrames` frames. */
|
|
613
|
-
function splitFrameRanges(ranges, segmentFrames) {
|
|
615
|
+
function splitFrameRanges(ranges, segmentFrames, frameLimit) {
|
|
614
616
|
const segments = [];
|
|
615
617
|
for (const range of ranges) {
|
|
616
|
-
|
|
618
|
+
const frames = Math.min(range.frames, frameLimit - range.firstFrame);
|
|
619
|
+
for (let offset = 0; offset < frames; offset += segmentFrames) {
|
|
617
620
|
segments.push({
|
|
618
621
|
firstFrame: range.firstFrame + offset,
|
|
619
|
-
frames: Math.min(segmentFrames,
|
|
622
|
+
frames: Math.min(segmentFrames, frames - offset),
|
|
620
623
|
});
|
|
621
624
|
}
|
|
622
625
|
}
|
|
@@ -710,7 +713,7 @@ function videoFilterChain(input) {
|
|
|
710
713
|
? [
|
|
711
714
|
...(metadata.isVFR
|
|
712
715
|
? []
|
|
713
|
-
: undoTickRoundingOnTheSourceFrameGrid(seek, videoStartOnMediaTimeline(metadata))),
|
|
716
|
+
: undoTickRoundingOnTheSourceFrameGrid(seek, videoStartOnMediaTimeline(metadata), metadata.fps)),
|
|
714
717
|
"settb=intb/2",
|
|
715
718
|
"setpts=PTS-lte(TB\\,0.001)",
|
|
716
719
|
`fps=${ffmpegFps}:start_time=0:round=up`,
|
|
@@ -871,11 +874,14 @@ outputDirOverride) {
|
|
|
871
874
|
return filters.length > 0 ? ["-vf", filters.join(",")] : [];
|
|
872
875
|
};
|
|
873
876
|
const segmentFrames = Math.round(EXTRACTION_SEGMENT_SECONDS * fps);
|
|
874
|
-
const
|
|
877
|
+
const windowFrames = extractionFrameCountForDuration(duration, normalizedFps, metadata.isVFR);
|
|
878
|
+
const framesBeforeSourceEnd = extractionFrameCountForDuration(playableDuration - startTime, normalizedFps, false);
|
|
879
|
+
const totalFrames = metadata.isVFR ? windowFrames : Math.min(windowFrames, framesBeforeSourceEnd);
|
|
875
880
|
// A VFR window rounds its frame count up, and only the segments' `-frames:v` bound
|
|
876
881
|
// emits that last slot: the fps filter ends a `-t` window at the nearest slot. A
|
|
877
882
|
// decimal rate counts zero frames here and keeps the `-t` window.
|
|
878
883
|
const boundVfrFrameCount = metadata.isVFR && totalFrames > 0;
|
|
884
|
+
const frameCap = sampleAtOutputFps && totalFrames > 0 ? ["-frames:v", String(totalFrames)] : [];
|
|
879
885
|
const extract = (hwaccel) => sampleAtOutputFps && (options.frameRanges || boundVfrFrameCount || totalFrames > segmentFrames)
|
|
880
886
|
? runSegmentedExtraction({
|
|
881
887
|
decodeArgs: [...hwaccel, ...decodeArgs],
|
|
@@ -886,11 +892,11 @@ outputDirOverride) {
|
|
|
886
892
|
fps,
|
|
887
893
|
sourceFps: metadata.fps,
|
|
888
894
|
gate: options.tailProbe ? inTailProbeSlot : inExtractionSlot,
|
|
889
|
-
segments: splitFrameRanges(options.frameRanges ?? [{ firstFrame: 0, frames: totalFrames }], segmentFrames),
|
|
895
|
+
segments: splitFrameRanges(options.frameRanges ?? [{ firstFrame: 0, frames: totalFrames }], segmentFrames, totalFrames > 0 ? totalFrames : Number.POSITIVE_INFINITY),
|
|
890
896
|
runOptions,
|
|
891
897
|
onProgress: options.onProgress,
|
|
892
898
|
})
|
|
893
|
-
: inExtractionSlot(async () => runFfmpeg([...hwaccel, ...args, ...filterArgsFor(startTime), ...encodeArgs], {
|
|
899
|
+
: inExtractionSlot(async () => runFfmpeg([...hwaccel, ...args, ...frameCap, ...filterArgsFor(startTime), ...encodeArgs], {
|
|
894
900
|
...runOptions,
|
|
895
901
|
onStderr: framesWrittenReader(options.onProgress),
|
|
896
902
|
timeout: stallTimeoutMs(ffmpegProcessTimeout, options.finalFrameOnly
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.9.28
|
|
3
|
+
"version": "0.9.28",
|
|
4
4
|
"description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"puppeteer": "~25.11.0",
|
|
62
62
|
"puppeteer-core": "^25.11.0",
|
|
63
63
|
"undici": "7.25.0",
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
64
|
+
"@hyperframes/core": "^0.9.28",
|
|
65
|
+
"@hyperframes/parsers": "^0.9.28"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^25.0.10",
|