Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
從HTML5提供了video標(biāo)簽,在網(wǎng)頁中播放視頻已經(jīng)變成一個(gè)非常簡單的事,只要一個(gè)video標(biāo)簽,src屬性設(shè)置為視頻的地址就完事了。由于src指向真實(shí)的視頻網(wǎng)絡(luò)地址,在早期一般網(wǎng)站資源文件不怎么通過referer設(shè)置防盜鏈,當(dāng)我們拿到視頻的地址后可以隨意的下載或使用(每次放假回家,就會(huì)有親戚找我?guī)兔囊恍┮曨l網(wǎng)站上下東西)。
目前的云存儲(chǔ)服務(wù)商大部分都支持referer防盜鏈。其原理就是在訪問資源時(shí),請(qǐng)求頭會(huì)帶上發(fā)起請(qǐng)求的頁面地址,判斷其不存在(表示直接訪問圖片地址)或不在白名單內(nèi),即為盜鏈。
可是從某個(gè)時(shí)間開始我們打開調(diào)試工具去看各大視頻網(wǎng)站的視頻src會(huì)發(fā)現(xiàn),它們統(tǒng)統(tǒng)變成了這樣的形式。
拿b站的一個(gè)視頻來看,紅框中的視頻地址,這個(gè)blob是個(gè)什么東西?。
其實(shí)這個(gè)Blob URL也不是什么新技術(shù),國內(nèi)外出來都有一陣子了,但是網(wǎng)上的相關(guān)的文章不多也不是很詳細(xì),今天就和大家一起分享學(xué)習(xí)一下。
Blob和ArrayBuffer
最早是數(shù)據(jù)庫直接用Blob來存儲(chǔ)二進(jìn)制數(shù)據(jù)對(duì)象,這樣就不用關(guān)注存儲(chǔ)數(shù)據(jù)的格式了。在web領(lǐng)域,Blob對(duì)象表示一個(gè)只讀原始數(shù)據(jù)的類文件對(duì)象,雖然是二進(jìn)制原始數(shù)據(jù)但是類似文件的對(duì)象,因此可以像操作文件對(duì)象一樣操作Blob對(duì)象。
ArrayBuffer對(duì)象用來表示通用的、固定長度的原始二進(jìn)制數(shù)據(jù)緩沖區(qū)。我們可以通過new ArrayBuffer(length)來獲得一片連續(xù)的內(nèi)存空間,它不能直接讀寫,但可根據(jù)需要將其傳遞到TypedArray視圖或 DataView 對(duì)象來解釋原始緩沖區(qū)。實(shí)際上視圖只是給你提供了一個(gè)某種類型的讀寫接口,讓你可以操作ArrayBuffer里的數(shù)據(jù)。TypedArray需指定一個(gè)數(shù)組類型來保證數(shù)組成員都是同一個(gè)數(shù)據(jù)類型,而DataView數(shù)組成員可以是不同的數(shù)據(jù)類型。
TypedArray視圖的類型數(shù)組對(duì)象有以下幾個(gè):
Blob與ArrayBuffer的區(qū)別是,除了原始字節(jié)以外它還提供了mime type作為元數(shù)據(jù),Blob和ArrayBuffer之間可以進(jìn)行轉(zhuǎn)換。
File對(duì)象其實(shí)繼承自Blob對(duì)象,并提供了提供了name , lastModifiedDate, size ,type 等基礎(chǔ)元數(shù)據(jù)。
創(chuàng)建Blob對(duì)象并轉(zhuǎn)換成ArrayBuffer:
//創(chuàng)建一個(gè)以二進(jìn)制數(shù)據(jù)存儲(chǔ)的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] };
創(chuàng)建一個(gè)相同數(shù)據(jù)的ArrayBuffer,并轉(zhuǎn)換成Blob:
//我們直接創(chuàng)建一個(gè)Uint8Array并填入上面的數(shù)據(jù) 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的相關(guān)內(nèi)容可以參看下面的資料:
URL.createObjectURL
video標(biāo)簽,audio標(biāo)簽還是img標(biāo)簽的src屬性,不管是相對(duì)路徑,絕對(duì)路徑,或者一個(gè)網(wǎng)絡(luò)地址,歸根結(jié)底都是指向一個(gè)文件資源的地址。既然我們知道了Blob其實(shí)是一個(gè)可以當(dāng)作文件用的二進(jìn)制數(shù)據(jù),那么只要我們可以生成一個(gè)指向Blob的地址,是不是就可以用在這些標(biāo)簽的src屬性上,答案肯定是可以的,這里我們要用到的就是URL.createObjectURL()。
const objectURL = URL.createObjectURL(object); //blob:http://localhost:1234/abcedfgh-1234-1234-1234-abcdefghijkl 復(fù)制代碼
這里的object參數(shù)是用于創(chuàng)建URL的File對(duì)象、Blob 對(duì)象或者 MediaSource 對(duì)象,生成的鏈接就是以blob:開頭的一段地址,表示指向的是一個(gè)二進(jìn)制數(shù)據(jù)。
其中l(wèi)ocalhost:1234是當(dāng)前網(wǎng)頁的主機(jī)名稱和端口號(hào),也就是location.host,而且這個(gè)Blob URL是可以直接訪問的。需要注意的是,即使是同樣的二進(jìn)制數(shù)據(jù),每調(diào)用一次URL.createObjectURL方法,就會(huì)得到一個(gè)不一樣的Blob URL。這個(gè)URL的存在時(shí)間,等同于網(wǎng)頁的存在時(shí)間,一旦網(wǎng)頁刷新或卸載,這個(gè)Blob URL就失效。
通過URL.revokeObjectURL(objectURL) 釋放一個(gè)之前已經(jīng)存在的、通過調(diào)用 URL.createObjectURL() 創(chuàng)建的 URL 對(duì)象。當(dāng)你結(jié)束使用某個(gè) URL 對(duì)象之后,應(yīng)該通過調(diào)用這個(gè)方法來讓瀏覽器知道不用在內(nèi)存中繼續(xù)保留對(duì)這個(gè)文件的引用了,允許平臺(tái)在合適的時(shí)機(jī)進(jìn)行垃圾收集。
如果是以文件協(xié)議打開的html文件(即url為file://開頭),則地址中http://localhost:1234會(huì)變成null,而且此時(shí)這個(gè)Blob URL是無法直接訪問的。
實(shí)戰(zhàn)一:上傳圖片預(yù)覽
有時(shí)我們通過input上傳圖片文件之前,會(huì)希望可以預(yù)覽一下圖片,這個(gè)時(shí)候就可以通過前面所學(xué)到的東西實(shí)現(xiàn),而且非常簡單。
html
<input id="upload" type="file" /> <img id="preview" src="" alt="預(yù)覽"/>
javascript
const upload = document.querySelector("#upload"); const preview = document.querySelector("#preview"); upload.onchange = function() { const file = upload.files[0]; //File對(duì)象 const src = URL.createObjectURL(file); preview.src = src; };
這樣一個(gè)圖片上傳預(yù)覽就實(shí)現(xiàn)了,同樣這個(gè)方法也適用于上傳視頻的預(yù)覽。
實(shí)戰(zhàn)二:以Blob URL加載網(wǎng)絡(luò)視頻
現(xiàn)在我們有一個(gè)網(wǎng)絡(luò)視頻的地址,怎么能將這個(gè)視頻地址變成Blob URL是形式呢,思路肯定是先要拿到存儲(chǔ)這個(gè)視頻原始數(shù)據(jù)的Blob對(duì)象,但是不同于input上傳可以直接拿到File對(duì)象,我們只有一個(gè)網(wǎng)絡(luò)地址。
我們知道平時(shí)請(qǐng)求接口我們可以使用xhr(jquery里的ajax和axios就是封裝的這個(gè))或fetch,請(qǐng)求一個(gè)服務(wù)端地址可以返回我們相應(yīng)的數(shù)據(jù),那如果我們用xhr或者fetch去請(qǐng)求一個(gè)圖片或視頻地址會(huì)返回什么呢?當(dāng)然是返回圖片和視頻的數(shù)據(jù),只不過要設(shè)置正確responseType才能拿到我們想要的格式數(shù)據(jù)。
function ajax(url, cb) { const xhr = new XMLHttpRequest(); xhr.open("get", url); xhr.responseType = "blob"; // ""|"text"-字符串 "blob"-Blob對(duì)象 "arraybuffer"-ArrayBuffer對(duì)象 xhr.onload = function() { cb(xhr.response); }; xhr.send(); }
注意XMLHttpRequest和Fetch API請(qǐng)求會(huì)有跨域問題,可以通過跨域資源共享(CORS)解決。
看到responseType可以設(shè)置blob和arraybuffer我們應(yīng)該就有譜了,請(qǐng)求返回一個(gè)Blob對(duì)象,或者返回ArrayBuffer對(duì)象轉(zhuǎn)換成Blob對(duì)象,然后通過createObjectURL生成地址賦值給視頻的src屬性就可以了,這里我們直接請(qǐng)求一個(gè)Blob對(duì)象。
ajax('video.mp4', function(res){ const src = URL.createObjectURL(res); video.src = src; })
用調(diào)試工具查看視頻標(biāo)簽的src屬性已經(jīng)變成一個(gè)Blob URL,表面上看起來是不是和各大視頻網(wǎng)站形式一致了,但是考慮一個(gè)問題,這種形式要等到請(qǐng)求完全部視頻數(shù)據(jù)才能播放,小視頻還好說,要是視頻資源大一點(diǎn)豈不爆炸,顯然各大視頻網(wǎng)站不可能這么干。
解決這個(gè)問題的方法就是流媒體,其帶給我們最直觀體驗(yàn)就是使媒體文件可以邊下邊播(像我這樣的90后男性最早體會(huì)到流媒體好處的應(yīng)該是源于那款快子頭的播放器),web端如果要使用流媒體,有多個(gè)流媒體協(xié)議可以供我們選擇。
HLS和MPEG DASH
HLS (HTTP Live Streaming), 是由 Apple 公司實(shí)現(xiàn)的基于 HTTP 的媒體流傳輸協(xié)議。HLS以ts為傳輸格式,m3u8為索引文件(文件中包含了所要用到的ts文件名稱,時(shí)長等信息,可以用播放器播放,也可以用vscode之類的編輯器打開查看),在移動(dòng)端大部分瀏覽器都支持,也就是說你可以用video標(biāo)簽直接加載一個(gè)m3u8文件播放視頻或者直播,但是在pc端,除了蘋果的Safari,需要引入庫來支持。
用到此方案的視頻網(wǎng)站比如優(yōu)酷,可以在視頻播放時(shí)通過調(diào)試查看Network里的xhr請(qǐng)求,會(huì)發(fā)現(xiàn)一個(gè)m3u8文件,和每隔一段時(shí)間請(qǐng)求幾個(gè)ts文件。
但是除了HLS,還有Adobe的HDS,微軟的MSS,方案一多就要有個(gè)標(biāo)準(zhǔn)點(diǎn)的東西,于是就有了MPEG DASH。
DASH(Dynamic Adaptive Streaming over HTTP) ,是一種在互聯(lián)網(wǎng)上傳送動(dòng)態(tài)碼率的Video Streaming技術(shù),類似于蘋果的HLS,DASH會(huì)通過media presentation description (MPD)將視頻內(nèi)容切片成一個(gè)很短的文件片段,每個(gè)切片都有多個(gè)不同的碼率,DASH Client可以根據(jù)網(wǎng)絡(luò)的情況選擇一個(gè)碼率進(jìn)行播放,支持在不同碼率之間無縫切換。
Youtube,B站都是用的這個(gè)方案。這個(gè)方案索引文件通常是mpd文件(類似HLS的m3u8文件功能),傳輸格式推薦的是fmp4(Fragmented MP4),文件擴(kuò)展名通常為.m4s或直接用.mp4。所以用調(diào)試查看b站視頻播放時(shí)的網(wǎng)絡(luò)請(qǐng)求,會(huì)發(fā)現(xiàn)每隔一段時(shí)間有幾個(gè)m4s文件請(qǐng)求。
不管是HLS還是DASH們,都有對(duì)應(yīng)的庫甚至是高級(jí)的播放器方便我們使用,但我們其實(shí)是想要學(xué)習(xí)一點(diǎn)實(shí)現(xiàn)。其實(shí)拋開掉索引文件的解析拿到實(shí)際媒體文件的傳輸?shù)刂罚瑪[在我們面前的只有一個(gè)如何將多個(gè)視頻數(shù)據(jù)合并讓video標(biāo)簽可以無縫播放。
與之相關(guān)的一篇B站文章推薦給感興趣的朋友:我們?yōu)槭裁词褂肈ASH
MediaSource
video標(biāo)簽src指向一個(gè)視頻地址,視頻播完了再將src修改為下一段的視頻地址然后播放,這顯然不符合我們無縫播放的要求。其實(shí)有了我們前面Blob URL的學(xué)習(xí),我們可能就會(huì)想到一個(gè)思路,用Blob URL指向一個(gè)視頻二進(jìn)制數(shù)據(jù),然后不斷將下一段視頻的二進(jìn)制數(shù)據(jù)添加拼接進(jìn)去。這樣就可以在不影響播放的情況下,不斷的更新視頻內(nèi)容并播放下去,想想是不是有點(diǎn)流的意思出來了。
要實(shí)現(xiàn)這個(gè)功能我們要通過MediaSource來實(shí)現(xiàn),MediaSource接口功能也很純粹,作為一個(gè)媒體數(shù)據(jù)容器可以和HTMLMediaElement進(jìn)行綁定。基本流程就是通過URL.createObjectURL創(chuàng)建容器的BLob URL,設(shè)置到video標(biāo)簽的src上,在播放過程中,我們?nèi)匀豢梢酝ㄟ^MediaSource.appendBuffer方法往容器里添加數(shù)據(jù),達(dá)到更新視頻內(nèi)容的目的。
實(shí)現(xiàn)代碼如下:
const video = document.querySelector('video'); //視頻資源存放路徑,假設(shè)下面有5個(gè)分段視頻 video1.mp4 ~ video5.mp4,第一個(gè)段為初始化視頻init.mp4 const assetURL = "http://www.demo.com"; //視頻格式和編碼信息,主要為判斷瀏覽器是否支持視頻格式,但如果信息和視頻不符可能會(huì)報(bào)錯(cuò) 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綁定,此處生成一個(gè)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代碼實(shí)現(xiàn)翻看上文,數(shù)據(jù)請(qǐng)求類型為arraybuffer ajax(url, function(buf) { //往容器中添加請(qǐng)求到的數(shù)據(jù),不會(huì)影響當(dāng)下的視頻播放。 sourceBuffer.appendBuffer(buf); }); } //每次appendBuffer數(shù)據(jù)更新完之后就會(huì)觸發(fā) sourceBuffer.addEventListener("updateend", function() { if (i === 1) { //第一個(gè)初始化視頻加載完就開始播放 video.play(); } if (i < 6) { //一段視頻加載完成后,請(qǐng)求下一段視頻 getNextVideo(`${assetURL}/video${i}.mp4`); } if (i === 6) { //全部視頻片段加載完關(guān)閉容器 mediaSource.endOfStream(); URL.revokeObjectURL(video.src); //Blob URL已經(jīng)使用并加載,不需要再次使用的話可以釋放掉。 } i++; }); //加載初始視頻 getNextVideo(`${assetURL}/init.mp4`); };
這段代碼修改自MDN的MediaSource詞條中的示例代碼,原例子中只有加載一段視頻,我修改為了多段視頻,代碼里面很多地方還可以優(yōu)化精簡,這里沒做就當(dāng)是為了方便我們看邏輯。
此時(shí)我們已經(jīng)基本實(shí)現(xiàn)了一個(gè)簡易的流媒體播放功能,如果愿意可以再加入m3u8或mpd文件的解析,設(shè)計(jì)一下UI界面,就可以實(shí)現(xiàn)一個(gè)流媒體播放器了。
最后提一下一個(gè)坑,很多人跑了MDN的MediaSource示例代碼,可能會(huì)發(fā)現(xiàn)使用官方提供的視頻是沒問題的,但是用了自己的mp4視頻就會(huì)報(bào)錯(cuò),這是因?yàn)閒mp4文件擴(kuò)展名通常為.m4s或直接用.mp4,但卻是特殊的mp4文件。
Fragmented MP4
通常我們使用的mp4文件是嵌套結(jié)構(gòu)的,客戶端必須要從頭加載一個(gè) MP4 文件,才能夠完整播放,不能從中間一段開始播放。而Fragmented MP4(簡稱fmp4),就如它的名字碎片mp4,是由一系列的片段組成,如果服務(wù)器支持 byte-range 請(qǐng)求,那么,這些片段可以獨(dú)立的進(jìn)行請(qǐng)求到客戶端進(jìn)行播放,而不需要加載整個(gè)文件。
我們可以通過這個(gè)網(wǎng)站判斷一個(gè)mp4文件是否為Fragmented MP4,網(wǎng)站地址。
我們通過FFmpeg或Bento4的mp4fragment來將普通mp4轉(zhuǎn)換為Fragmented MP4,兩個(gè)工具都是命令行工具,按照各自系統(tǒng)下載下來對(duì)應(yīng)的壓縮包,解壓后設(shè)置環(huán)境變量指向文件夾中的bin目錄,就可以使用相關(guān)命令了。
Bento4的mp4fragment,沒有太多參數(shù),命令如下:
mp4fragment video.mp4 video-fragmented.mp4
FFmpeg會(huì)需要設(shè)置一些參數(shù),命令如下:
ffmpeg -i video.mp4 -movflags empty_moov+default_base_moof+frag_keyframe video-fragmented.mp4
Tips:網(wǎng)上大部分的資料中轉(zhuǎn)換時(shí)是不帶default_base_moof這個(gè)參數(shù)的,雖然可以轉(zhuǎn)換成功,但是經(jīng)測試如果不添加此參數(shù)網(wǎng)頁中MediaSource處理視頻時(shí)會(huì)報(bào)錯(cuò)。
視頻的切割分段可以使用Bento4的mp4slipt,命令如下:
mp4split video.mp4 --media-segment video-%llu.mp4 --pattern-parameters N
最后
之所以寫這篇文章其實(shí)是之前公司有個(gè)需求要了解一下Blob URL,稍微看了一下,后來不了了之。這次忙里偷閑重拾起來把它搞清楚,一邊學(xué)習(xí)一邊記錄,這篇文章中的很多點(diǎn)展開了其實(shí)有很多內(nèi)容,希望大家看了這篇文章能夠有所啟發(fā)或引起興趣,我的目的也就達(dá)到了,另外視頻這方面的東西真的是有點(diǎn)深的,文章中如果有錯(cuò)誤和疏漏也歡迎大家指出,我將及時(shí)修正。
作者: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') // 查看標(biāo)題3前端頁面的index.html
})
/*
* 實(shí)現(xiàn)流傳送
*/
app.post('/', function(req, res) {
fs.createReadStream('./video.mp4') // 讀取當(dāng)前目錄下的video.mp4視頻
.on("open",chunk=>{
console.log("chunk", chunk) // 準(zhǔn)備好發(fā)送數(shù)據(jù)
})
.on("data",chunk=>{
console.log(chunk)
res.write(chunk); //發(fā)送數(shù)據(jù)
})
.on("end",()=>{
console.log('end')
res.end(); //發(fā)送結(jié)束
})
})
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) {//請(qǐng)求成功
//獲取blob對(duì)象
var blob = this.response;
console.log(this.response)
//獲取blob對(duì)象地址,并把值賦給容器
$("#video").attr("src", URL.createObjectURL(blob));
}
};
xhr.send();
</script>
</body>
</html>
瀏覽器中輸入地址:http://localhost:8008/index.html 即可看到效果。
Node.js實(shí)現(xiàn)視頻地址Blob加密
在此操作之前我們需要在火狐瀏覽器里安裝“User Agent Switcher”插件。
【重點(diǎn)】將插件安裝好了以后我們將瀏覽器設(shè)置為移動(dòng)端,然后才開始下面的操作。
1、打開視頻所在的網(wǎng)頁
圖1
圖2
2、在網(wǎng)頁空白地方單擊鼠標(biāo)【右鍵】—>【檢查】
3、此時(shí)我們按照下圖進(jìn)行操作,第一步點(diǎn)擊左下角帶箭頭的按鈕,第二步再點(diǎn)擊視頻中的播放按鈕(注意:當(dāng)前一定不要打開播放視頻),第三步我們將看到播放按鈕所對(duì)應(yīng)的網(wǎng)頁源代碼。
4、此時(shí),結(jié)合上一部操作找到源代碼以后,我們這個(gè)時(shí)候才打開視頻,讓視頻播放,我們將會(huì)看到視頻的地址。
5、雙擊視頻地址,復(fù)制。
6、粘貼復(fù)制好的視頻在瀏覽器地址框內(nèi),回車(Enter)。
此時(shí)我們可以看到視頻已經(jīng)被我們提取了出來。
7、鼠標(biāo)放在視頻界面上,右鍵,點(diǎn)擊【另存視頻為】
搞定!
如果你覺得有用,歡迎分享給身邊的人。關(guān)注我,我還有很多你意想不到的“黑科技”。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。