Implemented RTSP OPTIONS and RTSP DESCRIBE

This commit is contained in:
feyris-tan 2025-08-17 20:06:35 +02:00
parent 05857b9004
commit 2205d50b0a
12 changed files with 634 additions and 6 deletions

1
.gitignore vendored
View File

@ -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

View File

@ -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)

View File

@ -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<string, string>();
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();
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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<string, string> args;
private string verb;
public string RequestPath { get; set; }
protected RtspRequest(string verb)
{
this.args = new Dictionary<string, string>();
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<string, string> pair in args)
{
sb.AppendFormat("{0}: {1}\r\n", pair.Key, pair.Value);
}
sb.AppendFormat("\r\n");
return sb.ToString();
}
}
}

View File

@ -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"];
}
}
}
}

View File

@ -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"];
}
}
}
}

View File

@ -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<string, string> 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; }
}
}

View File

@ -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<string, string> kv;
public byte[] payload;
}
}

View File

@ -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;
}
}
}

View File

@ -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"];
}
}
}
}

View File

@ -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<OwnerNetworkTypeEnum>(oArgs[3]);
OwnerAddressType = Enum.Parse<OwnerAddressTypeEnum>(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<MediaTypeEnum>(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<OwnerNetworkTypeEnum>(cArgs[0]);
ConnectionAddressType = Enum.Parse<OwnerAddressTypeEnum>(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<string, string>();
MediaAttribute[aKey] = aValue;
break;
default:
throw new NotImplementedException(key);
}
}
}
public Dictionary<string, string> 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
}
}