具備離線串流的 PWA

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

漸進式網頁應用程式提供許多原先為原生應用程式保留的功能 部署至網路 PWA 屬於離線體驗。

更棒的是,離線串流媒體體驗會比較好 以幾種不同方式提供給使用者的加強功能。不過 這會產生真正的問題,媒體檔案的大小可能非常。以下內容 您可能會問:

  • 如何下載和儲存大型影片檔案?
  • 如何向使用者提供這些內容?

本文將討論這些問題的答案 參考我們建構的 Kino 示範 PWA 範例:如何在沒有 並運用任何功能性或呈現式架構範例如下 主要是用於教育目的,因為在大部分的情況下,您應該使用 現有的 Media Framework 是用來提供這類功能。

除非您有自行開發的商業案例,否則建構 PWA 離線串流將是一大挑戰本文將說明 用於為使用者提供高品質離線媒體的 API 與技術 無須專人管理

下載及儲存大型媒體檔案

漸進式網頁應用程式通常會使用便利的 Cache API 進行下載 並儲存提供離線體驗所需的資產:文件、 樣式表、圖片等

以下是在 Service Worker 中使用 Cache API 的基本範例:

const cacheStorageName = 'v1';

this.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheStorageName).then(function(cache) {
      return cache.addAll([
        'index.html',
        'style.css',
        'scripts.js',

        // Don't do this.
        'very-large-video.mp4',
      ]);
    })
  );
});

雖然上述範例在技術上有效,但使用 Cache API 有幾個 導致它使用大型檔案不切實際。

例如,Cache API 不會:

  • 可讓你輕鬆暫停及繼續下載
  • 追蹤下載進度
  • 提供可妥善回應 HTTP 範圍要求的方法

對任何影片應用程式而言,上述所有問題都是相當嚴重的限制。 讓我們看看其他可能更適合的方法。

如今,Fetch API 是用於跨瀏覽器以非同步方式存取遠端 檔案。在這個應用實例中,您能以串流方式存取大型影片檔案 使用 HTTP 範圍要求,以區塊形式將其逐步儲存為區塊。

現在您可以使用 Fetch API 讀取資料區塊,還須執行以下作業: 儲存圖片你的媒體可能會有很多相關的中繼資料 檔案,例如名稱、說明、執行階段長度、類別等

您不只是儲存一個媒體檔案 而是儲存結構化物件 而媒體檔案只是其屬性之一

在此情況下,IndexedDB API 提供絕佳解決方案可儲存 媒體資料和中繼資料它可以輕鬆保存大量的二進位資料 也提供索引,可讓您迅速執行資料查詢。

使用 Fetch API 下載媒體檔案

在 PWA 示範中,我們為 Fetch API 建立了幾項有趣的功能 軟體開發套件名為 Kino原始碼是公開的,歡迎查看。

  • 可暫停及繼續未完成的下載內容。
  • 在資料庫中儲存資料區塊的自訂緩衝區。

在說明這些功能的實作方式之前, 快速回顧如何使用 Fetch API 下載檔案。

/**
 * Downloads a single file.
 *
 * @param {string} url URL of the file to be downloaded.
 */
async function downloadFile(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  do {
    const { done, dataChunk } = await reader.read();
    // Store the `dataChunk` to IndexedDB.
  } while (!done);
}

請注意,await reader.read() 是迴圈嗎?系統會以這種方式 資料來自可讀取的串流。思考做法 這表示您可以先開始處理資料 再從網路接收檔案

繼續下載

下載作業暫停或中斷時,接收到的資料區塊將會 安全儲存於索引資料庫這樣您就能看到 在您的應用程式中繼續下載。Kino 示範 PWA 伺服器 支援恢復下載 HTTP 範圍要求簡直簡單許多:

async downloadFile() {
  // this.currentFileMeta contains data from IndexedDB.
  const { bytesDownloaded, url, downloadUrl } = this.currentFileMeta;
  const fetchOpts = {};

  // If we already have some data downloaded,
  // request everything from that position on.
  if (bytesDownloaded) {
    fetchOpts.headers = {
      Range: `bytes=${bytesDownloaded}-`,
    };
  }

  const response = await fetch(downloadUrl, fetchOpts);
  const reader = response.body.getReader();

  let dataChunk;
  do {
    dataChunk = await reader.read();
    if (!dataChunk.done) this.buffer.add(dataChunk.value);
  } while (!dataChunk.done && !this.paused);
}

IndexedDB 的自訂寫入緩衝區

本文件中,將 dataChunk 值寫入索引資料庫資料庫的過程 。這些值已有 ArrayBuffer 個執行個體,可以儲存 編入索引,我們可以直接建立適當形狀的物件 然後加以儲存

const dataItem = {
  url: fileUrl,
  rangeStart: dataStartByte,
  rangeEnd: dataEndByte,
  data: dataChunk,
}

// Name of the store that will hold your data.
const storeName = 'fileChunksStorage'

// `db` is an instance of `IDBDatabase`.
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const putRequest = store.put(data);

putRequest.onsuccess = () => { ... }

雖然這個方法有效,但您可能會發現 IndexedDB 寫入 遠比下載速度明顯慢並不是因為 IndexedDB 寫入 反之,是因為建立 LLM 後 我們會為從網路收到的每個資料區塊建立新的交易。

下載的區塊較小,可以很小,且可由串流發出 能夠快速帶動業務成長您必須限制索引資料庫的寫入頻率。在 Kino 示範 PWA 是我們實作中介寫入緩衝區的方式。

當資料區塊從網路傳入時,我們會先將其附加至緩衝區中。如果 如傳入的資料不適合,我們會將整個緩衝區排入資料庫中, 請先清除清除資料,再附加其餘資料因此,我們的索引資料庫 寫入頻率較低,進而大幅改善寫入速度 才需進行

從離線儲存空間提供媒體檔案

下載媒體檔案後,您可能會希望 Service Worker 則從 IndexedDB 提供該資料庫,而不是從網路擷取檔案。

/**
 * The main service worker fetch handler.
 *
 * @param {FetchEvent} event Fetch event.
 */
const fetchHandler = async (event) => {
  const getResponse = async () => {
    // Omitted Cache API code used to serve static assets.

    const videoResponse = await getVideoResponse(event);
    if (videoResponse) return videoResponse;

    // Fallback to network.
    return fetch(event.request);
  };
  event.respondWith(getResponse());
};
self.addEventListener('fetch', fetchHandler);

那麼,您必須在 getVideoResponse() 中做些什麼?

  • event.respondWith() 方法會預期 Response 物件做為參數。

  • Response() 建構函式指出 可用來將 Response 物件執行個體化:BlobBufferSourceReadableStream 等等。

  • 我們需要的物件不會將所有資料儲存在記憶體中,因此我們 則建議選擇 ReadableStream

此外,因為我們必須處理大型檔案,而我們希望瀏覽器能夠 只需請求目前所需的部分檔案 HTTP 範圍要求的一些基本支援。

/**
 * Respond to a request to fetch offline video file and construct a response
 * stream.
 *
 * Includes support for `Range` requests.
 *
 * @param {Request} request  Request object.
 * @param {Object}  fileMeta File meta object.
 *
 * @returns {Response} Response object.
 */
const getVideoResponse = (request, fileMeta) => {
  const rangeRequest = request.headers.get('range') || '';
  const byteRanges = rangeRequest.match(/bytes=(?<from>[0-9]+)?-(?<to>[0-9]+)?/);

  // Using the optional chaining here to access properties of
  // possibly nullish objects.
  const rangeFrom = Number(byteRanges?.groups?.from || 0);
  const rangeTo = Number(byteRanges?.groups?.to || fileMeta.bytesTotal - 1);

  // Omitting implementation for brevity.
  const streamSource = {
     pull(controller) {
       // Read file data here and call `controller.enqueue`
       // with every retrieved chunk, then `controller.close`
       // once all data is read.
     }
  }
  const stream = new ReadableStream(streamSource);

  // Make sure to set proper headers when supporting range requests.
  const responseOpts = {
    status: rangeRequest ? 206 : 200,
    statusText: rangeRequest ? 'Partial Content' : 'OK',
    headers: {
      'Accept-Ranges': 'bytes',
      'Content-Length': rangeTo - rangeFrom + 1,
    },
  };
  if (rangeRequest) {
    responseOpts.headers['Content-Range'] = `bytes ${rangeFrom}-${rangeTo}/${fileMeta.bytesTotal}`;
  }
  const response = new Response(stream, responseOpts);
  return response;

歡迎查看 Kino 示範 PWA 的 服務 Worker 原始碼,瞭解如何找出 說明如何讀取 IndexedDB 中的檔案資料,以及如何在當中建構串流 實際的應用程式數量

其他注意事項

解決主要障礙後,您現在可以開始添加一些 為影片應用程式增添絕佳功能以下是一些線上旅遊入口網站的使用者故事範例 Kino 示範 PWA 有哪些功能:

  • 整合 Media Session API,讓使用者控制媒體 使用專屬硬體媒體按鍵或媒體通知播放 彈出式視窗。
  • 快取與媒體檔案 (例如字幕) 相關的其他資產, 使用良好舊版 Cache API 的代表圖片
  • 支援在應用程式中下載影片串流 (DASH、HLS)。因為串流 資訊清單通常會宣告多個不同位元率的來源 並只下載一個媒體版本再儲存檔案 離線觀看

接下來,您將瞭解在預先載入音訊和視訊的情況下快速播放相關資訊。