IJKPLAYER源码分析-主要队列

news/2025/2/22 0:24:10/

 前言

    对IJKPLAYER播放器内核几个关键的队列的理解,将有助于掌控全局。因此,这里简要介绍所涉及到的几个关键队列实现:

  • PacketQueue:压缩数据队列,是一个带有首尾指针和回收单链表头指针的单链表结构,用来实现了队列,包括video、audio和subtitle均用此队列结构存储;
  • FrameQueue:解压数据队列,是一个由数组表达的环形队列,同样包括video、audio和subtitle均用此队列存储;
  • MessageQueue:其结构如同PacketQueue,不再赘述;

PacketQueue

    队列结构

    PacketQueue结构代码定义: 

typedef struct MyAVPacketList {AVPacket pkt;struct MyAVPacketList *next;int serial;
} MyAVPacketList;typedef struct PacketQueue {MyAVPacketList *first_pkt, *last_pkt;int nb_packets;int size;int64_t duration;int abort_request;int serial;SDL_mutex *mutex;SDL_cond *cond;
} PacketQueue;
  • first_pkt:队列头指针,也即单链表的头指针;
  • last_pkt:队列尾指针,也即单链表的尾指针;

  • nb_packets:队列元素个数(AVPacket个数),即first_pkt所指向的单链表元素个数;

  • size:AVPacket *pkt;size += pkt1->pkt.size + sizeof(*pkt1),即该队列所有元素的总字节数,包括AVPacket管理所占空间;

  • duration:#define MIN_PKT_DURATION 15;q->duration += FFMAX(pkt1->pkt.duration, MIN_PKT_DURATION);

  • abort_request:关闭播放器请求,层层传递到队列;

  • serial:主要是针对于点播而言,代表1次不同的seek请求,以此区分seek前后的操作及数据;

队列大小 

    相关PacketQueue队列大小的外围限制源码:

#define MAX_QUEUE_SIZE (15 * 1024 * 1024){ "max-buffer-size",                    "max buffer size should be pre-read",OPTION_OFFSET(dcc.max_buffer_size), OPTION_INT(MAX_QUEUE_SIZE, 0, MAX_QUEUE_SIZE) },
{ "min-frames",                         "minimal frames to stop pre-reading",OPTION_OFFSET(dcc.min_frames),      OPTION_INT(DEFAULT_MIN_FRAMES, MIN_MIN_FRAMES, MAX_MIN_FRAMES) },inline static void ffp_reset_demux_cache_control(FFDemuxCacheControl *dcc)
{dcc->min_frames                = DEFAULT_MIN_FRAMES;dcc->max_buffer_size           = MAX_QUEUE_SIZE;
}

    缺省情况下:

  • audio + video + subtitle队列size大小之和,<= max_buffer_size=15M;
  • audio / video / subtitle各自队列大小都 <= MIN_FRAMES=50000个AVPacket,对于video而言,即50000个帧压缩数据;

    以上2种情形,满足一个条件,队列即满。 

        /* if the queue are full, no need to read more */if (ffp->infinite_buffer<1 && !is->seek_req &&
#ifdef FFP_MERGE(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
#else(is->audioq.size + is->videoq.size + is->subtitleq.size > ffp->dcc.max_buffer_size
#endif|| (   stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq, MIN_FRAMES)&& stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq, MIN_FRAMES)&& stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq, MIN_FRAMES)))) {if (!is->eof) {ffp_toggle_buffering(ffp, 0);}/* wait 10 ms */SDL_LockMutex(wait_mutex);SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);SDL_UnlockMutex(wait_mutex);continue;}

何时继续读

     由上面分析可知,队列满后,会暂停调用av_read_frame读取AVPacket包,而是条件等待10ms,若is->continue_read_thread信号就绪,会返回以继续调用av_read_frame读AVPacket包;或者,10ms超时该信号仍未就绪,也会立刻返回,下一次loop判断队列是否满,若满则继续10ms等待上述逻辑,不然便会调用av_read_frame方法读取下一个AVPacket包。

    那么,continue_read_thread信号何时就绪呢?我们来继续分析:

static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) {memset(d, 0, sizeof(Decoder));d->avctx = avctx;d->queue = queue;d->empty_queue_cond = empty_queue_cond;d->start_pts = AV_NOPTS_VALUE;d->first_frame_decoded_time = SDL_GetTickHR();d->first_frame_decoded = 0;SDL_ProfilerReset(&d->decode_profiler, -1);
}decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);

    在初始化video解码器时,会调用decoder_init方法将 is->continue_read_thread赋值给Decoder的empty_queue_cond信号。Decoder的定义:

typedef struct Decoder {AVPacket pkt;AVPacket pkt_temp;PacketQueue *queue;AVCodecContext *avctx;int pkt_serial;int finished;int packet_pending;int bfsc_ret;uint8_t *bfsc_data;SDL_cond *empty_queue_cond;int64_t start_pts;AVRational start_pts_tb;int64_t next_pts;AVRational next_pts_tb;SDL_Thread *decoder_tid;SDL_Profiler decode_profiler;Uint64 first_frame_decoded_time;int    first_frame_decoded;
} Decoder;

     那么,何时发送is->empty_queue_cond信号呢?其实,是在video的解码线程里,其函数调用栈如下:

ffplay_video_thread => get_video_frame => decoder_decode_frame

    而后,在decoder_decode_frame方法里判断: 

if (d->queue->nb_packets == 0)SDL_CondSignal(d->empty_queue_cond);

    即:PacketQueue队列中的压缩数据,如已消费完毕便会发送该信号,继续av_read_frame读取AVPacket包,并同时开始缓冲。

    值得一提的是,由于audio、video和subtitle都会发射continue_read_thread信号,因此,此3者只要1个消费完毕,即会发送此信号,并开始缓冲。

播放缓冲

    PacketQueue的多少还会涉及到播放状态,若没有数据了,即开始加载,若队列满,则缓冲完毕,开始render播放。

    播放所涉及到的2个缓冲状态:

#define FFP_MSG_BUFFERING_START             500
#define FFP_MSG_BUFFERING_END               501
  • FFP_MSG_BUFFERING_START:无数据了,开始缓冲;
  • FFP_MSG_BUFFERING_END:队列满,缓冲完毕; 

    那么,在哪里通知开始缓冲的呢?有几种情况:

  • 解码时队列消费完毕:
  • seek请求:avformat_seek_file调用前后,开始seek了;

  • 重播:ijkmp_start_l => ffp_start_from_l;

    第1种情况调用栈:

ffplay_video_thread > get_video_frame => decoder_decode_frame => packet_queue_get_or_buffering => ffp_toggle_buffering(ffp, 1);

    第2种情况调用栈:

read_thread => if (is->seek_req) => ffp_toggle_buffering(ffp, 1) =>  avformat_seek_file => ffp_toggle_buffering(ffp, 1)

    第3种情况调用栈:

int ffp_start_from_l(FFPlayer *ffp, long msec)
{assert(ffp);VideoState *is = ffp->is;if (!is)return EIJK_NULL_IS_PTR;ffp->auto_resume = 1;ffp_toggle_buffering(ffp, 1);ffp_seek_to_l(ffp, msec);return 0;
}

     那么,何时通知缓冲完毕呢?有以下情况:

  • 缓冲队列满
  • 播放完毕
  • av_read_frame返回eof,即读取结束
  • 检查buffer状态发现队列满

FrameQueue

主结构

     FrameQueue结构代码定义: 

#define FRAME_QUEUE_SIZE 16
/* Common struct for handling all types of decoded data and allocated render buffers. */
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 */int width;int height;int format;AVRational sar;int uploaded;int flip_v;
} 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;
  •  queue[FRAME_QUEUE_SIZE]:是一个有FRAME_QUEUE_SIZE个Frame的数组,实际是环形队列,用以存储AVFrame,即解码后的video、audio和subtitle数据存在此处;
  • rindex:指向最近一个可读的Frame;
  • windex:指向下一个可写的Frame;
  • size:可display的帧个数;
  • max_size:queue环形队列最多存储FRAME_QUEUE_SIZE个Frame,但实际用多少个Frame的空间,是可以配置的,video缺省是3个Frame;
  • keep_last:此flag仅用于video,因为video显示时需缓存上次已显示的AVFrame,以计算duration,在队列初始化时设为1;audio和subtitle不需关注,缺省是0;
  • rindex_shown:与keep_last配合使用,仅用于video和audio,rindex + rindex_shown指向即将播放的帧,初始值为0,待显示1帧后,此值更新为1,后续不再变化;

  • pkt:FrameQueue所关联的PacketQueue;

keep_last & rindex_shown

    此2值仅用于video和audio,subtitle不用关注。

    keep_last在此处初始化:

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()))return AVERROR(ENOMEM);return 0;
}

    在FrameQueue队列初始化,最后一个参数keep_last,video和audio传入1,而subtitle传入0:

    /* start video display */if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0)goto fail;if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)goto fail;if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)goto fail;

    而后,仅在此方法里使用,其中rindex_shown在frame_queue_init初始化时设置为0:

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

    而frame_queue_next方法在video或audio播放1帧之后调用,指向FrameQueue的下1个待播放的帧。 

    所以,frame_queue_peek方法实际返回的是下一个待播放的video或audio帧:

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

    而frame_queue_peek_next方法返回的是下下一个待播放的video或audio帧:    

static Frame *frame_queue_peek_next(FrameQueue *f)
{return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size];
}

    而frame_queue_peek_last方法返回的是上一个已播放的video或audio帧:

static Frame *frame_queue_peek_last(FrameQueue *f)
{return &f->queue[f->rindex];
}

    那么,加入FrameQueue队列写满了,会怎么样?

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

    可以看到,如FrameQueue队列写满了,将条件等待该队列非满,方能写入1个帧。

    假如FrameQueue没有可读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队列空,则条件等待非空,才能读取数据。

MessageQueue

    其源码结构定义:

// based on PacketQueue in ffplay.ctypedef struct AVMessage {int what;int arg1;int arg2;void *obj;size_t len;void (*free_l)(void *obj);struct AVMessage *next;
} AVMessage;typedef struct MessageQueue {AVMessage *first_msg, *last_msg;int nb_messages;int abort_request;SDL_mutex *mutex;SDL_cond *cond;AVMessage *recycle_msg;int recycle_count;int alloc_count;
} MessageQueue;

    可以看到,此队列是IJKPLAYER参照FFPLAY的PacketQueue而来,因此,不再赘述。


http://www.ppmy.cn/news/49879.html

相关文章

解决vue pointerevent事件无法更改cursor问题 抓取图标(grab/grabbing)

vue pointerevent事件无法更改cursor问题 告诉你一个扎心的事情,CtrlF5就好了… 另外开F12调试工具且在双屏的副屏上也会出现这个bug… 故障重现 我想要实现一个抓取拖放的功能,鼠标按下修改指针为gragbbing状态,抬起恢复到grab 于是我大概和你一样,尝试在pointdown事件里写…

TypeScript(十二)模块

目录 引言 d.ts声明文件 declare关键字 全局声明 全局声明方式 全局声明一般用作 函数声明 在.ts中使用declare 外部模块&#xff08;文件模块&#xff09; 模块关键字module 声明模块 模块声明方式 模块通配符 模块导出 模块嵌套 模块的作用域 模块别名 内部…

C++ STL之string容器的模拟实现

目录 一、经典的string类问题 1.出现的问题 2.浅拷贝 3.深拷贝 二、string类的模拟实现 1.传统版的string类 2.现代版的string类&#xff08;采用移动语义&#xff09; 3.相关习题* 习题一 习题二 4.写时拷贝 5.完整版string类的模拟实现[注意重定义] MyString.h…

Scala之集合(3)

目录 WordCount案例&#xff1a; 需求分析与步骤&#xff1a; 拆分&#xff1a; 聚合&#xff1a; 格式转化&#xff1a; 方法1&#xff1a; 方法2&#xff1a; 排序&#xff1a; 方法1&#xff1a; 方法2&#xff1a; 取top3&#xff1a; 整体化简后的代码&#xf…

00后卷王的自述,我真有同事口中说的那么卷?

前言 前段时间去面试了一个公司&#xff0c;成功拿到了offer&#xff0c;薪资也从14k涨到了20k&#xff0c;对于工作都还没几年的我来说&#xff0c;还是比较满意的&#xff0c;毕竟一些工作5、6年的可能还没我高。 我可能就是大家口中的卷王&#xff0c;感觉自己年轻&#xf…

QMS-云质说质量 - 7 IATF 16949哪个条款严重不符合项最多?

云质QMS原创 转载请注明来源 作者&#xff1a;王洪石 引言 AIAG 《质量2020》报告的数据是否让你惊讶&#xff1f; AIAG与德勤合作发布的汽车行业《质量2020》报告指出&#xff0c;"OEMs和供应商都将问题解决和CSR&#xff08;Customer Specific Requirement顾客特定要求…

搞懂API,创建供外部系统更新数据 API 的最佳方法

在创建一个供外部系统更新本系统数据的 API 时&#xff0c;需要考虑以下几个方面&#xff1a; 身份认证和安全性&#xff1a;首先需要确保 API 能够安全地接收外部系统发送的请求&#xff0c;可以使用身份认证和加密等方式保护 API 的安全性&#xff0c;避免非法和恶意请求。 …

字节和阿里,谁的管理模式更先进?

有人说&#xff0c;字节跳动的成功&#xff0c;是商业模式和管理模式的成功。不无道理&#xff0c;相比阿里巴巴以KPI绩效考核、强制淘汰的组织管理模式来说&#xff0c;字节的模式有其先进的地方。 在商业模式上&#xff0c;字节用算法的方式&#xff0c;10倍速地提升了信息分…