@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.
@@ -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 args = buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder);
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 args = buildEncoderArgs(options, inputArgs, chunkPath, gpuEncoder);
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 false;
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 args = buildStreamingArgs(options, outputPath, gpuEncoder);
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 > resolvePlayableVideoDuration(metadata) + 1e-3 ||
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>>;
@@ -44,9 +44,10 @@ export async function selectUsableGpuEncoder(candidates, isUsable) {
44
44
  }
45
45
  return null;
46
46
  }
47
- export async function detectGpuEncoder() {
48
- const ffmpeg = spawn(getFfmpegBinary(), ["-encoders"], {
49
- stdio: ["pipe", "pipe", "pipe"],
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() + 30_000,
60
+ deadlineAtMs: Date.now() + deadlineMs,
61
+ terminationGraceMs,
60
62
  }).wait();
61
- if (outcome.reason !== "exit" || outcome.exitCode !== 0)
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.226",
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.226",
63
- "@hyperframes/core": "^0.8.226"
62
+ "@hyperframes/parsers": "^0.8.227",
63
+ "@hyperframes/core": "^0.8.227"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "^25.0.10",