From 660a77c3aa8f3f2ea1f3af708339c2284c2a0693 Mon Sep 17 00:00:00 2001 From: Davide Galli Date: Fri, 12 Jan 2024 16:08:18 +0100 Subject: [PATCH 1/4] Added extension management (onvif) for MJPEG RTP size over 2040x2040 --- RTSP/Rtp/JPEGPayload.cs | 70 ++++++++++++++++++++++++++++++++++++++++- RTSP/Rtp/RtpPacket.cs | 2 ++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/RTSP/Rtp/JPEGPayload.cs b/RTSP/Rtp/JPEGPayload.cs index 5b4c2bf..bfaed3d 100644 --- a/RTSP/Rtp/JPEGPayload.cs +++ b/RTSP/Rtp/JPEGPayload.cs @@ -601,12 +601,18 @@ public class JPEGPayload : IPayloadProcessor //private readonly List temporary_rtp_payloads = []; private readonly List> temporaryRtpPayloads = new(256); + private ReadOnlyMemory extensionMemory; + private bool hasExtensionMemory; + private int _currentDri; private int _currentQ; private int _currentType; private int _currentFrameWidth; private int _currentFrameHeight; + private int _extensionFrameWidth = 0; + private int _extensionFrameHeight = 0; + private bool _hasExternalQuantizationTable; private byte[] _jpegHeaderBytes = Array.Empty(); @@ -618,12 +624,21 @@ public List> ProcessRTPPacket(RtpPacket packet) { temporaryRtpPayloads.Add(packet.Payload); // Todo Could optimise this and go direct to Process Frame if just 1 packet in frame + if (packet.HasExtension) + { + extensionMemory = packet.Extension; + hasExtensionMemory = true; + } + if (packet.IsMarker) { // End Marker is set. Process the list of RTP Packets (forming 1 RTP frame) and save the results ReadOnlyMemory nalUnits = ProcessJPEGRTPFrame(temporaryRtpPayloads); temporaryRtpPayloads.Clear(); + extensionMemory = null; + hasExtensionMemory = false; + return new() { nalUnits }; } // we don't have a frame yet. Keep accumulating RTP packets @@ -634,6 +649,32 @@ private ReadOnlyMemory ProcessJPEGRTPFrame(List> rtp_ { _frameStream.SetLength(0); + if (hasExtensionMemory) + { + ReadOnlySpan extension = extensionMemory.Span; + int extensionType = (extension[0] << 8) + (extension[1] << 0); + if (extensionType == 0xFFD8) + { + int headerPosition = 4; + int extensionSize = extension.Length; + while (headerPosition < (extensionSize - 4)) + { + int blockType = (extension[headerPosition] << 8) + extension[headerPosition + 1]; + int blockSize = (extension[headerPosition + 2] << 8) + extension[headerPosition + 3]; + + if (blockType == 0xFFC0) + { + if (JpegExtractExtensionWidthHeight(extension, headerPosition, blockSize + 2, out int width, out int height) == 1) + { + _extensionFrameWidth = width / 8; + _extensionFrameHeight = height / 8; + } + } + headerPosition += (blockSize + 2); + } + } + } + foreach (ReadOnlyMemory payloadMemory in rtp_payloads) { var payload = payloadMemory.Span; @@ -650,6 +691,12 @@ private ReadOnlyMemory ProcessJPEGRTPFrame(List> rtp_ int height = payload[offset++] * 8; int dri = 0; + if (width == 0 && height == 0 && _extensionFrameWidth > 0 && _extensionFrameHeight > 0) + { + width = _extensionFrameWidth * 8; + height = _extensionFrameHeight * 8; + } + if (type > 63) { dri = payload[offset] << 8 | payload[offset]; @@ -875,6 +922,27 @@ private static void CreateHuffmanHeader(byte[] buffer, int offset, byte[] codele offset += ncodes; Buffer.BlockCopy(symbols, 0, buffer, offset, nsymbols); } - } + private static int JpegExtractExtensionWidthHeight(ReadOnlySpan extension, int headerPosition, int size, out int width, out int height) + { + width = -1; + height = -1; + + if (size < 17) { return -3; } + + int i = 0; + do + { + if (extension[headerPosition + i] == 0xFF && extension[headerPosition + i + 1] == 0xC0) + { + height = ((extension[headerPosition + i + 5] << 8) & 0x0000FF00) | (extension[headerPosition + i + 6] & 0x000000FF); + width = ((extension[headerPosition + i + 7] << 8) & 0x0000FF00) | (extension[headerPosition + i + 8] & 0x000000FF); + return 1; + } + ++i; + } + while (i < (size - 17)); + return 0; + } + } } diff --git a/RTSP/Rtp/RtpPacket.cs b/RTSP/Rtp/RtpPacket.cs index 276d3a9..1428dd8 100644 --- a/RTSP/Rtp/RtpPacket.cs +++ b/RTSP/Rtp/RtpPacket.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace Rtsp.Rtp { @@ -32,5 +33,6 @@ public RtpPacket(byte[] rawData) public int PayloadSize => rawData.Length - HeaderSize - ExtensionSize - PaddingSize; public ReadOnlyMemory Payload => rawData.AsMemory()[(HeaderSize + ExtensionSize)..^PaddingSize]; + public ReadOnlyMemory Extension => new(rawData[HeaderSize..ExtensionSize].ToArray()); } } From 128acb173f7cdc2328c4a881301ff6024346f814 Mon Sep 17 00:00:00 2001 From: Davide Galli Date: Fri, 19 Jan 2024 15:43:49 +0100 Subject: [PATCH 2/4] Updated with http connection, and some code cleanup/update. --- RTSP.Tests/RTSPUtilsTest.cs | 1 + RTSP/ArrayUtils.cs | 18 - RTSP/Authentication.cs | 130 +++- RTSP/AuthenticationBasic.cs | 42 +- RTSP/AuthenticationDigest.cs | 113 ++- RTSP/BitStream.cs | 100 --- RTSP/HttpBadResponseCodeException.cs | 20 + RTSP/HttpBadResponseException.cs | 26 + RTSP/IRTSPTransport.cs | 10 +- RTSP/Messages/RTSPChunk.cs | 6 + RTSP/Messages/RTSPData.cs | 1 + RTSP/Messages/RTSPMessage.cs | 73 +- RTSP/RTSP.csproj | 101 +-- RTSP/RTSPDataEventArgs.cs | 86 ++- RTSP/RTSPListener.cs | 831 +++++++++++---------- RTSP/RTSPMessageAuthExtension.cs | 30 + RTSP/RTSPTCPTransport.cs | 159 ++-- RTSP/Rtp/AACPayload.cs | 227 +++--- RTSP/Rtp/JPEGPayload.cs | 3 +- RTSP/RtspConstants.cs | 11 + RTSP/RtspHttpTransport.cs | 199 +++++ RTSP/UdpSocket.cs | 244 +++--- RTSP/Utils/ArrayUtils.cs | 78 ++ RTSP/Utils/BitStream.cs | 98 +++ RTSP/Utils/HeadersParser.cs | 27 + RTSP/Utils/MD5.cs | 263 +++++++ RTSP/Utils/NetworkClientFactory.cs | 27 + RTSP/Utils/NetworkCredentialExtensions.cs | 13 + RTSP/{ => Utils}/RTSPUtils.cs | 2 +- RTSP/Utils/WellKnownHeaders.cs | 11 + RtspCameraExample/Program.cs | 8 +- RtspCameraExample/RtspServer.cs | 30 +- RtspClientExample/Program.cs | 104 ++- RtspClientExample/RTSPClient.cs | 307 ++++---- RtspClientExample/RtspClientExample.csproj | 35 +- RtspMultiplexer/RtspDispatcher.cs | 2 +- RtspMultiplexer/RtspServer.cs | 3 +- 37 files changed, 2156 insertions(+), 1283 deletions(-) delete mode 100644 RTSP/ArrayUtils.cs delete mode 100644 RTSP/BitStream.cs create mode 100644 RTSP/HttpBadResponseCodeException.cs create mode 100644 RTSP/HttpBadResponseException.cs create mode 100644 RTSP/RTSPMessageAuthExtension.cs create mode 100644 RTSP/RtspConstants.cs create mode 100644 RTSP/RtspHttpTransport.cs create mode 100644 RTSP/Utils/ArrayUtils.cs create mode 100644 RTSP/Utils/BitStream.cs create mode 100644 RTSP/Utils/HeadersParser.cs create mode 100644 RTSP/Utils/MD5.cs create mode 100644 RTSP/Utils/NetworkClientFactory.cs create mode 100644 RTSP/Utils/NetworkCredentialExtensions.cs rename RTSP/{ => Utils}/RTSPUtils.cs (94%) create mode 100644 RTSP/Utils/WellKnownHeaders.cs diff --git a/RTSP.Tests/RTSPUtilsTest.cs b/RTSP.Tests/RTSPUtilsTest.cs index 1ac9d0e..9b0b293 100644 --- a/RTSP.Tests/RTSPUtilsTest.cs +++ b/RTSP.Tests/RTSPUtilsTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using Rtsp.Utils; using System; namespace Rtsp.Tests diff --git a/RTSP/ArrayUtils.cs b/RTSP/ArrayUtils.cs deleted file mode 100644 index 1630faf..0000000 --- a/RTSP/ArrayUtils.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Rtsp -{ - internal static class ArrayUtils - { - public static bool IsBytesEquals(byte[] bytes1, int offset1, int count1, byte[] bytes2, int offset2, int count2) - { - if (count1 != count2) - return false; - - for (int i = 0; i < count1; i++) - if (bytes1[offset1 + i] != bytes2[offset2 + i]) - return false; - - return true; - } - } - -} diff --git a/RTSP/Authentication.cs b/RTSP/Authentication.cs index 53bca1c..d0cecdd 100644 --- a/RTSP/Authentication.cs +++ b/RTSP/Authentication.cs @@ -1,32 +1,132 @@ using Rtsp.Messages; using System; -using System.Security.Cryptography; -using System.Text; +using System.Collections.Generic; +using System.Net; namespace Rtsp { - // WWW-Authentication and Authorization Headers public abstract class Authentication { - protected readonly string username; - protected readonly string password; - protected readonly string realm; + public NetworkCredential Credentials { get; } - // Constructor - public Authentication(string username, string password, string realm) + protected Authentication(NetworkCredential credentials) { - this.username = username; - this.password = password; - this.realm = realm; + Credentials = credentials ?? throw new ArgumentNullException(nameof(credentials)); } - public abstract string GetHeader(); + public abstract string GetResponse(uint nonceCounter, string uri, string method, byte[] entityBodyBytes); + public abstract bool IsValid(RtspMessage message); + + public static Authentication Create(NetworkCredential credential, string authenticateHeader) + { + authenticateHeader = authenticateHeader ?? + throw new ArgumentNullException(nameof(authenticateHeader)); + + if (authenticateHeader.StartsWith("Basic", StringComparison.OrdinalIgnoreCase)) + return new AuthenticationBasic(credential); + + if (authenticateHeader.StartsWith("Digest", StringComparison.OrdinalIgnoreCase)) + { + int spaceIndex = authenticateHeader.IndexOf(' '); + + if (spaceIndex != -1) + { + string parameters = authenticateHeader.Substring(++spaceIndex); + + Dictionary parameterNameToValueMap = ParseParameters(parameters); + + if (!parameterNameToValueMap.TryGetValue("REALM", out string realm)) + throw new ArgumentException("\"realm\" parameter is not found"); + if (!parameterNameToValueMap.TryGetValue("NONCE", out string nonce)) + throw new ArgumentException("\"nonce\" parameter is not found"); + + parameterNameToValueMap.TryGetValue("QOP", out string qop); + return new AuthenticationDigest(credential, realm, nonce, qop); + } + } + + throw new ArgumentOutOfRangeException(authenticateHeader, + $"Invalid authenticate header: {authenticateHeader}"); + } + + private static Dictionary ParseParameters(string parameters) + { + var parameterNameToValueMap = new Dictionary(); + + int parameterStartOffset = 0; + while (parameterStartOffset < parameters.Length) + { + int equalsSignIndex = parameters.IndexOf('=', parameterStartOffset); + + if (equalsSignIndex == -1) + break; + int parameterNameLength = equalsSignIndex - parameterStartOffset; + string parameterName = parameters.Substring(parameterStartOffset, parameterNameLength).Trim() + .ToUpperInvariant(); - public abstract bool IsValid(RtspMessage received_message); + ++equalsSignIndex; - + int nonSpaceIndex = equalsSignIndex; + if (nonSpaceIndex == parameters.Length) + break; + + while (parameters[nonSpaceIndex] == ' ') + if (++nonSpaceIndex == parameters.Length) + break; + + int parameterValueStartPos; + int parameterValueEndPos; + int commaIndex; + + if (parameters[nonSpaceIndex] == '\"') + { + parameterValueStartPos = parameters.IndexOf('\"', equalsSignIndex); + + if (parameterValueStartPos == -1) + break; + + ++parameterValueStartPos; + + parameterValueEndPos = parameters.IndexOf('\"', parameterValueStartPos); + + if (parameterValueEndPos == -1) + break; + + commaIndex = parameters.IndexOf(',', parameterValueEndPos + 1); + + if (commaIndex != -1) + parameterStartOffset = ++commaIndex; + else + parameterStartOffset = parameters.Length; + } + else + { + parameterValueStartPos = nonSpaceIndex; + + commaIndex = parameters.IndexOf(',', ++nonSpaceIndex); + + if (commaIndex != -1) + { + parameterValueEndPos = commaIndex; + parameterStartOffset = ++commaIndex; + } + else + { + parameterValueEndPos = parameters.Length; + parameterStartOffset = parameterValueEndPos; + } + } + + int parameterValueLength = parameterValueEndPos - parameterValueStartPos; + string parameterValue = parameters.Substring(parameterValueStartPos, parameterValueLength); + + parameterNameToValueMap[parameterName] = parameterValue; + } + + return parameterNameToValueMap; + } } -} \ No newline at end of file +} diff --git a/RTSP/AuthenticationBasic.cs b/RTSP/AuthenticationBasic.cs index fc3d818..cc6560f 100644 --- a/RTSP/AuthenticationBasic.cs +++ b/RTSP/AuthenticationBasic.cs @@ -1,32 +1,22 @@ using Rtsp.Messages; using System; -using System.Security.Cryptography; +using System.Net; using System.Text; namespace Rtsp { // WWW-Authentication and Authorization Headers - public class AuthenticationBasic : Authentication + public class AuthenticationBasic(NetworkCredential credentials) : Authentication(credentials) { - private const char quote = '\"'; - - // Constructor - public AuthenticationBasic(string username, string password, string realm) - : base(username, password, realm) - { - } - - public override string GetHeader() + public override string GetResponse(uint nonceCounter, string uri, string method, byte[] entityBodyBytes) { - return $"Basic realm=\"{realm}\""; + string usernamePasswordHash = $"{Credentials.UserName}:{Credentials.Password}"; + return $"Bassic {Convert.ToBase64String(Encoding.UTF8.GetBytes(usernamePasswordHash))}"; } - - - public override bool IsValid(RtspMessage received_message) + public override bool IsValid(RtspMessage message) { - - string? authorization = received_message.Headers["Authorization"]; + string? authorization = message.Headers["Authorization"]; // Check Username and Password @@ -39,7 +29,7 @@ public override bool IsValid(RtspMessage received_message) string decoded_username = decoded.Substring(0, split_position); string decoded_password = decoded.Substring(split_position + 1); - if ((decoded_username == username) && (decoded_password == password)) + if ((decoded_username == Credentials.UserName) && (decoded_password == Credentials.Password)) { // _logger.Debug("Basic Authorization passed"); return true; @@ -54,21 +44,7 @@ public override bool IsValid(RtspMessage received_message) return false; } + public override string ToString() => $"Authentication Basic"; - - // Generate Basic or Digest Authorization - public static string? GenerateAuthorization(string username, string password, - string realm, string nonce, string url, string command) - { - - if (username == null || username.Length == 0) return null; - if (password == null || password.Length == 0) return null; - if (realm == null || realm.Length == 0) return null; - - byte[] credentials = Encoding.UTF8.GetBytes(username + ":" + password); - string credentials_base64 = Convert.ToBase64String(credentials); - string basic_authorization = "Basic " + credentials_base64; - return basic_authorization; - } } } \ No newline at end of file diff --git a/RTSP/AuthenticationDigest.cs b/RTSP/AuthenticationDigest.cs index de60808..0435fe0 100644 --- a/RTSP/AuthenticationDigest.cs +++ b/RTSP/AuthenticationDigest.cs @@ -1,35 +1,62 @@ using Rtsp.Messages; +using Rtsp.Utils; using System; -using System.Security.Cryptography; +using System.Net; using System.Text; namespace Rtsp { - // WWW-Authentication and Authorization Headers public class AuthenticationDigest : Authentication { + private readonly string _realm; + private readonly string _nonce; + private readonly string? _qop; + private readonly string _cnonce; - private readonly string nonce; - private readonly MD5 md5 = MD5.Create(); - - // Constructor - public AuthenticationDigest(string username, string password, string realm) - : base(username, password, realm) + public AuthenticationDigest(NetworkCredential credentials, string realm, string nonce, string qop) : base(credentials) { - nonce = new Random().Next(100000000, 999999999).ToString(); // random 9 digit number + _realm = realm ?? throw new ArgumentNullException(nameof(realm)); + _nonce = nonce ?? throw new ArgumentNullException(nameof(nonce)); + + if (!string.IsNullOrEmpty(qop)) + { + int commaIndex = qop.IndexOf(','); + _qop = commaIndex > -1 ? qop[..commaIndex] : qop; + } + uint cnonce = (uint)Guid.NewGuid().GetHashCode(); + _cnonce = cnonce.ToString("X8"); } - public override string GetHeader() + public override string GetResponse(uint nonceCounter, string uri, string method, byte[] entityBodyBytes) { - return $"Digest realm=\"{realm}\", nonce=\"{nonce}\""; - } + string ha1 = MD5.GetHashHexValues($"{Credentials.UserName}:{_realm}:{Credentials.Password}"); + string ha2Argument = $"{method}:{uri}"; + bool hasQop = !string.IsNullOrEmpty(_qop); + if (hasQop && _qop!.Equals("auth-int", StringComparison.InvariantCultureIgnoreCase)) + { + ha2Argument = $"{ha2Argument}:{MD5.GetHashHexValues(entityBodyBytes)}"; + } + string ha2 = MD5.GetHashHexValues(ha2Argument); - public override bool IsValid(RtspMessage received_message) + StringBuilder sb = new(); + sb.AppendFormat("Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\"", Credentials.UserName, _realm, _nonce, uri); + if (!hasQop) + { + string response = MD5.GetHashHexValues($"{ha1}:{_nonce}:{ha2}"); + sb.AppendFormat(", response=\"{0}\"", response); + } + else + { + string response = MD5.GetHashHexValues($"{ha1}:{_nonce}:{nonceCounter:X8}:{_cnonce}:{_qop}:{ha2}"); + sb.AppendFormat(", response=\"{0}\", cnonce=\"{1}\", nc=\"{2:X8}\", qop=\"{3}\"", response, _cnonce, nonceCounter, _qop); + } + return sb.ToString(); + } + public override bool IsValid(RtspMessage message) { - - string? authorization = received_message.Headers["Authorization"]; + string? authorization = message.Headers["Authorization"]; // Check Username, URI, Nonce and the MD5 hashed Response if (authorization != null && authorization.StartsWith("Digest ")) @@ -69,15 +96,15 @@ public override bool IsValid(RtspMessage received_message) // Create the MD5 Hash using all parameters passed in the Auth Header with the // addition of the 'Password' - string hashA1 = CalculateMD5Hash(md5, auth_header_username + ":" + auth_header_realm + ":" + password); - string hashA2 = CalculateMD5Hash(md5, received_message.Method + ":" + auth_header_uri); - string expected_response = CalculateMD5Hash(md5, hashA1 + ":" + auth_header_nonce + ":" + hashA2); + string hashA1 = MD5.GetHashHexValues(auth_header_username + ":" + auth_header_realm + ":" + Credentials.Password); + string hashA2 = MD5.GetHashHexValues(message.Method + ":" + auth_header_uri); + string expected_response = MD5.GetHashHexValues(hashA1 + ":" + auth_header_nonce + ":" + hashA2); // Check if everything matches // ToDo - extract paths from the URIs (ignoring SETUP's trackID) - if ((auth_header_username == username) - && (auth_header_realm == realm) - && (auth_header_nonce == nonce) + if ((auth_header_username == Credentials.Password) + && (auth_header_realm == _realm) + && (auth_header_nonce == _nonce) && (auth_header_response == expected_response) ) { @@ -94,48 +121,6 @@ public override bool IsValid(RtspMessage received_message) } - - // Generate Basic or Digest Authorization - public static string? GenerateAuthorization(string username, string password, - string realm, string nonce, string url, string command) - { - - if (string.IsNullOrEmpty(username)) return null; - if (string.IsNullOrEmpty(password)) return null; - if (string.IsNullOrEmpty(realm)) return null; - if (string.IsNullOrEmpty(nonce)) return null; - - - MD5 md5 = MD5.Create(); - string hashA1 = CalculateMD5Hash(md5, username + ":" + realm + ":" + password); - string hashA2 = CalculateMD5Hash(md5, command + ":" + url); - string response = CalculateMD5Hash(md5, hashA1 + ":" + nonce + ":" + hashA2); - - string digest_authorization = $"Digest username=\"{username}\", " - + $"realm=\"{realm}\", " - + $"nonce=\"{nonce}\", " - + $"uri=\"{url}\", " - + $"response=\"{response}\""; - - return digest_authorization; - - } - - - - // MD5 (lower case) - private static string CalculateMD5Hash(MD5 md5_session, string input) - { - byte[] inputBytes = Encoding.UTF8.GetBytes(input); - byte[] hash = md5_session.ComputeHash(inputBytes); - - var output = new StringBuilder(); - for (int i = 0; i < hash.Length; i++) - { - output.Append(hash[i].ToString("x2")); - } - - return output.ToString(); - } + public override string ToString() => $"Authentication Digest: Realm {_realm}, Nonce {_nonce}"; } } \ No newline at end of file diff --git a/RTSP/BitStream.cs b/RTSP/BitStream.cs deleted file mode 100644 index d7668a7..0000000 --- a/RTSP/BitStream.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; - -// (c) 2018 Roger Hardiman, RJH Technical Consultancy Ltd -// Simple class to Read and Write bits in a bit stream. -// Data is written to the end of the bit stream and the bit stream can be returned as a Byte Array -// Data can be read from the head of the bit stream -// Example -// bitstream.AddValue(0xA,4); // Write 4 bit value -// bitstream.AddValue(0xB,4); -// bitstream.AddValue(0xC,4); -// bitstream.AddValue(0xD,4); -// bitstream.ToArray() -> {0xAB, 0xCD} // Return Byte Array -// bitstream.Read(8) -> 0xAB // Read 8 bit value - -namespace Rtsp -{ - - // Very simple bitstream - public class BitStream - { - - private List data = new List(); // List only stores 0 or 1 (one 'bit' per List item) - - // Constructor - public BitStream() - { - } - - public void AddValue(int value, int num_bits) - { - // Add each bit to the List - for (int i = num_bits - 1; i >= 0; i--) - { - data.Add((byte)((value >> i) & 0x01)); - } - } - - public void AddHexString(String hex_string) - { - char[] hex_chars = hex_string.ToUpper().ToCharArray(); - foreach (char c in hex_chars) - { - if ((c.Equals('0'))) this.AddValue(0, 4); - else if ((c.Equals('1'))) this.AddValue(1, 4); - else if ((c.Equals('2'))) this.AddValue(2, 4); - else if ((c.Equals('3'))) this.AddValue(3, 4); - else if ((c.Equals('4'))) this.AddValue(4, 4); - else if ((c.Equals('5'))) this.AddValue(5, 4); - else if ((c.Equals('6'))) this.AddValue(6, 4); - else if ((c.Equals('7'))) this.AddValue(7, 4); - else if ((c.Equals('8'))) this.AddValue(8, 4); - else if ((c.Equals('9'))) this.AddValue(9, 4); - else if ((c.Equals('A'))) this.AddValue(10, 4); - else if ((c.Equals('B'))) this.AddValue(11, 4); - else if ((c.Equals('C'))) this.AddValue(12, 4); - else if ((c.Equals('D'))) this.AddValue(13, 4); - else if ((c.Equals('E'))) this.AddValue(14, 4); - else if ((c.Equals('F'))) this.AddValue(15, 4); - } - } - - public int Read(int num_bits) - { - // Read and remove items from the front of the list of bits - if (data.Count < num_bits) return 0; - int result = 0; - for (int i = 0; i < num_bits; i++) - { - result = result << 1; - result = result + data[0]; - data.RemoveAt(0); - } - return result; - } - - public byte[] ToArray() - { - int num_bytes = (int)Math.Ceiling((double)data.Count / 8.0); - byte[] array = new byte[num_bytes]; - int ptr = 0; - int shift = 7; - for (int i = 0; i < data.Count; i++) - { - array[ptr] += (byte)(data[i] << shift); - if (shift == 0) - { - shift = 7; - ptr++; - } - else - { - shift--; - } - } - - return array; - } - } -} \ No newline at end of file diff --git a/RTSP/HttpBadResponseCodeException.cs b/RTSP/HttpBadResponseCodeException.cs new file mode 100644 index 0000000..dcf9a20 --- /dev/null +++ b/RTSP/HttpBadResponseCodeException.cs @@ -0,0 +1,20 @@ +using System.Net; +using System.Runtime.Serialization; +using System; + +namespace Rtsp; + +[Serializable] +public class HttpBadResponseCodeException : Exception +{ + public HttpStatusCode Code { get; } + + public HttpBadResponseCodeException(HttpStatusCode code) : base($"Bad response code: {code}") + { + Code = code; + } + + protected HttpBadResponseCodeException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } +} \ No newline at end of file diff --git a/RTSP/HttpBadResponseException.cs b/RTSP/HttpBadResponseException.cs new file mode 100644 index 0000000..eff7312 --- /dev/null +++ b/RTSP/HttpBadResponseException.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; +using System; + +namespace Rtsp; + +[Serializable] +public class HttpBadResponseException : Exception +{ + public HttpBadResponseException() + { + } + + public HttpBadResponseException(string message) : base(message) + { + } + + public HttpBadResponseException(string message, Exception inner) : base(message, inner) + { + } + + protected HttpBadResponseException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } +} \ No newline at end of file diff --git a/RTSP/IRTSPTransport.cs b/RTSP/IRTSPTransport.cs index 492bcaa..b52c607 100644 --- a/RTSP/IRTSPTransport.cs +++ b/RTSP/IRTSPTransport.cs @@ -15,10 +15,12 @@ public interface IRtspTransport /// Gets the remote address. /// /// The remote address. - string RemoteAddress - { - get; - } + string RemoteAddress { get; } + + /// + /// Keep track of issued commands... + /// + uint CommandCounter { get; } /// /// Closes this instance. diff --git a/RTSP/Messages/RTSPChunk.cs b/RTSP/Messages/RTSPChunk.cs index ac387f1..abe65d6 100644 --- a/RTSP/Messages/RTSPChunk.cs +++ b/RTSP/Messages/RTSPChunk.cs @@ -13,6 +13,12 @@ public abstract class RtspChunk : ICloneable /// Array of byte transmit with the message. public byte[]? Data { get; set; } + /// + /// Gets or sets the data length associated with the message. + /// + /// Integer with the length of message (usefull if using ArrayPool for avoiding gc pressure) + public int DataLength { get; set; } + /// /// Gets or sets the source port wich receive the message. /// diff --git a/RTSP/Messages/RTSPData.cs b/RTSP/Messages/RTSPData.cs index 8c3e106..94f51a8 100644 --- a/RTSP/Messages/RTSPData.cs +++ b/RTSP/Messages/RTSPData.cs @@ -34,6 +34,7 @@ public override string ToString() Channel = Channel, SourcePort = SourcePort, Data = Data is null ? null : Data.Clone() as byte[], + DataLength = DataLength, }; } } diff --git a/RTSP/Messages/RTSPMessage.cs b/RTSP/Messages/RTSPMessage.cs index 59e8316..c64a7ba 100644 --- a/RTSP/Messages/RTSPMessage.cs +++ b/RTSP/Messages/RTSPMessage.cs @@ -1,6 +1,7 @@ namespace Rtsp.Messages { using System; + using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; @@ -26,7 +27,7 @@ public static RtspMessage GetRtspMessage(string aRequestLine) // We can't determine the message if (string.IsNullOrEmpty(aRequestLine)) return new RtspMessage(); - string[] requestParts = aRequestLine.Split(new char[] { ' ' }, 3); + string[] requestParts = aRequestLine.Split([' '], 3); RtspMessage returnValue; if (requestParts.Length == 3) { @@ -61,11 +62,12 @@ public static RtspMessage GetRtspMessage(string aRequestLine) /// public RtspMessage() { - Data = Array.Empty(); + Data = []; + DataLength = 0; Creation = DateTime.Now; } - protected internal string[] commandArray = new string[] { string.Empty }; + protected internal string[] commandArray = [string.Empty]; /// /// Gets or sets the creation time. @@ -80,7 +82,7 @@ public RtspMessage() public string Command { get => commandArray is null ? string.Empty : string.Join(" ", commandArray); - set => commandArray = value is null ? new string[] { string.Empty } : value.Split(new char[] { ' ' }, 3); + set => commandArray = value is null ? new string[] { string.Empty } : value.Split([' '], 3); } /// @@ -108,7 +110,7 @@ public void AddHeader(string line) } //spliter - string[] elements = line.Split(new char[] { ':' }, 2); + string[] elements = line.Split([':'], 2); if (elements.Length == 2) { Headers[elements[0].Trim()] = elements[1].TrimStart(); @@ -150,10 +152,10 @@ public virtual string? Session { get { - if (!Headers.ContainsKey("Session")) + if (!Headers.TryGetValue("Session", out string? value)) return null; - return Headers["Session"]; + return value; } set { @@ -171,7 +173,8 @@ public void InitialiseDataFromContentLength() { dataLength = 0; } - Data = new byte[dataLength]; + Data = ArrayPool.Shared.Rent(dataLength); + DataLength = dataLength; } /// @@ -179,9 +182,9 @@ public void InitialiseDataFromContentLength() /// public void AdjustContentLength() { - if (Data?.Length > 0) + if (DataLength > 0) { - Headers["Content-Length"] = Data.Length.ToString(CultureInfo.InvariantCulture); + Headers["Content-Length"] = DataLength.ToString(CultureInfo.InvariantCulture); } else { @@ -209,14 +212,14 @@ public void SendTo(Stream stream) Contract.EndContractBlock(); Encoding encoder = Encoding.UTF8; - var outputString = new StringBuilder(); + StringBuilder outputString = new(); AdjustContentLength(); // output header outputString.Append(Command); outputString.Append("\r\n"); - foreach (var item in Headers) + foreach (KeyValuePair item in Headers) { outputString.AppendFormat("{0}: {1}\r\n", item.Key, item.Value); } @@ -227,18 +230,56 @@ public void SendTo(Stream stream) stream.Write(buffer, 0, buffer.Length); // Output data - if (Data?.Length > 0) - stream.Write(Data.AsSpan()); + if (Data != null && DataLength > 0) + { + stream.Write(Data, 0, DataLength); + } } stream.Flush(); } + /// + /// Like the methods, but will return the current byte[] stream without sending it. + /// + /// An array of Bytes + public byte[] Prepare() + { + MemoryStream ms = new(); + Encoding encoder = Encoding.UTF8; + StringBuilder outputString = new(); + + AdjustContentLength(); + + // output header + outputString.Append(Command); + outputString.Append("\r\n"); + foreach (KeyValuePair item in Headers) + { + outputString.AppendFormat("{0}: {1}\r\n", item.Key, item.Value); + } + outputString.Append("\r\n"); + byte[] buffer = encoder.GetBytes(outputString.ToString()); + lock (ms) + { + ms.Write(buffer, 0, buffer.Length); + + // Output data + if (Data != null && DataLength > 0) + { + ms.Write(Data, 0, DataLength); + } + } + + return ms.ToArray(); + } + + /// /// Create a string of the message for debug. /// public override string ToString() { - var stringBuilder = new StringBuilder(); + StringBuilder stringBuilder = new(); stringBuilder.AppendLine($"Commande : {Command}"); foreach (KeyValuePair item in Headers) @@ -264,7 +305,7 @@ public override object Clone() { RtspMessage returnValue = GetRtspMessage(Command); - foreach (var item in Headers) + foreach (KeyValuePair item in Headers) { returnValue.Headers.Add(item.Key, item.Value); } diff --git a/RTSP/RTSP.csproj b/RTSP/RTSP.csproj index 022b1e1..a1351a9 100644 --- a/RTSP/RTSP.csproj +++ b/RTSP/RTSP.csproj @@ -1,52 +1,53 @@  - - netstandard2.1 - Library - 0 - StyleCopViolations.xml - 9 - enable - True - SharpRTSP - ngraziano - SharpRTSP - Handle receive and send of Rtsp Messages - 1.1.0 - https://github.com/ngraziano/SharpRTSP - https://github.com/ngraziano/SharpRTSP - True - MIT - - - True - False - True - False - False - False - False - False - False - True - False - False - True - - - - - - - False - Full - Build - - - - - - - all - - + + netstandard2.1 + Library + 0 + StyleCopViolations.xml + Latest + enable + True + SharpRTSP + ngraziano + SharpRTSP + Handle receive and send of Rtsp Messages + 1.1.0 + https://github.com/ngraziano/SharpRTSP + https://github.com/ngraziano/SharpRTSP + True + MIT + Rtsp + + + True + False + True + False + False + False + False + False + False + True + False + False + True + + + + + + + False + Full + Build + + + + + + + all + + \ No newline at end of file diff --git a/RTSP/RTSPDataEventArgs.cs b/RTSP/RTSPDataEventArgs.cs index c37887b..a277034 100644 --- a/RTSP/RTSPDataEventArgs.cs +++ b/RTSP/RTSPDataEventArgs.cs @@ -1,25 +1,75 @@ using System; +using System.Collections.Generic; -namespace Rtsp +namespace Rtsp; + +/// +/// Event args containing information for message events. +/// +/// +/// Initializes a new instance of the class. +/// +/// A data. +public class RtspDataEventArgs(byte[] data, int length) : EventArgs { + /// - /// Event args containing information for message events. + /// Gets or sets the message. /// - public class RtspDataEventArgs : EventArgs - { - /// - /// Initializes a new instance of the class. - /// - /// A data. - public RtspDataEventArgs(byte[] data) - { - Data = data; - } + /// The message. + public byte[] Data { get; set; } = data; + public int DataLength { get; set; } = length; +} - /// - /// Gets or sets the message. - /// - /// The message. - public byte[] Data { get; set; } - } +/// +/// H264 SPS - PPS received values... +/// +/// +/// +public class SpsPpsEventArgs(byte[] sps, byte[] pps) : EventArgs +{ + public byte[] Sps { get; } = sps; + public byte[] Pps { get; } = pps; +} +/// +/// H265 VPS - SPS - PPS received values... +/// +/// +/// +/// +public class VpsSpsPpsEventArgs(byte[] vps, byte[] sps, byte[] pps) : EventArgs +{ + public byte[] Vps { get; } = vps; + public byte[] Sps { get; } = sps; + public byte[] Pps { get; } = pps; +} +/// +/// byte array data... +/// +/// +public class SimpleDataEventArgs(List> data) : EventArgs +{ + public List> Data { get; } = data; +} +//public delegate void ReceivedSimpleDataDelegate(List> data); +public class G711EventArgs(string format, List> data) : EventArgs +{ + public string Format { get; } = format; + public List> Data { get; } = data; +} +//public delegate void Received_G711_Delegate(string format, List> g711); +public class AMREventArgs(string format, List> data) : EventArgs +{ + public string Format { get; } = format; + public List> Data { get; } = data; +} +//public delegate void Received_AMR_Delegate(string format, List> amr); +public class AACEventArgs(string format, List> data, uint objectType, uint frequencyIndex, uint channelConfiguration) : EventArgs +{ + public string Format { get; } = format; + public List> Data { get; } = data; + public uint ObjectType { get; } = objectType; + public uint FrequencyIndex { get; } = frequencyIndex; + public uint ChannelConfiguration { get; } = channelConfiguration; } +//public delegate void Received_AAC_Delegate(string format, List> aac, uint ObjectType, uint FrequencyIndex, uint ChannelConfiguration); diff --git a/RTSP/RTSPListener.cs b/RTSP/RTSPListener.cs index 98769d7..bbc0283 100644 --- a/RTSP/RTSPListener.cs +++ b/RTSP/RTSPListener.cs @@ -1,363 +1,373 @@ -namespace Rtsp +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Rtsp.Messages; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace Rtsp; + +/// +/// Initializes a new instance of the class from a TCP connection. +/// +/// The connection. +public class RtspListener(IRtspTransport connection, ILogger? logger = null) : IDisposable { - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Logging.Abstractions; - using Rtsp.Messages; - using System; - using System.Collections.Generic; - using System.Diagnostics.Contracts; - using System.IO; - using System.Net.Sockets; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + private readonly ILogger _logger = logger as ILogger ?? NullLogger.Instance; + private readonly IRtspTransport _transport = connection ?? throw new ArgumentNullException(nameof(connection)); + private readonly Dictionary _sentMessage = []; + + private Thread? _listenTread; + private Stream _stream = connection.GetStream(); + + private int _sequenceNumber; /// - /// Rtsp lister + /// Gets the remote address. /// - public class RtspListener : IDisposable + /// The remote adress. + public string RemoteAdress { - private readonly ILogger _logger; - private readonly IRtspTransport _transport; - private readonly Dictionary _sentMessage = new(); - - private CancellationTokenSource? _cancelationTokenSource; - private Task? _mainTask; - private Stream _stream; - - private int _sequenceNumber; - - /// - /// Initializes a new instance of the class from a TCP connection. - /// - /// The connection. - /// Logger - public RtspListener(IRtspTransport connection, ILogger? logger = null) + get { - _logger = logger as ILogger ?? NullLogger.Instance; - - _transport = connection ?? throw new ArgumentNullException(nameof(connection)); - _stream = connection.GetStream(); + return _transport.RemoteAddress; } + } - /// - /// Gets the remote address. - /// - /// The remote adress. - public string RemoteAdress => _transport.RemoteAddress; - - /// - /// Starts this instance. - /// - public void Start() + /// + /// Starts this instance. + /// + public void Start() + { + _listenTread = new(new ThreadStart(DoJob)) { - _cancelationTokenSource = new(); - _mainTask = Task.Factory.StartNew(async () => await DoJobAsync(_cancelationTokenSource.Token), _cancelationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current); - } + Name = "DoJob" + }; + _listenTread.Start(); + } - /// - /// Stops this instance. - /// - public void Stop() - { - // brutally close the TCP socket.... - // I hope the teardown was sent elsewhere - _transport.Close(); - _cancelationTokenSource?.Cancel(); - } + /// + /// Stops this instance. + /// + public void Stop() + { + // brutally close the TCP socket.... + // I hope the teardown was sent elsewhere + _transport.Close(); - /// - /// Enable auto reconnect. - /// - public bool AutoReconnect { get; set; } - - /// - /// Occurs when message is received. - /// - public event EventHandler? MessageReceived; - - /// - /// Raises the event. - /// - /// The instance containing the event data. - protected void OnMessageReceived(RtspChunkEventArgs e) - { - MessageReceived?.Invoke(this, e); - } + } - /// - /// Occurs when Data is received. - /// - public event EventHandler? DataReceived; + /// + /// Enable auto reconnect. + /// + public bool AutoReconnect { get; set; } - /// - /// Raises the event. - /// - /// The instance containing the event data. - protected void OnDataReceived(RtspChunkEventArgs rtspChunkEventArgs) - { - DataReceived?.Invoke(this, rtspChunkEventArgs); - } + /// + /// Occurs when message is received. + /// + public event EventHandler? MessageReceived; - /// - /// Does the reading job. - /// - /// - /// This method read one message from TCP connection. - /// If it a response it add the associate question. - /// The stopping is made by the closing of the TCP connection. - /// - private async Task DoJobAsync(CancellationToken token) + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected void OnMessageReceived(RtspChunkEventArgs e) => MessageReceived?.Invoke(this, e); + + /// + /// Occurs when Data is received. + /// + public event EventHandler? DataReceived; + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected void OnDataReceived(RtspChunkEventArgs rtspChunkEventArgs) => DataReceived?.Invoke(this, rtspChunkEventArgs); + + /// + /// Does the reading job. + /// + /// + /// This method read one message from TCP connection. + /// If it a response it add the associate question. + /// The stopping is made by the closing of the TCP connection. + /// + private void DoJob() + { + try { - try + _logger.LogDebug("Connection Open"); + while (_transport.Connected) { - _logger.LogDebug("Connection Open"); - while (_transport.Connected && !token.IsCancellationRequested) - { - // La lectuer est blocking sauf si la connection est coupé - RtspChunk? currentMessage = await ReadOneMessageAsync(_stream); + // La lectuer est blocking sauf si la connection est coupé + RtspChunk? currentMessage = ReadOneMessage(_stream); - if (currentMessage != null) + if (currentMessage is not null) + { + if (currentMessage is not RtspData) { - if (_logger.IsEnabled(LogLevel.Debug) && currentMessage is not RtspData) + // on logue le tout + if (currentMessage.SourcePort != null) { - // on logue le tout - if (currentMessage.SourcePort != null) - _logger.LogDebug("Receive from {remoteAdress}", currentMessage.SourcePort.RemoteAdress); - _logger.LogDebug("{message}", currentMessage); + _logger.LogDebug("Receive from {remoteAdress}", currentMessage.SourcePort.RemoteAdress); } - switch (currentMessage) - { - case RtspResponse response: - lock (_sentMessage) + _logger.LogDebug("{message}", currentMessage); + } + + switch (currentMessage) + { + case RtspResponse response: + lock (_sentMessage) + { + // add the original question to the response. + if (_sentMessage.TryGetValue(response.CSeq, out RtspRequest originalRequest)) { - // add the original question to the response. - if (_sentMessage.TryGetValue(response.CSeq, out RtspRequest originalRequest)) - { - _sentMessage.Remove(response.CSeq); - response.OriginalRequest = originalRequest; - } - else - { - _logger.LogWarning("Receive response not asked {cseq}", response.CSeq); - } + _sentMessage.Remove(response.CSeq); + response.OriginalRequest = originalRequest; } - OnMessageReceived(new RtspChunkEventArgs(response)); - break; + else + { + _logger.LogWarning("Receive response not asked {cseq}", response.CSeq); + } + } + OnMessageReceived(new RtspChunkEventArgs(response)); + break; - case RtspRequest _: - OnMessageReceived(new RtspChunkEventArgs(currentMessage)); - break; - case RtspData _: - OnDataReceived(new RtspChunkEventArgs(currentMessage)); - break; - } - } - else - { - _stream.Close(); - _transport.Close(); + case RtspRequest _: + OnMessageReceived(new RtspChunkEventArgs(currentMessage)); + break; + case RtspData _: + OnDataReceived(new RtspChunkEventArgs(currentMessage)); + break; } + + } + else + { + _stream.Close(); + _transport.Close(); } } - catch (IOException error) - { - _logger.LogWarning(error, "IO Error"); - } - catch (SocketException error) - { - _logger.LogWarning(error, "Socket Error"); - } - catch (ObjectDisposedException error) + } + catch (IOException error) + { + _logger.LogWarning(error, "IO Error"); + _stream.Close(); + _transport.Close(); + } + catch (SocketException error) + { + _logger.LogWarning(error, "Socket Error"); + _stream.Close(); + _transport.Close(); + } + catch (ObjectDisposedException error) + { + _logger.LogWarning(error, "Object Disposed"); + } + catch (Exception error) + { + // throw; + _logger.LogWarning(error, "Unknow Error"); + } + finally + { + _stream.Close(); + _transport.Close(); + } + + _logger.LogDebug("Connection Close"); + } + + [Serializable] + private enum ReadingState + { + NewCommand, + Headers, + Data, + End, + InterleavedData, + MoreInterleavedData, + } + + /// + /// Sends the message. + /// + /// A message. + /// if it is Ok, otherwise + public bool SendMessage(RtspMessage message) + { + if (message == null) { throw new ArgumentNullException(nameof(message)); } + Contract.EndContractBlock(); + + if (!_transport.Connected) + { + if (!AutoReconnect) { - _logger.LogWarning(error, "Object Disposed"); + return false; } - catch (Exception error) + _logger.LogWarning("Reconnect to a client, strange !!"); + + try { - _logger.LogWarning(error, "Unknow Error"); - // throw; + Reconnect(); } - finally + catch (SocketException) { - _stream.Close(); - _transport.Close(); + // on a pas put se connecter on dit au manager de plus compter sur nous + return false; } - - _logger.LogDebug("Connection Close"); - } - - [Serializable] - private enum ReadingState - { - NewCommand, - Headers, - Data, - End, - InterleavedData, - MoreInterleavedData, } - /// - /// Sends the message. - /// - /// A message. - /// if it is Ok, otherwise - public bool SendMessage(RtspMessage message) + // if it it a request we store the original message + // and we renumber it. + //TODO handle lost message (for example every minute cleanup old message) + if (message is RtspRequest originalMessage) { - if (message == null) - throw new ArgumentNullException(nameof(message)); - Contract.EndContractBlock(); - - if (!_transport.Connected) + // Do not modify original message + message = (RtspMessage)message.Clone(); + _sequenceNumber++; + message.CSeq = _sequenceNumber; + lock (_sentMessage) { - if (!AutoReconnect) - return false; - - _logger.LogWarning("Reconnect to a client, strange !!"); - try - { - Reconnect(); - } - catch (SocketException) - { - // on a pas put se connecter on dit au manager de plus compter sur nous - return false; - } + _sentMessage.Add(message.CSeq, originalMessage); } + } - // if it it a request we store the original message - // and we renumber it. - //TODO handle lost message (for example every minute cleanup old message) - if (message is RtspRequest originalMessage) - { - // Do not modify original message - message = (RtspMessage)message.Clone(); - _sequenceNumber++; - message.CSeq = _sequenceNumber; - lock (_sentMessage) - { - _sentMessage.Add(message.CSeq, originalMessage); - } - } + _logger.LogDebug("Send Message\n {message}", message); - _logger.LogDebug("Send Message\n {message}", message); + if (_transport is RtspHttpTransport httpTransport) + { + byte[] data = message.Prepare(); + httpTransport.Write(data, 0, data.Length); + } + else + { message.SendTo(_stream); - return true; } + return true; + } - /// - /// Reconnect this instance of RtspListener. - /// - /// Error during socket - public void Reconnect() - { - //if it is already connected do not reconnect - if (_transport.Connected) - return; + /// + /// Reconnect this instance of RtspListener. + /// + /// Error during socket + public void Reconnect() + { + //if it is already connected do not reconnect + if (_transport.Connected) + return; - // If it is not connected listenthread should have die. - _mainTask?.Wait(); + // If it is not connected listenthread should have die. + if (_listenTread != null && _listenTread.IsAlive) + _listenTread.Join(); - _stream?.Dispose(); + _stream?.Dispose(); - // reconnect - _transport.Reconnect(); - _stream = _transport.GetStream(); + // reconnect + _transport.Reconnect(); + _stream = _transport.GetStream(); - // If listen thread exist restart it - if (_mainTask != null) - Start(); - } + // If listen thread exist restart it + if (_listenTread != null) + Start(); + } - /// - /// Reads one message. - /// - /// The Rtsp stream. - /// Message readen - public async Task ReadOneMessageAsync(Stream commandStream) + /// + /// Reads one message. + /// + /// The Rtsp stream. + /// Message readen + public RtspChunk? ReadOneMessage(Stream commandStream) + { + if (commandStream == null) { throw new ArgumentNullException(nameof(commandStream)); } + Contract.EndContractBlock(); + + ReadingState currentReadingState = ReadingState.NewCommand; + // current decode message , create a fake new to permit compile. + RtspChunk? currentMessage = null; + + int size = 0; + int byteReaden = 0; + List buffer = new(256); + string oneLine = string.Empty; + while (currentReadingState != ReadingState.End) { - if (commandStream == null) - throw new ArgumentNullException(nameof(commandStream)); - Contract.EndContractBlock(); - - ReadingState currentReadingState = ReadingState.NewCommand; - // current decode message , create a fake new to permit compile. - RtspChunk? currentMessage = null; - - int size = 0; - int byteReaden = 0; - var buffer = new List(256); - string oneLine = string.Empty; - while (currentReadingState != ReadingState.End) + // if the system is not reading binary data. + if (currentReadingState != ReadingState.Data && currentReadingState != ReadingState.MoreInterleavedData) { - // if the system is not reading binary data. - if (currentReadingState != ReadingState.Data && currentReadingState != ReadingState.MoreInterleavedData) + oneLine = string.Empty; + bool needMoreChar = true; + // I do not know to make readline blocking + while (needMoreChar) { - oneLine = string.Empty; - bool needMoreChar = true; - // I do not know to make readline blocking - while (needMoreChar) - { - int currentByte = commandStream.ReadByte(); + int currentByte = commandStream.ReadByte(); - switch (currentByte) - { - case -1: - // the read is blocking, so if we got -1 it is because the client close; - currentReadingState = ReadingState.End; - needMoreChar = false; - break; - case '\n': - oneLine = Encoding.UTF8.GetString(buffer.ToArray()); - buffer.Clear(); + switch (currentByte) + { + case -1: + // the read is blocking, so if we got -1 it is because the client close; + currentReadingState = ReadingState.End; + needMoreChar = false; + break; + case '\n': + oneLine = Encoding.UTF8.GetString([.. buffer]); + buffer.Clear(); + needMoreChar = false; + break; + case '\r': + // simply ignore this + break; + case '$': // if first caracter of packet is $ it is an interleaved data packet + if (currentReadingState == ReadingState.NewCommand && buffer.Count == 0) + { + currentReadingState = ReadingState.InterleavedData; needMoreChar = false; - break; - case '\r': - // simply ignore this - break; - case '$': // if first caracter of packet is $ it is an interleaved data packet - if (currentReadingState == ReadingState.NewCommand && buffer.Count == 0) - { - currentReadingState = ReadingState.InterleavedData; - needMoreChar = false; - } - else - { - goto default; - } - - break; - default: - buffer.Add((byte)currentByte); - break; - } + } + else + goto default; + break; + default: + buffer.Add((byte)currentByte); + break; } } + } - switch (currentReadingState) - { - case ReadingState.NewCommand: + switch (currentReadingState) + { + case ReadingState.NewCommand: + { currentMessage = RtspMessage.GetRtspMessage(oneLine); currentReadingState = ReadingState.Headers; - break; - case ReadingState.Headers: + } + break; + case ReadingState.Headers when currentMessage is RtspMessage rtspMessage: + { string line = oneLine; if (string.IsNullOrEmpty(line)) { currentReadingState = ReadingState.Data; - ((RtspMessage)currentMessage!).InitialiseDataFromContentLength(); + rtspMessage.InitialiseDataFromContentLength(); } else { - ((RtspMessage)currentMessage!).AddHeader(line); + rtspMessage.AddHeader(line); } - break; - case ReadingState.Data: - if (currentMessage!.Data!.Length > 0) + } + break; + case ReadingState.Data when currentMessage?.Data is not null: + { + if (currentMessage.DataLength > 0) { // Read the remaning data - int byteCount = await commandStream.ReadAsync( - currentMessage.Data.AsMemory(byteReaden, currentMessage.Data.Length - byteReaden)); + int byteCount = commandStream.Read(currentMessage.Data, byteReaden, currentMessage.DataLength - byteReaden); if (byteCount <= 0) { currentReadingState = ReadingState.End; @@ -367,10 +377,12 @@ public void Reconnect() _logger.LogDebug("Readen {byteReaden} byte of data", byteReaden); } // if we haven't read all go there again else go to end. - if (byteReaden >= currentMessage.Data.Length) + if (byteReaden >= currentMessage.DataLength) currentReadingState = ReadingState.End; - break; - case ReadingState.InterleavedData: + } + break; + case ReadingState.InterleavedData: + { currentMessage = new RtspData(); int channelByte = commandStream.ReadByte(); if (channelByte == -1) @@ -378,6 +390,7 @@ public void Reconnect() currentReadingState = ReadingState.End; break; } + ((RtspData)currentMessage).Channel = channelByte; int sizeByte1 = commandStream.ReadByte(); @@ -393,154 +406,160 @@ public void Reconnect() break; } size = (sizeByte1 << 8) + sizeByte2; - currentMessage.Data = new byte[size]; + + currentMessage.DataLength = size; + currentMessage.Data = ArrayPool.Shared.Rent(size); //new byte[size]; + currentReadingState = ReadingState.MoreInterleavedData; - break; - case ReadingState.MoreInterleavedData: - // apparently non blocking + } + break; + case ReadingState.MoreInterleavedData when currentMessage is not null: + // apparently non blocking + { + int byteCount = commandStream.Read(currentMessage.Data, byteReaden, size - byteReaden); + if (byteCount <= 0) { - int byteCount = await commandStream.ReadAsync( - currentMessage!.Data.AsMemory(byteReaden, size - byteReaden)); - if (byteCount <= 0) - { - currentReadingState = ReadingState.End; - break; - } - byteReaden += byteCount; - if (byteReaden < size) - currentReadingState = ReadingState.MoreInterleavedData; - else - currentReadingState = ReadingState.End; + currentReadingState = ReadingState.End; break; } - default: + byteReaden += byteCount; + currentReadingState = byteReaden < size ? ReadingState.MoreInterleavedData : ReadingState.End; break; - } + } + default: + break; } - if (currentMessage != null) - currentMessage.SourcePort = this; - return currentMessage; } + if (currentMessage != null) + currentMessage.SourcePort = this; + return currentMessage; + } - /// - /// Begins the send data. - /// - /// A Rtsp data. - /// The async callback. - /// A state. - public IAsyncResult? BeginSendData(RtspData aRtspData, AsyncCallback asyncCallback, object state) - { - if (aRtspData is null) - throw new ArgumentNullException(nameof(aRtspData)); - if (aRtspData.Data is null) - throw new ArgumentException("no data present", nameof(aRtspData)); + /// + /// Begins the send data. + /// + /// A Rtsp data. + /// The async callback. + /// A state. + public IAsyncResult? BeginSendData(RtspData aRtspData, AsyncCallback asyncCallback, object state) + { + if (aRtspData is null) { throw new ArgumentNullException(nameof(aRtspData)); } + if (aRtspData.Data is null) { throw new ArgumentException("no data present", nameof(aRtspData)); } + Contract.EndContractBlock(); - Contract.EndContractBlock(); + return BeginSendData(aRtspData.Channel, aRtspData.Data, asyncCallback, state); + } - return BeginSendData(aRtspData.Channel, aRtspData.Data, asyncCallback, state); - } + /// + /// Begins the send data. + /// + /// The channel. + /// The frame. + /// The async callback. + /// A state. + public IAsyncResult? BeginSendData(int channel, byte[] frame, AsyncCallback asyncCallback, object state) + { + if (frame == null) + throw new ArgumentNullException(nameof(frame)); + if (frame.Length > 0xFFFF) + throw new ArgumentException("frame too large", nameof(frame)); + Contract.EndContractBlock(); - /// - /// Begins the send data. - /// - /// The channel. - /// The frame. - /// The async callback. - /// A state. - public IAsyncResult? BeginSendData(int channel, byte[] frame, AsyncCallback asyncCallback, object state) + if (!_transport.Connected) { - if (frame == null) - throw new ArgumentNullException(nameof(frame)); - if (frame.Length > 0xFFFF) - throw new ArgumentException("frame too large", nameof(frame)); - Contract.EndContractBlock(); - - if (!_transport.Connected) - { - if (!AutoReconnect) - return null; // cannot write when transport is disconnected + if (!AutoReconnect) + return null; // cannot write when transport is disconnected - _logger.LogWarning("Reconnect to a client, strange."); - Reconnect(); - } + Reconnect(); + } - byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header + int length = 4 + frame.Length; + byte[] data = ArrayPool.Shared.Rent(length); + try + { + //byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header data[0] = 36; // '$' character data[1] = (byte)channel; data[2] = (byte)((frame.Length & 0xFF00) >> 8); - data[3] = (byte)(frame.Length & 0x00FF); + data[3] = (byte)((frame.Length & 0x00FF)); Array.Copy(frame, 0, data, 4, frame.Length); - return _stream.BeginWrite(data, 0, data.Length, asyncCallback, state); + return _stream.BeginWrite(data, 0, length, asyncCallback, state); } + finally { ArrayPool.Shared.Return(data, true); } + } - /// - /// Ends the send data. - /// - /// The result. - public void EndSendData(IAsyncResult result) + /// + /// Ends the send data. + /// + /// The result. + public void EndSendData(IAsyncResult result) + { + try { - try - { - _stream.EndWrite(result); - } - catch (Exception e) - { - // Error, for example stream has already been Disposed - _logger.LogDebug(e, "Error during end send (can be ignored) "); - } + _stream.EndWrite(result); } - - /// - /// Send data (Synchronous) - /// - /// The channel. - /// The frame. - public void SendData(int channel, byte[] frame) + catch (Exception e) { - if (frame == null) - throw new ArgumentNullException(nameof(frame)); - if (frame.Length > 0xFFFF) - throw new ArgumentException("frame too large", nameof(frame)); - Contract.EndContractBlock(); + // Error, for example stream has already been Disposed + //result = null; + _logger.LogDebug(e, "Error during end send (can be ignored) "); + } + } - if (!_transport.Connected) - { - if (!AutoReconnect) - throw new Exception("Connection is lost"); + /// + /// Send data (Synchronous) + /// + /// The channel. + /// The frame. + public void SendData(int channel, byte[] frame) + { + if (frame == null) { throw new ArgumentNullException(nameof(frame)); } + if (frame.Length > 0xFFFF) { throw new ArgumentException("frame too large", nameof(frame)); } + Contract.EndContractBlock(); - _logger.LogWarning("Reconnect to a client, strange."); - Reconnect(); - } + if (!_transport.Connected) + { + if (!AutoReconnect) { throw new Exception("Connection is lost"); } + + _logger.LogWarning("Reconnect to a client, strange."); + Reconnect(); + } - byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header + int length = 4 + frame.Length; + byte[] data = ArrayPool.Shared.Rent(length); + try + { + //byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header data[0] = 36; // '$' character data[1] = (byte)channel; data[2] = (byte)((frame.Length & 0xFF00) >> 8); - data[3] = (byte)(frame.Length & 0x00FF); + data[3] = (byte)((frame.Length & 0x00FF)); Array.Copy(frame, 0, data, 4, frame.Length); lock (_stream) { _stream.Write(data, 0, data.Length); } } + finally { ArrayPool.Shared.Return(data, true); } + } - #region IDisposable Membres - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + #region IDisposable Membres + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - Stop(); - _stream?.Dispose(); - } + Stop(); + _stream?.Dispose(); } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/RTSP/RTSPMessageAuthExtension.cs b/RTSP/RTSPMessageAuthExtension.cs new file mode 100644 index 0000000..a0e573e --- /dev/null +++ b/RTSP/RTSPMessageAuthExtension.cs @@ -0,0 +1,30 @@ +using Rtsp.Messages; +using System; +using System.Net; +using System.Security.Cryptography; +using System.Text; + +namespace Rtsp; + +public static class RTSPMessageAuthExtension +{ + public static void AddAuthorization(this RtspMessage message, NetworkCredential credentials, Authentication authentication, Uri uri, uint commandCounter) + { + switch (authentication) + { + case AuthenticationBasic basic: + { + string authorization = basic.GetResponse(commandCounter, string.Empty, string.Empty, []); + message.Headers.Add(RtspHeaderNames.Authorization, authorization); + } + break; + case AuthenticationDigest digest: + { + string authorization = digest.GetResponse(commandCounter, uri.AbsoluteUri, message.Method, []); + message.Headers.Add(RtspHeaderNames.Authorization, authorization); + + } + break; + } + } +} diff --git a/RTSP/RTSPTCPTransport.cs b/RTSP/RTSPTCPTransport.cs index 14344ff..de6b9e4 100644 --- a/RTSP/RTSPTCPTransport.cs +++ b/RTSP/RTSPTCPTransport.cs @@ -5,101 +5,98 @@ using System.Net; using System.Net.Sockets; -namespace Rtsp +namespace Rtsp; + +/// +/// TCP Connection for Rtsp +/// +public class RtspTcpTransport : IRtspTransport, IDisposable { + private readonly NetworkCredential _credentials; + private readonly IPEndPoint _currentEndPoint; + private TcpClient _RtspServerClient; + + private uint _commandCounter = 0; /// - /// TCP Connection for Rtsp + /// Initializes a new instance of the class. /// - public class RtspTcpTransport : IRtspTransport, IDisposable + /// The underlying TCP connection. + public RtspTcpTransport(TcpClient tcpConnection, NetworkCredential credentials) { - private readonly IPEndPoint _currentEndPoint; - private TcpClient _RtspServerClient; - - /// - /// Initializes a new instance of the class. - /// - /// The underlying TCP connection. - public RtspTcpTransport(TcpClient tcpConnection) - { - if (tcpConnection == null) - throw new ArgumentNullException(nameof(tcpConnection)); - Contract.EndContractBlock(); + if (tcpConnection == null) + throw new ArgumentNullException(nameof(tcpConnection)); + Contract.EndContractBlock(); - _currentEndPoint = (IPEndPoint)tcpConnection.Client.RemoteEndPoint; - _RtspServerClient = tcpConnection; - } + _currentEndPoint = (IPEndPoint)tcpConnection.Client.RemoteEndPoint; + _RtspServerClient = tcpConnection; + _credentials = credentials; + } - /// - /// Initializes a new instance of the class. - /// - /// A host. - /// A port number. - public RtspTcpTransport(string aHost, int aPortNumber) - : this(new TcpClient(aHost, aPortNumber)) - { - } + /// + /// Initializes a new instance of the class. + /// + /// A host. + /// A port number. + public RtspTcpTransport(Uri uri, NetworkCredential credentials) + : this(new TcpClient(uri.Host, uri.Port), credentials) + { + } - #region IRtspTransport Membres - /// - /// Gets the stream of the transport. - /// - /// A stream - public Stream GetStream() => _RtspServerClient.GetStream(); + #region IRtspTransport Membres - /// - /// Gets the remote address. - /// - /// The remote address. - public string RemoteAddress => string.Format(CultureInfo.InvariantCulture, "{0}:{1}", _currentEndPoint.Address, _currentEndPoint.Port); + /// + /// Gets the stream of the transport. + /// + /// A stream + public Stream GetStream() => _RtspServerClient.GetStream(); - /// - /// Closes this instance. - /// - public void Close() - { - Dispose(true); - } + /// + /// Gets the remote address. + /// + /// The remote address. + public string RemoteAddress => string.Format(CultureInfo.InvariantCulture, "{0}:{1}", _currentEndPoint.Address, _currentEndPoint.Port); - /// - /// Gets a value indicating whether this is connected. - /// - /// true if connected; otherwise, false. - public bool Connected => _RtspServerClient.Client != null && _RtspServerClient.Connected; - - /// - /// Reconnect this instance. - /// Must do nothing if already connected. - /// - /// Error during socket - public void Reconnect() - { - if (Connected) - return; - _RtspServerClient = new TcpClient(); - _RtspServerClient.Connect(_currentEndPoint); - } + public uint CommandCounter => ++_commandCounter; - #endregion + /// + /// Closes this instance. + /// + public void Close() => Dispose(true); - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + /// + /// Gets a value indicating whether this is connected. + /// + /// true if connected; otherwise, false. + public bool Connected => _RtspServerClient.Client != null && _RtspServerClient.Connected; + + /// + /// Reconnect this instance. + /// Must do nothing if already connected. + /// + /// Error during socket + public void Reconnect() + { + if (Connected) { return; } + + _commandCounter = 0; + _RtspServerClient = new TcpClient(); + _RtspServerClient.Connect(_currentEndPoint); + } + + #endregion - protected virtual void Dispose(bool disposing) + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - _RtspServerClient.Close(); - /* // free managed resources - if (managedResource != null) - { - managedResource.Dispose(); - managedResource = null; - }*/ - } + _RtspServerClient.Close(); } } -} +} \ No newline at end of file diff --git a/RTSP/Rtp/AACPayload.cs b/RTSP/Rtp/AACPayload.cs index 52b52b5..617316e 100644 --- a/RTSP/Rtp/AACPayload.cs +++ b/RTSP/Rtp/AACPayload.cs @@ -1,144 +1,145 @@ -using System; +using Rtsp.Utils; +using System; using System.Collections.Generic; -namespace Rtsp.Rtp -{ - // This class handles the AAC-hbd (High Bitrate) Payload - // It has methods to process the RTP Payload +namespace Rtsp.Rtp; - // (c) 2018 Roger Hardiman, RJH Technical Consultancy Ltd +// This class handles the AAC-hbd (High Bitrate) Payload +// It has methods to process the RTP Payload +// (c) 2018 Roger Hardiman, RJH Technical Consultancy Ltd - /* - RFC 3640 - 3.3.6. High Bit-rate AAC - This mode is signaled by mode=AAC-hbr.This mode supports the - transportation of variable size AAC frames.In one RTP packet, - either one or more complete AAC frames are carried, or a single - fragment of an AAC frame is carried.In this mode, the AAC frames - are allowed to be interleaved and hence receivers MUST support de- - interleaving.The maximum size of an AAC frame in this mode is 8191 - octets. +/* +RFC 3640 +3.3.6. High Bit-rate AAC - In this mode, the RTP payload consists of the AU Header Section, - followed by either one AAC frame, several concatenated AAC frames or - one fragmented AAC frame.The Auxiliary Section MUST be empty. For - each AAC frame contained in the payload, there MUST be an AU-header - in the AU Header Section to provide: +This mode is signaled by mode=AAC-hbr.This mode supports the +transportation of variable size AAC frames.In one RTP packet, +either one or more complete AAC frames are carried, or a single +fragment of an AAC frame is carried.In this mode, the AAC frames +are allowed to be interleaved and hence receivers MUST support de- +interleaving.The maximum size of an AAC frame in this mode is 8191 +octets. - a) the size of each AAC frame in the payload and +In this mode, the RTP payload consists of the AU Header Section, +followed by either one AAC frame, several concatenated AAC frames or +one fragmented AAC frame.The Auxiliary Section MUST be empty. For +each AAC frame contained in the payload, there MUST be an AU-header +in the AU Header Section to provide: - b) index information for computing the sequence(and hence timing) of - each AAC frame. +a) the size of each AAC frame in the payload and - To code the maximum size of an AAC frame requires 13 bits. - Therefore, in this configuration 13 bits are allocated to the AU- - size, and 3 bits to the AU-Index(-delta) field.Thus, each AU-header - has a size of 2 octets.Each AU-Index field MUST be coded with the - value 0. In the AU Header Section, the concatenated AU-headers MUST - be preceded by the 16-bit AU-headers-length field, as specified in - section 3.2.1. +b) index information for computing the sequence(and hence timing) of + each AAC frame. - In addition to the required MIME format parameters, the following - parameters MUST be present: sizeLength, indexLength, and - indexDeltaLength.AAC frames always have a fixed duration per Access - Unit; when interleaving in this mode, this specific duration MUST be - signaled by the MIME format parameter constantDuration.In addition, - the parameter maxDisplacement MUST be present when interleaving. + To code the maximum size of an AAC frame requires 13 bits. + Therefore, in this configuration 13 bits are allocated to the AU- + size, and 3 bits to the AU-Index(-delta) field.Thus, each AU-header + has a size of 2 octets.Each AU-Index field MUST be coded with the + value 0. In the AU Header Section, the concatenated AU-headers MUST + be preceded by the 16-bit AU-headers-length field, as specified in + section 3.2.1. - For example: + In addition to the required MIME format parameters, the following + parameters MUST be present: sizeLength, indexLength, and + indexDeltaLength.AAC frames always have a fixed duration per Access + Unit; when interleaving in this mode, this specific duration MUST be + signaled by the MIME format parameter constantDuration.In addition, + the parameter maxDisplacement MUST be present when interleaving. - m= audio 49230 RTP/AVP 96 - a= rtpmap:96 mpeg4-generic/48000/6 - a= fmtp:96 streamtype= 5; profile-level-id= 16; mode= AAC-hbr;config= 11B0; sizeLength= 13; indexLength= 3;indexDeltaLength= 3; constantDuration= 1024 +For example: - The hexadecimal value of the "config" parameter is the AudioSpecificConfig(), as defined in ISO/IEC 14496-3. - AudioSpecificConfig() specifies a 5.1 channel AAC stream with a sampling rate of 48 kHz.For the description of MIME parameters, see - section 4.1. + m= audio 49230 RTP/AVP 96 + a= rtpmap:96 mpeg4-generic/48000/6 + a= fmtp:96 streamtype= 5; profile-level-id= 16; mode= AAC-hbr;config= 11B0; sizeLength= 13; indexLength= 3;indexDeltaLength= 3; constantDuration= 1024 - */ +The hexadecimal value of the "config" parameter is the AudioSpecificConfig(), as defined in ISO/IEC 14496-3. +AudioSpecificConfig() specifies a 5.1 channel AAC stream with a sampling rate of 48 kHz.For the description of MIME parameters, see +section 4.1. +*/ - public class AACPayload : IPayloadProcessor - { - public int ObjectType { get; set; } = 0; - public int FrequencyIndex { get; set; } = 0; - public int SamplingFrequency { get; set; } = 0; - public int ChannelConfiguration { get; set; } = 0; - // Constructor - public AACPayload(string config_string) +public class AACPayload : IPayloadProcessor +{ + public uint ObjectType { get; set; } = 0; + public uint FrequencyIndex { get; set; } = 0; + public uint SamplingFrequency { get; set; } = 0; + public uint ChannelConfiguration { get; set; } = 0; + + // Constructor + public AACPayload(string config_string) + { + /*** + 5 bits: object type + if (object type == 31) + 6 bits + 32: object type + 4 bits: frequency index + if (frequency index == 15) + 24 bits: frequency + 4 bits: channel configuration + var bits: AOT Specific Config + ***/ + + // config is a string in hex eg 1490 or 1210 + // Read each ASCII character and add to a bit array + BitStream bs = new(); + bs.AddHexString(config_string); + + // Read 5 bits + ObjectType = bs.Read(5); + + // Read 4 bits + FrequencyIndex = bs.Read(4); + + if (FrequencyIndex == 15) { - /*** - 5 bits: object type - if (object type == 31) - 6 bits + 32: object type - 4 bits: frequency index - if (frequency index == 15) - 24 bits: frequency - 4 bits: channel configuration - var bits: AOT Specific Config - ***/ - - // config is a string in hex eg 1490 or 1210 - // Read each ASCII character and add to a bit array - BitStream bs = new(); - bs.AddHexString(config_string); - - // Read 5 bits - ObjectType = bs.Read(5); - - // Read 4 bits - FrequencyIndex = bs.Read(4); - - if (FrequencyIndex == 15) - { - // the Sampling Frequency is specified directly - SamplingFrequency = bs.Read(24); - } - - // Read 4 bits - ChannelConfiguration = bs.Read(4); + // the Sampling Frequency is specified directly + SamplingFrequency = bs.Read(24); } - public List> ProcessRTPPacket(RtpPacket packet) - { + // Read 4 bits + ChannelConfiguration = bs.Read(4); + } - // RTP Payload for MPEG4-GENERIC can consist of multple blocks. - // Each block has 3 parts - // Part 1 - Acesss Unit Header Length + Header - // Part 2 - Access Unit Auxiliary Data Length + Data (not used in AAC High Bitrate) - // Part 3 - Access Unit Audio Data + public List> ProcessRTPPacket(RtpPacket packet) + { - // The rest of the RTP packet is the AMR data - List> audio_data = new(); + // RTP Payload for MPEG4-GENERIC can consist of multple blocks. + // Each block has 3 parts + // Part 1 - Acesss Unit Header Length + Header + // Part 2 - Access Unit Auxiliary Data Length + Data (not used in AAC High Bitrate) + // Part 3 - Access Unit Audio Data - int position = 0; - var rtpPayloadMemory = packet.Payload; - var rtp_payload = packet.Payload.Span; + // The rest of the RTP packet is the AMR data + List> audio_data = new(256); - // 2 bytes for AU Header Length, 2 bytes of AU Header payload - while (position + 4 <= packet.PayloadSize) - { - // Get Size of the AU Header - int au_headers_length_bits = (rtp_payload[position] << 8) + (rtp_payload[position + 1] << 0); // 16 bits - int au_headers_length = (int)Math.Ceiling(au_headers_length_bits / 8.0); - position += 2; + int position = 0; + var rtpPayloadMemory = packet.Payload; + var rtp_payload = packet.Payload.Span; - // Examine the AU Header. Get the size of the AAC data - int aac_frame_size = (rtp_payload[position] << 8) + (rtp_payload[position + 1] << 0) >> 3; // 13 bits - int aac_index_delta = rtp_payload[position + 1] & 0x03; // 3 bits - position += au_headers_length; + while (true) + { + if (position + 4 > packet.PayloadSize) break; // 2 bytes for AU Header Length, 2 bytes of AU Header payload - // extract the AAC block - if (position + aac_frame_size > rtp_payload.Length) break; // not enough data to copy + // Get Size of the AU Header + int au_headers_length_bits = (rtp_payload[position] << 8) + (rtp_payload[position + 1] << 0); // 16 bits + int au_headers_length = (int)Math.Ceiling(au_headers_length_bits / 8.0); + position += 2; - audio_data.Add(rtpPayloadMemory[position..(position + aac_frame_size)]); - position += aac_frame_size; - } + // Examine the AU Header. Get the size of the AAC data + int aac_frame_size = (rtp_payload[position] << 8) + (rtp_payload[position + 1] << 0) >> 3; // 13 bits + int aac_index_delta = rtp_payload[position + 1] & 0x03; // 3 bits + position += au_headers_length; - return audio_data; + // extract the AAC block + if (position + aac_frame_size > rtp_payload.Length) break; // not enough data to copy + + audio_data.Add(rtpPayloadMemory[position..(position + aac_frame_size)]); + position += aac_frame_size; } + + return audio_data; } -} +} \ No newline at end of file diff --git a/RTSP/Rtp/JPEGPayload.cs b/RTSP/Rtp/JPEGPayload.cs index bfaed3d..35ccd76 100644 --- a/RTSP/Rtp/JPEGPayload.cs +++ b/RTSP/Rtp/JPEGPayload.cs @@ -1,4 +1,5 @@ -using System; +using Rtsp.Utils; +using System; using System.Collections.Generic; using System.IO; diff --git a/RTSP/RtspConstants.cs b/RTSP/RtspConstants.cs new file mode 100644 index 0000000..dced1b0 --- /dev/null +++ b/RTSP/RtspConstants.cs @@ -0,0 +1,11 @@ +namespace Rtsp; + +static class RtspConstants +{ + public const int DefaultHttpPort = 80; + public const int DefaultRtspPort = 554; + public static readonly byte[] RtspProtocolNameBytes = [(byte)'R', (byte)'T', (byte)'S', (byte)'P']; + public const int MaxResponseHeadersSize = 8 * 1024; + public static readonly byte[] DoubleCrlfBytes = [(byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n']; + public const int UdpReceiveBufferSize = 2048; +} \ No newline at end of file diff --git a/RTSP/RtspHttpTransport.cs b/RTSP/RtspHttpTransport.cs new file mode 100644 index 0000000..4123218 --- /dev/null +++ b/RTSP/RtspHttpTransport.cs @@ -0,0 +1,199 @@ +using Rtsp.Utils; +using System.Buffers; +using System.Collections.Specialized; +using System.IO; +using System.Net.Http; +using System.Net.Sockets; +using System.Net; +using System.Text; +using System; + +namespace Rtsp; + +public class RtspHttpTransport : IRtspTransport +{ + private readonly NetworkCredential _credentials; + private readonly Uri _uri; + + private Socket? _streamDataClient; + private Socket? _commandsClient; + + private Stream _dataNetworkStream = null!; + + private Authentication? _authentication; + + + private uint _commandCounter = 0; + private string _sessionCookie = string.Empty; + + public RtspHttpTransport(Uri uri, NetworkCredential credentials) + { + _credentials = credentials; + _uri = uri; + + Reconnect(); + } + + public string RemoteAddress => $"{_uri}"; + public bool Connected => _streamDataClient != null && _streamDataClient.Connected; + + public uint CommandCounter => ++_commandCounter; + + public void Close() + { + _streamDataClient?.Close(); + _commandsClient?.Close(); + } + + public Stream GetStream() + { + if (_streamDataClient == null || !_streamDataClient.Connected) + throw new InvalidOperationException("Client is not connected"); + + return _dataNetworkStream; + } + + public void Reconnect() + { + if (Connected) { return; } + + _commandCounter = 0; + _sessionCookie = Guid.NewGuid().ToString("N")[..10]; + _streamDataClient = Utils.NetworkClientFactory.CreateTcpClient(); + + int httpPort = _uri.Port != -1 ? _uri.Port : 80; + _streamDataClient.Connect(_uri.Host, httpPort); + _dataNetworkStream = new NetworkStream(_streamDataClient, false); + + string request = ComposeGetRequest(); + byte[] requestByte = Encoding.ASCII.GetBytes(request); + + _dataNetworkStream.Write(requestByte, 0, requestByte.Length); + + byte[] buffer = ArrayPool.Shared.Rent(RtspConstants.MaxResponseHeadersSize); + int read = ReadUntilEndOfHeaders(_dataNetworkStream, buffer, RtspConstants.MaxResponseHeadersSize); + + using MemoryStream ms = new(buffer, 0, read); + using StreamReader streamReader = new(ms, Encoding.ASCII); + + string responseLine = streamReader.ReadLine(); + if (string.IsNullOrEmpty(responseLine)) { throw new HttpBadResponseException("Empty response"); } + + string[] tokens = responseLine.Split(' '); + if (tokens.Length != 3) { throw new HttpRequestException("Invalid first response line"); } + + HttpStatusCode statusCode = (HttpStatusCode)int.Parse(tokens[1]); + if (statusCode == HttpStatusCode.OK) { return; } + + if (statusCode == HttpStatusCode.Unauthorized && + !_credentials.IsEmpty() && + _authentication == null) + { + NameValueCollection headers = HeadersParser.ParseHeaders(streamReader); + string authenticateHeader = headers.Get(WellKnownHeaders.WwwAuthenticate); + + if (string.IsNullOrEmpty(authenticateHeader)) + throw new HttpBadResponseCodeException(statusCode); + + _authentication = Authentication.Create(_credentials, authenticateHeader); + + _streamDataClient.Dispose(); + + Reconnect(); + return; + } + + throw new HttpBadResponseCodeException(statusCode); + } + + public void Write(byte[] buffer, int offset, int count) + { + using (_commandsClient = NetworkClientFactory.CreateTcpClient()) + { + int httpPort = _uri.Port != -1 ? _uri.Port : 80; + + _commandsClient.Connect(_uri.Host, httpPort); + + string base64CodedCommandString = Convert.ToBase64String(buffer, offset, count); + byte[] base64CommandBytes = Encoding.ASCII.GetBytes(base64CodedCommandString); + + string request = ComposePostRequest(base64CommandBytes); + byte[] requestBytes = Encoding.ASCII.GetBytes(request); + + ArraySegment[] sendList = [new(requestBytes), new(base64CommandBytes)]; + + _commandsClient.Send(sendList, SocketFlags.None); + } + } + + private string ComposeGetRequest() + { + //string authorizationHeader = GetAuthorizationHeader(CommandCounter, "GET", []); + + return $"GET {_uri.PathAndQuery} HTTP/1.0\r\n" + + $"x-sessioncookie: {_sessionCookie}\r\n\r\n"; + + } + + private string ComposePostRequest(byte[] commandBytes) + { + //string authorizationHeader = GetAuthorizationHeader(CommandCounter, "POST", commandBytes); + + return $"POST {_uri.PathAndQuery} HTTP/1.0\r\n" + + $"x-sessioncookie: {_sessionCookie}\r\n" + + "Content-Type: application/x-rtsp-tunnelled\r\n" + + $"Content-Length: {commandBytes.Length}\r\n\r\n"; + } + + private string GetAuthorizationHeader(uint counter, string method, byte[] requestBytes) + { + string authorizationHeader; + + if (_authentication != null) + { + string headerValue = _authentication.GetResponse(counter, _uri.PathAndQuery, method, requestBytes); + authorizationHeader = $"Authorization: {headerValue}\r\n"; + } + else + { + authorizationHeader = string.Empty; + } + + return authorizationHeader; + } + + private static int ReadUntilEndOfHeaders(Stream stream, byte[] buffer, int length) + { + int offset = 0; + + int endOfHeaders; + int totalRead = 0; + + do + { + int count = length - totalRead; + + if (count == 0) + throw new InvalidOperationException($"Response is too large (> {length / 1024} KB)"); + + int read = stream.Read(buffer, offset, count); + + if (read == 0) + throw new EndOfStreamException("End of http stream"); + + totalRead += read; + + int startIndex = offset - (RtspConstants.DoubleCrlfBytes.Length - 1); + + if (startIndex < 0) + startIndex = 0; + + endOfHeaders = ArrayUtils.IndexOfBytes(buffer, RtspConstants.DoubleCrlfBytes, startIndex, + totalRead - startIndex); + + offset += read; + } while (endOfHeaders == -1); + + return totalRead; + } +} diff --git a/RTSP/UdpSocket.cs b/RTSP/UdpSocket.cs index dfb0ed2..b97994b 100644 --- a/RTSP/UdpSocket.cs +++ b/RTSP/UdpSocket.cs @@ -1,160 +1,164 @@ using Rtsp.Messages; using System; +using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; -namespace Rtsp +namespace Rtsp; +public class UDPSocket { - public class UDPSocket - { - protected readonly UdpClient dataSocket; - protected readonly UdpClient controlSocket; + protected readonly UdpClient dataSocket; + protected readonly UdpClient controlSocket; - private Task? _dataReadTask; - private Task? _controlReadTask; + private Thread? data_read_thread; + private Thread? control_read_thread; - public int DataPort { get; protected set; } - public int ControlPort { get; protected set; } + public int DataPort { get; protected set; } + public int ControlPort { get; protected set; } - public PortCouple Ports => new(DataPort, ControlPort); + public PortCouple Ports => new(DataPort, ControlPort); - /// - /// Initializes a new instance of the class. - /// Creates two new UDP sockets using the start and end Port range - /// - public UDPSocket(int startPort, int endPort) - { - // open a pair of UDP sockets - one for data (video or audio) and one for the status channel (RTCP messages) - DataPort = startPort; - ControlPort = startPort + 1; + /// + /// Initializes a new instance of the class. + /// Creates two new UDP sockets using the start and end Port range + /// + public UDPSocket(int startPort, int endPort) + { + // open a pair of UDP sockets - one for data (video or audio) and one for the status channel (RTCP messages) + DataPort = startPort; + ControlPort = startPort + 1; - bool ok = false; - while (!ok && (ControlPort < endPort)) + bool ok = false; + while (ok == false && (ControlPort < endPort)) + { + // Video/Audio port must be odd and command even (next one) + try { - // Video/Audio port must be odd and command even (next one) - try - { - dataSocket = new UdpClient(DataPort); - controlSocket = new UdpClient(ControlPort); - ok = true; - } - catch (SocketException) - { - // Fail to allocate port, try again - dataSocket?.Close(); - controlSocket?.Close(); - - // try next data or control port - DataPort += 2; - ControlPort += 2; - } - - if (ok) - { - dataSocket!.Client.ReceiveBufferSize = 100 * 1024; - dataSocket!.Client.SendBufferSize = 65535; // default is 8192. Make it as large as possible for large RTP packets which are not fragmented - - controlSocket!.Client.DontFragment = false; - } + dataSocket = new UdpClient(DataPort); + controlSocket = new UdpClient(ControlPort); + ok = true; } + catch (SocketException) + { + // Fail to allocate port, try again + dataSocket?.Close(); + controlSocket?.Close(); - if (dataSocket == null || controlSocket == null) + // try next data or control port + DataPort += 2; + ControlPort += 2; + } + + if (ok) { - throw new InvalidOperationException("UDP Forwader host was not initialized, can't continue"); + dataSocket!.Client.ReceiveBufferSize = 100 * 1024; + dataSocket!.Client.SendBufferSize = 65535; // default is 8192. Make it as large as possible for large RTP packets which are not fragmented + + controlSocket!.Client.DontFragment = false; + } } - protected UDPSocket(UdpClient dataSocket, UdpClient controlSocket) + if (dataSocket == null || controlSocket == null) { - this.dataSocket = dataSocket; - this.controlSocket = controlSocket; + throw new InvalidOperationException("UDP Forwader host was not initialized, can't continue"); } + } - /// - /// Starts this instance. - /// - public void Start() - { - if (_dataReadTask != null) - { - throw new InvalidOperationException("Forwarder was stopped, can't restart it"); - } - _dataReadTask = Task.Factory.StartNew(async () => await DoWorkerJobAsync(dataSocket, OnDataReceived), TaskCreationOptions.LongRunning); - _controlReadTask = Task.Factory.StartNew(async () => await DoWorkerJobAsync(controlSocket, OnControlReceived), TaskCreationOptions.LongRunning); - } + protected UDPSocket(UdpClient dataSocket, UdpClient controlSocket) + { + this.dataSocket = dataSocket; + this.controlSocket = controlSocket; + } - /// - /// Stops this instance. - /// - public virtual void Stop() + /// + /// Starts this instance. + /// + public void Start() + { + if (data_read_thread != null) { - dataSocket.Close(); - controlSocket.Close(); + throw new InvalidOperationException("Forwarder was stopped, can't restart it"); } - /// - /// Occurs when data is received. - /// - public event EventHandler? DataReceived; + data_read_thread = new Thread(() => DoWorkerJob(dataSocket, DataPort, OnDataReceived)) + { + Name = "DataPort " + DataPort + }; + data_read_thread.Start(); - /// - /// Raises the event. - /// - protected void OnDataReceived(RtspDataEventArgs rtspDataEventArgs) + control_read_thread = new Thread(() => DoWorkerJob(controlSocket, ControlPort, OnControlReceived)) { - DataReceived?.Invoke(this, rtspDataEventArgs); - } + Name = "ControlPort " + ControlPort + }; + control_read_thread.Start(); + } - /// - /// Occurs when control is received. - /// - public event EventHandler? ControlReceived; + /// + /// Stops this instance. + /// + public virtual void Stop() + { + dataSocket.Close(); + controlSocket.Close(); + } - /// - /// Raises the event. - /// - protected void OnControlReceived(RtspDataEventArgs rtspDataEventArgs) - { - ControlReceived?.Invoke(this, rtspDataEventArgs); - } + /// + /// Occurs when message is received. + /// + public event EventHandler? DataReceived; + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected void OnDataReceived(RtspDataEventArgs rtspDataEventArgs) => DataReceived?.Invoke(this, rtspDataEventArgs); + + /// + /// Occurs when control is received. + /// + public event EventHandler? ControlReceived; + + /// + /// Raises the event. + /// + protected void OnControlReceived(RtspDataEventArgs rtspDataEventArgs) => ControlReceived?.Invoke(this, rtspDataEventArgs); + + /// + /// Does the video job. + /// + private void DoWorkerJob(UdpClient socket, int data_port, Action handler) + { - /// - /// Does the video job. - /// - private static async Task DoWorkerJobAsync(UdpClient socket, Action handler) + IPEndPoint ipEndPoint = new(IPAddress.Any, data_port); + try { - try - { - // loop until we get an exception eg the socket closed - while (true) - { - var data = await socket.ReceiveAsync(); - handler(new RtspDataEventArgs(data.Buffer)); - } - } - catch (ObjectDisposedException) - { - } - catch (SocketException) + // loop until we get an exception eg the socket closed + while (true) { + byte[] frame = socket.Receive(ref ipEndPoint); + handler(new(frame, frame.Length)); + } } - - /// - /// Write to the RTP Data Port - /// - public void WriteToDataPort(byte[] data, string hostname, int port) + catch (ObjectDisposedException) { - dataSocket.Send(data, data.Length, hostname, port); } - - /// - /// Write to the RTP Control Port - /// - public void WriteToControlPort(byte[] data, string hostname, int port) + catch (SocketException) { - dataSocket.Send(data, data.Length, hostname, port); } } -} + + /// + /// Write to the RTP Data Port + /// + public void WriteToDataPort(byte[] data, string hostname, int port) => dataSocket.Send(data, data.Length, hostname, port); + + /// + /// Write to the RTP Control Port + /// + public void WriteToControlPort(byte[] data, string hostname, int port) => dataSocket.Send(data, data.Length, hostname, port); + +} \ No newline at end of file diff --git a/RTSP/Utils/ArrayUtils.cs b/RTSP/Utils/ArrayUtils.cs new file mode 100644 index 0000000..dfdabcc --- /dev/null +++ b/RTSP/Utils/ArrayUtils.cs @@ -0,0 +1,78 @@ +namespace Rtsp.Utils; + +internal static class ArrayUtils +{ + public static bool IsBytesEquals(byte[] bytes1, int offset1, int count1, byte[] bytes2, int offset2, int count2) + { + if (count1 != count2) { return false; } + + for (int i = 0; i < count1; i++) + { + if (bytes1[offset1 + i] != bytes2[offset2 + i]) + { + return false; + } + } + return true; + } + + public static bool StartsWith(byte[] array, int offset, int count, byte[] pattern) + { + int patternLength = pattern.Length; + + if (count < patternLength) { return false; } + + for (int i = 0; i < patternLength; i++, offset++) + { + if (array[offset] != pattern[i]) + { + return false; + } + } + + return true; + } + + public static bool EndsWith(byte[] array, int offset, int count, byte[] pattern) + { + int patternLength = pattern.Length; + + if (count < patternLength) { return false; } + + offset = offset + count - patternLength; + + for (int i = 0; i < patternLength; i++, offset++) + { + if (array[offset] != pattern[i]) + { + return false; + } + } + return true; + } + + public static int IndexOfBytes(byte[] array, byte[] pattern, int startIndex, int count) + { + int patternLength = pattern.Length; + + if (count < patternLength) { return -1; } + + int endIndex = startIndex + count; + + int foundIndex = 0; + for (; startIndex < endIndex; startIndex++) + { + if (array[startIndex] != pattern[foundIndex]) + { + startIndex -= foundIndex; + foundIndex = 0; + } + else if (++foundIndex == patternLength) + { + return startIndex - foundIndex + 1; + } + } + + return -1; + } +} diff --git a/RTSP/Utils/BitStream.cs b/RTSP/Utils/BitStream.cs new file mode 100644 index 0000000..c61299e --- /dev/null +++ b/RTSP/Utils/BitStream.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +// (c) 2018 Roger Hardiman, RJH Technical Consultancy Ltd +// Simple class to Read and Write bits in a bit stream. +// Data is written to the end of the bit stream and the bit stream can be returned as a Byte Array +// Data can be read from the head of the bit stream +// Example +// bitstream.AddValue(0xA,4); // Write 4 bit value +// bitstream.AddValue(0xB,4); +// bitstream.AddValue(0xC,4); +// bitstream.AddValue(0xD,4); +// bitstream.ToArray() -> {0xAB, 0xCD} // Return Byte Array +// bitstream.Read(8) -> 0xAB // Read 8 bit value + +namespace Rtsp.Utils; + +// Very simple bitstream +public class BitStream +{ + + private List data = new(); // List only stores 0 or 1 (one 'bit' per List item) + + // Constructor + public BitStream() + { + } + + public void AddValue(int value, int num_bits) + { + // Add each bit to the List + for (int i = num_bits - 1; i >= 0; i--) + { + data.Add((byte)(value >> i & 0x01)); + } + } + + public void AddHexString(string hex_string) + { + char[] hex_chars = hex_string.ToUpper().ToCharArray(); + foreach (char c in hex_chars) + { + if (c.Equals('0')) AddValue(0, 4); + else if (c.Equals('1')) AddValue(1, 4); + else if (c.Equals('2')) AddValue(2, 4); + else if (c.Equals('3')) AddValue(3, 4); + else if (c.Equals('4')) AddValue(4, 4); + else if (c.Equals('5')) AddValue(5, 4); + else if (c.Equals('6')) AddValue(6, 4); + else if (c.Equals('7')) AddValue(7, 4); + else if (c.Equals('8')) AddValue(8, 4); + else if (c.Equals('9')) AddValue(9, 4); + else if (c.Equals('A')) AddValue(10, 4); + else if (c.Equals('B')) AddValue(11, 4); + else if (c.Equals('C')) AddValue(12, 4); + else if (c.Equals('D')) AddValue(13, 4); + else if (c.Equals('E')) AddValue(14, 4); + else if (c.Equals('F')) AddValue(15, 4); + } + } + + public uint Read(int num_bits) + { + // Read and remove items from the front of the list of bits + if (data.Count < num_bits) return 0; + uint result = 0; + for (int i = 0; i < num_bits; i++) + { + result <<= 1; + result += data[0]; + data.RemoveAt(0); + } + return result; + } + + public byte[] ToArray() + { + int num_bytes = (int)Math.Ceiling(data.Count / 8.0); + byte[] array = new byte[num_bytes]; + int ptr = 0; + int shift = 7; + for (int i = 0; i < data.Count; i++) + { + array[ptr] += (byte)(data[i] << shift); + if (shift == 0) + { + shift = 7; + ptr++; + } + else + { + shift--; + } + } + + return array; + } +} \ No newline at end of file diff --git a/RTSP/Utils/HeadersParser.cs b/RTSP/Utils/HeadersParser.cs new file mode 100644 index 0000000..3a6ce8b --- /dev/null +++ b/RTSP/Utils/HeadersParser.cs @@ -0,0 +1,27 @@ +using System.Collections.Specialized; +using System.IO; + +namespace Rtsp.Utils; +static class HeadersParser +{ + public static NameValueCollection ParseHeaders(StreamReader headersReader) + { + var headers = new NameValueCollection(); + + string header; + + while (!string.IsNullOrEmpty(header = headersReader.ReadLine())) + { + int colonPos = header.IndexOf(':'); + + if (colonPos == -1) { continue; } + + string key = header[..colonPos].Trim().ToUpperInvariant(); + string value = header[++colonPos..].Trim(); + + headers.Add(key, value); + } + + return headers; + } +} \ No newline at end of file diff --git a/RTSP/Utils/MD5.cs b/RTSP/Utils/MD5.cs new file mode 100644 index 0000000..bff1e1a --- /dev/null +++ b/RTSP/Utils/MD5.cs @@ -0,0 +1,263 @@ +using System; +using System.Text; + +namespace Rtsp.Utils +{ + static class MD5 + { + /// + /// Simple struct for the (a,b,c,d) which is used to compute the mesage digest. + /// + private struct ABCDStruct + { + public uint A; + public uint B; + public uint C; + public uint D; + } + + public static string GetHashHexValues(string value) + { + byte[] hashBytes = GetHash(value); + + return ToHexString(hashBytes); + } + + public static string GetHashHexValues(byte[] value) + { + byte[] hashBytes = GetHash(value); + + return ToHexString(hashBytes); + } + + public static byte[] GetHash(string input, Encoding encoding) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + + + var target = encoding.GetBytes(input); + return GetHash(target); + } + + public static byte[] GetHash(string input) + { + return GetHash(input, Encoding.UTF8); + } + + public static byte[] GetHash(byte[] input) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + //// Intitial values defined in RFC 1321 + var abcd = new ABCDStruct + { + A = 0x67452301, + B = 0xefcdab89, + C = 0x98badcfe, + D = 0x10325476 + }; + + //// We pass in the input array by block, the final block of data must be handled specialy for padding & length embeding + var startIndex = 0; + while (startIndex <= input.Length - 64) + { + GetHashBlock(input, ref abcd, startIndex); + startIndex += 64; + } + + //// The final data block. + return GetHashFinalBlock(input, startIndex, input.Length - startIndex, abcd, (long)input.Length * 8); + } + + private static string ToHexString(byte[] hashBytes, bool upperCase = false) + { + var sb = new StringBuilder(); + + string format = upperCase ? "X2" : "x2"; + + foreach (byte hashByte in hashBytes) + sb.Append(hashByte.ToString(format)); + + return sb.ToString(); + } + + private static byte[] GetHashFinalBlock(byte[] input, int ibStart, int cbSize, ABCDStruct abcd, long len) + { + var working = new byte[64]; + var length = BitConverter.GetBytes(len); + + //// Padding is a single bit 1, followed by the number of 0s required to make size congruent to 448 modulo 512. Step 1 of RFC 1321 + //// The CLR ensures that our buffer is 0-assigned, we don't need to explicitly set it. This is why it ends up being quicker to just + //// use a temporary array rather then doing in-place assignment (5% for small inputs) + Array.Copy(input, ibStart, working, 0, cbSize); + working[cbSize] = 0x80; + + //// We have enough room to store the length in this chunk + if (cbSize < 56) + { + Array.Copy(length, 0, working, 56, 8); + GetHashBlock(working, ref abcd, 0); + } + else //// We need an aditional chunk to store the length + { + GetHashBlock(working, ref abcd, 0); + //// Create an entirely new chunk due to the 0-assigned trick mentioned above, to avoid an extra function call clearing the array + working = new byte[64]; + Array.Copy(length, 0, working, 56, 8); + GetHashBlock(working, ref abcd, 0); + } + + var output = new byte[16]; + Array.Copy(BitConverter.GetBytes(abcd.A), 0, output, 0, 4); + Array.Copy(BitConverter.GetBytes(abcd.B), 0, output, 4, 4); + Array.Copy(BitConverter.GetBytes(abcd.C), 0, output, 8, 4); + Array.Copy(BitConverter.GetBytes(abcd.D), 0, output, 12, 4); + return output; + } + + //// Performs a single block transform of MD5 for a given set of abcd inputs + /* If implementing your own hashing framework, be sure to set the initial abcd correctly according to RFC 1321: + // A = 0x67452301; + // B = 0xefcdab89; + // C = 0x98badcfe; + // D = 0x10325476; + */ + private static void GetHashBlock(byte[] input, ref ABCDStruct abcdValue, int ibStart) + { + var temp = Converter(input, ibStart); + var a = abcdValue.A; + var b = abcdValue.B; + var c = abcdValue.C; + var d = abcdValue.D; + + a = R1(a, b, c, d, temp[0], 7, 0xd76aa478); + d = R1(d, a, b, c, temp[1], 12, 0xe8c7b756); + c = R1(c, d, a, b, temp[2], 17, 0x242070db); + b = R1(b, c, d, a, temp[3], 22, 0xc1bdceee); + a = R1(a, b, c, d, temp[4], 7, 0xf57c0faf); + d = R1(d, a, b, c, temp[5], 12, 0x4787c62a); + c = R1(c, d, a, b, temp[6], 17, 0xa8304613); + b = R1(b, c, d, a, temp[7], 22, 0xfd469501); + a = R1(a, b, c, d, temp[8], 7, 0x698098d8); + d = R1(d, a, b, c, temp[9], 12, 0x8b44f7af); + c = R1(c, d, a, b, temp[10], 17, 0xffff5bb1); + b = R1(b, c, d, a, temp[11], 22, 0x895cd7be); + a = R1(a, b, c, d, temp[12], 7, 0x6b901122); + d = R1(d, a, b, c, temp[13], 12, 0xfd987193); + c = R1(c, d, a, b, temp[14], 17, 0xa679438e); + b = R1(b, c, d, a, temp[15], 22, 0x49b40821); + + a = R2(a, b, c, d, temp[1], 5, 0xf61e2562); + d = R2(d, a, b, c, temp[6], 9, 0xc040b340); + c = R2(c, d, a, b, temp[11], 14, 0x265e5a51); + b = R2(b, c, d, a, temp[0], 20, 0xe9b6c7aa); + a = R2(a, b, c, d, temp[5], 5, 0xd62f105d); + d = R2(d, a, b, c, temp[10], 9, 0x02441453); + c = R2(c, d, a, b, temp[15], 14, 0xd8a1e681); + b = R2(b, c, d, a, temp[4], 20, 0xe7d3fbc8); + a = R2(a, b, c, d, temp[9], 5, 0x21e1cde6); + d = R2(d, a, b, c, temp[14], 9, 0xc33707d6); + c = R2(c, d, a, b, temp[3], 14, 0xf4d50d87); + b = R2(b, c, d, a, temp[8], 20, 0x455a14ed); + a = R2(a, b, c, d, temp[13], 5, 0xa9e3e905); + d = R2(d, a, b, c, temp[2], 9, 0xfcefa3f8); + c = R2(c, d, a, b, temp[7], 14, 0x676f02d9); + b = R2(b, c, d, a, temp[12], 20, 0x8d2a4c8a); + + a = R3(a, b, c, d, temp[5], 4, 0xfffa3942); + d = R3(d, a, b, c, temp[8], 11, 0x8771f681); + c = R3(c, d, a, b, temp[11], 16, 0x6d9d6122); + b = R3(b, c, d, a, temp[14], 23, 0xfde5380c); + a = R3(a, b, c, d, temp[1], 4, 0xa4beea44); + d = R3(d, a, b, c, temp[4], 11, 0x4bdecfa9); + c = R3(c, d, a, b, temp[7], 16, 0xf6bb4b60); + b = R3(b, c, d, a, temp[10], 23, 0xbebfbc70); + a = R3(a, b, c, d, temp[13], 4, 0x289b7ec6); + d = R3(d, a, b, c, temp[0], 11, 0xeaa127fa); + c = R3(c, d, a, b, temp[3], 16, 0xd4ef3085); + b = R3(b, c, d, a, temp[6], 23, 0x04881d05); + a = R3(a, b, c, d, temp[9], 4, 0xd9d4d039); + d = R3(d, a, b, c, temp[12], 11, 0xe6db99e5); + c = R3(c, d, a, b, temp[15], 16, 0x1fa27cf8); + b = R3(b, c, d, a, temp[2], 23, 0xc4ac5665); + + a = R4(a, b, c, d, temp[0], 6, 0xf4292244); + d = R4(d, a, b, c, temp[7], 10, 0x432aff97); + c = R4(c, d, a, b, temp[14], 15, 0xab9423a7); + b = R4(b, c, d, a, temp[5], 21, 0xfc93a039); + a = R4(a, b, c, d, temp[12], 6, 0x655b59c3); + d = R4(d, a, b, c, temp[3], 10, 0x8f0ccc92); + c = R4(c, d, a, b, temp[10], 15, 0xffeff47d); + b = R4(b, c, d, a, temp[1], 21, 0x85845dd1); + a = R4(a, b, c, d, temp[8], 6, 0x6fa87e4f); + d = R4(d, a, b, c, temp[15], 10, 0xfe2ce6e0); + c = R4(c, d, a, b, temp[6], 15, 0xa3014314); + b = R4(b, c, d, a, temp[13], 21, 0x4e0811a1); + a = R4(a, b, c, d, temp[4], 6, 0xf7537e82); + d = R4(d, a, b, c, temp[11], 10, 0xbd3af235); + c = R4(c, d, a, b, temp[2], 15, 0x2ad7d2bb); + b = R4(b, c, d, a, temp[9], 21, 0xeb86d391); + + abcdValue.A = unchecked(a + abcdValue.A); + abcdValue.B = unchecked(b + abcdValue.B); + abcdValue.C = unchecked(c + abcdValue.C); + abcdValue.D = unchecked(d + abcdValue.D); + } + + //// Manually unrolling these equations nets us a 20% performance improvement + private static uint R1(uint a, uint b, uint c, uint d, uint x, int s, uint t) + { + //// (b + Lsr((a + F(b, c, d) + x + t), s)) + //// F(x, y, z) ((x & y) | ((x ^ 0xFFFFFFFF) & z)) + return unchecked(b + Lsr(a + ((b & c) | ((b ^ 0xFFFFFFFF) & d)) + x + t, s)); + } + + private static uint R2(uint a, uint b, uint c, uint d, uint x, int s, uint t) + { + //// (b + Lsr((a + G(b, c, d) + x + t), s)) + //// G(x, y, z) ((x & z) | (y & (z ^ 0xFFFFFFFF))) + return unchecked(b + Lsr(a + ((b & d) | (c & (d ^ 0xFFFFFFFF))) + x + t, s)); + } + + private static uint R3(uint a, uint b, uint c, uint d, uint x, int s, uint t) + { + //// (b + Lsr((a + H(b, c, d) + k + i), s)) + //// H(x, y, z) (x ^ y ^ z) + return unchecked(b + Lsr(a + (b ^ c ^ d) + x + t, s)); + } + + private static uint R4(uint a, uint b, uint c, uint d, uint x, int s, uint t) + { + //// (b + Lsr((a + I(b, c, d) + k + i), s)) + //// I(x, y, z) (y ^ (x | (z ^ 0xFFFFFFFF))) + return unchecked(b + Lsr(a + (c ^ (b | (d ^ 0xFFFFFFFF))) + x + t, s)); + } + + //// Implementation of left rotate + //// s is an int instead of a uint becuase the CLR requires the argument passed to >>/<< is of + //// type int. Doing the demoting inside this function would add overhead. + private static uint Lsr(uint i, int s) + { + return (i << s) | (i >> (32 - s)); + } + + //// Convert input array into array of UInts + private static uint[] Converter(byte[] input, int ibStart) + { + var result = new uint[16]; + for (var i = 0; i < 16; i++) + { + result[i] = input[ibStart + i * 4]; + result[i] += (uint)input[ibStart + i * 4 + 1] << 8; + result[i] += (uint)input[ibStart + i * 4 + 2] << 16; + result[i] += (uint)input[ibStart + i * 4 + 3] << 24; + } + + return result; + } + } +} diff --git a/RTSP/Utils/NetworkClientFactory.cs b/RTSP/Utils/NetworkClientFactory.cs new file mode 100644 index 0000000..01616aa --- /dev/null +++ b/RTSP/Utils/NetworkClientFactory.cs @@ -0,0 +1,27 @@ +using System.Net.Sockets; + +namespace Rtsp.Utils; +internal static class NetworkClientFactory +{ + private const int TcpReceiveBufferDefaultSize = 64 * 1024; // 64 kb + private const int UdpReceiveBufferDefaultSize = 128 * 1024; // 128 kb + private const int SIO_UDP_CONNRESET = -1744830452; + private static readonly byte[] EmptyOptionInValue = [0, 0, 0, 0]; + + public static Socket CreateTcpClient() => new(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp) + { + ReceiveBufferSize = TcpReceiveBufferDefaultSize, + DualMode = true, + NoDelay = true, + }; + public static Socket CreateUdpClient() + { + Socket socket = new(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp) + { + ReceiveBufferSize = UdpReceiveBufferDefaultSize, + DualMode = true, + }; + socket.IOControl((IOControlCode)SIO_UDP_CONNRESET, EmptyOptionInValue, null); + return socket; + } +} diff --git a/RTSP/Utils/NetworkCredentialExtensions.cs b/RTSP/Utils/NetworkCredentialExtensions.cs new file mode 100644 index 0000000..a5ae6e8 --- /dev/null +++ b/RTSP/Utils/NetworkCredentialExtensions.cs @@ -0,0 +1,13 @@ +using System.Net; + +namespace Rtsp.Utils; +public static class NetworkCredentialExtensions +{ + public static bool IsEmpty(this NetworkCredential networkCredential) + { + if (string.IsNullOrEmpty(networkCredential.UserName) || networkCredential.Password == null) + return true; + + return false; + } +} \ No newline at end of file diff --git a/RTSP/RTSPUtils.cs b/RTSP/Utils/RTSPUtils.cs similarity index 94% rename from RTSP/RTSPUtils.cs rename to RTSP/Utils/RTSPUtils.cs index 4afaf2b..0f3229a 100644 --- a/RTSP/RTSPUtils.cs +++ b/RTSP/Utils/RTSPUtils.cs @@ -1,6 +1,6 @@ using System; -namespace Rtsp +namespace Rtsp.Utils { public static class RtspUtils { diff --git a/RTSP/Utils/WellKnownHeaders.cs b/RTSP/Utils/WellKnownHeaders.cs new file mode 100644 index 0000000..4bf9b3d --- /dev/null +++ b/RTSP/Utils/WellKnownHeaders.cs @@ -0,0 +1,11 @@ +namespace Rtsp.Utils; + +static class WellKnownHeaders +{ + public static readonly string ContentLength = "CONTENT-LENGTH"; + public static readonly string WwwAuthenticate = "WWW-AUTHENTICATE"; + public static readonly string ContentBase = "CONTENT-BASE"; + public static readonly string Public = "PUBLIC"; + public static readonly string Session = "SESSION"; + public static readonly string Transport = "TRANSPORT"; +} \ No newline at end of file diff --git a/RtspCameraExample/Program.cs b/RtspCameraExample/Program.cs index ba4c77a..99147d7 100644 --- a/RtspCameraExample/Program.cs +++ b/RtspCameraExample/Program.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Net; using System.Threading; namespace RtspCameraExample @@ -28,12 +29,13 @@ static void Main(string[] args) .AddConsole(); }); var demo = new Demo(); - + } class Demo { + NetworkCredential networkCredential; RtspServer rtspServer = null; SimpleH264Encoder h264_encoder = null; SimpleG711Encoder ulaw_encoder = null; @@ -45,6 +47,7 @@ class Demo string username = "user"; // or use NUL if there is no username string password = "password"; // or use NUL if there is no password + uint width = 192; uint height = 128; uint fps = 25; @@ -60,7 +63,8 @@ public Demo() ///////////////////////////////////////// // Step 1 - Start the RTSP Server ///////////////////////////////////////// - rtspServer = new RtspServer(port, username, password, loggerFactory); + networkCredential = new(username, password); + rtspServer = new RtspServer(port, networkCredential, loggerFactory); try { rtspServer.StartListen(); diff --git a/RtspCameraExample/RtspServer.cs b/RtspCameraExample/RtspServer.cs index ec560a8..6027f90 100644 --- a/RtspCameraExample/RtspServer.cs +++ b/RtspCameraExample/RtspServer.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Rtsp; +using Rtsp.Utils; using System; using System.Collections.Generic; using System.Diagnostics.Contracts; @@ -21,6 +22,7 @@ public class RtspServer : IDisposable const uint global_ssrc = 0x4321FADE; // 8 hex digits + private readonly NetworkCredential _credential; private readonly TcpListener _RTSPServerListener; private readonly ILoggerFactory _loggerFactory; private ManualResetEvent _Stopping; @@ -43,17 +45,19 @@ public class RtspServer : IDisposable /// A numero port. /// username. /// password. - public RtspServer(int portNumber, String username, String password, ILoggerFactory loggerFactory) + public RtspServer(int portNumber, NetworkCredential credential, ILoggerFactory loggerFactory) { if (portNumber < System.Net.IPEndPoint.MinPort || portNumber > System.Net.IPEndPoint.MaxPort) throw new ArgumentOutOfRangeException("aPortNumber", portNumber, "Port number must be between System.Net.IPEndPoint.MinPort and System.Net.IPEndPoint.MaxPort"); Contract.EndContractBlock(); - if (String.IsNullOrEmpty(username) == false - && String.IsNullOrEmpty(password) == false) + + _credential = credential; + + if (!credential.IsEmpty()) { - String realm = "SharpRTSPServer"; - auth = new AuthenticationDigest(username, password, realm); + string realm = "SharpRTSPServer"; + auth = new AuthenticationDigest(credential, realm, string.Empty, string.Empty); } else { @@ -92,7 +96,7 @@ private void AcceptConnection() Console.WriteLine("Connection from " + oneClient.Client.RemoteEndPoint.ToString()); // Hand the incoming TCP connection over to the RTSP classes - var rtsp_socket = new RtspTcpTransport(oneClient); + var rtsp_socket = new RtspTcpTransport(oneClient, _credential); RtspListener newListener = new RtspListener(rtsp_socket, _loggerFactory.CreateLogger()); newListener.MessageReceived += RTSP_Message_Received; //RTSPDispatcher.Instance.AddListener(newListener); @@ -179,7 +183,7 @@ private void RTSP_Message_Received(object sender, RtspChunkEventArgs e) { // Send a 401 Authentication Failed reply, then close the RTSP Socket Rtsp.Messages.RtspResponse authorization_response = (e.Message as Rtsp.Messages.RtspRequest).CreateResponse(); - authorization_response.AddHeader("WWW-Authenticate: " + auth.GetHeader()); + authorization_response.AddHeader("WWW-Authenticate: " + auth.GetResponse((uint)message.CSeq, listener.RemoteAdress, message.Method, message.Data)); authorization_response.ReturnCode = 401; listener.SendMessage(authorization_response); @@ -203,7 +207,7 @@ private void RTSP_Message_Received(object sender, RtspChunkEventArgs e) // Send a 401 Authentication Failed with extra info in WWW-Authenticate // to tell the Client if we are using Basic or Digest Authentication Rtsp.Messages.RtspResponse authorization_response = (e.Message as Rtsp.Messages.RtspRequest).CreateResponse(); - authorization_response.AddHeader("WWW-Authenticate: " + auth.GetHeader()); // 'Basic' or 'Digest' + authorization_response.AddHeader("WWW-Authenticate: " + auth.GetResponse((uint)message.CSeq, listener.RemoteAdress, message.Method, message.Data)); // 'Basic' or 'Digest' authorization_response.ReturnCode = 401; listener.SendMessage(authorization_response); return; @@ -625,7 +629,7 @@ public void FeedInRawNAL(uint timestamp_ms, List nal_array) checkTimeouts(out current_rtsp_count, out current_rtsp_play_count); - // Console.WriteLine(current_rtsp_count + " RTSP clients connected. " + current_rtsp_play_count + " RTSP clients in PLAY mode"); + // Console.WriteLine(current_rtsp_count + " RTSP clients connected. " + current_rtsp_play_count + " RTSP clients in PLAY mode"); if (current_rtsp_play_count == 0) return; @@ -765,8 +769,8 @@ public void FeedInRawNAL(uint timestamp_ms, List nal_array) // Go through each RTSP connection and output the NAL on the Video Session foreach (RTSPConnection connection in rtspConnectionList.ToArray()) // ToArray makes a temp copy of the list. - // This lets us delete items in the foreach - // eg when there is Write Error + // This lets us delete items in the foreach + // eg when there is Write Error { // Only process Sessions in Play Mode if (connection.play == false) continue; @@ -1026,8 +1030,8 @@ public void FeedInAudioPacket(uint timestamp_ms, byte[] audio_packet) // Go through each RTSP connection and output the NAL on the Video Session foreach (RTSPConnection connection in rtspConnectionList.ToArray()) // ToArray makes a temp copy of the list. - // This lets us delete items in the foreach - // eg when there is Write Error + // This lets us delete items in the foreach + // eg when there is Write Error { // Only process Sessions in Play Mode if (connection.play == false) continue; diff --git a/RtspClientExample/Program.cs b/RtspClientExample/Program.cs index c4bec8c..e5a97f3 100644 --- a/RtspClientExample/Program.cs +++ b/RtspClientExample/Program.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Rtsp.Utils; using System; using System.IO; using System.Threading; @@ -67,7 +68,7 @@ static void Main(string[] args) // The SPS/PPS comes from the SDP data // or it is the first SPS/PPS from the H264 video stream - client.ReceivedSpsPps += (sps, pps) => + client.ReceivedSpsPps += (sender, e) => { h264 = true; if (fs_v == null) @@ -75,13 +76,13 @@ static void Main(string[] args) string filename = "rtsp_capture_" + now + ".264"; fs_v = new FileStream(filename, FileMode.Create); } - WriteNalToFile(fs_v, sps); - WriteNalToFile(fs_v, pps); + WriteNalToFile(fs_v, e.Sps); + WriteNalToFile(fs_v, e.Pps); fs_v.Flush(true); }; - client.ReceivedVpsSpsPps += (vps, sps, pps) => + client.ReceivedVpsSpsPps += (sender, e) => { h265 = true; if (fs_v == null) @@ -90,9 +91,9 @@ static void Main(string[] args) fs_v = new FileStream(filename, FileMode.Create); } - WriteNalToFile(fs_v, vps); - WriteNalToFile(fs_v, sps); - WriteNalToFile(fs_v, pps); + WriteNalToFile(fs_v, e.Vps); + WriteNalToFile(fs_v, e.Sps); + WriteNalToFile(fs_v, e.Pps); fs_v.Flush(true); }; @@ -100,11 +101,11 @@ static void Main(string[] args) // Video NALs. May also include the SPS and PPS in-band for H264 - client.ReceivedNALs += (nalUnits) => + client.ReceivedNALs += (sender, e) => { if (fs_v != null) { - foreach (var nalUnitMem in nalUnits) + foreach (var nalUnitMem in e.Data) { var nalUnit = nalUnitMem.Span; // Output some H264 stream information @@ -148,29 +149,29 @@ static void Main(string[] args) } }; - client.ReceivedMp2t += (datas) => + client.ReceivedMp2t += (sender, e) => { if (fs_a == null) { string filename = "rtsp_capture_" + now + ".mp2"; fs_a = new FileStream(filename, FileMode.Create); } - foreach (var data in datas) + foreach (var data in e.Data) { fs_a?.Write(data.Span); } }; - client.ReceivedG711 += (format, g711) => + client.ReceivedG711 += (sender, e) => { - if (fs_a == null && format.Equals("PCMU")) + if (fs_a == null && e.Format.Equals("PCMU")) { string filename = "rtsp_capture_" + now + ".ul"; fs_a = new FileStream(filename, FileMode.Create); } - if (fs_a == null && format.Equals("PCMA")) + if (fs_a == null && e.Format.Equals("PCMA")) { string filename = "rtsp_capture_" + now + ".al"; fs_a = new FileStream(filename, FileMode.Create); @@ -178,16 +179,16 @@ static void Main(string[] args) if (fs_a != null) { - foreach (var data in g711) + foreach (var data in e.Data) { fs_a.Write(data.Span); } } }; - client.ReceivedAMR += (format, amr) => + client.ReceivedAMR += (sender, e) => { - if (fs_a == null && format.Equals("AMR")) + if (fs_a == null && e.Format.Equals("AMR")) { string filename = "rtsp_capture_" + now + ".amr"; fs_a = new FileStream(filename, FileMode.Create); @@ -197,7 +198,7 @@ static void Main(string[] args) if (fs_a != null) { - foreach (var data in amr) + foreach (var data in e.Data) { fs_a.Write(data.Span); } @@ -205,7 +206,7 @@ static void Main(string[] args) }; - client.ReceivedAAC += (format, aac, ObjectType, FrequencyIndex, ChannelConfiguration) => + client.ReceivedAAC += (sender, e) => { if (fs_a == null) { @@ -215,7 +216,7 @@ static void Main(string[] args) if (fs_a != null) { - foreach (var data in aac) + foreach (var data in e.Data) { // ASDT header format int protection_absent = 1; @@ -223,15 +224,15 @@ static void Main(string[] args) // int sample_freq = 4; // 4 = 44100 Hz // int channel_config = 2; // 2 = Stereo - Rtsp.BitStream bs = new Rtsp.BitStream(); + BitStream bs = new BitStream(); bs.AddValue(0xFFF, 12); // (a) Start of data bs.AddValue(0, 1); // (b) Version ID, 0 = MPEG4 bs.AddValue(0, 2); // (c) Layer always 2 bits set to 0 bs.AddValue(protection_absent, 1); // (d) 1 = No CRC - bs.AddValue((int)ObjectType - 1, 2); // (e) MPEG Object Type / Profile, minus 1 - bs.AddValue((int)FrequencyIndex, 4); // (f) + bs.AddValue((int)e.ObjectType - 1, 2); // (e) MPEG Object Type / Profile, minus 1 + bs.AddValue((int)e.FrequencyIndex, 4); // (f) bs.AddValue(0, 1); // (g) private bit. Always zero - bs.AddValue((int)ChannelConfiguration, 3); // (h) + bs.AddValue((int)e.ChannelConfiguration, 3); // (h) bs.AddValue(0, 1); // (i) originality bs.AddValue(0, 1); // (j) home bs.AddValue(0, 1); // (k) copyrighted id @@ -255,39 +256,66 @@ static void Main(string[] args) // Connect to RTSP Server Console.WriteLine("Connecting"); - client.Connect(url, RTSPClient.RTP_TRANSPORT.UDP, RTSPClient.MEDIA_REQUEST.VIDEO_AND_AUDIO); + client.Connect(url, string.Empty, string.Empty, RTSPClient.RTP_TRANSPORT.UDP, RTSPClient.MEDIA_REQUEST.VIDEO_AND_AUDIO); // Wait for user to terminate programme - // Check for null which is returned when running under some IDEs - // OR wait for the Streaming to Finish - eg an error on the RTSP socket - Console.WriteLine("Press ENTER to exit"); - ConsoleKeyInfo key = default; - while (key.Key != ConsoleKey.Enter && !client.StreamingFinished()) + if (client.SocketStatus == RTSPClient.RtspSocketStatus.Connected) { + Console.WriteLine("Press ENTER to exit"); + // client connected. - while (!Console.KeyAvailable && !client.StreamingFinished()) + // Check for null which is returned when running under some IDEs + // OR wait for the Streaming to Finish - eg an error on the RTSP socket + + ConsoleKeyInfo key = default; + while (key.Key != ConsoleKey.Enter && !client.StreamingFinished()) { - // Avoid maxing out CPU on systems that instantly return null for ReadLine - Thread.Sleep(250); + while (!Console.KeyAvailable && !client.StreamingFinished()) + { + // Avoid maxing out CPU on systems that instantly return null for ReadLine + + Thread.Sleep(250); + } + if (Console.KeyAvailable) + { + key = Console.ReadKey(); + } } - if (Console.KeyAvailable) + client.Stop(); + } + else + { + Console.WriteLine("Connection failed..."); + Console.WriteLine("Press ENTER to exit"); + + ConsoleKeyInfo key = default; + while (key.Key != ConsoleKey.Enter) { - key = Console.ReadKey(); + + while (!Console.KeyAvailable) + { + // Avoid maxing out CPU on systems that instantly return null for ReadLine + + Thread.Sleep(250); + } + if (Console.KeyAvailable) + { + key = Console.ReadKey(); + } } } - client.Stop(); fs_v?.Close(); - Console.WriteLine("Finished"); + Console.WriteLine("Finished"); } private static void WriteNalToFile(FileStream fs_v, ReadOnlySpan nal) { - fs_v.Write(new byte[] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); // Write Start Code + fs_v.Write([0x00, 0x00, 0x00, 0x01], 0, 4); // Write Start Code fs_v.Write(nal); } } diff --git a/RtspClientExample/RTSPClient.cs b/RtspClientExample/RTSPClient.cs index 515f0b3..c3cc721 100644 --- a/RtspClientExample/RTSPClient.cs +++ b/RtspClientExample/RTSPClient.cs @@ -3,10 +3,13 @@ using Rtsp.Messages; using Rtsp.Rtp; using Rtsp.Sdp; +using Rtsp.Utils; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Net; using System.Text; namespace RtspClientExample @@ -17,42 +20,39 @@ class RTSPClient private readonly ILoggerFactory _loggerFactory; // Events that applications can receive - public event Received_SPS_PPS_Delegate? ReceivedSpsPps; - public event Received_VPS_SPS_PPS_Delegate? ReceivedVpsSpsPps; - public event ReceivedSimpleDataDelegate? ReceivedNALs; // H264 or H265 - public event ReceivedSimpleDataDelegate? ReceivedMp2t; - public event ReceivedSimpleDataDelegate? ReceivedMJpeg; - public event Received_G711_Delegate? ReceivedG711; - public event Received_AMR_Delegate? ReceivedAMR; - public event Received_AAC_Delegate? ReceivedAAC; + public event EventHandler? ReceivedSpsPps; + public event EventHandler? ReceivedVpsSpsPps; + public event EventHandler? ReceivedNALs; // H264 or H265 + public event EventHandler? ReceivedMp2t; + public event EventHandler? ReceivedJpeg; + public event EventHandler? ReceivedG711; + public event EventHandler? ReceivedAMR; + public event EventHandler? ReceivedAAC; // Delegated functions (essentially the function prototype) - public delegate void Received_SPS_PPS_Delegate(byte[] sps, byte[] pps); // H264 - public delegate void Received_VPS_SPS_PPS_Delegate(byte[] vps, byte[] sps, byte[] pps); // H265 - public delegate void ReceivedSimpleDataDelegate(List> data); - public delegate void Received_G711_Delegate(string format, List> g711); - public delegate void Received_AMR_Delegate(string format, List> amr); - public delegate void Received_AAC_Delegate(string format, List> aac, int ObjectType, int FrequencyIndex, int ChannelConfiguration); + //public delegate void Received_SPS_PPS_Delegate(byte[] sps, byte[] pps); // H264 + //public delegate void Received_VPS_SPS_PPS_Delegate(byte[] vps, byte[] sps, byte[] pps); // H265 + //public delegate void ReceivedSimpleDataDelegate(List> data); + //public delegate void Received_G711_Delegate(string format, List> g711); + //public delegate void Received_AMR_Delegate(string format, List> amr); + //public delegate void Received_AAC_Delegate(string format, List> aac, uint ObjectType, uint FrequencyIndex, uint ChannelConfiguration); public enum RTP_TRANSPORT { UDP, TCP, MULTICAST }; public enum MEDIA_REQUEST { VIDEO_ONLY, AUDIO_ONLY, VIDEO_AND_AUDIO }; - private enum RTSP_STATUS { WaitingToConnect, Connecting, ConnectFailed, Connected }; + public enum RtspSocketStatus { WaitingToConnect, Connecting, ConnectFailed, Connected }; - RtspTcpTransport? rtspSocket; // RTSP connection - RTSP_STATUS rtspSocketStatus = RTSP_STATUS.WaitingToConnect; + IRtspTransport? rtspSocket; // RTSP connection + RtspSocketStatus rtspSocketStatus = RtspSocketStatus.WaitingToConnect; // this wraps around a the RTSP tcp_socket stream RtspListener? rtspClient; RTP_TRANSPORT rtpTransport = RTP_TRANSPORT.UDP; // Mode, either RTP over UDP or RTP over TCP using the RTSP socket - UDPSocket? videoUdpPair; // Pair of UDP ports used in RTP over UDP mode or in MULTICAST mode - UDPSocket? audioUdpPair; // Pair of UDP ports used in RTP over UDP mode or in MULTICAST mode - string url = ""; // RTSP URL (username & password will be stripped out - string username = ""; // Username - string password = ""; // Password + UDPSocket? videoUdpPair; // Pair of UDP ports used in RTP over UDP mode or in MULTICAST mode + UDPSocket? audioUdpPair; // Pair of UDP ports used in RTP over UDP mode or in MULTICAST mode + Uri? _uri; // RTSP URI (username & password will be stripped out string session = ""; // RTSP Session - string auth_type = string.Empty; // cached from most recent WWW-Authenticate reply - string realm = string.Empty; // cached from most recent WWW-Authenticate reply - string nonce = string.Empty; // cached from most recent WWW-Authenticate reply - uint ssrc = 12345; + private Authentication? _authentication; + private NetworkCredential _credentials; + readonly uint ssrc = 12345; bool clientWantsVideo = false; // Client wants to receive Video bool clientWantsAudio = false; // Client wants to receive Audio @@ -75,6 +75,9 @@ private enum RTSP_STATUS { WaitingToConnect, Connecting, ConnectFailed, Connecte // setup messages still to send readonly Queue setupMessages = new(); + + public RtspSocketStatus SocketStatus => rtspSocketStatus; + // Constructor public RTSPClient(ILoggerFactory loggerFactory) { @@ -83,63 +86,67 @@ public RTSPClient(ILoggerFactory loggerFactory) } - public void Connect(string url, RTP_TRANSPORT rtpTransport, MEDIA_REQUEST mediaRequest = MEDIA_REQUEST.VIDEO_AND_AUDIO) + public void Connect(string url, + string username, + string password, + RTP_TRANSPORT rtpTransport, MEDIA_REQUEST mediaRequest = MEDIA_REQUEST.VIDEO_AND_AUDIO) { - RtspUtils.RegisterUri(); - _logger.LogDebug("Connecting to {url} ", url); - this.url = url; + _logger.LogDebug($"Connecting to {url}"); + + _uri = new(url); // Use URI to extract username and password // and to make a new URL without the username and password - Uri uri = new(this.url); - var hostname = uri.Host; - var port = uri.Port; try { - if (uri.UserInfo.Length > 0) + if (_uri.UserInfo.Length > 0) + { + _credentials = new(_uri.UserInfo.Split([':'])[0], _uri.UserInfo.Split([':'])[1]); + _uri = new(_uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped)); + } + else { - username = uri.UserInfo.Split(new char[] { ':' })[0]; - password = uri.UserInfo.Split(new char[] { ':' })[1]; - this.url = uri.GetComponents((UriComponents.AbsoluteUri & ~UriComponents.UserInfo), - UriFormat.UriEscaped); + _credentials = new(username, password); } } catch { - username = string.Empty; - password = string.Empty; + _credentials = new NetworkCredential(); } // We can ask the RTSP server for Video, Audio or both. If we don't want audio we don't need to SETUP the audio channal or receive it - clientWantsVideo = (mediaRequest == MEDIA_REQUEST.VIDEO_ONLY || mediaRequest == MEDIA_REQUEST.VIDEO_AND_AUDIO); - clientWantsAudio = (mediaRequest == MEDIA_REQUEST.AUDIO_ONLY || mediaRequest == MEDIA_REQUEST.VIDEO_AND_AUDIO); + clientWantsVideo = (mediaRequest is MEDIA_REQUEST.VIDEO_ONLY or MEDIA_REQUEST.VIDEO_AND_AUDIO); + clientWantsAudio = (mediaRequest is MEDIA_REQUEST.AUDIO_ONLY or MEDIA_REQUEST.VIDEO_AND_AUDIO); // Connect to a RTSP Server. The RTSP session is a TCP connection - rtspSocketStatus = RTSP_STATUS.Connecting; + rtspSocketStatus = RtspSocketStatus.Connecting; try { - rtspSocket = new RtspTcpTransport(hostname, port); + rtspSocket = + _uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.InvariantCultureIgnoreCase) ? + new RtspHttpTransport(_uri, _credentials) : + new RtspTcpTransport(_uri, _credentials); } catch { - rtspSocketStatus = RTSP_STATUS.ConnectFailed; - _logger.LogWarning("Error - did not connect"); + rtspSocketStatus = RtspSocketStatus.ConnectFailed; + _logger.LogDebug("Error - did not connect"); return; } if (rtspSocket.Connected == false) { - rtspSocketStatus = RTSP_STATUS.ConnectFailed; - _logger.LogWarning("Error - did not connect"); + rtspSocketStatus = RtspSocketStatus.ConnectFailed; + _logger.LogDebug("Error - did not connect"); return; } - rtspSocketStatus = RTSP_STATUS.Connected; + rtspSocketStatus = RtspSocketStatus.Connected; // Connect a RTSP Listener to the RTSP Socket (or other Stream) to send RTSP messages and listen for RTSP replies - rtspClient = new RtspListener(rtspSocket, _loggerFactory.CreateLogger()) + rtspClient = new RtspListener(rtspSocket) { AutoReconnect = false }; @@ -174,12 +181,11 @@ public void Connect(string url, RTP_TRANSPORT rtpTransport, MEDIA_REQUEST mediaR // Nothing to do. Will open Multicast UDP sockets after the SETUP command } - // Send OPTIONS // In the Received Message handler we will send DESCRIBE, SETUP and PLAY RtspRequest options_message = new RtspRequestOptions { - RtspUri = new Uri(this.url) + RtspUri = _uri, }; rtspClient.SendMessage(options_message); } @@ -187,21 +193,21 @@ public void Connect(string url, RTP_TRANSPORT rtpTransport, MEDIA_REQUEST mediaR // return true if this connection failed, or if it connected but is no longer connected. public bool StreamingFinished() => rtspSocketStatus switch { - RTSP_STATUS.ConnectFailed => true, - RTSP_STATUS.Connected when !(rtspSocket?.Connected ?? false) => true, + RtspSocketStatus.ConnectFailed => true, + RtspSocketStatus.Connected when !(rtspSocket?.Connected ?? false) => true, _ => false, }; + public void Pause() { // Send PAUSE RtspRequest pause_message = new RtspRequestPause { - RtspUri = new Uri(url), + RtspUri = _uri, Session = session }; - pause_message.AddAuthorization(username, password, auth_type, realm, nonce, url); rtspClient?.SendMessage(pause_message); } @@ -210,10 +216,10 @@ public void Play() // Send PLAY RtspRequest play_message = new RtspRequestPlay { - RtspUri = new Uri(url), + RtspUri = _uri, Session = session }; - play_message.AddAuthorization(username, password, auth_type, realm, nonce, url); + play_message.AddAuthorization(_credentials, _authentication!, _uri!, rtspSocket!.CommandCounter); rtspClient?.SendMessage(play_message); } @@ -223,10 +229,11 @@ public void Stop() // Send TEARDOWN RtspRequest teardown_message = new RtspRequestTeardown { - RtspUri = new Uri(url), + RtspUri = _uri, Session = session }; - teardown_message.AddAuthorization(username, password, auth_type, realm, nonce, url); + + teardown_message.AddAuthorization(_credentials, _authentication!, _uri!, rtspSocket!.CommandCounter); rtspClient?.SendMessage(teardown_message); // Stop the keepalive timer @@ -238,22 +245,28 @@ public void Stop() // Drop the RTSP session rtspClient?.Stop(); - } + // A Video RTP packet has been received. public void VideoRtpDataReceived(object? sender, RtspDataEventArgs e) { if (e.Data is null) return; - var rtpPacket = new RtpPacket(e.Data); + var rtpPacket = new RtpPacket(e.Data[..e.DataLength]); + /*if (rtpPacket.PayloadType == 26) + { + _logger.Log("[WARN] No parser has been written for JPEG RTP packets. Please help write one"); + return; // ignore this data + } + else */ if (rtpPacket.PayloadType != video_payload) { // Check the payload type in the RTP packet matches the Payload Type value from the SDP - _logger.LogDebug("Ignoring this Video RTP payload"); + _logger.LogWarning("Ignoring this Video RTP payload"); return; // ignore this data } @@ -300,12 +313,12 @@ public void VideoRtpDataReceived(object? sender, RtspDataEventArgs e) if (sps != null && pps != null) { // Fire the Event - ReceivedSpsPps?.Invoke(sps, pps); + ReceivedSpsPps?.Invoke(this, new(sps, pps)); h264_sps_pps_fired = true; } } // we have a frame of NAL Units. Write them to the file - ReceivedNALs?.Invoke(nal_units); + ReceivedNALs?.Invoke(this, new(nal_units)); } if (videoPayloadProcessor is H265Payload) @@ -340,23 +353,23 @@ public void VideoRtpDataReceived(object? sender, RtspDataEventArgs e) if (vps != null && sps != null && pps != null) { // Fire the Event - ReceivedVpsSpsPps?.Invoke(vps, sps, pps); + ReceivedVpsSpsPps?.Invoke(this, new(vps, sps, pps)); h265_vps_sps_pps_fired = true; } } // we have a frame of NAL Units. Write them to the file - ReceivedNALs?.Invoke(nal_units); + ReceivedNALs?.Invoke(this, new(nal_units)); } if (videoPayloadProcessor is JPEGPayload) { - ReceivedMJpeg?.Invoke(nal_units); + ReceivedJpeg?.Invoke(this, new(nal_units)); } if (videoPayloadProcessor is MP2TransportPayload) { - ReceivedMp2t?.Invoke(nal_units); + ReceivedMp2t?.Invoke(this, new(nal_units)); } } @@ -381,7 +394,7 @@ public void AudioRtpDataReceived(object? sender, RtspDataEventArgs e) if (audioPayloadProcessor is null) { - _logger.LogWarning("No parser for RTP payload {audioPayload}", audio_payload); + _logger.LogWarning($"No parser for RTP payload {audio_payload}"); return; } @@ -394,7 +407,7 @@ public void AudioRtpDataReceived(object? sender, RtspDataEventArgs e) if (audio_frames.Count != 0) { // Write the audio frames to the file - ReceivedG711?.Invoke(audio_codec, audio_frames); + ReceivedG711?.Invoke(this, new(audio_codec, audio_frames)); } } else if (audioPayloadProcessor is AMRPayload) @@ -403,7 +416,7 @@ public void AudioRtpDataReceived(object? sender, RtspDataEventArgs e) if (audio_frames.Count != 0) { // Write the audio frames to the file - ReceivedAMR?.Invoke(audio_codec, audio_frames); + ReceivedAMR?.Invoke(this, new(audio_codec, audio_frames)); } } @@ -413,7 +426,7 @@ public void AudioRtpDataReceived(object? sender, RtspDataEventArgs e) if (audio_frames.Count != 0) { // Write the audio frames to the file - ReceivedAAC?.Invoke(audio_codec, audio_frames, aacPayload.ObjectType, aacPayload.FrequencyIndex, aacPayload.ChannelConfiguration); + ReceivedAAC?.Invoke(this, new(audio_codec, audio_frames, aacPayload.ObjectType, aacPayload.FrequencyIndex, aacPayload.ChannelConfiguration)); } } } @@ -438,16 +451,16 @@ public void RtcpControlDataReceived(object? sender, RtspDataEventArgs e) // There can be multiple RTCP packets transmitted together. Loop ever each one long packetIndex = 0; - while (packetIndex < e.Data.Length) + while (packetIndex < e.DataLength) { int rtcp_version = (e.Data[packetIndex + 0] >> 6); int rtcp_padding = (e.Data[packetIndex + 0] >> 5) & 0x01; int rtcp_reception_report_count = (e.Data[packetIndex + 0] & 0x1F); byte rtcp_packet_type = e.Data[packetIndex + 1]; // Values from 200 to 207 - uint rtcp_length = (uint)(e.Data[packetIndex + 2] << 8) + (uint)(e.Data[packetIndex + 3]); // number of 32 bit words + uint rtcp_length = (uint)(e.Data[packetIndex + 2] << 8) + e.Data[packetIndex + 3]; // number of 32 bit words uint rtcp_ssrc = (uint)(e.Data[packetIndex + 4] << 24) + (uint)(e.Data[packetIndex + 5] << 16) - + (uint)(e.Data[packetIndex + 6] << 8) + (uint)(e.Data[packetIndex + 7]); + + (uint)(e.Data[packetIndex + 6] << 8) + e.Data[packetIndex + 7]; // 200 = SR = Sender Report // 201 = RR = Receiver Report @@ -456,31 +469,31 @@ public void RtcpControlDataReceived(object? sender, RtspDataEventArgs e) // 204 = APP = Application Specific Method // 207 = XR = Extended Reports - _logger.LogDebug("RTCP Data. PacketType={rtcp_packet_type} SSRC={ssrc}", rtcp_packet_type, rtcp_ssrc); + _logger.LogDebug($"RTCP Data. PacketType={rtcp_packet_type} SSRC={rtcp_ssrc}"); if (rtcp_packet_type == 200) { // We have received a Sender Report // Use it to convert the RTP timestamp into the UTC time - UInt32 ntp_msw_seconds = (uint)(e.Data[packetIndex + 8] << 24) + (uint)(e.Data[packetIndex + 9] << 16) - + (uint)(e.Data[packetIndex + 10] << 8) + (uint)(e.Data[packetIndex + 11]); + uint ntp_msw_seconds = (uint)(e.Data[packetIndex + 8] << 24) + (uint)(e.Data[packetIndex + 9] << 16) + + (uint)(e.Data[packetIndex + 10] << 8) + e.Data[packetIndex + 11]; - UInt32 ntp_lsw_fractions = (uint)(e.Data[packetIndex + 12] << 24) + (uint)(e.Data[packetIndex + 13] << 16) - + (uint)(e.Data[packetIndex + 14] << 8) + (uint)(e.Data[packetIndex + 15]); + uint ntp_lsw_fractions = (uint)(e.Data[packetIndex + 12] << 24) + (uint)(e.Data[packetIndex + 13] << 16) + + (uint)(e.Data[packetIndex + 14] << 8) + e.Data[packetIndex + 15]; - UInt32 rtp_timestamp = (uint)(e.Data[packetIndex + 16] << 24) + (uint)(e.Data[packetIndex + 17] << 16) - + (uint)(e.Data[packetIndex + 18] << 8) + (uint)(e.Data[packetIndex + 19]); + uint rtp_timestamp = (uint)(e.Data[packetIndex + 16] << 24) + (uint)(e.Data[packetIndex + 17] << 16) + + (uint)(e.Data[packetIndex + 18] << 8) + e.Data[packetIndex + 19]; - double ntp = ntp_msw_seconds + (ntp_lsw_fractions / UInt32.MaxValue); + double ntp = ntp_msw_seconds + (ntp_lsw_fractions / uint.MaxValue); // NTP Most Signigicant Word is relative to 0h, 1 Jan 1900 // This will wrap around in 2036 - var time = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc); + DateTime time = new(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc); - time = time.AddSeconds((double)ntp_msw_seconds); // adds 'double' (whole&fraction) + time = time.AddSeconds(ntp_msw_seconds); // adds 'double' (whole&fraction) - _logger.LogDebug("RTCP time (UTC) for RTP timestamp {timestamp} is {time}", rtp_timestamp, time); + _logger.LogDebug($"RTCP time (UTC) for RTP timestamp {rtp_timestamp} is {time}"); // Send a Receiver Report try @@ -531,16 +544,15 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) if (e.Message is not RtspResponse message) return; - _logger.LogDebug("Received RTSP response to message {originalReques}", message.OriginalRequest); + _logger.LogDebug($"Received RTSP response to message {message.OriginalRequest}"); // If message has a 401 - Unauthorised Error, then we re-send the message with Authorization // using the most recently received 'realm' and 'nonce' if (!message.IsOk) { - _logger.LogDebug("Got Error in RTSP Reply {returnCode} {returnMessage}", message.ReturnCode, message.ReturnMessage); + _logger.LogDebug($"Got Error in RTSP Reply {message.ReturnCode} {message.ReturnMessage}"); - if (message.ReturnCode == 401 - && message.OriginalRequest?.Headers.ContainsKey(RtspHeaderNames.Authorization) == true) + if (message.ReturnCode == 401 && message.OriginalRequest is not null && (message.OriginalRequest.Headers.ContainsKey(RtspHeaderNames.Authorization) == true)) { // the authorization failed. _logger.LogError("Fail to authenticate stoping here"); @@ -558,44 +570,17 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) // EG: Digest realm="IP Camera(21388)", nonce="534407f373af1bdff561b7b4da295354", stale="FALSE" string www_authenticate = message.Headers[RtspHeaderNames.WWWAuthenticate] ?? string.Empty; - string auth_params = ""; - if (www_authenticate.StartsWith("basic", StringComparison.InvariantCultureIgnoreCase)) - { - auth_type = "Basic"; - auth_params = www_authenticate[5..]; - } - if (www_authenticate.StartsWith("digest", StringComparison.InvariantCultureIgnoreCase)) - { - auth_type = "Digest"; - auth_params = www_authenticate[6..]; - } - - string[] items = auth_params.Split(new char[] { ',' }); // NOTE, does not handle Commas in Quotes - - foreach (string item in items) - { - // Split on the = symbol and update the realm and nonce - string[] parts = item.Trim().Split(new char[] { '=' }, 2); // max 2 parts in the results array - if (parts.Length >= 2 && parts[0].Trim().Equals("realm")) - { - realm = parts[1].Trim(new char[] { ' ', '\"' }); // trim space and quotes - } - else if (parts.Length >= 2 && parts[0].Trim().Equals("nonce")) - { - nonce = parts[1].Trim(new char[] { ' ', '\"' }); // trim space and quotes - } - } + _authentication = Authentication.Create(_credentials, www_authenticate); - _logger.LogDebug("WWW Authorize parsed for {auth_type} {realm} {nonce}", - auth_type, realm, nonce); + _logger.LogDebug($"WWW Authorize parsed for {_authentication}"); } RtspMessage? resend_message = message.OriginalRequest?.Clone() as RtspMessage; if (resend_message is not null) { - resend_message.AddAuthorization(username, password, auth_type, realm, nonce, url); + resend_message.AddAuthorization(_credentials, _authentication!, _uri!, rtspSocket!.CommandCounter); rtspClient?.SendMessage(resend_message); } return; @@ -611,11 +596,12 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) // Eg DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD, GET_PARAMETER]} if (message.Headers.ContainsKey(RtspHeaderNames.Public)) { - foreach (string part in message.Headers[RtspHeaderNames.Public].Split(',')) + string[]? parts = message.Headers[RtspHeaderNames.Public]?.Split(','); + if (parts != null) { - if (part.Trim().Equals("GET_PARAMETER", StringComparison.OrdinalIgnoreCase)) + foreach (string part in parts) { - serverSupportsGetParameter = true; + if (part.Trim().ToUpper().Equals("GET_PARAMETER")) serverSupportsGetParameter = true; } } } @@ -631,9 +617,9 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) // Send DESCRIBE RtspRequest describe_message = new RtspRequestDescribe { - RtspUri = new Uri(url) + RtspUri = _uri }; - describe_message.AddAuthorization(username, password, auth_type, realm, nonce, url); + describe_message.AddAuthorization(_credentials, _authentication!, _uri!, rtspSocket!.CommandCounter); rtspClient?.SendMessage(describe_message); } else @@ -658,7 +644,7 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) // (iii) send a PLAY command if all the SETUP command have been sent if (message.OriginalRequest is RtspRequestSetup) { - _logger.LogDebug("Got reply from Setup. Session is {session}", message.Session); + _logger.LogDebug($"Got reply from Setup. Session is {message.Session}"); session = message.Session ?? ""; // Session value used with Play, Pause, Teardown and and additional Setups if (keepaliveTimer != null && message.Timeout > 0 && message.Timeout > keepaliveTimer.Interval / 1000) @@ -686,7 +672,7 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) { // Create the Pair of UDP Sockets in Multicast mode - videoUdpPair = new MulticastUDPSocket(multicastAddress, videoDataChannel.Value, multicastAddress, videoRtcpChannel.Value); + videoUdpPair = new MulticastUDPSocket(multicastAddress!, videoDataChannel.Value, multicastAddress!, videoRtcpChannel.Value); videoUdpPair.DataReceived += VideoRtpDataReceived; videoUdpPair.ControlReceived += RtcpControlDataReceived; videoUdpPair.Start(); @@ -709,11 +695,11 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) { if (dataMessage.Channel == videoDataChannel) { - VideoRtpDataReceived(sender, new RtspDataEventArgs(dataMessage.Data)); + VideoRtpDataReceived(sender, new RtspDataEventArgs(dataMessage.Data, dataMessage.DataLength)); } else if (dataMessage.Channel == videoRtcpChannel) { - RtcpControlDataReceived(sender, new RtspDataEventArgs(dataMessage.Data)); + RtcpControlDataReceived(sender, new RtspDataEventArgs(dataMessage.Data, dataMessage.DataLength)); } } }; @@ -729,11 +715,11 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) { if (dataMessage.Channel == audioDataChannel) { - AudioRtpDataReceived(sender, new RtspDataEventArgs(dataMessage.Data)); + AudioRtpDataReceived(sender, new RtspDataEventArgs(dataMessage.Data, dataMessage.DataLength)); } else if (dataMessage.Channel == audioRtcpChannel) { - RtcpControlDataReceived(sender, new RtspDataEventArgs(dataMessage.Data)); + RtcpControlDataReceived(sender, new RtspDataEventArgs(dataMessage.Data, dataMessage.DataLength)); } } }; @@ -756,10 +742,10 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) // Send PLAY RtspRequest play_message = new RtspRequestPlay { - RtspUri = new Uri(url), + RtspUri = _uri, Session = session }; - play_message.AddAuthorization(username, password, auth_type, realm, nonce, url); + play_message.AddAuthorization(_credentials, _authentication!, _uri!, rtspSocket!.CommandCounter); rtspClient?.SendMessage(play_message); } } @@ -767,11 +753,12 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) // If we get a reply to PLAY (which was our fourth command), then we should have video being received if (message.OriginalRequest is RtspRequestPlay) { - _logger.LogDebug("Got reply from Play {command} ", message.Command); + _logger.LogDebug($"Got reply from Play {message.Command} "); } } + private void HandleDescribeResponse(RtspResponse message) { if (message.Data == null) @@ -781,10 +768,11 @@ private void HandleDescribeResponse(RtspResponse message) } // Examine the SDP - _logger.LogDebug("Sdp:\n{sdp}", Encoding.UTF8.GetString(message.Data)); + _logger.LogDebug($"Sdp:\n{Encoding.UTF8.GetString(message.Data[..message.DataLength])}"); SdpFile sdp_data; - using (StreamReader sdp_stream = new(new MemoryStream(message.Data))) + + using (StreamReader sdp_stream = new(new MemoryStream(message.Data[..message.DataLength]))) { sdp_data = SdpFile.Read(sdp_stream); } @@ -800,7 +788,6 @@ private void HandleDescribeResponse(RtspResponse message) { foreach (Media media in sdp_data.Medias.Where(m => m.MediaType == Media.MediaTypes.video)) { - // search the attributes for control, rtpmap and fmtp // holds SPS and PPS in base64 (h264 video) AttributFmtp? fmtp = media.Attributs.FirstOrDefault(x => x.Key == "fmtp") as AttributFmtp; @@ -813,23 +800,6 @@ private void HandleDescribeResponse(RtspResponse message) fmtpPayloadNumber = fmtp.PayloadNumber; } - - // extract h265 donl if available... - - bool h265HasDonl = false; - - if ((rtpmap?.EncodingName?.ToUpper().Equals("H265") ?? false) && !string.IsNullOrEmpty(fmtp?.FormatParameter)) - { - var param = H265Parameters.Parse(fmtp.FormatParameter); - if(param.ContainsKey("sprop-max-don-diff") && int.TryParse(param["sprop-max-don-diff"], out int donl) && donl > 0) - { - h265HasDonl = true; - } - } - - // some cameras are really mess with the payload type. - // must check also the rtpmap for the corrent format to load (sending an h265 payload when giving an h264 stream [Some Bosch camera]) - if (rtpmap != null) { if ((fmtpPayloadNumber > -1 && rtpmap.PayloadNumber == fmtpPayloadNumber) || fmtpPayloadNumber == -1) @@ -838,7 +808,7 @@ private void HandleDescribeResponse(RtspResponse message) videoPayloadProcessor = rtpmap?.EncodingName?.ToUpper() switch { "H264" => new H264Payload(null), - "H265" => new H265Payload(h265HasDonl, null), + "H265" => new H265Payload(false, null), "JPEG" => new JPEGPayload(), _ => null, }; @@ -864,7 +834,7 @@ private void HandleDescribeResponse(RtspResponse message) videoPayloadProcessor = rtpmap?.EncodingName?.ToUpper() switch { "H264" => new H264Payload(null), - "H265" => new H265Payload(h265HasDonl, null), + "H265" => new H265Payload(false, null), "JPEG" => new JPEGPayload(), _ => null, }; @@ -887,7 +857,7 @@ private void HandleDescribeResponse(RtspResponse message) { byte[] sps = sps_pps[0]; byte[] pps = sps_pps[1]; - ReceivedSpsPps?.Invoke(sps, pps); + ReceivedSpsPps?.Invoke(this, new(sps, pps)); h264_sps_pps_fired = true; } } @@ -903,7 +873,7 @@ private void HandleDescribeResponse(RtspResponse message) byte[] vps = vps_sps_pps[0]; byte[] sps = vps_sps_pps[1]; byte[] pps = vps_sps_pps[2]; - ReceivedVpsSpsPps?.Invoke(vps, sps, pps); + ReceivedVpsSpsPps?.Invoke(this, new(vps, sps, pps)); h265_vps_sps_pps_fired = true; } } @@ -921,8 +891,7 @@ private void HandleDescribeResponse(RtspResponse message) RtspUri = video_uri }; setup_message.AddTransport(transport); - setup_message.AddAuthorization(username, password, auth_type, realm, nonce, url); - + setup_message.AddAuthorization(_credentials, _authentication!, _uri!, rtspSocket!.CommandCounter); // Add SETUP message to list of mesages to send setupMessages.Enqueue(setup_message); } @@ -983,8 +952,7 @@ private void HandleDescribeResponse(RtspResponse message) RtspUri = audio_uri, }; setup_message.AddTransport(transport); - setup_message.AddAuthorization(username, password, auth_type, realm, nonce, url); - + setup_message.AddAuthorization(_credentials, _authentication!, _uri!, rtspSocket!.CommandCounter); // Add SETUP message to list of mesages to send setupMessages.Enqueue(setup_message); } @@ -1013,13 +981,13 @@ private void HandleDescribeResponse(RtspResponse message) string sdp_control = attrib.Value; string control; // the "track" or "stream id" - if (sdp_control.ToLower().StartsWith("rtsp://")) + if (sdp_control.ToLower().StartsWith("rtsp://") || sdp_control.ToLower().StartsWith("http://")) { control = sdp_control; //absolute path } else { - control = url + "/" + sdp_control; // relative path + control = _uri!.AbsoluteUri + "/" + sdp_control; // relative path } controlUri = new Uri(control); } @@ -1069,7 +1037,7 @@ void SendKeepAlive(object? sender, System.Timers.ElapsedEventArgs e) serverSupportsGetParameter ? new RtspRequestGetParameter { - RtspUri = new Uri(url), + RtspUri = _uri, Session = session } : new RtspRequestOptions @@ -1078,11 +1046,8 @@ void SendKeepAlive(object? sender, System.Timers.ElapsedEventArgs e) }; - keepAliveMessage.AddAuthorization(username, password, auth_type, realm, nonce, url); + keepAliveMessage.AddAuthorization(_credentials, _authentication!, _uri!, rtspSocket!.CommandCounter); rtspClient?.SendMessage(keepAliveMessage); } - - - } -} +} \ No newline at end of file diff --git a/RtspClientExample/RtspClientExample.csproj b/RtspClientExample/RtspClientExample.csproj index 59a1c36..c4a8be9 100644 --- a/RtspClientExample/RtspClientExample.csproj +++ b/RtspClientExample/RtspClientExample.csproj @@ -1,19 +1,20 @@  - - net6.0 - Exe - false - enable - - - - - - - - all - - - - + + net6.0 + Latest + Exe + false + enable + + + + + + + + all + + + + \ No newline at end of file diff --git a/RtspMultiplexer/RtspDispatcher.cs b/RtspMultiplexer/RtspDispatcher.cs index 86d5908..3ffaa8c 100644 --- a/RtspMultiplexer/RtspDispatcher.cs +++ b/RtspMultiplexer/RtspDispatcher.cs @@ -238,7 +238,7 @@ private RtspListener GetRtspListenerForDestination(Uri destinationUri) else { destination = new RtspListener( - new RtspTcpTransport(destinationUri.Host, destinationUri.Port) + new RtspTcpTransport(destinationUri, new NetworkCredential()) ); // un peu pourri mais pas d'autre idée... diff --git a/RtspMultiplexer/RtspServer.cs b/RtspMultiplexer/RtspServer.cs index 09f42ea..33a9c38 100644 --- a/RtspMultiplexer/RtspServer.cs +++ b/RtspMultiplexer/RtspServer.cs @@ -1,6 +1,7 @@ namespace RtspMulticaster { using Rtsp; + using Rtsp.Utils; using System; using System.Diagnostics.Contracts; using System.Net; @@ -55,7 +56,7 @@ private void AcceptConnection() { TcpClient oneClient = _RTSPServerListener.AcceptTcpClient(); RtspListener newListener = new RtspListener( - new RtspTcpTransport(oneClient)); + new RtspTcpTransport(oneClient, new NetworkCredential())); RTSPDispatcher.Instance.AddListener(newListener); newListener.Start(); } From ed69465716b006c86278429b6c1dd393ccebc916 Mon Sep 17 00:00:00 2001 From: Davide Galli Date: Fri, 19 Jan 2024 15:52:14 +0100 Subject: [PATCH 3/4] camera sample broken --- RtspCameraExample/Program.cs | 12 ++++++------ RtspCameraExample/RtspServer.cs | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/RtspCameraExample/Program.cs b/RtspCameraExample/Program.cs index 91beaf2..1cd4d44 100644 --- a/RtspCameraExample/Program.cs +++ b/RtspCameraExample/Program.cs @@ -27,7 +27,7 @@ static void Main() .AddFilter("Rtsp", LogLevel.Debug) .AddConsole(); }); - var demo = new Demo(); + var demo = new Demo(loggerFactory); } @@ -35,9 +35,9 @@ static void Main() class Demo { NetworkCredential networkCredential; - RtspServer rtspServer = null; - SimpleH264Encoder h264_encoder = null; - SimpleG711Encoder ulaw_encoder = null; + RtspServer rtspServer; + SimpleH264Encoder h264Encoder; + SimpleG711Encoder ulaw_encoder; byte[] raw_sps; byte[] raw_pps; @@ -80,7 +80,7 @@ public Demo(ILoggerFactory loggerFactory) ///////////////////////////////////////// // Step 2 - Create the H264 Encoder. It will feed NALs into the RTSP server ///////////////////////////////////////// - h264Encoder = new SimpleH264Encoder(width, height, fps); + h264Encoder = new SimpleH264Encoder((int)width, (int)height, fps); //h264_encoder = new TinyH264Encoder(); // hard coded to 192x128 raw_sps = h264Encoder.GetRawSPS(); raw_pps = h264Encoder.GetRawPPS(); @@ -128,7 +128,7 @@ public Demo(ILoggerFactory loggerFactory) private void Video_source_ReceivedYUVFrame(uint timestamp_ms, int width, int height, Span yuv_data) { - + // Compress the YUV and feed into the RTSP Server byte[] raw_video_nal = h264Encoder.CompressFrame(yuv_data); bool isKeyframe = true; // the Simple/Tiny H264 Encoders only return I-Frames for every video frame. diff --git a/RtspCameraExample/RtspServer.cs b/RtspCameraExample/RtspServer.cs index c174d64..ef9571b 100644 --- a/RtspCameraExample/RtspServer.cs +++ b/RtspCameraExample/RtspServer.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Linq; +using Rtsp.Messages; // RTSP Server Example (c) Roger Hardiman, 2016, 2018, 2020 // Released uder the MIT Open Source Licence @@ -102,7 +103,7 @@ private void AcceptConnection() // Hand the incoming TCP connection over to the RTSP classes var rtsp_socket = new RtspTcpTransport(oneClient, _credential); RtspListener newListener = new RtspListener(rtsp_socket, _loggerFactory.CreateLogger()); - newListener.MessageReceived += RTSP_Message_Received; + newListener.MessageReceived += RTSPMessageReceived; //RTSPDispatcher.Instance.AddListener(newListener); // Add the RtspListener to the RTSPConnections List From 671a33b3cbcce4785b7baf7a756cf63155e0cd5f Mon Sep 17 00:00:00 2001 From: Davide Galli Date: Fri, 19 Jan 2024 17:34:49 +0100 Subject: [PATCH 4/4] Wrong code for async connection. Wrong test code for changed arraypool --- RTSP.Tests/RTSPListenerTest.cs | 5 ++-- RTSP/RTSPListener.cs | 43 ++++++++++++++-------------------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/RTSP.Tests/RTSPListenerTest.cs b/RTSP.Tests/RTSPListenerTest.cs index 4cd280b..5a41808 100644 --- a/RTSP.Tests/RTSPListenerTest.cs +++ b/RTSP.Tests/RTSPListenerTest.cs @@ -200,7 +200,7 @@ public void ReceiveData() Assert.AreEqual(11, dataMessage.Channel); Assert.AreSame(testedListener, dataMessage.SourcePort); - Assert.AreEqual(data, dataMessage.Data); + Assert.AreEqual(data, dataMessage.Data[..dataMessage.DataLength]); } [Test] @@ -294,7 +294,8 @@ public void SendDataAsync() var data = new RtspData { Channel = 12, - Data = Enumerable.Range(0, dataLenght).Select(x => (byte)x).ToArray() + Data = Enumerable.Range(0, dataLenght).Select(x => (byte)x).ToArray(), + DataLength = dataLenght, }; // Run diff --git a/RTSP/RTSPListener.cs b/RTSP/RTSPListener.cs index bbc0283..9da5e66 100644 --- a/RTSP/RTSPListener.cs +++ b/RTSP/RTSPListener.cs @@ -474,18 +474,15 @@ public void Reconnect() } int length = 4 + frame.Length; - byte[] data = ArrayPool.Shared.Rent(length); - try - { - //byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header - data[0] = 36; // '$' character - data[1] = (byte)channel; - data[2] = (byte)((frame.Length & 0xFF00) >> 8); - data[3] = (byte)((frame.Length & 0x00FF)); - Array.Copy(frame, 0, data, 4, frame.Length); - return _stream.BeginWrite(data, 0, length, asyncCallback, state); - } - finally { ArrayPool.Shared.Return(data, true); } + byte[] data = new byte[length]; + //byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header + data[0] = 36; // '$' character + data[1] = (byte)channel; + data[2] = (byte)((frame.Length & 0xFF00) >> 8); + data[3] = (byte)((frame.Length & 0x00FF)); + Array.Copy(frame, 0, data, 4, frame.Length); + return _stream.BeginWrite(data, 0, length, asyncCallback, state); + } /// @@ -526,21 +523,17 @@ public void SendData(int channel, byte[] frame) } int length = 4 + frame.Length; - byte[] data = ArrayPool.Shared.Rent(length); - try + byte[] data = new byte[length]; + //byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header + data[0] = 36; // '$' character + data[1] = (byte)channel; + data[2] = (byte)((frame.Length & 0xFF00) >> 8); + data[3] = (byte)((frame.Length & 0x00FF)); + Array.Copy(frame, 0, data, 4, frame.Length); + lock (_stream) { - //byte[] data = new byte[4 + frame.Length]; // add 4 bytes for the header - data[0] = 36; // '$' character - data[1] = (byte)channel; - data[2] = (byte)((frame.Length & 0xFF00) >> 8); - data[3] = (byte)((frame.Length & 0x00FF)); - Array.Copy(frame, 0, data, 4, frame.Length); - lock (_stream) - { - _stream.Write(data, 0, data.Length); - } + _stream.Write(data, 0, length); } - finally { ArrayPool.Shared.Return(data, true); } }