mediac 1.7.5 → 1.7.6
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/cmd/cmd_ffmpeg.js +200 -340
- package/cmd/cmd_remove.js +33 -27
- package/cmd/cmd_rename.js +91 -18
- package/cmd/cmd_shared.js +22 -1
- package/lib/argparser.js +116 -0
- package/lib/core.js +44 -2
- package/lib/encoding.js +2 -0
- package/lib/ffmpeg_presets.js +356 -0
- package/lib/ffprobe.js +58 -8
- package/lib/shared.js +10 -0
- package/package.json +1 -1
package/cmd/cmd_ffmpeg.js
CHANGED
|
@@ -8,37 +8,34 @@
|
|
|
8
8
|
import chalk from 'chalk'
|
|
9
9
|
import dayjs from 'dayjs'
|
|
10
10
|
import { execa } from 'execa'
|
|
11
|
-
import { fileTypeFromFile } from 'file-type'
|
|
12
11
|
import fs from 'fs-extra'
|
|
13
12
|
import iconv from "iconv-lite"
|
|
14
13
|
import inquirer from "inquirer"
|
|
15
14
|
import mm from 'music-metadata'
|
|
16
15
|
import { cpus } from "os"
|
|
17
16
|
import pMap from 'p-map'
|
|
18
|
-
import path
|
|
17
|
+
import path from "path"
|
|
19
18
|
import which from "which"
|
|
20
19
|
import * as core from '../lib/core.js'
|
|
21
20
|
import { asyncFilter, formatArgs } from '../lib/core.js'
|
|
22
21
|
import * as log from '../lib/debug.js'
|
|
23
22
|
import * as enc from '../lib/encoding.js'
|
|
23
|
+
import presets from '../lib/ffmpeg_presets.js'
|
|
24
24
|
import { getMediaInfo } from '../lib/ffprobe.js'
|
|
25
25
|
import * as mf from '../lib/file.js'
|
|
26
26
|
import * as helper from '../lib/helper.js'
|
|
27
|
-
import {
|
|
27
|
+
import { FFMPEG_BINARY } from '../lib/shared.js'
|
|
28
|
+
import { addEntryProps, applyFileNameRules } from './cmd_shared.js'
|
|
28
29
|
|
|
29
30
|
const LOG_TAG = "FFConv"
|
|
30
|
-
|
|
31
|
-
const PRESET_NAMES = []
|
|
32
|
-
const PRESET_MAP = new Map()
|
|
33
|
-
|
|
34
31
|
// ===========================================
|
|
35
32
|
// 命令内容执行
|
|
36
33
|
// ===========================================
|
|
37
34
|
|
|
38
35
|
|
|
39
36
|
export { aliases, builder, command, describe, handler }
|
|
40
|
-
|
|
41
|
-
const command = "ffmpeg <input> [
|
|
37
|
+
// directories 表示额外输入文件,用于支持多个目录
|
|
38
|
+
const command = "ffmpeg <input> [directories...]"
|
|
42
39
|
const aliases = ["transcode", "aconv", "vconv", "avconv"]
|
|
43
40
|
const describe = 'convert audio or video files using ffmpeg.'
|
|
44
41
|
|
|
@@ -55,12 +52,17 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
55
52
|
describe: "Folder store ouput files",
|
|
56
53
|
type: "string",
|
|
57
54
|
})
|
|
55
|
+
// 复杂字符串参数,单独解析
|
|
56
|
+
.option("params", {
|
|
57
|
+
describe: "complex combined string parameters for parse",
|
|
58
|
+
type: "string",
|
|
59
|
+
})
|
|
58
60
|
// 保持源文件目录结构
|
|
59
61
|
.option("output-tree", {
|
|
60
62
|
alias: 'otree',
|
|
61
63
|
describe: "keep folder tree structure in output folder",
|
|
62
64
|
type: "boolean",
|
|
63
|
-
default:
|
|
65
|
+
default: true,
|
|
64
66
|
})
|
|
65
67
|
// 正则,包含文件名规则
|
|
66
68
|
.option("include", {
|
|
@@ -70,17 +72,18 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
70
72
|
})
|
|
71
73
|
//字符串或正则,不包含文件名规则
|
|
72
74
|
// 如果是正则的话需要转义
|
|
73
|
-
//
|
|
75
|
+
// 默认排除含shana的文件和.m4a文件
|
|
74
76
|
.option("exclude", {
|
|
75
77
|
alias: "E",
|
|
76
78
|
type: "string",
|
|
77
|
-
default: '
|
|
79
|
+
default: 'shana|.m4a',
|
|
78
80
|
description: "filename exclude pattern ",
|
|
79
81
|
})
|
|
80
|
-
//
|
|
82
|
+
// 默认启用正则模式,禁用则为字符串模式
|
|
81
83
|
.option("regex", {
|
|
82
84
|
alias: 're',
|
|
83
85
|
type: "boolean",
|
|
86
|
+
default: true,
|
|
84
87
|
description: "match filenames by regex pattern",
|
|
85
88
|
})
|
|
86
89
|
// 需要处理的扩展名列表,默认为常见视频文件
|
|
@@ -92,7 +95,7 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
92
95
|
// 选择预设,从预设列表中选一个,预设等于一堆预定义参数
|
|
93
96
|
.option("preset", {
|
|
94
97
|
type: "choices",
|
|
95
|
-
choices:
|
|
98
|
+
choices: presets.getAllNames(),
|
|
96
99
|
default: 'hevc_2k',
|
|
97
100
|
describe: "convert preset args for ffmpeg command",
|
|
98
101
|
})
|
|
@@ -130,10 +133,17 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
130
133
|
default: 0,
|
|
131
134
|
describe: "chang max side for video",
|
|
132
135
|
})
|
|
136
|
+
// 视频帧率,FPS
|
|
137
|
+
.option("fps", {
|
|
138
|
+
alias: 'framerate',
|
|
139
|
+
type: "number",
|
|
140
|
+
default: 0,
|
|
141
|
+
describe: "output framerate value",
|
|
142
|
+
})
|
|
133
143
|
// 视频加速减速,默认不改动,范围0.25-4.0
|
|
134
144
|
.option("speed", {
|
|
135
145
|
type: "number",
|
|
136
|
-
default:
|
|
146
|
+
default: 0,
|
|
137
147
|
describe: "chang speed for video and audio",
|
|
138
148
|
})
|
|
139
149
|
// 视频选项
|
|
@@ -198,6 +208,12 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
198
208
|
describe: "Write error logs to file [json or text]",
|
|
199
209
|
type: "string",
|
|
200
210
|
})
|
|
211
|
+
// 硬件加速方式
|
|
212
|
+
.option("hwaccel", {
|
|
213
|
+
alias: "hw",
|
|
214
|
+
describe: "hardware acceleration for video decode and encode",
|
|
215
|
+
type: "string",
|
|
216
|
+
})
|
|
201
217
|
// 并行操作限制,并发数,默认为 CPU 核心数
|
|
202
218
|
.option("jobs", {
|
|
203
219
|
alias: "j",
|
|
@@ -213,13 +229,14 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
213
229
|
})
|
|
214
230
|
}
|
|
215
231
|
|
|
232
|
+
//todo 实现一种更方便的传参数的方法,比如逗号分割 -preset ab=128,aq=3,ac=aac,ap=aac_he;vb=1536,vq=23,vc=hevc_nvenc,vs=1280*720,vw=1280,vh=720;vf=xxx,cf=xxx,
|
|
216
233
|
|
|
217
234
|
const handler = cmdConvert
|
|
218
235
|
|
|
219
236
|
async function cmdConvert(argv) {
|
|
220
237
|
// 显示预设列表
|
|
221
238
|
if (argv.showPresets) {
|
|
222
|
-
for (const [key, value] of
|
|
239
|
+
for (const [key, value] of presets.getAllPresets()) {
|
|
223
240
|
log.show(key, core.pickTrueValues(value))
|
|
224
241
|
}
|
|
225
242
|
return
|
|
@@ -240,14 +257,31 @@ async function cmdConvert(argv) {
|
|
|
240
257
|
log.fileLog(`Preset: ${JSON.stringify(preset)}`, 'FFConv')
|
|
241
258
|
}
|
|
242
259
|
// 首先找到所有的视频和音频文件
|
|
243
|
-
|
|
260
|
+
const walkOpts = {
|
|
244
261
|
withFiles: true,
|
|
245
262
|
needStats: true,
|
|
246
263
|
entryFilter: (e) => e.isFile && helper.isMediaFile(e.name)
|
|
247
|
-
}
|
|
264
|
+
}
|
|
265
|
+
let fileEntries = await mf.walk(root, walkOpts)
|
|
266
|
+
// 处理额外目录参数
|
|
267
|
+
if (argv.directories?.length > 0) {
|
|
268
|
+
const extraDirs = new Set(argv.directories.map(d => path.resolve(d)))
|
|
269
|
+
for (const dirPath of extraDirs) {
|
|
270
|
+
const st = await fs.stat(dirPath)
|
|
271
|
+
if (st.isDirectory()) {
|
|
272
|
+
const dirFiles = await mf.walk(dirPath, walkOpts)
|
|
273
|
+
if (dirFiles.length > 0) {
|
|
274
|
+
log.show(logTag, `Add ${dirFiles.length} extra files from ${dirPath}`)
|
|
275
|
+
fileEntries = fileEntries.concat(dirFiles)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// 根据完整路径去重
|
|
281
|
+
fileEntries = core.uniqueByFields(fileEntries, 'path')
|
|
248
282
|
log.show(logTag, `Total ${fileEntries.length} files found [${preset.name}] (${helper.humanTime(startMs)})`)
|
|
249
283
|
// 再根据preset过滤找到的文件
|
|
250
|
-
if (preset.type === 'video' || preset
|
|
284
|
+
if (preset.type === 'video' || presets.isAudioExtract(preset)) {
|
|
251
285
|
// 视频转换模式,保留视频文件
|
|
252
286
|
// 提取音频模式,保留视频文件
|
|
253
287
|
fileEntries = fileEntries.filter(e => helper.isVideoFile(e.name))
|
|
@@ -259,7 +293,7 @@ async function cmdConvert(argv) {
|
|
|
259
293
|
// 应用文件名过滤规则
|
|
260
294
|
fileEntries = await applyFileNameRules(fileEntries, argv)
|
|
261
295
|
// 提取音频时不判断文件名
|
|
262
|
-
if (!preset
|
|
296
|
+
if (!presets.isAudioExtract(preset)) {
|
|
263
297
|
// 过滤掉压缩过的文件 关键词 shana tmp m4a
|
|
264
298
|
fileEntries = fileEntries.filter(entry => !/tmp|\.m4a/i.test(entry.name))
|
|
265
299
|
}
|
|
@@ -285,14 +319,15 @@ async function cmdConvert(argv) {
|
|
|
285
319
|
}
|
|
286
320
|
}
|
|
287
321
|
startMs = Date.now()
|
|
288
|
-
fileEntries
|
|
322
|
+
addEntryProps(fileEntries)
|
|
323
|
+
fileEntries = fileEntries.map((entry, index) => {
|
|
289
324
|
return {
|
|
290
|
-
...
|
|
325
|
+
...entry,
|
|
291
326
|
argv,
|
|
292
327
|
preset,
|
|
293
|
-
startMs: startMs,
|
|
294
|
-
index:
|
|
295
|
-
total: fileEntries.length,
|
|
328
|
+
// startMs: startMs,
|
|
329
|
+
// index: index,
|
|
330
|
+
// total: fileEntries.length,
|
|
296
331
|
errorFile: argv.errorFile,
|
|
297
332
|
testMode: testMode
|
|
298
333
|
}
|
|
@@ -348,11 +383,7 @@ async function cmdConvert(argv) {
|
|
|
348
383
|
}
|
|
349
384
|
// 记录开始时间
|
|
350
385
|
startMs = Date.now()
|
|
351
|
-
tasks
|
|
352
|
-
t.startMs = startMs
|
|
353
|
-
t.index = i
|
|
354
|
-
t.total = tasks.length
|
|
355
|
-
})
|
|
386
|
+
addEntryProps(tasks)
|
|
356
387
|
// 先写入一次LOG
|
|
357
388
|
await log.flushFileLog()
|
|
358
389
|
// 并发数视频1,音频4,或者参数指定
|
|
@@ -368,25 +399,25 @@ function fixEncoding(str = '') {
|
|
|
368
399
|
}
|
|
369
400
|
|
|
370
401
|
async function runFFmpegCmd(entry) {
|
|
371
|
-
const ipx = `${entry.index}/${entry.total}`
|
|
402
|
+
const ipx = `${entry.index + 1}/${entry.total}`
|
|
372
403
|
const logTag = chalk.green('FFCMD')
|
|
373
404
|
const ffmpegArgs = entry.ffmpegArgs
|
|
374
405
|
|
|
375
|
-
log.show(logTag,
|
|
406
|
+
log.show(logTag, `${ipx} Processing ${helper.pathShort(entry.path, 72)}`, helper.humanSize(entry.size), chalk.yellow(getDurationInfo(entry)), helper.humanTime(entry.startMs))
|
|
376
407
|
log.showGray(logTag, getEntryShowInfo(entry), chalk.yellow(entry.preset.name), helper.humanSize(entry.size))
|
|
377
408
|
|
|
378
|
-
|
|
379
409
|
log.showGray(logTag, 'ffmpeg', createFFmpegArgs(entry, true).join(' '))
|
|
380
|
-
const exePath = await which(
|
|
410
|
+
const exePath = await which(FFMPEG_BINARY)
|
|
381
411
|
if (entry.testMode) {
|
|
382
412
|
// 测试模式跳过
|
|
383
|
-
log.show(logTag,
|
|
413
|
+
log.show(logTag, `${ipx} Skipped ${entry.path} (${helper.humanSize(entry.size)}) [TestMode]`)
|
|
384
414
|
return
|
|
385
415
|
}
|
|
386
416
|
|
|
387
417
|
// 创建输出目录
|
|
388
418
|
await fs.mkdirp(entry.fileDstDir)
|
|
389
419
|
await fs.remove(entry.fileDstTemp)
|
|
420
|
+
const ffmpegStartMs = Date.now()
|
|
390
421
|
try {
|
|
391
422
|
// https://2ality.com/2022/07/nodejs-child-process.html
|
|
392
423
|
// Windows下 { shell: true } 必须,否则报错
|
|
@@ -396,22 +427,27 @@ async function runFFmpegCmd(entry) {
|
|
|
396
427
|
const { stdout, stderr } = await ffmpegProcess
|
|
397
428
|
// const stdoutFixed = fixEncoding(stdout || "")
|
|
398
429
|
// const stderrFixed = fixEncoding(stderr || "")
|
|
430
|
+
if (await fs.pathExists(entry.fileDst)) {
|
|
431
|
+
log.showYellow(logTag, `${ipx} DstExists ${entry.fileDst}`, helper.humanSize(entry.size), chalk.yellow(entry.preset.name), helper.humanTime(ffmpegStartMs))
|
|
432
|
+
await fs.remove(entry.fileDstTemp)
|
|
433
|
+
return
|
|
434
|
+
}
|
|
399
435
|
if (await fs.pathExists(entry.fileDstTemp)) {
|
|
400
436
|
const dstSize = (await fs.stat(entry.fileDstTemp))?.size || 0
|
|
401
437
|
if (dstSize > 20 * mf.FILE_SIZE_1K) {
|
|
402
438
|
await fs.move(entry.fileDstTemp, entry.fileDst)
|
|
403
|
-
log.showGreen(logTag,
|
|
404
|
-
log.fileLog(
|
|
439
|
+
log.showGreen(logTag, `${ipx} Done ${entry.fileDst}`, chalk.cyan(`${helper.humanSize(entry.size)}=>${helper.humanSize(dstSize)}`), chalk.yellow(entry.preset.name), helper.humanTime(ffmpegStartMs))
|
|
440
|
+
log.fileLog(`${ipx} Done <${entry.fileDst}> [${entry.preset.name}] (${helper.humanSize(dstSize)})`, 'FFCMD')
|
|
405
441
|
entry.ok = true
|
|
406
442
|
return entry
|
|
407
443
|
} else {
|
|
408
444
|
// 转换失败,删除临时文件
|
|
409
445
|
}
|
|
410
446
|
}
|
|
411
|
-
log.showYellow(logTag,
|
|
412
|
-
log.fileLog(
|
|
447
|
+
log.showYellow(logTag, `${ipx} Failed ${entry.path}`, entry.preset.name, helper.humanSize(dstSize))
|
|
448
|
+
log.fileLog(`${ipx} Failed <${entry.path}> [${entry.dstAudioBitrate || entry.preset.name}]`, 'FFCMD')
|
|
413
449
|
} catch (error) {
|
|
414
|
-
const errMsg = error.stderr || error.message
|
|
450
|
+
const errMsg = (error.stderr || error.message || '[Unknown]').substring(0, 240)
|
|
415
451
|
log.showRed(logTag, `Error(${ipx}) ${errMsg}`)
|
|
416
452
|
log.fileLog(`Error(${ipx}) <${entry.path}> [${entry.preset.name}] ${errMsg}`, 'FFCMD')
|
|
417
453
|
await writeErrorFile(entry, error)
|
|
@@ -440,7 +476,7 @@ async function writeErrorFile(entry, error) {
|
|
|
440
476
|
|
|
441
477
|
async function prepareFFmpegCmd(entry) {
|
|
442
478
|
const logTag = chalk.green('Prepare')
|
|
443
|
-
const ipx = `${entry.index}/${entry.total}`
|
|
479
|
+
const ipx = `${entry.index + 1}/${entry.total}`
|
|
444
480
|
log.info(logTag, `Processing(${ipx}) file: ${entry.path}`)
|
|
445
481
|
const isAudio = helper.isAudioFile(entry.path)
|
|
446
482
|
const [srcDir, srcBase, srcExt] = helper.pathSplit(entry.path)
|
|
@@ -449,9 +485,9 @@ async function prepareFFmpegCmd(entry) {
|
|
|
449
485
|
const dstExt = preset.format || srcExt
|
|
450
486
|
let fileDstDir
|
|
451
487
|
// 命令行参数指定输出目录
|
|
452
|
-
if (
|
|
488
|
+
if (argv.output) {
|
|
453
489
|
// 默认true 保留目录结构,可以防止文件名冲突
|
|
454
|
-
if (argv.
|
|
490
|
+
if (argv.outputTree) {
|
|
455
491
|
// 如果要保持源文件目录结构
|
|
456
492
|
fileDstDir = helper.pathRewrite(srcDir, preset.output)
|
|
457
493
|
} else {
|
|
@@ -462,25 +498,25 @@ async function prepareFFmpegCmd(entry) {
|
|
|
462
498
|
// 如果没有指定输出目录,直接输出在原文件同目录
|
|
463
499
|
fileDstDir = path.resolve(srcDir)
|
|
464
500
|
}
|
|
465
|
-
if (isAudio || preset
|
|
501
|
+
if (isAudio || presets.isAudioExtract(preset)) {
|
|
466
502
|
// 不带后缀只改扩展名的m4a文件,如果存在也需要首先忽略
|
|
467
503
|
// 可能是其它压缩工具生成的文件,不需要重复压缩
|
|
468
504
|
// 检查输出目录
|
|
469
|
-
const fileDstNoSuffix = path.join(fileDstDir, `${srcBase}${dstExt}`)
|
|
470
|
-
if (await fs.pathExists(fileDstNoSuffix)) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
505
|
+
// const fileDstNoSuffix = path.join(fileDstDir, `${srcBase}${dstExt}`)
|
|
506
|
+
// if (await fs.pathExists(fileDstNoSuffix)) {
|
|
507
|
+
// log.info(
|
|
508
|
+
// logTag,
|
|
509
|
+
// `${ipx} Skip[DstM4A]: ${helper.pathShort(entry.path)} (${helper.humanSize(entry.size)})`)
|
|
510
|
+
// return false
|
|
511
|
+
// }
|
|
476
512
|
// 检查源文件同目录
|
|
477
|
-
const fileDstSameDirNoSuffix = path.join(srcDir, `${srcBase}${dstExt}`)
|
|
478
|
-
if (await fs.pathExists(fileDstSameDirNoSuffix)) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
513
|
+
// const fileDstSameDirNoSuffix = path.join(srcDir, `${srcBase}${dstExt}`)
|
|
514
|
+
// if (await fs.pathExists(fileDstSameDirNoSuffix)) {
|
|
515
|
+
// log.info(
|
|
516
|
+
// logTag,
|
|
517
|
+
// `${ipx} Skip[DstSame]: ${helper.pathShort(entry.path)} (${helper.humanSize(entry.size)})`)
|
|
518
|
+
// return false
|
|
519
|
+
// }
|
|
484
520
|
}
|
|
485
521
|
try {
|
|
486
522
|
// 使用ffprobe读取媒体信息,速度较慢
|
|
@@ -489,8 +525,8 @@ async function prepareFFmpegCmd(entry) {
|
|
|
489
525
|
|
|
490
526
|
// ffprobe无法读取时长和比特率,可以认为文件损坏,或不支持的格式,跳过
|
|
491
527
|
if (!(entry.info?.format?.duration || entry.info?.format?.bit_rate)) {
|
|
492
|
-
log.showYellow(
|
|
493
|
-
log.fileLog(
|
|
528
|
+
log.showYellow(logTag, `${ipx} Skip[Corrupted]: ${entry.path} (${helper.humanSize(entry.size)})`)
|
|
529
|
+
log.fileLog(`${ipx} Skip[Corrupted]: <${entry.path}> (${helper.humanSize(entry.size)})`, 'Prepare')
|
|
494
530
|
return false
|
|
495
531
|
}
|
|
496
532
|
const audioCodec = entry.info?.audio?.codec_name
|
|
@@ -508,8 +544,8 @@ async function prepareFFmpegCmd(entry) {
|
|
|
508
544
|
// 可以读取码率,文件未损坏
|
|
509
545
|
} else {
|
|
510
546
|
// 如果无法获取元数据,认为不是合法的音频或视频文件,忽略
|
|
511
|
-
log.showYellow(
|
|
512
|
-
log.fileLog(
|
|
547
|
+
log.showYellow(logTag, `${ipx} Skip[Invalid]: ${entry.path} (${helper.humanSize(entry.size)})`)
|
|
548
|
+
log.fileLog(`${ipx} Skip[Invalid]: <${entry.path}> (${helper.humanSize(entry.size)})`, 'Prepare')
|
|
513
549
|
return false
|
|
514
550
|
}
|
|
515
551
|
}
|
|
@@ -523,14 +559,14 @@ async function prepareFFmpegCmd(entry) {
|
|
|
523
559
|
log.info(logTag, entry.path, mediaBitrate)
|
|
524
560
|
// 如果转换目标是音频,但是源文件不含音频流,忽略
|
|
525
561
|
if (entry.preset.type === 'audio' && !audioCodec) {
|
|
526
|
-
log.showYellow(
|
|
527
|
-
log.fileLog(
|
|
562
|
+
log.showYellow(logTag, `${ipx} Skip[NoAudio]: ${entry.path}`, getEntryShowInfo(entry), helper.humanSize(entry.size))
|
|
563
|
+
log.fileLog(`${ipx} Skip[NoAudio]: <${entry.path}> (${helper.humanSize(entry.size)})`, 'Prepare')
|
|
528
564
|
return false
|
|
529
565
|
}
|
|
530
566
|
// 如果转换目标是视频,但是源文件不含视频流,忽略
|
|
531
567
|
if (entry.preset.type === 'video' && !videoCodec) {
|
|
532
|
-
log.showYellow(
|
|
533
|
-
log.fileLog(
|
|
568
|
+
log.showYellow(logTag, `${ipx} Skip[NoVideo]: ${entry.path}`, getEntryShowInfo(entry), helper.humanSize(entry.size))
|
|
569
|
+
log.fileLog(`${ipx} Skip[NoVideo]: <${entry.path}> (${helper.humanSize(entry.size)})`, 'Prepare')
|
|
534
570
|
return false
|
|
535
571
|
}
|
|
536
572
|
// 输出文件名基本名,含前后缀,不含扩展名
|
|
@@ -545,21 +581,21 @@ async function prepareFFmpegCmd(entry) {
|
|
|
545
581
|
if (await fs.pathExists(fileDst)) {
|
|
546
582
|
log.info(
|
|
547
583
|
logTag,
|
|
548
|
-
|
|
584
|
+
`${ipx} Skip[Dst1]: ${helper.pathShort(entry.path)} (${helper.humanSize(entry.size)})`)
|
|
549
585
|
return false
|
|
550
586
|
}
|
|
551
587
|
if (await fs.pathExists(fileDstSameDir)) {
|
|
552
588
|
log.info(
|
|
553
589
|
logTag,
|
|
554
|
-
|
|
590
|
+
`${ipx} Skip[Dst2]: ${helper.pathShort(entry.path)} (${helper.humanSize(entry.size)})`)
|
|
555
591
|
return false
|
|
556
592
|
}
|
|
557
593
|
if (await fs.pathExists(fileDstTemp)) {
|
|
558
594
|
await fs.remove(fileDstTemp)
|
|
559
595
|
}
|
|
560
|
-
log.show(logTag,
|
|
561
|
-
log.
|
|
562
|
-
log.
|
|
596
|
+
log.show(logTag, `${ipx} TaskSRC: ${helper.pathShort(entry.path, 72)}`, helper.humanTime(entry.startMs))
|
|
597
|
+
log.info(logTag, `${ipx} TaskDST:`, `${fileDst}`)
|
|
598
|
+
log.showGray(logTag, `${ipx} Streams:`, getEntryShowInfo(entry), chalk.yellow(entry.preset.name), helper.humanSize(entry.size))
|
|
563
599
|
// log.show(logTag, `Entry(${ipx})`, entry)
|
|
564
600
|
const newEntry = {
|
|
565
601
|
...entry,
|
|
@@ -572,7 +608,7 @@ async function prepareFFmpegCmd(entry) {
|
|
|
572
608
|
// log.info(logTag, 'ffmpeg', ffmpegArgs.join(' '))
|
|
573
609
|
return newEntry
|
|
574
610
|
} catch (error) {
|
|
575
|
-
log.warn(logTag,
|
|
611
|
+
log.warn(logTag, `${ipx} Skip[Error]: ${entry.path}`, error)
|
|
576
612
|
}
|
|
577
613
|
}
|
|
578
614
|
|
|
@@ -605,12 +641,17 @@ function getEntryShowInfo(entry) {
|
|
|
605
641
|
const duration = entry?.info?.audio?.duration
|
|
606
642
|
|| entry?.info?.video?.duration
|
|
607
643
|
|| entry?.info?.format?.duration || 0
|
|
644
|
+
const fps = entry.info?.video?.r_frame_rate || 0
|
|
608
645
|
const showInfo = []
|
|
609
646
|
if (ac) showInfo.push(`audio|${ac}|${Math.round(entry.srcAudioBitrate / 1024)}K=>${getBestAudioBitrate(entry)}K|${helper.humanDuration(duration * 1000)}`)
|
|
610
|
-
if (vc) showInfo.push(`video|${vc}|${Math.round(entry.srcVideoBitrate / 1024)}K=>${getBestVideoBitrate(entry)}K|${helper.humanDuration(duration * 1000)}`,)
|
|
647
|
+
if (vc) showInfo.push(`video|${vc}|${Math.round(entry.srcVideoBitrate / 1024)}K=>${getBestVideoBitrate(entry)}K|${helper.humanDuration(duration * 1000)}|${fps}`,)
|
|
611
648
|
return showInfo.join(', ')
|
|
612
649
|
}
|
|
613
650
|
|
|
651
|
+
function getDurationInfo(entry) {
|
|
652
|
+
return helper.humanDuration((entry?.info?.audio?.duration || entry?.info?.video?.duration || entry?.info?.format?.duration || 0) * 1000)
|
|
653
|
+
}
|
|
654
|
+
|
|
614
655
|
// 读取单个音频文件的元数据
|
|
615
656
|
async function readMusicMeta(entry) {
|
|
616
657
|
try {
|
|
@@ -648,9 +689,11 @@ function calculateBitrate(entry) {
|
|
|
648
689
|
// 这个是文件整体码率,如果是是视频文件,等于是视频和音频的码率相加
|
|
649
690
|
const fileBitrate = entry.info?.format?.bit_rate || 0
|
|
650
691
|
let srcAudioBitrate = 0
|
|
692
|
+
let dstAudioBitrate = 0
|
|
651
693
|
let srcVideoBitrate = 0
|
|
652
694
|
let dstVideoBitrate = 0
|
|
653
695
|
if (helper.isAudioFile(entry.path)) {
|
|
696
|
+
// 对于音频文件,音频格式转换
|
|
654
697
|
if (entry.format?.lossless || helper.isAudioLossless(entry.path)) {
|
|
655
698
|
// 无损音频,设置默认值
|
|
656
699
|
srcAudioBitrate = srcAudioBitrate > 320 ? srcAudioBitrate : 999 * 1024
|
|
@@ -659,25 +702,35 @@ function calculateBitrate(entry) {
|
|
|
659
702
|
srcAudioBitrate = entry.format?.bitrate
|
|
660
703
|
|| entry.info?.audio?.bit_rate
|
|
661
704
|
|| fileBitrate || 0
|
|
705
|
+
// 有的文件无法获取音频码率,如opus,此时srcAudioBitrate=0
|
|
706
|
+
// opus用于极低码率音频,此时 dstAudioBitrate=48 可以接受
|
|
707
|
+
dstAudioBitrate = bitrateMap.find(br => srcAudioBitrate > br.threshold)?.value || 48
|
|
708
|
+
// 忽略计算结果,直接使用预设数值
|
|
709
|
+
if (!entry.preset.smartBitrate) {
|
|
710
|
+
dstAudioBitrate = entry.preset.audioBitrate
|
|
711
|
+
}
|
|
662
712
|
} else {
|
|
663
|
-
//
|
|
713
|
+
// 对于视频文件
|
|
714
|
+
// 计算码率原则
|
|
715
|
+
// 计算出的音频码率不高于源文件的音频码率
|
|
716
|
+
// 计算出的音频码率不高于预设指定的码率
|
|
664
717
|
srcAudioBitrate = entry.info?.audio?.bit_rate || 0
|
|
718
|
+
dstAudioBitrate = Math.min(Math.round(srcAudioBitrate / 1024), entry.preset.audioBitrate)
|
|
719
|
+
|
|
720
|
+
// 计算出的视频码率不高于源文件的视频码率
|
|
665
721
|
// 减去音频的码率,估算为48k
|
|
666
722
|
srcVideoBitrate = entry.info?.video?.bit_rate
|
|
667
723
|
|| fileBitrate - 48 * 1024 || 0
|
|
668
724
|
// 压缩后的视频比特率不能高于源文件比特率
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
725
|
+
dstVideoBitrate = Math.min(Math.round(srcVideoBitrate / 1024), entry.preset.videoBitrate)
|
|
726
|
+
// 忽略计算结果,直接使用预设数值
|
|
727
|
+
if (!entry.preset.smartBitrate) {
|
|
728
|
+
dstAudioBitrate = entry.preset.audioBitrate
|
|
672
729
|
dstVideoBitrate = entry.preset.videoBitrate
|
|
673
730
|
}
|
|
674
731
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const dstAudioBitrate = bitrateMap.find(br => srcAudioBitrate > br.threshold)?.value || 48
|
|
678
|
-
const result = { srcAudioBitrate, dstAudioBitrate, srcVideoBitrate, dstVideoBitrate }
|
|
679
|
-
// log.showRed(entry.path, result)
|
|
680
|
-
return result
|
|
732
|
+
|
|
733
|
+
return { srcAudioBitrate, dstAudioBitrate, srcVideoBitrate, dstVideoBitrate }
|
|
681
734
|
}
|
|
682
735
|
|
|
683
736
|
// 选择最佳视频码率
|
|
@@ -716,10 +769,46 @@ function getBestAudioQuality(entry) {
|
|
|
716
769
|
// 此函数仅读取参数,不修改preset对象
|
|
717
770
|
function createFFmpegArgs(entry, forDisplay = false) {
|
|
718
771
|
const preset = entry.preset
|
|
772
|
+
// 用于模板字符串的模板参数,针对当前文件
|
|
773
|
+
const entryPreset = {
|
|
774
|
+
...preset,
|
|
775
|
+
// 这几个会覆盖preset的预设数值
|
|
776
|
+
videoBitrate: getBestVideoBitrate(entry),
|
|
777
|
+
videoQuality: getBestVideoQuality(entry),
|
|
778
|
+
audioBitrate: getBestAudioBitrate(entry),
|
|
779
|
+
audioQuality: getBestAudioQuality(entry),
|
|
780
|
+
// 下面的是源文件参数
|
|
781
|
+
srcAudioBitrate: entry.srcAudioBitrate,
|
|
782
|
+
srcVideoBitrate: entry.srcVideoBitrate,
|
|
783
|
+
srcWidth: entry.info.video?.width || 0,
|
|
784
|
+
srcHeight: entry.info.video?.height || 0,
|
|
785
|
+
srcFrameRate: entry.info.video?.r_frame_rate || 0,
|
|
786
|
+
srcDuration: entry.info.format?.duration || 0,
|
|
787
|
+
srcSize: entry.info.format?.size || 0,
|
|
788
|
+
srcVideoCodec: entry.info.video?.codec_name,
|
|
789
|
+
srcAudioCodec: entry.info.audio?.codec_name,
|
|
790
|
+
srcFormat: entry.info.format?.format_name,
|
|
791
|
+
}
|
|
792
|
+
// 几种ffmpeg参数设置的时间和功耗
|
|
793
|
+
// ffmpeg -hide_banner -n -v error -stats -i
|
|
794
|
+
// 32s 110w
|
|
795
|
+
// ffmpeg -hide_banner -n -v error -stats -hwaccel auto -i
|
|
796
|
+
// 34s 56w
|
|
797
|
+
// ffmpeg -hide_banner -n -v error -stats -hwaccel d3d11va -hwaccel_output_format d3d11
|
|
798
|
+
// 27s 45w rm格式死机蓝屏
|
|
799
|
+
// ffmpeg -hide_banner -n -v error -stats -hwaccel cuda -hwaccel_output_format cuda
|
|
800
|
+
// 27s 41w
|
|
801
|
+
// ffmpeg -hide_banner -n -v error -stats -hwaccel cuda -i
|
|
802
|
+
// 31s 60w
|
|
719
803
|
// 显示详细信息
|
|
720
804
|
// let args = "-hide_banner -n -loglevel repeat+level+info -stats".split(" ")
|
|
721
805
|
// 只显示进度和错误
|
|
722
|
-
let args = "-hide_banner -n -v error
|
|
806
|
+
let args = "-hide_banner -n -v error".split(" ")
|
|
807
|
+
// 输出视频时才需要cuda加速,音频用cpu就行
|
|
808
|
+
if (preset.type === 'video') {
|
|
809
|
+
// -hwaccel cuda -hwaccel_output_format cuda
|
|
810
|
+
args = args.concat(["-stats", "-hwaccel", "cuda", "-hwaccel_output_format", "cuda"])
|
|
811
|
+
}
|
|
723
812
|
// 输入参数在输入文件前面,顺序重要
|
|
724
813
|
if (preset.inputArgs?.length > 0) {
|
|
725
814
|
args = args.concat(this.preset.inputArgs.split(' '))
|
|
@@ -732,16 +821,13 @@ function createFFmpegArgs(entry, forDisplay = false) {
|
|
|
732
821
|
if (preset.complexFilter?.length > 0) {
|
|
733
822
|
args.push('-filter_complex')
|
|
734
823
|
args.push(`"${formatArgs(preset.complexFilter, preset)}"`)
|
|
735
|
-
} else if (preset.filters
|
|
824
|
+
} else if (preset.filters.length > 0) {
|
|
736
825
|
args.push('-vf')
|
|
737
826
|
args.push(formatArgs(preset.filters, preset))
|
|
738
827
|
}
|
|
828
|
+
|
|
739
829
|
if (preset.videoArgs?.length > 0) {
|
|
740
|
-
const va = formatArgs(preset.videoArgs,
|
|
741
|
-
...preset,
|
|
742
|
-
videoBitrate: getBestVideoBitrate(entry),
|
|
743
|
-
videoQuality: getBestVideoQuality(entry)
|
|
744
|
-
})
|
|
830
|
+
const va = formatArgs(preset.videoArgs, entryPreset)
|
|
745
831
|
args = args.concat(va.split(' '))
|
|
746
832
|
}
|
|
747
833
|
if (preset.audioArgs?.length > 0) {
|
|
@@ -750,7 +836,7 @@ function createFFmpegArgs(entry, forDisplay = false) {
|
|
|
750
836
|
// audioArgsCopy: '-c:a copy',
|
|
751
837
|
// audioArgsEncode: '-c:a libfdk_aac -b:a {audioBitrate}k',
|
|
752
838
|
let audioArgsFixed = preset.audioArgs
|
|
753
|
-
if (preset
|
|
839
|
+
if (presets.isAudioExtract(preset)) {
|
|
754
840
|
if (entry.info?.audio?.codec_name === 'aac') {
|
|
755
841
|
audioArgsFixed = '-c:a copy'
|
|
756
842
|
} else {
|
|
@@ -759,11 +845,7 @@ function createFFmpegArgs(entry, forDisplay = false) {
|
|
|
759
845
|
} else {
|
|
760
846
|
audioArgsFixed = preset.audioArgs
|
|
761
847
|
}
|
|
762
|
-
const aa = formatArgs(audioArgsFixed,
|
|
763
|
-
...preset,
|
|
764
|
-
audioBitrate: getBestAudioBitrate(entry),
|
|
765
|
-
audioQuality: getBestAudioQuality(entry)
|
|
766
|
-
})
|
|
848
|
+
const aa = formatArgs(audioArgsFixed, entryPreset)
|
|
767
849
|
args = args.concat(aa.split(' '))
|
|
768
850
|
}
|
|
769
851
|
// 在输入文件后面
|
|
@@ -775,18 +857,21 @@ function createFFmpegArgs(entry, forDisplay = false) {
|
|
|
775
857
|
let extraArgsArray = []
|
|
776
858
|
// 添加自定义metadata字段
|
|
777
859
|
//description, comment, copyright
|
|
860
|
+
|
|
778
861
|
extraArgsArray.push(`-metadata`, `description="encoder=mediac-ffmpeg"`)
|
|
779
862
|
extraArgsArray.push(`-metadata`, `copyright="name=${entry.name}"`)
|
|
780
|
-
extraArgsArray.push(`-metadata`, `comment="preset=${preset.name},vargs=${formatArgs(preset.videoArgs,
|
|
863
|
+
extraArgsArray.push(`-metadata`, `comment="preset=${preset.name},vargs=${formatArgs(preset.videoArgs, entryPreset)},aargs=${formatArgs(preset.audioArgs, entryPreset)}"`)
|
|
781
864
|
// 检查源文件元数据
|
|
782
865
|
if (entry.tags?.title) {
|
|
783
866
|
const KEY_LIST = ['title', 'artist', 'album', 'albumartist', 'year']
|
|
784
867
|
// 验证 非空值,无乱码,值为字符串或数字
|
|
785
868
|
const validTags = core.filterFields(entry.tags, (key, value) => {
|
|
786
869
|
return KEY_LIST.includes(key)
|
|
787
|
-
&&
|
|
870
|
+
&& Boolean(value)
|
|
788
871
|
&& ((typeof value === 'string' && value.length > 0)
|
|
789
872
|
|| typeof value === 'number')
|
|
873
|
+
&& !enc.hasBadCJKChar(value)
|
|
874
|
+
&& !enc.hasBadUnicode(value)
|
|
790
875
|
})
|
|
791
876
|
extraArgsArray = extraArgsArray.concat(...Object.entries(validTags)
|
|
792
877
|
.map(([key, value]) => [`-metadata`, `${key}="${value}"`]))
|
|
@@ -812,242 +897,12 @@ function createFFmpegArgs(entry, forDisplay = false) {
|
|
|
812
897
|
return args
|
|
813
898
|
}
|
|
814
899
|
|
|
815
|
-
// ===========================================
|
|
816
|
-
// 数据类定义
|
|
817
|
-
// ===========================================
|
|
818
|
-
|
|
819
|
-
// ffmpeg命令参数预设类
|
|
820
|
-
class Preset {
|
|
821
|
-
constructor(name, {
|
|
822
|
-
format,
|
|
823
|
-
type,
|
|
824
|
-
prefix,
|
|
825
|
-
suffix,
|
|
826
|
-
videoArgs,
|
|
827
|
-
audioArgs,
|
|
828
|
-
inputArgs,
|
|
829
|
-
streamArgs,
|
|
830
|
-
extraArgs,
|
|
831
|
-
outputArgs,
|
|
832
|
-
filters,
|
|
833
|
-
complexFilter,
|
|
834
|
-
output,
|
|
835
|
-
videoBitrate = 0,
|
|
836
|
-
videoQuality = 0,
|
|
837
|
-
audioBitrate = 0,
|
|
838
|
-
audioQuality = 0,
|
|
839
|
-
dimension = 0,
|
|
840
|
-
speed = 1
|
|
841
|
-
} = {}) {
|
|
842
|
-
this.name = name
|
|
843
|
-
this.format = format
|
|
844
|
-
this.type = type
|
|
845
|
-
this.prefix = prefix
|
|
846
|
-
this.suffix = suffix
|
|
847
|
-
this.videoArgs = videoArgs
|
|
848
|
-
this.audioArgs = audioArgs
|
|
849
|
-
this.inputArgs = inputArgs
|
|
850
|
-
this.streamArgs = streamArgs
|
|
851
|
-
this.extraArgs = extraArgs
|
|
852
|
-
this.outputArgs = outputArgs
|
|
853
|
-
this.filters = filters
|
|
854
|
-
this.complexFilter = complexFilter
|
|
855
|
-
// 输出目录
|
|
856
|
-
this.output = output
|
|
857
|
-
// 视频码率和质量
|
|
858
|
-
this.videoBitrate = videoBitrate
|
|
859
|
-
this.videoQuality = videoQuality
|
|
860
|
-
// 音频码率和质量
|
|
861
|
-
this.audioBitrate = audioBitrate
|
|
862
|
-
this.audioQuality = audioQuality
|
|
863
|
-
// 视频尺寸
|
|
864
|
-
this.dimension = dimension
|
|
865
|
-
// 视频加速
|
|
866
|
-
this.speed = speed
|
|
867
|
-
// 元数据参数
|
|
868
|
-
// 用户从命令行设定的参数
|
|
869
|
-
// 优先级最高
|
|
870
|
-
this.userVideoBitrate = 0
|
|
871
|
-
this.userVideoQuality = 0
|
|
872
|
-
this.userAudioBitrate = 0
|
|
873
|
-
this.userAudioQuality = 0
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
update(source) {
|
|
877
|
-
for (const key in source) {
|
|
878
|
-
this[key] = source[key]
|
|
879
|
-
}
|
|
880
|
-
return this
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// 构造函数,参数为另一个 Preset 对象
|
|
884
|
-
static fromPreset(preset) {
|
|
885
|
-
return new Preset(preset.name, preset)
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// videoArgs = { args,codec,quality,bitrate,filters}
|
|
891
|
-
// audioOptons = {args,codec, quality,bitrate,filters}
|
|
892
|
-
// audioArgs = {prefix,suffix}
|
|
893
|
-
|
|
894
|
-
// HEVC基础参数
|
|
895
|
-
const HEVC_BASE = new Preset('hevc-base', {
|
|
896
|
-
format: '.mp4',
|
|
897
|
-
type: 'video',
|
|
898
|
-
intro: 'hevc|hevc_nvenc|libfdk_aac',
|
|
899
|
-
prefix: '[SHANA] ',
|
|
900
|
-
suffix: '_{preset}_{videoBitrate}k',
|
|
901
|
-
description: 'HEVC_BASE',
|
|
902
|
-
// 视频参数说明
|
|
903
|
-
// video_codec block '-c:v hevc_nvenc -profile:v main -tune:v hq'
|
|
904
|
-
// video_quality block '-cq {quality} -bufsize {bitrate} -maxrate {bitrate}'
|
|
905
|
-
videoArgs: '-c:v hevc_nvenc -profile:v main -tune:v hq -cq {videoQuality} -bufsize {videoBitrate}k -maxrate {videoBitrate}k',
|
|
906
|
-
// 音频参数说明
|
|
907
|
-
// audio_codec block '-c:a libfdk_aac'
|
|
908
|
-
// audio_quality block '-b:a {bitrate}'
|
|
909
|
-
audioArgs: '-c:a libfdk_aac -b:a {audioBitrate}k',
|
|
910
|
-
inputArgs: '',
|
|
911
|
-
streamArgs: '-map_metadata 0 -map_metadata:s:v 0:s:v',
|
|
912
|
-
// 快速读取和播放
|
|
913
|
-
outputArgs: '-movflags +faststart -movflags use_metadata_tags',
|
|
914
|
-
filters: "scale='if(gte(iw,ih),min({dimension},iw),-2)':'if(lt(iw,ih),min({dimension},ih),-2)'",
|
|
915
|
-
complexFilter: '',
|
|
916
|
-
})
|
|
917
|
-
|
|
918
|
-
// 音频AAC基础参数
|
|
919
|
-
const AAC_BASE = new Preset('aac_base', {
|
|
920
|
-
format: '.m4a',
|
|
921
|
-
type: 'audio',
|
|
922
|
-
intro: 'aac|libfdk_aac',
|
|
923
|
-
prefix: '',
|
|
924
|
-
suffix: '_{preset}_{audioBitrate}k',
|
|
925
|
-
description: 'AAC_BASE',
|
|
926
|
-
videoArgs: '',
|
|
927
|
-
// 音频参数说明
|
|
928
|
-
// audio_codec block '-c:a libfdk_aac'
|
|
929
|
-
// audio_quality block '-b:a {bitrate}'
|
|
930
|
-
audioArgs: '-map 0:a -c:a libfdk_aac -b:a {audioBitrate}k',
|
|
931
|
-
inputArgs: '',
|
|
932
|
-
streamArgs: '-map_metadata 0 -map_metadata:s:a 0:s:a',
|
|
933
|
-
outputArgs: '-movflags +faststart -movflags use_metadata_tags',
|
|
934
|
-
})
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const PRESET_AUDIO_EXTRACT = Preset.fromPreset(AAC_BASE).update({
|
|
938
|
-
name: 'audio_extract',
|
|
939
|
-
intro: 'aac|extract',
|
|
940
|
-
audioArgs: '-c:a copy',
|
|
941
|
-
streamArgs: '-vn -map 0:a:0 -map_metadata 0 -map_metadata:s:a 0:s:a'
|
|
942
|
-
})
|
|
943
|
-
|
|
944
|
-
function initializePresets() {
|
|
945
|
-
|
|
946
|
-
const PRESET_HEVC_ULTRA = Preset.fromPreset(HEVC_BASE).update({
|
|
947
|
-
name: 'hevc_ultra',
|
|
948
|
-
videoQuality: 20,
|
|
949
|
-
videoBitrate: 20480,
|
|
950
|
-
audioBitrate: 320,
|
|
951
|
-
dimension: 3840
|
|
952
|
-
})
|
|
953
|
-
|
|
954
|
-
const PRESET_HEVC_4K = Preset.fromPreset(HEVC_BASE).update({
|
|
955
|
-
name: 'hevc_4k',
|
|
956
|
-
videoQuality: 22,
|
|
957
|
-
videoBitrate: 10240,
|
|
958
|
-
audioBitrate: 256,
|
|
959
|
-
dimension: 3840
|
|
960
|
-
})
|
|
961
|
-
|
|
962
|
-
const PRESET_HEVC_2K = Preset.fromPreset(HEVC_BASE).update({
|
|
963
|
-
name: 'hevc_2k',
|
|
964
|
-
videoQuality: 22,
|
|
965
|
-
videoBitrate: 4096,
|
|
966
|
-
audioBitrate: 192,
|
|
967
|
-
dimension: 1920
|
|
968
|
-
})
|
|
969
|
-
|
|
970
|
-
const PRESET_HEVC_MEDIUM = Preset.fromPreset(HEVC_BASE).update({
|
|
971
|
-
name: 'hevc_medium',
|
|
972
|
-
videoQuality: 24,
|
|
973
|
-
videoBitrate: 2048,
|
|
974
|
-
audioBitrate: 128,
|
|
975
|
-
dimension: 1920
|
|
976
|
-
})
|
|
977
|
-
|
|
978
|
-
const PRESET_HEVC_LOW = Preset.fromPreset(HEVC_BASE).update({
|
|
979
|
-
name: 'hevc_low',
|
|
980
|
-
videoQuality: 26,
|
|
981
|
-
videoBitrate: 1536,
|
|
982
|
-
audioBitrate: 96,
|
|
983
|
-
dimension: 1920
|
|
984
|
-
})
|
|
985
|
-
|
|
986
|
-
const PRESET_HEVC_LOWEST = Preset.fromPreset(HEVC_BASE).update({
|
|
987
|
-
name: 'hevc_lowest',
|
|
988
|
-
videoQuality: 26,
|
|
989
|
-
videoBitrate: 512,
|
|
990
|
-
audioBitrate: 48,
|
|
991
|
-
dimension: 1920,
|
|
992
|
-
streamArgs: '-map [v] -map [a]',
|
|
993
|
-
// 音频参数说明
|
|
994
|
-
// audio_codec block '-c:a libfdk_aac -profile:a aac_he'
|
|
995
|
-
// audio_quality block '-b:a 48k'
|
|
996
|
-
audioArgs: '-c:a libfdk_aac -profile:a aac_he -b:a {audioBitrate}k',
|
|
997
|
-
// filters 和 complexFilter 不能共存,此预设使用 complexFilter
|
|
998
|
-
filters: '',
|
|
999
|
-
// 这里单引号必须,否则逗号需要转义,Windows太多坑
|
|
1000
|
-
complexFilter: "[0:v]setpts=PTS/{speed},scale='if(gt(iw,1280),min(1280,iw),-2)':'if(gt(ih,1280),min(1280,ih),-2)'[v];[0:a]atempo={speed}[a]"
|
|
1001
|
-
})
|
|
1002
|
-
|
|
1003
|
-
const presets = {
|
|
1004
|
-
//4K超高码率和质量
|
|
1005
|
-
PRESET_HEVC_ULTRA: PRESET_HEVC_ULTRA,
|
|
1006
|
-
//4k高码率和质量
|
|
1007
|
-
PRESET_HEVC_4K: PRESET_HEVC_4K,
|
|
1008
|
-
// 2K高码率和质量
|
|
1009
|
-
PRESET_HEVC_2K: PRESET_HEVC_2K,
|
|
1010
|
-
// 2K中码率和质量
|
|
1011
|
-
PRESET_HEVC_MEDIUM: PRESET_HEVC_MEDIUM,
|
|
1012
|
-
// 2K低码率和质量
|
|
1013
|
-
PRESET_HEVC_LOW: PRESET_HEVC_LOW,
|
|
1014
|
-
// 极低画质和码率,适用于教程类视频
|
|
1015
|
-
PRESET_HEVC_LOWEST: PRESET_HEVC_LOWEST,
|
|
1016
|
-
// 提取视频中的音频,复制或转换为AAC格式
|
|
1017
|
-
PRESET_AUDIO_EXTRACT: PRESET_AUDIO_EXTRACT,
|
|
1018
|
-
//音频AAC最高码率
|
|
1019
|
-
PRESET_AAC_HIGH: Preset.fromPreset(AAC_BASE).update({
|
|
1020
|
-
name: 'aac_high',
|
|
1021
|
-
audioBitrate: 320,
|
|
1022
|
-
}),
|
|
1023
|
-
//音频AAC中码率
|
|
1024
|
-
PRESET_AAC_MEDIUM: Preset.fromPreset(AAC_BASE).update({
|
|
1025
|
-
name: 'aac_medium',
|
|
1026
|
-
audioBitrate: 192,
|
|
1027
|
-
}),
|
|
1028
|
-
// 音频AAC低码率
|
|
1029
|
-
PRESET_AAC_LOW: Preset.fromPreset(AAC_BASE).update({
|
|
1030
|
-
name: 'aac_low',
|
|
1031
|
-
audioBitrate: 128,
|
|
1032
|
-
}),
|
|
1033
|
-
// 音频AAC极低码率,适用人声
|
|
1034
|
-
PRESET_AAC_VOICE: Preset.fromPreset(AAC_BASE).update({
|
|
1035
|
-
name: 'aac_voice',
|
|
1036
|
-
audioBitrate: 48,
|
|
1037
|
-
audioArgs: '-c:a libfdk_aac -profile:a aac_he -b:a {audioBitrate}k',
|
|
1038
|
-
})
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
core.modifyObjectWithKeyField(presets, 'description')
|
|
1042
|
-
for (const [key, preset] of Object.entries(presets)) {
|
|
1043
|
-
PRESET_NAMES.push(preset.name)
|
|
1044
|
-
PRESET_MAP.set(preset.name, preset)
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
900
|
function preparePreset(argv) {
|
|
1049
901
|
// 参数中指定的preset
|
|
1050
|
-
let preset =
|
|
902
|
+
let preset = presets.getPreset(argv.preset)
|
|
903
|
+
|
|
904
|
+
// log.show('ARGV', argv)
|
|
905
|
+
// log.show('P1', preset)
|
|
1051
906
|
// 克隆对象,不修改Map中的内容
|
|
1052
907
|
preset = structuredClone(preset)
|
|
1053
908
|
// 保存argv方便调试
|
|
@@ -1084,6 +939,17 @@ function preparePreset(argv) {
|
|
|
1084
939
|
if (argv.speed > 0) {
|
|
1085
940
|
preset.speed = argv.speed
|
|
1086
941
|
}
|
|
942
|
+
// 视频帧率
|
|
943
|
+
if (argv.framerate > 0) {
|
|
944
|
+
preset.framerate = argv.framerate
|
|
945
|
+
}
|
|
946
|
+
if (preset.framerate > 0) {
|
|
947
|
+
if (preset.filters?.length > 0) {
|
|
948
|
+
preset.filters += ',fps={framerate}'
|
|
949
|
+
} else {
|
|
950
|
+
preset.filters = 'fps={framerate}'
|
|
951
|
+
}
|
|
952
|
+
}
|
|
1087
953
|
// 视频码率,用户指定,优先级最高
|
|
1088
954
|
if (argv.videoBitrate > 0) {
|
|
1089
955
|
preset.userVideoBitrate = argv.videoBitrate
|
|
@@ -1098,12 +964,6 @@ function preparePreset(argv) {
|
|
|
1098
964
|
if (argv.audioQuality > 0) {
|
|
1099
965
|
preset.userAudioQuality = argv.audioQuality
|
|
1100
966
|
}
|
|
1101
|
-
|
|
967
|
+
// log.show('P2', preset)
|
|
1102
968
|
return preset
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
// 初始化调用
|
|
1109
|
-
initializePresets()
|
|
969
|
+
}
|