Retain selectable audio languages and DVB subtitles through live continuity
This commit is contained in:
@@ -19,6 +19,7 @@ public sealed class LiveStreamProbe
|
||||
private readonly Func<string, CancellationToken, Task<string>> runner;
|
||||
private readonly Dictionary<string, (Task<List<MediaStream>> Task, DateTime Until)> cache = new();
|
||||
private readonly object gate = new();
|
||||
private readonly Dictionary<string, (List<MediaStream> Streams, DateTime At)> lastKnown = new();
|
||||
private readonly SemaphoreSlim slots = new(8);
|
||||
private readonly TimeSpan timeout;
|
||||
public string LastError { get; private set; }
|
||||
@@ -64,6 +65,12 @@ public sealed class LiveStreamProbe
|
||||
return task.WaitAsync(caller);
|
||||
}
|
||||
|
||||
public List<MediaStream> LastKnown(string url, int? programme = null)
|
||||
{
|
||||
if (programme != null) url += "?programme=" + programme.Value.ToString(CultureInfo.InvariantCulture);
|
||||
lock (gate) return lastKnown.TryGetValue(url, out var value) && DateTime.UtcNow - value.At <= TimeSpan.FromHours(6) ? value.Streams.ToList() : null;
|
||||
}
|
||||
|
||||
private async Task Produce(string url, TaskCompletionSource<List<MediaStream>> completion)
|
||||
{
|
||||
var success = false;
|
||||
@@ -71,7 +78,17 @@ public sealed class LiveStreamProbe
|
||||
try
|
||||
{
|
||||
await slots.WaitAsync(deadline.Token);
|
||||
try { completion.TrySetResult(Parse(await runner(url, deadline.Token), Programme(url))); LastError = null; success = true; }
|
||||
try
|
||||
{
|
||||
var streams = Parse(await runner(url, deadline.Token), Programme(url));
|
||||
lock (gate)
|
||||
{
|
||||
foreach (var stale in lastKnown.Where(x => DateTime.UtcNow - x.Value.At > TimeSpan.FromHours(6)).Select(x => x.Key).ToArray()) lastKnown.Remove(stale);
|
||||
if (lastKnown.Count >= 256 && !lastKnown.ContainsKey(url)) lastKnown.Remove(lastKnown.OrderBy(x => x.Value.At).First().Key);
|
||||
lastKnown[url] = (streams, DateTime.UtcNow);
|
||||
}
|
||||
completion.TrySetResult(streams); LastError = null; success = true;
|
||||
}
|
||||
finally { slots.Release(); }
|
||||
}
|
||||
catch (Exception error) { LastError = error.Message; completion.TrySetException(new InvalidOperationException("Decoder returned no usable live stream within the probe deadline.", error)); }
|
||||
@@ -265,7 +282,7 @@ public sealed class LiveStreamProbe
|
||||
private static async Task<string> RunFile(string executable, string sample, int? programme, bool extended, CancellationToken token)
|
||||
{
|
||||
using var process = new Process { StartInfo = new ProcessStartInfo(executable) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true } };
|
||||
foreach (var argument in new[] { "-v", "error", "-f", "mpegts", "-analyzeduration", extended ? "20000000" : "3000000", "-probesize", extended ? "25165824" : "4194304", "-i", sample, "-show_streams", "-show_programs", "-of", "json" }) process.StartInfo.ArgumentList.Add(argument);
|
||||
foreach (var argument in new[] { "-v", "error", "-f", "mpegts", "-analyzeduration", extended ? "20000000" : "3000000", "-probesize", extended ? "25165824" : "4194304", "-i", sample, "-show_streams", "-show_programs", "-show_data", "-of", "json" }) process.StartInfo.ArgumentList.Add(argument);
|
||||
if (programme != null) { process.StartInfo.ArgumentList.Add("-select_streams"); process.StartInfo.ArgumentList.Add("p:" + programme.Value.ToString(CultureInfo.InvariantCulture)); }
|
||||
process.Start();
|
||||
var output = process.StandardOutput.ReadToEndAsync();
|
||||
@@ -302,7 +319,7 @@ public sealed class LiveStreamProbe
|
||||
string Text(string field) => item.TryGetProperty(field, out var value) ? value.ToString() : null;
|
||||
int? Number(string field) => int.TryParse(Text(field), out var value) ? value : null;
|
||||
var type = Text("codec_type");
|
||||
if (type != "video" && type != "audio") continue;
|
||||
if (type != "video" && type != "audio" && type != "subtitle") continue;
|
||||
var codec = Text("codec_name"); var index = Number("index");
|
||||
if (string.IsNullOrEmpty(codec) || index == null) continue;
|
||||
float? Rate(string field)
|
||||
@@ -312,8 +329,19 @@ public sealed class LiveStreamProbe
|
||||
if (values.Length == 1) return numerator;
|
||||
return float.TryParse(values[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var denominator) && denominator != 0 ? numerator / denominator : null;
|
||||
}
|
||||
var stream = new MediaStream { Type = type == "video" ? MediaStreamType.Video : MediaStreamType.Audio, Index = index.Value, Codec = codec, Width = Number("width"), Height = Number("height"), Channels = Number("channels"), SampleRate = Number("sample_rate"), BitRate = Number("bit_rate"), Profile = Text("profile"), Level = Number("level"), PixelFormat = Text("pix_fmt"), ColorRange = Text("color_range"), ColorSpace = Text("color_space"), ColorTransfer = Text("color_transfer"), ColorPrimaries = Text("color_primaries"), AverageFrameRate = Rate("avg_frame_rate"), RealFrameRate = Rate("r_frame_rate"), IsInterlaced = Text("field_order") is "tt" or "bb" or "tb" or "bt", ChannelLayout = Text("channel_layout") };
|
||||
var stream = new BroadcastMediaStream { TransportId = BroadcastTracks.TransportId(Text("id")), SubtitleExtraData = BroadcastTracks.ExtraData(Text("extradata")), Type = type == "video" ? MediaStreamType.Video : type == "audio" ? MediaStreamType.Audio : MediaStreamType.Subtitle, Index = index.Value, Codec = BroadcastTracks.JellyfinCodec(codec), Width = Number("width"), Height = Number("height"), Channels = Number("channels"), SampleRate = Number("sample_rate"), BitRate = Number("bit_rate"), Profile = Text("profile"), Level = Number("level"), PixelFormat = Text("pix_fmt"), ColorRange = Text("color_range"), ColorSpace = Text("color_space"), ColorTransfer = Text("color_transfer"), ColorPrimaries = Text("color_primaries"), AverageFrameRate = Rate("avg_frame_rate"), RealFrameRate = Rate("r_frame_rate"), IsInterlaced = Text("field_order") is "tt" or "bb" or "tb" or "bt", ChannelLayout = Text("channel_layout") };
|
||||
if (item.TryGetProperty("tags", out var tags) && tags.TryGetProperty("language", out var language)) stream.Language = language.GetString();
|
||||
if (item.TryGetProperty("tags", out var labels))
|
||||
{
|
||||
if (labels.TryGetProperty("title", out var title)) stream.Title = title.GetString();
|
||||
else if (labels.TryGetProperty("handler_name", out var handler)) stream.Title = handler.GetString();
|
||||
}
|
||||
if (item.TryGetProperty("disposition", out var disposition))
|
||||
{
|
||||
bool Flag(string key) => disposition.TryGetProperty(key, out var flag) && flag.TryGetInt32(out var value) && value != 0;
|
||||
stream.IsDefault = Flag("default"); stream.IsForced = Flag("forced");
|
||||
stream.IsHearingImpaired = Flag("hearing_impaired"); stream.IsVisualImpaired = Flag("visual_impaired"); stream.IsOriginal = Flag("original");
|
||||
}
|
||||
result.Add(stream);
|
||||
}
|
||||
if (!result.Any(s => s.Type == MediaStreamType.Video && s.Width > 0 && s.Height > 0)) throw new InvalidOperationException("Decoder stream has no identified video.");
|
||||
@@ -328,6 +356,6 @@ public sealed class LiveStreamProbe
|
||||
ColorRange = s.ColorRange, ColorSpace = s.ColorSpace, ColorTransfer = s.ColorTransfer,
|
||||
ColorPrimaries = s.ColorPrimaries, AverageFrameRate = s.AverageFrameRate,
|
||||
RealFrameRate = s.RealFrameRate, IsInterlaced = s.IsInterlaced,
|
||||
ChannelLayout = s.ChannelLayout, Language = s.Language
|
||||
ChannelLayout = s.ChannelLayout, Language = s.Language, Title = s.Title, IsDefault = s.IsDefault, IsForced = s.IsForced, IsHearingImpaired = s.IsHearingImpaired, IsOriginal = s.IsOriginal
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user