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; // 队⾸,队尾指针 3int 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_pktSDL_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);       //放入失败,释放AVPacketreturn 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;elseq->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--;    //节点数减1q->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);      //释放节点内存,只是释放节点,而不是释放AVPacketret = 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的作⽤展开分析。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

11-数据结构-栈和队列的应用(C语言)

栈和队列的应用 目录 栈和队列的应用 一、括号匹配&#xff08;栈&#xff09; 二、表达式的各种转换 (1)中缀转后缀(手工) (2)后缀转中缀表达式(手工) (3)中缀转后缀(栈) (4)中缀转后缀&#xff08;树&#xff09; (5)后缀表达式求值 (6)中缀表达式求值&#xff08;栈…

阻抗是什么?什么时候要考虑阻抗匹配?

在电路设计中&#xff0c;我们常常碰到跟阻抗有关的问题&#xff0c;那么到底什么是阻抗&#xff1f; 在具有电阻、电感和电容的电路里&#xff0c;对电路中电流所起的阻碍作用叫做阻抗。常用Z来表示&#xff0c;它的值由交流电的频率、电阻R、电感L、电容C相互作用来决定。由…

【黑马头条之xxl-Job分布式任务调度】

本笔记内容为黑马头条项目的分布式任务调度热点文章部分 目录 一、今日内容 1、需求分析 2、实现思路 3、定时计算 4、定时任务框架-xxljob 二、分布式任务调度 1、什么是分布式任务调度 2、xxl-Job简介 3、XXL-Job-环境搭建 4、配置部署调度中心-docker安装 5、xx…

系统学习Linux-Redis集群

目录 一、Redis主从复制 概念 作用 缺点 流程 二、Reids哨兵模式&#xff08;sentinel&#xff09; 概念 作用 缺点 结构 搭建 三、redis集群 概述 原理 架构细节 选举过程 实验环境模拟 一、Redis主从复制 概念 是指将一台Redis服务器的数据&#xff0c;复制…

安防监控视频汇聚平台EasyCVR分发的FLV视频流在VLC中无法播放是什么原因?

众所周知&#xff0c;TSINGSEE青犀视频汇聚平台EasyCVR可支持多协议方式接入&#xff0c;包括主流标准协议国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。在视频流的处理与分发上&#xff0c;视频监控…

java之junit Test

JUnit测试简介 1.什么是单元测试 单元测试是针对最小的功能单元编写测试代码Java程序最小的功能单元是方法单元测试就是针对单个Java方法的测试 2.测试驱动开发 3.单元测试的好处 确保单个方法运行正常如果修改了方法代码&#xff0c;只需确保其对应的单元测试通过测试代码…

佛祖保佑,永不宕机,永无bug

当我们的程序编译通过&#xff0c;能预防的bug也都预防了&#xff0c;其它的就只能交给天意了。当然请求佛祖的保佑也是必不可少的。 下面是一些常用的保佑图&#xff1a; 佛祖保佑图 ——————————————————————————————————————————…

【uniapp】uniapp设置安全区域:

文章目录 一、效果图:二、实现代码: 一、效果图: 二、实现代码: {"path": "pages/index/index","style": {"navigationStyle": "custom","navigationBarTextStyle": "white","navigationBarTitle…

List list=new ArrayList()抛出的ArrayIndexOutOfBoundsException异常

1.应用场景&#xff0c;今天生产日志监控到一组new ArrayList() 进行add 异常&#xff0c;具体日志如下&#xff1a; eptionHandler.handler(178): TXXYBUSSINESS|执行异常 java.util.concurrent.CompletionException: java.lang.ArrayIndexOutOfBoundsException: Index 1 out…

PyTorch翻译官网教程-NLP FROM SCRATCH: GENERATING NAMES WITH A CHARACTER-LEVEL RNN

官网链接 NLP From Scratch: Generating Names with a Character-Level RNN — PyTorch Tutorials 2.0.1cu117 documentation 使用字符级RNN生成名字 这是我们关于“NLP From Scratch”的三篇教程中的第二篇。在第一个教程中</intermediate/char_rnn_classification_tutor…

MongoDB的下载和安装

一、MongoD下载 下载地址&#xff1a;https://www.mongodb.com/try/download/community 二、安装 因为选择下载的是 .zip 文件&#xff0c;直接跳过安装&#xff0c;一步到位。 选择在任一磁盘创建空文件夹&#xff08;不要使用中文路径&#xff09;&#xff0c;解压之后把文…

android 如何分析应用的内存(十七)——使用MAT查看Android堆

android 如何分析应用的内存&#xff08;十七&#xff09;——使用MAT查看Android堆 前一篇文章&#xff0c;介绍了使用Android profiler中的memory profiler来查看Android的堆情况。 如Android 堆中有哪些对象&#xff0c;这些对象的引用情况是什么样子的。 可是我们依然面临…

flink kafka消费者如何处理kafka主题的rebalance

背景&#xff1a; 我们日常使用kafka客户端消费kafka主题的消息时&#xff0c;当消费者退出/加入消费者组&#xff0c;kafka主题分区数有变等事件发生时&#xff0c;都会导致rebalance的发生&#xff0c;此时一般情况下&#xff0c;如果我们不自己处理offset&#xff0c;我们不…

深入理解PyTorch中的NoamOpt优化器

深入理解PyTorch中的NoamOpt优化器 作者&#xff1a;安静到无声 个人主页 今天&#xff0c;我们将深入探讨一个在自然语言处理领域广泛使用的优化器——NoamOpt。这个优化器是基于PyTorch实现的&#xff0c;并且在"Attention is All You Need"这篇论文中首次提出。…

c++11-14-17_内存管理(RAII)_多线程

文章目录 前言&#xff1a;什么是RAII&#xff1f;指针/智能指针&#xff1a;使用智能指针管理内存资源&#xff1a;unique_ptr的使用&#xff1a;自定义删除器&#xff1a; shared_ptr的使用&#xff1a;shared_ptr指向同一个对象的不同成员&#xff1a;自定义删除函数&#x…

centos7 安装桌面

先装 xrdp $ sudo yum install -y epel-release $ sudo yum install -y xrdp $ sudo systemctl enable xrdp $ sudo systemctl start xrdp开防火墙端口 $ sudo firewall-cmd --add-port3389/tcp --permanent $ sudo firewall-cmd --reload比较喜欢 GNOME $ sudo yum groupin…

Stable Diffusion - 幻想 (Fantasy) 风格与糖果世界 (Candy Land) 人物提示词配置

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132212193 图像由 DreamShaper8 模型生成&#xff0c;融合糖果世界。 幻想 (Fantasy) 风格图像是一种以想象力为主导的艺术形式&#xff0c;创造了…

Vue.js2+Cesium1.103.0 七、Primitive 绘制航线元素

Vue.js2Cesium1.103.0 七、Primitive 绘制航线元素 用 Primitive 绘制航线元素&#xff0c;包括航点图标&#xff0c;航线线段&#xff0c;线段距离标注&#xff0c;航点序号&#xff0c;海拔标注&#xff0c;总航程等信息。 可同时绘制多条航线&#xff1b;可根据 id 清除指…

亚马逊 EC2服务器下部署java环境

1. jdk 1.8 安装 1.1 下载jdk包 官网 Java Downloads | Oracle tar.gz 包 下载下来 1.2 本地连接 服务器 我用的是亚马逊的ec2 系统是 ubuntu 的 ssh工具是 Mobaxterm , 公有dns 创建实例时的秘钥 链接 Mobaxterm 因为使用的 ubuntu 所以登录的 名称 就是 ubuntu 然后 …

php代码审计,php漏洞详解

文章目录 1、输入验证和输出显示2、命令注入(Command Injection)3、eval 注入(Eval Injection)4、跨网站脚本攻击(Cross Site Scripting, XSS)5、SQL 注入攻击(SQL injection)6、跨网站请求伪造攻击(Cross Site Request Forgeries, CSRF)7、Session 会话劫持(Session Hijacking…