ffplay数据结构分析(一)

本文为相关课程的学习记录,相关分析均来源于课程的讲解,主要学习音视频相关的操作,对字幕的处理不做分析

下面我们对ffplay的相关数据结构进行分析,本章主要是对PacketQueue的讲解

struct MyAVPacketList和PacketQueue队列

ffplay⽤PacketQueue保存解封装后的数据,即保存AVPacket。 ffplay⾸先定义了⼀个结构体 MyAVPacketList

typedef struct MyAVPacketList { 
AVPacket pkt; //解封装后的数据 
struct MyAVPacketList *next; //下⼀个节点 
int serial; //播放序列 
} MyAVPacketList;

可以理解为是队列的⼀个节点。可以通过其 next 字段访问下⼀个节点。
serial字段主要⽤于标记当前节点的播放序列号,ffplay中多处⽤到serial的概念,主要⽤来区分是否连续数据,每做⼀次seek,该serial都会做+1的递增,以区分不同的播放序列。

接着定义另⼀个结构体PacketQueue:

typedef struct PacketQueue { 
	MyAVPacketList *first_pkt, *last_pkt; // 队⾸,队尾指针 3
	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; 

该结构体内定义了“队列”⾃身的属性。上⾯的注释对每个字段作了简单的介绍,这⾥也看到了serial字段, MyAVPacketList的serial字段的赋值来⾃PacketQueue的serial,每个PacketQueue的serial是独⽴的。

⾳频、视频、字幕流都有⾃⼰独⽴的PacketQueue。

接下来我们也从队列的操作函数具体分析各个字段的含义。 PacketQueue 操作提供以下⽅法:

  • 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()

初始化⽤于初始各个字段的值,并创建mutex和cond:

/* packet queue handling */
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;
}

packet_queue_destroy()

相应的,packet_queue_destroy()销毁过程负责清理mutex和cond:

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

packet_queue_start()

启动队列

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);
}

flush_pkt定义是 static AVPacket flush_pkt;,是⼀个特殊的packet,主要⽤来作为⾮连续的两段数据的“分界”标记:

  • 插⼊ flush_pkt 触发PacketQueue其对应的serial,加1操作
  • 触发解码器清空⾃身缓存 avcodec_flush_buffers(),以备新序列的数据进⾏新解码

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()

向队列中放入一个节点

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的实现

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_private主要完成3件事:

  • 计算serial。serial标记了这个节点内的数据是何时的。⼀般情况下新增节点与上⼀个节点的serial是⼀ 样的,但当队列中加⼊⼀个flush_pkt后,后续节点的serial会⽐之前⼤1,⽤来区别不同播放序列的 packet.
  • 节点⼊队列操作。
  • 队列属性操作。更新队列中节点的数⽬、占⽤字节数(含AVPacket.data的⼤⼩)及其时⻓。主要⽤来控制Packet队列的⼤⼩,我们PacketQueue链表式的队列,在内存充⾜的条件下我们可以⽆限put⼊packet,如果我们要控制队列⼤⼩,则需要通过其变量size、duration、nb_packets三者单⼀或者综 合去约束队列的节点的数量,具体在read_thread进⾏分析。

packet_queue_get()

从队列中获取一个数据

/**
 * @brief packet_queue_get
 * @param q 队列
 * @param pkt 输出参数,即MyAVPacketList.pkt
 * @param block 调用者是否需要在没节点可取的情况下阻塞等待
 * @param serial 输出参数,即MyAVPacketList.serial
 * @return <0: aborted; =0: no packet; >0: has packet
 */
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;
}

该函数整体流程:

  • 加锁进⼊for循环,如果需要退出for循环,则break;
  • 当没有数据可读且block为1时则等待
    • ret = -1 终⽌获取packet
    • ret = 0 没有读取到packet
    • ret = 1 获取到了packet
  • 释放锁
    如果有取到数据,主要分3个步骤:
    1. 队列操作:出队列操作; nb_packets数目相应-1; duration 的也相应减少, size也相应占⽤的字节⼤⼩(pkt1->pkt.size + sizeof(*pkt1))
    2. 给输出参数赋值:就是MyAVPacketList的成员传递给输出参数pkt和serial
    3. 释放节点内存:释放放⼊队列时申请的节点内存(注意是节点内存⽽不是AVPacket的数据的内存)

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。

  • ⽐如⽤于退出播放和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);
}

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

PacketQueue总结 前⾯我们分析了PacketQueue的实现和主要的操作⽅法,现在总结下两个关键的点:
第⼀,PacketQueue的内存管理:
在这里插入图片描述
MyAVPacketList的内存是完全由PacketQueue维护的,在put的时候malloc,在get的时候free。
AVPacket分两块:

  • ⼀部分是AVPacket结构体的内存,这部分从MyAVPacketList的定义可以看出是和MyAVPacketList 共存亡的。
  • 另⼀部分是AVPacket字段指向的内存,这部分⼀般通过 av_packet_unref 函数释放。⼀般情况下,是在get后由调⽤者负责⽤ av_packet_unref 函数释放。特殊的情况是当碰到 packet_queue_flush 或put失败时,这时需要队列⾃⼰处理。

第⼆,serial的变化过程:
在这里插入图片描述
如上图所示,左边是队头,右边是队尾,从左往右标注了4个节点的serial,以及放⼊对应节点时queue的 serial。
可以看到放⼊flush_pkt的时候后,serial增加了1.
假设,现在要从队头取出⼀个节点,那么取出的节点是serial 1,⽽PacketQueue⾃身的queue已经增⻓到了2。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/65637.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

常量池-JVM(十九)

上篇文章说gc日志以及arthas。 Arthas & GC日志-JVM&#xff08;十八&#xff09; 一、常量池 常量池主要放两大类&#xff1a;字面量和符号引用。 字面量就是由字母、数字等构成的字符串或者数值常量。 符号引用主要包含三类常量。 类和接口的全限定名。字段的名称和…

【毕业项目】自主设计HTTP

博客介绍&#xff1a;运用之前学过的各种知识 自己独立做出一个HTTP服务器 自主设计WEB服务器 背景目标描述技术特点项目定位开发环境WWW介绍 网络协议栈介绍网络协议栈整体网络协议栈细节与http相关的重要协议 HTTP背景知识补充特点uri & url & urn网址url HTTP请求和…

python的virtualenv虚拟环境无法激活activate

目录 问题描述&#xff1a; 解决办法&#xff1a; 解决结果&#xff1a; 问题描述&#xff1a; PS D:\pythonProject\pythonProject\DisplayToolLibs\venv\Scripts> .\activate .\activate : 无法加载文件 D:\pythonProject\pythonProject\DisplayToolLibs\venv\Scripts\…

form-create-designer整合element-plus使用方法

最近在使用form-create-designer生成表单的时候遇到了很多问题和各种报错&#xff0c;按照官方文档的方法一步步来做&#xff0c;发现行不通&#xff0c;后来经过不断尝试&#xff0c;终于找到了使用方法&#xff0c;这里做一下总结。 1、安装所需的依赖包 npm install eleme…

Redhat Linux 安装MySQL安装手册

Redhat安装MySQL安装手册 1 下载2 上传服务器、解压并安装3 安装安装过程1&#xff1a;MySQL-shared-5.6.51-1.el7.x86_64.rpm安装过程2&#xff1a;MySQL-shared-compat-5.6.51-1.el7.x86_64.rpm安装过程3&#xff1a;MySQL-server-5.6.51-1.el7.x86_64.rpm安装过程4&#xff…

AWS-自定义ami的S3存取使用

需要提前配置好aws-cli哈 对应的区域 要统一 示例&#xff1a;即AWS-CLI 和 EC2、AMI、S3以上资源均要使用同已区域&#xff0c;以下拿新加坡举例 1.新建自定义AMI 2.查看ami状态 确认是可用状态&#xff0c;才能开始操作 3.aws-cli 开始存入s3 只能使用桶的根目录 开始上…

【golang】工作区与GOPATH

在学习go语言时&#xff0c;我们会从官网下载go语言的二进制包&#xff0c;然后解压并安装到某个目录&#xff0c;最后会配置环境变量&#xff0c;通过输入命令go version来验证是否安装成功。 配置了path环境后&#xff0c;我们还需要再配置3个环境变量&#xff0c;GOROOT、G…

XML(eXtensible Markup Language)

目录 为什么需要XML? 一 XML语法 1.文档声明 2.元素 语法: 3.属性 4.注释 5.CDATA节 二 树结构 三 转义字符 四 DOM4J 1.XML解析技术 2.dom4j介绍 3.dom4j基本使用 XML 指可扩展标记语言&#xff08;eXtensible Markup Language&#xff09;。 XML 被设计用来传…

【新】通达OA前台反序列化漏洞分析

0x01 前言 注&#xff1a;本文仅以安全研究为目的&#xff0c;分享对该漏洞的挖掘过程&#xff0c;文中涉及的所有漏洞均已报送给国家单位&#xff0c;请勿用做非法用途。 通达OA作为历史上出现漏洞较多的OA&#xff0c;在经过多轮的迭代之后已经很少前台的RCE漏洞了。一般来说…

Redis实战案例27-UV统计

1. Redis的HyperLogLog的统计功能 示例&#xff1a; 表明HyperLogLog不管加入重复元素多少次都不会让count&#xff0c;不会计数重复元素&#xff0c;所以适合做UV计数 2. 简单实现UV测试 通过单元测试&#xff0c;向 HyperLogLog 中添加 100 万条数据&#xff0c;看看内存占…

RocketMQ使用

说明&#xff1a;本文介绍RocketMQ的消费模式&消息类型&#xff0c;RocketMQ的安装参考及简单使用&#xff0c;参考&#xff1a;http://t.csdn.cn/BKFPj 消费模式 RocketMQ与RabbitMQ最大的区别在于&#xff0c;RocketMQ是根据消息的Topic锁定消费者的&#xff0c;Topic属…

黑马机器学习day1

1.sklearn数据集 sklearn.datasets datasets.load_*() 获取小规模的数据集 datasets.fetch_*(data_homeNone) 获取大规模数据集 函数的第一个参数是data_home,标识数据集下载目录&#xff0c;默认/scikit_learn_data/ 1.1sklearn小数据集 sklearn.da…

【JAVA】类和对象

作者主页&#xff1a;paper jie的博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文录入于《JAVASE语法系列》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精…

使用eXosip+ffmpeg、ffplay命令行实现sip客户端

文章目录 前言一、关键实现1、主要流程2、解决端口冲突&#xff08;1&#xff09;、出现原因&#xff08;2&#xff09;、解决方法 3、解析sdp&#xff08;1&#xff09;、定义实体&#xff08;2&#xff09;、解析视频&#xff08;3&#xff09;、解析音频 4、命令行推拉流&am…

Python中搭建IP代理池的妙招

在Python的爬虫世界里&#xff0c;你是否也想搭建一个功能强大的IP代理池&#xff0c;让你的爬虫无忧无虑地畅游各大网站&#xff1f;今天&#xff0c;我就来教你使用Scrapy框架搭建IP代理池&#xff0c;让你的爬虫更加智能、高效&#xff01;跟着我一步一步来&#xff0c;轻松…

(力扣)用两个队列实现栈---C语言

分享一首歌曲吧&#xff0c;希望在枯燥的刷题生活中带给你希望和勇气&#xff0c;加油&#xff01; 题目&#xff1a; 请你仅使用两个队列实现一个后入先出&#xff08;LIFO&#xff09;的栈&#xff0c;并支持普通栈的全部四种操作&#xff08;push、top、pop 和 empty&#…

【单片机】51单片机,TLC2543,驱动程序,读取adc

TLC2543 是一款 12 位精密模数转换器 (ADC)。 1~9、11、12——AIN0&#xff5e;AIN10为模拟输入端&#xff1b; 15——CS 为片选端&#xff1b; 17——DIN 为串行数据输入端&#xff1b;&#xff08;控制字输入端&#xff0c;用于选择转换及输出数据格式&#xff09; 16——…

大数据课程G2——Hbase的基本架构

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 掌握Hbase的基本架构; ⚪ 掌握Hbase的读写流程; ⚪ 掌握Hbase的设计与优化; 一、基本架构 1. HRegion 1. 在HBase中,会将一个表从行键方向上进行切分,切分成1个或者多个HRegion。 …

C++入门(小白篇1—编译器安装-代码注释等)

前言&#xff1a; 最近想学一下一下C看了一些博客内容写的倒是很充实&#xff0c;但是&#xff0c;细节不到位&#xff0c;我是有Python基础的&#xff0c;所以学习来蛮快的&#xff0c;但是对于小白的话&#xff0c;有好多小细节大多数博客还是不够详细&#xff0c;由此我想写…

对话Sam Altman与Greg Brockman:初心和过去,信念和现在,责任和未来

导读 近日&#xff0c;硅谷著名投资人Reid Hoffman和Aria Finger联手对Sam Altman和Greg Brockman进行了一场访谈&#xff0c;访谈涉及到主题有&#xff1a;OpenAI的使命&#xff0c;人工智能对教育、医疗等行业的变革性影响&#xff0c;人工智能如何面对监管&#xff0c;OpenA…