媒體來源額外資訊

法蘭索瓦博福特
François Beaufort
喬麥德利
Joe Medley

媒體來源擴充功能 (MSE) 是一種 JavaScript API,可讓您從音訊或影片片段建立播放串流。雖然本文沒有說明,但如果您想在網站中嵌入影片進行下列操作,就必須先瞭解 MSE:

  • 自動調整串流,這是根據裝置功能和網路狀況進行調整的另一種方式
  • 自動調整提供設定,例如插入廣告
  • 時光平移
  • 控管效能和下載大小
基本 MSE 資料流程
圖 1:基本 MSE 資料流程

您可以將 MSE 視為鏈結。如圖所示,下載檔案和媒體元素之間是多個圖層。

  • 用於播放媒體的 <audio><video> 元素。
  • 含有 SourceBufferMediaSource 例項,用於提供媒體元素。
  • 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.readyStateMediaSource.readyState。這些值在傳送相關事件前可能會變更。
  • 請先檢查 SourceBuffer.updating 布林值,再更新 SourceBuffermodetimestampOffsetappendWindowStartappendWindowEnd,或在 SourceBuffer 上呼叫 appendBuffer()remove(),確認之前的 appendBuffer()remove() 呼叫仍未仍在進行中。
  • 針對新增至 MediaSource 的所有 SourceBuffer 執行個體,請確認其 updating 值皆未為 true,再呼叫 MediaSource.endOfStream() 或更新 MediaSource.duration
  • 如果 MediaSource.readyState 值為 ended,則 appendBuffer()remove() 等呼叫,或設定 SourceBuffer.modeSourceBuffer.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.');
}
做為 blob 的來源屬性
圖 1:blob 的來源屬性

MediaSource 物件傳遞至 src 屬性似乎不太好。這些鍵通常是字串,但也可以是 blob。如果您檢查嵌入媒體的網頁,並檢查其媒體元素,即可看到我所代表的意義。

MediaSource 執行個體是否準備就緒?

URL.createObjectURL() 本身是同步性質,但會以非同步的方式處理連結。這會導致在使用 MediaSource 執行個體執行任何操作之前稍微延遲。幸好,您可以透過幾種方式測試。最簡單的方法是使用名為 readyStateMediaSource 屬性。readyState 屬性描述了 MediaSource 執行個體和媒體元素之間的關係。可能具有下列其中一個值:

  • closedMediaSource 例項未附加至媒體元素。
  • openMediaSource 執行個體已附加至媒體元素,且已準備好接收資料或正在接收資料。
  • ended - MediaSource 執行個體已附加至媒體元素,且其所有資料已傳送至該元素。

直接查詢這些選項可能會對效能造成負面影響。幸好,MediaSource 也會在 readyState 變更 (特別是 sourceopensourceclosedsourceended) 時觸發事件。在建構的範例中,我會使用 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 字串,但針對檔案的影片和音訊部分使用不同的轉碼器。

第 1 版 MSE 規格可讓使用者代理程式對是否需要 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);
    });
}

最終版本

以下是完整程式碼範例。希望您對 Media Source Extensions 有所瞭解

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);
    });
}

意見回饋