秋日的天气虽凉,但在下仍能穿短袖敲键盘,尤其是在猫猫尿床之后。
一、想要混代码圈最重要三点是什么?封装,封装,还是封装~
Frame结构体主要是为了对ffmpeg原本的AVFrame结构体进行二次封装,由于音频、视频、字幕流都是存储在这里面,所以Frame里面的部分变量只会在部分的流生效(例如width height在音频流时不会生效,毕竟音频没有宽高的说法)。
typedef struct Frame
{
AVFrame *frame; //存储数据帧
AVSubtitle sub; //字幕
int serial; //播放序列
double pts; //时间戳
double duration; //该帧的播放时长
int64_t pos; //该帧位于文件的字节位置,方便seek使用
int width; //图片宽度
int height; //图片高度
int format; //帧携带的数据格式
//视频的格式范围在 enum AVPixelFormat内,
//音频的格式范围在 enum AVSampleFormat内,
AVRational sar; //图像宽高比例,最常见的是 4:3 16:9
int uploaded; //记录是否显示过
int flip_v; //是否需要旋转播放,置为1是180旋转
} Frame;
FrameQueue结构体,把每一个Frame给串起,这样显示线程就知道下一个该轮到哪个小可爱被他读取了,排着队被读。FRAME_QUEUE_SIZE这个宏是视频,音频,字幕流里面的最大值,这个值越大,程序运行时开辟的内存空间就会越大。
可以看到 FrameQueue中的队列是以数组方式进行存储的,并不像PacketQueue中采用链表的方式进行存储,为什么要这样做呢?ffplay作者的设计目的为:
1.内存预先分配完成,不需要动态malloc。
2.读取速度快,读写可以同时操作,且不用上锁。
#define VIDEO_PICTURE_QUEUE_SIZE 3
#define SUBPICTURE_QUEUE_SIZE 16
#define SAMPLE_QUEUE_SIZE 9
#define FRAME_QUEUE_SIZE FFMAX(SAMPLE_QUEUE_SIZE, FFMAX(VIDEO_PICTURE_QUEUE_SIZE, SUBPICTURE_QUEUE_SIZE))
typedef struct FrameQueue {
Frame queue[FRAME_QUEUE_SIZE]; //数据帧存储队列
int rindex; //读索引
int windex; //写索引
int size; //总帧数
int max_size; //可存储的最大帧数
int keep_last; //此帧数据是否先保存,保存置为 1
int rindex_shown; //和keep_last在同一个if条件里判断,初始为 0
// f->keep_last && !f->rindex_shown
SDL_mutex *mutex; //互斥锁
SDL_cond *cond; //条件变量
PacketQueue *pktq; //包队列
} FrameQueue;
二、FrameQueue队列操作
初始化队列frame_queue_init(), f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE); 可以看到在给队列赋值的时候,会参考FRAME_QUEUE_SIZE。然后会给队列里每个frame一一申请空间。
static int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int max_size, int keep_last)
{
int i;
memset(f, 0, sizeof(FrameQueue));
if (!(f->mutex = SDL_CreateMutex())) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
return AVERROR(ENOMEM);
}
if (!(f->cond = SDL_CreateCond())) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
return AVERROR(ENOMEM);
}
f->pktq = pktq;
f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);
f->keep_last = !!keep_last; //将int取值的keep_last转换为bool 取值 (0或1)
for (i = 0; i < f->max_size; i++)
if (!(f->queue[i].frame = av_frame_alloc()))
return AVERROR(ENOMEM);
return 0;
}
frame_queue_destory销毁FrameQueue队列,需要先使用frame_queue_unref_item 函数释放对vp->frame中的数据缓冲区的引⽤,再使用av_frame_free函数释放vp->frame的空间。
static void frame_queue_unref_item(Frame *vp)
{
av_frame_unref(vp->frame);
avsubtitle_free(&vp->sub);
}
static void frame_queue_destory(FrameQueue *f)
{
int i;
for (i = 0; i < f->max_size; i++) {
Frame *vp = &f->queue[i];
frame_queue_unref_item(vp);
av_frame_free(&vp->frame);
}
SDL_DestroyMutex(f->mutex);
SDL_DestroyCond(f->cond);
}
frame_queue_signal函数是用来发送唤醒信号的。
static void frame_queue_signal(FrameQueue *f)
{
SDL_LockMutex(f->mutex);
SDL_CondSignal(f->cond);
SDL_UnlockMutex(f->mutex);
}
frame_queue_peek_writable获取可以写的Frame指针
static Frame *frame_queue_peek_writable(FrameQueue *f)
{
/* wait until we have space to put a new frame */
SDL_LockMutex(f->mutex);
while (f->size >= f->max_size &&
!f->pktq->abort_request) {
SDL_CondWait(f->cond, f->mutex);
}
SDL_UnlockMutex(f->mutex);
if (f->pktq->abort_request)
return NULL;
return &f->queue[f->windex];
}
frame_queue_peek_readable获取可读的Frame指针
static Frame *frame_queue_peek_readable(FrameQueue *f)
{
/* wait until we have a readable a new frame */
SDL_LockMutex(f->mutex);
while (f->size - f->rindex_shown <= 0 &&
!f->pktq->abort_request) {
SDL_CondWait(f->cond, f->mutex);
}
SDL_UnlockMutex(f->mutex);
if (f->pktq->abort_request)
return NULL;
return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}
**在FrameQueue队列尾部压⼊⼀帧,函数内只会更新计数与写指针,所以在调⽤frame_queue_push应,需要将Frame的数据先写⼊队列中。**
```c
static void frame_queue_push(FrameQueue *f)
{
if (++f->windex == f->max_size)
f->windex = 0;
SDL_LockMutex(f->mutex);
f->size++;
SDL_CondSignal(f->cond); //可以唤醒读frame_queue_peek_readable
SDL_UnlockMutex(f->mutex);
}
下面三个函数主要用例获取帧,分别是获取当前帧, 获取下⼀帧,以及获取最后一帧,相信各位看官老爷的英文能力。说一下为什么在获取当前帧的时候数组的下标为 (f->rindex + f->rindex_shown) % f->max_size , 首先f->rindex表示当前的读索引,f->rindex_shown表示这个帧是不是被读取过,读取过就表示当前读索引在上一帧,
static Frame *frame_queue_peek(FrameQueue *f)
{
return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}
static Frame *frame_queue_peek_next(FrameQueue *f)
{
return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size];
}
static Frame *frame_queue_peek_last(FrameQueue *f)
{
return &f->queue[f->rindex];
}
获取可以读取的帧,如果程序退出的话会是直接使用返回NULL
static Frame *frame_queue_peek_readable(FrameQueue *f)
{
/* wait until we have a readable a new frame */
SDL_LockMutex(f->mutex);
while (f->size - f->rindex_shown <= 0 &&
!f->pktq->abort_request) {
SDL_CondWait(f->cond, f->mutex);
}
SDL_UnlockMutex(f->mutex);
if (f->pktq->abort_request)
return NULL;
return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}
frame_queue_next出队列,通过frame_queue_unref_item释放frame的引用计数,同时将PackeQueue的队列读索引往后移动一下
static void frame_queue_next(FrameQueue *f)
{
if (f->keep_last && !f->rindex_shown) {
f->rindex_shown = 1;
return;
}
frame_queue_unref_item(&f->queue[f->rindex]);
if (++f->rindex == f->max_size)
f->rindex = 0;
SDL_LockMutex(f->mutex);
f->size--;
SDL_CondSignal(f->cond);
SDL_UnlockMutex(f->mutex);
}
三、总结
FrameQueue队列,写入一个新帧会将写索引加1,但读队列和删除旧帧的是分开来的,可以只读而不更新索引,也可以只更新索引不读取,读队列还引入了是否保留显示的最后一帧的选项。
读队列和写队列步骤的步骤如下:
- 调⽤frame_queue_peek_readable获取可以读取的一帧;
- 需要更新读索引的话,就调用frame_queue_peek_next;