commit 787949aea0ebcfe37db694f0f381c01028c7ce86 Author: feyris-tan Date: Thu May 7 16:13:15 2026 +0200 Import initial code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/pcap2mpe.sln b/pcap2mpe.sln new file mode 100644 index 0000000..69b3790 --- /dev/null +++ b/pcap2mpe.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pcap2mpe", "pcap2mpe\pcap2mpe.csproj", "{60779B2D-8351-4588-A6EA-6767677244C2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {60779B2D-8351-4588-A6EA-6767677244C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60779B2D-8351-4588-A6EA-6767677244C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60779B2D-8351-4588-A6EA-6767677244C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60779B2D-8351-4588-A6EA-6767677244C2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/pcap2mpe/DateTimeExtensions.cs b/pcap2mpe/DateTimeExtensions.cs new file mode 100644 index 0000000..a95fedb --- /dev/null +++ b/pcap2mpe/DateTimeExtensions.cs @@ -0,0 +1,12 @@ +namespace pcap2mpe; + +public static class DateTimeExtensions +{ + public static long ToUnixTime(this DateTime dt) + { + double totalSeconds = dt.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; + long tv = (long)totalSeconds; + return tv; + } + +} \ No newline at end of file diff --git a/pcap2mpe/DismantledPacket.cs b/pcap2mpe/DismantledPacket.cs new file mode 100644 index 0000000..3c75486 --- /dev/null +++ b/pcap2mpe/DismantledPacket.cs @@ -0,0 +1,98 @@ +using System.Net.NetworkInformation; +using SharpPcap; + +namespace pcap2mpe; + +public class DismantledPacket +{ + public static DismantledPacket Dismantle(byte[] packetCapture) + { + MemoryStream ms = new MemoryStream(packetCapture, false); + DismantledPacket result = new DismantledPacket(); + result.Destination = ms.ReadMacAddress(); + result.Source = ms.ReadMacAddress(); + ushort union = ms.ReadUInt16BigEndian(); + if (union <= 1500) + { + result.LlcSnap = true; + result.Payload = ms.ReadByteArray(union); + } + else + { + ushort ethertype = union; + bool moreToRead = true; + while (moreToRead) + { + moreToRead = false; + switch (ethertype) + { + case 0x0800: //IPv4 + result.Payload = ms.ReadRemainderAsBytes(); + break; + case 0x0806: //ARP + //MPE can only handle IP and LLC/SNAP, so unfortunately, we've got to discard these. + result.Discard = true; + break; + case 0x8100: //VLAN + if (result.VlanId != 0) + throw new NotImplementedException("nested VLAN"); + result.VlanId = ms.ReadUInt16BigEndian() & 0x0fff; + ethertype = ms.ReadUInt16BigEndian(); + if (ethertype <= 1500) + { + result.LlcSnap = true; + result.Payload = ms.ReadByteArray(ethertype); + moreToRead = false; + } + else + { + moreToRead = true; + } + continue; + default: + long llcSize = ms.GetAvailableBytes(); + llcSize += 8; + if (llcSize >= 1500) + { + //Drop oversized frames + continue; + } + result.LlcSnap = true; + result.Payload = BuildLlcSnapFrame(ms.ReadRemainderAsBytes(), ethertype); + break; + } + } + } + + return result; + } + + private static byte[] BuildLlcSnapFrame(byte[] packetCapture, ushort ethertype) + { + byte[] etherTypeBytes = BitConverter.GetBytes(ethertype); + + byte[] buffer = new byte[packetCapture.Length + 8]; + buffer[0] = 0xaa; + buffer[1] = 0xaa; + buffer[2] = 0x03; + buffer[3] = 0x00; + buffer[4] = 0x00; + buffer[5] = 0x00; + buffer[6] = etherTypeBytes[1]; + buffer[7] = etherTypeBytes[0]; + Array.Copy(packetCapture,0,buffer,8,packetCapture.Length); + return buffer; + } + + public int VlanId { get; set; } + + public bool Discard { get; set; } + + public byte[] Payload { get; set; } + + public bool LlcSnap { get; set; } + + public PhysicalAddress Source { get; set; } + + public PhysicalAddress Destination { get; set; } +} \ No newline at end of file diff --git a/pcap2mpe/DvbCrc32.cs b/pcap2mpe/DvbCrc32.cs new file mode 100644 index 0000000..d3508c5 --- /dev/null +++ b/pcap2mpe/DvbCrc32.cs @@ -0,0 +1,141 @@ +namespace pcap2mpe; + +public class DvbCrc32 +{ + private DvbCrc32() + { + } + + private static readonly uint[] table = + { + 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, + 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, + 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, + 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, + 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, + 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, + 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, + 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, + 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, + 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, + 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, + 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, + 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, + 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, + 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, + 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, + 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, + 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, + 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, + 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, + 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, + 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, + 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, + 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, + 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, + 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, + 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, + 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, + 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, + 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, + 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, + 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, + 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, + 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, + 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, + 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, + 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, + 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, + 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, + 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, + 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, + 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, + 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, + 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, + 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, + 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, + 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, + 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, + 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, + 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, + 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, + 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, + 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, + 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, + 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, + 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, + 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, + 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, + 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, + 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, + 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, + 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, + 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, + 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 + }; + + + public static bool ValidateCrc(MemoryStream ms, int offset, int end) + { + long restorePosition = ms.Position; + + uint crc = 0xffffffff; + while (offset < end) + { + ms.Position = offset; + uint b = (crc >> 24) & 0xff; + int c = (ms.ReadUInt8()) & 0xff; + crc = (crc << 8) ^ table[b ^ c]; + offset++; + } + + ms.Position = restorePosition; + return crc == 0; + } + + public static bool ValidateCrc(ReadOnlySpan data) + { + uint crc = 0xffffffff; + + for (int i = 0; i < data.Length; i++) + { + uint b = (crc >> 24) & 0xff; + int c = (data[i]) & 0xff; + crc = (crc << 8) ^ table[b ^ c]; + } + + return crc == 0; + } + + public static uint CreateCrc(ReadOnlySpan data) + { + uint crc = 0xffffffff; + + for (int i = 0; i < data.Length; i++) + { + uint b = (crc >> 24) & 0xff; + int c = (data[i]) & 0xff; + crc = (crc << 8) ^ table[b ^ c]; + } + + return crc; + } + + public static uint CreateCrc(MemoryStream ms, int offset, int end) + { + long restorePosition = ms.Position; + + uint crc = 0xffffffff; + while (offset < end) + { + ms.Position = offset; + uint b = (crc >> 24) & 0xff; + int c = (ms.ReadUInt8()) & 0xff; + crc = (crc << 8) ^ table[b ^ c]; + offset++; + } + + ms.Position = restorePosition; + return crc; + } +} \ No newline at end of file diff --git a/pcap2mpe/HOW_TO_USE.md b/pcap2mpe/HOW_TO_USE.md new file mode 100644 index 0000000..16d20db --- /dev/null +++ b/pcap2mpe/HOW_TO_USE.md @@ -0,0 +1,54 @@ +# Running single nodedly + +tsp -v --bitrate 133333 \ + -I null \ + -P regulate --packet-burst 14 \ + -P filter --every 133 --set-label 1 \ + -P craft --only-label 1 --pid 0x0098 --no-payload --pcr 0 \ + -P continuity --pid 0x0098 --fix \ + -P pcradjust --pid 0x0098 \ + -P merge --transparent "tsp -I ip -l 127.0.0.2 6969" \ + -P history \ + -O file --max-size 100000000 sampleC.ts + +# Building for Alpine +dotnet publish -c Release --self-contained -r linux-musl-x64 + + +# Building for Alpine on Banana Pi +dotnet publish -c Release --self-contained -r linux-musl-arm64 + +# TDT example + + + + + + + +# Multis +tsp -v --bitrate 230400 \ + -I null \ + -P regulate --packet-burst 14 \ + -P filter --every 230 --set-label 1 \ + -P craft --only-label 1 --pid 0x0098 --no-payload --pcr 0 \ + -P continuity --pid 0x0098 --fix \ + -P pcradjust --pid 0x0098 \ + -P merge --transparent "tsp -I ip -l 127.0.0.2 6969" \ + -P pmt --pmt-pid 0x0099 --add-stream-identifier \ + -P sdt -c --service-id 1 --name "pc-203" --type 0x0C \ + -P merge "tsp -v -I ip -l 192.168.1.197 6969 -P zap 1 -P svrename --id 2 1 -P remap 0x0098-0x0106=0x0198" \ + -P pmt --pmt-pid 0x0199 --remove-pid 0x0198 --pcr-pid 0x0098 --add-stream-identifier \ + -P sdt -c --service-id 2 --name "fsoca" --type 0x0C \ + -P merge "tsp -v -I ip -l 192.168.1.197 6970 -P zap 1 -P svrename --id 3 1 -P remap 0x0098-0x0106=0x0298" \ + -P pmt --pmt-pid 0x0299 --remove-pid 0x0298 --pcr-pid 0x0098 --add-stream-identifier \ + -P sdt -c --service-id 3 --name "fsaca" --type 0x0C \ + -P merge "tsp -v -I ip -l 192.168.1.197 6971 -P zap 1 -P svrename --id 4 1 -P remap 0x0098-0x0106=0x0398" \ + -P pmt --pmt-pid 0x0399 --remove-pid 0x0398 --pcr-pid 0x0098 --add-stream-identifier \ + -P sdt -c --service-id 4 --name "rwwgw1" --type 0x0C \ + -P inject "" --pid 0x14 --bitrate 2000 --stuffing \ + -P timeref --system-synchronous \ + -P cat -c \ + -P nit -c --build-service-list-descriptors \ + -P history \ + -O file --max-size 100000000 multisampleBigB.ts \ No newline at end of file diff --git a/pcap2mpe/Mpeg2WriterFactory.cs b/pcap2mpe/Mpeg2WriterFactory.cs new file mode 100644 index 0000000..2e3003c --- /dev/null +++ b/pcap2mpe/Mpeg2WriterFactory.cs @@ -0,0 +1,34 @@ +using System.Net; + +namespace pcap2mpe; + +public class Mpeg2WriterFactory +{ + public static Mpeg2Writer CreateWriter() + { + + string[] commandLineArgs = Environment.GetCommandLineArgs(); + if (commandLineArgs.Length == 2 || commandLineArgs.Length == 1 || commandLineArgs.Length == 0) + { + Mpeg2UdpSender udpSender = new Mpeg2UdpSender(new IPEndPoint(IPAddress.Parse("127.0.0.2"), 6969)); + return udpSender; + } + + switch (commandLineArgs[2]) + { + case "file": + string filename = String.Format("cap{0}.ts", DateTime.Now.Ticks); + FileInfo fi = new FileInfo(filename); + Console.WriteLine(fi.FullName); + return new Mpeg2FileWriter(fi); + case "udp": + IPAddress ip = IPAddress.Parse((commandLineArgs[3])); + int port = int.Parse(commandLineArgs[4]); + IPEndPoint endPoint = new IPEndPoint(ip, port); + Mpeg2UdpSender udpSender = new Mpeg2UdpSender(endPoint); + return udpSender; + default: + throw new NotImplementedException(commandLineArgs[2]); + } + } +} \ No newline at end of file diff --git a/pcap2mpe/PacketCounter.cs b/pcap2mpe/PacketCounter.cs new file mode 100644 index 0000000..4ae0e26 --- /dev/null +++ b/pcap2mpe/PacketCounter.cs @@ -0,0 +1,70 @@ +using System.Text; + +namespace pcap2mpe; + +public class PacketCounter +{ + private PacketCounter() { } + + private static PacketCounter _instance; + public static PacketCounter GetInstance() + { + if (_instance == null) + { + _instance = new PacketCounter(); + _instance.ipThisSecond = new int[4096]; + _instance.llcThisSecond = new int[4096]; + + _instance._thread = new Thread(_instance.Run); + _instance._thread.Name = "Packet Counter"; + _instance._thread.Priority = ThreadPriority.Lowest; + _instance._thread.Start(); + } + return _instance; + } + + public void Count(int vlan, bool llc) + { + if (llc) + { + llcThisSecond[vlan]++; + } + else + { + ipThisSecond[vlan]++; + } + } + + private Thread _thread; + private int[] ipThisSecond; + private int[] llcThisSecond; + + private void Run() + { + while (true) + { + Thread.Sleep(1000); + int hits = 0; + StringBuilder sb = new StringBuilder(); + sb.AppendFormat("({0}) ", DateTime.Now.ToLongTimeString()); + for (int i = 0; i < ipThisSecond.Length; i++) + { + if (ipThisSecond[i] > 0) + { + sb.AppendFormat("VLAN {0} IP: {1}, ", i, ipThisSecond[i]); + hits++; + } + + if (llcThisSecond[i] > 0) + { + sb.AppendFormat("VLAN {0} LLC: {1}, ", i, llcThisSecond[i]); + hits++; + } + } + if (hits > 0) + Console.WriteLine(sb); + Array.Clear(ipThisSecond, 0, ipThisSecond.Length); + Array.Clear(llcThisSecond,0, llcThisSecond.Length); + } + } +} \ No newline at end of file diff --git a/pcap2mpe/PacketHandlerQueue.cs b/pcap2mpe/PacketHandlerQueue.cs new file mode 100644 index 0000000..b3305b6 --- /dev/null +++ b/pcap2mpe/PacketHandlerQueue.cs @@ -0,0 +1,114 @@ +using pcap2mpe.Descriptors; +using SharpPcap; + +namespace pcap2mpe; + +public class PacketHandlerQueue +{ + public PacketHandlerQueue() + { + threadStateLocker = new object(); + queue = new Queue(); + knownVlans = new bool[4096]; + } + + private object threadStateLocker; + public void HandlePacket(object sender, PacketCapture e) + { + + lock (queue) + { + queue.Enqueue(e.GetPacket().Data); + } + + if (packetProcessingThread == null) + { + packetProcessingThread = new Thread(RunPacketProcessingThread); + } + + switch (packetProcessingThread.ThreadState) + { + case ThreadState.Unstarted: + packetProcessingThread.Start(); + break; + case ThreadState.Running: + break; + case ThreadState.Stopped: + packetProcessingThread = null; + break; + } + } + + private Queue queue; + private Thread packetProcessingThread; + private Mpeg2Writer _mpeg2Writer; + private TdtPsiGenerator _tdtPsiGenerator; + private PatPsiGenerator _patPsiGenerator; + private PmtPsiGenerator _pmtPsiGenerator; + private bool[] knownVlans; + private PacketCounter _packetCounter; + + private void RunPacketProcessingThread() + { + byte[] packet = null; + while (true) + { + lock (queue) + { + if (queue.Count == 0) + return; + else + packet = queue.Dequeue(); + } + + if (packet == null) + continue; + + DismantledPacket dismantledPacket = DismantledPacket.Dismantle(packet); + if (dismantledPacket == null) + continue; + if (dismantledPacket.Discard) + continue; + if (dismantledPacket.Payload == null) + continue; + if (dismantledPacket.Payload.Length == 0) + continue; + if (dismantledPacket.Payload.Length >= 4091) + { + //Too long + continue; + } + Span buildPsi = MpePsiGenerator.BuildPsi(dismantledPacket); + + if (_packetCounter == null) + _packetCounter = PacketCounter.GetInstance(); + _packetCounter.Count(dismantledPacket.VlanId, dismantledPacket.LlcSnap); + + if (_mpeg2Writer == null) + { + _mpeg2Writer = Mpeg2WriterFactory.CreateWriter(); + _tdtPsiGenerator = new TdtPsiGenerator(_mpeg2Writer); + _tdtPsiGenerator.Run(); + _patPsiGenerator = new PatPsiGenerator(_mpeg2Writer); + _patPsiGenerator.AddProgram(1, 0x0099); + _patPsiGenerator.Run(); + _pmtPsiGenerator = new PmtPsiGenerator(0x0099, 1, 0x0098, _mpeg2Writer); + _pmtPsiGenerator.AddStream(0x82, 0x0098); + _pmtPsiGenerator.Run(); + } + + if (!knownVlans[dismantledPacket.VlanId]) + { + ushort pid = (ushort)(0x0100 + dismantledPacket.VlanId); + PmtPsiGenerator.PmtGeneratorStream stream = _pmtPsiGenerator.AddStream(0x0d, pid); + stream.AddDescriptor(new _0x66_DataBroadcastIdDescriptor(0x0005)); + Console.WriteLine("Add PID {0}", pid); + knownVlans[dismantledPacket.VlanId] = true; + } + + _mpeg2Writer.EmitPsi(0x0100 + dismantledPacket.VlanId, buildPsi); + } + + + } +} \ No newline at end of file diff --git a/pcap2mpe/Program.cs b/pcap2mpe/Program.cs new file mode 100644 index 0000000..9a7cd68 --- /dev/null +++ b/pcap2mpe/Program.cs @@ -0,0 +1,46 @@ +using SharpPcap; + +namespace pcap2mpe; + +class Program +{ + static void Main(string[] args) + { + if (args.Length == 0) + { + Console.WriteLine("specify interface as first argument"); + return; + } + + if (args[0].Equals("lo")) + { + Console.WriteLine("can't use the loopback interface"); + return; + } + + var captureDeviceList = CaptureDeviceList.New(); + captureDeviceList.Refresh(); + + ILiveDevice targetDevice = null; + foreach (ILiveDevice liveDevice in captureDeviceList) + { + if (liveDevice.Name.Equals(args[0])) + { + targetDevice = liveDevice; + break; + } + } + + if (targetDevice == null) + { + Console.WriteLine("No target device found"); + return; + } + + PacketHandlerQueue queue = new PacketHandlerQueue(); + + targetDevice.OnPacketArrival += queue.HandlePacket; + targetDevice.Open(DeviceModes.Promiscuous, 9001); + targetDevice.Capture(); + } +} \ No newline at end of file diff --git a/pcap2mpe/Psi/BasePsiGenerator.cs b/pcap2mpe/Psi/BasePsiGenerator.cs new file mode 100644 index 0000000..09a212b --- /dev/null +++ b/pcap2mpe/Psi/BasePsiGenerator.cs @@ -0,0 +1,38 @@ +namespace pcap2mpe; + +public abstract class BasePsiGenerator +{ + public BasePsiGenerator(int pid, string psiType, int intervalMillis, Mpeg2Writer writer) + { + _pid = pid; + _psiType = psiType; + _intervalMillis = intervalMillis; + _writer = writer; + } + + private int _pid; + private Mpeg2Writer _writer; + private int _intervalMillis; + private string _psiType; + private Thread _thread; + public void Run() + { + _thread = new Thread(RunEx); + _thread.Name = String.Format("{0} PSI Generator", _psiType); + _thread.Start(); + } + + private void RunEx() + { + while (true) + { + Thread.Sleep(_intervalMillis); + byte[] buffer = GeneratePsi(); + _writer.EmitPsi(_pid, buffer); + } + } + + protected abstract byte[] GeneratePsi(); + + protected int versionNumber; +} \ No newline at end of file diff --git a/pcap2mpe/Psi/Descriptors/0x66_DataBroadcastIdDescriptor.cs b/pcap2mpe/Psi/Descriptors/0x66_DataBroadcastIdDescriptor.cs new file mode 100644 index 0000000..a6d73cf --- /dev/null +++ b/pcap2mpe/Psi/Descriptors/0x66_DataBroadcastIdDescriptor.cs @@ -0,0 +1,24 @@ +namespace pcap2mpe.Descriptors; + +public class _0x66_DataBroadcastIdDescriptor : TsDescriptor { + + public _0x66_DataBroadcastIdDescriptor(ushort id) + { + DataBroadcastId = id; + } + + public ushort DataBroadcastId { get; set; } + + public override byte GetDescriptorId() + { + return 0x66; + } + + public override byte[] Serialize() + { + byte[] result = BitConverter.GetBytes(DataBroadcastId); + if (BitConverter.IsLittleEndian) + Array.Reverse(result); + return result; + } +} \ No newline at end of file diff --git a/pcap2mpe/Psi/Descriptors/TsDescriptor.cs b/pcap2mpe/Psi/Descriptors/TsDescriptor.cs new file mode 100644 index 0000000..99075db --- /dev/null +++ b/pcap2mpe/Psi/Descriptors/TsDescriptor.cs @@ -0,0 +1,7 @@ +namespace pcap2mpe.Descriptors; + +public abstract class TsDescriptor +{ + public abstract byte GetDescriptorId(); + public abstract byte[] Serialize(); +} \ No newline at end of file diff --git a/pcap2mpe/Psi/MpePsiGenerator.cs b/pcap2mpe/Psi/MpePsiGenerator.cs new file mode 100644 index 0000000..084182e --- /dev/null +++ b/pcap2mpe/Psi/MpePsiGenerator.cs @@ -0,0 +1,68 @@ +using System.Net.NetworkInformation; + +namespace pcap2mpe; + +public class MpePsiGenerator +{ + public static Span BuildPsi(DismantledPacket packet) + { + byte[] macAddress = packet.Destination.GetAddressBytes(); + + byte[] outBuffer = new byte[4086]; + MemoryStream outStream = new MemoryStream(outBuffer); + outStream.WriteByte(0x3e); + + ushort sectionPrivateReservedLength = 0x0000; + sectionPrivateReservedLength |= 0x8000; //Section Syntax indicator ON + sectionPrivateReservedLength |= 0x4000; //Private Indicator ON + sectionPrivateReservedLength |= 0x3000; //Reserved shall be set to on. + sectionPrivateReservedLength |= GetLength(packet); //G + outStream.WriteUInt16(sectionPrivateReservedLength); + outStream.WriteByte(macAddress[5]); + outStream.WriteByte(macAddress[4]); + + byte reservedPayloadAddressLlcCurrent = 0x00; + reservedPayloadAddressLlcCurrent |= 0xc0; //reserved + //reservedPayloadAddressLlcCurrent |= 0x30; //payload scrambling + //reservedPayloadAddressLlcCurrent |= 0x0c; //address scrambling + if (packet.LlcSnap) + reservedPayloadAddressLlcCurrent |= 0x02; //LLC_SNAP + reservedPayloadAddressLlcCurrent |= 0x01; //current_next + outStream.WriteByte(reservedPayloadAddressLlcCurrent); + outStream.WriteByte(0); //section_number + outStream.WriteByte(0); //last_section_number + outStream.WriteByte(macAddress[3]); + outStream.WriteByte(macAddress[2]); + outStream.WriteByte(macAddress[1]); + outStream.WriteByte(macAddress[0]); + outStream.WriteByteArray(packet.Payload); + outStream.Flush(); + + uint crc32 = DvbCrc32.CreateCrc(outStream, 0, (int)outStream.Position); + outStream.WriteUInt32(crc32); + outStream.Flush(); + + bool valid = DvbCrc32.ValidateCrc(outStream, 0, (int)outStream.Position); + if (!valid) + throw new Exception("Invalid crc check"); + + return new Span(outBuffer, 0, (int)outStream.Position); + } + + private static ushort GetLength(DismantledPacket packet) + { + ushort result = 0; + result++; //MAC_address_6 + result++; //MAC_address_5 + result++; //reserved, payload_scrambling_control, address_scrambling_control, LLC_SNAP_flag, current_next_indicator + result++; //section_number; + result++; //last_section_number; + result++; //MAC_address_4 + result++; //MAC_address_3 + result++; //MAC_address_2 + result++; //MAC_address_1 + result += (ushort)packet.Payload.Length; //LLC_SNAP / IP_datagram_data_byte + result += 4; //CRC_32 + return result; + } +} \ No newline at end of file diff --git a/pcap2mpe/Psi/PatPsiGenerator.cs b/pcap2mpe/Psi/PatPsiGenerator.cs new file mode 100644 index 0000000..6541c5a --- /dev/null +++ b/pcap2mpe/Psi/PatPsiGenerator.cs @@ -0,0 +1,67 @@ +namespace pcap2mpe; + +public class PatPsiGenerator : BasePsiGenerator +{ + public PatPsiGenerator(Mpeg2Writer writer,ushort transportStreamId = 1) + : base(0x0000, "PAT", 500, writer) + { + TransportStreamId = transportStreamId; + content = new Dictionary(); + } + + + + private Dictionary content; + + public void AddProgram(ushort programId, ushort pmtPid) + { + content.Add(programId, pmtPid); + versionNumber++; + } + + public ushort TransportStreamId { get; } + + protected override byte[] GeneratePsi() + { + ushort sectionLength = (ushort)((content.Count * 4) + 5 + 4); + sectionLength &= 0x03ff; + if (versionNumber > 32) + versionNumber %= 32; + + MemoryStream ms = new MemoryStream(); + ms.WriteUInt8(0); //Table ID + + byte byte2 = 0; + byte2 |= 0x80; //section syntax indicator + //byte2 &= 0x40; //'0' + byte2 |= 0x30; //reserved + byte2 += (byte)((sectionLength & 0x0300) >> 8); + ms.WriteUInt8(byte2); //section length, part 1 + + byte byte3 = (byte)(sectionLength & 0x00ff); + ms.WriteUInt8(byte3); //section length, part 2 + + ms.WriteUInt16(TransportStreamId); //transport stream id + + + byte byte4 = 0; + byte4 |= 0xc0; //reserved + byte4 |= (byte)(versionNumber << 1); //version number + byte4 |= 0x01; //current_next_indicator + ms.WriteUInt8(byte4); + + ms.WriteUInt8(0); //section number + ms.WriteUInt8(0); //last section number + foreach (var (program_number, program_map_pid) in content) + { + ms.WriteUInt16(program_number); + ms.WriteUInt16((ushort)(program_map_pid & 0x1fff)); + } + + uint crc = DvbCrc32.CreateCrc(ms, 0, (int)ms.Position); + ms.WriteUInt32(crc); + + return ms.ToArray(); + } + +} \ No newline at end of file diff --git a/pcap2mpe/Psi/PmtPsiGenerator.cs b/pcap2mpe/Psi/PmtPsiGenerator.cs new file mode 100644 index 0000000..e9c51e8 --- /dev/null +++ b/pcap2mpe/Psi/PmtPsiGenerator.cs @@ -0,0 +1,142 @@ +using pcap2mpe.Descriptors; + +namespace pcap2mpe; + +public class PmtPsiGenerator : BasePsiGenerator +{ + public ushort ProgramNumber { get; } + public ushort PcrPid { get; } + + private List descriptors; + private List streams; + + public PmtPsiGenerator(int pid, ushort programNumber, ushort PcrPid, Mpeg2Writer writer) + : base(pid, "PMT", 500, writer) + { + ProgramNumber = programNumber; + this.PcrPid = PcrPid; + descriptors = new List(); + streams = new List(); + } + + public class PmtGeneratorStream + { + public PmtGeneratorStream(byte type, ushort pid) + { + descriptors = new List(); + this.StreamType = type; + this.ElementaryPid = pid; + } + + private List descriptors; + public byte StreamType { get; } + public ushort ElementaryPid { get; } + + public byte[] SerializeDescriptors() + { + MemoryStream ms = new MemoryStream(); + foreach (TsDescriptor descriptor in descriptors) + { + ms.WriteUInt8(descriptor.GetDescriptorId()); //descriptor_tag + + byte[] descriptorBytes = descriptor.Serialize(); + ms.WriteUInt8((byte)descriptorBytes.Length); //descriptor_length + ms.Write(descriptorBytes, 0, descriptorBytes.Length); + } + return ms.ToArray(); + } + + public void AddDescriptor(TsDescriptor privateDataSpecifierDescriptor) + { + descriptors.Add(privateDataSpecifierDescriptor); + } + } + + private byte[] SerializeStreams() + { + MemoryStream ms = new MemoryStream(); + foreach (PmtGeneratorStream stream in streams) + { + ms.WriteUInt8(stream.StreamType); + ms.WriteUInt16((ushort)(stream.ElementaryPid | 0xe000)); + + byte[] descriptorLoop = stream.SerializeDescriptors(); + ms.WriteUInt16((ushort)(descriptorLoop.Length | 0xf000)); //ES_Info_length + ms.Write(descriptorLoop, 0, descriptorLoop.Length); + } + + return ms.ToArray(); + } + + private byte[] SerializeDescriptors() + { + MemoryStream ms = new MemoryStream(); + foreach (TsDescriptor descriptor in descriptors) + { + ms.WriteUInt8(descriptor.GetDescriptorId()); //descriptor_tag + + byte[] descriptorBytes = descriptor.Serialize(); + ms.WriteUInt8((byte)descriptorBytes.Length); //descriptor_length + ms.Write(descriptorBytes, 0, descriptorBytes.Length); + } + return ms.ToArray(); + } + + + + protected override byte[] GeneratePsi() + { + byte[] descriptorBuffer = SerializeDescriptors(); + int programInfoLength = descriptorBuffer.Length; + byte[] streamBuffer = SerializeStreams(); + int sectionLength = 9 + descriptorBuffer.Length + streamBuffer.Length + 4; + + //TODO: calculate section length here + + MemoryStream ms = new MemoryStream(); + ms.WriteUInt8(0x02); //table_id + + byte byte2 = 0; + byte2 |= 0x80; //section_syntax_indicator + //'0' + byte2 |= 0x30; //reserved + byte2 += (byte)((sectionLength & 0x0300) >> 8); + ms.WriteUInt8(byte2); //section length, part 1 + + byte byte3 = (byte)(sectionLength & 0x00ff); + ms.WriteUInt8(byte3); //section length, part 2 + + ms.WriteUInt16(ProgramNumber); + + byte byte4 = 0; + byte4 |= 0xc0; //reserved + byte4 |= (byte)((versionNumber & 0x1f) << 1); //version number + byte4 |= 0x01; //current next indicator + ms.WriteUInt8(byte4); + + ms.WriteUInt8(0); //section number + ms.WriteUInt8(0); //last section number + + ms.WriteUInt16((ushort)(PcrPid | 0xe000)); //reserved & PCR_PID + + ms.WriteUInt16((ushort)(programInfoLength | 0xf000)); //reserved & program info length + + ms.Write(descriptorBuffer, 0, descriptorBuffer.Length); //descriptors() + + ms.Write(streamBuffer, 0, streamBuffer.Length); + + uint crc = DvbCrc32.CreateCrc(ms, 0, (int)ms.Position); + ms.WriteUInt32(crc); + + return ms.ToArray(); + } + + public PmtGeneratorStream AddStream(byte streamType, ushort pid) + { + PmtGeneratorStream stream = new PmtGeneratorStream(streamType, pid); + streams.Add(stream); + versionNumber++; + return stream; + } + +} \ No newline at end of file diff --git a/pcap2mpe/Psi/TdtPsiGenerator.cs b/pcap2mpe/Psi/TdtPsiGenerator.cs new file mode 100644 index 0000000..7603054 --- /dev/null +++ b/pcap2mpe/Psi/TdtPsiGenerator.cs @@ -0,0 +1,62 @@ +namespace pcap2mpe; + +public class TdtPsiGenerator : BasePsiGenerator +{ + public TdtPsiGenerator(Mpeg2Writer writer) : base(0x0014, "TDT", 1000, writer) + { + } + + + protected override byte[] GeneratePsi() + { + MemoryStream ms = new MemoryStream(); + ms.WriteByte(0x70); + + byte byte1 = 0; + //section syntax indicator = 0 + byte1 |= 0x40; //reserved future_use + byte1 |= 0x30; + ms.WriteByte(byte1); + + ms.WriteByte(5); //Section_length (always 5) + ms.Write(EncodeMjd(DateTime.Now), 0, 5); + return ms.ToArray(); + + } + + private static long MilliSecPerDay = (24 * 3600) * 1000; + private static long JulianEpochOffset = -40587 * MilliSecPerDay; + + internal static byte[] EncodeMjd(DateTime dt) + { + //shamelessly stolen from TSDuck's tsMJD.cpp + long time_ms = dt.ToUnixTime() * 1000; + + if (time_ms < JulianEpochOffset) + { + return null; + } + + long d = (time_ms - JulianEpochOffset) / 1000; //seconds since MJD epoch + byte[] days = BitConverter.GetBytes((ushort)(d / (24 * 3600))); + if (BitConverter.IsLittleEndian) + (days[1], days[0]) = (days[0], days[1]); + byte[] result = new byte[5]; + result[0] = days[0]; + result[1] = days[1]; + result[2] = EncodeBCD((int)((d / 3600) % 24)); + result[3] = EncodeBCD((int)((d / 60) % 60)); + result[4] = EncodeBCD((int)(d % 60)); + return result; + } + + private static byte EncodeBCD(int value) + { + byte result = 0; + result += (byte)(value % 10); + value /= 10; + result += (byte)((value % 10) << 4); + return result; + } + +} \ No newline at end of file diff --git a/pcap2mpe/StreamExtensions.cs b/pcap2mpe/StreamExtensions.cs new file mode 100644 index 0000000..669b81c --- /dev/null +++ b/pcap2mpe/StreamExtensions.cs @@ -0,0 +1,95 @@ +using System.Net.NetworkInformation; + +namespace pcap2mpe; + +public static class StreamExtensions +{ + public static PhysicalAddress ReadMacAddress(this Stream stream) + { + byte[] buffer = new byte[6]; + if (stream.Read(buffer, 0, 6) != 6) + { + throw new EndOfStreamException(); + } + return new PhysicalAddress(buffer); + } + + public static ushort ReadUInt16BigEndian(this Stream stream) + { + byte[] buffer = new byte[2]; + if (stream.Read(buffer, 0, 2) != 2) + { + throw new EndOfStreamException(); + } + (buffer[1], buffer[0]) = (buffer[0], buffer[1]); + return BitConverter.ToUInt16(buffer, 0); + } + + public static byte[] ReadByteArray(this Stream stream, int length) + { + byte[] buffer = new byte[length]; + int actuallyRead = stream.Read(buffer, 0, length); + if (actuallyRead != length) + { + Console.WriteLine("Packet was cut short during transmission. Expected {0} bytes, got {1}", length, + actuallyRead); + } + + return buffer; + } + + public static void WriteUInt16(this Stream stream, ushort value) + { + byte[] buffer = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(buffer); + } + stream.Write(buffer, 0, 2); + } + + public static void WriteByteArray(this Stream stream, byte[] value) + { + stream.Write(value, 0, value.Length); + } + + public static byte ReadUInt8(this Stream stream) + { + byte[] buffer = new byte[1]; + if (stream.Read(buffer, 0, 1) != 1) + { + throw new EndOfStreamException(); + } + + return buffer[0]; + } + + public static void WriteUInt32(this Stream stream, uint value) + { + byte[] buffer = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(buffer); + } + stream.Write(buffer, 0, 4); + + } + + public static byte[] ReadRemainderAsBytes(this Stream stream) + { + long remainder = stream.Length - stream.Position; + if (remainder >= int.MaxValue) + throw new NotImplementedException("oversized stream"); + return ReadByteArray(stream, (int)remainder); + } + + public static void WriteUInt8(this Stream stream, byte value) + { + stream.WriteByte(value); + } + + public static long GetAvailableBytes(this Stream stream) + { + return stream.Length - stream.Position; + } +} \ No newline at end of file diff --git a/pcap2mpe/Writers/Mpeg2FileWriter.cs b/pcap2mpe/Writers/Mpeg2FileWriter.cs new file mode 100644 index 0000000..75980f7 --- /dev/null +++ b/pcap2mpe/Writers/Mpeg2FileWriter.cs @@ -0,0 +1,25 @@ +namespace pcap2mpe; + +public class Mpeg2FileWriter : Mpeg2Writer +{ + public Mpeg2FileWriter(FileInfo file) + { + _fileInfo = file; + _fileStream = file.OpenWrite(); + } + + private FileInfo _fileInfo; + private FileStream _fileStream; + + protected override void WriteMpeg2PacketEx(byte[] packet) + { + _fileStream.Write(packet, 0, packet.Length); + } + + public override void Dispose() + { + _fileStream.Flush(); + _fileStream.Close(); + _fileStream.Dispose(); + } +} \ No newline at end of file diff --git a/pcap2mpe/Writers/Mpeg2UdpSender.cs b/pcap2mpe/Writers/Mpeg2UdpSender.cs new file mode 100644 index 0000000..33b4b57 --- /dev/null +++ b/pcap2mpe/Writers/Mpeg2UdpSender.cs @@ -0,0 +1,29 @@ +using System.Net; +using System.Net.Sockets; + +namespace pcap2mpe; + +public class Mpeg2UdpSender : Mpeg2Writer +{ + private readonly IPEndPoint _ipEndPoint; + private readonly UdpClient _udpClient; + + public Mpeg2UdpSender(IPEndPoint ipEndPoint) + { + Console.WriteLine("send to {0}", ipEndPoint); + _ipEndPoint = ipEndPoint; + _udpClient = new UdpClient(); + //_udpClient.Connect(_ipEndPoint); + } + + protected override void WriteMpeg2PacketEx(byte[] packet) + { + _udpClient.Send(packet, packet.Length, _ipEndPoint); + } + + public override void Dispose() + { + _udpClient.Close(); + _udpClient.Dispose(); + } +} \ No newline at end of file diff --git a/pcap2mpe/Writers/Mpeg2Writer.cs b/pcap2mpe/Writers/Mpeg2Writer.cs new file mode 100644 index 0000000..a70f38c --- /dev/null +++ b/pcap2mpe/Writers/Mpeg2Writer.cs @@ -0,0 +1,112 @@ +namespace pcap2mpe; + +public abstract class Mpeg2Writer : IDisposable +{ + public void EmitPsi(int pid, Span psi) + { + EmitPsi(pid, psi.ToArray()); + } + + public void EmitPsi(int pid, byte[] psi) + { + int psiOffset = 0; + while (psiOffset < psi.Length) + { + byte[] newPacket = new byte[188]; + Array.Fill(newPacket, (byte)0xFF); + int continuity = CalculateContinuityCounter(pid); + byte[] buildHeader = BuildHeader(psiOffset == 0, (uint)pid, continuity); + Array.Copy(buildHeader, 0, newPacket, 0, buildHeader.Length); + + byte packetSizeLeft = 188; + packetSizeLeft -= (byte)buildHeader.Length; + if (psiOffset == 0) + { + newPacket[buildHeader.Length] = 0; + packetSizeLeft--; + } + + int copySize = Math.Min(packetSizeLeft, psi.Length - psiOffset); + Array.Copy(psi, psiOffset, newPacket, 188 - packetSizeLeft, copySize); + psiOffset += copySize; + WriteMpeg2Packet(newPacket); + } + } + + private object writeLock = new object(); + private void WriteMpeg2Packet(byte[] packet) + { + if (packet.Length != 188) + throw new Exception("packet length mismatch"); + + lock (writeLock) + { + WriteMpeg2PacketEx(packet); + } + } + + private int[] continuities; + + private int CalculateContinuityCounter(int pid) + { + if (continuities == null) + continuities = new int[0x2000]; + return continuities[pid]++; + } + protected abstract void WriteMpeg2PacketEx(byte[] packet); + + public static byte[] BuildHeader(bool pusi, uint pid, int continuityCounter) + { + return BuildHeader(false, pusi, false, pid, 0, continuityCounter, null, true); + } + + public static byte[] BuildHeader(bool tei, bool pusi, bool transportPriority, uint pid, uint tsc, int continuityCounter, byte[] adaptionField = null, bool withPayload = true) + { + byte[] buffer = new byte[4 + (adaptionField != null ? adaptionField.Length + 1 : 0)]; + buffer[0] = (byte)'G'; + + if (tei) + buffer[1] |= 0x80; + + if (pusi) + buffer[1] |= 0x40; + + if (transportPriority) + buffer[1] |= 0x20; + + if (pid > 0x1fff) + throw new ArgumentOutOfRangeException(nameof(pid)); + + if ((pid & 0x1000) != 0) + buffer[1] |= 0x10; + + uint pid2 = (pid & 0x0f00) >> 8; + buffer[1] += (byte)pid2; + + buffer[2] = (byte)(pid & 0x00ff); + + if (tsc > 3) + throw new ArgumentOutOfRangeException(nameof(tsc)); + + tsc <<= 6; + buffer[3] += (byte)tsc; + + if (adaptionField != null) + { + buffer[3] |= 0x20; + buffer[4] = (byte)adaptionField.Length; + Array.Copy(adaptionField, 0, buffer, 5, adaptionField.Length); + } + + if (withPayload) + buffer[3] |= 0x10; + + continuityCounter &= 0x0000000f; + buffer[3] += (byte)continuityCounter; + return buffer; + } + + public abstract void Dispose(); +} + + diff --git a/pcap2mpe/Writers/NullMpeg2Writer.cs b/pcap2mpe/Writers/NullMpeg2Writer.cs new file mode 100644 index 0000000..1d4a80a --- /dev/null +++ b/pcap2mpe/Writers/NullMpeg2Writer.cs @@ -0,0 +1,15 @@ +namespace pcap2mpe; + + +public class NullMpeg2Writer : Mpeg2Writer +{ + protected override void WriteMpeg2PacketEx(byte[] packet) + { + + } + + public override void Dispose() + { + + } +} \ No newline at end of file diff --git a/pcap2mpe/pcap2mpe.csproj b/pcap2mpe/pcap2mpe.csproj new file mode 100644 index 0000000..884bb4a --- /dev/null +++ b/pcap2mpe/pcap2mpe.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + +