FFMpeg 实现视频编码、解码、封装、解封装、转码、缩放
FFmpeg音视频解码过程
- 初始化网络
avformat_network_init
- 打开文件
avformat_open_input
- 从文件中提取流信息
avformat_find_stream_info
- 在多个数据流中找到视频流(类型为
MEDIA_TYPE_VIDEO
)和音频流(AVMEDIA_TYPE_AUDIO
) - 查找相对应的解码器
avcodec_find_decoder
- 打开解码器
avcodec_open2
- 从数据流中读取数据到Packet中
av_read_frame
- 将数据送入到解码器内部
avcodec_send_packet
- 为解码帧分配内存
av_frame_alloc
- 接收解码帧
avcodec_receive_frame
- 解码完成后需要关闭
avformat_open_input
打开的输入流,avcodec_open2
打开的CODEC
注:注册所支持的文件格式和遍解码器CODEC
的av_register_all
接口已经遗弃,不再需要调用这个接口注册了。参考:ffmpeg4.2.2 av_register_all()的分析
打开一个视频解码器示例
// 获取视频流
videoStream = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
// 获取codec
AVCodec *vcodec = avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id);
// 构造AVCodecContext ,并将vcodec填入AVCodecContext中
AVCodecContext *vc = avcodec_alloc_context3(vcodec);
// 初始化AVCodecContext
avcodec_parameters_to_context(vc, ic->streams[videoStream]->codecpar);
// 打开解码器,由于之前调用avcodec_alloc_context3(vcodec)初始化了vc,
// 那么codec(第2个参数)可以填NULL
int ret = avcodec_open2(vc, NULL,NULL);
FFmpeg重要结构体
参考:FFMPEG中最关键的结构体之间的关系
AVCodecContext
音视频编解码器上下文。作为AVStream的一个字段存在。avcodec_open2
、avcodec_send_packet
、avcodec_receive_frame
打开编解码器和解码时依赖。在使用avcodec_open2
打开后,需要使用avcodec_close
将其关闭。
AVCodecContext* avcodecCtx = avformatCtx->streams[streamId]->codec;
AVCodec
AVCodec是存储编解码器信息的结构体,表示音视频编解码器,着重于功能函数。
参考: AVCodecContext和AVCodec
AVFormatContext
在FFmpeg中有很重要的作用,描述一个多媒体文件的构成及其基本信息,存放了视频编解码过程中的大部分信息。通常该结构体由avformat_open_input
分配存储空间,在最后调用avformat_input_close
关闭。
AVStream
AVStream
表示AVFormatContext
中一条具体的流结构,比如:音频流、视频流和字幕流等。在解码的过程中,作为AVFormatContext
的一个字段存在,不需要单独的处理。
AVStream* avstream = avformatCtx->streams[streamId];
AVpacket
用来存放解码之前的数据,它只是一个容器,其data
成员指向实际的数据缓冲区,在解码的过程中可由av_read_frame
创建和填充AVPacket
中的数据缓冲区,当数据缓冲区不再使用的时候可以调用av_free_apcket
释放这块缓冲区,最新接口改为av_packet_unref
释放。
对于视频(Video)来说,AVPacket通常包含一个压缩的Frame,而音频(Audio)则有可能包含多个压缩的Frame。并且,一个Packet有可能是空的,不包含任何压缩数据,只含有side data(side data,容器提供的关于Packet的一些附加信息。例如,在编码结束的时候更新一些流的参数)。
AVPacket
结构定义如下:
typedef struct AVPacket {
// 若为空,则表示AVPacket未使用引用计数管理负载数据,否则指向存储负载数据的引用计数AVBufferRef -> AVBuffer
AVBufferRef *buf;
// 基于AVStream->time_base的pts,若文件中没有,则为AV_NOPTS_VALUE。
// pts必须大于等于dts,因为显示不能早于解码
int64_t pts;
// 基于AVStream->time_base的dts,若文件中没有,则为AV_NOPTS_VALUE。
int64_t dts;
// 负载数据
uint8_t *data;
// 负载数据的长度
int size;
// 属于AVFormatContext中的哪个AVStream
int stream_index;
// AV_PKT_FLAG_KEY表示关键帧
int flags;
// 携带的不同类型的side data
AVPacketSideData *side_data;
int side_data_elems;
// 当前帧的持续时间,基于AVStream->time_base
int64_t duration;
// byte position in stream, -1 if unknown
int64_t pos;
} AVPacket;
参考:
FFmpeg数据结构:AVPacket解析
FFmpeg之AVPacket
AVFrame
typedef struct AVFrame {
//指向图片/频道平面的指针
uint8_t *data[AV_NUM_DATA_POINTERS];
//对于视频,每行图片的大小以字节为单位。
//对于音频,每个平面的大小(以字节为单位)。
int linesize[AV_NUM_DATA_POINTERS];
.....
//此帧描述的音频采样数(每个通道)
int nb_samples;
// 音频数据的采样率
int sample_rate;
.....
} AVFrame;
存放从AVPacket
中解码出来的原始数据,其必须通过av_frame_alloc
来创建,通过av_frame_free
来释放。
AVFrame *rgbFrame = av_frame_alloc();
和AVPacket
类似,AVFrame
中也有一块数据缓存空间data
,在调用av_frame_alloc
的时候并不会为这块缓存区域分配空间,需要使用av_image_alloc
来为其申请空间。
av_image_alloc(rgbFrame->data, rgbFrame->linesize, videoWidth, videoHeight, AV_PIX_FMT_RGB32, 1);
在解码的过程使用了两个AVFrame,这两个AVFrame分配缓存空间的方法也不相同:
一个AVFrame
用来存放从AVPacket
中解码出来的原始数据,这个AVFrame
的数据缓存空间通过调avcodec_decode_video
分配和填充。
另一个AVFrame
用来存放将解码出来的原始数据变换为需要的数据格式(例如RGB,RGBA)的数据,这个AVFrame
需要手动的分配数据缓存空间。
FFmpeg重要概念
PTS和DTS
PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。
DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
音频的PTS和DTS是一致的,而含有B帧的视频会存在DTS和PTS不一致的帧,我们这里主要通过PTS来控制播放时间。对解码后的AVFrame
使用av_frame_get_best_effort_timestamp
可以获取PTS,再乘以time_base即可得到实际时间。只有AVStream中获取的time_base才是对的,其他地方获取的可能会有问题。
avformatCtx->streams[streamId]->time_base * pts
FFmpeg相关API解析
解码相关
av_find_best_stream
获取音视频对应的stream_index。
av_find_best_stream(fmt, type, -1, -1, NULL, 0);
fmt
是avformat_open_input
获取的AVFormatContext
实例。
图像内存相关
av_image_alloc
此函数的功能是按照指定的宽、高、像素格式来申请图像内存。
int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
int w, int h, enum AVPixelFormat pix_fmt, int align);
参数说明如下:
pointers[4]:保存图像通道的地址。如果是RGB,则前三个指针分别指向R,G,B的内存地址,第四个指针保留不用。
linesizes[4]:保存图像每个通道的内存对齐的步长,即一行的对齐内存的宽度,此值大小等于图像宽度。
w: 要申请内存的图像宽度。
h: 要申请内存的图像高度。
pix_fmt: 要申请内存的图像的像素格式。
align: 用于内存对齐的值。
返回值:所申请的内存空间的总大小。如果是负值,表示申请失败。
av_image_get_buffer_size
针对给图像申请内存,有另外一种方式,即使用av_image_get_buffer_size
和av_malloc
来申请图像所需的内存。
int av_image_get_buffer_size(enum AVPixelFormat pix_fmt, int width, int height, int align);
align
:此参数是设定内存对齐的对齐数,也就是按多大的字节进行内存对齐。比如设置为1,表示按1字节对齐,那么得到的结果就是与实际的内存大小一样。再比如设置为4,表示按4字节对齐。也就是内存的起始地址必须是4的整倍数。
av_image_get_buffer_size
用来返回指定格式、宽高及对齐下的图像所需空间的大小。使用样例如下:
//计算指定像素格式、图像宽、高所需要的对齐的内存大小
int image_buf_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1);
//分配指定大小的内存空间
out_buffer = (unsigned char *)av_malloc(image_buf_size);
av_image_fill_arrays
/**
* Setup the data pointers and linesizes based on the specified image
* parameters and the provided array.
**/
int av_image_fill_arrays(uint8_t *dst_data[4], int dst_linesize[4],
const uint8_t *src,
enum AVPixelFormat pix_fmt, int width, int height, int align);
该函数主要是按照指定的格式、宽高及对齐方式,将src
的数据填充到dst_data
和dst_linesize
里。参数具体说明如下:
dst_data[4]: [out]对申请的内存格式化为三个通道后,分别保存其地址
dst_linesize[4]: [out]格式化的内存的步长(即内存对齐后的宽度)
src: [in]av_alloc()函数申请的内存地址。
pix_fmt: [in] 申请 src内存时的像素格式
width: [in]申请src内存时指定的宽度
height: [in]申请scr内存时指定的高度
align: [in]申请src内存时指定的对齐字节数。
SWS图像转换相关
重点参考:雷霄骅的最简单的基于FFmpeg的libswscale的示例,其中有对图像拉伸的算法SWS_BICUBIC
、SWS_BILINEAR
和SWS_POINT
做了详细的解释,并对像素格式AV_PIX_FMT_
有所描述。
sws_getContext
struct SwsContext *sws_getContext(
int srcW, /* 输入图像的宽度 */
int srcH, /* 输入图像的宽度 */
enum AVPixelFormat srcFormat, /* 输入图像的像素格式 */
int dstW, /* 输出图像的宽度 */
int dstH, /* 输出图像的高度 */
enum AVPixelFormat dstFormat, /* 输出图像的像素格式 */
int flags,/* 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR */
SwsFilter *srcFilter, /* 输入图像的滤波器信息, 若不需要传NULL */
SwsFilter *dstFilter, /* 输出图像的滤波器信息, 若不需要传NULL */
const double *param /* 特定缩放算法需要的参数(?),默认为NULL */
);
初始化SwsContext的函数之一,成功返回SwsContext
,否则返回NULL。参数说明:
其中flags
,可以设置为SWS_BICUBIC
性能比较好,SWS_FAST_BILINEAR
在性能和速度之间有一个比好好的平衡,而SWS_POINT
的效果比较差。
sws_getCachedContext
struct SwsContext *sws_getCachedContext(struct SwsContext *context,
int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags, SwsFilter *srcFilter,
SwsFilter *dstFilter, const double *param);
初始化SwsContext的函数之一,成功返回SwsContext
,否则返回NULL。参数说明:
int srcW, /* 输入图像的宽度 */
int srcH, /* 输入图像的宽度 */
enum AVPixelFormat srcFormat, /* 输入图像的像素格式 */
int dstW, /* 输出图像的宽度 */
int dstH, /* 输出图像的高度 */
enum AVPixelFormat dstFormat, /* 输出图像的像素格式 */
int flags,/* 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR */
SwsFilter *srcFilter, /* 输入图像的滤波器信息, 若不需要传NULL */
SwsFilter *dstFilter, /* 输出图像的滤波器信息, 若不需要传NULL */
const double *param /* 特定缩放算法需要的参数(?),默认为NULL */
sws_scale
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8\_t *const dst[], const int dstStride[]);
该函数实现了三个功能:1.图像色彩空间转换;2.分辨率缩放;3.前后图像滤波处理。
参数说明如下:
- 参数
SwsContext *c
, 转换格式的上下文。也就是 sws_getContext 函数返回的结果。 - 参数
const uint8_t *const srcSlice[]
, 输入图像的每个颜色通道的数据指针。其实就是解码后的AVFrame
中的data[]
数组。因为不同像素的存储格式不同,所以srcSlice[]
维数也有可能不同。
以YUV420P为例,它是planar格式,它的内存中的排布如下:
YYYYYYYY UUUU VVVV
使用FFmpeg解码后存储在AVFrame
的data[]
数组中时:
data[0]
——-Y分量,Y1, Y2, Y3, Y4, Y5, Y6, Y7, Y8……
data[1]
——-U分量,U1, U2, U3, U4……
data[2]
——-V分量,V1, V2, V3, V4……
像素格式名称后面有“P”的,代表是planar格式,否则就是packed格式。Planar格式不同的分量分别存储在不同的数组中,Packed格式的数据都存储在同一个数组中,例如AV_PIX_FMT_RGB24
存储方式如下:
data[0]
:R1, G1, B1, R2, G2, B2, R3, G3, B3, R4, G4, B4……
AVFrame
中的linesize[]
数组中保存的是对应通道的数据宽度 ,
linesize[0]
——-Y分量的宽度
linesize[1]
——-U分量的宽度
linesize[2]
——-V分量的宽度 - 参数
const int srcStride[]
,输入图像的每个颜色通道的跨度。也就是每个通道的行字节数,对应的是解码后的AVFrame
中的linesize[]
数组。根据它可以确立下一行的起始位置,不过stride和width不一定相同,这是因为:
a. 由于数据帧存储的对齐,有可能会向每行后面增加一些填充字节这样stride = width + N
;
b. packet色彩空间下,每个像素几个通道数据混合在一起,例如RGB24,每个像素3字节连续存放,因此下一行的位置需要跳过3*width字节。 - 参数
int srcSliceY, int srcSliceH
,定义在输入图像上处理区域,srcSliceY
是起始位置,srcSliceH
是处理多少行。
如果srcSliceY=0; srcSliceH=height
,表示一次性处理完整个图像。这种设置是为了多线程并行,例如可以创建两个线程,第一个线程处理 [0, h/2-1]行,第二个线程处理 [h/2, h-1]行。并行处理加快速度。 - 参数
uint8_t *const dst[], const int dstStride[]
定义输出图像信息(输出的每个颜色通道数据指针,每个颜色通道行字节数)
参考:FFmepg中的sws_scale() 函数分析
swr_convert
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count, const uint8_t **in , int in_count);
该函数主要是实现音频文件的编码转换。
sws_setColorspaceDetails
初始化颜色空间
int sws_setColorspaceDetails(struct SwsContext *c, const int inv_table[4],
int srcRange, const int table[4], int dstRange,
int brightness, int contrast, int saturation);
c:需要设定的SwsContext。
inv_table:描述输出YUV颜色空间的参数表。
srcRange:输入图像的取值范围(“1”代表JPEG标准,取值范围是0-255;“0”代表MPEG标准,取值范围是16-235)。
table:描述输入YUV颜色空间的参数表。
dstRange:输出图像的取值范围。
时间戳相关
AVRational
FFMPEG的很多结构中有AVRational time_base
这样的一个成员,它是AVRational结构的
typedef struct AVRational {
int num; ///< numerator 分数
int den; ///< denominator 分母
} AVRational;
AVRational这个结构标识一个分数,num为分数,den为分母。
实际上time_base的意思就是时间的刻度:
如(1,25),那么时间刻度就是1/25
(1,9000),那么时间刻度就是1/90000
那么,在刻度为1/25的体系下的time=5,转换成在刻度为1/90000体系下的时间time为(51/25)/(1/90000) = 36005=18000,即下面说到的函数av_rescale_q
。
参考:FFMPEG之TimeBase成员理解
av_rescale_q
// The operation is mathematically equivalent to `a * bq / cq`.
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;
该函数的作用是在不同的时间基之间进行转换。a
表示要原始值,bq
表示原来的时间基准;cq
表示要转换到的时间基准。
- 内部时间基准:
AV_TIME_BASE
AVFormatContext
中的参数多是基于AV_TIME_BASE
的。 - 流时间基准:
AVStream->time_base
AVStream
、AVPacket
和AVFrame
中的时间戳多是基于对应的流时间基准。
参考:FFmpeg时间戳
操作相关
av_seek_frame
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);
参数说明:
s // 描述了一个媒体文件或媒体流的构成和基本信息
stream_index //表示当前的seek是针对哪个基本流,比如视频或者音频等等
timestamp //将要定位的时间戳,时间戳会从以AV_TIME_BASE为单位向具体流的时间基自动转换。
flags //
AVSEEK_FLAG_BACKWARD 是seek到请求的timestamp之前最近的关键帧
AVSEEK_FLAG_BYTE 是基于字节位置的查找
AVSEEK_FLAG_ANY 是可以seek到任意帧,注意不一定是关键帧,因此使用时可能会导致花屏
AVSEEK_FLAG_FRAME 是基于帧数量快进
在进行seek操作时,时间戳必须基于流时间基准(AVStream->time_base
),比如:seek到第5秒,需做如下的操作:
// 首先计算出基于视频流时间基准的时间戳
int64_t timestamp = av_rescale_q(5 * AV_TIME_BASE, AV_TIME_BASE_Q, video_stream_->time_base);
// 然后seek
av_seek_frame(av_format_context, video_stream_index, timestamp, AVSEEK_FLAG_BACKWARD);
avcodec_flush_buffers
void avcodec_flush_buffers(AVCodecContext *avctx);
说明:回放过程中跳转播放,或者切换到一个不同的流,调用此函数,清空内部缓存的帧数据。
实际应用场景:在网络环境中,传输数据用UDP经常有丢包,而丢包很容易造成FFmpeg解码器缓冲的帧数增加,可以在一段时间内清空解码器缓存
切记:应该在接收到I帧的时候,清空缓存,否则出现画面不连续或者马赛克的情况