オフライン ストリーミングを使用する PWA

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

プログレッシブ ウェブアプリ: これまでネイティブ専用だった機能を数多く導入 ウェブに公開しますChronicle に関連する最も顕著な機能の 1 つは、 PWA はオフライン エクスペリエンスです。

さらに良いのはオフライン ストリーミング メディアのエクスペリエンスです。これは、 さまざまな方法でユーザーに提供できますただし、 これにより、メディア ファイルが非常に大きくなるという、真に特有の問題が発生します。まず 疑問点:

  • サイズの大きな動画ファイルをダウンロードして保存するにはどうすればよいですか?
  • どのようにユーザーに提示すればよいでしょうか。

この記事では、これらの疑問に対する回答をご紹介しますが、 Google が構築した Kino デモ PWA について紹介し、 Google Cloud なしでのオフライン ストリーミング メディア エクスペリエンスを 機能的または提示的なフレームワークで ML モデルを構築します以下の例は、 主に教育目的である 既存のメディア フレームワークのいずれかを使用して、これらの機能を提供できます。

独自のビジネスケースを作成するのに適したビジネスケースがない限り、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 を使用して、ブラウザをまたいでリモート API に非同期でアクセスする できます。このユースケースでは、大容量の動画ファイルをストリームとしてアクセスし、 HTTP 範囲リクエストを使用して、チャンクとして段階的に格納します。

Fetch API でデータのチャンクを読み取ることができるようになりました。以下の操作も必要です。 保存します。メディアに関連付けられたメタデータが大量にある可能性があります。 たとえば、名前、説明、ランタイムの長さ、カテゴリなどです。

1 つのメディアファイルだけでなく 構造化オブジェクトも保存できます メディアファイルはそのプロパティの 1 つにすぎません

この場合、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() がループしていることがわかります。この方法でチャンクを受信し 読み取り可能ストリーム内のデータが ネットワークから到着したときにどのように すべてのデータが届く前でも データの処理を開始できます ファイアウォールルールがあります

ダウンロードを再開しています

ダウンロードが一時停止または中断されると、受信したデータチャンクは IndexedDB データベースに安全に保存されます。そのボタンを表示して ダウンロードを再開できます。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 のカスタム書き込みバッファ

IndexedDB データベースに dataChunk 値を書き込むプロセス 説明しますこれらの値はすでに ArrayBuffer インスタンスであり、保存可能です 直接 IndexedDB に保存するため、適切な形状のオブジェクトを作成するだけです。 保存します。

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 が書き込みを行うためではなく、 レイテンシを増大させます 新しいトランザクションを ネットワークから受信するデータチャンクごとに

ダウンロードされるチャンクは比較的小さく、システムのストリームで出力できます。 必要があります。IndexedDB の書き込みレートを制限する必要があります。 Kino のデモ PWA では、中間書き込みバッファを実装しています。

データチャンクがネットワークから到着すると、まずそれらをバッファに追加します。条件 受信データが収まらない場合は、バッファをデータベースにフラッシュし、 残りのデータを追加する前に消去してください。結果として、IndexedDB が 書き込み頻度が低くなるため、書き込み速度が大幅に向上します。 向上します

オフライン ストレージからメディア ファイルを提供する

メディア ファイルのダウンロードが完了したら、通常は ネットワークからファイルを取得する代わりに、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 の Service Worker のソースコードをご覧ください。 IndexedDB からファイルデータを読み取り、Google Cloud でストリームを構築する方法を 確認しました

その他の考慮事項

主な障害がなくなったところで、次は 動画アプリケーションに追加すると便利ですでは、生成 AI の Kino デモ PWA には以下のような機能があります。

  • Media Session API の統合によりメディアを操作可能 専用のハードウェア メディアキーを使用するか、メディア通知から再生します。 クリックします。
  • メディア ファイルに関連するその他のアセット(字幕など)のキャッシュ保存 古い Cache API を使ってポスター画像を使いましょう。
  • アプリ内での動画ストリーム(DASH、HLS)のダウンロードに対応。ストリーミングが マニフェストでは通常、ビットレートの異なる複数のソースを宣言する場合、 マニフェスト ファイルを変換し、メディア バージョンを 1 つだけダウンロードしてから保存 オフライン再生用に 保存しておくことができます

次は、音声と動画のプリロードによる高速再生について説明します。