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