Retain selectable audio languages and DVB subtitles through live continuity

This commit is contained in:
2026-10-23 23:50:46 +02:00
parent 9a056b3070
commit bd7395f045
17 changed files with 666 additions and 57 deletions
+81
View File
@@ -0,0 +1,81 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using Jellyfin.Plugin.Enigma2;
using MediaBrowser.Model.Entities;
/// <summary>Self-authored DVB bitmap fixtures; no broadcaster samples or receiver connection.</summary>
public static class SyntheticBroadcastFixture
{
public static BroadcastTracks Tracks(bool normalize = true) => new(new MediaStream[] {
new BroadcastMediaStream { Type=MediaStreamType.Video,Index=0,TransportId=256,Codec="h264",Width=640,Height=360 },
new BroadcastMediaStream { Type=MediaStreamType.Audio,Index=1,TransportId=257,Codec="aac",Language="pol",Title="Polski",Channels=2,SampleRate=48000,IsDefault=true },
new BroadcastMediaStream { Type=MediaStreamType.Audio,Index=2,TransportId=258,Codec="aac",Language="eng",Title="English",Channels=2,SampleRate=48000 },
new BroadcastMediaStream { Type=MediaStreamType.Subtitle,Index=3,TransportId=259,Codec="DVBSUB",Language="pol",SubtitleExtraData=new byte[]{0,1,0,1},IsHearingImpaired=true },
new BroadcastMediaStream { Type=MediaStreamType.Subtitle,Index=4,TransportId=260,Codec="DVBSUB",Language="eng",SubtitleExtraData=new byte[]{0,1,0,1},IsForced=true },
new BroadcastMediaStream { Type=MediaStreamType.Subtitle,Index=5,TransportId=261,Codec="DVBTXT",Language="pol",SubtitleExtraData=new byte[]{16,136} }
},normalize);
private static long Pts(byte[] b,int p) => ((long)(b[p]&14)<<29)|((long)b[p+1]<<22)|((long)(b[p+2]&254)<<14)|((long)b[p+3]<<7)|((long)b[p+4]>>1);
private static byte[] Stamp(long t) => new[]{(byte)(33L|((t>>29)&14)),(byte)(t>>22),(byte)(1L|((t>>14)&254)),(byte)(t>>7),(byte)(1L|(t<<1))};
public static byte[] AddSubtitles(byte[] input)
{
input=BroadcastSlateProgram.Apply(input,Tracks(),false);
using var output=new MemoryStream();long next=0;var counters=new int[3];
for(var pos=0;pos<input.Length;pos+=188)
{
output.Write(input,pos,188);var pid=((input[pos+1]&31)<<8)|input[pos+2];
if(pid!=256||(input[pos+1]&64)==0)continue;
var p=pos+4+((input[pos+3]&32)!=0?1+input[pos+4]:0);
if(p+14>pos+188||input[p]!=0||input[p+1]!=0||input[p+2]!=1||(input[p+7]&128)==0)continue;
var pts=Pts(input,p+9);if(pts<next)continue;next=pts+180000;
for(var track=0;track<3;track++)
foreach(var packet in Packets(track==2?Teletext():Bitmap(track),pts,259+track,ref counters[track]))output.Write(packet);
}
return output.ToArray();
}
public static byte[][] Packets(byte[] bitmap,long pts,int pid,ref int counter)
{
var pes=new List<byte>{0,0,1,189};var length=bitmap.Length+8;
pes.Add((byte)(length>>8));pes.Add((byte)length);pes.AddRange(new byte[]{128,128,5});pes.AddRange(Stamp(pts));pes.AddRange(bitmap);
var result=new List<byte[]>();
for(var i=0;i<pes.Count;)
{
var packet=Enumerable.Repeat((byte)255,188).ToArray();packet[0]=71;packet[1]=(byte)((pid>>8)|(i==0?64:0));packet[2]=(byte)pid;
var count=Math.Min(184,pes.Count-i);var p=4;
if(count<184){packet[3]=(byte)(48|(counter++&15));packet[4]=(byte)(183-count);if(packet[4]>0)packet[5]=0;p+=184-count;}
else packet[3]=(byte)(16|(counter++&15));
pes.CopyTo(i,packet,p,count);i+=count;result.Add(packet);
}
return result.ToArray();
}
private static byte[] Teletext()
{
// A self-authored page 888, using standard Hamming-address and odd-parity text bytes.
byte Reverse(byte value) { byte result=0;for(var bit=0;bit<8;bit++)result=(byte)((result<<1)|((value>>bit)&1));return result; }
var hamming=new byte[]{21,2,73,94,100,115,56,47,208,199,140,155,161,182,253,234};
byte Character(char c) { var value=(byte)(c&127);if(System.Numerics.BitOperations.PopCount((uint)value)%2==0)value|=128;return Reverse(value); }
var result=new List<byte>{16};
void Row(int row,byte[] text){result.AddRange(new byte[]{3,44,199,228,Reverse(hamming[(row&1)<<3]),Reverse(hamming[row>>1])});result.AddRange(text);}
var header=new List<byte>();foreach(var n in new[]{8,8,0,8,0,8,0,0})header.Add(Reverse(hamming[n]));header.AddRange(Enumerable.Repeat(Character(' '),32));Row(0,header.ToArray());
Row(1,"SYNTHETIC TELETEXT SUBTITLE".PadRight(40).Select(Character).ToArray());return result.ToArray();
}
public static byte[] Bitmap(int variant)
{
var result=new List<byte>{32,0};
void Segment(byte type,byte[] data){result.AddRange(new byte[]{15,type,0,1,(byte)(data.Length>>8),(byte)data.Length});result.AddRange(data);}
Segment(20,new byte[]{0,2,127,1,103}); // display 640 x 360
Segment(16,new byte[]{2,11,0,255,0,64,1,44});
Segment(17,new byte[]{0,7,0,96,0,32,39,0,0,3,0,0,0,0,240,0});
var field=new List<byte>();
for(var y=0;y<16;y++)
{
field.Add(16);
for(var x=0;x<24;x++)field.Add((byte)((x/4+y/4+variant)%2==0?85:170));
field.Add(0);field.Add(240);
}
var data=new List<byte>{0,0,0,(byte)(field.Count>>8),(byte)field.Count,(byte)(field.Count>>8),(byte)field.Count};
data.AddRange(field);data.AddRange(field);Segment(19,data.ToArray());Segment(128,Array.Empty<byte>());result.Add(255);
return result.ToArray();
}
}