Program Association Table を解析

Program Association Table (PAT) は Transport Stream のパースについて考えるのにまず最初に注目すべきテーブルで、 program_number とその Program Map Table (PMT) の PID の紐付けを定義している。ここでいう program_number は、テレビ東京1なら"1072"というようなチャンネルごとに一意に決められている番号。Program Association Table は繰り返し構造を1つだけ持つわりと単純なデータ構造であり、定義はISO13818-1の2.4.4.3節表2-25にある (ARIB-STD-B10第2部の付録E表E-1にもある)。


program_association_section() {
table_id 8 uimsbf
section_syntax_indicator 1 bslbf
'0' 1 bslbf
reserved 2 bslbf
section_length 12 uimsbf
transport_stream_id 16 uimsbf
reserved 2 bslbf
version_number 5 uimsbf
current_next_indicator 1 bslbf
section_number 8 uimsbf
last_section_number 8 uimsbf
for (i = 0; i < N; i++) {
program_number 16 uimsbf
reserved 3 bslbf
if (program_number == '0') {
network_PID 13 uimsbf
} else {
program_map_PID 13 uimsbf
}
}
CRC_32 32 rpchof
}

データ構造は全てこのような擬似コードで定義されている。uimsbf とか bslbf とかいうのはビット列表記 (英文文書では Mnemonic と表記されている) で、uimsbf は unsigned integer most significant bit first (符号無し整数、最上位ビットが先頭), bslbf は bit string,left bit first (ビット列、左ビットが先頭) の略とされている。uimsbf が出てきたらビット演算して整数として扱い、bslbf が出てきたらそのままビット列として扱えば良い。


for ループに出てくる N は、section_length からループ前後の固定長部分から引いたものだ。section_length の値は、section_lengthフィールド以降からテーブルが終わるまでのバイト数で、transport_stream_id が2バイト、reserved から current_next_indicator までが1バイト、 section_number と last_section_number がそれぞれ1バイト、 CRC_32 が4バイトなので、可変長部分のバイト長は section_length から9を引いた数になり、これは1ループの大きさ4の倍数になっているはずである。


PATの場合は1ループの大きさが固定 (network_PID も program_map_PID も13ビットだから、1ループは4バイトで固定) なので、パースは比較的容易にできる (以下すべて Python3 での実装)。


# coding: utf-8

def ts(file):
"""TS ファイルを開いて188バイトずつ返すジェネレータ"""
with open(file, 'rb') as ts:
for packet in iter(lambda: ts.read(188), b''):
yield packet

def parse_packet(packet):
"""パケットの PID と payload を返す

payload が PSI データであり、pointer_field が0で、
payload_unit_start_indicator が1であることを前提とした簡易な実装
"""
pid = ((packet[1] & 0x1F) << 8) | packet[2]
payload = packet[5:]
return (pid, payload)

def parse_pat(payload):
"""PAT で定義されている program_number と PID のタプルを返すジェネレータ"""
section_length = ((payload[1] & 0x0F) << 8) | payload[2]
# PAT の for ループは8バイト目から始まり、 section_length から9を引いたバイト数だけ繰り返される
i = 8
pids_length = section_length - 9
while i < pids_length:
program_number = (payload[i] << 8) | payload[i+1]
pid = ((payload[i+2] & 0x1F) << 8) | payload[i+3]
if program_number != 0:
yield (program_number, pid)
i += 4

if __name__ == '__main__':
import sys
for packet in ts(sys.argv[1]):
pid, payload = parse_packet(packet)
if pid == 0x00:
for program_number, pid in parse_pat(payload):
print(program_number, pid)
break

テレビ東京のファイルを引数に渡してこのプログラムを実行すると以下の出力を得られる


0 16
151 257
152 513
153 515
755 1025
753 1027

これで program_number ごとの PID やnetwork_pid が分かったので、それらの PID で定義されている payload を Program Map Table (PMT) としてパースすれば、動画を構成している様々な情報へアクセスできるようになる。Program Map Table は ISO13818-1の2.4.4.8節表2-28で定義されていて、2重ループがあったり記述子を含んでいたりと PAT より複雑な構造になっている。次回はPMTの解析をしてみる。