今天天气雨后天晴,秋风微凉,写一写ffplay的数据结构。各位看官如果觉得过于啰嗦,请点击右上方x按钮。
一、封装自己的数据结构,存自己想存的东西
MyAVPacketList是对ffmpeg中AVPacket进行了封装,同时里面的serial被用作识别pkt是否为当前播放序列,如果不是则会丢弃。
typedef struct MyAVPacketList {
AVPacket *pkt; //demux后的数据包
int serial; //播放序列
} MyAVPacketList;
PacketQueue是用来存储MyAVPacketList的一个结构体,在函数packet_queue_put_private()会将外部传入的包存储进PacketQueuet中。AVFifo 是一个按字节存储数据的结构体,以前版本的ffplay此处使用的是“MyAVPacketList *first_pkt, *last_pkt” 队列指针进行数据存储。⾳频、视频、字幕流都有⾃⼰独⽴的PacketQueue。
typedef struct PacketQueue{
AVFifo *pkt_list; //数据存储缓存区域
int nb_packets; //队列内一共有多少元素
int size; //队列内所有元素的大小
int64_t duration; //队列里的元素一共可以播放多长时间
int abort_request; //视频播放是否退出
int serial; //播放序列来自于MyAVPacketList的播放序列
SDL_mutex *mutex; //线程锁,保证多线程时数据正确
SDL_cond *cond; //用于读写线程互相通知
}PacketQueue;
struct AVFifo {
uint8_t *buffer; //字节流Buffer
size_t elem_size, nb_elems;
size_t offset_r, offset_w;
// distinguishes the ambiguous situation offset_r == offset_w
int is_empty;
unsigned int flags;
size_t auto_grow_limit;
};
二、对自己的数据结构,做自己想做的事情
兵马未动粮草先行,先使用packet_queue_init()把packet初始化一下。
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); //申请pkt_list的空间
if (!q->pkt_list)
return AVERROR(ENOMEM);
q->mutex = SDL_CreateMutex(); //申请mutex的空间
if (!q->mutex) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
return AVERROR(ENOMEM);
}
q->cond = SDL_CreateCond(); //申请cond的空间
if (!q->cond) {
av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
return AVERROR(ENOMEM);
}
q->abort_request = 1;
return 0;
}
大军出征前的宣誓,启用PacketQueue, 初始化的时候 abort_request 被置为了1 在启动时置为0,只有为0的时候才可以正常播放,为1时无法播放。同时会将serial 进行加 1 重置播放序列。
static void packet_queue_start(PacketQueue *q)
{
SDL_LockMutex(q->mutex);
q->abort_request = 0;
q->serial++;
SDL_UnlockMutex(q->mutex);
}
粮草存储,packet_queue_put通过调用packet_queue_put_private将一个数据包封装成MyAVPacketList并存储进PacketQueue中。
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);
return ret;
}
packet_queue_put_private****做以下事情来存储数据
1.计算serial。serial标记了这个节点内的数据是何时的。⼀般情况下新增节点与上⼀个节点的serial是⼀
样的,但出现seek操作后队列中会加⼊⼀个flush_pkt,从而serial会加1,后续节点的serial就会比之前大一,以此来区别不同的播放序列。
2.通过av_fifo_write进行节点⼊队列操作。
3.队列属性操作。更新队列中节点的数⽬、占⽤字节数(含AVPacket.data的⼤⼩)及其时⻓。可以通过限制size、duration、nb_packets的最大值,来控制PacketQueue队列的大小。
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;
/* XXX: should duplicate packet data in DV case */
SDL_CondSignal(q->cond);
return 0;
}
av_packet_move_ref()会将src完整拷贝给dst,拷贝完成之后会通过get_packet_defaults()函数将src的数据清除,因为dst和src有各自的地址空间,所以src被清空不会影响到dst。
void av_packet_move_ref(AVPacket *dst, AVPacket *src)
{
*dst = *src;
get_packet_defaults(src);
}
边走边补充粮草,吃饱了才能干活呀。packet_queue_get目标就是获取AvPacket,主要是av_fifo_read()读取pkt_list中存储的帧,读成功后将 nb_packet 进行减一,注意size要减去的长度不仅仅是pkt1中数据的大小,还有pkt1自己的大小。后续只需要把取出来的帧av_packet_move_ref()拷贝到要返回的空间地址里就行了。
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,把serial传出
*serial = pkt1.serial;
av_packet_free(&pkt1.pkt);
ret = 1;
break;
} else if (!block) {
//队列中无数据,且为⾮阻塞调⽤
ret = 0;
break;
} else {
//此处不break。等待满足条件变量,重复上述步骤取数据
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}
放置空包进PacketQueue 2022/10/27号 github ffplay.c中 packet_queue_put_nullpacket 函数是需要传入一个包,再将这个包存储进PackeQueue队列中。看调用此函数的四个地方,三个是会将eof的值置为1表示播放结束,另外一个是关于mp3的图片显示调用,此处修改后就和名字对应不上了,所以对ffmpeg作者修改这个函数,本人还是存在一定疑问的,下面会贴出之前的此函数。
static int packet_queue_put_nullpacket(PacketQueue *q, AVPacket *pkt, int stream_index)
{
pkt->stream_index = stream_index;
return packet_queue_put(q, pkt);
}
原先的packet_queue_put_nullpacket 是真的会插入一个空的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);
}
打完仗准备跑路了,先清理一下物资,packet_queue_flush中先看看队列里还有木有包,有就将包全部取出释放了,释放完成后再将PacketQueue队列各参数初始化。主要用在:
1.退出播放时,清空PacketQueue
2.seek操作后,需清空PacketQueue之前缓存的节点数据,以便插⼊新节点数据
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);
q->nb_packets = 0;
q->size = 0;
q->duration = 0;
q->serial++;
SDL_UnlockMutex(q->mutex);
}
packet_queue_destroy()会销毁PacketQueue内部资源并清理mutex和cond,防止内存泄露。
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);
}
三、总结
1.PacketQueue的内存管理:
PacketQueue会维护MyAVPackeList的内,在put时(packet_queue_put_private函数内调用av_malloc)malloc,在get时(packet_queue_get函数内调用av_packet_free)free。
AVPacket内存分为两块空间:
1.AVPacket结构体的内存,这部分内存会和MyAVPacketList共存亡的。
2.AVPacket字段指向的内存,这部分需要通过 av_packet_unref 函数释放。
2.serial的变化:
每放入一个flush_pkt,serial就会加1.
3.PacketQueue设计思路:
- 设计⼀个多线程安全的队列,保存AVPacket,同时统计队列内已缓存的数据⼤⼩。(这个统计数据会
⽤来后续设置要缓存的数据量) - 引⼊serial的概念,区别前后数据包是否连续,主要应⽤于seek操作。
- 设计了两类特殊的packet——flush_pkt和nullpkt(类似⽤于多线程编程的事件模型——往队列中放⼊
flush事件、放⼊null事件)