From 31b0e28329eaa633fd6cbc2f0ca864e55d699848 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 20 Oct 2019 16:38:58 +0300 Subject: [PATCH 1/5] Multicast, improvements, bug fixes: bug fixes: 1) SDES packet header - padding flag must be "false". Fixed. 2) SDES chunk - text length should not include /0. Fixed. 3) SDES reports were sent to RTP port instead of RTCP. Fixed. 4) Since Socket.ReceiveAsync do not natively support cancellation, and receive task was waiting for complete of both RTP and RTCP receive, "normal" cancellation never took place. CloseRtspSessionAsync never called because sockets were closed due to CancelTimeout. As a result, TEARDOWN was never sent. I added WithCancellation task extension and now ReadAsync can be canceled immediately without wait for next incoming packet. I modified only UDP part, but extension can be used in other places too. improvements: 1) "Receiver Report Goodbye" packet is sent on session close. 2) UDP sockets are bound to only one interface, which IP is retrieved from already connected RTSP session. This improves security in certain usage scenarios, especially with multicast. new features: 1) UDP multicast support. New SETUP request asking for multicast. New socket extension for multicast groups. Implementation logic is similar to FFMPEG "forced multicast" or VLC --rtsp-mcast option. todo: 0) Unit tests update (if needed?) 1) so far tested on only one device - TRUEN HD encoder 2) Transport Protocol may be selected based on device capabilities (AUTO option?) Contained in no branch Contained in no tag Derives from tag: 1.1 + 52 commits --- RtspClientSharp/Rtcp/RtcpByePacket.cs | 33 +++- .../Rtcp/RtcpReceiverReportsProvider.cs | 13 +- RtspClientSharp/Rtcp/RtcpSdesNameItem.cs | 2 +- RtspClientSharp/Rtcp/RtcpSdesReportPacket.cs | 13 +- RtspClientSharp/RtpTransportProtocol.cs | 3 +- RtspClientSharp/Rtsp/IRtspTransportClient.cs | 1 + RtspClientSharp/Rtsp/RtspClientInternal.cs | 178 +++++++++++++++--- .../Rtsp/RtspHttpTransportClient.cs | 3 + .../Rtsp/RtspRequestMessageFactory.cs | 10 + .../Rtsp/RtspTcpTransportClient.cs | 3 + RtspClientSharp/Rtsp/RtspTransportClient.cs | 1 + RtspClientSharp/Utils/NetworkClientFactory.cs | 2 +- RtspClientSharp/Utils/SocketExtensions.cs | 30 +++ RtspClientSharp/Utils/TaskExtensions.cs | 18 +- 14 files changed, 268 insertions(+), 42 deletions(-) create mode 100644 RtspClientSharp/Utils/SocketExtensions.cs diff --git a/RtspClientSharp/Rtcp/RtcpByePacket.cs b/RtspClientSharp/Rtcp/RtcpByePacket.cs index bd1edf1..6007f6d 100644 --- a/RtspClientSharp/Rtcp/RtcpByePacket.cs +++ b/RtspClientSharp/Rtcp/RtcpByePacket.cs @@ -1,15 +1,31 @@ using System; using System.Collections.Generic; +using System.IO; +using RtspClientSharp.Rtsp; using RtspClientSharp.Utils; namespace RtspClientSharp.Rtcp { - class RtcpByePacket : RtcpPacket + class RtcpByePacket : RtcpPacket, ISerializablePacket { private readonly List _syncSourcesIds = new List(); public IEnumerable SyncSourcesIds => _syncSourcesIds; + public RtcpByePacket() + { + PaddingFlag = false; + PayloadType = 203; + } + + public RtcpByePacket(uint syncSourceId): this() + { + _syncSourcesIds.Add(syncSourceId); + SourceCount = 1; + DwordLength = 1; + Length = (DwordLength + 1) * 4; + } + protected override void FillFromByteSegment(ArraySegment byteSegment) { int offset = byteSegment.Offset; @@ -23,5 +39,20 @@ protected override void FillFromByteSegment(ArraySegment byteSegment) _syncSourcesIds.Add(ssrc); } } + + public new void Serialize(Stream stream) + { + base.Serialize(stream); + + if (_syncSourcesIds.Count > 0) + { + stream.WriteByte((byte)(_syncSourcesIds[0] >> 24)); + stream.WriteByte((byte)(_syncSourcesIds[0] >> 16)); + stream.WriteByte((byte)(_syncSourcesIds[0] >> 8)); + stream.WriteByte((byte)_syncSourcesIds[0]); + } + else + throw new RtspClientException("Can't make RTCP packet without Identifier"); + } } } \ No newline at end of file diff --git a/RtspClientSharp/Rtcp/RtcpReceiverReportsProvider.cs b/RtspClientSharp/Rtcp/RtcpReceiverReportsProvider.cs index 54e4a3c..33caeb4 100644 --- a/RtspClientSharp/Rtcp/RtcpReceiverReportsProvider.cs +++ b/RtspClientSharp/Rtcp/RtcpReceiverReportsProvider.cs @@ -23,7 +23,7 @@ public RtcpReceiverReportsProvider(IRtpStatisticsProvider rtpStatisticsProvider, _machineName = Environment.MachineName; } - public IEnumerable GetReportPackets() + public IEnumerable GetReportSdesPackets() { RtcpReceiverReportPacket receiverReport = CreateReceiverReport(); @@ -34,6 +34,17 @@ public IEnumerable GetReportPackets() yield return sdesReport; } + public IEnumerable GetReportByePackets() + { + RtcpReceiverReportPacket receiverReport = CreateReceiverReport(); + + yield return receiverReport; + + RtcpByePacket byeReport = new RtcpByePacket(_senderSyncSourceId); + + yield return byeReport; + } + private RtcpReceiverReportPacket CreateReceiverReport() { int fractionLost; diff --git a/RtspClientSharp/Rtcp/RtcpSdesNameItem.cs b/RtspClientSharp/Rtcp/RtcpSdesNameItem.cs index 5cdcfee..6583d8e 100644 --- a/RtspClientSharp/Rtcp/RtcpSdesNameItem.cs +++ b/RtspClientSharp/Rtcp/RtcpSdesNameItem.cs @@ -21,7 +21,7 @@ public override void Serialize(Stream stream) byte[] domainNameBytes = Encoding.ASCII.GetBytes(DomainName); stream.WriteByte(1); - stream.WriteByte((byte) (domainByteLength + 1)); + stream.WriteByte((byte) domainByteLength); stream.Write(domainNameBytes, 0, domainByteLength); stream.WriteByte(0); } diff --git a/RtspClientSharp/Rtcp/RtcpSdesReportPacket.cs b/RtspClientSharp/Rtcp/RtcpSdesReportPacket.cs index df41759..7b33664 100644 --- a/RtspClientSharp/Rtcp/RtcpSdesReportPacket.cs +++ b/RtspClientSharp/Rtcp/RtcpSdesReportPacket.cs @@ -18,18 +18,11 @@ public RtcpSdesReportPacket(IReadOnlyList chunks) SourceCount = chunks.Count; PayloadType = 202; + PaddingFlag = false; // this is different padding, see https://www.ietf.org/rfc/rfc3550.txt page 46 int length = chunks.Sum(chunk => chunk.SerializedLength); - int fraction = length % 4; - - if (fraction == 0) - PaddingFlag = false; - else - { - PaddingFlag = true; - _paddingByteCount = 4 - fraction; - } + _paddingByteCount = 4 - length % 4; DwordLength = (length + 3) / 4; Length = (DwordLength + 1) * 4; @@ -49,7 +42,7 @@ protected override void FillFromByteSegment(ArraySegment byteSegment) chunk.Serialize(stream); } - if (PaddingFlag) + if (_paddingByteCount > 0) stream.Write(PaddingBytes, 0, _paddingByteCount); } } diff --git a/RtspClientSharp/RtpTransportProtocol.cs b/RtspClientSharp/RtpTransportProtocol.cs index c947f68..83f1164 100644 --- a/RtspClientSharp/RtpTransportProtocol.cs +++ b/RtspClientSharp/RtpTransportProtocol.cs @@ -3,6 +3,7 @@ public enum RtpTransportProtocol { TCP, - UDP + UDP, + MULTICAST } } \ No newline at end of file diff --git a/RtspClientSharp/Rtsp/IRtspTransportClient.cs b/RtspClientSharp/Rtsp/IRtspTransportClient.cs index b461f8b..99a8679 100644 --- a/RtspClientSharp/Rtsp/IRtspTransportClient.cs +++ b/RtspClientSharp/Rtsp/IRtspTransportClient.cs @@ -9,6 +9,7 @@ namespace RtspClientSharp.Rtsp internal interface IRtspTransportClient : IDisposable { EndPoint RemoteEndPoint { get; } + EndPoint LocalEndPoint { get; } Task ConnectAsync(CancellationToken token); diff --git a/RtspClientSharp/Rtsp/RtspClientInternal.cs b/RtspClientSharp/Rtsp/RtspClientInternal.cs index 5784033..84c1216 100644 --- a/RtspClientSharp/Rtsp/RtspClientInternal.cs +++ b/RtspClientSharp/Rtsp/RtspClientInternal.cs @@ -31,6 +31,8 @@ sealed class RtspClientInternal : IDisposable private readonly Dictionary _streamsMap = new Dictionary(); private readonly ConcurrentDictionary _udpClientsMap = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _udpRtp2RtcpMap = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _udpJoinedGroupsMap = new ConcurrentDictionary(); private readonly Dictionary _reportProvidersMap = new Dictionary(); @@ -195,12 +197,16 @@ private async Task SetupTrackAsync(RtspMediaTrackInfo track, CancellationToken t try { - IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 0); + IPAddress localIpToServer = IPAddress.Any; + if (_rtspTransportClient.LocalEndPoint is IPEndPoint localIpEndPoint) + localIpToServer = localIpEndPoint.Address; + + IPEndPoint endPoint = new IPEndPoint(localIpToServer, 0); rtpClient.Bind(endPoint); int rtpPort = ((IPEndPoint)rtpClient.LocalEndPoint).Port; - endPoint = new IPEndPoint(IPAddress.Any, rtpPort + 1); + endPoint = new IPEndPoint(localIpToServer, rtpPort + 1); try { @@ -208,7 +214,7 @@ private async Task SetupTrackAsync(RtspMediaTrackInfo track, CancellationToken t } catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse) { - endPoint = new IPEndPoint(IPAddress.Any, 0); + endPoint = new IPEndPoint(localIpToServer, 0); rtcpClient.Bind(endPoint); } @@ -225,6 +231,14 @@ private async Task SetupTrackAsync(RtspMediaTrackInfo track, CancellationToken t throw; } } + else if (_connectionParameters.RtpTransport == RtpTransportProtocol.MULTICAST) + { + rtpClient = NetworkClientFactory.CreateUdpClient(); + rtcpClient = NetworkClientFactory.CreateUdpClient(); + + setupRequest = _requestMessageFactory.CreateSetupUdpMulticastRequest(track.TrackName); + setupResponse = await _rtspTransportClient.EnsureExecuteRequest(setupRequest, token); + } else { int channelCounter = _streamsMap.Count; @@ -241,9 +255,19 @@ private async Task SetupTrackAsync(RtspMediaTrackInfo track, CancellationToken t if (string.IsNullOrEmpty(transportHeader)) throw new RtspBadResponseException("Transport header is not found"); - string portsAttributeName = _connectionParameters.RtpTransport == RtpTransportProtocol.UDP - ? "server_port" - : "interleaved"; + string portsAttributeName; + switch (_connectionParameters.RtpTransport) + { + case RtpTransportProtocol.UDP: + portsAttributeName = "server_port"; + break; + case RtpTransportProtocol.MULTICAST: + portsAttributeName = "port"; + break; + default: + portsAttributeName = "interleaved"; + break; + } string[] transportAttributes = transportHeader.Split(TransportAttributesSeparator, StringSplitOptions.RemoveEmptyEntries); @@ -276,6 +300,73 @@ private async Task SetupTrackAsync(RtspMediaTrackInfo track, CancellationToken t _udpClientsMap[rtpChannelNumber] = rtpClient; _udpClientsMap[rtcpChannelNumber] = rtcpClient; + _udpRtp2RtcpMap[rtpChannelNumber] = rtcpChannelNumber; + } + else if (_connectionParameters.RtpTransport == RtpTransportProtocol.MULTICAST) + { + int equalSignIndex; + string destinationAttribute = transportAttributes.FirstOrDefault(a => a.StartsWith("destination", StringComparison.InvariantCultureIgnoreCase)); + + IPAddress destinationAddress; + + if (destinationAttribute != null && (equalSignIndex = destinationAttribute.IndexOf("=", StringComparison.CurrentCultureIgnoreCase)) != -1) + destinationAddress = IPAddress.Parse(destinationAttribute.Substring(++equalSignIndex).Trim()); + else + throw new RtspBadResponseException("Destination multicast IP is not found"); + + string sourceAttribute = transportAttributes.FirstOrDefault(a => a.StartsWith("source", StringComparison.InvariantCultureIgnoreCase)); + + IPAddress sourceAddress; + + if (sourceAttribute != null && (equalSignIndex = sourceAttribute.IndexOf("=", StringComparison.CurrentCultureIgnoreCase)) != -1) + sourceAddress = IPAddress.Parse(sourceAttribute.Substring(++equalSignIndex).Trim()); + else + sourceAddress = ((IPEndPoint)_rtspTransportClient.RemoteEndPoint).Address; + + Debug.Assert(rtpClient != null, nameof(rtpClient) + " != null"); + Debug.Assert(rtcpClient != null, nameof(rtcpClient) + " != null"); + + try + { + // if we are conected to several networks, take local IP from already existing TCP connection + IPAddress localIpToServer = IPAddress.Any; + if (_rtspTransportClient.LocalEndPoint is IPEndPoint localIpEndPoint) + localIpToServer = localIpEndPoint.Address; + + IPEndPoint endPointRtp = new IPEndPoint(localIpToServer, rtpChannelNumber); + + rtpClient.Bind(endPointRtp); + rtpClient.JoinMulticastSourceGroup(destinationAddress, localIpToServer, sourceAddress); + _udpJoinedGroupsMap[rtpClient] = new IPEndPoint(destinationAddress, rtpChannelNumber); + + try + { + IPEndPoint endPointRtcp = new IPEndPoint(localIpToServer, rtcpChannelNumber); + + rtcpClient.Bind(endPointRtcp); + rtcpClient.JoinMulticastSourceGroup(destinationAddress, localIpToServer, sourceAddress); + _udpJoinedGroupsMap[rtcpClient] = new IPEndPoint(destinationAddress, rtcpChannelNumber); + } + catch + { + rtcpClient.Close(); + throw; + } + } + catch + { + rtpClient.Close(); + throw; + } + + var udpHolePunchingPacketSegment = new ArraySegment(Array.Empty()); + + await rtpClient.SendToAsync(udpHolePunchingPacketSegment, SocketFlags.None, _udpJoinedGroupsMap[rtpClient]); + await rtcpClient.SendToAsync(udpHolePunchingPacketSegment, SocketFlags.None, _udpJoinedGroupsMap[rtcpClient]); + + _udpClientsMap[rtpChannelNumber] = rtpClient; + _udpClientsMap[rtcpChannelNumber] = rtcpClient; + _udpRtp2RtcpMap[rtpChannelNumber] = rtcpChannelNumber; } ParseSessionHeader(setupResponse.Headers[WellKnownHeaders.Session]); @@ -324,6 +415,11 @@ private async Task CloseRtspSessionAsync(CancellationToken token) if (_connectionParameters.RtpTransport == RtpTransportProtocol.TCP) await _rtspTransportClient.SendRequestAsync(teardownRequest, token); + else if (_connectionParameters.RtpTransport == RtpTransportProtocol.MULTICAST) + { + // There is no need to leave multicast group because it is done automatically by OS when socket closes + await _rtspTransportClient.EnsureExecuteRequest(teardownRequest, token); + } else await _rtspTransportClient.EnsureExecuteRequest(teardownRequest, token); } @@ -495,7 +591,7 @@ private async Task ReceiveOverTcpAsync(Stream rtspStream, CancellationToken toke while (!token.IsCancellationRequested) { - TpktPayload payload = await _tpktStream.ReadAsync(); + TpktPayload payload = await _tpktStream.ReadAsync().WithCancellation(token); if (_streamsMap.TryGetValue(payload.Channel, out ITransportStream stream)) stream.Process(payload.PayloadSegment); @@ -510,7 +606,7 @@ private async Task ReceiveOverTcpAsync(Stream rtspStream, CancellationToken toke foreach (KeyValuePair pair in _reportProvidersMap) { - IEnumerable packets = pair.Value.GetReportPackets(); + IEnumerable packets = pair.Value.GetReportSdesPackets(); ArraySegment byteSegment = SerializeRtcpPackets(packets, bufferStream); int rtcpChannel = pair.Key + 1; @@ -534,8 +630,10 @@ private Task ReceiveOverUdpAsync(CancellationToken token) if (transportStream is RtpStream rtpStream) { + if (!_udpClientsMap.TryGetValue(_udpRtp2RtcpMap[channelNumber], out Socket clientRtcp)) + throw new RtspClientException("RTP connection without RTCP"); RtcpReceiverReportsProvider receiverReportsProvider = _reportProvidersMap[channelNumber]; - receiveTask = ReceiveRtpFromUdpAsync(client, rtpStream, receiverReportsProvider, token); + receiveTask = ReceiveRtpFromUdpAsync(client, clientRtcp, rtpStream, receiverReportsProvider, token); } else receiveTask = ReceiveRtcpFromUdpAsync(client, transportStream, token); @@ -546,7 +644,7 @@ private Task ReceiveOverUdpAsync(CancellationToken token) return Task.WhenAll(waitList); } - private async Task ReceiveRtpFromUdpAsync(Socket client, RtpStream rtpStream, + private async Task ReceiveRtpFromUdpAsync(Socket client, Socket clientRtcp, RtpStream rtpStream, RtcpReceiverReportsProvider reportsProvider, CancellationToken token) { @@ -557,25 +655,46 @@ private async Task ReceiveRtpFromUdpAsync(Socket client, RtpStream rtpStream, int lastTimeRtcpReportsSent = Environment.TickCount; var bufferStream = new MemoryStream(); - while (!token.IsCancellationRequested) + IEnumerable packets; + ArraySegment byteSegment; + + try { - int read = await client.ReceiveAsync(bufferSegment, SocketFlags.None); + while (!token.IsCancellationRequested) + { + int read = await client.ReceiveAsync(bufferSegment, SocketFlags.None).WithCancellation(token); - var payloadSegment = new ArraySegment(readBuffer, 0, read); - rtpStream.Process(payloadSegment); + var payloadSegment = new ArraySegment(readBuffer, 0, read); + rtpStream.Process(payloadSegment); - int ticksNow = Environment.TickCount; - if (!TimeUtils.IsTimeOver(ticksNow, lastTimeRtcpReportsSent, nextRtcpReportInterval)) - continue; + int ticksNow = Environment.TickCount; + if (!TimeUtils.IsTimeOver(ticksNow, lastTimeRtcpReportsSent, nextRtcpReportInterval)) + continue; - lastTimeRtcpReportsSent = ticksNow; - nextRtcpReportInterval = GetNextRtcpReportIntervalMs(); + lastTimeRtcpReportsSent = ticksNow; + nextRtcpReportInterval = GetNextRtcpReportIntervalMs(); - IEnumerable packets = reportsProvider.GetReportPackets(); - ArraySegment byteSegment = SerializeRtcpPackets(packets, bufferStream); + packets = reportsProvider.GetReportSdesPackets(); + byteSegment = SerializeRtcpPackets(packets, bufferStream); - await client.SendAsync(byteSegment, SocketFlags.None); + if (_connectionParameters.RtpTransport == RtpTransportProtocol.UDP) + await clientRtcp.SendAsync(byteSegment, SocketFlags.None).WithCancellation(token); + else if (_connectionParameters.RtpTransport == RtpTransportProtocol.MULTICAST) + await clientRtcp.SendToAsync(byteSegment, SocketFlags.None, _udpJoinedGroupsMap[clientRtcp]).WithCancellation(token); + } + } + catch (OperationCanceledException) + { + if (!token.IsCancellationRequested) throw; } + + packets = reportsProvider.GetReportByePackets(); + byteSegment = SerializeRtcpPackets(packets, bufferStream); + + if (_connectionParameters.RtpTransport == RtpTransportProtocol.UDP) + await clientRtcp.SendAsync(byteSegment, SocketFlags.None); + else if (_connectionParameters.RtpTransport == RtpTransportProtocol.MULTICAST) + await clientRtcp.SendToAsync(byteSegment, SocketFlags.None, _udpJoinedGroupsMap[clientRtcp]); } private static async Task ReceiveRtcpFromUdpAsync(Socket client, ITransportStream stream, @@ -584,12 +703,19 @@ private static async Task ReceiveRtcpFromUdpAsync(Socket client, ITransportStrea var readBuffer = new byte[Constants.UdpReceiveBufferSize]; var bufferSegment = new ArraySegment(readBuffer); - while (!token.IsCancellationRequested) + try { - int read = await client.ReceiveAsync(bufferSegment, SocketFlags.None); + while (!token.IsCancellationRequested) + { + int read = await client.ReceiveAsync(bufferSegment, SocketFlags.None).WithCancellation(token); - var payloadSegment = new ArraySegment(readBuffer, 0, read); - stream.Process(payloadSegment); + var payloadSegment = new ArraySegment(readBuffer, 0, read); + stream.Process(payloadSegment); + } + } + catch (OperationCanceledException) + { + if (!token.IsCancellationRequested) throw; } } diff --git a/RtspClientSharp/Rtsp/RtspHttpTransportClient.cs b/RtspClientSharp/Rtsp/RtspHttpTransportClient.cs index 4816f8e..ebd29cb 100644 --- a/RtspClientSharp/Rtsp/RtspHttpTransportClient.cs +++ b/RtspClientSharp/Rtsp/RtspHttpTransportClient.cs @@ -22,9 +22,11 @@ class RtspHttpTransportClient : RtspTransportClient private Stream _dataNetworkStream; private uint _commandCounter; private EndPoint _remoteEndPoint = new IPEndPoint(IPAddress.None, 0); + private EndPoint _localEndPoint = new IPEndPoint(IPAddress.Any, 0); private int _disposed; public override EndPoint RemoteEndPoint => _remoteEndPoint; + public override EndPoint LocalEndPoint => _localEndPoint; public RtspHttpTransportClient(ConnectionParameters connectionParameters) : base(connectionParameters) @@ -45,6 +47,7 @@ public override async Task ConnectAsync(CancellationToken token) await _streamDataClient.ConnectAsync(connectionUri.Host, httpPort); _remoteEndPoint = _streamDataClient.RemoteEndPoint; + _localEndPoint = _streamDataClient.LocalEndPoint; _dataNetworkStream = new NetworkStream(_streamDataClient, false); string request = ComposeGetRequest(); diff --git a/RtspClientSharp/Rtsp/RtspRequestMessageFactory.cs b/RtspClientSharp/Rtsp/RtspRequestMessageFactory.cs index 5773aef..7e706ce 100644 --- a/RtspClientSharp/Rtsp/RtspRequestMessageFactory.cs +++ b/RtspClientSharp/Rtsp/RtspRequestMessageFactory.cs @@ -54,6 +54,16 @@ public RtspRequestMessage CreateSetupUdpUnicastRequest(string trackName, int rtp return rtspRequestMessage; } + public RtspRequestMessage CreateSetupUdpMulticastRequest(string trackName) + { + Uri trackUri = GetTrackUri(trackName); + + var rtspRequestMessage = new RtspRequestMessage(RtspMethod.SETUP, trackUri, ProtocolVersion, + NextCSeqProvider, _userAgent, SessionId); + rtspRequestMessage.Headers.Add("Transport", $"RTP/AVP;multicast"); + return rtspRequestMessage; + } + public RtspRequestMessage CreatePlayRequest() { Uri uri = GetContentBasedUri(); diff --git a/RtspClientSharp/Rtsp/RtspTcpTransportClient.cs b/RtspClientSharp/Rtsp/RtspTcpTransportClient.cs index d225e1a..c6f118d 100644 --- a/RtspClientSharp/Rtsp/RtspTcpTransportClient.cs +++ b/RtspClientSharp/Rtsp/RtspTcpTransportClient.cs @@ -14,9 +14,11 @@ class RtspTcpTransportClient : RtspTransportClient private Socket _tcpClient; private Stream _networkStream; private EndPoint _remoteEndPoint = new IPEndPoint(IPAddress.None, 0); + private EndPoint _localEndPoint = new IPEndPoint(IPAddress.Any, 0); private int _disposed; public override EndPoint RemoteEndPoint => _remoteEndPoint; + public override EndPoint LocalEndPoint => _localEndPoint; public RtspTcpTransportClient(ConnectionParameters connectionParameters) : base(connectionParameters) @@ -34,6 +36,7 @@ public override async Task ConnectAsync(CancellationToken token) await _tcpClient.ConnectAsync(connectionUri.Host, rtspPort); _remoteEndPoint = _tcpClient.RemoteEndPoint; + _localEndPoint = _tcpClient.LocalEndPoint; _networkStream = new NetworkStream(_tcpClient, false); } diff --git a/RtspClientSharp/Rtsp/RtspTransportClient.cs b/RtspClientSharp/Rtsp/RtspTransportClient.cs index f8f6ee4..28616e2 100644 --- a/RtspClientSharp/Rtsp/RtspTransportClient.cs +++ b/RtspClientSharp/Rtsp/RtspTransportClient.cs @@ -17,6 +17,7 @@ abstract class RtspTransportClient : IRtspTransportClient private Authenticator _authenticator; public abstract EndPoint RemoteEndPoint { get; } + public abstract EndPoint LocalEndPoint { get; } protected RtspTransportClient(ConnectionParameters connectionParameters) { diff --git a/RtspClientSharp/Utils/NetworkClientFactory.cs b/RtspClientSharp/Utils/NetworkClientFactory.cs index c402b81..dc4abd3 100644 --- a/RtspClientSharp/Utils/NetworkClientFactory.cs +++ b/RtspClientSharp/Utils/NetworkClientFactory.cs @@ -5,7 +5,7 @@ namespace RtspClientSharp.Utils static class NetworkClientFactory { private const int TcpReceiveBufferDefaultSize = 64 * 1024; - private const int UdpReceiveBufferDefaultSize = 128 * 1024; + private const int UdpReceiveBufferDefaultSize = 512 * 1024; private const int SIO_UDP_CONNRESET = -1744830452; private static readonly byte[] EmptyOptionInValue = { 0, 0, 0, 0 }; diff --git a/RtspClientSharp/Utils/SocketExtensions.cs b/RtspClientSharp/Utils/SocketExtensions.cs new file mode 100644 index 0000000..bb51cdd --- /dev/null +++ b/RtspClientSharp/Utils/SocketExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace RtspClientSharp.Utils +{ + static class SocketExtensions + { + public static void JoinMulticastGroup(this Socket socket, IPAddress multicastGroupIp, IPAddress localIp) + { + MulticastOption multicastOption = new MulticastOption(multicastGroupIp, localIp); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, multicastOption); + } + + public static void LeaveMulticastGroup(this Socket socket, IPAddress multicastGroupIp) + { + MulticastOption multicastOption = new MulticastOption(multicastGroupIp); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.DropMembership, multicastOption); + } + + public static void JoinMulticastSourceGroup(this Socket socket, IPAddress multicastGroupIp, IPAddress localIp, IPAddress sourceIp) + { + byte[] membershipAddresses = new byte[12]; // 3 IPs * 4 bytes (IPv4) + Buffer.BlockCopy(multicastGroupIp.GetAddressBytes(), 0, membershipAddresses, 0, 4); + Buffer.BlockCopy(sourceIp.GetAddressBytes(), 0, membershipAddresses, 4, 4); + Buffer.BlockCopy(localIp.GetAddressBytes(), 0, membershipAddresses, 8, 4); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddSourceMembership, membershipAddresses); + } + } +} \ No newline at end of file diff --git a/RtspClientSharp/Utils/TaskExtensions.cs b/RtspClientSharp/Utils/TaskExtensions.cs index 75fc15e..beccd2a 100644 --- a/RtspClientSharp/Utils/TaskExtensions.cs +++ b/RtspClientSharp/Utils/TaskExtensions.cs @@ -1,4 +1,6 @@ -using System.Runtime.CompilerServices; +using System; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; namespace RtspClientSharp.Utils @@ -17,5 +19,19 @@ private static void HandleException(Task task) { var ignore = task.Exception; } + + public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) + { + if (task != await Task.WhenAny(task, tcs.Task)) + { + throw new OperationCanceledException(cancellationToken); + } + } + + return task.Result; + } } } \ No newline at end of file From 3d6d5c565e42c59995bf2611bd41c500e1d2e0c2 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 20 Oct 2019 17:18:37 +0300 Subject: [PATCH 2/5] UnitTests updated --- .../Rtcp/RtcpReceiverReportsProviderTests.cs | 34 +++++++++++++++++-- .../Rtsp/RtspTransportClientEmulator.cs | 2 ++ .../Rtsp/RtspTransportClientTests.cs | 1 + 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/RtspClientSharp.UnitTests/Rtcp/RtcpReceiverReportsProviderTests.cs b/RtspClientSharp.UnitTests/Rtcp/RtcpReceiverReportsProviderTests.cs index b3b54b2..5e1ce04 100644 --- a/RtspClientSharp.UnitTests/Rtcp/RtcpReceiverReportsProviderTests.cs +++ b/RtspClientSharp.UnitTests/Rtcp/RtcpReceiverReportsProviderTests.cs @@ -20,7 +20,9 @@ public void GetReportPackets_FakeDataProviders_ResetStateOfRtpStatisticsProvider var rtcpReportsProvider = new RtcpReceiverReportsProvider(rtpStatisticsProviderMock.Object, rtcpSenderStatisticsProviderFake.Object, 1); - rtcpReportsProvider.GetReportPackets().ToList(); + rtcpReportsProvider.GetReportSdesPackets().ToList(); + + rtcpReportsProvider.GetReportByePackets().ToList(); rtpStatisticsProviderMock.Verify(x => x.ResetState()); } @@ -45,7 +47,7 @@ public void GetReportPackets_TestDataProviders_ReturnsPacketWithValidReceiverRep var rtcpReportsProvider = new RtcpReceiverReportsProvider(rtpStatisticsProviderFake.Object, rtcpSenderStatisticsProviderFake.Object, 1112234); - IReadOnlyList packets = rtcpReportsProvider.GetReportPackets().ToList(); + IReadOnlyList packets = rtcpReportsProvider.GetReportSdesPackets().ToList(); var receiverReportPacket = (RtcpReceiverReportPacket) packets.First(p => p is RtcpReceiverReportPacket); Assert.IsFalse(receiverReportPacket.PaddingFlag); @@ -60,6 +62,16 @@ public void GetReportPackets_TestDataProviders_ReturnsPacketWithValidReceiverRep Assert.AreEqual(2 << 16 | 10u, receiverReportPacket.Reports[0].ExtHighestSequenceNumberReceived); Assert.AreEqual(1234u, receiverReportPacket.Reports[0].LastNtpTimeSenderReportReceived); Assert.AreEqual(0u, receiverReportPacket.Reports[0].DelaySinceLastTimeSenderReportReceived); + + packets = rtcpReportsProvider.GetReportByePackets().ToList(); + + var receiverReportByePacket = (RtcpByePacket)packets.First(p => p is RtcpByePacket); + Assert.IsFalse(receiverReportByePacket.PaddingFlag); + Assert.AreNotEqual(0, receiverReportByePacket.SourceCount); + Assert.AreEqual(203, receiverReportByePacket.PayloadType); + Assert.AreNotEqual(0, receiverReportByePacket.DwordLength); + Assert.AreNotEqual(0, receiverReportByePacket.Length); + Assert.AreEqual(1112234u, receiverReportByePacket.SyncSourcesIds.First()); } [TestMethod] @@ -71,7 +83,7 @@ public void GetReportPackets_TestDataProviders_ReturnsPacketWithValidSdesReport( var rtcpReportsProvider = new RtcpReceiverReportsProvider(rtpStatisticsProviderFake.Object, rtcpSenderStatisticsProviderFake.Object, 1112234); - IReadOnlyList packets = rtcpReportsProvider.GetReportPackets().ToList(); + IReadOnlyList packets = rtcpReportsProvider.GetReportSdesPackets().ToList(); var sdesReportPacket = (RtcpSdesReportPacket) packets.First(p => p is RtcpSdesReportPacket); @@ -80,5 +92,21 @@ public void GetReportPackets_TestDataProviders_ReturnsPacketWithValidSdesReport( var nameItem = (RtcpSdesNameItem) sdesReportPacket.Chunks[0].Items.First(i => i is RtcpSdesNameItem); Assert.IsNotNull(nameItem.DomainName); } + + [TestMethod] + public void GetReportPackets_TestDataProviders_ReturnsPacketWithByeReport() + { + var rtpStatisticsProviderFake = new Mock(); + var rtcpSenderStatisticsProviderFake = new Mock(); + + var rtcpReportsProvider = new RtcpReceiverReportsProvider(rtpStatisticsProviderFake.Object, + rtcpSenderStatisticsProviderFake.Object, 1112234); + + IReadOnlyList packets = rtcpReportsProvider.GetReportByePackets().ToList(); + + var byePacket = (RtcpByePacket)packets.First(p => p is RtcpByePacket); + + Assert.AreEqual(1112234u, byePacket.SyncSourcesIds.First()); + } } } \ No newline at end of file diff --git a/RtspClientSharp.UnitTests/Rtsp/RtspTransportClientEmulator.cs b/RtspClientSharp.UnitTests/Rtsp/RtspTransportClientEmulator.cs index 08a695e..890bbae 100644 --- a/RtspClientSharp.UnitTests/Rtsp/RtspTransportClientEmulator.cs +++ b/RtspClientSharp.UnitTests/Rtsp/RtspTransportClientEmulator.cs @@ -57,6 +57,8 @@ class RtspTransportClientEmulator : IRtspTransportClient public EndPoint RemoteEndPoint => new IPEndPoint(IPAddress.Loopback, 11080); + public EndPoint LocalEndPoint => new IPEndPoint(IPAddress.Loopback, 11080); + public virtual Task ConnectAsync(CancellationToken token) { return Task.CompletedTask; diff --git a/RtspClientSharp.UnitTests/Rtsp/RtspTransportClientTests.cs b/RtspClientSharp.UnitTests/Rtsp/RtspTransportClientTests.cs index 8095a7b..3502706 100644 --- a/RtspClientSharp.UnitTests/Rtsp/RtspTransportClientTests.cs +++ b/RtspClientSharp.UnitTests/Rtsp/RtspTransportClientTests.cs @@ -19,6 +19,7 @@ private class RtspTransportClientFake : RtspTransportClient private MemoryStream _responseStream; public override EndPoint RemoteEndPoint => new IPEndPoint(0, 0); + public override EndPoint LocalEndPoint => new IPEndPoint(0, 0); public RtspTransportClientFake(ConnectionParameters connectionParameters, Func responseProvider) From 547acd390f80fcc6a8b96bf376011efecde3aa30 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 21 Oct 2019 15:47:26 +0300 Subject: [PATCH 3/5] Support for IPv6 and mixed IPv4&IPv6 addresses in multicast group functions --- RtspClientSharp/Utils/SocketExtensions.cs | 70 ++++++++++++++++++++--- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/RtspClientSharp/Utils/SocketExtensions.cs b/RtspClientSharp/Utils/SocketExtensions.cs index bb51cdd..115761d 100644 --- a/RtspClientSharp/Utils/SocketExtensions.cs +++ b/RtspClientSharp/Utils/SocketExtensions.cs @@ -9,22 +9,78 @@ static class SocketExtensions public static void JoinMulticastGroup(this Socket socket, IPAddress multicastGroupIp, IPAddress localIp) { MulticastOption multicastOption = new MulticastOption(multicastGroupIp, localIp); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, multicastOption); + if (multicastGroupIp.AddressFamily == AddressFamily.InterNetwork) + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, multicastOption); + else if (multicastGroupIp.AddressFamily == AddressFamily.InterNetworkV6) + socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.AddMembership, multicastOption); } public static void LeaveMulticastGroup(this Socket socket, IPAddress multicastGroupIp) { MulticastOption multicastOption = new MulticastOption(multicastGroupIp); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.DropMembership, multicastOption); + if (multicastGroupIp.AddressFamily == AddressFamily.InterNetwork) + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.DropMembership, multicastOption); + else if (multicastGroupIp.AddressFamily == AddressFamily.InterNetworkV6) + socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.DropMembership, multicastOption); } public static void JoinMulticastSourceGroup(this Socket socket, IPAddress multicastGroupIp, IPAddress localIp, IPAddress sourceIp) { - byte[] membershipAddresses = new byte[12]; // 3 IPs * 4 bytes (IPv4) - Buffer.BlockCopy(multicastGroupIp.GetAddressBytes(), 0, membershipAddresses, 0, 4); - Buffer.BlockCopy(sourceIp.GetAddressBytes(), 0, membershipAddresses, 4, 4); - Buffer.BlockCopy(localIp.GetAddressBytes(), 0, membershipAddresses, 8, 4); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddSourceMembership, membershipAddresses); + // let's try to convert all IPs to type provided by server in "destination" parameter + if (multicastGroupIp.AddressFamily == AddressFamily.InterNetwork) + { + if (localIp.AddressFamily != AddressFamily.InterNetwork) + { + if (localIp.IsIPv4MappedToIPv6) + localIp = localIp.MapToIPv4(); + else + localIp = IPAddress.Any; + } + if (sourceIp.AddressFamily != AddressFamily.InterNetwork) + { + if (sourceIp.IsIPv4MappedToIPv6) + sourceIp = sourceIp.MapToIPv4(); + else + sourceIp = IPAddress.None; + } + if (!IPAddress.None.Equals(sourceIp)) + { + byte[] membershipAddresses = new byte[12]; // 3 IPs * 4 bytes (IPv4) + Buffer.BlockCopy(multicastGroupIp.GetAddressBytes(), 0, membershipAddresses, 0, 4); + Buffer.BlockCopy(sourceIp.GetAddressBytes(), 0, membershipAddresses, 4, 4); + Buffer.BlockCopy(localIp.GetAddressBytes(), 0, membershipAddresses, 8, 4); + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddSourceMembership, membershipAddresses); + } + else + { + // if we don't have good source IP, join group without source + JoinMulticastGroup(socket, multicastGroupIp, localIp); + } + } + else if (multicastGroupIp.AddressFamily == AddressFamily.InterNetworkV6) + { + if (localIp.AddressFamily != AddressFamily.InterNetworkV6) + { + localIp = localIp.MapToIPv6(); + } + if (sourceIp.AddressFamily != AddressFamily.InterNetworkV6) + { + sourceIp = sourceIp.MapToIPv6(); + } + if (!IPAddress.None.Equals(sourceIp)) + { + byte[] membershipAddresses = new byte[48]; // 3 IPs * 16 bytes (IPv6) + Buffer.BlockCopy(multicastGroupIp.GetAddressBytes(), 0, membershipAddresses, 0, 16); + Buffer.BlockCopy(sourceIp.GetAddressBytes(), 0, membershipAddresses, 16, 16); + Buffer.BlockCopy(localIp.GetAddressBytes(), 0, membershipAddresses, 32, 16); + socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.AddSourceMembership, membershipAddresses); + } + else + { + // if we don't have good source IP, join group without source + JoinMulticastGroup(socket, multicastGroupIp, localIp); + } + } } } } \ No newline at end of file From adf94f34a7045919cab968957271d83e2f389302 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 23 Oct 2019 10:05:40 +0300 Subject: [PATCH 4/5] When joining Source Specific Multicast group Receiver to Sender UDP packets are sent to unicast IP of source (rfc5760) --- RtspClientSharp/Rtsp/RtspClientInternal.cs | 6 +++--- RtspClientSharp/Utils/SocketExtensions.cs | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/RtspClientSharp/Rtsp/RtspClientInternal.cs b/RtspClientSharp/Rtsp/RtspClientInternal.cs index 84c1216..be5cee9 100644 --- a/RtspClientSharp/Rtsp/RtspClientInternal.cs +++ b/RtspClientSharp/Rtsp/RtspClientInternal.cs @@ -344,8 +344,8 @@ private async Task SetupTrackAsync(RtspMediaTrackInfo track, CancellationToken t IPEndPoint endPointRtcp = new IPEndPoint(localIpToServer, rtcpChannelNumber); rtcpClient.Bind(endPointRtcp); - rtcpClient.JoinMulticastSourceGroup(destinationAddress, localIpToServer, sourceAddress); - _udpJoinedGroupsMap[rtcpClient] = new IPEndPoint(destinationAddress, rtcpChannelNumber); + IPAddress whereToReportRtcp = rtcpClient.JoinMulticastSourceGroup(destinationAddress, localIpToServer, sourceAddress); + _udpJoinedGroupsMap[rtcpClient] = new IPEndPoint(whereToReportRtcp, rtcpChannelNumber); } catch { @@ -361,8 +361,8 @@ private async Task SetupTrackAsync(RtspMediaTrackInfo track, CancellationToken t var udpHolePunchingPacketSegment = new ArraySegment(Array.Empty()); + // send "punch" packet to multicast group await rtpClient.SendToAsync(udpHolePunchingPacketSegment, SocketFlags.None, _udpJoinedGroupsMap[rtpClient]); - await rtcpClient.SendToAsync(udpHolePunchingPacketSegment, SocketFlags.None, _udpJoinedGroupsMap[rtcpClient]); _udpClientsMap[rtpChannelNumber] = rtpClient; _udpClientsMap[rtcpChannelNumber] = rtcpClient; diff --git a/RtspClientSharp/Utils/SocketExtensions.cs b/RtspClientSharp/Utils/SocketExtensions.cs index 115761d..9cb0b62 100644 --- a/RtspClientSharp/Utils/SocketExtensions.cs +++ b/RtspClientSharp/Utils/SocketExtensions.cs @@ -24,8 +24,17 @@ public static void LeaveMulticastGroup(this Socket socket, IPAddress multicastGr socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.DropMembership, multicastOption); } - public static void JoinMulticastSourceGroup(this Socket socket, IPAddress multicastGroupIp, IPAddress localIp, IPAddress sourceIp) + /// + /// Join Single Source Multicast Group (SSM) with fallback to Any Source Multicast Group scenario (ASM) + /// + /// Socket to join group + /// IP of multicast group + /// IP of interface able to reach source + /// IP of RTP/RTCP source + /// IP address where to send RTCP Receiver to Sender reports. Multicast group in case of ASM and unicast source IP for SSM. + public static IPAddress JoinMulticastSourceGroup(this Socket socket, IPAddress multicastGroupIp, IPAddress localIp, IPAddress sourceIp) { + IPAddress rtcpToSource = IPAddress.None; // let's try to convert all IPs to type provided by server in "destination" parameter if (multicastGroupIp.AddressFamily == AddressFamily.InterNetwork) { @@ -50,11 +59,13 @@ public static void JoinMulticastSourceGroup(this Socket socket, IPAddress multic Buffer.BlockCopy(sourceIp.GetAddressBytes(), 0, membershipAddresses, 4, 4); Buffer.BlockCopy(localIp.GetAddressBytes(), 0, membershipAddresses, 8, 4); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddSourceMembership, membershipAddresses); + rtcpToSource = sourceIp; } else { // if we don't have good source IP, join group without source JoinMulticastGroup(socket, multicastGroupIp, localIp); + rtcpToSource = multicastGroupIp; } } else if (multicastGroupIp.AddressFamily == AddressFamily.InterNetworkV6) @@ -74,13 +85,16 @@ public static void JoinMulticastSourceGroup(this Socket socket, IPAddress multic Buffer.BlockCopy(sourceIp.GetAddressBytes(), 0, membershipAddresses, 16, 16); Buffer.BlockCopy(localIp.GetAddressBytes(), 0, membershipAddresses, 32, 16); socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.AddSourceMembership, membershipAddresses); + rtcpToSource = sourceIp; } else { // if we don't have good source IP, join group without source JoinMulticastGroup(socket, multicastGroupIp, localIp); + rtcpToSource = multicastGroupIp; } } + return rtcpToSource; } } } \ No newline at end of file From bc570c99ccf7b6309678ffadc16ead96054413f2 Mon Sep 17 00:00:00 2001 From: andrey Date: Thu, 25 Nov 2021 19:09:40 +0300 Subject: [PATCH 5/5] Fixed error with padding of RtcpSdesPacket --- RtspClientSharp/Rtcp/RtcpSdesReportPacket.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RtspClientSharp/Rtcp/RtcpSdesReportPacket.cs b/RtspClientSharp/Rtcp/RtcpSdesReportPacket.cs index 7b33664..cfe28f9 100644 --- a/RtspClientSharp/Rtcp/RtcpSdesReportPacket.cs +++ b/RtspClientSharp/Rtcp/RtcpSdesReportPacket.cs @@ -42,7 +42,7 @@ protected override void FillFromByteSegment(ArraySegment byteSegment) chunk.Serialize(stream); } - if (_paddingByteCount > 0) + if ((_paddingByteCount > 0) && (_paddingByteCount < 4)) stream.Write(PaddingBytes, 0, _paddingByteCount); } }