diff --git a/skyscraper8.Tests/Tests/CoverageTests/MathTests.cs b/skyscraper8.Tests/Tests/CoverageTests/MathTests.cs
new file mode 100644
index 0000000..5c01822
--- /dev/null
+++ b/skyscraper8.Tests/Tests/CoverageTests/MathTests.cs
@@ -0,0 +1,19 @@
+using skyscraper8.Skyscraper.Math;
+
+namespace skyscraper8.Tests.NUnit.Tests.CoverageTests;
+
+[TestFixture]
+public class MathTests
+{
+ [Test]
+ public void TestPlsConversions()
+ {
+ int originalGoldCode = 131070;
+
+ int calculatedRootCode = PlsCodeConverter.GoldToRoot(originalGoldCode);
+ Assert.That(calculatedRootCode, Is.EqualTo(16416));
+
+ int calculatedGoldCode = PlsCodeConverter.RootToGold(calculatedRootCode);
+ Assert.That(calculatedGoldCode, Is.EqualTo(originalGoldCode));
+ }
+}
\ No newline at end of file
diff --git a/skyscraper8.sln.DotSettings.user b/skyscraper8.sln.DotSettings.user
index 0f7a660..6aa6dc7 100644
--- a/skyscraper8.sln.DotSettings.user
+++ b/skyscraper8.sln.DotSettings.user
@@ -17,6 +17,7 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
@@ -38,6 +39,7 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
diff --git a/skyscraper8/Program.cs b/skyscraper8/Program.cs
index 5e567cc..375da65 100644
--- a/skyscraper8/Program.cs
+++ b/skyscraper8/Program.cs
@@ -103,317 +103,337 @@ namespace skyscraper5
if (args.Length != 0)
{
- if (args[0].ToLowerInvariant().EndsWith(".ts"))
- {
- FileInfo fi = new FileInfo(args[0]);
- if (fi.Exists)
- HandleSingleFile(fi);
- else
- Console.WriteLine("{0} missing.", fi.FullName);
- return;
- }
+ if (args[0].ToLowerInvariant().EndsWith(".ts"))
+ {
+ FileInfo fi = new FileInfo(args[0]);
+ if (fi.Exists)
+ HandleSingleFile(fi);
+ else
+ Console.WriteLine("{0} missing.", fi.FullName);
+ return;
+ }
- if (args[0].ToLowerInvariant().EndsWith(".m3u8"))
- {
- M3U8Stream m3U8Stream = new M3U8Stream(args[0]);
- DataStorage dataStorage = new InMemoryScraperStorage();
- ObjectStorage objectStorage = new FilesystemStorage(new DirectoryInfo("."));
- SkyscraperContext skyscraperContext = new SkyscraperContext(new TsContext(), dataStorage, objectStorage);
- skyscraperContext.InitalizeFilterChain();
- skyscraperContext.IngestFromStream(m3U8Stream);
- return;
- }
+ if (args[0].ToLowerInvariant().EndsWith(".m3u8"))
+ {
+ M3U8Stream m3U8Stream = new M3U8Stream(args[0]);
+ DataStorage dataStorage = new InMemoryScraperStorage();
+ ObjectStorage objectStorage = new FilesystemStorage(new DirectoryInfo("."));
+ SkyscraperContext skyscraperContext =
+ new SkyscraperContext(new TsContext(), dataStorage, objectStorage);
+ skyscraperContext.InitalizeFilterChain();
+ skyscraperContext.IngestFromStream(m3U8Stream);
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("bumutest"))
- {
- FileInfo fi = new FileInfo(args[1]);
- FileStream fstream = fi.OpenRead();
-
- TsContext mpeg2 = new TsContext();
- SkyscraperContext skyscraper = new SkyscraperContext(mpeg2, new InMemoryScraperStorage(), new BlackholeObjectStorage());
- skyscraper.InitalizeFilterChain();
+ if (args[0].ToLowerInvariant().Equals("bumutest"))
+ {
+ FileInfo fi = new FileInfo(args[1]);
+ FileStream fstream = fi.OpenRead();
- MultiprotocolEncapsulationDecoder mpeDecoder = new MultiprotocolEncapsulationDecoder(skyscraper);
- PsiDecoder psiDecoder = new PsiDecoder(0x1019, mpeDecoder);
- mpeg2.RegisterPacketProcessor(0x1019, psiDecoder);
+ TsContext mpeg2 = new TsContext();
+ SkyscraperContext skyscraper = new SkyscraperContext(mpeg2, new InMemoryScraperStorage(),
+ new BlackholeObjectStorage());
+ skyscraper.InitalizeFilterChain();
- skyscraper.IngestFromStream(fstream);
- }
- if (args[0].Equals("aactest"))
- {
- new AacTestProgram().Run();
- return;
- }
- if (args[0].ToLowerInvariant().Equals("cscan"))
- {
- HandleCrazyScanToTestingSystem(args[1]);
- return;
- }
- if (args[0].ToLowerInvariant().Equals("cscan-live"))
- {
- HandleCrazyScanToLiveSystem(args[1]);
- return;
- }
- if (args[0].ToLowerInvariant().Equals("cscan-live-notimeout"))
- {
- HandleCrazyScanToLiveSystem(args[1], false);
- return;
- }
+ MultiprotocolEncapsulationDecoder mpeDecoder = new MultiprotocolEncapsulationDecoder(skyscraper);
+ PsiDecoder psiDecoder = new PsiDecoder(0x1019, mpeDecoder);
+ mpeg2.RegisterPacketProcessor(0x1019, psiDecoder);
- if (args[0].ToLowerInvariant().Equals("udpin"))
- {
- HandleUdpTesting();
- return;
- }
- if (args[0].ToLowerInvariant().Equals("udp-agent"))
- {
- HandleUdpLive();
- return;
- }
+ skyscraper.IngestFromStream(fstream);
+ }
- if (args[0].Equals("abertest"))
- {
- FileInfo fi = new FileInfo(args[1]);
- if (!fi.Exists)
- {
- Console.WriteLine("{0} not found", fi.FullName);
- return;
- }
- AbertisDemoProgram abertisDemo = new AbertisDemoProgram(fi);
- abertisDemo.Run();
- return;
- }
+ if (args[0].Equals("aactest"))
+ {
+ new AacTestProgram().Run();
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("t2mitest"))
- {
- new TestApp(args[1]).Run();
- return;
- }
-
- if (args[0].ToLowerInvariant().Equals("ioctltest"))
- {
- IoctlTest();
- return;
- }
-
- if (args[0].ToLowerInvariant().Equals("m2tstots"))
- {
- FileInfo infile = new FileInfo(args[1]);
- FileInfo outfile = new FileInfo(args[2]);
- new M2TsToTs(infile,outfile).Run();
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("cscan"))
+ {
+ HandleCrazyScanToTestingSystem(args[1]);
+ return;
+ }
- if (Directory.Exists(args[0]))
- {
- DirectoryInfo doThisDir = new DirectoryInfo(args[0]);
- ProcessDirectory(doThisDir);
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("cscan-live"))
+ {
+ HandleCrazyScanToLiveSystem(args[1]);
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("proxytest"))
- {
- TcpTsProxy tcpTsProxy = new TcpTsProxy();
- Thread.Sleep(5000);
- tcpTsProxy.Dispose();
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("cscan-live-notimeout"))
+ {
+ HandleCrazyScanToLiveSystem(args[1], false);
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("importtest"))
- {
- DirectoryInfo doThisDir = new DirectoryInfo(args[1]);
- InMemoryScraperStorage imss = new InMemoryScraperStorage();
- FilesystemStorage fs = new FilesystemStorage(new DirectoryInfo("."));
- TsFileCollectionImporter importer = new TsFileCollectionImporter(imss, fs, doThisDir);
- importer.Run();
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("udpin"))
+ {
+ HandleUdpTesting();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("udp-agent"))
+ {
+ HandleUdpLive();
+ return;
+ }
+
+ if (args[0].Equals("abertest"))
+ {
+ FileInfo fi = new FileInfo(args[1]);
+ if (!fi.Exists)
+ {
+ Console.WriteLine("{0} not found", fi.FullName);
+ return;
+ }
+
+ AbertisDemoProgram abertisDemo = new AbertisDemoProgram(fi);
+ abertisDemo.Run();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("t2mitest"))
+ {
+ new TestApp(args[1]).Run();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("ioctltest"))
+ {
+ IoctlTest();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("m2tstots"))
+ {
+ FileInfo infile = new FileInfo(args[1]);
+ FileInfo outfile = new FileInfo(args[2]);
+ new M2TsToTs(infile, outfile).Run();
+ return;
+ }
+
+ if (Directory.Exists(args[0]))
+ {
+ DirectoryInfo doThisDir = new DirectoryInfo(args[0]);
+ ProcessDirectory(doThisDir);
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("proxytest"))
+ {
+ TcpTsProxy tcpTsProxy = new TcpTsProxy();
+ Thread.Sleep(5000);
+ tcpTsProxy.Dispose();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("importtest"))
+ {
+ DirectoryInfo doThisDir = new DirectoryInfo(args[1]);
+ InMemoryScraperStorage imss = new InMemoryScraperStorage();
+ FilesystemStorage fs = new FilesystemStorage(new DirectoryInfo("."));
+ TsFileCollectionImporter importer = new TsFileCollectionImporter(imss, fs, doThisDir);
+ importer.Run();
+ return;
+ }
- //fsimport "D:\Skyscraper" "E:\\"
- if (args[0].ToLowerInvariant().Equals("fsimport"))
- {
- DirectoryInfo srcDir = new DirectoryInfo(args[1]);
- DirectoryInfo tgtDir = new DirectoryInfo(args[2]);
- FilesystemStorage fss = new FilesystemStorage(tgtDir);
- TsFileCollectionImporter importer = new TsFileCollectionImporter(fss, fss, srcDir);
- importer.Run();
- return;
- }
-
- if (args[0].ToLowerInvariant().Equals("webservertest"))
- {
- SkyscraperWebserver webserverTest = SkyscraperWebserver.GetInstance();
- webserverTest.Start();
- while (webserverTest.IsListening)
- {
- Thread.Sleep(1000);
- }
+ //fsimport "D:\Skyscraper" "E:\\"
+ if (args[0].ToLowerInvariant().Equals("fsimport"))
+ {
+ DirectoryInfo srcDir = new DirectoryInfo(args[1]);
+ DirectoryInfo tgtDir = new DirectoryInfo(args[2]);
+ FilesystemStorage fss = new FilesystemStorage(tgtDir);
+ TsFileCollectionImporter importer = new TsFileCollectionImporter(fss, fss, srcDir);
+ importer.Run();
+ return;
+ }
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("webservertest"))
+ {
+ SkyscraperWebserver webserverTest = SkyscraperWebserver.GetInstance();
+ webserverTest.Start();
+ while (webserverTest.IsListening)
+ {
+ Thread.Sleep(1000);
+ }
- if (args[0].ToLowerInvariant().Equals("gpstest"))
- {
- GpsManager.GpsTest();
- return;
- }
-
- if (args[0].ToLowerInvariant().Equals("what-can-i-receive"))
- {
- WhatCanIReceive.StandaloneProgram();
- return;
- }
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("make-dummy-ts"))
- {
- DummyTsGenerator dummyTsGenerator = new DummyTsGenerator();
- dummyTsGenerator.Run();
- return;
- }
-
- if (args[0].ToLowerInvariant().Equals("file-live"))
- {
- Program.HandleFileToLiveSystem(args[1]);
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("gpstest"))
+ {
+ GpsManager.GpsTest();
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("hlsproxy"))
- {
- DirectoryInfo di = new DirectoryInfo(args[1]);
- HlsProxy hlsproxy = new HlsProxy(di);
- hlsproxy.Run();
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("what-can-i-receive"))
+ {
+ WhatCanIReceive.StandaloneProgram();
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("hlsproxy-destructive"))
- {
- DirectoryInfo di = new DirectoryInfo(args[1]);
- HlsProxy hlsproxy = new HlsProxy(di);
- hlsproxy.DestructiveMode = true;
- hlsproxy.Run();
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("make-dummy-ts"))
+ {
+ DummyTsGenerator dummyTsGenerator = new DummyTsGenerator();
+ dummyTsGenerator.Run();
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("satip"))
- {
- QuickAndDirtySatIpClient qadsipc = new QuickAndDirtySatIpClient(args);
- qadsipc.Run();
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("file-live"))
+ {
+ Program.HandleFileToLiveSystem(args[1]);
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("satip-playout"))
- {
- QuickAndDirtySatIpClient qadsipc = new QuickAndDirtySatIpClient(args);
- qadsipc.SetPlayoutMode(args);
- qadsipc.Run();
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("hlsproxy"))
+ {
+ DirectoryInfo di = new DirectoryInfo(args[1]);
+ HlsProxy hlsproxy = new HlsProxy(di);
+ hlsproxy.Run();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("hlsproxy-destructive"))
+ {
+ DirectoryInfo di = new DirectoryInfo(args[1]);
+ HlsProxy hlsproxy = new HlsProxy(di);
+ hlsproxy.DestructiveMode = true;
+ hlsproxy.Run();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("satip"))
+ {
+ QuickAndDirtySatIpClient qadsipc = new QuickAndDirtySatIpClient(args);
+ qadsipc.Run();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("satip-playout"))
+ {
+ QuickAndDirtySatIpClient qadsipc = new QuickAndDirtySatIpClient(args);
+ qadsipc.SetPlayoutMode(args);
+ qadsipc.Run();
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("shannon"))
- {
- if (args.Length != 2)
- {
- Console.WriteLine("Specify ONE file, please.");
- return;
- }
- FileInfo fi = new FileInfo(args[1]);
- if (!fi.Exists)
- {
- Console.WriteLine("{0} doesn't exist.", fi.FullName);
- return;
- }
- FileStream fileStream = fi.OpenRead();
- EntropyCalculatorStream entropyCalculatorStream = new EntropyCalculatorStream();
- fileStream.CopyTo(entropyCalculatorStream);
- fileStream.Close();
- fileStream.Dispose();
- Console.WriteLine("Entropy value: {0} ({1}%)", entropyCalculatorStream.Entropy, entropyCalculatorStream.Percentage);
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("shannon"))
+ {
+ if (args.Length != 2)
+ {
+ Console.WriteLine("Specify ONE file, please.");
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("pcap-on"))
- {
- TogglePcapConfiguration(1);
- return;
- }
- if (args[0].ToLowerInvariant().Equals("pcap-off"))
- {
- TogglePcapConfiguration(2);
- return;
- }
+ FileInfo fi = new FileInfo(args[1]);
+ if (!fi.Exists)
+ {
+ Console.WriteLine("{0} doesn't exist.", fi.FullName);
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("subts-on"))
- {
- ToggleSubTsDumpConfiguration(true);
- return;
- }
+ FileStream fileStream = fi.OpenRead();
+ EntropyCalculatorStream entropyCalculatorStream = new EntropyCalculatorStream();
+ fileStream.CopyTo(entropyCalculatorStream);
+ fileStream.Close();
+ fileStream.Dispose();
+ Console.WriteLine("Entropy value: {0} ({1}%)", entropyCalculatorStream.Entropy,
+ entropyCalculatorStream.Percentage);
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("subts-off"))
- {
- ToggleSubTsDumpConfiguration(false);
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("pcap-on"))
+ {
+ TogglePcapConfiguration(1);
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("pts2bbf"))
- {
- if (args[1].ToLowerInvariant().Equals("bbfudpdecap"))
- {
- FileInfo fi = new FileInfo(args[2]);
- Pts2Bbf.Run(fi, true);
- return;
- }
- else
- {
- FileInfo fi = new FileInfo(args[1]);
- Pts2Bbf.Run(fi, false);
- return;
- }
- }
+ if (args[0].ToLowerInvariant().Equals("pcap-off"))
+ {
+ TogglePcapConfiguration(2);
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("pts2bbf2"))
- {
- FileInfo fi = new FileInfo(args[1]);
- Pts2Bbf2 pts2Bbf2 = new Pts2Bbf2(fi);
- pts2Bbf2.Run();
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("subts-on"))
+ {
+ ToggleSubTsDumpConfiguration(true);
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("stid135test"))
- {
- FileInfo fi = new FileInfo(args[1]);
- Stid135Test.Run(fi);
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("subts-off"))
+ {
+ ToggleSubTsDumpConfiguration(false);
+ return;
+ }
- if (args[0].ToLowerInvariant().Equals("make-catalogue"))
- {
- DirectoryInfo di = new DirectoryInfo(args[1]);
- logger.InfoFormat("Input directory: {0}", di.FullName);
- FileInfo fi = new FileInfo(args[2]);
- logger.InfoFormat("Output File: {0}", fi.FullName);
- CatalogueGenerator catalogueGenerator = new CatalogueGenerator(di, fi);
- catalogueGenerator.Run();
- catalogueGenerator.Dispose();
- return;
- }
- if (args[0].ToLowerInvariant().Equals("rotation-catalogue"))
- {
- TsRotationToCsv rotator = new TsRotationToCsv();
- rotator.SourceDir = new DirectoryInfo(args[1]);
- rotator.DestinationCsv = new FileInfo(args[2]);
- rotator.Run();
- return;
- }
+ if (args[0].ToLowerInvariant().Equals("pts2bbf"))
+ {
+ if (args[1].ToLowerInvariant().Equals("bbfudpdecap"))
+ {
+ FileInfo fi = new FileInfo(args[2]);
+ Pts2Bbf.Run(fi, true);
+ return;
+ }
+ else
+ {
+ FileInfo fi = new FileInfo(args[1]);
+ Pts2Bbf.Run(fi, false);
+ return;
+ }
+ }
- if (args[0].ToLowerInvariant().Equals("salvage"))
- {
- Salvager salvager = new Salvager();
- salvager.Run(args);
- return;
- }
- }
+ if (args[0].ToLowerInvariant().Equals("pts2bbf2"))
+ {
+ FileInfo fi = new FileInfo(args[1]);
+ Pts2Bbf2 pts2Bbf2 = new Pts2Bbf2(fi);
+ pts2Bbf2.Run();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("stid135test"))
+ {
+ FileInfo fi = new FileInfo(args[1]);
+ Stid135Test.Run(fi);
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("make-catalogue"))
+ {
+ DirectoryInfo di = new DirectoryInfo(args[1]);
+ logger.InfoFormat("Input directory: {0}", di.FullName);
+ FileInfo fi = new FileInfo(args[2]);
+ logger.InfoFormat("Output File: {0}", fi.FullName);
+ CatalogueGenerator catalogueGenerator = new CatalogueGenerator(di, fi);
+ catalogueGenerator.Run();
+ catalogueGenerator.Dispose();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("rotation-catalogue"))
+ {
+ TsRotationToCsv rotator = new TsRotationToCsv();
+ rotator.SourceDir = new DirectoryInfo(args[1]);
+ rotator.DestinationCsv = new FileInfo(args[2]);
+ rotator.Run();
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("salvage"))
+ {
+ Salvager salvager = new Salvager();
+ salvager.Run(args);
+ return;
+ }
+
+ if (args[0].ToLowerInvariant().Equals("satip-oldconfig"))
+ {
+ SatIpOldconfig satIpOldconfig = new SatIpOldconfig();
+ satIpOldconfig.Run();
+ return;
+ }
+ }
/*Passing passing = new Passing();
if (!passing.Boot())
diff --git a/skyscraper8/QuickAndDirtySatIpClient.cs b/skyscraper8/QuickAndDirtySatIpClient.cs
index 045c641..deaf70c 100644
--- a/skyscraper8/QuickAndDirtySatIpClient.cs
+++ b/skyscraper8/QuickAndDirtySatIpClient.cs
@@ -13,6 +13,7 @@ using skyscraper5.Mpeg2;
using skyscraper5.Skyscraper.Scraper;
using skyscraper5.Skyscraper.Scraper.Storage.Filesystem;
using skyscraper5.Skyscraper.Scraper.Storage.InMemory;
+using skyscraper8.Skyscraper;
using skyscraper8.Skyscraper.Scraper.Storage;
namespace skyscraper8
@@ -124,7 +125,25 @@ namespace skyscraper8
rtspClient.AutoReconnect = false;
Keepalive();
DiSEqC_Opcode opcode = BuildDiseqcOpcode();
- string url = RtspClient.MakeUrl(opcode, frequency, isS2, symbolRate, null, false);
+ string url = null;
+ if (oldconfig != null)
+ {
+ url = RtspClient.MakeUrl(oldconfig);
+ }
+ else
+ {
+ url = RtspClient.MakeUrl(opcode, frequency, isS2, symbolRate, null, false);
+ }
+
+ if (string.IsNullOrEmpty(url))
+ {
+ logger.Fatal("Somehow I reached a code-path that didn't generate a SAT>IP String. This is a bug, tell Fey.");
+ return;
+ }
+ else
+ {
+ logger.DebugFormat("SAT>IP Tuning String: {0}", url);
+ }
RtspDescribeResponse describe = rtspClient.GetDescribe(url);
SessionDescriptionProtocol sessionDescriptionProtocol = describe.GetSessionDescriptionProtocol();
@@ -250,8 +269,36 @@ namespace skyscraper8
private int destinationPort;
private bool playoutMode;
private bool multicastMode;
+ private readonly SatIpOldconfig oldconfig;
- private void Setup_OnRtcpPacket(byte[] data, int length)
+ public QuickAndDirtySatIpClient(SatIpOldconfig args)
+ {
+ this.oldconfig = args;
+
+ if (args.ServerIp.Equals("auto"))
+ {
+ this.ipAddress = this.AutodetectIPAddress();
+ }
+ else
+ {
+ this.ipAddress = IPAddress.Parse(args.ServerIp);
+ }
+
+ this.diseqcNumber = args.Diseqc;
+ this.polarity = args.Polarity.ToUpperInvariant()[0];
+ this.frequency = args.Frequency;
+ this.isS2 = ParseDvbStandard(args.System);
+ this.symbolRate = args.SymbolRate;
+
+ //Playout mode
+ this.destinationPort = args.DestinationPort;
+ this.playoutMode = true;
+ this.destinationIp = IPAddress.Parse(args.DestinationIp);
+ this.multicastMode = true;
+ }
+
+
+ private void Setup_OnRtcpPacket(byte[] data, int length)
{
//rtcps++;
}
diff --git a/skyscraper8/SatIp/RtspClient.cs b/skyscraper8/SatIp/RtspClient.cs
index 6a6891c..7029829 100644
--- a/skyscraper8/SatIp/RtspClient.cs
+++ b/skyscraper8/SatIp/RtspClient.cs
@@ -5,10 +5,13 @@ using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
+using log4net;
using skyscraper5.Skyscraper;
using skyscraper5.Skyscraper.IO.CrazycatStreamReader;
using skyscraper8.SatIp.RtspRequests;
using skyscraper8.SatIp.RtspResponses;
+using skyscraper8.Skyscraper;
+using skyscraper8.Skyscraper.Math;
namespace skyscraper8.SatIp
{
@@ -16,6 +19,8 @@ namespace skyscraper8.SatIp
{
private uint cseqCounter;
private const string USER_AGENT = "sophiaNetRtspClient/1.0";
+ private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name);
+
public RtspClient(string ip, int port)
{
@@ -339,6 +344,62 @@ namespace skyscraper8.SatIp
return sb.ToString();
}
+ public static string? MakeUrl(SatIpOldconfig config)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.AppendFormat("/?src={0}", config.Diseqc);
+ sb.AppendFormat("&freq={0}", config.Frequency);
+ sb.AppendFormat("&pol={0}", config.Polarity.ToLowerInvariant()[0]);
+ sb.AppendFormat("&msys=dvb{0}", config.System.ToLowerInvariant());
+ sb.AppendFormat("&sr={0}", config.SymbolRate);
+ sb.AppendFormat("&pids=all");
+
+ string xIsi = null;
+ int? xPls = null;
+ if (config.UseDigitalDevicesExtensions)
+ {
+ if (config.ForceBbframeOutput)
+ {
+ xIsi = "0x80000000";
+ }
+ else if (config.ForceIqOutput)
+ {
+ xIsi = "0x10000000";
+ }
+ else if (config.EnableMis)
+ {
+ xIsi = config.MisSelection.ToString();
+ }
+
+ switch (config.PlsMode)
+ {
+ case "none":
+ break;
+ case "gold":
+ xPls = config.PlsValue;
+ break;
+ case "root":
+ xPls = PlsCodeConverter.RootToGold(config.PlsValue);
+ break;
+ default:
+ logger.WarnFormat("Don't know what PLS code Type {0} is, assuming no PLS.",config.PlsMode);
+ break;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(xIsi))
+ {
+ sb.AppendFormat("&x_isi={0}", xIsi);
+ }
+
+ if (xPls.HasValue)
+ {
+ sb.AppendFormat("&x_pls={0}", xPls.Value);
+ }
+
+ return sb.ToString();
+ }
+
private bool disposed;
public void Dispose()
{
diff --git a/skyscraper8/SimpleServiceDiscoveryProtocol/SsdpClient.cs b/skyscraper8/SimpleServiceDiscoveryProtocol/SsdpClient.cs
index bf03811..7ea9c31 100644
--- a/skyscraper8/SimpleServiceDiscoveryProtocol/SsdpClient.cs
+++ b/skyscraper8/SimpleServiceDiscoveryProtocol/SsdpClient.cs
@@ -33,14 +33,22 @@ namespace skyscraper8.SimpleServiceDiscoveryProtocol
x =>
{
byte[] addressBytes = x.Address.GetAddressBytes();
- if (addressBytes[0] == 169 || addressBytes.Length != 4)
- {
- logger.WarnFormat("The address {0} will not be used because it's network type is not supported.", new IPAddress(addressBytes));
- return null;
+ if (addressBytes[0] == 169 || addressBytes.Length != 4)
+ {
+ logger.WarnFormat("The address {0} will not be used because it's network type is not supported.", x.Address);
+ return null;
}
Socket temp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
temp.Bind(x);
- temp.SendTo(Encoding.UTF8.GetBytes(SEARCH_STRING), SocketFlags.None, remoteEndPoint);
+ try
+ {
+ temp.SendTo(Encoding.UTF8.GetBytes(SEARCH_STRING), SocketFlags.None, remoteEndPoint);
+ }
+ catch (SocketException e)
+ {
+ logger.WarnFormat("The address {0} will not be used because an error occurred when broadcasting the search string: {1}",x.Address,e.Message);
+ }
+
return temp;
});
@@ -84,10 +92,10 @@ namespace skyscraper8.SimpleServiceDiscoveryProtocol
{
IPInterfaceProperties ipProperties = networkInterface.GetIPProperties();
UnicastIPAddressInformationCollection addresses = ipProperties.UnicastAddresses;
- if (addresses.Count == 0)
- {
- logger.WarnFormat("Network interface {0} does not have an unicast address.", networkInterface.Name);
- return IPAddress.Loopback;
+ if (addresses.Count == 0)
+ {
+ logger.WarnFormat("Network interface {0} does not have an unicast address.", networkInterface.Name);
+ return IPAddress.Loopback;
}
UnicastIPAddressInformation[] addressArray = addresses.ToArray();
Array.Sort(addressArray, (a, b) => RateIpAddress(b).CompareTo(RateIpAddress(a)));
diff --git a/skyscraper8/Skyscraper/Ini.cs b/skyscraper8/Skyscraper/Ini.cs
index 35ae392..3088777 100644
--- a/skyscraper8/Skyscraper/Ini.cs
+++ b/skyscraper8/Skyscraper/Ini.cs
@@ -90,6 +90,19 @@ namespace skyscraper5.Skyscraper
return Boolean.Parse(s);
}
+ public ushort ReadValue(string category, string key, ushort defaultVaule)
+ {
+ if (!ContainsKey(category))
+ return defaultVaule;
+
+ IniSection section = this[category];
+ if (!section.ContainsKey(key))
+ return defaultVaule;
+
+ string s = section[key];
+ return ushort.Parse(s);
+ }
+
public int ReadValue(string category, string key, int defaultVaule)
{
if (!ContainsKey(category))
@@ -103,6 +116,20 @@ namespace skyscraper5.Skyscraper
return Int32.Parse(s);
}
+ public byte ReadValue(string category, string key, byte defaultVaule)
+ {
+ if (!ContainsKey(category))
+ return defaultVaule;
+
+ IniSection section = this[category];
+ if (!section.ContainsKey(key))
+ return defaultVaule;
+
+ string s = section[key];
+ return Byte.Parse(s);
+ }
+
+
public string ReadValue(string category, string key, string defaultVaule)
{
if (!ContainsKey(category))
diff --git a/skyscraper8/Skyscraper/Math/PlsCodeConverter.cs b/skyscraper8/Skyscraper/Math/PlsCodeConverter.cs
new file mode 100644
index 0000000..8ad4a3d
--- /dev/null
+++ b/skyscraper8/Skyscraper/Math/PlsCodeConverter.cs
@@ -0,0 +1,32 @@
+namespace skyscraper8.Skyscraper.Math;
+
+public static class PlsCodeConverter
+{
+ public static int RootToGold(int root)
+ {
+ var g = 0;
+ var X = 1;
+ for( g = 0; g < 262144; g += 1)
+ {
+ if( X == root )
+ break;
+ X = ((( X ^ (X >> 7)) & 1) << 17) | (X >> 1);
+ }
+
+ return g;
+ }
+
+ public static int GoldToRoot(int gold)
+ {
+ var g = 0;
+ var X = 1;
+ for( g = 0; g < 262143; g += 1)
+ {
+ if( g == gold )
+ break;
+ X = ((( X ^ (X >> 7)) & 1) << 17) | (X >> 1);
+ }
+
+ return X;
+ }
+}
\ No newline at end of file
diff --git a/skyscraper8/Skyscraper/OldconfigEngine.cs b/skyscraper8/Skyscraper/OldconfigEngine.cs
new file mode 100644
index 0000000..ba8b65f
--- /dev/null
+++ b/skyscraper8/Skyscraper/OldconfigEngine.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+public enum ConfigType { Bool, Choice, String, ValidatedString }
+
+// Delegate signature for custom validation functions
+public delegate (bool IsValid, string? ErrorMessage) ConfigValidator(string input);
+
+public class ConfigOption
+{
+ public string Id { get; set; }
+ public string Description { get; set; }
+ public ConfigType Type { get; set; } = ConfigType.Bool;
+ public string DefaultValue { get; set; } = "y";
+ public List Choices { get; set; } = [];
+ public Func, bool>? Dependency { get; set; }
+
+ // Delegate for string validation rules
+ public ConfigValidator? Validator { get; set; }
+
+ public ConfigOption()
+ {
+ }
+
+ public ConfigOption(string id, string description, ConfigType type, string defaultValue)
+ {
+ Id = id;
+ Description = description;
+ Type = type;
+ DefaultValue = defaultValue;
+ }
+
+ public void SetChoices(params string[] args)
+ {
+ Choices = new List(args);
+ }
+}
+
+public class OldConfigEngine
+{
+ private readonly List _schema = [];
+ private readonly Dictionary _currentConfig = [];
+
+ public void RegisterOption(ConfigOption option) => _schema.Add(option);
+
+ public void LoadExistingConfig(string filePath)
+ {
+ if (!File.Exists(filePath)) return;
+ foreach (var line in File.ReadAllLines(filePath))
+ {
+ var trimmed = line.Trim();
+ if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#")) continue;
+ var parts = trimmed.Split('=', 2);
+ if (parts.Length == 2) _currentConfig[parts[0].Trim()] = parts[1].Trim();
+ }
+ }
+
+ public void RunInteractiveConfig()
+ {
+ Console.WriteLine("*");
+ Console.WriteLine("* Restart config based on current configuration");
+ Console.WriteLine("*");
+
+ foreach (var opt in _schema)
+ {
+ if (opt.Dependency != null && !opt.Dependency(_currentConfig))
+ continue;
+
+ if (_currentConfig.ContainsKey(opt.Id))
+ continue;
+
+ string selectedValue = PromptUser(opt);
+ _currentConfig[opt.Id] = selectedValue;
+ }
+
+ Console.WriteLine("\n*** End of configuration. ***");
+ }
+
+ public string PromptUser(ConfigOption opt)
+ {
+ while (true)
+ {
+ switch (opt.Type)
+ {
+ case ConfigType.Bool:
+ string choicesHint = opt.DefaultValue.ToLower() == "y" ? "[Y/n]" : "[y/N]";
+ Console.Write($"{opt.Description} ({opt.Id}) {choicesHint} ");
+
+ var key = Console.ReadKey(intercept: false);
+ Console.WriteLine();
+
+ if (key.Key == ConsoleKey.Enter)
+ return opt.DefaultValue;
+ if (key.Key == ConsoleKey.Y)
+ return "true";
+ if (key.Key == ConsoleKey.N)
+ return "false";
+ if (key.Key == ConsoleKey.Q || key.KeyChar == '?')
+ {
+ Console.WriteLine($" Help: Toggles option {opt.Id}. Select Y or N.");
+ continue;
+ }
+ break;
+
+ case ConfigType.Choice:
+ Console.WriteLine($"{opt.Description} ({opt.Id})");
+ for (int i = 0; i < opt.Choices.Count; i++)
+ {
+ string marker = opt.Choices[i] == opt.DefaultValue ? " (NEW)" : "";
+ Console.WriteLine($" {i + 1}. {opt.Choices[i]}{marker}");
+ }
+ Console.Write($"choice[1-{opt.Choices.Count}]: ");
+
+ string? choiceInput = Console.ReadLine()?.Trim();
+ if (string.IsNullOrEmpty(choiceInput)) return opt.DefaultValue;
+ if (int.TryParse(choiceInput, out int idx) && idx >= 1 && idx <= opt.Choices.Count)
+ {
+ return opt.Choices[idx - 1];
+ }
+ break;
+
+ case ConfigType.String:
+ Console.Write($"{opt.Description} ({opt.Id}) [{opt.DefaultValue}]: ");
+ string? strInput = Console.ReadLine()?.Trim();
+ return string.IsNullOrEmpty(strInput) ? opt.DefaultValue : strInput;
+
+ case ConfigType.ValidatedString:
+ Console.Write($"{opt.Description} ({opt.Id}) [{opt.DefaultValue}]: ");
+ string rawVal = Console.ReadLine()?.Trim() ?? string.Empty;
+ string valueToValidate = string.IsNullOrEmpty(rawVal) ? opt.DefaultValue : rawVal;
+
+ if (opt.Validator != null)
+ {
+ var (isValid, errorMessage) = opt.Validator(valueToValidate);
+ if (!isValid)
+ {
+ Console.WriteLine($" [Error] {errorMessage ?? "Invalid value provided."}");
+ continue; // Re-prompt on validation failure
+ }
+ }
+
+ return valueToValidate;
+ }
+
+ Console.WriteLine("Invalid choice. Try again.");
+ }
+ }
+
+ public void SaveConfig(string filePath)
+ {
+ using var writer = new StreamWriter(filePath);
+ writer.WriteLine($"# Automatically generated config - {DateTime.UtcNow:u}");
+ foreach (var kvp in _currentConfig)
+ {
+ writer.WriteLine($"{kvp.Key}={kvp.Value}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/skyscraper8/Skyscraper/SatIpOldconfig.cs b/skyscraper8/Skyscraper/SatIpOldconfig.cs
new file mode 100644
index 0000000..1484be3
--- /dev/null
+++ b/skyscraper8/Skyscraper/SatIpOldconfig.cs
@@ -0,0 +1,196 @@
+using System.Net;
+using skyscraper5.Skyscraper;
+using skyscraper5.Skyscraper.Plugins;
+using skyscraper5.Skyscraper.Scraper;
+
+namespace skyscraper8.Skyscraper;
+
+public class SatIpOldconfig
+{
+ private const string INI_CATEGORY = "satip_oldconfig";
+
+ public string ServerIp { get; private set; }
+ public int Diseqc{ get; private set; }
+ public string Polarity{ get; private set; }
+ public int Frequency{ get; private set; }
+ public string System{ get; private set; }
+ public int SymbolRate{ get; private set; }
+ public bool UseDigitalDevicesExtensions{ get; private set; }
+ public bool ForceBbframeOutput{ get; private set; }
+ public bool ForceIqOutput{ get; private set; }
+ public string PlsMode{ get; private set; }
+ public int PlsValue{ get; private set; }
+ public bool EnableMis{ get; private set; }
+ public byte MisSelection{ get; private set; }
+ public string DestinationIp{ get; private set; }
+ public ushort DestinationPort{ get; private set; }
+
+ public void Run()
+ {
+ PluginManager pluginManager = PluginManager.GetInstance();
+ Ini ini = pluginManager.Ini;
+ OldConfigEngine configEngine = new OldConfigEngine();
+
+ ServerIp = ini.ReadValue(INI_CATEGORY, "ip", "auto");
+ Diseqc = ini.ReadValue(INI_CATEGORY, "diseqc", 1);
+ Polarity = ini.ReadValue(INI_CATEGORY, "polarity", "H");
+ Frequency = ini.ReadValue(INI_CATEGORY, "frequency", 11523);
+ System = ini.ReadValue(INI_CATEGORY, "system", "S2");
+ SymbolRate = ini.ReadValue(INI_CATEGORY, "symbolRate", 22000);
+ UseDigitalDevicesExtensions = ini.ReadValue(INI_CATEGORY, "digitalDevicesExtensions", false);
+ ForceBbframeOutput = ini.ReadValue(INI_CATEGORY, "forceBbframeOutput", false);
+ ForceIqOutput = ini.ReadValue(INI_CATEGORY, "forceIqOutput", false);
+ PlsMode = ini.ReadValue(INI_CATEGORY, "plsMode", "none");
+ PlsValue = ini.ReadValue(INI_CATEGORY, "plsValue", 131070);
+ EnableMis = ini.ReadValue(INI_CATEGORY, "misEnabled", false);
+ MisSelection = ini.ReadValue(INI_CATEGORY, "misSelector", (byte)1);
+ DestinationIp = ini.ReadValue(INI_CATEGORY, "destinationIp", "239.192.0.1");
+ DestinationPort = ini.ReadValue(INI_CATEGORY, "destinationPort", (ushort)6969);
+
+
+ ConfigOption ipOption = new ConfigOption("ip", "The IP of the SAT>IP server or \"auto\"", ConfigType.ValidatedString, ServerIp);
+ ipOption.Validator = validateIpTuple;
+ ServerIp = configEngine.PromptUser(ipOption);
+
+ ConfigOption diseqcOption = new ConfigOption("diseqc", "DiSEqC Port 1,2,3 or 4?", ConfigType.ValidatedString, Diseqc.ToString());
+ diseqcOption.SetChoices("1", "2", "3", "4");
+ Diseqc = Int32.Parse(configEngine.PromptUser(diseqcOption));
+
+ ConfigOption polarityOption = new ConfigOption("polarity", "Polarity H or V?", ConfigType.Choice, Polarity);
+ polarityOption.SetChoices("H", "V");
+ Polarity = configEngine.PromptUser(polarityOption);
+
+ ConfigOption frequencyOption = new ConfigOption("frequency", "Frequency in MHz", ConfigType.ValidatedString, Frequency.ToString());
+ frequencyOption.Validator = frequencyValid;
+ Frequency = Int32.Parse(configEngine.PromptUser(frequencyOption));
+
+ ConfigOption systemOption = new ConfigOption("system", "Transmission system: S or S2?", ConfigType.Choice, System);
+ systemOption.SetChoices("S", "S2");
+ System = configEngine.PromptUser(systemOption);
+
+ ConfigOption symbolRateOption = new ConfigOption("symbolRate", "Symbol-Rate in k/syms", ConfigType.ValidatedString, SymbolRate.ToString());
+ frequencyOption.Validator = frequencyValid;
+ SymbolRate = Int32.Parse(configEngine.PromptUser(symbolRateOption));
+
+ ConfigOption ddExtensionsOption = new ConfigOption("ddExtensions", "Use Digital Devices OctopusNET extensions?", ConfigType.Bool, UseDigitalDevicesExtensions.ToString());
+ UseDigitalDevicesExtensions = Boolean.Parse(configEngine.PromptUser(ddExtensionsOption));
+
+ if (UseDigitalDevicesExtensions)
+ {
+ ConfigOption forceBbFrameOutputOption = new ConfigOption("forceBbframeOutput", "Force BBFRAME output?", ConfigType.Bool, ForceBbframeOutput.ToString());
+ ForceBbframeOutput = Boolean.Parse(configEngine.PromptUser(forceBbFrameOutputOption));
+
+ if (!ForceBbframeOutput)
+ {
+ ConfigOption forceIqOutputOption = new ConfigOption("forceIqOutput", "Output encapsulated IQ data?", ConfigType.Bool, ForceIqOutput.ToString());
+ ForceIqOutput = Boolean.Parse(configEngine.PromptUser(forceIqOutputOption));
+ }
+
+ ConfigOption plsModeOption = new ConfigOption("plsMode", "Physical Layer Scrambling Mode?", ConfigType.Choice, PlsMode);
+ plsModeOption.SetChoices("none", "root", "gold");
+ PlsMode = configEngine.PromptUser(plsModeOption);
+ if (!PlsMode.Equals("none"))
+ {
+ ConfigOption plsValueOption = new ConfigOption("plsValue", String.Format("{0} code?", PlsMode.ToUpperInvariant()), ConfigType.ValidatedString, PlsValue.ToString());
+ plsValueOption.Validator = frequencyValid;
+ PlsValue = Int32.Parse(configEngine.PromptUser(plsValueOption));
+ }
+
+ ConfigOption misEnabledOption = new ConfigOption("misEnabled", "Tune for Multi Input Stream?", ConfigType.Bool, "false");
+ EnableMis = Boolean.Parse(configEngine.PromptUser(misEnabledOption));
+ if (EnableMis)
+ {
+ ConfigOption misSelectorOptions = new ConfigOption("misValue", "Multi Input Stream Selection? (0-255)", ConfigType.ValidatedString, MisSelection.ToString());
+ misSelectorOptions.Validator = byteValidator;
+ MisSelection = Byte.Parse(configEngine.PromptUser(misSelectorOptions));
+ }
+ }
+
+ ConfigOption destIpOption = new ConfigOption("destinationIp", "SAT>IP Multicast destination IP address?",ConfigType.ValidatedString, DestinationIp);
+ destIpOption.Validator = serverIpValidatorTuple;
+ DestinationIp = configEngine.PromptUser(destIpOption);
+
+ ConfigOption destPortOption = new ConfigOption("destinationPort", "SAT>IP Multicast destination Port?", ConfigType.ValidatedString, DestinationPort.ToString());
+ destPortOption.Validator = destinationTypeValidatorTuple;
+ DestinationPort = ushort.Parse(configEngine.PromptUser(destPortOption));
+
+ ini.WriteValue(INI_CATEGORY, "ip", ServerIp);
+ ini.WriteValue(INI_CATEGORY, "diseqc", Diseqc);
+ ini.WriteValue(INI_CATEGORY, "polarity", Polarity);
+ ini.WriteValue(INI_CATEGORY, "frequency", Frequency);
+ ini.WriteValue(INI_CATEGORY, "system", System);
+ ini.WriteValue(INI_CATEGORY, "sybolRate", SymbolRate);
+ ini.WriteValue(INI_CATEGORY, "digitalDevicesExtensions", UseDigitalDevicesExtensions);
+ ini.WriteValue(INI_CATEGORY, "forceBbframeOutput", ForceBbframeOutput);
+ ini.WriteValue(INI_CATEGORY, "forceIqOutput", ForceIqOutput);
+ ini.WriteValue(INI_CATEGORY, "plsMode", PlsMode);
+ ini.WriteValue(INI_CATEGORY, "plsValue", PlsValue);
+ ini.WriteValue(INI_CATEGORY, "misEnabled", EnableMis);
+ ini.WriteValue(INI_CATEGORY, "misSelector", MisSelection);
+ ini.WriteValue(INI_CATEGORY, "destinationIp", DestinationIp);
+ ini.WriteValue(INI_CATEGORY, "destinationPort", DestinationPort);
+ pluginManager.SaveConfiguration();
+ Console.WriteLine("Settings saved.");
+
+ QuickAndDirtySatIpClient quickAndDirtySatIpClient = new QuickAndDirtySatIpClient(this);
+ quickAndDirtySatIpClient.Run();
+ }
+
+ private (bool IsValid, string? ErrorMessage) destinationTypeValidatorTuple(string input)
+ {
+ ushort number;
+ bool result = ushort.TryParse(input, out number);
+ if (!result)
+ {
+ return (false, "Port number must be numeric between 1024-65500");
+ }
+
+ if (number < 1024)
+ {
+ return (false, "Port Number must be above 1023");
+ }
+
+ if (number > 65500)
+ {
+ return (false, "Port Number must be below 65500");
+ }
+
+ return (true, "");
+ }
+
+ private (bool IsValid, string? ErrorMessage) serverIpValidatorTuple(string input)
+ {
+ if (input.Equals("auto"))
+ {
+ return (true, "");
+ }
+
+ IPAddress outAddress;
+ bool result = IPAddress.TryParse(input, out outAddress);
+ return (result, "Address must either be \"auto\" or a multicast IP address.");
+ }
+
+ private (bool IsValid, string? ErrorMessage) byteValidator(string input)
+ {
+ byte number;
+ bool result = Byte.TryParse(input, out number);
+ return (result, "Input must be a number between 0 and 255.");
+ }
+
+ private (bool IsValid, string? ErrorMessage) frequencyValid(string input)
+ {
+ int number;
+ bool result = Int32.TryParse(input, out number);
+ return (result, "Must be a natural 32-bit number.");
+ }
+
+ private (bool IsValid, string? ErrorMessage) validateIpTuple(string input)
+ {
+ if (input.Equals("auto"))
+ return (true,"");
+
+ IPAddress resultAddr = null;
+ bool result = IPAddress.TryParse(input, out resultAddr);
+ return (result, "Result must me a valid IPv4 address or \"auto\"");
+ }
+}
\ No newline at end of file