diff --git a/RTSP.Tests/Integration/RTSPListenerIntegrationTests.cs b/RTSP.Tests/Integration/RTSPListenerIntegrationTests.cs index 36b6dd8c..a072d50d 100644 --- a/RTSP.Tests/Integration/RTSPListenerIntegrationTests.cs +++ b/RTSP.Tests/Integration/RTSPListenerIntegrationTests.cs @@ -31,7 +31,7 @@ public class RTSPListenerIntegrationTests public async Task SendOption_WhenSent_Receives200OK(string uri) { // arrange - var socket = RtspUtils.CreateRtspTransportFromUrl(new(uri), AcceptAllCertificate); + var socket = RtspUtils.CreateRtspTransportFromUrl(new(uri), new(), AcceptAllCertificate); var listener = new RtspListener(socket); var taskCompletionSource = new TaskCompletionSource(); listener.MessageReceived += ListenerOnMessageReceived; diff --git a/RTSP/RTSPHttpTransport.cs b/RTSP/RTSPHttpTransport.cs index 6af5c075..23277ebe 100644 --- a/RTSP/RTSPHttpTransport.cs +++ b/RTSP/RTSPHttpTransport.cs @@ -206,7 +206,7 @@ public void Close() _dataClient?.Close(); } - public Stream GetStream() + public virtual Stream GetStream() { if (_dataClient?.Connected != true || _stream is null) throw new InvalidOperationException("Client is not connected"); diff --git a/RTSP/RTSPHttpsTransport.cs b/RTSP/RTSPHttpsTransport.cs new file mode 100644 index 00000000..3dbfdd4f --- /dev/null +++ b/RTSP/RTSPHttpsTransport.cs @@ -0,0 +1,18 @@ +using System; +using System.IO; +using System.Net.Security; + +namespace Rtsp; + +public class RTSPHttpsTransport(Uri uri, System.Net.NetworkCredential credentials, RemoteCertificateValidationCallback? userCertificateSelectionCallback = null) : RtspHttpTransport(uri, credentials) +{ + private readonly RemoteCertificateValidationCallback? _userCertificateSelectionCallback = userCertificateSelectionCallback; + + public override Stream GetStream() + { + var sslStream = new SslStream(base.GetStream(), true, _userCertificateSelectionCallback); + + sslStream.AuthenticateAsClient(RemoteAddress); + return sslStream; + } +} \ No newline at end of file diff --git a/RTSP/RTSPUtils.cs b/RTSP/RTSPUtils.cs index 3bcce41c..49294632 100644 --- a/RTSP/RTSPUtils.cs +++ b/RTSP/RTSPUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using System.Net.Security; namespace Rtsp @@ -16,14 +17,14 @@ public static void RegisterUri() } } - public static IRtspTransport CreateRtspTransportFromUrl(Uri uri, RemoteCertificateValidationCallback? userCertificateSelectionCallback = null) + public static IRtspTransport CreateRtspTransportFromUrl(Uri uri, NetworkCredential networkCredential, RemoteCertificateValidationCallback? userCertificateSelectionCallback = null) { return uri.Scheme switch { "rtsp" => new RtspTcpTransport(uri), "rtsps" => new RtspTcpTlsTransport(uri, userCertificateSelectionCallback), - "http" => new RtspHttpTransport(uri, new()), - // "https" => new RtspHttpTransport(uri, new()), + "http" => new RtspHttpTransport(uri, networkCredential), + "https" => new RTSPHttpsTransport(uri, networkCredential, userCertificateSelectionCallback), _ => throw new ArgumentException("The uri scheme is not supported", nameof(uri)) }; } diff --git a/RtspClientExample/Program.cs b/RtspClientExample/Program.cs index 13b3352d..e701ac9d 100644 --- a/RtspClientExample/Program.cs +++ b/RtspClientExample/Program.cs @@ -10,6 +10,18 @@ public static class Program { private static ILogger logger = null!; + private const string ProfileMJPEG = "JPEG"; + private const string ProfileH264 = "H264"; + private const string ProfileH265 = "H265"; + private const string ProfileMP2T = "MP2T"; + + private const string ProfilePCMU = "PCMU"; + private const string ProfilePCMA = "PCMA"; + private const string ProfileAMR = "AMR"; + private const string ProfileAAC = "AAC"; + + private static readonly byte[] halStartCode = [0x00, 0x00, 0x00, 0x01]; + static void Main() { var loggerFactory = LoggerFactory.Create(builder => @@ -35,13 +47,13 @@ static void Main() // string url = "rtsp://192.168.0.89/media/video2"; - // string url = "http://192.168.3.72/profile1/media.smp"; + string url = "http://192.168.3.72/profile1/media.smp"; bool usePlayback = false; // string url = "rtsp://192.168.3.72/ProfileG/Recording-1/recording/play.smp"; string username = "admin"; - string password = "admin"; + string password = "Admin123!"; // Axis Tests //String url = "rtsp://192.168.1.125/onvif-media/media.amp?profile=quality_h264"; //String url = "rtsp://user:password@192.168.1.102/onvif-media/media.amp?profile=quality_h264"; @@ -69,7 +81,7 @@ static void Main() // Happytime RTSP Server //string url = "rtsp://127.0.0.1/screenlive"; - string url = "http://127.0.0.1:8044/screenlive"; + //string url = "http://127.0.0.1:8044/screenlive"; // MJPEG Tests (Payload 26) //String url = "rtsp://192.168.1.125/onvif-media/media.amp?profile=mobile_jpeg"; @@ -178,9 +190,9 @@ private static void NewAACAudioStream(NewStreamEventArgs arg, RTSPClient client) var config = arg.StreamConfigurationData as AacStreamConfigurationData; Debug.Assert(config != null, "config is invalid"); - client.ReceivedAudioData += (_, args) => + void ReceiveAudioAAC(RTSPClient client, SimpleDataEventArgs dataArgs) { - foreach (var data in args.Data) + foreach (var data in dataArgs.Data) { // ASDT header format int protection_absent = 1; @@ -214,7 +226,9 @@ private static void NewAACAudioStream(NewStreamEventArgs arg, RTSPClient client) fs_a.Write(header, 0, header.Length); fs_a.Write(data.Span); } - }; + } + ; + client.SetupAudioPayload(ProfileAAC, ReceiveAudioAAC); } private static void NewAMRAudioStream(RTSPClient client) @@ -224,13 +238,14 @@ private static void NewAMRAudioStream(RTSPClient client) string filename = "rtsp_capture_" + now + ".amr"; FileStream fs_a = new(filename, FileMode.Create); fs_a.Write("#!AMR\n"u8); - client.ReceivedAudioData += (_, args) => + void ReceiveAudioAMR(RTSPClient client, SimpleDataEventArgs dataArgs) { - foreach (var data in args.Data) + foreach (var data in dataArgs.Data) { fs_a.Write(data.Span); } }; + client.SetupAudioPayload(ProfileAMR, ReceiveAudioAMR); } private static void NewGenericAudio(RTSPClient client, string extension) @@ -238,13 +253,14 @@ private static void NewGenericAudio(RTSPClient client, string extension) string now = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string filename = "rtsp_capture_" + now + "." + extension; FileStream fs_a = new(filename, FileMode.Create); - client.ReceivedAudioData += (_, args) => + void ReceiveAudioPCMA(RTSPClient client, SimpleDataEventArgs dataArgs) { - foreach (var data in args.Data) + foreach (var data in dataArgs.Data) { fs_a.Write(data.Span); } }; + client.SetupAudioPayload(ProfilePCMA, ReceiveAudioPCMA); } private static void NewMP2Stream(RTSPClient client) @@ -253,13 +269,15 @@ private static void NewMP2Stream(RTSPClient client) string filename = "rtsp_capture_" + now + ".mp2"; FileStream fs_v = new(filename, FileMode.Create); - client.ReceivedVideoData += (_, args) => + void ReceivedVideoData_MPT2(RTSPClient client, SimpleDataEventArgs dataArgs) { - foreach (var data in args.Data) + foreach (var data in dataArgs.Data) { fs_v?.Write(data.Span); } - }; + } + ; + client.SetupVideoPayload(ProfileMP2T, ReceivedVideoData_MPT2); } private static void NewMJPEGStream(RTSPClient client) @@ -268,18 +286,21 @@ private static void NewMJPEGStream(RTSPClient client) Directory.CreateDirectory("rtsp_capture_" + now); var indexImg = 0; - client.ReceivedVideoData += (_, args) => + void ReceivedVideoData_MJPEG(RTSPClient client, SimpleDataEventArgs dataArgs) { // Ugly to do it each time. // The interface need to change have an event on new file - foreach (var data in args.Data) + + foreach (var data in dataArgs.Data) { string filename = Path.Combine("rtsp_capture_" + now, indexImg++ + ".jpg"); using var fs = new FileStream(filename, FileMode.Create); fs.Write(data.Span); } - }; + } + ; + client.SetupVideoPayload(ProfileMJPEG, ReceivedVideoData_MJPEG); } private static void NewH265Stream(NewStreamEventArgs args, RTSPClient client) @@ -293,7 +314,7 @@ private static void NewH265Stream(NewStreamEventArgs args, RTSPClient client) WriteNalToFile(fs_v, h265StreamConfigurationData.SPS); WriteNalToFile(fs_v, h265StreamConfigurationData.PPS); } - client.ReceivedVideoData += (_, dataArgs) => + void ReceivedVideoData_H265(RTSPClient client, SimpleDataEventArgs dataArgs) { if (fs_v != null) { @@ -319,7 +340,9 @@ private static void NewH265Stream(NewStreamEventArgs args, RTSPClient client) fs_v.Write(nalUnit); } } - }; + } + ; + client.SetupVideoPayload(ProfileH265, ReceivedVideoData_H265); } private static void NewH264Stream(NewStreamEventArgs args, RTSPClient client) @@ -332,7 +355,8 @@ private static void NewH264Stream(NewStreamEventArgs args, RTSPClient client) WriteNalToFile(fs_v, h264StreamConfigurationData.SPS); WriteNalToFile(fs_v, h264StreamConfigurationData.PPS); } - client.ReceivedVideoData += (_, dataArgs) => + + void ReceivedVideoData_H264(RTSPClient client, SimpleDataEventArgs dataArgs) { foreach (var nalUnitMem in dataArgs.Data) { @@ -356,7 +380,9 @@ private static void NewH264Stream(NewStreamEventArgs args, RTSPClient client) } fs_v.Write(nalUnit); } - }; + } + ; + client.SetupVideoPayload(ProfileH264, ReceivedVideoData_H264); } private static void WriteNalToFile(FileStream fs_v, ReadOnlySpan nal) diff --git a/RtspClientExample/RTSPClient.cs b/RtspClientExample/RTSPClient.cs index b5dc5c4e..2d1c4235 100644 --- a/RtspClientExample/RTSPClient.cs +++ b/RtspClientExample/RTSPClient.cs @@ -9,9 +9,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Security; using System.Text; namespace RtspClientExample; @@ -30,8 +32,16 @@ private class KeepAliveContext() // Events that applications can receive public event EventHandler? NewVideoStream; public event EventHandler? NewAudioStream; - public event EventHandler? ReceivedVideoData; - public event EventHandler? ReceivedAudioData; + + /// + /// Used to inform user that connection is broken ([4/5]XX errors) + /// + public event EventHandler? ConnectionError; + + /// + /// Just if no video uri is available + /// + public event EventHandler? NoVideoPayload; public enum RTP_TRANSPORT { @@ -55,6 +65,11 @@ private enum RTSP_STATUS Connected }; + + string? _setupPreferredVideoRtpMap = null; + string? _setupPreferredAudioRtpMap = null; + + IRtspTransport? rtspSocket; // RTSP connection RTSP_STATUS rtspSocketStatus = RTSP_STATUS.WaitingToConnect; @@ -76,14 +91,35 @@ private enum RTSP_STATUS bool clientWantsVideo = false; // Client wants to receive Video bool clientWantsAudio = false; // Client wants to receive Audio - Uri? video_uri = null; // URI used for the Video Track + private int videoBaseClock = 0; // Should be used for appropiate Video decodification + private int audioBaseClock = 0; // Shpuld be used for appropiate Audio decodification + + private bool _ready = false; // Helper to avoid sending any method before setup has been completed. + // If this will happen, all the chain will break (and goodbye to connection, without errors)... - int video_payload = - -1; // Payload Type for the Video. (often 96 which is the first dynamic payload value. Bosch use 35) + /// + /// All the given Video Media playback on Setup request. + /// + private readonly Dictionary videoPayloadMapping = []; + /// + /// The Payload associated with the video id + /// + private readonly Dictionary videoPayloadProcessors = []; + /// + /// The video uris supported. + /// + private readonly List video_uris = []; + + private readonly Dictionary audioPayloadMapping = []; + private readonly Dictionary audioPayloadProcessors = []; + private readonly List audio_uris = []; + + private readonly Dictionary> audioPayloadEvents = []; + /// + /// This returns the appropiate video chunks to caller. + /// + private readonly Dictionary> videoPayloadEvents = []; - Uri? audio_uri = null; // URI used for the Audio Track - int audio_payload = -1; // Payload Type for the Video. (often 96 which is the first dynamic payload value) - string audio_codec = ""; // Codec used with Payload Types (eg "PCMA" or "AMR") /// /// If true, the client must send an "onvif-replay" header on every play request. @@ -94,9 +130,6 @@ private enum RTSP_STATUS bool serverSupportsGetParameter = false; private readonly System.Timers.Timer keepaliveTimer; - IPayloadProcessor? videoPayloadProcessor = null; - IPayloadProcessor? audioPayloadProcessor = null; - // setup messages still to send readonly Queue setupMessages = new(); @@ -118,8 +151,74 @@ public RTSPClient(ILoggerFactory loggerFactory) keepaliveTimer.Elapsed += SendKeepAlive; } - public void Connect(string url, string username, string password, RTP_TRANSPORT rtpTransport, - MEDIA_REQUEST mediaRequest = MEDIA_REQUEST.VIDEO_AND_AUDIO, bool playbackSession = false) + #region Payload Utilities + + //This section contains methods to grab informations for the video/audio payload. + //May be used from caller to known what I am asking for. (not able to explain better, sorry) + + + public string GetVideoPayloadName(int payloadType) + { + if (videoPayloadMapping.TryGetValue(payloadType, out string? name)) { return name; } + return string.Empty; + } + public string GetAudioPayloadName(int payloadType) + { + if (audioPayloadMapping.TryGetValue(payloadType, out string? name)) { return name; } + return string.Empty; + } + + public void SetupVideoPayload(string payloadType, Action handler) + { + if (!videoPayloadEvents.TryGetValue(payloadType, out _)) + { + videoPayloadEvents.Add(payloadType, handler); + } + } + public bool RemoveVideoPayload(string payloadType) => videoPayloadEvents.Remove(payloadType); + public void ClearVideoPayloads() + { + videoPayloadEvents.Clear(); + } + + + public void SetupAudioPayload(string payloadType, Action handler) + { + if (!audioPayloadEvents.TryGetValue(payloadType, out _)) + { + audioPayloadEvents.Add(payloadType, handler); + } + } + public bool RemoveAudioPayload(string payloadType) => audioPayloadEvents.Remove(payloadType); + public void ClearAudioPayloads() + { + audioPayloadEvents.Clear(); + } + + #endregion Payload Utilities + + /// + /// Connect the required rtsp url. + /// + /// Url to connect + /// Username + /// Password + /// Which rtp transport to use + /// Audio, video or both? + /// Is a playback session? + /// Do you need a specific video payback? + /// Do you need a speficic audio payback? + /// Needed for broken ssl certificates... + public void Connect( + string url, + string username, + string password, + RTP_TRANSPORT rtpTransport, + MEDIA_REQUEST mediaRequest = MEDIA_REQUEST.VIDEO_AND_AUDIO, + bool playbackSession = false, + string? rtpMapVideo = null, + string? rtpMapAudio = null, + RemoteCertificateValidationCallback? userCertificateSelectionCallback = null) { RtspUtils.RegisterUri(); @@ -127,6 +226,8 @@ public void Connect(string url, string username, string password, RTP_TRANSPORT _uri = new(url); _playbackSession = playbackSession; + _setupPreferredVideoRtpMap = rtpMapVideo; + _setupPreferredAudioRtpMap = rtpMapAudio; // Use URI to extract username and password // and to make a new URL without the username and password @@ -146,19 +247,21 @@ public void Connect(string url, string username, string password, RTP_TRANSPORT catch (Exception err) { _logger.LogWarning(err, "Fail to extract credential"); + _credentials = new(); } // 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 is MEDIA_REQUEST.VIDEO_ONLY or MEDIA_REQUEST.VIDEO_AND_AUDIO); - clientWantsAudio = (mediaRequest is MEDIA_REQUEST.AUDIO_ONLY or MEDIA_REQUEST.VIDEO_AND_AUDIO); + clientWantsVideo = mediaRequest.HasFlag(MEDIA_REQUEST.VIDEO_ONLY); + clientWantsAudio = mediaRequest.HasFlag(MEDIA_REQUEST.AUDIO_ONLY); // Connect to a RTSP Server. The RTSP session is a TCP connection rtspSocketStatus = RTSP_STATUS.Connecting; try { - rtspSocket = _uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.InvariantCultureIgnoreCase) - ? new RtspHttpTransport(_uri, _credentials) - : new RtspTcpTransport(_uri); + rtspSocket = RtspUtils.CreateRtspTransportFromUrl(_uri, _credentials, userCertificateSelectionCallback); + //rtspSocket = _uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.InvariantCultureIgnoreCase) + // ? new RtspHttpTransport(_uri, _credentials) + // : new RtspTcpTransport(_uri); } catch { @@ -227,6 +330,8 @@ public void Connect(string url, string username, string password, RTP_TRANSPORT RtspUri = _uri }; rtspClient.SendMessage(optionsMessage); + + _ready = false; } // return true if this connection failed, or if it connected but is no longer connected. @@ -237,12 +342,14 @@ public void Connect(string url, string username, string password, RTP_TRANSPORT _ => false, }; - public void Pause() + public bool Pause() { if (rtspSocket is null || _uri is null) { - throw new InvalidOperationException("Not connected"); + _logger.LogInformation("Not connected"); + return false; } + if (!_ready) { return false; } // Send PAUSE RtspRequest pauseMessage = new RtspRequestPause @@ -252,14 +359,17 @@ public void Pause() }; pauseMessage.AddAuthorization(_authentication, _uri, rtspSocket.NextCommandIndex()); rtspClient?.SendMessage(pauseMessage); + return true; } - public void Play() + public bool Play() { if (rtspSocket is null || _uri is null) { - throw new InvalidOperationException("Not connected"); + _logger.LogInformation("Not connected"); + return false; } + if (!_ready) { return false; } // Send PLAY var playMessage = new RtspRequestPlay @@ -278,6 +388,7 @@ public void Play() } rtspClient?.SendMessage(playMessage); + return true; } /// @@ -285,11 +396,16 @@ public void Play() /// /// The playback time to start from /// Speed information (1.0 means normal speed, -1.0 backward speed), other values >1.0 and <-1.0 allow a different speed - public void Play(DateTime seekTime, double speed = 1.0) + public bool Play(DateTime seekTime, double speed = 1.0) { if (rtspSocket is null || _uri is null) { - throw new InvalidOperationException("Not connected"); + _logger.LogInformation("Not connected"); + return false; + } + if (!_ready) + { + return false; } var playMessage = new RtspRequestPlay @@ -297,6 +413,7 @@ public void Play(DateTime seekTime, double speed = 1.0) RtspUri = _uri, Session = session, }; + playMessage.AddAuthorization(_authentication, _uri, rtspSocket.NextCommandIndex()); playMessage.AddPlayback(seekTime, speed); if (_playbackSession) { @@ -305,6 +422,7 @@ public void Play(DateTime seekTime, double speed = 1.0) } rtspClient?.SendMessage(playMessage); + return true; } /// @@ -314,12 +432,14 @@ public void Play(DateTime seekTime, double speed = 1.0) /// Ending time for playback /// Speed information (1.0 means normal speed, -1.0 backward speed), other values >1.0 and <-1.0 allow a different speed /// - public void Play(DateTime seekTimeFrom, DateTime seekTimeTo, double speed = 1.0) + public bool Play(DateTime seekTimeFrom, DateTime seekTimeTo, double speed = 1.0) { if (rtspSocket is null || _uri is null) { - throw new InvalidOperationException("Not connected"); + _logger.LogInformation("Not connected"); + return false; } + if (_ready) { return false; } if (seekTimeFrom > seekTimeTo) { @@ -333,6 +453,7 @@ public void Play(DateTime seekTimeFrom, DateTime seekTimeTo, double speed = 1.0) Session = session, }; + playMessage.AddAuthorization(_authentication, _uri, rtspSocket.NextCommandIndex()); playMessage.AddPlayback(seekTimeFrom, seekTimeTo, speed); if (_playbackSession) { @@ -341,10 +462,18 @@ public void Play(DateTime seekTimeFrom, DateTime seekTimeTo, double speed = 1.0) } rtspClient?.SendMessage(playMessage); + return true; } - public void Stop() + public bool Stop() { + if (rtspSocket is null || _uri is null) + { + _logger.LogInformation("Not connected"); + return false; + } + if (_ready) { return false; } + // Send TEARDOWN RtspRequest teardownMessage = new RtspRequestTeardown { @@ -365,6 +494,8 @@ public void Stop() rtspClient?.Stop(); // forget current auth state _authentication = null; + + return true; } /// @@ -378,25 +509,29 @@ private void VideoRtpDataReceived(object? sender, RtspDataEventArgs e) using var data = e.Data; var rtpPacket = new RtpPacket(data.Data.Span); - if (rtpPacket.PayloadType != video_payload) + + if (!videoPayloadProcessors.TryGetValue(rtpPacket.PayloadType, out IPayloadProcessor? videoPayloadProcessor)) { - // Check the payload type in the RTP packet matches the Payload Type value from the SDP - _logger.LogDebug("Ignoring this Video RTP payload"); - return; // ignore this data + _logger.LogWarning($"No videopayload for this type."); + return; } + if (!videoPayloadMapping.TryGetValue(rtpPacket.PayloadType, out string? payloadName)) + { + _logger.LogWarning($"No videopayload mapping for this type."); + return; + } if (videoPayloadProcessor is null) { - _logger.LogWarning("No video Processor"); + _logger.LogDebug("No video Processor"); return; } - // this will cache the Packets until there is a Frame - using var nalUnits = videoPayloadProcessor.ProcessPacket(rtpPacket); + using RawMediaFrame rawMediaFrame = videoPayloadProcessor.ProcessPacket(rtpPacket); - if (nalUnits.Any()) + if (rawMediaFrame.Any() && videoPayloadEvents.TryGetValue(payloadName, out Action? action)) { - ReceivedVideoData?.Invoke(this, new(nalUnits.Data, nalUnits.ClockTimestamp)); + action?.Invoke(this, new([.. rawMediaFrame.Data], rawMediaFrame.ClockTimestamp, rawMediaFrame.RtpTimestamp, videoBaseClock, rtpPacket.PayloadType)); } } @@ -406,28 +541,27 @@ private void AudioRtpDataReceived(object? sender, RtspDataEventArgs e) if (e.Data.Data.IsEmpty) return; - using var data = e.Data; // Received some Audio Data on the correct channel. - var rtpPacket = new RtpPacket(data.Data.Span); + RtpPacket rtpPacket = new(e.Data.Data.Span); + - // Check the payload type in the RTP packet matches the Payload Type value from the SDP - if (rtpPacket.PayloadType != audio_payload) + if (!audioPayloadProcessors.TryGetValue(rtpPacket.PayloadType, out IPayloadProcessor? audioPayloadProcessor)) { - _logger.LogDebug("Ignoring this Audio RTP payload"); - return; // ignore this data + _logger.LogDebug($"No videopayload for this type."); + return; } - if (audioPayloadProcessor is null) + if (!audioPayloadMapping.TryGetValue(rtpPacket.PayloadType, out string? payloadName)) { - _logger.LogWarning("No parser for RTP payload {audioPayload}", audio_payload); + _logger.LogDebug($"No videopayload mapping for this type."); return; } - using var audioFrames = audioPayloadProcessor.ProcessPacket(rtpPacket); + using RawMediaFrame rawMediaFrame = audioPayloadProcessor.ProcessPacket(rtpPacket); - if (audioFrames.Any()) + if (rawMediaFrame.Any() && audioPayloadEvents.TryGetValue(payloadName, out Action? action)) { - ReceivedAudioData?.Invoke(this, new(audioFrames.Data, audioFrames.ClockTimestamp)); + action?.Invoke(this, new([.. rawMediaFrame.Data], rawMediaFrame.ClockTimestamp, rawMediaFrame.RtpTimestamp, audioBaseClock, rtpPacket.PayloadType)); } } @@ -543,6 +677,38 @@ private void RtspMessageReceived(object? sender, RtspChunkEventArgs e) return; } + if (message.ReturnCode == 400) + { + _logger.LogError("[400] Bad request."); + ConnectionError?.Invoke(this, EventArgs.Empty); + Stop(); + return; + } + + if (message.ReturnCode == 403) + { + _logger.LogError("[403] User cannot access required resource."); + ConnectionError?.Invoke(this, EventArgs.Empty); + Stop(); + return; + } + + if (message.ReturnCode == 501) + { + _logger.LogError("[501] Method not implemented."); + ConnectionError?.Invoke(this, EventArgs.Empty); + Stop(); + return; + } + + if (message.ReturnCode == 503) + { + _logger.LogError("[503] Not available."); + ConnectionError?.Invoke(this, EventArgs.Empty); + Stop(); + return; + } + // Check if the Reply has an Authenticate header. if (message.ReturnCode == 401 && message.Headers.TryGetValue(RtspHeaderNames.WWWAuthenticate, out string? value)) @@ -623,8 +789,8 @@ private void HandleSetupResponse(RtspResponse message) keepaliveTimer.Interval = message.Timeout * 1000 / 2; } - bool isVideoChannel = message.OriginalRequest.RtspUri == video_uri; - bool isAudioChannel = message.OriginalRequest.RtspUri == audio_uri; + bool isVideoChannel = message.OriginalRequest.RtspUri != null && video_uris.Contains(message.OriginalRequest.RtspUri); // == video_uri; + bool isAudioChannel = message.OriginalRequest.RtspUri != null && audio_uris.Contains(message.OriginalRequest.RtspUri); // == audio_uri; Debug.Assert(isVideoChannel || isAudioChannel, "Unknown channel response"); // Check the Transport header @@ -724,8 +890,10 @@ private void HandleSetupResponse(RtspResponse message) } else { + // setup is completed, we can receive now all the events we want... + _ready = true; // use the event for setup completed, so the main program can call the Play command with or without the playback request. - SetupMessageCompleted?.Invoke(this, EventArgs.Empty); + SetupMessageCompleted?.Invoke(this, EventArgs.Empty); } } @@ -746,9 +914,8 @@ private void HandleDescribeResponse(RtspResponse message) sdp_data = SdpFile.ReadLoose(sdp_stream); } - // For old sony cameras, we need to use the control uri from the sdp - var customControlUri = sdp_data.Attributs.FirstOrDefault(x => x.Key == "control"); - if (customControlUri is not null && !string.Equals(customControlUri.Value, "*")) + Attribut? customControlUri = sdp_data.Attributs.FirstOrDefault(x => string.Equals(x.Key, "control", StringComparison.OrdinalIgnoreCase)); + if (customControlUri is not null && !string.Equals(customControlUri.Value, "*", StringComparison.OrdinalIgnoreCase)) { _uri = new Uri(_uri!, customControlUri.Value); } @@ -759,11 +926,20 @@ private void HandleDescribeResponse(RtspResponse message) { foreach (Media media in sdp_data.Medias.Where(m => m.MediaType == Media.MediaTypes.video)) { + int video_payload = -1; + IPayloadProcessor? videoPayloadProcessor = null; + // 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; AttributRtpMap? rtpmap = media.Attributs.FirstOrDefault(x => x.Key == "rtpmap") as AttributRtpMap; - video_uri = GetControlUri(media); + Uri? video_uri = GetControlUri(media); + + if (!string.IsNullOrEmpty(_setupPreferredVideoRtpMap) && !(rtpmap?.EncodingName?.Equals(_setupPreferredVideoRtpMap, StringComparison.OrdinalIgnoreCase) ?? true)) + { + _logger.LogDebug($"Not requested one."); + continue; + } int fmtpPayloadNumber = -1; if (fmtp != null) @@ -771,6 +947,12 @@ private void HandleDescribeResponse(RtspResponse message) fmtpPayloadNumber = fmtp.PayloadNumber; } + if (int.TryParse(rtpmap?.ClockRate, NumberStyles.Integer, NumberFormatInfo.CurrentInfo, out int clockRate)) + { + // a rtsp client can have a single clockrate by url (I hope)... + videoBaseClock = clockRate; + } + // extract h265 donl if available... bool h265HasDonl = false; @@ -779,7 +961,7 @@ private void HandleDescribeResponse(RtspResponse message) { 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) + int.TryParse(param["sprop-max-don-diff"], NumberStyles.Integer, CultureInfo.InvariantCulture, out int donl) && donl > 0) { h265HasDonl = true; } @@ -824,6 +1006,20 @@ private void HandleDescribeResponse(RtspResponse message) 33 => "MP2T", _ => string.Empty, }; + + } + else if (rtpmap != null) + { + payloadName = rtpmap.EncodingName?.ToUpperInvariant() ?? string.Empty; + videoPayloadProcessor = payloadName switch + { + "H264" => new H264Payload(null, memoryPool: null), + "H265" => new H265Payload(h265HasDonl, null, memoryPool: null), + "JPEG" => new JPEGPayload(), + "MP4V-ES" => new RawPayload(), + _ => null, + }; + video_payload = media.PayloadType; } } @@ -881,20 +1077,46 @@ private void HandleDescribeResponse(RtspResponse message) NewVideoStream?.Invoke(this, new(payloadName, streamConfigurationData)); } - break; + if (!videoPayloadProcessors.TryGetValue(video_payload, out _)) + { + videoPayloadProcessors.Add(video_payload, videoPayloadProcessor); + } + if (!videoPayloadMapping.TryGetValue(video_payload, out _)) + { + videoPayloadMapping.Add(video_payload, payloadName); + } + + if (video_uri != null && !video_uris.Contains(video_uri)) { video_uris.Add(video_uri); } + + if (!string.IsNullOrEmpty(_setupPreferredVideoRtpMap)) + { + // break here, the requested one has been setup. + // there should be no other video stream setup now... + break; + } } } + + if (videoPayloadProcessors.Count == 0) + { + // send an info about video not available? + NoVideoPayload?.Invoke(this, EventArgs.Empty); + } } if (clientWantsAudio) { foreach (var media in sdp_data.Medias.Where(m => m.MediaType == Media.MediaTypes.audio)) { + int audio_payload = -1; + string audio_codec; + IPayloadProcessor? audioPayloadProcessor = null; + // search the attributes for control, rtpmap and fmtp AttributFmtp? fmtp = media.Attributs.FirstOrDefault(x => x.Key == "fmtp") as AttributFmtp; AttributRtpMap? rtpmap = media.Attributs.FirstOrDefault(x => x.Key == "rtpmap") as AttributRtpMap; - audio_uri = GetControlUri(media); + Uri? audio_uri = GetControlUri(media); audio_payload = media.PayloadType; IStreamConfigurationData? streamConfigurationData = null; @@ -961,13 +1183,30 @@ private void HandleDescribeResponse(RtspResponse message) NewAudioStream?.Invoke(this, new(audio_codec, streamConfigurationData)); } - break; + if (!videoPayloadProcessors.TryGetValue(audio_payload, out _)) + { + videoPayloadProcessors.Add(audio_payload, audioPayloadProcessor); + } + if (!videoPayloadMapping.TryGetValue(audio_payload, out _)) + { + videoPayloadMapping.Add(audio_payload, audio_codec); + } + + if (audio_uri != null && !video_uris.Contains(audio_uri)) { audio_uris.Add(audio_uri); } + + if (!string.IsNullOrEmpty(_setupPreferredAudioRtpMap)) + { + // break here, the requested one has been setup. + // there should be no other video stream setup now... + break; + } } } } if (setupMessages.Count == 0) { + ConnectionError?.Invoke(this, EventArgs.Empty); // No SETUP messages were generated // So we cannot continue throw new ApplicationException("Unable to setup media stream"); diff --git a/RtspClientExample/RTSPEventArgs.cs b/RtspClientExample/RTSPEventArgs.cs index 1f6cc94b..fdfecfc1 100644 --- a/RtspClientExample/RTSPEventArgs.cs +++ b/RtspClientExample/RTSPEventArgs.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace RtspClientExample { @@ -23,7 +24,7 @@ public record H264StreamConfigurationData : IStreamConfigurationData public required byte[] PPS { get; init; } } - public record H265StreamConfigurationData: IStreamConfigurationData + public record H265StreamConfigurationData : IStreamConfigurationData { public required byte[] VPS { get; init; } public required byte[] SPS { get; init; } @@ -38,15 +39,14 @@ public record AacStreamConfigurationData : IStreamConfigurationData public int ChannelConfiguration { get; init; } } - public class SimpleDataEventArgs : EventArgs + public class SimpleDataEventArgs(List> data, DateTime clockTimeStamp, ulong rtpTimeStamp, int baseClock, int payloadType) : EventArgs { - public SimpleDataEventArgs(IEnumerable> data, DateTime timeStamp) - { - Data = data; - TimeStamp = timeStamp; - } - public DateTime TimeStamp { get; } - public IEnumerable> Data { get; } + public int PayloadType { get; } = payloadType; + public int BaseClock { get; } = baseClock; + public ulong RtpTimestamp { get; } = rtpTimeStamp; + public DateTime ClockTimeStamp { get; } = clockTimeStamp; + //public DateTime TimeStamp { get; } = timeStamp; + public List> Data { get; } = data; } } diff --git a/RtspClientExample/RTSPMessageAuthExtension.cs b/RtspClientExample/RTSPMessageAuthExtension.cs index 02920cc9..51f416b9 100644 --- a/RtspClientExample/RTSPMessageAuthExtension.cs +++ b/RtspClientExample/RTSPMessageAuthExtension.cs @@ -6,6 +6,13 @@ namespace RtspClientExample { public static class RTSPMessageAuthExtension { + /// + /// An helper method to add the Authorization header if required. + /// + /// Message to add to. + /// Authentication value + /// Uri to connect to + /// A counter for authorization info. public static void AddAuthorization(this RtspRequest message, Authentication? authentication, Uri uri, uint commandCounter) { if (authentication is null)