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 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(); var prefs = DispatchProxy.Create(); 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() : new Dictionary { ["language"] = Language }; } }