public Dictionary <int, Paragraph> GetTeletext(TeletextRunSettings teletextRunSettings, int pageNumber, int pageNumberBcd) { var timestamp = PresentationTimestamp.HasValue ? PresentationTimestamp.Value / 90 : 40; // do not allow timestamp to go back - treat lower timestamp as a reset/overflow var lastTimestamp = teletextRunSettings.GetLastTimestamp(pageNumber); teletextRunSettings.SetLastTimestamp(pageNumber, timestamp); timestamp += teletextRunSettings.GetAddTimestamp(pageNumber); if (lastTimestamp > 0 && lastTimestamp > timestamp) { teletextRunSettings.SetAddTimestamp(pageNumber, lastTimestamp); } // offset all time codes if first timestamp in ts file is > 1 sec timestamp = teletextRunSettings.SubtractStartMs(timestamp); if (timestamp < 40) { timestamp = 40; // Teletext.cs will subtract 40 ms (1 frame @25 fps) and this value must not be below 0 } var teletextPages = new Dictionary <int, Paragraph>(); var i = 1; while (i <= _dataBuffer.Length - 6) { var dataUnitId = _dataBuffer[i++]; var dataUnitLen = _dataBuffer[i++]; if (dataUnitId == (int)Teletext.DataUnitT.DataUnitEbuTeletextNonSubtitle || dataUnitId == (int)Teletext.DataUnitT.DataUnitEbuTeletextSubtitle) { if (dataUnitLen == 44) // teletext payload has always size 44 bytes { Teletext.ProcessTelxPacket((Teletext.DataUnitT)dataUnitId, new Teletext.TeletextPacketPayload(_dataBuffer, i), timestamp, teletextRunSettings, pageNumberBcd, pageNumber); } } i += dataUnitLen; } if (teletextRunSettings.PageNumberAndParagraph.ContainsKey(pageNumber) && teletextRunSettings.PageNumberAndParagraph[pageNumber] != null) { if (teletextPages.ContainsKey(pageNumber)) { teletextPages[pageNumber] = teletextRunSettings.PageNumberAndParagraph[pageNumber]; } else { teletextPages.Add(pageNumber, teletextRunSettings.PageNumberAndParagraph[pageNumber]); } } teletextRunSettings.PageNumberAndParagraph.Clear(); return(teletextPages); }
public Dictionary <int, Paragraph> GetTeletext(int packetId, TeletextRunSettings teletextRunSettings, int pageNumber, int pageNumberBcd, ulong?firstMs) { var lastTimestamp = PresentationTimestamp.HasValue ? PresentationTimestamp.Value / 90 : 40; if (firstMs.HasValue && lastTimestamp >= firstMs) { lastTimestamp -= firstMs.Value; } if (lastTimestamp < 40) { lastTimestamp = 40; // Teletext.cs will subtract 40 ms (1 frame @25 fps) and this value must not be below 0 } var teletextPages = new Dictionary <int, Paragraph>(); var i = 1; while (i <= _dataBuffer.Length - 6) { var dataUnitId = _dataBuffer[i++]; var dataUnitLen = _dataBuffer[i++]; if (dataUnitId == (int)Teletext.DataUnitT.DataUnitEbuTeletextNonSubtitle || dataUnitId == (int)Teletext.DataUnitT.DataUnitEbuTeletextSubtitle) { if (dataUnitLen == 44) // teletext payload has always size 44 bytes { Teletext.ProcessTelxPacket((Teletext.DataUnitT)dataUnitId, new Teletext.TeletextPacketPayload(_dataBuffer, i), lastTimestamp, teletextRunSettings, pageNumberBcd, pageNumber); } } i += dataUnitLen; } if (teletextRunSettings.PageNumberAndParagraph.ContainsKey(pageNumber) && teletextRunSettings.PageNumberAndParagraph[pageNumber] != null) { if (teletextPages.ContainsKey(pageNumber)) { teletextPages[pageNumber] = teletextRunSettings.PageNumberAndParagraph[pageNumber]; } else { teletextPages.Add(pageNumber, teletextRunSettings.PageNumberAndParagraph[pageNumber]); } } teletextRunSettings.PageNumberAndParagraph.Clear(); return(teletextPages); }
public static void ProcessTelxPacket(DataUnitT dataUnitId, TeletextPacketPayload packet, ulong timestamp, TeletextRunSettings teletextRunSettings, int targetPageNumberBcd, int targetPageNumberDec) { // variable names conform to ETS 300 706, chapter 7.1.2 var address = (TeletextHamming.UnHamming84(packet.Address[1]) << 4) | TeletextHamming.UnHamming84(packet.Address[0]); var m = address & 0x7; if (m == 0) { m = 8; } var y = (address >> 3) & 0x1f; var designationCode = y > 25 ? TeletextHamming.UnHamming84(packet.Data[0]) : 0x00; if (y == 0) // PAGE HEADER { // CC map var i = (TeletextHamming.UnHamming84(packet.Data[1]) << 4) | TeletextHamming.UnHamming84(packet.Data[0]); var flagSubtitle = (TeletextHamming.UnHamming84(packet.Data[5]) & 0x08) >> 3; CcMap[i] |= (byte)(flagSubtitle << (m - 1)); // Page number and control bits var pageNumber = (m << 8) | (TeletextHamming.UnHamming84(packet.Data[1]) << 4) | TeletextHamming.UnHamming84(packet.Data[0]); var charset = ((TeletextHamming.UnHamming84(packet.Data[7]) & 0x08) | (TeletextHamming.UnHamming84(packet.Data[7]) & 0x04) | (TeletextHamming.UnHamming84(packet.Data[7]) & 0x02)) >> 1; //uint8_t flag_suppress_header = unham_8_4(packet.data[6]) & 0x01; //uint8_t flag_inhibit_display = (unham_8_4(packet.data[6]) & 0x08) >> 3; // ETS 300 706, chapter 9.3.1.3: // When set to '1' the service is designated to be in Serial mode and the transmission of a page is terminated // by the next page header with a different page number. // When set to '0' the service is designated to be in Parallel mode and the transmission of a page is terminated // by the next page header with a different page number but the same magazine number. // The same setting shall be used for all page headers in the service. // ETS 300 706, chapter 7.2.1: Page is terminated by and excludes the next page header packet // having the same magazine address in parallel transmission mode, or any magazine address in serial transmission mode. _transmissionMode = (TransmissionMode)(TeletextHamming.UnHamming84(packet.Data[7]) & 0x01); // FIXME: Well, this is not ETS 300 706 kosher, however we are interested in DATA_UNIT_EBU_TELETEXT_SUBTITLE only if (_transmissionMode == TransmissionMode.TransmissionModeParallel && dataUnitId != DataUnitT.DataUnitEbuTeletextSubtitle) { return; } if (_receivingData && (_transmissionMode == TransmissionMode.TransmissionModeSerial && Page(pageNumber) != Page(targetPageNumberBcd) || _transmissionMode == TransmissionMode.TransmissionModeParallel && Page(pageNumber) != Page(targetPageNumberBcd) && m == Magazine(targetPageNumberBcd)) ) { _receivingData = false; return; } // Page transmission is terminated, however now we are waiting for our new page if (targetPageNumberBcd != pageNumber) { return; } // Now we have the beginning of page transmission; if there is page_buffer pending, process it if (_pageBuffer.Tainted) { // it would be nice, if subtitle hides on previous video frame, so we contract 40 ms (1 frame @25 fps) _pageBuffer.HideTimestamp = timestamp - 40; ProcessPage(_pageBuffer, teletextRunSettings, targetPageNumberDec); } _pageBuffer.ShowTimestamp = timestamp; _pageBuffer.HideTimestamp = 0; _pageBuffer.Text = new int[25, 40]; _pageBuffer.Tainted = false; _receivingData = true; _primaryCharset.G0X28 = (int)BoolT.Undef; var c = _primaryCharset.G0M29 != (int)BoolT.Undef ? _primaryCharset.G0M29 : charset; RemapG0Charset(c); /* * // I know -- not needed; in subtitles we will never need disturbing teletext page status bar * // displaying tv station name, current time etc. * if (flag_suppress_header == NO) { * for (uint8_t i = 14; i < 40; i++) page_buffer.text[y,i] = telx_to_ucs2(packet.data[i]); * //page_buffer.tainted = YES; * } */ } else if (m == Magazine(targetPageNumberBcd) && y >= 1 && y <= 23 && _receivingData) { // ETS 300 706, chapter 9.4.1: Packets X/26 at presentation Levels 1.5, 2.5, 3.5 are used for addressing // a character location and overwriting the existing character defined on the Level 1 page // ETS 300 706, annex B.2.2: Packets with Y = 26 shall be transmitted before any packets with Y = 1 to Y = 25; // so page_buffer.text[y,i] may already contain any character received // in frame number 26, skip original G0 character for (var i = 0; i < 40; i++) { if (_pageBuffer.Text[y, i] == 0x00) { _pageBuffer.Text[y, i] = TelxToUcs2(packet.Data[i]); } } _pageBuffer.Tainted = true; } else if (m == Magazine(targetPageNumberBcd) && y == 26 && _receivingData) { // ETS 300 706, chapter 12.3.2: X/26 definition var x26Row = 0; var triplets = new uint[13]; var j = 0; for (var i = 1; i < 40; i += 3, j++) { triplets[j] = TeletextHamming.UnHamming2418((packet.Data[i + 2] << 16) | (packet.Data[i + 1] << 8) | packet.Data[i]); } for (var j2 = 0; j2 < 13; j2++) { if (triplets[j2] == 0xffffffff) { // invalid data (HAM24/18 uncorrectable error detected), skip group _config.LogError($"! Unrecoverable data error; UNHAM24/18()={triplets[j2]}"); continue; } var data = (triplets[j2] & 0x3f800) >> 11; var mode = (triplets[j2] & 0x7c0) >> 6; var address2 = triplets[j2] & 0x3f; var rowAddressGroup = address2 >= 40 && address2 <= 63; // ETS 300 706, chapter 12.3.1, table 27: set active position if (mode == 0x04 && rowAddressGroup) { x26Row = (int)(address2 - 40); if (x26Row == 0) { x26Row = 24; } } // ETS 300 706, chapter 12.3.1, table 27: termination marker if (mode >= 0x11 && mode <= 0x1f && rowAddressGroup) { break; } // ETS 300 706, chapter 12.3.1, table 27: character from G2 set int x26Col; if (mode == 0x0f && !rowAddressGroup) { x26Col = (int)address2; if (data > 31) { _pageBuffer.Text[x26Row, x26Col] = TeletextTables.G2[0, data - 0x20]; } } // ETS 300 706, chapter 12.3.1, table 27: G0 character with diacritical mark if (mode >= 0x11 && mode <= 0x1f && !rowAddressGroup) { x26Col = (int)address2; if (data >= 65 && data <= 90) // A-Z { _pageBuffer.Text[x26Row, x26Col] = TeletextTables.G2Accents[mode - 0x11, data - 65]; } else if (data >= 97 && data <= 122) // a-z { _pageBuffer.Text[x26Row, x26Col] = TeletextTables.G2Accents[mode - 0x11, data - 71]; } else // other { _pageBuffer.Text[x26Row, x26Col] = TelxToUcs2((byte)data); } } } } else if (m == Magazine(targetPageNumberBcd) && y == 28 && _receivingData) { // TODO: // ETS 300 706, chapter 9.4.7: Packet X/28/4 // Where packets 28/0 and 28/4 are both transmitted as part of a page, packet 28/0 takes precedence over 28/4 for all but the color map entry coding. if (designationCode == 0 || designationCode == 4) { // ETS 300 706, chapter 9.4.2: Packet X/28/0 Format 1 // ETS 300 706, chapter 9.4.7: Packet X/28/4 uint triplet0 = TeletextHamming.UnHamming2418((packet.Data[3] << 16) | (packet.Data[2] << 8) | packet.Data[1]); if (triplet0 == 0xffffffff) { // invalid data (HAM24/18 uncorrectable error detected), skip group _config.LogError($"! Unrecoverable data error; UNHAM24/18()={triplet0}"); } else { // ETS 300 706, chapter 9.4.2: Packet X/28/0 Format 1 only if ((triplet0 & 0x0f) == 0x00) { _primaryCharset.G0X28 = (int)((triplet0 & 0x3f80) >> 7); RemapG0Charset(_primaryCharset.G0X28); } } } } else if (m == Magazine(targetPageNumberBcd) && y == 29) { // TODO: // ETS 300 706, chapter 9.5.1 Packet M/29/0 // Where M/29/0 and M/29/4 are transmitted for the same magazine, M/29/0 takes precedence over M/29/4. if (designationCode == 0 || designationCode == 4) { // ETS 300 706, chapter 9.5.1: Packet M/29/0 // ETS 300 706, chapter 9.5.3: Packet M/29/4 uint triplet0 = TeletextHamming.UnHamming2418((packet.Data[3] << 16) | (packet.Data[2] << 8) | packet.Data[1]); if (triplet0 == 0xffffffff) { // invalid data (HAM24/18 uncorrectable error detected), skip group _config.LogError($"! Unrecoverable data error; UNHAM24/18()={triplet0}"); } else { // ETS 300 706, table 11: Coding of Packet M/29/0 // ETS 300 706, table 13: Coding of Packet M/29/4 if ((triplet0 & 0xff) == 0x00) { _primaryCharset.G0M29 = (int)((triplet0 & 0x3f80) >> 7); // X/28 takes precedence over M/29 if (_primaryCharset.G0X28 == (int)BoolT.Undef) { RemapG0Charset(_primaryCharset.G0M29); } } } } } else if (m == 8 && y == 30) { // ETS 300 706, chapter 9.8: Broadcast Service Data Packets if (!_states.ProgrammeInfoProcessed) { // ETS 300 706, chapter 9.8.1: Packet 8/30 Format 1 if (TeletextHamming.UnHamming84(packet.Data[0]) < 2) { _config.LogInfoNoNewLine("- Programme Identification Data = "); for (var i = 20; i < 40; i++) { var c = TelxToUcs2(packet.Data[i]); // strip any control codes from PID, eg. TVP station if (c < 0x20) { continue; } _config.LogInfoNoNewLine(Ucs2ToUtf8(c)); } _config.LogInfo(string.Empty); // OMG! ETS 300 706 stores timestamp in 7 bytes in Modified Julian Day in BCD format + HH:MM:SS in BCD format // + timezone as 5-bit count of half-hours from GMT with 1-bit sign // In addition all decimals are incremented by 1 before transmission. long t = 0; // 1st step: BCD to Modified Julian Day t += (packet.Data[10] & 0x0f) * 10000; t += ((packet.Data[11] & 0xf0) >> 4) * 1000; t += (packet.Data[11] & 0x0f) * 100; t += ((packet.Data[12] & 0xf0) >> 4) * 10; t += packet.Data[12] & 0x0f; t -= 11111; // 2nd step: conversion Modified Julian Day to unix timestamp t = (t - 40587) * 86400; // 3rd step: add time t += 3600 * (((packet.Data[13] & 0xf0) >> 4) * 10 + (packet.Data[13] & 0x0f)); t += 60 * (((packet.Data[14] & 0xf0) >> 4) * 10 + (packet.Data[14] & 0x0f)); t += ((packet.Data[15] & 0xf0) >> 4) * 10 + (packet.Data[15] & 0x0f); t -= 40271; // 4th step: conversion to time_t var span = TimeSpan.FromTicks(t * TimeSpan.TicksPerSecond); var t2 = new DateTime(1970, 1, 1).Add(span); var localTime = TimeZoneInfo.ConvertTimeFromUtc(t2, TimeZoneInfo.Local); _config.LogInfo($"- Programme Timestamp (UTC) = {localTime.ToLongDateString()} {localTime.ToLongTimeString()}"); _config.LogInfo($"- Transmission mode = {(_transmissionMode == TransmissionMode.TransmissionModeSerial ? "serial" : "parallel")}"); _states.ProgrammeInfoProcessed = true; } } } }
static void ProcessPage(TeletextPage page, TeletextRunSettings teletextRunSettings, int pageNumber) { #if DEBUG for (int row = 1; row < 25; row++) { _config.LogInfoNoNewLine($"# DEBUG[{row}]: "); for (int col = 0; col < 40; col++) { _config.LogInfo($"{(page.Text[row, col]):X2} "); } _config.LogInfo(string.Empty); } _config.LogInfo(string.Empty); #endif // optimization: slicing column by column -- higher probability we could find boxed area start mark sooner bool pageIsEmpty = true; for (var col = 0; col < 40; col++) { for (var row = 1; row < 25; row++) { if (page.Text[row, col] == 0x0b) { pageIsEmpty = false; break; } } } if (pageIsEmpty) { return; } var paragraph = new Paragraph(); var usedLines = new List <int>(); var sb = new StringBuilder(); if (page.ShowTimestamp > page.HideTimestamp) { page.HideTimestamp = page.ShowTimestamp; } paragraph.StartTime = new TimeCode(page.ShowTimestamp); paragraph.EndTime = new TimeCode(page.HideTimestamp); // process data for (var row = 1; row < 25; row++) { // anchors for string trimming purpose var colStart = 40; var colStop = 40; bool boxOpen = false; for (var col = 0; col < 40; col++) { // replace all 0/B and 0/A characters with 0/20, as specified in ETS 300 706: // Unless operating in "Hold Mosaics" mode, each character space occupied by a // spacing attribute is displayed as a SPACE if (page.Text[row, col] == 0xb) // open the box { if (colStart == 40) { colStart = col; } else { page.Text[row, col] = 0x20; } boxOpen = true; } else if (page.Text[row, col] == 0xa) // close the box { page.Text[row, col] = 0x20; boxOpen = false; } // characters between 0xA and 0xB shouldn't be displayed // page->text[row][col] > 0x20 added to preserve color information else if (!boxOpen && colStart < 40 && page.Text[row, col] > 0x20) { page.Text[row, col] = 0x20; } } // line is empty if (colStart > 39) { continue; } for (var col = colStart + 1; col <= 39; col++) { if (page.Text[row, col] > 0x20) { if (colStop > 39) { colStart = col; } colStop = col; } if (page.Text[row, col] == 0xa) { break; } } // line is empty if (colStop > 39) { continue; } // ETS 300 706, chapter 12.2: Alpha White ("Set-After") - Start-of-row default condition. // used for color changes _before_ start box mark // white is default as stated in ETS 300 706, chapter 12.2 // black(0), red(1), green(2), yellow(3), blue(4), magenta(5), cyan(6), white(7) var foregroundColor = 0x7; bool fontTagOpened = false; for (var col = 0; col <= colStop; col++) { // v is just a shortcut var v = page.Text[row, col]; if (col < colStart) { if (v <= 0x7) { foregroundColor = v; } } if (col == colStart) { if (foregroundColor != 0x7 && _config.Colors) { sb.Append($"<font color=\"{TeletextColors[foregroundColor]}\">"); fontTagOpened = true; } } if (col >= colStart) { if (v <= 0x7) { // ETS 300 706, chapter 12.2: Unless operating in "Hold Mosaics" mode, // each character space occupied by a spacing attribute is displayed as a SPACE. if (_config.Colors) { if (fontTagOpened) { sb.Append("</font> "); fontTagOpened = false; } // black is considered as white for telxcc purpose // telxcc writes <font/> tags only when needed if (v > 0x0 && v < 0x7) { sb.Append($"<font color=\"{TeletextColors[v]}\">"); fontTagOpened = true; } } else { v = 0x20; } } if (v >= 0x20) { sb.Append(Ucs2ToUtf8(v)); } } } // no tag will left opened! if (_config.Colors && fontTagOpened) { sb.Append("</font>"); } // line delimiter sb.Append(Environment.NewLine); usedLines.Add(row); } sb.AppendLine(); var topAlign = usedLines.Count > 0 && usedLines.All(p => p < 6); paragraph.Text = (topAlign ? "{\\an8}" : "") + sb.ToString().TrimEnd(); if (!teletextRunSettings.PageNumberAndParagraph.ContainsKey(pageNumber)) { teletextRunSettings.PageNumberAndParagraph.Add(pageNumber, paragraph); } else { _config.LogError("New pageId in page: " + pageNumber); } }
/// <summary> /// Can be used with e.g. MemoryStream or FileStream /// </summary> /// <param name="ms">Input stream</param> /// <param name="callback">Optional callback event to follow progress</param> public void Parse(Stream ms, LoadTransportStreamCallback callback) { IsM2TransportStream = false; NumberOfNullPackets = 0; TotalNumberOfPackets = 0; TotalNumberOfPrivateStream1 = 0; TotalNumberOfPrivateStream1Continuation0 = 0; SubtitlePacketIds = new List <int>(); SubtitlePackets = new List <Packet>(); ms.Position = 0; const int packetLength = 188; IsM2TransportStream = IsM3TransportStream(ms); var packetBuffer = new byte[packetLength]; var m2TsTimeCodeBuffer = new byte[4]; long position = 0; SubtitlesLookup = new SortedDictionary <int, List <DvbSubPes> >(); TeletextSubtitlesLookup = new SortedDictionary <int, SortedDictionary <int, List <Paragraph> > >(); var teletextPesList = new Dictionary <int, List <DvbSubPes> >(); var teletextPages = new Dictionary <int, List <int> >(); ulong?firstMs = null; ulong?firstVideoMs = null; // check for Topfield .rec file ms.Seek(position, SeekOrigin.Begin); ms.Read(m2TsTimeCodeBuffer, 0, 3); if (m2TsTimeCodeBuffer[0] == 0x54 && m2TsTimeCodeBuffer[1] == 0x46 && m2TsTimeCodeBuffer[2] == 0x72) { position = 3760; } long transportStreamLength = ms.Length; ms.Seek(position, SeekOrigin.Begin); while (position < transportStreamLength) { if (IsM2TransportStream) { ms.Read(m2TsTimeCodeBuffer, 0, m2TsTimeCodeBuffer.Length); position += m2TsTimeCodeBuffer.Length; } var bytesRead = ms.Read(packetBuffer, 0, packetLength); if (bytesRead < packetLength) { break; // incomplete packet at end-of-file } if (packetBuffer[0] == Packet.SynchronizationByte) { var packet = new Packet(packetBuffer); if (packet.IsNullPacket) { NumberOfNullPackets++; } else if (!firstVideoMs.HasValue && packet.IsVideoStream) { if (packet.Payload != null && packet.Payload.Length > 10) { int presentationTimestampDecodeTimestampFlags = packet.Payload[7] >> 6; if (presentationTimestampDecodeTimestampFlags == Helper.B00000010 || presentationTimestampDecodeTimestampFlags == Helper.B00000011) { firstVideoMs = (ulong)packet.Payload[9 + 4] >> 1; firstVideoMs += (ulong)packet.Payload[9 + 3] << 7; firstVideoMs += (ulong)(packet.Payload[9 + 2] & Helper.B11111110) << 14; firstVideoMs += (ulong)packet.Payload[9 + 1] << 22; firstVideoMs += (ulong)(packet.Payload[9 + 0] & Helper.B00001110) << 29; firstVideoMs = firstVideoMs / 90; } } } else if (packet.IsPrivateStream1 || SubtitlePacketIds.Contains(packet.PacketId)) { TotalNumberOfPrivateStream1++; if (!SubtitlePacketIds.Contains(packet.PacketId)) { SubtitlePacketIds.Add(packet.PacketId); } if (packet.PayloadUnitStartIndicator) { firstMs = ProcessPackages(packet.PacketId, teletextPages, teletextPesList, firstMs); SubtitlePackets.RemoveAll(p => p.PacketId == packet.PacketId); } SubtitlePackets.Add(packet); if (packet.ContinuityCounter == 0) { TotalNumberOfPrivateStream1Continuation0++; } } TotalNumberOfPackets++; position += packetLength; if (TotalNumberOfPackets % 100000 == 0) { callback?.Invoke(position, transportStreamLength); } } else { // sync byte not found - search for it (will be very slow!) if (IsM2TransportStream) { position -= m2TsTimeCodeBuffer.Length; } position++; ms.Seek(position, SeekOrigin.Begin); } } foreach (var pid in SubtitlePackets.GroupBy(p => p.PacketId)) { firstMs = ProcessPackages(pid.Key, teletextPages, teletextPesList, firstMs); } SubtitlePackets.Clear(); callback?.Invoke(transportStreamLength, transportStreamLength); foreach (var packetId in teletextPesList.Keys) // teletext from PES packets { foreach (var page in teletextPages[packetId].OrderBy(p => p)) { var pageBcd = Teletext.DecToBec(page); Teletext.InitializeStaticFields(packetId, pageBcd); var teletextRunSettings = new TeletextRunSettings(); foreach (var pes in teletextPesList[packetId]) { var textDictionary = pes.GetTeletext(packetId, teletextRunSettings, page, pageBcd, firstMs); foreach (var dic in textDictionary) { if (!string.IsNullOrEmpty(dic.Value.Text)) { if (TeletextSubtitlesLookup.ContainsKey(packetId)) { var innerDic = TeletextSubtitlesLookup[packetId]; if (innerDic.ContainsKey(dic.Key)) { innerDic[dic.Key].Add(dic.Value); } else { innerDic.Add(dic.Key, new List <Paragraph> { dic.Value }); } } else { TeletextSubtitlesLookup.Add(packetId, new SortedDictionary <int, List <Paragraph> > { { dic.Key, new List <Paragraph> { dic.Value } } }); } } } } } } if (IsM2TransportStream) // m2ts blu-ray images from PES packets { DvbSubtitlesLookup = new SortedDictionary <int, List <TransportStreamSubtitle> >(); foreach (int pid in SubtitlesLookup.Keys) { var bdMs = new MemoryStream(); var list = SubtitlesLookup[pid]; var currentList = new List <DvbSubPes>(); var sb = new StringBuilder(); var subList = new List <TransportStreamSubtitle>(); var offset = (long)(firstVideoMs ?? 0); // when to use firstMs ? for (var index = 0; index < list.Count; index++) { var item = list[index]; item.WriteToStream(bdMs); currentList.Add(item); if (item.DataIdentifier == 0x80) { bdMs.Position = 0; var bdList = BluRaySupParser.ParseBluRaySup(bdMs, sb, true); if (bdList.Count > 0) { var startMs = currentList.First().PresentationTimestampToMilliseconds(); var endMs = index + 1 < list.Count ? list[index + 1].PresentationTimestampToMilliseconds() : startMs + (ulong)Configuration.Settings.General.NewEmptyDefaultMs; startMs = (ulong)((long)startMs - offset); endMs = (ulong)((long)endMs - offset); subList.Add(new TransportStreamSubtitle(bdList[0], startMs, endMs)); } bdMs.Dispose(); bdMs = new MemoryStream(); currentList.Clear(); } } if (subList.Count > 0) { DvbSubtitlesLookup.Add(pid, subList); } } SubtitlePacketIds.Clear(); foreach (int key in DvbSubtitlesLookup.Keys) { SubtitlePacketIds.Add(key); } return; } SubtitlePacketIds.Clear(); foreach (int key in SubtitlesLookup.Keys) { SubtitlePacketIds.Add(key); } SubtitlePacketIds.Sort(); // Merge packets and set start/end time DvbSubtitlesLookup = new SortedDictionary <int, List <TransportStreamSubtitle> >(); foreach (int pid in SubtitlePacketIds) { var subtitles = new List <TransportStreamSubtitle>(); var list = ParseAndRemoveEmpty(GetSubtitlePesPackets(pid)); var offset = (long)(firstVideoMs ?? 0); // when to use firstMs ? for (int i = 0; i < list.Count; i++) { var pes = list[i]; pes.ParseSegments(); if (pes.ObjectDataList.Count > 0) { var sub = new TransportStreamSubtitle { StartMilliseconds = pes.PresentationTimestampToMilliseconds(), Pes = pes }; if (i + 1 < list.Count && list[i + 1].PresentationTimestampToMilliseconds() > 25) { sub.EndMilliseconds = list[i + 1].PresentationTimestampToMilliseconds() - 25; } if (sub.EndMilliseconds < sub.StartMilliseconds || sub.EndMilliseconds - sub.StartMilliseconds > (ulong)Configuration.Settings.General.SubtitleMaximumDisplayMilliseconds) { sub.EndMilliseconds = sub.StartMilliseconds + (ulong)Configuration.Settings.General.SubtitleMaximumDisplayMilliseconds; } if (offset <= (long)sub.StartMilliseconds || offset < 0) { sub.StartMilliseconds = (ulong)((long)sub.StartMilliseconds - offset); sub.EndMilliseconds = (ulong)((long)sub.EndMilliseconds - offset); } else { if (subtitles.Count > 0) { offset = (long)sub.StartMilliseconds - (long)subtitles[subtitles.Count - 1].EndMilliseconds + 1000; sub.StartMilliseconds = (ulong)((long)sub.StartMilliseconds - offset); sub.EndMilliseconds = (ulong)((long)sub.EndMilliseconds - offset); } } subtitles.Add(sub); } } DvbSubtitlesLookup.Add(pid, subtitles); } SubtitlePacketIds.Clear(); foreach (int key in DvbSubtitlesLookup.Keys) { if (DvbSubtitlesLookup[key].Count > 0) { SubtitlePacketIds.Add(key); } } SubtitlePacketIds.Sort(); }