文章目录
PacketQueue详细介绍
packetQueue结构体说明
PacketQueue结构体在之前的文章中已经有所介绍,这里再重复一遍,已经熟知的可自行跳过。
代码具体实现如下:
typedef struct PacketQueue {
AVFifo *pkt_list; //数据存储缓存区域
int nb_packets; //包的数量, 即队列的元素数量
int size; //队列所有元素的数据大小的综合
int64_t duration; //队列所有节点播放时间总和
int abort_request; //用户请求退出标志
int serial; //播放序列号
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
ffplay使用MyAVPacketList来保存解封装后的数据,而使用PacketQueue来存储MyAVpacketList数据。在函数packet_queue_put_private()会将外部传入的包存储进PacketQueuet中。AVFifo 是一个按字节存储数据的结构体,MyAVPacketList中的数据就存于该结构体中。
MyAVPacketList的serial字段的赋值来⾃PacketQueue的serial,每个PacketQueue的serial是独⽴的。
注意:_⾳频、视频、字幕流都有⾃⼰独⽴的PacketQueue。
PacketQueue相关API说明
以下先给出所有方法的一个总览:
packet_queue_init:初始化
packet_queue_destroy:销毁
packet_queue_start:启⽤
packet_queue_abort:中⽌
packet_queue_get:获取⼀个节点
packet_queue_put:存⼊⼀个节点
packet_queue_put_nullpacket:存⼊⼀个空节点
packet_queue_flush:清除队列内所有的节点
packet_queue_init
代码实现如下:
static int packet_queue_init(PacketQueue *q)
{
memset(q, 0, sizeof(PacketQueue));
q->pkt_list = av_fifo_alloc2(1, sizeof(MyAVPacketList), AV_FIFO_FLAG_AUTO_GROW);
if (!q->pkt_list)
return AVERROR(ENOMEM);
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;
return 0;
}
ffpaly使用该函数来初始化PacketQueue结构体,主要是对PacketQueue的一些字段进行初始化, 创建锁和条件变量等等。
packet_queue_flush
ffpaly中使用这个函数来清空packetQueue的数据,包括释放掉其中的AVpacket数据, 代码实现如下:
static void packet_queue_flush(PacketQueue *q)
{
MyAVPacketList pkt1;
SDL_LockMutex(q->mutex);
while (av_fifo_read(q->pkt_list, &pkt1, 1) >= 0)
av_packet_free(&pkt1.pkt); //释放AVPacket的数据
q->nb_packets = 0;
q->size = 0;
q->duration = 0;
q->serial++;
SDL_UnlockMutex(q->mutex);
}
av_fifo_read用于从AvFifo中读取数据到MyAVPacketList中。函数循环取出MyAVPacketList数据后,释放了其AVpacket的数据,并且将队列的属性归于空队列的状态。
该函数在退出播放时,或者进行seek操作时均会使用。
packet_queue_destory
代码实现如下:
static void packet_queue_destroy(PacketQueue *q)
{
packet_queue_flush(q); //清空队列的所有元素
av_fifo_freep2(&q->pkt_list);
SDL_DestroyMutex(q->mutex);
SDL_DestroyCond(q->cond);
}
该函数用于销毁整个PacketQueue队列。先是调用了packet_queue_flush函数来清空队列的所有元素,在由函数av_fifo_freep2来释放q->pkt_list的内存,最后销毁锁和条件变量等。
这里看一下av_fifo_freep2函数的实现:
void av_fifo_freep2(AVFifo **f)
{
if (*f) {
av_freep(&(*f)->buffer);
av_freep(f);
}
}
可以看到其就是一个对AVFifo内存的一个释放,注意av_free和av_freep两个api的不同,av_freep除了会释放这个指针的内存外,还会将这个指针的值置为NULL。
packet_queue_start
ffplay在初始化队列之后,还不能直接使用哦,这里需要在使用packet_queue_start启动队列,实现如下:
static void packet_queue_start(PacketQueue *q)
{
SDL_LockMutex(q->mutex);
q->abort_request = 0;
q->serial++;
SDL_UnlockMutex(q->mutex);
}
这里会对serial加一,重置播放序列。同时将abort_request的值置为0,只有其值置为0后,视频才能播放。
packet_queue_abort
用于终止一个队列,代码实现如下:
static void packet_queue_abort(PacketQueue *q)
{
SDL_LockMutex(q->mutex);
q->abort_request = 1; //请求退出
SDL_CondSignal(q->cond); //释放一个条件变量
SDL_UnlockMutex(q->mutex);
}
SDL_CondSignal保证等待该条件的线程能够被激活,继续执行终止退出的流程,唤醒者会检查abort_request字段确定进入退出的流程。
packet_queue_put_private
ffplay使用这个函数实际向队列中插入元素,并处理相关的一些操作,代码实现如下:
static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
MyAVPacketList pkt1;
int ret;
if (q->abort_request)
return -1;
pkt1.pkt = pkt;
pkt1.serial = q->serial; //用队列序列号标记节点
ret = av_fifo_write(q->pkt_list, &pkt1, 1);
if (ret < 0)
return ret;
//更新队列的相关属性
q->nb_packets++;
q->size += pkt1.pkt->size + sizeof(pkt1);
q->duration += pkt1.pkt->duration;
//发出信号,表明当前队列中有数据了,通知等待中的读线程可以取数据了
SDL_CondSignal(q->cond);
return 0;
}
该函数主要做了以下几件事:
- 确定MyPacketList的serial序列号,从此可以看出,MyPacketList的serial值其实是由PacketQueue来确定的。
- 调用av_fifo_write函数,将pkt1数据实际写入到队列中。
- 更新队列的一些属性,如大小,长度等。
- 发出信号,通知阻塞的读线程可以来读数据了。
packet_queue_put
往队列中插入一个AVPacket数据包,实现如下:
static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
AVPacket *pkt1;
int ret;
pkt1 = av_packet_alloc();
if (!pkt1) {
av_packet_unref(pkt);
return -1;
}
av_packet_move_ref(pkt1, pkt);
SDL_LockMutex(q->mutex);
ret = packet_queue_put_private(q, pkt1); //核心实现
SDL_UnlockMutex(q->mutex);
if (ret < 0)
av_packet_free(&pkt1); //加入失败,释放packet
return ret;
}
这里将pkt的数据移动到pkt1中,再调用packet_queue_put_private来向队列中插入数据,最后如果插入失败则销毁释放packet内存。
packet_queue_get
从队列中读取一个节点,代码实现如下:
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;
}
if (av_fifo_read(q->pkt_list, &pkt1, 1) >= 0) { //从队列中尝试拿数据
//成功拿到数据
q->nb_packets--;
q->size -= pkt1.pkt->size + sizeof(pkt1);
q->duration -= pkt1.pkt->duration;
av_packet_move_ref(pkt, pkt1.pkt); //将数据拷贝到pkt中
if (serial)
*serial = pkt1.serial;
av_packet_free(&pkt1.pkt);
ret = 1;
break;
} else if (!block) { //没有读到数据,并且用户指定了非阻塞调用
ret = 0;
break;
} else { //没有读到数据,并且进行阻塞调用
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}
该函数的总体流程:
- 加锁
- 进⼊for循环,如果需要退出for循环,则break;当没有数据可读且block为1时则等待
ret = -1 终⽌获取packet
ret = 0 没有读取到packet
ret = 1 获取到了packet - 释放锁
可以注意到这里等待的信号会在packet_queue_private_put中发出。
packet_queue_put_nullpacket
这个api在旧版本的ffplay中,是会实际插入一个空包的,表示流的结束和冲刷解码器。但新版的函数中,变成了接受一个包,并插入队列,实现如下:
static int packet_queue_put_nullpacket(PacketQueue *q, AVPacket *pkt, int stream_index)
{
pkt->stream_index = stream_index;
return packet_queue_put(q, pkt);
}
这里也给出旧标准的相同函数,以作对比
static int packet_queue_put_nullpacket(PacketQueue *q, int stream_index)
{
AVPacket pkt1, *pkt = &pkt1;
av_init_packet(pkt);
pkt->data = NULL;
pkt->size = 0;
pkt->stream_index = stream_index;
return packet_queue_put(q, pkt);
}
新版如此的修改貌似使得该函数的名字和功能无关联了,我也不太理解做如此修改的意图是什么。