ffplay学习之FrameQueue队列(二)

秋日的天气虽凉,但在下仍能穿短袖敲键盘,尤其是在猫猫尿床之后。

一、想要混代码圈最重要三点是什么?封装,封装,还是封装~

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,但读队列和删除旧帧的是分开来的,可以只读而不更新索引,也可以只更新索引不读取,读队列还引入了是否保留显示的最后一帧的选项。
读队列和写队列步骤的步骤如下:

  1. 调⽤frame_queue_peek_readable获取可以读取的一帧;
  2. 需要更新读索引的话,就调用frame_queue_peek_next;

猜你喜欢

转载自blog.csdn.net/weixin_42764231/article/details/127582874