├── global.json
├── RTSP.Tests
├── Rtp
│ ├── Data
│ │ ├── jpeg_0.rtp
│ │ ├── jpeg_1.rtp
│ │ ├── jpeg_2.rtp
│ │ └── img_jpg_0.jpg
│ ├── RawMediaFrameTests.cs
│ └── JpegPayloadTests.cs
├── Sdp
│ ├── Data
│ │ ├── test1.sdp
│ │ ├── test3.sdp
│ │ ├── test5.sdp
│ │ ├── test9.sdp
│ │ ├── test2.sdp
│ │ ├── test7.sdp
│ │ ├── test8.sdp
│ │ ├── test4.sdp
│ │ ├── test6.sdp
│ │ └── testA.sdp
│ └── H264ParametersTests.cs
├── RTSPTests.nunit
├── RTSPUtilsTest.cs
├── Messages
│ ├── RtspDataTest.cs
│ ├── PortCoupleTests.cs
│ ├── RtspResponseTests.cs
│ └── RTSPMessageTest.cs
├── Onvif
│ └── RtpPacketOnvifUtilsTests.cs
├── RtspListenSocketTests.cs
├── TestUtils
│ ├── InBlockingStream.cs
│ └── InOutStream.cs
├── RTSP.Tests.csproj
├── BitStreamTests.cs
├── Authentication
│ └── AuthenticationBasicTests.cs
└── Integration
│ └── RTSPListenerIntegrationTests.cs
├── RTSP
├── Messages
│ ├── RTSPRequestRecord.cs
│ ├── RTSPRequestAnnounce.cs
│ ├── RTSPRequestPause.cs
│ ├── RTSPRequestPlay.cs
│ ├── RTSPRequestDescribe.cs
│ ├── RTSPRequestTeardown.cs
│ ├── RTSPRequestRedirect.cs
│ ├── RTSPRequestGetParameter.cs
│ ├── RTSPRequestSetParameter.cs
│ ├── RTSPHeaderUtils.cs
│ ├── RTSPChunk.cs
│ ├── RTSPRequestOptions.cs
│ ├── RTSPHeaderNames.cs
│ ├── RTSPRequestSetup.cs
│ ├── RTSPData.cs
│ ├── PortCouple.cs
│ └── RTSPRequest.cs
├── Sdp
│ ├── AV1Parameters.cs
│ ├── SdpTimeZone.cs
│ ├── H264Parameters.cs
│ ├── Bandwidth.cs
│ ├── ConnectionIP6.cs
│ ├── Timing.cs
│ ├── ConnectionIP4.cs
│ ├── H265Parameters.cs
│ ├── Media.cs
│ ├── Connection.cs
│ ├── Attribut.cs
│ ├── AttributRtpMap.cs
│ ├── AttributFmtp.cs
│ ├── H266Parameters.cs
│ ├── ParametersBase.cs
│ └── Origin.cs
├── Rtp
│ ├── MP2TransportPayload.cs
│ ├── G711Payload.cs
│ ├── IPayloadProcessor.cs
│ ├── RawPayload.cs
│ ├── RtpPacketUtil.cs
│ ├── AMRPayload.cs
│ ├── RtpPacket.cs
│ ├── RawMediaFrame.cs
│ ├── G711_1Payload.cs
│ └── JPEGDefaultTables.cs
├── NetworkCredentialExtensions.cs
├── HttpBadResponseException.cs
├── IRtspListenSocket.cs
├── Properties
│ └── AssemblyInfo.cs
├── RTSPHttpsTransport.cs
├── HttpBadResponseCodeException.cs
├── RTSPDataEventArgs.cs
├── RtspChunkEventArgs.cs
├── HeadersParser.cs
├── IRtpTransport.cs
├── RtspListenSocket.cs
├── Utils
│ ├── SentMessageList.cs
│ └── ReadOnlySequenceExtensions.cs
├── RtspOverHttpTLSListenSocket.cs
├── IRTSPTransport.cs
├── RtspTlsListenSocket.cs
├── Rtcp
│ ├── RtcpPacketUtil.cs
│ └── RtcpPacket.cs
├── RTSPUtils.cs
├── Onvif
│ ├── RtspMessageOnvifExtension.cs
│ └── RtpPacketOnvifUtils.cs
├── AuthenticationBasic.cs
├── MulticastUdpSocket.cs
├── RtpTcpTransport.cs
├── RTSP.csproj
├── BitStream.cs
├── RTSPTcpTlsTransport.cs
├── RTSPTCPTransport.cs
└── Authentication.cs
├── RtspCameraExample
├── App.config
├── RtspCameraExample.csproj
├── Properties
│ └── AssemblyInfo.cs
├── SimpleH264Encoder.cs
└── SimpleG711Encoder.cs
├── RtspClientExample
├── App.config
├── RtspClientExample.csproj
├── RTSPMessageAuthExtension.cs
├── Properties
│ └── AssemblyInfo.cs
└── RTSPEventArgs.cs
├── .gitignore
├── RtspMultiplexer
├── OriginContext.cs
├── Program.cs
├── RtspMultiplexer.csproj
├── NLog.config
├── RtspPushDescription.cs
├── RtspServer.cs
├── TcpToUdpForwader.cs
└── RtspSession.cs
├── TestConsoleDotNetFramework
├── App.config
├── packages.config
├── Program.cs
├── Properties
│ └── AssemblyInfo.cs
└── TestConsoleDotNetFramework.csproj
├── .github
└── workflows
│ ├── dotnet.yml
│ └── publish-nuget.yml
├── .editorconfig
├── LICENSE
└── RTSP.sln
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "10.0.100",
4 | "rollForward": "latestMinor"
5 | }
6 | }
--------------------------------------------------------------------------------
/RTSP.Tests/Rtp/Data/jpeg_0.rtp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngraziano/SharpRTSP/HEAD/RTSP.Tests/Rtp/Data/jpeg_0.rtp
--------------------------------------------------------------------------------
/RTSP.Tests/Rtp/Data/jpeg_1.rtp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngraziano/SharpRTSP/HEAD/RTSP.Tests/Rtp/Data/jpeg_1.rtp
--------------------------------------------------------------------------------
/RTSP.Tests/Rtp/Data/jpeg_2.rtp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngraziano/SharpRTSP/HEAD/RTSP.Tests/Rtp/Data/jpeg_2.rtp
--------------------------------------------------------------------------------
/RTSP.Tests/Rtp/Data/img_jpg_0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngraziano/SharpRTSP/HEAD/RTSP.Tests/Rtp/Data/img_jpg_0.jpg
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/RtspCameraExample/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/RtspClientExample/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/RTSP/Messages/RTSPRequestRedirect.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Messages;
2 |
3 | public class RtspRequestRedirect : RtspRequest
4 | {
5 | public RtspRequestRedirect()
6 | {
7 | Command = "REDIRECT * RTSP/1.0";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/RTSP/Messages/RTSPRequestSetParameter.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Messages;
2 |
3 | public class RtspRequestSetParameter : RtspRequest
4 | {
5 | public RtspRequestSetParameter()
6 | {
7 | Command = "SET_PARAMETER * RTSP/1.0";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/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/Sdp/AV1Parameters.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Sdp;
2 |
3 | using System.Collections.Generic;
4 |
5 | public class AV1Parameters : ParametersBase, IDictionary
6 | {
7 | public static AV1Parameters Parse(string parameterString) => Parse(parameterString);
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/.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/
--------------------------------------------------------------------------------
/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/RTSPTests.nunit:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/NetworkCredentialExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp;
2 |
3 | using System.Net;
4 |
5 | static class NetworkCredentialExtensions
6 | {
7 | extension(NetworkCredential networkCredential)
8 | {
9 | public bool IsEmpty()
10 | {
11 | return string.IsNullOrEmpty(networkCredential.UserName) || networkCredential.Password == null;
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/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/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/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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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.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/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.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/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.Tests/Sdp/Data/testA.sdp:
--------------------------------------------------------------------------------
1 | v=0
2 | o=- 0 0 IN IP4 127.0.0.1
3 | s=Big Buck Bunny 60fps 4K - Official Blender Foundation Short Film
4 | t=0 0
5 | a=tool:libavformat 62.1.103
6 | m=video 11111 RTP/AVP 96
7 | a=control:trackID=0
8 | c=IN IP4 127.0.0.1
9 | b=AS:893
10 | a=rtpmap:96 AV1/90000
11 | a=fmtp:96 profile=0;level-idx=8;tier=0
12 | m=audio 11113 RTP/AVP 97
13 | a=control:trackID=1
14 | c=IN IP4 127.0.0.1
15 | b=AS:127
16 | a=rtpmap:97 MPEG4-GENERIC/44100/2
17 | a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=12100000000000000000000000000000
18 |
--------------------------------------------------------------------------------
/TestConsoleDotNetFramework/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/RTSP/Sdp/H264Parameters.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Sdp;
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 |
8 | public class H264Parameters : ParametersBase, IDictionary
9 | {
10 | private const string HeaderName = "sprop-parameter-sets";
11 |
12 | public IList SpropParameterSets =>
13 | TryGetValue(HeaderName, out var value)
14 | ? value.Split(',').Select(Convert.FromBase64String).ToList()
15 | : [];
16 |
17 | public static H264Parameters Parse(string parameterString) => Parse(parameterString);
18 | }
--------------------------------------------------------------------------------
/RtspCameraExample/RtspCameraExample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net10.0
4 | Exe
5 | false
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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.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/IRtspListenSocket.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Rtsp;
5 |
6 | ///
7 | /// Interface for a RTSP server listening to a socket
8 | ///
9 | public interface IRtspListenSocket
10 | {
11 | ///
12 | /// Accept a new connection
13 | ///
14 | /// Connection accepeted
15 | Task AcceptAsync(CancellationToken cancellationToken);
16 |
17 | ///
18 | /// Start listening
19 | ///
20 | void Start();
21 |
22 | ///
23 | /// Stop listening
24 | ///
25 | void Stop();
26 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/RtspMultiplexer/RtspMultiplexer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net10.0
4 | Exe
5 |
6 |
7 |
8 |
9 |
10 |
11 | PreserveNewest
12 | Designer
13 |
14 |
15 | Designer
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/RtspClientExample/RtspClientExample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net10.0
4 | Exe
5 | false
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/RTSP/RTSPHttpsTransport.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net.Security;
4 |
5 | namespace Rtsp;
6 |
7 | public class RTSPHttpsTransport(Uri uri, System.Net.NetworkCredential credentials, RemoteCertificateValidationCallback? userCertificateSelectionCallback = null) : RtspHttpTransport(uri, credentials)
8 | {
9 | private readonly RemoteCertificateValidationCallback? _userCertificateSelectionCallback = userCertificateSelectionCallback;
10 |
11 | public override Stream GetStream()
12 | {
13 | var sslStream = new SslStream(base.GetStream(), true, _userCertificateSelectionCallback);
14 |
15 | sslStream.AuthenticateAsClient(Uri.Host);
16 | return sslStream;
17 | }
18 | }
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/RTSP/HeadersParser.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp;
2 |
3 | using System;
4 | using System.Collections.Specialized;
5 | using System.IO;
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 |
--------------------------------------------------------------------------------
/RTSP/IRtpTransport.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp;
2 |
3 | using System;
4 | using System.Threading.Tasks;
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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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.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 |
--------------------------------------------------------------------------------
/.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 I do not beleive in small is always better
10 | MA0051.maximum_lines_per_method = 150
11 | MA0051.maximum_statements_per_method = 100
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
29 |
30 | # Resolve TODO
31 | # TODO will not be fix soon so reduce the warning level
32 | dotnet_diagnostic.MA0026.severity = suggestion
33 |
--------------------------------------------------------------------------------
/TestConsoleDotNetFramework/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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/Messages/RTSPRequestSetup.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Messages;
2 |
3 | using System.Linq;
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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | ///
10 | /// An helper method to add the Authorization header if required.
11 | ///
12 | /// Message to add to.
13 | /// Authentication value
14 | /// Uri to connect to
15 | /// A counter for authorization info.
16 | public static void AddAuthorization(this RtspRequest message, Authentication? authentication, Uri uri, uint commandCounter)
17 | {
18 | if (authentication is null)
19 | {
20 | return;
21 | }
22 |
23 | string authorization = authentication.GetResponse(commandCounter, uri.AbsoluteUri, message.RequestTyped.ToString(), []);
24 | // remove if already one...
25 | message.Headers.Remove(RtspHeaderNames.Authorization);
26 | message.Headers.Add(RtspHeaderNames.Authorization, authorization);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/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/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/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/RtspListenSocket.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using Microsoft.Extensions.Logging.Abstractions;
5 | using System.Net.Sockets;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | public class RtspListenSocket : IRtspListenSocket
10 | {
11 | private readonly TcpListener _tcpListener;
12 | private readonly ILogger _logger;
13 |
14 | public RtspListenSocket(TcpListener tcpListener, ILoggerFactory? loggerFactory = null)
15 | {
16 | _tcpListener = tcpListener;
17 | _logger = loggerFactory?.CreateLogger() as ILogger ?? NullLogger.Instance;
18 | }
19 |
20 | public async Task AcceptAsync(CancellationToken cancellationToken)
21 | {
22 | #if NET8_0_OR_GREATER
23 | var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false);
24 | #else
25 | TcpClient client;
26 | using (cancellationToken.Register(() => _tcpListener.Stop()))
27 | {
28 | client = await _tcpListener.AcceptTcpClientAsync().ConfigureAwait(false);
29 | }
30 | #endif
31 | return new RtspTcpTransport(client);
32 | }
33 |
34 | public void Start()
35 | {
36 | _tcpListener.Start();
37 | }
38 |
39 | public void Stop()
40 | {
41 | _tcpListener.Stop();
42 | }
43 | }
--------------------------------------------------------------------------------
/RTSP/Utils/SentMessageList.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Utils;
2 |
3 | using Rtsp.Messages;
4 | using System.Collections.Generic;
5 | using System.Diagnostics.CodeAnalysis;
6 | using System.Linq;
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 |
--------------------------------------------------------------------------------
/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/RtspOverHttpTLSListenSocket.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using System.IO;
5 | using System.Net.Security;
6 | using System.Net.Sockets;
7 | using System.Security.Cryptography.X509Certificates;
8 |
9 | public class RtspOverHttpTLSListenSocket : RtspOverHttpListenSocket
10 | {
11 | private readonly X509Certificate2 _certificate;
12 | private readonly RemoteCertificateValidationCallback? _userCertificateValidationCallback;
13 |
14 | public RtspOverHttpTLSListenSocket(
15 | TcpListener tcpListener,
16 | X509Certificate2 certificate,
17 | RemoteCertificateValidationCallback? userCertificateValidationCallback = null,
18 | ILoggerFactory? loggerFactory = null)
19 | : base(tcpListener, loggerFactory)
20 | {
21 | _certificate = certificate;
22 | _userCertificateValidationCallback = userCertificateValidationCallback;
23 | }
24 |
25 | protected override Stream GetStream(TcpClient client)
26 | {
27 | var sslStream = new SslStream(
28 | client.GetStream(),
29 | leaveInnerStreamOpen: true,
30 | _userCertificateValidationCallback);
31 |
32 | sslStream.AuthenticateAsServer(
33 | _certificate,
34 | clientCertificateRequired: false,
35 | System.Security.Authentication.SslProtocols.Tls12,
36 | checkCertificateRevocation: false);
37 |
38 | return sslStream;
39 | }
40 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.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/IRTSPTransport.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp;
2 |
3 | using System;
4 | using System.Net;
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 | ///
18 | /// Gets the remote endpoint.
19 | ///
20 | /// The remote endpoint.
21 | IPEndPoint RemoteEndPoint { get; }
22 |
23 | ///
24 | /// Gets the remote endpoint.
25 | ///
26 | /// The remote endpoint.
27 | IPEndPoint LocalEndPoint { get; }
28 |
29 | ///
30 | /// Get next command index. Increment at each call.
31 | ///
32 | uint NextCommandIndex();
33 |
34 | ///
35 | /// Closes this instance.
36 | ///
37 | void Close();
38 |
39 | ///
40 | /// Gets a value indicating whether this is connected.
41 | ///
42 | /// if connected; otherwise, .
43 | bool Connected { get; }
44 |
45 | ///
46 | /// Reconnect this instance.
47 | /// Must do nothing if already connected.
48 | ///
49 | /// Error during socket
50 | void Reconnect();
51 | }
52 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/RTSP/Sdp/H265Parameters.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Sdp;
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | // Parse 'fmtp' attribute in SDP
8 | // Extract H265 fields
9 | // By Roger Hardiman, RJH Technical Consultancy Ltd
10 |
11 | public class H265Parameters : ParametersBase, IDictionary
12 | {
13 | public IList SpropParameterSets
14 | {
15 | get
16 | {
17 | List result = [];
18 |
19 | if (ContainsKey("sprop-vps") && this["sprop-vps"] != null)
20 | {
21 | result.AddRange(this["sprop-vps"].Split(',').Select(x => Convert.FromBase64String(x)));
22 | }
23 |
24 | if (ContainsKey("sprop-sps") && this["sprop-sps"] != null)
25 | {
26 | result.AddRange(this["sprop-sps"].Split(',').Select(x => Convert.FromBase64String(x)));
27 | }
28 |
29 | if (ContainsKey("sprop-pps") && this["sprop-pps"] != null)
30 | {
31 | result.AddRange(this["sprop-pps"].Split(',').Select(x => Convert.FromBase64String(x)));
32 | }
33 |
34 | return result;
35 | }
36 | }
37 |
38 | public IList VideoParameterSet => ParameterListFromBase64String("sprop-vps");
39 | public IList SequenceParameterSet => ParameterListFromBase64String("sprop-sps");
40 | public IList PictureParameterSet => ParameterListFromBase64String("sprop-pps");
41 | public IList SEIMessages => ParameterListFromBase64String("sprop-sei");
42 |
43 |
44 | public static H265Parameters Parse(string parameterString) => Parse(parameterString);
45 | }
--------------------------------------------------------------------------------
/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 List OutOfBandNal { get; init; }
23 | }
24 |
25 | public record H265StreamConfigurationData : IStreamConfigurationData
26 | {
27 | public required List OutOfBandNal { get; init; }
28 | }
29 |
30 | public record AacStreamConfigurationData : IStreamConfigurationData
31 | {
32 | public int ObjectType { get; init; }
33 | public int FrequencyIndex { get; init; }
34 | public int SamplingFrequency { get; init; }
35 | public int ChannelConfiguration { get; init; }
36 | }
37 |
38 | public class SimpleDataEventArgs(List> data, DateTime clockTimeStamp, ulong rtpTimeStamp, int baseClock, int payloadType) : EventArgs
39 | {
40 |
41 | public int PayloadType { get; } = payloadType;
42 | public int BaseClock { get; } = baseClock;
43 | public ulong RtpTimestamp { get; } = rtpTimeStamp;
44 | public DateTime ClockTimeStamp { get; } = clockTimeStamp;
45 | //public DateTime TimeStamp { get; } = timeStamp;
46 | public List> Data { get; } = data;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/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/RtspTlsListenSocket.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp;
2 |
3 | using Microsoft.Extensions.Logging;
4 | using Microsoft.Extensions.Logging.Abstractions;
5 | using System.Net.Security;
6 | using System.Net.Sockets;
7 | using System.Security.Cryptography.X509Certificates;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | public class RtspTlsListenSocket : IRtspListenSocket
12 | {
13 | private readonly TcpListener _tcpListener;
14 | private readonly X509Certificate2 _certificate;
15 | private readonly RemoteCertificateValidationCallback? _userCertificateValidationCallback;
16 | private readonly ILogger _logger;
17 |
18 | public RtspTlsListenSocket(TcpListener tcpListener,
19 | X509Certificate2 certificate, RemoteCertificateValidationCallback? userCertificateValidationCallback = null,
20 | ILoggerFactory? loggerFactory = null)
21 | {
22 | _tcpListener = tcpListener;
23 | _logger = loggerFactory?.CreateLogger() as ILogger ?? NullLogger.Instance;
24 | _certificate = certificate;
25 | _userCertificateValidationCallback = userCertificateValidationCallback;
26 | }
27 |
28 | public async Task AcceptAsync(CancellationToken cancellationToken)
29 | {
30 | #if NET8_0_OR_GREATER
31 | var client = await _tcpListener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false);
32 | #else
33 | TcpClient client;
34 | using (cancellationToken.Register(() => _tcpListener.Stop()))
35 | {
36 | client = await _tcpListener.AcceptTcpClientAsync().ConfigureAwait(false);
37 | }
38 | #endif
39 | return new RtspTcpTlsTransport(client, _certificate, _userCertificateValidationCallback);
40 | }
41 |
42 | public void Start()
43 | {
44 | _tcpListener.Start();
45 | }
46 |
47 | public void Stop()
48 | {
49 | _tcpListener.Stop();
50 | }
51 | }
--------------------------------------------------------------------------------
/RTSP.Tests/RtspListenSocketTests.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Tests;
2 |
3 | using NUnit.Framework;
4 | using Rtsp;
5 | using System;
6 | using System.Diagnostics;
7 | using System.Net;
8 | using System.Net.Sockets;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 |
12 | [TestFixture()]
13 | public class RtspListenSocketTests
14 | {
15 |
16 |
17 | [Test()]
18 | public void StartStopTest()
19 | {
20 | var tcplistener = new TcpListener(IPAddress.Loopback, 0);
21 | var testObj = new RtspListenSocket(tcplistener);
22 |
23 | testObj.Start();
24 | Assert.That((tcplistener.LocalEndpoint as IPEndPoint)?.Port, Is.GreaterThan(0));
25 | testObj.Stop();
26 | testObj.Start();
27 | testObj.Stop();
28 |
29 | // should success without execption
30 | }
31 |
32 | [Test]
33 | [CancelAfter(1000)]
34 |
35 | public async Task SimpleAccept(CancellationToken cancellationToken)
36 | {
37 | var tcplistener = new TcpListener(IPAddress.Loopback, 0);
38 | var testObj = new RtspListenSocket(tcplistener);
39 |
40 | testObj.Start();
41 | var listenEndpoint = tcplistener.LocalEndpoint as IPEndPoint;
42 | Debug.Assert(listenEndpoint != null);
43 |
44 |
45 | var acceptTask = testObj.AcceptAsync(cancellationToken);
46 | var client = new TcpClient();
47 | client.Connect(listenEndpoint);
48 |
49 | var result = await acceptTask;
50 |
51 | Assert.That(acceptTask.Result, Is.AssignableTo());
52 | }
53 |
54 | [Test()]
55 | [CancelAfter(1000)]
56 | public void AccpetWithoutStart(CancellationToken cancellationToken)
57 | {
58 | var tcplistener = new TcpListener(IPAddress.Loopback, 0);
59 | var testObj = new RtspListenSocket(tcplistener);
60 |
61 | Assert.ThrowsAsync(async () => await testObj.AcceptAsync(cancellationToken));
62 | }
63 | }
--------------------------------------------------------------------------------
/RTSP/Rtcp/RtcpPacketUtil.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Rtcp;
2 |
3 | using System;
4 | using System.Buffers.Binary;
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | _reservedData?.Dispose();
29 | _reservedData = null;
30 | base.Data = value;
31 | }
32 | }
33 |
34 | ///
35 | /// Create a string of the message for debug.
36 | ///
37 | public override string ToString()
38 | {
39 | var stringBuilder = new StringBuilder();
40 | stringBuilder.AppendLine("Data message");
41 | stringBuilder.AppendLine(Data.IsEmpty ? "Data : null" : $"Data length :-{Data.Length}-");
42 |
43 | return stringBuilder.ToString();
44 | }
45 |
46 | public int Channel { get; set; }
47 |
48 | ///
49 | /// Clones this instance.
50 | /// Listener is not cloned
51 | ///
52 | /// a clone of this instance
53 | public override object Clone() => new RtspData
54 | {
55 | Channel = Channel,
56 | SourcePort = SourcePort,
57 | Data = Data,
58 | };
59 |
60 | private void Dispose(bool disposing)
61 | {
62 | if (_disposedValue) return;
63 | if (disposing)
64 | {
65 | _reservedData?.Dispose();
66 | }
67 | Data = Memory.Empty;
68 | _disposedValue = true;
69 | }
70 |
71 | public void Dispose()
72 | {
73 | Dispose(disposing: true);
74 | GC.SuppressFinalize(this);
75 | }
76 | }
--------------------------------------------------------------------------------
/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/Utils/ReadOnlySequenceExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Utils;
2 |
3 | using System;
4 | using System.Buffers;
5 | using System.Runtime.CompilerServices;
6 |
7 | public static class ReadOnlySequenceExtensions
8 | {
9 | extension(in ReadOnlySequence buffer)
10 | {
11 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
12 | public (SequencePosition endOfLinePos, SequencePosition startOfLinePos)? FindEndOfLine()
13 | {
14 | const byte lf = (byte)'\n';
15 | var crlf = "\r\n"u8;
16 |
17 | var startOfSegment = buffer.Start;
18 | var position = startOfSegment;
19 | var findPos = buffer.Start;
20 | var checkForEndOfLine = false;
21 | while (buffer.TryGet(ref position, out var segment))
22 | {
23 | if (segment.Length == 0)
24 | continue;
25 |
26 | if (checkForEndOfLine)
27 | {
28 | var nbToskip = segment.Span[0] == lf ? 1 : 0;
29 | return (findPos, buffer.GetPosition(nbToskip, startOfSegment));
30 | }
31 |
32 | var pos = segment.Span.IndexOfAny(crlf);
33 | if (pos != -1)
34 | {
35 | findPos = buffer.GetPosition(pos, startOfSegment);
36 | // handle \n only
37 | if (segment.Span[pos] == lf)
38 | {
39 | return (findPos, buffer.GetPosition(pos + 1, startOfSegment));
40 | }
41 | if (pos + 1 < segment.Length)
42 | {
43 | // handle \r\n ou \r other in same segment
44 | var nbToskip = segment.Span[pos + 1] == lf ? 2 : 1;
45 | return (findPos, buffer.GetPosition(pos + nbToskip, startOfSegment));
46 | }
47 |
48 | // need to wait for next segment
49 | checkForEndOfLine = true;
50 | }
51 |
52 | startOfSegment = position;
53 | }
54 | return default;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/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/RTSPUtils.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp;
2 |
3 | using System;
4 | using System.Net;
5 | using System.Net.Security;
6 |
7 | public static class RtspUtils
8 | {
9 | ///
10 | /// Registers the rtsp scheùe for uri.
11 | ///
12 | public static void RegisterUri()
13 | {
14 | if (!UriParser.IsKnownScheme("rtsp"))
15 | {
16 | UriParser.Register(new HttpStyleUriParser(), "rtsp", 554);
17 | }
18 |
19 | if (!UriParser.IsKnownScheme("rtsps"))
20 | {
21 | // Port 322 is indicated in RFC 7826 (we are RTSP 1.0 but we keep the same port)
22 | UriParser.Register(new HttpStyleUriParser(), "rtsps", 322);
23 | }
24 | }
25 |
26 | public static IRtspTransport CreateRtspTransportFromUrl(Uri uri, NetworkCredential networkCredential, RemoteCertificateValidationCallback? userCertificateSelectionCallback = null)
27 | {
28 | return uri.Scheme switch
29 | {
30 | "rtsp" => new RtspTcpTransport(uri),
31 | "rtsps" => new RtspTcpTlsTransport(uri, userCertificateSelectionCallback),
32 | "http" => new RtspHttpTransport(uri, networkCredential),
33 | "https" => new RTSPHttpsTransport(uri, networkCredential, userCertificateSelectionCallback),
34 | _ => throw new ArgumentException("The uri scheme is not supported", nameof(uri)),
35 | };
36 | }
37 |
38 | public static IRtspTransport CreateRtspTransportFromUrl(Uri uri, RemoteCertificateValidationCallback? userCertificateSelectionCallback = null)
39 | {
40 | // Keep compatible argument with previous version
41 | // I need to check if any camera have authentification at http(s) level and not only at rtsp level
42 | return uri.Scheme switch
43 | {
44 | "rtsp" => new RtspTcpTransport(uri),
45 | "rtsps" => new RtspTcpTlsTransport(uri, userCertificateSelectionCallback),
46 | "http" => new RtspHttpTransport(uri, new()),
47 | "https" => new RTSPHttpsTransport(uri, new(), userCertificateSelectionCallback),
48 | _ => throw new ArgumentException("The uri scheme is not supported", nameof(uri)),
49 | };
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/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 bool isDisposed = false;
14 | private readonly CancellationTokenSource cancellationTokenSource = new();
15 |
16 | public override bool CanRead => !cancellationTokenSource.IsCancellationRequested;
17 |
18 | public override bool CanSeek => false;
19 |
20 | public override bool CanWrite => false;
21 |
22 | public override long Length => throw new NotSupportedException("Simulate NetworkStream");
23 |
24 | public override long Position { get => throw new NotSupportedException("Simulate NetworkStream"); set => throw new NotSupportedException("Simulate NetworkStream"); }
25 |
26 | public override void Flush() { }
27 |
28 | public override int Read(byte[] buffer, int offset, int count)
29 | {
30 | if (isDisposed) throw new ObjectDisposedException(nameof(InBlockingStream));
31 | // simulate blocking read for data
32 | // only unlock on close
33 | cancellationTokenSource.Token.WaitHandle.WaitOne();
34 | throw new IOException("Stream closed");
35 | }
36 |
37 | public override long Seek(long offset, SeekOrigin origin)
38 | {
39 | throw new NotSupportedException("Simulate NetworkStream");
40 | }
41 |
42 | public override void SetLength(long value)
43 | {
44 | throw new NotSupportedException("Simulate NetworkStream");
45 | }
46 |
47 | public override void Write(byte[] buffer, int offset, int count)
48 | {
49 | throw new NotSupportedException("Simulate only read");
50 | }
51 |
52 | protected override void Dispose(bool disposing)
53 | {
54 | if (disposing && !isDisposed)
55 | {
56 | isDisposed = true;
57 | cancellationTokenSource.Cancel();
58 | cancellationTokenSource.Dispose();
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/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/RTSP.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net10.0;net472
4 | 12.0
5 | true
6 | enable
7 |
8 |
9 |
10 |
11 |
12 | False
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | all
39 | runtime; build; native; contentfiles; analyzers; buildtransitive
40 |
41 |
42 |
43 |
44 | all
45 | runtime; build; native; contentfiles; analyzers; buildtransitive
46 |
47 |
48 |
--------------------------------------------------------------------------------
/RTSP/Onvif/RtspMessageOnvifExtension.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Onvif;
2 |
3 | using Rtsp.Messages;
4 | using System;
5 |
6 | public static class RtspMessageOnvifExtension
7 | {
8 | extension(RtspRequestPlay message)
9 | {
10 | // be aware:
11 | // seekTime:o returns datetime like yyyy-MM-dd hh:mm:ss, but onvif requires format to be yyyyMMddTHHmmss with the clock=
12 | // if using :o, will return a "not suported" error
13 |
14 | public void AddPlayback(DateTime seekTime, double scale = 1.0)
15 | {
16 | message.Headers.Add(RtspHeaderNames.Scale, FormattableString.Invariant($"{scale:0.0}"));
17 | message.Headers.Add(RtspHeaderNames.Range, $"clock={Seek(seekTime)}-");
18 | }
19 | public void AddPlayback(DateTime seekTimeFrom, DateTime seekTimeTo, double scale = 1.0)
20 | {
21 | message.Headers.Add(RtspHeaderNames.Scale, FormattableString.Invariant($"{scale:0.0}"));
22 | message.Headers.Add(RtspHeaderNames.Range, $"clock={Seek(seekTimeFrom)}-{Seek(seekTimeTo)}");
23 | }
24 | }
25 |
26 | private static string Seek(DateTime dt) => FormattableString.Invariant($"{dt:yyyyMMdd}T{dt:HHmmss}Z");
27 |
28 | extension(RtspMessage message)
29 | {
30 | ///
31 | /// Add the Require: onvif-replay header to the message for ONVIF compatibility
32 | ///
33 | /// Message to modify
34 | public void AddRequireOnvifRequest()
35 | {
36 | if (!message.Headers.ContainsKey(RtspHeaderNames.Require))
37 | {
38 | message.Headers.Add(RtspHeaderNames.Require, "onvif-replay");
39 | }
40 | }
41 |
42 | ///
43 | /// Add the Rate-Control header to the message for ONVIF replay compatibility
44 | ///
45 | /// Message to modify
46 | /// is rate controled by server, see onvif specification
47 | public void AddRateControlOnvifRequest(bool rateControl)
48 | {
49 | if (!message.Headers.ContainsKey(RtspHeaderNames.RateControl))
50 | {
51 | message.Headers.Add(RtspHeaderNames.RateControl, rateControl ? "yes" : "no");
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/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.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/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.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/MulticastUdpSocket.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp;
2 |
3 | using System;
4 | using System.Net;
5 | using System.Net.Sockets;
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 |
--------------------------------------------------------------------------------
/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/Rtcp/RtcpPacket.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Rtcp;
2 |
3 | using System;
4 | using System.Buffers.Binary;
5 | using System.Runtime.InteropServices;
6 |
7 | public readonly ref struct RtcpPacket
8 | {
9 | private readonly ReadOnlySpan rawData;
10 |
11 | public RtcpPacket(ReadOnlySpan rawData)
12 | {
13 | this.rawData = rawData;
14 | }
15 |
16 | public bool IsEmpty => rawData.IsEmpty;
17 | public bool IsWellFormed => rawData.Length >= 4 && Version == 2 && Length >= 0;
18 | public int Version => (rawData[0] >> 6) & 0x03;
19 | public bool HasPadding => ((rawData[0] >> 5) & 0x01) != 0;
20 | public int Count => rawData[0] & 0x1F;
21 | public int PacketType => rawData[1];
22 | public int Length => BinaryPrimitives.ReadUInt16BigEndian(rawData[2..4]);
23 |
24 | public uint SenderSsrc => PacketType switch
25 | {
26 | RtcpPacketUtil.RTCP_PACKET_TYPE_SENDER_REPORT or
27 | RtcpPacketUtil.RTCP_PACKET_TYPE_RECEIVER_REPORT
28 | => BinaryPrimitives.ReadUInt32BigEndian(rawData[4..8]),
29 | _ => throw new InvalidOperationException("No Sender SSRC for this type of packet"),
30 | };
31 |
32 | public SenderReportPacket SenderReport => PacketType switch
33 | {
34 | RtcpPacketUtil.RTCP_PACKET_TYPE_SENDER_REPORT
35 | => new SenderReportPacket(rawData, Count),
36 | _ => throw new InvalidOperationException("Packet is not of sender report type"),
37 | };
38 |
39 |
40 |
41 | public RtcpPacket Next => new(rawData[((Length + 1) * 4)..]);
42 |
43 | // NTP Most Significant Word is relative to 0h, 1 Jan 1900
44 | // This will wrap around in 2036
45 | private static readonly DateTime ntpStartTime = new(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);
46 |
47 | [StructLayout(LayoutKind.Auto)]
48 | public readonly ref struct SenderReportPacket
49 | {
50 | private readonly int count;
51 | private readonly ReadOnlySpan rawData;
52 | public SenderReportPacket(ReadOnlySpan rawData, int count)
53 | {
54 | this.rawData = rawData;
55 | this.count = count;
56 | }
57 |
58 | public DateTime Clock => ntpStartTime.AddSeconds(
59 | BinaryPrimitives.ReadUInt32BigEndian(rawData[8..12])
60 | + (BinaryPrimitives.ReadUInt32BigEndian(rawData[12..16]) / (double)uint.MaxValue)
61 | );
62 |
63 | public uint RtpTimestamp => BinaryPrimitives.ReadUInt32BigEndian(rawData[16..20]);
64 |
65 | public uint PacketCount => BinaryPrimitives.ReadUInt32BigEndian(rawData[20..24]);
66 | public uint OctetCount => BinaryPrimitives.ReadUInt32BigEndian(rawData[24..28]);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/RTSP/Sdp/H266Parameters.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Sdp;
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | // Parse 'fmtp' attribute in SDP
8 | // Extract H266 fields
9 | public class H266Parameters : ParametersBase, IDictionary
10 | {
11 | public IList SpropParameterSets
12 | {
13 | get
14 | {
15 | List result = [];
16 |
17 | if (ContainsKey("sprop-dci") && this["sprop-dci"] != null)
18 | {
19 | result.AddRange(this["sprop-dci"].Split(',').Select(x => Convert.FromBase64String(x)));
20 | }
21 | else
22 | {
23 | result.Add(Array.Empty());
24 | }
25 |
26 | if (ContainsKey("sprop-vps") && this["sprop-vps"] != null)
27 | {
28 | result.AddRange(this["sprop-vps"].Split(',').Select(x => Convert.FromBase64String(x)));
29 | }
30 | else
31 | {
32 | result.Add(Array.Empty());
33 | }
34 |
35 | if (ContainsKey("sprop-sps") && this["sprop-sps"] != null)
36 | {
37 | result.AddRange(this["sprop-sps"].Split(',').Select(x => Convert.FromBase64String(x)));
38 | }
39 | else
40 | {
41 | result.Add(Array.Empty());
42 | }
43 |
44 | if (ContainsKey("sprop-pps") && this["sprop-pps"] != null)
45 | {
46 | result.AddRange(this["sprop-pps"].Split(',').Select(x => Convert.FromBase64String(x)));
47 | }
48 | else
49 | {
50 | result.Add(Array.Empty());
51 | }
52 |
53 | if (ContainsKey("sprop-sei") && this["sprop-sei"] != null)
54 | {
55 | result.AddRange(this["sprop-sei"].Split(',').Select(x => Convert.FromBase64String(x)));
56 | }
57 | else
58 | {
59 | result.Add(Array.Empty());
60 | }
61 |
62 | return result;
63 | }
64 | }
65 |
66 | public byte[] DecodingCapabilityInformation => ParameterFromBase64String("sprop-dci");
67 | public IList VideoParameterSet => ParameterListFromBase64String("sprop-vps");
68 | public IList SequenceParameterSet => ParameterListFromBase64String("sprop-sps");
69 | public IList PictureParameterSet => ParameterListFromBase64String("sprop-pps");
70 | public IList SEIMessages => ParameterListFromBase64String("sprop-sei");
71 |
72 | public static H266Parameters Parse(string parameterString) => Parse(parameterString);
73 | }
--------------------------------------------------------------------------------
/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/RTSP.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0
4 | 14.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.10.0
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 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/Sdp/ParametersBase.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Sdp;
2 |
3 | using System;
4 | using System.Collections;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 |
8 | public class ParametersBase : IDictionary
9 | {
10 | private readonly Dictionary parameters = [];
11 |
12 | protected static T Parse(string parameterString) where T : ParametersBase, new()
13 | {
14 | var result = new T();
15 | foreach (var pair in parameterString.Split(';').Select(x => x.Trim().Split('=', 2)))
16 | {
17 | if (!string.IsNullOrWhiteSpace(pair[0]))
18 | result[pair[0]] = pair.Length > 1 ? pair[1] : string.Empty;
19 | }
20 |
21 | return result;
22 | }
23 |
24 | public override string ToString()
25 | {
26 | return parameters.Select(p => p.Key + (p.Value != null ? "=" + p.Value : string.Empty))
27 | .Aggregate((x, y) => x + ";" + y);
28 | }
29 |
30 | public string this[string index]
31 | {
32 | get => parameters[index];
33 | set => parameters[index] = value;
34 | }
35 |
36 | public int Count => parameters.Count;
37 |
38 | public bool IsReadOnly => ((IDictionary)parameters).IsReadOnly;
39 |
40 | public ICollection Keys => ((IDictionary)parameters).Keys;
41 |
42 | public ICollection Values => ((IDictionary)parameters).Values;
43 |
44 | public void Add(KeyValuePair item) => ((IDictionary)parameters).Add(item);
45 |
46 | public void Add(string key, string value) => parameters.Add(key, value);
47 |
48 | public void Clear() => parameters.Clear();
49 |
50 | public bool Contains(KeyValuePair item) =>
51 | ((IDictionary)parameters).Contains(item);
52 |
53 | public bool ContainsKey(string key) => parameters.ContainsKey(key);
54 |
55 | public void CopyTo(KeyValuePair[] array, int arrayIndex) =>
56 | ((IDictionary)parameters).CopyTo(array, arrayIndex);
57 |
58 | public IEnumerator> GetEnumerator() =>
59 | ((IDictionary)parameters).GetEnumerator();
60 |
61 | public bool Remove(KeyValuePair item) => ((IDictionary)parameters).Remove(item);
62 |
63 | public bool Remove(string key) => parameters.Remove(key);
64 |
65 | public bool TryGetValue(string key, out string value) => parameters.TryGetValue(key, out value!);
66 |
67 | IEnumerator IEnumerable.GetEnumerator() => ((IDictionary)parameters).GetEnumerator();
68 |
69 |
70 | public byte[] ParameterFromBase64String(string parameterName)
71 | {
72 | if (!TryGetValue(parameterName, out var value)) return [];
73 | return Convert.FromBase64String(value);
74 | }
75 |
76 | public List ParameterListFromBase64String(string parameterName)
77 | {
78 | if (!TryGetValue(parameterName, out var value)) return [];
79 | return [.. value.Split(',').Select(Convert.FromBase64String)];
80 | }
81 |
82 | }
--------------------------------------------------------------------------------
/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/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), new(), 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.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/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/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(
62 | _serverCertificate,
63 | clientCertificateRequired: false,
64 | SslProtocols.Tls12,
65 | checkCertificateRevocation: false);
66 | }
67 | else
68 | {
69 | sslStream.AuthenticateAsClient(RemoteEndPoint.ToString());
70 | }
71 | return sslStream;
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/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 endpoint.
51 | ///
52 | /// The remote endpoint.
53 | public IPEndPoint RemoteEndPoint { get; }
54 |
55 | ///
56 | /// Gets the local endpoint.
57 | ///
58 | /// The local endpoint.
59 | public IPEndPoint LocalEndPoint { get; }
60 |
61 | public uint NextCommandIndex() => ++_commandCounter;
62 |
63 | ///
64 | /// Closes this instance.
65 | ///
66 | public void Close()
67 | {
68 | Dispose(true);
69 | }
70 |
71 | ///
72 | /// Gets a value indicating whether this is connected.
73 | ///
74 | /// if connected; otherwise, .
75 | public bool Connected => _RtspServerClient.Client != null && _RtspServerClient.Connected;
76 |
77 | ///
78 | /// Reconnect this instance.
79 | /// Must do nothing if already connected.
80 | ///
81 | /// Error during socket
82 | public void Reconnect()
83 | {
84 | if (Connected)
85 | return;
86 | _RtspServerClient = new TcpClient();
87 | _RtspServerClient.Connect(RemoteEndPoint);
88 | }
89 |
90 | #endregion
91 |
92 | public void Dispose()
93 | {
94 | Dispose(true);
95 | GC.SuppressFinalize(this);
96 | }
97 |
98 | protected virtual void Dispose(bool disposing)
99 | {
100 | if (disposing)
101 | {
102 | _RtspServerClient.Close();
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/RTSP/Onvif/RtpPacketOnvifUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Buffers.Binary;
3 |
4 | namespace Rtsp.Onvif;
5 |
6 | public static class RtpPacketOnvifUtils
7 | {
8 | private const ushort MARKER_TS_EXT = 0xABAC;
9 | private const ushort MARKER_SOF0 = 0xffc0; // start-of-frame, baseline scan
10 | private const ushort MARKER_SOI = 0xffd8; // start of image
11 |
12 | ///
13 | /// Provide the Jpeg frame extension method, for frame size > 2048x2048
14 | ///
15 | /// The header to check
16 | /// Frame width and height
17 | public static (ushort frameWidth, ushort frameHeight) ProcessJpegFrameExtension(ReadOnlySpan extension)
18 | {
19 | var headerPosition = 0;
20 | ushort frameWidth = 0;
21 | ushort frameHeight = 0;
22 | int extensionType = BinaryPrimitives.ReadUInt16BigEndian(extension[headerPosition..]);
23 | if (extensionType == MARKER_SOI)
24 | {
25 | // 2 for type, 2 for length
26 | headerPosition += sizeof(ushort) + sizeof(ushort);
27 | int extensionSize = extension.Length;
28 | while (headerPosition < (extensionSize - (sizeof(ushort) + sizeof(ushort))))
29 | {
30 | ushort blockType = BinaryPrimitives.ReadUInt16BigEndian(extension[headerPosition..]);
31 | ushort blockSize = BinaryPrimitives.ReadUInt16BigEndian(extension[(headerPosition + 2)..]);
32 |
33 | if (blockType == MARKER_SOF0)
34 | {
35 | frameHeight = BinaryPrimitives.ReadUInt16BigEndian(extension[(headerPosition + 5)..]);
36 | frameWidth = BinaryPrimitives.ReadUInt16BigEndian(extension[(headerPosition + 7)..]);
37 | }
38 | headerPosition += (blockSize + 2);
39 | }
40 | }
41 | return (frameWidth, frameHeight);
42 | }
43 |
44 | private static readonly DateTime dt_1900 = new(1900, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
45 |
46 | ///
47 | /// Extract timestamp from jpeg extension.
48 | ///
49 | /// The extension header
50 | /// returns position after read. Used when JPEG extension is appended to this extension
51 | /// Timestamp of the frame or on error
52 | public static DateTime ProcessRTPTimestampExtension(ReadOnlySpan extension, out int headerPosition)
53 | {
54 | headerPosition = 0;
55 | // RTP extension has a minmum length of 4 32bit words (more if JPEG extension is appended).
56 | if (extension.Length < 4 * 4)
57 | {
58 | return DateTime.MinValue;
59 | }
60 |
61 | int extensionType = BinaryPrimitives.ReadUInt16BigEndian(extension);
62 | if (extensionType != MARKER_TS_EXT)
63 | {
64 | return DateTime.MinValue;
65 | }
66 |
67 | headerPosition += sizeof(ushort);
68 | // var headerLength = BinaryPrimitives.ReadUInt16BigEndian(extension[headerPosition..]);
69 | //if (headerLength == 3)
70 | {
71 | headerPosition += sizeof(ushort);
72 |
73 | uint seconds = BinaryPrimitives.ReadUInt32BigEndian(extension[headerPosition..]);
74 | uint fractions = BinaryPrimitives.ReadUInt32BigEndian(extension[(headerPosition + sizeof(uint))..]);
75 |
76 | headerPosition += sizeof(uint) + sizeof(uint);
77 |
78 | //uint data = BinaryPrimitives.ReadUInt16BigEndian(extension[headerPosition..]);
79 | // C [1 bit] -> all
80 | // E [1 bit] -> all
81 | // D [1 bit] -> all
82 | // T [1 bit] -> only not jpeg
83 | // MBZ [4/5 bits] -> [4 if not jpeg, 5 in jpeg] reserved
84 | // CSeq [8 bits] -> 1 byte
85 | // padding [8 bits] -> just padding values.
86 |
87 | headerPosition += sizeof(uint);
88 | double msec = fractions * 1000.0 / uint.MaxValue;
89 |
90 | return dt_1900.AddSeconds(seconds).AddMilliseconds(msec);
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/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.SET_PARAMETER)]
68 | [GenericTestCase(RtspRequest.RequestType.ANNOUNCE)]
69 | [GenericTestCase(RtspRequest.RequestType.RECORD)]
70 | [GenericTestCase(RtspRequest.RequestType.REDIRECT)]
71 | public void CheckRequestType(RtspRequest.RequestType expectedType) where T : RtspRequest, new()
72 | {
73 | RtspRequest onMessage = new T();
74 | Assert.That(onMessage.RequestTyped, Is.EqualTo(expectedType));
75 | }
76 |
77 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
78 | private class GenericTestCaseAttribute(params object[] arguments) : TestCaseAttribute(arguments), ITestBuilder
79 | {
80 | IEnumerable ITestBuilder.BuildFrom(IMethodInfo method, Test? suite)
81 | {
82 | var testedMethod = method.IsGenericMethodDefinition ? method.MakeGenericMethod(typeof(T)) : method;
83 | return BuildFrom(testedMethod, suite);
84 | }
85 | }
86 | #endif
87 | }
88 | }
--------------------------------------------------------------------------------
/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/Messages/RTSPRequest.cs:
--------------------------------------------------------------------------------
1 | namespace Rtsp.Messages;
2 |
3 | using System;
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.SET_PARAMETER => new RtspRequestSetParameter(),
58 | RequestType.ANNOUNCE => new RtspRequestAnnounce(),
59 | RequestType.RECORD => new RtspRequestRecord(),
60 | RequestType.REDIRECT => new RtspRequestRedirect(),
61 | _ => new RtspRequest(),
62 | };
63 | }
64 |
65 | ///
66 | /// Initializes a new instance of the class.
67 | ///
68 | public RtspRequest()
69 | {
70 | Command = "OPTIONS * RTSP/1.0";
71 | }
72 |
73 | ///
74 | /// Gets the request.
75 | ///
76 | /// The request in string format.
77 | public string Request => commandArray[0];
78 |
79 | ///
80 | /// Gets the request.
81 | /// The return value is typed with if the value is not
82 | /// recognise the value is sent. The string value can be got by
83 | ///
84 | /// The request.
85 | public RequestType RequestTyped
86 | {
87 | get => ParseRequest(commandArray[0]);
88 | set
89 | {
90 | if (Enum.IsDefined(typeof(RequestType), value))
91 | commandArray[0] = value.ToString();
92 | else
93 | commandArray[0] = nameof(RequestType.UNKNOWN);
94 | }
95 | }
96 |
97 | private Uri? _rtspUri;
98 |
99 | ///
100 | /// Gets or sets the Rtsp asked URI.
101 | ///
102 | /// The Rtsp asked URI.
103 | /// The request with uri * is return with null URI
104 | public Uri? RtspUri
105 | {
106 | get
107 | {
108 | if (commandArray.Length < 2 || string.Equals(commandArray[1], "*", StringComparison.InvariantCulture))
109 | {
110 | return null;
111 | }
112 | if (_rtspUri == null)
113 | {
114 | Uri.TryCreate(commandArray[1], UriKind.Absolute, out _rtspUri);
115 | }
116 | return _rtspUri;
117 | }
118 | set
119 | {
120 | _rtspUri = value;
121 | if (commandArray.Length < 2)
122 | {
123 | Array.Resize(ref commandArray, 3);
124 | }
125 | commandArray[1] = value is not null ? value.ToString() : "*";
126 | }
127 | }
128 |
129 | ///
130 | /// Gets the associate OK response with the request.
131 | ///
132 | /// an Rtsp response corresponding to request.
133 | public virtual RtspResponse CreateResponse()
134 | {
135 | var returnValue = new RtspResponse
136 | {
137 | ReturnCode = 200,
138 | CSeq = CSeq,
139 | };
140 | if (Headers.TryGetValue(RtspHeaderNames.Session, out var value))
141 | {
142 | returnValue.Headers[RtspHeaderNames.Session] = value;
143 | }
144 |
145 | return returnValue;
146 | }
147 |
148 | public object? ContextData { get; set; }
149 | }
150 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------