@hyperframes/engine 0.8.226 → 0.8.227
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/services/chunkEncoder.d.ts +1 -1
- package/dist/services/chunkEncoder.js +7 -5
- package/dist/services/frameCapture.js +3 -1
- package/dist/services/streamingEncoder.d.ts +1 -1
- package/dist/services/streamingEncoder.js +6 -5
- package/dist/services/videoFrameExtractor.js +4 -3
- package/dist/utils/gpuEncoder.d.ts +5 -1
- package/dist/utils/gpuEncoder.js +58 -6
- package/package.json +3 -3
|
@@ -105,7 +105,7 @@ export declare function appendRateCapArgs(args: string[], options: {
|
|
|
105
105
|
maxBitrate?: string;
|
|
106
106
|
bufferSize?: string;
|
|
107
107
|
}): void;
|
|
108
|
-
export declare function buildEncoderArgs(options: EncoderOptions, inputArgs: string[], outputPath: string, gpuEncoder?: GpuEncoder): string[];
|
|
108
|
+
export declare function buildEncoderArgs(options: EncoderOptions, inputArgs: string[], outputPath: string, gpuEncoder?: GpuEncoder, nvencPresets?: ReadonlySet<string>): string[];
|
|
109
109
|
export declare function encodeFramesFromDir(framesDir: string, framePattern: string, outputPath: string, options: EncoderOptions, signal?: AbortSignal, config?: Partial<Pick<EngineConfig, "ffmpegEncodeTimeout">>, onFramesEncoded?: (frames: number) => void): Promise<EncodeResult>;
|
|
110
110
|
export declare function buildConcatArgs(concatListPath: string, outputPath: string): string[];
|
|
111
111
|
/**
|
|
@@ -9,7 +9,7 @@ import { frameSequencePath } from "./extractedFrameIndex.js";
|
|
|
9
9
|
import { closeSync, copyFileSync, existsSync, ftruncateSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, statSync, writeFileSync, writeSync, } from "fs";
|
|
10
10
|
import { join, dirname, extname, resolve } from "path";
|
|
11
11
|
import { DEFAULT_CONFIG } from "../config.js";
|
|
12
|
-
import { getCachedGpuEncoder, getGpuEncoderName, buildVideoToolboxRateControlArgs, mapPresetForGpuEncoder, } from "../utils/gpuEncoder.js";
|
|
12
|
+
import { getCachedGpuEncoder, getCachedNvencPresets, getGpuEncoderName, buildVideoToolboxRateControlArgs, mapPresetForGpuEncoder, } from "../utils/gpuEncoder.js";
|
|
13
13
|
import { getHdrEncoderColorParams, hdrFrameTagsFilter } from "../utils/hdr.js";
|
|
14
14
|
import { withEvenDimensionPad } from "../utils/evenDimensions.js";
|
|
15
15
|
import { SDR_CAPTURE_TO_BT709_FILTER, SDR_RGB_TO_TAGGED_BT709_FILTER, } from "../utils/sdrCaptureColor.js";
|
|
@@ -169,7 +169,7 @@ export function appendRateCapArgs(args, options) {
|
|
|
169
169
|
}
|
|
170
170
|
args.push("-maxrate", String(Math.round(cap)), "-bufsize", String(Math.round(buffer)));
|
|
171
171
|
}
|
|
172
|
-
export function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
|
|
172
|
+
export function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null, nvencPresets) {
|
|
173
173
|
const { fps, codec = "h264", preset = "medium", quality = 23, bitrate, pixelFormat = "yuv420p", vp9CpuUsed, useGpu = false, } = options;
|
|
174
174
|
// libx264 cannot encode HDR. If a caller passes hdr with codec=h264 we'd
|
|
175
175
|
// produce a "half-HDR" file (BT.2020 container tags but a BT.709 VUI block
|
|
@@ -190,7 +190,7 @@ export function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = nu
|
|
|
190
190
|
args.push("-c:v", encoderName);
|
|
191
191
|
switch (gpuEncoder) {
|
|
192
192
|
case "nvenc":
|
|
193
|
-
args.push("-preset", mapPresetForGpuEncoder("nvenc", preset));
|
|
193
|
+
args.push("-preset", mapPresetForGpuEncoder("nvenc", preset, nvencPresets));
|
|
194
194
|
if (bitrate)
|
|
195
195
|
args.push("-b:v", bitrate);
|
|
196
196
|
else
|
|
@@ -437,7 +437,8 @@ export async function encodeFramesFromDir(framesDir, framePattern, outputPath, o
|
|
|
437
437
|
}
|
|
438
438
|
const inputPath = frameSequencePath(framesDir, framePattern);
|
|
439
439
|
const inputArgs = ["-framerate", fpsToFfmpegArg(options.fps), "-i", inputPath];
|
|
440
|
-
const
|
|
440
|
+
const nvencPresets = gpuEncoder === "nvenc" ? await getCachedNvencPresets() : undefined;
|
|
441
|
+
const args = buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder, nvencPresets);
|
|
441
442
|
const encodeTimeout = config?.ffmpegEncodeTimeout ?? DEFAULT_CONFIG.ffmpegEncodeTimeout;
|
|
442
443
|
let framesRead = 0;
|
|
443
444
|
const result = await runFfmpeg(args, {
|
|
@@ -587,7 +588,8 @@ export async function encodeFramesChunkedConcat(framesDir, framePattern, outputP
|
|
|
587
588
|
let gpuEncoder = null;
|
|
588
589
|
if (options.useGpu)
|
|
589
590
|
gpuEncoder = await getCachedGpuEncoder();
|
|
590
|
-
const
|
|
591
|
+
const nvencPresets = gpuEncoder === "nvenc" ? await getCachedNvencPresets() : undefined;
|
|
592
|
+
const args = buildEncoderArgs(options, inputArgs, chunkPath, gpuEncoder, nvencPresets);
|
|
591
593
|
const encodeTimeout = config?.ffmpegEncodeTimeout ?? DEFAULT_CONFIG.ffmpegEncodeTimeout;
|
|
592
594
|
let framesRead = 0;
|
|
593
595
|
const processResult = await runFfmpeg(args, {
|
|
@@ -1538,7 +1538,7 @@ export async function pollVideosReady(page, skipIds, timeoutMs, intervalMs = 100
|
|
|
1538
1538
|
return true;
|
|
1539
1539
|
if (ve.networkState === HTMLMediaElement.NETWORK_NO_SOURCE)
|
|
1540
1540
|
return true;
|
|
1541
|
-
return
|
|
1541
|
+
return !ve.getAttribute("src") && !ve.querySelector("source");
|
|
1542
1542
|
}));
|
|
1543
1543
|
}, skipIds));
|
|
1544
1544
|
};
|
|
@@ -1635,6 +1635,8 @@ export async function collectMediaReadinessWarnings(page, skipIds, timeoutMs) {
|
|
|
1635
1635
|
const mediaElement = media;
|
|
1636
1636
|
if (skipped.has(mediaElement.id))
|
|
1637
1637
|
continue;
|
|
1638
|
+
if (!mediaElement.getAttribute("src") && !mediaElement.querySelector("source"))
|
|
1639
|
+
continue;
|
|
1638
1640
|
const src = mediaElement.currentSrc || mediaElement.getAttribute("src") || "(no src)";
|
|
1639
1641
|
const failed = Boolean(mediaElement.error) ||
|
|
1640
1642
|
mediaElement.networkState === HTMLMediaElement.NETWORK_NO_SOURCE;
|
|
@@ -104,7 +104,7 @@ export interface StreamingEncoder {
|
|
|
104
104
|
* Exported so unit tests can assert on the constructed CLI without spawning
|
|
105
105
|
* FFmpeg — see streamingEncoder.test.ts.
|
|
106
106
|
*/
|
|
107
|
-
export declare function buildStreamingArgs(options: StreamingEncoderOptions, outputPath: string, gpuEncoder?: GpuEncoder): string[];
|
|
107
|
+
export declare function buildStreamingArgs(options: StreamingEncoderOptions, outputPath: string, gpuEncoder?: GpuEncoder, nvencPresets?: ReadonlySet<string>): string[];
|
|
108
108
|
/**
|
|
109
109
|
* Spawn a streaming FFmpeg encoder that accepts frame buffers on stdin.
|
|
110
110
|
*/
|
|
@@ -19,7 +19,7 @@ import { trackChildProcess } from "../utils/processTracker.js";
|
|
|
19
19
|
import { ManagedChildProcess, } from "../utils/managedChildProcess.js";
|
|
20
20
|
import { existsSync, mkdirSync, statSync } from "fs";
|
|
21
21
|
import { dirname } from "path";
|
|
22
|
-
import { getCachedGpuEncoder, getGpuEncoderName, buildVideoToolboxRateControlArgs, mapPresetForGpuEncoder, } from "../utils/gpuEncoder.js";
|
|
22
|
+
import { getCachedGpuEncoder, getCachedNvencPresets, getGpuEncoderName, buildVideoToolboxRateControlArgs, mapPresetForGpuEncoder, } from "../utils/gpuEncoder.js";
|
|
23
23
|
import { ffmpegStatsReader, formatFfmpegError, isExternalFfmpegInterruption, } from "../utils/runFfmpeg.js";
|
|
24
24
|
import { getFfmpegBinary } from "../utils/ffmpegBinaries.js";
|
|
25
25
|
import { getHdrEncoderColorParams, hdrFrameTagsFilter } from "../utils/hdr.js";
|
|
@@ -98,7 +98,7 @@ export function createFrameReorderBuffer(startFrame, endFrame) {
|
|
|
98
98
|
* Exported so unit tests can assert on the constructed CLI without spawning
|
|
99
99
|
* FFmpeg — see streamingEncoder.test.ts.
|
|
100
100
|
*/
|
|
101
|
-
export function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
|
|
101
|
+
export function buildStreamingArgs(options, outputPath, gpuEncoder = null, nvencPresets) {
|
|
102
102
|
const { fps, codec = "h264", preset = "medium", quality = 23, bitrate, pixelFormat = "yuv420p", vp9CpuUsed, useGpu = false, imageFormat = "jpeg", } = options;
|
|
103
103
|
// Input args: pipe from stdin
|
|
104
104
|
const args = [];
|
|
@@ -137,7 +137,7 @@ export function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
|
|
|
137
137
|
args.push("-c:v", encoderName);
|
|
138
138
|
switch (gpuEncoder) {
|
|
139
139
|
case "nvenc":
|
|
140
|
-
args.push("-preset", mapPresetForGpuEncoder("nvenc", preset));
|
|
140
|
+
args.push("-preset", mapPresetForGpuEncoder("nvenc", preset, nvencPresets));
|
|
141
141
|
if (bitrate)
|
|
142
142
|
args.push("-b:v", bitrate);
|
|
143
143
|
else
|
|
@@ -278,7 +278,7 @@ export function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
|
|
|
278
278
|
else if (options.videoLayer) {
|
|
279
279
|
const video = `[1:v]${options.videoLayer.filters.join(",")},format=gbrp[v]`;
|
|
280
280
|
const tail = withEvenDimensionPad(SDR_RGB_TO_TAGGED_BT709_FILTER, pixelFormat, options.width, options.height);
|
|
281
|
-
args.push("-filter_complex", `${video};[0:v]format=rgba[o];[v][o]overlay=format=gbrp,${tail}[vout]`, "-map", "[vout]");
|
|
281
|
+
args.push("-filter_complex", `${video};[0:v]format=rgba[o];[v][o]overlay=eof_action=endall:format=gbrp,${tail}[vout]`, "-map", "[vout]");
|
|
282
282
|
}
|
|
283
283
|
else {
|
|
284
284
|
// Pad odd dimensions up to even so 4:2:0 encoders (software and
|
|
@@ -309,7 +309,8 @@ export async function spawnStreamingEncoder(outputPath, options, signal, config)
|
|
|
309
309
|
if (options.useGpu) {
|
|
310
310
|
gpuEncoder = await getCachedGpuEncoder();
|
|
311
311
|
}
|
|
312
|
-
const
|
|
312
|
+
const nvencPresets = gpuEncoder === "nvenc" ? await getCachedNvencPresets() : undefined;
|
|
313
|
+
const args = buildStreamingArgs(options, outputPath, gpuEncoder, nvencPresets);
|
|
313
314
|
const ffmpeg = spawn(getFfmpegBinary(), args, {
|
|
314
315
|
stdio: ["pipe", "pipe", "pipe"],
|
|
315
316
|
// See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
|
|
@@ -709,10 +709,12 @@ function videoFilterChain(input) {
|
|
|
709
709
|
}
|
|
710
710
|
return (seek) => [...head, ...sampling(seek), ...tail];
|
|
711
711
|
}
|
|
712
|
+
const VIDEO_LAYER_TAIL_SLACK_SECONDS = 0.5;
|
|
712
713
|
/** ffmpeg input and filters showing the same frames and colour as extraction; undefined if unsupported. */
|
|
713
714
|
export async function buildVideoLayerInput(videoPath, startTime, duration, fps, canvas, fit = "fill") {
|
|
714
715
|
const metadata = await extractMediaMetadata(videoPath);
|
|
715
|
-
if (startTime + duration >
|
|
716
|
+
if (startTime + duration >
|
|
717
|
+
resolvePlayableVideoDuration(metadata) + VIDEO_LAYER_TAIL_SLACK_SECONDS ||
|
|
716
718
|
isHdrColorSpaceUtil(metadata.colorSpace) ||
|
|
717
719
|
metadata.isVFR ||
|
|
718
720
|
metadata.hasAlpha ||
|
|
@@ -733,6 +735,7 @@ export async function buildVideoLayerInput(videoPath, startTime, duration, fps,
|
|
|
733
735
|
? `scale=${canvas.width}:${canvas.height}:force_original_aspect_ratio=increase:flags=bicubic,crop=${canvas.width}:${canvas.height}`
|
|
734
736
|
: `scale=${canvas.width}:${canvas.height}:flags=bicubic`);
|
|
735
737
|
}
|
|
738
|
+
filters.push(`tpad=stop_mode=clone:stop_duration=${VIDEO_LAYER_TAIL_SLACK_SECONDS * 2}`);
|
|
736
739
|
return {
|
|
737
740
|
inputArgs: [
|
|
738
741
|
"-noaccurate_seek",
|
|
@@ -741,8 +744,6 @@ export async function buildVideoLayerInput(videoPath, startTime, duration, fps,
|
|
|
741
744
|
...urlInputArgs(videoPath),
|
|
742
745
|
"-i",
|
|
743
746
|
videoPath,
|
|
744
|
-
"-t",
|
|
745
|
-
formatFfmpegSeconds(duration),
|
|
746
747
|
],
|
|
747
748
|
filters,
|
|
748
749
|
};
|
|
@@ -31,4 +31,8 @@ export declare function getProbeArgs(encoder: ConcreteGpuEncoder): string[];
|
|
|
31
31
|
* - `videotoolbox`, `vaapi`, `amf`, `null`: no remap (they either ignore
|
|
32
32
|
* `-preset` entirely or accept the libx264 vocabulary).
|
|
33
33
|
*/
|
|
34
|
-
export declare function mapPresetForGpuEncoder(encoder: GpuEncoder, preset: string): string;
|
|
34
|
+
export declare function mapPresetForGpuEncoder(encoder: GpuEncoder, preset: string, nvencPresets?: ReadonlySet<string>): string;
|
|
35
|
+
/** The names an encoder lists under its `-preset` option in `ffmpeg -h encoder=<name>`. */
|
|
36
|
+
export declare function parseEncoderPresetNames(helpText: string): ReadonlySet<string>;
|
|
37
|
+
/** The presets this ffmpeg's h264_nvenc takes; empty when it cannot be read, which keeps the pN names. */
|
|
38
|
+
export declare function getCachedNvencPresets(): Promise<ReadonlySet<string>>;
|
package/dist/utils/gpuEncoder.js
CHANGED
|
@@ -44,9 +44,10 @@ export async function selectUsableGpuEncoder(candidates, isUsable) {
|
|
|
44
44
|
}
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
/** ffmpeg's stdout, or null when it did not exit cleanly within the deadline. */
|
|
48
|
+
async function ffmpegStdout(args, deadlineMs, terminationGraceMs) {
|
|
49
|
+
const ffmpeg = spawn(getFfmpegBinary(), args, {
|
|
50
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
50
51
|
// See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
|
|
51
52
|
windowsHide: true,
|
|
52
53
|
});
|
|
@@ -56,9 +57,14 @@ export async function detectGpuEncoder() {
|
|
|
56
57
|
stdout += data.toString();
|
|
57
58
|
});
|
|
58
59
|
const outcome = await new ManagedChildProcess(ffmpeg, {
|
|
59
|
-
deadlineAtMs: Date.now() +
|
|
60
|
+
deadlineAtMs: Date.now() + deadlineMs,
|
|
61
|
+
terminationGraceMs,
|
|
60
62
|
}).wait();
|
|
61
|
-
|
|
63
|
+
return outcome.reason === "exit" && outcome.exitCode === 0 ? stdout : null;
|
|
64
|
+
}
|
|
65
|
+
export async function detectGpuEncoder() {
|
|
66
|
+
const stdout = await ffmpegStdout(["-encoders"], 30_000);
|
|
67
|
+
if (stdout === null)
|
|
62
68
|
return null;
|
|
63
69
|
const candidates = getCompiledGpuEncoders(stdout);
|
|
64
70
|
return selectUsableGpuEncoder(candidates, canUseGpuEncoder).catch(() => null);
|
|
@@ -213,9 +219,11 @@ const QSV_PRESET_MAP = {
|
|
|
213
219
|
* - `videotoolbox`, `vaapi`, `amf`, `null`: no remap (they either ignore
|
|
214
220
|
* `-preset` entirely or accept the libx264 vocabulary).
|
|
215
221
|
*/
|
|
216
|
-
export function mapPresetForGpuEncoder(encoder, preset) {
|
|
222
|
+
export function mapPresetForGpuEncoder(encoder, preset, nvencPresets) {
|
|
217
223
|
switch (encoder) {
|
|
218
224
|
case "nvenc":
|
|
225
|
+
if (nvencPresets && nvencPresets.size > 0 && !nvencPresets.has("p4"))
|
|
226
|
+
return legacyNvencPreset(preset, nvencPresets);
|
|
219
227
|
if (/^p[1-7]$/.test(preset))
|
|
220
228
|
return preset;
|
|
221
229
|
return NVENC_PRESET_MAP[preset] ?? "p4";
|
|
@@ -225,3 +233,47 @@ export function mapPresetForGpuEncoder(encoder, preset) {
|
|
|
225
233
|
return preset;
|
|
226
234
|
}
|
|
227
235
|
}
|
|
236
|
+
// Builds on an older NVENC SDK list `default slow medium fast hp hq ...` and reject p1..p7.
|
|
237
|
+
const LEGACY_NVENC_PRESET_MAP = {
|
|
238
|
+
ultrafast: "hp",
|
|
239
|
+
superfast: "hp",
|
|
240
|
+
veryfast: "hp",
|
|
241
|
+
faster: "fast",
|
|
242
|
+
fast: "fast",
|
|
243
|
+
medium: "medium",
|
|
244
|
+
slow: "slow",
|
|
245
|
+
slower: "slow",
|
|
246
|
+
veryslow: "slow",
|
|
247
|
+
placebo: "slow",
|
|
248
|
+
};
|
|
249
|
+
function legacyNvencPreset(preset, accepted) {
|
|
250
|
+
if (accepted.has(preset) && !/^p[1-7]$/.test(preset))
|
|
251
|
+
return preset;
|
|
252
|
+
const mapped = LEGACY_NVENC_PRESET_MAP[preset] ?? "medium";
|
|
253
|
+
return accepted.has(mapped) ? mapped : "default";
|
|
254
|
+
}
|
|
255
|
+
/** The names an encoder lists under its `-preset` option in `ffmpeg -h encoder=<name>`. */
|
|
256
|
+
export function parseEncoderPresetNames(helpText) {
|
|
257
|
+
const names = new Set();
|
|
258
|
+
let inPreset = false;
|
|
259
|
+
for (const line of helpText.split(/\r?\n/)) {
|
|
260
|
+
if (/^\s{1,3}-\S/.test(line)) {
|
|
261
|
+
inPreset = /^\s{1,3}-preset\s/.test(line);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const entry = inPreset ? /^\s{4,}([a-z][a-z0-9_]*)\s+-?\d+\s+[E.]\S{3}/.exec(line) : null;
|
|
265
|
+
if (entry)
|
|
266
|
+
names.add(entry[1]);
|
|
267
|
+
}
|
|
268
|
+
return names;
|
|
269
|
+
}
|
|
270
|
+
let cachedNvencPresets = undefined;
|
|
271
|
+
/** The presets this ffmpeg's h264_nvenc takes; empty when it cannot be read, which keeps the pN names. */
|
|
272
|
+
export function getCachedNvencPresets() {
|
|
273
|
+
cachedNvencPresets ??= readNvencPresets();
|
|
274
|
+
return cachedNvencPresets;
|
|
275
|
+
}
|
|
276
|
+
async function readNvencPresets() {
|
|
277
|
+
const help = await ffmpegStdout(["-hide_banner", "-h", "encoder=h264_nvenc"], GPU_PROBE_TIMEOUT_MS, GPU_PROBE_KILL_GRACE_MS).catch(() => null);
|
|
278
|
+
return help === null ? new Set() : parseEncoderPresetNames(help);
|
|
279
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.227",
|
|
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/parsers": "^0.8.
|
|
63
|
-
"@hyperframes/core": "^0.8.
|
|
62
|
+
"@hyperframes/parsers": "^0.8.227",
|
|
63
|
+
"@hyperframes/core": "^0.8.227"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^25.0.10",
|