Share decoder live sources and bound web playback startup

This commit is contained in:
2026-10-12 20:59:30 +02:00
parent 92052d588f
commit cecd5b5625
13 changed files with 579 additions and 37 deletions
+50
View File
@@ -5,6 +5,7 @@ using System.Reflection;
using System.Security.Claims;
using System.Text.Json.Nodes;
using System.Threading;
using System.Linq;
using MediaBrowser.Common.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
@@ -57,8 +58,57 @@ try
result=await Request(user,"?DecoderIncludeHidden=true&Limit=2");Check((int)result["TotalRecordCount"]==2,"preference editor can restore hidden channels");
}
finally { if(Directory.Exists(folder))Directory.Delete(folder,true); }
var probeJson="""{"streams":[{"index":0,"codec_type":"video","codec_name":"h264","width":1920,"height":1080,"field_order":"tt","avg_frame_rate":"25/1"},{"index":1,"codec_type":"audio","codec_name":"mp2","channels":2,"sample_rate":"48000"}]}""";
var calls=0;
var sharedProbe=new LiveStreamProbe(async (url,token)=>{Interlocked.Increment(ref calls);await System.Threading.Tasks.Task.Delay(100,token);return probeJson;});
var probes=await System.Threading.Tasks.Task.WhenAll(System.Linq.Enumerable.Range(0,4).Select(_=>sharedProbe.Get("channel",CancellationToken.None)));
Check(calls==1&&probes.All(x=>x.Count==2),"four simultaneous requests share one upstream probe");
Check(probes[0][0].Index==0&&probes[0][0].IsInterlaced&&probes[0][0].AverageFrameRate==25,"real stream indexes and interlacing preserved");
await sharedProbe.Get("channel",CancellationToken.None);Check(calls==1,"successful probe cached");
using(var cancelled=new CancellationTokenSource(20))
{
var abandoned=sharedProbe.Get("another",cancelled.Token);var surviving=sharedProbe.Get("another",CancellationToken.None);
try{await abandoned;throw new Exception("cancellation expected");}catch(OperationCanceledException){}
Check((await surviving).Count==2&&calls==2,"one cancelled caller does not break shared probe");
}
var failingCalls=0;
var stalled=new LiveStreamProbe(async (url,token)=>{Interlocked.Increment(ref failingCalls);await System.Threading.Tasks.Task.Delay(10000,token);return probeJson;},TimeSpan.FromMilliseconds(100));
var watch=System.Diagnostics.Stopwatch.StartNew();
for(var i=0;i<2;i++)try{await stalled.Get("stalled",CancellationToken.None);throw new Exception("failure expected");}catch(InvalidOperationException){}
Check(watch.Elapsed.TotalSeconds<2&&failingCalls==1,"stalled source bounded and failed retries coalesced");
try{LiveStreamProbe.Parse("{\"streams\":[]}");throw new Exception("invalid video expected");}catch(InvalidOperationException){}
Check(true,"empty source rejected before playback");
var relayHandler=new RelayHandler();
using(var relay=new LiveStreamRelay(new System.Net.Http.HttpClient(relayHandler),8096,TimeSpan.FromMilliseconds(100)))
{
var address=relay.Register("http://receiver/channel");var id=address.Split('/').Last();
Check(relay.Register("http://receiver/channel")==address,"one stable relay address for a channel");
var first=await relay.Subscribe(id,CancellationToken.None);var second=await relay.Subscribe(id,CancellationToken.None);
var one=await first.Reader.ReadAsync();var two=await second.Reader.ReadAsync();
Check(relayHandler.Calls==1&&one[0]==0x47&&two[0]==0x47,"two data consumers share one receiver HTTP connection");
await first.DisposeAsync();Check((await second.Reader.ReadAsync()).Length>0&&relayHandler.Calls==1,"one consumer stopping does not interrupt the other");
var context=new DefaultHttpContext();context.Connection.RemoteIpAddress=System.Net.IPAddress.Parse("192.0.2.5");
var controller=new LiveRelayController(relay){ControllerContext=new Microsoft.AspNetCore.Mvc.ControllerContext{HttpContext=context}};
await controller.Stream(id,CancellationToken.None);Check(context.Response.StatusCode==403,"relay denied to non-loopback clients");
await second.DisposeAsync();await System.Threading.Tasks.Task.Delay(250);
Check(System.Text.Json.JsonSerializer.Serialize(relay.Status()).Contains("\"ActiveSources\":0"),"receiver connection released after final consumer");
}
public class PathsProxy : DispatchProxy
{
public string Folder;
protected override object Invoke(MethodInfo method,object[] args)=>(method.Name=="get_DataPath" || method.Name=="get_PluginConfigurationsPath")?Folder:null;
}
public class RelayHandler : System.Net.Http.HttpMessageHandler
{
public int Calls;
protected override System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(System.Net.Http.HttpRequestMessage request,CancellationToken token)
{ Interlocked.Increment(ref Calls);return System.Threading.Tasks.Task.FromResult(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK){Content=new System.Net.Http.StreamContent(new PacketStream())}); }
}
public class PacketStream : Stream
{
public override bool CanRead=>true;public override bool CanSeek=>false;public override bool CanWrite=>false;public override long Length=>throw new NotSupportedException();public override long Position{get=>0;set=>throw new NotSupportedException();}
public override async System.Threading.Tasks.ValueTask<int> ReadAsync(Memory<byte> buffer,CancellationToken token=default)
{ await System.Threading.Tasks.Task.Delay(10,token);var size=Math.Min(buffer.Length,188*256);size-=size%188;buffer.Span[..size].Clear();for(int i=0;i<size;i+=188)buffer.Span[i]=0x47;return size; }
public override int Read(byte[] b,int o,int c)=>throw new NotSupportedException();public override void Flush(){}public override long Seek(long o,SeekOrigin s)=>throw new NotSupportedException();public override void SetLength(long n)=>throw new NotSupportedException();public override void Write(byte[] b,int o,int c)=>throw new NotSupportedException();
}