ffplay学习之PacketQueue队列(一)

今天天气雨后天晴,秋风微凉,写一写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设计思路:

  1. 设计⼀个多线程安全的队列,保存AVPacket,同时统计队列内已缓存的数据⼤⼩。(这个统计数据会
    ⽤来后续设置要缓存的数据量)
  2. 引⼊serial的概念,区别前后数据包是否连续,主要应⽤于seek操作。
  3. 设计了两类特殊的packet——flush_pkt和nullpkt(类似⽤于多线程编程的事件模型——往队列中放⼊
    flush事件、放⼊null事件)

猜你喜欢

转载自blog.csdn.net/weixin_42764231/article/details/127545066