わかさトラップ回避の為のtsパケットの解説

こんにちは。二次元エンジニアです。

さて、春アニメが軒並み終盤を迎え、新アニメも出そろい、暑い夏を感じる季節となりました。
皆様は来期どんなアニメをご覧になりますでしょうか?
僕は「ソードアートオンラインⅡ」と「アルドノア・ゼロ」と「Free!」と「グラスリップ」と・・・などなど
合計32本ほど視聴予定です。

いつものアニメ一覧

さて、今回はアニメに少し関連したネタです。
さすがに週数十本も見るような方はシステマティックに録画と視聴を運用していると思います。
僕の周りでよくあるパターンは
torne最強杉ワロタ組」「リアルタイムで見ないとかw組」「レコーダーあるよ組」
そして「PT3のようなPC用チューナで録画して保存している組」だと思います。
今回は一番最後の
「PT3のようなPC用チューナで録画して保存している組」の方々向けの記事となっています。

わかさトラップとは?

皆さんはわかさトラップをご存知でしょうか?
わかさトラップとは巷で行われている地デジ録画をしたtsファイルに稀に起こる現象です。
tsファイルをmp4などにエンコードしようとした際に
SD画質からHD画質への映像の切り替わりポイントでエンコードが失敗してしまいます。
主にT○ky○MXさんの深夜のアニメを録画した時に起こるようです。
数年ほど前に「わかさ◯活」のCMでよく起こっていたため、この呼び方になっています。

今回は、このわかさトラップを回避するためのtsパケットの読み方を解説したいと思います。
その前に この記事はあくまでtsパケットを勉強し、
バイナリを読む事に対する敷居を下げる事が目的です。

※地上デジで録画した物を私的利用の範囲を超えて利用する事は犯罪です。
決して行わないでください。不正利用ダメ!絶対ダメ!
次の新しいアニメに出会うため、積極的に円盤を買いましょう!
なお、今回説明する内容は以下に全部書いてあります。
http://www.arib.or.jp/english/html/overview/doc/2-STD-B10v4_7.pdf

必要な物

tsパケットってなに?

スクリーンショット 2014-04-21 1.08.32

地上デジタル放送ではMPEG2-TSという規格でデータが送信されています。
この中には、フルセグのデータ、ワンセグのデータ、番組情報、
データ放送の情報等がまとめて入っています。
これらのデータは全てパケットの中身を示すtsパケットバイトを含む、
188バイトのパケットの連続で構成されています。 従って、実際にデータを読む時にはこのパケットを次々に解釈して
データを取り出すことになります。
今回のわかさトラップ回避では、
このパケットの中から番組情報を示すPMTパケットを読むことにより、
番組情報の切り替わりポイントで頭出しを行うことで
回避をしたいと思います。
まずはこのPMTパケットを探すため、tsヘッダ部分を説明します。

tsヘッダを読む

一つのパケットの先頭に存在するtsパケットは以下のようになっています。

  • sync_byte(同期ワード)
  • transport_error_indicator(トランスポートエラーインジケータ)
  • payload_unit_start_indicator(ペイロードユニットスタートインジケータ)
  • transport_priority(トランスポート優先度)
  • PID(プログラムID)
  • transport_scrambling_control(トランスポートスクランブルコントロール)
  • adaptation_field_control(アダプテーションフィールドコントロール)
  • continuity_control(巡回カウンター)

この中で大事なパラメータは同期ワード、payload_unit_start_indicator、PIDの部分です。
tsパケットの同期ワードは必ず「0x47」になります。
188バイトのパケットサイズでは入りきらない場合、複数パケットに連続してデータが入ります。
後続パケットはスタートパケットと同じPIDを持っていますので、
後続なのかそうでないのかを確認しなければ上手く動きません。
これを簡単に見分けるのが、payload_unit_start_indicatorで、
後続パケットの場合「0」となります。
最後にPIDはパケットを識別するIDになります。
このPIDを手繰っていくことで、データを読みます。

まずはPATパケットを探します

PATパケットとは、プログラムアソシエーションテーブルの略称で、
各サービスのストリーム情報が入っているPMTパケットのPIDが格納されています。
そもそもtsファイルには複数のストリームが格納できる仕様になっているため、
このPMTパケットにストリームの情報が格納されます。
PATパケットのPIDは「0x00」で予約されています。
従って下図の赤で示している「47 60」(sync_byte=01000111 transport_error_indicator=0 payload_unit_start_indicator=1 transport_priority=0 PID=0000000000000)を探すこととなります。

さて、このPATパケットを読みます。
http://www.arib.or.jp/english/html/overview/doc/2-STD-B10v4_7.pdf
の16ページ目に詳細が載っています。

パケット先頭の8バイトはtsヘッダなので、一旦読み飛ばして「00 00 B0 1D」から読みます。
「00 00 B0 1D」のうち先頭の「0x00」については区切り文字なので無視します。
下図のPAT構造に照らし合わせながら見るのですが、ここで大事なのはセクション長の部分です。
一つのPATの中に可変個のPMTパケットのPIDが格納されるのためです。
このパケットではセクション長は「0xB01D」から先頭4bitを切った「0x001D」となります。
したがって、29バイトとなります。直後のバイトから0xFFまでちょうど29バイトになっています。
さて、実際のPMT情報はこのセクション長データの直後から9バイト
(PAT情報5バイト+番組情報4バイト)先から入っています。
上図のオレンジ部分です。このデータの場合は4つ入っています。
最後の4バイト(上図緑)は誤り検知用のCRCとなっています。
tsパケットは188バイト全てを使わなかった場合はすべて1埋めされるため、
CRCより後は0xFFの連続となっています。

今回のデータでは、 「5C 38 E1 01」「5C 39 E1 02」「5D B8 FF C8」「5D B9 FF C9」の4つのPMT情報があるようです。
PATデータ構造の繰り返し部分の下の方がこれらのPMT情報の構造になっています。
今回はPMTパケットのPIDが必要なので、これらの下部13ビットが必要になります。
「0x1FFF」で&を取って、それぞれマスクをかけます。
今回は「0x0101」「0x0102」「0x1FC8」「0x1FC9」の4つのPIDが抽出できました。

PMTパケットを探します

先ほど見つけたPMTのPIDからPMTパケットを探します。
今回は「0x0101」を探します。
メインストリームとなるPMT情報はほぼ100%で先頭にあるので、
私のプログラムでは決めうちしています。

単純にバイナリエディタで検索できないので、ちょっとプログラムを書きます。

# -*- coding: utf-8 -*-
import struct
import sys
import os.path

argvs = sys.argv
input_file = argvs[1]
f = open(input_file, 'rb')
pmt = ''
while True:
    x = f.read(188)   #188バイトづつ読み込む

    if x == "" :
        break

    pid = "0x%02x%02x" % ( ord(x[1])&0x1F, ord(x[2]) )  #sync_byte、transport_error_indicator、payload_unit_start_indicator、transport_priorityをマスクしてPIDを抽出

    # PAT
    if pid == "0x0000" and pmt == '':
        pmt = "0x%02x%02x" % ( ord(x[19])&0x1F, ord(x[20]) ) #便宜的に一番最初のPMT情報だけ抽出(「5C 38 E1 01」)。

    if pmt != '' and pid==pmt and (ord(x[1])&0x40) == 0x40: #「payload_unit_start_indicator」のチェックをするために、「0x40」でマスク
        print "0x%02x%02x%02x%02x" % (ord(x[0]), ord(x[1]), ord(x[2]), ord(x[3])) #頭4バイトを出力
        exit()
f.close()

今回の場合は「0x47610115」というtsヘッダが見つかりました。これをバイナリエディタで検索すると下図のようなパケットをゲットしました。 PMTパケットは可変部分(赤い部分)が2カ所含まれるため多少面倒になっています。 最初の赤枠の繰り返し部分までの情報は今回特に必要ではないので、 スキップしパケットの先頭から16,17バイト目の番組情報長を取得します。 この値の分だけ次の記述子領域1の繰り返し回数があります。 パケットの赤枠の部分の「0xF00F」が番組情報長です。 上部3bitは「1111」になっているので、「0xF」つまり15が番組情長です。 記述子領域1は1セクション8bitなので15バイトあるようです。 ここから15バイト先は「0x02E121F0」になります。

ちょっと分かりにくいので、形式を変えて説明します。

47 61 01 15 #tsヘッダ
00 #区切り
02 B0 DF 5C 38 DD 00 00 E1 00 F0 0F #固定長部分
09 04 00 05 E0 31 F6 04 00 0E E0 32 C1 01 84 #可変長部分
#一つ目のstream
02 E1 21 F0 06 #固定部分。先頭0x02はstream_typeで動画を意味する。PIDは0x121
52 01 00 C8 01 53 #可変長部分
#二つ目のstream
0F E1 12 F0 03 #固定部分。先頭0x0Fは音声を意味する。PIDは0x112
52 01 10 #可変長部分
#三つ目のstream以降はstream_typeが0x0Dでちょっと良く分かりません。
0D E7 40 F0 0F 52 01 40 FD 0A 00 0C 33 3F 00 03 00 03 FF BF 0D E7 50 F0 0A 52 01 50 FD 05 00 0C 1F FF BF 0D ・・・・ 39 42 00 FF FF FF FF FF FF FF

ここまで来るとほぼわかさトラップは回避できます。
わかさトラップの仕込まれた動画では、
このPMT内の動画streamのPIDが番組の切り替わり時に変化してしまいます。
この動画では、「0x121」であった動画streamのPIDが、本編が始まると「0x111」に変わります。
ffmpegではこの変化に対応できず、「0x121」のstreamを見続けてしまい、エンコードが失敗します。

わかさトラップ回避

わかさトラップが起こる原因は分かりました。これを修正したいとおもいます。
手順は以下の通りです。
①ファイル中のPMT情報を見て、動画streamのPIDが変化している点を見つける。
②変化点のパケット以前のデータを全て捨てて、頭出しを行う。
③保存して、エンコードする。

非常に簡単です。
変化点のパケット以前のデータを全て捨てるとファイルが壊れてしまいそうですが、
読み出し側は頭出しされたファイルに含まれるPATパケットとPMTパケットをみるため、
188バイトの構造が壊れない限り問題ありません。
PATパケットとPMTパケットは複数回出現するため、ほぼ問題ないでしょう。

サンプルプログラムです。
ファイル全体をスキャンすると時間がかかるため、5分の1だけスキャンしています。

# -*- coding: utf-8 -*-
import struct
import sys
import os.path

argvs = sys.argv

input_file = argvs[1]
output_file = argvs[2]

file_size = os.path.getsize(input_file)
limit_size = int(file_size / 5)

f = open(input_file, 'rb')
fw = open(output_file, 'wb')
pmt = '' #PMTパケットのPID
fore_pid = '' #PMTパケット内のstreamのPID
while True:
    x = f.read(188)

    if x == "" :
        break
    if f.tell() >= limit_size:
        break

    pid = "0x%02x%02x" % ( ord(x[1])&0x1F, ord(x[2]) )

    # PAT
    if pid == "0x0000" and pmt == '':
        pmt = "0x%02x%02x" % ( ord(x[19])&0x1F, ord(x[20]) )

    if pmt != '' and pid==pmt and (ord(x[1])&0x40) == 0x40:
        plen = ((ord(x[15])&0xF) * 16 * 16) + ord(x[16]) #情報長を取得
        index = 17 + plen #可変長部分をスキップ
        if ord(x[index]) == 0x02:
            first = "0x%02x%02x" % ( ord(x[index+1]), ord(x[index+2]) ) #最初のpidを取得

            if fore_pid!='' and fore_pid!=first: #PIDが変化した
                fw.write(x)
                while True:
                    xx = f.read(188)
                    if xx == "" :
                        break
                    fw.write(xx)
                fw.close()
                f.close()
                exit() #終了
            fore_pid = first
f.close()
fw.close()

#わかさトラップでないファイルはそのままコピー
import shutil
shutil.copyfile(input_file, output_file)

終わりに

今のところ上のサンプルプログラムで特に問題が出た事はありません。
ひどく遅いプログラムなので、書き換えたいことだけが。。。

参考のPDFを見ると分かるのですが、tsが読めると色々分かるようです。
CMカットとかも出来ると非常にありがたい事になります。
是非みなさんも触ってみてください。

最後に
地上デジで録画した物を私的利用の範囲を超えて利用する事は犯罪です。
決して行わないでください。不正利用ダメ!絶対ダメ!
面白いアニメを作ってくれる制作会社がつぶれないように円盤を買いましょう!
ウィーザーバリスターズのブルーレイ初動610枚の1枚は僕です。

では、この記事でアニメライフが充実した方がいれば幸いです。

小ネタ

今回はバイナリエディタにHexFiendを使っています。
MACだと0xEDってのもありますが、Hexで検索のできるHexFiendの方がいいかんじです。
http://ridiculousfish.com/hexfiend/