ijkPlayer源码导读-0.FrameQueue&PacketQueue

748 阅读7分钟

一个播放器的实现,都是先解封装,然后将解封装数据(后面统一称为包数据)放入到队列中,再从队列里取出数据进行解码,解码后的数据(后面统一称为帧数据)放入到等待播放的队列中,再从其中取出数据进行相应的渲染或播放。

所以在ijk(或ffplay,ijk是在ffplay的基础上实现)中实现播放器功能会用到上面说的数据结构,其中PacketQueue用于存储解封装后的包数据(AVPacket),FrameQueue用于存储编码后的帧数据(ijk中音频解码后数据是AVFrame,视频软解码后将AVFrame转换成SDL_VoutOverlay)。解码的核心流程都和这两个数据结构息息相关,因此了解这两个数据结构才能更好的理解ijk的源码。

PacketQueue

首先来看一下PacketQueue,它就是一个链表的结构,链表上的结点是MyAVPacketList。

typedef struct PacketQueue {
    // first_pkt用于快速获取头结点,last_pkt用于快速插入到链表中
    MyAVPacketList *first_pkt, *last_pkt;
    int nb_packets;
    int size;
    int64_t duration;
    int abort_request;
    int serial; // 序列号,一次flush刷新一次
    SDL_mutex *mutex;
    SDL_cond *cond;
    // 复用链表
    MyAVPacketList *recycle_pkt;
    int recycle_count;
    int alloc_count;

    int is_buffer_indicator;
} PacketQueue;

typedef struct MyAVPacketList {
    AVPacket pkt;
    struct MyAVPacketList *next;
    int serial;
} MyAVPacketList;
put操作

从代码可以看出,AVPacket的写入是线程安全的,写入操作也比较简单,逻辑大致如下:

  • 判断复用队列里面是否有可用结点,如果可用,那么取出来进行复用

  • 如果队列为空,申请一个新的结点

  • 判断当前加入的数据是否为刷新标志,如果是的话更新序列号serial

  • 判断last结点是否为空,如果为空,说明当前队列都是空的,直接将first结点赋给当前结点

  • 如果不为空,添加到last结点后面,并将新的结点设置为last结点

    可以看到put操作过程中是没有做队列大小限制的,那是不是就可以一直加载,一个视频文件那么大内存够么?从代码层面看的确是可以一直去put的,要避免oom还需要外部逻辑控制,就比如说它统计了缓存的时长与缓存的数据量大小,ijk可以通过配置参数对其进行控制,从而进行AVPacket队列大小进行控制。

static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
    int ret;

    SDL_LockMutex(q->mutex);
    ret = packet_queue_put_private(q, pkt);
    SDL_UnlockMutex(q->mutex);

    if (pkt != &flush_pkt && ret < 0)
        av_packet_unref(pkt);

    return ret;
}

static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
    MyAVPacketList *pkt1;

    if (q->abort_request)
       return -1;

#ifdef FFP_MERGE
    pkt1 = av_malloc(sizeof(MyAVPacketList));
#else
    pkt1 = q->recycle_pkt;
    if (pkt1) {
        // 取出复用链表第一个结点
        q->recycle_pkt = pkt1->next;
        q->recycle_count++;
    } else {
        // 申请结点
        q->alloc_count++;
        pkt1 = av_malloc(sizeof(MyAVPacketList));
    }
#ifdef FFP_SHOW_PKT_RECYCLE
    int total_count = q->recycle_count + q->alloc_count;
    if (!(total_count % 50)) {
        av_log(ffp, AV_LOG_DEBUG, "pkt-recycle \t%d + \t%d = \t%d\n", q->recycle_count, q->alloc_count, total_count);
    }
#endif
#endif
    if (!pkt1)
        return -1;
    pkt1->pkt = *pkt;
    pkt1->next = NULL;
    //一次flush序列加一
    if (pkt == &flush_pkt)
        q->serial++;
    pkt1->serial = q->serial;

    if (!q->last_pkt)
        q->first_pkt = pkt1;// 如果队列为空
    else
        q->last_pkt->next = pkt1; //添加到队列尾部
    q->last_pkt = pkt1;
    q->nb_packets++;
    // 统计占用大小
    q->size += pkt1->pkt.size + sizeof(*pkt1);
    //增加pkt的时长,用于计算缓存时长
    q->duration += FFMAX(pkt1->pkt.duration, MIN_PKT_DURATION);

    /* XXX: should duplicate packet data in DV case */
    SDL_CondSignal(q->cond);
    return 0;
}
get操作
  • 判断队列是否为空,如果不为空,取出first结点,将first指向当前的first->next
  • 如果成功取出结点,将当前结点添加到复用链表的第一个结点中,以便复用
  • 这里所谓的复用是复用的”壳“,核心内容AVPacket还是在put的时候传入
  • 当队列为空时,根据外部传入参数决定是否阻塞等待生产数据。
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
{
    MyAVPacketList *pkt1;
    int ret;

    SDL_LockMutex(q->mutex);

    for (;;) {
        if (q->abort_request) {
            ret = -1;
            break;
        }
        pkt1 = q->first_pkt;
        //队列不为空
        if (pkt1) {
            q->first_pkt = pkt1->next;
            if (!q->first_pkt)
                q->last_pkt = NULL;
            q->nb_packets--;
            q->size -= pkt1->pkt.size + sizeof(*pkt1);
            //减去当前pkt的时间
            q->duration -= FFMAX(pkt1->pkt.duration, MIN_PKT_DURATION);
            *pkt = pkt1->pkt;
            //更新Decoder的pkt_serial变量,防止突然的seek导致依然解码上段序列的数据
            if (serial)
                *serial = pkt1->serial;
#ifdef FFP_MERGE
            av_free(pkt1);
#else
            // 添加到复用链表的第一个结点中
            pkt1->next = q->recycle_pkt;
            q->recycle_pkt = pkt1;
#endif
            ret = 1;
            break;
        } else if (!block) {
            ret = 0;
            break;
        } else {
            //等待生产数据
            SDL_CondWait(q->cond, q->mutex);
        }
    }
    SDL_UnlockMutex(q->mutex);
    return ret;
}

FrameQueue

FrameQueue用于存储Frame,而Frame是对解码后的AVFrame(音频使用),AVSubtitle(字幕使用),SDL_VoutOverlay(视频使用)的一个封装。本身是一个数组,通过控制读写index实现一个环形的队列。

typedef struct Frame {
    AVFrame *frame;
    AVSubtitle sub;
    int serial;
    double pts;           /* presentation timestamp for the frame */
    double duration;      /* estimated duration of the frame */
    int64_t pos;          /* byte position of the frame in the input file */
#ifdef FFP_MERGE
    SDL_Texture *bmp;
#else
    SDL_VoutOverlay *bmp;// 绘制操作数据结构
#endif
    int allocated;
    int width;
    int height;
    int format;
    AVRational sar;
    int uploaded;
} Frame;

//环形队列
typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];
    int rindex;//当前读取位置
    int windex;//当前写入位置
    int size;//当前队列大小
    int max_size;//队列最大容量
    int keep_last;//是否保留上一个可读节点
    int rindex_shown;//当前节点是否已经展示
    SDL_mutex *mutex;
    SDL_cond *cond;
    PacketQueue *pktq;
} FrameQueue;
初始化

队列中会复用AVFrame,初始化过程会将队列用到的AVFrame都申请出来。

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;
    for (i = 0; i < f->max_size; i++)
        if (!(f->queue[i].frame = av_frame_alloc()))//会将队列用到的AVFrame都申请出来
            return AVERROR(ENOMEM);
    return 0;
}
push操作

FrameQueue的写入是分为三步的:首先frame_queue_peek_writable,获取出可读的Frame,也就是数组中windex所在的Frame,然后向Frame中填充数据,最后frame_queue_push,将可写的index往前移或回到0。

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;
    // 返回当前可写index
    return &f->queue[f->windex];
}

static void frame_queue_push(FrameQueue *f)
{
  	// 当前可写index前移,如果该index与数组大小相等,重置为0
    if (++f->windex == f->max_size)
        f->windex = 0;
    SDL_LockMutex(f->mutex);
  	// 修改数量
    f->size++;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

下面看一个例子,解码audio时是如何使用FrameQueue的。

static int audio_thread(void *arg)
{
    FFPlayer *ffp = arg;
    VideoState *is = ffp->is;
    AVFrame *frame = av_frame_alloc();
    Frame *af;
  	// 解码获取frame
  	got_frame = decoder_decode_frame(ffp, &is->auddec, frame, NULL)
  	.................
    if (!(af = frame_queue_peek_writable(&is->sampq)))// 获取Frame
      goto the_end;
    // 填充数据
    af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
    af->pos = frame->pkt_pos;
    af->serial = is->auddec.pkt_serial;
    //时长
    af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});
    // 将frame数据copy到af->frame中
    av_frame_move_ref(af->frame, frame);
    // 前移windex
    frame_queue_push(&is->sampq);
    .................
}
get操作

FrameQueue的获取操作和写入类似,也是分为多步:先调用frame_queue_peek_readable获取出可用数据,再调用frame_queue_next去修改rindex。需要注意的是FrameQueue中的keep_last字段,音频和视频这个字段都是传入的1,也就是需要保存上一帧的数据,主要在视频播放的时候会用上。

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];
}

static void frame_queue_next(FrameQueue *f)
{
    // 音频和视频的keep_last为1
    if (f->keep_last && !f->rindex_shown) {
        f->rindex_shown = 1;
        return;
    }
    // 释放上一帧的AVFrame引用的数据
    // 只有音频帧会被回收,视频的AVFrame在构造数据时自己回收
    frame_queue_unref_item(&f->queue[f->rindex]);
    // 更新rindex
    if (++f->rindex == f->max_size)
        f->rindex = 0;
    SDL_LockMutex(f->mutex);
    f->size--;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

ijk中使用的实例:音频播放前取出音频帧进行重采样。

static int audio_decode_frame(FFPlayer *ffp)
{
    VideoState *is = ffp->is;
    int data_size, resampled_data_size;
    int64_t dec_channel_layout;
    av_unused double audio_clock0;
    int wanted_nb_samples;
    Frame *af;
reload:
    do {
        if (!(af = frame_queue_peek_readable(&is->sampq)))
            return -1;
        frame_queue_next(&is->sampq);
    } while (af->serial != is->audioq.serial);
}

结语

以上是个人见解,如果错误或不同见解,欢迎指正和交流。

前些年粗略看过一遍ijk的源码,并按每个线程的处理逻辑总结过大致流程,但是现在翻开感觉已经忘得差不多了,后面我将会按播放视频中一些过程(如播放器初始化,视频软/硬解,音频播放,暂停/恢复,缓冲等)进行源码分析,到时候会同步更新出来。下一期应该会是下面这张图的内容。

image-20230412213544371.png