從HTML5提供了video標簽,在網頁中播放視頻已經變成一個非常簡單的事,只要一個video標簽,src屬性設置為視頻的地址就完事了。由于src指向真實的視頻網絡地址,在早期一般網站資源文件不怎么通過referer設置防盜鏈,當我們拿到視頻的地址后可以隨意的下載或使用(每次放假回家,就會有親戚找我幫忙從一些視頻網站上下東西)。
目前的云存儲服務商大部分都支持referer防盜鏈。其原理就是在訪問資源時,請求頭會帶上發起請求的頁面地址,判斷其不存在(表示直接訪問圖片地址)或不在白名單內,即為盜鏈。
可是從某個時間開始我們打開調試工具去看各大視頻網站的視頻src會發現,它們統統變成了這樣的形式。
拿b站的一個視頻來看,紅框中的視頻地址,這個blob是個什么東西?。
其實這個Blob URL也不是什么新技術,國內外出來都有一陣子了,但是網上的相關的文章不多也不是很詳細,今天就和大家一起分享學習一下。
Blob和ArrayBuffer
最早是數據庫直接用Blob來存儲二進制數據對象,這樣就不用關注存儲數據的格式了。在web領域,Blob對象表示一個只讀原始數據的類文件對象,雖然是二進制原始數據但是類似文件的對象,因此可以像操作文件對象一樣操作Blob對象。
ArrayBuffer對象用來表示通用的、固定長度的原始二進制數據緩沖區。我們可以通過new ArrayBuffer(length)來獲得一片連續的內存空間,它不能直接讀寫,但可根據需要將其傳遞到TypedArray視圖或 DataView 對象來解釋原始緩沖區。實際上視圖只是給你提供了一個某種類型的讀寫接口,讓你可以操作ArrayBuffer里的數據。TypedArray需指定一個數組類型來保證數組成員都是同一個數據類型,而DataView數組成員可以是不同的數據類型。
TypedArray視圖的類型數組對象有以下幾個:
Blob與ArrayBuffer的區別是,除了原始字節以外它還提供了mime type作為元數據,Blob和ArrayBuffer之間可以進行轉換。
File對象其實繼承自Blob對象,并提供了提供了name , lastModifiedDate, size ,type 等基礎元數據。
創建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.createObjectURL() 創建的 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,網站地址。
我們通過FFmpeg或Bento4的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,稍微看了一下,后來不了了之。這次忙里偷閑重拾起來把它搞清楚,一邊學習一邊記錄,這篇文章中的很多點展開了其實有很多內容,希望大家看了這篇文章能夠有所啟發或引起興趣,我的目的也就達到了,另外視頻這方面的東西真的是有點深的,文章中如果有錯誤和疏漏也歡迎大家指出,我將及時修正。
作者:wangzy2019
鏈接:https://juejin.im/post/5d1ea7a8e51d454fd8057bea
來源:掘金
紫霞仙子
let http = require('http');
let fs = require('fs');
let express = require('express');
let app = express();
/**
* 前端訪問頁面
*/
app.get('/', function(req, res) {
res.sendfile('./index.html') // 查看標題3前端頁面的index.html
})
/*
* 實現流傳送
*/
app.post('/', function(req, res) {
fs.createReadStream('./video.mp4') // 讀取當前目錄下的video.mp4視頻
.on("open",chunk=>{
console.log("chunk", chunk) // 準備好發送數據
})
.on("data",chunk=>{
console.log(chunk)
res.write(chunk); //發送數據
})
.on("end",()=>{
console.log('end')
res.end(); //發送結束
})
})
let port = 8008;
var server = http.createServer(app);
server.listen(port);
console.log('listening on port:' + port)
<html>
<head>
<meta charset="utf-8" />
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<style type="text/css">
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
text-align: center;
}
.video{
width: 384px;
height: 683px;
margin: 0 auto;
display: inline-block;
}
</style>
</head>
<body>
<video class="video" id="video" controls="controls"></video>
<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.open('POST', './', true);
xhr.responseType = 'blob'; // 注意是blob
xhr.onload = function(e) {
if (this.status == 200) {//請求成功
//獲取blob對象
var blob = this.response;
console.log(this.response)
//獲取blob對象地址,并把值賦給容器
$("#video").attr("src", URL.createObjectURL(blob));
}
};
xhr.send();
</script>
</body>
</html>
瀏覽器中輸入地址:http://localhost:8008/index.html 即可看到效果。
Node.js實現視頻地址Blob加密
在此操作之前我們需要在火狐瀏覽器里安裝“User Agent Switcher”插件。
【重點】將插件安裝好了以后我們將瀏覽器設置為移動端,然后才開始下面的操作。
1、打開視頻所在的網頁
圖1
圖2
2、在網頁空白地方單擊鼠標【右鍵】—>【檢查】
3、此時我們按照下圖進行操作,第一步點擊左下角帶箭頭的按鈕,第二步再點擊視頻中的播放按鈕(注意:當前一定不要打開播放視頻),第三步我們將看到播放按鈕所對應的網頁源代碼。
4、此時,結合上一部操作找到源代碼以后,我們這個時候才打開視頻,讓視頻播放,我們將會看到視頻的地址。
5、雙擊視頻地址,復制。
6、粘貼復制好的視頻在瀏覽器地址框內,回車(Enter)。
此時我們可以看到視頻已經被我們提取了出來。
7、鼠標放在視頻界面上,右鍵,點擊【另存視頻為】
搞定!
如果你覺得有用,歡迎分享給身邊的人。關注我,我還有很多你意想不到的“黑科技”。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。