提到網頁播放視頻,大部分前端首先想到的確定是: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
的播放效果就不是很理想:前端
理想狀態下,咱們但願的播放效果是:html5
在這種狀況下,普通的video
標籤就沒法知足需求了java
<video width="600" controls> <source src="demo.mp4" type="video/mp4"> </video>
咱們播放demo.mp4
視頻時,瀏覽器其實已經作過了部分優化,並不會等待視頻所有下載完成後纔開始播放,而是先請求部分數據git
咱們在請求頭添加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
斷點續傳和本文接下來將要介紹的視頻分段下載,就須要使用這個狀態碼
咱們先來看看市面上各大視頻網站是如何播放視頻?
嗶哩嗶哩:
騰訊視頻:
愛奇藝:
能夠看到,上述網站的video
標籤指向的都是一個以blob
開頭的地址: blob:https://www.bilibili.com/0159a831-92c9-43d1-8979-fe42b40b0735
,該地址有幾個特色:
blob:當前網站域名/一串字符
其實,這個地址是經過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
接受一個File
,Blob
或者MediaSource
對象做爲參數,返回的ObjectURL
就是這個對象的引用
Blob是一個由不可改變的原始數據組成的相似文件的對象;它們能夠做爲文本或二進制數據來讀取,或者轉換成一個ReadableStream以便用來用來處理數據
咱們經常使用的File
對象就是繼承並拓展了Blob
對象的能力
<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)) // 方法
<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
進行加載,便可實現前端的圖片預覽功能
同理,若是咱們用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
加載原始視頻相比,並沒有優點
結合前面介紹的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()
實現原理:
Range
拉取數據sourceBuffer
,MediaSource
對數據進行解碼處理video
進行播放咱們此次只請求了視頻的前5M數據,能夠看到,視頻可以成功播放幾秒,而後畫面就卡住了。
接下來咱們要作的就是,監聽視頻的播放時間,若是緩衝數據即將不夠時,就繼續下載下一個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 }
固然咱們還有不少問題須要考慮,例如:
Range
的請求範圍video
有足夠的數據可以播放視頻詳細分段下載過程,見完整代碼
視頻服務通常分爲:
不一樣的服務,選擇的流媒體協議也各不相同。主流的協議有: 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-streaming,hls.js,flv.js。同時搭配一些解碼轉碼庫,也能夠很方便的在瀏覽器端進行文件的實時轉碼,例如mp4box.js,ffmpeg.js
本文簡單介紹了 Media Source Extensions
實現視頻漸進式播放的原理,涉及到基礎的點播直播相關知識。因爲音視頻技術涉及的內容不少,加上本人水平的限制,因此只能幫助你們初步入個門而已