mediac 1.8.0 → 1.8.2

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.
@@ -24,8 +24,8 @@ import * as core from '../lib/core.js'
24
24
  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
- import tryfp from '../lib/tryfp.js'
28
- import { calculateScale, compressImage } from "./cmd_shared.js"
27
+ import * as tryfp from '../lib/tryfp.js'
28
+ import { compressImage } from "./cmd_shared.js"
29
29
 
30
30
  //
31
31
  export { aliases, builder, command, describe, handler }
package/cmd/cmd_ffmpeg.js CHANGED
@@ -22,10 +22,9 @@ import { asyncFilter, formatArgs } from '../lib/core.js'
22
22
  import * as log from '../lib/debug.js'
23
23
  import * as enc from '../lib/encoding.js'
24
24
  import presets from '../lib/ffmpeg_presets.js'
25
- import { getMediaInfo } from '../lib/ffprobe.js'
26
25
  import * as mf from '../lib/file.js'
27
26
  import * as helper from '../lib/helper.js'
28
- import { FFMPEG_BINARY } from '../lib/shared.js'
27
+ import { getMediaInfo } from '../lib/mediainfo.js'
29
28
  import { addEntryProps, applyFileNameRules, calculateScale } from './cmd_shared.js'
30
29
 
31
30
  const LOG_TAG = "FFConv"
@@ -157,7 +156,7 @@ const builder = function addOptions(ya, helpOrVersionSet) {
157
156
  })
158
157
  // 视频选项,指定码率
159
158
  .option("video-bitrate", {
160
- alias: "vbk",
159
+ alias: "vb",
161
160
  type: "number",
162
161
  default: 0,
163
162
  describe: "Set video bitrate (in kbytes) in ffmpeg command",
@@ -179,7 +178,7 @@ const builder = function addOptions(ya, helpOrVersionSet) {
179
178
  })
180
179
  // 音频选项,指定码率
181
180
  .option("audio-bitrate", {
182
- alias: "abk",
181
+ alias: "ab",
183
182
  type: "number",
184
183
  default: 0,
185
184
  describe: "Set audio bitrate (in kbytes) in ffmpeg command",
@@ -356,8 +355,8 @@ async function cmdConvert(argv) {
356
355
  testMode: testMode
357
356
  }
358
357
  })
359
- log.showYellow(logTag, 'ARGV:', core.pickTrueValues(argv))
360
- log.showYellow(logTag, 'PRESET:', core.pickTrueValues(preset))
358
+ log.showYellow(logTag, 'ARGV:', argv)
359
+ log.showYellow(logTag, 'PRESET:', preset)
361
360
  const prepareAnswer = await inquirer.prompt([
362
361
  {
363
362
  type: 'confirm',
@@ -408,9 +407,9 @@ async function cmdConvert(argv) {
408
407
  const lastTask = tasks.slice(-1)[0]
409
408
  !testMode && log.fileLog(`ffmpegArgs:`, lastTask.ffmpegArgs.flat(), 'FFConv')
410
409
  log.show('-----------------------------------------------------------')
411
- log.showYellow(logTag, 'PRESET:', core.pickTrueValues(lastTask.debugPreset))
410
+ log.showYellow(logTag, 'PRESET:', lastTask.debugPreset)
412
411
  log.showCyan(logTag, 'CMD: ffmpeg', lastTask.ffmpegArgs.flat().join(' '))
413
- const totalDuration = tasks.reduce((acc, t) => acc + t.info?.format.duration || 0, 0)
412
+ const totalDuration = tasks.reduce((acc, t) => acc + t.info?.duration || 0, 0)
414
413
  log.show('-----------------------------------------------------------')
415
414
  testMode && log.showYellow('++++++++++ TEST MODE (DRY RUN) ++++++++++')
416
415
  log.showYellow(logTag, 'Please CHECK above details BEFORE continue!')
@@ -461,7 +460,7 @@ async function runFFmpegCmd(entry) {
461
460
  log.showGray(logTag, getEntryShowInfo(entry))
462
461
  log.showGray(logTag, `ffmpeg`, entry.ffmpegArgs.flat().join(' '))
463
462
  // }
464
- const exePath = await which(FFMPEG_BINARY)
463
+ const exePath = await which('ffmpeg')
465
464
  if (entry.testMode) {
466
465
  // 测试模式跳过
467
466
  log.show(logTag, `${ipx} Skipped ${entry.path} (${helper.humanSize(entry.size)}) [TestMode]`)
@@ -563,39 +562,19 @@ async function prepareFFmpegCmd(entry) {
563
562
  // 如果没有指定输出目录,直接输出在原文件同目录
564
563
  fileDstDir = path.resolve(srcDir)
565
564
  }
566
- if (isAudio || presets.isAudioExtract(preset)) {
567
- // 不带后缀只改扩展名的m4a文件,如果存在也需要首先忽略
568
- // 可能是其它压缩工具生成的文件,不需要重复压缩
569
- // 检查输出目录
570
- // const fileDstNoSuffix = path.join(fileDstDir, `${srcBase}${dstExt}`)
571
- // if (await fs.pathExists(fileDstNoSuffix)) {
572
- // log.info(
573
- // logTag,
574
- // `${ipx} Skip[DstM4A]: ${helper.pathShort(entry.path)} (${helper.humanSize(entry.size)})`)
575
- // return false
576
- // }
577
- // 检查源文件同目录
578
- // const fileDstSameDirNoSuffix = path.join(srcDir, `${srcBase}${dstExt}`)
579
- // if (await fs.pathExists(fileDstSameDirNoSuffix)) {
580
- // log.info(
581
- // logTag,
582
- // `${ipx} Skip[DstSame]: ${helper.pathShort(entry.path)} (${helper.humanSize(entry.size)})`)
583
- // return false
584
- // }
585
- }
586
565
  try {
587
566
  // 使用ffprobe读取媒体信息,速度较慢
588
- // 注意flac和ape格式的stream里没有bit_rate字段 format里有
567
+ // 注意flac和ape格式的stream里没有bitrate字段 format里有
589
568
  entry.info = await getMediaInfo(entry.path, { audio: isAudio })
590
569
 
591
570
  // ffprobe无法读取时长和比特率,可以认为文件损坏,或不支持的格式,跳过
592
- if (!(entry.info?.format?.duration && entry.info?.format?.bit_rate)) {
571
+ if (!(entry.info?.duration && entry.info?.bitrate)) {
593
572
  log.showYellow(logTag, `${ipx} Skip[BadFormat]: ${entry.path} (${helper.humanSize(entry.size)})`)
594
573
  log.fileLog(`${ipx} Skip[BadFormat]: <${entry.path}> (${helper.humanSize(entry.size)})`, 'Prepare')
595
574
  return false
596
575
  }
597
- const audioCodec = entry.info?.audio?.codec_name
598
- const videoCodec = entry.info?.video?.codec_name
576
+ const audioCodec = entry.info?.audio?.format
577
+ const videoCodec = entry.info?.video?.format
599
578
  if (isAudio) {
600
579
  // 检查音频文件
601
580
  // 放前面,因为 dstAudioBitrate 会用于前缀后缀参数
@@ -605,7 +584,7 @@ async function prepareFFmpegCmd(entry) {
605
584
  entry.tags = meta?.tags
606
585
  // 如果ffprobe或music-metadata获取的数据中有比特率数据
607
586
  log.info(entry.name, preset.name)
608
- if (entry.format?.bitrate || entry.info?.audio.bit_rate || entry.info?.format?.bit_rate) {
587
+ if (entry.format?.bitrate || entry.info?.audio.bitrate || entry.info?.bitrate) {
609
588
  // 可以读取码率,文件未损坏
610
589
  } else {
611
590
  // 如果无法获取元数据,认为不是合法的音频或视频文件,忽略
@@ -613,11 +592,24 @@ async function prepareFFmpegCmd(entry) {
613
592
  log.fileLog(`${ipx} Skip[Invalid]: <${entry.path}> (${helper.humanSize(entry.size)})`, 'Prepare')
614
593
  return false
615
594
  }
595
+ } 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
+
616
605
  }
617
606
  // 获取原始音频码率,计算目标音频码率
618
607
  // vp9视频和opus音频无法获取码率
619
608
  const dstArgs = calculateDstArgs(entry)
620
609
 
610
+ log.info('>>>', entry.name)
611
+ log.info(dstArgs)
612
+
621
613
  // 计算后的视频和音频码率,关联文件
622
614
  // 与预设独立,优先级高于预设
623
615
  // srcXX单位为bytes dstXXX单位为kbytes
@@ -628,13 +620,13 @@ async function prepareFFmpegCmd(entry) {
628
620
  log.info(logTag, entry.path, dstArgs)
629
621
  // 如果转换目标是音频,但是源文件不含音频流,忽略
630
622
  if (entry.preset.type === 'audio' && !audioCodec) {
631
- log.showYellow(logTag, `${ipx} Skip[NoAudio]: ${entry.path}`, getEntryShowInfo(newEntry))
623
+ log.showYellow(logTag, `${ipx} Skip[NoAudio]: ${entry.path} (${helper.humanSize(entry.size)})`)
632
624
  log.fileLog(`${ipx} Skip[NoAudio]: <${entry.path}> (${helper.humanSize(entry.size)})`, 'Prepare')
633
625
  return false
634
626
  }
635
627
  // 如果转换目标是视频,但是源文件不含视频流,忽略
636
628
  if (entry.preset.type === 'video' && !videoCodec) {
637
- log.showYellow(logTag, `${ipx} Skip[NoVideo]: ${entry.path}`, getEntryShowInfo(newEntry))
629
+ log.showYellow(logTag, `${ipx} Skip[NoVideo]: ${entry.path} (${helper.humanSize(entry.size)})`)
638
630
  log.fileLog(`${ipx} Skip[NoVideo]: <${entry.path}> (${helper.humanSize(entry.size)})`, 'Prepare')
639
631
  return false
640
632
  }
@@ -676,8 +668,6 @@ async function prepareFFmpegCmd(entry) {
676
668
  let fileSubtitle = path.join(srcDir, `${srcBase}.ass`)
677
669
  if (!(await fs.pathExists(fileSubtitle))) {
678
670
  fileSubtitle = null
679
- } else {
680
- fileSubtitle = path.basename(fileSubtitle)
681
671
  }
682
672
  log.show(logTag, `${ipx} FR: ${helper.pathShort(entry.path, 80)}`, chalk.yellow(entry.preset.name), helper.humanTime(entry.startMs))
683
673
  log.showGray(logTag, `${ipx} TO:`, fileDst)
@@ -798,10 +788,11 @@ function minNoZero(...numbers) {
798
788
  return Math.min(...fNumbers)
799
789
  }
800
790
 
801
- // 计算视频和音频码率
791
+ const PIXELS_1080P = 1920 * 1080
792
+ // 计算视频和音频码率等各种目标文件数据
802
793
  function calculateDstArgs(entry) {
803
794
  const ep = entry.preset
804
- const iformat = entry.info?.format
795
+ const info = entry.info
805
796
  const ivideo = entry.info?.video
806
797
  const iaudio = entry.info?.audio
807
798
 
@@ -810,10 +801,13 @@ function calculateDstArgs(entry) {
810
801
  let dstAudioBitrate = 0
811
802
  let srcVideoBitrate = 0
812
803
  let dstVideoBitrate = 0
804
+
805
+ const reqAudioBitrate = ep.userArgs.audioBitrate || ep.audioBitrate
806
+ const reqVideoBitrate = ep.userArgs.videoBitrate || ep.videoBitrate
813
807
  if (helper.isAudioFile(entry.path)) {
814
808
  // 音频文件
815
809
  // 文件信息中的码率值
816
- const fileBitrate = entry.format?.bitrate || iformat?.bit_rate || iaudio?.bit_rate || 0
810
+ const fileBitrate = entry.format?.bitrate || info?.bitrate || iaudio?.bitrate || 0
817
811
  if (fileBitrate > 0) {
818
812
  srcAudioBitrate = fileBitrate
819
813
  } else {
@@ -833,7 +827,7 @@ function calculateDstArgs(entry) {
833
827
  dstAudioBitrate = bitrateMap.find(br => srcAudioBitrate > br.threshold)?.value || 48 * 1000
834
828
  } else {
835
829
  // 智能码率关闭,直接使用用户值或预设值
836
- dstAudioBitrate = ep.userArgs.audioBitrate || ep.audioBitrate
830
+ dstAudioBitrate = reqAudioBitrate
837
831
  }
838
832
  } else {
839
833
  // 有的文件无法获取音频码率,如opus,此时srcAudioBitrate=0
@@ -845,23 +839,24 @@ function calculateDstArgs(entry) {
845
839
  } else {
846
840
  // 视频文件
847
841
  // 这个是文件整体码率,如果是是视频文件,等于是视频和音频的码率相加
848
- const fileBitrate = iformat?.bit_rate || 0
849
- srcAudioBitrate = iaudio?.bit_rate || 0
842
+ const fileBitrate = info?.bitrate || 0
843
+ srcAudioBitrate = iaudio?.bitrate || 0
850
844
  // 计算出的视频码率不高于源文件的视频码率
851
845
  // 减去音频的码率,估算为48k
852
- srcVideoBitrate = ivideo?.bit_rate
846
+ srcVideoBitrate = ivideo?.bitrate
853
847
  || fileBitrate - 48 * 1000 || 0
854
848
 
855
849
  // 音频和视频码率 用户指定>预设
856
850
  // 音频和视频码率都不能高于原码率
857
- const reqAudioBitrate = ep.userArgs.audioBitrate || ep.audioBitrate
858
851
  dstAudioBitrate = minNoZero(srcAudioBitrate, reqAudioBitrate)
859
- const reqVideoBitrate = ep.userArgs.videoBitrate || ep.videoBitrate
860
852
  dstVideoBitrate = minNoZero(srcVideoBitrate, reqVideoBitrate)
853
+
854
+ log.info(entry.name, "fileBitrate", fileBitrate, "srcVideoBitrate", srcVideoBitrate, "reqVideoBitrate", reqVideoBitrate, "dstVideoBitrate", dstVideoBitrate)
855
+
861
856
  }
862
857
 
863
858
  // 源文件时长
864
- const srcDuration = iformat?.duration
859
+ const srcDuration = info?.duration
865
860
  || ivideo?.duration
866
861
  || iaudio?.duration || 0
867
862
  // 如果目标帧率大于原帧率,就将目标帧率设置为0,即让ffmpeg自动处理,不添加帧率参数
@@ -878,6 +873,26 @@ function calculateDstArgs(entry) {
878
873
  const dstDimension = ep.userArgs.dimension || ep.dimension
879
874
  const { dstWidth, dstHeight } = calculateScale(ivideo?.width || 0, ivideo?.height || 0, dstDimension)
880
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
+
881
896
  const dstSpeed = ep.userArgs.speed || ep.speed
882
897
 
883
898
  // 用于模板字符串的模板参数,针对当前文件
@@ -891,11 +906,11 @@ function calculateDstArgs(entry) {
891
906
  srcDuration,
892
907
  srcWidth: ivideo?.width || 0,
893
908
  srcHeight: ivideo?.height || 0,
894
- srcDuration: iformat?.duration || 0,
895
- srcSize: iformat?.size || 0,
896
- srcVideoCodec: ivideo?.codec_name,
897
- srcAudioCodec: iaudio?.codec_name,
898
- srcFormat: iformat?.format_name,
909
+ srcDuration: info?.duration || 0,
910
+ srcSize: info?.size || 0,
911
+ srcVideoCodec: ivideo?.format,
912
+ srcAudioCodec: iaudio?.format,
913
+ srcFormat: info?.format,
899
914
  // 计算出来的参数
900
915
  dstAudioBitrate,
901
916
  dstVideoBitrate,
@@ -906,6 +921,9 @@ function calculateDstArgs(entry) {
906
921
  dstWidth,
907
922
  dstHeight,
908
923
  dstSpeed,
924
+ // 码率智能缩放
925
+ audioBitrateScaled: dstAudioBitrate !== reqAudioBitrate,
926
+ videoBitrateScaled: dstVideoBitrate !== dstVideoBitrateFixed,
909
927
  // 会覆盖preset的同名预设值
910
928
  // videoBitrate: dstVideoBitrate,
911
929
  videoBitrateK: `${Math.round(dstVideoBitrate / 1000)}K`,
@@ -927,6 +945,9 @@ function createFFmpegArgs(entry, forDisplay = false) {
927
945
  // 不要使用 entry.perset,下面复制一份针对每个entry
928
946
  const tempPreset = { ...entry.preset, ...entry.dstArgs, }
929
947
 
948
+ log.info('>>>>', entry.name)
949
+ log.info(tempPreset)
950
+
930
951
  // 输入参数
931
952
  let inputArgs = []
932
953
 
@@ -939,7 +960,7 @@ function createFFmpegArgs(entry, forDisplay = false) {
939
960
  }
940
961
  }
941
962
 
942
- log.info('createFFmpegArgs', 'tempPreset', entry.name, core.pickTrueValues(tempPreset))
963
+ log.info('createFFmpegArgs', 'tempPreset', entry.name, tempPreset)
943
964
 
944
965
  // 几种ffmpeg参数设置的时间和功耗
945
966
  // ffmpeg -hide_banner -n -v error -stats -i
package/cmd/cmd_remove.js CHANGED
@@ -21,9 +21,9 @@ import { promisify } from 'util'
21
21
  import { comparePathSmartBy } from "../lib/core.js"
22
22
  import * as log from '../lib/debug.js'
23
23
  import * as enc from '../lib/encoding.js'
24
- import { getMediaInfo } from '../lib/ffprobe.js'
25
24
  import * as mf from '../lib/file.js'
26
25
  import * as helper from '../lib/helper.js'
26
+ import { getMediaInfo } from '../lib/mediainfo.js'
27
27
  import { addEntryProps, applyFileNameRules } from './cmd_shared.js'
28
28
 
29
29
  // a = all, f = files, d = directories
@@ -115,6 +115,15 @@ const builder = function addOptions(ya, helpOrVersionSet) {
115
115
  // 文件名列表文本文件,或者一个目录,里面包含的文件作为文件名列表来源
116
116
  description: "File name list file, or dir contains files for file name",
117
117
  })
118
+ // 视频模式,按照视频文件的元数据删除
119
+ // duration,dimension(width,height),bitrate
120
+ // 参数格式 缩写 du=xx,w=xx,h=xx,dm=xx,bit=xx
121
+ // duration=xx,width=xx,height=xx,bitrate=xx
122
+ .option("video", {
123
+ alias: "vdm",
124
+ type: "string",
125
+ description: "Remove files by video metadata",
126
+ })
118
127
  // 要处理的文件类型 文件或目录或所有,默认只处理文件
119
128
  .option("type", {
120
129
  type: "choices",
package/cmd/cmd_rename.js CHANGED
@@ -16,9 +16,9 @@ import argparser from '../lib/argparser.js'
16
16
  import * as core from '../lib/core.js'
17
17
  import * as log from '../lib/debug.js'
18
18
  import * as enc from '../lib/encoding.js'
19
- import { getMediaInfo } from '../lib/ffprobe.js'
20
19
  import * as mf from '../lib/file.js'
21
20
  import * as helper from '../lib/helper.js'
21
+ import { getMediaInfo } from '../lib/mediainfo.js'
22
22
  import { mergePath } from '../lib/path-merge.js'
23
23
  import { applyFileNameRules, cleanFileName, renameFiles } from "./cmd_shared.js"
24
24
 
@@ -407,9 +407,8 @@ async function preRename(f) {
407
407
  let tplValues = isAudio ? info.audio : info.video
408
408
  tplValues = {
409
409
  ...tplValues,
410
- duration_s: Math.floor(duration),
411
- duration_m: Math.floor(duration / 60),
412
- bitrate_k: Math.floor(bitrate / 1000),
410
+ duration: `${helper.humanSeconds(duration)}`,
411
+ bitrate: `${Math.floor(bitrate / 1000)}K`,
413
412
  }
414
413
  // 替换模板字符串
415
414
  const base = tmpNewBase || oldBase
package/lib/core.js CHANGED
@@ -345,6 +345,25 @@ export function copyFields(source, target, ignoreKeys = []) {
345
345
  }
346
346
  }
347
347
 
348
+ // 直接修改原对象, 删除某些键值
349
+ export function removeFieldsIn(obj, removeKeys = []) {
350
+ for (const key in obj) {
351
+ if (obj.hasOwnProperty(key)
352
+ && removeKeys.includes(key)) {
353
+ delete obj[key]
354
+ }
355
+ }
356
+ }
357
+
358
+ // 直接修改原对象,删除符合条件的键值
359
+ export function removeFieldsBy(obj, filterFunc) {
360
+ for (const [key, value] of Object.entries(obj)) {
361
+ // 如果提供了过滤函数,并且该键值对不符合过滤函数的条件,则跳过该键值对
362
+ if (filterFunc && filterFunc(key, value)) {
363
+ delete obj[key]
364
+ }
365
+ }
366
+ }
348
367
 
349
368
  // 去掉一个object所有值不是基本类型的字段
350
369
  // 保留值类型为字符串、数字、布尔值的字段
@@ -24,6 +24,7 @@ const BIT_RATE_1600K = UNIT_KB * 1600
24
24
  const BIT_RATE_2000K = UNIT_KB * 2000
25
25
  const BIT_RATE_4000K = UNIT_KB * 4000
26
26
  const BIT_RATE_6000K = UNIT_KB * 6000
27
+ const BIT_RATE_8M = UNIT_MB * 8
27
28
  const BIT_RATE_10M = UNIT_MB * 10
28
29
  const BIT_RATE_16M = UNIT_MB * 16
29
30
 
@@ -279,6 +280,8 @@ const HEVC_BASE = new FFmpegPreset('hevc-base', {
279
280
  // 快速读取和播放
280
281
  outputArgs: '-movflags +faststart -movflags use_metadata_tags',
281
282
  // -vf 'scale=if(gte(iw\,ih)\,min(1280\,iw)\,-2):if(lt(iw\,ih)\,min(1280\,ih)\,-2)'
283
+ // 前面如果不加 hwupload_cuda 某些10bit视频会报错
284
+ // 查了半天,发现Nvidia和Intel都不支持H264-10bit的硬解,但是HEVC-10bit可以
282
285
  filters: "scale_cuda='if(gte(iw,ih),min({dimension},iw),-2)':'if(lt(iw,ih),min({dimension},ih),-2)':interp_algo=lanczos",
283
286
  complexFilter: '',
284
287
  })
@@ -299,7 +302,31 @@ const PRESET_HEVC_4K = FFmpegPreset.fromPreset(HEVC_BASE).update({
299
302
  dimension: 3840
300
303
  })
301
304
 
302
- const PRESET_HEVC_HIGH = FFmpegPreset.fromPreset(HEVC_BASE).update({
305
+ const PRESET_HEVC_4K_LOW = FFmpegPreset.fromPreset(HEVC_BASE).update({
306
+ name: 'hevc_4kl',
307
+ videoQuality: 24,
308
+ videoBitrate: BIT_RATE_6000K,
309
+ audioBitrate: BIT_RATE_256K,
310
+ dimension: 3840
311
+ })
312
+
313
+ const PRESET_HEVC_4K_LOWEST = FFmpegPreset.fromPreset(HEVC_BASE).update({
314
+ name: 'hevc_4kt',
315
+ videoQuality: 26,
316
+ videoBitrate: BIT_RATE_4000K,
317
+ audioBitrate: BIT_RATE_192K,
318
+ dimension: 3840
319
+ })
320
+
321
+ const PRESET_HEVC_2K_ULTRA = FFmpegPreset.fromPreset(HEVC_BASE).update({
322
+ name: 'hevc_2ku',
323
+ videoQuality: 22,
324
+ videoBitrate: BIT_RATE_8M,
325
+ audioBitrate: BIT_RATE_256K,
326
+ dimension: 1920
327
+ })
328
+
329
+ const PRESET_HEVC_2K_HIGH = FFmpegPreset.fromPreset(HEVC_BASE).update({
303
330
  name: 'hevc_2kh',
304
331
  videoQuality: 22,
305
332
  videoBitrate: BIT_RATE_6000K,
@@ -315,7 +342,7 @@ const PRESET_HEVC_2K = FFmpegPreset.fromPreset(HEVC_BASE).update({
315
342
  dimension: 1920
316
343
  })
317
344
 
318
- const PRESET_HEVC_MEDIUM = FFmpegPreset.fromPreset(HEVC_BASE).update({
345
+ const PRESET_HEVC_2K_MEDIUM = FFmpegPreset.fromPreset(HEVC_BASE).update({
319
346
  name: 'hevc_2km',
320
347
  videoQuality: 26,
321
348
  videoBitrate: BIT_RATE_2000K,
@@ -323,7 +350,7 @@ const PRESET_HEVC_MEDIUM = FFmpegPreset.fromPreset(HEVC_BASE).update({
323
350
  dimension: 1920
324
351
  })
325
352
 
326
- const PRESET_HEVC_LOW = FFmpegPreset.fromPreset(HEVC_BASE).update({
353
+ const PRESET_HEVC_2K_LOW = FFmpegPreset.fromPreset(HEVC_BASE).update({
327
354
  name: 'hevc_2kl',
328
355
  videoQuality: 26,
329
356
  videoBitrate: BIT_RATE_1600K,
@@ -331,7 +358,7 @@ const PRESET_HEVC_LOW = FFmpegPreset.fromPreset(HEVC_BASE).update({
331
358
  dimension: 1920
332
359
  })
333
360
 
334
- const PRESET_HEVC_LOWEST = FFmpegPreset.fromPreset(HEVC_BASE).update({
361
+ const PRESET_HEVC_2K_LOWEST = FFmpegPreset.fromPreset(HEVC_BASE).update({
335
362
  name: 'hevc_2kt',
336
363
  videoQuality: 28,
337
364
  videoBitrate: BIT_RATE_1200K,
@@ -485,23 +512,25 @@ function initPresets() {
485
512
  PRESET_H264_MEDIUM: PRESET_H264_MEDIUM,
486
513
  // H264 LOW
487
514
  PRESET_H264_LOW: PRESET_H264_LOW,
488
- //
489
- PRESET_HEVC_ULTRA: PRESET_HEVC_ULTRA,
490
515
  //4K超高码率和质量
491
516
  //4K超高码率和质量
492
517
  PRESET_HEVC_ULTRA: PRESET_HEVC_ULTRA,
493
518
  //4k高码率和质量
494
519
  PRESET_HEVC_4K: PRESET_HEVC_4K,
520
+ PRESET_HEVC_4K_LOW: PRESET_HEVC_4K_LOW,
521
+ PRESET_HEVC_4K_LOWEST: PRESET_HEVC_4K_LOWEST,
522
+ // 2K极致质量
523
+ PRESET_HEVC_2K_ULTRA: PRESET_HEVC_2K_ULTRA,
495
524
  // 2K高码率和质量
496
- PRESET_HEVC_HIGH: PRESET_HEVC_HIGH,
525
+ PRESET_HEVC_HIGH: PRESET_HEVC_2K_HIGH,
497
526
  // 2K默认码率和质量
498
527
  PRESET_HEVC_2K: PRESET_HEVC_2K,
499
528
  // 2K中码率和质量
500
- PRESET_HEVC_MEDIUM: PRESET_HEVC_MEDIUM,
529
+ PRESET_HEVC_MEDIUM: PRESET_HEVC_2K_MEDIUM,
501
530
  // 2K低码率和质量
502
- PRESET_HEVC_LOW: PRESET_HEVC_LOW,
531
+ PRESET_HEVC_LOW: PRESET_HEVC_2K_LOW,
503
532
  // 极低画质和码率
504
- PRESET_HEVC_LOWEST: PRESET_HEVC_LOWEST,
533
+ PRESET_HEVC_LOWEST: PRESET_HEVC_2K_LOWEST,
505
534
  // 极速模式,适用于视频教程
506
535
  PRESET_HEVC_SPEED: PRESET_HEVC_SPEED,
507
536
  // 提取视频中的音频,复制或转换为AAC格式
package/lib/file.js CHANGED
@@ -80,8 +80,8 @@ export async function walk(root, options = {}) {
80
80
  ctime: st.ctime,
81
81
  mtime: st.mtime,
82
82
  size: st.size || 0,
83
- isDir: st.isDirectory(),
84
- isFile: st.isFile(),
83
+ isDir: st?.isDirectory() || false,
84
+ isFile: st?.isFile() || false,
85
85
  index,
86
86
  }
87
87
  log.debug(
@@ -97,7 +97,7 @@ export async function walk(root, options = {}) {
97
97
  }
98
98
  return entry
99
99
  } catch (error) {
100
- log.warn(
100
+ log.error(
101
101
  logTag,
102
102
  error,
103
103
  pathShort(fpath)
@@ -0,0 +1,278 @@
1
+ /*
2
+ * Project: mediac
3
+ * Created: 2024-05-02 17:22:06
4
+ * Modified: 2024-05-02 17:22:06
5
+ * Author: mcxiaoke (github@mcxiaoke.com)
6
+ * License: Apache License 2.0
7
+ */
8
+
9
+ import { removeFieldsBy, roundNum } from './core.js'
10
+
11
+ class MediaStreamBase {
12
+ constructor(
13
+ {
14
+ type, // 媒体类型
15
+ format, // 格式名称
16
+ codec, // 编解码器名称
17
+ profile, // 编解码器配置概况
18
+ size, // 文件大小
19
+ duration, // 时长
20
+ bitrate, // 比特率
21
+ }
22
+
23
+ ) {
24
+ this.type = type // 媒体类型
25
+ this.format = format // 格式名称
26
+ this.codec = codec // 编解码器名称
27
+ this.profile = profile // 编解码器配置概况
28
+ this.size = size || 0 // 文件大小
29
+ this.duration = duration // 时长
30
+ this.bitrate = bitrate // 比特率
31
+ }
32
+ }
33
+
34
+ class Video extends MediaStreamBase {
35
+ constructor(
36
+ {
37
+ type, // 媒体类型
38
+ format, // 格式名称
39
+ codec, // 编解码器名称
40
+ profile, // 编解码器配置概况
41
+ size, // 文件大小
42
+ duration, // 时长
43
+ bitrate, // 比特率
44
+
45
+ framerate, // 帧率
46
+ bitDepth, // 位深 8bit or 10bit
47
+ width, // 视频宽度
48
+ height, // 视频高度
49
+ aspectRatio, // 宽高比
50
+ pixelFormat, // 像素格式
51
+ }
52
+
53
+ ) {
54
+ super({ type, format, codec, profile, size, duration, bitrate })
55
+ this.framerate = framerate // 帧率
56
+ this.bitDepth = bitDepth // 位深 8bit or 10bit
57
+ this.width = width // 视频宽度
58
+ this.height = height // 视频高度
59
+ this.aspectRatio = aspectRatio
60
+ this.pixelFormat = pixelFormat
61
+ }
62
+
63
+ }
64
+
65
+ class Audio extends MediaStreamBase {
66
+ constructor(
67
+ {
68
+ type, // 媒体类型
69
+ format, // 格式名称
70
+ codec, // 编解码器名称
71
+ profile, // 编解码器配置概况
72
+ size, // 文件大小
73
+ duration, // 时长
74
+ bitrate, // 比特率
75
+ sampleRate, // 采样率
76
+ }
77
+
78
+ ) {
79
+ super({ type, format, codec, profile, size, duration, bitrate })
80
+ this.sampleRate = sampleRate
81
+ }
82
+ }
83
+
84
+ class MediaInfo {
85
+ constructor(
86
+ {
87
+ provider, // 解析器
88
+ format, // 文件的格式类型(如:video/mp4)
89
+ size, // 文件大小,单位根据情况设定(如字节)
90
+ duration, // 时长,单位根据情况设定(如秒)
91
+ bitrate, // 平均比特率,单位根据情况设定(如bps)
92
+ createdAt, // 创建时间
93
+ audio, // audio stream 音频流
94
+ video, // video stream 视频流
95
+ }
96
+ ) {
97
+ this.provider = provider
98
+ this.format = format
99
+ this.size = size
100
+ this.duration = duration
101
+ this.bitrate = bitrate
102
+ this.createdAt = createdAt
103
+ this.audio = audio
104
+ this.video = video
105
+ }
106
+ }
107
+
108
+ function fromFFprobe(data) {
109
+ const obj = {
110
+ type: data['codec_type'], // 媒体类型
111
+ format: data['codec_name'], // 格式名称
112
+ codec: data['codec_tag_string'], // 编解码器名称
113
+ profile: data['profile'], // 编解码器配置概况
114
+ bitDepth: data['bits_per_raw_sample']
115
+ || data['bits_per_raw_sample'], // 位深 8bit or 10bit
116
+ size: data['size']
117
+ || data['tags']?.['NUMBER_OF_BYTES'], // 文件大小
118
+ duration: data['duration']
119
+ || data['tags']?.['DURATION'], // 时长
120
+ bitrate: data['bit_rate']
121
+ || data['tags']?.['BPS'], // 比特率
122
+ framerate: data['r_frame_rate'], // 帧率
123
+ pixelFormat: data['pix_fmt'],
124
+ width: data['width'], // 视频宽度
125
+ height: data['height'], // 视频高度
126
+ aspectRatio: data['display_aspect_ratio'],
127
+ sampleRate: data['sample_rate'],
128
+ language: data['tags']?.['language'], // 语言
129
+ }
130
+ return createStreamData(obj)
131
+ }
132
+
133
+ function fromMediaInfo(data) {
134
+ // console.log('fromMediaInfo', data)
135
+ const obj = {
136
+ type: data['@type'].toLowerCase(), // 媒体类型
137
+ format: data['Format'].toLowerCase(), // 格式名称
138
+ codec: data['CodecID'], // 编解码器名称
139
+ profile: data['Format_Profile'], // 编解码器配置概况
140
+ bitDepth: data['BitDepth'], // 位深 8bit or 10bit
141
+ size: data['StreamSize'], // 文件大小
142
+ duration: data['Duration'], // 时长
143
+ bitrate: data['BitRate'], // 比特率
144
+ framerate: data['FrameRate_Num'] || data['FrameRate'], // 帧率
145
+ pixelFormat: data['ColorSpace'] ?
146
+ data['ColorSpace'] + data['ChromaSubsampling'] : undefined,
147
+ width: data['Width'], // 视频宽度
148
+ height: data['Height'], // 视频高度
149
+ aspectRatio: data['DisplayAspectRatio'],
150
+ sampleRate: data['SamplingRate'],
151
+ language: data['language'], // 语言
152
+ }
153
+ return createStreamData(obj)
154
+ }
155
+
156
+ function createStreamData(obj) {
157
+ const isAudio = obj.type === 'audio'
158
+ const stream = isAudio ? (new Audio(obj)) : (new Video(obj))
159
+ removeFieldsBy(stream, (k, v) => v === undefined || v === null)
160
+ return stream
161
+ }
162
+
163
+ // 解析ffprobe json输出,返回MediaInfo
164
+ export function fromFFprobeJson(data) {
165
+ // console.log('MediaInfo.fromFFprobeJson', data)
166
+ const root = data.format
167
+ const ad = data.streams?.find(obj => obj.codec_type === "audio")
168
+ const vd = data.streams?.find(obj => obj.codec_type === "video")
169
+ const obj = {
170
+ provider: 'ffprobe',
171
+ format: root['format_long_name'],
172
+ size: root['size'],
173
+ duration: root['duration'],
174
+ bitrate: root['bit_rate'],
175
+ createdAt: root['tags']?.['creation_time'],
176
+ audio: ad && fromFFprobe(ad),
177
+ video: vd && fromFFprobe(vd),
178
+ }
179
+ const info = new MediaInfo(convertNumber(obj))
180
+ removeFieldsBy(info, (k, v) => v === undefined || v === null)
181
+ return info
182
+ }
183
+
184
+ // 解析mediainfo json输出,返回MediaInfo
185
+ export function fromMediaInfoJson(data) {
186
+ // console.log('MediaInfo.fromMediaInfoJson', data)
187
+ const root = data.media?.track?.find(o => o['@type'] === "General")
188
+ const ad = data.media?.track?.find(o => o['@type'] === "Audio")
189
+ const vd = data.media?.track?.find(o => o['@type'] === "Video")
190
+ const obj = {
191
+ provider: 'mediainfo',
192
+ format: root['Format']?.toLowerCase(),
193
+ size: root['FileSize'],
194
+ duration: root['Duration'],
195
+ bitrate: root['OverallBitRate'],
196
+ createdAt: root['Encoded_Date'],
197
+ audio: ad && fromMediaInfo(ad),
198
+ video: vd && fromMediaInfo(vd),
199
+ }
200
+ const info = new MediaInfo(convertNumber(obj))
201
+ removeFieldsBy(info, (k, v) => v === undefined || v === null)
202
+ return info
203
+ }
204
+
205
+ // 计算平均码率的方法
206
+ // 如果是单音频文件,还有一个码率计算方式
207
+ // fileSize / duration * 8 = bitrate
208
+ // 或者如果知道流大小,也可以计算出来
209
+ // streamSize / duration * 8 = bitrate
210
+ // 还可以用用ffmpeg读取元数据,需要解析
211
+ // ffmpeg -hide_banner -i video.mp4 -c copy -f null -
212
+
213
+ // "tags": {
214
+ // "title": "Encode By H-Enc",
215
+ // "BPS": "2430147",
216
+ // "DURATION": "00:16:44.003000000",
217
+ // }
218
+ // 提取tags里DURATION字段
219
+ // 示例 00:16:43.946000000
220
+ // 示例 00:16:44.003000000
221
+ // 分割 (00):(16):(44.003000000)
222
+ function extractDuration(timeString) {
223
+ // 使用 match 方法匹配时间字符串中的各个部分
224
+ const match = timeString.match(/(\d{2}):(\d{2}):(\d{2}(?:\.\d+))?/)
225
+
226
+ if (match) {
227
+ // log.showBlue(timeString, match)
228
+ // 解构赋值提取匹配到的时间部分
229
+ const [, hours, minutes, seconds] = match.map(Number)
230
+ // 将时间部分转换成秒
231
+ return roundNum(hours * 3600 + minutes * 60 + seconds)
232
+ } else {
233
+ return 0
234
+ }
235
+ }
236
+
237
+ // 字符串值转为数字值,修改原对象
238
+ function convertNumber(obj) {
239
+ // 兼容空值
240
+ if (!obj) return obj
241
+ // 遍历对象的所有属性
242
+ for (const [key, value] of Object.entries(obj)) {
243
+ // 匹配 DURATION
244
+ if (key === 'DURATION') {
245
+ obj[key] = extractDuration(value)
246
+ continue
247
+ }
248
+ // 对于对象类型,递归处理
249
+ if (typeof value === 'object') {
250
+ obj[key] = convertNumber(value)
251
+ continue
252
+ }
253
+ // 检查属性的值是否为字符串类型且可以转换为数字
254
+ else if (typeof value === 'string') {
255
+ // 匹配字符串数字
256
+ if (/^\d+(\.\d+)?$/.test(value)) {
257
+ // 解析字符串数字
258
+ // 如果可以转换为数字,则将其转换并更新对象的值
259
+ obj[key] = roundNum(parseFloat(value))
260
+ } else if (key.includes('frame_rate')) {
261
+ // 解析 '25/1' 这种 r_frame_rate 字段值
262
+ const regex = /(\d+)\/(\d+)/
263
+ const match = value.match(regex)
264
+ if (match) {
265
+ const numerator = parseInt(match[1])
266
+ const denominator = parseInt(match[2])
267
+ if (denominator !== 0) {
268
+ obj[key] = roundNum(numerator / denominator)
269
+ } else {
270
+ obj[key] = 0
271
+ }
272
+ }
273
+ }
274
+ continue
275
+ }
276
+ }
277
+ return obj
278
+ }
@@ -0,0 +1,99 @@
1
+ /*
2
+ * Project: mediac
3
+ * Created: 2024-04-20 17:00:36
4
+ * Modified: 2024-04-20 17:00:36
5
+ * Author: mcxiaoke (github@mcxiaoke.com)
6
+ * License: Apache License 2.0
7
+ */
8
+
9
+ import { execa } from 'execa'
10
+ import iconv from 'iconv-lite'
11
+ import { roundNum } from './core.js'
12
+ import * as log from './debug.js'
13
+ import { fromFFprobeJson, fromMediaInfoJson } from './media_parser.js'
14
+ import { trySmartAsync } from './tryfp.js'
15
+
16
+ import which from 'which'
17
+
18
+ export const FFMPEG_BINARY = 'ffmpeg'
19
+ export const FFPROBE_BINARY = 'ffprobe'
20
+ export const MEDIAINFO_BINARY = 'mediainfo'
21
+
22
+ // 检测可执行文件是否存在
23
+ const HAS_FFPROBE_EXE = await which(FFPROBE_BINARY, { nothrow: true })
24
+ const HAS_MEDIAINFO_EXE = await which(MEDIAINFO_BINARY, { nothrow: true })
25
+
26
+ function fixEncoding(str = '') {
27
+ return iconv.decode(Buffer.from(str, 'binary'), 'gbk')
28
+ }
29
+
30
+ // ffprobe -v error -show_entries 'stream=codec_name,codec_long_name,profile,codec_type,codec_tag_string,width,height,display_aspect_ratio,pix_fmt,duration,bit_rate,sample_rate,sample_fmt,time_base,r_frame_rate,channels,bits_per_sample:format=format_name,format_long_name,duration,size,bit_rate:format_tags:stream_tags' -of json
31
+ // 获取媒体文件信息 使用ffprobe
32
+ async function ffprobeCall(filePath) {
33
+ // 只选择需要的字段,避免乱码和非法JSON
34
+ // 有的文件视频和音频duration和bit_rate放在stream_tags里
35
+ const propsSelected = 'stream=codec_name,codec_long_name,profile,codec_type,codec_tag_string,width,height,display_aspect_ratio,pix_fmt,duration,bit_rate,sample_rate,sample_fmt,time_base,r_frame_rate,avg_frame_rate,channels,bits_per_sample,bits_per_raw_sample:format=format_name,format_long_name,duration,size,bit_rate:stream_tags:format_tags=creation_time'
36
+ const cmdArgs = ['-v', 'error']
37
+ cmdArgs.push('-show_entries', propsSelected)
38
+ cmdArgs.push('-of', 'json', filePath)
39
+ // 使用 execa 执行 ffprobe 命令
40
+ const { stdout, stderr } = await execa(FFPROBE_BINARY, cmdArgs, { encoding: 'binary' })
41
+ const info = JSON.parse(stdout)
42
+ log.debug('ffprobeCall', filePath, info)
43
+ return info
44
+ }
45
+
46
+ // 获取媒体文件信息 使用mediainfo
47
+ async function mediainfoCall(filePath) {
48
+ const cmdArgs = ['--Output=JSON', filePath]
49
+ // 使用 execa 执行 ffprobe 命令
50
+ const { stdout, stderr } = await execa(MEDIAINFO_BINARY, cmdArgs, { encoding: 'binary' })
51
+ log.showGray(stderr)
52
+ const result = JSON.parse(stdout)
53
+ log.debug('ffprobeCall', filePath, result)
54
+ return result
55
+ }
56
+
57
+ async function parseFFProbe(filePath) {
58
+ return fromFFprobeJson(await ffprobeCall(filePath))
59
+ }
60
+
61
+ async function parseMediaInfo(filePath) {
62
+ return fromMediaInfoJson(await mediainfoCall(filePath))
63
+ }
64
+
65
+
66
+ export async function getMediaInfo(filePath, options = { ffprobe: false }) {
67
+ if (!HAS_FFPROBE_EXE && !HAS_MEDIAINFO_EXE) {
68
+ throw new Error('both ffprobe and mediainfo binary not found')
69
+ }
70
+ let [err1, data1] = await trySmartAsync(options.ffprobe ? parseFFProbe : parseMediaInfo)(filePath)
71
+ if (err1) {
72
+ let [err2, data2] = await trySmartAsync(options.ffprobe ? parseMediaInfo : parseFFProbe)(filePath)
73
+ if (data2) {
74
+ return data2
75
+ }
76
+ err1 && log.error('getMediaInfo', fixEncoding(err1?.message))
77
+ err2 && log.error('getMediaInfo', fixEncoding(err2?.message))
78
+ } else {
79
+ return data1
80
+ }
81
+ }
82
+
83
+ export async function getSimpleInfo(filePath, options = {}) {
84
+ const info = await getMediaInfo(filePath, options)
85
+ const arr = []
86
+ if (!info?.duration) {
87
+ arr.push('ERROR: failed to get media info!')
88
+ }
89
+ arr.push(`format=${info?.format},duration=${info?.duration}s,bitrate=${roundNum((info?.bitrate || 0) / 1000)}K`)
90
+ if (info?.audio) {
91
+ const a = info?.audio
92
+ arr.push(`a:codec=${a.format},bitrate=${roundNum(a.bitrate || 0 / 1000)}K`)
93
+ }
94
+ if (info?.video) {
95
+ const v = info?.video
96
+ arr.push(`v:codec=${v.format},bitrate=${roundNum(v.bitrate || 0 / 1000)}K,fps=${v.framerate},size=${v.width}x${v.height}`)
97
+ }
98
+ return arr
99
+ }
package/lib/shared.js CHANGED
@@ -6,5 +6,3 @@
6
6
  * License: Apache License 2.0
7
7
  */
8
8
 
9
- export const FFMPEG_BINARY = 'ffmpeg'
10
- export const FFPROBE_BINARY = 'ffprobe'
package/lib/tryfp.js CHANGED
@@ -44,10 +44,8 @@ const trySmartAsync = tryWrapAsync(throwNativeErr)
44
44
  const tryCatch = tryWrap()
45
45
  const tryCatchAsync = tryWrapAsync()
46
46
 
47
- export default {
48
- setNoneValue,
49
- trySmart,
50
- trySmartAsync,
51
- tryCatch,
52
- tryCatchAsync,
53
- }
47
+ export {
48
+ setNoneValue, tryCatch,
49
+ tryCatchAsync, trySmart,
50
+ trySmartAsync
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediac",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "type": "module",
5
5
  "description": "MediaCli is a multimedia file processing tool that utilizes ffmpeg and exiftool, among others, to compress/convert/rename/delete/organize media files, including images, videos, and audio.",
6
6
  "main": "index.js",
package/lib/ffprobe.js DELETED
@@ -1,205 +0,0 @@
1
- /*
2
- * Project: mediac
3
- * Created: 2024-04-20 17:00:36
4
- * Modified: 2024-04-20 17:00:36
5
- * Author: mcxiaoke (github@mcxiaoke.com)
6
- * License: Apache License 2.0
7
- */
8
-
9
- import { execa } from 'execa'
10
- import * as log from '../lib/debug.js'
11
- import { roundNum } from './core.js'
12
- import { FFPROBE_BINARY } from './shared.js'
13
-
14
- // ffprobe -v error -show_entries 'stream=codec_name,codec_long_name,profile,codec_type,codec_tag_string,width,height,display_aspect_ratio,pix_fmt,duration,bit_rate,sample_rate,sample_fmt,time_base,r_frame_rate,channels,bits_per_sample:format=format_name,format_long_name,duration,size,bit_rate:format_tags:stream_tags' -of json
15
- // 获取媒体文件信息的函数
16
- export async function getMediaInfo(filePath, options = { video: false, audio: false, fullData: false }) {
17
- // 只选择需要的字段,避免乱码和非法JSON
18
- // 有的文件视频和音频duration和bit_rate放在stream_tags里
19
- const propsSelected = 'stream=codec_name,codec_long_name,profile,codec_type,codec_tag_string,width,height,display_aspect_ratio,pix_fmt,duration,bit_rate,sample_rate,sample_fmt,time_base,r_frame_rate,avg_frame_rate,channels,bits_per_sample:format=format_name,format_long_name,duration,size,bit_rate:stream_tags'
20
- const selectJsonArgs = ['-v', 'error']
21
- // 只选择 audio stream
22
- if (options.audio) {
23
- selectJsonArgs.push('-select_streams', 'a:0')
24
- }
25
- if (options.video) {
26
- //只选择 video stream
27
- selectJsonArgs.push('-select_streams', 'v:0')
28
- }
29
- selectJsonArgs.push('-show_entries', propsSelected)
30
- selectJsonArgs.push('-of', 'json', filePath)
31
- try {
32
- // 使用 execa 执行 ffprobe 命令
33
- const { stdout, stderr } = await execa(FFPROBE_BINARY, selectJsonArgs)
34
- // console.log(stdout)
35
- // console.log(stderr)
36
- // return parseFFProbeOutput(stdout)
37
- const info = JSON.parse(stdout)
38
- // 只返回stream第一项
39
- // format作为子项
40
- log.debug('getMediaInfo', filePath, info)
41
- // 只返回第一条视频轨和第一条音轨
42
- // 如果有fullData参数,返回全部streams数据
43
- const result = {
44
- video: convertNumber(info.streams.find(obj => obj.codec_type === "video")),
45
- audio: convertNumber(info.streams.find(obj => obj.codec_type === "audio")),
46
- format: convertNumber(info.format),
47
- streams: options.fullData ? info.streams : undefined,
48
- }
49
- if (result.video) {
50
- result.video.duration = result.video?.duration || result.video?.tags?.DURATION
51
- result.video.bit_rate = result.video?.bit_rate || result.video?.tags?.BPS
52
- result.video.framerate = result.video?.r_frame_rate || result.video?.avg_frame_rate
53
- }
54
- // 特殊处理,有些视频文件的时长和码率参数放在stream_tags里
55
- if (result.audio) {
56
- result.audio.duration = result.audio?.duration || result.audio?.tags?.DURATION
57
- result.audio.bit_rate = result.audio?.bit_rate || result.audio?.tags?.BPS
58
- }
59
- return result
60
- } catch (error) {
61
- log.warn(`ERROR:`, error.message?.slice(-160))
62
- }
63
- }
64
-
65
- export async function getSimpleInfo(filePath, options = {}) {
66
- const info = await getMediaInfo(filePath, options)
67
- const arr = []
68
- if (info?.audio) {
69
- const a = info?.audio
70
- arr.push(`a:codec=${a.codec_name},bitrate=${roundNum(a.bit_rate / 1000)}K,time=${Math.floor(a.duration)}s`)
71
- }
72
- if (info?.video) {
73
- const v = info?.video
74
- arr.push(`v:codec=${v.codec_name},bitrate=${roundNum(v.bit_rate / 1000)}K,time=${Math.floor(v.duration)}s,fps=${v.r_frame_rate},size=${v.width}x${v.height}`)
75
- }
76
-
77
- return arr
78
- }
79
-
80
- // "tags": {
81
- // "title": "Encode By H-Enc",
82
- // "BPS": "2430147",
83
- // "DURATION": "00:16:44.003000000",
84
- // }
85
- // 提取tags里DURATION字段
86
- // 示例 00:16:43.946000000
87
- // 示例 00:16:44.003000000
88
- // 分割 (00):(16):(44.003000000)
89
- function extractDuration(timeString) {
90
- // 使用 match 方法匹配时间字符串中的各个部分
91
- const match = timeString.match(/(\d{2}):(\d{2}):(\d{2}(?:\.\d+))?/)
92
-
93
- if (match) {
94
- // log.showBlue(timeString, match)
95
- // 解构赋值提取匹配到的时间部分
96
- const [, hours, minutes, seconds] = match.map(Number)
97
- // 将时间部分转换成秒
98
- return roundNum(hours * 3600 + minutes * 60 + seconds)
99
- } else {
100
- return 0
101
- }
102
- }
103
-
104
- // 字符串值转为数字值,修改原对象
105
- function convertNumber(obj) {
106
- // 兼容空值
107
- if (!obj) return obj
108
- // 遍历对象的所有属性
109
- for (const [key, value] of Object.entries(obj)) {
110
- // 匹配 DURATION
111
- if (key === 'DURATION') {
112
- obj[key] = extractDuration(value)
113
- continue
114
- }
115
- // 对于对象类型,递归处理
116
- if (typeof value === 'object') {
117
- obj[key] = convertNumber(value)
118
- continue
119
- }
120
- // 检查属性的值是否为字符串类型且可以转换为数字
121
- else if (typeof value === 'string') {
122
- // 匹配字符串数字
123
- if (/^\d+(\.\d+)?$/.test(value)) {
124
- // 解析字符串数字
125
- // 如果可以转换为数字,则将其转换并更新对象的值
126
- obj[key] = roundNum(parseFloat(value))
127
- } else if (key.includes('frame_rate')) {
128
- // 解析 '25/1' 这种 r_frame_rate 字段值
129
- const regex = /(\d+)\/(\d+)/
130
- const match = value.match(regex)
131
- if (match) {
132
- const numerator = parseInt(match[1])
133
- const denominator = parseInt(match[2])
134
- if (denominator !== 0) {
135
- obj[key] = roundNum(numerator / denominator)
136
- } else {
137
- obj[key] = 0
138
- }
139
- }
140
- }
141
- continue
142
- }
143
- }
144
- return obj
145
- }
146
-
147
- // 返回数据示例 video/audio/format 三个字段
148
-
149
- // {
150
- // video: {
151
- // codec_name: 'hevc',
152
- // codec_long_name: 'H.265 / HEVC (High Efficiency Video Coding)',
153
- // profile: 'Main 10',
154
- // codec_type: 'video',
155
- // codec_tag_string: '[0][0][0][0]',
156
- // width: 1920,
157
- // height: 1080,
158
- // display_aspect_ratio: '16:9',
159
- // pix_fmt: 'yuv420p10le',
160
- // r_frame_rate: 23.98,
161
- // avg_frame_rate: 23.98,
162
- // time_base: '1/1000',
163
- // tags: {
164
- // title: 'Encode By H-Enc',
165
- // BPS: '2430147',
166
- // DURATION: '00:16:44.003000000',
167
- // NUMBER_OF_FRAMES: '24072',
168
- // NUMBER_OF_BYTES: '304984449',
169
- // _STATISTICS_WRITING_APP: "mkvmerge v70.0.0 ('Caught A Lite Sneeze') 64-bit",
170
- // _STATISTICS_WRITING_DATE_UTC: '2022-10-05 18:40:29',
171
- // _STATISTICS_TAGS: 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES'
172
- // }
173
- // },
174
- // audio: {
175
- // codec_name: 'aac',
176
- // codec_long_name: 'AAC (Advanced Audio Coding)',
177
- // profile: 'LC',
178
- // codec_type: 'audio',
179
- // codec_tag_string: '[0][0][0][0]',
180
- // sample_fmt: 'fltp',
181
- // sample_rate: 48000,
182
- // channels: 2,
183
- // bits_per_sample: 0,
184
- // r_frame_rate: 0,
185
- // avg_frame_rate: 0,
186
- // time_base: '1/1000',
187
- // tags: {
188
- // BPS: '261491',
189
- // DURATION: '00:16:43.946000000',
190
- // NUMBER_OF_FRAMES: '47060',
191
- // NUMBER_OF_BYTES: '32815429',
192
- // _STATISTICS_WRITING_APP: "mkvmerge v70.0.0 ('Caught A Lite Sneeze') 64-bit",
193
- // _STATISTICS_WRITING_DATE_UTC: '2022-10-05 18:40:29',
194
- // _STATISTICS_TAGS: 'BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES'
195
- // }
196
- // },
197
- // format: {
198
- // format_name: 'matroska,webm',
199
- // format_long_name: 'Matroska / WebM',
200
- // duration: 1004,
201
- // size: 338094926,
202
- // bit_rate: 2693975
203
- // },
204
- // streams: undefined
205
- // }