Align decoder startup deadlines and deploy resilient 7.0.8
This commit is contained in:
@@ -39,6 +39,12 @@ public sealed class LiveStreamProbe
|
||||
}
|
||||
}
|
||||
|
||||
// Distinguish a rejected finite sample from process startup/I/O/cancellation failures.
|
||||
public sealed class ProbeProcessException : InvalidOperationException
|
||||
{
|
||||
public ProbeProcessException(int exitCode) : base("Decoder stream probe process failed (exit code " + exitCode.ToString(CultureInfo.InvariantCulture) + ").") { }
|
||||
}
|
||||
|
||||
public Task<List<MediaStream>> Get(string url, CancellationToken caller, int? programme = null)
|
||||
{
|
||||
if (programme != null) url += "?programme=" + programme.Value.ToString(CultureInfo.InvariantCulture);
|
||||
@@ -166,12 +172,19 @@ public sealed class LiveStreamProbe
|
||||
if (await Snapshot(limits.InitialBytes))
|
||||
{
|
||||
attempts = 1;
|
||||
json = await fileProbe(samplePath, programme, false, token);
|
||||
analysisReturned = true;
|
||||
try { Parse(json, programme); return json; }
|
||||
catch (Exception error) when (error is InvalidOperationException || error is JsonException)
|
||||
try
|
||||
{
|
||||
cause = "initial_video_unavailable";
|
||||
json = await fileProbe(samplePath, programme, false, token);
|
||||
analysisReturned = true;
|
||||
}
|
||||
catch (ProbeProcessException) { cause = "initial_probe_process_failed"; }
|
||||
if (analysisReturned)
|
||||
{
|
||||
try { Parse(json, programme); return json; }
|
||||
catch (Exception error) when (error is InvalidOperationException || error is JsonException)
|
||||
{
|
||||
cause = "initial_video_unavailable";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +211,7 @@ public sealed class LiveStreamProbe
|
||||
cause = error is OperationCanceledException ? "deadline"
|
||||
: error is IOException ? "capture_io_failure"
|
||||
: error is HttpRequestException ? "capture_http_failure"
|
||||
: error.Message.StartsWith("Decoder stream probe process failed", StringComparison.Ordinal) ? "probe_process_failed"
|
||||
: error is ProbeProcessException ? "probe_process_failed"
|
||||
: attempts > 0 && !analysisReturned ? "probe_analysis_failure"
|
||||
: attempts > 0 ? ProbeCause(json, programme) : capturedBytes == 0 ? "capture_empty" : "capture_unavailable";
|
||||
throw new InvalidOperationException($"Decoder probe failed: cause={cause}; expectedProgramme={programme?.ToString(CultureInfo.InvariantCulture) ?? "none"}; {ProbeSummary(json)}; sampleBytes={sampleBytes}; capturedBytes={capturedBytes}; elapsedMs={watch.ElapsedMilliseconds}; attempts={attempts}; captureStop={captureStop}.");
|
||||
@@ -261,7 +274,7 @@ public sealed class LiveStreamProbe
|
||||
{
|
||||
await process.WaitForExitAsync(token);
|
||||
await errors;
|
||||
if (process.ExitCode != 0) throw new InvalidOperationException("Decoder stream probe process failed (exit code " + process.ExitCode.ToString(CultureInfo.InvariantCulture) + ").");
|
||||
if (process.ExitCode != 0) throw new ProbeProcessException(process.ExitCode);
|
||||
return await output;
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -23,8 +23,17 @@ public sealed class LiveStreamRelay : IDisposable
|
||||
private readonly object gate = new();
|
||||
private int opened;
|
||||
private readonly TimeSpan idleGrace;
|
||||
private readonly TimeSpan startupTimeout;
|
||||
private readonly TimeSpan readTimeout;
|
||||
public LiveStreamRelay(IServerApplicationHost host) : this(new HttpClient(new SocketsHttpHandler { ConnectTimeout = TimeSpan.FromSeconds(3), AllowAutoRedirect = false }), host.HttpPort) { }
|
||||
public LiveStreamRelay(HttpClient client, int port, TimeSpan? idleGrace = null) { this.client = client; client.Timeout = Timeout.InfiniteTimeSpan; this.port = port; this.idleGrace = idleGrace ?? TimeSpan.FromSeconds(15); }
|
||||
public LiveStreamRelay(HttpClient client, int port, TimeSpan? idleGrace = null, TimeSpan? startupTimeout = null, TimeSpan? readTimeout = null)
|
||||
{
|
||||
this.startupTimeout = startupTimeout ?? TimeSpan.FromSeconds(20);
|
||||
this.readTimeout = readTimeout ?? TimeSpan.FromSeconds(8);
|
||||
if (this.startupTimeout <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(startupTimeout));
|
||||
if (this.readTimeout <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(readTimeout));
|
||||
this.client = client; client.Timeout = Timeout.InfiniteTimeSpan; this.port = port; this.idleGrace = idleGrace ?? TimeSpan.FromSeconds(15);
|
||||
}
|
||||
public string Register(string url)
|
||||
{
|
||||
lock (gate)
|
||||
@@ -96,7 +105,7 @@ public sealed class LiveStreamRelay : IDisposable
|
||||
foreach (var chunk in history) subscription.Buffer.Writer.TryWrite(chunk);
|
||||
initial = ready.Task;
|
||||
}
|
||||
try { await initial.WaitAsync(TimeSpan.FromSeconds(12), token); return subscription; }
|
||||
try { await initial.WaitAsync(owner.startupTimeout + TimeSpan.FromSeconds(1), token); return subscription; }
|
||||
catch { await subscription.DisposeAsync(); throw; }
|
||||
}
|
||||
public void Remove(Subscription subscription)
|
||||
@@ -123,21 +132,25 @@ public sealed class LiveStreamRelay : IDisposable
|
||||
Exception failure = null;
|
||||
try
|
||||
{
|
||||
using var opening = CancellationTokenSource.CreateLinkedTokenSource(token); opening.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
// Headers and the first MPEG-TS chunk share one startup deadline.
|
||||
// A slow tuner may accept HTTP before it starts delivering packets.
|
||||
using var opening = CancellationTokenSource.CreateLinkedTokenSource(token); opening.CancelAfter(owner.startupTimeout);
|
||||
Interlocked.Increment(ref owner.opened);
|
||||
using var response = await owner.client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, opening.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
using var stream = await response.Content.ReadAsStreamAsync(token);
|
||||
var buffer = new byte[188 * 256 + 188]; var carry = 0;
|
||||
var buffer = new byte[188 * 256 + 188]; var carry = 0; var started = false;
|
||||
while (true)
|
||||
{
|
||||
using var reading = CancellationTokenSource.CreateLinkedTokenSource(token); reading.CancelAfter(TimeSpan.FromSeconds(8));
|
||||
var count = await stream.ReadAtLeastAsync(buffer.AsMemory(carry, buffer.Length - carry), 188 * 256 - carry, throwOnEndOfStream: false, cancellationToken: reading.Token);
|
||||
using var reading = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
if (started) reading.CancelAfter(owner.readTimeout);
|
||||
var count = await stream.ReadAtLeastAsync(buffer.AsMemory(carry, buffer.Length - carry), 188 * 256 - carry, throwOnEndOfStream: false, cancellationToken: started ? reading.Token : opening.Token);
|
||||
if (count == 0) throw new IOException("Decoder closed the live stream.");
|
||||
count += carry; var bytes = count / 188 * 188;
|
||||
if (bytes > 0)
|
||||
{
|
||||
if (buffer[0] != 0x47 || (bytes >= 376 && buffer[188] != 0x47)) throw new IOException("Decoder did not return MPEG-TS packets.");
|
||||
started = true;
|
||||
var chunk = buffer.AsSpan(0, bytes).ToArray();
|
||||
lock (gate)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user