ffmpeg_helper.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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. // GetFFMPEGInfo 获取 视频的 FFMPEG 信息,包含音频和字幕
  28. // 优先会导出 中、英、日、韩 类型的,字幕如果没有语言类型,则也导出,然后需要额外的字幕语言的判断去辅助标记(读取文件内容)
  29. func (f *FFMPEGHelper) GetFFMPEGInfo(videoFileFullPath string) (bool, *FFMPEGInfo, error) {
  30. const args = "-v error -show_format -show_streams -print_format json"
  31. cmdArgs := strings.Fields(args)
  32. cmdArgs = append(cmdArgs, videoFileFullPath)
  33. cmd := exec.Command("ffprobe", cmdArgs...)
  34. buf := bytes.NewBufferString("")
  35. //指定输出位置
  36. cmd.Stderr = buf
  37. cmd.Stdout = buf
  38. err := cmd.Start()
  39. if err != nil {
  40. return false, nil, err
  41. }
  42. err = cmd.Wait()
  43. if err != nil {
  44. return false, nil, err
  45. }
  46. // 解析得到的字符串反馈
  47. bok, ffMPEGInfo := f.parseJsonString2GetFFProbeInfo(videoFileFullPath, buf.String())
  48. if bok == false {
  49. return false, nil, nil
  50. }
  51. // 在函数调用完毕后,判断是否需要清理
  52. defer func() {
  53. if bok == false && ffMPEGInfo != nil {
  54. err := os.RemoveAll(ffMPEGInfo.GetCacheFolderFPath())
  55. if err != nil {
  56. log_helper.GetLogger().Errorln("GetFFMPEGInfo - RemoveAll", err.Error())
  57. return
  58. }
  59. }
  60. }()
  61. // 判断这个视频是否已经导出过内置的字幕和音频文件了
  62. if ffMPEGInfo.IsExported() == false {
  63. // 说明缓存不存在,需要导出,这里需要注意,如果导出失败了,这个文件夹要清理掉
  64. if my_util.IsDir(ffMPEGInfo.GetCacheFolderFPath()) == true {
  65. // 如果存在则,先清空一个这个文件夹
  66. err = my_util.ClearFolder(ffMPEGInfo.GetCacheFolderFPath())
  67. if err != nil {
  68. bok = false
  69. return bok, nil, err
  70. }
  71. } else {
  72. // 如果不存在则,创建文件夹
  73. err = os.MkdirAll(ffMPEGInfo.GetCacheFolderFPath(), os.ModePerm)
  74. if err != nil {
  75. bok = false
  76. return bok, nil, err
  77. }
  78. }
  79. // 开始导出
  80. // 构建导出的命令参数
  81. subArgs, audioArgs := f.getAudioAndSubExportArgs(videoFileFullPath, ffMPEGInfo)
  82. // 执行导出
  83. execErrorString, err := f.exportAudioAndSubtitles(subArgs, audioArgs)
  84. if err != nil {
  85. log_helper.GetLogger().Errorln("exportAudioAndSubtitles", execErrorString)
  86. bok = false
  87. return bok, nil, err
  88. }
  89. }
  90. // 查找当前这个视频外置字幕列表
  91. err = ffMPEGInfo.GetExternalSubInfos(f.SubParserHub)
  92. if err != nil {
  93. return false, nil, err
  94. }
  95. return bok, ffMPEGInfo, nil
  96. }
  97. func (f *FFMPEGHelper) GetAudioInfo(audioFileFullPath string) (bool, float64, error) {
  98. const args = "-v error -show_format -show_streams -print_format json -f s16le -ac 1 -ar 16000"
  99. cmdArgs := strings.Fields(args)
  100. cmdArgs = append(cmdArgs, audioFileFullPath)
  101. cmd := exec.Command("ffprobe", cmdArgs...)
  102. buf := bytes.NewBufferString("")
  103. //指定输出位置
  104. cmd.Stderr = buf
  105. cmd.Stdout = buf
  106. err := cmd.Start()
  107. if err != nil {
  108. return false, 0, err
  109. }
  110. err = cmd.Wait()
  111. if err != nil {
  112. return false, 0, err
  113. }
  114. bok, duration := f.parseJsonString2GetAudioInfo(buf.String())
  115. if bok == false {
  116. return false, 0, errors.New("ffprobe get " + audioFileFullPath + " duration error")
  117. }
  118. return true, duration, nil
  119. }
  120. // ExportAudioAndSubArgsByTimeRange 根据输入的时间轴导出音频分段信息 "0:1:27" "28.2"
  121. func (f *FFMPEGHelper) ExportAudioAndSubArgsByTimeRange(audioFullPath, subFullPath string, startTimeString, timeLength string) (string, string, string, error) {
  122. outStartTimeString := strings.ReplaceAll(startTimeString, ":", "-")
  123. outStartTimeString = strings.ReplaceAll(outStartTimeString, ".", "#")
  124. outTimeLength := strings.ReplaceAll(timeLength, ".", "#")
  125. frontName := strings.ReplaceAll(filepath.Base(audioFullPath), filepath.Ext(audioFullPath), "")
  126. outAudioName := frontName + "_" + outStartTimeString + "_" + outTimeLength + filepath.Ext(audioFullPath)
  127. outSubName := frontName + "_" + outStartTimeString + "_" + outTimeLength + common.SubExtSRT
  128. var outAudioFullPath = filepath.Join(filepath.Dir(audioFullPath), outAudioName)
  129. var outSubFullPath = filepath.Join(filepath.Dir(audioFullPath), outSubName)
  130. // 导出音频
  131. if my_util.IsFile(outAudioFullPath) == true {
  132. err := os.Remove(outAudioFullPath)
  133. if err != nil {
  134. return "", "", "", err
  135. }
  136. }
  137. args := f.getAudioExportArgsByTimeRange(audioFullPath, startTimeString, timeLength, outAudioFullPath)
  138. execFFMPEG, err := f.execFFMPEG(args)
  139. if err != nil {
  140. return "", "", execFFMPEG, err
  141. }
  142. // 导出字幕
  143. if my_util.IsFile(outSubFullPath) == true {
  144. err := os.Remove(outSubFullPath)
  145. if err != nil {
  146. return "", "", "", err
  147. }
  148. }
  149. args = f.getSubExportArgsByTimeRange(subFullPath, startTimeString, timeLength, outSubFullPath)
  150. execFFMPEG, err = f.execFFMPEG(args)
  151. if err != nil {
  152. return "", "", execFFMPEG, err
  153. }
  154. return outAudioFullPath, outSubFullPath, "", nil
  155. }
  156. // ExportSubArgsByTimeRange 根据输入的时间轴导出字幕分段信息 "0:1:27" "28.2"
  157. func (f *FFMPEGHelper) ExportSubArgsByTimeRange(subFullPath string, startTimeString, timeLength string) (string, string, error) {
  158. outStartTimeString := strings.ReplaceAll(startTimeString, ":", "-")
  159. outStartTimeString = strings.ReplaceAll(outStartTimeString, ".", "#")
  160. outTimeLength := strings.ReplaceAll(timeLength, ".", "#")
  161. frontName := strings.ReplaceAll(filepath.Base(subFullPath), filepath.Ext(subFullPath), "")
  162. outSubName := frontName + "_" + outStartTimeString + "_" + outTimeLength + common.SubExtSRT
  163. var outSubFullPath = filepath.Join(filepath.Dir(subFullPath), outSubName)
  164. // 导出字幕
  165. if my_util.IsFile(outSubFullPath) == true {
  166. err := os.Remove(outSubFullPath)
  167. if err != nil {
  168. return "", "", err
  169. }
  170. }
  171. args := f.getSubExportArgsByTimeRange(subFullPath, startTimeString, timeLength, outSubFullPath)
  172. execFFMPEG, err := f.execFFMPEG(args)
  173. if err != nil {
  174. return "", execFFMPEG, err
  175. }
  176. return outSubFullPath, "", nil
  177. }
  178. // parseJsonString2GetFFProbeInfo 使用 ffprobe 获取视频的 stream 信息,从中解析出字幕和音频的索引
  179. func (f *FFMPEGHelper) parseJsonString2GetFFProbeInfo(videoFileFullPath, inputFFProbeString string) (bool, *FFMPEGInfo) {
  180. streamsValue := gjson.Get(inputFFProbeString, "streams.#")
  181. if streamsValue.Exists() == false {
  182. return false, nil
  183. }
  184. ffmpegInfo := NewFFMPEGInfo(videoFileFullPath)
  185. for i := 0; i < int(streamsValue.Num); i++ {
  186. oneIndex := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.index", i))
  187. oneCodecName := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.codec_name", i))
  188. oneCodecType := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.codec_type", i))
  189. oneTimeBase := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.time_base", i))
  190. oneStartTime := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.start_time", i))
  191. oneLanguage := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.tags.language", i))
  192. // 任意一个字段不存在则跳过
  193. if oneIndex.Exists() == false {
  194. continue
  195. }
  196. if oneCodecName.Exists() == false {
  197. continue
  198. }
  199. if oneCodecType.Exists() == false {
  200. continue
  201. }
  202. if oneTimeBase.Exists() == false {
  203. continue
  204. }
  205. if oneStartTime.Exists() == false {
  206. continue
  207. }
  208. // 这里需要区分是字幕还是音频
  209. if oneCodecType.String() == codecTypeSub {
  210. // 字幕
  211. // 这里非必须解析到 language 字段,把所有的都导出来,然后通过额外字幕语言判断即可
  212. oneDurationTS := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.duration_ts", i))
  213. oneDuration := gjson.Get(inputFFProbeString, fmt.Sprintf("streams.%d.duration", i))
  214. // 必须存在的
  215. if oneDurationTS.Exists() == false {
  216. continue
  217. }
  218. if oneDuration.Exists() == false {
  219. continue
  220. }
  221. // 非必须存在的
  222. nowLanguageString := ""
  223. if oneLanguage.Exists() == true {
  224. nowLanguageString = oneLanguage.String()
  225. // 只导出 中、英、日、韩
  226. if language.IsSupportISOString(nowLanguageString) == false {
  227. continue
  228. }
  229. }
  230. subInfo := NewSubtitleInfo(int(oneIndex.Num), oneCodecName.String(), oneCodecType.String(),
  231. oneTimeBase.String(), oneStartTime.String(),
  232. int(oneDurationTS.Num), oneDuration.String(), nowLanguageString)
  233. ffmpegInfo.SubtitleInfoList = append(ffmpegInfo.SubtitleInfoList, *subInfo)
  234. } else if oneCodecType.String() == codecTypeAudio {
  235. // 音频
  236. // 这里必要要能够解析到 language 字段
  237. if oneLanguage.Exists() == false {
  238. continue
  239. }
  240. // 只导出 中、英、日、韩
  241. if language.IsSupportISOString(oneLanguage.String()) == false {
  242. continue
  243. }
  244. audioInfo := NewAudioInfo(int(oneIndex.Num), oneCodecName.String(), oneCodecType.String(),
  245. oneTimeBase.String(), oneStartTime.String(), oneLanguage.String())
  246. ffmpegInfo.AudioInfoList = append(ffmpegInfo.AudioInfoList, *audioInfo)
  247. } else {
  248. continue
  249. }
  250. }
  251. return true, ffmpegInfo
  252. }
  253. // parseJsonString2GetAudioInfo 获取 pcm 音频的长度
  254. func (f *FFMPEGHelper) parseJsonString2GetAudioInfo(inputFFProbeString string) (bool, float64) {
  255. durationValue := gjson.Get(inputFFProbeString, "format.duration")
  256. if durationValue.Exists() == false {
  257. return false, 0
  258. }
  259. return true, durationValue.Float()
  260. }
  261. // exportAudioAndSubtitles 导出音频和字幕文件
  262. func (f *FFMPEGHelper) exportAudioAndSubtitles(subArgs, audioArgs []string) (string, error) {
  263. // 这里导出依赖的是 ffmpeg 这个程序,需要的是构建导出的语句
  264. execErrorString, err := f.execFFMPEG(subArgs)
  265. if err != nil {
  266. return execErrorString, err
  267. }
  268. execErrorString, err = f.execFFMPEG(audioArgs)
  269. if err != nil {
  270. return execErrorString, err
  271. }
  272. return "", nil
  273. }
  274. // execFFMPEG 执行 ffmpeg 命令
  275. func (f *FFMPEGHelper) execFFMPEG(cmds []string) (string, error) {
  276. cmd := exec.Command("ffmpeg", cmds...)
  277. buf := bytes.NewBufferString("")
  278. //指定输出位置
  279. cmd.Stderr = buf
  280. cmd.Stdout = buf
  281. err := cmd.Start()
  282. if err != nil {
  283. return buf.String(), err
  284. }
  285. err = cmd.Wait()
  286. if err != nil {
  287. return buf.String(), err
  288. }
  289. return "", nil
  290. }
  291. // getAudioAndSubExportArgs 构建从原始视频导出字幕、音频的 ffmpeg 的参数
  292. func (f *FFMPEGHelper) getAudioAndSubExportArgs(videoFileFullPath string, ffmpegInfo *FFMPEGInfo) ([]string, []string) {
  293. /*
  294. 导出多个字幕
  295. ffmpeg.exe -i xx.mp4 -vn -an -map 0:7 subs-7.srt -map 0:6 subs-6.srt
  296. 导出音频,从 1m 27s 开始,导出向后的 28 s,转换为 mp3 格式
  297. ffmpeg.exe -i xx.mp4 -vn -map 0:1 -ss 00:1:27 -f mp3 -t 28 audio.mp3
  298. 导出音频,转换为 mp3 格式
  299. ffmpeg.exe -i xx.mp4 -vn -map 0:1 -f mp3 audio.mp3
  300. 导出音频,转换为 16000k 16bit 单通道 采样率的 test.pcm
  301. 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
  302. 截取字幕的时间片段
  303. ffmpeg.exe -i "subs-3.srt" -ss 00:1:27 -t 28 subs-3-cut-from-org.srt
  304. */
  305. var subArgs = make([]string, 0)
  306. var audioArgs = make([]string, 0)
  307. // 基础的输入视频参数
  308. subArgs = append(subArgs, "-i")
  309. audioArgs = append(audioArgs, "-i")
  310. subArgs = append(subArgs, videoFileFullPath)
  311. audioArgs = append(audioArgs, videoFileFullPath)
  312. // 字幕导出的参数构建
  313. subArgs = append(subArgs, "-vn") // 不输出视频流
  314. subArgs = append(subArgs, "-an") // 不输出音频流
  315. for _, subtitleInfo := range ffmpegInfo.SubtitleInfoList {
  316. f.addSubMapArg(&subArgs, subtitleInfo.Index,
  317. filepath.Join(ffmpegInfo.GetCacheFolderFPath(), subtitleInfo.GetName()+common.SubExtSRT))
  318. f.addSubMapArg(&subArgs, subtitleInfo.Index,
  319. filepath.Join(ffmpegInfo.GetCacheFolderFPath(), subtitleInfo.GetName()+common.SubExtASS))
  320. }
  321. // 音频导出的参数构建
  322. audioArgs = append(audioArgs, "-vn")
  323. for _, audioInfo := range ffmpegInfo.AudioInfoList {
  324. f.addAudioMapArg(&audioArgs, audioInfo.Index,
  325. filepath.Join(ffmpegInfo.GetCacheFolderFPath(), audioInfo.GetName()+extPCM))
  326. }
  327. return audioArgs, subArgs
  328. }
  329. // getAudioAndSubExportArgsByTimeRange 导出某个时间范围内的音频和字幕文件文件 startTimeString 00:1:27 timeLeng 向后多少秒
  330. func (f *FFMPEGHelper) getAudioExportArgsByTimeRange(audioFullPath string, startTimeString, timeLeng, outAudioFullPath string) []string {
  331. /*
  332. 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
  333. ffmpeg.exe -i aa.srt -ss 00:1:27 -t 28 bb.srt
  334. */
  335. var audioArgs = make([]string, 0)
  336. // 指定读取的音频文件编码格式
  337. audioArgs = append(audioArgs, "-ar")
  338. audioArgs = append(audioArgs, "16000")
  339. audioArgs = append(audioArgs, "-ac")
  340. audioArgs = append(audioArgs, "1")
  341. audioArgs = append(audioArgs, "-f")
  342. audioArgs = append(audioArgs, "s16le")
  343. audioArgs = append(audioArgs, "-i")
  344. audioArgs = append(audioArgs, audioFullPath)
  345. audioArgs = append(audioArgs, "-ss")
  346. audioArgs = append(audioArgs, startTimeString)
  347. audioArgs = append(audioArgs, "-t")
  348. audioArgs = append(audioArgs, timeLeng)
  349. // 指定导出的音频文件编码格式
  350. audioArgs = append(audioArgs, "-acodec")
  351. audioArgs = append(audioArgs, "pcm_s16le")
  352. audioArgs = append(audioArgs, "-f")
  353. audioArgs = append(audioArgs, "s16le")
  354. audioArgs = append(audioArgs, "-ac")
  355. audioArgs = append(audioArgs, "1")
  356. audioArgs = append(audioArgs, "-ar")
  357. audioArgs = append(audioArgs, "16000")
  358. audioArgs = append(audioArgs, outAudioFullPath)
  359. return audioArgs
  360. }
  361. func (f *FFMPEGHelper) getSubExportArgsByTimeRange(subFullPath string, startTimeString, timeLength, outSubFullPath string) []string {
  362. /*
  363. ffmpeg.exe -i aa.srt -ss 00:1:27 -t 28 bb.srt
  364. */
  365. var subArgs = make([]string, 0)
  366. subArgs = append(subArgs, "-i")
  367. subArgs = append(subArgs, subFullPath)
  368. subArgs = append(subArgs, "-ss")
  369. subArgs = append(subArgs, startTimeString)
  370. subArgs = append(subArgs, "-t")
  371. subArgs = append(subArgs, timeLength)
  372. subArgs = append(subArgs, outSubFullPath)
  373. return subArgs
  374. }
  375. // addSubMapArg 构建字幕的导出参数
  376. func (f *FFMPEGHelper) addSubMapArg(subArgs *[]string, index int, subSaveFullPath string) {
  377. *subArgs = append(*subArgs, "-map")
  378. *subArgs = append(*subArgs, fmt.Sprintf("0:%d", index))
  379. *subArgs = append(*subArgs, subSaveFullPath)
  380. }
  381. // addAudioMapArg 构建音频的导出参数
  382. func (f *FFMPEGHelper) addAudioMapArg(subArgs *[]string, index int, audioSaveFullPath string) {
  383. // -acodec pcm_s16le -f s16le -ac 1 -ar 16000
  384. *subArgs = append(*subArgs, "-map")
  385. *subArgs = append(*subArgs, fmt.Sprintf("0:%d", index))
  386. *subArgs = append(*subArgs, "-acodec")
  387. *subArgs = append(*subArgs, "pcm_s16le")
  388. *subArgs = append(*subArgs, "-f")
  389. *subArgs = append(*subArgs, "s16le")
  390. *subArgs = append(*subArgs, "-ac")
  391. *subArgs = append(*subArgs, "1")
  392. *subArgs = append(*subArgs, "-ar")
  393. *subArgs = append(*subArgs, "16000")
  394. *subArgs = append(*subArgs, audioSaveFullPath)
  395. }
  396. const (
  397. codecTypeSub = "subtitle"
  398. codecTypeAudio = "audio"
  399. extMP3 = ".mp3"
  400. extPCM = ".pcm"
  401. )