前言
对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而来,因此,不再赘述。