diff --git a/skyscraper8/Atsc/A331/A331Exception.cs b/skyscraper8/Atsc/A331/A331Exception.cs new file mode 100644 index 0000000..e15cf49 --- /dev/null +++ b/skyscraper8/Atsc/A331/A331Exception.cs @@ -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) + { + } + } +} diff --git a/skyscraper8/Atsc/A331/Atsc3Fdt.cs b/skyscraper8/Atsc/A331/Atsc3Fdt.cs new file mode 100644 index 0000000..832aca3 --- /dev/null +++ b/skyscraper8/Atsc/A331/Atsc3Fdt.cs @@ -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 _files; + public IReadOnlyList Files + { + get + { + if (_files == null) + return ImmutableList.Empty; + + return _files.AsReadOnly(); + } + } + + public void AddFile(Atsc3FdtFile resultFile) + { + if (_files == null) + _files = new List(); + + _files.Add(resultFile); + } + + + } +} diff --git a/skyscraper8/Atsc/A331/Atsc3FdtFile.cs b/skyscraper8/Atsc/A331/Atsc3FdtFile.cs new file mode 100644 index 0000000..c471407 --- /dev/null +++ b/skyscraper8/Atsc/A331/Atsc3FdtFile.cs @@ -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 result) + { + foreach (KeyValuePair 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; } + + + } +} diff --git a/skyscraper8/Atsc/A331/Atsc3FilenameTable.cs b/skyscraper8/Atsc/A331/Atsc3FilenameTable.cs new file mode 100644 index 0000000..8e89888 --- /dev/null +++ b/skyscraper8/Atsc/A331/Atsc3FilenameTable.cs @@ -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> staticFilenames; + public void LearnFilename(IPEndPoint endpoint, Atsc3Fdt fdt) + { + foreach (Atsc3FdtFile file in fdt.Files) + { + if (staticFilenames == null) + staticFilenames = new List>(); + staticFilenames.Add(new Tuple(endpoint, file.TOI, file.ContentLocation)); + } + } + } +} diff --git a/skyscraper8/Atsc/A331/Atsc3Unpacker.cs b/skyscraper8/Atsc/A331/Atsc3Receiver.cs similarity index 54% rename from skyscraper8/Atsc/A331/Atsc3Unpacker.cs rename to skyscraper8/Atsc/A331/Atsc3Receiver.cs index d3fb204..340427d 100644 --- a/skyscraper8/Atsc/A331/Atsc3Unpacker.cs +++ b/skyscraper8/Atsc/A331/Atsc3Receiver.cs @@ -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 nonAtsc3; + private Dictionary, 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(); + nonAtsc3.Add(destination); + return; + } + + ulong tsi = lctFrame.LctHeader.TransportSessionIdentifier; + ulong toi = lctFrame.LctHeader.TransportObjectIdentifier; + Tuple fluteCoordinate = new Tuple(internetHeader.DestinationAddress, udpPacket.DestinationPort, tsi, toi); + FluteListener targetListener; + + if (flutes == null) + { + flutes = new Dictionary, 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(" extractXmlAttributes = ExtractXmlAttributes(xr); + HandleFdtInstanceAttributes(result, extractXmlAttributes); + break; + case "fdt:File": + Dictionary 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 extractXmlAttributes) + { + foreach (KeyValuePair 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 ExtractXmlAttributes(XmlReader xr) + { + Dictionary result = new Dictionary(); + if (!xr.HasAttributes) + return result; + + xr.MoveToFirstAttribute(); + do + { + result.Add(xr.Name,xr.Value); + } while (xr.MoveToNextAttribute()); + + return result; + } } } diff --git a/skyscraper8/Atsc/AtscException.cs b/skyscraper8/Atsc/AtscException.cs new file mode 100644 index 0000000..b1d7321 --- /dev/null +++ b/skyscraper8/Atsc/AtscException.cs @@ -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) + { + } + } +} diff --git a/skyscraper8/Ietf/FLUTE/FluteListener.cs b/skyscraper8/Ietf/FLUTE/FluteListener.cs index 23d161f..a0abaec 100644 --- a/skyscraper8/Ietf/FLUTE/FluteListener.cs +++ b/skyscraper8/Ietf/FLUTE/FluteListener.cs @@ -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, FluteBlock>(); + if (lctFrame.Atsc3Compliant) + { + if (blocks.Count == 0) + { + Tuple fakeKey = new Tuple(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 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; + } } } diff --git a/skyscraper8/Ietf/FLUTE/FluteUtilities.cs b/skyscraper8/Ietf/FLUTE/FluteUtilities.cs index e16c9cf..9b7cc50 100644 --- a/skyscraper8/Ietf/FLUTE/FluteUtilities.cs +++ b/skyscraper8/Ietf/FLUTE/FluteUtilities.cs @@ -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 } }