TSパケットからはじめる(7) - EIT

EITでTSファイルの番組情報を取得することができます。descriptor()は前回のSDT同様にタグ記述子となります。

EITデータ構造

はじめにTsEITクラスを作成してしまいます。ちょっと長いですがタグ記述子をたくさんデコードしているだけです。クラスのメソッドとして分けた方が見やすくなるかも知れません。

public class TsEIT
{
    public string program_name { get; set; }
    public string program_detail { get; set; }
    public int video_resolution { get; set; }
    public int AudioType { get; set; }
    public int AudioSamplingRate { get; set; }
    public DateTime start_time { get; set; }
    public TimeSpan duration { get; set; }
    public List<string> genre { get; set; }
    public Dictionary<string, string> extended { get; set; }

    public int transport_stream_id { get; set; }
    public int service_id { get; set; }

    //public bool result { get; set; } = true;
    public int tableId { get; private set; }
    public int eventId { get; private set; }

    public TsEIT(byte[] rows)
    {
        using (MemoryStream ms = new MemoryStream(rows))
        {

            this.tableId = ms.ReadByte();
            int section_length = (ms.ReadByte() & 0x0f) << 8 | ms.ReadByte();

            if (section_length > rows.Length) { return; }
            this.service_id = ms.ReadByte() * 256 + ms.ReadByte();
            ms.Position += 3;
            this.transport_stream_id = ms.ReadByte() << 8 | ms.ReadByte();

            ms.Position += 0x04;
            section_length -= (0x0b + 4); // CRC32分の4バイト

            while (section_length != 0)
            {
                long position = ms.Position;
                this.eventId = ms.ReadByte() * 256 + ms.ReadByte();
                this.start_time = Utils.MJD_decode(ms.ReadByte() << 8 | ms.ReadByte());
                this.start_time = start_time.AddHours(Utils.TimeConvert((byte)ms.ReadByte()));
                this.start_time = start_time.AddMinutes(Utils.TimeConvert((byte)ms.ReadByte()));
                this.start_time = start_time.AddSeconds(Utils.TimeConvert((byte)ms.ReadByte()));
                this.duration = new TimeSpan(Utils.TimeConvert((byte)ms.ReadByte()), Utils.TimeConvert((byte)ms.ReadByte()), Utils.TimeConvert((byte)ms.ReadByte()));

                int descriptors_loop_length = (ms.ReadByte() & 0xf) << 8 | ms.ReadByte();
                if (descriptors_loop_length > rows.Length) { return; }
                B24Decoder b24convert = null;
                List<byte> description_store = new List<byte>();

                section_length -= (int)(ms.Position - position);

                while (descriptors_loop_length != 0)
                {
                    int tag = ms.ReadByte();
                    int tag_length = ms.ReadByte();
                    if (tag_length < 0) return;
                    descriptors_loop_length -= (tag_length + 2); // tagとtag_lengthの2バイト分

                    if (descriptors_loop_length < 0) return;

                    switch (tag)
                    {

                        case 0x4d: // 短形式イベント記述子(タイトルと詳細)
                            string language_code = Encoding.ASCII.GetString(new byte[] { (byte)ms.ReadByte(), (byte)ms.ReadByte(), (byte)ms.ReadByte() });
                            b24convert = new B24Decoder();
                            this.program_name = b24convert.AtriToString(Utils.ReadBytes(ms, ms.ReadByte()));
                            this.program_detail = b24convert.AtriToString(Utils.ReadBytes(ms, ms.ReadByte()));
                            break;

                        case 0x4e: // 拡張形式イベント記述子(Extended関連、出演者とか)
                            ms.Position += 4;
                            b24convert = new B24Decoder();
                            int length_of_items = ms.ReadByte();

                            while (length_of_items != 0)
                            {
                                if (length_of_items < 0) return;

                                int item_description_length = ms.ReadByte();
                                string item_description = b24convert.AtriToString(Utils.ReadBytes(ms, item_description_length));
                                int item_char_length = ms.ReadByte();

                                if (item_char_length == -1) return;
                                if (string.IsNullOrEmpty(item_description))
                                {
                                    int before = description_store.Count;
                                    description_store.AddRange(Utils.ReadBytes(ms, item_char_length));
                                    int after = description_store.Count;
                                }
                                else
                                {
                                    if (this.extended == null) { this.extended = new Dictionary<string, string>(); }

                                    if (description_store.Count != 0)
                                    {

                                        string item_char = b24convert.AtriToString(description_store.ToArray());
                                        this.extended[this.extended.Keys.Last()] = item_char;
                                        description_store.Clear();

                                    }

                                    description_store.AddRange(Utils.ReadBytes(ms, item_char_length));
                                    if (!this.extended.ContainsKey(item_description))
                                    {
                                        this.extended.Add(item_description, string.Empty);
                                    }
                                }

                                length_of_items -= (item_description_length + item_char_length + 2);
                            }

                            int text_length = ms.ReadByte();
                            if (text_length != 0) { ms.Position -= text_length; }
                            break;

                        case 0x50: // コンポーネント記述子(解像度 1080p等)
                            this.video_resolution = (ms.ReadByte() & 0x0f) << 8 | ms.ReadByte();
                            ms.Position += (tag_length - 2);
                            break;

                        case 0x54: // コンテント記述子(ジャンル)
                            for (int i = 0; i < (tag_length / 2); i++)
                            {
                                if (this.genre == null) { this.genre = new List<string>(); }
                                byte b = (byte)ms.ReadByte();
                                int content_nibble_level_1 = (b & 0xf0) >> 4;
                                int content_nibble_level_2 = (b & 0x0f);
                                this.genre.Add(string.Format("{0}|{1}", content_nibble_level_1, content_nibble_level_2));
                                ms.Position++;
                            }
                            //ms.Position += tag_length; 
                            break;


                        case 0xc4: // 音声コンポーネント記述子(ステレオの48khzとか)
                            ms.Position++;
                            this.AudioType = ms.ReadByte() & 0x0f;  // 0x03は 2/0モード(ステレオ)
                            ms.Position += 3;
                            this.AudioSamplingRate = (ms.ReadByte() & 0x0e) >> 1;  // 0x07は48kHz
                            ms.Position += (tag_length - 6);
                            break;

                        default:
                            ms.Position += tag_length;
                            break;
                    }

                    section_length -= (tag_length + 2);
                    if (section_length > rows.Length) return;
                    // Console.WriteLine("TAG:0x{0}, {1}, {2}, {3}/{4}", tag.ToString("X"), section_length, descriptors_loop_length, ms.Position, ms.Length);
                }

                if (description_store.Count != 0)
                {
                    if (string.IsNullOrEmpty(this.extended[this.extended.Keys.Last()]))
                    {
                        this.extended[this.extended.Keys.Last()] = b24convert.AtriToString(description_store.ToArray());
                    }
                    else
                    {
                        string buffer = this.extended[this.extended.Keys.Last()] + b24convert.AtriToString(description_store.ToArray());
                        this.extended[this.extended.Keys.Last()] = buffer;
                    }
                }
            }
        }
    }
}

public static class Utils
{
    public static byte[] ReadBytes(MemoryStream ms, int length)
    {
        List<byte> buffer = new List<byte>();
	for(int i = 0; i < length;i++)
        {
	    buffer.Add((byte)ms.ReadByte());
        }
	return buffer.ToArray();
    }

    public static int TimeConvert(byte b)
    {
        return (b >> 4) * 10 + (b & 0x0f);
    }

    public static DateTime MJD_decode(int mjd)
    {
        int mjd2 = (mjd < 15079) ? mjd + 0x10000 : mjd;

	int year = (int)(((double)mjd2 - 15078.2) / 365.25);
	int year2 = (int)((double)year * 365.25);
	int month = (int)(((double)mjd2 - 14956.1 - (double)year2) / 30.6001);
	int month2 = (int)((double)month * 30.6001);
	int day = mjd2 - 14956 - year2 - month2;

	if ((month == 14) || (month == 15))
	{
		year += 1901;
		month = month - 13;
	}
	else
	{
		year += 1900;
		month = month - 1;
	}

	return new DateTime(year, month, day);
    }
}

タグ記述子を仕様書から確認できれば特に難しい点はないかと思います。

EITの自ストリームの番組情報はPID=0x12、TableID=0x4E、ServiceIDも決め打ちしないといけなくなります。以前に作成したPSICollectorクラスを使用してパケットを収集する場合には、PID、TableID、PATの最初に含まれるプログラム番号を指定します。

PSICollector eit_collector = new PSICollector(0x12, 0x4e, pat.program_pid.Keys.First());

MJD関数に関しては仕様書のままです。

注意点としてはEITを取得する位置です。例えば、10:00:00から始まる番組を録画していた場合にTSファイルは9:59:50くらいから録画しているかも知れません。この場合、先頭から読み出すとEITは10:00:00前の番組情報を取得してしまいます。

興味があれば他のテーブルIDや他のサービスIDを取得する等、次の番組情報等、自ストリーム以外の情報も取得できるので確認してみて下さい。

EIT取得例(念のためにモザイク処理)

TsEITクラスの紹介だけになってしまいました。
これでTSパケット編は終わろうかと思っていたのですがもう少し続きます。


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