爲何視頻網站的視頻連接地址是blob?

自從HTML5提供了video標籤,在網頁中播放視頻已經變成一個很是簡單的事,只要一個video標籤,src屬性設置爲視頻的地址就完事了。因爲src指向真實的視頻網絡地址,在早期通常網站資源文件不怎麼經過referer設置防盜鏈,當咱們拿到視頻的地址後能夠隨意的下載或使用(每次放假回家,就會有親戚找我幫忙從一些視頻網站上下東西)。javascript

目前的雲存儲服務商大部分都支持referer防盜鏈。其原理就是在訪問資源時,請求頭會帶上發起請求的頁面地址,判斷其不存在(表示直接訪問資源地址)或不在白名單內,即爲盜鏈。 html

但是從某個時間開始咱們打開調試工具去看各大視頻網站的視頻src會發現,它們通通變成了這樣的形式。前端

拿b站的一個視頻來看,紅框中的視頻地址,這個blob是個什麼東西?。java

其實這個Blob URL也不是什麼新技術,國內外出來都有一陣子了,可是網上的相關的文章很少也不是很詳細,今天就和你們一塊兒分享學習一下。jquery

Blob和ArrayBuffer

最先是數據庫直接用Blob來存儲二進制數據對象,這樣就不用關注存儲數據的格式了。在web領域,Blob對象表示一個只讀原始數據的類文件對象,雖然是二進制原始數據可是相似文件的對象,所以能夠像操做文件對象同樣操做Blob對象。ios

ArrayBuffer對象用來表示通用的、固定長度的原始二進制數據緩衝區。咱們能夠經過new ArrayBuffer(length)來得到一片連續的內存空間,它不能直接讀寫,但可根據須要將其傳遞到TypedArray視圖或 DataView 對象來解釋原始緩衝區。實際上視圖只是給你提供了一個某種類型的讀寫接口,讓你能夠操做ArrayBuffer裏的數據。TypedArray需指定一個數組類型來保證數組成員都是同一個數據類型,而DataView數組成員能夠是不一樣的數據類型。git

TypedArray視圖的類型數組對象有如下幾個:程序員

  • Int8Array:8位有符號整數,長度1個字節。
  • Uint8Array:8位無符號整數,長度1個字節。
  • Uint8ClampedArray:8位無符號整數,長度1個字節,溢出處理不一樣。
  • Int16Array:16位有符號整數,長度2個字節。
  • Uint16Array:16位無符號整數,長度2個字節。
  • Int32Array:32位有符號整數,長度4個字節。
  • Uint32Array:32位無符號整數,長度4個字節。
  • Float32Array:32位浮點數,長度4個字節。
  • Float64Array:64位浮點數,長度8個字節。

Blob與ArrayBuffer的區別是,除了原始字節之外它還提供了mime type做爲元數據,Blob和ArrayBuffer之間能夠進行轉換。github

File對象其實繼承自Blob對象,並提供了提供了name , lastModifiedDate, size ,type 等基礎元數據。 web

建立Blob對象並轉換成ArrayBuffer:

//建立一個以二進制數據存儲的html文件
const text = "<div>hello world</div>";
const blob = new Blob([text], { type: "text/html" }); // Blob {size: 22, type: "text/html"}
//以文本讀取
const textReader = new FileReader();
textReader.readAsText(blob);
textReader.onload = function() {
  console.log(textReader.result); // <div>hello world</div>
};
//以ArrayBuffer形式讀取
const bufReader = new FileReader();
bufReader.readAsArrayBuffer(blob);
bufReader.onload = function() {
  console.log(new Uint8Array(bufReader.result)); // Uint8Array(22) [60, 100, 105, 118, 62, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 60, 47, 100, 105, 118, 62]
};
複製代碼

建立一個相同數據的ArrayBuffer,並轉換成Blob:

//咱們直接建立一個Uint8Array並填入上面的數據
const u8Buf = new Uint8Array([60, 100, 105, 118, 62, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 60, 47, 100, 105, 118, 62]);
const u8Blob = new Blob([u8Buf], { type: "text/html" }); // Blob {size: 22, type: "text/html"}
const textReader = new FileReader();

textReader.readAsText(u8Blob);
textReader.onload = function() {
  console.log(textReader.result); // 一樣獲得div>hello world</div>
};
複製代碼

更多Blob和ArrayBuffer的相關內容能夠參看下面的資料:

URL.createObjectURL

video標籤,audio標籤仍是img標籤的src屬性,不論是相對路徑,絕對路徑,或者一個網絡地址,歸根結底都是指向一個文件資源的地址。既然咱們知道了Blob實際上是一個能夠看成文件用的二進制數據,那麼只要咱們能夠生成一個指向Blob的地址,是否是就能夠用在這些標籤的src屬性上,答案確定是能夠的,這裏咱們要用到的就是URL.createObjectURL()。

const objectURL = URL.createObjectURL(object); //blob:http://localhost:1234/abcedfgh-1234-1234-1234-abcdefghijkl
複製代碼

這裏的object參數是用於建立URL的File對象、Blob 對象或者 MediaSource 對象,生成的連接就是以blob:開頭的一段地址,表示指向的是一個二進制數據。

其中localhost:1234是當前網頁的主機名稱和端口號,也就是location.host,並且這個Blob URL是能夠直接訪問的。須要注意的是,即便是一樣的二進制數據,每調用一次URL.createObjectURL方法,就會獲得一個不同的Blob URL。這個URL的存在時間,等同於網頁的存在時間,一旦網頁刷新或卸載,這個Blob URL就失效。

經過URL.revokeObjectURL(objectURL) 能夠釋放 URL 對象。當你結束使用某個 URL 對象以後,應該經過調用這個方法來讓瀏覽器知道不用在內存中繼續保留對這個文件的引用了,容許平臺在合適的時機進行垃圾收集。

若是是以文件協議打開的html文件(即url爲file://開頭),則地址中http://localhost:1234會變成null,並且此時這個Blob URL是沒法直接訪問的。

實戰一:上傳圖片預覽

有時咱們經過input上傳圖片文件以前,會但願能夠預覽一下圖片,這個時候就能夠經過前面所學到的東西實現,並且很是簡單。

html

<input id="upload" type="file" />
<img id="preview" src="" alt="預覽"/>
複製代碼

javascript

const upload = document.querySelector("#upload");
const preview = document.querySelector("#preview");

upload.onchange = function() {
  const file = upload.files[0]; //File對象
  const src = URL.createObjectURL(file); 
  preview.src = src;
};
複製代碼

這樣一個圖片上傳預覽就實現了,一樣這個方法也適用於上傳視頻的預覽。

實戰二:以Blob URL加載網絡視頻

如今咱們有一個網絡視頻的地址,怎麼能將這個視頻地址變成Blob URL是形式呢,思路確定是先要拿到存儲這個視頻原始數據的Blob對象,可是不一樣於input上傳能夠直接拿到File對象,咱們只有一個網絡地址。

咱們知道平時請求接口咱們可使用xhr(jquery裏的ajax和axios就是封裝的這個)或fetch,請求一個服務端地址能夠返回咱們相應的數據,那若是咱們用xhr或者fetch去請求一個圖片或視頻地址會返回什麼呢?固然是返回圖片和視頻的數據,只不過要設置正確responseType才能拿到咱們想要的格式數據。

function ajax(url, cb) {
  const xhr = new XMLHttpRequest();
  xhr.open("get", url);
  xhr.responseType = "blob"; // "text"-字符串 "blob"-Blob對象 "arraybuffer"-ArrayBuffer對象
  xhr.onload = function() {
    cb(xhr.response);
  };
  xhr.send();
}
複製代碼

注意XMLHttpRequest和Fetch API請求會有跨域問題,能夠經過跨域資源共享(CORS)解決。

看到responseType能夠設置blob和arraybuffer咱們應該就有譜了,請求返回一個Blob對象,或者返回ArrayBuffer對象轉換成Blob對象,而後經過createObjectURL生成地址賦值給視頻的src屬性就能夠了,這裏咱們直接請求一個Blob對象。

ajax('video.mp4', function(res){
    const src = URL.createObjectURL(res); 
    video.src = src;
})
複製代碼

用調試工具查看視頻標籤的src屬性已經變成一個Blob URL,表面上看起來是否是和各大視頻網站形式一致了,可是考慮一個問題,這種形式要等到請求徹底部視頻數據才能播放,小視頻還好說,要是視頻資源大一點豈不爆炸,顯然各大視頻網站不可能這麼幹。

解決這個問題的方法就是流媒體,其帶給咱們最直觀體驗就是使媒體文件能夠邊下邊播(像我這樣的90後男性最先體會到流媒體好處的應該是源於那款快子頭的播放器),web端若是要使用流媒體,有多個流媒體協議能夠供咱們選擇。

HLS和MPEG DASH

HLS (HTTP Live Streaming), 是由 Apple 公司實現的基於 HTTP 的媒體流傳輸協議。HLS以ts爲傳輸格式,m3u8爲索引文件(文件中包含了所要用到的ts文件名稱,時長等信息,能夠用播放器播放,也能夠用vscode之類的編輯器打開查看),在移動端大部分瀏覽器都支持,也就是說你能夠用video標籤直接加載一個m3u8文件播放視頻或者直播,可是在pc端,除了蘋果的Safari,須要引入庫來支持。

用到此方案的視頻網站好比優酷,能夠在視頻播放時經過調試查看Network裏的xhr請求,會發現一個m3u8文件,和每隔一段時間請求幾個ts文件。

可是除了HLS,還有Adobe的HDS,微軟的MSS,方案一多就要有個標準點的東西,因而就有了MPEG DASH。

DASH(Dynamic Adaptive Streaming over HTTP) ,是一種在互聯網上傳送動態碼率的Video Streaming技術,相似於蘋果的HLS,DASH會經過media presentation description (MPD)將視頻內容切片成一個很短的文件片斷,每一個切片都有多個不一樣的碼率,DASH Client能夠根據網絡的狀況選擇一個碼率進行播放,支持在不一樣碼率之間無縫切換。

Youtube,B站都是用的這個方案。這個方案索引文件一般是mpd文件(相似HLS的m3u8文件功能),傳輸格式推薦的是fmp4(Fragmented MP4),文件擴展名一般爲.m4s或直接用.mp4。因此用調試查看b站視頻播放時的網絡請求,會發現每隔一段時間有幾個m4s文件請求。

不論是HLS仍是DASH們,都有對應的庫甚至是高級的播放器方便咱們使用,但咱們實際上是想要學習一點實現。其實拋開掉索引文件的解析拿到實際媒體文件的傳輸地址,擺在咱們面前的只有一個如何將多個視頻數據合併讓video標籤能夠無縫播放。

與之相關的一篇B站文章推薦給感興趣的朋友:咱們爲何使用DASH

MediaSource

video標籤src指向一個視頻地址,視頻播完了再將src修改成下一段的視頻地址而後播放,這顯然不符合咱們無縫播放的要求。其實有了咱們前面Blob URL的學習,咱們可能就會想到一個思路,用Blob URL指向一個視頻二進制數據,而後不斷將下一段視頻的二進制數據添加拼接進去。這樣就能夠在不影響播放的狀況下,不斷的更新視頻內容並播放下去,想一想是否是有點流的意思出來了。

要實現這個功能咱們要經過MediaSource來實現,MediaSource接口功能也很純粹,做爲一個媒體數據容器能夠和HTMLMediaElement進行綁定。基本流程就是經過URL.createObjectURL建立容器的BLob URL,設置到video標籤的src上,在播放過程當中,咱們仍然能夠經過MediaSource.appendBuffer方法往容器裏添加數據,達到更新視頻內容的目的。

實現代碼以下:

const video = document.querySelector('video');
//視頻資源存放路徑,假設下面有5個分段視頻 video1.mp4 ~ video5.mp4,第一個段爲初始化視頻init.mp4
const assetURL = "http://www.demo.com";
//視頻格式和編碼信息,主要爲判斷瀏覽器是否支持視頻格式,但若是信息和視頻不符可能會報錯
const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'; 
if ('MediaSource' in window && MediaSource.isTypeSupported(mimeCodec)) {
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource); //將video與MediaSource綁定,此處生成一個Blob URL
  mediaSource.addEventListener('sourceopen', sourceOpen); //能夠理解爲容器打開
} else {
  //瀏覽器不支持該視頻格式
  console.error('Unsupported MIME type or codec: ', mimeCodec);
}

function sourceOpen () {
  const mediaSource = this;
  const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
  let i = 1;
  function getNextVideo(url) {
    //ajax代碼實現翻看上文,數據請求類型爲arraybuffer
    ajax(url, function(buf) {
      //往容器中添加請求到的數據,不會影響當下的視頻播放。
      sourceBuffer.appendBuffer(buf);
    });
  }
  //每次appendBuffer數據更新完以後就會觸發
  sourceBuffer.addEventListener("updateend", function() {
    if (i === 1) {
      //第一個初始化視頻加載完就開始播放
      video.play();
    }
    if (i < 6) {
      //一段視頻加載完成後,請求下一段視頻
      getNextVideo(`${assetURL}/video${i}.mp4`);
    }
    if (i === 6) {
      //所有視頻片斷加載完關閉容器
      mediaSource.endOfStream();
      URL.revokeObjectURL(video.src); //Blob URL已經使用並加載,不須要再次使用的話能夠釋放掉。
    }
    i++;
  });
  //加載初始視頻
  getNextVideo(`${assetURL}/init.mp4`);
};

複製代碼

這段代碼修改自MDN的MediaSource詞條中的示例代碼,原例子中只有加載一段視頻,我修改成了多段視頻,代碼裏面不少地方還能夠優化精簡,這裏沒作就當是爲了方便咱們看邏輯。

此時咱們已經基本實現了一個簡易的流媒體播放功能,若是願意能夠再加入m3u8或mpd文件的解析,設計一下UI界面,就能夠實現一個流媒體播放器了。

最後提一下一個坑,不少人跑了MDN的MediaSource示例代碼,可能會發現使用官方提供的視頻是沒問題的,可是用了本身的mp4視頻就會報錯,這是由於fmp4文件擴展名一般爲.m4s或直接用.mp4,但倒是特殊的mp4文件。

Fragmented MP4

一般咱們使用的mp4文件是嵌套結構的,客戶端必需要從頭加載一個 MP4 文件,纔可以完整播放,不能從中間一段開始播放。而Fragmented MP4(簡稱fmp4),就如它的名字碎片mp4,是由一系列的片斷組成,若是服務器支持 byte-range 請求,那麼,這些片斷能夠獨立的進行請求到客戶端進行播放,而不須要加載整個文件。

咱們能夠經過這個網站判斷一個mp4文件是否爲Fragmented MP4,網站地址

咱們經過FFmpegBento4的mp4fragment來將普通mp4轉換爲Fragmented MP4,兩個工具都是命令行工具,按照各自系統下載下來對應的壓縮包,解壓後設置環境變量指向文件夾中的bin目錄,就可使用相關命令了。

Bento4的mp4fragment,沒有太多參數,命令以下:

mp4fragment video.mp4 video-fragmented.mp4
複製代碼

FFmpeg會須要設置一些參數,命令以下:

ffmpeg -i video.mp4 -movflags empty_moov+default_base_moof+frag_keyframe video-fragmented.mp4
複製代碼

Tips:網上大部分的資料中轉換時是不帶default_base_moof這個參數的,雖然能夠轉換成功,可是經測試若是不添加此參數網頁中MediaSource處理視頻時會報錯。

視頻的切割分段可使用Bento4的mp4slipt,命令以下:

mp4split video.mp4 --media-segment video-%llu.mp4 --pattern-parameters N
複製代碼

最後

之因此寫這篇文章實際上是以前公司有個需求要了解一下Blob URL,稍微看了一下,後來不了了之。此次忙裏偷閒重拾起來把它搞清楚,一邊學習一邊記錄,這篇文章中的不少點展開了其實有不少內容,但願你們看了這篇文章可以有所啓發或引發興趣,個人目的也就達到了,另外視頻這方面的東西真的是有點深的,文章中若是有錯誤和疏漏也歡迎你們指出,我將及時修正。

文章備份倉庫:GITHUB地址

文章列表

相關文章
相關標籤/搜索