从Chrome源码看audio/video流媒体实现二(转)
第一篇主要介绍了Chrome加载音视频的缓冲控制机制和编解码基础,本篇将比较深入地介绍解码播放的过程。以Chromium 69版本做研究。
由于Chromium默认不能播放Mp4,所以需要需要改一下源码重新编译一下。
1. 编译一个能播放mp4的Chromium
自行编译出来的Chromium是无法播放mp4视频,在官网下载的也不行,终端会提示这个错误:
[69542:775:0714/132557.522659:ERROR:render_media_log.cc(30)] MediaEvent: PIPELINE_ERROR DEMUXER_ERROR_NO_SUPPORTED_STREAMS
说是在demux即多路解复用的时候发生了错误,不支持当前流格式,也就是Chromium不支持mp4格式的解析,这是为什么呢?经过一番搜索和摸索,发现只要把ffmpeg的编译模式从Chromium改成Chrome就可以。编辑third_party/ffmpeg/ffmpeg_options.gni这个文件,把前面几行代码改一下——把_default_ffmpeg_branding强制设置成Chrome,再重新编译一下就行了,如下代码所示:
# if (is_chrome_branded) {
_default_ffmpeg_branding = "Chrome"
# } else {
# _default_ffmpeg_branding = "Chromium"
# }复制代码
编译出来的Chromium就能播放视频了。Chromium工程的编译目标branding可以设置成Chrome(正式版)/Chromium/Chrome OS三种模式,ffmpeg的编译设定会根据这个branding类型自动选择它自己的branding,如上代码的判断,如果branding是Chrome,会额外多加一些解码器,就能够播放mp4了。
不过如果你想编译成正式版Chrome,由于缺少相关的主题theme文件,是编译不了的。
另外它还有一个proprietary_codecs的设置:
proprietary_codecs = is_chrome_branded || is_chromecast复制代码
编译正式版的Chrome会默认打开,它的作用是增加一些额外的解码器,如对EME(Encrypted Media Extensions)加密媒体扩展的支持。
最后一开始不能打开和能打开播放的效果对比如下图所示:
那么为什么Chromium不直接打开mp4支持呢,它可能受到mp4或者ffmpeg的一些开源协议和专利限制。
2. mp4格式和解复用
一个视频可以有3个轨道(Track):视频、音频和文本,但是数据的存储是一维的,从1个字节到第n个字节,那么视频应该放哪里,音频应该放哪里,mp4/avi等格式此做了规定,把视轨音轨合成一个mp4文件的过程就叫多路复用(mux),而把mp4文件里的音视频轨分离出来就叫多路解复用(demux),这两个词是从通信领域来的。
假设现在有个需求,需要取出用户上传视频的第一帧做为封面。这就要求我们去解析mp4文件,并做解码。
我们以这个mountain.mp4时长为1s大小487kB的mp4视频做为研究对象,用sublime等编辑器打开显示其原始的二进制内容,如下图所示:
上图是用16进制表示的原始二进制内容,两个16进制(0000)就表示1个字节,如上图第4个字节是0x18。
6674 7970 => ftyp
ftyp盒子的作用是用来标志当前文件类型,紧接着的4个字节表示它是一个微软的MPEG-4格式,即平常说的mp4:
6d70 3432 => mp42
综上,第1个盒子整体解析如下图所示:
同样对第二个盒子做分析,如下图所示:
最后面是一个mdat的盒子,这个就是放多媒体数据的盒子,大小为492242B,它占据了mp4文件的绝大部分空间。moov里的chunk的位置偏移offset就是相对于mdat的。
上面我们一个字节一个字节对照着解析比较累,可以用一些现成的工具,如这个在线的MP4Box.js或者是这个MP4Parser,如下图所示,moov里面总共有两个轨道的盒子:
展开视频轨道的子盒子,找到stsz这个盒子,可以看到总共有24帧,每一帧的大小也是可以见到,如下图所示:
这里我们发现最大的一帧有98KB,最小的一帧只有3KB,一帧就表示一张图像,为什么不同帧差别会这么大呢?
因为有些帧是关键帧(I帧,Intra frame),包含了该帧的完整图象信息,所以比较大,I帧可做为参考帧。另一些帧只是记录和参考帧的差异,叫帧间预测帧(Inter frame),所以比较小,预测帧有前向预测帧P帧和双向预测帧B帧,P帧是参考前面解码过的图像,而B帧参考双向的。所以只是拿到预测帧是没有意义的,需要它前面的那个参考帧才能解码。参考帧(h264)的压缩比类似jpg,一般可达7比1,而预测帧的压缩比可达几十比1。
接着这些帧是怎么存放的呢,它们分别是放在哪些chunk里面的呢,每个chunk的位置又在哪里?如下图stco的盒子所示:
可以看到,总共有3个chunk,每个chunk的位置offsset也都指明。而每个chunk有多少个sample的信息是放在stsc这个盒子里面,如下图所示:
而终止位置是236645 + 6274 = 242919,所以第13帧存放在mdat的[236645, 242919)区间。这里有一篇文章介绍了怎么取mp4帧数据的算法,和我们上面分析的过程类似。
这个帧(13帧)这么小,它很可能不是一个关键帧。具体怎么判断它是不是一个关键帧,主要通过帧头部信息里的nal类型,值为5的则为关键帧,这个要涉及到具体的解码过程了。
还有一个问题,怎么知道这个mp4是h264编码,而不是h265之类的,这个通过avc1盒子可以知道,如下图所示:
avc1就是h.264的别名,这个盒子里面放了很多解码需要的参数,如level、SPS、PPS等,如最大参考帧的数目等,参考上图注解。如果最大参考帧放得比较宽,可以使用的参考帧比较多的时候,压缩比能得到提升,但是解码的效率就会降低,并且在seek寻址的时候也不方便,需要往后读很多帧,或者往前保留很多帧,特别是流式播放的时候可能需要提前下载很多内容。上面SPS分析得到的最大参考帧数目是3(max_num_ref_frames).
接下来怎么对图像帧进行解码还原成rgb图像呢?
3. 视频帧解码
I帧的解码不需要参考帧,解码过程比较类似于JPG,P帧和B帧需要依赖前后帧才能还原完整内容,所以帧的解码顺序通常不是按照播放顺序来的。我们不妨研究一下上面的示例视频的所有帧的类型,可以借助一个在线网站Online Video GOP Analyzer,分析结果如下图所示:
x轴表示从0到23共24帧,y轴表示每一帧的大小,绿色的是关键帧I帧,红色的是前向预测帧P帧,蓝色的表示双向预测帧B帧,可以清楚地看到,在体积上I帧 > P帧 > B帧。这24个帧的排列顺序:
I B B B P B B B P ... B P I
首尾两帧都是I帧,刚好形成一个GOP图像序列(group of pictures),在一个GOP序列里面,I帧是起始帧,接下来是B帧和P帧(可能会没有B帧)。
上图的帧顺序是按照每个帧播放时间戳PTS(presentation timestamp)依次递增,其中第12帧(中间红色柱子)推导的播放时间点PTS是0.54s。
但是存储顺序和解码顺序并不是按照播放的顺序来的,可对比第2步里的帧的大小图:
其中,sample_sizes是存储的顺序,柱形图的顺序是按照PST,两者对比可以看到每一帧的解码时间戳DTS(decode timestamp)是按照以下顺序:
I P B B B P B B B ...
在一个GOP序列里面,I帧是起始帧,最先解析,然后就是P帧,最后才是B帧,可以猜测因为P帧依赖于I帧,所以要先P帧要先于B帧,而B帧可能要依赖于I帧和P帧,所以最后才能解析。那怎么才能知道具体的依赖关系,也就是每一帧的参考帧列表呢?
首先每一帧的播放顺序POC(Picture Order Count)可以从每一帧的头部信息计算得到,借助一些如JW Reference软件,能够查到从存储顺序的第1帧到第5帧POC依次为:
0 8 2 4 6 ...
这里是按照2递增的,换算成1的话就是:
0 4 1 2 3 ...
与上面的分析一致。
接着怎么知道帧间预测帧B帧和P帧的参考帧是谁呢?在回答这个问题之前需要知道参考帧参考的是什么,在jpg/h264里面把图片划分为一个个的宏块(macroblock),一个宏块是16 * 16px,以宏块为单位进行存储,记录的颜色信息是以YCbCr格式,Y是指亮度也就是灰度,Cb是指蓝色分量,Cr是红色的分量。如下图所示:
帧间预测帧的宏块只是记录了差值,所以需要找到参考帧列表的相似宏块。由相似宏块和差值还原完整内容。
而参考帧列表是在解码过程中动态维护的,放到一个DPB(decoded picture buffer)的数据结构里面,里面有两个list,list0放的是前向的,list1放的是后向的,依据最大参考帧数目DPB的空间有限,满了之后会有一定的策略清空或者重置。
在参考帧里面找到匹配的宏块就叫运动估计,借助匹配块恢复完整宏块就是运动补偿。如下图所示:
上图第3帧B帧的一个块找到的匹配块有3个,分别是相邻的I、P、B,这3帧就是它的参考帧。箭头方向就是表示运动矢量,通过上图示意,可以知道物体是从上往下运动的(注意上面的顺序是存储顺序IPBB,而播放顺序是IBBP)。
运动估计和运动补偿的算法有多种,h264有推荐的使用算法。
至此我们知道了解码的基本原理,具体怎么把那一帧的图像解码为rgb图片,我在《wasm + ffmpeg实现前端截取视频帧功能》把ffmpeg编译成wasm,然后在前端页实现了这个功能。主要利用ffmpeg的解码,Chrome也是用的ffmpeg做为它的解码引擎。关键调用函数为avcodec_decode_video2(这个已被deprecated,下文会继续提及)。
借助ffmpeg,我们能够把所有的帧解析出来变成rgb图片,这些图片怎么形成一个视频呢?
4. 视频播放
最直观的做法就是根据帧率,如上面的示例视频帧率为25fps,1s有25帧,每一帧播放间隔时长为1s / 25 = 0.04s,即每隔40ms就播放一帧,这样就形成一个视频了。利用ffmpeg的av_frame_get_best_effort_timestamp函数可以得到每一帧的PST播放时间,理论上以开始播放的时间为起点,在相应的时间差播放对应PST的帧就可以了。实现上可以让播放视频的线程sleep相邻两个帧的pst时间差,时间到了线程唤醒后再display显示新的帧。
实际上为了更好地保证音视频同步,需要以当前音频播放的时间做一个修正。例如,如果解码视频的线程卡了跟不上了,和音频的时间audioClock相差太多,超过一个阈值如0.1s那么这一帧就丢掉了,不要展示了,相反如果是解码视频线程快了,那么delay一下,让播放视频的线程休眠更长的时间,保持当前帧不动。更科学的方法是让音频和视频同时以当前播放的时间做修正,即记录一下开始播放的系统时间,用当前系统时间减掉开始时间就得到播放时间。Chrome就是这么做的,当我们看Chrome源码的时候会发现这个过程比上面描述得要复杂。
5. Chrome视频播放过程
我们从多路解复用开始说起,Chrome的多路解复用是在src/media/filters/ffmpeg_demuxer.cc里面进行的,先借助buffer数据初始化一个format_context,记录视频格式信息,然后调avformat_find_stream_info得到所有的streams,一个stream包含一个轨道,循环streams,根据codec_id区分audio、video、text三种轨道,记录每种轨道的数量,设置播放时长duration,用fist_pts初始化播放开始时间start_time。并实例化一个DemuxerStream对象,这个对象会记录视频宽高、是否有旋转角度等,初始化audio_config和video_config,给解码的时候使用。这里面的每一步几乎都是通过PostTask进行的,即把函数当作一个任务抛给media线程处理,同时传递一个处理完成的回调函数。如果其中有一步挂了就不会进行下一步,例如遇到不支持的容器格式,在第一步初始化就会失败,就不会调回调函数往下走了。
解码是使用ffmpeg的avcodec_send_packet和avcodec_receive_frame进行的音视频解码,上文提到的avcodec_decode_video2已经被弃用,ffmpeg3起引入了新的解码函数。
解码和解复用都是在media线程处理的,如下图所示:
音频解码完成会放到audio_buffer_renderer_algorithm的AudioBufferQueue里面,等待AudioOutputDevice线程读取。为什么起名叫algorithm,因为它还有一个作用就是实现WSOLA语音时长调整算法,即所谓的变速不变调,因为在JS里面我们是可以设置audio/video的playback调整播放速度。
视频解码完成会放到video_buffer_renderer_algorithm.cc的buffer队列里面,这个类的作用也是为了保证流畅的播放体验,包括上面讨论的时钟同步的关系。
准备渲染的时候会先给video_frame_compositor.cc,这个在media里的合成器充当media和Chrome Compositor(最终合成)的一个中介,由它把处理好的frame给最终合成并渲染,之前的文章已经提过Chrome是使用skia做为渲染库,主要通过它提供的Cavans类操作绘图。
Chrome使用的ffmpeg是有所删减的,支持的格式有限,不然的话光是ffmpeg就要10多个MB了。
以上就是整体的过程,具体的细节如怎么做音视频同步等,本篇没有深入去研究。
7. 小结
本篇介绍了很多了视频解码的概念,包括mp4容器的格式特点,怎么进行多路解复用取出音视频数据,什么是I帧、B帧和P帧。介绍了在解码过程中播放时间PST和解码顺序DST往往是不一致的(如果有B帧),B帧和P帧通过运动估计、运动补偿进行解码还原。最后介绍Chrome是怎么利用ffmpeg进行解码,分析了Chrome播放视频的整体过程。
阅读完本篇内容并不能成为一个多媒体高手,但是可以对多媒体的很多概念有一个基本了解,当你去参加一些多媒体技术会议的时候就不会听得雾里云里的。本篇把很多多媒体基础串了起来,这也是我研究了很久才得到的一些认知,我的感受是多媒体领域的水很深,需要有耐心扎进去才能有所成,但多媒体又是提高生活质量的一个很重要的媒介。
从Chrome源码看audio/video流媒体实现二(转)的更多相关文章
- 从Chrome源码看audio/video流媒体实现一(转)
现在绝大多数的网站已经从flash播放器转向了浏览器原生的audio/video播放器,浏览器是如何加载和解析多媒体资源的,这对于web开发者来说是一个黑盒,所以很有必要看一下浏览器是怎么实现的,Ch ...
- 从Chrome源码看浏览器的事件机制
.aligncenter { clear: both; display: block; margin-left: auto; margin-right: auto } .crayon-line spa ...
- 从Chrome源码看JS Array的实现
.aligncenter { clear: both; display: block; margin-left: auto; margin-right: auto } .crayon-line spa ...
- 从Chrome源码看浏览器如何构建DOM树
.aligncenter { clear: both; display: block; margin-left: auto; margin-right: auto } p { font-size: 1 ...
- 从微信小程序开发者工具源码看实现原理(一)- - 小程序架构设计
使用微信小程序开发已经很长时间了,对小程序开发已经相当熟练了:但是作为一名对技术有追求的前端开发,仅仅熟练掌握小程序的开发感觉还是不够的,我们应该更进一步的去理解其背后实现的原理以及对应的考量,这可能 ...
- 从微信小程序开发者工具源码看实现原理(四)- - 自适应布局
从前面从微信小程序开发者工具源码看实现原理(一)- - 小程序架构设计可以知道,小程序大部分是通过web技术进行渲染的,也就是最终通过浏览器的dom tree + cssom来生成渲染树:既然最终是通 ...
- 从源码看Azkaban作业流下发过程
上一篇零散地罗列了看源码时记录的一些类的信息,这篇完整介绍一个作业流在Azkaban中的执行过程,希望可以帮助刚刚接手Azkaban相关工作的开发.测试. 一.Azkaban简介 Azkaban作为开 ...
- Google Chrome 源码下载地址 (Google Chrome Source Code Download)
1. Google Chrome 源码 SVN 地址:http://src.chromium.org/svn.包含有 Chrome.Gears.Webkit.GCC 等源码以及编译依赖工具.Chrom ...
- 解密随机数生成器(二)——从java源码看线性同余算法
Random Java中的Random类生成的是伪随机数,使用的是48-bit的种子,然后调用一个linear congruential formula线性同余方程(Donald Knuth的编程艺术 ...
随机推荐
- caffe中LetNet-5卷积神经网络模型文件lenet.prototxt理解
caffe在 .\examples\mnist文件夹下有一个 lenet.prototxt文件,这个文件定义了一个广义的LetNet-5模型,对这个模型文件逐段分解一下. name: "Le ...
- CSS3个人盲点总结【总结中..........】
~:表示同辈元素之后指定类型的元素,如;elm1 ~ elm2表示,elm1之后的所有elm2元素,且elm1与elm2都是在同一个父级元素. +:表示同辈元素的兄弟元素. \A:一个空白换行符 &l ...
- yum 命令讲解
(一)yum介绍 Yum(全称为 Yellow dogUpdater, Modified)是一个在Fedora和RedHat以及CentOS中的Shell前端软件包管理器.基于RPM包管理,能够从指定 ...
- BZOJ 3569 询问删除指定的k条边后图是否连通 线性基
思路: 这题思路好鬼畜啊-- 绝对是神思路 //By SiriusRen #include <cstdio> #include <algorithm> using namesp ...
- Kettle学习系列之Kettle能做什么?(三)
不多说,直接上干货! PDI(Kettle) 都能做什么? 可以说凡是有数据整合.转换.迁移的场景都可以使用PDI,他代替了完成数据转换任务的手工编码,降低了开发难度. 同时,我们可以在自己实际业务里 ...
- javax.validation参数校验
在实体字段加注解: /** * 机构名称 */ @ApiParam(name = "orgName", value = "机构名称") @Size(max = ...
- 新疆大学(新大)OJ xju 1009: 一带一路 prim求最短路径+O(n)素数筛选
1009: 一带一路 时间限制: 1 Sec 内存限制: 128 MB 题目描述 一带一路是去去年习大大提出来的建设“新丝绸之路经济带”和“21世纪海上丝绸之路”的战略构想.其中就包括我们新疆乌鲁木 ...
- swift语言点评十六-Initialization && Deinitialization
initial value:必须初始化.不影响观察者 Classes and structures must set all of their stored properties to an appr ...
- 使用js获取url中的get参数并转成json格式
写在前面的 没啥说的 上代码 思路就是先获取到?后面的参数区,然后 利用字符串转数组方法获取到各个参数 var json = {}; var url = 'https://www.baidu.com/ ...
- PCA一些性质的定性理解
1.通过本征向量和本征值求主成分 关系:本征值是本征向量的缩放倍数,本征值大的对应的本征向量上的样本的数目就越多:相反本征值越小的,就本征向量上的样本数量就会少.因此可以求出PCA的主成分 主成分分析 ...