TSパケットからはじめる(1) - TS概要

目的


地上波/BS/CS放送はMPEG2-TS(MPEG2 Transport Stream)形式で送信されてくるのでそれをチューナで基本的にそのまま受信して保存しています。そのため、録画されたTSファイルには動画データ、音声データ、字幕データの他にも様々な情報が含まれた形で保存されています。今回目的としているのは番組情報に関するデータを取得することです。

TSパケット構成


地デジ放送等では複数の動作データ、複数の音声データ、複数の字幕データ等が含まれていたりしますが、これらのデータを管理するための情報も必要になります。データとして送信されてくるのは固定サイズ188バイト単位(TSパケット)で様々なデータが入り混じった状況になります。TSパケットを識別するための情報がTSヘッダ情報に記載されており、その中のPID(パケット識別子)と呼ばれるものです。

PID値でパケットを識別

このTSパケットのヘッダは4バイトで構成されており、先ほど紹介したPIDもヘッダの中に含まれています。さらに拡張ヘッダを持つ場合と持たない場合があり、この拡張ヘッダのことをアダプテーションフィールドと呼びます。拡張ヘッダがあるかどうかは4バイトヘッダの中から判断できます。

TSパケット構成

TSパケットは以下のような定義がされております。"sync byte"から"continuity counter"までが4バイトで、アダプテーションフィールドは可変長、残りはデータ領域です。

TSパケット構造

PIDは3バイト目の下位5ビットと4バイト目で13ビット構成であることが分かります。188バイトを読み込み続けてPIDを取得してみたいと思います。

using System;
using System.Collections.Generic;
using System.IO;

namespace TsSample1
{
    class Program
    {
        static void Main(string[] args)
        {
            SortedDictionary<int, int> packet_list = new SortedDictionary<int, int>();
            using (FileStream fs = new FileStream(args[0], FileMode.Open, FileAccess.Read))
            {
                using (BinaryReader br = new BinaryReader(fs))
                {
                    byte[] pktt;
                    while((pktt = br.ReadBytes(188)).Length != 0)
                    {
                        int PID = (pktt[1] & 0x1f) << 8 | pktt[2];
                        if (packet_list.ContainsKey(PID))
                        {
                            packet_list[PID]++;
                        }
                        else
                        {
                            packet_list.Add(PID, 1);
                        }
                    }
                }
            }
            foreach(var key in packet_list.Keys)
            {
                Console.WriteLine("PID = 0x{0}\t\tCount = {1}", key.ToString("X2"), packet_list[key]);
            }
        }
    }
}

引数にTSファイルを指定すると以下のようになりました。

PID = 0x00              Count = 3085
PID = 0x01              Count = 32
PID = 0x10              Count = 309
PID = 0x11              Count = 154
PID = 0x12              Count = 15049
PID = 0x14              Count = 62
PID = 0x23              Count = 35
PID = 0x24              Count = 308
PID = 0x28              Count = 10
PID = 0x29              Count = 8
PID = 0x100             Count = 2718777
PID = 0x110             Count = 59847
PID = 0x111             Count = 139
PID = 0x130             Count = 244
PID = 0x138             Count = 307
PID = 0x140             Count = 45646
PID = 0x160             Count = 183298
PID = 0x161             Count = 17182
PID = 0x162             Count = 24668
PID = 0x170             Count = 55432
PID = 0x171             Count = 15542
PID = 0x172             Count = 571
PID = 0x1F0             Count = 6158
PID = 0x1FF             Count = 5165
PID = 0x901             Count = 3078

色んなPIDが入り混じっていることが分かりますね。188バイトを受け取ってヘッダをデコードするクラスを生成してみます。

public class TsPacket
{
    public int sync_byte { get; private set; }
    public int transport_error_indicator { get; private set; }
    public int payload_unit_start_indicator { get; private set; }
    public int transport_priority { get; private set; }
    public int PID { get; private set; }
    public int transport_scrambling_control { get; private set; }
    public int adaptation_field_control { get; private set; }
    public int continuity_counter { get; private set; }

    public int adaptation_field_length { get; private set; } = 0;

    public TsPacket(byte[] rows)
    {
        this.sync_byte = rows[0];
        this.transport_error_indicator = (rows[1] & 0x80) >> 7;
        this.payload_unit_start_indicator = (rows[1] & 0x40) >> 6;
        this.transport_priority = (rows[1] & 0x20) >> 5;
        this.PID = (rows[1] & 0x1f) << 8 | rows[2];
        this.transport_scrambling_control = (rows[3] & 0xc0) >> 6;
        this.adaptation_field_control = (rows[3] & 0x30) >> 4;
        this.continuity_counter = (rows[3] & 0x0f);

        if (this.adaptation_field_control == 0x02 || this.adaptation_field_control == 0x03)
        {
            this.adaptation_field_length = rows[4];
        }
    }
}

adaptation field controlが0x02ですが2進数は10となります。TSパケット構造では2進数10の先頭ビットが1の場合はアダプテーションフィールドが存在することを意味します。2ビット目が1の場合はデータ(ペイロード)が存在しています。

パケット種別PESとPSI

動画データ、音声データ、字幕データ等は常時データとして流れており、一般的にストリームと呼ばれます。これらのストリームのデータのことをPES(Packetized Elementary Stream)と呼びます。ストリーム以外のプログラム(番組)情報など、PAT、PMT、EIT等のことをPSI(Program Specific Information)と呼ばれます。

何が言いたいかというとTSパケットにはヘッダ(アダプテーションフィールド含めて)以外にペイロード(要はデータ)が含まれますが、このペイロードの扱い方がPES、PSIで若干異なります

  • PESペイロード: ヘッダ(アダプテーションフィールドを含めて)を除いたデータが全てペイロードとなります。

PESはHeader(アダプテーションフィールドを含めて)以外は全てペイロード
  • PSIペイロード: payload unit start indicatorが0の時はヘッダ(アダプテーションフィールドを含めて)を除いたデータが全てペイロードとなります。payload unit start indicatorが1の時は1バイト目(ポインターフィールドと呼ぶ)が0の時は、2バイト目以降は全てペイロードとなります。X(0以外)の時は2バイト目以降にX分だけ以前のペイロードが含まれており、2バイト目からX分進んだ先に先頭ペイロードが格納されています。

PSIのPayload unit start indicatorとペイロードの関係

コメント : 途中から何言っているか分かりませんね。書いている本人もどんだけ説明下手なんだと思います。ということで上の図を追加しました。

ポインターフィールドの0とそれ以外のペイロード

補足するとTSパケットは188バイト単位で送られて来ますが、1つのパケットで完結しているデータもあれば複数のパケットにまたがっている場合もあります。複数パケットの先頭であることを意味するのがpayload unit start indicatorとなります。このpayload unit start indicatorフラグが有効な時に最初の1バイト目がポインターフィールド値となり、0以外の場合には上記図の下段のような形でペイロードが格納されています。

TSヘッダ補足

長くなりそうですのでヘッダに含まれるcontinuity_counterを最後に紹介しておきます。continuity_counterは4ビットなので0x0~0xFの16個となります。元々通信環境が補償されているわけではないため、パケットロスする可能性があります。それをこのcontinuity_counterで判断しています。

オレンジ色と青色は別のPID

このcontinuity_counterはPID毎にカウントされ、0x00の次は0x01、0x01の次は0x02となり、0x0Fの次は0x00となります。16個のパケットをロスするとパットロスを検知できない問題はありますが、そこまで綺麗にドロップすることはないかと思います。

ポイントをまとめると、

  • TSパケットは188バイトで4バイトのヘッダ

  • 先頭パケットは0x47

  • PID(パケット識別子)でTSパケットを識別

  • 拡張ヘッダ(アダプテーションフィールド)はある時と無い時があり可変長

  • PES、PSI(ポインターフィールド考慮が必要)はペイロードの扱いが異なる

  • continuity_counterでパケットロスを検知

これらの情報から目的のパケットを収集することができるかというと、以下の点が解決していません。

  • TSパケットがPESかPSIかはどのように区別するのか?

  • パケット収集にはどのくらいのサイズまで集めれば良いのか?(次のpayload unit start indicatorフラグが有効なパケットが来るまで?)

この記事が気に入ったらサポートをしてみませんか?