Align decoder startup deadlines and deploy resilient 7.0.8

This commit is contained in:
2026-10-13 09:07:29 +02:00
parent 0a834d863b
commit 0ab790d1e4
10 changed files with 527 additions and 66 deletions
+19 -6
View File
@@ -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)
{