Added options for SAT>IP Multicast Playout and building a date-ordered TS list to CSV.

This commit is contained in:
Fey 2026-04-06 16:42:27 +02:00
parent 8de4b56c6f
commit 03f194bd34
7 changed files with 1419 additions and 1272 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"profiles": { "profiles": {
"skyscraper8": { "skyscraper8": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "satip-playout auto 1 V 12597 S2 45000 6970", "commandLineArgs": "rotation-catalogue F:\\\\ F:\\\\rotate-us.csv",
"remoteDebugEnabled": false "remoteDebugEnabled": false
}, },
"Container (Dockerfile)": { "Container (Dockerfile)": {

View File

@ -1,79 +1,79 @@
using log4net; using log4net;
using skyscraper5.Skyscraper.IO.CrazycatStreamReader; using skyscraper5.Skyscraper.IO.CrazycatStreamReader;
using skyscraper8.SatIp; using skyscraper8.SatIp;
using skyscraper8.SatIp.RtspResponses; using skyscraper8.SatIp.RtspResponses;
using skyscraper8.SimpleServiceDiscoveryProtocol; using skyscraper8.SimpleServiceDiscoveryProtocol;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using skyscraper5.Mpeg2; using skyscraper5.Mpeg2;
using skyscraper5.Skyscraper.Scraper; using skyscraper5.Skyscraper.Scraper;
using skyscraper5.Skyscraper.Scraper.Storage.Filesystem; using skyscraper5.Skyscraper.Scraper.Storage.Filesystem;
using skyscraper5.Skyscraper.Scraper.Storage.InMemory; using skyscraper5.Skyscraper.Scraper.Storage.InMemory;
using skyscraper8.Skyscraper.Scraper.Storage; using skyscraper8.Skyscraper.Scraper.Storage;
namespace skyscraper8 namespace skyscraper8
{ {
internal class QuickAndDirtySatIpClient internal class QuickAndDirtySatIpClient
{ {
private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name); private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name);
private const bool ENABLE_TS_WRITER = true; private const bool ENABLE_TS_WRITER = true;
public QuickAndDirtySatIpClient(string[] args) public QuickAndDirtySatIpClient(string[] args)
{ {
if (args.Length == 1) if (args.Length == 1)
{ {
logger.Fatal("Hey, what's your SAT>IP Server's IP Address? You can also say \"autodetect\" here."); logger.Fatal("Hey, what's your SAT>IP Server's IP Address? You can also say \"autodetect\" here.");
return; return;
} }
if (args[1].ToLowerInvariant().Contains("auto")) if (args[1].ToLowerInvariant().Contains("auto"))
{ {
ipAddress = AutodetectIPAddress(); ipAddress = AutodetectIPAddress();
} }
else else
{ {
ipAddress = IPAddress.Parse(args[1]); ipAddress = IPAddress.Parse(args[1]);
} }
if (args.Length == 2) if (args.Length == 2)
{ {
logger.Fatal("You didn't specify a DiSEqc setting. (Number between 1-4)"); logger.Fatal("You didn't specify a DiSEqc setting. (Number between 1-4)");
return; return;
} }
diseqcNumber = Int32.Parse(args[2]); diseqcNumber = Int32.Parse(args[2]);
if (args.Length == 3) if (args.Length == 3)
{ {
logger.Fatal("You didn't specify a polarity. Use H or V."); logger.Fatal("You didn't specify a polarity. Use H or V.");
return; return;
} }
polarity = args[3].ToUpperInvariant()[0]; polarity = args[3].ToUpperInvariant()[0];
if (args.Length == 4) if (args.Length == 4)
{ {
logger.Fatal("Please specify a frequency."); logger.Fatal("Please specify a frequency.");
return; return;
} }
frequency = Int32.Parse(args[4]); frequency = Int32.Parse(args[4]);
if (args.Length == 5) if (args.Length == 5)
{ {
logger.Fatal("What DVB Standard do we have here? S or S2?"); logger.Fatal("What DVB Standard do we have here? S or S2?");
return; return;
} }
isS2 = ParseDvbStandard(args[5]); isS2 = ParseDvbStandard(args[5]);
if (args.Length == 6) if (args.Length == 6)
{ {
logger.Fatal("What's the symbol rate?"); logger.Fatal("What's the symbol rate?");
return; return;
} }
symbolRate = Int32.Parse(args[6]); symbolRate = Int32.Parse(args[6]);
} }
public void SetPlayoutMode(string[] args) public void SetPlayoutMode(string[] args)
{ {
if (args.Length == 7) if (args.Length == 7)
@ -83,66 +83,79 @@ namespace skyscraper8
} }
destinationPort = int.Parse(args[7]); destinationPort = int.Parse(args[7]);
playoutMode = true; playoutMode = true;
} if (args.Length == 10)
{
private IPAddress AutodetectIPAddress() if (args[8].ToLowerInvariant().Equals("multicast"))
{ {
SsdpDevice firstSatIpServer = SsdpClient.GetFirstSatIpServer(1000, null); destinationIp = IPAddress.Parse(args[9]);
if (firstSatIpServer == null) multicastMode = true;
{ }
logger.WarnFormat("Didn't find any SAT>IP servers."); else
return null; {
} logger.FatalFormat("Don't know what {0} means.", args[8]);
IPAddress ipAddress = firstSatIpServer.GetIpAddress(); return;
logger.InfoFormat("Found SAT>IP Server at {0}", ipAddress); }
return ipAddress; }
} }
private int symbolRate; private IPAddress AutodetectIPAddress()
private bool isS2; {
private int frequency; SsdpDevice firstSatIpServer = SsdpClient.GetFirstSatIpServer(1000, null);
private char polarity; if (firstSatIpServer == null)
private int diseqcNumber; {
private IPAddress ipAddress; logger.WarnFormat("Didn't find any SAT>IP servers.");
private RtspClient rtspClient; return null;
public void Run() }
{ IPAddress ipAddress = firstSatIpServer.GetIpAddress();
rtspClient = new RtspClient(ipAddress, 554); logger.InfoFormat("Found SAT>IP Server at {0}", ipAddress);
rtspClient.AutoReconnect = false; return ipAddress;
Keepalive(); }
DiSEqC_Opcode opcode = BuildDiseqcOpcode();
string url = RtspClient.MakeUrl(opcode, frequency, isS2, symbolRate, null, false); private int symbolRate;
RtspDescribeResponse describe = rtspClient.GetDescribe(url); private bool isS2;
SessionDescriptionProtocol sessionDescriptionProtocol = describe.GetSessionDescriptionProtocol(); private int frequency;
private char polarity;
packetQueue = new Queue<byte[]>(); private int diseqcNumber;
RtspSetupResponse setup = rtspClient.GetSetup(url, destinationPort); private IPAddress ipAddress;
if (setup.RtspStatusCode == 404) private RtspClient rtspClient;
{ public void Run()
logger.Fatal("Your SAT>IP server doesn't have a tuner available that can talk to the requested frequency."); {
return; rtspClient = new RtspClient(ipAddress, 554);
} rtspClient.AutoReconnect = false;
setup.OnRtcpPacket += Setup_OnRtcpPacket; Keepalive();
setup.OnRtpPacket += Setup_OnRtpPacket; DiSEqC_Opcode opcode = BuildDiseqcOpcode();
string url = RtspClient.MakeUrl(opcode, frequency, isS2, symbolRate, null, false);
if (dataStorage == null) RtspDescribeResponse describe = rtspClient.GetDescribe(url);
dataStorage = new InMemoryScraperStorage(); SessionDescriptionProtocol sessionDescriptionProtocol = describe.GetSessionDescriptionProtocol();
if (objectStorage == null)
objectStorage = new FilesystemStorage(new DirectoryInfo(".")); packetQueue = new Queue<byte[]>();
RtspSetupResponse setup = rtspClient.GetSetup(url, destinationPort, multicastMode, destinationIp);
if (setup.RtspStatusCode == 404)
{
logger.Fatal("Your SAT>IP server doesn't have a tuner available that can talk to the requested frequency.");
return;
}
setup.OnRtcpPacket += Setup_OnRtcpPacket;
setup.OnRtpPacket += Setup_OnRtpPacket;
if (dataStorage == null)
dataStorage = new InMemoryScraperStorage();
if (objectStorage == null)
objectStorage = new FilesystemStorage(new DirectoryInfo("."));
if (!playoutMode) if (!playoutMode)
{ {
context = new SkyscraperContext(new TsContext(), dataStorage, objectStorage); context = new SkyscraperContext(new TsContext(), dataStorage, objectStorage);
context.EnableTimeout = true; context.EnableTimeout = true;
context.TimeoutSeconds = 60; context.TimeoutSeconds = 60;
context.InitalizeFilterChain(); context.InitalizeFilterChain();
} }
RtspPlayResponse play = rtspClient.GetPlay(setup); RtspPlayResponse play = rtspClient.GetPlay(setup);
DateTime lastTimestamp = DateTime.Now; DateTime lastTimestamp = DateTime.Now;
bool initMessagePrinted = false; bool initMessagePrinted = false;
while (true) while (true)
{ {
if (playoutMode) if (playoutMode)
{ {
Thread.Sleep(1000); Thread.Sleep(1000);
@ -152,8 +165,8 @@ namespace skyscraper8
logger.InfoFormat("Began SAT>IP playout to port {0}", destinationPort); logger.InfoFormat("Began SAT>IP playout to port {0}", destinationPort);
initMessagePrinted = true; initMessagePrinted = true;
} }
} }
else else
{ {
if (packetQueue.Count >= 1) if (packetQueue.Count >= 1)
{ {
@ -181,135 +194,136 @@ namespace skyscraper8
{ {
Keepalive(url); Keepalive(url);
lastTimestamp = DateTime.Now; lastTimestamp = DateTime.Now;
} }
} }
} }
rtspClient.GetTeardown(setup); rtspClient.GetTeardown(setup);
rtspClient.Dispose(); rtspClient.Dispose();
} }
private FileStream fs; private FileStream fs;
private uint stuffingBytes; private uint stuffingBytes;
private Queue<byte[]> packetQueue; private Queue<byte[]> packetQueue;
private ObjectStorage objectStorage; private ObjectStorage objectStorage;
private DataStorage dataStorage; private DataStorage dataStorage;
private SkyscraperContext context; private SkyscraperContext context;
private void Setup_OnRtpPacket(byte[] data, int length) private void Setup_OnRtpPacket(byte[] data, int length)
{ {
for (int i = 12; i < length; i += 1) for (int i = 12; i < length; i += 1)
{ {
if (data[i] == 'G') if (data[i] == 'G')
{ {
byte[] buffer = new byte[188]; byte[] buffer = new byte[188];
Array.Copy(data, i, buffer, 0, 188); Array.Copy(data, i, buffer, 0, 188);
lock (packetQueue) lock (packetQueue)
{ {
packetQueue.Enqueue(buffer); packetQueue.Enqueue(buffer);
} }
DumpPacket(buffer); DumpPacket(buffer);
i += 187; i += 187;
} }
else if (data[i] == 0xff) else if (data[i] == 0xff)
{ {
stuffingBytes++; stuffingBytes++;
} }
} }
} }
private void DumpPacket(byte[] buffer) private void DumpPacket(byte[] buffer)
{ {
if (!ENABLE_TS_WRITER) if (!ENABLE_TS_WRITER)
return; return;
if (fs == null) if (fs == null)
{ {
string fname = String.Format("{0}.ts", DateTime.Now.Ticks); string fname = String.Format("{0}.ts", DateTime.Now.Ticks);
fs = File.OpenWrite(fname); fs = File.OpenWrite(fname);
} }
fs.Write(buffer, 0, buffer.Length); fs.Write(buffer, 0, buffer.Length);
} }
private int rtcps; private int rtcps;
private IPAddress destinationIp; private IPAddress destinationIp;
private int destinationPort; private int destinationPort;
private bool playoutMode; private bool playoutMode;
private bool multicastMode;
private void Setup_OnRtcpPacket(byte[] data, int length) private void Setup_OnRtcpPacket(byte[] data, int length)
{ {
//rtcps++; //rtcps++;
} }
private DiSEqC_Opcode BuildDiseqcOpcode() private DiSEqC_Opcode BuildDiseqcOpcode()
{ {
DiSEqC_Opcode opcode = DiSEqC_Opcode.DISEQC_HIGH_NIBBLE; DiSEqC_Opcode opcode = DiSEqC_Opcode.DISEQC_HIGH_NIBBLE;
switch (diseqcNumber) switch (diseqcNumber)
{ {
case 1: case 1:
opcode |= DiSEqC_Opcode.DISEQC_OPTION_A; opcode |= DiSEqC_Opcode.DISEQC_OPTION_A;
opcode |= DiSEqC_Opcode.DISEQC_POSITION_A; opcode |= DiSEqC_Opcode.DISEQC_POSITION_A;
break; break;
case 2: case 2:
opcode |= DiSEqC_Opcode.DISEQC_OPTION_A; opcode |= DiSEqC_Opcode.DISEQC_OPTION_A;
opcode |= DiSEqC_Opcode.DISEQC_POSITION_B; opcode |= DiSEqC_Opcode.DISEQC_POSITION_B;
break; break;
case 3: case 3:
opcode |= DiSEqC_Opcode.DISEQC_OPTION_B; opcode |= DiSEqC_Opcode.DISEQC_OPTION_B;
opcode |= DiSEqC_Opcode.DISEQC_POSITION_A; opcode |= DiSEqC_Opcode.DISEQC_POSITION_A;
break; break;
case 4: case 4:
opcode |= DiSEqC_Opcode.DISEQC_OPTION_B; opcode |= DiSEqC_Opcode.DISEQC_OPTION_B;
opcode |= DiSEqC_Opcode.DISEQC_POSITION_B; opcode |= DiSEqC_Opcode.DISEQC_POSITION_B;
break; break;
default: default:
throw new ArgumentOutOfRangeException("Your DiSEqC position should be a number between 1 and 4."); throw new ArgumentOutOfRangeException("Your DiSEqC position should be a number between 1 and 4.");
} }
switch (polarity) switch (polarity)
{ {
case 'H': case 'H':
opcode |= DiSEqC_Opcode.DISEQC_HORIZONTAL; opcode |= DiSEqC_Opcode.DISEQC_HORIZONTAL;
break; break;
case 'V': case 'V':
opcode |= DiSEqC_Opcode.DISEQC_VERTICAL; opcode |= DiSEqC_Opcode.DISEQC_VERTICAL;
break; break;
default: default:
throw new ArgumentException("The polarity should be H or V."); throw new ArgumentException("The polarity should be H or V.");
} }
return opcode; return opcode;
} }
private bool ParseDvbStandard(string standard) private bool ParseDvbStandard(string standard)
{ {
standard = standard.ToUpperInvariant(); standard = standard.ToUpperInvariant();
if (standard.StartsWith("DVB")) if (standard.StartsWith("DVB"))
standard = standard.Substring(3); standard = standard.Substring(3);
if (standard.StartsWith("-")) if (standard.StartsWith("-"))
standard = standard.Substring(1); standard = standard.Substring(1);
switch (standard) switch (standard)
{ {
case "S": case "S":
return false; return false;
case "S2": case "S2":
return true; return true;
case "S2X": case "S2X":
return true; return true;
default: default:
throw new ArgumentException(String.Format("I have no idea what kind of Standard {0} is supposed to be.", standard)); throw new ArgumentException(String.Format("I have no idea what kind of Standard {0} is supposed to be.", standard));
} }
} }
private void Keepalive(string url = "/") private void Keepalive(string url = "/")
{ {
RtspOptionsResponse options = rtspClient.GetOptions("/"); RtspOptionsResponse options = rtspClient.GetOptions("/");
if (options.RtspStatusCode != 200) if (options.RtspStatusCode != 200)
{ {
throw new RtspException(String.Format("Unexpected RTSP Status code. Wanted {0}, got {1}", 200, options.RtspStatusCode)); throw new RtspException(String.Format("Unexpected RTSP Status code. Wanted {0}, got {1}", 200, options.RtspStatusCode));
} }
} }
} }
} }

View File

@ -99,7 +99,7 @@ namespace skyscraper8.SatIp
return result; return result;
} }
public RtspSetupResponse GetSetup(string url, int destinationPort = 0) public RtspSetupResponse GetSetup(string url, int destinationPort = 0, bool multicastMode = false, IPAddress multicastIp = null)
{ {
RtspSetupRequest request = new RtspSetupRequest(); RtspSetupRequest request = new RtspSetupRequest();
request.RequestPath = url; request.RequestPath = url;
@ -123,7 +123,14 @@ namespace skyscraper8.SatIp
rtcpSocket.Bind(new IPEndPoint(ListenIp, 0)); rtcpSocket.Bind(new IPEndPoint(ListenIp, 0));
int rtcpPort = GetPortFromEndPoint(rtcpSocket.LocalEndPoint); int rtcpPort = GetPortFromEndPoint(rtcpSocket.LocalEndPoint);
request.SetRtpAvpUnicast(rtpPort, rtcpPort); if (multicastMode && multicastIp != null)
{
request.SetRtpAvpMulticast(multicastIp, rtpPort, rtcpPort);
}
else
{
request.SetRtpAvpUnicast(rtpPort, rtcpPort);
}
RtspResponseHeader response = GetResponse(request.ListHeaders(RootPath)); RtspResponseHeader response = GetResponse(request.ListHeaders(RootPath));
RtspSetupResponse setupResponse = new RtspSetupResponse(response); RtspSetupResponse setupResponse = new RtspSetupResponse(response);

View File

@ -1,57 +1,63 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Net;
using System.Threading.Tasks; using System.Text;
using System.Threading.Tasks;
namespace skyscraper8.SatIp.RtspRequests
{ namespace skyscraper8.SatIp.RtspRequests
internal class RtspSetupRequest : RtspRequest {
{ internal class RtspSetupRequest : RtspRequest
public RtspSetupRequest() : base("SETUP") {
{ public RtspSetupRequest() : base("SETUP")
} {
}
public uint CSeq
{ public uint CSeq
set {
{ set
base.args["CSeq"] = Convert.ToString(value); {
} base.args["CSeq"] = Convert.ToString(value);
get }
{ get
return uint.Parse(base.args["CSeq"]); {
} return uint.Parse(base.args["CSeq"]);
} }
}
public string UserAgent
{ public string UserAgent
set {
{ set
base.args["User-Agent"] = value; {
} base.args["User-Agent"] = value;
get }
{ get
return base.args["User-Agent"]; {
} return base.args["User-Agent"];
} }
}
public string Transport
{ public string Transport
set {
{ set
base.args["Transport"] = value; {
} base.args["Transport"] = value;
get }
{ get
return base.args["Transport"]; {
} return base.args["Transport"];
} }
}
public void SetRtpAvpUnicast(int rtpPort, int rtcpPort)
{ public void SetRtpAvpUnicast(int rtpPort, int rtcpPort)
Transport = String.Format("RTP/AVP;unicast;client_port={0}-{1}", rtpPort, rtcpPort); {
} Transport = String.Format("RTP/AVP;unicast;client_port={0}-{1}", rtpPort, rtcpPort);
} }
} public void SetRtpAvpMulticast(IPAddress targetIp, int rtpPort, int rtcpPort)
{
Transport = String.Format("RTP/AVP;multicast;destination={2};port={0}-{1}", rtpPort, rtcpPort, targetIp.ToString());
}
}
}

View File

@ -6,7 +6,7 @@ namespace skyscraper5.Skyscraper.IO.TunerInterface
{ {
[SkyscraperPlugin] [SkyscraperPlugin]
[TunerFactoryId(2,"skyscraper5 Remote Stream Reader")] [TunerFactoryId(2,"skyscraper5 Remote Stream Reader")]
internal class RemoteStreamReaderTunerFactory : ITunerFactory public class RemoteStreamReaderTunerFactory : ITunerFactory
{ {
public string Hostname { get; set; } public string Hostname { get; set; }

View File

@ -0,0 +1,112 @@
using log4net;
using log4net.Repository.Hierarchy;
using skyscraper5.Docsis.MacManagement;
using skyscraper5.Skyscraper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace skyscraper8.Skyscraper
{
internal class TsRotationToCsv
{
public DirectoryInfo SourceDir { get; set; }
public FileInfo DestinationCsv { get; set; }
private List<FileInfo> files;
private ILog logger;
public void Run()
{
logger = LogManager.GetLogger(typeof(TsRotationToCsv));
files = new List<FileInfo>();
ScrapeDirectory(SourceDir);
files.Sort(new FileInfoComparerByDate(logger));
DumpCsv();
}
private void DumpCsv()
{
StreamWriter sw = new StreamWriter(DestinationCsv.FullName);
foreach (FileInfo file in files)
{
sw.WriteLine(String.Format("\"{0}\";{1};{2}", file.FullName, file.Length, file.LastWriteTime));
}
sw.Flush();
sw.Close();
}
private void ScrapeDirectory(DirectoryInfo di)
{
try
{
foreach (FileSystemInfo fsi in di.GetFileSystemInfos())
{
switch (fsi)
{
case FileInfo fi:
if (fi.Extension.ToLowerInvariant().Equals(".ts"))
{
files.Add(fi);
if (files.Count % 100 == 0)
logger.InfoFormat("Listed {0} files.", files.Count);
}
break;
case DirectoryInfo subdirectory:
ScrapeDirectory(subdirectory);
break;
default:
throw new NotImplementedException(di.GetType().Name);
}
}
}
catch (Exception e)
{
logger.WarnFormat(e.Message);
}
}
private class FileInfoComparerByDate : Comparer<FileInfo>
{
private ILog logger;
private int ups, downs, stands;
public FileInfoComparerByDate(ILog logger)
{
this.logger = logger;
}
public override int Compare(FileInfo? x, FileInfo? y)
{
long xTime = x.LastWriteTime.ToUnixTime();
long yTime = y.LastWriteTime.ToUnixTime();
int ops;
int v = xTime.CompareTo(yTime);
switch(v)
{
case -1:
ups++;
ops = ups;
break;
case 0:
stands++;
ops = stands;
break;
case 1:
downs++;
ops = downs;
break;
default:
throw new NotImplementedException(v.ToString());
}
if (ops % 1000 == 0)
{
logger.InfoFormat("Sort progress: {0} up, {1} down, {2} stand.", ups, downs, stands);
}
return v;
}
}
}
}