FFplay暂停分析
暂停也是播放器非常常见的功能。对于 FFplay 播放器,可以通过 p
键 或者空格键 来切换暂停状态。
先来看一下处理 p
键 的代码,如下:
从上图可以看到,调了 toggle_pause()
函数,注意这个 cur_stream
参数,其实这个参数不是视频流或者音频流,而是 FFplay 的全局管理器 VideoState
toggle_pause()
函数的实现如下:
static void toggle_pause(VideoState *is)
{stream_toggle_pause(is);is->step = 0;
}
后面为什么会把 is->step
置为 0 ?is->step
变量是给逐帧播放用的,为 0 代表退出逐帧播放模式。
这是因为逐帧播放实际上就是用切换暂停状态来实现的,每播放一帧就立即暂停。具体请看《FFplay逐帧播放分析》
再来看一下 stream_toggle_pause()
函数的实现,如下:
从 启动状态 切换到 暂停状态的时候,is->paused
等于 0,所以是不会跑进去 if (is->paused) {...}
里面的。
上图中用 set_clock()
更新了外部时钟,set_clock()
函数会把 Clock::pts
更新到当前最新的播放时刻。
通常情况下,get_clock()
函数获取当前的最新播放时刻,是用 Clock::pts
+ 消逝的时间。
但是消逝的时间可以是 0 的,什么情况下消逝的时间是 0 呢?就是暂停的时候,当播放器暂停的时候,他外部时钟也会暂停,所以消逝的时间为 0 ,如下:
stream_toggle_pause()
函数才需要更新外部时钟的 pts 成最新的值,如果不更新,在暂停状态下获取到的外部时钟播放时刻就不准。
在暂停状态下,哪段代码还会调 get_clock()
获取外部时钟的播放时刻呢?这个后面揭晓。
stream_toggle_pause()
函数最后还会把 4 个暂停变量都被设置成 1,或者 从 1 切换成 0 。如下:
is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
我们来看一下这 4 个暂停变量会影响哪些代码逻辑?
首先是 main 主线程,主线程主要是处理键盘事件 跟 播放视频画面,键盘事件不会受到暂停状态影响,该处理还是继续处理。
播放视频画面 的函数是 video_refresh()
,但是暂停状态下也不会调 video_refresh()
,如下:
但是暂停状态下,如果改变了 ffplay
窗口大小,is->forece_refresh
机会变成 1,就会调 video_refresh()
。
因此 main 主线程,在暂停状态下,只会不断检测,处理键盘事件以及一些窗口事件,大部分时候并不会调 video_refresh()
播放视频画面。
再来看一下 read_thread解复用线程,在暂停状态下,它在干什么?
上图的是为了兼容一些网络流的播放,有些流媒体协议支持暂停跟播放操作,当暂停的时候,服务器端就不会再推流过来了。对于本地播放,上面的代码是没用的。
我翻了一圈 read_thread()
函数的代码,发现它并不会因为 pause 变成 1 而停下,read_thread()
线程即使在暂停状态下,也是不断运行,不断读取数据,直到 塞满队列,塞满队列就会休眠 10 ms,如下:
感兴趣的读者可以在暂停状态下往 SDL_CondWaitTimeout()
那里打个断点,会不断跑进去那里的逻辑。
再来看一下 video_thread视频解码线程,在暂停状态下,它在干什么?
研究 video_thread()
函数的代码,我们发现了第一个在暂停状态下,获取外部时钟的地方,也就是 video_thread()
里面的 get_video_frame()
函数
当外部时钟是 主时钟的时候,这里的 get_master_clock()
获取的就是外部时钟的播放时刻。因此如果在 stream_toggle_pause()
里面不更新外部时钟,这里获取到的时间就是错误的,会导致误判,丢帧。
翻了一圈 video_thread()
解码线程的代码,发现也不会受到 paused 的影响,还是会正常解码,但是如果塞满 FrameQueue
队列的时候,就会一直阻塞在 frame_queue_peek_writable()
函数里面。
再来看一下 audio_thread音频解码线程,在暂停状态下,它在干什么?
研究发现,跟 video_thread()
视频解码类似,也不会受到 paused 的影响,还是会正常解码,但是如果塞满 FrameQueue
队列的时候,就会一直阻塞在 frame_queue_peek_writable()
函数里面。
最后来看一下 sdl_audio_callback音频播放线程分析,在暂停状态下,它在干什么?
首先,sdl_audio_callback()
是回调函数,所以你可以猜到,肯定不会阻塞,肯定会返回去给 SDL 。
首先,sdl_audio_callback()
是会受到 paused 的影响的,如下:
暂停状态下,audio_decode_frame()
函数会直接返回 -1,就会导致 输出静音数据,只要直接把 stream 指向的内存数据设置为 0 就是输出静音数据了。
memset(stream, 0, len1);
虽然是输出静音数据,但是音频播放线程还是在跑的,没有阻塞,她在跑,就会更新音频时钟,如下:
虽然会更新音频时钟,但是因为在暂停状态下没有跑进去 audio_decode_frame(),所以 is->audio_clock
没有更新。因此即便更新音频时钟,也是用原来的值来更新的。
所以音频时钟相当于没有更新。
读者可以在 sdl_audio_callback()
入口加个日志,如下:
会发现虽然音频时钟一直在跑,但是这个 get_clock()
返回的值是没有增长的。
总结一下,从启动状态切换到暂停状态,影响的地方。
1,导致视频播放函数没有调用,导致 FrameQueue
堆积,所以视频解码线程会阻塞在 frame_queue_peek_writable()
函数里面
2,导致音频播放线程直接输出静音数据,没有从 FrameQueue
读数据,导致 FrameQueue
堆积,所以音频解码线程会阻塞在 frame_queue_peek_writable()
函数里面。
3,音频解码线程,视频解码线程阻塞,一直不从 PacketQueue 拿数据去解码,导致 PacketQueue
堆积,从而导致 read_thread
解复用线程不再从文件读取数据了。
回到 stream_toggle_pause()
函数,当从暂停状态切换到启动状态的时候,有一段逻辑非常奇怪,如下:
可以看到,它会会更新 is->frame_timer
跟 视频时钟,为什么要这么做呢?
因为 is->frame_timer
的单位是系统时间,代表窗口现在这一帧画面是在什么系统时间开始播放的。更新 is->frame_timer
之后代表窗口当前画面是从此刻开始播放的。
暂停之后,系统时间是一直在跑的。例如,暂停 8 秒钟之后,再调 av_gettime_relative()
会发现比之前多了 8 秒。
因此切换回来的时候,要及时更新 is->frame_timer
,这个变量的含义不能变的。如果你不更新,就代表视频播放线程卡顿,没有调度过来,导致的这帧画面播放了 8 秒,虽然他确实是播放了 8秒,但是不能体现在 is->frame_timer
变量上面。
后面在 video_refresh()
里面会用 is->frame_timer
来判断是否已经超过预定的播放时刻,超过了就会丢帧,如下:
上图中,只有超过 10 秒才会纠正 is->frame_timer
,如果你只暂停了 8 秒就不会纠正。
因此,如果在 暂停状态切换到启动状态的时候,不更新 is->frame_timer
,就会导致 8 秒的视频帧被丢弃,因为会误判它们都已经过了预定的播放时刻。
再来看一下,从暂停状态切换到启动状态的时候,为啥需要更新视频时钟,如下:
注意,他是先把视频时钟的暂停状态恢复,然后再更新时钟的,所以 get_clock()
获取的是 pts + 消逝的时间,消逝的时间其实就等于暂停了多久。
其实我也不太清楚 这里更新 视频时钟的 pts 的具体作用,因为暂停状态已经恢复了,随意即使这里不执行 set_clock()
,get_clock()
获取到的也是正确的播放时刻。
个人猜测是字幕的场景用的,因为那里用了 clock 里面的 pts 字段。
至此,ffplay
播放器暂停功能分析完毕。
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:
Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习
FFplay暂停分析相关推荐
- FFMPEG 3.4.2 - ffplay源代码分析 (三)
1.数据结构之VideoState VideoState是所有其他数据结构的母体. main 线程启动新线程read_thread,初始化VideoState. AVFormatContext保存与& ...
- FFmpeg学习笔记之ffplay流程分析
背景说明 FFmpeg是一个开源,免费,跨平台的视频和音频流方案,它提供了一套完整的录制.转换以及流化音视频的解决方案.而ffplay是有ffmpeg官方提供的一个基于ffmpeg的简单播放器.学习f ...
- FFplay退出分析
当 FFplay播放器播放完一个 mp4 文件的时候,画面就会停止在最后一帧,并不会自动退出,如下: 有两种方式可以退出 FFplay 播放器. 1,在命令行使用 autoexit 参数,这是一个布尔 ...
- FFplay序列号分析
在之前的几篇文章里面,都零零散散提及过序列号这个概念.但是 序列号 这个概念对于 FFplay 播放器 非常重要的,很多代码都跟序列号有关.所以单独写一篇文章介绍序列号. 序列号 主要是给 快进快退 ...
- FFplay源代码分析:整体流程图
转自:雷博 http://blog.csdn.net/leixiaohua1020/article/details/11980843 转载于:https://www.cnblogs.com/dk666 ...
- 音视频技术之ffplay源码分析-音视频同步
音视频同步的目的是为了使播放的声音和显示的画面保持一致.视频按帧播放,图像显示设备每次显示一帧画面,视频播放速度由帧率确定,帧率指示每秒显示多少帧:音频按采样点播放,声音播放设备每次播放一个采样点,声 ...
- ffplay.c学习-2-数据读取线程
ffplay.c学习-2-数据读取线程 目录 准备⼯作 avformat_alloc_context 创建上下⽂ ic->interrupt_callback avformat_open_inp ...
- ffplay.c学习-1-框架及数据结构
ffplay.c学习-1-框架及数据结构 目录 ffplay.c的意义 FFplay框架分析 数据结构分析 struct VideoState 播放器封装 struct Clock 时钟封装 stru ...
- ffplay for mfc 代码备忘
之前上传了一个开源播放器工程ffplay for mfc.它将ffmpeg项目中的ffplay播放器(ffplay.c)移植到了VC的环境下,并且使用MFC做了一套界面.它可以完成一个播放器播放视频的 ...
最新文章
- mysql命令创建模式_mysql在命令行模式下创建数据库时要显式指定字符集
- 档案中级职称计算机需要考几个模块,2020年职称申报需要准备哪些档案资料?这些细节必须知道!...
- PM Basic Skill---Communicate Plan
- UA MATH564 概率不等式 QE练习题
- 打开高效文本编辑之门_Linux awk之关联数组
- appium java 测试用例_如何在C#中使用Appium编写测试用例?
- 前端程序员能力不足?表现在哪几点,你需要加强的地方!
- 《软件需求分析(第二版)》第 1 章——软件需求基础知识 重点部分总结
- html资源文件放在哪里,09 Spring Boot开发web项目之静态资源放哪里?
- 查找在Git中删除文件的时间
- 【LeetCode】【数组】题号:485,最大连续1的个数
- 摄影测量学之共线方程的应用
- mysql中gtid关闭方法_CDH-mysql 开启关闭 gtid
- 第二章 Maven的安装和配置
- Visual studio +Intel Fortran 环境安装与设置
- 原神ios android,原神ios和安卓数据互通吗 原神ios和安卓能一起玩吗
- 详解rem布局-利用rem布局实现移动端高清显示
- 雷电模拟器 手机模拟器 安装 连接
- Mac苹果 M1配置cocoapods
- 携程网今日瘫痪 官方称遭到不明攻击
热门文章
- Python读取雷达位姿数据方案二
- windows下使用OpenGL实现yuv420p转rgb播放视频(三重纹理实现)
- LitePal基本使用概述
- Stata:模型结果如何导入到Word和Excel。
- App打开小程序,无法跳转回App的问题
- 【LeetCode】6. ZigZag Conversion Z 字形变换
- Json文件转换为Excel文件!涉及读文件,时间戳转化,写文档
- [笔记].电机行业常用的中英文对照
- php封装多段mp4,解决ffmpeg将多段视频裁剪拼接后卡顿现象
- 成都正规python培训班