相关文章推荐
强悍的创口贴  ·  使用 Java 管理 Blob ...·  2 月前    · 
睿智的牛肉面  ·  python ...·  2 月前    · 
骑白马的香菜  ·  Java ...·  2 月前    · 
温文尔雅的紫菜汤  ·  java - ...·  1 年前    · 
帅气的枇杷  ·  Window localStorage ...·  2 年前    · 
web视频基础教程

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 视频时,浏览器其实已经做过了部分优化,并不会等待视频全部下载完成后才开始播放,而是先请求部分数据


206状态码


我们在请求头添加

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

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

哔哩哔哩:

b站


腾讯视频:

腾讯


爱奇艺:

爱奇艺


可以看到,上述网站的 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 接受一个 File Blob 或者 MediaSource 对象作为参数,返回的 ObjectURL 就是这个对象的引用

Blob

Blob是一个由不可改变的原始数据组成的类似文件的对象;它们可以作为文本或二进制数据来读取,或者转换成一个ReadableStream以便用来用来处理数据

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

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()


MS加载流程


实现原理:

1. 通过请求头 Range 拉取数据

2. 将数据喂给 sourceBuffer MediaSource 对数据进行解码处理

3. 通过 video 进行播放

我们这次只请求了视频的前5M数据,可以看到,视频能够成功播放几秒,然后画面就卡住了。


等待缓冲


接下来我们要做的就是,监听视频的播放时间,如果缓冲数据即将不够时,就继续下载下一个5M数据

const isTimeEnough = () => {
  // 当前缓冲数据是否足够播放
  for (let i = 0; i < this.video.buffered.length; i++) {
    const bufferend = this.video.buffered.end(i);