Files
jellyfin-plugin-enigma2/Tests/ContinuityTests.cs
T

70 lines
5.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.Enigma2;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
public static class ContinuityTests
{
public static void Run(Action<bool, string> check)
{
check(StartupMessages.ResolveLanguage("de-DE", "pl", "fr") == "de", "explicit Jellyfin UI language wins over HTTP and server language");
check(StartupMessages.ResolveLanguage(null, "en;q=0,uk-UA;q=0.8,pl;q=0.6", "de") == "uk", "language matching honours regional locale, quality and q=0");
check(StartupMessages.ResolveLanguage(null, null, "pl-PL") == "pl", "missing client locale uses Jellyfin server UI culture");
check(StartupMessages.ResolveLanguage("../../secret", "xx;q=1,de;q=oops", "xx") == "en", "unsupported and malformed locales use safe English fallback");
var cfg = DispatchProxy.Create<IServerConfigurationManager, FeedbackConfig>();
var prefs = DispatchProxy.Create<IDisplayPreferencesManager, FeedbackPreferences>();
var proxy = (FeedbackPreferences)(object)prefs; var user = Guid.NewGuid(); proxy.User = user;
var messages = new StartupMessages(cfg, prefs);
check(messages.Resolve(user, null, null) == "pl", "empty server profile uses actual server UI setting");
proxy.Language = "fr-FR";
check(messages.Resolve(user, null, "de") == "fr" && proxy.LastUser == user, "stored interface language is read only for the authenticated profile");
check(messages.Resolve(user, "uk", "de") == "uk", "current device UI choice overrides stored profile language");
messages.RememberClientLanguage(user, "web-a", "es");
check(messages.ClientLanguage(user, "web-a") == "es" && messages.ClientLanguage(user, "tv-a") == null && messages.ClientLanguage(Guid.NewGuid(), "web-a") == null,
"UI language handoff isolates users and devices");
var keys = messages.Texts("en").Keys.Order().ToArray();
foreach (var locale in new[] { "pl", "en", "de", "fr", "es", "uk" })
check(messages.Texts(locale).Keys.Order().SequenceEqual(keys) && messages.Text(locale, "slate_retrying").Contains('\n'), "complete shared text and FFmpeg slate catalogue: " + locale);
check(Math.Abs(ContinuityTimeline.SegmentSeconds * 50 - 112) < 0.00001 && Math.Abs(ContinuityTimeline.SegmentSeconds * 48000 / 1024 - 105) < 0.00001,
"segment boundary aligns whole video frames and whole AAC frames");
var packet = new byte[188]; packet[0]=71;packet[1]=65;packet[2]=0;packet[3]=16;packet[6]=1;packet[7]=224;packet[10]=128;packet[11]=128;packet[12]=5;
packet[13]=33;packet[15]=1;packet[17]=1;
var rebased=TransportStreamClock.Rebase(packet,(1L<<33)+90000);
var p=201;long pts=((long)(rebased[p]&14)<<29)|((long)rebased[p+1]<<22)|((long)(rebased[p+2]&254)<<14)|((long)rebased[p+3]<<7)|((long)rebased[p+4]>>1);
check(pts==90000&&rebased.Length==376&&rebased[5]==128&&packet[13]==33,"TS rebasing signals continuity reset, wraps 33-bit PTS and preserves cached input");
var now = DateTime.UtcNow; var timeline = new ContinuityTimeline(now);
check(!timeline.Playlist().Contains("ENDLIST") && timeline.Find(0).Slate == "waiting", "an immediately playable slate precedes decoder readiness");
timeline.Live(new byte[] { 1 }, "first", 2, now.AddSeconds(4));
check(timeline.Find(2).Discontinuity && timeline.Find(2).Video[0] == 1, "slate to real video marks an HLS discontinuity");
timeline.Tick(now.AddSeconds(11), true);
check(timeline.Find(3).Slate == "retrying" && !timeline.Playlist().Contains("ENDLIST"), "input loss keeps the same playlist alive with a retry slate");
timeline.Live(new byte[] { 2 }, "second", 2, now.AddSeconds(15));
check(timeline.Find(4).Discontinuity && timeline.Find(4).Video[0] == 2, "recovered producer returns to live video in the same stream");
for (var i = 0; i < 10000; i++) timeline.Tick(now.AddSeconds(20 + i * ContinuityTimeline.SegmentSeconds), true);
check(timeline.Find(0) == null && !timeline.Playlist().Contains("ENDLIST") && timeline.Find(10004) != null, "six-hour failures keep a bounded monotonic live timeline");
}
}
public class FeedbackConfig : DispatchProxy
{
protected override object Invoke(MethodInfo method, object[] args) => method.Name == "get_Configuration" ? new ServerConfiguration { UICulture = "pl" } : null;
}
public class FeedbackPreferences : DispatchProxy
{
public Guid User, LastUser;
public string Language;
protected override object Invoke(MethodInfo method, object[] args)
{
if (method.Name != "ListCustomItemDisplayPreferences") throw new Exception("Unexpected preference mutation");
LastUser = (Guid)args[0]; if (LastUser != User) throw new Exception("Wrong profile");
return Language == null ? new Dictionary<string, string>() : new Dictionary<string, string> { ["language"] = Language };
}
}