HLS低遅延ライブ配信(LL-HLS)の原理と実践導入

カテゴリ:テクノロジーと発展


最近、友達のために小規模なライブ配信をやろうと思い、仲間を集めて試合を見ることにした。最初はRTMP+Flashの構成を使っていたが、今やブラウザはFlashを廃止してしまい、プラグインのインストールが必要で面倒だった。その後HLSに切り替えたが、互換性は良いものの遅延が十数秒に跳ね上がり、グループのメッセージと映像が半ゴール分ずれてしまい、体験は最悪だった。そこでHLSの遅延を削減する方法を調べ始め、LL-HLS(低遅延HLS)に行き着いた。

正直、LL-HLSが出たときはあまり気にしていなかった。Appleがまた絵に描いた餅を言っていると思ったからだ。しかし実際に試してみると、確かに遅延は3〜5秒に抑えられ、以前のRTMPと同程度で、しかも完全にHTTPプロトコルベースであり、変なポートを使う必要もなく、CDNとの連携も容易だった。ここでは、私が試行錯誤して得た理解とハマったポイントを紹介する。

LL-HLSの核となる考え方:セグメントをさらに細分化する

従来のHLSの遅延が大きい理由は、データがtsやm4sのセグメントで構成されており、1セグメントが通常6秒で、プレーヤーのバッファも加わり、遅延が簡単に10〜15秒になることだ。LL-HLSでは、各セグメントをさらに小さなPartial Segment(パーシャルセグメント)に分割する。例えば0.5秒単位だ。m3u8に#EXT-X-PARTタグが追加されてこれらのパーシャルセグメントが示され、ヘッダが事前にレンダリングされることで、プレーヤーはセグメントが完全に生成される前に小さなブロックのダウンロードを開始できる。

もう1つの重要な要素はPreload Hint(プリロードヒント)だ。サーバーはm3u8に特別な#EXT-X-PRELOAD-HINTを追加し、「まだ終わっていないデータをすぐに提供できる」とプレーヤーに伝える。プレーヤーはこれを受けてHTTPロングポーリングやチャンク転送を使ってそのデータを取得し続け、サーバーから新しいチャンクが到着するとすぐに更新を取得できる。これにより従来の待機時間がミリ秒単位に圧縮される。

さらにAppleはHTTP/2プッシュや多重化も推奨しているが、私の実際のテストでは、パーシャルセグメントが十分に小さければHTTP/1.1のロングポーリングでも動作する。ただし接続数は増える。

実践導入:サーバーからプレーヤーまでハマりどころだらけ

原理はそれほど複雑に見えないが、実際に構築する際には多くの考慮点がある。

サーバー: よく使われるNginxの標準nginx-rtmp-moduleはHLSのセグメント生成はできるが、LL-HLSのサポートは限定的だ(古いバージョンは基本的にサポートしていない)。私はその後、Nginx公式のHLSモジュールにパッチを当てて使うか、またはlal(優秀な開発者によるGo製ストリーミングサーバー)のようなオープンソースのソリューションを直接使った。lalはネイティブでLL-HLSをサポートしている。またffmpegに-hls_init_time 0.5-hls_playlist_type eventを付けてプッシュすることもできるが、ffmpegのLL-HLS機能はまだ発展途上であり、パーシャルセグメントの時間が合わないことがある。

パーシャルセグメントサイズと遅延のバランス: Partial Segmentの時間を短くしすぎると(例えば0.2秒)、サーバーの計算負荷とネットワークオーバーヘッドが大幅に増加し、遅延は減るがカクつきが増える。私は0.5秒がバランス点であることを試した。プリロードヒントと組み合わせることで、全体の遅延を3〜4秒に抑えられ、一般ユーザーはほとんど遅延を感じない。このパラメータはサーバー側のセグメントエンジンで適切に設定する必要がある。例えばlalではfragment_duration = 1000(ミリ秒)に設定し、part_duration = 500とする。

クライアントサポート: これが最も厄介だった。SafariとiOSのHLSはLL-HLSをネイティブでサポートしており、そのまま使える。しかし他のブラウザではサードパーティのプレーヤーに頼る必要がある。hls.jsはバージョン1.0からLL-HLSをサポートしたが、バージョンアップの過程でしばしばバグがあり、例えば#EXT-X-PRELOAD-HINTを解析しないことがあった。テスト中はよくM3U8Player(https://m3u8player.link/)というオンラインプレーヤーを使い、LL-HLSのm3u8リンクを入力して遅延の状況を確認していた。友人に検証してもらうのにも便利で、ちょっとした手抜きツールだった。

時刻同期の落とし穴: LL-HLSではサーバーとクライアントのクロック同期が非常に重要である。プリロードヒントにはシーケンス番号(SN)とPART情報が含まれており、時刻のずれが大きいと、プレーヤーがどのブロックが最新かを誤判定し、重複リクエストや永遠に追いつかない状態になる。私の環境ではサーバー時間をNTPで固定し、クライアントは基本的にブラウザなので自然に同期の問題は少なかった。

キャッシュの落とし穴: CDNのキャッシュをオフにしないと、パーシャルセグメントがキャッシュされ、プレーヤーは常に最も古いブロックを取得することになり、遅延が再び十数秒に戻ってしまう。そのため、私はm3u8のファイル名に自動でランダムパラメータを追加するAPIを用意し、CDNでm3u8と最新数秒のts/m4sファイルをキャッシュしないように設定した。この点は見落とされがちである。

まとめ(特にまとめることもないが)

一周回って、LL-HLSは確かにHLSの遅延を実用的なレベルまで削減できるが、通常のHLSよりも導入コストは高くなる。とはいえ、互換性は依然として良好であり、現在では主要なクラウドサービスプロバイダーもサポートを始めている。もしライブ配信をやっていて、複雑なシグナリングが必要なWebRTCを使いたくないなら、LL-HLSは検討に値する方向性だ。次はサーバー上で自動化スクリプトを作成し、ワンクリックでLL-HLSストリームを生成できるようにしようと考えている。そうすれば、今後友人と試合を見るときに遅延の不満を聞かなくて済む。