在 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);
}
}
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 源码 的精简,这个缓存模型设计的很赞!
下一篇为大家介绍如何将视频帧渲染到屏幕上。