鼎鼎大名的FFmpeg不用多作介绍,基本是音视频技术必备的基础库之一,提供了强大的音视频处理方案。本文记录FFmpeg的一些基本知识,基于4.0.2,有时间会慢慢增改。(PS:可能有错误)
FFmpeg最常用的结构体
解协议(http,rtsp,rtmp,mms)
协议(文件)操作的顶层结构是AVIOContext,这个对象实现了带缓冲的读写操作;FFmpeg的输入对象AVFormat的pb字段指向一个AVIOContext。
AVIOContext的opaque实际指向一个URLContext对象,这个对象封装了协议对象及协议操作对象,其中prot指向具体的协议操作对象,priv_data指向具体的协议对象。
URLProtocol为协议操作对象,针对每种协议,会有一个这样的对象,每个协议操作对象和一个协议对象关联。
注意:FFmpeg中文件也被当做一种协议:file。
解封装(flv,avi,rmvb,mp4)
AVFormatContext主要存储视音频封装格式中包含的信息;AVInputFormat存储输入视音频使用的封装格式。每种视音频封装格式都对应一个AVInputFormat结构。
解码(h264,mpeg2,aac,mp3)
每个AVStream存储一个视频/音频流的相关数据;每个AVStream对应一个AVCodecContext,存储该视频/音频流使用解码方式的相关数据;每个AVCodecContext中对应一个AVCodec,包含该视频/音频对应的解码器。每种解码器都对应一个AVCodec结构。
编解码数据
视频每个Packet是一帧;音频每个Packet可能包含若干帧。
解码前数据:AVPacket
解码后数据:AVFrame
解码基本流程
从封装文件中拿到流。
从流中读取数据包到packet。
将packet解码为frame。
处理frame。
转到步骤2。
详细:
创建AVFormatContext,用以管理文件的输入输出,可以直接赋予空指针,然后由后续函数分配内存:
|
|
也可以用函数分配内存:
|
|
然后用以下函数打开输入:
|
|
该函数会读取媒体文件的文件头并将文件格式相关的信息存储在我们作为第一个参数传入的AVFormatContext中。第二个参数为视频地址,这个可以是本地视频文件地址,也可以是视频流地址。第三个参数用于指定媒体文件格式,第四个参数是文件格式相关选项。如果后面这两个参数传入的是NULL,那么 libavformat 将自动探测文件格式。如果文件打开失败,返回值为负值,要调用avformat_free_context()及时释放掉AVFormatContext(好像会自动释放?)。如果打开成功,返回值为0,且等到后面不再需要输入文件的操作时,要调用avformat_close_input(AVFormatContext **s)来关闭输入。
接着需要将视音频流的信息读取到AVFormatContext,AVFormatContext中有信息,才能进行查找视频流、音频流及相应的解码器的操作:
|
|
第二个参数一般填NULL。返回值>=0表示成功。
调试函数:
|
|
可以为我们打印 AVFormatContext 中都有哪些信息。url是文件,index和output一般填0。AVFormatContext 里包含了下面这些跟媒体信息有关的成员:
struct AVInputFormat *iformat:输入数据的封装格式
AVIOContext *pb:输入数据的缓存
unsigned int nb_streams:视音频流的个数
AVStream **streams:视音频流
char filename[1024]:文件名
int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000,即除以AV_TIME_BASE)
int bit_rate:比特率(单位bps,转换为kbps需要除以1000)
AVDictionary *metadata:元数据
接下来需要初始化视音频的AVCodec(解码器)和AVCodecContext(解码器上下文)。注意,这里音频的AVCodec和AVCodecContext和视频的是分开的,但是它们的流程是一模一样的。首先根据类型找到音频或视频的序号,并在同时匹配到最适合的解码器:
|
|
AVMediaType是AVMEDIA_TYPE_VIDEO或AVMEDIA_TYPE_AUDIO,wanted_stream_nb和related_stream传-1。decoder_ret传入一个新建的AVCodec *codec,这样可以直接将查找到的解码器填充进去,当然有可能查找失败,所以应该判断一下codec是否为NULL。flags传0。函数返回值为相应流的序号,负值代表失败。
通过序号就能找到视频流或者音频流:
|
|
接下来通过匹配到的解码器创建AVCodecContext(解码器上下文)并把视/音频流里的参数传到视/音频解码器中:
|
|
codecContext为NULL表示失败。
|
|
返回负值表示失败。
接下来就可以打开解码器上下文准备进行解码操作了:
|
|
最后一个参数填NULL,返回负值表示失败。
分配AVPacket和AVFrame的内存:
|
|
这两个语句分配的内存是AVPacket和AVFrame结构本身的内存,而不包括其指向的实际数据的部分,这些需要另外分配。失败时返回NULL。此外AVPacket也可以不使用指针动态分配内存,而是直接定义AVPacket pkt,然后用其他方法获得相应数据。
循环调用函数:
|
|
该函数从流中读取一个数据包,把它存储在AVPacket数据结构中,其中packet.data这个指针会指向这些数据。注意av_read_frame不会调用av_packet_unref,只会调用av_init_packet将引用计数的指针指向NULL。因此如果该packet没有拷贝到别处用于其他用途,则在下一次av_read_frame前实际数据占用的内存需要手动通过av_packet_unref()函数来释放(一般放在av_read_frame最后,保证该次循环不会再使用改packet)。
然后在循环中先调用avcodec_send_packet(AVCodecContext avctx, const AVPacket avpkt)发送
再调用avcodec_receive_frame(AVCodecContext avctx, AVFrame frame)接收。注意该函数会先调用av_frame_unref(frame),故如果每次使用的是同一个frame去接收解码后的数据,那么每次传进去就会把前面的数据释放掉,导致就只有一个frame是有用的。
如果发送函数报AVERROR(EAGAIN)的错,表示已发送的AVPacket还没有被接收,不允许发送新的AVPacket。如果是接收函数报这个错,表示没有新的AVPacket可以接收,需要先发送AVPacket才能执行这个函数。而如果报AVERROR_EOF的错,在以上4个函数中都表示编解码器处于已经刷新完成的状态,没有数据可以进行发送和接收操作。
最后释放内存
|
|
av_frame_free(&frame)和av_packet_free(&pkt)分别对应于av_frame_alloc()和av_packet_alloc(),不同之处在于av_frame_alloc()和av_packet_alloc()会先调用av_frame_unref()释放buf内存,再释放AVFrame本身的内存,av_packet_alloc()同理。
注意av_frame_ref对src的buf增加一个引用,即使用同一个数据,只是这个数据引用计数加1。av_frame_unref把自身对buf的引用释放掉,数据的引用计数减1,当引用为0就释放buf。
图像/音频数据内存分配与释放
|
|
返回对应图像格式和大小的图像所占的字节数,最后一个参数是内存对齐的对齐数,也就是按多大的字节进行内存对齐。比如设置为1,表示按1字节对齐,那么得到的结果就是与实际的内存大小一样。再比如设置为4,表示按4字节对齐。也就是内存的起始地址必须是4的整倍数。
|
|
申请指定字节数的内存,返回相应的指针,NULL表示分配内存失败。
|
|
函数自身不具备内存申请的功能,此函数类似于格式化已经申请的内存,即通过av_malloc()函数申请的内存空间。再者,av_image_fill_arrays()中参数具体说明(中括号里表明是输入还是输出):
dst_data[4]:[out]对申请的内存格式化为三个通道后,分别保存其地址。
dst_linesize[4]: [out]格式化的内存的步长(即内存对齐后的宽度) 注:linesize每行的大小不一定等于图像的宽度,因为pack格式图像的所有分量储存在同一个通道中,如data[0]。
*src: [in]av_malloc()函数申请的内存地址(av_malloc返回的指针)。
pix_fmt: [in] 申请 src内存时的像素格式。
[in]申请src内存时指定的宽度。
height: [in]申请scr内存时指定的高度。
align: [in]申请src内存时指定的对齐字节数。
通常上面三个函数一起调用。或者用下面的一步到位的简化函数:
|
|
pointers[4]:保存图像通道的地址。如果是RGB,则前三个指针分别指向R,G,B的内存地址。第四个指针保留不用。
linesizes[4]:保存图像每个通道的内存对齐的步长,即一行的对齐内容的宽度,此值大小在planar图像中等于图像宽度。
w: 要申请内存的图像宽度。
h: 要申请内存的图像高度。
pix_fmt: 要申请内存的图像的像素格式。
align: 用于内存对齐的值。一般为1。
返回值:所申请的内存空间的总大小。如果是负值,表示申请失败。
同样,音频也有上述类似的函数:
|
|
linesize是函数里面计算出来的,可以传NULL。
|
|
|
|
audio_data:保存音频通道的地址,也有planar和pack之分。
linesize:允许为NULL。
|
|
释放指针并置为NULL,上面分配的dst_data[4]/pointers[4]最后要用此函数释放。当然也可以定义一个AVframe,用其相应的结构承载内存,最后由av_frame_free(&frame)负责释放
图像转换libswscale
该库可以改变图像尺寸,转换像素格式等,当对解码后的图像进行保存或显示的时候需要用到,因为图像原格式并不一定适合存储或用SDL播放。总体流程是:
sws_getContext():初始化一个SwsContext。
sws_scale():处理图像数据。
sws_freeContext():释放一个SwsContext。
具体
|
|
AVPixelFormat 为输入和输出图片数据的类型,eg:AV_PIX_FMT_YUV420、PAV_PIX_FMT_RGB24;int flags 为scale算法种类;后面三个指针一般为NULL。
出错返回NULL。
|
|
const uint8_t *const srcSlice[],uint8_t *const dst[]:输输出图像数据各颜色通道的buffer指针数组;
const int srcStride[],const int dstStride[]:输入输出图像据各颜色通道每行存储的字节数数组;
int srcSliceY:从输入图像数据的第多少列开始逐行扫描,通常设0;
int srcSliceH:需要扫描多少行,通常为输入图像数据的高度;
返回输出图像高度。
音频转换libswresample
该库可以转换音频格式,当对解码后的音频进行保存或播放的时候需要用到,因为音频原格式并不一定适合存储或用SDL播放(比如SDL播放音频不支持平面格式)。总体流程是:
创建SwrContext,并设置转换所需的参数:通道数量、channel layout、sample rate。
设置了所有参数后,用swr_init(struct SwrContext *)初始化
调用swr_convert()进行转换。
swr_free()释放上下文。
具体
使用swr_alloc_set_opts设置SwrContext:
|
|
上述两种方法设置的是将5.1声道,采样格式为AV_SAMPLE_FMT_FLTP,采样率为48KHz的音频转换为2声道,采样格式为AV_SAMPLE_FMT_S16,采样率为44.1KHz。
其中的参数channel_layout是一个64位整数,每个值为1的位对应一个通道。在头文件channel_layout.h中为将每个通道定义了一个mask,一个channel_layout就是某些channel mask的组合。可以用以下函数根据channel_layout得到通道数:
|
|
也可以根据通道数得到默认的channel_layout:
|
|
调用swr_convert进行转换
|
|
out:输出缓冲区。
out_count:转换后每个通道的sample个数
in:输入缓冲区,一般为(const uint8_t **)frame->data
in_count:输入音频每个通道的sample个数,一般为frame->nb_samples
其返回值为转换后每个通道的sample个数。
转换后的sample个数的计算公式为:src_nb_samples * dst_sample_rate / src_sample_rate,其代码如下:
|
|
函数av_rescale_rnd是按照指定的舍入方式计算a * b / c 。
函数swr_get_delay得到输入sample和输出sample之间的延迟,并且其返回值的根据传入的第二个参数不同而不同。如果是输入的采样率,则返回值是输入sample个数;如果输入的是输出采样率,则返回值是输出sample个数。
时间戳timestamp
时间戳是以时间基(timebase)为单位的具体时间表示,有PTS和DTS两种,一般在有B帧编码的情况下两者都会用到,没有B帧时,两者一般保持一样。
DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。
时间基timebase
ffmpeg存在多个时间基(time_base),对应不同的阶段(结构体),每个time_base具体的值不一样,ffmpeg提供函数在各个time_base中进行切换。
首先要知道AVRatioal的定义如下:
|
|
ffmpeg提供了一个把AVRatioal结构转换成double的函数:
|
|
不同的时间基
AV_TIME_BASE
ffmpeg中的“内部时间基”,以微秒为单位,作为某些变量的基本时间单位,比如AVFormatContext中的duration即以其倒数(AV_TIME_BASE_Q)为基本单位,意味着这个流的长度为duration微秒,要除以AV_TIME_BASE(即乘以AV_TIME_BASE_Q)才能得到单位是秒的结果。此处也说明它和别的time_base刚好相反,因为别的time_base都是直接用秒的倒数来表示的。AV_TIME_BASE定义为:
|
|
AV_TIME_BASE_Q
ffmpeg内部时间基的分数表示,实际上它是AV_TIME_BASE的倒数。从它的定义能很清楚的看到这点,1秒除以1000000即1微秒:
|
|
AVStream->time_base
根据时钟采样率来决定的,单位为秒,如:1/90000。根据封装格式不一样,avformat_write_header()可能修改AVStream->time_base,比如mpegts修改为90000,flv修改为1000,mp4根据设置time_base,如果小于10000,会将time_base*2的幂直到大于10000。AVPacket的pts和dts以AVStream的time_base为单位。
AVCodecContext->time_base
根据视频帧率/音频采样率来决定的,单位为秒,如:通常视频的该time_base值是 1/framerate,音频则是1/samplerate。时间戳pts、dts每增加1实际上代表的是增加了一个time_base的时间。AVFrame的pts和dts以AVStream的time_base为单位,而AVFrame里面的pkt_pts和pkt_dts是拷贝自AVPacket,同样以AVStream->time_base为单位。AVFrame的pts用av_frame_get_best_effort_timestamp获取比较好。
InputStream这个结构的pts和dts以AV_TIME_BASE为单位
问题的关键是不同的场景下取到的数据帧的time是相对哪个时间体系的:
demux出来的帧的time:是相对于源AVStream的timebase。
编码器出来的帧的time:是相对于源AVCodecContext的timebase。
mux存入文件等容器的time:是相对于目的AVStream的timebase。
这里的time指pts。
计算
根据pts来计算某一帧在整个视频中的时间位置(PTS转常规时间):
time(秒) = pts * av_q2d(st->time_base)
计算视频长度:
time(秒) = st->duration * av_q2d(st->time_base)
常规时间转PTS:(存疑?)
pts = time * 1/av_q2d(st->time_base)
这里的st是一个AVStream对象指针。pts应该也是AVStream->time_base为单位。似乎一般都是用AVStream的time_base和相应单位的pts来得到真实的时间。
ffmpeg提供了不同时间基之间的转换函数:
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq)
这个函数的作用是计算a * bq / cq,来把时间戳从一个时基调整到另外一个时基。在进行时基转换的时候,应该首选这个函数,因为它可以避免溢出的情况发生。av_rescale_q(pts, timebase1, timebase2)的含义即是:
new_pts = pts (timebase1.num / timebase1.den ) (timebase2.den / timebase2.num)
几个含义
st为AVStream:
fps = st->avg_frame_rate:平均帧率
tbr = st->r_frame_rate:这是可以准确表示所有时间戳的最低帧率(它是流中所有帧率的最小公倍数),猜测值。
tbn = st->time_base:AVStream的timebase
tbc = st->codec->time_base:AVCodecContext的timebase。
最新评论