MPEG-TS多重化(TSMF)の仕組みとbondriver_BDA等における実際のTSMF対応実装

■ はじめに

 ケーブルテレビ放送におけるMPEG TSではTSMFと呼ばれる方法によって、複数のTSが一つのTSにまとめられて送信されている場合がある。今回はTSMFによって多重化されたTSの構造を理解し、bondriver_BDAやいわゆるTSMF.txtにおけるTSMF処理を読み解くことにより、TSMFによって多重化されたTSから一つのTSを取り出す方法を検討する。

■ TSMFによって多重化されたTSの構造

◆ 用語の整理

  • 多重フレームヘッダ

TSMFの1フレーム:大体53パケット(後述)の先頭のパケット。

  • TSID

TSを識別するためのID。放送局、放送ストリームによって一意のIDが付与されているらしい。

  • ONID

オリジナルネットワークID。BSの場合0x0004。

  • 相対TS番号(相対ストリーム番号)

TSMFで多重化する際にTSにつける番号。大体1~15の15個用意されている。

  • スロット

TSMFのフレームの多重フレームヘッダの後に続く部分。1フレームが53パケットとすると52スロットある。

◆ 多重フレームの構造

多重フレームは一番最初の多重フレームヘッダのパケットと、(複数のTS)のパケットいくつかで構成されている。(複数のTS)のパケットいくつかのそれぞれの領域はスロットと呼ばれており、未使用のスロットはnullパケットで埋められている。 スロットの数は多重フレームヘッダで定義されるが、現状52しか選択肢が存在しないため、1つの多重フレームは1パケットのヘッダと52パケットの(複数のTS)パケットで構成されることとなる。

◆ 多重フレームヘッダの構造

https://www.soumu.go.jp/main_content/000324860.pdfの242ページ~250ページや、 https://www.soumu.go.jp/main_content/000329701.pdf などに記述がある。

◆ 実際のTSMFフレームの観察してみる

まず、TSMFで放送されているTSを入手する。dvb5-zap等を用いてnullパケットが含まれる形式で保存すると1個のヘッダと52個のスロットという形が認識しやすい。スクランブルを解除する必要はない。

TSを入手したら、Wiresharkを使用してTSを観察する。Wiresharkを使用すると以下の画像のようにパケットをある程度見やすい形で表示できる。選択されているパケットが多重フレームヘッダである。PIDは0x002fとなっている。この後にTSパケットが52個続き、再び多重フレームヘッダが現れる。

f:id:akkkix:20210321015549p:plain

上の資料をもとにKaitai Structを使用してヘッダの構造を見やすい形で表示したものが以下。

f:id:akkkix:20210324025213p:plain

相対TS1と2が使用されていることや、スロットの割当は相対TS1が最も多く、その次に未使用スロットが多く、そしてTS2が2つのみの割当であることが読み取れる。実際、相対TS1には映像が流れているため一番専有スロットが多いのは自然である。

■ 既存のTSMF対応実装

◆ bondriver_BDA

bondriver_BDAでは

[BonDriver] TSMF処理の追加 · radi-sh/BonDriver_BDA@56f7892 · GitHub

でTSMFの対応実装が行われている。

このコミットのTSMF処理の大まかな構造はTSMF処理が有効であった場合CBonTuner::DecodeProcThreadがTSMF処理をおこなうCTSMFParser::ParseTsBufferを呼び、CTSMFParser::ParseTsBufferが1パケット処理するCTSMFParser::ParseOnePacketを呼び、CTSMFParser::ParseOnePacketが多重フレームヘッダを処理するCTSMFParser::ParseTSMFHeaderを呼ぶという様になっている。

以下で示すソースコードの内、複数行コメントの行は筆者が追加したものである。

以下はParseTsBufferのParseOnePacket呼び出し付近のコード片である。(TSMF.cppの74行~78行)ParseOnePacketにTSパケットを渡し、必要なTSパケットと判断された場合(つまりParseOnePacketがTRUEを返した場合)はバッファにコピーするという処理を行っている。(不要なTSパケットは破棄している)

     if (ParseOnePacket(readBuf + readBufPos, readBufSize - readBufPos)) {
            // 必要なTSMFフレームをテンポラリバッファへ追加
            memcpy(tempBuf + tempBufPos, readBuf + readBufPos, 188);
            tempBufPos += 188;
        }

以下はParseOnePacketの内容である。(TSMF.cppの201行から256行)

BOOL CTSMFParser::ParseOnePacket(const BYTE * buf, size_t len)
{

    /*
   中略
   */

    /*
   設定ファイルから読み込んだTSIDが0xffffである場合、ヘッダを含むすべてのパケットを必要であるとして処理する
   処理しようとしているチャンネルの設定が存在しない場合はTSIDが0xffffとなっている
   */
    if (TSID == 0xffff)
        // TSID指定が0xffffならば全てのスロットを返す
        return TRUE;

    /*
   parseTSMFheaderを呼び出してパケットがヘッダであるかどうか判断する
   パケットがヘッダである場合は現在処理しているスロット番号を保持するslot_counterを0にもどして
   FALSEを返す(呼び出し元でパケットが破棄される
   */
    if (ParseTSMFHeader(buf, len)) {
        // TSMF多重フレームヘッダ
        slot_counter = 0;
        return FALSE;
    }

    /*
   なんらかの原因(例えば↑で処理したパケットがヘッダであるのにも関わらずCRCが合わなかった)で
   slot_counterが0~51の値以外となった場合はパケットを破棄
   この場合は結果的に次多重フレームヘッダを見つけるまではすべてのパケットを破棄することとなる
   */
    if (slot_counter < 0 || slot_counter > 51)
        // TSMF多重フレームの同期がとれていない
        return FALSE;
    /*
   現在処理しているスロット番号を+1
   */
    slot_counter++;

    /*
   設定ファイルで相対TS番号でTSを取り出す設定になっているかどうかを判断し
   設定ファイルで設定されているTSIDを相対TS番号として使用して取り出すか、
   TSIDとして使用しONIDと一緒に使用してTSを取り出すかの処理。
   ts_numberには相対TS番号が入る(1~15) 設定ファイル中で相対TSIDを指定する場合は0~14の値
   */
    int ts_number = 0;
    if (IsRelative) {
        // 相対TS番号を直接指定
        ts_number = (int)TSID + 1;
    }
    else {
        // ONIDとTSIDで指定
        for (int i = 0; i < 15; i++) {
            if (TSMFData.stream_info[i].stream_id == TSID && (ONID == 0xffff || TSMFData.stream_info[i].original_network_id == ONID)) {
                ts_number = i + 1;
                break;
            }
        }
    }
    /*
   ↑で得た相対TS番号を使用してパケットが必要なものか不要なものか判断
   必要ならTRUE
   不要ならFALSE(呼び出し元でパケットが破棄される)が返される
   */
    if (ts_number < 1 || ts_number > 15)
        // 該当する相対TS番号が無い
        return FALSE;

    if (TSMFData.stream_info[ts_number - 1].stream_status == 0)
        // その相対TS番号は未使用
        return FALSE;

    if (TSMFData.relative_stream_number[slot_counter - 1] != ts_number)
        // このスロットは他の相対TS番号用スロットか未割当
        return FALSE;

    return TRUE;
}

以下はParseTSMFHeaderの内容である。(TSMF.cppの129行から199行)

/*
パケットが多重フレームヘッダであった場合はTSMF処理用の変数(TSMFData~)を更新しつつTRUEを返す
パケットが多重フレームヘッダでない場合や破損した多重フレームヘッダであった場合はFALSEを返す
(パケットサイズが合わない、同期バイトが変、固定値が変、CRCが変など)
*/
BOOL CTSMFParser::ParseTSMFHeader(const BYTE * buf, size_t len)
{

    static constexpr WORD FRAME_SYNC_MASK = 0x1fff;
    static constexpr WORD FRAME_SYNC_F = 0x1a86;
    static constexpr WORD FRAME_SYNC_I = ~FRAME_SYNC_F & FRAME_SYNC_MASK;

    // パケットサイズ
    if (len < 188)
        return FALSE;

    // 同期バイト
    BYTE sync_byte = buf[0];
    if (sync_byte != TS_PACKET_SYNC_BYTE)
        return FALSE;

    /*
   多重フレームヘッダのPIDは仕様上は0x0011から0x002fの値を取るが大体0x002fであるため決め打ちとしている
   */
    // 多重フレームPID
    WORD frame_PID = (buf[1] << 8) | buf[2];
    if (frame_PID != 0x002F)
        return FALSE;

    // 固定値
    if ((buf[3] & 0xf0) != 0x10)
        return FALSE;

    // 多重フレーム同期信号
    WORD frame_sync = ((buf[4] << 8) | buf[5]) & FRAME_SYNC_MASK;
    if (frame_sync != FRAME_SYNC_F && frame_sync != FRAME_SYNC_I)
        return FALSE;

    // CRC
    if (crc32(&buf[4], 184) != 0)
        return FALSE;

    // 連続性指標
    TSMFData.continuity_counter = buf[3] & 0x0f;

    // 変更指示
    TSMFData.version_number = (buf[6] & 0xE0) >> 5;

    /*
   スロット配置法の区別は現状0x0しか考えなくて良い
   */
    // スロット配置法の区別
    TSMFData.relative_stream_number_mode = (buf[6] & 0x10) >> 4;
    if (TSMFData.relative_stream_number_mode != 0x0)
        return FALSE;
    /*
   多重フレームの形式は現状0x1しか考えなくて良い
   */
    // 多重フレーム形式
    TSMFData.frame_type = (buf[6] & 0x0f);
    if (TSMFData.frame_type != 0x1)
        return FALSE;

    // 相対ストリーム番号毎の情報
    for (int i = 0; i < 15; i++) {
        // 相対ストリーム番号に対する有効、無効指示
        TSMFData.stream_info[i].stream_status = (buf[7 + (i / 8)] & (0x80 >> (i % 8))) >> (7 - (i % 8));
        // ストリーム識別/相対ストリーム番号対応情報
        TSMFData.stream_info[i].stream_id = (buf[9 + (i * 4)] << 8) | buf[10 + (i * 4)];
        // オリジナルネットワ-ク識別/相対ストリーム番号対応情報
        TSMFData.stream_info[i].original_network_id = (buf[11 + (i * 4)] << 8) | buf[12 + (i * 4)];
        // 受信状態
        TSMFData.stream_info[i].receive_status = (buf[69 + (i / 4)] & (0xc0 >> ((i % 4) * 2))) >> ((3 - (i % 4)) * 2);
    }

    // 緊急警報指示
    TSMFData.emergency_indicator = buf[72] & 0x01;

    // 相対ストリーム番号対スロット対応情報
    for (int i = 0; i < 52; i++) {
        TSMFData.relative_stream_number[i] = (buf[73 + (i / 2)] & (0xf0 >> ((i % 2) * 4))) >> ((1 - (i % 2)) * 4);
    }
    /*
   ここまできたら正常なヘッダであったと判断しTRUEを返す
   */
    return TRUE;
}

◆ TSMF.txt(TVTest用の簡易TSMF対応patch)

TSMF.txtでは存在するTSが2個や3個などの少数であることや映像が流れているTSはいつも単一であることを仮定(?)し、専有するスロットの数が一定数未満であるTSを破棄して一つのTSを取り出している。

専有するスロットの数が一定数以上であるTSが複数個ある場合は出力されるTSに複数のTSが混ざることとなる。(恐らくこの場合はスロットがずれるため、TSMFのTSとしてinvalidなものとなる)

■ TSMF対応実装の検討

既存の放送視聴、録画ソフトウェアや新規にこのようなプログラムを開発する際のTSMF対応実装を検討する。

まず、TSMF処理にスクランブル解除が必要かあるが、不要である。多重フレームヘッダはスクランブルされておらず、スロットに入っているパケットもスクランブルされたまま取り出しても問題は発生しない。

多重フレームにTSが入っているという構図を考えるとデスクランブル前にTSを取り出すという順は自然であるが、逆に、デスクランブル後にTSを取り出すという順序を取ることができるのか気になる。EMMパケットの関係でデスクランブルに問題が発生する気がするが、筆者はスクランブル処理に対しての知識を持ち合わせていないのでコメントなどで指摘していただけると有り難い。

また、TSMF処理の前にNULLパケットを落としてはいけない。スロットがずれて処理が困難になる。

次に、TSMF処理の内容であるが、基本的な方針はbondriver_BDAと変わらない以下のような方針で実装を行えば良い。

  • 多重フレームヘッダの情報を保持する変数とスロットを数える変数を用意する(bondriver_BDAでのTSMFData,slot_counter)
  • 多重フレームの同期を取るためにまずはパケットを破棄しつつ多重フレームヘッダを探す。
  • 何らかの原因で同期が外れた場合もパケットを破棄しつつ多重フレームヘッダを探す。
  • 多重フレームヘッダを見つけた場合(ヘッダのPIDは0x002fの決め打ちとする)は情報を保持する変数を書き換えてスロットを数える変数をリセットする。(ここから同期が取れた状態となる)
  • 同期が取れたら多重フレームヘッダの情報をもとに必要なスロットに入っているパケットを出力、多重フレームヘッダと不要なパケットは破棄

必要なTSをどのように決めるかは、以下の3つの案が考えられる。

  • 案1:ONID,TSIDを指定して一つのTSを取り出す

この方法はbondriver_BDAで採用されている方法の一つで一番確実な方法に思える。しかし、ONIDやTSIDを指定するとなるとユーザーがONIDやTSIDを自分で調査して設定しないといけないという点や、ONIDやTSIDを設定する入力を用意しないとならないという実装の面倒さがある。

  • 案2:相対TS番号を指定して一つのTSを取り出す

この方法はbondriver_BDAで採用されているもう一つの方法である。相対TS番号は大体1~15の15個までしかないので相対TS番号がわからない場合でも総当りで一つのTSを取り出すことができる。相対TS番号を設定する入力を実装しないとならないという実装の面倒さは案1とあまり変わらない。しかし、この場合はヘッダのTSIDやONIDなどを取得せずとも分離処理が行える。

  • 案3:スロットを専有している数が最も多いTSを取り出す

この方法はTSMF.txtが採用している方法に近いが、常に単一のTSを取り出すことができる。簡易的な方法であるが、映像が入っているTSが一つであることを仮定すると、必然的に映像が入っているTSが一番多くのスロットを専有すると考えられるため多くの場合でうまく1つのTSを取り出すことができると考えられる。案1、案2と異なり、ユーザーが設定を行う必要がないのが利点である。

最後に、TSMFを分離するための最小限の実装を考えたとき必要となる多重フレームヘッダのフィールドを以下に示す。

  • ( PID )

多重フレームヘッダの認識に必要。0x002Fであるか確認

  • TSIDと相対TSの対応

相対TSを指定して分離する場合には不要

  • スロットと相対TSの対応

■ おわり

次回はオープンソースの録画ソフトウェアであるMirakurunを例に実際にTSMFの処理の実装、検証を行い、TSMF対応の実装を引き続き検討する。

間違い、意見等あればぜひコメントやTwitter等で教えていただけると嬉しいです。