mediac 1.8.2 → 1.8.5
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_compress.js +44 -20
- package/cmd/cmd_dcim.js +2 -1
- package/cmd/cmd_decode.js +15 -15
- package/cmd/cmd_ffmpeg.js +161 -79
- package/cmd/cmd_moveup.js +1 -0
- package/cmd/cmd_prefix.js +17 -5
- package/cmd/cmd_remove.js +121 -74
- package/cmd/cmd_rename.js +104 -67
- package/cmd/cmd_shared.js +21 -9
- package/cmd/cmd_zipu.js +2 -2
- package/lib/core.js +15 -1
- package/lib/encoding.js +2 -1
- package/lib/exif.js +3 -3
- package/lib/ffmpeg_presets.js +30 -13
- package/lib/file.js +4 -4
- package/lib/helper.js +8 -4
- package/lib/media_parser.js +26 -11
- package/lib/mediainfo.js +52 -12
- package/package.json +2 -1
package/cmd/cmd_compress.js
CHANGED
|
@@ -25,7 +25,7 @@ import * as log from '../lib/debug.js'
|
|
|
25
25
|
import * as mf from '../lib/file.js'
|
|
26
26
|
import * as helper from '../lib/helper.js'
|
|
27
27
|
import * as tryfp from '../lib/tryfp.js'
|
|
28
|
-
import { compressImage } from "./cmd_shared.js"
|
|
28
|
+
import { applyFileNameRules, calculateScale, compressImage } from "./cmd_shared.js"
|
|
29
29
|
|
|
30
30
|
//
|
|
31
31
|
export { aliases, builder, command, describe, handler }
|
|
@@ -40,11 +40,11 @@ const WIDTH_DEFAULT = 6000
|
|
|
40
40
|
|
|
41
41
|
const builder = function addOptions(ya, helpOrVersionSet) {
|
|
42
42
|
return ya
|
|
43
|
-
.option("
|
|
43
|
+
.option("delete-source-files", {
|
|
44
44
|
alias: "p",
|
|
45
45
|
type: "boolean",
|
|
46
46
|
default: false,
|
|
47
|
-
description: "
|
|
47
|
+
description: "Delete original image files after compress",
|
|
48
48
|
})
|
|
49
49
|
// 输出目录,默认输出文件与原文件同目录
|
|
50
50
|
.option("output", {
|
|
@@ -52,6 +52,32 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
52
52
|
describe: "Folder store ouput files",
|
|
53
53
|
type: "string",
|
|
54
54
|
})
|
|
55
|
+
// 正则,包含文件名规则
|
|
56
|
+
.option("include", {
|
|
57
|
+
alias: "I",
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "filename include pattern",
|
|
60
|
+
})
|
|
61
|
+
//字符串或正则,不包含文件名规则
|
|
62
|
+
// 如果是正则的话需要转义
|
|
63
|
+
.option("exclude", {
|
|
64
|
+
alias: "E",
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "filename exclude pattern ",
|
|
67
|
+
})
|
|
68
|
+
// 默认启用正则模式,禁用则为字符串模式
|
|
69
|
+
.option("regex", {
|
|
70
|
+
alias: 're',
|
|
71
|
+
type: "boolean",
|
|
72
|
+
default: true,
|
|
73
|
+
description: "match filenames by regex pattern",
|
|
74
|
+
})
|
|
75
|
+
// 需要处理的扩展名列表,默认为常见视频文件
|
|
76
|
+
.option("extensions", {
|
|
77
|
+
alias: "e",
|
|
78
|
+
type: "string",
|
|
79
|
+
describe: "include files by extensions (eg. .wav|.flac)",
|
|
80
|
+
})
|
|
55
81
|
// 压缩后的文件后缀,默认为 _Z4K
|
|
56
82
|
.option("suffix", {
|
|
57
83
|
alias: "S",
|
|
@@ -59,10 +85,10 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
59
85
|
type: "string",
|
|
60
86
|
default: "_Z4K",
|
|
61
87
|
})
|
|
62
|
-
.option("
|
|
88
|
+
.option("delete-source-files-only", {
|
|
63
89
|
type: "boolean",
|
|
64
90
|
default: false,
|
|
65
|
-
description: "Just delete original image files only",
|
|
91
|
+
description: "Just delete original image files only, no compression",
|
|
66
92
|
})
|
|
67
93
|
// 是否覆盖已存在的压缩后文件
|
|
68
94
|
.option("force", {
|
|
@@ -118,12 +144,7 @@ const handler = cmdCompress
|
|
|
118
144
|
async function cmdCompress(argv) {
|
|
119
145
|
const testMode = !argv.doit
|
|
120
146
|
const logTag = "cmdCompress"
|
|
121
|
-
const root =
|
|
122
|
-
assert.strictEqual("string", typeof root, "root must be string")
|
|
123
|
-
if (!root || !(await fs.pathExists(root))) {
|
|
124
|
-
log.error(logTag, `Invalid Input: '${root}'`)
|
|
125
|
-
throw new Error(`Invalid Input: ${root}`)
|
|
126
|
-
}
|
|
147
|
+
const root = await helper.validateInput(argv.input)
|
|
127
148
|
if (!testMode) {
|
|
128
149
|
log.fileLog(`Root:${root}`, logTag)
|
|
129
150
|
log.fileLog(`Argv:${JSON.stringify(argv)}`, logTag)
|
|
@@ -133,8 +154,8 @@ async function cmdCompress(argv) {
|
|
|
133
154
|
const quality = argv.quality || QUALITY_DEFAULT
|
|
134
155
|
const minFileSize = (argv.size || SIZE_DEFAULT) * 1024
|
|
135
156
|
const maxWidth = argv.width || WIDTH_DEFAULT
|
|
136
|
-
const purgeOnly = argv.
|
|
137
|
-
const purgeSource = argv.
|
|
157
|
+
const purgeOnly = argv.deleteSourceFilesOnly || false
|
|
158
|
+
const purgeSource = argv.deleteSourceFiles || false
|
|
138
159
|
log.show(`${logTag} input:`, root)
|
|
139
160
|
// 如果有force标志,就不过滤文件名
|
|
140
161
|
const RE_THUMB = argv.force ? /@_@/ : /Z4K|P4K|M4K|feature|web|thumb$/i
|
|
@@ -152,8 +173,10 @@ async function cmdCompress(argv) {
|
|
|
152
173
|
log.showYellow(logTag, "no files found, abort.")
|
|
153
174
|
return
|
|
154
175
|
}
|
|
176
|
+
// 应用文件名过滤规则
|
|
177
|
+
files = await applyFileNameRules(files, argv)
|
|
155
178
|
log.show(logTag, `total ${files.length} files found (all)`)
|
|
156
|
-
if (files.length === 0) {
|
|
179
|
+
if (!files || files.length === 0) {
|
|
157
180
|
log.showYellow("Nothing to do, abort.")
|
|
158
181
|
return
|
|
159
182
|
}
|
|
@@ -317,10 +340,11 @@ async function preCompress(f) {
|
|
|
317
340
|
} else {
|
|
318
341
|
if (im?.exif) {
|
|
319
342
|
log.info(logTag, "force:", fileDst)
|
|
320
|
-
const
|
|
343
|
+
const [err, iexif] = tryfp.tryCatch(exif)(im.exif)
|
|
321
344
|
// 跳过以前由mediac压缩过的图片,避免重复压缩
|
|
322
|
-
if (!f.force) {
|
|
323
|
-
|
|
345
|
+
if (!f.force && iexif?.Image) {
|
|
346
|
+
const md = iexif?.Image
|
|
347
|
+
if (md?.Copyright?.includes("mediac")
|
|
324
348
|
|| md.Software?.includes("mediac")
|
|
325
349
|
|| md.Artist?.includes("mediac") && !f.force) {
|
|
326
350
|
log.info(logTag, "skip:", fileDst)
|
|
@@ -339,12 +363,12 @@ async function preCompress(f) {
|
|
|
339
363
|
}
|
|
340
364
|
|
|
341
365
|
if (err) {
|
|
342
|
-
log.warn(logTag, "sharp",
|
|
343
|
-
log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${
|
|
366
|
+
log.warn(logTag, "sharp", err.message, fileSrc)
|
|
367
|
+
log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${err.message}`, logTag)
|
|
344
368
|
return
|
|
345
369
|
}
|
|
346
370
|
|
|
347
|
-
const { dstWidth, dstHeight } =
|
|
371
|
+
const { dstWidth, dstHeight } = calculateScale(im.width, im.height, maxWidth)
|
|
348
372
|
if (f.total < 1000 || f.index > f.total - 1000) {
|
|
349
373
|
log.show(logTag, `${f.index}/${f.total}`,
|
|
350
374
|
helper.pathShort(fileSrc),
|
package/cmd/cmd_dcim.js
CHANGED
|
@@ -11,7 +11,7 @@ import fs from 'fs-extra'
|
|
|
11
11
|
import inquirer from "inquirer"
|
|
12
12
|
import path from "path"
|
|
13
13
|
|
|
14
|
-
import { renameFiles } from "./cmd_shared.js"
|
|
14
|
+
import { addEntryProps, renameFiles } from "./cmd_shared.js"
|
|
15
15
|
|
|
16
16
|
import * as log from '../lib/debug.js'
|
|
17
17
|
import * as exif from '../lib/exif.js'
|
|
@@ -136,6 +136,7 @@ const handler = async function cmdRename(argv) {
|
|
|
136
136
|
log.showYellow(LOG_TAG, "Nothing to do, exit now.")
|
|
137
137
|
return
|
|
138
138
|
}
|
|
139
|
+
files = addEntryProps(files)
|
|
139
140
|
log.show(
|
|
140
141
|
LOG_TAG,
|
|
141
142
|
`Total ${files.length} media files ready to rename by exif`,
|
package/cmd/cmd_decode.js
CHANGED
|
@@ -60,7 +60,7 @@ const handler = async function cmdDecode(argv) {
|
|
|
60
60
|
}
|
|
61
61
|
const fromEnc = argv.fromEnc?.length > 0 ? [argv.fromEnc] : ENC_LIST
|
|
62
62
|
const toEnc = argv.toEnc?.length > 0 ? [argv.toEnc] : ENC_LIST
|
|
63
|
-
const threhold = log.isVerbose() ?
|
|
63
|
+
const threhold = log.isVerbose() ? 0 : 50
|
|
64
64
|
log.show(logTag, `Input:`, strArgs)
|
|
65
65
|
log.show(logTag, `fromEnc:`, JSON.stringify(fromEnc))
|
|
66
66
|
log.show(logTag, `toEnc:`, JSON.stringify(toEnc))
|
|
@@ -88,19 +88,19 @@ function showResults(r) {
|
|
|
88
88
|
let cr = chardet.analyse(Buffer.from(str))
|
|
89
89
|
cr = cr.filter(ct => ct.confidence >= 70)
|
|
90
90
|
cr?.length > 0 && print('Encoding', cr)
|
|
91
|
-
print('String', Array.from(str))
|
|
92
|
-
print('Unicode', Array.from(str).map(c => c.codePointAt(0).toString(16)))
|
|
93
|
-
const badUnicode = enc.checkBadUnicode(str)
|
|
91
|
+
// print('String', Array.from(str))
|
|
92
|
+
// print('Unicode', Array.from(str).map(c => c.codePointAt(0).toString(16)))
|
|
93
|
+
const badUnicode = enc.checkBadUnicode(str, true)
|
|
94
94
|
badUnicode?.length > 0 && log.show('badUnicode:', badUnicode)
|
|
95
|
-
log.info(`MESSY_UNICODE=${enc.REGEX_MESSY_UNICODE.test(str)}`,
|
|
96
|
-
|
|
97
|
-
log.info(`OnlyJapanese=${unicode.strOnlyJapanese(str)}`,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
log.info(`HasHangul=${unicode.strHasHangul(str)}`,
|
|
102
|
-
|
|
103
|
-
log.info(`HasChinese=${unicode.strHasChinese(str)}`,
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
// log.info(`MESSY_UNICODE=${enc.REGEX_MESSY_UNICODE.test(str)}`,
|
|
96
|
+
// `MESSY_CJK=${enc.REGEX_MESSY_CJK.test(str)}`)
|
|
97
|
+
// log.info(`OnlyJapanese=${unicode.strOnlyJapanese(str)}`,
|
|
98
|
+
// `OnlyJpHan=${unicode.strOnlyJapaneseHan(str)}`,
|
|
99
|
+
// `HasHiraKana=${unicode.strHasHiraKana(str)}`
|
|
100
|
+
// )
|
|
101
|
+
// log.info(`HasHangul=${unicode.strHasHangul(str)}`,
|
|
102
|
+
// `OnlyHangul=${unicode.strOnlyHangul(str)}`)
|
|
103
|
+
// log.info(`HasChinese=${unicode.strHasChinese(str)}`,
|
|
104
|
+
// `OnlyChinese=${unicode.strOnlyChinese(str)}`,
|
|
105
|
+
// `OnlyChn3500=${enc.RE_CHARS_MOST_USED.test(str)}`)
|
|
106
106
|
}
|
package/cmd/cmd_ffmpeg.js
CHANGED
|
@@ -24,7 +24,7 @@ import * as enc from '../lib/encoding.js'
|
|
|
24
24
|
import presets from '../lib/ffmpeg_presets.js'
|
|
25
25
|
import * as mf from '../lib/file.js'
|
|
26
26
|
import * as helper from '../lib/helper.js'
|
|
27
|
-
import { getMediaInfo } from '../lib/mediainfo.js'
|
|
27
|
+
import { getMediaInfo, getSimpleInfo } from '../lib/mediainfo.js'
|
|
28
28
|
import { addEntryProps, applyFileNameRules, calculateScale } from './cmd_shared.js'
|
|
29
29
|
|
|
30
30
|
const LOG_TAG = "FFConv"
|
|
@@ -161,6 +161,12 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
161
161
|
default: 0,
|
|
162
162
|
describe: "Set video bitrate (in kbytes) in ffmpeg command",
|
|
163
163
|
})
|
|
164
|
+
// 直接复制视频流,不重新编码
|
|
165
|
+
.option("video-copy", {
|
|
166
|
+
type: "boolean",
|
|
167
|
+
default: false,
|
|
168
|
+
describe: "Copy video stream to ouput, no re-encoding",
|
|
169
|
+
})
|
|
164
170
|
// 视频选项,指定视频质量参数
|
|
165
171
|
.option("video-quality", {
|
|
166
172
|
alias: "vq",
|
|
@@ -227,11 +233,17 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
227
233
|
type: "number",
|
|
228
234
|
})
|
|
229
235
|
// 如果目标文件已存在或转换成功,删除源文件
|
|
230
|
-
.option("
|
|
236
|
+
.option("delete-source-files", {
|
|
231
237
|
type: "boolean",
|
|
232
238
|
default: false,
|
|
233
239
|
description: "delete source file if destination is exists",
|
|
234
240
|
})
|
|
241
|
+
// 显示视频参数
|
|
242
|
+
.option("info", {
|
|
243
|
+
type: "boolean",
|
|
244
|
+
default: false,
|
|
245
|
+
description: "show info of media files",
|
|
246
|
+
})
|
|
235
247
|
// 启用调试参数
|
|
236
248
|
.option("debug", {
|
|
237
249
|
type: "boolean",
|
|
@@ -325,6 +337,15 @@ async function cmdConvert(argv) {
|
|
|
325
337
|
log.showYellow(logTag, 'No files left after rules, nothing to do.')
|
|
326
338
|
return
|
|
327
339
|
}
|
|
340
|
+
// 仅显示视频文件参数,不进行转换操作
|
|
341
|
+
if (argv.info) {
|
|
342
|
+
for (const entry of fileEntries) {
|
|
343
|
+
log.showGreen(logTag, `${entry.path}`)
|
|
344
|
+
const info = await getMediaInfo(entry.path)
|
|
345
|
+
log.show(logTag, JSON.stringify(info))
|
|
346
|
+
}
|
|
347
|
+
return
|
|
348
|
+
}
|
|
328
349
|
if (fileEntries.length > 5000) {
|
|
329
350
|
const continueAnswer = await inquirer.prompt([
|
|
330
351
|
{
|
|
@@ -355,6 +376,7 @@ async function cmdConvert(argv) {
|
|
|
355
376
|
testMode: testMode
|
|
356
377
|
}
|
|
357
378
|
})
|
|
379
|
+
|
|
358
380
|
log.showYellow(logTag, 'ARGV:', argv)
|
|
359
381
|
log.showYellow(logTag, 'PRESET:', preset)
|
|
360
382
|
const prepareAnswer = await inquirer.prompt([
|
|
@@ -375,7 +397,7 @@ async function cmdConvert(argv) {
|
|
|
375
397
|
let tasks = await pMap(fileEntries, prepareFFmpegCmd, { concurrency: argv.jobs || (core.isUNCPath(root) ? 4 : cpus().length) })
|
|
376
398
|
|
|
377
399
|
// 如果选择了清理源文件
|
|
378
|
-
if (argv.
|
|
400
|
+
if (argv.deleteSourceFiles) {
|
|
379
401
|
// 删除目标文件已存在的源文件
|
|
380
402
|
let dstExitsTasks = tasks.filter(t => t && t.dstExists && !t.fileDst)
|
|
381
403
|
if (dstExitsTasks.length > 0) {
|
|
@@ -457,7 +479,7 @@ async function runFFmpegCmd(entry) {
|
|
|
457
479
|
|
|
458
480
|
// 每10个输出一次ffmpeg详细信息,避免干扰
|
|
459
481
|
// if (entry.index % 10 === 0) {
|
|
460
|
-
log.
|
|
482
|
+
log.showCyan(logTag, getEntryShowInfo(entry))
|
|
461
483
|
log.showGray(logTag, `ffmpeg`, entry.ffmpegArgs.flat().join(' '))
|
|
462
484
|
// }
|
|
463
485
|
const exePath = await which('ffmpeg')
|
|
@@ -553,7 +575,7 @@ async function prepareFFmpegCmd(entry) {
|
|
|
553
575
|
// 默认true 保留目录结构,可以防止文件名冲突
|
|
554
576
|
if (argv.outputTree) {
|
|
555
577
|
// 如果要保持源文件目录结构
|
|
556
|
-
fileDstDir = helper.pathRewrite(root, srcDir, preset.output)
|
|
578
|
+
fileDstDir = helper.pathRewrite(entry.root, srcDir, preset.output)
|
|
557
579
|
} else {
|
|
558
580
|
// 不保留源文件目录结构,只保留源文件父目录
|
|
559
581
|
fileDstDir = path.join(preset.output, path.basename(srcDir))
|
|
@@ -565,7 +587,7 @@ async function prepareFFmpegCmd(entry) {
|
|
|
565
587
|
try {
|
|
566
588
|
// 使用ffprobe读取媒体信息,速度较慢
|
|
567
589
|
// 注意flac和ape格式的stream里没有bitrate字段 format里有
|
|
568
|
-
entry.info = await getMediaInfo(entry.path
|
|
590
|
+
entry.info = await getMediaInfo(entry.path)
|
|
569
591
|
|
|
570
592
|
// ffprobe无法读取时长和比特率,可以认为文件损坏,或不支持的格式,跳过
|
|
571
593
|
if (!(entry.info?.duration && entry.info?.bitrate)) {
|
|
@@ -593,15 +615,16 @@ async function prepareFFmpegCmd(entry) {
|
|
|
593
615
|
return false
|
|
594
616
|
}
|
|
595
617
|
} else {
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
|
|
618
|
+
// 检查目标宽高和原始文件宽高,不放大
|
|
619
|
+
const reqDimension = argv.dimension || preset.dimension
|
|
620
|
+
const sw = entry.info?.video?.width || 0
|
|
621
|
+
const sh = entry.info?.video?.height || 0
|
|
622
|
+
// if (sw < reqDimension && sh < reqDimension) {
|
|
623
|
+
// // 忽略
|
|
624
|
+
// log.showYellow(logTag, `${ipx} Skip[Dimension]: (${sw}x${sh},${reqDimension}) ${entry.path}`)
|
|
625
|
+
// log.fileLog(`${ipx} Skip[Dimension]: (${sw}x${sh}) <${entry.path}>`, 'Prepare')
|
|
626
|
+
// return false
|
|
627
|
+
// }
|
|
605
628
|
}
|
|
606
629
|
// 获取原始音频码率,计算目标音频码率
|
|
607
630
|
// vp9视频和opus音频无法获取码率
|
|
@@ -664,10 +687,39 @@ async function prepareFFmpegCmd(entry) {
|
|
|
664
687
|
}
|
|
665
688
|
}
|
|
666
689
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
690
|
+
|
|
691
|
+
if (!isAudio) {
|
|
692
|
+
// https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new
|
|
693
|
+
// H264 10Bit Nvidia和Intel都不支持硬解,直接跳过
|
|
694
|
+
// H264 High-L5以上也不支持
|
|
695
|
+
if (entry.info?.video?.format === 'h264'
|
|
696
|
+
&& (entry.info?.video?.bitDepth === 10
|
|
697
|
+
|| (entry.info?.video?.profile?.includes('High') && entry.info?.video?.level >= 50))) {
|
|
698
|
+
log.warn(logTag, `${ipx} useCPUDecode ${entry.path} ${entry.info?.video?.pixelFormat}`, helper.humanSize(entry.size), chalk.white(JSON.stringify(entry.info.video)))
|
|
699
|
+
log.fileLog(`${ipx} useCPUDecode <${entry.path}> (${helper.humanSize(entry.size)})`, 'Prepare')
|
|
700
|
+
// 添加标志,使用软解,替换解码参数
|
|
701
|
+
// 在组装ffmpeg参数时判断和替换
|
|
702
|
+
// 解码和滤镜参数都需要修改
|
|
703
|
+
entry.info.useCPUDecode = true
|
|
704
|
+
// return false
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// 找到并添加字幕文件,当前目录和subs子目录
|
|
709
|
+
const subExts = ['.ass', '.ssa', '.srt']
|
|
710
|
+
const subtitles = []
|
|
711
|
+
for (const ext of subExts) {
|
|
712
|
+
const sub1 = path.join(srcDir, `${srcBase}${ext}`)
|
|
713
|
+
const sub2 = path.join(srcDir, 'subs', `${srcBase}${ext}`)
|
|
714
|
+
if (await fs.pathExists(sub1)) {
|
|
715
|
+
subtitles.push(sub1)
|
|
716
|
+
}
|
|
717
|
+
if (await fs.pathExists(sub2)) {
|
|
718
|
+
subtitles.push(sub2)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (subtitles.length > 0) {
|
|
722
|
+
log.showCyan(logTag, `${ipx} SubTitles:`, subtitles.join(' '))
|
|
671
723
|
}
|
|
672
724
|
log.show(logTag, `${ipx} FR: ${helper.pathShort(entry.path, 80)}`, chalk.yellow(entry.preset.name), helper.humanTime(entry.startMs))
|
|
673
725
|
log.showGray(logTag, `${ipx} TO:`, fileDst)
|
|
@@ -678,7 +730,7 @@ async function prepareFFmpegCmd(entry) {
|
|
|
678
730
|
fileDstBase,
|
|
679
731
|
fileDst,
|
|
680
732
|
fileDstTemp,
|
|
681
|
-
|
|
733
|
+
subtitles,
|
|
682
734
|
}
|
|
683
735
|
newEntry.ffmpegArgs = createFFmpegArgs(newEntry)
|
|
684
736
|
log.info(logTag, 'ffmpeg', newEntry.ffmpegArgs.flat().join(' '))
|
|
@@ -722,12 +774,7 @@ function getEntryShowInfo(entry) {
|
|
|
722
774
|
showText.push(`pt:${entry.preset.name}`)
|
|
723
775
|
showText.push(`sz:${helper.humanSize(args.size)}`)
|
|
724
776
|
showText.push(`ts:${helper.humanSeconds(args.srcDuration)}`)
|
|
725
|
-
|
|
726
|
-
if (args.dstVideoBitrate !== args.srcVideoBitrate) {
|
|
727
|
-
showText.push(`vb:${kNum(args.srcVideoBitrate)}=>${kNum(args.dstVideoBitrate)}`)
|
|
728
|
-
} else {
|
|
729
|
-
showText.push(`vb:${kNum(args.srcVideoBitrate)}`)
|
|
730
|
-
}
|
|
777
|
+
|
|
731
778
|
showText.push(`a:${ac}`)
|
|
732
779
|
if (args.dstAudioBitrate !== args.srcAudioBitrate) {
|
|
733
780
|
showText.push(`ab:${kNum(args.srcAudioBitrate)}=>${kNum(args.dstAudioBitrate)}`)
|
|
@@ -737,8 +784,12 @@ function getEntryShowInfo(entry) {
|
|
|
737
784
|
if (args.dstAudioQuality > 0) {
|
|
738
785
|
showText.push(`aq:${args.dstAudioQuality}`)
|
|
739
786
|
}
|
|
740
|
-
|
|
741
|
-
|
|
787
|
+
|
|
788
|
+
showText.push(`v:${vc}`)
|
|
789
|
+
if (args.dstVideoBitrate !== args.srcVideoBitrate) {
|
|
790
|
+
showText.push(`vb:${kNum(args.srcVideoBitrate)}=>${kNum(args.dstVideoBitrate)}`)
|
|
791
|
+
} else {
|
|
792
|
+
showText.push(`vb:${kNum(args.srcVideoBitrate)}`)
|
|
742
793
|
}
|
|
743
794
|
if (args.dstFrameRate > 0 && args.dstFrameRate !== args.srcFrameRate) {
|
|
744
795
|
showText.push(`fps:${args.srcFrameRate}=>${args.dstFrameRate}`)
|
|
@@ -748,6 +799,11 @@ function getEntryShowInfo(entry) {
|
|
|
748
799
|
if (args.speed > 0) {
|
|
749
800
|
showText.push(`sp:${args.speed}`)
|
|
750
801
|
}
|
|
802
|
+
if (args.srcWidth !== args.dstWidth || args.srcHeight !== args.dstHeight) {
|
|
803
|
+
showText.push(`${args.srcWidth}x${args.srcHeight}=>${args.dstWidth}x${args.dstHeight}`)
|
|
804
|
+
} else {
|
|
805
|
+
showText.push(`${args.srcWidth}x${args.srcHeight}`)
|
|
806
|
+
}
|
|
751
807
|
return showText.join(',')
|
|
752
808
|
}
|
|
753
809
|
|
|
@@ -802,8 +858,28 @@ function calculateDstArgs(entry) {
|
|
|
802
858
|
let srcVideoBitrate = 0
|
|
803
859
|
let dstVideoBitrate = 0
|
|
804
860
|
|
|
861
|
+
let srcFrameRate = 0
|
|
862
|
+
let dstFrameRate = 0
|
|
863
|
+
let dstWidth = 0
|
|
864
|
+
let dstHeight = 0
|
|
865
|
+
|
|
866
|
+
// 源文件时长
|
|
867
|
+
const srcDuration = info?.duration
|
|
868
|
+
|| ivideo?.duration
|
|
869
|
+
|| iaudio?.duration || 0
|
|
870
|
+
|
|
871
|
+
const srcWidth = ivideo?.width || 0
|
|
872
|
+
const srcHeight = ivideo?.height || 0
|
|
873
|
+
|
|
805
874
|
const reqAudioBitrate = ep.userArgs.audioBitrate || ep.audioBitrate
|
|
806
875
|
const reqVideoBitrate = ep.userArgs.videoBitrate || ep.videoBitrate
|
|
876
|
+
|
|
877
|
+
const dstAudioQuality = ep.userArgs.audioQuality || ep.audioQuality
|
|
878
|
+
const dstVideoQuality = ep.userArgs.videoQuality || ep.videoQuality
|
|
879
|
+
|
|
880
|
+
const dstSpeed = ep.userArgs.speed || ep.speed
|
|
881
|
+
const dstDimension = ep.userArgs.dimension || ep.dimension
|
|
882
|
+
|
|
807
883
|
if (helper.isAudioFile(entry.path)) {
|
|
808
884
|
// 音频文件
|
|
809
885
|
// 文件信息中的码率值
|
|
@@ -838,6 +914,11 @@ function calculateDstArgs(entry) {
|
|
|
838
914
|
dstAudioBitrate = minNoZero(dstAudioBitrate, srcAudioBitrate)
|
|
839
915
|
} else {
|
|
840
916
|
// 视频文件
|
|
917
|
+
const dstWH = calculateScale(srcWidth, srcHeight, dstDimension)
|
|
918
|
+
dstWidth = dstWH.dstWidth
|
|
919
|
+
dstHeight = dstWH.dstHeight
|
|
920
|
+
const srcPixels = srcWidth * srcHeight
|
|
921
|
+
const dstPixels = dstWidth * dstHeight
|
|
841
922
|
// 这个是文件整体码率,如果是是视频文件,等于是视频和音频的码率相加
|
|
842
923
|
const fileBitrate = info?.bitrate || 0
|
|
843
924
|
srcAudioBitrate = iaudio?.bitrate || 0
|
|
@@ -849,51 +930,37 @@ function calculateDstArgs(entry) {
|
|
|
849
930
|
// 音频和视频码率 用户指定>预设
|
|
850
931
|
// 音频和视频码率都不能高于原码率
|
|
851
932
|
dstAudioBitrate = minNoZero(srcAudioBitrate, reqAudioBitrate)
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
933
|
+
// 如果源文件不是1080p,这里码率需要考虑分辨率
|
|
934
|
+
const pixelsScale = PIXELS_1080P / srcPixels
|
|
935
|
+
dstVideoBitrate = minNoZero(srcVideoBitrate * pixelsScale, reqVideoBitrate)
|
|
855
936
|
|
|
937
|
+
log.info(entry.name, "fileBitrate", fileBitrate, "srcVideoBitrate", srcVideoBitrate, "reqVideoBitrate", reqVideoBitrate, "dstVideoBitrate", dstVideoBitrate, "pixelsScale", pixelsScale)
|
|
938
|
+
// 小于1080p分辨率,码率也需要缩放
|
|
939
|
+
if (dstPixels > 0 && dstPixels < PIXELS_1080P) {
|
|
940
|
+
let scaleFactor = dstPixels / PIXELS_1080P
|
|
941
|
+
// 如果目标码率是4K,暂时吧不考虑
|
|
942
|
+
// 如果目标码率不是1080p,根据分辨率智能缩放
|
|
943
|
+
// 示例 辨率1920*1080的目标码率是 1600k
|
|
944
|
+
// 1280*720码率 960k
|
|
945
|
+
// scaleFactor = Math.sqrt(scaleFactor)
|
|
946
|
+
// 缩放码率,平滑系数
|
|
947
|
+
scaleFactor = core.smoothChange(scaleFactor, 1, 0.2)
|
|
948
|
+
// log.info('scaleFactor', scaleFactor)
|
|
949
|
+
dstVideoBitrate = Math.round(dstVideoBitrate * scaleFactor)
|
|
950
|
+
// 目标分辨率,不能大于源文件分辨率
|
|
951
|
+
dstVideoBitrate = minNoZero(dstVideoBitrate, srcVideoBitrate)
|
|
952
|
+
// 取整
|
|
953
|
+
// dstVideoBitrate = Math.floor(dstVideoBitrate / 1000) * 1000
|
|
954
|
+
}
|
|
856
955
|
}
|
|
857
956
|
|
|
858
|
-
// 源文件时长
|
|
859
|
-
const srcDuration = info?.duration
|
|
860
|
-
|| ivideo?.duration
|
|
861
|
-
|| iaudio?.duration || 0
|
|
862
957
|
// 如果目标帧率大于原帧率,就将目标帧率设置为0,即让ffmpeg自动处理,不添加帧率参数
|
|
863
958
|
// 源文件帧率
|
|
864
|
-
|
|
959
|
+
srcFrameRate = ivideo?.framerate || 0
|
|
865
960
|
// 预设或用户帧率,用户指定>预设
|
|
866
961
|
const reqFrameRate = ep.userArgs.framerate || ep.framerate
|
|
867
962
|
// 计算出的目标帧率
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const dstAudioQuality = ep.userArgs.audioQuality || ep.audioQuality
|
|
871
|
-
const dstVideoQuality = ep.userArgs.videoQuality || ep.videoQuality
|
|
872
|
-
|
|
873
|
-
const dstDimension = ep.userArgs.dimension || ep.dimension
|
|
874
|
-
const { dstWidth, dstHeight } = calculateScale(ivideo?.width || 0, ivideo?.height || 0, dstDimension)
|
|
875
|
-
|
|
876
|
-
const dstPixels = dstWidth * dstHeight
|
|
877
|
-
const dstVideoBitrateFixed = dstVideoBitrate
|
|
878
|
-
if (dstPixels > 0) {
|
|
879
|
-
let scaleFactor = dstPixels / PIXELS_1080P
|
|
880
|
-
// dstVideoBitrate针对目标是1080p的数据
|
|
881
|
-
// 如果目标码率不是1080p,根据分辨率智能缩放
|
|
882
|
-
// 示例 辨率1920*1080的目标码率是 1600k
|
|
883
|
-
// 1280*720码率 960k
|
|
884
|
-
// 3840*2160码率 2560k
|
|
885
|
-
// 0.9~1.1 之间不缩放
|
|
886
|
-
if (scaleFactor > 1.1) {
|
|
887
|
-
scaleFactor *= 0.4
|
|
888
|
-
} else if (scaleFactor < 0.9) {
|
|
889
|
-
scaleFactor *= 1.6
|
|
890
|
-
} else {
|
|
891
|
-
scaleFactor = 1
|
|
892
|
-
}
|
|
893
|
-
dstVideoBitrate = Math.round(dstVideoBitrate * scaleFactor)
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const dstSpeed = ep.userArgs.speed || ep.speed
|
|
963
|
+
dstFrameRate = reqFrameRate < srcFrameRate ? reqFrameRate : 0
|
|
897
964
|
|
|
898
965
|
// 用于模板字符串的模板参数,针对当前文件
|
|
899
966
|
// 额外模板参数
|
|
@@ -904,9 +971,9 @@ function calculateDstArgs(entry) {
|
|
|
904
971
|
srcVideoBitrate,
|
|
905
972
|
srcFrameRate,
|
|
906
973
|
srcDuration,
|
|
907
|
-
srcWidth:
|
|
908
|
-
srcHeight:
|
|
909
|
-
srcDuration:
|
|
974
|
+
srcWidth: srcWidth,
|
|
975
|
+
srcHeight: srcHeight,
|
|
976
|
+
srcDuration: srcDuration,
|
|
910
977
|
srcSize: info?.size || 0,
|
|
911
978
|
srcVideoCodec: ivideo?.format,
|
|
912
979
|
srcAudioCodec: iaudio?.format,
|
|
@@ -917,13 +984,12 @@ function calculateDstArgs(entry) {
|
|
|
917
984
|
dstAudioQuality,
|
|
918
985
|
dstVideoQuality,
|
|
919
986
|
dstFrameRate,
|
|
920
|
-
dstDimension,
|
|
921
987
|
dstWidth,
|
|
922
988
|
dstHeight,
|
|
923
989
|
dstSpeed,
|
|
924
990
|
// 码率智能缩放
|
|
925
|
-
|
|
926
|
-
|
|
991
|
+
audioBitScale: core.roundNum(dstAudioBitrate / srcAudioBitrate),
|
|
992
|
+
videoBitScale: core.roundNum(dstVideoBitrate / srcVideoBitrate),
|
|
927
993
|
// 会覆盖preset的同名预设值
|
|
928
994
|
// videoBitrate: dstVideoBitrate,
|
|
929
995
|
videoBitrateK: `${Math.round(dstVideoBitrate / 1000)}K`,
|
|
@@ -986,8 +1052,12 @@ function createFFmpegArgs(entry, forDisplay = false) {
|
|
|
986
1052
|
inputArgs.push("-v", entry.argv.debug ? "repeat+level+info" : "error")
|
|
987
1053
|
// 输出视频时才需要cuda加速,音频用cpu就行
|
|
988
1054
|
if (tempPreset.type === 'video') {
|
|
989
|
-
|
|
990
|
-
|
|
1055
|
+
inputArgs.push("-stats")
|
|
1056
|
+
// 使用cuda硬件解码
|
|
1057
|
+
if (!entry.info.useCPUDecode) {
|
|
1058
|
+
inputArgs.push("-hwaccel", "cuda", "-hwaccel_output_format", "cuda")
|
|
1059
|
+
}
|
|
1060
|
+
// 不支持硬解的格式,如H264-10bit,使用软解
|
|
991
1061
|
}
|
|
992
1062
|
// 输入参数在输入文件前面,顺序重要
|
|
993
1063
|
if (tempPreset.inputArgs?.length > 0) {
|
|
@@ -995,12 +1065,18 @@ function createFFmpegArgs(entry, forDisplay = false) {
|
|
|
995
1065
|
}
|
|
996
1066
|
inputArgs.push('-i')
|
|
997
1067
|
inputArgs.push(forDisplay ? "input.mkv" : `"${entry.path}"`)
|
|
998
|
-
// 添加MP4
|
|
999
|
-
if (entry.
|
|
1000
|
-
|
|
1001
|
-
|
|
1068
|
+
// 添加MP4内嵌字幕文件,支持多个字幕文件
|
|
1069
|
+
if (entry.subtitles?.length > 0) {
|
|
1070
|
+
// 用于显示和调试
|
|
1071
|
+
tempPreset.subtitles = entry.subtitles.map(item => path.basename(item))
|
|
1072
|
+
entry.subtitles.forEach(item => {
|
|
1073
|
+
inputArgs.push('-i')
|
|
1074
|
+
inputArgs.push(`"${item}"`)
|
|
1075
|
+
})
|
|
1002
1076
|
const subArgs = '-c:s mov_text -metadata:s:s:0 language=chi -disposition:s:0 default'
|
|
1003
1077
|
inputArgs = inputArgs.concat(subArgs.split(' '))
|
|
1078
|
+
} else {
|
|
1079
|
+
inputArgs.push('-c:s mov_text')
|
|
1004
1080
|
}
|
|
1005
1081
|
//
|
|
1006
1082
|
//===============================================================
|
|
@@ -1022,8 +1098,13 @@ function createFFmpegArgs(entry, forDisplay = false) {
|
|
|
1022
1098
|
middleArgs.push('-filter_complex')
|
|
1023
1099
|
middleArgs.push(`"${formatArgs(tempPreset.complexFilter, tempPreset)}"`)
|
|
1024
1100
|
} else if (tempPreset.filters?.length > 0) {
|
|
1101
|
+
let tempFilters = tempPreset.filters
|
|
1102
|
+
// 使用软解时,需要传输数据道GPU
|
|
1103
|
+
if (entry.info.useCPUDecode) {
|
|
1104
|
+
tempFilters = 'hwupload_cuda,' + tempFilters
|
|
1105
|
+
}
|
|
1025
1106
|
middleArgs.push('-vf')
|
|
1026
|
-
middleArgs.push(formatArgs(
|
|
1107
|
+
middleArgs.push(formatArgs(tempFilters, tempPreset))
|
|
1027
1108
|
}
|
|
1028
1109
|
// 视频参数
|
|
1029
1110
|
if (tempPreset.videoArgs?.length > 0) {
|
|
@@ -1045,7 +1126,8 @@ function createFFmpegArgs(entry, forDisplay = false) {
|
|
|
1045
1126
|
// 针对视频文件
|
|
1046
1127
|
if (helper.isVideoFile(entry.path)) {
|
|
1047
1128
|
// 如果目标码率大于源文件码率,则不重新编码,考虑误差
|
|
1048
|
-
const shouldCopy = tempPreset.
|
|
1129
|
+
const shouldCopy = tempPreset.srcAudioBitrate > 0
|
|
1130
|
+
&& tempPreset.dstAudioBitrate + 2000 > tempPreset.srcAudioBitrate
|
|
1049
1131
|
// 如果用户指定不重新编码
|
|
1050
1132
|
if (shouldCopy || tempPreset.userArgs.audioCopy) {
|
|
1051
1133
|
tempPreset.audioArgs = '-c:a copy'
|
package/cmd/cmd_moveup.js
CHANGED