ffplay⽤PacketQueue保存解封装后的数据,即保存AVPacket。
typedef struct PacketQueue {
MyAVPacketList *first_pkt, *last_pkt; // 队⾸,队尾指针
int nb_packets; // 包数量,也就是队列元素数量
int size; // 队列所有元素的数据⼤⼩总和
int64_t duration; // 队列所有元素的数据播放持续时间
int abort_request; // ⽤户退出请求标志
int serial; // 播放序列号,和MyAVPacketList的serial作⽤相同
SDL_mutex *mutex; // ⽤于维持PacketQueue的多线程安全(SDL_mutex可以按pthread_mutex_t理解)
SDL_cond *cond; // ⽤于读、写线程相互通知(SDL_cond可以按pthread_cond_t理解)
} PacketQueue;
MyAVPacketList
1 是队列的⼀个节点。可以通过其 next 字段访问下⼀个节点。
2 serial字段主要⽤于标记当前节点的播放序列号,ffplay中多处⽤到serial的概念,主要⽤来区分是否连续数据,每做⼀次seek,该serial都会做+1的递增,以区分不同的播放序列。
typedef struct MyAVPacketList {
AVPacket pkt; //解封装后的数据
struct MyAVPacketList *next; //下⼀个节点
int serial; //播放序列
} MyAVPacketList;
3 MyAVPacketList的serial字段的赋值来⾃PacketQueue的serial
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
// AVPacket 要转成 MyAVPacketList才能插入
{
MyAVPacketList *pkt1;
if (q->abort_request)
return -1;
pkt1 = av_malloc(sizeof(MyAVPacketList));
if (!pkt1)
return -1;
pkt1->pkt = *pkt;
pkt1->next = NULL;
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);
q->duration += pkt1->pkt.duration;
/* XXX: should duplicate packet data in DV case */
SDL_CondSignal(q->cond);
return 0;
}
⾳频、视频、字幕流都有⾃⼰独⽴的PacketQueue。
PacketQueue audioq;
PacketQueue videoq;
PacketQueue subtitleq;
每个PacketQueue的serial是独⽴的
(可以看出 更新serial 要给每个PacketQueue进行刷新)
if (is->seek_req) {
int64_t seek_target = is->seek_pos;
int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
// FIXME the +-2 is due to rounding being not done in the correct direction in generation
// of the seek_pos/seek_rel variables
ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR,
"%s: error while seeking\n", is->ic->filename);
} else {
if (is->audio_stream >= 0) {
packet_queue_flush(&is->audioq);
packet_queue_put(&is->audioq, &flush_pkt);
}
if (is->subtitle_stream >= 0) {
packet_queue_flush(&is->subtitleq);
packet_queue_put(&is->subtitleq, &flush_pkt);
}
if (is->video_stream >= 0) {
packet_queue_flush(&is->videoq);
packet_queue_put(&is->videoq, &flush_pkt);
}
if (is->seek_flags & AVSEEK_FLAG_BYTE) {
set_clock(&is->extclk, NAN, 0);
} else {
set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
}
}
is->seek_req = 0;
is->queue_attachments_req = 1;
is->eof = 0;
if (is->paused)
step_to_next_frame(is);
}
一 初始化与销毁
static int packet_queue_init(PacketQueue *q)
{
memset(q, 0, sizeof(PacketQueue));
q->mutex = SDL_CreateMutex();
if (!q->mutex) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
return AVERROR(ENOMEM);
}
q->cond = SDL_CreateCond();
if (!q->cond) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
return AVERROR(ENOMEM);
}
q->abort_request = 1;//在packet_queue_start和packet_queue_abort
时修改到该值
return 0;
}
static void packet_queue_destroy(PacketQueue *q)
{
packet_queue_flush(q);//先清除所有的节点
SDL_DestroyMutex(q->mutex);
SDL_DestroyCond(q->cond);
}
问题
1 audio video 字幕 的 PacketQueue 都是唯一的,为啥还需要锁跟信号量?
2 abort_request 为啥在 destroy 中没有搞?
3 还有更好的函数可以做清除嘛? packet_queue_flush
二 插入数据 packet_queue_put_private
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
MyAVPacketList *pkt1;
if (q->abort_request) //如果已中⽌,则放⼊失败
return -1;
pkt1 = av_malloc(sizeof(MyAVPacketList)); //分配节点内存
if (!pkt1) //内存不⾜,则放⼊失败
return -1;
// 没有做引⽤计数,那这⾥也说明av_read_frame不会释放替⽤户释放buffer。
pkt1->pkt = *pkt; //拷⻉AVPacket(浅拷⻉,AVPacket.data等内存并没有拷
⻉)
pkt1->next = NULL;
if (pkt == &flush_pkt)
//如果放⼊的是flush_pkt,需要增加队列的播放序列号,以区分不连续的两段数据
q->serial++;
pkt1->serial = q->serial;
/* 队列操作:如果last_pkt为空,说明队列是空的,新增节点为队头;
* 否则,队列有数据,则让原队尾的next为新增节点。 最后将队尾指向新增节点
*/
if (!q->last_pkt)
q->first_pkt = pkt1;
else
q->last_pkt->next = pkt1;
q->last_pkt = pkt1;
//队列属性操作:增加节点数、cache⼤⼩、cache总时⻓, ⽤来控制队列的⼤⼩
q->nb_packets++;
q->size += pkt1->pkt.size + sizeof(*pkt1);
q->duration += pkt1->pkt.duration;
/* XXX: should duplicate packet data in DV case */
//发出信号,表明当前队列中有数据了,通知等待中的读线程可以取数据了
SDL_CondSignal(q->cond);
return 0;
}
一 对于packet_queue_put_private主要完成3件事:
1 计算serial。serial标记了这个节点内的数据是何时的。⼀般情况下新增节点与上⼀个节点的serial是⼀
样的,但当队列中加⼊⼀个flush_pkt后,后续节点的serial会⽐之前⼤1,⽤来区别不同播放序列的
packet.
2 节点⼊队列操作。
3 队列属性操作。更新队列中节点的数⽬、占⽤字节数(含AVPacket.data的⼤⼩)及其时⻓。主要⽤来
控制Packet队列的⼤⼩,我们PacketQueue链表式的队列,在内存充⾜的条件下我们可以⽆限put⼊
packet,如果我们要控制队列⼤⼩,则需要通过其变量size、duration、nb_packets三者单⼀或者综
合去约束队列的节点的数量,具体在read_thread进⾏分析。
二 问题
1 SDL_CondSignal(q->cond); 通知机制是什么样的?
2 pkt.duration 怎么来的?
3 pkt1->pkt.size 怎么来的?
三 取出数据 packet_queue_get
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
//param block 调⽤者是否需要在没节点可取的情况下阻塞等待
{
MyAVPacketList *pkt1;
int ret;
SDL_LockMutex(q->mutex);
for (;;) {
if (q->abort_request) {
ret = -1;
break;
}
pkt1 = q->first_pkt; //MyAVPacketList *pkt1; 从队头拿数据
if (pkt1) { //队列中有数据
q->first_pkt = pkt1->next; //队头移到第⼆个节点
if (!q->first_pkt)
q->last_pkt = NULL;
q->nb_packets--; //put 的反操做
q->size -= pkt1->pkt.size + sizeof(*pkt1);
q->duration -= pkt1->pkt.duration;
*pkt = pkt1->pkt;
if (serial) //如果需要输出serial,把serial输出
*serial = pkt1->serial;
av_free(pkt1);
ret = 1;
break;
} else if (!block) { //队列中没有数据,且⾮阻塞调⽤
ret = 0;
break;
} else { //队列中没有数据,且阻塞调⽤
//这⾥没有break。for循环的另⼀个作⽤是在条件变量满⾜后重复上述代码取出节点
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}
四 不同类型 AVPacket 策略
五 限制PacketQueue队列里面的packet个数
if (infinite_buffer<1 &&
(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
|| (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
/* wait 10 ms */
SDL_LockMutex(wait_mutex);
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
}
- audioq,videoq,subtitleq三个PacketQueue的总字节数达到了MAX_QUEUE_SIZE(15M,为什么
是15M?这⾥只是⼀个经验计算值,⽐如4K视频的码率以50Mbps计算,则15MB可以缓存2.4秒,从
这么计算实际上如果我们真的是播放4K⽚源,15MB是偏⼩的数值,有些⽚源⽐较坑 同⼀个⽂件位置
附近的pts差值超过5秒,此时如果视频要缓存5秒才能做同步,那15MB的缓存⼤⼩就不够了) - ⾳频、视频、字幕流都已有够⽤的包(stream_has_enough_packets)