web視頻基礎教程

前言

提到網頁播放視頻,大部分前端首先想到的確定是:javascript

<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標籤就能夠輕鬆實現視頻播放功能html

可是,當視頻的文件很大時,使用video的播放效果就不是很理想:前端

  1. 播放不流暢(尤爲在:首次初始化視頻 場景時卡頓很是明顯)
  2. 浪費帶寬,若是用戶僅僅觀看了一個視頻的前幾秒,可能已經被提早下載了幾十兆流量了。即浪費了用戶的流量,也浪費了服務器的昂貴帶寬

理想狀態下,咱們但願的播放效果是:html5

  1. 邊播放,邊下載(漸進式下載),無需一次性下載視頻(流媒體)
  2. 視頻碼率的無縫切換(DASH)
  3. 隱藏真實的視頻訪問地址,防止盜鏈和下載(Object URL)

在這種狀況下,普通的video標籤就沒法知足需求了java

206 狀態碼

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

咱們播放demo.mp4視頻時,瀏覽器其實已經作過了部分優化,並不會等待視頻所有下載完成後纔開始播放,而是先請求部分數據git

206

咱們在請求頭添加github

Range: bytes=3145728-4194303

表示須要文件的第3145728字節到第4194303字節區間的數據web

後端響應頭返回ajax

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

Content-Range表示返回了文件的第3145728字節到第4194303字節區間的數據,請求文件的總大小是25641810字節
Content-Length表示此次請求返回了1048576字節(4194303 - 3145728 + 1)npm

斷點續傳和本文接下來將要介紹的視頻分段下載,就須要使用這個狀態碼

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文件的各類信息,包括視頻大小,分辨率,分段視頻的碼率。。。

嗶哩嗶哩網站就是採用的DASH協議

HLS協議的m3u8索引文件就相似DASH的mpd描述文件

協議 索引文件 傳輸格式
DASH mpd m4s
HLS m3u8 ts

開源庫

咱們以前使用原生Media Source手寫的加載過程,其實市面上已經有了成熟的開源庫能夠拿來即用,例如:http-streaminghls.jsflv.js。同時搭配一些解碼轉碼庫,也能夠很方便的在瀏覽器端進行文件的實時轉碼,例如mp4box.jsffmpeg.js

總結

本文簡單介紹了 Media Source Extensions 實現視頻漸進式播放的原理,涉及到基礎的點播直播相關知識。因爲音視頻技術涉及的內容不少,加上本人水平的限制,因此只能幫助你們初步入個門而已

參考

相關文章
相關標籤/搜索