探秘GoAV:解锁Golang音视频处理的强大工具
概述
音视频处理一直是多媒体领域的核心,也是各种直播、点播、短视频、流媒体应用的基础。在 Golang 生态中,GoAV(通常指 github.com/giorgisio/goav
或者其分支)为我们提供了一套高效、易用的 FFmpeg(libav)跨平台绑定,让我们可以在 Go 语言中直接调用 FFmpeg 的底层 API,完成“解复用→解码→过滤→编码→复用”等全流程操作。本文将带你从 环境准备、基础概念、核心模块与 API、典型示例代码、ASCII 图解 以及 注意事项 等多角度详解 GoAV,让你快速上手并掌握 Golang 音视频处理的强大工具。
一、环境准备与安装
1.1 安装 FFmpeg 开发库
GoAV 底层依赖 FFmpeg 的 C/C++ 库(libavcodec、libavformat、libavutil、libavfilter、libswscale、libswresample 等),因此需要先安装系统层面的 FFmpeg 开发包。
在 Linux(以 Ubuntu 为例):
sudo apt update sudo apt install -y libavcodec-dev libavformat-dev libavutil-dev libavfilter-dev libswscale-dev libswresample-dev pkg-config
在 macOS(使用 Homebrew):
brew install ffmpeg pkg-config
- 在 Windows:
需要手动下载并编译 FFmpeg,或者使用第三方编译的 “FFmpeg dev” 库,将.dll
/.lib
放到系统路径,并配置好PKG_CONFIG_PATH
。也可参考 GoAV 官方文档提供的 Windows 编译说明。
安装完成后,使用 pkg-config --cflags --libs libavformat libavcodec libavutil
等命令测试能否正确输出链接信息。
1.2 下载并安装 GoAV 包
在你的 Go 项目中执行:
go get -u github.com/giorgisio/goav/...
或者使用其分支(如
goav-ffmpeg
):go get -u github.com/3d0c/gmf
这两者本质类似,前者绑定 FFmpeg 旧版本,后者绑定新版 FFmpeg API。本文以
github.com/giorgisio/goav
为示例代码组织,但大部分概念在其它 GoFFmpeg 绑定中都通用。创建
go.mod
:go mod init your_module_name go mod tidy
此时已经可以在 Go 中直接 import "github.com/giorgisio/goav/avcodec"
、import "github.com/giorgisio/goav/avformat"
等包进行开发。
二、核心概念与模块
在 GoAV(FFmpeg)中,音视频处理大致分为以下几个阶段与模块,每个模块对应一个或多个 Go 包:
┌───────────────┐
│ 源文件(URL/本地文件/流) │
└───────┬───────┘
│ avformat (Demuxer)
▼
┌───────────────┐
│ 解复用器(分离音视频流) │
└───────┬───────┘
│ avcodec (Decoder)
▼
┌───────────────┐
│ 解码器(将压缩帧→原始帧) │
└───────┬───────┘
│ avfilter (可选:视频/音频滤镜)
▼
┌───────────────┐
│ 滤镜/缩放/采样 │
└───────┬───────┘
│ avcodec (Encoder)
▼
┌───────────────┐
│ 编码器(原始帧→压缩帧) │
└───────┬───────┘
│ avformat (Muxer)
▼
┌───────────────┐
│ 复用器(合并音视频流) │
└───────────────┘
│
▼
输出文件/网络推流
avformat
(Demuxer / Muxer)avformat.OpenInput
:打开媒体源(文件/流),读取封装格式头信息。avformat.FindStreamInfo
:获取音视频流信息(比特率、编码格式、分辨率、采样率等)。avformat.AvReadFrame
:循环读取一帧压缩数据(AVPacket
)。avformat.NewOutputContext
/avformat.AvformatAllocOutputContext2
:创建输出上下文,用于写文件/推流。avformat.AvWriteHeader
/avformat.AvWriteFrame
/avformat.AvWriteTrailer
:依次写入封装头、压缩帧、封装尾。
avcodec
(Decoder / Encoder)- Decoder:
avcodec.AvcodecFindDecoder
→avcodec.AvcodecAllocContext3
→ 给上下文中设置参数(宽高、像素格式、采样率、通道布局等) →avcodec.AvcodecOpen2
→avcodec.AvcodecSendPacket
→avcodec.AvcodecReceiveFrame
→ 获取解码后原始帧(AVFrame
)。 - Encoder:
avcodec.AvcodecFindEncoder
→avcodec.AvcodecAllocContext3
→ 设置编码上下文参数(目标编码格式、分辨率、码率、帧率、GOP 大小等) →avcodec.AvcodecOpen2
→avcodec.AvcodecSendFrame
→avcodec.AvcodecReceivePacket
→ 获取编码后压缩帧(AVPacket
)。
- Decoder:
avfilter
(可选:滤镜)- 提供视频缩放(
scale
)、像素格式转换(format
)、音频采样率转换(aresample
)、剪裁(crop
)、旋转(transpose
)、水印、字幕合成等功能。 - 典型流程:
avfilter.AvfilterGraphAlloc
→avfilter.AvfilterGraphCreateFilter
→avfilter.AvfilterLink
→avfilter.AvfilterGraphConfig
→ 依次avfilter.AvBuffersrcAddFrame
→avfilter.AvBuffersinkGetFrame
获取滤镜后帧。
- 提供视频缩放(
swscale
/swresample
(纯 C 函数,Go 端可直接调用)swscale.SwsGetContext
→swscale.SwsScale
:用于图像缩放与像素格式转换。swresample.SwrAlloc
→swresample.SwrInit
→swresample.SwrConvert
:用于音频采样率、通道布局、样本格式转换。
三、典型示例代码
接下来通过几个“实战示例”来巩固上面提到的各模块 API 用法与流程。
示例 1:将 MP4 转为 H.264 + AAC 的 MP4(不改变分辨率/采样率)
3.1.1 步骤概览
- 打开输入文件 → 获取视频/音频流索引。
- 为视频流创建解码器上下文,为音频流创建解码器上下文。
- 为输出文件创建
avformat
上下文,添加新的输出视频流(H.264)和音频流(AAC),分别设置编码参数。 - 打开输出编码器(H.264 Encoder、AAC Encoder),同时复制输入的视频/音频流时基、时戳信息。
循环
AvReadFrame
,根据pkt.StreamIndex
判断是视频还是音频:- 视频:发送到视频解码器 → 接收原始帧 → (可选:缩放/滤镜) → 发送到视频编码器 → 接收压缩帧 → 写入输出封装。
- 音频:发送到音频解码器 → 接收原始 PCM 帧 → (可选:重采样) → 发送到 AAC 编码器 → 接收编码帧 → 写入输出封装。
- 循环结束后,发送空包刷新解码器和编码器,最后写入
AvWriteTrailer
,关闭所有资源。
3.1.2 关键代码示例(核心片段、简化版)
package main
import (
"fmt"
"os"
"github.com/giorgisio/goav/avcodec"
"github.com/giorgisio/goav/avformat"
"github.com/giorgisio/goav/avutil"
"github.com/giorgisio/goav/swresample"
"github.com/giorgisio/goav/swscale"
)
func checkErr(err error) {
if err != nil {
panic(err)
}
}
func main() {
inputFile := "input.mp4"
outputFile := "output_transcode.mp4"
// -----------------
// 1. 打开输入文件
// -----------------
var ictx *avformat.Context
if avformat.AvformatOpenInput(&ictx, inputFile, nil, nil) != 0 {
panic("无法打开输入文件")
}
defer ictx.AvformatCloseInput()
if ictx.AvformatFindStreamInfo(nil) < 0 {
panic("无法获取流信息")
}
// 查找视频流 & 音频流索引
var videoStreamIndex, audioStreamIndex int = -1, -1
for i := 0; i < int(ictx.NbStreams()); i++ {
st := ictx.Streams()[i]
codecType := st.CodecParameters().AvCodecGetType()
if codecType == avformat.AVMEDIA_TYPE_VIDEO && videoStreamIndex < 0 {
videoStreamIndex = i
}
if codecType == avformat.AVMEDIA_TYPE_AUDIO && audioStreamIndex < 0 {
audioStreamIndex = i
}
}
if videoStreamIndex < 0 || audioStreamIndex < 0 {
panic("没有检测到视频或音频流")
}
// ------------------------------
// 2. 为解码器打开解码上下文
// ------------------------------
// 视频解码器上下文
vidSt := ictx.Streams()[videoStreamIndex]
vcodecPar := vidSt.CodecParameters()
vdec := avcodec.AvcodecFindDecoder(avcodec.CodecId(vcodecPar.GetCodecId()))
if vdec == nil {
panic("无法找到视频解码器")
}
vdecCtx := vdec.AvcodecAllocContext3()
if vdecCtx.AvcodecParametersToContext(vcodecPar) < 0 {
panic("无法复制视频解码参数")
}
if vdecCtx.AvcodecOpen2(vdec, nil) < 0 {
panic("无法打开视频解码器")
}
defer vdecCtx.AvcodecClose()
// 音频解码器上下文
audSt := ictx.Streams()[audioStreamIndex]
acodecPar := audSt.CodecParameters()
adec := avcodec.AvcodecFindDecoder(avcodec.CodecId(acodecPar.GetCodecId()))
if adec == nil {
panic("无法找到音频解码器")
}
adecCtx := adec.AvcodecAllocContext3()
if adecCtx.AvcodecParametersToContext(acodecPar) < 0 {
panic("无法复制音频解码参数")
}
if adecCtx.AvcodecOpen2(adec, nil) < 0 {
panic("无法打开音频解码器")
}
defer adecCtx.AvcodecClose()
// ------------------------------
// 3. 创建输出上下文并添加流
// ------------------------------
var octx *avformat.Context
avformat.AvformatAllocOutputContext2(&octx, nil, "", outputFile)
if octx == nil {
panic("无法创建输出上下文")
}
defer func() {
if octx.Oformat().GetOutputFormatName() == nil {
// 如果未输出成功,那么调用 AvformatFreeContext
octx.AvformatFreeContext()
}
}()
// 视频编码器: H.264
vcodecEnc := avcodec.AvcodecFindEncoder(avcodec.AV_CODEC_ID_H264)
if vcodecEnc == nil {
panic("找不到 H.264 编码器")
}
vOutStream := octx.AvformatNewStream(nil)
vencCtx := vcodecEnc.AvcodecAllocContext3()
// 设置编码上下文参数,参考输入视频参数
vencCtx.SetCodecType(avcodec.AVMEDIA_TYPE_VIDEO)
vencCtx.SetWidth(vdecCtx.Width())
vencCtx.SetHeight(vdecCtx.Height())
vencCtx.SetTimeBase(avutil.NewRational(1, 25)) // 帧率 25fps
vencCtx.SetPixFmt(avcodec.AV_PIX_FMT_YUV420P) // 常用像素格式
vencCtx.SetBitRate(400000) // 码率 400kbps
if octx.Oformat().GetFlags()&avformat.AVFMT_GLOBALHEADER != 0 {
vencCtx.SetFlags(vencCtx.Flags() | avcodec.CODEC_FLAG_GLOBAL_HEADER)
}
if vencCtx.AvcodecOpen2(vcodecEnc, nil) < 0 {
panic("无法打开视频编码器")
}
vencCtx.AvcodecParametersFromContext(vOutStream.CodecParameters())
vOutStream.SetTimeBase(avutil.NewRational(1, 25))
// 音频编码器: AAC
acodecEnc := avcodec.AvcodecFindEncoder(avcodec.AV_CODEC_ID_AAC)
if acodecEnc == nil {
panic("找不到 AAC 编码器")
}
aOutStream := octx.AvformatNewStream(nil)
aencCtx := acodecEnc.AvcodecAllocContext3()
aencCtx.SetCodecType(avcodec.AVMEDIA_TYPE_AUDIO)
aencCtx.SetSampleRate(44100) // 44.1kHz
aencCtx.SetChannelLayout(avutil.AV_CH_LAYOUT_STEREO) // 双声道
aencCtx.SetChannels(2)
aencCtx.SetSampleFmt(avcodec.AV_SAMPLE_FMT_FLTP) // AAC 常用浮点格式
aencCtx.SetBitRate(64000) // 64kbps
if octx.Oformat().GetFlags()&avformat.AVFMT_GLOBALHEADER != 0 {
aencCtx.SetFlags(aencCtx.Flags() | avcodec.CODEC_FLAG_GLOBAL_HEADER)
}
if aencCtx.AvcodecOpen2(acodecEnc, nil) < 0 {
panic("无法打开音频编码器")
}
aencCtx.AvcodecParametersFromContext(aOutStream.CodecParameters())
aOutStream.SetTimeBase(avutil.NewRational(1, aencCtx.SampleRate()))
// 打开输出文件
if octx.Oformat().GetFlags()&avformat.AVFMT_NOFILE == 0 {
if avformat.AvioOpen(&octx.Pb, outputFile, avformat.AVIO_FLAG_WRITE) < 0 {
panic("无法打开输出文件")
}
}
// 写入封装头
if octx.AvformatWriteHeader(nil) < 0 {
panic("无法写入文件头")
}
// ------------------------------
// 4. 读取输入帧并编码输出
// ------------------------------
pkt := avcodec.AvPacketAlloc()
frame := avutil.AvFrameAlloc()
defer avcodec.AvPacketFree(&pkt)
defer avutil.AvFrameFree(frame)
swsCtx := swscale.SwsGetcontext(
vdecCtx.Width(), vdecCtx.Height(), vdecCtx.PixFmt(),
vencCtx.Width(), vencCtx.Height(), vencCtx.PixFmt(),
swscale.SWS_BILINEAR, nil, nil, nil,
)
if swsCtx == nil {
panic("无法初始化 SwsContext")
}
// 音频重采样上下文
swrCtx := swresample.SwrAlloc()
swresample.SwrAllocSetOpts2(
swrCtx,
aencCtx.ChannelLayout(), aencCtx.SampleFmt(), aencCtx.SampleRate(),
adecCtx.ChannelLayout(), adecCtx.SampleFmt(), adecCtx.SampleRate(),
0, nil,
)
if swrCtx.SwrInit() < 0 {
panic("无法初始化 SwrContext")
}
for {
if ictx.AvReadFrame(pkt) < 0 {
break // 到文件尾
}
switch pkt.StreamIndex() {
case videoStreamIndex:
// 视频解码
if vdecCtx.AvcodecSendPacket(pkt) < 0 {
fmt.Println("发送视频包到解码器失败")
continue
}
for vdecCtx.AvcodecReceiveFrame(frame) == 0 {
// 转换像素格式 (例如输入为 YUV444 → H.264 编码器要求 YUV420P)
dstFrame := avutil.AvFrameAlloc()
dstFrame.SetFormat(vencCtx.PixFmt())
dstFrame.SetWidth(vencCtx.Width())
dstFrame.SetHeight(vencCtx.Height())
dstFrame.AvFrameGetBuffer(0)
swscale.SwsScale2(swsCtx,
avutil.Data(frame), avutil.Linesize(frame),
0, vdecCtx.Height(),
avutil.Data(dstFrame), avutil.Linesize(dstFrame),
)
// 编码
dstFrame.SetPts(frame.Pts())
if vencCtx.AvcodecSendFrame(dstFrame) < 0 {
fmt.Println("发送视频帧到编码器失败")
dstFrame.AvFrameFree()
break
}
for {
outPkt := avcodec.AvPacketAlloc()
if vencCtx.AvcodecReceivePacket(outPkt) < 0 {
outPkt.AvPacketFree()
break
}
outPkt.SetStreamIndex(vOutStream.Index())
outPkt.SetPts(avutil.AvRescaleQRnd(
outPkt.Pts(), vencCtx.TimeBase(), vOutStream.TimeBase(),
avutil.AV_ROUND_NEAR_INF|avutil.AV_ROUND_PASS_MINMAX))
outPkt.SetDts(avutil.AvRescaleQRnd(
outPkt.Dts(), vencCtx.TimeBase(), vOutStream.TimeBase(),
avutil.AV_ROUND_NEAR_INF|avutil.AV_ROUND_PASS_MINMAX))
octx.AvInterleavedWriteFrame(outPkt)
outPkt.AvPacketFree()
}
dstFrame.AvFrameFree()
}
case audioStreamIndex:
// 音频解码
if adecCtx.AvcodecSendPacket(pkt) < 0 {
fmt.Println("发送音频包到解码器失败")
continue
}
for adecCtx.AvcodecReceiveFrame(frame) == 0 {
// 重采样:输入样本格式 → AAC 编码器需要的样本格式
dstAudioFrame := avutil.AvFrameAlloc()
dstAudioFrame.SetChannelLayout(aencCtx.ChannelLayout())
dstAudioFrame.SetFormat(int32(aencCtx.SampleFmt()))
dstAudioFrame.SetSampleRate(aencCtx.SampleRate())
// 分配缓存
swresample.SwrConvertFrame(swrCtx, dstAudioFrame, frame)
dstAudioFrame.SetPts(frame.Pts())
// 编码
if aencCtx.AvcodecSendFrame(dstAudioFrame) < 0 {
fmt.Println("发送音频帧到编码器失败")
dstAudioFrame.AvFrameFree()
break
}
for {
outPkt := avcodec.AvPacketAlloc()
if aencCtx.AvcodecReceivePacket(outPkt) < 0 {
outPkt.AvPacketFree()
break
}
outPkt.SetStreamIndex(aOutStream.Index())
outPkt.SetPts(avutil.AvRescaleQRnd(
outPkt.Pts(), aencCtx.TimeBase(), aOutStream.TimeBase(),
avutil.AV_ROUND_NEAR_INF|avutil.AV_ROUND_PASS_MINMAX))
outPkt.SetDts(avutil.AvRescaleQRnd(
outPkt.Dts(), aencCtx.TimeBase(), aOutStream.TimeBase(),
avutil.AV_ROUND_NEAR_INF|avutil.AV_ROUND_PASS_MINMAX))
octx.AvInterleavedWriteFrame(outPkt)
outPkt.AvPacketFree()
}
dstAudioFrame.AvFrameFree()
}
}
pkt.AvPacketUnref()
}
// 发送空包刷新编码器
vencCtx.AvcodecSendPacket(nil)
for {
outPkt := avcodec.AvPacketAlloc()
if vencCtx.AvcodecReceivePacket(outPkt) < 0 {
outPkt.AvPacketFree()
break
}
outPkt.SetStreamIndex(vOutStream.Index())
octx.AvInterleavedWriteFrame(outPkt)
outPkt.AvPacketFree()
}
aencCtx.AvcodecSendFrame(nil)
for {
outPkt := avcodec.AvPacketAlloc()
if aencCtx.AvcodecReceivePacket(outPkt) < 0 {
outPkt.AvPacketFree()
break
}
outPkt.SetStreamIndex(aOutStream.Index())
octx.AvInterleavedWriteFrame(outPkt)
outPkt.AvPacketFree()
}
// 写入封装尾
octx.AvWriteTrailer()
fmt.Println("转码完成:", outputFile)
}
核心流程解析:
- 解复用:
AvReadFrame(pkt)
读取原始AVPacket
(压缩包),此时尚未解码。- 解码:调用
AvcodecSendPacket
把pkt
送到解码器,再循环AvcodecReceiveFrame
取出AVFrame
(解码后原始帧),可包含 YUV 图像或 PCM 样本。- 转换与滤镜(可选):视频使用
sws_scale
进行像素格式转换,音频使用swr_convert
进行重采样。- 编码:把(可能已经经过转换的)
AVFrame
送入编码器,循环从编码器取回压缩后的AVPacket
。- 复用:将编码后
AVPacket
写入输出封装(AvInterleavedWriteFrame
会负责根据流索引与时戳对齐,插入合适位置)。- 刷新:解码器在读完所有输入包后需通过发送空包让解码器输出缓存帧;编码器在读完所有输入帧后也需发送空包让缓存中剩余压缩包输出。
- 收尾:调用
AvWriteTrailer
写入封装尾部数据,完成文件构建。
示例 2:从视频文件中提取一张高清封面(JPEG 图片)
有时需要从视频中抽取第一帧或指定时间的关键帧,保存为 JPEG 图像,可用作封面缩略图。下面示例演示如何抽取第 100 帧并保存为 JPEG。
3.2.1 步骤概览
- 打开输入视频 → 查找视频流索引 → 打开视频解码器。
- 循环读取
AVPacket
,只处理视频流对应的包 → 解码得到AVFrame
。 - 第 N 帧(由计数判断)时,将原始 YUV 帧转换为 RGB24(或其他目标像素格式)→ 将 RGB 填充到 Go
image.RGBA
或image.YCbCr
数据结构 → 使用 Go 标准库的image/jpeg
序列化并保存到文件。
3.2.2 关键代码示例
package main
import (
"fmt"
"image"
"image/jpeg"
"os"
"github.com/giorgisio/goav/avcodec"
"github.com/giorgisio/goav/avformat"
"github.com/giorgisio/goav/avutil"
"github.com/giorgisio/goav/swscale"
)
func main() {
inputFile := "video.mp4"
outputImage := "thumb.jpg"
// 1. 打开文件 & 查找流
var ictx *avformat.Context
if avformat.AvformatOpenInput(&ictx, inputFile, nil, nil) != 0 {
panic("无法打开视频文件")
}
defer ictx.AvformatCloseInput()
if ictx.AvformatFindStreamInfo(nil) < 0 {
panic("无法获取流信息")
}
var vidIdx int = -1
for i := 0; i < int(ictx.NbStreams()); i++ {
if ictx.Streams()[i].CodecParameters().AvCodecGetType() == avformat.AVMEDIA_TYPE_VIDEO {
vidIdx = i
break
}
}
if vidIdx < 0 {
panic("未找到视频流")
}
// 2. 打开解码器
vidSt := ictx.Streams()[vidIdx]
decPar := vidSt.CodecParameters()
dec := avcodec.AvcodecFindDecoder(avcodec.CodecId(decPar.GetCodecId()))
if dec == nil {
panic("找不到解码器")
}
decCtx := dec.AvcodecAllocContext3()
decCtx.AvcodecParametersToContext(decPar)
decCtx.AvcodecOpen2(dec, nil)
defer decCtx.AvcodecClose()
// 3. 设置转为 RGB24 的 SwsContext
swsCtx := swscale.SwsGetcontext(
decCtx.Width(), decCtx.Height(), decCtx.PixFmt(),
decCtx.Width(), decCtx.Height(), avcodec.AV_PIX_FMT_RGB24,
swscale.SWS_BILINEAR, nil, nil, nil,
)
if swsCtx == nil {
panic("无法创建 SwsContext")
}
pkt := avcodec.AvPacketAlloc()
frame := avutil.AvFrameAlloc()
defer avcodec.AvPacketFree(&pkt)
defer avutil.AvFrameFree(frame)
rgbFrame := avutil.AvFrameAlloc()
rgbBufferSize := avutil.AvImageGetBufferSize(avcodec.AV_PIX_FMT_RGB24, decCtx.Width(), decCtx.Height(), 1)
rgbBuffer := avutil.AvMalloc(uintptr(rgbBufferSize))
defer avutil.AvFree(rgbBuffer)
avutil.AvImageFillArrays(
(*[]uint8)(unsafe.Pointer(&rgbFrame.Data())),
(*[]int32)(unsafe.Pointer(&rgbFrame.Linesize())),
(*uint8)(rgbBuffer),
avcodec.AV_PIX_FMT_RGB24,
decCtx.Width(),
decCtx.Height(),
1,
)
rgbFrame.SetWidth(decCtx.Width())
rgbFrame.SetHeight(decCtx.Height())
rgbFrame.SetFormat(avcodec.AV_PIX_FMT_RGB24)
frameCount := 0
targetFrame := 100 // 提取第 100 帧
for ictx.AvReadFrame(pkt) >= 0 {
if pkt.StreamIndex() != vidIdx {
pkt.AvPacketUnref()
continue
}
if decCtx.AvcodecSendPacket(pkt) < 0 {
fmt.Println("解码失败")
pkt.AvPacketUnref()
continue
}
for decCtx.AvcodecReceiveFrame(frame) == 0 {
frameCount++
if frameCount == targetFrame {
// 转换到 RGB24
swscale.SwsScale2(swsCtx,
avutil.Data(frame), avutil.Linesize(frame),
0, decCtx.Height(),
avutil.Data(rgbFrame), avutil.Linesize(rgbFrame),
)
saveFrameAsJPEG(rgbFrame, decCtx.Width(), decCtx.Height(), outputImage)
fmt.Println("已保存封面到", outputImage)
return
}
}
pkt.AvPacketUnref()
}
fmt.Println("未达到目标帧数")
}
func saveFrameAsJPEG(frame *avutil.Frame, width, height int, filename string) {
// 将 RGB24 数据转换为 Go image.RGBA
img := image.NewRGBA(image.Rect(0, 0, width, height))
data := frame.Data()[0] // RGB24 连续数据
linesize := frame.Linesize()[0] // 每行字节数 = width * 3
for y := 0; y < height; y++ {
row := data[y*linesize : y*linesize+width*3]
for x := 0; x < width; x++ {
r := row[x*3]
g := row[x*3+1]
b := row[x*3+2]
img.SetRGBA(x, y, image.RGBAColor{R: r, G: g, B: b, A: 255})
}
}
// 写入 JPEG
f, err := os.Create(filename)
if err != nil {
panic(err)
}
defer f.Close()
opt := &jpeg.Options{Quality: 90}
if err := jpeg.Encode(f, img, opt); err != nil {
panic(err)
}
}
解析要点:
SwsGetcontext
:指定输入像素格式(解码后通常是AV_PIX_FMT_YUV420P
)与输出像素格式 (AV_PIX_FMT_RGB24
),并设置目标宽高;AvImageGetBufferSize
&AvImageFillArrays
:为rgbFrame
分配并填充缓冲,使其可存放转换后的 RGB 数据;SwsScale2
:真正进行像素格式转换,将YUV420P
→RGB24
;- 将
AVFrame
中的 RGB 数据逐像素复制到 Go 标准库image.RGBA
,然后用jpeg.Encode
写文件。
四、ASCII 图解:GoAV 全流程示意
下面用 ASCII 图示将“MP4 转码示例”中的关键流程做一个简化说明,帮助你把握 GoAV (FFmpeg) 的数据流与模块间关系。
┌───────────────────────────────────────────────────────────────────────────┐
│ Go 代码 │
│ 1. avformat.OpenInput(input.mp4) │
│ 2. avformat.FindStreamInfo │
│ 3. avcodec.AvcodecFindDecoder → ↑解码器上下文 │
│ 4. avcodec.AvcodecOpen2 │
│ 5. avformat.AvformatAllocOutputContext2(output.mp4) │
│ 6. avcodec.AvcodecFindEncoder (H.264/AAC) │
│ 7. avcodec.AvcodecAllocContext3 → ↑编码器上下文 │
│ 8. avcodec.AvcodecOpen2 (编码器) │
│ 9. octx.AvformatWriteHeader │
│ │
│ 循环 avformat.AvReadFrame(pkt) → pkt │
│ │ │
│ ├── if pkt.StreamIndex == 视频流 → │
│ │ ├── vdecCtx.AvcodecSendPacket(pkt) │
│ │ └── for vdecCtx.AvcodecReceiveFrame(frame) → 解码后 AVFrame │
│ │ ├── swscale.SwsScale(frame → dstFrame) │
│ │ ├── dstFrame.SetPts(frame.Pts) │
│ │ ├── vencCtx.AvcodecSendFrame(dstFrame) │
│ │ └── for vencCtx.AvcodecReceivePacket(outPkt) → 编码器压缩 AVPacket │
│ │ └── outPkt.SetStreamIndex(视频输出流索引) │
│ │ └── octx.AvInterleavedWriteFrame(outPkt) │
│ │ │
│ └── if pkt.StreamIndex == 音频流 → │
│ ├── adecCtx.AvcodecSendPacket(pkt) │
│ └── for adecCtx.AvcodecReceiveFrame(frame) → 解码后 PCM AVFrame │
│ ├── swresample.SwrConvert(frame → dstAudioFrame) │
│ ├── dstAudioFrame.SetPts(frame.Pts) │
│ ├── aencCtx.AvcodecSendFrame(dstAudioFrame) │
│ └── for aencCtx.AvcodecReceivePacket(outPkt) → 编码后 AAC AVPacket │
│ └── outPkt.SetStreamIndex(音频输出流索引) │
│ └── octx.AvInterleavedWriteFrame(outPkt) │
│ │
│ 循环结束后, │
│ vencCtx.AvcodecSendPacket(nil) → 刷新视频编码缓存 │
│ aencCtx.AvcodecSendFrame(nil) → 刷新音频编码缓存 │
│ octx.AvWriteTrailer() │
└───────────────────────────────────────────────────────────────────────────┘
模块间数据流:
avformat
负责读/写封装格式,产生/接收压缩数据(AVPacket
)。avcodec
负责编解码,将压缩包AVPacket
↔ 原始帧AVFrame
。swscale
、swresample
均属于“转换/滤镜”模块,将AVFrame
在像素格式或采样格式上进行转换。- 编码后
AVPacket
再由avformat
按照指定封装输出到文件或网络。
五、注意事项与实战指南
资源管理
- FFmpeg 和 GoAV 中大量结构体(
AVFrame
、AVPacket
、编码/解码上下文、SwsContext
、SwrContext
等)需要显式释放。例如:AvFrameFree
、AvPacketFree
、AvcodecClose
、AvcodecFreeContext
、SwsFreeContext
、SwrFree
、AvformatFreeContext
、AvioClose
等。务必在defer
中妥善清理,否则容易造成 C 层内存泄漏。
- FFmpeg 和 GoAV 中大量结构体(
线程与 Goroutine
- FFmpeg 的大部分 API 在一个线程(即一个
OS Thread
)中调用即可。Go 默认的 Goroutine 是 M\:N 模型,会在用户态调度多个 Goroutine 到少量 OS Thread(M)上。当你在一个 Goroutine 中调用 FFmpeg API 时,该 Goroutine 会被调度到一个可用 M 上执行,FFmpeg 内部也会创建自己的线程(如多线程解码、过滤等),会与 Go M 之间并行。 - 避免在多个不同的 Goroutine 并行“共享”同一个
AVCodecContext
、SwsContext
等不支持线程安全的对象,否则会引发数据竞态。必要时可在多个 Goroutine 间使用互斥锁(sync.Mutex
)或每个 Goroutine 单独创建各自的上下文。
- FFmpeg 的大部分 API 在一个线程(即一个
性能优化
- 对于循环读取与写入操作,尽量 复用
AVPacket
与AVFrame
(调用AvPacketUnref
、AvFrameUnref
清空内部数据后复用),避免不断分配/释放带来的性能开销。 - 使用
SwsContext
、SwrContext
的时候,要只初始化一次,在整个处理过程中重复使用,完成后再释放。 - 尽可能采用硬件加速(如果 FFmpeg 编译时开启了
--enable-hwaccel
支持,如 NVENC、VAAPI、VideoToolbox 等),GoAV 也支持设置硬件解码/编码设备,但需要额外编译 FFmpeg 并在 GoAV 代码中使用相关 API。
- 对于循环读取与写入操作,尽量 复用
错误处理与日志
FFmpeg 的大多数函数返回
<0
表示错误,或者在av_strerror
中输出错误信息。建议在panic(err)
前打印可读的错误码与字符串,例如:buf := make([]byte, 1024) avutil.AvStrerror(errCode, buf, 1024) fmt.Printf("错误:%s\n", string(buf))
- 开发时可在环境变量中设置
FFREPORT=file=ffreport.log:level=32
,让 FFmpeg 输出更详细的日志,便于排查问题。
跨平台兼容
- 不同系统(Linux、macOS、Windows)FFmpeg 常量、编译选项有所不同,部分封装格式或编解码器在某个平台不支持。例如 macOS 上默认可能未启用 x264,需要你自行编译 FFmpeg。
- GoAV API 在不同平台上保持一致,但底层
pkg-config
信息可能不同。记得在 macOS 或 Windows 上正确设置PKG_CONFIG_PATH
,或手动在cgo
中指定#cgo CFLAGS
、#cgo LDFLAGS
。
六、小结
通过本文,你已经了解了:
- GoAV(FFmpeg)基础概念:
avformat
、avcodec
、swscale
、swresample
、解复用/解码/滤镜/编码/复用等模块。 - 环境搭建与安装:如何在 Linux/macOS/Windows 上安装 FFmpeg 开发库,并在 Go 中导入 GoAV。
- 核心 API 调用流程:典型的“MP4 转码”示例和“视频帧抽取”示例,涉及
AvReadFrame
、AvcodecSendPacket
、AvcodecReceiveFrame
、SwsScale
、AvcodecSendFrame
、AvcodecReceivePacket
、AvWriteFrame
等方法。 - ASCII 图解:帮助你梳理 Go 代码与 FFmpeg 底层 C 库之间的调用流程,以及各模块之间数据的传递。
- 注意事项与优化建议:资源释放、线程安全、性能优化、跨平台兼容等关键点。
掌握了 GoAV,你就拥有了一把“解锁 Golang 音视频处理”的利器。未来可在短视频平台、媒体服务器、直播推流、实时特效、视音频 AI 分析等场景中自由驰骋,将 Go 的高并发与 FFmpeg 的强大编解码能力结合起来,创造更多可能。
评论已关闭