using System; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using MediaBrowser.Model.Entities; namespace Jellyfin.Plugin.Enigma2; /// Only the status picture is encoded. Unsupported programs stay entirely in stream-copy mode. public sealed class LosslessSlateProfile { public int Width { get; private init; } public int Height { get; private init; } public double Rate { get; private init; } public string RateArgument { get; private init; } public string Aspect { get; private init; } public string Profile { get; private init; } public int Gop { get; private init; } public double SegmentSeconds => Gop / Rate; public static LosslessSlateProfile Create(BroadcastTracks tracks) { var video = tracks.Input[0]; // A guessed codec/layout is unsafe during cold-start outages. Never convert the channel to fit a slate. if (video.Codec != "h264" || video.PixelFormat != "yuv420p" || video.IsInterlaced || video.Width is not (> 0 and <= 16384) || video.Height is not (> 0 and <= 16384) || video.Width % 2 != 0 || video.Height % 2 != 0 || video.ColorTransfer is "smpte2084" or "arib-std-b67" || tracks.AudioCount == 0 || tracks.AudioCount > 8 || tracks.Input.Where(s => s.Type == MediaStreamType.Audio).Any(s => s.Codec != "aac" || s.Profile != "LC" || s.Channels != 2 || s.SampleRate != 48000)) return null; var profile = video.Profile switch { "Constrained Baseline" or "Baseline" => "baseline", "Main" => "main", "High" => "high", _ => null }; var rate = (double)(video.AverageFrameRate ?? video.RealFrameRate ?? 0); var aspect = (video as BroadcastMediaStream)?.SampleAspectRatio; if (profile == null || !double.IsFinite(rate) || rate <= 0 || rate > 240 || aspect == null || !Regex.IsMatch(aspect, @"^[1-9][0-9]{0,5}:[1-9][0-9]{0,5}$")) return null; var rational = (video as BroadcastMediaStream)?.FrameRateRational; if (rational == null || !Regex.IsMatch(rational, @"^[1-9][0-9]{0,8}/[1-9][0-9]{0,8}$")) rational = rate.ToString("0.########", CultureInfo.InvariantCulture); // Repeating a cached clip must end on complete video AND AAC frames. // If that needs a long clip (e.g. 23.976/59.94), retain the original // broadcast without a status clip rather than rounding its cadence. var gop = Enumerable.Range(Math.Max(1, (int)(rate * 1.5)), Math.Max(1, (int)(rate * 1.5) + 2)) .Where(n => Math.Abs(n / rate * 48000 / 1024 - Math.Round(n / rate * 48000 / 1024)) < 0.00001) .OrderBy(n => Math.Abs(n / rate - ContinuityTimeline.SegmentSeconds)).FirstOrDefault(); if (gop == 0) return null; return new() { Width = video.Width.Value, Height = video.Height.Value, Rate = rate, RateArgument = rational, Aspect = aspect.Replace(':', '/'), Profile = profile, Gop = gop }; } }