From 2205d50b0a73566a2217d6572be337244906997f Mon Sep 17 00:00:00 2001 From: feyris-tan <4116042+feyris-tan@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:06:35 +0200 Subject: [PATCH] Implemented RTSP OPTIONS and RTSP DESCRIBE --- .gitignore | 1 + skyscraper8/Program.cs | 13 +- skyscraper8/SatIp/RtspClient.cs | 194 ++++++++++++++++++ skyscraper8/SatIp/RtspException.cs | 23 +++ skyscraper8/SatIp/RtspRequest.cs | 34 +++ .../SatIp/RtspRequests/RtspDescribeRequest.cs | 52 +++++ .../SatIp/RtspRequests/RtspOptionsRequest.cs | 40 ++++ skyscraper8/SatIp/RtspResponse.cs | 31 +++ skyscraper8/SatIp/RtspResponseHeader.cs | 17 ++ .../RtspResponses/RtspDescribeResponse.cs | 54 +++++ .../RtspResponses/RtspOptionsResponse.cs | 32 +++ .../SatIp/SessionDescriptionProtocol.cs | 149 ++++++++++++++ 12 files changed, 634 insertions(+), 6 deletions(-) create mode 100644 skyscraper8/SatIp/RtspClient.cs create mode 100644 skyscraper8/SatIp/RtspException.cs create mode 100644 skyscraper8/SatIp/RtspRequest.cs create mode 100644 skyscraper8/SatIp/RtspRequests/RtspDescribeRequest.cs create mode 100644 skyscraper8/SatIp/RtspRequests/RtspOptionsRequest.cs create mode 100644 skyscraper8/SatIp/RtspResponse.cs create mode 100644 skyscraper8/SatIp/RtspResponseHeader.cs create mode 100644 skyscraper8/SatIp/RtspResponses/RtspDescribeResponse.cs create mode 100644 skyscraper8/SatIp/RtspResponses/RtspOptionsResponse.cs create mode 100644 skyscraper8/SatIp/SessionDescriptionProtocol.cs diff --git a/.gitignore b/.gitignore index 5d88d9e..cdef35e 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,4 @@ imgui.ini /GUIs/skyscraper8.UI.ImGui.MonoGame/bin/Debug/net8.0 /GUIs/skyscraper8.UI.ImGui.MonoGame/obj/Debug/net8.0 /GUIs/skyscraper8.UI.ImGui.MonoGame/obj +/.vs/skyscraper8/CopilotIndices/17.14.995.13737 diff --git a/skyscraper8/Program.cs b/skyscraper8/Program.cs index b2d8dbf..63db317 100644 --- a/skyscraper8/Program.cs +++ b/skyscraper8/Program.cs @@ -26,6 +26,8 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.InteropServices; using skyscraper5.Skyscraper.Scraper.StreamAutodetection; +using skyscraper8.SatIp; +using skyscraper8.SatIp.RtspResponses; [assembly: log4net.Config.XmlConfigurator(ConfigFile = "log4net.config")] namespace skyscraper5 @@ -35,12 +37,11 @@ namespace skyscraper5 private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name); private static void IntegrationTest() { - FileStream fileStream = File.OpenRead("C:\\Temp\\ule-iww.ts"); - TsContext tesContext = new TsContext(); - SkyscraperContext skyscraperContext = new SkyscraperContext(tesContext, new InMemoryScraperStorage(), new NullObjectStorage()); - skyscraperContext.InitalizeFilterChain(); - tesContext.RegisterPacketProcessor(0x666, new StreamTypeAutodetection(0x666, skyscraperContext)); - skyscraperContext.IngestFromStream(fileStream); + RtspClient rtspClient = new RtspClient("172.20.20.121", 554); + RtspOptionsResponse options = rtspClient.GetOptions("/"); + string url = RtspClient.MakeUrl(DiSEqC_Opcode.DISEQC_OPTION_A | DiSEqC_Opcode.DISEQC_POSITION_A | DiSEqC_Opcode.DISEQC_HORIZONTAL, 11954, true, 27500); + RtspDescribeResponse describe = rtspClient.GetDescribe(url); + SessionDescriptionProtocol sessionDescriptionProtocol = describe.GetSessionDescriptionProtocol(); } static void Main(string[] args) diff --git a/skyscraper8/SatIp/RtspClient.cs b/skyscraper8/SatIp/RtspClient.cs new file mode 100644 index 0000000..337b3b9 --- /dev/null +++ b/skyscraper8/SatIp/RtspClient.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using skyscraper5.Skyscraper; +using skyscraper5.Skyscraper.IO.CrazycatStreamReader; +using skyscraper8.SatIp.RtspRequests; +using skyscraper8.SatIp.RtspResponses; + +namespace skyscraper8.SatIp +{ + internal class RtspClient : Validatable + { + private uint cseqCounter; + private const string USER_AGENT = "sophiaNetRtspClient/1.0"; + + public RtspClient(string ip, int port) + { + IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Parse(ip), port); + this.TcpClient = new TcpClient(); + this.TcpClient.Connect(ipEndPoint); + this.RootPath = string.Format("rtsp://{0}:{1}", ip, port); + this.NetworkStream = TcpClient.GetStream(); + this.BufferedStream = new BufferedStream(this.NetworkStream); + this.StreamReader = new StreamReader(this.BufferedStream); + this.StreamWriter = new StreamWriter(this.BufferedStream); + this.cseqCounter = 2; + this.ListenIp = GetListenIp(this.TcpClient.Client.LocalEndPoint); + RtspOptionsResponse rtspOptionsResponse = GetOptions("/"); + this.Valid = rtspOptionsResponse.Valid; + } + + public IPAddress ListenIp { get; set; } + + private IPAddress GetListenIp(EndPoint clientLocalEndPoint) + { + IPEndPoint ipEndPoint = clientLocalEndPoint as IPEndPoint; + if (ipEndPoint == null) + { + throw new NotImplementedException(clientLocalEndPoint.GetType().Name); + } + + if (ipEndPoint.Address.AddressFamily == AddressFamily.InterNetwork) + { + return ipEndPoint.Address; + } + + if (ipEndPoint.Address.IsIPv4MappedToIPv6) + { + return ipEndPoint.Address.MapToIPv4(); + } + + throw new NotImplementedException(String.Format("Don't know whether I can listen on IP {0}", ipEndPoint.ToString())); + } + + public string RootPath { get; set; } + + private TcpClient TcpClient { get; set; } + private NetworkStream NetworkStream { get; set; } + private BufferedStream BufferedStream { get; set; } + private StreamReader StreamReader { get; set; } + private StreamWriter StreamWriter { get; set; } + + public RtspOptionsResponse GetOptions(string url) + { + RtspOptionsRequest request = new RtspOptionsRequest(); + request.RequestPath = url; + request.CSeq = cseqCounter++; + request.UserAgent = USER_AGENT; + RtspResponseHeader header = GetResponse(request.ListHeaders(RootPath)); + RtspOptionsResponse result = new RtspOptionsResponse(header); + return result; + } + + public RtspDescribeResponse GetDescribe(string url) + { + RtspDescribeRequest request = new RtspDescribeRequest(); + request.RequestPath = url; + request.CSeq = cseqCounter++; + request.UserAgent = USER_AGENT; + request.Accept = "application/sdp"; + RtspResponseHeader header = GetResponse(request.ListHeaders(RootPath)); + RtspDescribeResponse result = new RtspDescribeResponse(header); + return result; + } + + private RtspResponseHeader GetResponse(string request) + { + StreamWriter.Write(request); + StreamWriter.Flush(); + + RtspResponseHeader result = new RtspResponseHeader(); + + string response = StreamReader.ReadLine(); + if (!response.StartsWith("RTSP/")) + throw new RtspException("Invalid RTSP response."); + + response = response.Substring(5); + + string versionString = response.Substring(0, 3); + result.rtspVersion = Version.Parse(versionString); + response = response.Substring(4); + + string statusCodeString = response.Substring(0,3); + result.statusCode = ushort.Parse(statusCodeString); + + response = response.Substring(4); + result.statusLine = response; + + long contentLength = 0; + + result.kv = new Dictionary(); + while (true) + { + string lineIn = StreamReader.ReadLine(); + if (string.IsNullOrEmpty(lineIn)) + break; + + int indexOf = lineIn.IndexOf(": "); + string key = lineIn.Substring(0, indexOf); + string value = lineIn.Substring(indexOf + 2); + result.kv.Add(key, value); + + if (key.Equals("Content-Length")) + { + contentLength = long.Parse(value); + } + } + + if (contentLength > 0) + { + StreamReader.DiscardBufferedData(); + byte[] buffer = new byte[contentLength]; + int sucessfullyRead = BufferedStream.Read(buffer, 0, (int)contentLength); + if (sucessfullyRead != contentLength) + { + throw new IOException("incomplete read"); + } + + result.payload = buffer; + } + + return result; + } + + public static string MakeUrl(DiSEqC_Opcode diseqcChannel, int freq, bool isS2, int symbolrate) + { + byte diseqc; + if (diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_OPTION_A) && diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_POSITION_A)) + { + diseqc = 1; + } + else if (diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_OPTION_A) && diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_POSITION_B)) + { + diseqc = 2; + } + else if (diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_OPTION_B) && diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_POSITION_A)) + { + diseqc = 3; + } + else if (diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_OPTION_B) && diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_POSITION_B)) + { + diseqc = 4; + } + else + { + throw new ArgumentOutOfRangeException(nameof(diseqcChannel)); + } + + char pol; + if (diseqcChannel.HasFlag(DiSEqC_Opcode.DISEQC_HORIZONTAL)) + { + pol = 'h'; + } + else + { + pol = 'v'; + } + + StringBuilder sb = new StringBuilder(); + sb.AppendFormat("/?src={0}", diseqc); + sb.AppendFormat("&freq={0}", freq); + sb.AppendFormat("&pol={0}", pol); + sb.AppendFormat("&msys={0}", isS2 ? "dvbs2" : "dvbs"); + sb.AppendFormat("&sr={0}", symbolrate); + sb.AppendFormat("&pids=all"); + + return sb.ToString(); + } + } +} diff --git a/skyscraper8/SatIp/RtspException.cs b/skyscraper8/SatIp/RtspException.cs new file mode 100644 index 0000000..3240f41 --- /dev/null +++ b/skyscraper8/SatIp/RtspException.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace skyscraper8.SatIp +{ + public class RtspException : Exception + { + public RtspException() + { + } + + public RtspException(string message) : base(message) + { + } + + public RtspException(string message, Exception inner) : base(message, inner) + { + } + } +} diff --git a/skyscraper8/SatIp/RtspRequest.cs b/skyscraper8/SatIp/RtspRequest.cs new file mode 100644 index 0000000..1095d0f --- /dev/null +++ b/skyscraper8/SatIp/RtspRequest.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace skyscraper8.SatIp +{ + internal abstract class RtspRequest + { + protected readonly Dictionary args; + private string verb; + public string RequestPath { get; set; } + + protected RtspRequest(string verb) + { + this.args = new Dictionary(); + this.verb = verb; + } + + internal string ListHeaders(string rootpath) + { + StringBuilder sb = new StringBuilder(); + sb.AppendFormat("{0} {1}{2} RTSP/1.0\r\n", verb, rootpath, RequestPath); + foreach (KeyValuePair pair in args) + { + sb.AppendFormat("{0}: {1}\r\n", pair.Key, pair.Value); + } + + sb.AppendFormat("\r\n"); + return sb.ToString(); + } + } +} diff --git a/skyscraper8/SatIp/RtspRequests/RtspDescribeRequest.cs b/skyscraper8/SatIp/RtspRequests/RtspDescribeRequest.cs new file mode 100644 index 0000000..41e1e92 --- /dev/null +++ b/skyscraper8/SatIp/RtspRequests/RtspDescribeRequest.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using skyscraper8.SatIp.RtspResponses; + +namespace skyscraper8.SatIp.RtspRequests +{ + internal class RtspDescribeRequest : RtspRequest + { + public RtspDescribeRequest() : base("DESCRIBE") + { + } + + public uint CSeq + { + set + { + base.args["CSeq"] = Convert.ToString(value); + } + get + { + return uint.Parse(base.args["CSeq"]); + } + } + + public string UserAgent + { + set + { + base.args["User-Agent"] = value; + } + get + { + return base.args["User-Agent"]; + } + } + + public string Accept + { + set + { + base.args["Accept"] = value; + } + get + { + return base.args["Accept"]; + } + } + } +} diff --git a/skyscraper8/SatIp/RtspRequests/RtspOptionsRequest.cs b/skyscraper8/SatIp/RtspRequests/RtspOptionsRequest.cs new file mode 100644 index 0000000..9bf9c72 --- /dev/null +++ b/skyscraper8/SatIp/RtspRequests/RtspOptionsRequest.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace skyscraper8.SatIp.RtspRequests +{ + internal class RtspOptionsRequest : RtspRequest + { + public RtspOptionsRequest() + : base("OPTIONS") + { + } + + public uint CSeq + { + set + { + base.args["CSeq"] = Convert.ToString(value); + } + get + { + return uint.Parse(base.args["CSeq"]); + } + } + + public string UserAgent + { + set + { + base.args["User-Agent"] = value; + } + get + { + return base.args["User-Agent"]; + } + } + } +} diff --git a/skyscraper8/SatIp/RtspResponse.cs b/skyscraper8/SatIp/RtspResponse.cs new file mode 100644 index 0000000..1b7fa5b --- /dev/null +++ b/skyscraper8/SatIp/RtspResponse.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using skyscraper5.Skyscraper; + +namespace skyscraper8.SatIp +{ + internal abstract class RtspResponse : Validatable + { + private readonly string statusLine; + protected readonly Dictionary args; + protected readonly byte[] payload; + + protected RtspResponse(RtspResponseHeader header) + { + RtspVersion = header.rtspVersion; + RtspStatusCode = header.statusCode; + statusLine = header.statusLine; + args = header.kv; + payload = header.payload; + Valid = true; + } + + + public ushort RtspStatusCode { get; private set; } + + public Version RtspVersion { get; private set; } + } +} diff --git a/skyscraper8/SatIp/RtspResponseHeader.cs b/skyscraper8/SatIp/RtspResponseHeader.cs new file mode 100644 index 0000000..aedf30e --- /dev/null +++ b/skyscraper8/SatIp/RtspResponseHeader.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace skyscraper8.SatIp +{ + internal struct RtspResponseHeader + { + public Version rtspVersion; + public ushort statusCode; + public string statusLine; + public Dictionary kv; + public byte[] payload; + } +} diff --git a/skyscraper8/SatIp/RtspResponses/RtspDescribeResponse.cs b/skyscraper8/SatIp/RtspResponses/RtspDescribeResponse.cs new file mode 100644 index 0000000..b1731f7 --- /dev/null +++ b/skyscraper8/SatIp/RtspResponses/RtspDescribeResponse.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace skyscraper8.SatIp.RtspResponses +{ + internal class RtspDescribeResponse : RtspResponse + { + public RtspDescribeResponse(RtspResponseHeader header) : base(header) + { + } + + public uint CSeq + { + get + { + return uint.Parse(base.args["CSeq"]); + } + } + + public string ContentType + { + get + { + return base.args["Content-Type"]; + } + } + + public long ContentLength + { + get + { + return long.Parse(base.args["Content-Length"]); + } + } + + public SessionDescriptionProtocol GetSessionDescriptionProtocol() + { + if (!ContentType.Equals("application/sdp")) + { + throw new RtspException(String.Format("Invalid MIME Type. Expected {0}, got {1}.", "application/sdp", ContentType)); + } + + MemoryStream ms = new MemoryStream(base.payload, false); + StreamReader sr = new StreamReader(ms); + SessionDescriptionProtocol result = new SessionDescriptionProtocol(sr); + ms.Close(); + sr.Close(); + return result; + } + } +} diff --git a/skyscraper8/SatIp/RtspResponses/RtspOptionsResponse.cs b/skyscraper8/SatIp/RtspResponses/RtspOptionsResponse.cs new file mode 100644 index 0000000..05a1457 --- /dev/null +++ b/skyscraper8/SatIp/RtspResponses/RtspOptionsResponse.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace skyscraper8.SatIp.RtspResponses +{ + internal class RtspOptionsResponse : RtspResponse + { + public RtspOptionsResponse(RtspResponseHeader header) + : base(header) + { + } + + public uint CSeq + { + get + { + return uint.Parse(base.args["CSeq"]); + } + } + + public string Public + { + get + { + return base.args["Public"]; + } + } + } +} diff --git a/skyscraper8/SatIp/SessionDescriptionProtocol.cs b/skyscraper8/SatIp/SessionDescriptionProtocol.cs new file mode 100644 index 0000000..b011a20 --- /dev/null +++ b/skyscraper8/SatIp/SessionDescriptionProtocol.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace skyscraper8.SatIp +{ + internal class SessionDescriptionProtocol + { + public SessionDescriptionProtocol(StreamReader sr) + { + while (!sr.EndOfStream) + { + string line = sr.ReadLine(); + int indexOf = line.IndexOf('='); + string key = line.Substring(0, indexOf); + string value = line.Substring(indexOf + 1); + switch (key) + { + case "v": + SdpVersion = int.Parse(value); + break; + case "o": + string[] oArgs = value.Split(' '); + Username = oArgs[0]; + SessionId = ulong.Parse(oArgs[1]); + SessionVersion = uint.Parse(oArgs[2]); + OwnerNetworkType = Enum.Parse(oArgs[3]); + OwnerAddressType = Enum.Parse(oArgs[4]); + OwnerAddress = IPAddress.Parse(oArgs[5]); + break; + case "s": + SessionName = value; + break; + case "t": + string[] tArgs = value.Split(' '); + SessionStartTime = uint.Parse(tArgs[0]); + SessionStopType = uint.Parse(tArgs[1]); + break; + case "m": + string[] mArgs = value.Split(' '); + MediaType = Enum.Parse(mArgs[0], true); + MediaPort = uint.Parse(mArgs[1]); + MediaProtocol = mArgs[2]; + MediaFormat = (MediaFormatEnum)int.Parse(mArgs[3]); + break; + case "c": + string[] cArgs = value.Split(' '); + ConnectionNetworkType = Enum.Parse(cArgs[0]); + ConnectionAddressType = Enum.Parse(cArgs[1]); + ConnectionAddress = IPAddress.Parse(cArgs[2]); + break; + case "a": + int aIndex = value.IndexOf(':'); + string aKey = value.Substring(0, aIndex); + string aValue = value.Substring(aIndex + 1); + if (MediaAttribute == null) + MediaAttribute = new Dictionary(); + MediaAttribute[aKey] = aValue; + break; + default: + throw new NotImplementedException(key); + } + } + } + + public Dictionary MediaAttribute { get; set; } + + public IPAddress ConnectionAddress { get; set; } + + public OwnerAddressTypeEnum ConnectionAddressType { get; set; } + + public OwnerNetworkTypeEnum ConnectionNetworkType { get; set; } + + public MediaFormatEnum MediaFormat { get; set; } + + public string MediaProtocol { get; set; } + + public uint MediaPort { get; set; } + + public MediaTypeEnum MediaType { get; set; } + + public uint SessionStopType { get; set; } + + public uint SessionStartTime { get; set; } + + public string SessionName { get; set; } + + public IPAddress OwnerAddress { get; set; } + + public OwnerAddressTypeEnum OwnerAddressType { get; set; } + + public OwnerNetworkTypeEnum OwnerNetworkType { get; set; } + + public uint SessionVersion { get; set; } + + public ulong SessionId { get; set; } + + public string Username { get; set; } + + public int SdpVersion { get; private set; } + } + + public enum OwnerNetworkTypeEnum + { + IN + } + + public enum OwnerAddressTypeEnum + { + IP4 + } + + public enum MediaTypeEnum + { + Video, + } + + public enum MediaFormatEnum + { + PCMU = 0, + GSM = 3, + G723 = 4, + DVI4_32kbit = 5, + DVI4_64kbit = 6, + LPC = 7, + PCMA = 8, + G722 = 9, + L16Stereo = 10, + L16Mono = 11, + QCELP = 12, + ComfortNoise = 13, + MPA = 14, + G728 = 15, + DVI4_44kbit = 16, + DVI4_88kbit = 17, + G729 = 18, + CellB = 25, + Jpeg = 26, + NV = 28, + H261 = 31, + MPV = 32, + M2T = 33, + H263 = 34 + + } +}