ffplay-PacketQueue 剖析

PacketQueue 总结

PacketQueue设计思路:

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

1 内存管理

在这里插入图片描述

MyAVPacketList的内存是完全由PacketQueue维护的,在put的时候malloc,在get的时候free。

AVPacket分两块:

⼀部分是AVPacket结构体的内存,这部分从MyAVPacketList的定义可以看出是和MyAVPacketList 共存亡的。

另⼀部分是AVPacket字段指向的内存,这部分⼀般通过 av_packet_unref 函数释放。⼀般情况 下,是在get后由调⽤者负责⽤ av_packet_unref 函数释放。特殊的情况是当碰到 packet_queue_flush 或put失败时,这时需要队列⾃⼰处理。

2 serial的变化过程:

在这里插入图片描述

可以看到放⼊flush_pkt的时候后,serial增加了1. 假设,现在要从队头取出⼀个节点,那么取出的节点是serial 1,⽽PacketQueue⾃身的queue已经增⻓到了2

3源码函数剖析

packet_queue_init:初始化
packet_queue_destroy:销毁
packet_queue_start:启⽤
packet_queue_abort:中⽌
packet_queue_get:获取⼀个节点
packet_queue_put:存⼊⼀个节点
packet_queue_put_private:执行存入节点的真正函数
packet_queue_put_nullpacket:存⼊⼀个空节点
packet_queue_flush:清除队列内所有的节点

函数解析

packet_queue_init(初始化)

注意 一开始 q->abort_request = 1;

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;
    return 0;
}

packet_queue_destroy(销毁)

packet_queue_flush 清楚所有节点 这个后面有说

static void packet_queue_destroy(PacketQueue *q)
{
    
    
    packet_queue_flush(q); //先清除所有的节点
    SDL_DestroyMutex(q->mutex);
    SDL_DestroyCond(q->cond);
}

packet_queue_start(启动)

注意这里
q->abort_request = 0;
packet_queue_put_private(q, &flush_pkt); //这里放入了一个flush_pkt
当发现队列有flush_pkt 就会packet_queue_flush的

static void packet_queue_start(PacketQueue *q)
{
    
    
    SDL_LockMutex(q->mutex);
    q->abort_request = 0;
    packet_queue_put_private(q, &flush_pkt); //这里放入了一个flush_pkt
    SDL_UnlockMutex(q->mutex);
}

packet_queue_abort(中止)

q->abort_request = 1;

static void packet_queue_abort(PacketQueue *q)
{
    
    
    SDL_LockMutex(q->mutex);

    q->abort_request = 1;       // 请求退出

    SDL_CondSignal(q->cond);    //释放一个条件信号

    SDL_UnlockMutex(q->mutex);
}

packet_queue_get(获取一个节点)

逻辑和之前的播放器大差不差
该函数整体流程:

加锁进⼊for循环,如果需要退出for循环,则break;当没有数据可读且block为1时则等待 
ret = -1 终⽌获取packet 
ret = 0 没有读取到packet 
ret = 1 获取到了packet 释放锁

注意这 三块代码

MyAVPacketList *pkt1;

*pkt = pkt1->pkt;
        if (serial) //如果需要输出serial,把serial输出
            *serial = pkt1->serial;

else if (!block) {    //队列中没有数据,且非阻塞调用
        ret = 0;
        break;
    } else {    //队列中没有数据,且阻塞调用
        //这里没有break。for循环的另一个作用是在条件变量满足后重复上述代码取出节点
        SDL_CondWait(q->cond, q->mutex);
    }
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;    //MyAVPacketList *pkt1; 从队头拿数据
        if (pkt1) {
    
         //队列中有数据
            q->first_pkt = pkt1->next;  //队头移到第二个节点
            if (!q->first_pkt)
                q->last_pkt = NULL;
            q->nb_packets--;    //节点数减1
            q->size -= pkt1->pkt.size + sizeof(*pkt1);  //cache大小扣除一个节点
            q->duration -= pkt1->pkt.duration;  //总时长扣除一个节点
            //返回AVPacket,这里发生一次AVPacket结构体拷贝,AVPacket的data只拷贝了指针
            *pkt = pkt1->pkt;
            if (serial) //如果需要输出serial,把serial输出
                *serial = pkt1->serial;
            av_free(pkt1);      //释放节点内存,只是释放节点,而不是释放AVPacket
            ret = 1;
            break;
        } else if (!block) {
    
        //队列中没有数据,且非阻塞调用
            ret = 0;
            break;
        } else {
    
        //队列中没有数据,且阻塞调用
            //这里没有break。for循环的另一个作用是在条件变量满足后重复上述代码取出节点
            SDL_CondWait(q->cond, q->mutex);
        }
    }
    SDL_UnlockMutex(q->mutex);  // 释放锁
    return ret;
}

packet_queue_put(存入一个节点)

主要2点

1 packet_queue_put_private
2 if (pkt != &flush_pkt && ret < 0)
    av_packet_unref(pkt);       //放入失败,释放AVPacket
    这里是!=flush_pkt && ret<0 
    需要增加队列的播放序列号,以区分不连续的两段数据
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);       //放入失败,释放AVPacket

    return ret;
}

packet_queue_put_private(实现)

对于packet_queue_put_private
主要完成3件事:

计算serial。serial标记了这个节点内的数据是何时的。⼀般情况下新增节点与上⼀个节点的serial是⼀ 样的,但当队列中加⼊⼀个flush_pkt后,后续节点的serial会⽐之前⼤1,⽤来区别不同播放序列的 packet. 

节点⼊队列操作。
 
队列属性操作。更新队列中节点的数⽬、占⽤字节数(含AVPacket.data的⼤⼩)及其时⻓。主要⽤来 控制Packet队列的⼤⼩,我们PacketQueue链表式的队列,在内存充⾜的条件下我们可以⽆限put⼊ packet,如果我们要控制队列⼤⼩,则需要通过其变量size、duration、nb_packets三者单⼀或者综 合去约束队列的节点的数量,具体在read_thread进⾏分析。

重点

if (pkt == &flush_pkt)//如果放入的是flush_pkt,需要增加队列的播放序列号,以区分不连续的两段数据
{
    q->serial++;
    printf("q->serial = %d\n", q->serial);
}
pkt1->serial = q->serial; 

是队列不是循环队列

q->nb_packets++;
q->size += pkt1->pkt.size + sizeof(*pkt1);
q->duration += pkt1->pkt.duration;
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++;
        printf("q->serial = %d\n", 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_nullpacket(存入一个空节点)

放⼊“空包”(nullpacket)。放⼊空包意味着流的结束,⼀般在媒体数据读取完成的时候放⼊空包。放⼊ 空包,⽬的是为了冲刷解码器,将编码器⾥⾯所有frame都读取出来:

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(清除队列内所有的节点)

packet_queue_flush⽤于将packet队列中的所有节点清除,包括节点对应的AVPacket。
函数主体的for循环是队列遍历
遍历过程释放节点和AVPacket(AVpacket对应的数据也被释放掉)。
最后将PacketQueue的属性恢复为空队列状态

⽤于

退出播放和seek播放: 退出播放,则要清空packet queue的节点 
seek播放,要清空seek之前缓存的节点数据,以便插⼊新节点数据
static void packet_queue_flush(PacketQueue *q)
{
    
    
    MyAVPacketList *pkt, *pkt1;

    SDL_LockMutex(q->mutex);
    for (pkt = q->first_pkt; pkt; pkt = pkt1) {
    
    
        pkt1 = pkt->next;
        av_packet_unref(&pkt->pkt);
        av_freep(&pkt);
    }
    q->last_pkt = NULL;
    q->first_pkt = NULL;
    q->nb_packets = 0;
    q->size = 0;
    q->duration = 0;
    SDL_UnlockMutex(q->mutex);
}

猜你喜欢

转载自blog.csdn.net/qq_33329316/article/details/124409679
今日推荐