• web视频基础教程


    https://segmentfault.com/a/1190000038788218

    前言

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

    <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

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

    哔哩哔哩:
    bili-v

    腾讯视频:
    ten-v

    爱奇艺:
    iqi-v

    可以看到,上述网站的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对象的能力

    image.png

    <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进行加载,即可实现前端的图片预览功能

    blob-pre

    同理,如果我们用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()

    mse

    实现原理:

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

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

    video-load

    接下来我们要做的就是,监听视频的播放时间,如果缓冲数据即将不够时,就继续下载下一个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文件的各种信息,包括视频大小,分辨率,分段视频的码率。。。

    b站的点播就是采用的DASH协议

    HLS协议的m3u8索引文件就类似DASH的mpd描述文件

    协议索引文件传输格式
    DASH mpd m4s
    HLS m3u8 ts

    开源库

    我们之前使用原生Media Source手写的加载过程,其实市面上已经有了成熟的开源库可以拿来即用,例如:http-streaminghls.jsflv.js。同时搭配一些解码转码库,也可以很方便的在浏览器端进行文件的实时转码,例如mp4box.jsffmpeg.js

    总结

    本文简单介绍了 Media Source Extensions 实现视频渐进式播放的原理,涉及到基础的点播直播相关知识。由于音视频技术涉及的内容很多,加上本人水平的限制,所以只能帮助大家初步入个门而已

    参考

  • 相关阅读:
    P1007 独木桥
    P1789 【Mc生存】插火把
    P2658 汽车拉力比赛
    1959 拔河比赛
    P1936 水晶灯火灵
    websocket
    瀑布流布局
    Router
    图片占位
    单位
  • 原文地址:https://www.cnblogs.com/linus-tan/p/14817563.html
Copyright © 2020-2023  润新知