ffmpeg笔记

Mar 20, 2017


近一两年内开源项目中对我工作影响和帮助最大的非ffmpeg莫属,ffmpeg的强大以及被使用的普遍性令人惊讶。每次阅读ffmpeg的源代码都感觉收获颇多,经常将从ffmpeg里面吸收的代码技巧和音视频算法用于公司的实际项目中,解决实际问题的同时也提示自己的代码质量。感谢开源的精神与力量。


ffmpeg编译与配置




ffmpeg中基本对象


理解ffmpeg中主要基本对象的实现细节对于分析ffmpeg中编解码流程至关重要,尤其是编解码中内存的管理。我们知道c语言实现的项目,相当一部分工作就是内存的管理,内存的管理是难点,也是重点。ffmpeg中关于内存管理的技巧值得我们好好研究学习。


AVBuffer


AVBuffer是ffmpeg中内存管理的基石,是其他对象(AVPacket、AVFrame)的元对象,其代码实现非常的有技术含量。

AVBuffer对象分成AVBuffer和AVBufferRef两个不同层次的对象,其中AVBuffer为私有对象(it is opaque and not meant to be accessed by the caller directly),它表示的是内存数据的本身,实现定义在buffer_internal.h头文件中。

struct AVBuffer {
    uint8_t *data; /**< data described by this buffer */
    int      size; /**< size of data in bytes */
    atomic_uint refcount;
    void (*free)(void *opaque, uint8_t *data);
    void *opaque;
    int flags;
};

AVBufferRef表示的是对AVBuffer描述的内存数据的引用,它是一个对外的接口对象,定义在buffer.h头文件中。

/**
 * A reference to a data buffer.
 *
 * The size of this struct is not a part of the public ABI and it is not meant
 * to be allocated directly.
 */
typedef struct AVBufferRef {
    AVBuffer *buffer;
    uint8_t *data;
    int      size;
} AVBufferRef;

为什么要在AVBuffer对象的基础上再设计出一个AVBufferRef对象呢?从软件设计模式(dedign pattern)来说,这是一种代码模式(proxy)实现。如果提供API接口直接访问AVBuffer对象,基于引用计数实现免拷贝的机制不太好实现,另外,对于 多线程下读写buffer内存数据的控制也不太好实现。其实,最大的好处是,可以很方便的定义API函数来操作被代理的对象,而不是直接操作对象变量。

AVBufferRef对象中的data字段主要是为了写访问avBuffer的控制,这个字段也是最容易引起混淆和不解的地方。而其实,AVBuffer对象绝大部分情况下都是以只读 方式访问的,因为引用计数已经实现为原子操作,所以referencing 和 unreferencing avBuffer是多线程安全的,无须额外加锁保护。

AVBuffer组件不只是提供了一般的buffer创建与读写访问接口,还提供了内存池的功能,以实现某些业务下需要内存池技术解决频繁申请释放内存带来的副作用。

avBuffer池主要由AVBufferPool对象和BufferPoolEntry对象实现,这两个对象都是对外不可见的opaque对象,也定义在buffer_internal.h中。

typedef struct BufferPoolEntry {
    uint8_t *data;
    void *opaque;
    void (*free)(void *opaque, uint8_t *data);
    AVBufferPool *pool;
    struct BufferPoolEntry *next;
} BufferPoolEntry;


struct AVBufferPool {
    AVMutex mutex;
    BufferPoolEntry *pool;
    atomic_uint refcount;
    int size;
    void *opaque;
    AVBufferRef* (*alloc)(int size);
    AVBufferRef* (*alloc2)(void *opaque, int size);
    void         (*pool_free)(void *opaque);
};	

通过上面的对象结构体定义,以及avbuffer池的几个API接口可见,avbuffer池功能的实现很简单,就是在avbuffer基本功能的基础上添加了回收复用使用结束的avbuffer对象。


avBuffer的创建

为了兼容所管理的内存来自于外部独立申请的情况(例如,GPU内存),avBuffer提供了av_buffer_alloc()、av_buffer_create()两个API用于创建avBuffer对象。

/**
 * Allocate an AVBuffer of the given size using av_malloc().
 *
 * @return an AVBufferRef of given size or NULL when out of memory
 */
AVBufferRef *av_buffer_alloc(int size);


/**
 * Create an AVBuffer from an existing array.
 *
 * If this function is successful, data is owned by the AVBuffer. The caller may
 * only access data through the returned AVBufferRef and references derived from
 * it.
 * If this function fails, data is left untouched.
 * @param data   data array
 * @param size   size of data in bytes
 * @param free   a callback for freeing this buffer's data
 * @param opaque parameter to be got for processing or passed to free
 * @param flags  a combination of AV_BUFFER_FLAG_*
 *
 * @return an AVBufferRef referring to data on success, NULL on failure.
 */
AVBufferRef *av_buffer_create(uint8_t *data, int size,
			      void (*free)(void *opaque, uint8_t *data),
			      void *opaque, int flags);

从函数原型入参来看,av_buffer_alloc()接口创建的内存对象是由avBuffer组件内部调用系统默认内存申请函数创建的,而av_buffer_create()接口需要 调用者提供已经申请好的内存指针data,以及如何释放这块内存的方法。

如果我们使用的是avbuffer池的功能,那么在调用av_buffer_pool_init()或者av_buffer_pool_init2()后,通过调用av_buffer_pool_get()接口创建新的 avbuffer对象或者是复用之前创建并且已经被释放回avbuffer池的avbuffer对象。

实现av_buffer_pool_init()的两个版本也是为了兼容所管理的内存来自于外部独立申请的情况,

/**
 * Allocate and initialize a buffer pool.
 *
 * @param size size of each buffer in this pool
 * @param alloc a function that will be used to allocate new buffers when the
 * pool is empty. May be NULL, then the default allocator will be used
 * (av_buffer_alloc()).
 * @return newly created buffer pool on success, NULL on error.
 */
AVBufferPool *av_buffer_pool_init(int size, AVBufferRef* (*alloc)(int size));


/**
 * Allocate and initialize a buffer pool with a more complex allocator.
 *
 * @param size size of each buffer in this pool
 * @param opaque arbitrary user data used by the allocator
 * @param alloc a function that will be used to allocate new buffers when the
 *              pool is empty.
 * @param pool_free a function that will be called immediately before the pool
 *                  is freed. I.e. after av_buffer_pool_uninit() is called
 *                  by the caller and all the frames are returned to the pool
 *                  and freed. It is intended to uninitialize the user opaque
 *                  data.
 * @return newly created buffer pool on success, NULL on error.
 */
AVBufferPool *av_buffer_pool_init2(int size, void *opaque,
				   AVBufferRef* (*alloc)(void *opaque, int size),
				   void (*pool_free)(void *opaque));

要注意的是,如果使用avbuffer池接口来管理外部申请的内存,那么提供给av_buffer_pool_init2()中的alloc回调函数,其实现中必须使用av_buffer_create() 接口来创建新avbuffer对象。


引用与释放引用

通过引用的API函数实现av_buffer_ref()可以看出,使用高一层的对象代理实际的对象的好处,对实际对象的操作可以很好的打包封装起来,不至于散落在各个角落。

AVBufferRef *av_buffer_ref(AVBufferRef *buf)
{
    AVBufferRef *ret = av_mallocz(sizeof(*ret));

    if (!ret)
	return NULL;

    *ret = *buf;

    atomic_fetch_add_explicit(&buf->buffer->refcount, 1, memory_order_relaxed);

    return ret;
}

因为使用了两层对象,在释放引用时,要记住也需要两层释放对象,代理层的avBufferRef对象在非avBuffer池接口和池接口流程中释放引用时始终都需要被free掉。

无论是否使用avBuffer池,引用和释放引用的调用的接口和流程一模一样,因为avBuffer对象中的free方法在两者创建时别注册了不同的实现。


写访问

ffmpeg中avBuffer对象大部分情况下用于只读访问。写访问相关的接口主要是av_buffer_realloc()函数和av_buffer_make_writable()函数。前者实现很简单,后者 比较复杂,容易混淆。

int av_buffer_make_writable(AVBufferRef **pbuf)
{
    AVBufferRef *newbuf, *buf = *pbuf;

    if (av_buffer_is_writable(buf))
	return 0;

    newbuf = av_buffer_alloc(buf->size);
    if (!newbuf)
	return AVERROR(ENOMEM);

    memcpy(newbuf->data, buf->data, buf->size);

    buffer_replace(pbuf, &newbuf);

    return 0;
}

上面实现中需要注意和理解的是代理对象中data字段的使用,很容易和实际对象avBuffer中的data混淆。

@note Two different references to the same buffer can point to different parts of the buffer (i.e. their AVBufferRef.data will not be equal).

结合上面这段头文件中的注释,好好体会代理对象在写访问中的作用。


AVFrame


ffmpeg使用AVFrame对象描述解码后的音频或者视频原始数据。按照一般设计,同一个结构体描述不同对象应该会使用union来优化内存占用,但是ffmpeg并没有这么做,不知其中原因。

avframe对象是基于avbuffer对象实现内存管理的,我们的分析也侧重于内存管理方面,暂不涉及码流的属性参数字段。

typedef struct AVFrame {
#define AV_NUM_DATA_POINTERS 8
 
    uint8_t *data[AV_NUM_DATA_POINTERS];
    int linesize[AV_NUM_DATA_POINTERS];
    uint8_t **extended_data;

    /**
     * for some private data of the user
     */
    void *opaque;
    
    AVBufferRef *buf[AV_NUM_DATA_POINTERS];
    AVBufferRef **extended_buf;
    int        nb_extended_buf;

    AVBufferRef *hw_frames_ctx;
    
    AVBufferRef *opaque_ref;
} AVFrame;

AVFrame对象中的data和linesize字段是配合使用的,用于存放音视频帧解码后的CPU内存数据,而对于GPU硬件解码的场景,只使用data数组的第4个位置,即data[3] 保存surface_id。要注意的是,surface_id往往是一个整形变量,而data是一个指针数组,所以存放前需要强制转换类型(具体见ffmpeg硬解章节中的va_get_buffer)。

如果AVFrame对象装载的内存是GPU的内存的话,AVFrame对象中的buf对象只是用来记录引用计数;如果装载的内存是CPU的内存的话,data数组中的指针保存的就是avBuffer对象数组buf中对应的buf->data地址,用一行代码描述如下:

frame->data[i] = frame->buf[i]->data

从hw_frames_ctx字段和opaque_ref字段可以看出,avBuffer对象在ffmpeg中的通用性还是非常强的。

对于AVFrame对象提供的内存相关操作API接口,只要理解了AVBuffer对象的行为,那么就很容易理解和掌握。下面分析其中核心的几个API函数实现细节。

int av_frame_ref(AVFrame *dst, const AVFrame *src)
{
    int i, ret = 0;

    ...

    /* duplicate the frame data if it's not refcounted */
    if (!src->buf[0]) {
	ret = av_frame_get_buffer(dst, 32);//[0]
	if (ret < 0)
	    return ret;

	ret = av_frame_copy(dst, src);//[1]
	if (ret < 0)
	    av_frame_unref(dst);

	return ret;
    }

    /* ref the buffers */
    for (i = 0; i < FF_ARRAY_ELEMS(src->buf); i++) {
	if (!src->buf[i])
	    continue;
	dst->buf[i] = av_buffer_ref(src->buf[i]);//[2]
	if (!dst->buf[i]) {
	    ret = AVERROR(ENOMEM);
	    goto fail;
	}
    }

    if (src->extended_buf) {
	dst->extended_buf = av_mallocz_array(sizeof(*dst->extended_buf),
				       src->nb_extended_buf);
	if (!dst->extended_buf) {
	    ret = AVERROR(ENOMEM);
	    goto fail;
	}
	dst->nb_extended_buf = src->nb_extended_buf;

	for (i = 0; i < src->nb_extended_buf; i++) {
	    dst->extended_buf[i] = av_buffer_ref(src->extended_buf[i]);//[3]
	    if (!dst->extended_buf[i]) {
		ret = AVERROR(ENOMEM);
		goto fail;
	    }
	}
    }

    if (src->hw_frames_ctx) {
	dst->hw_frames_ctx = av_buffer_ref(src->hw_frames_ctx);//[4]
	if (!dst->hw_frames_ctx) {
	    ret = AVERROR(ENOMEM);
	    goto fail;
	}
    }

    /* duplicate extended data */
    if (src->extended_data != src->data) {
	int ch = src->channels;

	if (!ch) {
	    ret = AVERROR(EINVAL);
	    goto fail;
	}
	CHECK_CHANNELS_CONSISTENCY(src);

	dst->extended_data = av_malloc_array(sizeof(*dst->extended_data), ch);
	if (!dst->extended_data) {
	    ret = AVERROR(ENOMEM);
	    goto fail;
	}
	memcpy(dst->extended_data, src->extended_data, sizeof(*src->extended_data) * ch);
    } else
	dst->extended_data = dst->data;

    memcpy(dst->data,     src->data,     sizeof(src->data));
    memcpy(dst->linesize, src->linesize, sizeof(src->linesize));

    return 0;

fail:
    av_frame_unref(dst);
    return ret;
}

av_frame_ref()函数首先通过src->buf[0]来判断当前的src是否已经支持直接引用了,如果不支持,就需要调用[0]处的av_frame_get_buffer()函数,这个函数 内部调用av_buffer_alloc()函数创建可引用的内存。接着是[1]处调用av_frame_copy()进行深度拷贝。

如果src支持直接引用的话,事情就很简单,就是上面的[2][3][4]处那样,直接调用avBuffer对象提供的av_buffer_ref方法即可。

AVFrame *av_frame_alloc(void)
{
    AVFrame *frame = av_mallocz(sizeof(*frame));

    if (!frame)
	return NULL;

    frame->extended_data = NULL;
    get_frame_defaults(frame);

    return frame;
}

AVFrame对象的分配接口av_frame_alloc()很简单,需要理解的是它申请出来的对象类似于一个空壳对象,里面还没有挂载实际的内存。ffmpeg会通过libavcodec/utils.c中的get_buffer_internal()函数进行内存的装载。

static int get_buffer_internal(AVCodecContext *avctx, AVFrame *frame, int flags)
{
	const AVHWAccel *hwaccel = avctx->hwaccel;

	...

	if(hwaccel){
		if(hwaccel->alloc_frame){
			hwaccel->alloc_frame(avctx, frame);//[0]
		}
	}else{
		avctx->sw_pix_fmt = avctx->pix_fmt;
	}

	avctx->get_buffer2(avctx, frame, flag);//[1]

	...

	return ret;
}

为了通用性,ffmpeg没有自己实现内存装载方法,而是在AVCodec对象中提供get_buffer2()回调方法,让用户自己根据实际情况进行实现。当然,有些硬件环境下 也可以直接实现AVHWAccel对象中的alloc_frame()方法完成内存的装载。

除了ffmpeg内部会调用av_frame_alloc()接口外,还有一种场景下经常需要调用,比如我们需要将一张yuv格式的图片或者是一段yuv格式的录像载入为avframe对象,然后才可以调用ffmpeg其他接口来处理这种图片,那么该如何做到呢?调用流程如下:

FILE *fp_in = NULL;
AVFrame *frame_in;
unsigned char *frame_buffer_in;

fp_in = fopen("xxx.yuv", "rb");

/**
 * 先分配一个'空壳'avframe对象.
 */
frame_in = av_frame_alloc();

/**
 * 申请一张图片所需要的实际内存。
 */
frame_buffer_in = av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, in_width, in_height, 1));

/**
 * 根据图片格式和宽度信息填充avframe对象中的linesize字段,即yuv各个分量的数据量大小。
 */
av_image_fill_linesizes(frame_in->linesize, AV_PIX_FMT_YUV420P, in_width);

while(fread(frame_buffer_in, in_width*in_height*3/2,1, fp_in) == 1){
	/**
	 * 将读取的图片信息装载进avframe中。
	 */
	av_image_fill_pointers(frame_in->data, AV_PIX_FMT_YUV420P,in_widht, frame_buffer_in, frame_in->linesize);
	
	...
}


AVPacket


ffmpeg中AVPacktet对象从结构体定义上来看是比较简单的,但是其提供的API非常容易混淆,一旦API调用的不合理,很容易造成内存泄漏。AVPacket对象和AVFrame 对象是两个想对立的对象,前者用来描述编码压缩后的音视频数据,后者用来描述解码后的音视频裸数据。

typedef struct AVPacket {

    AVBufferRef *buf;
    
    ...
    
    uint8_t *data;
    int   size;
    int   stream_index;
    /**
     * A combination of AV_PKT_FLAG values
     */
    int   flags;
    /**
     * Additional packet data that can be provided by the container.
     * Packet can contain several types of side information.
     */
    AVPacketSideData *side_data;
    int side_data_elems;

    int64_t duration;
    int64_t pos;                            ///< byte position in stream, -1 if unknown
} AVPacket;

AVPacket对象中和内存相关的只有buf,data,size这个几个字段,buf是我们熟悉的AVBufferRef对象,所以,AVPacket对象和AVFrame对象一样,都是基于AVBuffer 对象管理内存的。纵观avbuffer、avframe、avpakcet这三个对象,每个对象都含有uint8_t *data字段,可能会出现一时间觉得这个字段很多余,没有什么用,但是 实际使用这些对象的接口后会发现,这个字段主要用来解决有时我们需要直接访问这个三个对象的payload的需求。也就是说,有时我们不想通过avbufferRef对象来间接访问payload,而是直接访问payload的指针。

API接口中最容易混淆的是av_packet_alloc()和av_new_packet(),不过只要清楚接口的代码实现,还是很容易理解区分的。

/**
 * Allocate an AVPacket and set its fields to default values.  The resulting
 * struct must be freed using av_packet_free().
 *
 * @return An AVPacket filled with default values or NULL on failure.
 *
 * @note this only allocates the AVPacket itself, not the data buffers. Those
 * must be allocated through other means such as av_new_packet.
 *
 * @see av_new_packet
 */
AVPacket *av_packet_alloc(void)
{
    AVPacket *pkt = av_mallocz(sizeof(AVPacket));
    if (!pkt)
	return pkt;

    av_packet_unref(pkt);

    return pkt;
}

显然,av_packet_alloc()接口只是分配一个AVPacket空壳对象,它还没有挂载实际的数据内存。

static int packet_alloc(AVBufferRef **buf, int size)
{
    int ret;
    if (size < 0 || size >= INT_MAX - AV_INPUT_BUFFER_PADDING_SIZE)
	return AVERROR(EINVAL);

    ret = av_buffer_realloc(buf, size + AV_INPUT_BUFFER_PADDING_SIZE);
    if (ret < 0)
	return ret;

    memset((*buf)->data + size, 0, AV_INPUT_BUFFER_PADDING_SIZE);

    return 0;
}

/**
 * Allocate the payload of a packet and initialize its fields with
 * default values.
 *
 * @param pkt packet
 * @param size wanted payload size
 * @return 0 if OK, AVERROR_xxx otherwise
 */
int av_new_packet(AVPacket *pkt, int size)
{
    AVBufferRef *buf = NULL;
    int ret = packet_alloc(&buf, size);
    if (ret < 0)
	return ret;

    av_init_packet(pkt);
    pkt->buf      = buf;
    pkt->data     = buf->data;
    pkt->size     = size;

    return 0;
}

和av_packet_alloc()明显不同的就是av_new_packet()通过调用av_buffer_realloc()分配了实际的payload。这里需要注意的是,虽然av_buffer_realloc()函数 可以重复调用而不会出现内存泄漏情况,但是不意味着可以重复调用av_new_packet(),否则会内存泄漏。下面通过vaapi encode中的一个代码片段来了解一下av_new_pakcet()接口的常见用法。

for(buf = buf_list; buf; buf = buf->next){
	
	av_new_packet(pkt, buf->size);
	
	memcpy(pkt->data, buf->buf, buf->size);
}

AVPacket对象的其他接口都是比较容易理解的,下面再贴出了两个常用的接口.

void av_packet_unref(AVPacket *pkt)
{
    av_packet_free_side_data(pkt);
    av_buffer_unref(&pkt->buf);
    av_init_packet(pkt);
    pkt->data = NULL;
    pkt->size = 0;
}

int av_packet_ref(AVPacket *dst, const AVPacket *src)
{
    int ret;

    ret = av_packet_copy_props(dst, src);
    if (ret < 0)
	return ret;

    if (!src->buf) {
	ret = packet_alloc(&dst->buf, src->size);
	if (ret < 0)
	    goto fail;
	if (src->size)
	    memcpy(dst->buf->data, src->data, src->size);

	dst->data = dst->buf->data;
    } else {
	dst->buf = av_buffer_ref(src->buf);
	if (!dst->buf) {
	    ret = AVERROR(ENOMEM);
	    goto fail;
	}
	dst->data = src->data;
    }

    dst->size = src->size;

    return 0;
fail:
    av_packet_free_side_data(dst);
    return ret;
}

显然,avPackte对象和AVFrame对象的ref和unref基本是一样的,都是基于avbuffer对象的ref和unref方法。关于avpacket对象的使用实例,在下面的vaapi硬件编码章节会有更多的介绍。



avfilter


其实一开始接触ffmpeg的时候,我就对其中的avfilter模块非常感兴趣,因为我感觉avfilter模块是ffmpeg中软件设计技术含量最高的部分,架构非常的优美,面向对象的非常到位。消化掌握这个模块,学习其中代码架构的思想和手法,对于自身技术有非常大的提高。

avfilter的软件架构


其实了解过常见的音视频处理开源代码项目后,你会发现各个项目的代码架构和设计思想基本是上类似。比如,这个avfilter模块的架构和gstreamer就非常类似。下面 从对象模型链路创建与管理数据流控制 这三个方面来剖析avfilter的软件架构。


对象模型

功能模块的建模是整个软件架构设计中的核心和难点,对象建模的是否合理直接关系到架构的可行性和代码实现的难以程度。对象的建模无非就是将功能实现的场景类比 为我们现实世界中的一个类似的场景。因为人类的想象和思维构造根本就是完全来源于现实世界的经验,所以电影里所有想象出的外星生物逃不出人类定义的框架,无非就是多了几个眼睛等等。

avfilter模块中核心对象包含有,AVFilter、AVFliterLink、AVFilterPad。这些对象其实可以看成是抽象了现实生活中的音响系统场景,AVFilter代表着如,CD机(source)、功率放大器(前级处理)、音响(sink)等等设备;AVFilterLink代表的就是前面这些设备的连接线,如同轴线等等;我们知道CD机、音响等等设备上都有好多音频接口,AVFilterPad就是代表这些接口的。这样思考之后,是不是觉得avfilter模块一下子亲切好理解多了呢?

/**
 * Filter definition. This defines the pads a filter contains, and all the
 * callback functions used to interact with the filter.
 */
typedef struct AVFilter {
    /**
     * Filter name. Must be non-NULL and unique among filters.
     */
    const char *name;

    ...

    const AVFilterPad *inputs;
    const AVFilterPad *outputs;

    ...

    int (*preinit)(AVFilterContext *ctx);
    int (*init)(AVFilterContext *ctx);
    int (*init_dict)(AVFilterContext *ctx, AVDictionary **options);
    void (*uninit)(AVFilterContext *ctx);
    int (*query_formats)(AVFilterContext *);

    ...

    /**
     * Used by the filter registration system. Must not be touched by any other
     * code.
     */
    struct AVFilter *next;

    int (*process_command)(AVFilterContext *, const char *cmd, const char *arg, char *res, int res_len, int flags);
    int (*init_opaque)(AVFilterContext *ctx, void *opaque);
    int (*activate)(AVFilterContext *ctx);
} AVFilter;

其实AVFilter只是一个对象模板,AVFilterContext才是对象实例。AVFilter描述了所有对象实例的相同部分,下面先贴出AVFilterContext的定义,然后通过比较模板和实例的区别来剖析。

/** An instance of a filter */
struct AVFilterContext {
    const AVClass *av_class;        ///< needed for av_log() and filters common options

    const AVFilter *filter;         ///< the AVFilter of which this is an instance

    char *name;                     ///< name of this filter instance

    AVFilterPad   *input_pads;      ///< array of input pads
    AVFilterLink **inputs;          ///< array of pointers to input links
    unsigned    nb_inputs;          ///< number of input pads

    AVFilterPad   *output_pads;     ///< array of output pads
    AVFilterLink **outputs;         ///< array of pointers to output links
    unsigned    nb_outputs;         ///< number of output pads

    void *priv;                     ///< private data for use by the filter

    struct AVFilterGraph *graph;    ///< filtergraph this filter belongs to

    ...

    /**
     * An opaque struct for libavfilter internal use.
     */
    AVFilterInternal *internal;

    struct AVFilterCommand *command_queue;

    ...

    AVBufferRef *hw_device_ctx;

    ...

    /**
     * Ready status of the filter.
     * A non-0 value means that the filter needs activating;
     * a higher value suggests a more urgent activation.
     */
    unsigned ready;
};

从上面两个结构体定义可以看出,AVFilter模板对象中两个AVFilterPad对象字段说明所有的AVFilter实例都有接口属性,AVFilterPad在AVFilter中属于属性,而在AVFilterContext中只是一个容器,除了接口这个容器,对应每个接口AVFilterContext中还有AVFilterLink容器。这些容器在后面将要讲到的链路创建与管理部分发挥核心作用。

比较容易混淆和弄不清的是AVFilter对象自身提供的方法和其属性AVFilterPad提供的方法的区别,下面先贴出AVFilterPad对象定义:

/**
 * A filter pad used for either input or output.
 */
struct AVFilterPad {
   
    const char *name;

    /**
     * AVFilterPad type.
     */
    enum AVMediaType type;
   
    AVFrame *(*get_video_buffer)(AVFilterLink *link, int w, int h);
    AVFrame *(*get_audio_buffer)(AVFilterLink *link, int nb_samples);
    int (*filter_frame)(AVFilterLink *link, AVFrame *frame);
    int (*poll_frame)(AVFilterLink *link);
    int (*request_frame)(AVFilterLink *link);
    int (*config_props)(AVFilterLink *link);

    int needs_fifo;
    int needs_writable;
};

和AVFilter、AVFilterContext、AVFilterLink定义在外部头文件中不同的是,AVFilterPad对象定义在内部头文件internal.h中。AVFilter对象自身提供的方法和其属性AVFilterPad提供的方法的主要区别是,后者的方法只用来管理数据的流动,因为我们知道音响设备上的接口主要功能也就是传输数据流,而且不同的接口传输数据流的格式和方法都不一样,所以这些方法定义在AVFilter这个通用模板上肯定不合适。这样一来,前者中定义的方法显然就是所有设备都需要支持或者说都具有的功能属性。

AVFilterLink对象用来描述两个设备之间的链路信息,链路创建与管理和数据流控制的关键信息都存储在其中。

/**
 * A link between two filters. This contains pointers to the source and
 * destination filters between which this link exists, and the indexes of
 * the pads involved. In addition, this link also contains the parameters
 * which have been negotiated and agreed upon between the filter, such as
 * image dimensions, format, etc.
 *
 * Applications must not normally access the link structure directly.
 * Use the buffersrc and buffersink API instead.
 * In the future, access to the header may be reserved for filters
 * implementation.
 */
struct AVFilterLink {
    AVFilterContext *src;       ///< source filter
    AVFilterPad *srcpad;        ///< output pad on the source filter
    AVFilterContext *dst;       ///< dest filter
    AVFilterPad *dstpad;        ///< input pad on the dest filter
    enum AVMediaType type;      ///< filter media type

    ...

    /**
     * Graph the filter belongs to.
     */
    struct AVFilterGraph *graph;

    ...

    int frame_wanted_out;

    AVBufferRef *hw_frames_ctx;

#ifndef FF_INTERNAL_FIELDS
    char reserved[0xF000];

#else /* FF_INTERNAL_FIELDS */

    /**
     * Queue of frames waiting to be filtered.
     */
    FFFrameQueue fifo;
    int frame_blocked_in;
    int status_in;
    int64_t status_in_pts;
    int status_out;

#endif /* FF_INTERNAL_FIELDS */

};

至此,avfilter模块中的对象模型基本剖析完成了。


链路创建与管理

avfilter中的链路创建主要使用硬绑定方法,支持手动和解析命令行自动创建链路。无论哪种方式创建,都需要调用核心接口avfilter_link()。

int avfilter_link(AVFilterContext *src, unsigned srcpad,
		  AVFilterContext *dst, unsigned dstpad)
{
    AVFilterLink *link;

    ...

    link = av_mallocz(sizeof(*link));
    if (!link)
	return AVERROR(ENOMEM);

    src->outputs[srcpad] = dst->inputs[dstpad] = link;//[0]

    link->src     = src;
    link->dst     = dst;
    link->srcpad  = &src->output_pads[srcpad];
    link->dstpad  = &dst->input_pads[dstpad];
    link->type    = src->output_pads[srcpad].type;
    av_assert0(AV_PIX_FMT_NONE == -1 && AV_SAMPLE_FMT_NONE == -1);
    link->format  = -1;
    ff_framequeue_init(&link->fifo, &src->graph->internal->frame_queues);

    return 0;
}

上面[0]处的绑定可以想象为是将音频线(link)两头分别插进两个待链接设备的接口,低下的对于这个”音视频线”的初始化其实应该是在插入前完成的,可以想象成是 这个线是根据连接的设备现场制作的。

手段调用接口创建链路比较麻烦,下面主要介绍自动创建实现的细节。先创建一个最简单的链路如下:

input --> hflip -->output

下面贴出创建上面这个链路需要调用的接口流程:

AVFilterContext *buffersink_ctx, *buffersrc_ctx;


avfilter_register_all();

AVFilterGraph *filter_graph = avfilter_grahp_alloc();

/**
 * 创建buffer这个filter实例,并放进graph的filter容器中。
 */
char args[512];
AVFilter *buffersrc = avfilter_get_by_name("buffer");
snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:
		pixel_aspect=%d/%d",w,h,AV_PIX_FMT_YUV420P,1,25,1,1);
avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph);


/**
 * 创建buffersink实例,并放进graph的filter容器中。
 */
AVFilter *buffersink = avfilter_get_by_name("buffersink"); 
enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE};
buffersink_params = av_buffersink_params_alloc();
buffersink_params->pixel_fmts = pix_fmts;
avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, buffersink_params, filter_graph);
av_free(buffersink_params);

/**
 * 创建两个用于构建链路的节点(endpoint)对象实例,任何的链路都必须有'in'和'out'
 * 这两个endpoint。
 */
AVFilterInOut *outputs = avfilter_inout_alloc();
AVFilterInOut *inputs  = avfilter_inout_alloc();

outputs->name		= av_strdup("in");
outputs->filter_ctx	= buffersrc_ctx;
outputs->pad_idx	= 0;
outputs->next		= NULL;

inputs->name		= av_strdup("out");
inputs->filter_ctx 	= buffersink_ctx;
inputs->pad_idx		= 0;
inputs->next		= NULL;

/**
 * 自动创建hflip实例并构建最终的链路。
 */
const char *filter_desc = "hflip";
avfilter_graph_parse_ptr(filter_graph, filter_desc, &inputs, &outputs, NULL);

/**
 * 配置创建的链路。
 */
avfilter_graph_config(filter_graph, NULL);

上面代码中涉及到两个新的对象,AVFilterInOut和AVFilterGraph,前者对于自动创建链路非常重要,可以把它想象为是音响设备上接口出的标签, 贴上标签后,就可以对照着相应的标签轻松地连接音频线了。后者比较容易理解,描述的就是整个链路的信息。

从上面代码也可以看出,avfilter_graph_parse_ptr()函数是完成链路构建的主要接口,在剖析这个函数前,我们再举个复杂点的例子来说明AVFilterInout这个对象 对于链路构建的重要性。

		[main]
input --> split ------------------------------> overlay --> output
	     |					    ^	
	     | [tmp]			     [flip] |	
	     + -----> crop ---> vflip -------------+

要构建上面这个比较复杂的链路,在上面的代码流程中只要简单的修改掉描述字符串filter_desc即可。

filter_desc = "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2"

上面的字符串中,分号;区分的是一条条独立的线性链路(linear chain),连接各个线性链路靠的就是方括号[]代表的标签,而在一个线性链路的内部是通过逗号,来分隔两个需要连接的filters。

分析过avfilter_graph_parse_ptr()代码可知,上面的字符串是有所省略的,完整的字符串描述应该如下:

[in]split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2[out]

因为任何的链路都需要[in]和[out]这两个标签,所以avfilter_graph_parse_ptr()会内部默认自动添加上这两个标签。知道这两个默认标签的存在后,我们应该很好理解上面代码中如下片段的作用了:

outputs->name		= av_strdup("in");
outputs->filter_ctx	= buffersrc_ctx;
outputs->pad_idx	= 0;
outputs->next		= NULL;

inputs->name		= av_strdup("out");
inputs->filter_ctx 	= buffersink_ctx;
inputs->pad_idx		= 0;
inputs->next		= NULL;

上面代码的作用就是会将标签名为”in”的buffer filter的输出了解到split的输入,将标签名为”out”的buffersink filter的输入连接到overlay的输出。

了解这些细节后,我们就可以深入剖析构建链路的核心函数avfilter_graph_parse_ptr()了。为了简单起见,下面代码省去了创建默认[in][out]标签的部分。

int avfilter_graph_parse_ptr(AVFilterGraph *graph, const char *filters,
			 AVFilterInOut **open_inputs_ptr, AVFilterInOut **open_outputs_ptr,
			 void *log_ctx)
{
    int index = 0, ret = 0;
    char chr = 0;

    AVFilterInOut *curr_inputs = NULL;
    AVFilterInOut *open_inputs  = open_inputs_ptr  ? *open_inputs_ptr  : NULL;
    AVFilterInOut *open_outputs = open_outputs_ptr ? *open_outputs_ptr : NULL;

    if ((ret = parse_sws_flags(&filters, graph)) < 0)
	goto end;

    do {
	AVFilterContext *filter;
	const char *filterchain = filters;
	filters += strspn(filters, WHITESPACES);

	if ((ret = parse_inputs(&filters, &curr_inputs, &open_outputs, log_ctx)) < 0)//[0]
	    goto end;

	if ((ret = parse_filter(&filter, &filters, graph, index, log_ctx)) < 0)//[1]
	    goto end;

	...

	if ((ret = link_filter_inouts(filter, &curr_inputs, &open_inputs, log_ctx)) < 0)//[2]
	    goto end;

	if ((ret = parse_outputs(&filters, &curr_inputs, &open_inputs, &open_outputs,
				 log_ctx)) < 0)//[3]
	    goto end;

	filters += strspn(filters, WHITESPACES);
	chr = *filters++;
	
	...
	
	index++;
    } while (chr == ',' || chr == ';');//[4]

    ...
    
}

除了对于缩放filter的特殊处理外,整个函数就是一个while循环,从[4]处的循环控制条件可见,链路创建是一次处理一个filter的。[0][1][3]处分别处理一条linear chain中处于开头的filter、中间的filter以及最后的filter。比如,上面split和crop就是处于开头的filter,通过[0]处的parse_inputs建立连接的,并且依靠的是[tmp]这个标签;crop和vflip属于处于中间的filter,通过[2]处的link_filter_inouts建立连接;



ffmpeg硬解(vaapi-decode)流程分析


本章节主要关注的是ffmpeg中硬件解码h264的流程分析,而其中的解码算法细节实现暂不展开。已故雷神雷宵铧的博客中关于ffmpeg的h264解码已经比较详细的分析, 但是不同版本的ffmpeg解码部分函数实现差别很大,其博客中的一些函数分析已经过时,而且没有涉及硬件解码的情况。但是雷神文章中对于ffmpeg解码的总体流程分析还是很有指导意义的,对雷神 的钻研精神表示敬意。

本文都是基于较新的v3.3.3版本的ffmpeg进行代码分析。


主体流程


ffmpeg中使用AVCodec结构体描述解码器对象,其中h264解码器为实现在src/libavcodec/h264dec.c中的ff_h264_decoder实例。无论是软解还是硬解,解码流程的入口都是avcodec_decodec_video2()这个API接口调用ff_h264_decoder中注册的h264_decode_frame()方法。

/*libavcodec/h264dec.c*/
static int h264_decode_frame(AVCodecContext *avctx, void *data, int *get_frame, AVPacket *avpkt)
{
	...
	
	/**
	 * end of stream, output what is still in the buffers
	 */
	if(buf_size == 0)
		return send_next_delayed_frame(h, pict, got_frame, 0);
		
	...
	
	decode_nal_units(h, buf, buf_size);//[0]
	 
	...
	
	ff_h264_field_end(h, &h->slice_ctx[0], 0);//[1]
	
	/**
	 * wait for second field.
	 */

	if(h->next_output_pic)
		finalize_frame(h, pict, h->next_output_pic, got_frame);//[2]
		
	...	
}

上面的主要处理流程是,[0]处进行码流分析,如果是软解就接着进行解码工作,如果是硬件就只设置解码器的参数,然后在[1]处进行实际的硬解码,[2]处是控制 POC,决定输出哪一帧。

/*libavcodec/h264dec.c*/
static int decodec_nal_units(H264Context *h, const uint8_t *buf, int buf_size)
{
	...
	
	/**
	 * 拆分协议信息。
	 */
	ff_h245_packet_split(&h->pkt, buf, buf_size, avctx, ...);
	
	...
	
	/**
	 * 对上面拆分出的各个NALU进行逐个解析。
	 */
	for(i = 0; i < h->pkt.nb_nals; i++)
	{
		...
		
		switch(nal->type){
			case H264_NAL_IDR_SLICE:
			case H264_NAL_SLICE_SCALABLE:
			case H264_NAL_SLICE:
				h->has_slice = 1;
				
				ff_h264_queue_decode_slice(h, nal);//[0]
				
				if(h->current_slice == 1){
					...
					
					/**
					 * avctx中的hwaccel句柄如果有效,说明开启的是vaapi流程。
					 */
					if(h->avctx->hwaccel)
						h->avctx->hwaccel->start_frame(...);	
						
					...	
				}
				
				max_slice_ctx = avctx->hwaccel ? 1 : h->nb_silce_ctx;
				if(h->nb_slice_ctx_queued == max_slice_ctx){
					if(h->avctx->hwaccel){
						/**
						 * 这里只是进行硬件解码器的参数设置。
						 */
						avctx->hwaccel->decoded_slice(...);
						h->nb_slice_ctx_queued = 0;
					}else{
						...
						/**
						 * 如果是软解,调用
						 * ff_h264_execute_decode_slice()进行解码。
						 */
					}
				}
				
				break;
				
			case H264_NAL_SEI:
				/**
				 * 解析SEI信息。
				 */
				 ff_h264_sei_decode(&h->sei, &nal->gb, &h->ps, avctx);
				
			case H264_NAL_SPS:
				/**
				 * 解析sps信息。
				 */
				ff_h264_decode_seq_parameter_set(&tmp_gb, avctx, &h->ps, 0);
				...
			break;
			
			case H264_NAL_PPS:
				/**
				 * 解析PPS信息。
				 */
				ff_h264_decode_picture_parameter_set(...);
				...
			break;
			
			case xxx:
				...
			
		}
		
		/**
		 * error handle
		 */
		 ...
	}
}

上面处理中,主要分成两个阶段,一个是解析非slice类型的NALU,包括PPS,SPS,SEI等,另外一个就是解析slice。而解析非slice类型NALU也是 为了解析slice服务的。上面[0]处就是解析slice,实现比较复杂,涉及到帧解码、场解码的兼容;根据不同POC类型计算POC值等等。

当slice信息也解析完成后,最终解析码流的所需要的所有信息已经拿到了,下面就是开启硬件解码器(hwaccel->start_frame()),并将解析的信息按需传入 (hwaccel->decoded_slice())。

下面先分析[0]处的ff_h264_queue_decode_slice()函数实现。

int ff_h264_queue_decode_slice(H264Context *h, H2645NAL *nal)
{
	H264SliceContext *sl = h->slice_ctx + h->nb_slice_ctx_queued;
	int first_slice = sl = h->slice_ctx && !h->current_slice;
	
	...
	
	if(nal->type == H264_NAL_SLICE_SCALABLE && nal->svc_ext_flag == 1)
		h264_slice_header_in_scalable_ext_parse(h, sl, nal);//[0]
	else
		h264_slice_header_parse(h, sl, nal);//[1]
		
	...
	
	if(sl->first_mb_addr == 0){
		if(h->current_slice){
			/**
			 * 场解码处理
			 */
			...
		}
		/**
		 * 场解码处理
		 */
		...
	}
	
	if(!first_slice){
		/**
		 * 场解码中pps和sps一致性检测。
		 */
		...
	}
	
	if(h->current_slice == 0){
		/**
		 * 帧解码处理、或者是场解码中上半场处理。
		 */
		h264_field_start(h, sl, nal, first_slice);//[3]
	
	}else{
		/**
		 * 场解码中一致性检测。
		 */
		...
	}
	
	h264_slice_init(h, sl, nal);//[4]

	h->nb_slice_ctx_queued++;
	
	return 0;
}

上面代码中[0],[1]功能类似,都是解析slice的头信息,解析时结合起前面解析出的pps和sps信息,计算出frame_num;如果poc_type是0的话,需要计算出poc_lsb,以及qp相关的信息。

上面很多逻辑都是针对于场解码的,这部分细节暂不展开。从[3]处的代码可以看出,h264_field_start()兼容处理了帧解码和场解码情况,因为可以将帧解码看成是场解码的一种特殊情况,通过这样的抽象可以提高代码的复用程度,这时软件实现中常用的技巧。下面就来看看h264_field_start()函数的具体实现:

/**
 * This function is called right after decoding the slice header for a first
 * slice in a field(or a frame). It decides whether we are decoding a new frame
 * or a second field in a pair and does the necessary setup
 */
static int h264_field_start(...)
{
	...
	
	if(h->poc.frame_num != h->poc.prev_frame_num){
		...
	}
	
	if(h->first_filed){
		...
	}
	
	...

	if(h->first_field){
	
	}else{
		/*frame or first field in a potentially complementary pair*/
		h->first_field = FIELD_PICTURE(h);
	}
	
	if(!FIELD_PICTURE(h) || h->first_field){
		h264_frame_start(h);//[0]
	}else{
		...
	}
	
	...
	
	ff_h264_init_poc(...);//[1]
	
	...
	
	if(!FIELD_PICTURE(h) || !h->first_field || h->missing_fileds > 1){
		...
		
		h264_select_output_frame(h);//[2]
	}
	
	return 0;
}

上面代码中仍然很多逻辑控制都是针对于场解码的,而[0]处是只针对于帧解码的,[1][2]处是帧解码和场解码通用的处理。[1]处从函数名称就可以看出,是计算poc的,具体实现就是根据poc_type的类型来区分计算,细节可以参考h264的ITU。上面代码功能的重点是[0]处申请解码输出内存,以及[2]处对于解码后帧是否输出、以及顺序的控制。下面分别分析这两个函数的实现。

static int h264_frame_start(H264Context *h)
{
	...
	
	release_unused_pictures(h, 1);//[0]
	h->cur_pic_ptr = NULL;
	
	i = find_unused_picture(h);
	
	pic = &h->DPB[i];
	
	...
	
	/**
	 * Zero key_frame here; IDR markings per slice in frame or fields are ORed in later.
	 */
	pic->f->key_frame = 0; 
	...
	
	alloc_picture(h, pic);//[1]
	
	h->cur_pic_ptr = pic;
	ff_h264_unref_picture(h, &h->cur_pic);
	
	...
	
	ff_h264_ref_picture(h, &h->cur_pic, h->cur_pic_ptr);
	
	...
	

	h->cur_pic_ptr->reference = 0;
	
	h->next_output_pic = NULL;

	...
	
	return 0;
}

上面代码主要是通过[1]处申请解码输出的内存,对于硬件解码,输出内存为GPU的内存。申请出的内存会作为参考帧由DPB管理,DPB是在解码器初始化时申请的, 关于解码器的初始化,后面有单独小节详细分析。下面先来看看alloc_picture()的实现:

static int alloc_picture(H264Context *h, H264Picture *pic)
{
	...
	
	pic->tf.f = pic->f;
	/**
	 * 在硬件解码中,该函数会调用我们在初始化时注册的申请GPU内存的
	 * 的回调函数avctx->get_buffer2.
	 */
	ff_thread_get_buffer(h->avctx, &pic->tf, pic->reference ? AV_GET_BUFFER_FLAGS_REF:0);//[0]
	
	...
	
	/**
	 * 分配硬件解码vaapi对象,该对象在vaapi_h264.c文件中注册为ff_h264_vaapi_hwaccel。
	 * 这个vaapi对象作为h264解码器对象的priv域,相对于一个后端引擎。
	 * 这是软件分层设计的常规手法。
	 */
	if(h->avctx->hwaccel){
		const AVHWAccel *hwaccel = h->avctx->hwaccel;
		if(hwaccel->frame_priv_data_size){
			pic->hwaccel_priv_buf = av_buffer_allocz(hwaccel->frame_priv_data_size);
			
			pic->hwaccel_picture_private = pic->hwaccel_priv_buf->data;
		}
	}
	
	...
	
	return ret;
}

上面代码主要是申请GPU内存,以及创建vaapi解码对象。[0]处最终会调用get_buffer_internal()函数,其实现如下:

static int get_buffer_internal(AVCodecContext *avctx, AVFrame *frame, int flags)
{
	const AVHWAccel *hwaccel = avctx->hwaccel;
	
	...
	
	if(hwaccel){
		if(hwaccel->alloc_frame){
			hwaccel->alloc_frame(avctx, frame);//[0]
		}
	}else{
		avctx->sw_pix_fmt = avctx->pix_fmt;
	}
	
	avctx->get_buffer2(avctx, frame, flag);//[1]
		
	...
	
	return ret;
}

因为上面代码中[0]处在vaapi_h264对象中并没有定义,所以这个函数的功能主要就是[1]处调用avctx->get_buffer2方法。依据GPU的不同,get_buffer2 的回调实现各异,但是总体流程一致,需要完成的流程如下:

static int va_get_buffer(struct AVCodecContext *avctx, AVFrame *pic, int flags)
{
	AVBufferRef *surf_buf = NULL;
	vaapi_surface_t *surface = NULL;
	
	...
	
	surface = (uint8_t *)(uintptr_t)(surface_id);//[0]
	
	surf_buf = av_buffer_create(NULL, 0, free_surf_buf, surface, AV_BUFFER_FLAG_READONLY);
	
	pic->buf[0] = surf_buf;
	/**
	 * 软解时使用data[0-2]传递解码数据的CPU内存地址,
	 * 硬解时使用data[3]传递解码后GPU内存句柄,即surface_id。
	 */
	pic->data[3] = surface;

	return 0;
}

上面代码[0]处是需要根据不同GPU自己实现的申请GPU内存的方法,然后将描述这块内存的指针和释放内存的方法传入av_buffer_create()接口函数。 最后,重点是将pic->buf和pic->data正确赋值。

到这里,h264_frame_start()函数的所有实现细节基本剖析完成了,下面回过头来分析一下h264_select_output_frame()函数的实现。

h264_select_output_frame()函数是纯逻辑控制,通过之前计算出的poc来调整显示输出帧的顺序,下面只贴出函数中决定解码完成后将要 输出的帧的部分代码:

static int h264_select_output_frame(H264Context *h)
{
	H264Picture *cur = h->cur_pic_ptr;

	...

	pics = 0;
	while(h->delayed_pic[pics])
		pics++;

	h->delayed_pic[pics++] = cur;

	...

	if(!out_of_order || pics > h->avctx->has_b_frames){
	        /**
		 * next_output_pic就是指向了下个要输出的帧,这帧可能是
		 * 之前解码后缓存到h->delayed_pic[]中的,也可能就是上面通过调用
		 * va_get_buffer()刚刚创建的GPU用来存储解码后数据的内存。
		 */
		h->next_output_pic = out;
		
		...
	}
	
	...
}

到这里,ff_h264_queue_decode_slice()函数实现就剖析完成了,之后,对于软解来说直接调用ff_h264_execute_decode_slices()完成 最终实际的解码工作,而对于硬解,需要先传递解码器需要的参数信息来配置解码器,也就是上面分析decode_nal_units()函数调用的 avctx->hwaccel->start_frame()和avctx->hwaccel->decode_slice()两个vaapi_h264对象提供的方法。

关于VAAPI的调用流程,见这篇笔记

vaapi_h264对象中start_frame()和decode_slice()方法对应完成的工作,就是上面这篇笔记中描述的基本流程中的第三点,即填充各种decode buffers。 也就是调用vaapi接口vaCreateBuffer()创建各种buffer,前者创建的buffer类型包含VAPictureParameterBufferType、VAQMatrixBufferType;后者创建的为VASliceParameterBufferType、VASliceDataBUfferType。

到了这里,对于硬件解码来说,解码的前期准备工作算是完成了,下面就是真正开始解码工作,也就是硬件解码引擎真正运作。这部分代码定义在ff_h264_filed_end()函数中,也是h264_decode_frame()接口调用的倒数第二个函数。

int ff_h264_field_end(H264Context *h. H264SliceContext *sl, int in_setup)
{
	AVCodecContext *const avctx = h->avctx;
	
	...
	
	if(avctx->hwaccel){
		avctx->hwaccel->end_frame(avctx);
	}

	...
}

可以看出来,ff_h264_field_end()函数完全是为了硬件解码而设计的,因为软解在之前都已经直接完成了。 vaapi_h264对象中的end_frame()方法完成的是上面这篇笔记中描述的基本流程中的最后一点,即将上面的buffers传输到后端server,让 后端server完成最终的解码。最后一个流程的API调用比较模式化,都是:

vaBeginPicture() -> vaRenderPicture() -> vaEndPicture()

解码最后一步就是输出解码后的帧,这个功能finalize_frame()函数完成。

static int finalize_frame(H264Context *h, AVFrame *dst, H264Picture *out, int *got_frame)
{
	...
	
	output_frame(h, dst, out);
	
	*got_frame = 1;

	...
}

static int output_frame(H264Context *h, AVFrame *dst, H264Picture *srcp)
{
	AVFrame *src = srcp->f;
	
	...
	
	av_frame_ref(dst, src);
	
	...
}

dst是我们在调用avcodec_decode_video2()API时传入的输出帧,av_frame_ref(dst,src)完成了解码帧的输出。 至此,硬件加速解码的主体流程分析完毕,下面回过头来分析一下解码前的初始化流程。下面以函数调用图来总结一下上面的流程分析。

ffmpeg_1


帧内存管理流程


下面先通过一个流程图,总体上说明一下解码时,解码数据的控制与流向。

ffmpeg_0



ffmpeg硬编(vaapi-encode)流程分析



解决ffmpeg解码首帧I帧不输出问题




ffserver


ffserver is a multimedia streaming server for live broadcasts. With it, you can streamover HTTP,RTP and RSTP.

ffserver


配置文件


研究ffserver得从配置文件开始,doc/ffserver.conf 是配置文件的模板。ffserver reads a configuration file containing global options and settings for each stream and feed.

主要的语法规则: The configuration file consists of global options and dedicated sections, which must be introduced by “" on a separate line and must be terminated by a line in the form "”. ARGS is a optional.

下面分别说明配置文件中的各个模块


Feed section

Feed 就是输入源的意思,这个配置模块就是告诉ffserver关于输入源的一些信息。比如这个输入源的名字,这个源数据量的大小,以及哪些地址的客户端可以接受这个源数据。You must use ‘ffmpeg’ to send a live feed to ffserver. In this example, you can type:ffmpeg http://localhost:8090/feed1.ffm. 第一种方式就是使用ffmpeg程序执行命令产生实时的数据流,比如执行ffmpeg -i INPUTFILE http://localhost:8090/feed1.ffm,其中feed1.ffm就是这个源的名字。ffserver can also do time shifting.It means that it can stream any previously recorded live stream.You must specify a path where the feed is stored on disk.You alsa specify the maximum size of the feed,where 0 means unlimited.第二种就是从硬盘中加载已经录制好的源,当然格式必须是.ffm的。这样我们可以写个feed配置块。

feed1


Stream section

在说明stream配置模块前,先说明一下feed和stream的关系。A “live-stream” or “sream” is a resource published by ffserver,and made accessible through the HTTP protocol to cliens. A stream can be connected to a feed, or to a file. In the first case, the published stream is forwarded from the corresponding feed generated by a running instance of ffpmeg.也就是说,stream是对feed的封装。Multiple streams can be connected to the same feed.下面是一个实例图:

graph


stream配置中主要包含三块内容,这个stream是对接的哪个feed;stream的格式;stream中的音视频码流参数。下面是ffserver目前支持的格式:

mpeg MPEG-1 multiplexed video and audio
mpegvideo only MPEG-1 video
mp2 MPEG-2 audio(use AudioCodec to select layer 2 and 3 codec)
ogg ogg format(vorbis audio codec)
rm RealNetworks-compatible stream.Multiplexed audio and video.
ra RealNetworks-compatible stream.Audio only.
mpjpeg Multipart JPEG
jpeg Generate a single JPEG image
mjpeg Generate a M_JPEG stream
asf ASF compatible streaming(Windows Media Player format)
swf Macromedia Flash compatible stream
avi AVI format(MPEG-4 video, MPEG audio sound)


下面是一个配置实例:

stream2


最后,还要一个special stream.用来在网页上监控ffserver的状态信息。

stream3


This page will help us to moniter the server.This page is call with this url:http://localhost:8090/stat.html and you will get this page.执行如下的命令:

ffser_cmd


得到如下的状态:

status


从上图可以看到我们已经成功的创建了一个名为test1.mpg的流,在另外一个终端上执行ffplay http://localhost:8090/test1.mpg或者执行cvlc http://localhost:8090/test1.mpg都可以进行拉流播放。