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.
@@ -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("purge", {
43
+ .option("delete-source-files", {
44
44
  alias: "p",
45
45
  type: "boolean",
46
46
  default: false,
47
- description: "Purge original image files",
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("purge-only", {
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 = path.resolve(argv.input)
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.purgeOnly || false
137
- const purgeSource = argv.purge || false
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 md = exif(im.exif)?.Image
343
+ const [err, iexif] = tryfp.tryCatch(exif)(im.exif)
321
344
  // 跳过以前由mediac压缩过的图片,避免重复压缩
322
- if (!f.force) {
323
- if (md.Copyright?.includes("mediac")
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", error.message, fileSrc)
343
- log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${error.message}`, logTag)
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 } = calculateImageScale(im.width, im.height, maxWidth)
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() ? 1 : 50
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
- `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)}`)
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("purge-source-files", {
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.purgeSourceFiles) {
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.showGray(logTag, getEntryShowInfo(entry))
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, { audio: isAudio })
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
- // H264 10Bit Nvidia和Intel都不支持硬解,直接跳过
597
- if (entry.info?.video?.format === 'h264'
598
- && entry.info?.video.bitDepth === 10
599
- && entry.info?.video.profile?.includes('10')) {
600
- log.showYellow(logTag, `${ipx} Skip[H264_10Bit]: ${entry.path} ${entry.info?.video?.pixelFormat} (${helper.humanSize(entry.size)})`)
601
- log.fileLog(`${ipx} Skip[H264_10Bit]: <${entry.path}> (${helper.humanSize(entry.size)})`, 'Prepare')
602
- return false
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
- let fileSubtitle = path.join(srcDir, `${srcBase}.ass`)
669
- if (!(await fs.pathExists(fileSubtitle))) {
670
- fileSubtitle = null
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
- fileSubtitle,
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
- showText.push(`v:${vc}`)
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
- if (args.srcWidth !== args.dstWidth || args.srcHeight !== args.dstHeight) {
741
- showText.push(`dm:${args.srcWidth}x${args.srcHeight}=>${args.dstWidth}x${args.dstHeight}`)
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
- dstVideoBitrate = minNoZero(srcVideoBitrate, reqVideoBitrate)
853
-
854
- log.info(entry.name, "fileBitrate", fileBitrate, "srcVideoBitrate", srcVideoBitrate, "reqVideoBitrate", reqVideoBitrate, "dstVideoBitrate", dstVideoBitrate)
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
- const srcFrameRate = ivideo?.framerate || 0
959
+ srcFrameRate = ivideo?.framerate || 0
865
960
  // 预设或用户帧率,用户指定>预设
866
961
  const reqFrameRate = ep.userArgs.framerate || ep.framerate
867
962
  // 计算出的目标帧率
868
- let dstFrameRate = reqFrameRate < srcFrameRate ? reqFrameRate : 0
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: ivideo?.width || 0,
908
- srcHeight: ivideo?.height || 0,
909
- srcDuration: info?.duration || 0,
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
- audioBitrateScaled: dstAudioBitrate !== reqAudioBitrate,
926
- videoBitrateScaled: dstVideoBitrate !== dstVideoBitrateFixed,
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
- // -hwaccel cuda -hwaccel_output_format cuda
990
- inputArgs = inputArgs.concat(["-stats", "-hwaccel", "cuda", "-hwaccel_output_format", "cuda"])
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.fileSubtitle) {
1000
- inputArgs.push('-i')
1001
- inputArgs.push(`"${entry.fileSubtitle}"`)
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(tempPreset.filters, tempPreset))
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.dstAudioBitrate + 2000 > tempPreset.srcAudioBitrate
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
@@ -151,6 +151,7 @@ const handler = async function cmdMoveUp(argv) {
151
151
  log.showYellow(logTag, "New Name:", fileDst)
152
152
  }
153
153
  }
154
+ //todo check file size
154
155
  if (await fs.pathExists(fileDst)) {
155
156
  log.showYellow(logTag, "Exists:", fileDst)
156
157
  continue