Skip to content

Latest commit

 

History

History
283 lines (229 loc) · 7.37 KB

0x05.md

File metadata and controls

283 lines (229 loc) · 7.37 KB

0x05 渲染线程与 AVFrame 缓存队列

0x04 教程中,我们实现多线程解码,将 AVPacket “转成” 了 AVFrame,这篇教程就来学习下,如何缓存这些AVFrame,然后创建一个渲染的线程去消耗这些 AVFrame ,因此这里又是一个生产者和消费者模式!

思路分析

本篇教程的重点是设计 AVFrame 缓存队列,渲染线程提前介入,是为了模拟流程,持续消耗 AVFrame 而已,因此这里不考虑音视频同步问题,队列里有就消耗。

为了渲染时获取 AVFrame 的逻辑简洁,队列大小可控性高,因此音视频的解码帧进行分开缓存,即音频帧使用一个缓存队列,视频帧使用一个缓存队列。

渲染线程生命周期

1、创建线程(在解码线程开始工作后创建)

//音视频解码线程开始工作
[self.audioDecodeThread start];
[self.videoDecodeThread start];
//准备渲染线程
[self prepareRendererThread];

- (void)prepareRendererThread
{
    self.rendererThread = [[MRThread alloc] initWithTarget:self selector:@selector(rendererThreadFunc) object:nil];
    self.rendererThread.name = @"renderer";
}

2、启动线程(创建好了就开始,赶在循环读包之前)

//渲染线程开始工作
[self.rendererThread start];
//循环读包
[self readPacketLoop:formatCtx];

3、销毁线程,调用 _stop 的时候

[self.rendererThread cancel];
[self.rendererThread join];
self.rendererThread = nil;

4、渲染逻辑

- (void)rendererThreadFunc
{
    ///调用了stop方法,则不再读包
    while (!self.abort_request) {
        
        //队列里缓存帧大于0,则取出
        if (frame_queue_nb_remaining(&sampq) > 0) {
            Frame *ap = frame_queue_peek(&sampq);
            av_log(NULL, AV_LOG_VERBOSE, "render audio frame %lld\n", ap->frame->pts);
            //释放该节点存储的frame的内存
            frame_queue_pop(&sampq);
        }
        
        if (frame_queue_nb_remaining(&pictq) > 0) {
            Frame *vp = frame_queue_peek(&pictq);
            av_log(NULL, AV_LOG_VERBOSE, "render video frame %lld\n", vp->frame->pts);
            frame_queue_pop(&pictq);
        }
        
        usleep(1000 * 40);
    }
}

通过数组实现 AVFrame 缓存队列

1、定义数组元素,存放 AVFrame

typedef struct Frame {
    AVFrame *frame;
    double pts;           /* presentation timestamp for the frame */
} Frame;

2、定义队列

typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];
    int rindex; //读索引
    int windex; //写索引
    int size;   //缓存元素个数
    int max_size;//最大容量
    //
    dispatch_semaphore_t mutex;
    char *name; //队列名字
    //标记为停止
    int abort_request;
} FrameQueue;

3、队列初始化

/*
[0,0,0,0,0,0,0,0]
 |
 windex
 |
 rindex
*/
static __inline__ int frame_queue_init(FrameQueue *f, int max_size, const char *name)
{
    int i;
    memset((void*)f, 0, sizeof(FrameQueue));
    f->name = av_strdup(name);
    f->mutex = dispatch_semaphore_create(1);
    f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);
    
    //填充每个元素的 frame
    for (i = 0; i < f->max_size; i++) {
        if (!(f->queue[i].frame = av_frame_alloc())) {
            return AVERROR(ENOMEM);
        }
    }
    return 0;
}

4、获取一个可写的节点(阻塞等待)

/*
 size=3
 [1,1,1,0,0,0,0,0]
        |
        windex
 |
 rindex
 */
static __inline__ Frame *frame_queue_peek_writable(FrameQueue *f)
{
    /* wait until we have space to put a new frame */
    int ret = 0;
    //加锁
    dispatch_semaphore_wait(f->mutex, DISPATCH_TIME_FOREVER);
    int is_loged = 0;//避免重复打日志
  	//当前大小大于等于最大容量,说明没有空余,需要等待
    while (f->size >= f->max_size) {
        //停止了直接返回
        if (f->abort_request) {
            ret = -1;
            break;
        }
        
        if (!is_loged) {
            is_loged = 1;
            av_log(NULL, AV_LOG_VERBOSE, "%s frame queue is full(%d)\n",f->name,f->size);
        }
        //等待10ms
        dispatch_semaphore_signal(f->mutex);
        usleep(10000);
        dispatch_semaphore_wait(f->mutex, DISPATCH_TIME_FOREVER);
    }
    //解锁
    dispatch_semaphore_signal(f->mutex);
    if (ret < 0) {
        return NULL;
    }
    //获取到了一个可写位置
    Frame *af = &f->queue[f->windex];
    return af;
}

5、移动写指针位置,增加队列里已存储数量

/*
size=4
[1,1,1,1,0,0,0,0]
         |
         windex
|
rindex
*/
static __inline__ void frame_queue_push(FrameQueue *f)
{
    //加锁
    dispatch_semaphore_wait(f->mutex, DISPATCH_TIME_FOREVER);
    
    //写指针超过了总长度时,将写指针归零,指向头部
    if (++f->windex == f->max_size) {
        f->windex = 0;
    }
    //队列已存储数量加1
    f->size ++;
    av_log(NULL, AV_LOG_VERBOSE, "frame_queue_push %s (%d/%d)\n", f->name, f->windex, f->size);
    //解锁
    dispatch_semaphore_signal(f->mutex);
}

6、获取队列里缓存帧的数量

static __inline__ int frame_queue_nb_remaining(FrameQueue *f)
{
    int r = 0;
    dispatch_semaphore_wait(f->mutex, DISPATCH_TIME_FOREVER);
    r = f->size;
    dispatch_semaphore_signal(f->mutex);
    return r;
}

7、获取当前读指针指向的节点

static __inline__ Frame *frame_queue_peek(FrameQueue *f)
{
    return &f->queue[(f->rindex) % f->max_size];
}

8、移动读指针位置,减少队列里已存储数量

static __inline__ void frame_queue_pop(FrameQueue *f)
{
    dispatch_semaphore_wait(f->mutex, DISPATCH_TIME_FOREVER);
    //取出读指针指向的元素
    Frame *vp = &f->queue[f->rindex];
    //释放frame内部引用数据,与av_frame_move_ref对应
    av_frame_unref(vp->frame);
    //后移读指针,如果超出读的范围则归零
    if (++f->rindex == f->max_size){
        f->rindex = 0;
    }
    //缓存大小减1
    f->size--;
    av_log(NULL, AV_LOG_VERBOSE, "frame_queue_pop %s (%d/%d)\n", f->name, f->windex, f->size);
    dispatch_semaphore_signal(f->mutex);
}

9、释放队列内存

static __inline__ void frame_queue_destory(FrameQueue *f)
{
    for (int i = 0; i < f->max_size; i++) {
        Frame *vp = &f->queue[i];
        //释放frame内部引用数据,与av_frame_move_ref对应
        av_frame_unref(vp->frame);
        //释放avframe内存,与init时av_frame_alloc对应
        av_frame_free(&vp->frame);
    }
}

总结

还记得我们在 0x03 教程中实现的 AVPacket 缓存队列吗?AVFrame 缓存队列的设计与 AVPacket 不一样,前者是通过数组实现并且是环形的,后者是使用链表实现的。这么设计有以下几个原因:

  • 缓存的 AVPacket 相比 AVFrame 在数量上比较多,使用数组不太合适
  • AVFrame 支持引用计数的形式管理内存,所以适合预先分配好内存,无需动态申请

AVFrame 缓存队列的优势:

  • 高效读写模型;锁的范围比较小,最好情况下可边读边写,因为此时读写指针指向的是不同的元素
  • 高效内存模型;元素内存提前分配,后续直接ref即可

这篇教程里的 AVFrame 缓存队列设计是 FFPlay 源码 的精简,这个缓存模型设计的很赞!

下一篇为大家介绍如何将视频帧渲染到屏幕上。