前言

提到网页播放视频,大部分前端首先想到的必定是:

<video width="600" controls>  <source src="demo.mp4" type="video/mp4">  <source src="demo.ogg" type="video/ogg">  <source src="demo.webm" type="video/webm">  您的浏览器不反对 video 标签。</video>

确实,一个简略的video标签就能够轻松实现视频播放性能

然而,当视频的文件很大时,应用video的播放成果就不是很现实:

  1. 播放不晦涩(尤其在:首次初始化视频 场景时卡顿非常明显)
  2. 节约带宽,如果用户仅仅观看了一个视频的前几秒,可能曾经被提前下载了几十兆流量了。即节约了用户的流量,也节约了服务器的低廉带宽

现实状态下,咱们心愿的播放成果是:

  1. 边播放,边下载(渐进式下载),无需一次性下载视频(流媒体)
  2. 视频码率的无缝切换(DASH)
  3. 暗藏实在的视频拜访地址,避免盗链和下载(Object URL)

在这种状况下,一般的video标签就无奈满足需要了

206 状态码

<video width="600" controls>  <source src="demo.mp4" type="video/mp4"></video>

咱们播放demo.mp4视频时,浏览器其实曾经做过了局部优化,并不会期待视频全副下载实现后才开始播放,而是先申请局部数据

咱们在申请头增加

Range: bytes=3145728-4194303

示意须要文件的第3145728字节到第4194303字节区间的数据

后端响应头返回

Content-Length: 1048576Content-Range: bytes 3145728-4194303/25641810

Content-Range示意返回了文件的第3145728字节到第4194303字节区间的数据,申请文件的总大小是25641810字节
Content-Length示意这次申请返回了1048576字节(4194303 - 3145728 + 1)

断点续传和本文接下来将要介绍的视频分段下载,就须要应用这个状态码

Object URL

咱们先来看看市面上各大视频网站是如何播放视频?

哔哩哔哩:

腾讯视频:

爱奇艺:

能够看到,上述网站的video标签指向的都是一个以blob结尾的地址: blob:https://www.bilibili.com/0159a831-92c9-43d1-8979-fe42b40b0735,该地址有几个特点:

  1. 格局固定: blob:以后网站域名/一串字符
  2. 无奈间接在浏览器地址栏拜访
  3. 即便是同一个视频,每次新关上页面,生成的地址都不同

其实,这个地址是通过URL.createObjectURL生成的Object URL

const obj = {name: 'deepred'};const blob = new Blob([JSON.stringify(obj)], {type : 'application/json'});const objectURL = URL.createObjectURL(blob);console.log(objectURL); // blob:https://anata.me/06624c66-be01-4ec5-a351-84d716eca7c0

createObjectURL承受一个FileBlob或者MediaSource对象作为参数,返回的ObjectURL就是这个对象的援用

Blob

Blob是一个由不可扭转的原始数据组成的相似文件的对象;它们能够作为文本或二进制数据来读取,或者转换成一个ReadableStream以便用来用来解决数据

咱们罕用的File对象就是继承并拓展了Blob对象的能力

<input id="upload" type="file" />
const upload = document.querySelector("#upload");const file = upload.files[0];file instanceof File; // truefile instanceof Blob; // trueFile.prototype instanceof Blob; // true

咱们也能够创立一个自定义的blob对象

const obj = {hello: 'world'};const blob = new Blob([JSON.stringify(obj, null, 2)], {type : 'application/json'});blob.size; // 属性blob.text().then(res => console.log(res)) // 办法

Object URL的利用

<input id="upload" type="file" /><img id="preview" alt="预览" />
const upload = document.getElementById('upload');const preview = document.getElementById("preview");upload.addEventListener('change', () => {  const file = upload.files[0];  const src = URL.createObjectURL(file);  preview.src = src;});

createObjectURL返回的Object URL间接通过img进行加载,即可实现前端的图片预览性能

同理,如果咱们用video加载Object URL,是不是就能播放视频了?

index.html

<video controls width="800"></video>

demo.js

function fetchVideo(url) {  return new Promise((resolve, reject) => {    const xhr = new XMLHttpRequest();    xhr.open('GET', url);    xhr.responseType = 'blob'; // 文件类型设置成blob    xhr.onload = function() {      resolve(xhr.response);    };    xhr.onerror = function () {      reject(xhr);    };    xhr.send();  })}async function init() {  const res = await fetchVideo('./demo.mp4');  const url = URL.createObjectURL(res);  document.querySelector('video').src = url;}init();

文件目录如下:

├── demo.mp4├── index.html├── demo.js

应用http-server简略启动一个动态服务器

npm i http-server -ghttp-server -p 4444 -c-1

拜访http://127.0.0.1:4444/,video标签确实可能失常播放视频,但咱们应用ajax异步申请了全副的视频数据,这和间接应用video加载原始视频相比,并无劣势

Media Source Extensions

联合后面介绍的206状态码,咱们能不能通过ajax申请局部的视频片段(segments),先缓冲到video标签里,而后当视频行将播放完结前,持续下载局部视频,实现分段播放呢?

答案当然是必定的,然而咱们不能间接应用video加载原始分片数据,而是要通过 MediaSource API

须要留神的是,一般的mp4格式文件,是无奈通过MediaSource进行加载的,须要咱们应用一些转码工具,将一般的mp4转换成fmp4(Fragmented MP4)。为了简略演示,咱们这里不应用实时转码,而是间接通过MP4Box工具,间接将一个残缺的mp4转换成fmp4

#### 每4s宰割1段mp4box -dash 4000 demo.mp4

运行命令,会生成一个demo_dashinit.mp4视频文件和一个demo_dash.mpd配置文件。其中demo_dashinit.mp4就是被转码后的文件,这次咱们能够应用MediaSource进行加载了

文件目录如下:

├── demo.mp4├── demo_dashinit.mp4├── demo_dash.mpd├── index.html├── demo.js

index.html

<video width="600" controls></video>

demo.js

class Demo {  constructor() {    this.video = document.querySelector('video');    this.baseUrl = '/demo_dashinit.mp4';    this.mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';    this.mediaSource = null;    this.sourceBuffer = null;    this.init();  }  init = () => {    if ('MediaSource' in window && MediaSource.isTypeSupported(this.mimeCodec)) {      const mediaSource = new MediaSource();      this.video.src = URL.createObjectURL(mediaSource); // 返回object url      this.mediaSource = mediaSource;      mediaSource.addEventListener('sourceopen', this.sourceOpen); // 监听sourceopen事件    } else {      console.error('不反对MediaSource');    }  }  sourceOpen = async () => {    const sourceBuffer = this.mediaSource.addSourceBuffer(this.mimeCodec); // 返回sourceBuffer    this.sourceBuffer = sourceBuffer;    const start = 0;    const end = 1024 * 1024 * 5 - 1; // 加载视频结尾的5M数据。如果你的视频文件很大,5M兴许无奈启动视频,能够适当改大点    const range = `${start}-${end}`;    const initData = await this.fetchVideo(range);    this.sourceBuffer.appendBuffer(initData);    this.sourceBuffer.addEventListener('updateend', this.updateFunct, false);  }  updateFunct = () => {      }  fetchVideo = (range) => {    const url = this.baseUrl;    return new Promise((resolve, reject) => {      const xhr = new XMLHttpRequest();      xhr.open('GET', url);      xhr.setRequestHeader("Range", "bytes=" + range); // 增加Range头      xhr.responseType = 'arraybuffer';      xhr.onload = function (e) {        if (xhr.status >= 200 && xhr.status < 300) {          return resolve(xhr.response);        }        return reject(xhr);      };      xhr.onerror = function () {        reject(xhr);      };      xhr.send();    })  }}const demo = new Demo()

实现原理:

  1. 通过申请头Range拉取数据
  2. 将数据喂给sourceBufferMediaSource对数据进行解码解决
  3. 通过video进行播放

咱们这次只申请了视频的前5M数据,能够看到,视频可能胜利播放几秒,而后画面就卡住了。

接下来咱们要做的就是,监听视频的播放工夫,如果缓冲数据行将不够时,就持续下载下一个5M数据

const isTimeEnough = () => {  // 以后缓冲数据是否足够播放  for (let i = 0; i < this.video.buffered.length; i++) {    const bufferend = this.video.buffered.end(i);    if (this.video.currentTime < bufferend && bufferend - this.video.currentTime >= 3) // 提前3s下载视频      return true  }  return false}

当然咱们还有很多问题须要思考,例如:

  1. 每次申请分段数据时,如何更新Range的申请范畴
  2. 首次申请数据时,如何确保video有足够的数据可能播放视频
  3. 兼容性问题
  4. 更多细节。。。。

具体分段下载过程,见残缺代码

流媒体协定

视频服务个别分为:

  1. 点播
  2. 直播

不同的服务,抉择的流媒体协定也各不相同。支流的协定有: RTMP、HTTP-FLV、HLS、DASH、webRTC等等,详见《流媒体协定的意识》

咱们之前的示例,其实就是应用的DASH协定进行的点播服务。还记得当初应用mp4box生成的demo_dash.mpd文件吗?mpd(Media Presentation Description)文件就存储了fmp4文件的各种信息,包含视频大小,分辨率,分段视频的码率。。。

哔哩哔哩网站就是采纳的DASH协定

HLS协定的m3u8索引文件就相似DASH的mpd形容文件

协定索引文件传输格局
DASHmpdm4s
HLSm3u8ts

开源库

咱们之前应用原生Media Source手写的加载过程,其实市面上曾经有了成熟的开源库能够拿来即用,例如:http-streaming,hls.js,flv.js。同时搭配一些解码转码库,也能够很不便的在浏览器端进行文件的实时转码,例如mp4box.js,ffmpeg.js

总结

本文简略介绍了 Media Source Extensions 实现视频渐进式播放的原理,波及到根底的点播直播相干常识。因为音视频技术波及的内容很多,加上自己程度的限度,所以只能帮忙大家初步入个门而已

参考

  • 为什么视频网站的视频链接地址是blob?
  • 从天猫某流动视频不必要的3次申请说起
  • 咱们为什么应用DASH
  • 应用 MediaSource 搭建流式播放器
  • Web 视频播放的那些事儿
  • Building a simple MPEG-DASH streaming player
  • 前端视频直播技术总结及video.js在h5页面中的利用
  • 流媒体协定的意识
  • 让html5视频反对分段渐进式下载的播放