@xiping/node-utils 1.0.68 → 1.0.70

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.
@@ -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,11 +94,17 @@ function buildTranscodeArgs(inputPath, outputPath, format, encoders, progressPat
91
94
  }
92
95
  }
93
96
  else {
94
- if (encoders.hevcNvenc) {
95
- args.push("-c:v", "hevc_nvenc", "-preset", "slow", "-rc", "constqp", "-qp", "28");
97
+ const isMacOS = os.platform() === "darwin";
98
+ if (!forceSoftware && isMacOS && encoders.hevcVideoToolbox) {
99
+ // macOS 上优先使用 VideoToolbox 硬件编码
100
+ // 使用固定质量参数,避免完全依赖默认码率/质量导致输出不可控
101
+ args.push("-c:v", "hevc_videotoolbox", "-q:v", "60");
102
+ }
103
+ else if (!forceSoftware && encoders.hevcNvenc) {
104
+ args.push("-c:v", "hevc_nvenc", "-preset", "slow", "-rc", "constqp", "-qp", "22");
96
105
  }
97
106
  else if (encoders.libx265) {
98
- args.push("-c:v", "libx265", "-crf", "28");
107
+ args.push("-c:v", "libx265", "-crf", "22");
99
108
  }
100
109
  else {
101
110
  throw new Error("没有可用的 HEVC 编码器");
@@ -109,10 +118,10 @@ function buildTranscodeArgs(inputPath, outputPath, format, encoders, progressPat
109
118
  }
110
119
  const PROGRESS_POLL_INTERVAL_MS = 400;
111
120
  /** 使用 spawn 执行 ffmpeg 转码。进度通过 -progress 文件轮询获取,避免 stderr 管道缓冲导致 0 直接跳到 99。 */
112
- function runTranscodeWithProgress(inputPath, outputPath, format, encoders, totalDuration, onProgress) {
121
+ function runTranscodeWithProgress(inputPath, outputPath, format, encoders, forceSoftware, totalDuration, onProgress) {
113
122
  return new Promise((resolve, reject) => {
114
123
  const progressPath = path.join(os.tmpdir(), `ffmpeg-progress-${process.pid}-${Date.now()}`);
115
- const args = buildTranscodeArgs(inputPath, outputPath, format, encoders, progressPath);
124
+ const args = buildTranscodeArgs(inputPath, outputPath, format, encoders, forceSoftware, progressPath);
116
125
  log("ffmpeg spawn", { totalDuration, format, outputPath, progressPath });
117
126
  const child = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
118
127
  let lastPercent = -1;
@@ -233,7 +242,7 @@ export async function transcoding(inputPath, config = {}) {
233
242
  const reportProgress = (progress) => {
234
243
  finalConfig.onProgress?.(progress);
235
244
  };
236
- await runTranscodeWithProgress(validatedPath, tempOutputPath, format, encoders, duration, reportProgress);
245
+ await runTranscodeWithProgress(validatedPath, tempOutputPath, format, encoders, finalConfig.forceSoftware, duration, reportProgress);
237
246
  if (finalConfig.generateThumbnail) {
238
247
  log("generating thumbnail", finalConfig.thumbnailOptions ?? {});
239
248
  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.68",
3
+ "version": "1.0.70",
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": "7a87687336caa06df816ac28f7a79ab92f856447",
28
+ "gitHead": "61f56a865103626696a6b73c0527c2cb5fc859c1",
29
29
  "publishConfig": {
30
30
  "access": "public",
31
31
  "registry": "https://registry.npmjs.org/"