├── .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 |
--------------------------------------------------------------------------------