From ddd4181dc35ccfbc878124dff2e26e4595c76744 Mon Sep 17 00:00:00 2001 From: fey Date: Wed, 2 Sep 2026 22:38:45 +0200 Subject: [PATCH] Added preliminary subtitle handling. --- skyscraper8/Isdb/Matenro.cs | 11 +- .../Isdb/Subtitles/IIsdbSubtitleHandler.cs | 2 +- skyscraper8/Isdb/Subtitles/IsdbCaption.cs | 261 ++++++++++++++++++ .../Isdb/Subtitles/IsdbSubtitlePesParser.cs | 116 +++++++- 4 files changed, 385 insertions(+), 5 deletions(-) create mode 100644 skyscraper8/Isdb/Subtitles/IsdbCaption.cs diff --git a/skyscraper8/Isdb/Matenro.cs b/skyscraper8/Isdb/Matenro.cs index 5dfc591..f4859dd 100644 --- a/skyscraper8/Isdb/Matenro.cs +++ b/skyscraper8/Isdb/Matenro.cs @@ -163,6 +163,7 @@ public class Matenro : IPatEventHandler, ICatEventHandler, IBatEventHandler, ISd throw new NotImplementedException(); } + private bool timePresent; public void OnTdtTime(DateTime utcTime) { throw new NotImplementedException(); @@ -220,7 +221,7 @@ public class Matenro : IPatEventHandler, ICatEventHandler, IBatEventHandler, ISd continue; case StreamType.IsdbSubtitles: case StreamType.IsdbTeletext: - DvbContext.RegisterPacketProcessor(pmtStream.ElementaryPid, new PesDecoder(new IsdbSubtitlePesParser(this))); + DvbContext.RegisterPacketProcessor(pmtStream.ElementaryPid, new PesDecoder(new IsdbSubtitlePesParser(this, pmtStream.ElementaryPid))); continue; case StreamType.TryAutodetect: DvbContext.RegisterPacketProcessor(pmtStream.ElementaryPid, new StreamTypeAutodetection(pmtStream.ElementaryPid, this)); @@ -305,4 +306,12 @@ public class Matenro : IPatEventHandler, ICatEventHandler, IBatEventHandler, ISd { throw new NotImplementedException(); } + + public void OnCaption(int pid, IsdbCaption plaintextCaption) + { + if (!timePresent) + return; + + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/skyscraper8/Isdb/Subtitles/IIsdbSubtitleHandler.cs b/skyscraper8/Isdb/Subtitles/IIsdbSubtitleHandler.cs index 5782869..452439f 100644 --- a/skyscraper8/Isdb/Subtitles/IIsdbSubtitleHandler.cs +++ b/skyscraper8/Isdb/Subtitles/IIsdbSubtitleHandler.cs @@ -2,5 +2,5 @@ namespace skyscraper8.Isdb.Subtitles; public interface IIsdbSubtitleHandler { - + void OnCaption(int pid, IsdbCaption plaintextCaption); } \ No newline at end of file diff --git a/skyscraper8/Isdb/Subtitles/IsdbCaption.cs b/skyscraper8/Isdb/Subtitles/IsdbCaption.cs new file mode 100644 index 0000000..3d19f28 --- /dev/null +++ b/skyscraper8/Isdb/Subtitles/IsdbCaption.cs @@ -0,0 +1,261 @@ +using System.Drawing; +using System.Text; +using log4net; +using skyscraper5.Dvb.DataBroadcasting.IntModel; +using skyscraper5.Skyscraper.IO; + +namespace skyscraper8.Isdb.Subtitles; + +public class IsdbCaption +{ + private static readonly ILog Logger = LogManager.GetLogger(typeof(IsdbCaption)); + + public IsdbCaption(byte[] buffer) + { + byte[] outBuffer = new byte[buffer.Length]; + int inPointer = 0; + int outPointer = 0; + + while (inPointer < buffer.Length) + { + byte b = buffer[inPointer++]; + switch (b) + { + case 0x00: //NUL + break; + case 0x07: //BEL + throw new NotImplementedException("BEL"); + case 0x08: //APB + throw new NotImplementedException("APB"); + case 0x09: //APF + throw new NotImplementedException("APF"); + case 0x0A: //APD + throw new NotImplementedException("APD"); + case 0x0B: //APU + throw new NotImplementedException("APU"); + case 0x0C: //CS + this.ClearScreen = true; + break; + case 0x0D: //APR + throw new NotImplementedException("APR"); + case 0x0E: //LS1 + throw new NotImplementedException("LS1"); + case 0x0F: //LS0 + throw new NotImplementedException("LS0"); + case 0x16: //PAPF + throw new NotImplementedException("PAPF"); + case 0x18: //CAN + throw new NotImplementedException("CAN"); + case 0x19: //SS2 + throw new NotImplementedException("SS2"); + case 0x1B: //ESC + throw new NotImplementedException("ESC"); + case 0x1C: //APS + int y = buffer[inPointer++]; + int x = buffer[inPointer++]; + x -= 0x40; + y -= 0x40; + ActivePositionSet = new Point(x, y); + break; + case 0x1D: + throw new NotImplementedException("SS3"); + case 0x1E: + throw new NotImplementedException("RS"); + case 0x1F: + throw new NotImplementedException("US"); + case 0x20: //SP (Space) + outBuffer[outPointer++] = 0x20; + break; + case 0x7F: //DEL + throw new NotImplementedException("DEL"); + case 0x80: //BKF + throw new NotImplementedException("BKF"); + case 0x81: //RDF + throw new NotImplementedException("RDF"); + case 0x82: //GRF + throw new NotImplementedException("GRF"); + case 0x83: //YLF + throw new NotImplementedException("YLF"); + case 0x84: //BLF + throw new NotImplementedException("BLF"); + case 0x85: //MGF + throw new NotImplementedException("MGF"); + case 0x86: //CNF + throw new NotImplementedException("CNF"); + case 0x87: //WHF + ForegroundColor = Color.White; + break; + case 0x88: //SSZ + CharacterSize = Subtitles.CharacterSize.Small; + break; + case 0x89: //MSZ + throw new NotImplementedException("MSZ"); + case 0x8A: //NSZ + throw new NotImplementedException("NSZ"); + case 0x8B: + throw new NotImplementedException("SZX"); + case 0x90: //COL + byte colP1 = buffer[inPointer++]; + switch (colP1) + { + case 0x50: BackgroundColor = Color.Black; break; + case 0x58: BackgroundColor = Color.Transparent; break; + default: + //see 6-STD-B24v6_4-1p3-E1.pdf, page 108 + throw new NotImplementedException(String.Format("colP1 0x{0:X2}", colP1)); + } + break; + case 0x91: //FLC + throw new NotImplementedException("FLC"); + case 0x92: //CDC + throw new NotImplementedException("CDC"); + case 0x93: //POL + throw new NotImplementedException("POL"); + case 0x94: //WLF + throw new NotImplementedException("WLF"); + case 0x95: //MACRO + throw new NotImplementedException("MACRO"); + case 0x97: //HLC + throw new NotImplementedException("HLC"); + case 0x98: //RPC + throw new NotImplementedException("RPC"); + case 0x99: //SPL + throw new NotImplementedException("SPL"); + case 0x9A: //STL + throw new NotImplementedException("STL"); + case 0x9B: //CSI + ControlSequenceIntroduced = true; + string csiArguments = ""; + while (inPointer < buffer.Length) + { + byte csiByte = buffer[inPointer++]; + if (csiByte >= 0x20 && csiByte <= 0x3F) + { + csiArguments += (char)csiByte; + } + else if (csiByte >= 0x40 && csiByte <= 0x7E) + { + char command = (char)csiByte; + HandleCsiCommand(command, csiArguments); + break; + } + } + break; + case 0x9D: //TIME + throw new NotImplementedException("TIME"); + case 0xA0: //10/0 + throw new NotImplementedException("10/0"); + case 0xFF: + throw new NotImplementedException("15/15"); + default: + outBuffer[outPointer++] = b; + break; + } + } + Text = Encoding.UTF8.GetString(outBuffer, 0, outPointer); + } + + private void HandleCsiCommand(char command, string csiArguments) + { + //Um den passenden CMD zu finden, die 6-STD-B24v6_4-1p3-E1.pdf durchsuchen und Table 7-17 (ab Seite 113) nach Final character durchsuchen. + //Wenn cmd zum Beispiel 0x53 ist, dann nach 05/3 suchen. + byte cmd = (byte)command; + csiArguments = csiArguments.TrimEnd(); + switch (cmd) + { + case 0x53: + WritingFormat = (WritingFormat)int.Parse(csiArguments); + break; + case 0x56: + string[] sdfArgs = csiArguments.Split(';'); + DisplayFormat = new Size(int.Parse(sdfArgs[0]), int.Parse(sdfArgs[1])); + break; + case 0x5f: + string[] sdpArgs = csiArguments.Split(';'); + DisplayPosition = new Point(int.Parse(sdpArgs[0]), int.Parse(sdpArgs[1])); + break; + case 0x57: + string[] ssmArgs = csiArguments.Split(';'); + CharacterCompositionDots = new Size(int.Parse(ssmArgs[0]), int.Parse(ssmArgs[1])); + break; + case 0x58: + HorizontalSpacing = int.Parse(csiArguments); + break; + case 0x59: + VerticalSpace = int.Parse(csiArguments); + break; + case 0x6e: + int rcsColor = int.Parse(csiArguments); + bool isTransparent = rcsColor > 8; + if (isTransparent) + rcsColor -= 8; + switch (rcsColor) + { + case 0: RasterColour = Color.Black; break; + case 1: RasterColour = Color.Red; break; + case 2: RasterColour = Color.Green; break; + case 3: RasterColour = Color.Yellow; break; + case 4: RasterColour = Color.Blue; break; + case 5: RasterColour = Color.Magenta; break; + case 6: RasterColour = Color.Cyan; break; + case 7: RasterColour = Color.White; break; + case 8: RasterColour = Color.Transparent; break; + } + if (isTransparent) + RasterColour = Color.FromArgb(RasterColour.Value.A,RasterColour.Value.R / 2,RasterColour.Value.G / 2,RasterColour.Value.B / 2); + break; + default: + throw new NotImplementedException(String.Format("cmd 0x{0:X2}", cmd)); + } + } + + public Color? RasterColour { get; set; } + + public int? VerticalSpace { get; set; } + + public int? HorizontalSpacing { get; set; } + + public Size? CharacterCompositionDots { get; set; } + + public Point? DisplayPosition { get; set; } + + public Size? DisplayFormat { get; set; } + + public WritingFormat? WritingFormat { get; set; } + + public string Text { get; private set; } + + public bool ControlSequenceIntroduced { get; private set; } + + public bool ClearScreen { get; private set; } + + public CharacterSize? CharacterSize { get; private set; } + + public Color? ForegroundColor { get; private set; } + public Color? BackgroundColor { get; private set; } + public Point? ActivePositionSet { get; private set; } +} + +public enum CharacterSize +{ + Small, + Medium, + Large +} + +public enum WritingFormat +{ + HorizontalStandardDensity = 0, + VerticalStandardDensity = 1, + HorizontalHighDensity = 2, + VerticalHighDensity = 3, + HorizontalWritingWestern = 4, + HorizontalWriting1920_1080 = 5, + VerticalWriting1920_1080 = 6, + HorizontalWriting960_540 = 7, + VerticalWriting960_540 = 8, + HorizontalWriting720_480 = 9, + VerticalWriting720_480 = 10, + HorizontalWriting1280_720 = 11, + VerticalWriting1280_720 = 12, +} \ No newline at end of file diff --git a/skyscraper8/Isdb/Subtitles/IsdbSubtitlePesParser.cs b/skyscraper8/Isdb/Subtitles/IsdbSubtitlePesParser.cs index e09c47c..24989fe 100644 --- a/skyscraper8/Isdb/Subtitles/IsdbSubtitlePesParser.cs +++ b/skyscraper8/Isdb/Subtitles/IsdbSubtitlePesParser.cs @@ -1,5 +1,6 @@ using log4net; using skyscraper5.Mpeg2; +using skyscraper5.Skyscraper.IO; namespace skyscraper8.Isdb.Subtitles; @@ -9,13 +10,122 @@ class IsdbSubtitlePesParser : IPesProcessor private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name); - public IsdbSubtitlePesParser(IIsdbSubtitleHandler handler) + public IsdbSubtitlePesParser(IIsdbSubtitleHandler handler, int pmtStreamElementaryPid) { Handler = handler; + Pid = pmtStreamElementaryPid; } - + + public int Pid { get; private set; } + public void ProcessPesPacket(PesPacket packet) { - throw new NotImplementedException(); + //Peel back the outer layer, as stated by 6-STD-B37v2_4-E1.pdf, clause 2.2.3.6, Table 2-25 + byte[] payload = packet.Payload; + MemoryStream ms = new MemoryStream(payload, false); + + byte dataIdentifier = ms.ReadUInt8(); + byte privateStreamId = ms.ReadUInt8(); + + byte byte3 = ms.ReadUInt8(); + int reserved = (byte3 & 0xf0) >> 4; + int pesDataPacketHeaderLength = (byte3 & 0x0f); + byte[] pesDataPacketHeader = ms.ReadBytes(pesDataPacketHeaderLength); + + if (pesDataPacketHeaderLength > 0) + { + throw new NotImplementedException("PES packet header length not implemented"); + } + + //Finally, we can start parsing the data group 6-STD-B24v6_4-1p3-E1.pdf, clause 9.2 + byte byte4 = ms.ReadUInt8(); + int dataGroupId = (byte4 & 0xfc) >> 2; + int dataGroupVersion = (byte4 & 0x03); + + byte dataGroupLinkNumber = ms.ReadUInt8(); + byte lastDataGroupLinkNumber = ms.ReadUInt8(); + ushort dataGroupSize = ms.ReadUInt16BE(); + long expectedRemaining = ms.GetAvailableBytes() - 2; + if (dataGroupSize != expectedRemaining) + { + throw new IsdbException("Unexpected data group size: " + dataGroupSize); + } + + byte[] dataGroupSizeBytes = ms.ReadBytes(dataGroupSize); + switch (dataGroupId) + { + case 0: + ParseCaptionManagement(dataGroupSizeBytes); + break; + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + ParseCaptionStatement(dataGroupSizeBytes); + break; + } + + + ushort crc16 = ms.ReadUInt16BE(); + + } + + private void ParseCaptionManagement(byte[] dataGroupSizeBytes) + { + throw new NotImplementedException("Caption management not implemented"); + } + + private void ParseCaptionStatement(byte[] dataGroupSizeBytes) + { + MemoryStream ms = new MemoryStream(dataGroupSizeBytes, false); + byte byteA = ms.ReadUInt8(); + int tmd = (byteA & 0xc0) >> 6; + int reserved = (byteA & 0x3f); + if (tmd == 1 || tmd == 2) + { + byte[] stm = ms.ReadBytes(5); + } + + uint dataUnitLoopLength = ms.ReadUInt24BE(); + long expectedDataUnitLoopLength = ms.GetAvailableBytes(); + if (dataUnitLoopLength != expectedDataUnitLoopLength) + { + throw new IsdbException("Unexpected data unit-loop length: " + dataUnitLoopLength); + } + + byte[] dataUnit = ms.ReadBytes(dataUnitLoopLength); + ParseDataUnit(dataUnit); + } + + private void ParseDataUnit(byte[] dataUnit) + { + MemoryStream ms = new MemoryStream(dataUnit, false); + byte unitSeperator = ms.ReadUInt8(); + if (unitSeperator != 0x1f) + { + throw new IsdbException("Unexpected data unit-seperator: " + unitSeperator); + } + byte dataUnitParameter = ms.ReadUInt8(); + uint dataUnitSize = ms.ReadUInt24BE(); + long expectedDataUnitSize = ms.GetAvailableBytes(); + if (dataUnitSize != expectedDataUnitSize) + { + throw new IsdbException("Unexpected data unit-size: " + dataUnitSize); + } + byte[] dataUnitData = ms.ReadBytes(dataUnitSize); + switch (dataUnitParameter) + { + case 0x20: + IsdbCaption plaintextCaption = new IsdbCaption(dataUnitData); + logger.InfoFormat("Caption on PID 0x{0:X4} - {1}", Pid, plaintextCaption.Text); + Handler.OnCaption(Pid, plaintextCaption); + break; + default: + throw new IsdbException("Unexpected data unit-parameter: " + dataUnitParameter); + } } } \ No newline at end of file