HLS 再生の改善

まずは、良いニュースです。ブライトコーブでは、ブライトコーブ プレーヤーでの HLS 再生の改善、高速化、安定化に懸命に取り組んでいます!実現にあたっては、推測を捨て、いかなる先入観も持たずに、直面する問題を検証する必要がありました。

課題

Media Source Extensions(MSE)を利用する再生エンジンの重要な役割のひとつは、任意の時点でサーバーからリクエストすべき動画データ(セグメントまたはフラグメントと呼ばれます)を決定することにあります。

ビデオ オンデマンド(非ライブ)HLS ソースの場合、その決断はきわめてシンプルです。全セグメントとその(おおよその)継続時間がわかっているのです。そうした情報があれば、ダウンロードすべきセグメントを選ぶのは簡単です。

ライブ HLS ストリームの場合は、残念ながら、それほどシンプルではありません。セグメントの履歴全体が無いだけでなく、HLS の「PROGRAM-DATETIME」タグ(HLS 仕様に最近追加されたもの)もなく、バリアント再生リストをまたいでセグメントを関連づけられる簡単な方法もありません。プレーヤーに残された唯一の選択肢は、推論的にセグメントをダウンロードし、メディアの内部タイムスタンプを使用することです。

つまり、ライブ再生の問題点は、「未知のもの」が多くあるために、最初に正しいセグメントを選択することが難しくなってしまう時点があるということです。

古いフェッチ アルゴリズム

フェッチ アルゴリズムの誤ったセグメントを選んでしまう傾向に対抗するために、ブライトコーブでは、制御理論からいくつかのコンセプトを借用しました。これまでのフェッチ アルゴリズムは、以下のようなものでした。

  1. 与えられた限られた情報をもとに、最善の推論をする。

  2. 推論が誤っていた場合、リクエストから得られた情報を用いて、より良い推論をする(「エラー」を低下させる)

  3. 繰り返し

ここでは、反復によりアルゴリズムが改善され、最終的に正しいセグメントをダウンロードすることが期待されています。しかし、「エラー」とは何かと考えはじめると、問題が生じます。ブライトコーブのアルゴリズムでは、データを欠いている動画バッファー領域をエラーと定義していたのです。

ここで想定されるのは、セグメント「A」に続いてセグメント「C」をフェッチすると、「B」の大きさのギャップが生まれ、このギャップを埋める必要が生じる、ということです。この場合、アルゴリズムはそのエラーを埋めるために逆戻りし、セグメント「B」を選択してから続きの「D」へ進む必要があります。Ye Olde Fetcher

* 図中語句、上から

  1. アルゴリズムがセグメント B のフェッチを命じる
  2. レンディション スイッチ。アルゴリズムが限られた情報をもとに、セグメント D' をフェッチすると判断する
  3. アルゴリズムが正しいセグメント C' をフェッチするが、オーディオフレームが欠けているため、別のギャップが生じる
  4. アルゴリズムはまだ、ギャップを埋めるための最善の解決策はセグメント C' だと考えている

ありがたいことに、99% の時点では、この方式がきわめてうまく機能します。しかし残念ながら、残りの 1% の時点では、そもそも埋められないギャップを埋めようとして立ち往生してしまいます。こうしたことが起きる原因は通常、再生しているソースの性質にありました。一部の HLS ソースではセグメント分けに問題があり、すべてのバリアントでオーディオと動画が同じ時点でセグメント分けされていません。これがギャップにつながります。一部の HLS ソースでは、フレーム(オーディオまたは動画)が壊れていたり欠けていたりすることがあり、これもギャップにつながります。

原因が何であれ、バッファーにそうした埋められない部分があると、アルゴリズムがそれを埋めようとして立ち往生してしまう状況が生じました。そこでブライトコーブは、フェッチ アルゴリズムが立ち往生するのを防ぐために、多くのアプローチを組み込みました。

  1. ごく小さなギャップはソースに固有のものと捉えて無視する

  2. アルゴリズムが前回の反復時と同じセグメントをフェッチしようとしている場合は、その先にある 1 つまたは複数のセグメントをフェッチさせる

  3. 境界の 90% 以上がバッファーに相当するセグメントをロードしたものと捉え、不要な帯域幅の消費を避ける

こうした各戦略の問題は、ごく限られた状況で正常に機能しないことがあるという点です。ブライトコーブが試みたそれぞれの「解決策」では、そうしたごく限られた状況が数を増していました。多くの場合、フェッチ アルゴリズムに小さな変更を加えるだけでも、それまでは機能していた変則的な状況下で機能しなくなりました。

仕切り直し

これらの問題から、必然的に 1 つの結論が導き出されました — すなわち、アプローチを根本的に変える必要があるという結論です。問題を検証した結果、フェッチ アルゴリズムが機能する仕組みをめぐって多くの推測が存在し、それが状況をさらに難しくしていることがわかりました。

そうした推測の一例が、「フェッチ アルゴリズムは、すでにバッファーされたセグメントのリクエストを常に避けるはずである」というものです。しかし、シークの効果、バッファーのガベージ コレクション(mse がソースの背後で自動的に実行するもの)、そして本来的にギャップを生じるソースを組み合わせると、バッファーの状態を推論するのがきわめて困難になる、という問題があります。結局のところ、ブライトコーブのアルゴリズムは、絶えず変化する mse のバッファーに強く依存していたのです。

新しいフェッチ アルゴリズムでは、前述したものやそのほか多くの推測を排し、状況をできる限りシンプルにしています。たとえば、プレーヤーは、すべてのシークのあとにバッファーをクリーンアップするようになりました。これにより、バッファーの状態をより簡単に推論できるようになり、すでにバッファー内に存在しているセグメントのロードを防ぐことをしなくなります。

前進あるのみ

推測を再検証した結果、時点の 100% で正確に推論することは不可能であるものの、時点の 100% で保守的に推論することは十分に可能であることがわかりました。保守的な推論とは、必要とされるセグメントの位置にあるセグメント、またはその前にあるセグメントを推論することです。保守的な推論を行えば、プレイリストにあるセグメントを単純に前進して通過し、つねに正しいセグメントを見つけることができます。

その点を理解したブライトコーブは、問題の本質を根本的に改善しました。いまでは、最初の推論をしたあとには、つねに隣接する領域をフェッチするようになっています。つまり、バッファーの状態 — ギャップ — は定義上、メディアに本来的に備わるもので、フェッチ アルゴリズムの挙動に起因するものではないとされるため、その詳細はもはや問題ではないということです。

New Segment Fetcher

* 図中語句、上から

  1. アルゴリズムがセグメント B のフェッチを命じる
  2. レンディションスイッチ。アルゴリズムがセグメント B' に関して保守的な推論を行う
  3. 前進:アルゴリズムがセグメント C' のフェッチを命じるが、オーディオ フレームが欠けているため、ギャップが生じる
  4. 前進:ギャップを無視し、アルゴリズムがセグメント D' のフェッチを命じる

唯一残された疑問は、ライブプレイリストの複数バリアント間でセグメントと時点をどのように関連づけるかという点でした。この目的のために、ブライトコーブは「同期ポイント」というコンセプトを導入します。同期ポイントは、セグメントインデックスとディスプレイタイム――player.currentTime() をコールすると得られる時間――の間の既知のマッピングとして定義されます。新しいフェッチアルゴリズムは、3つのモードのみで動作します。

  1. ダウンロードを開始するセグメントを保守的に推論する

  2. 単純にプレイリストを前進する

  3. 同期ポイントの作成を試みる

最後(3 番目)の状態になるのは、フェッチ アルゴリズムが推論のために保存したどの情報も使用できない場合に限られます。これはまれなケースですが、仮に生じた場合には、必ずセグメント — 任意のセグメント — をダウンロードし、メディアの内部タイムスタンプを用いて「同期ポイント」を作成します。そうすると、フェッチアルゴリズムは前進する前に、この同期ポイントを用いて保守的な推論ができるようになります。

これらの変更は、最終的には HLS 再生エクスペリエンスの改善につながります。特に、ライブ再生をより迅速に開始し、より安定して再生できるようになるはずです。これらの変更を自分で試してみたい方は、テストプレーヤーを 5.12.0 - ベータ版にアップデートしてください。

以下に、curl を用いて実行する方法を示しています。

curl -XPATCH --data '{
  "player": {
    "template": {
      "version": "5.12.0-beta"
    }
  }
}' \
--header 'Content-Type: application/json' \
"https://players.api.brightcove.com/v1/accounts/$ACCOUNT/players/$PLAYER/configuration"

皆様のご意見をお待ちしています!