图片来源: https://ultimatewebsitedesign...本文做者:hsyhtml
对于视频的在线播放,按视频内容的实时性能够分为点播(VOD)和直播(Live Streaming)。现现在在 Web 环境下须要进行视频播放时,一般可使用 video 标签,经过它将视频播放的各个环节都托管给浏览器。前端
视频的在线播放,站在视频消费者这一侧来看,主要的技术环节在于视频的解码、显示效率,以及视频数据的传输效率。Web 标准中经过 video 标签,将这两个环节进行解耦,开发者没必要关心视频数据的解码、显示环节,而在数据加载环节进行一些扩展。git
在点播的场景下,视频的生产者已经预先准备好了视频的数据内容,站在视频消费者这一侧的开发者只须要指定 video 标签 src 属性为对应的资源地址便可。可是在一些复杂的需求中,好比须要细致地控制视频数据的预加载时机和数据量,那么则须要对视频的一些规格参数以及相关的技术点作进一步的了解。github
本文将对视频点播场景下,对数据加载环节进行拓展所需了解的技术内容作简单的介绍。web
视频文件有一些常见的规格参数,它们做为视频相关技术内容的起点,简单地了解它们有助于更快的理解在此之上的内容。算法
起初视频的来源都是天然界中的事物对光的反射,被图像采集设备按必定的频率采集后进行保存,在须要观看的时候,将保存的内容按必定的频率进行播放。这个大体的流程一直延续至今,只不过因为数字技术的成熟,视频的内容也能够直接经过软件编辑生成。浏览器
人类能够识别的光谱和光线变化的频率都具备必定的范围,低于或者高于这个范围的变化都没法被绝大部分人所感知。所以在保存以及播放视频的时候,须要结合人的感官体验以及软硬件资源的限制对视频的各个参数作相应的调整。缓存
视频的播放原理相似幻灯片的快速切换。安全
每一次切换显示的画面,称之为一帧(Frame)。而帧率则表示每秒切换的帧数,所以它的单位是 FPS(Frames Per Second)。帧率并不和视频的清晰度直接相关,但它却也是影响感官体验的重要因素。bash
人类对画面的切换频率的感知度有一个范围,通常 60FPS 左右是一个比较合适的范围。但这并非绝对的,在须要记录一个变化须臾之间的镜头时,准备足够的帧数才能捕捉到细微的变化;当拍摄一个缓慢的镜头推动的效果时,帧率并不须要过高,分辨率会起到更高的做用。
帧率的选取除了须要结合播放内容,还须要结合显示设备的刷新频率,不然选取了太高的帧率而显示设备不支持的话,多余的帧也只会被丢弃。
在视频播放时,显示到屏幕上的每一帧包含的像素数量是一致的。像素是显示设备上发光原件的最小单位,最终呈现的画面是由若干个像素组合起来所展现的。
分辨率表示视频每一帧包含的像素,以 水平方向的像素数量 × 垂直方向的像素数量
来表示,好比 720p = 1280 × 720
就是一个比较常见分辨率。
这里的 p
表示的是逐行扫描(Progressive Scanning),与之对应的是 i
表示的是隔行扫描(Interlaced Scanning),见下图
最左边一列是逐行扫描,中间一列是隔行扫描。可见隔行扫描会丢失一些画面信息,相反的会更快地收集画面,画面的像素信息即文件大小相较逐行扫描也会偏小。
当视频的分辨率低于显示设备的分辨率时,设备上的像素点多于视频显示所需的像素点,这时就会用上各式的补间算法(Interpolation Algorithm)来为显示设备上那些未被利用的像素点生成色值信息,以彻底点亮显示设备的全部像素点,不然将会致使屏幕上出现黑点。
关于补间算法,这里有一小段视频能够做为参考 Resizing Images。
由于那些像素上的信息是算法生成的,因此当视频的分辨率明显小于显示设备的分辨率时,这样的像素点就会变得过多,从而致使感官上的清晰度降低。而若是视频的清晰度超过设备的分辨率上限,则多出的信息会被丢去,所以也不会呈现更好的效果。
因此视频分辨率的选择,须要同时结合显示设备的分辨率。
比特率的单位是 bit/s
,表示视频每秒长度中包含的比特数。
目前的视频的画面采集设备能够收集很是大量的像素信息,视频的做者为了方便对视频进行分发,须要将原始视频进行压缩转码。
比特率的大小,受到单位时间内的视频文件的体积所影响,而影响视频文件的体积的因素为:原始视频内容、转码选取的分辨率、帧率以及转码所采用的编码方案(Codec)。
所以比特率并非一个和视频清晰度直接关联的参数。
若是采用的是离线播放的话,那么比特率将不是一个重要的参数。而若是采用在线点播的方式观看视频时,视频的比特率则成了必需要考量的重要指标。
比特率表示为了显示一秒的画面所需传输的比特数。它能够方便的和带宽作比较。在线点播时,须要保证在有限的带宽条件下,每秒传输尽量多的比特,这些比特须要保证画面的传递不会出现问题。
由于计算机只能按照既定的程序逻辑来执行,因此视频数据须要按预先制定好的格式进行整理后才能保存设备上。在格式的制定中,主要保存两类信息:
元数据包含对主体数据的一些描述信息,好比会记录视频的大小、分辨率、音频和视频的编码方案等。
视频元数据和主体数据如何组合到一块儿进行保存,须要容器格式来指定,常见的容器格式包括 MP四、AVI 等。对于视频来讲通常会选择 MP4 做为容器格式,由于它被各个系统和设备普遍支持。
针对视频主体数据,则须要另外的参数来指定,一般称之为编码方案。不一样的编码方案会视频文件的体积和最终的播放效果之间作取舍。
上文已经简单介绍了一些视频相关的参数。在点播场景下,视频的生产者已经完成了视频的生产工做,视频内容采用某一种编码方案进行编码,并和其余一些信息一块儿,以某个容器格式进行保存。站在视频消费者角度,为了尽快地可以观看到视频的内容,确定不能采用加载完整个视频数据后才进行播放的形式,所以从技术上必须支持对视频数据进行分段地加载以及解码播放。
文章的开头已经提到,Web 标准中已经将解码播放和数据加载的环节进行了解耦,所以做为开发者只须要实现对视频数据的分段加载。
在具体了解分段加载的技术细节以前,能够经过一个简单的例子来感觉整个分段加载的流程。
这个例子将以 HLS 协议进行展开。HLS 协议只是众多视频数据分段加载协议中的一种,关于它的一些细节将在下一节进行介绍。先经过运行一个例子来对数据分段加载有一个具体的感觉。
经过下面这条命令来启动这个例子,在此以前请确保电脑上已经安装了 ffmpeg 以及所处网络的通畅:
wget -qO- https://gist.githubusercontent.com/hsiaosiyuan0/412a4ca26d00ff7c45318227e7599c3d/raw/de5e3c467800d1f2315e74ba71f0eb6c6760a7a0/hls.sh | bash
这段命令会加载并执行一段网络上的脚本。这段脚本将会执行下面的工做:
若是没法运行这段脚本的话,也能够经过这段 视频 或者 gif 来得到演示的内容。
在演示中能够看到浏览器会对分段的数据进行加载,而且当网络环境变化后,会加载不一样清晰度的分段内容。
HLS 是苹果公司为知足在线点播需求而推出的流媒体播放协议。由于这个协议基于 HTTP 协议,因此能够充分利用现存的针对 HTTP 协议的技术内容和优化措施。
经过这个图能够发现,视频录制完成后须要先上传到 Web 服务器进行分段和索引,这期间的消耗会让用户接受到的信息产生必定的滞后。因此虽然之为 Live Streaming,可是它和实时(Realtime)之间仍是有一些差距,文章开头提到的 Live Streaming 指的是实时的数据流。
苹果公司不光制定了 HLS 协议的细节,对于协议的实施也给出了一套解决方案,能够从 About Apple's HTTP Live Streaming Tools 中得到对于这些工具的介绍。因为协议自己是开源的,依然可使用相似上一节例子中的 ffmpeg 来完成一些相同的工做。
HLS 协议的关键,同时也是数据分段加载的关键在于两部分:
对于分段的策略,一般状况下会选择将视频按照相同的播放时间分割成一个个小段,好比上面的例子中将视频按 10s 来分割每个段。
而在 HLS 协议中,经过 m3u8 文件来对分段的内容进行描述和索引。就如同上面的例子同样,视频的制做者除了须要将视频进行分段之外,还须要生成对这些分段进行描述的 m3u8 文件,而视频的消费者,只要获得 m3u8 文件,就能够灵活的选择分段的加载形式了。
接下来将经过进一步了解 m3u8 的细节,以了解分段的内容。
m3u 是一种保存音视频文件播放所需信息的文件格式,m3u8 则是该格式在使用 UTF8 进行编码时的缩写。
m3u8 文件在 HLS 协议中起到 Playlist 的做用,相似音乐播放软件中的“歌单”,不过歌单面向的对象是用户,而 m3u8 则是供播放器选用。
由于是采用的 UTF8 进行的编码,因此可使用常见的文本编辑器打开它们。下图是上文例子中产生的 m3u8 文件的内容:
完整的 m3u8 格式的说明见 rfc8216 - HTTP Live Streaming。截图涉及到的相关协议内容为:
#
开头的行,空行将和注释一块儿被解释程序忽略#
开头的行能够表示注释或者标签(Tag),紧接着 #
的内容为 EXT
(区分大小写)则表示该行是标签,不然都视为注释,会被解释程序所忽略#EXTM3U
表示的是标签。该标签用以表示当前文件是对 m3u 格式的扩展,协议规定该标签必须出如今文件开头的第一行#EXT
开头的行都是标签,它们被 URI 行分隔,用于修饰它们下方的 URI 资源AttributeName=AttributeValue
。所以 #EXT-X-STREAM-INF
标签的值即为属性列表<img src="https://p1.music.126.net/3uNyvqTL4TZksYVL52XqWg==/109951164871804276.png" width="400" />
#EXT-X-STREAM-INF
标签的做用是描述可变流(Variant Stream)的信息。其中的属性含义大都在第一节已经介绍过了。BANDWIDTH
表示该流中各个分段的比特率的峰值,另外一个常见的 AVERAGE-BANDWIDTH
虽然没有出现,可是它表示综合了各个分段的比特率的平均值。客户端会根据这两个属性的值、结合自身当前的带宽来选择合适的流下的下一个分段数据因此在上文的例子中,经过向 video 指定一个包含三个不一样分辨率的 Master Playlist,浏览器会根据自身当下的带宽速率,自动的在三个数据流之间进行切换。当前若是没有须要自动切换清晰度的场景,也能够向 video 标签直接指定一个 Media Playlist 来完成对某个特定的分辨率的流的播放。
HLS 协议由于是苹果公司推出的,因此能够在苹果公司的设备上获得普遍的支持,在运行于这些设备上的 Safari 浏览器中,video 标签能够直接做为 HLS 协议的客户端来运行。
在那些不支持 HLS 的浏览器环境中,一般可使用另外一种相似的协议,该协议直接由 MPEG 组织开发和维护,称之为 MPEG-DASH,MPEG 是一个国际上专门制定视频播放相关协议的组织,所以 DASH 协议相比 HLS 更具普遍性。
做为另外一个基于 HTTP 协议的流媒体播放协议,DASH 具备和 HLS 类似的技术细节。例如它经过 Media Presentation Description(mpd)文件提供相似 m3u8 文件的功能。下图是 mpd 文件内容的截图:
DASH 协议并不被主流浏览器厂商直接支持,一般须要借助额外的客户端插件配合,好比接下来将介绍的 MSE 来完成数据的分段加载。在考虑移动端播放的状况下,一般来讲选用 HLS 协议可以适配更多的设备,这是由于支持 DASH 的播放的设备一般均可以经过 HLS 插件来对 HLS 协议提供支持,反之则否则。
MSE,Media Source Extension 是 Web 标准中制定的针对音视频数据加载的接口,Web 应用程序能够经过这个接口实现本身的音视频数据的加载方案。由于 WebRTC 不是针对点播的协议,因此 MSE 成为了点播场景下惟一的音视频数据加载接口。
上文提到的 HLS 和 DASH 协议,在那些不原生支持它们可是支持 MSE 的浏览器中,均可以经过基于 MSE 实现对应的插件来完成相应的支持。
下图是 MSE 目前的兼容性概览,另见 Caniuse - MediaSource:
图中指的是 iPadOS,在 iOS 上目前仍是不支持的。在安卓设备上整体来讲支持度比较高,因此上文提到采用 HLS 协议可以适配更多的设备,在苹果设备上原生支持,在安卓设备上经过基于 MSE 的 HLS 插件来对其进行支持。
MSE 的目的是将音视频的播放和数据加载进行解耦。Web 开发者没必要介入或者干预现有的音视频解码和播放控制的行为,只须要经过 MediaSource 向音视频元素传递播放所需的数据便可,下图能够演示它们之间的关系:
音视频元素只负责对音视频数据进行解码播放,应用自定义音视频数据的加载策略并对数据进行加载,二者之间经过 MediaSource 进行交互。
使用 MSE 是为了自定义音视频数据加载的策略,而数据加载策略中重要的一个功能点在于,为了节约带宽,须要能够终止那些正在进行的已知无用的请求的继续加载。而 Fetch API 里面终止数据加载的功能须要结合 AbortController 来使用。
下面是 AbortController 目前的兼容性概览,另见 Caniuse - AbortController:
除非必要,不然彻底不用支持 IE,所以在不考虑 IE 的状况下,它和 MSE 具备类似程度的兼容性,能够将二者结合在一块儿使用。
在 AbortController 不可用的状况下 XMLHttpRequest.abort() 是另个具备较高兼容性的方案。不过 AbortController 对资源能够起到“分组”管理的特性,是 XMLHttpRequest 默认所不具有的。
针对视频加载速率的优化,一般来讲有两个方向,分别是「动态切换清晰度」和「视频内容预加载」,接下来将对这两个技术点作简单的介绍。
动态切换清晰度指的是在视频播放的过程当中,根据设备的当前带宽在不一样清晰度的流之间进行切换的功能。
在使用 HLS 协议时,服务端预先将视频进行压缩分段,并提供 m3u8 文件,客户端拿到 m3u8 文件后,根据当前的带宽动态的切换分段的加载。
好比针对同一个视频,准备了三个不一样清晰度的流,并分别将视频流分隔为3段:
720P --seg11-- --seg12-- --seg13-- 480P --seg21-- --seg22-- --seg23-- 360P --seg31-- --seg32-- --seg33--
最简单的算法下,客户端能够先选择中间分辨率的流 480P 来加载第一段的视频(seg21),根据加载的时长以及 seg21 自己的大小能够获得当前的下载速率,若是低于 480P 正常播放所需的带宽,好比 1500kps,则下一段视频将从知足当前带宽的流中选取,在上面的例子中就是加载 360P 的 seg32。
固然由于带宽是动态变化的,且是一个估算的值,例子中仅经过一个分段就进行切换很大程度上是不合理的,这里只是做为演示。更为实用的算法能够参考 DASH 协议中的 Adaptive Bit Rate Logic。
视频内容预加载顾名思义就是在视频未开始播放前,就预先加载一部分视频内容,这样用户点击播放时,即刻就能得到反馈。
视频内容预加载的算法基于两点进行展开:
对于第一点来讲,须要结合业务的需求来作具体的调整。
对于第二点来讲,最简单的算法能够对正在播放的视频设置一个缓冲区的安全阙值,好比 5s,若是当前正在播放的视频其缓存区中有 5s 的内容还没有被播放,则能够尝试进行下一条视频的预加载。
因此总得来讲,预加载依赖于技术上的可行性,具体的策略则须要按照实际的需求来制定。在不支持 MSE 的场景下,能够经过 video 标签的 preload 属性来开启预加载,不过预加载的时机以及预加载的数据量则都是处于托管的状态。
上文对 HLS 和 DASH 协议以及 MSE 作了简单的介绍,也对视频加载速率的优化方式作了简单的介绍。在使用 HLS 和 DASH 的状况下,均可以直接选用现有的开源实现来得到客户端的播放支持,好比 hls.js 和 dash.js。
使用 HLS 和 DASH 协议都有一个前提条件,就是视频文件须要按照协议的规定以及业务实际所需进行预先分段,换句话说就是须要服务端的配合。
下面将介绍一种在缺少 HLS 和 DASH 的支持下,利用 HTTP Ranges 和 MP4 容器格式加上 MSE 来完成对视频内容分段加载的可行性。这个形式几乎不须要服务端的额外配合,也能够经过对这个可行性的介绍,进一步了解 MSE 协议以及 MP4 容器格式。
音视频文件,在点播场景下一般都会经过 CDN 网络加速内容的分发。CDN 网络中的 Web 服务器一般都会支持 HTTP Range Request 功能。经过这个功能,可以让客户端在预先知道文件内容的状况下,按需取得文件的某一部分。
因此分段加载一段视频数据的能力是 CDN 网络默认就提供的,剩下的只是如何可以让客户端预知一段视频的内容。在 HLS 或者 DASH 协议中,都是经过索引文件 m3u8 或 mpd 来实现的,客户端经过加载这个索引文件就预知了整个视频的分段信息和它们的相对偏移量,所以顺利的完成后续的加载工做。
m3u8 文件的功能,基本能够直接利用 MP4 容器格式来提供,这是由于 MP4 容器格式中定义了一个名为 moov 盒子,它记录了相似分段的信息,关于 MP4 容器格式的细节下一节将会介绍。
moov 根据制做视频的软件不一样,有时会处于整个视频文件的末尾,所以为了让客户端播放软件可以尽快地加载到 moov 盒子,CDN 服务提供商都会建议视频提供者先将视频的 moov 信息进行前移后再进行分发。好比 NOS 官网上就有相似的建议,见 Flash Player点播。
可见在这个方案中,对 MP4 文件格式的了解成了重要的一环,接下来将会简单介绍 MP4 文件格式。
MP4 是一个容器格式,之因此强调它是一个容器格式,是由于它的做用是将全部用于描述视频的信息进行有组织的统一存放。另外一个经常提到的编码格式只是 MP4 容器包含的众多视频信息中的一个。
MP4 格式大体这几个特色:
能够经过 MP4Box.js 提供的在线工具,放入这段演示视频,来查看 MP4 文件的结构信息:
除了在线工具之外,也能够经过 AtomicParsley 这个命令行程序快速地获得 MP4 文件的内部盒子结构。
使用下面的命令便可:
AtomicParsley 1.mp4 -T
上图中,左边 Box Tree View 展现的是文件内包含的盒子,以及这些模块的层级关系,右边则是当前选中的盒子上的属性。
上图中顶层的盒子包括 ftyp
,free
,mdat
,moov
。moov
包含另一些盒子,以子节点的形式进行展现。mdat
盒子中保存的是音视频数据的主体,moov
盒子中包含了用于描述这些数据的元信息。
MP4 中众多的盒子类型和它们的从属关系以下图,另见 ISO/IEC 14496-12:
上图表格中的缩进展现了盒子之间的包含关系。moov 盒子中比较重要的盒子就是 trak
盒子。MP4 格式中将画面数据和音频数据分开保存,通常来讲一段视频将包含一段画面数据和一段音频数据,它们的信息将分别经过两个独立的 trak 盒子来保存。
trak 盒子中主要记录音视频的编码信息,以及对音视频数据块的索引,数据块的主体存放在 mdat 盒子中。
所以只要客户端获得一段视频的 moov 信息,那么就能够按需地加载 mdat 中的音视频数据。
在了解盒子之间的关系以后,能够经过下图来简单的了解盒子的数据结构:
盒子的数据总体能够分为 head 和 body 两部分,这样分类虽然不是协议内规定的,但倒是一个方便理解的方式。须要注意的是头部中的 size
数据,它表示的是整个盒子所占的数据大小。这样的设计使得客户端在快速的在盒子之间进行偏移,而且只选择本身感受兴趣的部分进行延迟解析。
若是把文件整个当作是一个大的盒子,那么 ftyp
,moov
这些处于顶层的盒子则是它的子节点。盒子在其父级节点中的顺序不被要求是固定的。好比 ftyp
在标准中只被要求尽早的出现,而并非必须放到起始的位置。
正是由于盒子在容器中的顺序是不固定的,客户端软件只能经过不断地请求顶层的盒子来获取到 moov 盒子,能够经过运行一小段程序来具体演示这个过程。这里是演示代码的源码,能够经过下面的命令运行该演示程序:
deno --allow-net https://gist.githubusercontent.com/hsiaosiyuan0/a7b215b53b48b9e66d2e0bad9eb8d1dd/raw/6bf88be0c81d55f620b1d94d51f5a56b55691007/locate-moov.ts
若是不方便运行这段脚本,也能够直接查看这里的结果演示。
这段脚本中正是利用了 box 盒子数据结构的特色,从文件的起始位置开始请求第一个8个字节的内容,这也是上文提到的盒子的 head 部分,它们将包含盒子的大小以及盒子的类型,各占4个字节。随后的请求将不断地累加偏移地址以请求接下来紧挨着的盒子,可是每次依然只请求盒子的头部信息,通过几回偏移后,将会获得 moov 盒子的信息。
经过这个例子也能够看出将 moov 信息前置的意义,将有效得减小为了定位 moov 盒子的偏移请求次数。
到目前为止已经介绍了 MSE,HTTP Ranges 和 MP4 容器格式。经过对 MP4 容器格式的了解,发现它内部的数据其实也是分块存储的,而且分块的信息保存在 moov 盒子中。
那么彷佛按照 moov 盒子的分块信息加载分块的数据而后借助 Media Source 就能进行视频的播放了,好比下面的这段代码摘自 Google Developer - Media Source Extensions,其中最重要的一段代码就是 appendBuffer
的调用:
应用代码加载了分段的数据,而后调用 appendBuffer
将数据经过 Media Source 传递给音视频播放器。
实际上事情要稍微复杂一些,这是由于 MP4 容器格式只是众多容器格式中的一种,而 MSE 做为一个通用的数据接口,它将对接各类容器格式的数据,所以它对外制定了一个通用的数据分段格式。这个分段格式被称为 MSE-FORMAT-ISOBMFF,它是基于 ISO base media file format, ISOBMFF 的修改版。
一般所说的 MP4,即 MPEG-4 Part 14,它和 ISOBMFF 的关系为:
能够看到 MP4 是 ISOBMFF 格式的拓展,将包含更多样化的信息。而 MSE-FORMAT-ISOBMFF 是在 ISOBMFF 的基础上引入了分段的概念。
MSE-FORMAT-ISOBMFF 下数据的分段加载形式以下:
上图展现了 MSE-FORMAT-ISOBMFF 中的两个主要分段类型:initialization segment 和 media segment。这些分段又由一些盒子组成。
所以为了让 MP4 格式的文件能够被 MSE 播放,须要在格式上作相似下面从右往左的转换,总体的流程为:
最右边是本来的视频内容,中间虚线部分为开发者自行编写的内容,须要完成对原始视频时间的下载和再组装的任务。
转换的过程并不涉及到对视频数据的转码,只是容器格式的相互转换,可是即便这样整个过程依然十分繁琐,所以借助现有的实现能够方便地完成这个工做。
mp4box.js 就是这样在这方面功能比较丰富的实现。不过虽然功能比较多,可是文档还不是很是丰富,能够参考仓库中的测试案例。
为了方便快速获得一个本地可运行的演示,这里准备了一个演示项目。在演示项目中,经过自定义的 Downloader 来对数据进行加载,加载后的数据将交给 mp4box 进行从新组装,组装完成后再加入到 Media Source 中传递给播放器。若是是要完成预加载的功能,只须要对 Downloader 按业务所需稍加修改就能够了。
本文从视频的常见参数开始,到一种不须要服务端额外配合的点播方式结束,介绍了 Web 环境下的视频点播功能所涉及的一些技术点。由于这方面涉及的内容不少,加上水平和时间的限制,因此暂时只能呈现这些内容。但愿它们能起到一些帮助做用,同时也期待你们的宝贵意见和建议。
本文发布自 网易云音乐前端团队,文章未经受权禁止任何形式的转载。咱们一直在招人,若是你刚好准备换工做,又刚好喜欢云音乐,那就 加入咱们!