The Atsc3Receiver.cs now understands ATSC3 FDTs.
All checks were successful
🚀 Pack skyscraper8 / make-zip (push) Successful in 1m25s

This commit is contained in:
feyris-tan 2026-06-08 20:52:21 +02:00
parent 2e173d4156
commit 4fe40e082a
9 changed files with 399 additions and 7 deletions

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace skyscraper8.Atsc.A331
{
public class A331Exception : AtscException
{
public A331Exception()
{
}
public A331Exception(string message) : base(message)
{
}
public A331Exception(string message, Exception inner) : base(message, inner)
{
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace skyscraper8.Atsc.A331
{
internal class Atsc3Fdt
{
public uint Expires { get; set; }
public uint EfdtVersion { get; set; }
private List<Atsc3FdtFile> _files;
public IReadOnlyList<Atsc3FdtFile> Files
{
get
{
if (_files == null)
return ImmutableList<Atsc3FdtFile>.Empty;
return _files.AsReadOnly();
}
}
public void AddFile(Atsc3FdtFile resultFile)
{
if (_files == null)
_files = new List<Atsc3FdtFile>();
_files.Add(resultFile);
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace skyscraper8.Atsc.A331
{
internal class Atsc3FdtFile
{
public Atsc3FdtFile(Dictionary<string, string> result)
{
foreach (KeyValuePair<string, string> entry in result)
{
switch (entry.Key)
{
case "Content-Location":
this.ContentLocation = entry.Value;
break;
case "TOI":
this.TOI = ulong.Parse(entry.Value);
break;
case "Content-Length":
this.ContentLength = int.Parse(entry.Value);
break;
case "Content-Type":
this.ContentType = entry.Value;
break;
default:
throw new NotImplementedException(entry.Key);
}
}
}
public string ContentType { get; set; }
public int ContentLength { get; set; }
public ulong TOI { get; set; }
public string ContentLocation { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace skyscraper8.Atsc.A331
{
internal class Atsc3FilenameTable
{
private List<Tuple<IPEndPoint, ulong, string>> staticFilenames;
public void LearnFilename(IPEndPoint endpoint, Atsc3Fdt fdt)
{
foreach (Atsc3FdtFile file in fdt.Files)
{
if (staticFilenames == null)
staticFilenames = new List<Tuple<IPEndPoint, ulong, string>>();
staticFilenames.Add(new Tuple<IPEndPoint, ulong, string>(endpoint, file.TOI, file.ContentLocation));
}
}
}
}

View File

@ -17,7 +17,7 @@ namespace skyscraper8.Atsc.A331
{
[SkyscraperPlugin]
[PluginPriority(3)]
internal class Atsc3Unpacker : ISkyscraperMpePlugin
internal class Atsc3Receiver : ISkyscraperMpePlugin
{
private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name);
private static readonly IPAddress LLS_IP = IPAddress.Parse("224.0.23.60");
@ -45,9 +45,17 @@ namespace skyscraper8.Atsc.A331
if (atsc3detected && internetHeader.IsDestinationMulticast)
return true;
ushort destPort = ipv4Packet.GetUInt16BE(2);
if (nonAtsc3 != null)
{
IPEndPoint possibleEvildoer = new IPEndPoint(internetHeader.DestinationAddress, destPort);
if (nonAtsc3.Contains(possibleEvildoer))
return false;
}
if (internetHeader.DestinationAddress.Equals(LLS_IP))
{
if (ipv4Packet.GetUInt16BE(2) == LLS_PORT)
if (destPort == LLS_PORT)
{
return true;
}
@ -56,10 +64,22 @@ namespace skyscraper8.Atsc.A331
return false;
}
private HashSet<IPEndPoint> nonAtsc3;
private Dictionary<Tuple<IPAddress, ushort, ulong, ulong>, FluteListener> flutes;
private Atsc3FilenameTable filenames;
public void HandlePacket(InternetHeader internetHeader, byte[] ipv4Packet)
{
_stopProcessIndicator = false;
UserDatagram udpPacket = new UserDatagram(ipv4Packet);
IPEndPoint destination = new IPEndPoint(internetHeader.DestinationAddress, udpPacket.DestinationPort);
if (nonAtsc3 != null)
{
if (nonAtsc3.Contains(destination))
{
return;
}
}
if (internetHeader.DestinationAddress.Equals(LLS_IP) && udpPacket.DestinationPort == LLS_PORT)
{
//Handle LLS
@ -74,6 +94,63 @@ namespace skyscraper8.Atsc.A331
}
LctFrame lctFrame = new LctFrame(udpPacket.Payload, true);
if (lctFrame.Version != 1)
{
//Doesn't look like FLUTE, go away.
if (nonAtsc3 == null)
nonAtsc3 = new HashSet<IPEndPoint>();
nonAtsc3.Add(destination);
return;
}
ulong tsi = lctFrame.LctHeader.TransportSessionIdentifier;
ulong toi = lctFrame.LctHeader.TransportObjectIdentifier;
Tuple<IPAddress, ushort, ulong, ulong> fluteCoordinate = new Tuple<IPAddress, ushort, ulong, ulong>(internetHeader.DestinationAddress, udpPacket.DestinationPort, tsi, toi);
FluteListener targetListener;
if (flutes == null)
{
flutes = new Dictionary<Tuple<IPAddress, ushort, ulong, ulong>, FluteListener>();
filenames = new Atsc3FilenameTable();
}
if (flutes.ContainsKey(fluteCoordinate))
{
targetListener = flutes[fluteCoordinate];
}
else
{
FluteListener newListener = new FluteListener(internetHeader.DestinationAddress, udpPacket.DestinationPort, tsi, toi);
flutes.Add(fluteCoordinate, newListener);
targetListener = newListener;
}
targetListener.PushPacket(lctFrame);
_stopProcessIndicator = true;
if (targetListener.IsComplete())
{
GuessedFluteDataType guessedFluteDataType = FluteUtilities.GuessDataType(targetListener);
ProcessMetafile(guessedFluteDataType, targetListener, destination);
}
}
private bool ProcessMetafile(GuessedFluteDataType guessedFluteDataType, FluteListener targetListener, IPEndPoint destination)
{
switch (guessedFluteDataType)
{
case GuessedFluteDataType.Xml:
string contentsAsString = targetListener.GetContentsAsString();
if (contentsAsString.Contains("<FDT-Instance "))
{
Atsc3Fdt fdt = Atsc3Utilities.ParseFdt(contentsAsString);
filenames.LearnFilename(destination, fdt);
return true;
}
return false;
default:
throw new NotImplementedException(guessedFluteDataType.ToString());
}
}
private void HandleLls(LlsTable lls)

View File

@ -1,10 +1,11 @@
using System;
using skyscraper8.Atsc.A331.Schema;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using skyscraper8.Atsc.A331.Schema;
namespace skyscraper8.Atsc.A331
{
@ -36,5 +37,94 @@ namespace skyscraper8.Atsc.A331
SysTimeType result = (SysTimeType)deserialize;
return result;
}
public static Atsc3Fdt ParseFdt(string rawXml)
{
Atsc3Fdt result = null;
NameTable nt = new NameTable();
XmlNamespaceManager ns = new XmlNamespaceManager(nt);
ns.AddNamespace("fdt", "ignore");
ns.AddNamespace("afdt", "ignore");
XmlParserContext context = new XmlParserContext(nt, ns, null, XmlSpace.None);
XmlReaderSettings settings = new XmlReaderSettings()
{
ConformanceLevel = ConformanceLevel.Fragment,
IgnoreWhitespace = true,
IgnoreComments = true
};
XmlReader xr = XmlReader.Create(new StringReader(rawXml), settings, context);
while (xr.Read())
{
switch (xr.NodeType)
{
case XmlNodeType.XmlDeclaration:
//Ok, we know this is valid XML.
break;
case XmlNodeType.Whitespace:
//Understandable.
break;
case XmlNodeType.Element:
switch (xr.Name)
{
case "FDT-Instance":
result = new Atsc3Fdt();
Dictionary<string, string> extractXmlAttributes = ExtractXmlAttributes(xr);
HandleFdtInstanceAttributes(result, extractXmlAttributes);
break;
case "fdt:File":
Dictionary<string, string> fileAttributes = ExtractXmlAttributes(xr);
Atsc3FdtFile resultFile = new Atsc3FdtFile(fileAttributes);
result.AddFile(resultFile);
break;
default:
throw new NotImplementedException(xr.ToString());
}
break;
case XmlNodeType.EndElement:
break;
default:
throw new NotImplementedException(xr.ToString());
}
}
return result;
}
private static void HandleFdtInstanceAttributes(Atsc3Fdt result, Dictionary<string, string> extractXmlAttributes)
{
foreach (KeyValuePair<string, string> entry in extractXmlAttributes)
{
switch (entry.Key)
{
case "Expires":
result.Expires = uint.Parse(entry.Value);
break;
case "efdtVersion":
case "afdt:efdtVersion":
result.EfdtVersion = uint.Parse(entry.Value);
break;
default:
throw new NotImplementedException(entry.Key);
}
}
}
private static Dictionary<string, string> ExtractXmlAttributes(XmlReader xr)
{
Dictionary<string, string> result = new Dictionary<string, string>();
if (!xr.HasAttributes)
return result;
xr.MoveToFirstAttribute();
do
{
result.Add(xr.Name,xr.Value);
} while (xr.MoveToNextAttribute());
return result;
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using skyscraper5;
namespace skyscraper8.Atsc
{
public class AtscException : SkyscraperException
{
public AtscException()
{
}
public AtscException(string message) : base(message)
{
}
public AtscException(string message, Exception inner) : base(message, inner)
{
}
}
}

View File

@ -1,4 +1,7 @@
using skyscraper8.DvbNip;
using log4net.Core;
using MimeKit;
using skyscraper5.Teletext.Wss;
using skyscraper8.DvbNip;
using System;
using System.Collections.Generic;
using System.IO.Compression;
@ -9,8 +12,6 @@ using System.Runtime.Serialization;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using MimeKit;
using skyscraper5.Teletext.Wss;
namespace skyscraper8.Ietf.FLUTE
{
@ -73,6 +74,21 @@ namespace skyscraper8.Ietf.FLUTE
if (blocks == null)
blocks = new Dictionary<Tuple<ushort, ushort>, FluteBlock>();
if (lctFrame.Atsc3Compliant)
{
if (blocks.Count == 0)
{
Tuple<ushort, ushort> fakeKey = new Tuple<ushort, ushort>(12345, 6789);
FluteBlock fakeBlock = new FluteBlock(0, 0, new byte[transferLength]);
blocks.Add(fakeKey, fakeBlock);
}
FluteBlock atsc3Block = blocks.First().Value;
Array.Copy(lctFrame.Payload, 0, atsc3Block.Payload, lctFrame.Atsc3ObjectStartOffset.Value, lctFrame.Payload.Length);
_dataWritten += lctFrame.Payload.Length;
return;
}
if (lctFrame.FecHeader == null)
return;
@ -293,5 +309,31 @@ namespace skyscraper8.Ietf.FLUTE
public double LastReportedProgress { get; internal set; }
public long? TrimmedLength { get; set; }
internal byte[] GetFirstBlock()
{
if (blocks == null)
return null;
if (blocks.Count == 0)
return null;
if (blocks.Count == 1)
return blocks.Values.First().Payload;
else
{
List<FluteBlock> blockPayloads = blocks.Values.ToList();
blockPayloads.Sort(new FluteBlockComparer());
return blockPayloads[0].Payload;
}
}
internal string GetContentsAsString()
{
Stream stream = ToStream();
StreamReader sr = new StreamReader(stream);
string result = sr.ReadToEnd();
stream.Dispose();
return result;
}
}
}

View File

@ -21,6 +21,24 @@ namespace skyscraper8.Ietf.FLUTE
return result;
}
public static FDTInstanceType UnpackFluteFdt(string source)
{
if (fdtSerializer == null)
fdtSerializer = new XmlSerializer(typeof(FDTInstanceType));
try
{
StringReader sr = new StringReader(source);
FDTInstanceType result = (FDTInstanceType)fdtSerializer.Deserialize(sr);
return result;
}
catch (InvalidOperationException e)
{
Console.WriteLine(e);
return null;
}
}
public static FDTInstanceType UnpackFluteFdt(Stream ms)
{
if (fdtSerializer == null)
@ -62,5 +80,18 @@ namespace skyscraper8.Ietf.FLUTE
}
}
public static GuessedFluteDataType GuessDataType(FluteListener targetListener)
{
byte[] firstBlock = targetListener.GetFirstBlock();
if (firstBlock[0] == 0x3c && firstBlock[1] == 0x3f && firstBlock[2] == 0x78 && firstBlock[3] == 0x6d && firstBlock[4] == 0x6c)
return GuessedFluteDataType.Xml;
return GuessedFluteDataType.Unknown;
}
}
public enum GuessedFluteDataType
{
Unknown,
Xml
}
}