using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using MediaBrowser.Model.Entities; namespace Jellyfin.Plugin.Enigma2; // Probe-only transport details; the public Jellyfin contract remains MediaStream. public sealed class BroadcastMediaStream : MediaStream { public int? TransportId { get; set; } public bool IsVisualImpaired { get; set; } public byte[] SubtitleExtraData { get; set; } } /// A stable, explicitly mapped program shared by native negotiation and FFmpeg. public sealed class BroadcastTracks { public MediaStream[] Input { get; } public MediaStream[] Output { get; } public bool Normalize { get; } public string Key { get; } public int AudioCount => Input.Count(s => s.Type == MediaStreamType.Audio); public bool HasSubtitles => Input.Any(s => s.Type == MediaStreamType.Subtitle); public BroadcastTracks(IEnumerable streams, bool normalize = true, bool preserveResolution = true) { Normalize = normalize; var all = streams.ToArray(); Input = all.Where(s => s.Type == MediaStreamType.Video).Take(1) .Concat(all.Where(s => s.Type == MediaStreamType.Audio)) .Concat(all.Where(s => s.Type == MediaStreamType.Subtitle)).ToArray(); if (Input.Length == 0 || Input[0].Type != MediaStreamType.Video || Input.Length > 48) throw new ArgumentException("Invalid broadcast program layout"); if (Input.Any(s => s.Type == MediaStreamType.Subtitle && s.Codec is not ("DVBSUB" or "DVBTXT"))) throw new NotSupportedException("Unsupported broadcast subtitle codec; tracks cannot be silently discarded"); Output = Input.Select((s, i) => { var copy = LiveStreamProbe.ParseLiveMetadata(s); copy.Index = i; if (s is BroadcastMediaStream { IsVisualImpaired: true } && string.IsNullOrWhiteSpace(copy.Title)) copy.Title = "Audio description"; if (normalize && copy.Type == MediaStreamType.Video) { copy.Codec = "h264"; copy.Width = preserveResolution ? s.Width : ContinuityStreams.Width; copy.Height = preserveResolution ? s.Height : ContinuityStreams.Height; copy.Level = null; copy.Profile = "Constrained Baseline"; copy.PixelFormat = "yuv420p"; copy.IsInterlaced = false; copy.AverageFrameRate = copy.RealFrameRate = ContinuityStreams.FrameRate; copy.ColorRange = copy.ColorSpace = copy.ColorTransfer = copy.ColorPrimaries = null; copy.BitRate = null; } if (normalize && copy.Type == MediaStreamType.Audio) { copy.Codec = "aac"; copy.Channels = 2; copy.SampleRate = 48000; copy.BitRate = 128000; copy.Profile = "LC"; copy.ChannelLayout = "stereo"; } return copy; }).ToArray(); // Stable per-channel track cache; no URLs, credentials or user identities in its key. Key = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(Input.Select(s => new { s.Type, s.Index, s.Codec, s.Language, s.Title, s.IsDefault, s.IsForced, s.IsHearingImpaired, s.Channels, s.Width, s.Height, Pid = (s as BroadcastMediaStream)?.TransportId, Extra = (s as BroadcastMediaStream)?.SubtitleExtraData }))))).ToLowerInvariant(); } public IEnumerable MapArguments() { for (var i = 0; i < Input.Length; i++) { yield return "-map"; yield return Input[i] is BroadcastMediaStream { TransportId: { } pid } ? "0:i:" + pid.ToString(CultureInfo.InvariantCulture) : Input[i].Index >= 0 ? "0:" + Input[i].Index.ToString(CultureInfo.InvariantCulture) : "0:" + (Input[i].Type == MediaStreamType.Video ? "v" : Input[i].Type == MediaStreamType.Audio ? "a" : "s") + ":" + Input.Take(i).Count(s => s.Type == Input[i].Type); yield return "-streamid"; yield return i.ToString(CultureInfo.InvariantCulture) + ":" + (256 + i).ToString(CultureInfo.InvariantCulture); } } public IEnumerable MetadataArguments(bool audioVideoOnly = false) { for (var i = 0; i < Output.Length; i++) { var s = Output[i]; if (audioVideoOnly && s.Type == MediaStreamType.Subtitle) continue; if (!string.IsNullOrWhiteSpace(s.Language)) { yield return "-metadata:s:" + i; yield return "language=" + s.Language; } if (!string.IsNullOrWhiteSpace(s.Title)) { yield return "-metadata:s:" + i; yield return "title=" + s.Title; } yield return "-disposition:" + i; var flags = new List(); if (s.IsDefault) flags.Add("default"); if (s.IsForced) flags.Add("forced"); if (s.IsHearingImpaired) flags.Add("hearing_impaired"); yield return flags.Count == 0 ? "0" : string.Join('+', flags); } } public static string JellyfinCodec(string codec) => codec switch { "dvb_subtitle" => "DVBSUB", "dvb_teletext" => "DVBTXT", "hdmv_pgs_subtitle" => "PGSSUB", "dvd_subtitle" => "DVDSUB", "subrip" => "srt", "ass" => "ssa", _ => codec }; public static int? TransportId(string value) { if (value != null && value.StartsWith("0x", StringComparison.OrdinalIgnoreCase) && int.TryParse(value.AsSpan(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hex) && hex < 8191 && hex >= 0) return hex; return int.TryParse(value, out var dec) && dec >= 0 && dec < 8191 ? dec : null; } public static byte[] ExtraData(string value) { if (value == null || value.Length > 8192) return Array.Empty(); var hex = new StringBuilder(); foreach (var line in value.Split('\n')) { var match = Regex.Match(line, @"^[0-9a-fA-F]{8}: ([0-9a-fA-F ]+?)(?: |$)"); if (match.Success) hex.Append(match.Groups[1].Value.Replace(" ", "")); } return hex.Length % 2 == 0 ? Convert.FromHexString(hex.ToString()) : Array.Empty(); } public static BroadcastTracks Fallback(bool preserveResolution = true) => new(new[] { new MediaStream { Type = MediaStreamType.Video, Index = -1, Codec = "h264" }, new MediaStream { Type = MediaStreamType.Audio, Index = -1, Codec = "aac", IsDefault = true } }, true, preserveResolution); }