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