136 lines
7.7 KiB
C#
136 lines
7.7 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Data.Enums;
|
|
using Jellyfin.Plugin.Enigma2;
|
|
using MediaBrowser.Common.Configuration;
|
|
using MediaBrowser.Controller.Entities;
|
|
using MediaBrowser.Controller.Library;
|
|
using MediaBrowser.Controller.LiveTv;
|
|
using MediaBrowser.Model.Serialization;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
public static class SavedChannelPlaybackTests
|
|
{
|
|
public const string Old = "1:0:1:3D66:2C88:13E:820000:0:0:0:";
|
|
public const string Current = "1:0:1:13F1:5DC:13E:820000:0:0:0:";
|
|
public static async Task Run(Action<bool,string> check)
|
|
{
|
|
var previous = Plugin.Instance;
|
|
var folder = Path.Combine(Path.GetTempPath(),"saved-channel-"+Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(folder);
|
|
try
|
|
{
|
|
var paths=DispatchProxy.Create<IApplicationPaths,SavedPaths>();((SavedPaths)(object)paths).Folder=folder;
|
|
var plugin=new Plugin(paths,DispatchProxy.Create<IXmlSerializer,NoXmlFiles>());
|
|
plugin.Configuration.HostName="receiver";plugin.Configuration.WebInterfacePort="80";
|
|
plugin.Configuration.StreamingPort="8001";plugin.Configuration.OnlyOneBouquet=true;
|
|
plugin.Configuration.TVBouquet="Polskie";plugin.Configuration.FetchPiconsFromWebInterface=true;
|
|
var library=DispatchProxy.Create<ILibraryManager,SavedChannelLibrary>();
|
|
var handler=new ReceiverHandler();
|
|
using var relay=new LiveStreamRelay(new HttpClient(handler),8096,TimeSpan.FromMilliseconds(50));
|
|
var calls=0;
|
|
var probe=new LiveStreamProbe((url,token)=>
|
|
{
|
|
calls++;
|
|
check(url.EndsWith("?programme=5105",StringComparison.Ordinal),"saved channel probes the migrated SID rather than the historical SID");
|
|
return Task.FromResult("{\"programs\":[{\"program_id\":5105,\"streams\":[{\"index\":0,\"codec_type\":\"video\",\"codec_name\":\"h264\",\"width\":1920,\"height\":1080}]}],\"streams\":[{\"index\":0,\"codec_type\":\"video\",\"codec_name\":\"h264\",\"width\":1920,\"height\":1080}]}");
|
|
});
|
|
var service=new LiveTvService(NullLogger<LiveTvService>.Instance,new ReceiverFactory(),null,probe,relay,library);
|
|
var saved=await service.GetChannelStream(Old,null,CancellationToken.None);
|
|
var current=await service.GetChannelStream(Current,null,CancellationToken.None);
|
|
check(saved.Path==current.Path&&calls==1,"saved and current native channels share the same relay and metadata probe");
|
|
check(((SavedChannelLibrary)(object)library).Calls==1,"current reference avoids the native historical lookup");
|
|
var id=saved.Path.Split('/').Last();
|
|
await using var first=await relay.Subscribe(id,CancellationToken.None);
|
|
await using var second=await relay.Subscribe(id,CancellationToken.None);
|
|
await first.Reader.ReadAsync();await second.Reader.ReadAsync();
|
|
check(handler.Calls==1&&handler.Reference==Current,"both native IDs open only one upstream using the migrated receiver reference");
|
|
using var cancelled=new CancellationTokenSource();cancelled.Cancel();
|
|
try { await service.GetChannelStream(Old,null,cancelled.Token);throw new Exception("Expected cancellation"); }
|
|
catch(OperationCanceledException) { check(handler.Calls==1,"cancelled channel lookup opens no extra upstream"); }
|
|
|
|
var offline=new LiveTvService(NullLogger<LiveTvService>.Instance,new ReceiverFactory(new UnavailableBouquet(false)),null,probe,relay,library);
|
|
check((await offline.GetChannelStream(Current,null,CancellationToken.None)).Path==current.Path&&((SavedChannelLibrary)(object)library).Calls==1,
|
|
"unavailable web interface preserves direct playback without historical lookup");
|
|
var stalled=new LiveTvService(NullLogger<LiveTvService>.Instance,new ReceiverFactory(new UnavailableBouquet(true)),null,probe,relay,library);
|
|
var watch=Stopwatch.StartNew();
|
|
check((await stalled.GetChannelStream(Current,null,CancellationToken.None)).Path==current.Path&&watch.Elapsed<TimeSpan.FromSeconds(7),
|
|
"stalled bouquet lookup is bounded to five seconds and preserves requested playback");
|
|
}
|
|
finally
|
|
{
|
|
typeof(Plugin).GetProperty(nameof(Plugin.Instance)).SetValue(null,previous);
|
|
Directory.Delete(folder,true);
|
|
}
|
|
}
|
|
|
|
public class NoXmlFiles:DispatchProxy
|
|
{
|
|
protected override object Invoke(MethodInfo method,object[] args)
|
|
{
|
|
if(method.Name=="SerializeToFile")return null; // Keep default configuration in memory.
|
|
throw new Exception("No persisted configuration expected in isolated test");
|
|
}
|
|
}
|
|
public class SavedPaths:DispatchProxy
|
|
{
|
|
public string Folder;
|
|
protected override object Invoke(MethodInfo method,object[] args) => method.ReturnType==typeof(string)?Folder:null;
|
|
}
|
|
public class SavedChannelLibrary:DispatchProxy
|
|
{
|
|
public int Calls;
|
|
protected override object Invoke(MethodInfo method,object[] args)
|
|
{
|
|
if(method.Name!="GetItemList"||args[0] is not InternalItemsQuery q||!q.IncludeItemTypes.SequenceEqual(new[]{BaseItemKind.LiveTvChannel}))
|
|
throw new Exception("Expected metadata lookup restricted to live TV channels");
|
|
Calls++;
|
|
return new BaseItem[]{new LiveTvChannel{ExternalId=Old,Name="TV PULS HD",ServiceName="Enigma2"},
|
|
new LiveTvChannel{ExternalId=Old,Name="Different channel",ServiceName="Other provider"}};
|
|
}
|
|
}
|
|
private sealed class ReceiverFactory:IHttpClientFactory
|
|
{
|
|
private readonly HttpMessageHandler handler;
|
|
public ReceiverFactory(HttpMessageHandler handler=null){this.handler=handler;}
|
|
public HttpClient CreateClient(string name)=>new(handler??new BouquetHandler());
|
|
}
|
|
private sealed class UnavailableBouquet:HttpMessageHandler
|
|
{
|
|
private readonly bool stall;
|
|
public UnavailableBouquet(bool stall){this.stall=stall;}
|
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken token)
|
|
{
|
|
if(stall)await Task.Delay(Timeout.InfiniteTimeSpan,token);
|
|
throw new HttpRequestException("Receiver web interface unavailable");
|
|
}
|
|
}
|
|
private sealed class BouquetHandler:HttpMessageHandler
|
|
{
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken token)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
var body=string.IsNullOrEmpty(request.RequestUri.Query)
|
|
? "<e2servicelist><e2service><e2servicereference>1:7:1:0:0:0:0:0:0:0:FROM BOUQUET</e2servicereference><e2servicename>Polskie</e2servicename></e2service></e2servicelist>"
|
|
: "<e2servicelist><e2service><e2servicereference>"+Current+"</e2servicereference><e2servicename>TV PULS HD</e2servicename></e2service></e2servicelist>";
|
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK){Content=new StringContent(body)});
|
|
}
|
|
}
|
|
private sealed class ReceiverHandler:HttpMessageHandler
|
|
{
|
|
public int Calls;public string Reference;
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken token)
|
|
{
|
|
Calls++;Reference=request.RequestUri.AbsolutePath.TrimStart('/');
|
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK){Content=new StreamContent(new PacketStream())});
|
|
}
|
|
}
|
|
}
|