TSパケットからはじめる(8) - TS保存

TSファイルをエンコードしたりすると番組情報のEITは消えてしまいます。別ファイルに情報として保存しておけば良いのですが、使い勝手が良い生TSファイルのまま最小構成で残しておきたいと思います。

  • TSファイル最初のPCRを含むパケット

  • 今まで紹介したPAT/PMT/SDT/EIT

  • NITは検出できれば入れる

  • TSファイル最初のTOT/TDTとその直後のPCRを含むパケット

PCRはProgram Clock Referenceで例えばTSファイル一番最初に検知したPCR値とTSファイルの一番最後に検知したPCRの差を計算するとTSファイルの再生可能な時間を取得できます。TOT/TDTには日時の情報が入っていますが、規定では30秒以下に1回以上は送らないといけないのですが下手した30秒の誤差がでます。PCRは100ms以下の送信が推奨されています。

上記合わせて2、3KB程度のtsファイルとなります。番組情報として開始時間は分かりますが、実際の開始時間は不明なのでPCR、TOD/TDTから開始時間を分かるようにするために入れます。また、NITはなかなか落ちてこないのですが、NIT以外の情報が取得が完了するまでに取得していたら含めることにします。

試しに実際のデータを見てみましょう。
まず、左側の数値がTSパケット数を意味しています。先頭から162個目にPCRパケットがあったということです。その時のPCR値です。また、TDTを見つけた時にはTDTに含まれる日時です。本例ではTDTは5秒毎に来ていることが分かります。

162:PCR = 2000615682463
785:PCR = 2000617477125
1107:TDT = 2021/09/26 22:30:00
1420:PCR = 2000619254165
2056:PCR = 2000621035271
...
47320:PCR = 2000749353555
47534:TDT = 2021/09/26 22:30:05
47960:PCR = 2000751133306
...
95168:PCR = 2000884807109
95207:TDT = 2021/09/26 22:30:10
95790:PCR = 2000886586860
...
142095:PCR = 2001018479556
142590:TDT = 2021/09/26 22:30:15
142716:PCR = 2001020257951

最初をPCRを基準値0としてPCR値を計算したのが→です。

162:PCR = 0 → 0785:PCR = 1,794,662 → 0.061107:TDT = 2021/09/26 22:30:00
1420:PCR = 3,571,702 → 0.132056:PCR = 5,352,808 → 0.20秒
...
47320:PCR = 133,671,092 → 4.9547534:TDT = 2021/09/26 22:30:05
47960:PCR = 135,450,843 → 5.02秒
...
95168:PCR = 269,124,646 → 9.9795207:TDT = 2021/09/26 22:30:10
95790:PCR = 270,904,397 → 10.33秒
...
142095:PCR = 402,797,093 → 14.92142590:TDT = 2021/09/26 22:30:15
142716:PCR = 404,575,488 → 14.98

TDT直後のPCRは開始から0.13秒となりTDTは22:30:00となります。このPCR値をそのまま使用するかどうかは難しいですがそのまま使えば、TSファイルの開始時間は22:30:00 - 0.13秒 = 2021/09/26 22:29:59.87となります。

TDT前後のPCRを足して割るとか、PESの値も使用する等、さらに正確な値を決められるかも知れませんが、そこまでの必要性はないかもです。

TsPacketクラスにPCRを取得する処理を追加

private static int limit_packet = 94000;

public bool HasPCR
{
    get
    {
        if (this.HasAdaptationField)
        {
            return (this.Rows[5] & 0x10) != 0 ? true : false;
        }
        return false;
    }
}
public Int64 PCR
{
    get
    {
        int apadaptation_length = this.Rows[4];
        if (apadaptation_length > 7 && this.HasPCR)
        {
            Int64 PCR_base = (Int64)Rows[6] << 25 | (Int64)Rows[7] << 17 | (Int64)Rows[8] << 9 | (Int64)Rows[9] << 1 | (Int64)Rows[10] >> 7;
            Int64 PCR_ext = ((Int64)Rows[10] & 0x01) << 8 | (Int64)Rows[11];
            return (PCR_base * 300 + PCR_ext);
        }
        else return -1;
    }
}

開始直後のPCR値とTDT(PID=0x14)を取得してその後のPCRも取得する

private byte[] GetTdtPCR()
{
    Int64 Start_PCR = -1;
    Int64 After_PCR = -1;
    int count = 0;
    tsr.TSPosition = 0;

    TsTDT tdt = null;
    List<byte> rows = new List<byte>();
    PSICollector tdt_collector = new PSICollector(0x14);

    TsPacket pktt;
    while ((pktt = tsr.Read()) != null)
    {
        count++;
        if (Start_PCR == -1)
        {
            if (pktt.HasAdaptationField && pktt.HasPCR)
            {
                Start_PCR = pktt.PCR;
                rows.AddRange(pktt.Rows);
            }
        }

        if (tdt_collector.IsFinished && After_PCR == -1)
        {
            if (pktt.HasAdaptationField && pktt.HasPCR)
            {
                After_PCR = pktt.PCR;
                rows.AddRange(pktt.Rows);
                break;
            }
        }

        if (!tdt_collector.IsFinished && tdt_collector.AddPacket(pktt))
        {
            // tdt = new TsTDT(tdt_collector.Payload.ToArray());
            rows.AddRange(pktt.Rows);
        }

        if (count >= limit_packet)
        {
            break;
        }
    }

    return rows.ToArray();
}

ファイルオープン処理時に既に保存した生tsファイルがあればそちらを見にいくようにする。

public string CacheFolder { getset; } = @"C:\Home\tmp";
private string CacheFile { getset; }
private bool IsCacheMode { getset; } = false;

public bool Open(string Filename)
{
    FileInfo fi = new FileInfo(Filename);
    this.CacheFile = string.Format(@"{0}\{1}.ts"this.CacheFolder, Utils.Md5Sum(fi.Name));
    if (File.Exists(this.CacheFile))
    {
        this.tsr = new TsReader(this.CacheFile);
        this.IsCacheMode = true;
    }
    else
    {
        this.tsr = new TsReader(Filename);
    }
    return true;
}

public static class Utils
{
	public static string Md5Sum(string srcStr)
	{
		System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
		byte[] srcBytes = System.Text.Encoding.UTF8.GetBytes(srcStr);
		byte[] destBytes = md5.ComputeHash(srcBytes);
		System.Text.StringBuilder destStrBuilder;
		destStrBuilder = new System.Text.StringBuilder();
		foreach (byte curByte in destBytes)
		{
			destStrBuilder.Append(curByte.ToString("x2"));
		}
		return destStrBuilder.ToString();
	}
}

EIT、SDT等の情報を取得するのとtsファイルのまま保存

public bool GetProgramInfo(bool IsSave)
{
    int count = 0;
    List<byte> rows = new List<byte>();

    if (!this.IsCacheMode)
    {
        tsr.TSPosition = tsr.TSLength / 2;
    }
    TsPacket pktt = null;
    PSICollector pat_collector = new PSICollector(0x00);
    PSICollector pmt_collector = null;
    PSICollector sdt_collector = new PSICollector(0x110x42);
    PSICollector eit_collector = null;
    PSICollector nit_collector = new PSICollector(0x10);

    Dictionary<PSICollector, bool> collects = new Dictionary<PSICollector, bool>();
    collects.Add(sdt_collector, false);
    collects.Add(nit_collector, false);

    while ((pktt = tsr.Read()) != null)
    {
        count++;
        if (!pat_collector.IsFinished && pat_collector.AddPacket(pktt))
        {
            TsPAT pat = new TsPAT(pat_collector.Payload.ToArray());
            pmt_collector = new PSICollector(pat.program_pid[pat.program_pid.Keys.First()]);
            eit_collector = new PSICollector(0x120x4e, pat.program_pid.Keys.First());
            collects.Add(pmt_collector, false);
            collects.Add(eit_collector, false);
        }

        if (pat_collector.IsFinished)
        {
            bool IsFinish = true;
            for (int i = 0; i < collects.Count; i++)
            {
                if (!collects[collects.Keys.ElementAt(i)])
                {
                    if (collects.Keys.ElementAt(i).AddPacket(pktt))
                    {
                        collects[collects.Keys.ElementAt(i)] = true;
                    }
                    else
                    {
                        IsFinish = false;
                    }
                }
            }
            if (IsFinish) { break; }
        }

        if (count >= limit_packet)
        {
            break;
        }
    }

    if (sdt_collector.IsFinished) { this.SDT = new TsSDT(collects.Keys.ElementAt(0).Payload.ToArray()); }
    if (pmt_collector.IsFinished) { this.PMT = new TsPMT(collects.Keys.ElementAt(2).Payload.ToArray()); }
    if (eit_collector.IsFinished) { this.EIT = new TsEIT(collects.Keys.ElementAt(3).Payload.ToArray()); }

    if (this.EIT == null)
    {
        return false;
    }

    byte[] TdtPcrs = this.GetTdtPCR();
    if (TdtPcrs.Length != 0)
    {
        byte[] start_pcr = new byte[188];
        Array.Copy(TdtPcrs, 0, start_pcr, 0188);
        rows.AddRange(start_pcr);
    }
    rows.AddRange(pat_collector.Rows);
    rows.AddRange(pmt_collector.Rows);
    rows.AddRange(sdt_collector.Rows);
    if (nit_collector.IsFinished) { rows.AddRange(nit_collector.Rows); }
    rows.AddRange(eit_collector.Rows);
    if (TdtPcrs.Length != 0)
    {
        byte[] remain_pcr = new byte[188 * 2];
        Array.Copy(TdtPcrs, 188, remain_pcr, 0188 * 2);
        rows.AddRange(remain_pcr);
    }

    if (IsSave && !this.IsCacheMode)
    {
        if (!File.Exists(this.CacheFile))
        {
            using (BinaryWriter bw = new BinaryWriter(new FileStream(tsname, FileMode.Create)))
            {
                bw.Write(rows.ToArray());
            }
        }
    }

    return true;
}

TSファイルのままなので例えばrplsinfoコマンド等を使用して番組情報をちゃんと取得できます。但し、rplsinfoでは先頭からファイルを読んでもらうために-F 0をつける必要はあります。

まだ運用とか決めていませんがtsファイルをエンコードしてts形式として、最後に追加しちゃえば余計なファイル管理もなくなるかな…

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