Verify decoder integration E2E and fix live metadata probing and mapping

This commit is contained in:
2026-10-12 21:41:26 +02:00
parent cecd5b5625
commit f5cdfa285e
15 changed files with 657 additions and 17 deletions
+53 -3
View File
@@ -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
};
}