音声と動画をプリロードして高速再生

リソースをアクティブにプリロードしてメディア再生を高速化する方法。

François Beaufort
François Beaufort

<ph type="x-smartling-placeholder">

再生開始が早ければ、より多くのユーザーが動画を視聴したり、 生成できます。これは既知の事実です。この記事では 音声と動画の再生を高速化するために使用できる リソースをプリロードできます。

クレジット: Copyright Blender Foundation |www.blender.org

メディア ファイルをプリロードする 3 つの方法を紹介します。まず、それぞれの長所について説明します。 デメリット

すごい... でも...
動画のプリロード属性 ウェブサーバーでホストされている固有のファイルに使用するのが簡単です。 ブラウザはこの属性を完全に無視する場合があります。
リソースの取得は、HTML ドキュメントが完全に読み込まれ、 表示されます。
Media Source Extensions(MSE)は、メディア要素の preload 属性を無視します。これは、アプリが 提供します
リンクのプリロード ブラウザは動画リソースをブロックせずに強制的にリクエストします。 ドキュメントの onload イベントに対して行われます。 HTTP 範囲リクエストには互換性がありません。
MSE およびファイル・セグメントとの互換性がある リソース全体を取得するときは、小さいメディア ファイル(5 MB 未満)にのみ使用します。
手動バッファリング フル コントロール 複雑なエラー処理はウェブサイトが行います。

動画のプリロード属性

動画のソースがウェブサーバーでホストされている固有のファイルである場合は、 動画の preload 属性を使用して、 プリロードする情報やコンテンツが多いつまり、メディアソース拡張機能です。 (MSE)preload と互換性がありません。

リソースの取得は、最初の HTML ドキュメントが 完全に読み込まれて解析された(例: DOMContentLoaded イベントが発生した) 他とはまったく異なる load イベントは、リソースの作成時に 取得されています。

preload 属性を metadata に設定すると、ユーザーが 動画が必要ですが、メタデータ(ディメンション、トラッキング list、duration など)が望ましいです。なお、Chrome 64 の場合、preload のデフォルト値は 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 は preload の値を強制的に none
  • Android 4.3 では、Chrome は preload の値を none に強制します。これは、 バグ
  • モバイル接続(2G、3G、4G)では、Chrome は preload の値を強制的に metadata

ヒント

ウェブサイトに同じドメインの多数の動画リソースがある場合は、 preload の値を metadata に設定するか、poster を定義することをおすすめします。 preloadnone に設定します。そうすれば 同一ドメインへの HTTP 接続の最大数( HTTP 1.1 仕様など)に準拠しないためです。なお 動画がユーザー エクスペリエンスの主要な要素でない場合に、ページの読み込み速度を改善する。

<ph type="x-smartling-placeholder">

他の記事説明されているように、リンクのプリロードは宣言型フェッチであり、 ブラウザがリソースを強制的に要求することを ダウンロード中に load イベントをブロックします。リソース <link rel="preload"> で読み込まれたファイルは、ブラウザのローカルに保存されます。また、 DOM、JavaScript、Java などで明示的に参照されるまで、実質的に無効になる (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"> を使用して動画の最初のセグメントをプリロードし、Media Source Extensions で使用する方法を示しています。これらのツールに MSE JavaScript API を使用する場合は、MSE の基本をご覧ください。

わかりやすくするため、動画全体が 2 つの サイズの小さいファイル(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>

サポート

<ph type="x-smartling-placeholder">

次のコマンドを使用すると、<link rel=preload> のさまざまな as 型のサポートを検出できます。 スニペット:

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 と Service Worker の詳しい説明に入る前に、 MSE を使用して動画を手動でバッファリングする方法を学びました。以下の例は、ウェブサーバーが サーバーが HTTP Range をサポートする ファイルの場合とよく似ていますが、 分割しますなお、一部のミドルウェア ライブラリ(Google の Shaka など)は、 PlayerJW 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>

考慮事項

メディア バッファリングの操作は全体的に管理できるようになりました。 デバイスのバッテリー残量を考慮し、「データセーバー モード」ユーザー設定と ネットワーク情報を考慮する必要があります。

バッテリーの認識

ユーザーのバッテリーレベルを考慮し、考える前に 動画のプリロードについて 説明しておきます電池残量が 100% のときに 低くなります

プリロードを無効にするか、 デバイスのバッテリーが切れています。

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 Client Hints リクエスト ヘッダーを使用して高速で軽量な通信を実現する 「データ節約」にオプトインしているユーザーにアプリケーションを配布できます。モードを できます。このリクエスト ヘッダーを特定することで、アプリケーションで コストやパフォーマンスの制約があるお客様に 最適化されたユーザーエクスペリエンスを提供できます できます。

詳細については、Delivering Fast and Light Applications with 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 本の動画を含むウェブページがある場合は、おそらく 1 本の動画を取得するのに十分なメモリがあります。 各セグメント ファイルを作成します。ただし、10 個の非表示 <video> は作成しないでください。 要素と 10 個の MediaSource オブジェクトを取得し、そのデータのフィードを開始します。

以下の 2 部構成の例では、最初の複数のセグメントを事前キャッシュに保存する方法を示します。 パワフルで使いやすい 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;
    });

動画を再生

ユーザーが再生ボタンをクリックすると、動画の最初のセグメントが取得されます。 キャッシュ API で使用でき、可能な場合はすぐに再生を開始できます。 それ以外の場合は、ネットワークから取得します。ブラウザでは ユーザーがキャッシュを削除できるようにします。

前述のように、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 で Range レスポンスを作成する

次に、動画ファイル全体を取得して、 どうすればよいでしょうか。ブラウザが HTTP Range リクエストを送信すると、 キャッシュ API では動画全体をレンダラのメモリに取り込めません。 Range レスポンスは現在サポートしています。

これらのリクエストをインターセプトして、カスタマイズされた Range を返す方法を見てみましょう。 レスポンスが返されます。

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 のレスポンス タイムが無視されています。 通信速度が遅くなります

公式のサンプル メディア アプリをご覧ください。特に、 Range の処理方法に関する完全なソリューションについては、ranged-response.js ファイルをご覧ください。 できます。