ffmpeg_helper.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. package ffmpeg_helper
  2. import (
  3. "bytes"
  4. "errors"
  5. "fmt"
  6. "github.com/allanpk716/ChineseSubFinder/internal/common"
  7. "github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser/ass"
  8. "github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser/srt"
  9. "github.com/allanpk716/ChineseSubFinder/internal/pkg/language"
  10. "github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
  11. "github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
  12. "github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_parser_hub"
  13. "github.com/tidwall/gjson"
  14. "os"
  15. "os/exec"
  16. "path/filepath"
  17. "strings"
  18. )
  19. type FFMPEGHelper struct {
  20. SubParserHub *sub_parser_hub.SubParserHub // 字幕内容的解析器
  21. }
  22. func NewFFMPEGHelper() *FFMPEGHelper {
  23. return &FFMPEGHelper{
  24. SubParserHub: sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser()),
  25. }
  26. }
  27. // Version 获取版本信息,如果不存在 FFMPEG 和 ffprobe 则报错
  28. func (f FFMPEGHelper) Version() (string, error) {
  29. outMsg0, err := f.getVersion("ffmpeg")
  30. if err != nil {
  31. return "", err
  32. }
  33. outMsg1, err := f.getVersion("ffprobe")
  34. if err != nil {
  35. return "", err
  36. }
  37. return outMsg0 + "\r\n" + outMsg1, nil
  38. }
  39. // GetFFMPEGInfo 获取 视频的 FFMPEG 信息,包含音频和字幕
  40. // 优先会导出 中、英、日、韩 类型的,字幕如果没有语言类型,则也导出,然后需要额外的字幕语言的判断去辅助标记(读取文件内容)
  41. func (f *FFMPEGHelper) GetFFMPEGInfo(videoFileFullPath string, exportType ExportType) (bool, *FFMPEGInfo, error) {
  42. const args = "-v error -show_format -show_streams -print_format json"
  43. cmdArgs := strings.Fields(args)
  44. cmdArgs = append(cmdArgs, videoFileFullPath)
  45. cmd := exec.Command("ffprobe", cmdArgs...)
  46. buf := bytes.NewBufferString("")
  47. //指定输出位置
  48. cmd.Stderr = buf
  49. cmd.Stdout = buf
  50. err := cmd.Start()
  51. if err != nil {
  52. return false, nil, err
  53. }
  54. err = cmd.Wait()
  55. if err != nil {
  56. return false, nil, err
  57. }
  58. // 解析得到的字符串反馈
  59. bok, ffMPEGInfo := f.parseJsonString2GetFFProbeInfo(videoFileFullPath, buf.String())
  60. if bok == false {
  61. return false, nil, nil
  62. }
  63. nowCacheFolderPath, err := ffMPEGInfo.GetCacheFolderFPath()
  64. if err != nil {
  65. return false, nil, err
  66. }
  67. // 在函数调用完毕后,判断是否需要清理
  68. defer func() {
  69. if bok == false && ffMPEGInfo != nil {
  70. err := os.RemoveAll(nowCacheFolderPath)
  71. if err != nil {
  72. log_helper.GetLogger().Errorln("GetFFMPEGInfo - RemoveAll", err.Error())
  73. return
  74. }
  75. }
  76. }()
  77. // 查找当前这个视频外置字幕列表
  78. err = ffMPEGInfo.GetExternalSubInfos(f.SubParserHub)
  79. if err != nil {
  80. return false, nil, err
  81. }
  82. // 判断这个视频是否已经导出过内置的字幕和音频文件了
  83. if ffMPEGInfo.IsExported(exportType) == false {
  84. // 说明缓存不存在,需要导出,这里需要注意,如果导出失败了,这个文件夹要清理掉
  85. if my_util.IsDir(nowCacheFolderPath) == true {
  86. // 如果存在则,先清空一个这个文件夹
  87. err = my_util.ClearFolder(nowCacheFolderPath)
  88. if err != nil {
  89. bok = false
  90. return bok, nil, err
  91. }
  92. }
  93. // 开始导出
  94. // 构建导出的命令参数
  95. exportAudioArgs, exportSubArgs := f.getAudioAndSubExportArgs(videoFileFullPath, ffMPEGInfo)
  96. // 执行导出,音频和内置的字幕
  97. execErrorString, err := f.exportAudioAndSubtitles(exportAudioArgs, exportSubArgs, exportType)
  98. if err != nil {
  99. log_helper.GetLogger().Errorln("exportAudioAndSubtitles", execErrorString)
  100. bok = false
  101. return bok, nil, err
  102. }
  103. // 导出后,需要把现在导出的文件的路径给复制给 ffMPEGInfo 中
  104. // 音频是否导出了
  105. ffMPEGInfo.isAudioExported(nowCacheFolderPath)
  106. // 字幕都要导出了
  107. ffMPEGInfo.isSubExported(nowCacheFolderPath)
  108. // 创建 exportedMakeFileName 这个文件
  109. // 成功,那么就需要生成这个 exportedMakeFileName 文件
  110. err = ffMPEGInfo.CreateExportedMask()
  111. if err != nil {
  112. return false, nil, err
  113. }
  114. }
  115. return bok, ffMPEGInfo, nil
  116. }
  117. // GetAudioDurationInfo 获取音频的长度信息
  118. func (f *FFMPEGHelper) GetAudioDurationInfo(audioFileFullPath string) (bool, float64, error) {
  119. const args = "-v error -show_format -show_streams -print_format json -f s16le -ac 1 -ar 16000"
  120. cmdArgs := strings.Fields(args)
  121. cmdArgs = append(cmdArgs, audioFileFullPath)
  122. cmd := exec.Command("ffprobe", cmdArgs...)
  123. buf := bytes.NewBufferString("")
  124. //指定输出位置
  125. cmd.Stderr = buf
  126. cmd.Stdout = buf
  127. err := cmd.Start()
  128. if err != nil {
  129. return false, 0, err
  130. }
  131. err = cmd.Wait()
  132. if err != nil {
  133. return false, 0, err
  134. }
  135. bok, duration := f.parseJsonString2GetAudioInfo(buf.String())
  136. if bok == false {
  137. return false, 0, errors.New("ffprobe get " + audioFileFullPath + " duration error")
  138. }
  139. return true, duration, nil
  140. }
  141. // ExportAudioAndSubArgsByTimeRange 根据输入的时间轴导出音频分段信息 "0:1:27" "28.2"
  142. func (f *FFMPEGHelper) ExportAudioAndSubArgsByTimeRange(audioFullPath, subFullPath string, startTimeString, timeLength string) (string, string, string, error) {
  143. outStartTimeString := strings.ReplaceAll(startTimeString, ":", "-")
  144. outStartTimeString = strings.ReplaceAll(outStartTimeString, ".", "#")
  145. outTimeLength := strings.ReplaceAll(timeLength, ".", "#")
  146. frontName := strings.ReplaceAll(filepath.Base(audioFullPath), filepath.Ext(audioFullPath), "")
  147. outAudioName := frontName + "_" + outStartTimeString + "_" + outTimeLength + filepath.Ext(audioFullPath)
  148. outSubName := frontName + "_" + outStartTimeString + "_" + outTimeLength + common.SubExtSRT
  149. var outAudioFullPath = filepath.Join(filepath.Dir(audioFullPath), outAudioName)
  150. var outSubFullPath = filepath.Join(filepath.Dir(audioFullPath), outSubName)
  151. // 导出音频
  152. if my_util.IsFile(outAudioFullPath) == true {
  153. err := os.Remove(outAudioFullPath)
  154. if err != nil {
  155. return "", "", "", err
  156. }
  157. }
  158. args := f.getAudioExportArgsByTimeRange(audioFullPath, startTimeString, timeLength, outAudioFullPath)
  159. execFFMPEG, err := f.execFFMPEG(args)
  160. if err != nil {
  161. return "", "", execFFMPEG, err
  162. }
  163. // 导出字幕
  164. if my_util.IsFile(outSubFullPath) == true {
  165. err := os.Remove(outSubFullPath)
  166. if err != nil {
  167. return "", "", "", err
  168. }
  169. }
  170. args = f.getSubExportArgsByTimeRange(subFullPath, startTimeString, timeLength, outSubFullPath)
  171. execFFMPEG, err = f.execFFMPEG(args)
  172. if err != nil {
  173. return "", "", execFFMPEG, err
  174. }
  175. return outAudioFullPath, outSubFullPath, "", nil
  176. }
  177. // ExportSubArgsByTimeRange 根据输入的时间轴导出字幕分段信息 "0:1:27" "28.2"
  178. func (f *FFMPEGHelper) ExportSubArgsByTimeRange(subFullPath, outName string, startTimeString, timeLength string) (string, string, error) {
  179. outStartTimeString := strings.ReplaceAll(startTimeString, ":", "-")
  180. outStartTimeString = strings.ReplaceAll(outStartTimeString, ".", "#")
  181. outTimeLength := strings.ReplaceAll(timeLength, ".", "#")
  182. frontName := strings.ReplaceAll(filepath.Base(subFullPath), filepath.Ext(subFullPath), "")
  183. outSubName := frontName + "_" + outStartTimeString + "_" + outTimeLength + "_" + outName + common.SubExtSRT
  184. var outSubFullPath = filepath.Join(filepath.Dir(subFullPath), outSubName)
  185. // 导出字幕
  186. if my_util.IsFile(outSubFullPath) == true {
  187. err := os.Remove(outSubFullPath)
  188. if err != nil {
  189. return "", "", err
  190. }
  191. }
  192. args := f.getSubExportArgsByTimeRange(subFullPath, startTimeString, timeLength, outSubFullPath)
  193. execFFMPEG, err := f.execFFMPEG(args)
  194. if err != nil {
  195. return "", execFFMPEG, err
  196. }
  197. return outSubFullPath, "", nil
  198. }
  199. // parseJsonString2GetFFProbeInfo 使用 ffprobe 获取视频的 stream 信息,从中解析出字幕和音频的索引
  200. func (f *FFMPEGHelper) parseJsonString2GetFFProbeInfo(videoFileFullPath, inputFFProbeString string) (bool, *FFMPEGInfo) {
  201. streamsValue := gjson.Get(inputFFProbeString, "streams.#")
  202. if streamsValue.Exists() == false {
  203. return false, nil
  204. }
  205. ffmpegInfo := NewFFMPEGInfo(videoFileFullPath)
  206. // 进行字幕和音频的缓存,优先当然是导出 中、英、日、韩 相关的字幕和音频
  207. // 但是如果都没得这些的时候,那么也需要导出至少一个字幕或者音频,用于字幕的校正
  208. cacheAudios := make([]AudioInfo, 0)
  209. cacheSubtitleInfos := make([]SubtitleInfo, 0)
  210. for i := 0; i < int(streamsValue.Num); i++ {
  211. oneIndex := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.index", i))
  212. oneCodecName := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.codec_name", i))
  213. oneCodecType := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.codec_type", i))
  214. oneTimeBase := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.time_base", i))
  215. oneStartTime := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.start_time", i))
  216. oneLanguage := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.tags.language", i))
  217. // 任意一个字段不存在则跳过
  218. if oneIndex.Exists() == false {
  219. continue
  220. }
  221. if oneCodecName.Exists() == false {
  222. continue
  223. }
  224. if oneCodecType.Exists() == false {
  225. continue
  226. }
  227. if oneTimeBase.Exists() == false {
  228. continue
  229. }
  230. if oneStartTime.Exists() == false {
  231. continue
  232. }
  233. // 这里需要区分是字幕还是音频
  234. if oneCodecType.String() == codecTypeSub {
  235. // 字幕
  236. // 只解析 subrip 类型的,不支持 hdmv_pgs_subtitle 的字幕导出
  237. if f.isSupportSubCodecName(oneCodecName.String()) == false {
  238. continue
  239. }
  240. // 这里非必须解析到 language 字段,把所有的都导出来,然后通过额外字幕语言判断即可
  241. oneDurationTS := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.duration_ts", i))
  242. oneDuration := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.duration", i))
  243. // 必须存在的
  244. if oneDurationTS.Exists() == false {
  245. continue
  246. }
  247. if oneDuration.Exists() == false {
  248. continue
  249. }
  250. // 非必须存在的
  251. nowLanguageString := ""
  252. if oneLanguage.Exists() == true {
  253. nowLanguageString = oneLanguage.String()
  254. // 只导出 中、英、日、韩
  255. if language.IsSupportISOString(nowLanguageString) == false {
  256. subInfo := NewSubtitleInfo(int(oneIndex.Num), oneCodecName.String(), oneCodecType.String(),
  257. oneTimeBase.String(), oneStartTime.String(),
  258. int(oneDurationTS.Num), oneDuration.String(), nowLanguageString)
  259. // 不符合的也存在下来,万一,符合要求的一个都没得的时候,就需要从里面挑几个出来了
  260. cacheSubtitleInfos = append(cacheSubtitleInfos, *subInfo)
  261. continue
  262. }
  263. }
  264. subInfo := NewSubtitleInfo(int(oneIndex.Num), oneCodecName.String(), oneCodecType.String(),
  265. oneTimeBase.String(), oneStartTime.String(),
  266. int(oneDurationTS.Num), oneDuration.String(), nowLanguageString)
  267. ffmpegInfo.SubtitleInfoList = append(ffmpegInfo.SubtitleInfoList, *subInfo)
  268. } else if oneCodecType.String() == codecTypeAudio {
  269. // 音频
  270. // 这里必要要能够解析到 language 字段
  271. if oneLanguage.Exists() == false {
  272. // 不符合的也存在下来,万一,符合要求的一个都没得的时候,就需要从里面挑几个出来了
  273. audioInfo := NewAudioInfo(int(oneIndex.Num), oneCodecName.String(), oneCodecType.String(),
  274. oneTimeBase.String(), oneStartTime.String(), oneLanguage.String())
  275. cacheAudios = append(cacheAudios, *audioInfo)
  276. continue
  277. }
  278. // 只导出 中、英、日、韩
  279. if language.IsSupportISOString(oneLanguage.String()) == false {
  280. // 不符合的也存在下来,万一,符合要求的一个都没得的时候,就需要从里面挑几个出来了
  281. audioInfo := NewAudioInfo(int(oneIndex.Num), oneCodecName.String(), oneCodecType.String(),
  282. oneTimeBase.String(), oneStartTime.String(), oneLanguage.String())
  283. cacheAudios = append(cacheAudios, *audioInfo)
  284. continue
  285. }
  286. audioInfo := NewAudioInfo(int(oneIndex.Num), oneCodecName.String(), oneCodecType.String(),
  287. oneTimeBase.String(), oneStartTime.String(), oneLanguage.String())
  288. ffmpegInfo.AudioInfoList = append(ffmpegInfo.AudioInfoList, *audioInfo)
  289. } else {
  290. continue
  291. }
  292. }
  293. // 如何没有找到合适的字幕,那么就要把缓存的字幕选一个填充进去
  294. if len(ffmpegInfo.SubtitleInfoList) == 0 {
  295. if len(cacheSubtitleInfos) != 0 {
  296. ffmpegInfo.SubtitleInfoList = append(ffmpegInfo.SubtitleInfoList, cacheSubtitleInfos[0])
  297. }
  298. }
  299. // 如何没有找到合适的音频,那么就要把缓存的音频选一个填充进去
  300. if len(ffmpegInfo.AudioInfoList) == 0 {
  301. if len(cacheAudios) != 0 {
  302. ffmpegInfo.AudioInfoList = append(ffmpegInfo.AudioInfoList, cacheAudios[0])
  303. }
  304. } else {
  305. // 音频只需要导出一个就行了,取第一个
  306. newAudioList := make([]AudioInfo, 0)
  307. newAudioList = append(newAudioList, ffmpegInfo.AudioInfoList[0])
  308. ffmpegInfo.AudioInfoList = newAudioList
  309. }
  310. return true, ffmpegInfo
  311. }
  312. // parseJsonString2GetAudioInfo 获取 pcm 音频的长度
  313. func (f *FFMPEGHelper) parseJsonString2GetAudioInfo(inputFFProbeString string) (bool, float64) {
  314. durationValue := gjson.Get(inputFFProbeString, "format.duration")
  315. if durationValue.Exists() == false {
  316. return false, 0
  317. }
  318. return true, durationValue.Float()
  319. }
  320. // exportAudioAndSubtitles 导出音频和字幕文件
  321. func (f *FFMPEGHelper) exportAudioAndSubtitles(audioArgs, subArgs []string, exportType ExportType) (string, error) {
  322. // 这里导出依赖的是 ffmpeg 这个程序,需要的是构建导出的语句
  323. if exportType == SubtitleAndAudio {
  324. execErrorString, err := f.execFFMPEG(audioArgs)
  325. if err != nil {
  326. return execErrorString, err
  327. }
  328. execErrorString, err = f.execFFMPEG(subArgs)
  329. if err != nil {
  330. return execErrorString, err
  331. }
  332. } else if exportType == Audio {
  333. execErrorString, err := f.execFFMPEG(audioArgs)
  334. if err != nil {
  335. return execErrorString, err
  336. }
  337. } else if exportType == Subtitle {
  338. execErrorString, err := f.execFFMPEG(subArgs)
  339. if err != nil {
  340. return execErrorString, err
  341. }
  342. } else {
  343. return "", errors.New("FFMPEGHelper ExportType not support")
  344. }
  345. return "", nil
  346. }
  347. // execFFMPEG 执行 ffmpeg 命令
  348. func (f *FFMPEGHelper) execFFMPEG(cmds []string) (string, error) {
  349. cmd := exec.Command("ffmpeg", cmds...)
  350. buf := bytes.NewBufferString("")
  351. //指定输出位置
  352. cmd.Stderr = buf
  353. cmd.Stdout = buf
  354. err := cmd.Start()
  355. if err != nil {
  356. return buf.String(), err
  357. }
  358. err = cmd.Wait()
  359. if err != nil {
  360. return buf.String(), err
  361. }
  362. return "", nil
  363. }
  364. // getAudioAndSubExportArgs 构建从原始视频导出字幕、音频的 ffmpeg 的参数 audioArgs, subArgs
  365. func (f *FFMPEGHelper) getAudioAndSubExportArgs(videoFileFullPath string, ffmpegInfo *FFMPEGInfo) ([]string, []string) {
  366. /*
  367. 导出多个字幕
  368. ffmpeg.exe -i xx.mp4 -vn -an -map 0:7 subs-7.srt -map 0:6 subs-6.srt
  369. 导出音频,从 1m 27s 开始,导出向后的 28 s,转换为 mp3 格式
  370. ffmpeg.exe -i xx.mp4 -vn -map 0:1 -ss 00:1:27 -f mp3 -t 28 audio.mp3
  371. 导出音频,转换为 mp3 格式
  372. ffmpeg.exe -i xx.mp4 -vn -map 0:1 -f mp3 audio.mp3
  373. 导出音频,转换为 16000k 16bit 单通道 采样率的 test.pcm
  374. ffmpeg.exe -i xx.mp4 -vn -map 0:1 -ss 00:1:27 -t 28 -acodec pcm_s16le -f s16le -ac 1 -ar 16000 test.pcm
  375. 截取字幕的时间片段
  376. ffmpeg.exe -i "subs-3.srt" -ss 00:1:27 -t 28 subs-3-cut-from-org.srt
  377. */
  378. var subArgs = make([]string, 0)
  379. var audioArgs = make([]string, 0)
  380. // 基础的输入视频参数
  381. subArgs = append(subArgs, "-i")
  382. audioArgs = append(audioArgs, "-i")
  383. subArgs = append(subArgs, videoFileFullPath)
  384. audioArgs = append(audioArgs, videoFileFullPath)
  385. // 字幕导出的参数构建
  386. subArgs = append(subArgs, "-vn") // 不输出视频流
  387. subArgs = append(subArgs, "-an") // 不输出音频流
  388. nowCacheFolderPath, err := ffmpegInfo.GetCacheFolderFPath()
  389. if err != nil {
  390. log_helper.GetLogger().Errorln("getAudioAndSubExportArgs", videoFileFullPath, err.Error())
  391. return nil, nil
  392. }
  393. for _, subtitleInfo := range ffmpegInfo.SubtitleInfoList {
  394. f.addSubMapArg(&subArgs, subtitleInfo.Index,
  395. filepath.Join(nowCacheFolderPath, subtitleInfo.GetName()+common.SubExtSRT))
  396. f.addSubMapArg(&subArgs, subtitleInfo.Index,
  397. filepath.Join(nowCacheFolderPath, subtitleInfo.GetName()+common.SubExtASS))
  398. }
  399. // 音频导出的参数构建
  400. audioArgs = append(audioArgs, "-vn")
  401. for _, audioInfo := range ffmpegInfo.AudioInfoList {
  402. f.addAudioMapArg(&audioArgs, audioInfo.Index,
  403. filepath.Join(nowCacheFolderPath, audioInfo.GetName()+extPCM))
  404. }
  405. return audioArgs, subArgs
  406. }
  407. // getAudioAndSubExportArgsByTimeRange 导出某个时间范围内的音频和字幕文件文件 startTimeString 00:1:27 timeLeng 向后多少秒
  408. func (f *FFMPEGHelper) getAudioExportArgsByTimeRange(audioFullPath string, startTimeString, timeLeng, outAudioFullPath string) []string {
  409. /*
  410. ffmpeg.exe -ar 16000 -ac 1 -f s16le -i aa.pcm -ss 00:1:27 -t 28 -acodec pcm_s16le -f s16le -ac 1 -ar 16000 bb.pcm
  411. ffmpeg.exe -i aa.srt -ss 00:1:27 -t 28 bb.srt
  412. */
  413. var audioArgs = make([]string, 0)
  414. // 指定读取的音频文件编码格式
  415. audioArgs = append(audioArgs, "-ar")
  416. audioArgs = append(audioArgs, "16000")
  417. audioArgs = append(audioArgs, "-ac")
  418. audioArgs = append(audioArgs, "1")
  419. audioArgs = append(audioArgs, "-f")
  420. audioArgs = append(audioArgs, "s16le")
  421. audioArgs = append(audioArgs, "-i")
  422. audioArgs = append(audioArgs, audioFullPath)
  423. audioArgs = append(audioArgs, "-ss")
  424. audioArgs = append(audioArgs, startTimeString)
  425. audioArgs = append(audioArgs, "-t")
  426. audioArgs = append(audioArgs, timeLeng)
  427. // 指定导出的音频文件编码格式
  428. audioArgs = append(audioArgs, "-acodec")
  429. audioArgs = append(audioArgs, "pcm_s16le")
  430. audioArgs = append(audioArgs, "-f")
  431. audioArgs = append(audioArgs, "s16le")
  432. audioArgs = append(audioArgs, "-ac")
  433. audioArgs = append(audioArgs, "1")
  434. audioArgs = append(audioArgs, "-ar")
  435. audioArgs = append(audioArgs, "16000")
  436. audioArgs = append(audioArgs, outAudioFullPath)
  437. return audioArgs
  438. }
  439. func (f *FFMPEGHelper) getSubExportArgsByTimeRange(subFullPath string, startTimeString, timeLength, outSubFullPath string) []string {
  440. /*
  441. ffmpeg.exe -i aa.srt -ss 00:1:27 -t 28 bb.srt
  442. */
  443. var subArgs = make([]string, 0)
  444. subArgs = append(subArgs, "-i")
  445. subArgs = append(subArgs, subFullPath)
  446. subArgs = append(subArgs, "-ss")
  447. subArgs = append(subArgs, startTimeString)
  448. subArgs = append(subArgs, "-t")
  449. subArgs = append(subArgs, timeLength)
  450. subArgs = append(subArgs, outSubFullPath)
  451. return subArgs
  452. }
  453. // addSubMapArg 构建字幕的导出参数
  454. func (f *FFMPEGHelper) addSubMapArg(subArgs *[]string, index int, subSaveFullPath string) {
  455. *subArgs = append(*subArgs, "-map")
  456. *subArgs = append(*subArgs, fmt.Sprintf("0:%d", index))
  457. *subArgs = append(*subArgs, subSaveFullPath)
  458. }
  459. // addAudioMapArg 构建音频的导出参数
  460. func (f *FFMPEGHelper) addAudioMapArg(subArgs *[]string, index int, audioSaveFullPath string) {
  461. // -acodec pcm_s16le -f s16le -ac 1 -ar 16000
  462. *subArgs = append(*subArgs, "-map")
  463. *subArgs = append(*subArgs, fmt.Sprintf("0:%d", index))
  464. *subArgs = append(*subArgs, "-acodec")
  465. *subArgs = append(*subArgs, "pcm_s16le")
  466. *subArgs = append(*subArgs, "-f")
  467. *subArgs = append(*subArgs, "s16le")
  468. *subArgs = append(*subArgs, "-ac")
  469. *subArgs = append(*subArgs, "1")
  470. *subArgs = append(*subArgs, "-ar")
  471. *subArgs = append(*subArgs, "16000")
  472. *subArgs = append(*subArgs, audioSaveFullPath)
  473. }
  474. func (f FFMPEGHelper) getVersion(exeName string) (string, error) {
  475. const args = "-version"
  476. cmdArgs := strings.Fields(args)
  477. cmd := exec.Command(exeName, cmdArgs...)
  478. buf := bytes.NewBufferString("")
  479. //指定输出位置
  480. cmd.Stderr = buf
  481. cmd.Stdout = buf
  482. err := cmd.Start()
  483. if err != nil {
  484. return "", err
  485. }
  486. err = cmd.Wait()
  487. if err != nil {
  488. return "", err
  489. }
  490. return buf.String(), nil
  491. }
  492. // isSupportSubCodecName 是否是 FFMPEG 支持的 CodecName
  493. func (f FFMPEGHelper) isSupportSubCodecName(name string) bool {
  494. switch name {
  495. case Subtitle_StreamCodec_subrip,
  496. Subtitle_StreamCodec_ass,
  497. Subtitle_StreamCodec_ssa,
  498. Subtitle_StreamCodec_srt:
  499. return true
  500. default:
  501. return false
  502. }
  503. }
  504. const (
  505. codecTypeSub = "subtitle"
  506. codecTypeAudio = "audio"
  507. extMP3 = ".mp3"
  508. extPCM = ".pcm"
  509. )
  510. type ExportType int
  511. const (
  512. Subtitle ExportType = iota // 导出字幕
  513. Audio // 导出音频
  514. SubtitleAndAudio // 导出字幕和音频
  515. )
  516. /*
  517. FFMPEG 支持的字幕 Codec Name:
  518. ..S... arib_caption ARIB STD-B24 caption
  519. DES... ass ASS (Advanced SSA) subtitle (decoders: ssa ass ) (encoders: ssa ass )
  520. DES... dvb_subtitle DVB subtitles (decoders: dvbsub ) (encoders: dvbsub )
  521. ..S... dvb_teletext DVB teletext
  522. DES... dvd_subtitle DVD subtitles (decoders: dvdsub ) (encoders: dvdsub )
  523. D.S... eia_608 EIA-608 closed captions (decoders: cc_dec )
  524. D.S... hdmv_pgs_subtitle HDMV Presentation Graphic Stream subtitles (decoders: pgssub )
  525. ..S... hdmv_text_subtitle HDMV Text subtitle
  526. D.S... jacosub JACOsub subtitle
  527. D.S... microdvd MicroDVD subtitle
  528. DES... mov_text MOV text
  529. D.S... mpl2 MPL2 subtitle
  530. D.S... pjs PJS (Phoenix Japanimation Society) subtitle
  531. D.S... realtext RealText subtitle
  532. D.S... sami SAMI subtitle
  533. ..S... srt SubRip subtitle with embedded timing
  534. ..S... ssa SSA (SubStation Alpha) subtitle
  535. D.S... stl Spruce subtitle format
  536. DES... subrip SubRip subtitle (decoders: srt subrip ) (encoders: srt subrip )
  537. D.S... subviewer SubViewer subtitle
  538. D.S... subviewer1 SubViewer v1 subtitle
  539. DES... text raw UTF-8 text
  540. ..S... ttml Timed Text Markup Language
  541. D.S... vplayer VPlayer subtitle
  542. DES... webvtt WebVTT subtitle
  543. DES... xsub XSUB
  544. */
  545. const (
  546. Subtitle_StreamCodec_subrip = "subrip"
  547. Subtitle_StreamCodec_ass = "ass"
  548. Subtitle_StreamCodec_ssa = "ssa"
  549. Subtitle_StreamCodec_srt = "srt"
  550. )