Files
jellyfin-plugin-enigma2/Jellyfin.Plugin.Enigma2/BroadcastSlateProgram.cs

109 lines
6.3 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Plugin.Enigma2;
/// <summary>Declare silent audio and empty DVB subtitle tracks on status clips with stable transport PIDs.</summary>
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) | ((long)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,
tracks.Input[0].Width ?? ContinuityStreams.Width, tracks.Input[0].Height ?? ContinuityStreams.Height));
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, int width, int height)
{
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, (byte)((width - 1) >> 8), (byte)(width - 1), (byte)((height - 1) >> 8), (byte)(height - 1),
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<byte> { 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<byte> { 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<byte>();
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<byte>();
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();
}
}