FFplay源码分析-streams_open

2024-06-24 01:58

本文主要是介绍FFplay源码分析-streams_open,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《FFmpeg原理》的社群来了,想加入社群的朋友请购买 VIP 版,VIP 版有更高级的内容与答疑服务。


本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8

FFplay 源码分析系列以一条简单的命令开始,ffplay -i a.mp4。a.mp4下载链接:百度网盘,提取码:nl0s 。


如下图所示,本文主要讲解 streams_open() 函数的内部逻辑。

在这里插入图片描述
在这里插入图片描述

如上图所示,streams_open() 里面比较重要的函数就是 初始化 frame_queue 跟 packet_queue,我画一个数据结构图会更容易理解。

数据结构图,我画着画着,不小心画出了整体的数据关系图,不过大家暂时只需要关注 Frame 那块就行。

在这里插入图片描述

如上图所示,我先简单讲解一下上面全局的数据关系图。

可以看到 struct VideoState 统领了全局。每一个数据流,音频流,视频流,数据流,都有各自的 struct FrameQueuestruct PacketQueuestruct Decoder

  • struct FrameQueue : frame 缓存队列,用于给SDL播放。
  • struct PacketQueue: packet 缓存队列,用来解码后 插入 frame 缓存队列。
  • struct Decoder :解码器封装。

有一点需要注意的是 FrameQueue 是一个静态数组,PacketQueue 是一个动态链表。 FrameQueue 相对复杂一点,后面会详细讲解。


先介绍一下 struct Frame 结构体里面各个成员的作用。

  • *AVFrame frame; 存储视频或音频的 frame。
  • AVSubtitle sub; 存储字幕数据,不用关注。
  • int serial; Frame 的 serial 号,作用是 拖进度条,改变播放位置后,丢弃之前缓存的旧Frame。第三篇文章说过,改变播放位置,就会插入一个 flush_pktPacketQueue 队列,导致 PacketQueue::serial + 1 ,但之前 FrameQueue 里里面缓存的 Frame的serial 并没有+1,所以 旧的 Frame::serial 不等于新的 PacketQueue::serial ,所以播放位置改变之后,缓存里面旧的Frame就会丢弃不进行播放。
  • double pts; 由AVFrame的pts转换得来,播放时间,单位是 秒。
  • double duration; 持续时间,由 frame_rate 帧率计算得到,单位是 秒。
  • int64_t pos; 等于 AVFrame::pkt_pos。
  • int width; 等于 AVFrame::width
  • int height; 等于 AVFrame::height
  • int format; 等于 AVFrame::format
  • AVRational sar; 等于 AVFrame::sample_aspect_ratio。 sar 这个变量需要仔细讲解一下,我刚开始也被这个sar搞懵。我之前以为 sar 等于 width/height 。后来发现不是。其实sar 是以前的显示设备设计的历史遗留问题,不用过多关注,只需要知道,显示的时候用 sar 这个比例拉伸width 跟height 作为显示窗口,图像播放就不会扭曲了。推荐阅读,ffmpeg解析出的视频参数PAR,DAR,SAR的意义 跟 theory-videoaspectratios
  • int uploaded; uploaded只对视频帧有效,音频帧没用。当前AVFrame的数据是否已经渲染到 VideoState::vid_texture ,vid_texture 是一个 SDL_Texture 结构。视频帧为什么要定义这么一个变量uploaded呢?是因为音视频同步有时候需要重复播放上一帧视频,上一帧如果已经拷贝过给 SDL_Texture 了,uploaded 就会置为 1,就可以直接把 VideoState::vid_texture 丢给 SDL 的render渲染就行,节省开销。
  • int flip_v; 貌似是控制播放翻转的,AVFrame::linesize 如果是负数,flip_v 就是 1。不用特别关注。

接着介绍一下 struct FrameQueue 结构体里面各个成员的作用。

  • Frame queue[FRAME_QUEUE_SIZE] ; Frame 静态数组,用来存储多个Frame,形成队列,FRAME_QUEUE_SIZE等于16。
  • int rindex; 读索引,当前读取到那个Frame,通过 queue[rindex] 可以取出 Frame。(实际上还有一个 rindex_shown,这个变量后续讲解)
  • int windex; 写索引,通过 queue[windex] 判断解码出来的AVFrame 应该写到哪里。
  • int size; 当前队列已经缓存了多少个 Frame。
  • int max_size; 队列最多可缓存多少 Frame,max_size 可以 小于 FRAME_QUEUE_SIZE。
  • int keep_last; 播放之后是否保存上一帧在队列里面不销毁
  • int rindex_shown; 需要重点讲解
  • *SDL_mutex mutex; 锁,队列修改的时候需要加锁。
  • *SDL_cond cond; 条件变量,用于解码线程跟播放线程通信。例如如果 FrameQueue 数据满了,没法再写就会加锁,然后wait cond等待这个条件变量被唤醒。那这个条件变量什么时候会被唤醒呢?播放的时候需要取 FrameQueue 的数据出来播放,播放完后销毁一帧数据,这时候 FrameQueue 数据就不满了,就会 signal cond,通知解码线程继续解码,继续写 FrameQueue 。相反,如果 FrameQueue 数据空了,播放线程也会加锁,然后wait cond等待这个条件变量被唤,解码线程有数据写入 FrameQueue 了,就会通过 cond 唤醒播放线程继续播放。
  • *PacketQueue pktq; 对应的 Packet 队列,frame 都是从 packet 解码出来的。

FrameQueue 里面的 keep_lastrindex_shown 这两个变量比较难懂,我仔细讲解一下。keep_last 其实是一个临时变量,他的作用是控制 rindex_shown 变成 1。rindex_shown 这个变量名我个人认为起的不太好,我一眼看这个变量没法看出他是做什么的。或者说这个逻辑本身就复杂,跟变量命名无关。

keep_last 是如何控制 rindex_shown 变成 1 的呢,请看下面代码。

static void frame_queue_next(FrameQueue *f)
{if (f->keep_last && !f->rindex_shown) {f->rindex_shown = 1; //注意这行代码。return;}frame_queue_unref_item(&f->queue[f->rindex]);if (++f->rindex == f->max_size)  //注意这行代码。f->rindex = 0;SDL_LockMutex(f->mutex);f->size--;SDL_CondSignal(f->cond);SDL_UnlockMutex(f->mutex);
}

可以看到 f->keep_last && !f->rindex_shown 会导致 rindex_show 置为 1,然后就return 了,没有执行后面的 frame_queue_unref_item()++f->rindex 也没有执行,也就是说 rindex 读索引没有偏移。

到这里,读者可能会有疑惑,如果 rindex 读索引没有偏移,下次用 queue[rindex] 的方式读下一帧不就读不到了吗?

对,queue[rindex] 确实读不到下一帧,所以ffplay 读取队列帧用的是 queue[ rindex + rindex_shown ]

分析到这里,可能读者到这里还是不太理解,我举个例子。

假如现在播放线程从 FrameQueue 里面取第一帧出来播放,因为是第一帧,所以 rindex_shown 此时是 0,rindex 也是 0。queue[ rindex + rindex_shown ]

这样确实取出来的是 0 索引的Frame。播放完之后,就会调 frame_queue_next() 函数,因为是第一次调 frame_queue_next(),所以就跑进去 if (f->keep_last && !f->rindex_shown) 这个条件,然后return,第一帧的frame数据没有销毁, rindex 读索引也没有偏移,但是,但是 rindex_shown 变成 1了,queue[ rindex + rindex_shown ] 就能正确读到 第二帧了。

其实源代码读取播放帧 真正的用法 是 queue[ (rindex + rindex_shown) % f->max_size] ,里面还有 % f->max_size 操作,这是个回环处理。例如如果 rindex 已经递增成 16 ,max_size 是16,那 16%16 ,取余数。就是0,rindex再+1,17%16,余数就是1。

这个就是 keep_lastrindex_shown 的作用,保存播放的上一帧在队列里面。那保存在队列里面有什么用呢?用视频流举例,当SDL窗口变小的时候,ffplay 可以取上一帧frame,重新渲染 texture 来适应缩小后的窗口大小。

还有一个注意的地方是,字幕流没有用到 keep_last 跟 rindex_shown,音频流好像也是没用到 keep_last 跟 rindex_shown,虽然音频流初始化队列的时候把 keep_last 设置为1,我个人感觉好像只有视频流用到 keep_last 跟 rindex_shown。

下面是初始化 Frame 队列代码,注意看最后一个参数 keep_last 是1 还是 0。

if (frame_queue_init(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_SIZE, 1) < 0)goto fail;
if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)goto fail;
if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)goto fail;

好了,分析到这里,一部分的数据结构已经分析完毕,文章开头的 frame_queue_init() 自行看代码即可理解,就是一些变量赋值。

然后 **stream_open() **后面的 init_clock() 初始化时钟,本文暂时不详细分析时钟,先简单介绍时钟的作用,clock 是用来控制音视频同步的,如果只播放视频,实际上clock这个结构体是没用的,只播放音频也是。视频就按照帧率播放,音频就按照采样率播放就行。但如果音视频同时播放,时间长了会产生不同步,至于为什么会不同步,自行百度,答案太多了,我不粘贴了。

stream_open() 函数的最后 调用了SDL_CreateThread 创建一个新的线程,read_thread,这是第二个线程。

is->read_tid     = SDL_CreateThread(read_thread, "read_thread", is);

read_thread() 函数下篇文章再继续分析。


ffplay 源码分析,stream_open() 分析完毕。

©版权所属:弦外之音。

由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。

这篇关于FFplay源码分析-streams_open的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1088869

相关文章

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

衡石分析平台使用手册-单机安装及启动

单机安装及启动​ 本文讲述如何在单机环境下进行 HENGSHI SENSE 安装的操作过程。 在安装前请确认网络环境,如果是隔离环境,无法连接互联网时,请先按照 离线环境安装依赖的指导进行依赖包的安装,然后按照本文的指导继续操作。如果网络环境可以连接互联网,请直接按照本文的指导进行安装。 准备工作​ 请参考安装环境文档准备安装环境。 配置用户与安装目录。 在操作前请检查您是否有 sud

线性因子模型 - 独立分量分析(ICA)篇

序言 线性因子模型是数据分析与机器学习中的一类重要模型,它们通过引入潜变量( latent variables \text{latent variables} latent variables)来更好地表征数据。其中,独立分量分析( ICA \text{ICA} ICA)作为线性因子模型的一种,以其独特的视角和广泛的应用领域而备受关注。 ICA \text{ICA} ICA旨在将观察到的复杂信号

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。