Files
jellyfin-plugin-enigma2/Jellyfin.Plugin.Enigma2/LosslessSlateProfile.cs
T

49 lines
2.9 KiB
C#

using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using MediaBrowser.Model.Entities;
namespace Jellyfin.Plugin.Enigma2;
/// <summary>Only the status picture is encoded. Unsupported programs stay entirely in stream-copy mode.</summary>
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 };
}
}