預先載入音訊和視訊,快速播放

如何主動預先載入資源,加快媒體播放速度。

François Beaufort
François Beaufort

加快播放速度意味著有更多人在觀看影片或聆聽 音訊。這是已知的錯誤。本文將介紹 可主動提高音訊和影片播放速度的技巧 根據您的用途預先載入資源。

製作人員名單:Copyright Blender Foundation |www.blender.org

我會說明預先載入媒體檔案的三種方法,首先從專業人員開始 和缺點

太棒了... 不過...
影片預先載入屬性 用於網路伺服器託管的專屬檔案。 瀏覽器可能會完全忽略這個屬性。
資源擷取作業會在 HTML 文件完全載入後開始, 剖析。
媒體來源擴充功能 (MSE) 會忽略媒體元素的 preload 屬性,因為應用程式是由 向 MSE 提供媒體內容
預先載入連結 強制瀏覽器在不封鎖影片的情況下發出影片資源請求 文件的 onload 事件。 HTTP 範圍要求不相容。
與 MSE 和檔案區隔相容。 擷取完整資源時,僅適用於小型媒體檔案 (小於 5 MB)。
手動緩衝處理 完全控制 複雜錯誤處理是網站負責的網站。

影片預先載入屬性

如果影片來源是網路伺服器代管的專屬檔案,建議你 使用影片 preload 屬性向瀏覽器提供提示,方法為 預先載入的資訊或內容。換句話說,媒體來源額外資訊 (MSE)preload 不相容。

只有在初始 HTML 文件發生後,系統才會開始擷取資源 已完全載入並剖析 (例如已觸發 DOMContentLoaded 事件) 而截然不同的 load 事件會在資源觸發時 上次擷取到的內容

preload 屬性設為 metadata,代表使用者並未 則需要影片,但擷取其中繼資料 (維度、音軌) 清單、持續時間等) 都合適,請注意,首先使用 Chrome 64preload 的預設值是 metadata。(原價:auto )。

<video id="video" preload="metadata" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

preload 屬性設為 auto 表示瀏覽器可以快取 或累積足夠的資料 以進一步緩衝處理

<video id="video" preload="auto" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

不過,您還是需要注意一些事項。這只是提示 會忽略 preload 屬性。截至本文撰寫時, 已套用至 Chrome:

  • 啟用 Data Saver 時,Chrome 會強制 preload 值: none
  • 在 Android 4.3 中,Chrome 會強制 preload 值設為 none,因為 Android 錯誤
  • 使用行動網路連線 (2G、3G 和 4G) 時,Chrome 會強制 preload 的值為 metadata

提示

如果您的網站包含許多位於相同網域的影片資源, 建議您將 preload 值設為 metadata 或定義 poster 屬性並將 preload 設為 none。如此一來 連至相同網域的 HTTP 連線數量上限 (根據 HTTP 1.1 規格),可停止載入資源。請注意,這也可能 加快網頁速度 (如果影片不屬於核心使用者體驗)。

如其他文章已介紹連結預先載入是一種宣告式擷取作業, 可讓您強制瀏覽器在沒有資源的情況下 也會封鎖 load 事件及網頁下載期間。資源 透過 <link rel="preload"> 載入的物件會儲存在本機瀏覽器中 在 DOM、JavaScript、 或 CSS

預先載入與預先擷取不同之處,在於前者著重於目前的導覽, 根據類型 (指令碼、樣式、字型、 影片、音訊等)。應用於為目前的 工作階段。

預先載入完整影片

以下說明如何在網站上預先載入完整影片, JavaScript 要求擷取影片內容,並以資源形式從快取讀取內容 瀏覽器可能已快取過網頁。如果預先載入要求 但完成後,將進行一般網路擷取作業。

<link rel="preload" as="video" href="https://cdn.com/small-file.mp4">

<video id="video" controls></video>

<script>
  // Later on, after some condition has been met, set video source to the
  // preloaded video URL.
  video.src = 'https://cdn.com/small-file.mp4';
  video.play().then(() => {
    // If preloaded video URL was already cached, playback started immediately.
  });
</script>
敬上

由於預先載入的資源會由 在本例中,as 預先載入連結值為 video。如果是音訊 元素,應該是 as="audio"

預先載入第一個片段

以下範例說明如何使用 <link rel="preload"> 預先載入影片的第一個片段,並搭配媒體來源擴充功能使用。如果還不熟悉 MSE JavaScript API,請參閱 MSE 基本概念

為簡單起見,我們假設整部影片已經分割為 file_1.webmfile_2.webmfile_3.webm 等較小的檔案。

<link rel="preload" as="fetch" href="https://cdn.com/file_1.webm">

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // If video is preloaded already, fetch will return immediately a response
    // from the browser cache (memory cache). Otherwise, it will perform a
    // regular network fetch.
    fetch('https://cdn.com/file_1.webm')
    .then(response => response.arrayBuffer())
    .then(data => {
      // Append the data into the new sourceBuffer.
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch file_2.webm when user starts playing video.
    })
    .catch(error => {
      // TODO: Show "Video is not available" message to user.
    });
  }
</script>
敬上

支援

您可以使用as<link rel=preload> 程式碼片段:

function preloadFullVideoSupported() {
  const link = document.createElement('link');
  link.as = 'video';
  return (link.as === 'video');
}

function preloadFirstSegmentSupported() {
  const link = document.createElement('link');
  link.as = 'fetch';
  return (link.as === 'fetch');
}

手動緩衝處理

深入探討 Cache API 和服務工作站之前,先來看看 如何使用 MSE 手動緩衝影片緩衝處理。以下範例假設您的網站 伺服器支援 HTTP Range 但與 file 區隔請注意,部分中介軟體程式庫,例如 Google 的 Shaka 程式庫 播放器JW PlayerVideo.js 都屬於 會是你的生活好幫手

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // Fetch beginning of the video by setting the Range HTTP request header.
    fetch('file.webm', { headers: { range: 'bytes=0-567139' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      sourceBuffer.appendBuffer(data);
      sourceBuffer.addEventListener('updateend', updateEnd, { once: true });
    });
  }

  function updateEnd() {
    // Video is now ready to play!
    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);

    // Fetch the next segment of video when user starts playing the video.
    video.addEventListener('playing', fetchNextSegment, { once: true });
  }

  function fetchNextSegment() {
    fetch('file.webm', { headers: { range: 'bytes=567140-1196488' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      const sourceBuffer = mediaSource.sourceBuffers[0];
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch further segment and append it.
    });
  }
</script>

注意事項

由於您現在已擁有完整的媒體緩衝體驗,建議您 考量裝置的電池電量、「節省數據模式」使用者偏好設定和 網路資訊。

電池感知

將使用者的電池電量納入考量裝置再考慮 關於預先載入影片的資訊這樣可以延長電池電量時 偏低。

停用預先載入功能,或至少預先載入解析度較低的影片 裝置電力耗盡。

if ('getBattery' in navigator) {
  navigator.getBattery()
  .then(battery => {
    // If battery is charging or battery level is high enough
    if (battery.charging || battery.level > 0.15) {
      // TODO: Preload the first segment of a video.
    }
  });
}

偵測「節省數據用量」

使用 Save-Data 用戶端提示要求標頭,兼顧快速與便利性 選擇使用「節省數據用量」模型 。透過識別此要求標頭,應用程式就能自訂和 提供最優質的使用者體驗,同時兼顧成本和效能 使用者。

詳情請參閱運用 Save-Data 提供快速推送應用程式

根據網路資訊自動載入

建議您先檢查 navigator.connection.type,再預先載入。時間 已設為 cellular,可以防止預先載入,並建議使用者 行動網路業者可能會收取頻寬費用,而且在啟動後 自動播放先前快取的內容。

if ('connection' in navigator) {
  if (navigator.connection.type == 'cellular') {
    // TODO: Prompt user before preloading video
  } else {
    // TODO: Preload the first segment of a video.
  }
}

查看網路資訊範例,瞭解如何回應網路 做出變更

預先快取多個第一個區隔

現在,如果我想在沒有系統的情況下 得知使用者最後會選擇的媒體內容為何?如果使用者透過 內含 10 部影片的網頁 可能有足夠的記憶體 可以擷取其中之一 但這裡不應該建立 10 個隱藏的<video> 元素和 10 個 MediaSource 物件,並開始動態饋給該資料。

以下兩部分的範例說明如何預先快取多個第一個區段 Cache API請注意 也可以使用 IndexedDB 達成我們目前並未使用 Service Worker,因為 Cache API 也可透過 window 物件存取。

擷取並快取

const videoFileUrls = [
  'bat_video_file_1.webm',
  'cow_video_file_1.webm',
  'dog_video_file_1.webm',
  'fox_video_file_1.webm',
];

// Let's create a video pre-cache and store all first segments of videos inside.
window.caches.open('video-pre-cache')
.then(cache => Promise.all(videoFileUrls.map(videoFileUrl => fetchAndCache(videoFileUrl, cache))));

function fetchAndCache(videoFileUrl, cache) {
  // Check first if video is in the cache.
  return cache.match(videoFileUrl)
  .then(cacheResponse => {
    // Let's return cached response if video is already in the cache.
    if (cacheResponse) {
      return cacheResponse;
    }
    // Otherwise, fetch the video from the network.
    return fetch(videoFileUrl)
    .then(networkResponse => {
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, networkResponse.clone());
      return networkResponse;
    });
  });
}

請注意,如果你使用 HTTP Range 要求,必須手動重新建立 Response 物件,因為 Cache API「目前」不支援 Range 回應。成為 請注意,呼叫 networkResponse.arrayBuffer() 會擷取整個內容 傳回至轉譯器記憶體,這也是為什麼建議您使用 小數值

為了方便參考,我已修改上述範例的部分以儲存 HTTP 範圍 要求傳送至影片友善快取

    ...
    return fetch(videoFileUrl, { headers: { range: 'bytes=0-567139' } })
    .then(networkResponse => networkResponse.arrayBuffer())
    .then(data => {
      const response = new Response(data);
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, response.clone());
      return response;
    });

播放影片

使用者按下播放按鈕時,我們會擷取影片的第一個片段 可在適用情況下立即開始播放。 否則我們會直接從網路擷取請注意,瀏覽器 而使用者可以選擇清除快取

如先前所述,我們使用 MSE 將該影片的第一個片段輸入至影片 元素。

function onPlayButtonClick(videoFileUrl) {
  video.load(); // Used to be able to play video later.

  window.caches.open('video-pre-cache')
  .then(cache => fetchAndCache(videoFileUrl, cache)) // Defined above.
  .then(response => response.arrayBuffer())
  .then(data => {
    const mediaSource = new MediaSource();
    video.src = URL.createObjectURL(mediaSource);
    mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

    function sourceOpen() {
      URL.revokeObjectURL(video.src);

      const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
      sourceBuffer.appendBuffer(data);

      video.play().then(() => {
        // TODO: Fetch the rest of the video when user starts playing video.
      });
    }
  });
}
敬上

使用 Service Worker 建立範圍回應

現在,如果您擷取整個影片檔案 Cache API?當瀏覽器傳送 HTTP Range 要求時,您當然不會 需要將整個影片匯入轉譯器記憶體,因為 Cache API 沒有 目前支援 Range 回應。

接下來,我會示範如何攔截這些要求,並傳回自訂的 Range 收到來自 Service Worker 的回應。

addEventListener('fetch', event => {
  event.respondWith(loadFromCacheOrFetch(event.request));
});

function loadFromCacheOrFetch(request) {
  // Search through all available caches for this request.
  return caches.match(request)
  .then(response => {

    // Fetch from network if it's not already in the cache.
    if (!response) {
      return fetch(request);
      // Note that we may want to add the response to the cache and return
      // network response in parallel as well.
    }

    // Browser sends a HTTP Range request. Let's provide one reconstructed
    // manually from the cache.
    if (request.headers.has('range')) {
      return response.blob()
      .then(data => {

        // Get start position from Range request header.
        const pos = Number(/^bytes\=(\d+)\-/g.exec(request.headers.get('range'))[1]);
        const options = {
          status: 206,
          statusText: 'Partial Content',
          headers: response.headers
        }
        const slicedResponse = new Response(data.slice(pos), options);
        slicedResponse.setHeaders('Content-Range': 'bytes ' + pos + '-' +
            (data.size - 1) + '/' + data.size);
        slicedResponse.setHeaders('X-From-Cache': 'true');

        return slicedResponse;
      });
    }

    return response;
  }
}

請注意,我使用 response.blob() 重新建立此切割線 只是留下檔案控制代碼 response.arrayBuffer() 會將整個檔案匯入轉譯器記憶體。

我可以透過我的自訂 X-From-Cache HTTP 標頭判斷這項要求是否 來自快取或網路包括 ShakaPlayer,以便忽略回應時間, 每個網路速度

請參閱官方的 Sample Media 應用程式,特別是 ranged-response.js 檔案,適用於處理 Range 的完整解決方案 要求。