系列-脚本

将视频批量转换至 AV1 编码

运行环境:

单独使用 FFmpeg

1
2
3
ffmpeg -i "input.mp4" \
-vf "colorspace=iall=bt709:all=bt709:fast=1" \
-c:v libsvtav1 -crf 30 -preset 8 -c:a copy "output.mkv"
参数含义说明
-i "input.mp4"指定输入文件。这里是 "input.mp4"
-vf "colorspace=iall=bt709:all=bt709:fast=1"使用 colorspace 视频滤镜:
- iall=bt709:假设输入是 bt709 色彩空间(不信任输入的 metadata)。
- all=bt709:将输出转换为 bt709 色彩空间。
- fast=1:快速模式,提高性能,牺牲一点精度。
-c:v libsvtav1设置视频编码器为 libsvtav1,即使用 SVT-AV1 编码器编码视频。
-crf 30Constant Rate Factor,质量控制参数,数值越大体积越小,画质越低(范围一般为 0-63)。
-preset 8编码速度预设,8 通常是 veryslow,压缩效率高但编码速度慢。
-c:a copy音频流直接复制,不重新编码。
"output.mkv"指定输出文件名为 output.mkv

使用 Node.js 进行批量转换

1
node toAV1.js /dir-to-your-videos/

运行完成转换后的 AV1 编码视频文件在原视频目录的 dist 文件夹中。

toAV1.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
const trash = require("trash");

const inputDir = process.argv[2];

if (!inputDir || !fs.existsSync(inputDir)) {
console.error("❌ 请传入有效的目录路径");
process.exit(1);
}

// 支持的视频扩展名
const VIDEO_EXTS = [".mp4", ".mov", ".mkv", ".avi", ".webm", ".m4v"];

// 输出目录
const distDir = path.join(inputDir, "dist");
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir);
}

async function encodeVideo(filePath, indexStr) {
const fileName = path.basename(filePath);
const baseName = path.parse(fileName).name;
const outputPath = path.join(distDir, `${baseName}.mkv`);

return new Promise((resolve, reject) => {
console.log(
`🎬 正在编码 ${indexStr}${fileName}${path.relative(inputDir, outputPath)}`
);

const ffmpeg = spawn(
"ffmpeg",
[
"-i",
filePath,
"-vf",
"colorspace=iall=bt709:all=bt709:fast=1",
"-c:v",
"libsvtav1",
"-crf",
"30",
"-preset",
"8",
"-c:a",
"copy",
outputPath,
],
{ stdio: "inherit" }
);

ffmpeg.on("close", async (code) => {
if (code === 0) {
console.log(`✅ ${indexStr} 编码完成:${fileName}`);
try {
await trash([filePath]);
console.log(`🗑️ 已将原始文件移至废纸篓:${fileName}`);
} catch (err) {
console.error(`⚠️ 移动到废纸篓失败:${err.message}`);
}
resolve();
} else {
console.error(`❌ 编码失败:${fileName}`);
reject(new Error(`FFmpeg 退出码 ${code}`));
}
});
});
}

(async () => {
const files = fs
.readdirSync(inputDir)
.filter((file) => VIDEO_EXTS.includes(path.extname(file).toLowerCase()))
.map((file) => path.join(inputDir, file));

if (files.length === 0) {
console.log("📭 没有找到视频文件");
return;
}

for (const [index, file] of files.entries()) {
try {
const indexStr = `${index}/${files.length}`
await encodeVideo(file, indexStr);
} catch (err) {
console.error(`❌ 出错:${err.message}`);
}
}

console.log("🏁 所有文件处理完毕");
})();