videojs-contirb-hls v0.17.x における冗長ストリームの扱い

冗長ストリームに関する実装

HLS には本来、冗長ストリームと呼ばれる仕組みが備わっています。 Apple が提供する HLS のドキュメントによれば次のように説明されています。

プレイリストに代替ストリームが含まれる場合、代替帯域幅または代替デバイスとして動作する以外 に、失敗のフォールバックとしても動作します。iOS 3.1以降、クライアントがストリームのインデッ クスファイルを再ロードできなくなると(404エラーなどを原因として)、クライアントは代替スト リームへの切り替えを試みます。 HTTPライブストリーミングの概要 (TP40008332 0.0.0)

マスタープレイリストの例

例えば、次のマスタープレイリストは帯域幅ごとに mainbackup の計2つずつプレイリストを所有しています。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=220000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=256x144
main/144/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=220000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=256x144
backup/144/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=730000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=512x288
main/288/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=730000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=512x288
backup/288/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1600000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=768x432
main/432/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1600000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=768x432
backup/432/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2760000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=1024x576
main/576/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2760000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=1024x576
backup/576/playlist.m3u8

HLS プレイヤーの実装は、最初に冒頭の main/144/playlist.m3u8 というプレイリストを読み込んで再生しつつ、その後は再生環境にあわせて適切なより上位のプレイリストに切り替えて再生を試みます。この過程の中で、いずれかのプレイリスト(インデックスファイル)が 40x や 50x であれば、他の有効なプレイリストに切り替える処理が発生すべきです。

videojs-contrib-hls v0.17.9 における実装

結論から言うと Video.js v4 対応版の最終である videojs/videojs-contrib-hls v0.17.9 には、冗長ストリームのサポートがありません

つらい。 ||| orz

エラーハンドリングでがんばる

ひとつでも無効なプレイリストに触れると、contirb-hls は容赦なくエラーを投げてくるのでそれを捕捉してアレします。

player.on('error', function() {
  const mediaError = player.error();

  // network error = 2 / server error = 4
  if (mediaError.message.indexOf('HLS playlist request error') === 0) {

    // エラーメッセージを取り除く(タイムアウトをかけないと、他の同期的エラーハンドリングと競合する)
    setTimeout(() => player.error(null), 4);

    const hlsTech        = player.hls;
    const playlistLoader = hlsTech.playlists;
    const errorPlaylist  = hlsTech.selectPlaylist();

    // マスタープレイリストの取得に失敗してたら諦めましょう
    if (!playlistLoader.master) {
      return;
    }

    // 読み込みに失敗したプレイリストをマスタープレイリストから取り除く
    delete playlistLoader.master.playlists[errorPlaylist.uri];
    playlistLoader.master.playlists.forEach(function(list, i) {
      if (list.uri === errorPlaylist.uri) {
        playlistLoader.master.playlists.splice(i, 1);
      }
    });

    // HAVE_MASTER や SWITCHING_MEDIA のままだと代替ストリームに切り替わらないので、仮に HAVE_METADATA としておく
    playlistLoader.state = 'HAVE_METADATA';

    // 先頭のプレイリストでエラーが発生した場合 `media_` が undefined になるが
    // のちの .media() でエラーになるので、仮に errorPlaylist を代入しておく
    if (!playlistLoader.media_) {
      playlistLoader.media_ = errorPlaylist;
    }

    // Playlist を再選択して、あらためて media の初期化を試みる
    playlistLoader.media(hlsTech.selectPlaylist());
  }
});

無理矢理感あふれる上に、console を眺めるとエラーがでます。かなしい。

最新版は対応してるっぽい

そんなわけでモンキーパッチ的にアレしたわけですが、Video.js v5 対応版にシフトした videojs-contrib-hls v1.x の少なくとも最新版では冗長ストリームに対応しているようでした。

videojs-contrib-hls/videojs-hls.js at 25033e312281bfa1eb227d96055975568cb63c57 を眺める該当する実装があります。やってることは、さすがに前述のコードほど乱暴ではないものの、まあまあ似たような処理ですね。

現状は安定性などの観点からVideo.js v5 + contrib-hls 最新版の組み合わせに移行できていませんが、そのうち切り替えて恩恵に預かりたいところ。

よかったですね。