Media Source Extensions (MSE) 是 JavaScript API,可讓您建立從音訊或影片片段播放的串流。雖然本文中未提及,但如果您想在網站上嵌入具有下列用途的影片,則需要瞭解 MSE:
- 智慧串流,也就是根據裝置功能和網路情況調整
- 自動調整回覆,例如廣告插播
- 時間偏移
- 控管效能和下載大小
您可以將 MSE 視為鏈條。如圖所示,下載的檔案和媒體元素之間有幾個層級。
- 用於播放媒體的
<audio>
或<video>
元素。 - 含有
SourceBuffer
的MediaSource
例項,用於提供媒體元素。 fetch()
或 XHR 呼叫,用於擷取Response
物件中的媒體資料。- 呼叫
Response.arrayBuffer()
以餵食MediaSource.SourceBuffer
。
實際上,鏈結會如下所示:
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.log('The Media Source Extensions API is not supported.');
}
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
fetch(videoUrl)
.then(function (response) {
return response.arrayBuffer();
})
.then(function (arrayBuffer) {
sourceBuffer.addEventListener('updateend', function (e) {
if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
mediaSource.endOfStream();
}
});
sourceBuffer.appendBuffer(arrayBuffer);
});
}
如果您到目前為止可從說明中排序出部分內容,請立即停止閱讀。如需更詳盡的說明,請繼續閱讀。 我將透過建立基本 MSE 範例,逐步說明這個鏈結。每個建構步驟都會在先前的步驟中新增程式碼。
清晰說明
這篇文章會說明在網頁上播放媒體的所有須知事項嗎?否,這只是為了協助您瞭解其他地方可能出現的更複雜程式碼。為了方便說明,本文件會簡化並排除許多內容。我們認為可以採用這個做法,因為我們也建議使用 Google 的 Shaka Player 等程式庫。我會在整個過程中指出我刻意簡化的部分。
部分未涵蓋的事項
以下是幾項我不會涵蓋的內容,順序不分先後。
- 播放控制項。由於使用 HTML5
<audio>
和<video>
元素,我們免費取得這些內容。 - 處理錯誤。
適用於正式環境
以下是建議在實際工作環境中使用 MSE 相關 API 時應注意的事項:
- 呼叫這些 API 前,請先處理任何錯誤事件或 API 例外狀況,並檢查
HTMLMediaElement.readyState
和MediaSource.readyState
。這些值可能會在相關事件傳送前變更。 - 請先檢查
SourceBuffer.updating
布林值,再更新SourceBuffer
的mode
、timestampOffset
、appendWindowStart
、appendWindowEnd
,或是在SourceBuffer
上呼叫appendBuffer()
或remove()
,以確保先前的appendBuffer()
和remove()
呼叫仍在進行中。 - 針對新增至
MediaSource
的所有SourceBuffer
例項,請在呼叫MediaSource.endOfStream()
或更新MediaSource.duration
之前,確認其updating
值皆為 false。 - 如果
MediaSource.readyState
值為ended
,appendBuffer()
和remove()
等呼叫,或是設定SourceBuffer.mode
或SourceBuffer.timestampOffset
,都會導致這個值轉換為open
。也就是說,您應該準備好處理多個sourceopen
事件。 - 處理
HTMLMediaElement error
事件時,MediaError.message
的內容可能有助於判斷失敗的根本原因,尤其是在測試環境中難以重現的錯誤。
將 MediaSource 例項附加至媒體元素
如同現今的網頁開發中的許多事情,您必須先偵測特徵偵測。接著,取得媒體元素 (<audio>
或 <video>
元素)。最後,建立 MediaSource
的例項。系統會將其轉換為網址,並傳遞至媒體元素的來源屬性。
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
// Is the MediaSource instance ready?
} else {
console.log('The Media Source Extensions API is not supported.');
}
將 MediaSource
物件傳遞至 src
屬性看似有點奇怪,這些通常是字串,但也可能是 blob。當您檢查含有嵌入媒體的網頁並檢查其媒體元素時,您就會看到了這個意思。
MediaSource 例項是否已就緒?
URL.createObjectURL()
本身為同步性質,但會以非同步方式處理連結。這會導致您在使用 MediaSource
執行個體時稍微延遲。幸運的是,我們有方法可以測試這項問題。最簡單的方法是使用名為 readyState
的 MediaSource
屬性。readyState
屬性會說明 MediaSource
例項與媒體元素之間的關係。可包含下列其中一個值:
closed
:MediaSource
例項未附加至媒體元素。open
:MediaSource
例項已附加至媒體元素,並準備接收資料或正在接收資料。ended
:MediaSource
例項會附加至媒體元素,且其所有資料也已傳遞至該元素。
直接查詢這些選項可能會對效能產生負面影響。幸好,MediaSource
也會在 readyState
變更時觸發事件,特別是 sourceopen
、sourceclosed
、sourceended
。就我建構的例子而言,我使用了 sourceopen
事件來告知我擷取影片及緩衝處理的時間。
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
<strong>mediaSource.addEventListener('sourceopen', sourceOpen);</strong>
} else {
console.log("The Media Source Extensions API is not supported.")
}
<strong>function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
// Create a SourceBuffer and get the media file.
}</strong>
請注意,我也會呼叫 revokeObjectURL()
。我知道這似乎太早了,但在媒體元素的 src
屬性連結至 MediaSource
例項後,我隨時可以執行這項操作。呼叫這個方法不會刪除任何物件。它「確實」允許平台在適當時間處理垃圾收集,這也是我立即呼叫的原因。
建立 SourceBuffer
接下來,我們要建立 SourceBuffer
,這是實際在媒體來源和媒體元素之間傳送資料的物件。SourceBuffer
必須針對您要載入的媒體檔案類型進行設定。
實際上,您可以呼叫 addSourceBuffer()
並傳入適當的值。請注意,在下方範例中,MIME 類型字串包含 MIME 類型和兩個編碼器。這是影片檔案的 mime 字串,但會為檔案的影片和音訊部分使用不同的編解碼器。
MSE 規格第 1 版允許使用者代理程式在要求 MIME 類型和編解碼時有所不同。有些使用者代理程式不要求,但允許使用 MIME 類型。部分使用者代理程式 (例如 Chrome) 需要編碼器,以便處理不自行描述編碼器的 mime 類型。與其嘗試將所有內容排序,不如只加入二個體系。
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.log('The Media Source Extensions API is not supported.');
}
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
<strong>
var mime = 'video/webm; codecs="opus, vp09.00.10.08"'; // e.target refers to
the mediaSource instance. // Store it in a variable so it can be used in a
closure. var mediaSource = e.target; var sourceBuffer =
mediaSource.addSourceBuffer(mime); // Fetch and process the video.
</strong>;
}
取得媒體檔案
如果您在網路上搜尋 MSE 範例,會發現許多範例都是使用 XHR 擷取媒體檔案。為了讓內容更前衛,我將使用 Fetch API 和其傳回的 Promise。如果您在 Safari 中嘗試執行這項操作,就必須使用 fetch()
polyfill 才能運作。
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
<strong>
fetch(videoUrl) .then(function(response){' '}
{
// Process the response object.
}
);
</strong>;
}
正式版播放器會提供多個版本的相同檔案,以支援不同的瀏覽器。可以為音訊和影片使用不同的檔案,讓系統可根據語言設定選取音訊。
實際的程式碼也會有多個不同解析度的媒體檔案副本,以便因應不同的裝置功能和網路狀況。這類應用程式可使用區間要求或區段,以區塊方式載入及播放影片。這可讓系統在媒體播放時調整網路狀況。您可能聽過 DASH 或 HLS 等字詞,這兩種方法都能達成這項目標。完整討論這個主題超出本介紹的範圍。
處理回應物件
程式碼看起來幾乎完成,但媒體無法播放。我們需要從 Response
物件取得媒體資料,並傳送至 SourceBuffer
。
將資料從回應物件傳遞至 MediaSource
例項的常用方法,是從回應物件取得 ArrayBuffer
,然後將其傳遞至 SourceBuffer
。請先呼叫 response.arrayBuffer()
,這會將承諾傳回緩衝區。在我的程式碼中,我已將此承諾傳遞至第二個 then()
子句,並將其附加至 SourceBuffer
。
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
fetch(videoUrl)
.then(function(response) {
<strong>return response.arrayBuffer();</strong>
})
<strong>.then(function(arrayBuffer) {
sourceBuffer.appendBuffer(arrayBuffer);
});</strong>
}
呼叫 endOfStream()
所有 ArrayBuffers
都已附加,且系統不會再預期其他媒體資料時,請呼叫 MediaSource.endOfStream()
。這會將 MediaSource.readyState
變更為 ended
,並觸發 sourceended
事件。
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
fetch(videoUrl)
.then(function(response) {
return response.arrayBuffer();
})
.then(function(arrayBuffer) {
<strong>sourceBuffer.addEventListener('updateend', function(e) {
if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
mediaSource.endOfStream();
}
});</strong>
sourceBuffer.appendBuffer(arrayBuffer);
});
}
最終版本
以下是完整程式碼範例。希望您已經瞭解媒體來源擴充功能
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.log('The Media Source Extensions API is not supported.');
}
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
fetch(videoUrl)
.then(function (response) {
return response.arrayBuffer();
})
.then(function (arrayBuffer) {
sourceBuffer.addEventListener('updateend', function (e) {
if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
mediaSource.endOfStream();
}
});
sourceBuffer.appendBuffer(arrayBuffer);
});
}