Verify decoder integration E2E and fix live metadata probing and mapping
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<AssemblyVersion>7.0.4.0</AssemblyVersion>
|
||||
<FileVersion>7.0.4.0</FileVersion>
|
||||
<AssemblyVersion>7.0.6.0</AssemblyVersion>
|
||||
<FileVersion>7.0.6.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net6.0|AnyCPU'">
|
||||
|
||||
@@ -2,7 +2,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -23,8 +25,9 @@ public sealed class LiveStreamProbe
|
||||
public LiveStreamProbe(IMediaEncoder encoder) : this((url, token) => Run(encoder.ProbePath, url, token)) { }
|
||||
public LiveStreamProbe(Func<string, CancellationToken, Task<string>> runner, TimeSpan? timeout = null) { this.runner = runner; this.timeout = timeout ?? TimeSpan.FromSeconds(15); }
|
||||
|
||||
public Task<List<MediaStream>> Get(string url, CancellationToken caller)
|
||||
public Task<List<MediaStream>> Get(string url, CancellationToken caller, int? programme = null)
|
||||
{
|
||||
if (programme != null) url += "?programme=" + programme.Value.ToString(CultureInfo.InvariantCulture);
|
||||
Task<List<MediaStream>> task;
|
||||
lock (gate)
|
||||
{
|
||||
@@ -48,7 +51,7 @@ public sealed class LiveStreamProbe
|
||||
try
|
||||
{
|
||||
await slots.WaitAsync(deadline.Token);
|
||||
try { completion.TrySetResult(Parse(await runner(url, deadline.Token))); success = true; }
|
||||
try { completion.TrySetResult(Parse(await runner(url, deadline.Token))); 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)); }
|
||||
@@ -56,9 +59,45 @@ public sealed class LiveStreamProbe
|
||||
}
|
||||
|
||||
private static async Task<string> Run(string executable, string url, CancellationToken token)
|
||||
{
|
||||
// ffprobe may wait for an EPG/data PID forever on live MPEG-TS. Give it a finite
|
||||
// sample from the shared relay, preserving the same PID/index ordering.
|
||||
var sample = Path.Combine(Path.GetTempPath(), "enigma-probe-" + Guid.NewGuid().ToString("N") + ".ts");
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient(new SocketsHttpHandler { ConnectTimeout = TimeSpan.FromSeconds(3) }) { Timeout = Timeout.InfiniteTimeSpan };
|
||||
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
using var input = await response.Content.ReadAsStreamAsync(token);
|
||||
await using (var file = new FileStream(sample, FileMode.CreateNew, FileAccess.Write, FileShare.None, 65536, useAsync: true))
|
||||
{
|
||||
using var capture = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
capture.CancelAfter(TimeSpan.FromSeconds(4));
|
||||
var buffer = new byte[188 * 256];
|
||||
try
|
||||
{
|
||||
while (file.Length < 4 * 1024 * 1024)
|
||||
{
|
||||
var count = await input.ReadAsync(buffer, capture.Token);
|
||||
if (count == 0) break;
|
||||
await file.WriteAsync(buffer.AsMemory(0, count), token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (!token.IsCancellationRequested && file.Length > 0) { }
|
||||
if (file.Length == 0) throw new InvalidOperationException("Decoder returned an empty probe sample.");
|
||||
}
|
||||
var query = new Uri(url).Query;
|
||||
var programme = query.StartsWith("?programme=", StringComparison.Ordinal) && int.TryParse(query.Substring(11), out var value) ? value : (int?)null;
|
||||
return await RunFile(executable, sample, programme, token);
|
||||
}
|
||||
finally { if (File.Exists(sample)) File.Delete(sample); }
|
||||
}
|
||||
|
||||
private static async Task<string> RunFile(string executable, string sample, int? programme, 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", "-rw_timeout", "8000000", "-seekable", "0", "-skip_estimate_duration_from_pts", "1", "-f", "mpegts", "-analyzeduration", "1500000", "-probesize", "1048576", "-fpsprobesize", "10", "-i", url, "-show_streams", "-of", "json" }) process.StartInfo.ArgumentList.Add(argument);
|
||||
foreach (var argument in new[] { "-v", "error", "-f", "mpegts", "-analyzeduration", "3000000", "-probesize", "4194304", "-i", sample, "-show_streams", "-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();
|
||||
var errors = process.StandardError.ReadToEndAsync();
|
||||
@@ -102,4 +141,15 @@ public sealed class LiveStreamProbe
|
||||
if (!result.Any(s => s.Type == MediaStreamType.Video && s.Width > 0 && s.Height > 0)) throw new InvalidOperationException("Decoder stream has no identified video.");
|
||||
return result;
|
||||
}
|
||||
|
||||
public static MediaStream ParseLiveMetadata(MediaStream s) => new MediaStream
|
||||
{
|
||||
Index = -1, Type = s.Type, Codec = s.Codec, Width = s.Width, Height = s.Height,
|
||||
Channels = s.Channels, SampleRate = s.SampleRate, BitRate = s.BitRate,
|
||||
Profile = s.Profile, Level = s.Level, PixelFormat = s.PixelFormat,
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1173,7 +1173,14 @@ namespace Jellyfin.Plugin.Enigma2
|
||||
|
||||
var streamUrl = string.Format("{0}/{1}{2}", baseUrl, channelOid, trancodingUrl);
|
||||
streamUrl = _streamRelay.Register(streamUrl);
|
||||
var streams = await _streamProbe.Get(streamUrl, cancellationToken).ConfigureAwait(false);
|
||||
var fields = channelOid.Split(':');
|
||||
var programme = !Plugin.Instance.Configuration.TranscodedStream && fields.Length > 3 && int.TryParse(fields[3], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var sid) ? sid : (int?)null;
|
||||
var streams = await _streamProbe.Get(streamUrl, cancellationToken, programme).ConfigureAwait(false);
|
||||
|
||||
// A live TS may enumerate its EPG/data PID before AV on the next open.
|
||||
// Jellyfin maps positions in MediaStreams rather than MPEG-TS PID values.
|
||||
// Its supported unknown-index path selects AV automatically and omits data.
|
||||
streams = streams.Select(s => LiveStreamProbe.ParseLiveMetadata(s)).ToList();
|
||||
|
||||
return new MediaSourceInfo
|
||||
{
|
||||
@@ -1183,6 +1190,7 @@ namespace Jellyfin.Plugin.Enigma2
|
||||
Container = Plugin.Instance.Configuration.TranscodedStream ? "mpegts" : "ts",
|
||||
IsInfiniteStream = true,
|
||||
SupportsProbing = false,
|
||||
AnalyzeDurationMs = 1500,
|
||||
SupportsDirectPlay = false,
|
||||
MediaStreams = streams
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user