@xiping/node-utils 1.0.66 → 1.0.69

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.
@@ -58,6 +58,7 @@ async function extractFrames(videoPath, tempDir, frames, interval, maxConcurrenc
58
58
  "-q:v", "10",
59
59
  "-an",
60
60
  "-threads", "4",
61
+ "-strict", "unofficial", // 允许 limited-range YUV 输入,兼容 FFmpeg 7.x MJPEG 编码器
61
62
  framePath,
62
63
  ]);
63
64
  if (result.code === 0) {
@@ -19,6 +19,8 @@ export interface TranscodingConfig {
19
19
  thumbnailOptions?: Partial<ThumbnailOptions>;
20
20
  /** 目标编码格式,默认 'hevc' */
21
21
  format?: "av1" | "hevc";
22
+ /** 是否强制使用软编码(禁用硬件编码),默认 false */
23
+ forceSoftware?: boolean;
22
24
  /** 是否替换原文件(删除原文件并将输出重命名为原路径),默认 false */
23
25
  replaceOriginal?: boolean;
24
26
  /** 转换进度回调 */
@@ -29,6 +31,8 @@ export interface AvailableEncoders {
29
31
  libaomAv1: boolean;
30
32
  hevcNvenc: boolean;
31
33
  libx265: boolean;
34
+ /** macOS 上的 HEVC 硬编码(VideoToolbox) */
35
+ hevcVideoToolbox: boolean;
32
36
  }
33
37
  /** 同步检测可用编码器 */
34
38
  export declare function getAvailableEncoders(): AvailableEncoders;
@@ -11,6 +11,7 @@ const log = (message, data) => {
11
11
  const defaultConfig = {
12
12
  generateThumbnail: true,
13
13
  format: "hevc",
14
+ forceSoftware: false,
14
15
  replaceOriginal: false,
15
16
  };
16
17
  /** 同步检测可用编码器 */
@@ -22,6 +23,8 @@ export function getAvailableEncoders() {
22
23
  libaomAv1: stdout.includes("libaom-av1"),
23
24
  hevcNvenc: stdout.includes("hevc_nvenc"),
24
25
  libx265: stdout.includes("libx265"),
26
+ // macOS VideoToolbox 硬件编码(HEVC)
27
+ hevcVideoToolbox: stdout.includes("hevc_videotoolbox"),
25
28
  };
26
29
  }
27
30
  function validateAndSanitizePath(inputPath) {
@@ -77,10 +80,10 @@ function parseOutTimeFromProgressFile(content) {
77
80
  return lastUs != null ? lastUs / 1e6 : null;
78
81
  }
79
82
  /** 根据格式与可用编码器构建 ffmpeg 参数数组;progressPath 用于 -progress 以获取实时进度(避免 stderr 缓冲导致 0 直接跳到 99) */
80
- function buildTranscodeArgs(inputPath, outputPath, format, encoders, progressPath) {
83
+ function buildTranscodeArgs(inputPath, outputPath, format, encoders, forceSoftware, progressPath) {
81
84
  const args = ["-i", inputPath];
82
85
  if (format === "av1") {
83
- if (encoders.av1Nvenc) {
86
+ if (!forceSoftware && encoders.av1Nvenc) {
84
87
  args.push("-c:v", "av1_nvenc", "-preset", "slow", "-rc", "constqp", "-qp", "28");
85
88
  }
86
89
  else if (encoders.libaomAv1) {
@@ -91,7 +94,12 @@ function buildTranscodeArgs(inputPath, outputPath, format, encoders, progressPat
91
94
  }
92
95
  }
93
96
  else {
94
- if (encoders.hevcNvenc) {
97
+ const isMacOS = os.platform() === "darwin";
98
+ if (!forceSoftware && isMacOS && encoders.hevcVideoToolbox) {
99
+ // macOS 上优先使用 VideoToolbox 硬件编码
100
+ args.push("-c:v", "hevc_videotoolbox");
101
+ }
102
+ else if (!forceSoftware && encoders.hevcNvenc) {
95
103
  args.push("-c:v", "hevc_nvenc", "-preset", "slow", "-rc", "constqp", "-qp", "28");
96
104
  }
97
105
  else if (encoders.libx265) {
@@ -109,10 +117,10 @@ function buildTranscodeArgs(inputPath, outputPath, format, encoders, progressPat
109
117
  }
110
118
  const PROGRESS_POLL_INTERVAL_MS = 400;
111
119
  /** 使用 spawn 执行 ffmpeg 转码。进度通过 -progress 文件轮询获取,避免 stderr 管道缓冲导致 0 直接跳到 99。 */
112
- function runTranscodeWithProgress(inputPath, outputPath, format, encoders, totalDuration, onProgress) {
120
+ function runTranscodeWithProgress(inputPath, outputPath, format, encoders, forceSoftware, totalDuration, onProgress) {
113
121
  return new Promise((resolve, reject) => {
114
122
  const progressPath = path.join(os.tmpdir(), `ffmpeg-progress-${process.pid}-${Date.now()}`);
115
- const args = buildTranscodeArgs(inputPath, outputPath, format, encoders, progressPath);
123
+ const args = buildTranscodeArgs(inputPath, outputPath, format, encoders, forceSoftware, progressPath);
116
124
  log("ffmpeg spawn", { totalDuration, format, outputPath, progressPath });
117
125
  const child = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
118
126
  let lastPercent = -1;
@@ -233,7 +241,7 @@ export async function transcoding(inputPath, config = {}) {
233
241
  const reportProgress = (progress) => {
234
242
  finalConfig.onProgress?.(progress);
235
243
  };
236
- await runTranscodeWithProgress(validatedPath, tempOutputPath, format, encoders, duration, reportProgress);
244
+ await runTranscodeWithProgress(validatedPath, tempOutputPath, format, encoders, finalConfig.forceSoftware, duration, reportProgress);
237
245
  if (finalConfig.generateThumbnail) {
238
246
  log("generating thumbnail", finalConfig.thumbnailOptions ?? {});
239
247
  await getThumbnail(validatedPath, finalConfig.thumbnailOptions ?? {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiping/node-utils",
3
- "version": "1.0.66",
3
+ "version": "1.0.69",
4
4
  "description": "node-utils",
5
5
  "type": "module",
6
6
  "author": "The-End-Hero <527409987@qq.com>",
@@ -25,7 +25,7 @@
25
25
  "bugs": {
26
26
  "url": "https://github.com/The-End-Hero/xiping/issues"
27
27
  },
28
- "gitHead": "477bf1fb5b65d5e34b2641eb24068bc064ebb648",
28
+ "gitHead": "3bf85a7229343f0c7141d60ceac026773a2c98cb",
29
29
  "publishConfig": {
30
30
  "access": "public",
31
31
  "registry": "https://registry.npmjs.org/"