using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using MediaBrowser.Model.Entities; namespace Jellyfin.Plugin.Enigma2; /// Declare silent audio and empty DVB subtitle tracks on status clips with stable transport PIDs. public static class BroadcastSlateProgram { public static byte[] Apply(byte[] clip, BroadcastTracks tracks, bool clearSubtitles = true) { if (clip.Length % 188 != 0) throw new InvalidDataException("Unaligned status clip"); var section = Section(tracks); using var output = new MemoryStream(clip.Length + 188 * 8); var cc = 0; var written = false; var cleared = false; for (var start = 0; start < clip.Length; start += 188) { var pid = ((clip[start + 1] & 31) << 8) | clip[start + 2]; if (pid != 4096) { output.Write(clip, start, 188); if (clearSubtitles && !cleared && pid == 256 && (clip[start + 1] & 64) != 0) { var p = start + 4 + ((clip[start + 3] & 32) != 0 ? clip[start + 4] + 1 : 0); if (p + 14 <= start + 188 && clip[p] == 0 && clip[p + 1] == 0 && clip[p + 2] == 1 && (clip[p + 7] & 128) != 0) { long pts = ((long)(clip[p + 9] & 14) << 29) | ((long)clip[p + 10] << 22) | ((long)(clip[p + 11] & 254) << 14) | ((long)clip[p + 12] << 7) | (clip[p + 13] >> 1); for (var i = 0; i < tracks.Output.Length; i++) if (tracks.Output[i].Codec == "DVBSUB") output.Write(ClearSubtitle(tracks.Input[i], 256 + i, pts)); cleared = true; } } continue; } if ((clip[start + 1] & 64) == 0) continue; written = true; var offset = 0; while (offset < section.Length) { var packet = new byte[188]; Array.Fill(packet, (byte)255); packet[0] = 71; packet[1] = (byte)(16 | (offset == 0 ? 64 : 0)); packet[2] = 0; packet[3] = (byte)(16 | (cc++ & 15)); var p = 4; if (offset == 0) packet[p++] = 0; var count = Math.Min(188 - p, section.Length - offset); section.AsSpan(offset, count).CopyTo(packet.AsSpan(p)); offset += count; output.Write(packet); } } if (!written) throw new InvalidDataException("Status clip has no expected program map"); return output.ToArray(); } private static byte[] ClearSubtitle(MediaStream stream, int pid, long pts) { var extra = (stream as BroadcastMediaStream)?.SubtitleExtraData; var page = extra is { Length: >= 2 } ? (extra[0] << 8) | extra[1] : 1; var payload = new byte[] { 32, 0, 15, 20, (byte)(page >> 8), (byte)page, 0, 5, 0, 7, 127, 4, 55, 15, 16, (byte)(page >> 8), (byte)page, 0, 2, 1, 3, 15, 128, (byte)(page >> 8), (byte)page, 0, 0, 255 }; var pes = new List { 0, 0, 1, 189, 0, (byte)(payload.Length + 8), 128, 128, 5, (byte)(33L | ((pts >> 29) & 14)), (byte)(pts >> 22), (byte)(1L | ((pts >> 14) & 254)), (byte)(pts >> 7), (byte)(1L | (pts << 1)) }; pes.AddRange(payload); var packet = Enumerable.Repeat((byte)255, 188).ToArray(); packet[0] = 71; packet[1] = (byte)(64 | (pid >> 8)); packet[2] = (byte)pid; packet[3] = 48; packet[4] = (byte)(183 - pes.Count); packet[5] = 0; pes.CopyTo(packet, 188 - pes.Count); return packet; } public static byte[] Section(BroadcastTracks tracks) { var data = new List { 2, 176, 0, 0, 1, 193, 0, 0, 225, 0, 240, 0 }; for (var i = 0; i < tracks.Output.Length; i++) { var s = tracks.Output[i]; var descriptors = new List(); var language = (s.Language ?? "und").Split(',')[0]; if (language.Length != 3 || language.Any(c => c < 'a' || c > 'z')) language = "und"; var lang = Encoding.ASCII.GetBytes(language); var type = s.Type == MediaStreamType.Video ? 27 : s.Type == MediaStreamType.Audio ? 15 : 6; if (s.Type == MediaStreamType.Audio) { descriptors.AddRange(new byte[] { 10, 4 }); descriptors.AddRange(lang); descriptors.Add((byte)(s.IsHearingImpaired ? 2 : 0)); } if (s.Type == MediaStreamType.Subtitle) { var extra = (tracks.Input[i] as BroadcastMediaStream)?.SubtitleExtraData ?? Array.Empty(); if (s.Codec == "DVBSUB") { descriptors.AddRange(new byte[] { 89, 8 }); descriptors.AddRange(lang); descriptors.Add((byte)(s.IsHearingImpaired ? 32 : 16)); // ffprobe DVB extradata stores composition/ancillary page IDs in big endian. descriptors.AddRange(extra.Length >= 4 ? extra.Take(4) : new byte[] { 0, 1, 0, 1 }); } else if (s.Codec == "DVBTXT") { descriptors.AddRange(new byte[] { 86, 5 }); descriptors.AddRange(lang); descriptors.Add(extra.Length >= 2 ? extra[0] : (byte)17); descriptors.Add(extra.Length >= 2 ? extra[1] : (byte)0); } } var pid = 256 + i; data.Add((byte)type); data.Add((byte)(224 | (pid >> 8))); data.Add((byte)pid); data.Add((byte)(240 | (descriptors.Count >> 8))); data.Add((byte)descriptors.Count); data.AddRange(descriptors); } var length = data.Count - 3 + 4; if (length > 1021) throw new InvalidDataException("Broadcast program map exceeds MPEG-TS limit"); data[1] = (byte)(176 | (length >> 8)); data[2] = (byte)length; uint crc = 0xffffffff; foreach (var value in data) { crc ^= (uint)value << 24; for (var bit = 0; bit < 8; bit++) crc = (crc & 0x80000000) != 0 ? (crc << 1) ^ 0x04c11db7 : crc << 1; } data.Add((byte)(crc >> 24)); data.Add((byte)(crc >> 16)); data.Add((byte)(crc >> 8)); data.Add((byte)crc); return data.ToArray(); } }