14万字玩转前端-Video-播放器-多图预警

2次阅读

共计 23939 个字符,预计需要花费 60 分钟才能阅读完成。

Web 开发者们始终以来想在 Web 中应用音频和视频,但早些时候,传统的 Web 技术不可能在 Web 中嵌入音频和视频,所以一些像 Flash、Silverlight 的专利技术在解决这些内容上变得很受欢迎。这些技术可能失常的工作,然而却有着一系列的问题,包含无奈很好的反对 HTML/CSS 个性、平安问题,以及可行性问题。

侥幸的是,当 HTML5 规范颁布后,其中蕴含许多的新个性,包含 <video><audio> 标签,以及一些 JavaScript APIs 用于对其进行管制。随着通信技术和网络技术的一直倒退,目前音视频曾经成为大家生存中不可或缺的一部分。此外,随同着 5G 技术的缓缓遍及,实时音视频畛域还会有更大的设想空间。

接下来本文将从八个方面动手,全方位带你一起摸索前端 Video 播放器和支流的流媒体技术。浏览完本文后,你将理解以下内容:

  • 为什么一些网页中的 Video 元素,其视频源地址是采纳 Blob URL 的模式;
  • 什么是 HTTP Range 申请及流媒体技术相干概念;
  • 理解 HLS、DASH 的概念、自适应比特率流技术及流媒体加密技术;
  • 理解 FLV 文件构造、flv.js 的性能个性与应用限度及外部的工作原理;
  • 理解 MSE(Media Source Extensions)API 及相干的应用;
  • 理解视频播放器的原理、多媒体封装格局及 MP4 与 Fragmented MP4 封装格局的区别;

在最初的 阿宝哥有话说 环节,阿宝哥将介绍如何实现播放器截图、如何基于截图生成 GIF、如何应用 Canvas 播放视频及如何实现色度键控等性能。

一、传统的播放模式

大多数 Web 开发者对 <video> 都不会生疏,在以下 HTML 片段中,咱们申明了一个 <video> 元素并设置相干的属性,而后通过 <source> 标签设置视频源和视频格式:

<video id="mse" autoplay=true playsinline controls="controls">
   <source src="https://h5player.bytedance.com/video/mp4/xgplayer-demo-720p.mp4" type="video/mp4">
   你的浏览器不反对 Video 标签
</video>

上述代码在浏览器渲染之后,在页面中会显示一个 Video 视频播放器,具体如下图所示:

(图片起源:https://h5player.bytedance.co…)

通过 Chrome 开发者工具,咱们能够晓得当播放 xgplayer-demo-720p.mp4 视频文件时,发了 3 个 HTTP 申请:

此外,从图中能够分明地看到,头两个 HTTP 申请响应的状态码是 206。这里咱们来剖析第一个 HTTP 申请的申请头和响应头:

在下面的申请头中,有一个 range: bytes=0- 首部信息,该信息用于检测服务端是否反对 Range 申请。如果在响应中存在 Accept-Ranges 首部(并且它的值不为“none”),那么示意该服务器反对范畴申请。

在下面的响应头中,Accept-Ranges: bytes 示意界定范畴的单位是 bytes。这里 Content-Length 也是无效信息,因为它提供了要下载的视频的残缺大小。

1.1 从服务器端申请特定的范畴

如果服务器反对范畴申请的话,你能够应用 Range 首部来生成该类申请。该首部批示服务器应该返回文件的哪一或哪几局部。

1.1.1 繁多范畴

咱们能够申请资源的某一部分。这里咱们应用 Visual Studio Code 中的 REST Client 扩大来进行测试,在这个例子中,咱们应用 Range 首部来申请 www.example.com 首页的前 1024 个字节。

对于应用 REST Client 发动的 繁多范畴申请 ,服务器端会返回状态码为 206 Partial Content 的响应。而响应头中的 Content-Length 首部当初用来示意先前申请范畴的大小(而不是整个文件的大小)。Content-Range 响应首部则示意这一部分内容在整个资源中所处的地位。

1.1.2 多重范畴

Range 头部也反对一次申请文档的多个局部。申请范畴用一个逗号分隔开。比方:

$ curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"

对于该申请会返回以下响应信息:

因为咱们是申请文档的多个局部,所以每个局部都会领有独立的 Content-TypeContent-Range 信息,并且应用 boundary 参数对响应体进行划分。

1.1.3 条件式范畴申请

当从新开始申请更多资源片段的时候,必须确保自从上一个片段被接管之后该资源没有进行过批改。

If-Range 申请首部能够用来生成条件式范畴申请:如果条件满足的话,条件申请就会失效,服务器会返回状态码为 206 Partial 的响应,以及相应的音讯主体。如果条件未能失去满足,那么就会返回状态码为 200 OK 的响应,同时返回整个资源。该首部能够与 Last-Modified 验证器或者 ETag 一起应用,然而二者不能同时应用。

1.1.4 范畴申请的响应

与范畴申请相干的有三种状态:

  • 在申请胜利的状况下,服务器会返回 206 Partial Content 状态码。
  • 在申请的范畴越界的状况下(范畴值超过了资源的大小),服务器会返回 416 Requested Range Not Satisfiable(申请的范畴无奈满足)状态码。
  • 在不反对范畴申请的状况下,服务器会返回 200 OK 状态码。

残余的两个申请,阿宝哥就不再详细分析了。感兴趣的小伙伴,能够应用 Chrome 开发者工具查看一下具体的申请报文。通过第 3 个申请,咱们能够晓得整个视频的大小大概为 7.9 MB。若播放的视频文件太大或呈现网络不稳固,则会导致播放时,须要期待较长的工夫,这重大升高了用户体验。

那么如何解决这个问题呢?要解决该问题咱们能够应用流媒体技术,接下来咱们来介绍流媒体。

二、流媒体

流媒体是指将一连串的媒体数据压缩后,通过网上分段发送数据,在网上即时传输影音以供参观的一种技术与过程,此技术使得数据包得以像流水一样发送;如果不应用此技术,就必须在应用前下载整个媒体文件。

流媒体理论指的是一种新的媒体传送形式,有声音流、视频流、文本流、图像流、动画流等,而非一种新的媒体。流媒体最次要的技术特色就是流式传输,它使得数据能够像流水一样传输。流式传输是指通过网络传送媒体技术的总称。实现流式传输次要有两种形式:程序流式传输(Progressive Streaming)和实时流式传输(Real Time Streaming)。

目前网络上常见的流媒体协定:

通过上表可知,不同的协定有着不同的优缺点。在理论应用过程中,咱们通常会在平台兼容的条件下选用最优的流媒体传输协定。比方,在浏览器里做直播,选用 HTTP-FLV 协定是不错的,性能优于 RTMP+Flash,提早能够做到和 RTMP+Flash 一样甚至更好。

而因为 HLS 提早较大,个别只适宜视频点播的场景,但因为它在挪动端领有较好的兼容性,所以在承受高提早的条件下,也是能够利用在直播场景。

讲到这里置信有些小伙伴会好奇,对于 Video 元素来说应用流媒体技术之后与传统的播放模式有什么直观的区别。上面阿宝哥以常见的 HLS 流媒体协定为例,来简略比照一下它们之间的区别。

通过观察上图,咱们能够很显著地看到,当应用 HLS 流媒体网络传输协定时,<video> 元素 src 属性应用的是 blob:// 协定。讲到该协定,咱们就不得不聊一下 Blob 与 Blob URL。

2.1 Blob

Blob(Binary Large Object)示意二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个繁多个体的汇合。Blob 通常是影像、声音或多媒体文件。 在 JavaScript 中 Blob 类型的对象示意不可变的相似文件对象的原始数据。

Blob 由一个可选的字符串 type(通常是 MIME 类型)和 blobParts 组成:

MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩大类型,是设定某种扩展名的文件用一种应用程序来关上的形式类型,当该扩展名文件被拜访的时候,浏览器会主动应用指定应用程序来关上。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。

常见的 MIME 类型有:超文本标记语言文本 .html text/html、PNG 图像 .png image/png、一般文本 .txt text/plain 等。

为了更直观的感触 Blob 对象,咱们先来应用 Blob 构造函数,创立一个 myBlob 对象,具体如下图所示:

如你所见,myBlob 对象含有两个属性:size 和 type。其中 size 属性用于示意数据的大小(以字节为单位),type 是 MIME 类型的字符串。Blob 示意的不肯定是 JavaScript 原生格局的数据。比方 File 接口基于 Blob,继承了 blob 的性能并将其扩大使其反对用户零碎上的文件。

2.2 Blob URL/Object URL

Blob URL/Object URL 是一种伪协定,容许 Blob 和 File 对象用作图像,下载二进制数据链接等的 URL 源。在浏览器中,咱们应用 URL.createObjectURL 办法来创立 Blob URL,该办法接管一个 Blob 对象,并为其创立一个惟一的 URL,其模式为 blob:<origin>/<uuid>,对应的示例如下:

blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641

浏览器外部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL → Blob 映射。因而,此类 URL 较短,但能够拜访 Blob。生成的 URL 仅在以后文档关上的状态下才无效。但如果你拜访的 Blob URL 不再存在,则会从浏览器中收到 404 谬误。

上述的 Blob URL 看似很不错,但实际上它也有副作用。尽管存储了 URL → Blob 的映射,但 Blob 自身仍驻留在内存中,浏览器无奈开释它。映射在文档卸载时主动革除,因而 Blob 对象随后被开释。然而,如果应用程序寿命很长,那不会很快产生。因而,如果咱们创立一个 Blob URL,即便不再须要该 Blob,它也会存在内存中。

针对这个问题,咱们能够调用 URL.revokeObjectURL(url) 办法,从外部映射中删除援用,从而容许删除 Blob(如果没有其余援用),并开释内存。

2.3 Blob vs ArrayBuffer

其实在前端除了 Blob 对象 之外,你还可能会遇到 ArrayBuffer 对象 。它用于示意通用的,固定长度的原始二进制数据缓冲区。你不能间接操纵 ArrayBuffer 的内容,而是须要创立一个 TypedArray 对象或 DataView 对象,该对象以特定格局示意缓冲区,并应用该对象读取和写入缓冲区的内容。

Blob 对象与 ArrayBuffer 对象领有各自的特点,它们之间的区别如下:

  • 除非你须要应用 ArrayBuffer 提供的写入 / 编辑的能力,否则 Blob 格局可能是最好的。
  • Blob 对象是不可变的,而 ArrayBuffer 是能够通过 TypedArrays 或 DataView 来操作。
  • ArrayBuffer 是存在内存中的,能够间接操作。而 Blob 能够位于磁盘、高速缓存内存和其余不可用的地位。
  • 尽管 Blob 能够间接作为参数传递给其余函数,比方 window.URL.createObjectURL()。然而,你可能仍须要 FileReader 之类的 File API 能力与 Blob 一起应用。
  • Blob 与 ArrayBuffer 对象之间是能够互相转化的:

    • 应用 FileReader 的 readAsArrayBuffer() 办法,能够把 Blob 对象转换为 ArrayBuffer 对象;
    • 应用 Blob 构造函数,如 new Blob([new Uint8Array(data]);,能够把 ArrayBuffer 对象转换为 Blob 对象。

在前端 AJAX 场景下,除了常见的 JSON 格局之外,咱们也可能会用到 Blob 或 ArrayBuffer 对象:

function GET(url, callback) {let xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.responseType = 'arraybuffer'; // or xhr.responseType = "blob";
  xhr.send();

  xhr.onload = function(e) {if (xhr.status != 200) {alert("Unexpected status code" + xhr.status + "for" + url);
      return false;
    }
    callback(new Uint8Array(xhr.response)); // or new Blob([xhr.response]);
  };
}

在以上示例中,通过为 xhr.responseType 设置不同的数据类型,咱们就能够依据理论须要获取对应类型的数据了。介绍完上述内容,上面咱们先来介绍目前利用比拟宽泛的 HLS 流媒体传输协定。

三、HLS

3.1 HLS 简介

HTTP Live Streaming(缩写是 HLS)是由苹果公司提出基于 HTTP 的流媒体网络传输协定,它是苹果公司 QuickTime X 和 iPhone 软件系统的一部分。它的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些。当媒体流正在播放时,客户端能够抉择从许多不同的备用源中以不同的速率下载同样的资源,容许流媒体会话适应不同的数据速率。

此外,当用户的信号强度产生抖动时,视频流会动静调整以提供杰出的再现成果。

(图片起源:https://www.wowza.com/blog/hl…)

最后,仅 iOS 反对 HLS。但当初 HLS 已成为专有格局,简直所有设施都反对它。顾名思义,HLS(HTTP Live Streaming)协定通过规范的 HTTP Web 服务器传送视频内容。这意味着你无需集成任何非凡的基础架构即可散发 HLS 内容。

HLS 领有以下个性:

  • HLS 将播放应用 H.264 或 HEVC / H.265 编解码器编码的视频。
  • HLS 将播放应用 AAC 或 MP3 编解码器编码的音频。
  • HLS 视频流个别被切成 10 秒的片段。
  • HLS 的传输 / 封装格局是 MPEG-2 TS。
  • HLS 反对 DRM(数字版权治理)。
  • HLS 反对各种广告规范,例如 VAST 和 VPAID。

为什么苹果要提出 HLS 这个协定,其实它的次要是为了解决 RTMP 协定存在的一些问题。比方 RTMP 协定不应用规范的 HTTP 接口传输数据,所以在一些非凡的网络环境下可能被防火墙屏蔽掉。然而 HLS 因为应用的 HTTP 协定传输数据,通常状况下不会遇到被防火墙屏蔽的状况。除此之外,它也很容易通过 CDN(内容散发网络)来传输媒体流。

3.2 HLS 自适应比特流

HLS 是一种自适应比特率流协定。因而,HLS 流能够动静地使视频分辨率自适应每个人的网络情况。如果你正在应用高速 WiFi,则能够在手机上流式传输高清视频。然而,如果你在无限数据连贯的公共汽车或地铁上,则能够以较低的分辨率观看雷同的视频。

在开始一个流媒体会话时,客户端会下载一个蕴含元数据的 Extended M3U(m3u8)Playlist 文件,用于寻找可用的媒体流。

(图片起源:https://www.wowza.com/blog/hl…)

为了便于大家的了解,咱们应用 hls.js 这个 JavaScript 实现的 HLS 客户端,所提供的 在线示例,来看一下具体的 m3u8 文件。

x36xhzz.m3u8

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
url_0/193039199_mp4_h264_aac_hd_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=246440,CODECS="mp4a.40.5,avc1.42000d",RESOLUTION=320x184,NAME="240"
url_2/193039199_mp4_h264_aac_ld_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=460560,CODECS="mp4a.40.5,avc1.420016",RESOLUTION=512x288,NAME="380"
url_4/193039199_mp4_h264_aac_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=836280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=848x480,NAME="480"
url_6/193039199_mp4_h264_aac_hq_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=6221600,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1920x1080,NAME="1080"
url_8/193039199_mp4_h264_aac_fhd_7.m3u8

通过观察 Master Playlist 对应的 m3u8 文件,咱们能够晓得该视频反对以下 5 种不同清晰度的视频:

  • 1920×1080(1080P)
  • 1280×720(720P)
  • 848×480(480P)
  • 512×288
  • 320×184

而不同清晰度视频对应的媒体播放列表,会定义在各自的 m3u8 文件中。这里咱们以 720P 的视频为例,来查看其对应的 m3u8 文件:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:11
#EXTINF:10.000,
url_462/193039199_mp4_h264_aac_hd_7.ts
#EXTINF:10.000,
url_463/193039199_mp4_h264_aac_hd_7.ts
#EXTINF:10.000,
url_464/193039199_mp4_h264_aac_hd_7.ts
#EXTINF:10.000,
...
url_525/193039199_mp4_h264_aac_hd_7.ts
#EXT-X-ENDLIST

当用户选定某种清晰度的视频之后,将会下载该清晰度对应的媒体播放列表(m3u8 文件),该列表中就会列出每个片段的信息。HLS 的传输 / 封装格局是 MPEG-2 TS(MPEG-2 Transport Stream),是一种传输和存储蕴含视频、音频与通信协议各种数据的规范格局,用于数字电视广播系统,如 DVB、ATSC、IPTV 等等。

须要留神的是利用一些现成的工具,咱们是能够把多个 TS 文件合并为 mp4 格局的视频文件。 如果要做视频版权保护,那咱们能够思考应用对称加密算法,比方 AES-128 对切片进行对称加密。当客户端进行播放时,先依据 m3u8 文件中配置的密钥服务器地址,获取对称加密的密钥,而后再下载分片,当分片下载实现后再应用匹配的对称加密算法进行解密播放。

对上述过程感兴趣的小伙伴能够参考 Github 上 video-hls-encrypt 这个我的项目,该我的项目深入浅出介绍了基于 HLS 流媒体协定视频加密的解决方案并提供了残缺的示例代码。

(图片起源:https://github.com/hauk0101/v…)

介绍完苹果公司推出的 HLS(HTTP Live Streaming)技术,接下来咱们来介绍另一种基于 HTTP 的动静自适应流 —— DASH。

四、DASH

4.1 DASH 简介

基于 HTTP 的动静自适应流(英语:Dynamic Adaptive Streaming over HTTP,缩写 DASH,也称 MPEG-DASH)是一种自适应比特率流技术,使高质量流媒体能够通过传统的 HTTP 网络服务器以互联网传递。 相似苹果公司的 HTTP Live Streaming(HLS)计划,MPEG-DASH 会将内容分解成一系列小型的基于 HTTP 的文件片段,每个片段蕴含很短长度的可播放内容,而内容总长度可能长达数小时。

内容将被制成多种比特率的备选片段,以提供多种比特率的版本供选用。当内容被 MPEG-DASH 客户端回放时,客户端将依据以后网络条件主动抉择下载和播放哪一个备选计划。客户端将抉择可及时下载的最高比特率片段进行播放,从而防止播放卡顿或从新缓冲事件。也因如此,MPEG-DASH 客户端能够无缝适应一直变动的网络条件并提供高质量的播放体验,领有更少的卡顿与从新缓冲发生率。

MPEG-DASH 是首个基于 HTTP 的自适应比特率流解决方案,它也是一项国际标准。MPEG-DASH 不应该与传输协定混同 —— MPEG-DASH 应用 TCP 传输协定。 不同于 HLS、HDS 和 Smooth Streaming,DASH 不关怀编解码器,因而它能够承受任何编码格局编码的内容,如 H.265、H.264、VP9 等。

尽管 HTML5 不间接反对 MPEG-DASH,然而已有一些 MPEG-DASH 的 JavaScript 实现容许在网页浏览器中通过 HTML5 Media Source Extensions(MSE)应用 MPEG-DASH。另有其余 JavaScript 实现,如 bitdash 播放器反对应用 HTML5 加密媒体扩大播放有 DRM 的 MPEG-DASH。当与 WebGL 联合应用,MPEG-DASH 基于 HTML5 的自适应比特率流还可实现 360° 视频的实时和按需的高效流式传输。

4.2 DASH 重要概念

  • MPD:媒体文件的形容文件(manifest),作用相似 HLS 的 m3u8 文件。
  • Representation:对应一个可抉择的输入(alternative)。如 480p 视频,720p 视频,44100 采样音频等都应用 Representation 形容。
  • Segment(分片):每个 Representation 会划分为多个 Segment。Segment 分为 4 类,其中,最重要的是:Initialization Segment(每个 Representation 都蕴含 1 个 Init Segment),Media Segment(每个 Representation 的媒体内容蕴含若干 Media Segment)。

(图片起源:https://blog.csdn.net/yue_hua…)

在国内 Bilibili 于 2018 年开始应用 DASH 技术,至于为什么抉择 DASH 技术。感兴趣的小伙伴能够浏览 咱们为什么应用 DASH 这篇文章。

讲了那么多,置信有些小伙伴会好奇 MPD 文件长什么样?这里咱们来看一下西瓜视频播放器 DASH 示例中的 MPD 文件:

<?xml version="1.0"?>
<!-- MPD file Generated with GPAC version 0.7.2-DEV-rev559-g61a50f45-master  at 2018-06-11T11:40:23.972Z-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" type="static" mediaPresentationDuration="PT0H1M30.080S" maxSegmentDuration="PT0H0M1.000S" profiles="urn:mpeg:dash:profile:full:2011">
 <ProgramInformation moreInformationURL="http://gpac.io">
  <Title>xgplayer-demo_dash.mpd generated by GPAC</Title>
 </ProgramInformation>

 <Period duration="PT0H1M30.080S">
  <AdaptationSet segmentAlignment="true" maxWidth="1280" maxHeight="720" maxFrameRate="25" par="16:9" lang="eng">
   <ContentComponent id="1" contentType="audio" />
   <ContentComponent id="2" contentType="video" />
   <Representation id="1" mimeType="video/mp4" codecs="mp4a.40.2,avc3.4D4020" width="1280" height="720" frameRate="25" sar="1:1" startWithSAP="0" bandwidth="6046495">
    <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
    <BaseURL>xgplayer-demo_dashinit.mp4</BaseURL>
    <SegmentList timescale="1000" duration="1000">
     <Initialization range="0-1256"/>
      <SegmentURL mediaRange="1257-1006330" indexRange="1257-1300"/>
      <SegmentURL mediaRange="1006331-1909476" indexRange="1006331-1006374"/>
      ...
      <SegmentURL mediaRange="68082016-68083543" indexRange="68082016-68082059"/>
    </SegmentList>
   </Representation>
  </AdaptationSet>
 </Period>
</MPD>

(文件起源:https://h5player.bytedance.co…)

在播放视频时,西瓜视频播放器会依据 MPD 文件,主动申请对应的分片进行播放。

后面咱们曾经提到了 Bilibili,接下来不得不提其开源的一个驰名的开源我的项目 —— flv.js,不过在介绍它之前咱们须要来理解一下 FLV 流媒体格式。

五、FLV

5.1 FLV 文件构造

FLV 是 FLASH Video 的简称,FLV 流媒体格式是随着 Flash MX 的推出倒退而来的视频格式。因为它造成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的呈现无效地解决了视频文件导入 Flash 后,使导出的 SWF 文件体积宏大,不能在网络上很好的应用等问题。

FLV 文件由 FLV Header 和 FLV Body 两局部形成,而 FLV Body 由一系列的 Tag 形成:

5.1.1 FLV 头文件

FLV 头文件:(9 字节)

  • 1-3:前 3 个字节是文件格式标识(FLV 0x46 0x4C 0x56)。
  • 4-4:第 4 个字节是版本(0x01)。
  • 5-5:第 5 个字节的前 5 个 bit 是保留的必须是 0。

    • 第 5 个字节的第 6 个 bit 音频类型标记(TypeFlagsAudio)。
    • 第 5 个字节的第 7 个 bit 也是保留的必须是 0。
    • 第 5 个字节的第 8 个 bit 视频类型标记(TypeFlagsVideo)。
  • 6-9: 第 6-9 的四个字节还是保留的,其数据为 00000009。
  • 整个文件头的长度,个别是 9(3+1+1+4)。
5.1.2 tag 根本格局

tag 类型信息,固定长度为 15 字节:

  • 1-4:前一个 tag 长度(4 字节),第一个 tag 就是 0。
  • 5-5:tag 类型(1 字节);0x8 音频;0x9 视频;0x12 脚本数据。
  • 6-8:tag 内容大小(3 字节)。
  • 9-11:工夫戳(3 字节,毫秒)(第 1 个 tag 的时候总是为 0,如果是脚本 tag 就是 0)。
  • 12-12:工夫戳扩大(1 字节)让工夫戳变成 4 字节(以存储更长时间的 flv 工夫信息),本字节作为工夫戳的最高位。

在 flv 回放过程中,播放程序是依照 tag 的工夫戳程序播放。任何退出到文件中工夫设置数据格式都将被疏忽。

  • 13-15:streamID(3 字节)总是 0。

FLV 格局具体的结构图如下图所示:

在浏览器中 HTML5 的 <video> 是不反对间接播放 FLV 视频格式,须要借助 flv.js 这个开源库来实现播放 FLV 视频格式的性能。

5.2 flv.js 简介

flv.js 是用纯 JavaScript 编写的 HTML5 Flash Video(FLV)播放器,它底层依赖于 Media Source Extensions。在理论运行过程中,它会主动解析 FLV 格式文件并喂给原生 HTML5 Video 标签播放音视频数据,使浏览器在不借助 Flash 的状况下播放 FLV 成为可能。

5.2.1 flv.js 的个性
  • 反对播放 H.264 + AAC / MP3 编码的 FLV 文件;
  • 反对播放多段分段视频;
  • 反对播放 HTTP FLV 低提早实时流;
  • 反对播放基于 WebSocket 传输的 FLV 实时流;
  • 兼容 Chrome,FireFox,Safari 10,IE11 和 Edge;
  • 极低的开销,反对浏览器的硬件加速。
5.2.2 flv.js 的限度
  • MP3 音频编解码器无奈在 IE11/Edge 上运行;
  • HTTP FLV 直播流不反对所有的浏览器。
5.2.3 flv.js 的应用
<script src="flv.min.js"></script>
<video id="videoElement"></video>
<script>
    if (flvjs.isSupported()) {var videoElement = document.getElementById('videoElement');
        var flvPlayer = flvjs.createPlayer({
            type: 'flv',
            url: 'http://example.com/flv/video.flv'
        });
        flvPlayer.attachMediaElement(videoElement);
        flvPlayer.load();
        flvPlayer.play();}
</script>

5.3 flv.js 工作原理

flv.js 的工作原理是将 FLV 文件流转换为 ISO BMFF(Fragmented MP4)片段,而后通过 Media Source Extensions API 将 mp4 段喂给 HTML5 <video> 元素。flv.js 的设计架构图如下图所示:

(图片起源:https://github.com/bilibili/f…)

无关 flv.js 工作原理更具体的介绍,感兴趣的小伙们能够浏览 花椒开源我的项目实时互动流媒体播放器 这篇文章。当初咱们曾经介绍了 hls.js 和 flv.js 这两个支流的流媒体解决方案,其实它们的胜利离不开 Media Source Extensions 这个幕后英雄默默地反对。因而,接下来阿宝哥将带大家一起认识一下 MSE(Media Source Extensions)。

六、MSE

6.1 MSE API

媒体源扩大 API(Media Source Extensions)提供了实现无插件且基于 Web 的流媒体的性能。应用 MSE,媒体串流可能通过 JavaScript 创立,并且能通过应用 audiovideo 元素进行播放。

近几年来,咱们曾经能够在 Web 应用程序上无插件地播放视频和音频了。然而,现有架构过于简略,只能满足一次播放整个曲目的须要,无奈实现拆分 / 合并数个缓冲文件。晚期的流媒体次要应用 Flash 进行服务,以及通过 RTMP 协定进行视频串流的 Flash 媒体服务器。

媒体源扩大(MSE)实现后,状况就不一样了。MSE 使咱们能够把通常的单个媒体文件的 src 值替换成援用 MediaSource 对象(一个蕴含行将播放的媒体文件的筹备状态等信息的容器),以及援用多个 SourceBuffer 对象(代表多个组成整个串流的不同媒体块)的元素。

为了便于大家了解,咱们来看一下根底的 MSE 数据流:

MSE 让咱们可能依据内容获取的大小和频率,或是内存占用详情(例如什么时候缓存被回收),进行更加精准地管制。它是基于它可扩大的 API 建设自适应比特率流客户端(例如 DASH 或 HLS 的客户端)的根底。

在古代浏览器中发明能兼容 MSE 的媒体十分费时费力,还要耗费大量计算机资源和能源。此外,还须应用内部应用程序将内容转换成适合的格局。尽管浏览器反对兼容 MSE 的各种媒体容器,但采纳 H.264 视频编码、AAC 音频编码和 MP4 容器的格局是十分常见的,所以 MSE 须要兼容这些支流的格局。此外 MSE 还为开发者提供了一个 API,用于运行时检测容器和编解码是否受反对。

6.2 MediaSource 接口

MediaSource 是 Media Source Extensions API 示意媒体资源 HTMLMediaElement 对象的接口。MediaSource 对象能够附着在 HTMLMediaElement 在客户端进行播放。在介绍 MediaSource 接口前,咱们先来看一下它的结构图:

(图片起源 —— https://www.w3.org/TR/media-s…)

要了解 MediaSource 的结构图,咱们得先来介绍一下客户端音视频播放器播放一个视频流的次要流程:

获取流媒体 -> 解协定 -> 解封装 -> 音、视频解码 -> 音频播放及视频渲染(需解决音视频同步)。

因为采集的原始音视频数据比拟大,为了不便网络传输,咱们通常会应用编码器,如常见的 H.264 或 AAC 来压缩原始媒体信号。最常见的媒体信号是视频,音频和字幕。比方,日常生活中的电影,就是由不同的媒体信号组成,除静止图片外,大多数电影还含有音频和字幕。

常见的视频编解码器有:H.264,HEVC,VP9 和 AV1。而音频编解码器有:AAC,MP3 或 Opus。每个媒体信号都有许多不同的编解码器。上面咱们以西瓜视频播放器的 Demo 为例,来直观感受一下音频轨、视频轨和字幕轨:

当初咱们来开始介绍 MediaSource 接口的相干内容。

6.2.1 状态
enum ReadyState {
    "closed", // 批示以后源未附加到媒体元素。"open", // 源曾经被媒体元素关上,数据行将被增加到 SourceBuffer 对象中
    "ended" // 源仍附加到媒体元素,但 endOfStream() 已被调用。};
6.2.2 流终止异样
enum EndOfStreamError {
    "network", // 终止播放并收回网络谬误信号。"decode" // 终止播放并收回解码谬误信号。};
6.2.3 结构器
[Constructor]
interface MediaSource : EventTarget {
    readonly attribute SourceBufferList    sourceBuffers;
    readonly attribute SourceBufferList    activeSourceBuffers;
    readonly attribute ReadyState          readyState;
             attribute unrestricted double duration;
             attribute EventHandler        onsourceopen;
             attribute EventHandler        onsourceended;
             attribute EventHandler        onsourceclose;
  
    SourceBuffer addSourceBuffer(DOMString type);
    void         removeSourceBuffer(SourceBuffer sourceBuffer);
    void         endOfStream(optional EndOfStreamError error);
    void         setLiveSeekableRange(double start, double end);
    void         clearLiveSeekableRange();
    static boolean isTypeSupported(DOMString type);
};
6.2.4 属性
  • MediaSource.sourceBuffers —— 只读:返回一个 SourceBufferList 对象,蕴含了这个 MediaSource 的 SourceBuffer 的对象列表。
  • MediaSource.activeSourceBuffers —— 只读:返回一个 SourceBufferList 对象,蕴含了这个 MediaSource.sourceBuffers 中的 SourceBuffer 子集的对象—即提供以后被选中的视频轨(video track),启用的音频轨(audio tracks)以及显示 / 暗藏的字幕轨(text tracks)的对象列表
  • MediaSource.readyState —— 只读:返回一个蕴含以后 MediaSource 状态的汇合,即便它以后没有附着到一个 media 元素(closed),或者已附着并筹备接管 SourceBuffer 对象(open),亦或者已附着但这个流已被 MediaSource.endOfStream() 敞开。
  • MediaSource.duration:获取和设置以后正在推流媒体的持续时间。
  • onsourceopen:设置 sourceopen 事件对应的事件处理程序。
  • onsourceended:设置 sourceended 事件对应的事件处理程序。
  • onsourceclose:设置 sourceclose 事件对应的事件处理程序。
6.2.5 办法
  • MediaSource.addSourceBuffer():创立一个带有给定 MIME 类型的新的 SourceBuffer 并增加到 MediaSource 的 SourceBuffers 列表。
  • MediaSource.removeSourceBuffer():删除指定的 SourceBuffer 从这个 MediaSource 对象中的 SourceBuffers 列表。
  • MediaSource.endOfStream():示意流的完结。
6.2.6 静态方法
  • MediaSource.isTypeSupported():返回一个 Boolean 值表明给定的 MIME 类型是否被以后的浏览器反对—— 这意味着是否能够胜利的创立这个 MIME 类型的 SourceBuffer 对象。
6.2.7 应用示例
var vidElement = document.querySelector('video');

if (window.MediaSource) {// (1)
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen); 
} else {console.log("The Media Source Extensions API is not supported.")
}

function sourceOpen(e) {URL.revokeObjectURL(vidElement.src);
  var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime); // (2)
  var videoUrl = 'hello-mse.mp4';
  fetch(videoUrl) // (3)
    .then(function(response) {return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {sourceBuffer.addEventListener('updateend', function(e) {(4)
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {mediaSource.endOfStream(); 
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer); // (5)
    });
}

以上示例介绍了如何应用 MSE API,接下来咱们来剖析一下次要的工作流程:

  • (1) 判断以后平台是否反对 Media Source Extensions API,若反对的话,则创立 MediaSource 对象,且绑定 sourceopen 事件处理函数。
  • (2) 创立一个带有给定 MIME 类型的新的 SourceBuffer 并增加到 MediaSource 的 SourceBuffers 列表。
  • (3) 从近程流服务器下载视频流,并转换成 ArrayBuffer 对象。
  • (4) 为 sourceBuffer 对象增加 updateend 事件处理函数,在视频流传输实现后敞开流。
  • (5) 往 sourceBuffer 对象中增加已转换的 ArrayBuffer 格局的视频流数据。

下面阿宝哥只是简略介绍了一下 MSE API,想深刻理解它理论利用的小伙伴,能够进一步理解一下 hls.jsflv.js 我的项目。接下来阿宝哥将介绍音视频根底之多媒体容器格局。

七、多媒体封装格局

个别状况下,一个残缺的视频文件是由音频和视频两局部组成的。常见的 AVI、RMVB、MKV、ASF、WMV、MP4、3GP、FLV 等文件只能算是一种封装格局。H.264,HEVC,VP9 和 AV1 等就是视频编码格局,MP3、AAC 和 Opus 等就是音频编码格局。 比方:将一个 H.264 视频编码文件和一个 AAC 音频编码文件按 MP4 封装规范封装当前,就失去一个 MP4 后缀的视频文件,也就是咱们常见的 MP4 视频文件了。

音视频编码的次要目标是压缩原始数据的体积,而封装格局(也称为多媒体容器),比方 MP4,MKV,是用来存储 / 传输编码数据,并按肯定规定把音视频、字幕等数据组织起来,同时还会蕴含一些元信息,比方以后流中蕴含哪些编码类型、工夫戳等,播放器能够依照这些信息来匹配解码器、同步音视频。

为了能更好地了解多媒体封装格局,咱们再来回顾一下视频播放器的原理。

7.1 视频播放器原理

视频播放器是指能播放以数字信号模式存储的视频的软件,也指具备播放视频性能的电子器件产品。大多数视频播放器(除了多数波形文件外)携带解码器以还原通过压缩的媒体文件,视频播放器还要内置一整套转换频率以及缓冲的算法。大多数的视频播放器还能反对播放音频文件。

视频播放根本解决流程大抵包含以下几个阶段:

(1)解协定

从原始的流媒体协定数据中删除信令数据,只保留音视频数据,如采纳 RTMP 协定传输的数据,通过解协定后输入 flv 格局的数据。

(2)解封装

拆散音频和视频压缩编码数据,常见的封装格局 MP4,MKV,RMVB,FLV,AVI 这些格局。从而将曾经压缩编码的视频、音频数据放到一起。例如 FLV 格局的数据通过解封装后输入 H.264 编码的视频码流和 AAC 编码的音频码流。

(3)解码

视频,音频压缩编码数据,还原成非压缩的视频,音频原始数据,音频的压缩编码标准包含 AAC,MP3,AC-3 等,视频压缩编码标准蕴含 H.264,MPEG2,VC-1 等通过解码失去非压缩的视频色彩数据如 YUV420P,RGB 和非压缩的音频数据如 PCM 等。

(4)音视频同步

将同步解码进去的音频和视频数据别离送至零碎声卡和显卡播放。

理解完视频播放器的原理,下一步咱们来介绍多媒体封装格局。

7.2 多媒体封装格局

对于数字媒体数据来说,容器就是一个能够将多媒体数据混在一起寄存的货色,就像是一个包装箱,它能够对音、视频数据进行打包装箱,将原来的两块独立的媒体数据整合到一起,当然也能够单单只寄存一种类型的媒体数据。

有时候,多媒体容器也称封装格局,它只是为编码后的多媒体数据提供了一个“外壳”,也就是将所有的解决好的音频、视频或字幕都包装到一个文件容器内出现给观众,这个包装的过程就叫封装。 罕用的封装格局有:MP4,MOV,TS,FLV,MKV 等。这里咱们来介绍大家比拟相熟的 MP4 封装格局。

7.2.1 MP4 封装格局

MPEG-4 Part 14(MP4)是最罕用的容器格局之一,通常以 .mp4 文件结尾。它用于 HTTP(DASH)上的动静自适应流,也能够用于 Apple 的 HLS 流。MP4 基于 ISO 根本媒体文件格式(MPEG-4 Part 12),该格局基于 QuickTime 文件格式。MPEG 代表动静图像专家组,是国际标准化组织(ISO)和国际电工委员会(IEC)的单干。MPEG 的成立是为了设置音频和视频压缩与传输的规范。

MP4 反对多种编解码器,罕用的视频编解码器是 H.264 和 HEVC,而罕用的音频编解码器是 AAC,AAC 是驰名的 MP3 音频编解码器的后继产品。

MP4 是由一些列的 box 组成,它的最小组成单元是 box。MP4 文件中的所有数据都装在 box 中,即 MP4 文件由若干个 box 组成,每个 box 有类型和长度,能够将 box 了解为一个数据对象块。box 中能够蕴含另一个 box,这种 box 称为 container box。

一个 MP4 文件首先会有且仅有 一个 ftype 类型的 box,作为 MP4 格局的标记并蕴含对于文件的一些信息,之后会有且只有一个 moov 类型的 box(movie box),它是一种 container box,能够有多个,也能够没有,媒体数据的构造由 metadata 进行形容。

置信有些读者会有疑难 —— 理论的 MP4 文件构造是怎么样的?通过应用 mp4box.js 提供的在线服务,咱们能够不便的查看本地或在线 MP4 文件外部的构造:

mp4box.js 在线地址:https://gpac.github.io/mp4box…

因为 MP4 文件构造比较复杂(不信请看下图),这里咱们就不持续开展,有趣味的读者,能够自行浏览相干文章。

接下来,咱们来介绍 Fragmented MP4 容器格局。

7.2.2 Fragmented MP4 封装格局

MP4 ISO Base Media 文件格式规范容许以 fragmented 形式组织 box,这也就意味着 MP4 文件能够组织成这样的构造,由一系列的短的 metadata/data box 对组成,而不是一个长的 metadata/data 对。Fragmented MP4 文件构造如下图所示,图中只蕴含了两个 fragments:

(图片起源 —— https://alexzambelli.com/blog…)

在 Fragmented MP4 文件中含有三个十分要害的 boxes:moovmoofmdat

  • moov(movie metadata box):用于寄存多媒体 file-level 的元信息。
  • mdat(media data box):和一般 MP4 文件的 mdat 一样,用于寄存媒体数据,不同的是一般 MP4 文件只有一个 mdat box,而 Fragmented MP4 文件中,每个 fragment 都会有一个 mdat 类型的 box。
  • moof(movie fragment box):用于寄存 fragment-level 的元信息。该类型的 box 在一般的 MP4 文件中是不存在的,而在 Fragmented MP4 文件中,每个 fragment 都会有一个 moof 类型的 box。

Fragmented MP4 文件中的 fragment 由 moofmdat 两局部组成,每个 fragment 能够蕴含一个音频轨或视频轨,并且也会蕴含足够的元信息,以保障这部分数据能够独自解码。Fragment 的构造如下图所示:

(图片起源 —— https://alexzambelli.com/blog…)

同样,利用 mp4box.js 提供的在线服务,咱们也能够清晰的查看 Fragmented MP4 文件的内部结构:

咱们曾经介绍了 MP4 和 Fragmented MP4 这两种容器格局,咱们用一张图来总结一下它们之间的次要区别:

八、阿宝哥有话说

8.1 如何实现视频本地预览

视频本地预览的性能次要利用 URL.createObjectURL() 办法来实现。URL.createObjectURL() 静态方法会创立一个 DOMString,其中蕴含一个示意参数中给出的对象的 URL。这个 URL 的生命周期和创立它的窗口中的 document 绑定。这个新的 URL 对象示意指定的 File 对象或 Blob 对象。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title> 视频本地预览示例 </title>
  </head>
  <body>
    <h3> 阿宝哥:视频本地预览示例 </h3>
    <input type="file" accept="video/*" onchange="loadFile(event)" />
    <video
      id="previewContainer"
      controls
      width="480"
      height="270"
      style="display: none;"
    ></video>

    <script>
      const loadFile = function (event) {const reader = new FileReader();
        reader.onload = function () {const output = document.querySelector("#previewContainer");
          output.style.display = "block";
          output.src = URL.createObjectURL(new Blob([reader.result]));
        };
        reader.readAsArrayBuffer(event.target.files[0]);
      };
    </script>
  </body>
</html>

8.2 如何实现播放器截图

播放器截图性能次要利用 CanvasRenderingContext2D.drawImage() API 来实现。Canvas 2D API 中的 CanvasRenderingContext2D.drawImage() 办法提供了多种形式在 Canvas 上绘制图像。

drawImage API 的语法如下:

void ctx.drawImage(image, dx, dy);

void ctx.drawImage(image, dx, dy, dWidth, dHeight);

void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

其中 image 参数示意绘制到上下文的元素。容许任何的 canvas 图像源(CanvasImageSource),例如:CSSImageValue,HTMLImageElement,SVGImageElement,HTMLVideoElement,HTMLCanvasElement,ImageBitmap 或者 OffscreenCanvas。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title> 播放器截图示例 </title>
  </head>
  <body>
    <h3> 阿宝哥:播放器截图示例 </h3>
    <video id="video" controls="controls" width="460" height="270" crossorigin="anonymous">
      <!-- 请替换为理论视频地址 -->
      <source src="https://xxx.com/vid_159411468092581" />
    </video>
    <button onclick="captureVideo()"> 截图 </button>
    <script>
      let video = document.querySelector("#video");
      let canvas = document.createElement("canvas");
      let img = document.createElement("img");
      img.crossOrigin = "";
      let ctx = canvas.getContext("2d");

      function captureVideo() {
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
        img.src = canvas.toDataURL();
        document.body.append(img);
      }
    </script>
  </body>
</html>

当初咱们曾经晓得如何获取视频的每一帧,其实在联合 gif.js 这个库提供的 GIF 编码性能,咱们就能够疾速地实现截取视频帧生成 GIF 动画的性能。这里阿宝哥不持续开展介绍,有趣味的小伙伴能够浏览 应用 JS 间接截取 视频片段 生成 gif 动画 这篇文章。

8.3 如何实现 Canvas 播放视频

应用 Canvas 播放视频次要是利用 ctx.drawImage(video, x, y, width, height) 来对视频以后帧的图像进行绘制,其中 video 参数就是页面中的 video 对象。所以如果咱们依照特定的频率一直获取 video 以后画面,并渲染到 Canvas 画布上,就能够实现应用 Canvas 播放视频的性能。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title> 应用 Canvas 播放视频 </title>
  </head>
  <body>
    <h3> 阿宝哥:应用 Canvas 播放视频 </h3>
    <video id="video" controls="controls" style="display: none;">
      <!-- 请替换为理论视频地址 -->
      <source src="https://xxx.com/vid_159411468092581" />
    </video>
    <canvas
      id="myCanvas"
      width="460"
      height="270"
      style="border: 1px solid blue;"
    ></canvas>
    <div>
      <button id="playBtn"> 播放 </button>
      <button id="pauseBtn"> 暂停 </button>
    </div>
    <script>
      const video = document.querySelector("#video");
      const canvas = document.querySelector("#myCanvas");
      const playBtn = document.querySelector("#playBtn");
      const pauseBtn = document.querySelector("#pauseBtn");
      const context = canvas.getContext("2d");
      let timerId = null;

      function draw() {if (video.paused || video.ended) return;
        context.clearRect(0, 0, canvas.width, canvas.height);
        context.drawImage(video, 0, 0, canvas.width, canvas.height);
        timerId = setTimeout(draw, 0);
      }

      playBtn.addEventListener("click", () => {if (!video.paused) return;
        video.play();
        draw();});

      pauseBtn.addEventListener("click", () => {if (video.paused) return;
        video.pause();
        clearTimeout(timerId);
      });
    </script>
  </body>
</html>

8.4 如何实现色度键控(绿屏成果)

上一个示例咱们介绍了应用 Canvas 播放视频,那么可能有一些小伙伴会有疑难,为什么要通过 Canvas 绘制视频,Video 标签不“香”么?这是因为 Canvas 提供了 getImageDataputImageData 办法使得开发者能够动静地更改每一帧图像的显示内容。这样的话,咱们就能够实时地操纵视频数据来合成各种视觉特效到正在出现的视频画面中。

比方 MDN 上的”应用 canvas 解决视频“的教程中就演示了如何应用 JavaScript 代码执行色度键控(绿屏或蓝屏成果)。所谓的色度键控,又称色调嵌空,是一种去背合成技术。Chroma 为纯色之意,Key 则是抽离色彩之意。把被拍摄的人物或物体搁置于绿幕的后面,并进行去背地,将其替换成其余的背景。此技术在电影、电视剧及游戏制作中被大量应用,色键也是虚构摄影棚(Virtual studio)与视觉效果(Visual effects)当中的一个重要环节。

上面咱们来看一下要害代码:

processor.computeFrame = function computeFrame() {this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
    let frame = this.ctx1.getImageData(0, 0, this.width, this.height);
    let l = frame.data.length / 4;

    for (let i = 0; i < l; i++) {let r = frame.data[i * 4 + 0];
      let g = frame.data[i * 4 + 1];
      let b = frame.data[i * 4 + 2];
      if (g > 100 && r > 100 && b < 43)
        frame.data[i * 4 + 3] = 0;
    }
    this.ctx2.putImageData(frame, 0, 0);
    return;
}

以上的 computeFrame() 办法负责获取一帧数据并执行色度键控成果。利用色度键控技术,咱们还能够实现纯客户端实时蒙版弹幕。这里阿宝哥就不具体介绍了,感兴趣的小伙伴能够浏览一下创宇前端 弹幕不挡人!基于色键技术的纯客户端实时蒙版弹幕 这篇文章。

九、参考资源

  • Baike – 流媒体
  • MDN – Video_and_audio_content
  • MDN – Range_requests
  • MDN – Media_Source_Extensions_API
  • Wiki – MPEG-DASH
  • w3.org – Media Source Extensions

正文完
 0