使用第三方框架 go-astiav 绑定的ffmpeg 5.1
ffmpeg -version
ffmpeg version 5.1.3 Copyright (c) 2000-2022 the FFmpeg developers
built with Apple clang version 14.0.0 (clang-1400.0.29.201)
configuration: --prefix=/usr/local/ffmpeg --enable-gpl --enable-version3 --enable-nonfree --enable-postproc --disable-libass --enable-libfdk-aac --enable-libfreetype --enable-libopenjpeg --enable-libopus --enable-libspeex --enable-libvorbis --enable-libvpx --enable-libx264 --enable-static --enable-shared --enable-debug=3 --enable-ffplay --enable-libvpx --enable-encoder=libvpx_vp8 --enable-encoder=libvpx_vp9 --enable-decoder=vp8 --enable-decoder=vp9 --enable-parser=vp8 --enable-parser=vp9
libavutil 57. 28.100 / 57. 28.100
libavcodec 59. 37.100 / 59. 37.100
libavformat 59. 27.100 / 59. 27.100
libavdevice 59. 7.100 / 59. 7.100
libavfilter 8. 44.100 / 8. 44.100
libswscale 6. 7.100 / 6. 7.100
libswresample 4. 7.100 / 4. 7.100
libpostproc 56. 6.100 / 56. 6.100
详细代码如下:
package main
import (
"errors"
"fmt"
"log"
"strings"
"github.com/asticode/go-astiav"
)
var (
//定义一个字符串数组
inputs = []string{
"/Users/tianwen/GolandProjects/immediate-video-deal/asserts/475/475_2_1682325633772.webm",
"/Users/tianwen/GolandProjects/immediate-video-deal/asserts/475/475_2_1682325643877.webm",
"/Users/tianwen/GolandProjects/immediate-video-deal/asserts/475/475_2_1682325684043.webm",
}
mergeOutput = "/Users/tianwen/GolandProjects/immediate-video-deal/asserts/a.webm"
inputFormatContexts []*astiav.FormatContext
recodes []Recode
)
// 请调整下方的代码,使得可以将多个视频文件合并成一个视频文件
// 要求:
func main() {
// Handle ffmpeg logs
astiav.SetLogLevel(astiav.LogLevelDebug)
astiav.SetLogCallback(func(l astiav.LogLevel, fmt, msg, parent string) {
log.Printf("ffmpeg log: %s (level: %d)\n", strings.TrimSpace(msg), l)
})
// Alloc output format context
outputFormatContext, err := astiav.AllocOutputFormatContext(nil, "", output)
if err != nil {
log.Fatal(fmt.Errorf("main: allocating output format context failed: %w", err))
}
if outputFormatContext == nil {
log.Fatal(errors.New("main: output format context is nil"))
}
defer func() {
outputFormatContext.Free()
if len(inputFormatContexts) > 0 {
for _, v := range inputFormatContexts {
v.Free()
}
}
}()
// If this is a file, we need to use an io context
if !outputFormatContext.OutputFormat().Flags().Has(astiav.IOFormatFlagNofile) {
// Create io context
ioContext := astiav.NewIOContext()
// Open io context
if err = ioContext.Open(mergeOutput, astiav.NewIOContextFlags(astiav.IOContextFlagWrite)); err != nil {
log.Fatal(fmt.Errorf("main: opening io context failed: %w", err))
}
defer ioContext.Closep() //nolint:errcheck
// Update output format context
outputFormatContext.SetPb(ioContext)
}
// Loop through streams
outputStreams := make(map[int]*astiav.Stream) // Indexed by input stream index
for _, input := range inputs {
// Alloc input format context
inputFormatContext := astiav.AllocFormatContext()
if inputFormatContext == nil {
log.Fatal(errors.New("main: input format context is nil"))
}
// Open input
if err := inputFormatContext.OpenInput(input, nil, nil); err != nil {
log.Fatal(fmt.Errorf("main: opening input failed: %w", err))
}
// Find stream info
if err := inputFormatContext.FindStreamInfo(nil); err != nil {
log.Fatal(fmt.Errorf("main: finding stream info failed: %w", err))
}
for index, is := range inputFormatContext.Streams() {
fmt.Println(is.TimeBase())
// Only process audio or video
if is.CodecParameters().MediaType() != astiav.MediaTypeAudio &&
is.CodecParameters().MediaType() != astiav.MediaTypeVideo {
continue
}
if nil == outputStreams[index] {
// Add stream to output format context
os := outputFormatContext.NewStream(nil)
if os == nil {
log.Fatal(errors.New("main: output stream is nil"))
}
// Copy codec parameters
if err = is.CodecParameters().Copy(os.CodecParameters()); err != nil {
log.Fatal(fmt.Errorf("main: copying codec parameters failed: %w", err))
}
// Reset codec tag
os.CodecParameters().SetCodecTag(0)
// Add output stream
outputStreams[index] = os
}
}
inputFormatContexts = append(inputFormatContexts, inputFormatContext)
recodes = append(recodes, Recode{0, 0, 0, 0})
}
// Write header
if err = outputFormatContext.WriteHeader(nil); err != nil {
log.Fatal(fmt.Errorf("main: writing header failed: %w", err))
}
// Alloc packet
pkt := astiav.AllocPacket()
defer pkt.Free()
for i, inputFormatContext := range inputFormatContexts {
// Loop through packets
for {
// Read frame
if err = inputFormatContext.ReadFrame(pkt); err != nil {
if errors.Is(err, astiav.ErrEof) {
break
}
log.Fatal(fmt.Errorf("main: reading frame failed: %w", err))
}
if pkt.Dts() < 0 && pkt.Pts() < 0 {
continue
}
// Get input stream
inputStream := inputFormatContext.Streams()[pkt.StreamIndex()]
// Get output stream
outputStream := outputStreams[pkt.StreamIndex()]
// Update packet
pkt.SetStreamIndex(outputStream.Index())
if pkt.StreamIndex() == 0 { // video stream
recodes[i].VideoDts = pkt.Dts()
recodes[i].VideoPts = pkt.Pts()
pkt.SetDts(getTs(i, 1) + pkt.Dts())
pkt.SetPts(getTs(i, 2) + pkt.Pts())
} else if pkt.StreamIndex() == 1 { // audio stream
recodes[i].VoiceDts = pkt.Dts()
recodes[i].VoicePts = pkt.Pts()
pkt.SetDts(getTs(i, 3) + pkt.Dts())
pkt.SetPts(getTs(i, 4) + pkt.Pts())
}
pkt.RescaleTs(inputStream.TimeBase(), outputStream.TimeBase())
pkt.SetPos(-1)
// Write frame
if err = outputFormatContext.WriteInterleavedFrame(pkt); err != nil {
log.Fatal(fmt.Errorf("main: writing interleaved frame failed: %w", err))
}
}
}
// Write trailer
if err = outputFormatContext.WriteTrailer(); err != nil {
log.Fatal(fmt.Errorf("main: writing trailer failed: %w", err))
}
// Success
log.Println("success")
}
func getTs(index, kind int) int64 {
var ts int64 = 0
for i, recode := range recodes {
if i == index {
break
}
switch kind {
case 1:
ts += recode.VideoDts
case 2:
ts += recode.VideoPts
case 3:
ts += recode.VoiceDts
case 4:
ts += recode.VoicePts
}
}
return ts
}
type Recode struct {
VideoDts int64 `json:"videoDts"` // 视频最后编码时间
VideoPts int64 `json:"videoPts"` // 视频最后播放时间
VoiceDts int64 `json:"voiceDts"` // 声音最后编码时间
VoicePts int64 `json:"voicePts"` // 声音最后播放时间
}