@hyperframes/engine 0.8.224 → 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.
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
? [
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
931
|
+
formatFfmpegSeconds(totalDuration),
|
|
927
932
|
"-y",
|
|
928
933
|
outputPath,
|
|
929
934
|
];
|
|
@@ -313,6 +313,7 @@ export interface TimelineExtractionWindow {
|
|
|
313
313
|
}
|
|
314
314
|
type TimelineWindowVideo = Pick<VideoElement, "start" | "end" | "mediaStart"> & Partial<Pick<VideoElement, "playbackRate">> & Partial<Pick<VideoElement, "loop">>;
|
|
315
315
|
export declare function isVideoPastTimelineEnd(video: Pick<VideoElement, "start">, timelineEnd: number): boolean;
|
|
316
|
+
export declare function alignClipStartToFirstOutputFrame(video: VideoElement, fps: number): void;
|
|
316
317
|
/** Move a clip to its extraction window; a rate lane is shifted so it keeps integrating from the new origin. */
|
|
317
318
|
export declare function rebaseVideoToWindow(video: VideoElement, window: TimelineExtractionWindow): void;
|
|
318
319
|
/**
|
|
@@ -18,7 +18,7 @@ import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
|
|
|
18
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";
|
|
19
19
|
import { resolveReferencedStart } from "./referenceResolver.js";
|
|
20
20
|
import { isKnownInactiveTimelineWindow } from "./mediaTimelineWindow.js";
|
|
21
|
-
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";
|
|
22
22
|
import { inputAlphaOpaqueWarning, probeInputAlphaPlane } from "../utils/alphaPlaneProbe.js";
|
|
23
23
|
import { redactUrlQueriesInError, urlInputArgs } from "../utils/urlInput.js";
|
|
24
24
|
import { analyzeCompositionHdr, isHdrColorSpace as isHdrColorSpaceUtil, } from "../utils/hdr.js";
|
|
@@ -593,12 +593,12 @@ async function runSegmentedExtraction(job) {
|
|
|
593
593
|
...decodeArgs,
|
|
594
594
|
"-noaccurate_seek",
|
|
595
595
|
"-ss",
|
|
596
|
-
|
|
596
|
+
formatFfmpegSeconds(job.ffmpegSeek(seek)),
|
|
597
597
|
"-i",
|
|
598
598
|
videoPath,
|
|
599
599
|
// Read one frame past the segment and keep exactly its own frames.
|
|
600
600
|
"-t",
|
|
601
|
-
|
|
601
|
+
formatFfmpegSeconds((frames + 1) / fps),
|
|
602
602
|
"-frames:v",
|
|
603
603
|
String(frames),
|
|
604
604
|
"-start_number",
|
|
@@ -709,13 +709,13 @@ outputDirOverride) {
|
|
|
709
709
|
// sample. This is intentionally reserved for the one-frame path: input
|
|
710
710
|
// seeking is faster, but valid unindexed transports (notably MPEG-TS with
|
|
711
711
|
// a negative timestamp base) can seek to EOF and emit zero frames.
|
|
712
|
-
args.push("-i", videoPath, "-ss",
|
|
712
|
+
args.push("-i", videoPath, "-ss", formatFfmpegSeconds(Math.max(0, ffmpegSeek(startTime))), "-frames:v", "1");
|
|
713
713
|
}
|
|
714
714
|
else {
|
|
715
715
|
// An accurate seek drops the frame on screen at startTime; the fps sampling below needs it.
|
|
716
716
|
if (sampleAtOutputFps)
|
|
717
717
|
args.push("-noaccurate_seek");
|
|
718
|
-
args.push("-ss",
|
|
718
|
+
args.push("-ss", formatFfmpegSeconds(ffmpegSeek(startTime)), "-i", videoPath, "-t", formatFfmpegSeconds(duration));
|
|
719
719
|
}
|
|
720
720
|
const vfFilters = [];
|
|
721
721
|
if (keepsHdrForChrome) {
|
|
@@ -917,14 +917,17 @@ const FINAL_FRAME_LOGICAL_DURATION_SECONDS = 1e-6;
|
|
|
917
917
|
export function isVideoPastTimelineEnd(video, timelineEnd) {
|
|
918
918
|
return video.start >= timelineEnd;
|
|
919
919
|
}
|
|
920
|
-
function alignClipStartToFirstOutputFrame(video, fps) {
|
|
920
|
+
export function alignClipStartToFirstOutputFrame(video, fps) {
|
|
921
921
|
if (video.loop || !Number.isFinite(video.start) || video.start < 0 || !(fps > 0))
|
|
922
922
|
return;
|
|
923
923
|
const position = exportClipWindow(video.start, Infinity, fps).start * fps;
|
|
924
924
|
const firstFrame = Math.abs(position - Math.round(position)) < 1e-6 ? Math.round(position) : Math.ceil(position);
|
|
925
925
|
const shift = firstFrame / fps - video.start;
|
|
926
|
-
|
|
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;
|
|
927
929
|
return;
|
|
930
|
+
}
|
|
928
931
|
const playbackRate = normalizeRateSpec(video.playbackRate);
|
|
929
932
|
video.mediaStart += sourceTimeAt(playbackRate, shift);
|
|
930
933
|
video.playbackRate = shiftRateLane(playbackRate, shift);
|
package/dist/utils/ffprobe.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/utils/ffprobe.js
CHANGED
|
@@ -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}`)) ??
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.8.
|
|
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.
|
|
63
|
-
"@hyperframes/parsers": "^0.8.
|
|
62
|
+
"@hyperframes/core": "^0.8.225",
|
|
63
|
+
"@hyperframes/parsers": "^0.8.225"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^25.0.10",
|