Import initial code

This commit is contained in:
feyris-tan 2026-05-07 16:13:15 +02:00
commit 787949aea0
23 changed files with 1288 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

16
pcap2mpe.sln Normal file
View File

@ -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

View File

@ -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;
}
}

View File

@ -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; }
}

141
pcap2mpe/DvbCrc32.cs Normal file
View File

@ -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<byte> 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<byte> 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;
}
}

54
pcap2mpe/HOW_TO_USE.md Normal file
View File

@ -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
<?xml version="1.0" encoding="UTF-8"?>
<tsduck>
<TDT UTC_time="1984-01-01 00:13:37"/>
</tsduck>
<?xml version=\"1.0\" encoding=\"UTF-8\"?><tsduck><TDT UTC_time=\"1984-01-01 00:13:37\"/></tsduck>
# 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 "<?xml version=\"1.0\" encoding=\"UTF-8\"?><tsduck><TDT UTC_time=\"1984-01-01 00:13:37\"/></tsduck>" --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

View File

@ -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]);
}
}
}

70
pcap2mpe/PacketCounter.cs Normal file
View File

@ -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);
}
}
}

View File

@ -0,0 +1,114 @@
using pcap2mpe.Descriptors;
using SharpPcap;
namespace pcap2mpe;
public class PacketHandlerQueue
{
public PacketHandlerQueue()
{
threadStateLocker = new object();
queue = new Queue<byte[]>();
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<byte[]> 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<byte> 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);
}
}
}

46
pcap2mpe/Program.cs Normal file
View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
namespace pcap2mpe.Descriptors;
public abstract class TsDescriptor
{
public abstract byte GetDescriptorId();
public abstract byte[] Serialize();
}

View File

@ -0,0 +1,68 @@
using System.Net.NetworkInformation;
namespace pcap2mpe;
public class MpePsiGenerator
{
public static Span<byte> 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<byte>(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;
}
}

View File

@ -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<ushort, ushort>();
}
private Dictionary<ushort, ushort> 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();
}
}

View File

@ -0,0 +1,142 @@
using pcap2mpe.Descriptors;
namespace pcap2mpe;
public class PmtPsiGenerator : BasePsiGenerator
{
public ushort ProgramNumber { get; }
public ushort PcrPid { get; }
private List<TsDescriptor> descriptors;
private List<PmtGeneratorStream> streams;
public PmtPsiGenerator(int pid, ushort programNumber, ushort PcrPid, Mpeg2Writer writer)
: base(pid, "PMT", 500, writer)
{
ProgramNumber = programNumber;
this.PcrPid = PcrPid;
descriptors = new List<TsDescriptor>();
streams = new List<PmtGeneratorStream>();
}
public class PmtGeneratorStream
{
public PmtGeneratorStream(byte type, ushort pid)
{
descriptors = new List<TsDescriptor>();
this.StreamType = type;
this.ElementaryPid = pid;
}
private List<TsDescriptor> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,112 @@
namespace pcap2mpe;
public abstract class Mpeg2Writer : IDisposable
{
public void EmitPsi(int pid, Span<byte> 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();
}

View File

@ -0,0 +1,15 @@
namespace pcap2mpe;
public class NullMpeg2Writer : Mpeg2Writer
{
protected override void WriteMpeg2PacketEx(byte[] packet)
{
}
public override void Dispose()
{
}
}

14
pcap2mpe/pcap2mpe.csproj Normal file
View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpPcap" Version="6.3.1" />
</ItemGroup>
</Project>