Preserve native broadcast video and add patient shared stream startup

This commit is contained in:
2026-10-25 16:59:56 +01:00
parent bd7395f045
commit 11a34a5195
27 changed files with 708 additions and 139 deletions
+74
View File
@@ -0,0 +1,74 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.Enigma2;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
public static class PatientStartupE2E
{
private static int connections, active, maximum;
public static async Task Run(string encoder, string folder)
{
folder=Path.GetFullPath(folder);if(Directory.Exists(folder))throw new Exception("Choose a new output directory");Directory.CreateDirectory(folder);
var file=Path.Combine(folder,"fixture.ts");
var info=new ProcessStartInfo(encoder){UseShellExecute=false,RedirectStandardError=true};
foreach(var a in new[]{"-v","error","-nostdin","-filter_threads","1","-f","lavfi","-i","testsrc2=size=320x180:rate=25","-f","lavfi","-i","sine=sample_rate=48000","-t","4","-c:v","libx264","-threads","1","-preset","ultrafast","-g","25","-c:a","aac","-ac","2","-f","mpegts",file})info.ArgumentList.Add(a);
using(var make=Process.Start(info)){var error=make.StandardError.ReadToEndAsync();await make.WaitForExitAsync();if(make.ExitCode!=0)throw new Exception(await error);}
var bytes=await File.ReadAllBytesAsync(file);
using var listener=new TcpListener(IPAddress.Loopback,0);listener.Start();var port=((IPEndPoint)listener.LocalEndpoint).Port;listener.Stop();
var builder=WebApplication.CreateBuilder();builder.Logging.ClearProviders();builder.WebHost.UseUrls($"http://127.0.0.1:{port}");
using var relay=new LiveStreamRelay(new System.Net.Http.HttpClient(),port,TimeSpan.FromMilliseconds(200),TimeSpan.FromSeconds(StartupBudget.Seconds),TimeSpan.FromSeconds(StartupBudget.StallSeconds));
builder.Services.AddSingleton(relay);builder.Services.AddControllers().AddApplicationPart(typeof(LiveRelayController).Assembly);
await using var app=builder.Build();app.MapControllers();
app.MapGet("/late.ts",async context=>
{
Interlocked.Increment(ref connections);maximum=Math.Max(maximum,Interlocked.Increment(ref active));
context.Response.ContentType="video/mp2t";await context.Response.StartAsync(context.RequestAborted);
try
{
await Task.Delay(TimeSpan.FromSeconds(35),context.RequestAborted);
while(!context.RequestAborted.IsCancellationRequested)
for(var at=0;at<bytes.Length;at+=188*32)
{
var n=Math.Min(188*32,bytes.Length-at);
await context.Response.Body.WriteAsync(bytes.AsMemory(at,n),context.RequestAborted);await context.Response.Body.FlushAsync(context.RequestAborted);
await Task.Delay(TimeSpan.FromSeconds(4d*n/bytes.Length),context.RequestAborted);
}
}
catch(OperationCanceledException){}catch(IOException){}
finally{Interlocked.Decrement(ref active);}
});
await app.StartAsync();
try
{
var proxy=DispatchProxy.Create<IMediaEncoder,PatientEncoderProxy>();((PatientEncoderProxy)(object)proxy).ProbePath=Path.Combine(Path.GetDirectoryName(encoder),"ffprobe");
var probe=new LiveStreamProbe(proxy);var url=relay.Register($"http://127.0.0.1:{port}/late.ts");var watch=Stopwatch.StartNew();
var result=await Task.WhenAll(Enumerable.Range(0,4).Select(_=>probe.Get(url,CancellationToken.None,1))).WaitAsync(TimeSpan.FromSeconds(95));
if(watch.Elapsed.TotalSeconds<35||watch.Elapsed.TotalSeconds>=90||result.Any(x=>x[0].Width!=320||x[0].Height!=180))throw new Exception("Delayed source failed the configured budget");
await Task.Delay(800);
if(maximum!=1||connections!=1||active!=0)throw new Exception("Receiver sharing or cleanup failed");
var report=new{Passed=true,SyntheticOnly=true,ReceiverConnectionsOpened=0,ConfiguredStartupSeconds=StartupBudget.Seconds,DelayedFirstDataSeconds=35,
ElapsedSeconds=watch.Elapsed.TotalSeconds,ConcurrentRequesters=4,PhysicalFixtureConnections=connections,MaximumConcurrentFixtureConnections=maximum,RemainingConnections=active,
ActualFfprobeUsed=true,PhysicalAndroidTvTested=false};
await File.WriteAllTextAsync(Path.Combine(folder,"result.json"),JsonSerializer.Serialize(report,new JsonSerializerOptions{WriteIndented=true}));Console.WriteLine(JsonSerializer.Serialize(report));
}
finally{relay.Dispose();await app.StopAsync();}
}
}
public class PatientEncoderProxy : DispatchProxy
{
public string ProbePath;
protected override object Invoke(MethodInfo method,object[] args)=>method.Name=="get_ProbePath"?ProbePath:null;
}