110 lines
6.4 KiB
C#
110 lines
6.4 KiB
C#
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; }
|
|
}
|
|
|
|
/// <summary>A stable, explicitly mapped program shared by native negotiation and FFmpeg.</summary>
|
|
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<MediaStream> 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<string> 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<string> 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<string>(); 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<byte>();
|
|
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<byte>();
|
|
}
|
|
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);
|
|
}
|