├── .editorconfig ├── .github └── workflows │ ├── dotnet.yml │ └── publish-nuget.yml ├── .gitignore ├── LICENSE ├── README.md ├── RTSP.Tests ├── Authentication │ └── AuthenticationBasicTests.cs ├── BitStreamTests.cs ├── Integration │ └── RTSPListenerIntegrationTests.cs ├── Messages │ ├── PortCoupleTests.cs │ ├── RTSPMessageTest.cs │ ├── RtspDataTest.cs │ ├── RtspResponseTests.cs │ └── RtspTransportTest.cs ├── Onvif │ └── RtpPacketOnvifUtilsTests.cs ├── RTSP.Tests.csproj ├── RTSPListenerTest.cs ├── RTSPTests.nunit ├── RTSPUtilsTest.cs ├── Rtp │ ├── Data │ │ ├── img_jpg_0.jpg │ │ ├── jpeg_0.rtp │ │ ├── jpeg_1.rtp │ │ └── jpeg_2.rtp │ ├── JpegPayloadTests.cs │ └── RawMediaFrameTests.cs ├── Sdp │ ├── Data │ │ ├── test1.sdp │ │ ├── test2.sdp │ │ ├── test3.sdp │ │ ├── test4.sdp │ │ ├── test5.sdp │ │ ├── test6.sdp │ │ ├── test7.sdp │ │ ├── test8.sdp │ │ └── test9.sdp │ ├── H264ParametersTests.cs │ └── SdpFileTest.cs ├── TestUtils │ ├── InBlockingStream.cs │ └── InOutStream.cs └── Utils │ └── ReadOnlySequenceExtensionsTests.cs ├── RTSP.sln ├── RTSP ├── Authentication.cs ├── AuthenticationBasic.cs ├── AuthenticationDigest.cs ├── BitStream.cs ├── HeadersParser.cs ├── HttpBadResponseCodeException.cs ├── HttpBadResponseException.cs ├── IRTSPTransport.cs ├── IRtpTransport.cs ├── Messages │ ├── PortCouple.cs │ ├── RTSPChunk.cs │ ├── RTSPData.cs │ ├── RTSPHeaderNames.cs │ ├── RTSPHeaderUtils.cs │ ├── RTSPMessage.cs │ ├── RTSPRequest.cs │ ├── RTSPRequestAnnounce.cs │ ├── RTSPRequestDescribe.cs │ ├── RTSPRequestGetParameter.cs │ ├── RTSPRequestOptions.cs │ ├── RTSPRequestPause.cs │ ├── RTSPRequestPlay.cs │ ├── RTSPRequestRecord.cs │ ├── RTSPRequestSetup.cs │ ├── RTSPRequestTeardown.cs │ ├── RTSPResponse.cs │ └── RTSPTransport.cs ├── MulticastUdpSocket.cs ├── NetworkCredentialExtensions.cs ├── Onvif │ ├── RtpPacketOnvifUtils.cs │ └── RtspMessageOnvifExtension.cs ├── Properties │ └── AssemblyInfo.cs ├── RTSP.csproj ├── RTSPDataEventArgs.cs ├── RTSPHttpTransport.cs ├── RTSPListener.cs ├── RTSPTCPTransport.cs ├── RTSPTcpTlsTransport.cs ├── RTSPUtils.cs ├── Rtcp │ ├── RtcpPacket.cs │ └── RtcpPacketUtil.cs ├── Rtp │ ├── AACPayload.cs │ ├── AMRPayload.cs │ ├── G711Payload.cs │ ├── G711_1Payload.cs │ ├── H264Payload.cs │ ├── H265Payload.cs │ ├── IPayloadProcessor.cs │ ├── JPEGDefaultTables.cs │ ├── JPEGPayload.cs │ ├── MP2TransportPayload.cs │ ├── RawMediaFrame.cs │ ├── RawPayload.cs │ ├── RtpPacket.cs │ └── RtpPacketUtil.cs ├── RtpTcpTransport.cs ├── RtspChunkEventArgs.cs ├── Sdp │ ├── Attribut.cs │ ├── AttributFmtp.cs │ ├── AttributRtpMap.cs │ ├── Bandwidth.cs │ ├── Connection.cs │ ├── ConnectionIP4.cs │ ├── ConnectionIP6.cs │ ├── H264Parameters.cs │ ├── H265Parameters.cs │ ├── Media.cs │ ├── Origin.cs │ ├── SdpFile.cs │ ├── SdpTimeZone.cs │ └── Timing.cs ├── UdpSocket.cs └── Utils │ ├── ReadOnlySequenceExtensions.cs │ └── SentMessageList.cs ├── RtspCameraExample ├── App.config ├── CJOCh264bitstream.cs ├── CJOCh264encoder.cs ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── RtspCameraExample.csproj ├── RtspServer.cs ├── SimpleG711Encoder.cs ├── SimpleH264Encoder.cs └── TestCard.cs ├── RtspClientExample ├── App.config ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── RTSPClient.cs ├── RTSPEventArgs.cs ├── RTSPMessageAuthExtension.cs └── RtspClientExample.csproj ├── RtspMultiplexer ├── Fowarder.cs ├── NLog.config ├── NLog.xsd ├── OriginContext.cs ├── Program.cs ├── RtspDispatcher.cs ├── RtspMultiplexer.csproj ├── RtspPushDescription.cs ├── RtspPushManager.cs ├── RtspServer.cs ├── RtspSession.cs ├── TcpToUdpForwader.cs └── UdpForwarder.cs ├── TestConsoleDotNetFramework ├── App.config ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── TestConsoleDotNetFramework.csproj └── packages.config └── example.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CA1510: Can't be used in netstandart 2.0 4 | dotnet_diagnostic.CA1510.severity = silent 5 | 6 | # Just because I don't like this rule 7 | dotnet_diagnostic.IDE0290.severity = silent 8 | 9 | # For now increase, double the default value 10 | MA0051.maximum_lines_per_method = 120 11 | MA0051.maximum_statements_per_method = 80 12 | 13 | # Only validate the first type in a file 14 | MA0048.only_validate_first_type = true 15 | 16 | # MA0110: Use the Regex source generator 17 | # Not supported in netstandard 2.0 and 2.1 18 | # so can't be used in this project 19 | dotnet_diagnostic.MA0110.severity = none 20 | 21 | # not supported in netstandard 2.0 22 | dotnet_diagnostic.MA0111.severity = none 23 | 24 | # not supported in netstandard 2.0 25 | dotnet_diagnostic.MA0089.severity = none 26 | 27 | # not supported in netstandart 2.0 28 | dotnet_diagnostic.RCS1261.severity = none -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-dotnet@v4 16 | with: 17 | dotnet-version: 9.x 18 | - name: Restore dependencies 19 | run: dotnet restore RTSP.sln 20 | - name: Build 21 | run: dotnet build --no-restore RTSP.sln 22 | - name: Test 23 | run: dotnet test --no-build --verbosity normal --framework net9.0 --filter TestCategory!=Integration RTSP.sln 24 | -------------------------------------------------------------------------------- /.github/workflows/publish-nuget.yml: -------------------------------------------------------------------------------- 1 | # Action to publish a new version of the package to NuGet 2 | 3 | name: Publish 4 | 5 | permissions: 6 | contents: read 7 | 8 | on: 9 | release: 10 | types: [released] 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: 9.x 20 | - name: Restore dependencies 21 | run: dotnet restore RTSP.sln 22 | - name: Build 23 | run: dotnet build --no-restore --configuration Release RTSP.sln 24 | - name: Publish 25 | run: dotnet nuget push RTSP/bin/Release/SharpRTSP*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json 26 | env: 27 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | TestResults/ 3 | MolesAssemblies/ 4 | bin/ 5 | obj/ 6 | packages/ 7 | *.suo 8 | *.Cache 9 | *.g.cs 10 | *.VisualState.xml 11 | TestResult.xml 12 | SharpRTSP.FxCop.xml 13 | RTSP.Tests/cover.xml 14 | RTSP.Tests/TestResult.xml 15 | RTSP.Tests/Coverage/ 16 | .vs/ 17 | *.TMP 18 | *.user 19 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Library to Handle RTSP in C# 2 | 3 | MIT License 4 | 5 | Copyright (C) 2016 Nicolas GRAZIANO 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /RTSP.Tests/Authentication/AuthenticationBasicTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Rtsp; 3 | using Rtsp.Messages; 4 | using System.Net; 5 | 6 | namespace RTSP.Tests.Authentication 7 | { 8 | public class AuthenticationBasicTests 9 | { 10 | [Test] 11 | public void GetResponseTest() 12 | { 13 | const string wanted = "Basic dXNlcm5hbWVAZXhhbXBsZS5jb206UGFzc3dvcmRAIVhZWg=="; 14 | var testObject = new AuthenticationBasic(new NetworkCredential("username@example.com", "Password@!XYZ"), "Test Realm"); 15 | 16 | var header = testObject.GetResponse(0, "rtsp://test/uri", "GET_PARAMETER", []); 17 | 18 | Assert.That(header, Is.EqualTo(wanted)); 19 | } 20 | 21 | [Test] 22 | public void IsValidTest() 23 | { 24 | var message = new RtspRequest(); 25 | message.Headers.Add("Authorization", "Basic dXNlcm5hbWVAZXhhbXBsZS5jb206UGFzc3dvcmRAIVhZWg=="); 26 | var testObject = new AuthenticationBasic(new NetworkCredential("username@example.com", "Password@!XYZ"), "Test Realm"); 27 | 28 | var result = testObject.IsValid(message); 29 | 30 | Assert.That(result, Is.True); 31 | } 32 | 33 | [Test] 34 | public void IsValidCaseUserNameTest() 35 | { 36 | var message = new RtspRequest(); 37 | message.Headers.Add("Authorization", "Basic dXNlcm5hbWVAZXhhbXBsZS5jb206UGFzc3dvcmRAIVhZWg=="); 38 | var testObject = new AuthenticationBasic(new NetworkCredential("USERNAME@example.com", "Password@!XYZ"), "Test Realm"); 39 | 40 | var result = testObject.IsValid(message); 41 | 42 | Assert.That(result, Is.True); 43 | } 44 | 45 | [Test] 46 | public void IsValidWrongCasePasswordTest() 47 | { 48 | var message = new RtspRequest(); 49 | message.Headers.Add("Authorization", "Basic dXNlcm5hbWVAZXhhbXBsZS5jb206UGFzc3dvcmRAIVhZWg=="); 50 | var testObject = new AuthenticationBasic(new NetworkCredential("username@example.com", "password@!XYZ"), "Test Realm"); 51 | 52 | var result = testObject.IsValid(message); 53 | 54 | Assert.That(result, Is.False); 55 | } 56 | 57 | [Test] 58 | public void IsValidMisingPasswordTest() 59 | { 60 | var message = new RtspRequest(); 61 | message.Headers.Add("Authorization", "Basic dXNlcm5hbWVAZXhhbXBsZS5jb20="); 62 | var testObject = new AuthenticationBasic(new NetworkCredential("username@example.com", "password@!XYZ"), "Test Realm"); 63 | 64 | var result = testObject.IsValid(message); 65 | 66 | Assert.That(result, Is.False); 67 | } 68 | 69 | [Test] 70 | public void IsValidInvalidBase64Test() 71 | { 72 | var message = new RtspRequest(); 73 | message.Headers.Add("Authorization", "Basic invalid$$$$"); 74 | var testObject = new AuthenticationBasic(new NetworkCredential("username@example.com", "password@!XYZ"), "Test Realm"); 75 | 76 | var result = testObject.IsValid(message); 77 | 78 | Assert.That(result, Is.False); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /RTSP.Tests/BitStreamTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Rtsp.Tests 4 | { 5 | [TestFixture] 6 | public class BitStreamTests 7 | { 8 | [Test] 9 | public void AddValueTest() 10 | { 11 | BitStream bitstream = new(); 12 | 13 | bitstream.AddValue(0xA, 4); 14 | bitstream.AddValue(0xB, 4); 15 | bitstream.AddValue(0xC, 4); 16 | bitstream.AddValue(0xD, 4); 17 | bitstream.AddValue(0xE, 4); 18 | var vals = bitstream.ToArray(); 19 | Assert.That(vals, Is.EqualTo(new byte[] { 0xAB, 0xCD, 0xE0 })); 20 | } 21 | 22 | [Test] 23 | public void ReadTest() 24 | { 25 | BitStream bitstream = new(); 26 | bitstream.AddHexString("ABCDEF1234567890"); 27 | Assert.That(bitstream.Read(8), Is.EqualTo(0xAB)); 28 | Assert.That(bitstream.Read(4), Is.EqualTo(0xC)); 29 | Assert.That(bitstream.Read(4), Is.EqualTo(0xD)); 30 | Assert.That(bitstream.Read(8), Is.EqualTo(0xEF)); 31 | Assert.That(bitstream.Read(16), Is.EqualTo(0x1234)); 32 | Assert.That(bitstream.Read(2), Is.EqualTo(1)); 33 | Assert.That(bitstream.Read(2), Is.EqualTo(1)); 34 | } 35 | 36 | [Test] 37 | public void ReadInvalidData() 38 | { 39 | BitStream bitStream = new(); 40 | 41 | Assert.That(() => bitStream.AddHexString("GER"), Throws.ArgumentException); 42 | } 43 | 44 | [Test] 45 | public void ReadTooMuch() 46 | { 47 | BitStream bitStream = new(); 48 | bitStream.AddHexString("AB"); 49 | 50 | Assert.That(() => bitStream.Read(10), Throws.InvalidOperationException); 51 | } 52 | 53 | [Test] 54 | public void ReadTestLowerCase() 55 | { 56 | BitStream bitstream = new(); 57 | bitstream.AddHexString("abcdef1234567890"); 58 | Assert.That(bitstream.Read(8), Is.EqualTo(0xAB)); 59 | Assert.That(bitstream.Read(4), Is.EqualTo(0xC)); 60 | Assert.That(bitstream.Read(4), Is.EqualTo(0xD)); 61 | Assert.That(bitstream.Read(8), Is.EqualTo(0xEF)); 62 | Assert.That(bitstream.Read(16), Is.EqualTo(0x1234)); 63 | Assert.That(bitstream.Read(2), Is.EqualTo(1)); 64 | Assert.That(bitstream.Read(2), Is.EqualTo(1)); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /RTSP.Tests/Integration/RTSPListenerIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Rtsp.Messages; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Security; 6 | using System.Security.Cryptography.X509Certificates; 7 | using System.Threading.Tasks; 8 | 9 | namespace Rtsp.Tests.Integration; 10 | 11 | /// 12 | /// RtspListenerIntegrationTests - These tests use the RtspTcpTransport and RtspListener to connect to a server (e.g. MediaMTX) 13 | /// These have been marked as Category Integration to allow filtration out of continuous integration pipelines 14 | /// To run these tests follow the guidelines on 15 | /// - Enable RTSPS in MediaMtx on port 8322 and rtsp on port 8554 16 | /// - Use Rest API to add a remote stream to MediaMtx on the /Test path 17 | /// - Use Wireshark to listen for traffic on local loopback with filter "tcp.port == 8322 || tcp.port == 8554" 18 | /// - Run Test 19 | /// - Observe TLS/SSL handshake and communication between client and server 20 | /// 21 | public class RTSPListenerIntegrationTests 22 | { 23 | private readonly Dictionary> _messageQueue = []; 24 | 25 | private readonly object _lock = new(); 26 | 27 | [TestCase("rtsp://localhost:8554/Test")] 28 | [TestCase("rtsps://localhost:8322/Test")] 29 | [Category("Integration")] 30 | [Explicit("These tests require a running RTSP server and are not suitable for CI pipelines")] 31 | public async Task SendOption_WhenSent_Receives200OK(string uri) 32 | { 33 | // arrange 34 | var socket = RtspUtils.CreateRtspTransportFromUrl(new(uri), AcceptAllCertificate); 35 | var listener = new RtspListener(socket); 36 | var taskCompletionSource = new TaskCompletionSource(); 37 | listener.MessageReceived += ListenerOnMessageReceived; 38 | listener.Start(); 39 | 40 | var message = new RtspRequestOptions 41 | { 42 | RtspUri = new Uri(uri) 43 | }; 44 | 45 | // act 46 | if (listener.SendMessage(message)) 47 | { 48 | lock (_lock) 49 | { 50 | _messageQueue.Add(message, taskCompletionSource); 51 | } 52 | 53 | var result = await taskCompletionSource.Task; 54 | 55 | Assert.That(result, Is.Not.Null); 56 | Assert.That(result.ReturnCode, Is.EqualTo(200)); 57 | } 58 | else 59 | { 60 | Assert.Fail("Unable to send message"); 61 | } 62 | } 63 | 64 | private bool AcceptAllCertificate(object? sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) 65 | { 66 | return true; 67 | } 68 | 69 | private void ListenerOnMessageReceived(object? sender, RtspChunkEventArgs e) 70 | { 71 | if (e.Message is not RtspResponse message || message.OriginalRequest is null) 72 | return; 73 | 74 | lock (_lock) 75 | { 76 | if (_messageQueue.TryGetValue(message.OriginalRequest, out TaskCompletionSource? value)) 77 | { 78 | value.SetResult(message); 79 | _messageQueue.Remove(message.OriginalRequest); 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /RTSP.Tests/Messages/PortCoupleTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Rtsp.Messages.Tests 4 | { 5 | [TestFixture()] 6 | public class PortCoupleTests 7 | { 8 | [Test()] 9 | public void PortCoupleOnePort() 10 | { 11 | var pc = new PortCouple(1212); 12 | Assert.That(pc.First, Is.EqualTo(1212)); 13 | Assert.That(pc.IsSecondPortPresent, Is.False); 14 | } 15 | 16 | [Test()] 17 | public void PortCoupleTwoPort() 18 | { 19 | var pc = new PortCouple(1212, 1215); 20 | Assert.That(pc.First, Is.EqualTo(1212)); 21 | Assert.That(pc.IsSecondPortPresent, Is.True); 22 | Assert.That(pc.Second, Is.EqualTo(1215)); 23 | } 24 | 25 | [Test()] 26 | public void ParseOnePort() 27 | { 28 | var pc = PortCouple.Parse("1212"); 29 | Assert.That(pc.First, Is.EqualTo(1212)); 30 | Assert.That(pc.IsSecondPortPresent, Is.False); 31 | } 32 | 33 | [Test()] 34 | public void ParseOneEqualPort() 35 | { 36 | var pc = PortCouple.Parse("1212-1212"); 37 | Assert.That(pc.First, Is.EqualTo(1212)); 38 | Assert.That(pc.IsSecondPortPresent, Is.False); 39 | } 40 | 41 | [Test()] 42 | public void ParseTwoPort() 43 | { 44 | var pc = PortCouple.Parse("1212-1215"); 45 | Assert.That(pc.First, Is.EqualTo(1212)); 46 | Assert.That(pc.IsSecondPortPresent, Is.True); 47 | Assert.That(pc.Second, Is.EqualTo(1215)); 48 | } 49 | 50 | [Test()] 51 | public void ToStringOnePort() 52 | { 53 | var pc = new PortCouple(1212); 54 | Assert.That(pc.ToString(), Is.EqualTo("1212")); 55 | } 56 | 57 | [Test()] 58 | public void ToStringTwoPort() 59 | { 60 | var pc = new PortCouple(1212, 1215); 61 | Assert.That(pc.ToString(), Is.EqualTo("1212-1215")); 62 | } 63 | 64 | [Test()] 65 | public void ToStringOneEqualPort() 66 | { 67 | var pc = PortCouple.Parse("1212-1212"); 68 | Assert.That(pc.ToString(), Is.EqualTo("1212")); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /RTSP.Tests/Messages/RTSPMessageTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | using NUnit.Framework.Interfaces; 5 | using NUnit.Framework.Internal; 6 | using NUnit.Framework.Internal.Builders; 7 | 8 | namespace Rtsp.Messages.Tests 9 | { 10 | [TestFixture] 11 | public class RtspMessageTest 12 | { 13 | //Put a name on test to permit VSNunit to handle them. 14 | [Test] 15 | [TestCase("OPTIONS * RTSP/1.0", RtspRequest.RequestType.OPTIONS, TestName = "GetRtspMessageRequest-OPTIONS")] 16 | [TestCase("SETUP rtsp://audio.example.com/twister/audio.en RTSP/1.0", RtspRequest.RequestType.SETUP, 17 | TestName = "GetRtspMessageRequest-SETUP")] 18 | [TestCase("PLAY rtsp://audio.example.com/twister/audio.en RTSP/1.0", RtspRequest.RequestType.PLAY, 19 | TestName = "GetRtspMessageRequest-PLAY")] 20 | public void GetRtspMessageRequest(string requestLine, RtspRequest.RequestType requestType) 21 | { 22 | RtspMessage oneMessage = RtspMessage.GetRtspMessage(requestLine); 23 | Assert.That(oneMessage, Is.InstanceOf()); 24 | 25 | var oneRequest = oneMessage as RtspRequest; 26 | Assert.That(oneRequest, Is.Not.Null); 27 | Assert.That(oneRequest.RequestTyped, Is.EqualTo(requestType)); 28 | } 29 | 30 | //Put a name on test to permit VSNunit to handle them. 31 | [Test] 32 | [TestCase("RTSP/1.0 551 Option not supported", 551, "Option not supported", 33 | TestName = "GetRtspMessageResponse-551")] 34 | public void GetRtspMessageResponse(string requestLine, int returnCode, string returnMessage) 35 | { 36 | RtspMessage oneMessage = RtspMessage.GetRtspMessage(requestLine); 37 | Assert.That(oneMessage, Is.InstanceOf()); 38 | 39 | var oneResponse = oneMessage as RtspResponse; 40 | Assert.That(oneResponse, Is.Not.Null); 41 | using (Assert.EnterMultipleScope()) 42 | { 43 | Assert.That(oneResponse.ReturnCode, Is.EqualTo(returnCode)); 44 | Assert.That(oneResponse.ReturnMessage, Is.EqualTo(returnMessage)); 45 | } 46 | } 47 | 48 | [Test] 49 | public void SeqWrite() 50 | { 51 | RtspMessage oneMessage = new() 52 | { 53 | CSeq = 123 54 | }; 55 | 56 | Assert.That(oneMessage.Headers["CSeq"], Is.EqualTo("123")); 57 | } 58 | #if NET8_0_OR_GREATER 59 | [Test] 60 | [GenericTestCase(RtspRequest.RequestType.OPTIONS)] 61 | [GenericTestCase(RtspRequest.RequestType.DESCRIBE)] 62 | [GenericTestCase(RtspRequest.RequestType.SETUP)] 63 | [GenericTestCase(RtspRequest.RequestType.PLAY)] 64 | [GenericTestCase(RtspRequest.RequestType.PAUSE)] 65 | [GenericTestCase(RtspRequest.RequestType.TEARDOWN)] 66 | [GenericTestCase(RtspRequest.RequestType.GET_PARAMETER)] 67 | [GenericTestCase(RtspRequest.RequestType.ANNOUNCE)] 68 | [GenericTestCase(RtspRequest.RequestType.RECORD)] 69 | public void CheckRequestType(RtspRequest.RequestType expectedType) where T : RtspRequest, new() 70 | { 71 | RtspRequest onMessage = new T(); 72 | Assert.That(onMessage.RequestTyped, Is.EqualTo(expectedType)); 73 | } 74 | 75 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 76 | private class GenericTestCaseAttribute(params object[] arguments) : TestCaseAttribute(arguments), ITestBuilder 77 | { 78 | IEnumerable ITestBuilder.BuildFrom(IMethodInfo method, Test? suite) 79 | { 80 | var testedMethod = method.IsGenericMethodDefinition ? method.MakeGenericMethod(typeof(T)) : method; 81 | return BuildFrom(testedMethod, suite); 82 | } 83 | } 84 | #endif 85 | } 86 | } -------------------------------------------------------------------------------- /RTSP.Tests/Messages/RtspDataTest.cs: -------------------------------------------------------------------------------- 1 | using NSubstitute; 2 | using NUnit.Framework; 3 | 4 | namespace Rtsp.Messages.Tests 5 | { 6 | [TestFixture] 7 | public class RtspDataTest 8 | { 9 | [Test] 10 | public void Clone() 11 | { 12 | RtspData testObject = new() 13 | { 14 | Channel = 1234, 15 | Data = new byte[] { 45, 63, 36, 42, 65, 00, 99 }, 16 | SourcePort = new RtspListener(Substitute.For()) 17 | }; 18 | var cloneObject = testObject.Clone() as RtspData; 19 | 20 | Assert.That(cloneObject, Is.Not.Null); 21 | using (Assert.EnterMultipleScope()) 22 | { 23 | Assert.That(cloneObject.Channel, Is.EqualTo(testObject.Channel)); 24 | Assert.That(cloneObject.Data, Is.EqualTo(testObject.Data)); 25 | Assert.That(cloneObject.SourcePort, Is.SameAs(testObject.SourcePort)); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /RTSP.Tests/Messages/RtspResponseTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Rtsp.Messages.Tests 4 | { 5 | [TestFixture()] 6 | public class RtspResponseTests 7 | { 8 | [Test()] 9 | public void SetSession() 10 | { 11 | var testObject = new RtspResponse 12 | { 13 | Session = "12345" 14 | }; 15 | 16 | Assert.That(testObject.Headers[RtspHeaderNames.Session], Is.EqualTo("12345")); 17 | } 18 | 19 | [Test()] 20 | public void SetSessionAndTimeout() 21 | { 22 | var testObject = new RtspResponse 23 | { 24 | Session = "12345", 25 | Timeout = 10 26 | }; 27 | 28 | Assert.That(testObject.Headers[RtspHeaderNames.Session], Is.EqualTo("12345;timeout=10")); 29 | } 30 | 31 | [Test()] 32 | public void ReadSessionAndDefaultTimeout() 33 | { 34 | var testObject = new RtspResponse(); 35 | 36 | testObject.Headers[RtspHeaderNames.Session] = "12345"; 37 | using (Assert.EnterMultipleScope()) 38 | { 39 | Assert.That(testObject.Session, Is.EqualTo("12345")); 40 | Assert.That(testObject.Timeout, Is.EqualTo(60)); 41 | } 42 | } 43 | 44 | [Test()] 45 | public void ReadSessionAndTimeout() 46 | { 47 | var testObject = new RtspResponse(); 48 | 49 | testObject.Headers[RtspHeaderNames.Session] = "12345;timeout=33"; 50 | using (Assert.EnterMultipleScope()) 51 | { 52 | Assert.That(testObject.Session, Is.EqualTo("12345")); 53 | Assert.That(testObject.Timeout, Is.EqualTo(33)); 54 | } 55 | } 56 | 57 | [Test()] 58 | public void ChangeTimeout() 59 | { 60 | var testObject = new RtspResponse(); 61 | 62 | testObject.Headers[RtspHeaderNames.Session] = "12345;timeout=29"; 63 | testObject.Timeout = 33; 64 | using (Assert.EnterMultipleScope()) 65 | { 66 | Assert.That(testObject.Session, Is.EqualTo("12345")); 67 | Assert.That(testObject.Timeout, Is.EqualTo(33)); 68 | Assert.That(testObject.Headers[RtspHeaderNames.Session], Is.EqualTo("12345;timeout=33")); 69 | } 70 | } 71 | 72 | [Test()] 73 | public void ChangeSession() 74 | { 75 | var testObject = new RtspResponse(); 76 | 77 | testObject.Headers[RtspHeaderNames.Session] = "12345;timeout=33"; 78 | 79 | testObject.Session = "456"; 80 | using (Assert.EnterMultipleScope()) 81 | { 82 | Assert.That(testObject.Session, Is.EqualTo("456")); 83 | Assert.That(testObject.Timeout, Is.EqualTo(33)); 84 | Assert.That(testObject.Headers[RtspHeaderNames.Session], Is.EqualTo("456;timeout=33")); 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /RTSP.Tests/Messages/RtspTransportTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Rtsp.Messages.Tests 4 | { 5 | [TestFixture] 6 | public class RtspTransportTest 7 | { 8 | /*Transport = "Transport" ":" 9 | 1\#transport-spec 10 | transport-spec = transport-protocol/profile[/lower-transport] 11 | *parameter 12 | transport-protocol = "RTP" 13 | profile = "AVP" 14 | lower-transport = "TCP" | "UDP" 15 | parameter = ( "unicast" | "multicast" ) 16 | | ";" "destination" [ "=" address ] 17 | | ";" "interleaved" "=" channel [ "-" channel ] 18 | | ";" "append" 19 | | ";" "ttl" "=" ttl 20 | | ";" "layers" "=" 1*DIGIT 21 | | ";" "port" "=" port [ "-" port ] 22 | | ";" "client_port" "=" port [ "-" port ] 23 | | ";" "server_port" "=" port [ "-" port ] 24 | | ";" "ssrc" "=" ssrc 25 | | ";" "mode" = <"> 1\#mode <"> 26 | ttl = 1*3(DIGIT) 27 | port = 1*5(DIGIT) 28 | ssrc = 8*8(HEX) 29 | channel = 1*3(DIGIT) 30 | address = host 31 | mode = <"> *Method <"> | Method 32 | 33 | */ 34 | 35 | [Test] 36 | public void DefaultValue() 37 | { 38 | RtspTransport testValue = new(); 39 | Assert.That(testValue.IsMulticast, Is.True); 40 | using (Assert.EnterMultipleScope()) 41 | { 42 | Assert.That(testValue.LowerTransport, Is.EqualTo(RtspTransport.LowerTransportType.UDP)); 43 | Assert.That(testValue.Mode, Is.EqualTo("PLAY")); 44 | } 45 | } 46 | 47 | [Test] 48 | public void ParseMinimal() 49 | { 50 | RtspTransport testValue = RtspTransport.Parse("RTP/AVP"); 51 | Assert.That(testValue.IsMulticast, Is.True); 52 | using (Assert.EnterMultipleScope()) 53 | { 54 | Assert.That(testValue.LowerTransport, Is.EqualTo(RtspTransport.LowerTransportType.UDP)); 55 | Assert.That(testValue.Mode, Is.EqualTo("PLAY")); 56 | } 57 | } 58 | 59 | [Test] 60 | public void Parse1() 61 | { 62 | RtspTransport testValue = RtspTransport.Parse("RTP/AVP/UDP;destination"); 63 | Assert.That(testValue.IsMulticast, Is.True); 64 | using (Assert.EnterMultipleScope()) 65 | { 66 | Assert.That(testValue.LowerTransport, Is.EqualTo(RtspTransport.LowerTransportType.UDP)); 67 | Assert.That(testValue.Mode, Is.EqualTo("PLAY")); 68 | } 69 | } 70 | 71 | [Test] 72 | public void Parse2() 73 | { 74 | // not realistic 75 | RtspTransport testValue = RtspTransport.Parse("RTP/AVP/TCP;multicast;destination=test.example.com;ttl=234;ssrc=cd3b20a5;mode=RECORD"); 76 | Assert.That(testValue.IsMulticast, Is.True); 77 | using (Assert.EnterMultipleScope()) 78 | { 79 | Assert.That(testValue.LowerTransport, Is.EqualTo(RtspTransport.LowerTransportType.TCP)); 80 | Assert.That(testValue.Destination, Is.EqualTo("test.example.com")); 81 | Assert.That(testValue.SSrc, Is.EqualTo("cd3b20a5")); 82 | Assert.That(testValue.Mode, Is.EqualTo("RECORD")); 83 | } 84 | } 85 | 86 | [Test] 87 | public void Parse3() 88 | { 89 | RtspTransport testValue = RtspTransport.Parse("RTP/AVP/TCP;interleaved=3-4"); 90 | Assert.That(testValue.IsMulticast, Is.False); 91 | using (Assert.EnterMultipleScope()) 92 | { 93 | Assert.That(testValue.LowerTransport, Is.EqualTo(RtspTransport.LowerTransportType.TCP)); 94 | Assert.That(testValue.Interleaved?.First, Is.EqualTo(3)); 95 | } 96 | using (Assert.EnterMultipleScope()) 97 | { 98 | Assert.That(testValue.Interleaved?.IsSecondPortPresent, Is.True); 99 | Assert.That(testValue.Interleaved?.Second, Is.EqualTo(4)); 100 | } 101 | } 102 | 103 | [Test] 104 | public void Parse4() 105 | { 106 | RtspTransport testValue = RtspTransport.Parse("RTP/AVP;unicast;destination=1.2.3.4;source=3.4.5.6;server_port=5000-5001;client_port=5003-5004"); 107 | using (Assert.EnterMultipleScope()) 108 | { 109 | Assert.That(testValue.IsMulticast, Is.False); 110 | Assert.That(testValue.ServerPort, Is.Not.Null); 111 | Assert.That(testValue.ClientPort, Is.Not.Null); 112 | } 113 | using (Assert.EnterMultipleScope()) 114 | { 115 | Assert.That(testValue.LowerTransport, Is.EqualTo(RtspTransport.LowerTransportType.UDP)); 116 | Assert.That(testValue.Destination, Is.EqualTo("1.2.3.4")); 117 | Assert.That(testValue.Source, Is.EqualTo("3.4.5.6")); 118 | Assert.That(testValue.ServerPort.First, Is.EqualTo(5000)); 119 | Assert.That(testValue.ServerPort.Second, Is.EqualTo(5001)); 120 | Assert.That(testValue.ClientPort.First, Is.EqualTo(5003)); 121 | Assert.That(testValue.ClientPort.Second, Is.EqualTo(5004)); 122 | } 123 | } 124 | 125 | [Test] 126 | public void ToStringTCP() 127 | { 128 | RtspTransport transport = new() 129 | { 130 | LowerTransport = RtspTransport.LowerTransportType.TCP, 131 | Interleaved = new PortCouple(0, 1), 132 | }; 133 | Assert.That(transport.ToString(), Is.EqualTo("RTP/AVP/TCP;unicast;interleaved=0-1")); 134 | } 135 | 136 | [Test] 137 | public void ToStringUDPUnicast() 138 | { 139 | RtspTransport transport = new() 140 | { 141 | LowerTransport = RtspTransport.LowerTransportType.UDP, 142 | IsMulticast = false, 143 | ClientPort = new PortCouple(5000, 5001), 144 | ServerPort = new PortCouple(5002, 5003), 145 | Destination = "1.2.3.4" 146 | }; 147 | Assert.That(transport.ToString(), Is.EqualTo("RTP/AVP/UDP;unicast;destination=1.2.3.4;client_port=5000-5001;server_port=5002-5003")); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /RTSP.Tests/Onvif/RtpPacketOnvifUtilsTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Rtsp.Onvif; 3 | using System; 4 | 5 | namespace Rtsp.Tests.Onvif 6 | { 7 | [TestFixture] 8 | public class RtpPacketOnvifUtilsTests 9 | { 10 | [Test] 11 | public void ProcessSimpleRTPTimestampExtensionTest() 12 | { 13 | byte[] extension = [ 14 | 0xAB, 0xAC, 0x00, 0x03, 15 | 0x00, 0x00, 0x00, 0x00, 16 | 0x00, 0x00, 0x00, 0x00, 17 | 0x00, 0x00, 0x00, 0x00, 18 | ]; 19 | 20 | var extensionSpan = new ReadOnlySpan(extension); 21 | var timestamp = RtpPacketOnvifUtils.ProcessRTPTimestampExtension(extensionSpan, out int headerPosition); 22 | Assert.That(timestamp, Is.EqualTo(new DateTime(1900, 01, 01))); 23 | extensionSpan = extensionSpan[headerPosition..]; 24 | Assert.That(extensionSpan.Length, Is.Zero); 25 | } 26 | 27 | [Test] 28 | public void ProcessSimpleRTPTimestampExtensionTestOtherExtension() 29 | { 30 | byte[] extension = [ 31 | 0xAA, 0xAA, 0x00, 0x00 32 | ]; 33 | 34 | var timestamp = RtpPacketOnvifUtils.ProcessRTPTimestampExtension(extension, out int headerPosition); 35 | using (Assert.EnterMultipleScope()) 36 | { 37 | Assert.That(timestamp, Is.EqualTo(DateTime.MinValue)); 38 | Assert.That(headerPosition, Is.Zero); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /RTSP.Tests/RTSP.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0;net472 4 | 12.0 5 | true 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | False 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | all 47 | runtime; build; native; contentfiles; analyzers; buildtransitive 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /RTSP.Tests/RTSPTests.nunit: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RTSP.Tests/RTSPUtilsTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | 4 | namespace Rtsp.Tests 5 | { 6 | [TestFixture] 7 | public class RtspUtilsTest 8 | { 9 | [Test] 10 | public void RegisterUri() 11 | { 12 | RtspUtils.RegisterUri(); 13 | 14 | // Check that rtsp is well registred 15 | Assert.That(Uri.CheckSchemeName("rtsp"), Is.True); 16 | 17 | // Check that the default port is well defined. 18 | Uri testUri = new("rtsp://exemple.com/test"); 19 | Assert.That(testUri.Port, Is.EqualTo(554)); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /RTSP.Tests/Rtp/Data/img_jpg_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngraziano/SharpRTSP/df649d34a4a10cdb991e97c7f1c6f9ad68e08172/RTSP.Tests/Rtp/Data/img_jpg_0.jpg -------------------------------------------------------------------------------- /RTSP.Tests/Rtp/Data/jpeg_0.rtp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngraziano/SharpRTSP/df649d34a4a10cdb991e97c7f1c6f9ad68e08172/RTSP.Tests/Rtp/Data/jpeg_0.rtp -------------------------------------------------------------------------------- /RTSP.Tests/Rtp/Data/jpeg_1.rtp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngraziano/SharpRTSP/df649d34a4a10cdb991e97c7f1c6f9ad68e08172/RTSP.Tests/Rtp/Data/jpeg_1.rtp -------------------------------------------------------------------------------- /RTSP.Tests/Rtp/Data/jpeg_2.rtp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngraziano/SharpRTSP/df649d34a4a10cdb991e97c7f1c6f9ad68e08172/RTSP.Tests/Rtp/Data/jpeg_2.rtp -------------------------------------------------------------------------------- /RTSP.Tests/Rtp/JpegPayloadTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Rtsp.Rtp; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace RTSP.Tests.Rtp 9 | { 10 | public class JpegPayloadTests 11 | { 12 | private readonly Assembly selfAssembly = Assembly.GetExecutingAssembly(); 13 | 14 | private RtpPacket ReadPacket(string resourceName) 15 | { 16 | using var rtpFile = selfAssembly.GetManifestResourceStream($"RTSP.Tests.Rtp.Data.{resourceName}.rtp"); 17 | Debug.Assert(rtpFile != null, "Missing test file"); 18 | using var testReader = new StreamReader(rtpFile); 19 | byte[] buffer = new byte[16 * 1024]; 20 | using var ms = new MemoryStream(); 21 | int read; 22 | while ((read = rtpFile!.Read(buffer, 0, buffer.Length)) > 0) 23 | { 24 | ms.Write(buffer, 0, read); 25 | } 26 | return new RtpPacket(ms.ToArray()); 27 | } 28 | 29 | private byte[] ReadBytes(string resourceName) 30 | { 31 | using var rtpFile = selfAssembly.GetManifestResourceStream($"RTSP.Tests.Rtp.Data.{resourceName}"); 32 | Debug.Assert(rtpFile != null, "Missing test file"); 33 | using var testReader = new StreamReader(rtpFile); 34 | byte[] buffer = new byte[16 * 1024]; 35 | using var ms = new MemoryStream(); 36 | int read; 37 | while ((read = rtpFile!.Read(buffer, 0, buffer.Length)) > 0) 38 | { 39 | ms.Write(buffer, 0, read); 40 | } 41 | return ms.ToArray(); 42 | } 43 | 44 | [Test] 45 | public void Read1() 46 | { 47 | JPEGPayload jpegPayloadParser = new(); 48 | 49 | var r0 = jpegPayloadParser.ProcessPacket(ReadPacket("jpeg_0")); 50 | var r1 = jpegPayloadParser.ProcessPacket(ReadPacket("jpeg_1")); 51 | var r2 = jpegPayloadParser.ProcessPacket(ReadPacket("jpeg_2")); 52 | using (Assert.EnterMultipleScope()) 53 | { 54 | Assert.That(r0.Data, Is.Empty); 55 | Assert.That(r1.Data, Is.Empty); 56 | Assert.That(r2.Data, Is.Not.Empty); 57 | } 58 | 59 | var jpeg = r2.Data.First(); 60 | var expected = ReadBytes("img_jpg_0.jpg"); 61 | 62 | Assert.That(jpeg.ToArray(), Is.EqualTo(expected)); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /RTSP.Tests/Rtp/RawMediaFrameTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace Rtsp.Rtp.Tests 6 | { 7 | [TestFixture()] 8 | public class RawMediaFrameTests 9 | { 10 | [Test()] 11 | public void AnyTest() 12 | { 13 | List> data = [ 14 | new byte[] {0x01 }.AsMemory(), 15 | ]; 16 | RawMediaFrame rawMediaFrame = new(data, []) { ClockTimestamp = DateTime.MinValue, RtpTimestamp = 0 }; 17 | Assert.That(rawMediaFrame.Any(), Is.True); 18 | } 19 | 20 | [Test()] 21 | public void AnyEmptyTest() 22 | { 23 | RawMediaFrame rawMediaFrame = RawMediaFrame.Empty; 24 | Assert.That(rawMediaFrame.Any(), Is.False); 25 | } 26 | 27 | [Test()] 28 | public void AnyDisposedTest() 29 | { 30 | { 31 | // Validate that Empty RawMediaFrame do not throw ObjectDisposedException when used multiple times 32 | using RawMediaFrame rawMediaFrame = RawMediaFrame.Empty; 33 | Assert.That(rawMediaFrame.Any(), Is.False); 34 | } 35 | { 36 | using RawMediaFrame rawMediaFrame = RawMediaFrame.Empty; 37 | Assert.That(rawMediaFrame.Any(), Is.False); 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /RTSP.Tests/Sdp/Data/test1.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=bob 2808844564 2808844564 IN IP4 host.biloxi.example.com 3 | s= 4 | c=IN IP4 host.biloxi.example.com 5 | t=0 0 6 | m=audio 49172 RTP/AVP 0 8 7 | a=rtpmap:0 PCMU/8000 8 | a=rtpmap:8 PCMA/8000 9 | m=video 0 RTP/AVP 31 10 | a=rtpmap:31 H261/90000 -------------------------------------------------------------------------------- /RTSP.Tests/Sdp/Data/test2.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Teleste 749719680 2684264576 IN IP4 172.16.200.193 3 | s=COD_9003-P2-0 4 | i=Teleste MPH H.264 Encoder - HK01121135 5 | c=IN IP4 232.16.200.207/16 6 | t=0 0 7 | r=0 0 8 | a=x-plgroup:COD_9003 9 | m=video 5008 RTP/AVP 98 10 | a=rtpmap:98 H264/90000 11 | a=fmtp:98 profile-level-id=42A01E; sprop-parameter-sets=Z01AH/QFgJP6,aP48gA==; packetization-mode=1; 12 | a=control:trackID=0 13 | -------------------------------------------------------------------------------- /RTSP.Tests/Sdp/Data/test3.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Teleste 3072471296 2857058560 IN IP4 172.18.200.200 3 | s=COD_9003-P3-0 4 | i=Teleste MPH H.264 Encoder - HK01121135 5 | c=IN IP4 232.16.200.209/16 6 | t=0 0 7 | r=0 0 8 | a=x-plgroup:COD_9003 9 | m=video 5008 RTP/AVP 26 10 | a=rtpmap:26 JPEG/90000 11 | a=control:trackID=0 12 | -------------------------------------------------------------------------------- /RTSP.Tests/Sdp/Data/test4.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1707291593123122 1 IN IP4 192.168.3.80 3 | s=profile1 4 | u=http:/// 5 | e=admin@ 6 | t=0 0 7 | a=control:* 8 | a=range:npt=00.000- 9 | m=video 0 RTP/AVP 96 10 | b=AS:5000 11 | a=control:track1 12 | a=rtpmap:96 H264/90000 13 | a=recvonly 14 | a=fmtp:96 profile-level-id=676400; sprop-parameter-sets=Z2QAKqwsaoHgCJ+WbgICAgQ=","aO4xshs=; packetization-mode=1 15 | m=audio 0 RTP/AVP 8 16 | b=AS:1000 17 | a=control:track2 18 | a=rtpmap:8 pcma/8000 19 | a=ptime:40 20 | a=recvonly 21 | -------------------------------------------------------------------------------- /RTSP.Tests/Sdp/Data/test5.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | c=IN IP4 0.0.0.0 3 | o=- 98969043 98969053 IN IP4 172.30.139.205 4 | s=Session99 5 | m=video 0 RTP/AVP 98 6 | c=IN IP4 0.0.0.0 7 | a=rtpmap:98 H264/90000 8 | a=fmtp:98 packetization-mode=1; profile-level-id=4d401f; sprop-parameter-sets=Z01AH42NQCgC3/gLcBAQFAAAD6AAALuDoYAGMsAAb5Qu8uNDAAxlgADfKF3lwoA=,aO44gA== 9 | a=framerate:6.25 10 | a=control:trackID=0 11 | a=recvonly -------------------------------------------------------------------------------- /RTSP.Tests/Sdp/Data/test6.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1109162014219182 0 IN IP4 0.0.0.0 3 | s=HIK Media Server V3.0.2 4 | i=HIK Media Server Session Description : standard 5 | e=NONE 6 | c=IN c=IN IP4 0.0.0.0 7 | t=0 0 8 | a=control:* 9 | a=range:npt=now- 10 | m=video 0 RTP/AVP 96 11 | a=rtpmap:96 H264/90000 12 | a=fmtp:96 profile-level-id=4D0014;packetization-mode=0;sprop-parameter-sets=Z2QAFK2EAQwgCGEAQwgCGEAQwgCEK3Cw/QgAAOpgAAr8hCA=,aO48sA== 13 | a=control:trackID=video 14 | a=Media_header:MEDIAINFO=494D4B48010100000400000100000000000000000000000000000000000000000000000000000000; 15 | a=appversion:1.0 -------------------------------------------------------------------------------- /RTSP.Tests/Sdp/Data/test7.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | c=IN IP6 FF1E:03AD::7F2E:172A:1E24 3 | o=- 98969043 98969053 IN IP6 2201:056D::112E:144A:1E24 4 | s=Session99 5 | m=video 0 RTP/AVP 98 6 | c=IN IP4 0.0.0.0 7 | a=rtpmap:98 H264/90000 8 | a=fmtp:98 packetization-mode=1; profile-level-id=4d401f; sprop-parameter-sets=Z01AH42NQCgC3/gLcBAQFAAAD6AAALuDoYAGMsAAb5Qu8uNDAAxlgADfKF3lwoA=,aO44gA== 9 | a=framerate:6.25 10 | a=control:trackID=0 11 | a=recvonly -------------------------------------------------------------------------------- /RTSP.Tests/Sdp/Data/test8.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | c=IN IP6 FF1E:03AD::7F2E:172A:1E24 3 | o=extra value - 98969043 98969053 IN IP6 2201:056D::112E:144A:1E24 4 | s=Session99 5 | m=video 0 RTP/AVP 98 6 | c=IN IP4 0.0.0.0 7 | a=rtpmap:98 H264/90000 8 | a=fmtp:98 packetization-mode=1; profile-level-id=4d401f; sprop-parameter-sets=Z01AH42NQCgC3/gLcBAQFAAAD6AAALuDoYAGMsAAb5Qu8uNDAAxlgADfKF3lwoA=,aO44gA== 9 | a=framerate:6.25 10 | a=control:trackID=0 11 | a=recvonly -------------------------------------------------------------------------------- /RTSP.Tests/Sdp/Data/test9.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | c=IN IP6 FF1E:03AD::7F2E:172A:1E24 3 | o=- 98969043 98969053 IN 4 | s=Session99 5 | m=video 0 RTP/AVP 98 6 | c=IN IP4 0.0.0.0 7 | a=rtpmap:98 H264/90000 8 | a=fmtp:98 packetization-mode=1; profile-level-id=4d401f; sprop-parameter-sets=Z01AH42NQCgC3/gLcBAQFAAAD6AAALuDoYAGMsAAb5Qu8uNDAAxlgADfKF3lwoA=,aO44gA== 9 | a=framerate:6.25 10 | a=control:trackID=0 11 | a=recvonly -------------------------------------------------------------------------------- /RTSP.Tests/Sdp/H264ParametersTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace Rtsp.Sdp.Tests 4 | { 5 | [TestFixture()] 6 | public class H264ParametersTests 7 | { 8 | [Test()] 9 | public void Parse() 10 | { 11 | var parsed = H264Parameters.Parse("profile-level-id=42A01E; sprop-parameter-sets=Z01AH/QFgJP6,aP48gA==; packetization-mode=1;"); 12 | 13 | Assert.That(parsed, Has.Count.EqualTo(3)); 14 | using (Assert.EnterMultipleScope()) 15 | { 16 | Assert.That(parsed["profile-level-id"], Is.EqualTo("42A01E")); 17 | Assert.That(parsed["packetization-mode"], Is.EqualTo("1")); 18 | } 19 | var sprop = parsed.SpropParameterSets; 20 | Assert.That(sprop, Has.Count.EqualTo(2)); 21 | 22 | byte[] result1 = [0x67, 0x4D, 0x40, 0x1F, 0xF4, 0x05, 0x80, 0x93, 0xFA]; 23 | Assert.That(sprop[0], Is.EqualTo(result1)); 24 | byte[] result2 = [0x68, 0xFE, 0x3C, 0x80]; 25 | Assert.That(sprop[1], Is.EqualTo(result2)); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /RTSP.Tests/TestUtils/InBlockingStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace RTSP.Tests.TestUtils 10 | { 11 | public class InBlockingStream : Stream 12 | { 13 | private readonly CancellationTokenSource cancellationTokenSource = new(); 14 | 15 | public override bool CanRead => !cancellationTokenSource.IsCancellationRequested; 16 | 17 | public override bool CanSeek => false; 18 | 19 | public override bool CanWrite => false; 20 | 21 | public override long Length => throw new NotSupportedException("Simulate NetworkStream"); 22 | 23 | public override long Position { get => throw new NotSupportedException("Simulate NetworkStream"); set => throw new NotSupportedException("Simulate NetworkStream"); } 24 | 25 | public override void Flush() { } 26 | 27 | public override int Read(byte[] buffer, int offset, int count) 28 | { 29 | // simulate blocking read for data 30 | // only unlock on close 31 | cancellationTokenSource.Token.WaitHandle.WaitOne(); 32 | throw new IOException("Stream closed"); 33 | } 34 | 35 | public override long Seek(long offset, SeekOrigin origin) 36 | { 37 | throw new NotSupportedException("Simulate NetworkStream"); 38 | } 39 | 40 | public override void SetLength(long value) 41 | { 42 | throw new NotSupportedException("Simulate NetworkStream"); 43 | } 44 | 45 | public override void Write(byte[] buffer, int offset, int count) 46 | { 47 | throw new NotSupportedException("Simulate only read"); 48 | } 49 | 50 | protected override void Dispose(bool disposing) 51 | { 52 | if (disposing) 53 | { 54 | cancellationTokenSource.Cancel(); 55 | cancellationTokenSource.Dispose(); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /RTSP.Tests/TestUtils/InOutStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace RTSP.Tests.TestUtils 5 | { 6 | public class InOutStream : Stream 7 | { 8 | public required Stream In { get; init; } 9 | public required Stream Out { get; init; } 10 | 11 | public override bool CanRead => In.CanRead; 12 | 13 | public override bool CanSeek => false; 14 | 15 | public override bool CanWrite => Out.CanWrite; 16 | 17 | public override long Length => throw new NotSupportedException("Simulate NetworkStream"); 18 | 19 | public override long Position 20 | { 21 | get => throw new NotSupportedException("Simulate NetworkStream"); 22 | set => throw new NotSupportedException("Simulate NetworkStream"); 23 | } 24 | 25 | public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException("Simulate NetworkStream"); 26 | 27 | public override void SetLength(long value) => throw new NotSupportedException("Simulate NetworkStream"); 28 | 29 | public override void Flush() 30 | { 31 | } 32 | 33 | public override int Read(byte[] buffer, int offset, int count) 34 | { 35 | return In.Read(buffer, offset, count); 36 | } 37 | 38 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) 39 | { 40 | return In.BeginRead(buffer, offset, count, callback, state); 41 | } 42 | 43 | public override int EndRead(IAsyncResult asyncResult) 44 | { 45 | return In.EndRead(asyncResult); 46 | } 47 | 48 | public override void Write(byte[] buffer, int offset, int count) 49 | { 50 | Out.Write(buffer, offset, count); 51 | } 52 | 53 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) 54 | { 55 | return Out.BeginWrite(buffer, offset, count, callback, state); 56 | } 57 | 58 | public override void EndWrite(IAsyncResult asyncResult) 59 | { 60 | Out.EndWrite(asyncResult); 61 | } 62 | 63 | protected override void Dispose(bool disposing) 64 | { 65 | if (disposing) 66 | { 67 | In.Dispose(); 68 | Out.Dispose(); 69 | } 70 | base.Dispose(disposing); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /RTSP.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32210.238 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RTSP", "RTSP\RTSP.csproj", "{0AD96152-EB0C-4F31-B4F4-583CE88A5046}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RTSP.Tests", "RTSP.Tests\RTSP.Tests.csproj", "{823924AE-1792-4F94-8FF1-D58A958EA5F4}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RtspMultiplexer", "RtspMultiplexer\RtspMultiplexer.csproj", "{A6A683BF-B8A4-4E62-85D2-08F4304F6CBB}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RtspClientExample", "RtspClientExample\RtspClientExample.csproj", "{5708B80A-8A16-4145-B0CA-3A317237DAC5}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RtspCameraExample", "RtspCameraExample\RtspCameraExample.csproj", "{DA8B426A-92A8-4944-B061-EC642EB0A4DA}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Éléments de solution", "Éléments de solution", "{3DADCBA5-0451-4C68-B8AB-F43ABE3321D5}" 17 | ProjectSection(SolutionItems) = preProject 18 | .editorconfig = .editorconfig 19 | EndProjectSection 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {0AD96152-EB0C-4F31-B4F4-583CE88A5046}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {0AD96152-EB0C-4F31-B4F4-583CE88A5046}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {0AD96152-EB0C-4F31-B4F4-583CE88A5046}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {0AD96152-EB0C-4F31-B4F4-583CE88A5046}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {823924AE-1792-4F94-8FF1-D58A958EA5F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {823924AE-1792-4F94-8FF1-D58A958EA5F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {823924AE-1792-4F94-8FF1-D58A958EA5F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {823924AE-1792-4F94-8FF1-D58A958EA5F4}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {A6A683BF-B8A4-4E62-85D2-08F4304F6CBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {A6A683BF-B8A4-4E62-85D2-08F4304F6CBB}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {A6A683BF-B8A4-4E62-85D2-08F4304F6CBB}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {A6A683BF-B8A4-4E62-85D2-08F4304F6CBB}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {5708B80A-8A16-4145-B0CA-3A317237DAC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {5708B80A-8A16-4145-B0CA-3A317237DAC5}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {5708B80A-8A16-4145-B0CA-3A317237DAC5}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {5708B80A-8A16-4145-B0CA-3A317237DAC5}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {DA8B426A-92A8-4944-B061-EC642EB0A4DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {DA8B426A-92A8-4944-B061-EC642EB0A4DA}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {DA8B426A-92A8-4944-B061-EC642EB0A4DA}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {DA8B426A-92A8-4944-B061-EC642EB0A4DA}.Release|Any CPU.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(SolutionProperties) = preSolution 49 | HideSolutionNode = FALSE 50 | EndGlobalSection 51 | GlobalSection(ExtensibilityGlobals) = postSolution 52 | SolutionGuid = {58F85D35-F013-42F0-97FE-78384008042A} 53 | EndGlobalSection 54 | EndGlobal 55 | -------------------------------------------------------------------------------- /RTSP/Authentication.cs: -------------------------------------------------------------------------------- 1 | using Rtsp.Messages; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net; 5 | 6 | namespace Rtsp 7 | { 8 | // WWW-Authentication and Authorization Headers 9 | public abstract class Authentication 10 | { 11 | public NetworkCredential Credentials { get; } 12 | 13 | protected Authentication(NetworkCredential credentials) 14 | { 15 | Credentials = credentials ?? throw new ArgumentNullException(nameof(credentials)); 16 | } 17 | 18 | public abstract string GetServerResponse(); 19 | public abstract string GetResponse(uint nonceCounter, string uri, string method, byte[] entityBodyBytes); 20 | public abstract bool IsValid(RtspRequest receivedMessage); 21 | 22 | public static Authentication Create(NetworkCredential credential, string authenticateHeader) 23 | { 24 | authenticateHeader = authenticateHeader ?? 25 | throw new ArgumentNullException(nameof(authenticateHeader)); 26 | 27 | if (authenticateHeader.StartsWith("Basic", StringComparison.OrdinalIgnoreCase)) 28 | { 29 | int spaceIndex = authenticateHeader.IndexOf(' ', StringComparison.Ordinal); 30 | 31 | if (spaceIndex != -1) 32 | { 33 | string parameters = authenticateHeader[++spaceIndex..]; 34 | 35 | Dictionary parameterNameToValueMap = ParseParameters(parameters); 36 | if (!parameterNameToValueMap.TryGetValue("REALM", out var realm) || realm is null) 37 | throw new ArgumentException("\"realm\" parameter is not found in header", nameof(authenticateHeader)); 38 | return new AuthenticationBasic(credential, realm); 39 | } 40 | } 41 | 42 | if (authenticateHeader.StartsWith("Digest", StringComparison.OrdinalIgnoreCase)) 43 | { 44 | int spaceIndex = authenticateHeader.IndexOf(' ', StringComparison.Ordinal); 45 | 46 | if (spaceIndex != -1) 47 | { 48 | string parameters = authenticateHeader[++spaceIndex..]; 49 | 50 | Dictionary parameterNameToValueMap = ParseParameters(parameters); 51 | 52 | if (!parameterNameToValueMap.TryGetValue("REALM", out var realm) || realm is null) 53 | throw new ArgumentException("\"realm\" parameter is not found in header", nameof(authenticateHeader)); 54 | if (!parameterNameToValueMap.TryGetValue("NONCE", out var nonce) || nonce is null) 55 | throw new ArgumentException("\"nonce\" parameter is not found in header", nameof(authenticateHeader)); 56 | 57 | parameterNameToValueMap.TryGetValue("QOP", out var qop); 58 | return new AuthenticationDigest(credential, realm, nonce, qop); 59 | } 60 | } 61 | 62 | throw new ArgumentOutOfRangeException(nameof(authenticateHeader), 63 | $"Invalid authenticate header: {authenticateHeader}"); 64 | } 65 | 66 | private static Dictionary ParseParameters(string parameters) 67 | { 68 | Dictionary parameterNameToValueMap = new(StringComparer.OrdinalIgnoreCase); 69 | 70 | int parameterStartOffset = 0; 71 | while (parameterStartOffset < parameters.Length) 72 | { 73 | int equalsSignIndex = parameters.IndexOf('=', parameterStartOffset); 74 | 75 | if (equalsSignIndex == -1) { break; } 76 | 77 | int parameterNameLength = equalsSignIndex - parameterStartOffset; 78 | string parameterName = parameters.Substring(parameterStartOffset, parameterNameLength).Trim().ToUpperInvariant(); 79 | 80 | ++equalsSignIndex; 81 | 82 | int nonSpaceIndex = equalsSignIndex; 83 | 84 | if (nonSpaceIndex == parameters.Length) { break; } 85 | 86 | while (parameters[nonSpaceIndex] == ' ') 87 | { 88 | if (++nonSpaceIndex == parameters.Length) 89 | { break; } 90 | } 91 | 92 | int parameterValueStartPos; 93 | int parameterValueEndPos; 94 | int commaIndex; 95 | 96 | if (parameters[nonSpaceIndex] == '\"') 97 | { 98 | parameterValueStartPos = parameters.IndexOf('\"', equalsSignIndex); 99 | 100 | if (parameterValueStartPos == -1) { break; } 101 | 102 | ++parameterValueStartPos; 103 | 104 | parameterValueEndPos = parameters.IndexOf('\"', parameterValueStartPos); 105 | 106 | if (parameterValueEndPos == -1) { break; } 107 | 108 | commaIndex = parameters.IndexOf(',', parameterValueEndPos + 1); 109 | 110 | parameterStartOffset = commaIndex != -1 ? ++commaIndex : parameters.Length; 111 | } 112 | else 113 | { 114 | parameterValueStartPos = nonSpaceIndex; 115 | 116 | commaIndex = parameters.IndexOf(',', ++nonSpaceIndex); 117 | 118 | if (commaIndex != -1) 119 | { 120 | parameterValueEndPos = commaIndex; 121 | parameterStartOffset = ++commaIndex; 122 | } 123 | else 124 | { 125 | parameterValueEndPos = parameters.Length; 126 | parameterStartOffset = parameterValueEndPos; 127 | } 128 | } 129 | 130 | int parameterValueLength = parameterValueEndPos - parameterValueStartPos; 131 | parameterNameToValueMap[parameterName] = parameters.Substring(parameterValueStartPos, parameterValueLength); 132 | } 133 | 134 | return parameterNameToValueMap; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /RTSP/AuthenticationBasic.cs: -------------------------------------------------------------------------------- 1 | using Rtsp.Messages; 2 | using System; 3 | using System.Net; 4 | using System.Text; 5 | 6 | namespace Rtsp 7 | { 8 | public class AuthenticationBasic : Authentication 9 | { 10 | public const string AUTHENTICATION_PREFIX = "Basic "; 11 | 12 | private readonly string _realm; 13 | 14 | public AuthenticationBasic(NetworkCredential credentials, string realm) : base(credentials) 15 | { 16 | _realm = realm ?? throw new ArgumentNullException(nameof(realm)); 17 | } 18 | 19 | public override string GetResponse(uint nonceCounter, string uri, string method, 20 | byte[] entityBodyBytes) 21 | { 22 | string usernamePasswordHash = $"{Credentials.UserName}:{Credentials.Password}"; 23 | return AUTHENTICATION_PREFIX + Convert.ToBase64String(Encoding.UTF8.GetBytes(usernamePasswordHash)); 24 | } 25 | 26 | public override string GetServerResponse() 27 | { 28 | return $"{AUTHENTICATION_PREFIX}realm=\"{_realm}\""; 29 | } 30 | 31 | public override bool IsValid(RtspRequest receivedMessage) 32 | { 33 | string? authorization = receivedMessage.Headers["Authorization"]; 34 | 35 | // Check Username and Password 36 | if (authorization?.StartsWith(AUTHENTICATION_PREFIX, StringComparison.OrdinalIgnoreCase) != true) 37 | { 38 | return false; 39 | } 40 | // remove 'Basic ' 41 | string base64Str = authorization[AUTHENTICATION_PREFIX.Length..]; 42 | string decoded; 43 | try 44 | { 45 | byte[] data = Convert.FromBase64String(base64Str); 46 | decoded = Encoding.UTF8.GetString(data); 47 | } 48 | catch 49 | { 50 | return false; 51 | } 52 | 53 | return decoded.Split(':', 2) switch 54 | { 55 | [string username, string password] => string.Equals(username, Credentials.UserName, StringComparison.OrdinalIgnoreCase) 56 | && string.Equals(password, Credentials.Password, StringComparison.Ordinal), 57 | _ => false, 58 | }; 59 | } 60 | public override string ToString() => "Authentication Basic"; 61 | } 62 | } -------------------------------------------------------------------------------- /RTSP/AuthenticationDigest.cs: -------------------------------------------------------------------------------- 1 | using Rtsp.Messages; 2 | using System; 3 | using System.Globalization; 4 | using System.Net; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | 8 | namespace Rtsp 9 | { 10 | // WWW-Authentication and Authorization Headers 11 | public class AuthenticationDigest : Authentication 12 | { 13 | private readonly string _realm; 14 | private readonly string _nonce; 15 | private readonly string? _qop; 16 | private readonly string _cnonce; 17 | 18 | public AuthenticationDigest(NetworkCredential credentials, string realm, string nonce, string? qop) : base(credentials) 19 | { 20 | _realm = realm ?? throw new ArgumentNullException(nameof(realm)); 21 | _nonce = nonce ?? throw new ArgumentNullException(nameof(nonce)); 22 | 23 | if (!string.IsNullOrEmpty(qop)) 24 | { 25 | int commaIndex = qop!.IndexOf(',', StringComparison.OrdinalIgnoreCase); 26 | _qop = commaIndex > -1 ? qop![..commaIndex] : qop; 27 | } 28 | uint cnonce = (uint)Guid.NewGuid().GetHashCode(); 29 | _cnonce = cnonce.ToString("X8"); 30 | } 31 | 32 | public override string GetServerResponse() 33 | { 34 | //TODO implement correctly 35 | return $"Digest realm=\"{_realm}\", nonce=\"{_nonce}\""; 36 | } 37 | 38 | public override string GetResponse(uint nonceCounter, string uri, string method, 39 | byte[] entityBodyBytes) 40 | { 41 | MD5 md5 = MD5.Create(); 42 | string ha1 = CalculateMD5Hash(md5, $"{Credentials.UserName}:{_realm}:{Credentials.Password}"); 43 | string ha2Argument = $"{method}:{uri}"; 44 | bool hasQop = !string.IsNullOrEmpty(_qop); 45 | 46 | if (hasQop && _qop!.Equals("auth-int", StringComparison.InvariantCultureIgnoreCase)) 47 | { 48 | ha2Argument = $"{ha2Argument}:{CalculateMD5Hash(md5, entityBodyBytes)}"; 49 | } 50 | string ha2 = CalculateMD5Hash(md5, ha2Argument); 51 | 52 | StringBuilder sb = new(); 53 | sb.AppendFormat(CultureInfo.InvariantCulture, "Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\"", Credentials.UserName, _realm, _nonce, uri); 54 | if (!hasQop) 55 | { 56 | string response = CalculateMD5Hash(md5, $"{ha1}:{_nonce}:{ha2}"); 57 | sb.AppendFormat(CultureInfo.InvariantCulture, ", response=\"{0}\"", response); 58 | } 59 | else 60 | { 61 | string response = CalculateMD5Hash(md5, $"{ha1}:{_nonce}:{nonceCounter:X8}:{_cnonce}:{_qop}:{ha2}"); 62 | sb.AppendFormat(CultureInfo.InvariantCulture, ", response=\"{0}\", cnonce=\"{1}\", nc=\"{2:X8}\", qop=\"{3}\"", response, _cnonce, nonceCounter, _qop); 63 | } 64 | return sb.ToString(); 65 | } 66 | public override bool IsValid(RtspRequest receivedMessage) 67 | { 68 | string? authorization = receivedMessage.Headers["Authorization"]; 69 | 70 | // Check Username, URI, Nonce and the MD5 hashed Response 71 | if (authorization?.StartsWith("Digest ", StringComparison.Ordinal) == true) 72 | { 73 | // remove 'Digest ' 74 | var valueStr = authorization[7..]; 75 | string? username = null; 76 | string? realm = null; 77 | string? nonce = null; 78 | string? uri = null; 79 | string? response = null; 80 | 81 | foreach (string value in valueStr.Split(',')) 82 | { 83 | string[] tuple = value.Trim().Split('=', 2); 84 | if (tuple.Length != 2) 85 | { 86 | continue; 87 | } 88 | string var = tuple[1].Trim([' ', '\"']); 89 | if (tuple[0].Equals("username", StringComparison.OrdinalIgnoreCase)) 90 | { 91 | username = var; 92 | } 93 | else if (tuple[0].Equals("realm", StringComparison.OrdinalIgnoreCase)) 94 | { 95 | realm = var; 96 | } 97 | else if (tuple[0].Equals("nonce", StringComparison.OrdinalIgnoreCase)) 98 | { 99 | nonce = var; 100 | } 101 | else if (tuple[0].Equals("uri", StringComparison.OrdinalIgnoreCase)) 102 | { 103 | uri = var; 104 | } 105 | else if (tuple[0].Equals("response", StringComparison.OrdinalIgnoreCase)) 106 | { 107 | response = var; 108 | } 109 | } 110 | 111 | // Create the MD5 Hash using all parameters passed in the Auth Header with the 112 | // addition of the 'Password' 113 | MD5 md5 = MD5.Create(); 114 | string hashA1 = CalculateMD5Hash(md5, username + ":" + realm + ":" + Credentials.Password); 115 | string hashA2 = CalculateMD5Hash(md5, receivedMessage.RequestTyped + ":" + uri); 116 | string expectedResponse = CalculateMD5Hash(md5, hashA1 + ":" + nonce + ":" + hashA2); 117 | 118 | // Check if everything matches 119 | // ToDo - extract paths from the URIs (ignoring SETUP's trackID) 120 | return (string.Equals(username, Credentials.UserName, StringComparison.OrdinalIgnoreCase)) 121 | && (string.Equals(realm, _realm, StringComparison.OrdinalIgnoreCase)) 122 | && (string.Equals(nonce, _nonce, StringComparison.OrdinalIgnoreCase)) 123 | && (string.Equals(response, expectedResponse, StringComparison.OrdinalIgnoreCase)); 124 | } 125 | return false; 126 | } 127 | 128 | // MD5 (lower case) 129 | private static string CalculateMD5Hash(MD5 md5_session, string input) 130 | { 131 | byte[] inputBytes = Encoding.UTF8.GetBytes(input); 132 | return CalculateMD5Hash(md5_session, inputBytes); 133 | } 134 | private static string CalculateMD5Hash(MD5 md5_session, byte[] input) 135 | { 136 | byte[] hash = md5_session.ComputeHash(input); 137 | 138 | var output = new StringBuilder(); 139 | foreach (var t in hash) 140 | { 141 | output.Append(t.ToString("x2")); 142 | } 143 | 144 | return output.ToString(); 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /RTSP/BitStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | // (c) 2018 Roger Hardiman, RJH Technical Consultancy Ltd 6 | // Simple class to Read and Write bits in a bit stream. 7 | // Data is written to the end of the bit stream and the bit stream can be returned as a Byte Array 8 | // Data can be read from the head of the bit stream 9 | // Example 10 | // bitstream.AddValue(0xA,4); // Write 4 bit value 11 | // bitstream.AddValue(0xB,4); 12 | // bitstream.AddValue(0xC,4); 13 | // bitstream.AddValue(0xD,4); 14 | // bitstream.ToArray() -> {0xAB, 0xCD} // Return Byte Array 15 | // bitstream.Read(8) -> 0xAB // Read 8 bit value 16 | 17 | namespace Rtsp 18 | { 19 | // Very simple bitstream 20 | public class BitStream 21 | { 22 | /// 23 | /// List only stores 0 or 1 (one 'bit' per List item) 24 | /// 25 | private readonly List data = []; 26 | 27 | public void AddValue(int value, int num_bits) 28 | { 29 | // Add each bit to the List 30 | for (int i = num_bits - 1; i >= 0; i--) 31 | { 32 | data.Add((byte)((value >> i) & 0x01)); 33 | } 34 | } 35 | 36 | public void AddHexString(string hexString) 37 | { 38 | foreach (var c in hexString) 39 | { 40 | var value = c switch 41 | { 42 | >= 'a' and <= 'f' => c - 'a' + 10, 43 | >= 'A' and <= 'F' => c - 'A' + 10, 44 | >= '0' and <= '9' => c - '0', 45 | _ => throw new ArgumentException("Invalid hex character", nameof(hexString)), 46 | }; 47 | AddValue(value, 4); 48 | } 49 | } 50 | 51 | public int Read(int num_bits) 52 | { 53 | // Read and remove items from the front of the list of bits 54 | if (data.Count < num_bits) 55 | { 56 | throw new InvalidOperationException("Not enough bits to read"); 57 | } 58 | 59 | int result = data 60 | .Take(num_bits) 61 | .Aggregate(0, (agg, value) => (agg << 1) + value); 62 | data.RemoveRange(0, num_bits); 63 | 64 | return result; 65 | } 66 | 67 | public byte[] ToArray() 68 | { 69 | // number of byte rounded up 70 | int num_bytes = (data.Count + 7) / 8; 71 | byte[] array = new byte[num_bytes]; 72 | int ptr = 0; 73 | int shift = 7; 74 | for (int i = 0; i < data.Count; i++) 75 | { 76 | array[ptr] += (byte)(data[i] << shift); 77 | if (shift == 0) 78 | { 79 | shift = 7; 80 | ptr++; 81 | } 82 | else 83 | { 84 | shift--; 85 | } 86 | } 87 | 88 | return array; 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /RTSP/HeadersParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.IO; 4 | 5 | namespace Rtsp 6 | { 7 | internal static class HeadersParser 8 | { 9 | public static NameValueCollection ParseHeaders(StreamReader headersReader) 10 | { 11 | NameValueCollection headers = new(StringComparer.InvariantCultureIgnoreCase); 12 | string? header; 13 | while (!string.IsNullOrEmpty(header = headersReader.ReadLine())) 14 | { 15 | int colonPos = header.IndexOf(':', StringComparison.InvariantCulture); 16 | if (colonPos == -1) { continue; } 17 | string key = header[..colonPos].Trim().ToUpperInvariant(); 18 | string value = header[++colonPos..].Trim(); 19 | 20 | headers.Add(key, value); 21 | } 22 | return headers; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /RTSP/HttpBadResponseCodeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace Rtsp 5 | { 6 | [Serializable] 7 | public class HttpBadResponseCodeException : Exception 8 | { 9 | public HttpStatusCode Code { get; } 10 | 11 | public HttpBadResponseCodeException(HttpStatusCode code) : base($"Bad response code: {code}") 12 | { 13 | Code = code; 14 | } 15 | 16 | public HttpBadResponseCodeException() { } 17 | 18 | public HttpBadResponseCodeException(string? message) : base(message) 19 | { 20 | } 21 | 22 | public HttpBadResponseCodeException(string? message, Exception? innerException) : base(message, innerException) 23 | { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RTSP/HttpBadResponseException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Rtsp 4 | { 5 | [Serializable] 6 | public class HttpBadResponseException : Exception 7 | { 8 | public HttpBadResponseException() 9 | { 10 | } 11 | 12 | public HttpBadResponseException(string message) : base(message) 13 | { 14 | } 15 | 16 | public HttpBadResponseException(string message, Exception inner) : base(message, inner) 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /RTSP/IRTSPTransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace Rtsp 5 | { 6 | /// 7 | /// Interface for Transport of Rtsp (TCP, TCP+SSL,..) 8 | /// 9 | public interface IRtspTransport 10 | { 11 | /// 12 | /// Gets the stream of the transport. 13 | /// 14 | /// A stream 15 | System.IO.Stream GetStream(); 16 | 17 | [Obsolete("Get the address from the RemoteEndPoint instead.")] 18 | /// 19 | /// Gets the remote address. 20 | /// 21 | /// The remote address. 22 | /// This property actually returns an IP:Port pair or a URI, depending on the underlying transport. 23 | string RemoteAddress { get; } 24 | 25 | /// 26 | /// Gets the remote endpoint. 27 | /// 28 | /// The remote endpoint. 29 | IPEndPoint RemoteEndPoint { get; } 30 | 31 | /// 32 | /// Gets the remote endpoint. 33 | /// 34 | /// The remote endpoint. 35 | IPEndPoint LocalEndPoint { get; } 36 | 37 | /// 38 | /// Get next command index. Increment at each call. 39 | /// 40 | uint NextCommandIndex(); 41 | 42 | /// 43 | /// Closes this instance. 44 | /// 45 | void Close(); 46 | 47 | /// 48 | /// Gets a value indicating whether this is connected. 49 | /// 50 | /// if connected; otherwise, . 51 | bool Connected { get; } 52 | 53 | /// 54 | /// Reconnect this instance. 55 | /// Must do nothing if already connected. 56 | /// 57 | /// Error during socket 58 | void Reconnect(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /RTSP/IRtpTransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Rtsp 5 | { 6 | public interface IRtpTransport : IDisposable 7 | { 8 | event EventHandler? DataReceived; 9 | event EventHandler? ControlReceived; 10 | 11 | void Start(); 12 | void Stop(); 13 | 14 | /// 15 | /// Write to the RTP Control Port 16 | /// 17 | /// Buffer to send 18 | void WriteToControlPort(ReadOnlySpan data); 19 | Task WriteToControlPortAsync(ReadOnlyMemory data); 20 | 21 | /// 22 | /// Write to the RTP Data Port 23 | /// 24 | /// Buffer to send 25 | void WriteToDataPort(ReadOnlySpan data); 26 | Task WriteToDataPortAsync(ReadOnlyMemory data); 27 | } 28 | } -------------------------------------------------------------------------------- /RTSP/Messages/PortCouple.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages; 2 | 3 | using System; 4 | using System.Diagnostics.Contracts; 5 | using System.Globalization; 6 | 7 | /// 8 | /// Describe a couple of port used to transfer video and command. 9 | /// 10 | public class PortCouple 11 | { 12 | /// 13 | /// Gets or sets the first port number. 14 | /// 15 | /// The first port. 16 | public int First { get; set; } 17 | /// 18 | /// Gets or sets the second port number. 19 | /// 20 | /// If not present the value is 0 21 | /// The second port. 22 | public int Second { get; set; } 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | public PortCouple() 28 | { } 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// 32 | /// The first port. 33 | public PortCouple(int first) 34 | { 35 | First = first; 36 | Second = 0; 37 | } 38 | /// 39 | /// Initializes a new instance of the class. 40 | /// 41 | /// The first port. 42 | /// The second port. 43 | public PortCouple(int first, int second) 44 | { 45 | First = first; 46 | Second = second; 47 | } 48 | 49 | /// 50 | /// Gets a value indicating whether this instance has second port. 51 | /// 52 | /// 53 | /// true if this instance has second port; otherwise, false. 54 | /// 55 | public bool IsSecondPortPresent => Second != 0; 56 | 57 | /// 58 | /// Parses the int values of port. 59 | /// 60 | /// A string value. 61 | /// The port couple 62 | /// is null. 63 | public static PortCouple Parse(string stringValue) 64 | { 65 | if (stringValue == null) 66 | throw new ArgumentNullException(nameof(stringValue)); 67 | Contract.Requires(!string.IsNullOrEmpty(stringValue)); 68 | 69 | var values = stringValue.Split('-'); 70 | 71 | _ = int.TryParse(values[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int tempValue); 72 | PortCouple result = new(tempValue); 73 | 74 | tempValue = 0; 75 | if (values.Length > 1) 76 | { 77 | _ = int.TryParse(values[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out tempValue); 78 | 79 | // this check is needed because some Hanwha's nvr returns a 1-1 as interleaved string, resulting in a 1-1 port couple 80 | // will set up ports as 1-0 81 | if (tempValue == result.First) { tempValue = 0; } 82 | } 83 | 84 | result.Second = tempValue; 85 | 86 | return result; 87 | } 88 | 89 | /// 90 | /// Returns a that represents this instance. 91 | /// 92 | /// 93 | /// A that represents this instance. 94 | /// 95 | public override string ToString() 96 | { 97 | return IsSecondPortPresent ? FormattableString.Invariant($"{First}-{Second}") : First.ToString(CultureInfo.InvariantCulture); 98 | } 99 | } -------------------------------------------------------------------------------- /RTSP/Messages/RTSPChunk.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages; 2 | 3 | using System; 4 | 5 | /// 6 | /// Class which represent each message exchanged on Rtsp socket. 7 | /// 8 | public abstract class RtspChunk : ICloneable 9 | { 10 | /// 11 | /// Gets or sets the data associate with the message. 12 | /// 13 | /// Array of byte transmit with the message. 14 | public virtual Memory Data { get; set; } = Memory.Empty; 15 | 16 | /// 17 | /// Gets or sets the source port which receive the message. 18 | /// 19 | /// The source port. 20 | public RtspListener? SourcePort { get; set; } 21 | 22 | public abstract object Clone(); 23 | } -------------------------------------------------------------------------------- /RTSP/Messages/RTSPData.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages; 2 | 3 | using System; 4 | using System.Buffers; 5 | using System.Text; 6 | 7 | /// 8 | /// Message which represent data. ($ limited message) 9 | /// 10 | public sealed class RtspData : RtspChunk, IDisposable 11 | { 12 | private IMemoryOwner? _reservedData; 13 | private bool _disposedValue; 14 | 15 | public RtspData() { } 16 | 17 | public RtspData(IMemoryOwner reservedData, int size) 18 | { 19 | _reservedData = reservedData; 20 | base.Data = reservedData.Memory[..size]; 21 | } 22 | 23 | public override Memory Data 24 | { 25 | get => base.Data; 26 | set 27 | { 28 | if (_reservedData != null) 29 | { 30 | _reservedData.Dispose(); 31 | _reservedData = null; 32 | } 33 | base.Data = value; 34 | } 35 | } 36 | 37 | /// 38 | /// Create a string of the message for debug. 39 | /// 40 | public override string ToString() 41 | { 42 | var stringBuilder = new StringBuilder(); 43 | stringBuilder.AppendLine("Data message"); 44 | stringBuilder.AppendLine(Data.IsEmpty ? "Data : null" : $"Data length :-{Data.Length}-"); 45 | 46 | return stringBuilder.ToString(); 47 | } 48 | 49 | public int Channel { get; set; } 50 | 51 | /// 52 | /// Clones this instance. 53 | /// Listener is not cloned 54 | /// 55 | /// a clone of this instance 56 | public override object Clone() => new RtspData 57 | { 58 | Channel = Channel, 59 | SourcePort = SourcePort, 60 | Data = Data, 61 | }; 62 | 63 | private void Dispose(bool disposing) 64 | { 65 | if (_disposedValue) return; 66 | if (disposing) 67 | { 68 | _reservedData?.Dispose(); 69 | } 70 | Data = Memory.Empty; 71 | _disposedValue = true; 72 | } 73 | 74 | public void Dispose() 75 | { 76 | Dispose(disposing: true); 77 | GC.SuppressFinalize(this); 78 | } 79 | } -------------------------------------------------------------------------------- /RTSP/Messages/RTSPHeaderNames.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages; 2 | 3 | /// 4 | /// Class containing helper constant for general use headers. 5 | /// 6 | public static class RtspHeaderNames 7 | { 8 | public const string ContentBase = "Content-Base"; 9 | public const string ContentEncoding = "Content-Encoding"; 10 | public const string ContentType = "Content-Type"; 11 | 12 | public const string Public = "Public"; 13 | public const string Session = "Session"; 14 | public const string Transport = "Transport"; 15 | public const string CSeq = "CSeq"; 16 | 17 | public const string WWWAuthenticate = "WWW-Authenticate"; 18 | public const string Authorization = "Authorization"; 19 | 20 | public const string RateControl = "Rate-Control"; 21 | public const string Require = "Require"; 22 | 23 | public const string Range = "Range"; 24 | public const string Scale = "Scale"; 25 | } 26 | -------------------------------------------------------------------------------- /RTSP/Messages/RTSPHeaderUtils.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages; 2 | 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | public static class RTSPHeaderUtils 7 | { 8 | public static IList ParsePublicHeader(string? headerValue) => 9 | string.IsNullOrEmpty(headerValue) ? [] : headerValue!.Split(',').Select(m => m.Trim()).ToList(); 10 | 11 | public static IList ParsePublicHeader(RtspResponse response) 12 | => ParsePublicHeader(response.Headers.TryGetValue(RtspHeaderNames.Public, out var value) ? value : null); 13 | } -------------------------------------------------------------------------------- /RTSP/Messages/RTSPRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Rtsp.Messages 4 | { 5 | /// 6 | /// An Rtsp Request 7 | /// 8 | public class RtspRequest : RtspMessage 9 | { 10 | /// 11 | /// Request type. 12 | /// 13 | public enum RequestType 14 | { 15 | UNKNOWN, 16 | DESCRIBE, 17 | ANNOUNCE, 18 | GET_PARAMETER, 19 | OPTIONS, 20 | PAUSE, 21 | PLAY, 22 | RECORD, 23 | REDIRECT, 24 | SETUP, 25 | SET_PARAMETER, 26 | TEARDOWN, 27 | } 28 | 29 | /// 30 | /// Parses the request command. 31 | /// 32 | /// A string request command. 33 | /// The typed request. 34 | internal static RequestType ParseRequest(string aStringRequest) 35 | { 36 | if (!Enum.TryParse(aStringRequest, ignoreCase: true, out RequestType returnValue)) 37 | returnValue = RequestType.UNKNOWN; 38 | return returnValue; 39 | } 40 | 41 | /// 42 | /// Gets the Rtsp request. 43 | /// 44 | /// A request parts. 45 | /// the parsed request 46 | internal static RtspMessage GetRtspRequest(string[] aRequestParts) 47 | { 48 | return ParseRequest(aRequestParts[0]) switch 49 | { 50 | RequestType.OPTIONS => new RtspRequestOptions(), 51 | RequestType.DESCRIBE => new RtspRequestDescribe(), 52 | RequestType.SETUP => new RtspRequestSetup(), 53 | RequestType.PLAY => new RtspRequestPlay(), 54 | RequestType.PAUSE => new RtspRequestPause(), 55 | RequestType.TEARDOWN => new RtspRequestTeardown(), 56 | RequestType.GET_PARAMETER => new RtspRequestGetParameter(), 57 | RequestType.ANNOUNCE => new RtspRequestAnnounce(), 58 | RequestType.RECORD => new RtspRequestRecord(), 59 | /* 60 | RequestType.REDIRECT => new RtspRequestRedirect(), 61 | RequestType.SET_PARAMETER => new RtspRequestSetParameter(), 62 | */ 63 | _ => new RtspRequest(), 64 | }; 65 | } 66 | 67 | /// 68 | /// Initializes a new instance of the class. 69 | /// 70 | public RtspRequest() 71 | { 72 | Command = "OPTIONS * RTSP/1.0"; 73 | } 74 | 75 | /// 76 | /// Gets the request. 77 | /// 78 | /// The request in string format. 79 | public string Request => commandArray[0]; 80 | 81 | /// 82 | /// Gets the request. 83 | /// The return value is typed with if the value is not 84 | /// recognise the value is sent. The string value can be got by 85 | /// 86 | /// The request. 87 | public RequestType RequestTyped 88 | { 89 | get => ParseRequest(commandArray[0]); 90 | set 91 | { 92 | if (Enum.IsDefined(typeof(RequestType), value)) 93 | commandArray[0] = value.ToString(); 94 | else 95 | commandArray[0] = nameof(RequestType.UNKNOWN); 96 | } 97 | } 98 | 99 | private Uri? _rtspUri; 100 | 101 | /// 102 | /// Gets or sets the Rtsp asked URI. 103 | /// 104 | /// The Rtsp asked URI. 105 | /// The request with uri * is return with null URI 106 | public Uri? RtspUri 107 | { 108 | get 109 | { 110 | if (commandArray.Length < 2 || string.Equals(commandArray[1], "*", StringComparison.InvariantCulture)) 111 | { 112 | return null; 113 | } 114 | if (_rtspUri == null) 115 | { 116 | Uri.TryCreate(commandArray[1], UriKind.Absolute, out _rtspUri); 117 | } 118 | return _rtspUri; 119 | } 120 | set 121 | { 122 | _rtspUri = value; 123 | if (commandArray.Length < 2) 124 | { 125 | Array.Resize(ref commandArray, 3); 126 | } 127 | commandArray[1] = value != null ? value.ToString() : "*"; 128 | } 129 | } 130 | 131 | /// 132 | /// Gets the associate OK response with the request. 133 | /// 134 | /// an Rtsp response corresponding to request. 135 | public virtual RtspResponse CreateResponse() 136 | { 137 | var returnValue = new RtspResponse 138 | { 139 | ReturnCode = 200, 140 | CSeq = CSeq, 141 | }; 142 | if (Headers.TryGetValue(RtspHeaderNames.Session, out var value)) 143 | { 144 | returnValue.Headers[RtspHeaderNames.Session] = value; 145 | } 146 | 147 | return returnValue; 148 | } 149 | 150 | public object? ContextData { get; set; } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /RTSP/Messages/RTSPRequestAnnounce.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages 2 | { 3 | public class RtspRequestAnnounce : RtspRequest 4 | { 5 | public RtspRequestAnnounce() 6 | { 7 | Command = "ANNOUNCE * RTSP/1.0"; 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /RTSP/Messages/RTSPRequestDescribe.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages 2 | { 3 | public class RtspRequestDescribe : RtspRequest 4 | { 5 | public RtspRequestDescribe() 6 | { 7 | Command = "DESCRIBE * RTSP/1.0"; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /RTSP/Messages/RTSPRequestGetParameter.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages 2 | { 3 | public class RtspRequestGetParameter : RtspRequest 4 | { 5 | public RtspRequestGetParameter() 6 | { 7 | Command = "GET_PARAMETER * RTSP/1.0"; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /RTSP/Messages/RTSPRequestOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages 2 | { 3 | public class RtspRequestOptions : RtspRequest 4 | { 5 | public RtspRequestOptions() 6 | { 7 | Command = "OPTIONS * RTSP/1.0"; 8 | } 9 | 10 | /// 11 | /// Gets the associate OK response with the request. 12 | /// 13 | /// 14 | /// an Rtsp response corresponding to request. 15 | /// 16 | public override RtspResponse CreateResponse() 17 | { 18 | var response = base.CreateResponse(); 19 | // Add generic supported operations. 20 | response.Headers.Add(RtspHeaderNames.Public, "OPTIONS,DESCRIBE,ANNOUNCE,SETUP,PLAY,PAUSE,TEARDOWN,GET_PARAMETER,SET_PARAMETER,REDIRECT"); 21 | 22 | return response; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /RTSP/Messages/RTSPRequestPause.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages 2 | { 3 | public class RtspRequestPause : RtspRequest 4 | { 5 | public RtspRequestPause() 6 | { 7 | Command = "PAUSE * RTSP/1.0"; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /RTSP/Messages/RTSPRequestPlay.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages 2 | { 3 | public class RtspRequestPlay : RtspRequest 4 | { 5 | public RtspRequestPlay() 6 | { 7 | Command = "PLAY * RTSP/1.0"; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /RTSP/Messages/RTSPRequestRecord.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages 2 | { 3 | public class RtspRequestRecord : RtspRequest 4 | { 5 | public RtspRequestRecord() 6 | { 7 | Command = "RECORD * RTSP/1.0"; 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /RTSP/Messages/RTSPRequestSetup.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace Rtsp.Messages 4 | { 5 | public class RtspRequestSetup : RtspRequest 6 | { 7 | public RtspRequestSetup() 8 | { 9 | Command = "SETUP * RTSP/1.0"; 10 | } 11 | 12 | /// 13 | /// Gets the transports associate with the request. 14 | /// 15 | /// The transport. 16 | public RtspTransport[] GetTransports() 17 | { 18 | if (!Headers.TryGetValue(RtspHeaderNames.Transport, out string? transportString) || transportString is null) 19 | { 20 | return [new()]; 21 | } 22 | 23 | return transportString.Split(',').Select(RtspTransport.Parse).ToArray(); 24 | } 25 | 26 | public void AddTransport(RtspTransport newTransport) 27 | { 28 | var actualTransport = string.Empty; 29 | if (Headers.TryGetValue(RtspHeaderNames.Transport, out string? value)) 30 | { 31 | actualTransport = value + ","; 32 | } 33 | Headers[RtspHeaderNames.Transport] = actualTransport + newTransport; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /RTSP/Messages/RTSPRequestTeardown.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Messages 2 | { 3 | public class RtspRequestTeardown : RtspRequest 4 | { 5 | public RtspRequestTeardown() 6 | { 7 | Command = "TEARDOWN * RTSP/1.0"; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /RTSP/MulticastUdpSocket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | 5 | namespace Rtsp 6 | { 7 | public class MulticastUDPSocket : UDPSocket 8 | { 9 | private readonly IPAddress dataMulticastAddress; 10 | private readonly IPAddress controlMulticastAddress; 11 | 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// Used with Multicast mode with the Multicast Address and Port 15 | /// 16 | public MulticastUDPSocket(string data_multicast_address, int data_multicast_port, string control_multicast_address, int control_multicast_port) 17 | : base(new UdpClient(), new UdpClient()) 18 | { 19 | // open a pair of UDP sockets - one for data (video or audio) and one for the status channel (RTCP messages) 20 | DataPort = data_multicast_port; 21 | ControlPort = control_multicast_port; 22 | 23 | try 24 | { 25 | var dataEndPoint = new IPEndPoint(IPAddress.Any, DataPort); 26 | var controlEndPoint = new IPEndPoint(IPAddress.Any, ControlPort); 27 | 28 | dataMulticastAddress = IPAddress.Parse(data_multicast_address); 29 | controlMulticastAddress = IPAddress.Parse(control_multicast_address); 30 | 31 | dataSocket.Client.Bind(dataEndPoint); 32 | dataSocket.JoinMulticastGroup(dataMulticastAddress); 33 | 34 | controlSocket.Client.Bind(controlEndPoint); 35 | controlSocket.JoinMulticastGroup(controlMulticastAddress); 36 | 37 | dataSocket.Client.ReceiveBufferSize = 100 * 1024; 38 | dataSocket.Client.SendBufferSize = 65535; // default is 8192. Make it as large as possible for large RTP packets which are not fragmented 39 | 40 | controlSocket.Client.DontFragment = false; 41 | } 42 | catch (SocketException) 43 | { 44 | // Fail to allocate port, try again 45 | dataSocket?.Close(); 46 | controlSocket?.Close(); 47 | throw; 48 | } 49 | 50 | if (dataSocket == null || controlSocket == null) 51 | { 52 | throw new InvalidOperationException("UDP Forwader host was not initialized, can't continue"); 53 | } 54 | } 55 | 56 | /// 57 | /// Stops this instance. 58 | /// 59 | public override void Stop() 60 | { 61 | // leave the multicast groups 62 | dataSocket.DropMulticastGroup(dataMulticastAddress); 63 | controlSocket.DropMulticastGroup(controlMulticastAddress); 64 | base.Stop(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /RTSP/NetworkCredentialExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace Rtsp 4 | { 5 | static class NetworkCredentialExtensions 6 | { 7 | public static bool IsEmpty(this NetworkCredential networkCredential) 8 | { 9 | return string.IsNullOrEmpty(networkCredential.UserName) || networkCredential.Password == null; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /RTSP/Onvif/RtpPacketOnvifUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | 4 | namespace Rtsp.Onvif; 5 | public static class RtpPacketOnvifUtils 6 | { 7 | private const ushort MARKER_TS_EXT = 0xABAC; 8 | private const ushort MARKER_SOF0 = 0xffc0; // start-of-frame, baseline scan 9 | private const ushort MARKER_SOI = 0xffd8; // start of image 10 | 11 | /// 12 | /// Provide the Jpeg frame extension method, for frame size > 2048x2048 13 | /// 14 | /// The header to check 15 | /// Frame width and height 16 | public static (ushort frameWidth, ushort frameHeight) ProcessJpegFrameExtension(ReadOnlySpan extension) 17 | { 18 | var headerPosition = 0; 19 | ushort frameWidth = 0; 20 | ushort frameHeight = 0; 21 | int extensionType = BinaryPrimitives.ReadUInt16BigEndian(extension[headerPosition..]); 22 | if (extensionType == MARKER_SOI) 23 | { 24 | // 2 for type, 2 for length 25 | headerPosition += sizeof(ushort) + sizeof(ushort); 26 | int extensionSize = extension.Length; 27 | while (headerPosition < (extensionSize - (sizeof(ushort) + sizeof(ushort)))) 28 | { 29 | ushort blockType = BinaryPrimitives.ReadUInt16BigEndian(extension[headerPosition..]); 30 | ushort blockSize = BinaryPrimitives.ReadUInt16BigEndian(extension[(headerPosition + 2)..]); 31 | 32 | if (blockType == MARKER_SOF0) 33 | { 34 | frameHeight = BinaryPrimitives.ReadUInt16BigEndian(extension[(headerPosition + 5)..]); 35 | frameWidth = BinaryPrimitives.ReadUInt16BigEndian(extension[(headerPosition + 7)..]); 36 | } 37 | headerPosition += (blockSize + 2); 38 | } 39 | } 40 | return (frameWidth, frameHeight); 41 | } 42 | 43 | private static readonly DateTime dt_1900 = new(1900, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); 44 | 45 | /// 46 | /// Extract timestamp from jpeg extension. 47 | /// 48 | /// The extension header 49 | /// returns position after read. Used when JPEG extension is appended to this extension 50 | /// Timestamp of the frame or on error 51 | public static DateTime ProcessRTPTimestampExtension(ReadOnlySpan extension, out int headerPosition) 52 | { 53 | headerPosition = 0; 54 | // RTP extension has a minmum length of 4 32bit words (more if JPEG extension is appended). 55 | if (extension.Length < 4 * 4) 56 | { 57 | return DateTime.MinValue; 58 | } 59 | 60 | int extensionType = BinaryPrimitives.ReadUInt16BigEndian(extension); 61 | if (extensionType != MARKER_TS_EXT) 62 | { 63 | return DateTime.MinValue; 64 | } 65 | 66 | headerPosition += sizeof(ushort); 67 | // var headerLength = BinaryPrimitives.ReadUInt16BigEndian(extension[headerPosition..]); 68 | //if (headerLength == 3) 69 | { 70 | headerPosition += sizeof(ushort); 71 | 72 | uint seconds = BinaryPrimitives.ReadUInt32BigEndian(extension[headerPosition..]); 73 | uint fractions = BinaryPrimitives.ReadUInt32BigEndian(extension[(headerPosition + sizeof(uint))..]); 74 | 75 | headerPosition += sizeof(uint) + sizeof(uint); 76 | 77 | //uint data = BinaryPrimitives.ReadUInt16BigEndian(extension[headerPosition..]); 78 | // C [1 bit] -> all 79 | // E [1 bit] -> all 80 | // D [1 bit] -> all 81 | // T [1 bit] -> only not jpeg 82 | // MBZ [4/5 bits] -> [4 if not jpeg, 5 in jpeg] reserved 83 | // CSeq [8 bits] -> 1 byte 84 | // padding [8 bits] -> just padding values. 85 | 86 | headerPosition += sizeof(uint); 87 | double msec = fractions * 1000.0 / uint.MaxValue; 88 | 89 | return dt_1900.AddSeconds(seconds).AddMilliseconds(msec); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /RTSP/Onvif/RtspMessageOnvifExtension.cs: -------------------------------------------------------------------------------- 1 | using Rtsp.Messages; 2 | using System; 3 | 4 | namespace Rtsp.Onvif 5 | { 6 | public static class RtspMessageOnvifExtension 7 | { 8 | // be aware: 9 | // seekTime:o returns datetime like yyyy-MM-dd hh:mm:ss, but onvif requires format to be yyyyMMddTHHmmss with the clock= 10 | // if using :o, will return a "not suported" error 11 | 12 | public static void AddPlayback(this RtspRequestPlay message, DateTime seekTime, double scale = 1.0) 13 | { 14 | message.Headers.Add(RtspHeaderNames.Scale, FormattableString.Invariant($"{scale:0.0}")); 15 | message.Headers.Add(RtspHeaderNames.Range, $"clock={Seek(seekTime)}-"); 16 | } 17 | public static void AddPlayback(this RtspRequestPlay message, DateTime seekTimeFrom, DateTime seekTimeTo, double scale = 1.0) 18 | { 19 | message.Headers.Add(RtspHeaderNames.Scale, FormattableString.Invariant($"{scale:0.0}")); 20 | message.Headers.Add(RtspHeaderNames.Range, $"clock={Seek(seekTimeFrom)}-{Seek(seekTimeTo)}"); 21 | } 22 | 23 | private static string Seek(DateTime dt) => FormattableString.Invariant($"{dt:yyyyMMdd}T{dt:HHmmss}Z"); 24 | 25 | /// 26 | /// Add the Require: onvif-replay header to the message for ONVIF compatibility 27 | /// 28 | /// Message to modify 29 | public static void AddRequireOnvifRequest(this RtspMessage message) 30 | { 31 | if (!message.Headers.ContainsKey(RtspHeaderNames.Require)) 32 | { 33 | message.Headers.Add(RtspHeaderNames.Require, "onvif-replay"); 34 | } 35 | } 36 | 37 | /// 38 | /// Add the Rate-Control header to the message for ONVIF replay compatibility 39 | /// 40 | /// Message to modify 41 | /// is rate controled by server, see onvif specification 42 | public static void AddRateControlOnvifRequest(this RtspMessage message, bool rateControl) 43 | { 44 | if (!message.Headers.ContainsKey(RtspHeaderNames.RateControl)) 45 | { 46 | message.Headers.Add(RtspHeaderNames.RateControl, rateControl ? "yes" : "no"); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /RTSP/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // L'affectation de la valeur false à ComVisible rend les types invisibles dans cet assembly 6 | // aux composants COM. Si vous devez accéder à un type dans cet assembly à partir de 7 | // COM, affectez la valeur true à l'attribut ComVisible sur ce type. 8 | [assembly: ComVisible(false)] 9 | 10 | // Le GUID suivant est pour l'ID de la typelib si ce projet est exposé à COM 11 | [assembly: Guid("3998708c-4d61-413b-b442-749ca0d11817")] 12 | 13 | 14 | [assembly: InternalsVisibleTo("Rtsp.Tests")] 15 | [assembly: InternalsVisibleTo("Rtsp.Explorables")] 16 | -------------------------------------------------------------------------------- /RTSP/RTSP.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0;netstandard2.1;net8.0;net9.0 4 | 13.0 5 | Library 6 | 0 7 | enable 8 | True 9 | SharpRTSP 10 | ngraziano 11 | SharpRTSP 12 | Handle receive and send of Rtsp Messages 13 | 1.8.2 14 | https://github.com/ngraziano/SharpRTSP 15 | https://github.com/ngraziano/SharpRTSP 16 | True 17 | MIT 18 | README.md 19 | Rtsp 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /RTSP/RTSPDataEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Rtsp.Messages; 2 | using System; 3 | 4 | namespace Rtsp 5 | { 6 | /// 7 | /// Event args containing information for message events. 8 | /// 9 | public class RtspDataEventArgs : EventArgs 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Data . 15 | public RtspDataEventArgs(RtspData data) 16 | { 17 | Data = data; 18 | } 19 | 20 | /// 21 | /// Gets or sets the message. 22 | /// 23 | /// The message. 24 | public RtspData Data { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RTSP/RTSPTCPTransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | 8 | namespace Rtsp 9 | { 10 | /// 11 | /// TCP Connection for Rtsp 12 | /// 13 | public class RtspTcpTransport : IRtspTransport, IDisposable 14 | { 15 | private TcpClient _RtspServerClient; 16 | private uint _commandCounter; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The underlying TCP connection. 22 | public RtspTcpTransport(TcpClient tcpConnection) 23 | { 24 | if (tcpConnection == null) 25 | throw new ArgumentNullException(nameof(tcpConnection)); 26 | Contract.EndContractBlock(); 27 | 28 | RemoteEndPoint = tcpConnection.Client.RemoteEndPoint as IPEndPoint ?? throw new InvalidOperationException("The local endpoint can not be determined."); 29 | LocalEndPoint = tcpConnection.Client.LocalEndPoint as IPEndPoint ?? throw new InvalidOperationException("The remote endpoint can not be determined."); 30 | _RtspServerClient = tcpConnection; 31 | } 32 | 33 | /// 34 | /// Initializes a new instance of the class. 35 | /// 36 | /// The RTSP uri to connect to. 37 | public RtspTcpTransport(Uri uri) 38 | : this(new TcpClient(uri.Host, uri.Port)) 39 | { } 40 | 41 | #region IRtspTransport Membres 42 | 43 | /// 44 | /// Gets the stream of the transport. 45 | /// 46 | /// A stream 47 | public virtual Stream GetStream() => _RtspServerClient.GetStream(); 48 | 49 | /// 50 | /// Gets the remote address. 51 | /// 52 | /// The remote address. 53 | public string RemoteAddress => RemoteEndPoint.ToString(); 54 | 55 | /// 56 | /// Gets the remote endpoint. 57 | /// 58 | /// The remote endpoint. 59 | public IPEndPoint RemoteEndPoint { get; } 60 | 61 | /// 62 | /// Gets the local endpoint. 63 | /// 64 | /// The local endpoint. 65 | public IPEndPoint LocalEndPoint { get; } 66 | 67 | public uint NextCommandIndex() => ++_commandCounter; 68 | 69 | /// 70 | /// Closes this instance. 71 | /// 72 | public void Close() 73 | { 74 | Dispose(true); 75 | } 76 | 77 | /// 78 | /// Gets a value indicating whether this is connected. 79 | /// 80 | /// if connected; otherwise, . 81 | public bool Connected => _RtspServerClient.Client != null && _RtspServerClient.Connected; 82 | 83 | /// 84 | /// Reconnect this instance. 85 | /// Must do nothing if already connected. 86 | /// 87 | /// Error during socket 88 | public void Reconnect() 89 | { 90 | if (Connected) 91 | return; 92 | _RtspServerClient = new TcpClient(); 93 | _RtspServerClient.Connect(RemoteEndPoint); 94 | } 95 | 96 | #endregion 97 | 98 | public void Dispose() 99 | { 100 | Dispose(true); 101 | GC.SuppressFinalize(this); 102 | } 103 | 104 | protected virtual void Dispose(bool disposing) 105 | { 106 | if (disposing) 107 | { 108 | _RtspServerClient.Close(); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /RTSP/RTSPTcpTlsTransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Security; 4 | using System.Net.Sockets; 5 | using System.Security.Authentication; 6 | using System.Security.Cryptography.X509Certificates; 7 | 8 | namespace Rtsp 9 | { 10 | /// 11 | /// TCP Connection with TLS for Rtsp 12 | /// 13 | public class RtspTcpTlsTransport : RtspTcpTransport 14 | { 15 | private readonly RemoteCertificateValidationCallback? _userCertificateValidationCallback; 16 | private readonly X509Certificate2? _serverCertificate; 17 | 18 | /// 19 | /// Initializes a new instance of the class as a SSL/TLS Client 20 | /// 21 | /// The underlying TCP connection. 22 | /// The user certificate validation callback, if default should be used. 23 | public RtspTcpTlsTransport(TcpClient tcpConnection, RemoteCertificateValidationCallback? userCertificateValidationCallback = null) : base(tcpConnection) 24 | { 25 | _userCertificateValidationCallback = userCertificateValidationCallback; 26 | } 27 | 28 | /// 29 | /// Initializes a new instance of the class as a SSL/TLS Client. 30 | /// 31 | /// The RTSP uri to connect to. 32 | /// The user certificate validation callback, if default should be used. 33 | public RtspTcpTlsTransport(Uri uri, RemoteCertificateValidationCallback? userCertificateValidationCallback) 34 | : this(new TcpClient(uri.Host, uri.Port), userCertificateValidationCallback) 35 | { 36 | } 37 | 38 | /// 39 | /// Initializes a new instance of the class as a SSL/TLS Server with a certificate. 40 | /// 41 | /// The underlying TCP connection. 42 | /// The certificate for the TLS Server. 43 | /// The user certificate validation callback, if default should be used. 44 | public RtspTcpTlsTransport(TcpClient tcpConnection, X509Certificate2 certificate, RemoteCertificateValidationCallback? userCertificateValidationCallback = null) 45 | : this(tcpConnection, userCertificateValidationCallback) 46 | { 47 | _serverCertificate = certificate; 48 | } 49 | 50 | /// 51 | /// Gets the stream of the transport. 52 | /// 53 | /// A stream 54 | public override Stream GetStream() 55 | { 56 | var sslStream = new SslStream(base.GetStream(), leaveInnerStreamOpen: true, _userCertificateValidationCallback); 57 | 58 | // Use presence of server certificate to select if this is the SSL/TLS Server or the SSL/TLS Client 59 | if (_serverCertificate is not null) 60 | { 61 | sslStream.AuthenticateAsServer(_serverCertificate, false, SslProtocols.Tls12, false); 62 | } 63 | else 64 | { 65 | sslStream.AuthenticateAsClient(RemoteAddress); 66 | } 67 | return sslStream; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /RTSP/RTSPUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Security; 3 | 4 | namespace Rtsp 5 | { 6 | public static class RtspUtils 7 | { 8 | /// 9 | /// Registers the rtsp scheùe for uri. 10 | /// 11 | public static void RegisterUri() 12 | { 13 | if (!UriParser.IsKnownScheme("rtsp")) 14 | { 15 | UriParser.Register(new HttpStyleUriParser(), "rtsp", 554); 16 | } 17 | } 18 | 19 | public static IRtspTransport CreateRtspTransportFromUrl(Uri uri, RemoteCertificateValidationCallback? userCertificateSelectionCallback = null) 20 | { 21 | return uri.Scheme switch 22 | { 23 | "rtsp" => new RtspTcpTransport(uri), 24 | "rtsps" => new RtspTcpTlsTransport(uri, userCertificateSelectionCallback), 25 | "http" => new RtspHttpTransport(uri, new()), 26 | // "https" => new RtspHttpTransport(uri, new()), 27 | _ => throw new ArgumentException("The uri scheme is not supported", nameof(uri)) 28 | }; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /RTSP/Rtcp/RtcpPacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | 4 | namespace Rtsp.Rtcp 5 | { 6 | public readonly ref struct RtcpPacket 7 | { 8 | private readonly ReadOnlySpan rawData; 9 | 10 | public RtcpPacket(ReadOnlySpan rawData) 11 | { 12 | this.rawData = rawData; 13 | } 14 | 15 | public bool IsEmpty => rawData.IsEmpty; 16 | public bool IsWellFormed => rawData.Length >= 4 && Version == 2 && Length >= 0; 17 | public int Version => (rawData[0] >> 6) & 0x03; 18 | public bool HasPadding => ((rawData[0] >> 5) & 0x01) != 0; 19 | public int Count => rawData[0] & 0x1F; 20 | public int PacketType => rawData[1]; 21 | public int Length => BinaryPrimitives.ReadUInt16BigEndian(rawData[2..4]); 22 | 23 | public uint SenderSsrc => PacketType switch 24 | { 25 | RtcpPacketUtil.RTCP_PACKET_TYPE_SENDER_REPORT or 26 | RtcpPacketUtil.RTCP_PACKET_TYPE_RECEIVER_REPORT 27 | => BinaryPrimitives.ReadUInt32BigEndian(rawData[4..8]), 28 | _ => throw new InvalidOperationException("No Sender SSRC for this type of packet"), 29 | }; 30 | 31 | public SenderReportPacket SenderReport => PacketType switch 32 | { 33 | RtcpPacketUtil.RTCP_PACKET_TYPE_SENDER_REPORT 34 | => new SenderReportPacket(rawData, Count), 35 | _ => throw new InvalidOperationException("Packet is not of sender report type"), 36 | }; 37 | 38 | 39 | 40 | public RtcpPacket Next => new(rawData[((Length + 1) * 4)..]); 41 | 42 | // NTP Most Significant Word is relative to 0h, 1 Jan 1900 43 | // This will wrap around in 2036 44 | private static readonly DateTime ntpStartTime = new(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc); 45 | 46 | public readonly ref struct SenderReportPacket 47 | { 48 | private readonly int count; 49 | private readonly ReadOnlySpan rawData; 50 | public SenderReportPacket(ReadOnlySpan rawData, int count) 51 | { 52 | this.rawData = rawData; 53 | this.count = count; 54 | } 55 | 56 | public DateTime Clock => ntpStartTime.AddSeconds( 57 | BinaryPrimitives.ReadUInt32BigEndian(rawData[8..12]) 58 | + (BinaryPrimitives.ReadUInt32BigEndian(rawData[12..16]) / (double)uint.MaxValue) 59 | ); 60 | 61 | public uint RtpTimestamp => BinaryPrimitives.ReadUInt32BigEndian(rawData[16..20]); 62 | 63 | public uint PacketCount => BinaryPrimitives.ReadUInt32BigEndian(rawData[20..24]); 64 | public uint OctetCount => BinaryPrimitives.ReadUInt32BigEndian(rawData[24..28]); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /RTSP/Rtcp/RtcpPacketUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | 4 | namespace Rtsp.Rtcp 5 | { 6 | public static class RtcpPacketUtil 7 | { 8 | public const int RTCP_VERSION = 2; 9 | public const int RTCP_PACKET_TYPE_SENDER_REPORT = 200; 10 | public const int RTCP_PACKET_TYPE_RECEIVER_REPORT = 201; 11 | 12 | private static readonly DateTime ntpStartTime = new(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc); 13 | 14 | public static void WriteHeader(Span packet, int version, bool hasPadding, int reportCount, int packetType, int length, uint ssrc) 15 | { 16 | packet[0] = (byte)((version << 6) + ((hasPadding ? 1 : 0) << 5) + reportCount); 17 | packet[1] = (byte)packetType; 18 | BinaryPrimitives.WriteUInt16BigEndian(packet[2..], (ushort)length); 19 | BinaryPrimitives.WriteUInt32BigEndian(packet[4..], ssrc); 20 | } 21 | 22 | public static void WriteSenderReport(Span rtcpSenderReport, DateTime wallClock, uint rtpTimestamp, uint rtpPacketCount, uint octetCount) 23 | { 24 | // NTP Most Signigicant Word is relative to 0h, 1 Jan 1900 25 | // This will wrap around in 2036 26 | TimeSpan tmpTime = wallClock - ntpStartTime; 27 | double totalSeconds = tmpTime.TotalSeconds; 28 | 29 | // whole number of seconds 30 | uint ntp_msw_seconds = (uint)Math.Truncate(totalSeconds); 31 | // fractional part, scaled between 0 and MaxInt 32 | uint ntp_lsw_fractions = (uint)(totalSeconds % 1 * uint.MaxValue); 33 | 34 | BinaryPrimitives.WriteUInt32BigEndian(rtcpSenderReport[8..], ntp_msw_seconds); 35 | BinaryPrimitives.WriteUInt32BigEndian(rtcpSenderReport[12..], ntp_lsw_fractions); 36 | BinaryPrimitives.WriteUInt32BigEndian(rtcpSenderReport[16..], rtpTimestamp); 37 | BinaryPrimitives.WriteUInt32BigEndian(rtcpSenderReport[20..], rtpPacketCount); 38 | BinaryPrimitives.WriteUInt32BigEndian(rtcpSenderReport[24..], octetCount); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /RTSP/Rtp/AMRPayload.cs: -------------------------------------------------------------------------------- 1 | using Rtsp.Onvif; 2 | using System; 3 | using System.Buffers; 4 | using System.Collections.Generic; 5 | 6 | namespace Rtsp.Rtp 7 | { 8 | /// 9 | /// This class handles the AMR Payload 10 | /// 11 | public class AMRPayload : IPayloadProcessor 12 | { 13 | private readonly MemoryPool _memoryPool; 14 | 15 | public AMRPayload(MemoryPool? memoryPool = null) 16 | { 17 | _memoryPool = memoryPool ?? MemoryPool.Shared; 18 | } 19 | 20 | public RawMediaFrame ProcessPacket(RtpPacket packet) 21 | { 22 | // TODO check the RFC to handle the different modes 23 | 24 | // Octet-Aligned Mode (RFC 4867 Section 4.4.1) 25 | // First byte is the Payload Header 26 | if (packet.PayloadSize < 1) 27 | { 28 | return RawMediaFrame.Empty; 29 | } 30 | // byte payloadHeader = payload[0]; 31 | 32 | int lenght = packet.PayloadSize - 1; 33 | IMemoryOwner owner = _memoryPool.Rent(lenght); 34 | // The rest of the RTP packet is the AMR data 35 | packet.Payload[1..].CopyTo(owner.Memory.Span); 36 | 37 | return new([owner.Memory[..lenght]], [owner]) 38 | { 39 | ClockTimestamp = RtpPacketOnvifUtils.ProcessRTPTimestampExtension(packet.Extension, headerPosition: out _), 40 | RtpTimestamp = packet.Timestamp, 41 | }; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /RTSP/Rtp/G711Payload.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace Rtsp.Rtp 4 | { 5 | // This class handles the G711 Payload 6 | // It has methods to process the RTP Payload 7 | public class G711Payload : RawPayload 8 | { 9 | public G711Payload(MemoryPool? memoryPool = null) : base(memoryPool) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RTSP/Rtp/G711_1Payload.cs: -------------------------------------------------------------------------------- 1 | using Rtsp.Onvif; 2 | using System; 3 | using System.Buffers; 4 | using System.Collections.Generic; 5 | 6 | namespace Rtsp.Rtp 7 | { 8 | /// 9 | /// This class handles the G711.1 Payload 10 | /// 11 | public class G711_1Payload : IPayloadProcessor 12 | { 13 | private readonly MemoryPool _memoryPool; 14 | 15 | public G711_1Payload(MemoryPool? memoryPool = null) 16 | { 17 | _memoryPool = memoryPool ?? MemoryPool.Shared; 18 | } 19 | 20 | public RawMediaFrame ProcessPacket(RtpPacket packet) 21 | { 22 | // Look at the Header. This tells us the G711 mode being used 23 | 24 | // Mode Index (MI) is 25 | // 1 - R1 40 octets containg Layer 0 data 26 | // 2 - R2a 50 octets containing Layer 0 plus Layer 1 data 27 | // 3 - R2b 50 octets containing Layer 0 plus Layer 2 data 28 | // 4 - R3 60 octets containing Layer 0 plus Layer 1 plus Layer 2 data 29 | 30 | var rtpPayload = packet.Payload; 31 | byte modeIndex = (byte)(rtpPayload[0] & 0x07); 32 | int sizeOfOneFrame = modeIndex switch 33 | { 34 | 1 => 40, 35 | 2 => 50, 36 | 3 => 50, 37 | 4 => 60, 38 | _ => 0, 39 | }; 40 | if (sizeOfOneFrame == 0) 41 | { 42 | // ERROR 43 | return RawMediaFrame.Empty; 44 | } 45 | 46 | List> audioDatas = []; 47 | List> owners = []; 48 | 49 | // Extract each audio frame and place in the audio_data List 50 | int frame_start = 1; // starts just after the MI header 51 | while (frame_start + sizeOfOneFrame < rtpPayload.Length) 52 | { 53 | // Return just the basic u-Law or A-Law audio (the Layer 0 audio) 54 | var owner = _memoryPool.Rent(40); 55 | owners.Add(owner); 56 | var memory = owner.Memory[..40]; 57 | // only copy the Layer 0 data (the first 40 bytes) 58 | rtpPayload[frame_start..(frame_start + 40)].CopyTo(memory.Span); 59 | audioDatas.Add(memory); 60 | frame_start += sizeOfOneFrame; 61 | } 62 | return new(audioDatas, owners) 63 | { 64 | ClockTimestamp = RtpPacketOnvifUtils.ProcessRTPTimestampExtension(packet.Extension, headerPosition: out _), 65 | RtpTimestamp = packet.Timestamp, 66 | }; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /RTSP/Rtp/IPayloadProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Rtp 2 | { 3 | public interface IPayloadProcessor 4 | { 5 | 6 | /// 7 | /// Process an RtpPacket and return a RawMediaFrame containing the data of the stream. 8 | /// 9 | /// return value should be disposed after copying the data to allow buffer to be reuse 10 | /// packet to handle 11 | /// RawMedia frame containing the stream data or empty if more packet are needed 12 | RawMediaFrame ProcessPacket(RtpPacket packet); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RTSP/Rtp/JPEGDefaultTables.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Rtp 2 | { 3 | /// 4 | /// Contains the default quantizers and huffman tables for JPEG extract from the RFC 2435 5 | /// 6 | public static class JPEGDefaultTables 7 | { 8 | public static readonly byte[] DefaultQuantizers = [ 9 | #pragma warning disable format 10 | 16, 11, 12, 14, 12, 10, 16, 14, 11 | 13, 14, 18, 17, 16, 19, 24, 40, 12 | 26, 24, 22, 22, 24, 49, 35, 37, 13 | 29, 40, 58, 51, 61, 60, 57, 51, 14 | 56, 55, 64, 72, 92, 78, 64, 68, 15 | 87, 69, 55, 56, 80, 109, 81, 87, 16 | 95, 98, 103, 104, 103, 62, 77, 113, 17 | 121, 112, 100, 120, 92, 101, 103, 99, 18 | 17, 18, 18, 24, 21, 24, 47, 26, 19 | 26, 47, 99, 66, 56, 66, 99, 99, 20 | 99, 99, 99, 99, 99, 99, 99, 99, 21 | 99, 99, 99, 99, 99, 99, 99, 99, 22 | 99, 99, 99, 99, 99, 99, 99, 99, 23 | 99, 99, 99, 99, 99, 99, 99, 99, 24 | 99, 99, 99, 99, 99, 99, 99, 99, 25 | 99, 99, 99, 99, 99, 99, 99, 99, 26 | #pragma warning restore format 27 | ]; 28 | 29 | private static readonly byte[] LumDcCodelens = [0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]; 30 | 31 | private static readonly byte[] LumDcSymbols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; 32 | 33 | private static readonly byte[] LumAcCodelens = [0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d]; 34 | 35 | private static readonly byte[] LumAcSymbols = [ 36 | #pragma warning disable format 37 | 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 38 | 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 39 | 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 40 | 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 41 | 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 42 | 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 43 | 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 44 | 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 45 | 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 46 | 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 47 | 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 48 | 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 49 | 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 50 | 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 51 | 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 52 | 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 53 | 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 54 | 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 55 | 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 56 | 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 57 | 0xf9, 0xfa, 58 | #pragma warning restore format 59 | ]; 60 | 61 | private static readonly byte[] ChmDcCodelens = [0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,]; 62 | 63 | private static readonly byte[] ChmDcSymbols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,]; 64 | 65 | private static readonly byte[] ChmAcCodelens = [0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77,]; 66 | 67 | private static readonly byte[] ChmAcSymbols = [ 68 | #pragma warning disable format 69 | 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 70 | 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 71 | 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 72 | 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 73 | 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 74 | 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 75 | 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 76 | 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 77 | 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 78 | 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 79 | 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 80 | 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 81 | 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 82 | 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 83 | 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 84 | 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 85 | 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 86 | 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 87 | 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 88 | 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 89 | 0xf9, 0xfa, 90 | #pragma warning restore format 91 | ]; 92 | 93 | public static readonly HuffmanTable LumDcTable = new(LumDcCodelens, LumDcSymbols, 0, 0); 94 | public static readonly HuffmanTable LumAcTable = new(LumAcCodelens, LumAcSymbols, 0, 1); 95 | public static readonly HuffmanTable ChmDcTable = new(ChmDcCodelens, ChmDcSymbols, 1, 0); 96 | public static readonly HuffmanTable ChmAcTable = new(ChmAcCodelens, ChmAcSymbols, 1, 1); 97 | } 98 | 99 | public record struct HuffmanTable(byte[] Codelens, byte[] Symbols, byte Number, byte Class); 100 | } 101 | -------------------------------------------------------------------------------- /RTSP/Rtp/MP2TransportPayload.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace Rtsp.Rtp 4 | { 5 | // TODO check the RFC 2250 6 | public class MP2TransportPayload : RawPayload 7 | { 8 | public MP2TransportPayload(MemoryPool? memoryPool = null) 9 | : base(memoryPool) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RTSP/Rtp/RawMediaFrame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Rtsp.Rtp 7 | { 8 | public class RawMediaFrame : IDisposable 9 | { 10 | private bool disposedValue; 11 | private readonly IEnumerable> _data; 12 | private readonly IEnumerable> _owners; 13 | 14 | public IEnumerable> Data 15 | { 16 | get 17 | { 18 | if (disposedValue) throw new ObjectDisposedException(nameof(RawMediaFrame)); 19 | return _data; 20 | } 21 | } 22 | 23 | public required DateTime ClockTimestamp { get; init; } 24 | public required uint RtpTimestamp { get; init; } 25 | 26 | public RawMediaFrame(IEnumerable> data, IEnumerable> owners) 27 | { 28 | _data = data; 29 | _owners = owners; 30 | } 31 | 32 | public bool Any() => Data.Any(); 33 | 34 | protected virtual void Dispose(bool disposing) 35 | { 36 | if (!disposedValue) 37 | { 38 | if (disposing) 39 | { 40 | foreach (var owner in _owners) 41 | { 42 | owner.Dispose(); 43 | } 44 | } 45 | disposedValue = true; 46 | } 47 | } 48 | 49 | public void Dispose() 50 | { 51 | Dispose(disposing: true); 52 | GC.SuppressFinalize(this); 53 | } 54 | 55 | public static RawMediaFrame Empty => new([], []) { RtpTimestamp = 0, ClockTimestamp = DateTime.MinValue }; 56 | } 57 | } -------------------------------------------------------------------------------- /RTSP/Rtp/RawPayload.cs: -------------------------------------------------------------------------------- 1 | using Rtsp.Onvif; 2 | using System; 3 | using System.Buffers; 4 | using System.Collections.Generic; 5 | 6 | namespace Rtsp.Rtp 7 | { 8 | public class RawPayload : IPayloadProcessor 9 | { 10 | private readonly MemoryPool _memoryPool; 11 | 12 | public RawPayload(MemoryPool? memoryPool = null) 13 | { 14 | _memoryPool = memoryPool ?? MemoryPool.Shared; 15 | } 16 | 17 | public RawMediaFrame ProcessPacket(RtpPacket packet) 18 | { 19 | var owner = _memoryPool.Rent(packet.PayloadSize); 20 | var memory = owner.Memory[..packet.PayloadSize]; 21 | packet.Payload.CopyTo(memory.Span); 22 | return new RawMediaFrame([memory], [owner]) 23 | { 24 | ClockTimestamp = RtpPacketOnvifUtils.ProcessRTPTimestampExtension(packet.Extension, headerPosition: out _), 25 | RtpTimestamp = packet.Timestamp, 26 | }; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /RTSP/Rtp/RtpPacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | 4 | namespace Rtsp.Rtp 5 | { 6 | public readonly ref struct RtpPacket 7 | { 8 | private readonly ReadOnlySpan rawData; 9 | 10 | public RtpPacket(ReadOnlySpan rawData) 11 | { 12 | this.rawData = rawData; 13 | } 14 | 15 | public bool IsWellFormed => rawData.Length >= 12 && Version == 2 && PayloadSize >= 0; 16 | 17 | public int Version => (rawData[0] >> 6) & 0x03; 18 | public bool HasPadding => (rawData[0] & 0x20) > 0; 19 | public bool HasExtension => (rawData[0] & 0x10) > 0; 20 | public int CsrcCount => rawData[0] & 0x0F; 21 | public bool IsMarker => (rawData[1] & 0x80) > 0; 22 | public int PayloadType => rawData[1] & 0x7F; 23 | public int SequenceNumber => BinaryPrimitives.ReadUInt16BigEndian(rawData[2..]); 24 | public uint Timestamp => BinaryPrimitives.ReadUInt32BigEndian(rawData[4..]); 25 | public uint Ssrc => BinaryPrimitives.ReadUInt32BigEndian(rawData[8..]); 26 | 27 | public int? ExtensionHeaderId => HasExtension ? (rawData[HeaderSize] << 8) + rawData[HeaderSize + 1] : null; 28 | 29 | private int HeaderSize => 12 + (CsrcCount * 4); 30 | 31 | private int ExtensionSize => HasExtension ? ((rawData[HeaderSize + 2] << 8) + rawData[HeaderSize + 3] + 1) * 4 : 0; 32 | 33 | private int PaddingSize => HasPadding ? rawData[^1] : 0; 34 | 35 | public int PayloadSize => rawData.Length - HeaderSize - ExtensionSize - PaddingSize; 36 | 37 | public ReadOnlySpan Payload => rawData[(HeaderSize + ExtensionSize)..^PaddingSize]; 38 | public ReadOnlySpan Extension => rawData[HeaderSize..(HeaderSize + ExtensionSize)]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /RTSP/Rtp/RtpPacketUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | 4 | namespace Rtsp.Rtp 5 | { 6 | public static class RtpPacketUtil 7 | { 8 | public const int RTP_VERSION = 2; 9 | 10 | public static void WriteHeader(Span packet, int version, bool padding, bool hasExtension, 11 | int csrcCount, bool marker, int payloadType) 12 | { 13 | packet[0] = (byte)((version << 6) | ((padding ? 1 : 0) << 5) | ((hasExtension ? 1 : 0) << 4) | csrcCount); 14 | packet[1] = (byte)(((marker ? 1 : 0) << 7) | (payloadType & 0x7F)); 15 | } 16 | 17 | public static int DataOffset(int csrcCount, int? extensionDataSizeInWord) 18 | => 12 + (csrcCount * 4) + ((extensionDataSizeInWord + 1) * 4 ?? 0); 19 | 20 | public static void WriteSequenceNumber(Span packet, ushort sequenceNumber) 21 | => BinaryPrimitives.WriteUInt16BigEndian(packet[2..], sequenceNumber); 22 | 23 | public static void WriteTimestamp(Span packet, uint timestamp) 24 | => BinaryPrimitives.WriteUInt32BigEndian(packet[4..], timestamp); 25 | 26 | public static void WriteSSRC(Span packet, uint ssrc) 27 | => BinaryPrimitives.WriteUInt32BigEndian(packet[8..], ssrc); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /RTSP/RtpTcpTransport.cs: -------------------------------------------------------------------------------- 1 | using Rtsp.Messages; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace Rtsp 6 | { 7 | public class RtpTcpTransport : IRtpTransport 8 | { 9 | private bool disposedValue; 10 | private readonly RtspListener rtspListener; 11 | 12 | public event EventHandler? DataReceived; 13 | public event EventHandler? ControlReceived; 14 | 15 | public int ControlChannel { get; set; } = int.MaxValue; 16 | public int DataChannel { get; set; } = int.MaxValue; 17 | 18 | /// 19 | /// Get the control and data channels numbers 20 | /// 21 | public PortCouple Channels => new(DataChannel, ControlChannel); 22 | 23 | public RtpTcpTransport(RtspListener rtspListener) 24 | { 25 | this.rtspListener = rtspListener; 26 | } 27 | 28 | public void WriteToControlPort(ReadOnlySpan data) => rtspListener.SendData(ControlChannel, data); 29 | 30 | public Task WriteToControlPortAsync(ReadOnlyMemory data) => rtspListener.SendDataAsync(ControlChannel, data); 31 | 32 | public void WriteToDataPort(ReadOnlySpan data) => rtspListener.SendData(DataChannel, data); 33 | 34 | public Task WriteToDataPortAsync(ReadOnlyMemory data) => rtspListener.SendDataAsync(DataChannel, data); 35 | 36 | protected virtual void Dispose(bool disposing) 37 | { 38 | if (!disposedValue) 39 | { 40 | if (disposing) 41 | { 42 | Stop(); 43 | } 44 | disposedValue = true; 45 | } 46 | } 47 | 48 | public void Dispose() 49 | { 50 | // Ne changez pas ce code. Placez le code de nettoyage dans la méthode 'Dispose(bool disposing)' 51 | Dispose(disposing: true); 52 | GC.SuppressFinalize(this); 53 | } 54 | 55 | public void Start() 56 | { 57 | rtspListener.DataReceived += RtspListenerDataReceived; 58 | } 59 | 60 | public void Stop() 61 | { 62 | rtspListener.DataReceived -= RtspListenerDataReceived; 63 | } 64 | private void RtspListenerDataReceived(object? sender, RtspChunkEventArgs e) 65 | { 66 | if (e.Message is not RtspData dataMessage || dataMessage.Data.IsEmpty) return; 67 | if (dataMessage.Channel == ControlChannel) 68 | { 69 | ControlReceived?.Invoke(this, new RtspDataEventArgs(dataMessage)); 70 | } 71 | else if (dataMessage.Channel == DataChannel) 72 | { 73 | DataReceived?.Invoke(this, new RtspDataEventArgs(dataMessage)); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /RTSP/RtspChunkEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Rtsp 4 | { 5 | using Messages; 6 | /// 7 | /// Event args containing information for message events. 8 | /// 9 | public class RtspChunkEventArgs : EventArgs 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// A message. 15 | public RtspChunkEventArgs(RtspChunk aMessage) 16 | { 17 | Message = aMessage; 18 | } 19 | 20 | /// 21 | /// Gets or sets the message. 22 | /// 23 | /// The message. 24 | public RtspChunk Message { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RTSP/Sdp/Attribut.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | 5 | namespace Rtsp.Sdp 6 | { 7 | public class Attribut 8 | { 9 | private static readonly Dictionary> attributMap = new(StringComparer.Ordinal) 10 | { 11 | {AttributRtpMap.NAME, () => new AttributRtpMap() }, 12 | {AttributFmtp.NAME , () => new AttributFmtp() }, 13 | }; 14 | 15 | public virtual string Key { get; } 16 | public virtual string Value { get; protected set; } = string.Empty; 17 | 18 | public static void RegisterNewAttributeType(string key, Func attributTypeConstructor) 19 | { 20 | attributMap[key] = attributTypeConstructor; 21 | } 22 | 23 | public Attribut(string key) 24 | { 25 | Key = key; 26 | } 27 | 28 | public static Attribut ParseInvariant(string value) 29 | { 30 | if (value == null) 31 | throw new ArgumentNullException(nameof(value)); 32 | Contract.EndContractBlock(); 33 | 34 | var listValues = value.Split(':', 2); 35 | 36 | // Call parser of child type 37 | if (!attributMap.TryGetValue(listValues[0], out var childContructor)) 38 | { 39 | childContructor = () => new Attribut(listValues[0]); 40 | } 41 | 42 | var returnValue = childContructor.Invoke(); 43 | // Parse the value. Note most attributes have a value but recvonly does not have a value 44 | if (listValues.Length > 1) returnValue.ParseValue(listValues[1]); 45 | 46 | return returnValue; 47 | } 48 | 49 | protected virtual void ParseValue(string value) 50 | { 51 | Value = value; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /RTSP/Sdp/AttributFmtp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | 6 | namespace Rtsp.Sdp 7 | { 8 | public class AttributFmtp : Attribut 9 | { 10 | public const string NAME = "fmtp"; 11 | 12 | private readonly Dictionary parameters = new(StringComparer.Ordinal); 13 | 14 | public AttributFmtp() : base(NAME) 15 | { 16 | } 17 | 18 | public override string Value 19 | { 20 | get 21 | { 22 | return string.Format(CultureInfo.InvariantCulture, "{0} {1}", PayloadNumber, FormatParameter); 23 | } 24 | protected set 25 | { 26 | ParseValue(value); 27 | } 28 | } 29 | 30 | public int PayloadNumber { get; set; } 31 | 32 | // temporary aatibute to store remaning data not parsed 33 | public string? FormatParameter { get; set; } 34 | 35 | // Extract the Payload Number and the Format Parameters 36 | protected override void ParseValue(string value) 37 | { 38 | var parts = value.Split(' ', 2); 39 | 40 | if (int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int payloadNumber)) 41 | { 42 | PayloadNumber = payloadNumber; 43 | } 44 | if (parts.Length > 1) 45 | { 46 | FormatParameter = parts[1]; 47 | 48 | // Split on ';' to get a list of items. 49 | // Then Trim each item and then Split on the first '=' 50 | // Add them to the dictionary 51 | parameters.Clear(); 52 | foreach (var pair in parts[1].Split(';').Select(x => x.Trim().Split(['='], 2))) 53 | { 54 | if (!string.IsNullOrWhiteSpace(pair[0])) 55 | parameters[pair[0]] = pair.Length > 1 ? pair[1] : string.Empty; 56 | } 57 | } 58 | } 59 | 60 | public string this[string index] => parameters.TryGetValue(index, out string? value) ? value : string.Empty; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /RTSP/Sdp/AttributRtpMap.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Rtsp.Sdp 4 | { 5 | public class AttributRtpMap : Attribut 6 | { 7 | // Format 8 | // rtpmap: / [/] 9 | // Examples 10 | // rtpmap:96 H264/90000 11 | // rtpmap:8 PCMA/8000 12 | 13 | public const string NAME = "rtpmap"; 14 | 15 | public AttributRtpMap() : base(NAME) 16 | { 17 | } 18 | 19 | public override string Value 20 | { 21 | get 22 | { 23 | if (string.IsNullOrEmpty(EncodingParameters)) 24 | { 25 | return string.Format(CultureInfo.InvariantCulture, "{0} {1}/{2}", PayloadNumber, EncodingName, ClockRate); 26 | } 27 | return string.Format(CultureInfo.InvariantCulture, "{0} {1}/{2}/{3}", PayloadNumber, EncodingName, ClockRate, EncodingParameters); 28 | } 29 | protected set 30 | { 31 | ParseValue(value); 32 | } 33 | } 34 | 35 | public int PayloadNumber { get; set; } 36 | public string? EncodingName { get; set; } 37 | public string? ClockRate { get; set; } 38 | public string? EncodingParameters { get; set; } 39 | 40 | protected override void ParseValue(string value) 41 | { 42 | var parts = value.Split([' ', '/']); 43 | 44 | if (parts.Length >= 1 && int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int tmp_payloadNumber)) 45 | { 46 | PayloadNumber = tmp_payloadNumber; 47 | } 48 | if (parts.Length >= 2) 49 | { 50 | EncodingName = parts[1]; 51 | } 52 | if (parts.Length >= 3) 53 | { 54 | ClockRate = parts[2]; 55 | } 56 | if (parts.Length >= 4) 57 | { 58 | EncodingParameters = parts[3]; 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /RTSP/Sdp/Bandwidth.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace Rtsp.Sdp 5 | { 6 | public class Bandwidth 7 | { 8 | public required string Type { get; init; } 9 | public required int Value { get; init; } 10 | 11 | internal static Bandwidth Parse(string value) 12 | { 13 | var splitted = value.Split(':', 2); 14 | if (splitted.Length != 2) 15 | { 16 | throw new ArgumentOutOfRangeException(nameof(value), "Invalid bandwidth format"); 17 | } 18 | 19 | if (!int.TryParse(splitted[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var bwParsed)) 20 | { 21 | throw new ArgumentOutOfRangeException(nameof(value), "Invalid bandwidth format"); 22 | } 23 | return new Bandwidth() { Type = splitted[0], Value = bwParsed }; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RTSP/Sdp/Connection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace Rtsp.Sdp 6 | { 7 | public abstract class Connection 8 | { 9 | private const string _ConnectionRegexString = @"IN (?(IP4|IP6)) (?
[0-9a-zA-Z\.\/\:]*)"; 10 | 11 | private static readonly Regex _ConnectionRegex = new(_ConnectionRegexString, RegexOptions.ExplicitCapture, TimeSpan.FromSeconds(1)); 12 | 13 | public string Host { get; set; } = string.Empty; 14 | 15 | /// 16 | /// Gets or sets the number of address specifed in connection. 17 | /// 18 | /// The number of the address. 19 | public int NumberOfAddress { get; set; } = 1; 20 | 21 | public static Connection Parse(string value) 22 | { 23 | if (value is null) 24 | throw new ArgumentNullException(nameof(value)); 25 | 26 | var matches = _ConnectionRegex.Matches(value); 27 | 28 | if (matches.Count > 0) 29 | { 30 | var firstMatch = matches[0]; 31 | var type = firstMatch.Groups["Type"]; 32 | return type.Value switch 33 | { 34 | "IP4" => ConnectionIP4.Parse(firstMatch.Groups["Address"].Value), 35 | "IP6" => ConnectionIP6.Parse(firstMatch.Groups["Address"].Value), 36 | _ => throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, 37 | "Address type {0} not suported", firstMatch.Groups["Address"].Value)), 38 | }; 39 | } 40 | 41 | throw new FormatException("Unrecognised Connection value"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /RTSP/Sdp/ConnectionIP4.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Sdp 2 | { 3 | using System; 4 | using System.Globalization; 5 | 6 | public class ConnectionIP4 : Connection 7 | { 8 | public int Ttl { get; set; } 9 | 10 | internal new static ConnectionIP4 Parse(string ipAddress) 11 | { 12 | string[] parts = ipAddress.Split('/'); 13 | 14 | if (parts.Length > 3) 15 | throw new FormatException("Too much address subpart in " + ipAddress); 16 | 17 | var result = new ConnectionIP4 18 | { 19 | Host = parts[0], 20 | }; 21 | 22 | if (parts.Length > 1) 23 | { 24 | if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int ttl)) 25 | throw new FormatException("Invalid TTL format : " + parts[1]); 26 | result.Ttl = ttl; 27 | } 28 | if (parts.Length > 2) 29 | { 30 | if (!int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out int numberOfAddress)) 31 | throw new FormatException("Invalid number of address : " + parts[2]); 32 | result.NumberOfAddress = numberOfAddress; 33 | } 34 | 35 | return result; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /RTSP/Sdp/ConnectionIP6.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Sdp 2 | { 3 | using System; 4 | using System.Globalization; 5 | 6 | public class ConnectionIP6 : Connection 7 | { 8 | internal new static ConnectionIP6 Parse(string ipAddress) 9 | { 10 | string[] parts = ipAddress.Split('/'); 11 | 12 | if (parts.Length > 2) 13 | throw new FormatException("Too much address subpart in " + ipAddress); 14 | 15 | var result = new ConnectionIP6 16 | { 17 | Host = parts[0], 18 | }; 19 | 20 | if (parts.Length > 1) 21 | { 22 | if (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int numberOfAddress)) 23 | throw new FormatException("Invalid number of address : " + parts[1]); 24 | result.NumberOfAddress = numberOfAddress; 25 | } 26 | 27 | return result; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /RTSP/Sdp/H264Parameters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Rtsp.Sdp 7 | { 8 | public class H264Parameters : IDictionary 9 | { 10 | private const string HeaderName = "sprop-parameter-sets"; 11 | private readonly Dictionary parameters = []; 12 | 13 | public IList SpropParameterSets => 14 | TryGetValue(HeaderName, out var value) 15 | ? value.Split(',').Select(Convert.FromBase64String).ToList() 16 | : []; 17 | 18 | public static H264Parameters Parse(string parameterString) 19 | { 20 | var result = new H264Parameters(); 21 | foreach (var pair in parameterString.Split(';').Select(x => x.Trim().Split('=', 2))) 22 | { 23 | if (!string.IsNullOrWhiteSpace(pair[0])) 24 | result[pair[0]] = pair.Length > 1 ? pair[1] : string.Empty; 25 | } 26 | 27 | return result; 28 | } 29 | 30 | public override string ToString() 31 | { 32 | return parameters.Select(p => p.Key + (p.Value != null ? "=" + p.Value : string.Empty)) 33 | .Aggregate((x, y) => x + ";" + y); 34 | } 35 | 36 | public string this[string index] 37 | { 38 | get => parameters[index]; 39 | set => parameters[index] = value; 40 | } 41 | 42 | public int Count => parameters.Count; 43 | 44 | public bool IsReadOnly => ((IDictionary)parameters).IsReadOnly; 45 | 46 | public ICollection Keys => ((IDictionary)parameters).Keys; 47 | 48 | public ICollection Values => ((IDictionary)parameters).Values; 49 | 50 | public void Add(KeyValuePair item) => ((IDictionary)parameters).Add(item); 51 | 52 | public void Add(string key, string value) => parameters.Add(key, value); 53 | 54 | public void Clear() => parameters.Clear(); 55 | 56 | public bool Contains(KeyValuePair item) => 57 | ((IDictionary)parameters).Contains(item); 58 | 59 | public bool ContainsKey(string key) => parameters.ContainsKey(key); 60 | 61 | public void CopyTo(KeyValuePair[] array, int arrayIndex) => 62 | ((IDictionary)parameters).CopyTo(array, arrayIndex); 63 | 64 | public IEnumerator> GetEnumerator() => 65 | ((IDictionary)parameters).GetEnumerator(); 66 | 67 | public bool Remove(KeyValuePair item) => ((IDictionary)parameters).Remove(item); 68 | 69 | public bool Remove(string key) => parameters.Remove(key); 70 | 71 | public bool TryGetValue(string key, out string value) => parameters.TryGetValue(key, out value!); 72 | 73 | IEnumerator IEnumerable.GetEnumerator() => ((IDictionary)parameters).GetEnumerator(); 74 | } 75 | } -------------------------------------------------------------------------------- /RTSP/Sdp/H265Parameters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | // Parse 'fmtp' attribute in SDP 7 | // Extract H265 fields 8 | // By Roger Hardiman, RJH Technical Consultancy Ltd 9 | 10 | namespace Rtsp.Sdp 11 | { 12 | public class H265Parameters : IDictionary 13 | { 14 | private readonly Dictionary parameters = []; 15 | 16 | public IList SpropParameterSets 17 | { 18 | get 19 | { 20 | List result = []; 21 | 22 | if (ContainsKey("sprop-vps") && this["sprop-vps"] != null) 23 | { 24 | result.AddRange(this["sprop-vps"].Split(',').Select(x => Convert.FromBase64String(x))); 25 | } 26 | 27 | if (ContainsKey("sprop-sps") && this["sprop-sps"] != null) 28 | { 29 | result.AddRange(this["sprop-sps"].Split(',').Select(x => Convert.FromBase64String(x))); 30 | } 31 | 32 | if (ContainsKey("sprop-pps") && this["sprop-pps"] != null) 33 | { 34 | result.AddRange(this["sprop-pps"].Split(',').Select(x => Convert.FromBase64String(x))); 35 | } 36 | 37 | return result; 38 | } 39 | } 40 | 41 | public static H265Parameters Parse(string parameterString) 42 | { 43 | var result = new H265Parameters(); 44 | foreach (var pair in parameterString.Split(';').Select(x => x.Trim().Split('=', 2))) 45 | { 46 | if (!string.IsNullOrWhiteSpace(pair[0])) 47 | result[pair[0]] = pair.Length > 1 ? pair[1] : string.Empty; 48 | } 49 | 50 | return result; 51 | } 52 | 53 | public override string ToString() => 54 | parameters.Select(p => p.Key + (p.Value != null ? "=" + p.Value : string.Empty)) 55 | .Aggregate((x, y) => x + ";" + y); 56 | 57 | public string this[string index] 58 | { 59 | get => parameters[index]; 60 | set => parameters[index] = value; 61 | } 62 | 63 | public int Count => parameters.Count; 64 | 65 | public bool IsReadOnly => ((IDictionary)parameters).IsReadOnly; 66 | 67 | public ICollection Keys => ((IDictionary)parameters).Keys; 68 | 69 | public ICollection Values => ((IDictionary)parameters).Values; 70 | 71 | public void Add(KeyValuePair item) => ((IDictionary)parameters).Add(item); 72 | 73 | public void Add(string key, string value) => parameters.Add(key, value); 74 | 75 | public void Clear() => parameters.Clear(); 76 | 77 | public bool Contains(KeyValuePair item) => 78 | ((IDictionary)parameters).Contains(item); 79 | 80 | public bool ContainsKey(string key) => parameters.ContainsKey(key); 81 | 82 | public void CopyTo(KeyValuePair[] array, int arrayIndex) => 83 | ((IDictionary)parameters).CopyTo(array, arrayIndex); 84 | 85 | public IEnumerator> GetEnumerator() => 86 | ((IDictionary)parameters).GetEnumerator(); 87 | 88 | public bool Remove(KeyValuePair item) => ((IDictionary)parameters).Remove(item); 89 | 90 | public bool Remove(string key) => parameters.Remove(key); 91 | 92 | public bool TryGetValue(string key, out string value) => parameters.TryGetValue(key, out value!); 93 | 94 | IEnumerator IEnumerable.GetEnumerator() => ((IDictionary)parameters).GetEnumerator(); 95 | } 96 | } -------------------------------------------------------------------------------- /RTSP/Sdp/Media.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Globalization; 3 | 4 | namespace Rtsp.Sdp 5 | { 6 | public class Media 7 | { 8 | public Media(string mediaString) 9 | { 10 | // Example is 'video 0 RTP/AVP 26; 11 | var parts = mediaString.Split(' ', 4); 12 | 13 | if (parts.Length >= 1) 14 | { 15 | MediaType = parts[0] switch 16 | { 17 | "video" => MediaTypes.video, 18 | "audio" => MediaTypes.audio, 19 | "text" => MediaTypes.text, 20 | "application" => MediaTypes.application, 21 | "message" => MediaTypes.message, 22 | _ => MediaTypes.unknown,// standard does allow for future types to be defined 23 | }; 24 | } 25 | 26 | if (parts.Length >= 4) 27 | { 28 | if (int.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out int pt)) 29 | { 30 | PayloadType = pt; 31 | } 32 | else 33 | { 34 | PayloadType = 0; 35 | } 36 | } 37 | } 38 | 39 | // RFC4566 Media Types 40 | public enum MediaTypes { video, audio, text, application, message, unknown }; 41 | 42 | public IList Connections { get; set; } = []; 43 | 44 | public IList Bandwidths { get; } = []; 45 | 46 | public MediaTypes MediaType { get; set; } 47 | 48 | public int PayloadType { get; set; } 49 | 50 | public IList Attributs { get; } = []; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /RTSP/Sdp/Origin.cs: -------------------------------------------------------------------------------- 1 | namespace Rtsp.Sdp; 2 | 3 | using System; 4 | using System.Linq; 5 | 6 | /// 7 | /// Object ot represent origin in a Session Description Protocol 8 | /// 9 | public class Origin 10 | { 11 | /// 12 | /// Parses the specified origin string. 13 | /// 14 | /// The string to convert to origin object. 15 | /// The parsed origin object 16 | public static Origin Parse(string originString) 17 | { 18 | if (originString == null) 19 | throw new ArgumentNullException(nameof(originString)); 20 | 21 | string[] parts = originString.Split(' '); 22 | 23 | if (parts.Length != 6) 24 | throw new FormatException("Number of element invalid in origin string."); 25 | 26 | return new() 27 | { 28 | Username = parts[0], 29 | SessionId = parts[1], 30 | SessionVersion = parts[2], 31 | NetType = parts[3], 32 | AddressType = parts[4], 33 | UnicastAddress = parts[5], 34 | }; 35 | } 36 | 37 | public static Origin ParseLoose(string originString) 38 | { 39 | if (originString == null) 40 | throw new ArgumentNullException(nameof(originString)); 41 | 42 | string[] parts = originString.Split(' '); 43 | // some camera report invalid origin with more than 6 elements 44 | // the goods values are at the end. 45 | parts = [.. parts.Skip(parts.Length - 6)]; 46 | 47 | return new() 48 | { 49 | Username = parts.ElementAtOrDefault(0) ?? "-", 50 | SessionId = parts.ElementAtOrDefault(1) ?? "0", 51 | SessionVersion = parts.ElementAtOrDefault(2) ?? "0", 52 | NetType = parts.ElementAtOrDefault(3) ?? "IN", 53 | AddressType = parts.ElementAtOrDefault(4) ?? "IP4", 54 | UnicastAddress = parts.ElementAtOrDefault(5) ?? "0.0.0.0", 55 | }; 56 | } 57 | 58 | /// 59 | /// Gets or sets the username. 60 | /// 61 | /// It is the user's login on the originating host, or it is "-" 62 | /// if the originating host does not support the concept of user IDs. 63 | /// This MUST NOT contain spaces 64 | /// The username. 65 | public string Username { get; set; } = string.Empty; 66 | 67 | /// 68 | /// Gets or sets the session id. 69 | /// 70 | /// It is a numeric string such that the tuple of , 71 | /// , , , and forms a 72 | /// globally unique identifier for the session. The method of 73 | /// allocation is up to the creating tool, but it has been 74 | /// suggested that a Network Time Protocol (NTP) format timestamp be 75 | /// used to ensure uniqueness 76 | /// The session id. 77 | public string SessionId { get; set; } = string.Empty; 78 | 79 | /// 80 | /// Gets or sets the session version. 81 | /// 82 | /// The session version. 83 | public string SessionVersion { get; set; } = string.Empty; 84 | 85 | /// 86 | /// Gets or sets the type of the net. 87 | /// 88 | /// The type of the net. 89 | public string NetType { get; set; } = string.Empty; 90 | 91 | /// 92 | /// Gets or sets the type of the address. 93 | /// 94 | /// The type of the address. 95 | public string AddressType { get; set; } = string.Empty; 96 | 97 | /// 98 | /// Gets or sets the unicast address (IP or FQDN). 99 | /// 100 | /// The unicast address. 101 | public string UnicastAddress { get; set; } = string.Empty; 102 | 103 | public override string ToString() 104 | { 105 | return string.Join(" ", new[] 106 | { 107 | Username, 108 | SessionId, 109 | SessionVersion, 110 | NetType, 111 | AddressType, 112 | UnicastAddress, 113 | }); 114 | } 115 | } -------------------------------------------------------------------------------- /RTSP/Sdp/SdpTimeZone.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | 4 | namespace Rtsp.Sdp 5 | { 6 | public class SdpTimeZone 7 | { 8 | public required string RawValue { get; init; } 9 | 10 | public static SdpTimeZone ParseInvariant(string value) 11 | { 12 | if (value == null) 13 | throw new ArgumentNullException(nameof(value)); 14 | Contract.EndContractBlock(); 15 | 16 | return new() 17 | { 18 | RawValue = value, 19 | }; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /RTSP/Sdp/Timing.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace Rtsp.Sdp 5 | { 6 | public class Timing 7 | { 8 | public required long StartTime { get; init; } 9 | public required long StopTime { get; init; } 10 | 11 | internal static Timing Parse(string timing) 12 | { 13 | var parts = timing.Split(' '); 14 | if (parts.Length != 2) 15 | { 16 | throw new ArgumentException("Invalid timing format, need two number", nameof(timing)); 17 | } 18 | 19 | if (!long.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out long start)) 20 | { 21 | throw new ArgumentException("Invalid timing format, start time is not a number", nameof(timing)); 22 | } 23 | if (!long.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out long stop)) 24 | { 25 | throw new ArgumentException("Invalid timing format, stop time is not a number", nameof(timing)); 26 | } 27 | 28 | return new() 29 | { 30 | StartTime = start, 31 | StopTime = stop, 32 | }; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /RTSP/Utils/ReadOnlySequenceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Rtsp.Utils 6 | { 7 | public static class ReadOnlySequenceExtensions 8 | { 9 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 10 | public static (SequencePosition endOfLinePos, SequencePosition startOfLinePos)? FindEndOfLine(in this ReadOnlySequence buffer) 11 | { 12 | const byte lf = (byte)'\n'; 13 | var crlf = "\r\n"u8; 14 | 15 | var startOfSegment = buffer.Start; 16 | var position = startOfSegment; 17 | var findPos = buffer.Start; 18 | var checkForEndOfLine = false; 19 | while (buffer.TryGet(ref position, out var segment)) 20 | { 21 | if (segment.Length == 0) 22 | continue; 23 | 24 | if (checkForEndOfLine) 25 | { 26 | var nbToskip = segment.Span[0] == lf ? 1 : 0; 27 | return (findPos, buffer.GetPosition(nbToskip, startOfSegment)); 28 | } 29 | 30 | var pos = segment.Span.IndexOfAny(crlf); 31 | if (pos != -1) 32 | { 33 | findPos = buffer.GetPosition(pos, startOfSegment); 34 | // handle \n only 35 | if (segment.Span[pos] == lf) 36 | { 37 | return (findPos, buffer.GetPosition(pos + 1, startOfSegment)); 38 | } 39 | if (pos + 1 < segment.Length) 40 | { 41 | // handle \r\n ou \r other in same segment 42 | var nbToskip = segment.Span[pos + 1] == lf ? 2 : 1; 43 | return (findPos, buffer.GetPosition(pos + nbToskip, startOfSegment)); 44 | } 45 | 46 | // need to wait for next segment 47 | checkForEndOfLine = true; 48 | } 49 | 50 | startOfSegment = position; 51 | } 52 | return default; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /RTSP/Utils/SentMessageList.cs: -------------------------------------------------------------------------------- 1 | using Rtsp.Messages; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | 6 | namespace Rtsp.Utils 7 | { 8 | internal class SentMessageList 9 | { 10 | private readonly Dictionary _sentMessage = []; 11 | private uint _nbAddSinceLastCleanup; 12 | 13 | public void Add(int cSeq, RtspRequest originalMessage) 14 | { 15 | lock (_sentMessage) 16 | { 17 | _nbAddSinceLastCleanup++; 18 | if (_sentMessage.Count > 10 && _nbAddSinceLastCleanup > 100) 19 | { 20 | //cleanup 21 | foreach (var key in _sentMessage.Keys.Where(k => k < cSeq - 100).ToArray()) 22 | { 23 | _sentMessage.Remove(key); 24 | } 25 | _nbAddSinceLastCleanup = 0; 26 | } 27 | 28 | _sentMessage[cSeq] = originalMessage; 29 | } 30 | } 31 | 32 | public bool TryPopValue(int cSeq, [MaybeNullWhen(false)] out RtspRequest? originalRequest) 33 | { 34 | lock (_sentMessage) 35 | { 36 | if (_sentMessage.TryGetValue(cSeq, out originalRequest)) 37 | { 38 | _sentMessage.Remove(cSeq); 39 | return true; 40 | } 41 | return false; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /RtspCameraExample/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /RtspCameraExample/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("RtspCameraExample")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("RtspCameraExample")] 12 | [assembly: AssemblyCopyright("Copyright © 2016")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("5fa2077a-80e3-409a-9124-35a3d44b4a4e")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /RtspCameraExample/RtspCameraExample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | Exe 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /RtspCameraExample/SimpleG711Encoder.cs: -------------------------------------------------------------------------------- 1 | // Audio Encoder taken from the NAudio Project which is MIT Licenced 2 | namespace RtspCameraExample 3 | { 4 | public static class SimpleG711Encoder 5 | { 6 | public static byte[] EncodeULaw(short[] pcm) 7 | { 8 | byte[] output = new byte[pcm.Length]; 9 | for (int i = 0; i < pcm.Length; i++) 10 | { 11 | output[i] = MuLawEncoder.LinearToMuLawSample(pcm[i]); 12 | } 13 | return output; 14 | } 15 | 16 | public static byte[] EncodeALaw(short[] pcm) 17 | { 18 | byte[] output = new byte[pcm.Length]; 19 | for (int i = 0; i < pcm.Length; i++) 20 | { 21 | output[i] = ALawEncoder.LinearToALawSample(pcm[i]); 22 | } 23 | return output; 24 | } 25 | 26 | // 27 | // From NAudo (MIT Licence) 28 | // https://github.com/naudio/NAudio/tree/master/NAudio.Core/Codecs 29 | // 30 | 31 | /// 32 | /// mu-law encoder 33 | /// based on code from: 34 | /// http://hazelware.luggle.com/tutorials/mulawcompression.html 35 | /// 36 | public static class MuLawEncoder 37 | { 38 | private const int cBias = 0x84; 39 | private const int cClip = 32635; 40 | 41 | private static readonly byte[] MuLawCompressTable = 42 | [ 43 | 0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3, 44 | 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, 45 | 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, 46 | 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, 47 | 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 48 | 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 49 | 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 50 | 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 51 | 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 52 | 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 53 | 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 54 | 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 55 | 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 56 | 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 57 | 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 58 | 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7 59 | ]; 60 | 61 | /// 62 | /// Encodes a single 16 bit sample to mu-law 63 | /// 64 | /// 16 bit PCM sample 65 | /// mu-law encoded byte 66 | public static byte LinearToMuLawSample(short sample) 67 | { 68 | int sign = (sample >> 8) & 0x80; 69 | if (sign != 0) 70 | sample = (short)-sample; 71 | if (sample > cClip) 72 | sample = cClip; 73 | sample = (short)(sample + cBias); 74 | int exponent = (int)MuLawCompressTable[(sample >> 7) & 0xFF]; 75 | int mantissa = (sample >> (exponent + 3)) & 0x0F; 76 | int compressedByte = ~(sign | (exponent << 4) | mantissa); 77 | 78 | return (byte)compressedByte; 79 | } 80 | } 81 | 82 | /// 83 | /// A-law encoder 84 | /// 85 | public static class ALawEncoder 86 | { 87 | private const int cBias = 0x84; 88 | private const int cClip = 32635; 89 | private static readonly byte[] ALawCompressTable = 90 | [ 91 | 1,1,2,2,3,3,3,3, 92 | 4,4,4,4,4,4,4,4, 93 | 5,5,5,5,5,5,5,5, 94 | 5,5,5,5,5,5,5,5, 95 | 6,6,6,6,6,6,6,6, 96 | 6,6,6,6,6,6,6,6, 97 | 6,6,6,6,6,6,6,6, 98 | 6,6,6,6,6,6,6,6, 99 | 7,7,7,7,7,7,7,7, 100 | 7,7,7,7,7,7,7,7, 101 | 7,7,7,7,7,7,7,7, 102 | 7,7,7,7,7,7,7,7, 103 | 7,7,7,7,7,7,7,7, 104 | 7,7,7,7,7,7,7,7, 105 | 7,7,7,7,7,7,7,7, 106 | 7,7,7,7,7,7,7,7 107 | ]; 108 | 109 | /// 110 | /// Encodes a single 16 bit sample to a-law 111 | /// 112 | /// 16 bit PCM sample 113 | /// a-law encoded byte 114 | public static byte LinearToALawSample(short sample) 115 | { 116 | int sign; 117 | int exponent; 118 | int mantissa; 119 | byte compressedByte; 120 | 121 | sign = ((~sample) >> 8) & 0x80; 122 | if (sign == 0) 123 | sample = (short)-sample; 124 | if (sample > cClip) 125 | sample = cClip; 126 | if (sample >= 256) 127 | { 128 | exponent = (int)ALawCompressTable[(sample >> 8) & 0x7F]; 129 | mantissa = (sample >> (exponent + 3)) & 0x0F; 130 | compressedByte = (byte)((exponent << 4) | mantissa); 131 | } 132 | else 133 | { 134 | compressedByte = (byte)(sample >> 4); 135 | } 136 | compressedByte ^= (byte)(sign ^ 0x55); 137 | return compressedByte; 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /RtspCameraExample/SimpleH264Encoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace RtspCameraExample 5 | { 6 | // Simple H264 Encoder 7 | // Written by Jordi Cenzano (www.jordicenzano.name) 8 | // 9 | // Ported to C# by Roger Hardiman www.rjh.org.uk 10 | 11 | // This is a very simple lossless H264 encoder. No compression is used and so the output NAL data is as 12 | // large as the input YUV data. 13 | // It is used for a quick example of H264 encoding in pure .Net without needing OS specific APIs 14 | // or cross compiled C libraries. 15 | // 16 | // SimpleH264Encoder can use any image Width or Height 17 | public class SimpleH264Encoder 18 | { 19 | private readonly CJOCh264encoder h264encoder = new(); 20 | 21 | // Constuctor 22 | public SimpleH264Encoder(int width, int height) 23 | { 24 | // Initialise H264 encoder. 25 | h264encoder.IniCoder(width, height, CJOCh264encoder.SampleFormat.SAMPLE_FORMAT_YUV420p); 26 | // NAL array will contain SPS and PPS 27 | 28 | } 29 | 30 | // Raw SPS with no Size Header and no 00 00 00 01 headers 31 | public byte[] GetRawSPS() => h264encoder?.sps?.Skip(4).ToArray() ?? []; 32 | 33 | public byte[] GetRawPPS() => h264encoder?.pps?.Skip(4).ToArray() ?? []; 34 | 35 | public ReadOnlySpan CompressFrame(ReadOnlySpan yuv_data) 36 | { 37 | // Get the NAL (which has the 00 00 00 01 header) 38 | var nal_with_header = h264encoder.CodeAndSaveFrame(yuv_data); 39 | 40 | return nal_with_header[4..]; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /RtspClientExample/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /RtspClientExample/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("RtspClientExample")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("RtspClientExample")] 12 | [assembly: AssemblyCopyright("Copyright © 2016")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("28474167-2637-4660-ab75-0854a1cd0d78")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /RtspClientExample/RTSPEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace RtspClientExample 5 | { 6 | public class NewStreamEventArgs : EventArgs 7 | { 8 | public NewStreamEventArgs(string streamType, IStreamConfigurationData? streamConfigurationData) 9 | { 10 | StreamType = streamType; 11 | StreamConfigurationData = streamConfigurationData; 12 | } 13 | 14 | public string StreamType { get; } 15 | public IStreamConfigurationData? StreamConfigurationData { get; } 16 | } 17 | 18 | public interface IStreamConfigurationData; 19 | 20 | public record H264StreamConfigurationData : IStreamConfigurationData 21 | { 22 | public required byte[] SPS { get; init; } 23 | public required byte[] PPS { get; init; } 24 | } 25 | 26 | public record H265StreamConfigurationData: IStreamConfigurationData 27 | { 28 | public required byte[] VPS { get; init; } 29 | public required byte[] SPS { get; init; } 30 | public required byte[] PPS { get; init; } 31 | } 32 | 33 | public record AacStreamConfigurationData : IStreamConfigurationData 34 | { 35 | public int ObjectType { get; init; } 36 | public int FrequencyIndex { get; init; } 37 | public int SamplingFrequency { get; init; } 38 | public int ChannelConfiguration { get; init; } 39 | } 40 | 41 | public class SimpleDataEventArgs : EventArgs 42 | { 43 | public SimpleDataEventArgs(IEnumerable> data, DateTime timeStamp) 44 | { 45 | Data = data; 46 | TimeStamp = timeStamp; 47 | } 48 | 49 | public DateTime TimeStamp { get; } 50 | public IEnumerable> Data { get; } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /RtspClientExample/RTSPMessageAuthExtension.cs: -------------------------------------------------------------------------------- 1 | using Rtsp; 2 | using Rtsp.Messages; 3 | using System; 4 | 5 | namespace RtspClientExample 6 | { 7 | public static class RTSPMessageAuthExtension 8 | { 9 | public static void AddAuthorization(this RtspRequest message, Authentication? authentication, Uri uri, uint commandCounter) 10 | { 11 | if (authentication is null) 12 | { 13 | return; 14 | } 15 | 16 | string authorization = authentication.GetResponse(commandCounter, uri.AbsoluteUri, message.RequestTyped.ToString(), []); 17 | // remove if already one... 18 | message.Headers.Remove(RtspHeaderNames.Authorization); 19 | message.Headers.Add(RtspHeaderNames.Authorization, authorization); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /RtspClientExample/RtspClientExample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | Exe 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /RtspMultiplexer/NLog.config: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /RtspMultiplexer/OriginContext.cs: -------------------------------------------------------------------------------- 1 | namespace RtspMultiplexer; 2 | 3 | using Rtsp; 4 | 5 | /// 6 | /// Class to store source information of the request. 7 | /// 8 | internal class OriginContext 9 | { 10 | public int OriginCSeq { get; internal set; } 11 | public RtspListener OriginSourcePort { get; internal set; } 12 | } 13 | -------------------------------------------------------------------------------- /RtspMultiplexer/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using RtspMultiplexer; 3 | using System; 4 | 5 | NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); 6 | 7 | _logger.Info("Starting"); 8 | RtspServer monServeur = new(8554); 9 | 10 | monServeur.StartListen(); 11 | RTSPDispatcher.Instance.StartQueue(); 12 | 13 | while (Console.ReadLine() != "q") 14 | { 15 | } 16 | 17 | monServeur.StopListen(); 18 | RTSPDispatcher.Instance.StopQueue(); 19 | -------------------------------------------------------------------------------- /RtspMultiplexer/RtspMultiplexer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | PreserveNewest 12 | Designer 13 | 14 | 15 | Designer 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /RtspMultiplexer/RtspPushDescription.cs: -------------------------------------------------------------------------------- 1 | namespace RtspMultiplexer; 2 | 3 | using System.Collections.Generic; 4 | 5 | public class RtspPushDescription 6 | { 7 | public string Sdp { get; } 8 | public string AbsolutePath { get; } 9 | 10 | private string pushSession; 11 | private readonly Dictionary forwarders = []; 12 | 13 | public RtspPushDescription(string absolutePath, string sdp) 14 | { 15 | AbsolutePath = absolutePath; 16 | Sdp = sdp; 17 | } 18 | 19 | public void AddForwarders(string session, string path, Forwarder forwarder) 20 | { 21 | if (string.IsNullOrEmpty(pushSession)) 22 | { 23 | pushSession = session; 24 | } 25 | else 26 | { 27 | // TODO better session management 28 | if (pushSession != session) 29 | throw new System.Exception("Invalid state"); 30 | } 31 | forwarders.Add(path, forwarder); 32 | 33 | forwarder.ToMulticast = true; 34 | forwarder.ForwardHostVideo = "239.0.0.1"; 35 | forwarder.ForwardPortVideo = forwarder.FromForwardVideoPort; 36 | } 37 | 38 | public Forwarder GetForwarderFor(string path) 39 | { 40 | // TODO change to return only info and not all forwarder 41 | return forwarders[path]; 42 | } 43 | 44 | public void Start(string session) 45 | { 46 | // TODO better session management 47 | if (pushSession != session) 48 | throw new System.Exception("Invalid state"); 49 | foreach (var forwarder in forwarders.Values) 50 | { 51 | forwarder.Start(); 52 | } 53 | } 54 | 55 | internal void Stop(string session) 56 | { 57 | // TODO better session management 58 | if (pushSession != session) 59 | throw new System.Exception("Invalid state"); 60 | foreach (var forwarder in forwarders.Values) 61 | { 62 | forwarder.Stop(); 63 | } 64 | forwarders.Clear(); 65 | pushSession = null; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /RtspMultiplexer/RtspServer.cs: -------------------------------------------------------------------------------- 1 | namespace RtspMultiplexer; 2 | 3 | using Rtsp; 4 | using System; 5 | using System.Diagnostics.Contracts; 6 | using System.Net; 7 | using System.Net.Sockets; 8 | using System.Threading; 9 | 10 | public class RtspServer : IDisposable 11 | { 12 | private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); 13 | 14 | private readonly TcpListener _RTSPServerListener; 15 | private ManualResetEvent _Stopping; 16 | private Thread _ListenTread; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// listen port number. 22 | public RtspServer(int portNumber) 23 | { 24 | if (portNumber < IPEndPoint.MinPort || portNumber > IPEndPoint.MaxPort) 25 | throw new ArgumentOutOfRangeException(nameof(portNumber), portNumber, "Port number must be between System.Net.IPEndPoint.MinPort and System.Net.IPEndPoint.MaxPort"); 26 | Contract.EndContractBlock(); 27 | 28 | RtspUtils.RegisterUri(); 29 | _RTSPServerListener = new TcpListener(IPAddress.Any, portNumber); 30 | } 31 | 32 | /// 33 | /// Starts the listen. 34 | /// 35 | public void StartListen() 36 | { 37 | _RTSPServerListener.Start(); 38 | 39 | _Stopping = new ManualResetEvent(false); 40 | _ListenTread = new Thread(new ThreadStart(AcceptConnection)); 41 | _ListenTread.Start(); 42 | } 43 | 44 | /// 45 | /// Accepts the connection. 46 | /// 47 | private void AcceptConnection() 48 | { 49 | try 50 | { 51 | while (!_Stopping.WaitOne(0)) 52 | { 53 | TcpClient oneClient = _RTSPServerListener.AcceptTcpClient(); 54 | RtspListener newListener = new(new RtspTcpTransport(oneClient)); 55 | RTSPDispatcher.Instance.AddListener(newListener); 56 | newListener.Start(); 57 | } 58 | } 59 | catch (SocketException error) 60 | { 61 | _logger.Warn(error, "Got an error listening, I have to handle the stopping which also throw an error"); 62 | } 63 | catch (Exception error) 64 | { 65 | _logger.Error(error, "Got an error listening..."); 66 | throw; 67 | } 68 | } 69 | 70 | public void StopListen() 71 | { 72 | _RTSPServerListener.Stop(); 73 | _Stopping.Set(); 74 | _ListenTread.Join(); 75 | } 76 | 77 | #region IDisposable Membres 78 | 79 | public void Dispose() 80 | { 81 | Dispose(true); 82 | GC.SuppressFinalize(this); 83 | } 84 | 85 | protected virtual void Dispose(bool disposing) 86 | { 87 | if (disposing) 88 | { 89 | StopListen(); 90 | _Stopping.Dispose(); 91 | } 92 | } 93 | 94 | #endregion 95 | } 96 | -------------------------------------------------------------------------------- /RtspMultiplexer/RtspSession.cs: -------------------------------------------------------------------------------- 1 | namespace RtspMultiplexer; 2 | 3 | using Rtsp.Messages; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics.Contracts; 7 | using System.Threading; 8 | 9 | public class RtspSession 10 | { 11 | private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); 12 | 13 | public RtspSession() 14 | { 15 | State = SessionState.Init; 16 | } 17 | 18 | public string Name { get; set; } 19 | 20 | private Thread _timeoutThread; 21 | private readonly AutoResetEvent _dataReceive = new(false); 22 | private bool _stoping = false; 23 | 24 | /// 25 | /// Server State 26 | /// 27 | internal enum SessionState 28 | { 29 | /// 30 | /// The initial state, no valid SETUP has been received yet. 31 | /// 32 | Init, 33 | /// 34 | /// Last SETUP received was successful, reply sent or after 35 | /// playing, last PAUSE received was successful, reply sent. 36 | /// 37 | Ready, 38 | /// 39 | /// Last PLAY received was successful, reply sent. Data is being 40 | /// sent. 41 | /// 42 | Playing, 43 | /// 44 | /// The server is recording media data. 45 | /// 46 | Recording, 47 | } 48 | 49 | internal SessionState State 50 | { 51 | get; 52 | set; 53 | } 54 | 55 | public Dictionary ListOfForwader { get; } = []; 56 | 57 | public int Timeout { get; set; } 58 | 59 | private readonly List _clientList = []; 60 | 61 | public bool IsNeeded => _clientList.Count > 0; 62 | 63 | public void Start(string clientAddress) 64 | { 65 | _logger.Info("Starting session: {0} ", Name); 66 | 67 | if (!_clientList.Contains(clientAddress)) 68 | _clientList.Add(clientAddress); 69 | 70 | if (_timeoutThread?.IsAlive == true) 71 | { 72 | _logger.Debug("Session: {0} was already running", Name); 73 | return; 74 | } 75 | 76 | foreach (var item in ListOfForwader) 77 | { 78 | item.Value.CommandReceive += CommandReceive; 79 | item.Value.Start(); 80 | } 81 | _timeoutThread = new Thread(new ThreadStart(TimeoutDetecter)); 82 | _stoping = false; 83 | _timeoutThread.Start(); 84 | } 85 | 86 | /// 87 | /// Detect Timeouts . 88 | /// 89 | private void TimeoutDetecter() 90 | { 91 | _logger.Debug("Start waiting for timeout of {0}s", Timeout); 92 | // wait until timeout, set of _dataReceive will reset timeout 93 | while (_dataReceive.WaitOne(Timeout * 1000)) 94 | { 95 | if (_stoping) 96 | break; 97 | } 98 | if (!_stoping) 99 | { 100 | // if we are here we timeOut 101 | _logger.Warn("Session {0} timeout", Name); 102 | TearDown(); 103 | } 104 | } 105 | 106 | internal void TearDown() 107 | { 108 | //TODO vérifier ce bout de code.... 109 | // Je suis vraiement pas sur là. 110 | foreach (var destinationUri in ListOfForwader.Keys) 111 | { 112 | RtspRequest tearDownMessage = new() 113 | { 114 | RequestTyped = RtspRequest.RequestType.TEARDOWN, 115 | RtspUri = destinationUri 116 | }; 117 | RTSPDispatcher.Instance.Enqueue(tearDownMessage); 118 | } 119 | Stop(); 120 | } 121 | 122 | internal void Stop(string clientAdress) 123 | { 124 | _clientList.Remove(clientAdress); 125 | 126 | if (!IsNeeded) 127 | Stop(); 128 | } 129 | 130 | private void Stop() 131 | { 132 | _logger.Info("Stopping session: {0} ", Name); 133 | 134 | foreach (var item in ListOfForwader) 135 | { 136 | item.Value.CommandReceive -= CommandReceive; 137 | item.Value.Stop(); 138 | } 139 | 140 | // stop the timeout detect 141 | _stoping = true; 142 | _dataReceive.Set(); 143 | } 144 | 145 | private void CommandReceive(object sender, EventArgs e) 146 | { 147 | _dataReceive.Set(); 148 | } 149 | 150 | internal void Handle(RtspRequest _) 151 | { 152 | CommandReceive(this, EventArgs.Empty); 153 | } 154 | 155 | /// 156 | /// Gets the key name of the session. 157 | /// This value is contruct with the destination name and the session header name. 158 | /// 159 | /// The original asked URI. 160 | /// Session header value. 161 | /// 162 | internal static string GetSessionName(Uri uri, string aSessionHeaderValue) 163 | { 164 | Contract.Requires(uri != null); 165 | Contract.Requires(aSessionHeaderValue != null); 166 | 167 | return GetSessionName(uri.Authority, aSessionHeaderValue); 168 | } 169 | 170 | /// 171 | /// Gets the name of the session. 172 | /// 173 | /// A destination. 174 | /// A session header value. 175 | /// 176 | internal static string GetSessionName(string aDestination, string aSessionHeaderValue) 177 | { 178 | Contract.Requires(aDestination != null); 179 | Contract.Requires(aSessionHeaderValue != null); 180 | 181 | return aDestination + "|Session:" + aSessionHeaderValue; 182 | } 183 | 184 | internal void AddForwarder(Uri uri, Forwarder forwarder) 185 | { 186 | Contract.Requires(uri != null); 187 | 188 | // Configuration change, remove the old forwarder 189 | if (ListOfForwader.TryGetValue(uri, out Forwarder existingForwarder)) 190 | { 191 | existingForwarder.Stop(); 192 | ListOfForwader.Remove(uri); 193 | } 194 | 195 | ListOfForwader.Add(uri, forwarder); 196 | } 197 | 198 | /// 199 | /// Gets or sets the destination name of the current session.. 200 | /// 201 | /// The destination. 202 | public string Destination { get; set; } 203 | } 204 | -------------------------------------------------------------------------------- /RtspMultiplexer/TcpToUdpForwader.cs: -------------------------------------------------------------------------------- 1 | namespace RtspMultiplexer; 2 | 3 | using Rtsp; 4 | using Rtsp.Messages; 5 | using System; 6 | using System.Diagnostics.Contracts; 7 | using System.Net; 8 | using System.Net.Sockets; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | public class TCPtoUDPForwader : Forwarder 13 | { 14 | private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); 15 | 16 | private Thread _forwardCThread; 17 | 18 | public TCPtoUDPForwader() : base() 19 | { 20 | ForwardInterleavedCommand = -1; 21 | } 22 | 23 | /// 24 | /// Gets or sets the forward command port. 25 | /// 26 | /// The forward command. 27 | public RtspListener ForwardCommand { get; set; } 28 | /// 29 | /// Gets or sets the source interleaved video. 30 | /// 31 | /// The source interleaved video. 32 | public int SourceInterleavedVideo { get; set; } 33 | /// 34 | /// Gets or sets the forward interleaved command. 35 | /// 36 | /// The forward interleaved command. 37 | public int ForwardInterleavedCommand { get; set; } 38 | 39 | /// 40 | /// Starts this instance. 41 | /// 42 | public override void Start() 43 | { 44 | _logger.Debug("Forward from TCP channel:{0} => {1}:{2}", SourceInterleavedVideo, ForwardHostVideo, ForwardPortVideo); 45 | ForwardVUdpPort.Connect(ForwardHostVideo, ForwardPortVideo); 46 | 47 | ForwardCommand.DataReceived += HandleDataReceive; 48 | 49 | if (ForwardInterleavedCommand >= 0) 50 | { 51 | _forwardCThread = new Thread(new ThreadStart(DoCommandJob)); 52 | _forwardCThread.Start(); 53 | } 54 | } 55 | 56 | /// 57 | /// Stops this instance. 58 | /// 59 | public override void Stop() 60 | { 61 | if (ToMulticast && ForwardInterleavedCommand >= 0) 62 | { 63 | IPAddress multicastAdress = IPAddress.Parse(ForwardHostVideo); 64 | ListenCUdpPort.DropMulticastGroup(multicastAdress); 65 | } 66 | 67 | ForwardCommand.DataReceived -= HandleDataReceive; 68 | 69 | ListenCUdpPort.Close(); 70 | ForwardVUdpPort.Close(); 71 | } 72 | 73 | /// 74 | /// Does the command job. 75 | /// 76 | private void DoCommandJob() 77 | { 78 | IPEndPoint udpEndPoint = new(IPAddress.Any, ListenCommandPort); 79 | if (ToMulticast) 80 | { 81 | IPAddress multicastAdress = IPAddress.Parse(ForwardHostVideo); 82 | ListenCUdpPort.JoinMulticastGroup(multicastAdress); 83 | _logger.Debug("Forward Command from multicast {0}:{1} => TCP interleaved {2}", ForwardHostVideo, ListenCommandPort, ForwardInterleavedCommand); 84 | } 85 | else 86 | { 87 | _logger.Debug("Forward Command from {0} => TCP interleaved {1}", ListenCommandPort, ForwardInterleavedCommand); 88 | } 89 | 90 | byte[] frame; 91 | try 92 | { 93 | // Todo use cancellation token 94 | while (true) 95 | { 96 | frame = ListenCUdpPort.Receive(ref udpEndPoint); 97 | ForwardCommand.SendDataAsync(ForwardInterleavedCommand, frame) 98 | .ContinueWith(_ => CommandFrameSended(frame), TaskContinuationOptions.OnlyOnRanToCompletion) 99 | .ContinueWith(t => _logger.Error(t.Exception, "Error during command forwarding"), TaskContinuationOptions.OnlyOnFaulted); 100 | } 101 | //The break of the loop is made by close wich raise an exception 102 | } 103 | catch (ObjectDisposedException) 104 | { 105 | _logger.Debug("Forward command closed"); 106 | } 107 | catch (SocketException) 108 | { 109 | _logger.Debug("Forward command closed"); 110 | } 111 | } 112 | 113 | /// 114 | /// Handles the data receive. 115 | /// 116 | /// The sender. 117 | /// The instance containing the event data. 118 | public void HandleDataReceive(object sender, RtspChunkEventArgs e) 119 | { 120 | if (e == null) 121 | throw new ArgumentNullException(nameof(e)); 122 | Contract.EndContractBlock(); 123 | try 124 | { 125 | if (e.Message is RtspData data) 126 | { 127 | ReadOnlyMemory frame = data.Data; 128 | if (data.Channel == SourceInterleavedVideo) 129 | { 130 | ForwardVUdpPort.BeginSend(frame.ToArray(), frame.Length, new AsyncCallback(EndSendVideo), frame); 131 | } 132 | } 133 | } 134 | catch (Exception error) 135 | { 136 | _logger.Warn(error, "Error during frame forwarding"); 137 | } 138 | } 139 | 140 | /// 141 | /// Ends the send video. 142 | /// 143 | /// The result. 144 | private void EndSendVideo(IAsyncResult result) 145 | { 146 | try 147 | { 148 | int nbOfByteSend = ForwardVUdpPort.EndSend(result); 149 | byte[] frame = (byte[])result.AsyncState; 150 | VideoFrameSended(nbOfByteSend, frame); 151 | } 152 | catch (Exception error) 153 | { 154 | _logger.Error(error, "Error during video forwarding"); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /TestConsoleDotNetFramework/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TestConsoleDotNetFramework/Program.cs: -------------------------------------------------------------------------------- 1 | using Rtsp; 2 | using Rtsp.Messages; 3 | using Rtsp.Rtp; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Security.Policy; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace TestConsole 12 | { 13 | internal class Program 14 | { 15 | static void Main(string[] args) 16 | { 17 | RtspUtils.RegisterUri(); 18 | RtspTcpTransport transport = new RtspTcpTransport(new Uri(args[0])); 19 | RtspListener listener = new RtspListener(transport); 20 | 21 | listener.MessageReceived += (sender, e) => 22 | { 23 | Console.WriteLine("Received " + e.Message); 24 | }; 25 | listener.Start(); 26 | 27 | RtspRequest optionsMessage = new RtspRequestOptions(); 28 | listener.SendMessage(optionsMessage); 29 | 30 | RtspRequest describeMessage = new RtspRequestDescribe(); 31 | listener.SendMessage(describeMessage); 32 | 33 | 34 | 35 | Console.WriteLine("Press enter to exit"); 36 | Console.ReadLine(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /TestConsoleDotNetFramework/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // Les informations générales relatives à un assembly dépendent de 6 | // l'ensemble d'attributs suivant. Changez les valeurs de ces attributs pour modifier les informations 7 | // associées à un assembly. 8 | [assembly: AssemblyTitle("TestConsole")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("TestConsole")] 13 | [assembly: AssemblyCopyright("Copyright © 2024")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // L'affectation de la valeur false à ComVisible rend les types invisibles dans cet assembly 18 | // aux composants COM. Si vous devez accéder à un type dans cet assembly à partir de 19 | // COM, affectez la valeur true à l'attribut ComVisible sur ce type. 20 | [assembly: ComVisible(false)] 21 | 22 | // Le GUID suivant est pour l'ID de la typelib si ce projet est exposé à COM 23 | [assembly: Guid("63bc1863-2796-4935-a2de-11b0b1e29b4a")] 24 | 25 | // Les informations de version pour un assembly se composent des quatre valeurs suivantes : 26 | // 27 | // Version principale 28 | // Version secondaire 29 | // Numéro de build 30 | // Révision 31 | // 32 | // Vous pouvez spécifier toutes les valeurs ou indiquer les numéros de build et de révision par défaut 33 | // en utilisant '*', comme indiqué ci-dessous : 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /TestConsoleDotNetFramework/TestConsoleDotNetFramework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {63BC1863-2796-4935-A2DE-11B0B1E29B4A} 8 | Exe 9 | TestConsole 10 | TestConsole 11 | v4.8 12 | 512 13 | true 14 | true 15 | 16 | 17 | 18 | AnyCPU 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | ..\packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll 39 | 40 | 41 | ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.8.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll 42 | 43 | 44 | ..\packages\Microsoft.Extensions.Logging.Abstractions.8.0.0\lib\net462\Microsoft.Extensions.Logging.Abstractions.dll 45 | 46 | 47 | ..\packages\SharpRTSP.1.2.1\lib\netstandard2.0\RTSP.dll 48 | 49 | 50 | 51 | ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll 52 | 53 | 54 | 55 | ..\packages\System.Configuration.ConfigurationManager.8.0.0\lib\net462\System.Configuration.ConfigurationManager.dll 56 | 57 | 58 | 59 | ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll 60 | 61 | 62 | 63 | ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll 64 | 65 | 66 | ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll 67 | 68 | 69 | ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /TestConsoleDotNetFramework/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------