乐趣区

关于视频处理:web视频基础教程

前言

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

<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: 1048576
Content-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; // true
file instanceof Blob; // true
File.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 -g

http-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 形容文件

协定 索引文件 传输格局
DASH mpd m4s
HLS m3u8 ts

开源库

咱们之前应用原生 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 视频反对分段渐进式下载的播放
退出移动版