本文主要介绍了JavaScript中的视频流,主要受众是web开发工程师。大部分例子用到了HTML和现代JavaScript。
对原生视频API的需求
从年的初期至晚期,网页上的视频播放主要依赖于flash插件。
视频位置的警告信息,告知用户应当安装flash插件
这是因为当时在浏览器中没有其他工具可以播放视频。对于用户来说,你要么选择安装flash或Silverlight这类第三方插件,要么就没办法播放视频。
为了解决这个问题,WHATWG开始起草HTML的新版本,其中就包含了内置支持(不借助任何插件)音频和视频的播放功能。这就是现在众所周知的HTML5。
HTML5引入了许多新功能,其中包含了video标签。
这个新标签允许我们直接在HTML中嵌入视频,就像用img标签嵌入图片那样。它很方便,但是相比于以前的flash方式存在一些不足:
我们可能需要实时在多个视频清晰度之间切换以避免缓冲问题直播是另外一个很难解决的问题在视频播放时如何根据用户的选择更新音频的语言?
好消息是这些问题在大多数浏览器中是可以解决的,多亏了HTML5规范。本文将介绍视频是如何在网页上播放的。
video标签
上面已经说了,在HTML5中嵌入一个视频非常简单。你只需要在网页中增加一个video标签,外加几个属性即可。
例如,你可以这样写:
在一个支持HTML5和解码器的浏览器中打开此HTML文件时,会播放some_video.mp4文件。例如:
video标签还提供了各种API,用于播放,暂停,调整播放进度或者播放速率等。这些API可以在JavaScript中直接使用:
但是,现在我们在网上看到的视频播放器远远比这些要复杂。例如,切换视频清晰度,支持直播等。
这些网站实际上还是在用video标签。不过它们不是简单地把视频文件设置为src属性,而是用到了很多功能强大的API-媒体源扩展(MediaSourceExtensions)。
媒体源扩展(MediaSourceExtensions)
媒体源扩展(MediaSourceExtensions,常缩写为MSE)是W3C的一个规范,现在大多数新式浏览器均已支持。这个规范的目的就是在HTML和JavaScript中可以直接支持这些复杂的使用场景。
这些“扩展”把一个名为MediaSource的对象暴露给JavaScript。从字面意思即可得知,它就是视频源,简单的说,它就是代表视频数据的对象。
上面已经提到,我们仍然用的是HTML5的video标签。同时,我们仍然在用它的src属性。只不过这时的src属性值不再是一个指向视频文件的链接,而是一个指向MediaSource对象的链接。
你可能不理解上段的最后一句话。这里我们讨论的不是一个URL,我们讨论的是JavaScript这门语言的一个抽象概念。怎么可能把它作为一个URL设置到video标签上呢?
为了支持这种使用场景,W3C定义了一个名为URL.createObjectURL的静态方法。这个API可以创建一个URL,但是这个URL并非指向线上的资源文件,而是直接指向在客户端创建的一个JavaScript对象。
如下展示了MediaSource是如何添加到video标签上的:
仅此而已,现在你知道那些视频网站是如何在网页上播放视频的了!
开个玩笑而已。现在我们知道了MediaSource,但是我们该怎么用呢?
MSE规范不止于此。它还定义了另外一个概念,SourceBuffer。
SourceBuffers
视频不是直接“推入”到MediaSource中播放的,这时需要用到SourceBuffer。
MediaSource会包含一个或多个SourceBuffer。每个SourceBuffer代表不同类型的内容。
为了简便起见,我们假设只有三种类型:
音频视频音频和视频
实际上,“类型”是由它的MIME类型来定义的,有可能会包含所使用的解码器信息。
SourceBuffer都会关联到一个单独的MediaSource上,在JavaScript中直接用它们把视频数据添加到HTML5的video标签上。
举个例子,一个常见的使用场景是在MediaSource里有两个SourceBuffer:一个用于视频数据,另一个用于音频数据。
把视频和音频分离,这样可以在服务器端分别管理它们。这样做有一些好处,我们稍后讨论。如下是使用方式:
至此,我们可以手动把视频和音频数据动态地添加到video标签里了。
备注:现在是时候说一下音频和视频数据自身了。之前的例子中,你可能注意到了音频和视频数据格式都是mp4。MSE规范并没有规定浏览器必须支持哪些格式。对于视频数据来说,两个最常用的格式是mp4和webm。前者众所周知,后者是Google赞助的项目。现代浏览器中对两者都支持。
媒体片段(MediaSegments)
至此,仍然有一些问题没有得到回答:
我们是否必须等待完整的内容下载之后,才能推入到SourceBuffer里(然后才能播放)?我们如何在多个清晰度和语言之间切换?如何在现场直播还没有结束时播放实时内容?
在之前的章节的例子中,我们用一个文件代表完整的音频数据,一个文件代表完整的视频数据。对于简单的使用场景是足够了,但对于大部分视频网站提供的复杂功能来说远远不够(切换语言和清晰度,现场直播等)。
功能完备的视频播放器,它们播放时使用的视频和音频数据通常是切割后的多个“片段”。这些片段的大小没有要求,不过通常是2到10秒的内容。
这些视频/音频片段构成了完整的视频/音频内容。这些数据“片段”让我们的上个例子可以更加灵活:我们逐步推入多个片段,而不是一次性把全部内容推入。
这是一个简化的例子:
这意味着我们也需要在服务器上托管这些片段。对于上个例子来说,我们的服务器上至少有如下文件:
注意:这些音频或者视频文件可能并不是在服务器端分割的,客户端也可能使用了HTTPRange请求头来获取这些分割后的文件。不过,这些都是实现的细节。在这里我们认为片段文件托管在服务器端就好了。
这样我们就不必等待完整的音频或视频文件下载后才开始播放。我们通常只需要二者的第一个片段即可。
当然了,大部分播放器不会像上面那样手写逻辑逐个加载每个视频和音频片段,但是它们遵循了同样的思想:按照顺序下载片段然后推入sourcebuffer。
要直观地感受上述逻辑,可以在Firefox/Chrome/Edge浏览器的开发者工具切换到Network面板,然后打开你经常访问的视频网站上的一个视频。
你可以看到各种视频和音频的片段正在快速下载:
自适应流(adaptivestreaming)
许多视频播放器都有一个“自适应清晰度”的功能,视频的清晰度会根据用户的网络和处理速度自动选择。
这是网页播放器的一个核心功能,称之为“自适应流”。
之所以能有这个功能要归功于媒体片段的概念。
在服务器端,以不同的清晰度分别生成片段。例如,在服务器上可以这样存储文件:
网页播放器会根据网络和CPU条件的改变自动选择对应的片段下载。
所有这些都是在JavaScript中完成的。对于音频片段,可以这样处理:
如你所见,我们把不同清晰度的片段放在一起也没问题,在JavaScript中一切都是透明的。
多语言切换
在更复杂的网页视频播放器上,如Netflix,AmazonPrimeVideo或MyCanal,还可以根据用户的设置在多语言音频之间切换。
这样你就可以听懂影片的内容了,这个功能对你来说非常简单了。
和自适应流类似,我们也需要在服务器端存储多语言的片段文件:
此时视频播放器不是基于客户端的性能来切换语言了,而是根据用户的首选项。
对于音频片段,可以在客户端这样处理:
在切换语言时你可能需要“清空”之前的SourceBuffer内容,以避免多种语言的音频内容混杂在一起。
可以使用SourceBuffer.prototype.remove方法来实现,它以一个开始时间和一个结束时间为参数,单位是秒:
当然了,把自适应流和多语言结合在一起也是可行的。在服务器端我们可以这样组织:
在客户端需要同时管理语言和网络情况:
如你所见,同样的内容有很多种组织方式。
这样把视频和音频片段分离开来而不是合为一体存储,有另外的好处。如果合为一体存储,我们在服务器端需要针对每种可能性排列组合,会占用更多存储空间:
这样做的话文件更多,很多内容是重复的(相同的视频数据被重复存在了多个文件里)。
很显然这么做在服务器端非常低效。对于客户端来说同样如此,因为切换音频的语言时需要重新下载整个视频文件(会耗费大量网络带宽)。
实时内容
我们还没有说到直播。
直播在互联网上已经是司空见惯,由于视频和音频文件可以切割为片段,直播也极大地简化了。
为了尽可能简单地解释它地工作原理,我们可以假设一个Youtube频道在4秒前刚开始直播。
如果我们的片段的时长是2秒,现在Youtube服务器上已经生成了两个音频片段和两个视频片段:
两个片段代表了从0秒到2秒的内容(1个音频片段+1个视频片段)两个片段代表了从2秒到4秒的内容(还是1个音频片段+1个视频片段)
第5秒时,我们还没有生成接下来的片段,所以现在服务器上还是只有上面的4个片段。
第6秒后,新的片段生成了,服务器上的片段如下:
这种做法在服务器端是非常顺理成章的,实时内容实际上并非连续的,和非实时内容一样是切割为片段的,只不过这些片段会随着时间逐渐生成。
现在问题来了,在JS中我们如何得知某个时间点服务器上有什么片段可供使用呢?
在客户端我们可以用一个定时器来检查服务器上是否有了新的片段。可以按照“segmentX.mp4”的命名规则,每次在上次的基础上把“X”加一(segment0.mp4,然后2秒后是segment1.mp4等)。
不过,在许多情况下这样做非常不精确:片段的时长可能是不同的,服务器在生成片段时可能会有延迟,服务器可能会为了节省存储空间删除掉旧的片段。。。
在客户端,你想在片段生成后尽快获得它们,同时还要避免在它们生成之前过早地请求(这样会遇到HTTP错误)。
这个问题一般可以用一个传输协议(有时称之为StreamingMediaProtocol)解决。
传输协议
在本文深度介绍不同的传输协议不太现实。我们只需要知道它们都有一个核心的概念:Manifest。
Manifest是一个文件,它告诉我们在服务器上有哪些片段。
有了它,你可以得到我们在本文中学习到的大部分信息:
哪些语言的音频可供使用以及它们存在服务器的哪个位置可供使用的各种音频和视频的清晰度当然了,还有直播此刻有哪些片段可用
在网上最常用的传输协议是:
DASHYoutube,Netflix和AmazonPrimeVideo采用了此协议。DASH的manifest称之为MediaPresentationDescription(或MPD),格式是基于XML的。HLS由Apple开发,被DailyMotion,Twitch.tv以及其他网站使用。HLS的manifest成为播放列表,格式是m3u8。SmoothStreaming由微软开发,在微软多个产品线和MyCanal使用。它的manifest称为Manifests,是基于XML的。
现实世界的现状
若你所见,网页中视频其背后的核心概念是使用JavaScript中动态地推入片段。
这种行为很快就变得复杂起来,因为视频播放器必须支持各种功能:
它必须下载并解析manifest文件它必须推测当前的网络状况它需要记录用户的首选项(例如首选语言)它必须知道要下载哪个片段它必须管理一个片段队列,在正确的时间下载正确的片段(同时下载所有片段的做法非常低效:一般需要先下载最早的片段然后才是后续的片段)它还必须支持字幕,通常用JS来管理有些视频播放器还支持缩略图,把鼠标置于进度条上可以看到缩略图许多视频网站还需要DRM管理以及许多其他功能。。。
不管怎样,这些复杂的网页视频播放器其核心都是基于MediaSource和SourceBuffer。
这就是为什么这些任务一般是用类库执行。通常这些类库不会定义图形界面。它们会提供丰富的API,接收manifest和各种首选项为参数,然后在正确的时间点把正确的片段推入sourcebuffer。
这样在设计视频网站和应用时,可以做到模块化和灵活性。
开源的网页视频播放器
现在有许多视频播放器采用了本文中介绍的技术。例如:
rx-player:支持DASH和SmoothStreaming,支持配置和扩展。dash.js:播放DASH内容,支持众多DASH功能。hls.js:知名HLS播放器。被众多大牌厂商采用。shaka-player:DASH和HLS播放器。由Google维护。
HTML5MultimediaDevelopmentCookbook京东好评率%京东配送¥.36购买