一个播放器的实现,都是先解封装,然后将解封装数据(后面统一称为包数据)放入到队列中,再从队列里取出数据进行解码,解码后的数据(后面统一称为帧数据)放入到等待播放的队列中,再从其中取出数据进行相应的渲染或播放。
所以在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的源码,并按每个线程的处理逻辑总结过大致流程,但是现在翻开感觉已经忘得差不多了,后面我将会按播放视频中一些过程(如播放器初始化,视频软/硬解,音频播放,暂停/恢复,缓冲等)进行源码分析,到时候会同步更新出来。下一期应该会是下面这张图的内容。