hls.js の tsdemuxer.js のコードを読む

個人用メモ。

hls.jsがやっていることをかなり大雑把に書いておくと、JavaScriptでプレイリスト(.m3u8)を取得・パースし、プレイリストに書かれたセグメントファイル(.ts、MPEG2-TS)を取得・パースし、画像と音声のデータ(コーデックは MPEG-4 AVCだったり、MPEG-4 AAC)を取り出して、ごにょごにょした後(ぼかし)にMediaSourceExtensionを使ってvideoタグで再生する、といったかんじ。
JavaScriptで画像・音声データのバイナリをパースしたり操作する処理はヘビィなので、UIスレッドではなく、WebWorkerで別スレッドでおこなうようにしている。
MediaSourceExtensionsが何者か、どういうふうに使うかは、Media Source Extensionsを使ってみた (WebM編) - Qiita が分かりやすい。

今日は、上の「セグメントファイル(.ts、MPEG2-TS)を取得・パースし」のパースの部分を読んでみた。

おもむろにコードを読んでも意味不明なので、デジタル映像の「アーカイブ&デリバリー」に関する技術情報サイト|mpeg.co.jp > VIDEO-ITを取り巻く市場と技術MPEG-2システム - Wikipedia を読んで、簡単にMPEG2-TSがどういったデータ形式になっているか、要素の意味を事前に理解した。(本当はちゃんとした規格のドキュメントを読んだほうがいいんだろうけど、厳密に書かれている分細部に囚われてしまうので、取っ掛かりとしてはこれでいいんじゃないかな...)

セグメントファイルのパースは、 tsdemuxer.js でおこなっている。ひとつの入力(パケットの並び)を複数の出力に分離しているので、demuxer。

セグメントファイルは、188バイト固定長のトランスポートパケットがつながったもの。それぞれのトランスポートパケットに、分割された音声・画像データやプログラム(チャンネル)の情報が含まれている。トランスポートパケットをループで順になめているのが hls.js/tsdemuxer.js at 9ccbc9bc7f629a693fa77863e91d4abd59c1a4da · video-dev/hls.js · GitHub 。次の行で同期ワード( 0x47 )をみて、トランスポートパケットの先頭であるかチェックしている。

トランスポートパケットの識別

PIDは、パケットが扱っているデータが画像か音声かプログラムに関するものか、識別するためのもの( hls.js/tsdemuxer.js at 9ccbc9bc7f629a693fa77863e91d4abd59c1a4da · video-dev/hls.js · GitHub)。

パケットは(おそらく)以下のPAT, PMT, 音声パケット or 画像パケットの順にパースしていく。

PIDが0で固定なのが、PAT。プログラム(チャンネル)の一覧を格納しているもの。hls.jsの場合はひとつで十分なので、parsePAT 関数が返した値を PMT のIDとしている(PMTについては以下で説明)。(hls.js/tsdemuxer.js at 9ccbc9bc7f629a693fa77863e91d4abd59c1a4da · video-dev/hls.js · GitHub)

PMTは、特定のプログラムに含まれる画像や音声それぞれのPIDを格納しているもの。hls.jsでは、それをパースして、画像のPIDは、変数 avcId、音声のPIDは 変数 aacId に代入している。( hls.js/tsdemuxer.js at 9ccbc9bc7f629a693fa77863e91d4abd59c1a4da · video-dev/hls.js · GitHub)

あとは後続のパケットで、PIDが avcId、aacId と一致するか調べて、PES(符号化された画像・音声データを分割してパケット化したやつ)のパースをおこなっている(ぼかし)。

識別に関してはこれくらい。最近、この手のバイナリをパースするJavaScriptのコードを読む機会が多いんですが、大分慣れてきた。次回は、PESのパースまわりを読んでみようと思います。