using System; using Jellyfin.Plugin.Enigma2; using MediaBrowser.Model.Entities; public static class LosslessSlateTests { public static BroadcastTracks Tracks(int width, int height, float rate) => new(new MediaStream[] { new BroadcastMediaStream { Type=MediaStreamType.Video, Index=0, Codec="h264", Width=width, Height=height, AverageFrameRate=rate, RealFrameRate=rate, Profile="Constrained Baseline", PixelFormat="yuv420p", SampleAspectRatio="1:1" }, new MediaStream { Type=MediaStreamType.Audio, Index=1, Codec="aac", Profile="LC", Channels=2, SampleRate=48000 } }); public static void Run(Action check) { foreach (var rate in new[] { 24f, 25f, 30f, 50f, 60f, 120f }) { var tracks=Tracks(3840,2160,rate); var profile=LosslessSlateProfile.Create(tracks); check(profile?.Rate==rate && profile.Width==3840 && profile.Height==2160 && tracks.Output[0].AverageFrameRate==rate, $"status picture matches source rate {rate} and 4K size without changing channel metadata"); } var original=Tracks(3840,2160,60000f/1001); var video=(BroadcastMediaStream)original.Input[0];video.FrameRateRational="60000/1001"; check(LosslessSlateProfile.Create(original)==null && original.Output[0].AverageFrameRate==60000f/1001, "fractional cadence is preserved by native fallback instead of rounded for a status image"); var key=new BroadcastTracks(original.Input).Key;video.AverageFrameRate=24; check(new BroadcastTracks(original.Input).Key!=key,"source rate changes invalidate cached status images"); video.Codec="hevc";video.PixelFormat="yuv420p10le";video.ColorTransfer="smpte2084";video.ColorPrimaries="bt2020";video.Level=153; original.Input[1].Codec="ac3";original.Input[1].Channels=6; var hdr=new BroadcastTracks(original.Input,true,false); check(!hdr.Normalize && hdr.Output[0].Codec=="hevc" && hdr.Output[0].PixelFormat=="yuv420p10le" && hdr.Output[0].ColorTransfer=="smpte2084" && hdr.Output[0].Level==153 && hdr.Output[0].Width==3840 && hdr.Output[1].Codec=="ac3" && hdr.Output[1].Channels==6, "legacy slate/Full HD options cannot normalize 4K HDR10 or surround audio"); check(LosslessSlateProfile.Create(hdr)==null,"HDR/HEVC keeps original transport instead of converting the channel to fit a status picture"); var interlaced=Tracks(1920,1080,25);interlaced.Input[0].IsInterlaced=true; check(LosslessSlateProfile.Create(interlaced)==null && new BroadcastTracks(interlaced.Input).Output[0].IsInterlaced, "interlaced broadcast is preserved without plugin deinterlacing"); check(LosslessSlateProfile.Create(BroadcastTracks.Fallback())==null,"unknown cold-start metadata cannot force a guessed status format"); var unknown=Tracks(1280,720,60);((BroadcastMediaStream)unknown.Input[0]).SampleAspectRatio=null; check(LosslessSlateProfile.Create(unknown)==null,"unknown sample aspect ratio uses unmodified broadcast fallback"); var now=DateTime.UtcNow;var timeline=new ContinuityTimeline(now,false); var first=Packet(12000,9000);var second=Packet(195000,189000); timeline.Live(first,"producer",2,now);timeline.Live(second,"producer",2,now.AddSeconds(2)); long Decode(long sequence,byte[] packet) { var segment=timeline.Find(sequence); return TransportStreamClock.ClockBounds(TransportStreamClock.Rebase(packet,segment.Start90k,false,segment.ClockOffset90k)).Value.Decode; } check(Decode(1,second)-Decode(0,first)==180000,"copied B-frames retain their decode-clock interval despite different first presentation timestamps"); timeline.Tick(now.AddSeconds(9),true);var retry=timeline.Find(2); var recovered=Packet(372000,369000);timeline.Live(recovered,"producer",2,now.AddSeconds(10)); check(Decode(3,recovered)>=retry.Start90k+(long)(retry.Duration*90000),"short source stall followed by the same producer cannot rewind behind the inserted status picture"); timeline=new ContinuityTimeline(now,false);first=Packet((1L<<33)-6000,(1L<<33)-9000);second=Packet(93000,90000); timeline.Live(first,"wrapped",1.1,now);timeline.Live(second,"wrapped",1.1,now.AddSeconds(1.1)); check(Decode(1,second)-Decode(0,first)==99000,"copied transport keeps a monotonic decode clock across the 33-bit timestamp wrap"); } private static byte[] Packet(long pts,long dts) { 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]=192;packet[12]=10; void Stamp(int at,long time,int prefix) {time&=(1L<<33)-1;packet[at]=(byte)((long)prefix|((time>>29)&14)|1);packet[at+1]=(byte)(time>>22);packet[at+2]=(byte)(1|((time>>14)&254));packet[at+3]=(byte)(time>>7);packet[at+4]=(byte)(1|(time<<1));} Stamp(13,pts,48);Stamp(18,dts,16);return packet; } }