diff --git a/examples/OpenAIExamples/AliceAndBob/Program.cs b/examples/OpenAIExamples/AliceAndBob/Program.cs index f75c8fca04..715917a391 100644 --- a/examples/OpenAIExamples/AliceAndBob/Program.cs +++ b/examples/OpenAIExamples/AliceAndBob/Program.cs @@ -34,6 +34,8 @@ using SIPSorcery.OpenAI.Realtime.Models; using SIPSorceryMedia.Abstractions; using SIPSorcery.Media; +using System.Buffers; +using CommunityToolkit.HighPerformance.Buffers; namespace demo; @@ -161,11 +163,15 @@ private static void InitialiseWindowsAudioEndPoint(IWebRTCEndPoint webrtcEndPoin webrtcEndPoint.OnAudioFrameReceived += (EncodedAudioFrame encodedAudioFrame) => { - var decodedSample = audioEncoder.DecodeAudio(encodedAudioFrame.EncodedAudio, AudioCommonlyUsedFormats.OpusWebRTC); + using var buffer = new ArrayPoolBufferWriter(8192); + audioEncoder.DecodeAudio(encodedAudioFrame.EncodedAudio.Span, AudioCommonlyUsedFormats.OpusWebRTC, buffer); + var decodedSample = buffer.WrittenSpan; - var samples = decodedSample - .Select(s => new Complex(s / 32768f, 0f)) - .ToArray(); + var samples = new Complex[decodedSample.Length]; + for (int i = 0; i < samples.Length; i++) + { + samples[i] = new Complex(decodedSample[i] / 32768f, 0f); + } var frame = _audioScopeForm?.Invoke(() => _audioScopeForm.ProcessAudioSample(samples, audioScopeNumber)); }; diff --git a/examples/SIPExamples/CustomAudioCodec/Program.cs b/examples/SIPExamples/CustomAudioCodec/Program.cs index 709041057e..9549792777 100644 --- a/examples/SIPExamples/CustomAudioCodec/Program.cs +++ b/examples/SIPExamples/CustomAudioCodec/Program.cs @@ -14,18 +14,23 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; +using System.Buffers.Binary; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.HighPerformance; +using CommunityToolkit.HighPerformance.Buffers; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Extensions.Logging; using SIPSorcery.Media; using SIPSorcery.SIP; using SIPSorcery.SIP.App; -using SIPSorceryMedia.Windows; using SIPSorceryMedia.Abstractions; +using SIPSorceryMedia.Windows; namespace demo { @@ -50,15 +55,21 @@ public G729Codec() _g729Decoder = new G729Decoder(); } - public short[] DecodeAudio(byte[] encodedSample, AudioFormat format) + public void DecodeAudio(ReadOnlySpan encodedSample, AudioFormat format, IBufferWriter destination) { - var pcm = _g729Decoder.Process(encodedSample); - return pcm.Where((x, i) => i % 2 == 0).Select((y, i) => (short)(pcm[i * 2 + 1] << 8 | pcm[i * 2])).ToArray(); + using var buffer = new ArrayPoolBufferWriter(8192); + _g729Decoder.Process(encodedSample, buffer); + + var pcm = buffer.WrittenSpan; + for (int i = 0; i < pcm.Length / 2; i++) + { + destination.Write(BinaryPrimitives.ReadInt16LittleEndian(pcm.Slice(i * 2, 2))); + } } - public byte[] EncodeAudio(short[] pcm, AudioFormat format) + public void EncodeAudio(ReadOnlySpan pcm, AudioFormat format, IBufferWriter destination) { - return _g729Encoder.Process(pcm.SelectMany(x => new byte[] { (byte)(x), (byte)(x >> 8) }).ToArray()); + _g729Encoder.Process(MemoryMarshal.AsBytes(pcm), destination); } } @@ -105,7 +116,8 @@ static async Task Main() Console.WriteLine("Hanging up established call."); userAgent.Hangup(); } - }; + } + ; exitCts.Cancel(); }; diff --git a/examples/SIPExamples/RecordCall/Program.cs b/examples/SIPExamples/RecordCall/Program.cs index a8ef79ef87..e49976c609 100644 --- a/examples/SIPExamples/RecordCall/Program.cs +++ b/examples/SIPExamples/RecordCall/Program.cs @@ -102,13 +102,13 @@ private static void OnRtpPacketReceived(IPEndPoint remoteEndPoint, SDPMediaTypes { if (rtpPacket.Header.PayloadType == (int)SDPWellKnownMediaFormatsEnum.PCMA) { - short pcm = NAudio.Codecs.ALawDecoder.ALawToLinearSample(sample[index]); + short pcm = NAudio.Codecs.ALawDecoder.ALawToLinearSample(sample.Span[index]); byte[] pcmSample = new byte[] { (byte)(pcm & 0xFF), (byte)(pcm >> 8) }; _waveFile.Write(pcmSample, 0, 2); } else { - short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(sample[index]); + short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(sample.Span[index]); byte[] pcmSample = new byte[] { (byte)(pcm & 0xFF), (byte)(pcm >> 8) }; _waveFile.Write(pcmSample, 0, 2); } diff --git a/examples/SIPExamples/RecordIncomingCall/Program.cs b/examples/SIPExamples/RecordIncomingCall/Program.cs index 5dff7087dc..9444b295ff 100644 --- a/examples/SIPExamples/RecordIncomingCall/Program.cs +++ b/examples/SIPExamples/RecordIncomingCall/Program.cs @@ -81,13 +81,13 @@ private static void OnRtpPacketReceived(IPEndPoint remoteEndPoint, SDPMediaTypes { if (rtpPacket.Header.PayloadType == (int)SDPWellKnownMediaFormatsEnum.PCMA) { - short pcm = NAudio.Codecs.ALawDecoder.ALawToLinearSample(sample[index]); + short pcm = NAudio.Codecs.ALawDecoder.ALawToLinearSample(sample.Span[index]); byte[] pcmSample = new byte[] { (byte)(pcm & 0xFF), (byte)(pcm >> 8) }; _waveFile.Write(pcmSample, 0, 2); } else { - short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(sample[index]); + short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(sample.Span[index]); byte[] pcmSample = new byte[] { (byte)(pcm & 0xFF), (byte)(pcm >> 8) }; _waveFile.Write(pcmSample, 0, 2); } diff --git a/examples/SIPExamples/SIPCallResampleAudio/Program.cs b/examples/SIPExamples/SIPCallResampleAudio/Program.cs index 02d35b9c09..c8fec860d9 100644 --- a/examples/SIPExamples/SIPCallResampleAudio/Program.cs +++ b/examples/SIPExamples/SIPCallResampleAudio/Program.cs @@ -96,7 +96,7 @@ private static void RtpSession_OnRtpPacketReceived(IPEndPoint remoteEndPoint, SD for (int index = 0; index < sample.Length; index++) { - short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(sample[index]); + short pcm = NAudio.Codecs.MuLawDecoder.MuLawToLinearSample(sample.Span[index]); float s16 = pcm / 32768f; for (int i = 0; i < _ratio; i++) diff --git a/examples/SIPExamples/VideoPhoneCmdLine/Program.cs b/examples/SIPExamples/VideoPhoneCmdLine/Program.cs index 5cf535e49c..0f1e1c0d5d 100755 --- a/examples/SIPExamples/VideoPhoneCmdLine/Program.cs +++ b/examples/SIPExamples/VideoPhoneCmdLine/Program.cs @@ -34,6 +34,7 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; using System.Drawing; using System.Linq; @@ -110,13 +111,13 @@ public DecoderVideoSink(IVideoEncoder videoDecoder) public void GotVideoRtp(IPEndPoint remoteEndPoint, uint ssrc, uint seqnum, uint timestamp, int payloadID, bool marker, byte[] payload) => throw new ApplicationException("This Video End Point requires full video frames rather than individual RTP packets."); - public void GotVideoFrame(IPEndPoint remoteEndPoint, uint timestamp, byte[] frame, VideoFormat format) + public void GotVideoFrame(IPEndPoint remoteEndPoint, uint timestamp, ReadOnlyMemory frame, VideoFormat format) { if (OnVideoSinkDecodedSample != null) { try { - foreach (var decoded in _videoDecoder.DecodeVideo(frame, VideoPixelFormatsEnum.Bgr, format.Codec)) + foreach (var decoded in _videoDecoder.DecodeVideo(frame.ToArray(), VideoPixelFormatsEnum.Bgr, format.Codec)) { OnVideoSinkDecodedSample(decoded.Sample, decoded.Width, decoded.Height, (int)(decoded.Width * 3), VideoPixelFormatsEnum.Bgr); } diff --git a/examples/TurnServerExample/Program.cs b/examples/TurnServerExample/Program.cs index dbc0142cc0..04e9e7bdd5 100644 --- a/examples/TurnServerExample/Program.cs +++ b/examples/TurnServerExample/Program.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: Program.cs // // Description: An example TURN server (RFC 5766) console application that @@ -104,13 +104,13 @@ static async Task RunClientDemo(int serverPort) var challenge = await ReceiveStun(stream); var errorAttr = challenge.Attributes.FirstOrDefault( a => a.AttributeType == STUNAttributeTypesEnum.ErrorCode); - int errorCode = errorAttr.Value[2] * 100 + errorAttr.Value[3]; + int errorCode = errorAttr.Value.Span[2] * 100 + errorAttr.Value.Span[3]; Console.WriteLine($"[Client] Got {errorCode} challenge with REALM and NONCE."); // Extract nonce from the challenge var nonce = Encoding.UTF8.GetString( challenge.Attributes.First( - a => a.AttributeType == STUNAttributeTypesEnum.Nonce).Value); + a => a.AttributeType == STUNAttributeTypesEnum.Nonce).Value.Span); Console.WriteLine("[Client] Sending authenticated Allocate..."); var authReq = new STUNMessage(STUNMessageTypesEnum.Allocate); @@ -191,12 +191,12 @@ await peer.SendAsync(response, response.Length, while (read < remaining) read += await stream.ReadAsync(full, 4 + read, remaining - read); - var dataInd = STUNMessage.ParseSTUNMessage(full, full.Length); + var dataInd = STUNMessage.ParseSTUNMessage(full); var dataAttr = dataInd.Attributes.FirstOrDefault( a => a.AttributeType == STUNAttributeTypesEnum.Data); if (dataAttr != null) { - Console.WriteLine($"[Client] Received via relay: \"{Encoding.UTF8.GetString(dataAttr.Value)}\"\n"); + Console.WriteLine($"[Client] Received via relay: \"{Encoding.UTF8.GetString(dataAttr.Value.Span)}\"\n"); } } } @@ -229,8 +229,9 @@ static byte[] ComputeHmacKey(string username, string realm, string password) static async Task SendStun(NetworkStream stream, STUNMessage msg, byte[] hmacKey = null) { - var bytes = msg.ToByteBuffer(hmacKey, false); - await stream.WriteAsync(bytes, 0, bytes.Length); + var bytes = new byte[msg.GetByteBufferSize(hmacKey, false)]; + msg.WriteToBuffer(bytes, hmacKey, false); + await stream.WriteAsync(bytes); await stream.FlushAsync(); } @@ -250,7 +251,7 @@ static async Task ReceiveStun(NetworkStream stream) while (read < remaining) read += await stream.ReadAsync(full, 4 + read, remaining - read); - return STUNMessage.ParseSTUNMessage(full, full.Length); + return STUNMessage.ParseSTUNMessage(full); } #endregion diff --git a/examples/WebRTCExamples/FfmpegToWebRTC/FfmpegToWebRTC.csproj b/examples/WebRTCExamples/FfmpegToWebRTC/FfmpegToWebRTC.csproj index 739059b4db..4215fcc0e4 100755 --- a/examples/WebRTCExamples/FfmpegToWebRTC/FfmpegToWebRTC.csproj +++ b/examples/WebRTCExamples/FfmpegToWebRTC/FfmpegToWebRTC.csproj @@ -4,6 +4,7 @@ Exe net8.0 x64 + 9.0 diff --git a/examples/WebRTCExamples/FfmpegToWebRTC/Program.cs b/examples/WebRTCExamples/FfmpegToWebRTC/Program.cs index 677d2a6ada..b545c6b06e 100644 --- a/examples/WebRTCExamples/FfmpegToWebRTC/Program.cs +++ b/examples/WebRTCExamples/FfmpegToWebRTC/Program.cs @@ -270,7 +270,7 @@ private static RTCPeerConnection Createpc(WebSocketContext context, SDPAudioVide if (media == SDPMediaTypesEnum.video && pc.VideoDestinationEndPoint != null) { //logger.LogDebug($"Forwarding {media} RTP packet to webrtc peer timestamp {rtpPkt.Header.Timestamp}."); - pc.SendRtpRaw(media, rtpPkt.Payload, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); + pc.SendRtpRaw(media, rtpPkt.Payload.Span, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); } }; } diff --git a/examples/WebRTCExamples/JanusWebRTCStream/Program.cs b/examples/WebRTCExamples/JanusWebRTCStream/Program.cs index 2c289ad9e5..d3a64880eb 100644 --- a/examples/WebRTCExamples/JanusWebRTCStream/Program.cs +++ b/examples/WebRTCExamples/JanusWebRTCStream/Program.cs @@ -15,9 +15,11 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Drawing; using System.Drawing.Imaging; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; @@ -86,7 +88,7 @@ static async Task Main() MediaStreamTrack videoTrack = new MediaStreamTrack(videoSink.GetVideoSourceFormats(), MediaStreamStatusEnum.SendRecv); pc.addTrack(videoTrack); - pc.OnVideoFrameReceived += videoSink.GotVideoFrame; + pc.OnVideoFrameReceived += (remoteEndPoint, timestamp, frame, format) => videoSink.GotVideoFrame(remoteEndPoint, timestamp, frame.ToArray(), format); videoSource.OnVideoSourceEncodedSample += pc.SendVideo; pc.OnVideoFormatsNegotiated += (formats) => diff --git a/examples/WebRTCExamples/RtspToWebRTCAudioAndVideo/WebSocketSignalingServer.cs b/examples/WebRTCExamples/RtspToWebRTCAudioAndVideo/WebSocketSignalingServer.cs index f11d544850..b574a3113a 100644 --- a/examples/WebRTCExamples/RtspToWebRTCAudioAndVideo/WebSocketSignalingServer.cs +++ b/examples/WebRTCExamples/RtspToWebRTCAudioAndVideo/WebSocketSignalingServer.cs @@ -114,7 +114,7 @@ private RTCPeerConnection Createpc(WebSocketContext context) { try { - pc.SendRtpRaw(media, rtpPkt.Payload, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); + pc.SendRtpRaw(media, rtpPkt.Payload.Span, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); } catch (Exception ex) { @@ -125,7 +125,7 @@ private RTCPeerConnection Createpc(WebSocketContext context) { try { - pc.SendRtpRaw(media, rtpPkt.Payload, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); + pc.SendRtpRaw(media, rtpPkt.Payload.Span, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); } catch (Exception ex) { diff --git a/examples/WebRTCExamples/SIPToWebRtcBridgeVideo/Program.cs b/examples/WebRTCExamples/SIPToWebRtcBridgeVideo/Program.cs index 717caa7862..488384214a 100755 --- a/examples/WebRTCExamples/SIPToWebRtcBridgeVideo/Program.cs +++ b/examples/WebRTCExamples/SIPToWebRtcBridgeVideo/Program.cs @@ -28,6 +28,7 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Net; @@ -226,7 +227,7 @@ private static void ForwardAudioToPeerConnection(IPEndPoint remote, SDPMediaType } } - private static void ForwardVideoFrameToSIP(IPEndPoint remoteEP, uint timestamp, byte[] frame, VideoFormat format) + private static void ForwardVideoFrameToSIP(IPEndPoint remoteEP, uint timestamp, ReadOnlyMemory frame, VideoFormat format) { if (_rtpSession != null && !_rtpSession.IsClosed) { @@ -234,7 +235,7 @@ private static void ForwardVideoFrameToSIP(IPEndPoint remoteEP, uint timestamp, } } - private static void ForwardVideoFrameToPeerConnection(IPEndPoint remoteEP, uint timestamp, byte[] frame, VideoFormat format) + private static void ForwardVideoFrameToPeerConnection(IPEndPoint remoteEP, uint timestamp, ReadOnlyMemory frame, VideoFormat format) { if (_peerConnection != null && _peerConnection.connectionState == RTCPeerConnectionState.connected) { diff --git a/examples/WebRTCExamples/WebRTCEchoServer/WebRTCEchoServer.cs b/examples/WebRTCExamples/WebRTCEchoServer/WebRTCEchoServer.cs index 3204734a40..ade995f5d7 100755 --- a/examples/WebRTCExamples/WebRTCEchoServer/WebRTCEchoServer.cs +++ b/examples/WebRTCExamples/WebRTCEchoServer/WebRTCEchoServer.cs @@ -90,7 +90,7 @@ public async Task GotOffer(RTCSessionDescriptionInit pc.OnRtpPacketReceived += (IPEndPoint rep, SDPMediaTypesEnum media, RTPPacket rtpPkt) => { - pc.SendRtpRaw(media, rtpPkt.Payload, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); + pc.SendRtpRaw(media, rtpPkt.Payload.Span, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); //_logger.LogDebug($"RTP {media} pkt received, SSRC {rtpPkt.Header.SyncSource}, SeqNum {rtpPkt.Header.SequenceNumber}."); }; pc.OnRtpEvent += async (ep, ev, hdr) => diff --git a/examples/WebRTCExamples/WebRTCGetStartedDataChannel/Program.cs b/examples/WebRTCExamples/WebRTCGetStartedDataChannel/Program.cs index 76965ccd2f..4078fd3d6c 100755 --- a/examples/WebRTCExamples/WebRTCGetStartedDataChannel/Program.cs +++ b/examples/WebRTCExamples/WebRTCGetStartedDataChannel/Program.cs @@ -197,7 +197,7 @@ private static string DoJavscriptSHA256(byte[] buffer) using (var sha256 = SHA256.Create()) { - return sha256.ComputeHash(hashOfHashes).HexStr(); + return TypeExtensions.HexStr(sha256.ComputeHash(hashOfHashes)); } } diff --git a/examples/WebRTCExamples/WebRTCOpenGL/Program.cs b/examples/WebRTCExamples/WebRTCOpenGL/Program.cs index 84a6fce8ea..6d5214cefe 100755 --- a/examples/WebRTCExamples/WebRTCOpenGL/Program.cs +++ b/examples/WebRTCExamples/WebRTCOpenGL/Program.cs @@ -41,6 +41,8 @@ using AudioScope; using System.Numerics; using SIPSorceryMedia.Abstractions; +using System.Buffers; +using CommunityToolkit.HighPerformance.Buffers; namespace demo { @@ -158,11 +160,15 @@ private static Task CreatePeerConnection() if (media == SDPMediaTypesEnum.audio) { - var decodedSample = audioEncoder.DecodeAudio(rtpPkt.Payload, pc.AudioStream.NegotiatedFormat.ToAudioFormat()); - - var samples = decodedSample - .Select(s => new Complex(s / 32768f, 0f)) - .ToArray(); + using var buffer = new ArrayPoolBufferWriter(8192); + audioEncoder.DecodeAudio(rtpPkt.Payload.Span, pc.AudioStream.NegotiatedFormat.ToAudioFormat(), buffer); + var decodedSample = buffer.WrittenSpan; + + var samples = new Complex[decodedSample.Length]; + for (int i = 0; i < samples.Length; i++) + { + samples[i] = new Complex(decodedSample[i] / 32768f, 0f); + } var frame = _audioScopeForm.Invoke(() => _audioScopeForm.ProcessAudioSample(samples)); diff --git a/examples/WebRTCExamples/WebRTCOpenGLSource/Program.cs b/examples/WebRTCExamples/WebRTCOpenGLSource/Program.cs index b67ea05f8d..57a4bef484 100644 --- a/examples/WebRTCExamples/WebRTCOpenGLSource/Program.cs +++ b/examples/WebRTCExamples/WebRTCOpenGLSource/Program.cs @@ -44,6 +44,7 @@ using AudioScope; using System.Numerics; using SIPSorceryMedia.Abstractions; +using CommunityToolkit.HighPerformance.Buffers; namespace demo; @@ -157,23 +158,27 @@ private static Task CreatePeerConnection() } }; - audioSource.OnAudioSourceEncodedSample += (uint durationRtpUnits, byte[] sample) => + audioSource.OnAudioSourceEncodedSample += (durationRtpUnits, sample) => { //logger.LogDebug($"RTP {media} pkt received, SSRC {rtpPkt.Header.SyncSource}, payload {rtpPkt.Header.PayloadType}, SeqNum {rtpPkt.Header.SequenceNumber}."); - var decodedSample = audioEncoder.DecodeAudio(sample, pc.AudioStream.NegotiatedFormat.ToAudioFormat()); + using var buffer = new ArrayPoolBufferWriter(8192); + audioEncoder.DecodeAudio(sample.Span, pc.AudioStream.NegotiatedFormat.ToAudioFormat(), buffer); + var decodedSample = buffer.WrittenSpan; - var samples = decodedSample - .Select(s => new Complex(s / 32768f, 0f)) - .ToArray(); + var samples = new Complex[decodedSample.Length]; + for (int i = 0; i < samples.Length; i++) + { + samples[i] = new Complex(decodedSample[i] / 32768f, 0f); + } - var frame = _audioScopeForm.Invoke(() => _audioScopeForm.ProcessAudioSample(samples)); + var frame = _audioScopeForm.Invoke(() => _audioScopeForm.ProcessAudioSample(samples)); - videoEncoderEndPoint.ExternalVideoSourceRawSample(AUDIO_PACKET_DURATION, - FormAudioScope.AUDIO_SCOPE_WIDTH, - FormAudioScope.AUDIO_SCOPE_HEIGHT, - frame, - VideoPixelFormatsEnum.Rgb); + videoEncoderEndPoint.ExternalVideoSourceRawSample(AUDIO_PACKET_DURATION, + FormAudioScope.AUDIO_SCOPE_WIDTH, + FormAudioScope.AUDIO_SCOPE_HEIGHT, + frame, + VideoPixelFormatsEnum.Rgb); }; pc.onconnectionstatechange += async (state) => diff --git a/examples/WebRTCExamples/WebRTCReceiveAudio/Program.cs b/examples/WebRTCExamples/WebRTCReceiveAudio/Program.cs index feb434de3f..cd95358994 100755 --- a/examples/WebRTCExamples/WebRTCReceiveAudio/Program.cs +++ b/examples/WebRTCExamples/WebRTCReceiveAudio/Program.cs @@ -145,7 +145,7 @@ private static async Task SendSDPOffer(WebSocketContext conte if (media == SDPMediaTypesEnum.audio) { - windowsAudioEP.GotAudioRtp(rep, rtpPkt.Header.SyncSource, rtpPkt.Header.SequenceNumber, rtpPkt.Header.Timestamp, rtpPkt.Header.PayloadType, rtpPkt.Header.MarkerBit == 1, rtpPkt.Payload); + windowsAudioEP.GotAudioRtp(rep, rtpPkt.Header.SyncSource, rtpPkt.Header.SequenceNumber, rtpPkt.Header.Timestamp, rtpPkt.Header.PayloadType, rtpPkt.Header.MarkerBit == 1, rtpPkt.Payload.ToArray()); } }; diff --git a/examples/WebRTCExamples/WebRTCTestPatternServer/README.md b/examples/WebRTCExamples/WebRTCTestPatternServer/README.md index 364ae36a39..ce1a9d4c44 100644 --- a/examples/WebRTCExamples/WebRTCTestPatternServer/README.md +++ b/examples/WebRTCExamples/WebRTCTestPatternServer/README.md @@ -14,4 +14,4 @@ You will need `.NET` installed. - If you are feeling brave you can open `3x3.html` in a browser for 9 separate Peer Connections. -![3x3 screenshot](3x3.png) \ No newline at end of file +[3x3 screenshot](3x3.png) \ No newline at end of file diff --git a/examples/WebRTCExamples/WebRTCtoFfplay/Program.cs b/examples/WebRTCExamples/WebRTCtoFfplay/Program.cs index d764ce2a09..00d1e18b41 100644 --- a/examples/WebRTCExamples/WebRTCtoFfplay/Program.cs +++ b/examples/WebRTCExamples/WebRTCtoFfplay/Program.cs @@ -201,12 +201,12 @@ private static RTCPeerConnection Createpc(WebSocketContext context) if (media == SDPMediaTypesEnum.audio && rtpSession.AudioDestinationEndPoint != null) { //logger.LogDebug($"Forwarding {media} RTP packet to ffplay timestamp {rtpPkt.Header.Timestamp}."); - rtpSession.SendRtpRaw(media, rtpPkt.Payload, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); + rtpSession.SendRtpRaw(media, rtpPkt.Payload.Span, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); } else if (media == SDPMediaTypesEnum.video && rtpSession.VideoDestinationEndPoint != null) { //logger.LogDebug($"Forwarding {media} RTP packet to ffplay timestamp {rtpPkt.Header.Timestamp}."); - rtpSession.SendRtpRaw(media, rtpPkt.Payload, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); + rtpSession.SendRtpRaw(media, rtpPkt.Payload.Span, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); } }; pc.OnRtpClosed += (reason) => rtpSession.Close(reason); diff --git a/examples/WebRTCExamples/WebRTCtoFfplay/WebRTCtoFfplay.csproj b/examples/WebRTCExamples/WebRTCtoFfplay/WebRTCtoFfplay.csproj index a2a6ba86e2..b6a2496229 100755 --- a/examples/WebRTCExamples/WebRTCtoFfplay/WebRTCtoFfplay.csproj +++ b/examples/WebRTCExamples/WebRTCtoFfplay/WebRTCtoFfplay.csproj @@ -3,6 +3,7 @@ Exe net8.0 + 9.0 diff --git a/examples/WebRTCLightningExamples/WebRTCLightningGetStarted/Services/PaidWebRtcConnection.cs b/examples/WebRTCLightningExamples/WebRTCLightningGetStarted/Services/PaidWebRtcConnection.cs index f6734419de..1d540e4514 100755 --- a/examples/WebRTCLightningExamples/WebRTCLightningGetStarted/Services/PaidWebRtcConnection.cs +++ b/examples/WebRTCLightningExamples/WebRTCLightningGetStarted/Services/PaidWebRtcConnection.cs @@ -142,7 +142,7 @@ private async Task ClosePeerConnectionResources(Timer? setBitmapSourceTimer, IVi if (videoSource != null) { - await videoSource!.CloseVideo(); + await videoSource.CloseVideo(); } } diff --git a/examples/webrtccmdline/EchoServer.cs b/examples/webrtccmdline/EchoServer.cs index b3cb90a036..f24a4669a7 100644 --- a/examples/webrtccmdline/EchoServer.cs +++ b/examples/webrtccmdline/EchoServer.cs @@ -227,7 +227,7 @@ public async Task GotOffer(RTCSessionDescriptionInit offer) pc.OnRtpPacketReceived += (IPEndPoint rep, SDPMediaTypesEnum media, RTPPacket rtpPkt) => { - pc.SendRtpRaw(media, rtpPkt.Payload, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); + pc.SendRtpRaw(media, rtpPkt.Payload.Span, rtpPkt.Header.Timestamp, rtpPkt.Header.MarkerBit, rtpPkt.Header.PayloadType); }; pc.OnTimeout += (mediaType) => logger.LogWarning($"Timeout for {mediaType}."); diff --git a/examples/webrtccmdline/webrtccmdline.csproj b/examples/webrtccmdline/webrtccmdline.csproj old mode 100755 new mode 100644 index 9afc94cd5b..ab33d2cffe --- a/examples/webrtccmdline/webrtccmdline.csproj +++ b/examples/webrtccmdline/webrtccmdline.csproj @@ -5,16 +5,16 @@ net10.0 AnyCPU true + 9.0 - - - - - + + + + diff --git a/src/SIPSorcery.OpenAI.Realtime/Extensions/WebRTCExtensions.cs b/src/SIPSorcery.OpenAI.Realtime/Extensions/WebRTCExtensions.cs index e5e5d52ab2..09d70fde78 100644 --- a/src/SIPSorcery.OpenAI.Realtime/Extensions/WebRTCExtensions.cs +++ b/src/SIPSorcery.OpenAI.Realtime/Extensions/WebRTCExtensions.cs @@ -164,7 +164,7 @@ public static void PipeAudioTo(this RTCPeerConnection source, RTCPeerConnection { // Pipe audio payloads receied from the source WebRTC peer connection to the destination peer connection. source.OnAudioFrameReceived += (encodeAudioFrame) => destination.SendAudio( - RtpTimestampExtensions.ToRtpUnits(encodeAudioFrame.DurationMilliSeconds, destination.AudioStream.NegotiatedFormat.ToAudioFormat().RtpClockRate), + RtpTimestampExtensions.ToRtpUnits(encodeAudioFrame.DurationMilliSeconds, destination.AudioStream!.NegotiatedFormat.ToAudioFormat().RtpClockRate), encodeAudioFrame.EncodedAudio); } } diff --git a/src/SIPSorcery.OpenAI.Realtime/WebRTC/IWebRTCEndPoint.cs b/src/SIPSorcery.OpenAI.Realtime/WebRTC/IWebRTCEndPoint.cs index 6efc49ecad..40a3294af1 100644 --- a/src/SIPSorcery.OpenAI.Realtime/WebRTC/IWebRTCEndPoint.cs +++ b/src/SIPSorcery.OpenAI.Realtime/WebRTC/IWebRTCEndPoint.cs @@ -79,7 +79,7 @@ public interface IWebRTCEndPoint /// /// Duration of the frame in milliseconds. /// The Opus encoded audio payload. - void SendAudio(uint durationMilliseconds, byte[] encodedAudio); + void SendAudio(uint durationMilliseconds, ReadOnlyMemory encodedAudio); /// /// Sends a control message across the data channel. diff --git a/src/SIPSorcery.OpenAI.Realtime/WebRTC/WebRTCEndPoint.cs b/src/SIPSorcery.OpenAI.Realtime/WebRTC/WebRTCEndPoint.cs index 0408ef8b82..d0907f73e5 100644 --- a/src/SIPSorcery.OpenAI.Realtime/WebRTC/WebRTCEndPoint.cs +++ b/src/SIPSorcery.OpenAI.Realtime/WebRTC/WebRTCEndPoint.cs @@ -142,7 +142,7 @@ private RTCPeerConnection CreatePeerConnection(RTCConfiguration? pcConfig) { if (pc.signalingState == RTCSignalingState.have_local_offer) { - _logger.LogTrace($"Local SDP:\n{pc.localDescription.sdp}"); + _logger.LogTrace($"Local SDP:\n{pc.localDescription?.sdp}"); } else if (pc.signalingState is RTCSignalingState.have_remote_offer or RTCSignalingState.stable) { @@ -171,7 +171,7 @@ private RTCPeerConnection CreatePeerConnection(RTCConfiguration? pcConfig) return pc; } - public void SendAudio(uint durationRtpUnits, byte[] sample) + public void SendAudio(uint durationRtpUnits, ReadOnlyMemory sample) { PeerConnection.Match( pc => diff --git a/src/SIPSorcery.VP8/VP8Codec.cs b/src/SIPSorcery.VP8/VP8Codec.cs index db0e5c41a7..66a6a0af48 100644 --- a/src/SIPSorcery.VP8/VP8Codec.cs +++ b/src/SIPSorcery.VP8/VP8Codec.cs @@ -14,6 +14,7 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; diff --git a/src/SIPSorcery.VP8/Vp8NetVideoEncoderEndPoint.cs b/src/SIPSorcery.VP8/Vp8NetVideoEncoderEndPoint.cs index e99a06547c..bab2758d0c 100644 --- a/src/SIPSorcery.VP8/Vp8NetVideoEncoderEndPoint.cs +++ b/src/SIPSorcery.VP8/Vp8NetVideoEncoderEndPoint.cs @@ -14,6 +14,7 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; @@ -117,11 +118,12 @@ public void ExternalVideoSourceRawSample(uint durationMilliseconds, int width, i } } - public void GotVideoFrame(IPEndPoint remoteEndPoint, uint timestamp, byte[] frame, VideoFormat format) + public void GotVideoFrame(IPEndPoint remoteEndPoint, uint timestamp, ReadOnlyMemory frame, VideoFormat format) { if (!_isClosed) { - foreach (var decoded in _vp8Codec.DecodeVideo(frame, VideoPixelFormatsEnum.Bgr, VideoCodecsEnum.VP8)) + // TODO: use ReadOnlySequence without ToArray() to avoid unnecessary copying of the frame data. + foreach (var decoded in _vp8Codec.DecodeVideo(frame.ToArray(), VideoPixelFormatsEnum.Bgr, VideoCodecsEnum.VP8)) { OnVideoSinkDecodedSample(decoded.Sample, decoded.Width, decoded.Height, (int)(decoded.Width * 3), VideoPixelFormatsEnum.Bgr); } diff --git a/src/SIPSorcery/SIPSorcery.csproj b/src/SIPSorcery/SIPSorcery.csproj index b74d46e25c..38af451a5b 100644 --- a/src/SIPSorcery/SIPSorcery.csproj +++ b/src/SIPSorcery/SIPSorcery.csproj @@ -18,38 +18,79 @@ + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0;net9.0;net10.0 + true 14.0 + enable true $(NoWarn);SYSLIB0050 True @@ -74,7 +115,6 @@ https://github.com/sipsorcery-org/sipsorcery git master - true SIP WebRTC VoIP RTP SDP STUN ICE SIPSorcery -v10.0.8: Bug fixes. -v10.0.7: Network address change fix for Unity. @@ -107,6 +147,10 @@ 10.0.8 + + $(NoWarn);CS8600;CS8601;CS8602;CS8604 + + true diff --git a/src/SIPSorcery/app/Media/AudioSendOnlyMediaSession.cs b/src/SIPSorcery/app/Media/AudioSendOnlyMediaSession.cs index 4991d8f2ae..07a225ffb1 100644 --- a/src/SIPSorcery/app/Media/AudioSendOnlyMediaSession.cs +++ b/src/SIPSorcery/app/Media/AudioSendOnlyMediaSession.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +#nullable disable + +using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; diff --git a/src/SIPSorcery/app/Media/Codecs/AudioEncoder.cs b/src/SIPSorcery/app/Media/Codecs/AudioEncoder.cs index 9c7da8f6e2..cb7c46fd83 100644 --- a/src/SIPSorcery/app/Media/Codecs/AudioEncoder.cs +++ b/src/SIPSorcery/app/Media/Codecs/AudioEncoder.cs @@ -14,293 +14,335 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; +using System.Runtime.InteropServices; +using CommunityToolkit.HighPerformance.Buffers; using Concentus; using Concentus.Enums; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; -namespace SIPSorcery.Media +namespace SIPSorcery.Media; + +public class AudioEncoder : IAudioEncoder, IDisposable { - public class AudioEncoder : IAudioEncoder, IDisposable - { - private const int G722_BIT_RATE = 64000; // G722 sampling rate is 16KHz with bits per sample of 16. - private const int OPUS_SAMPLE_RATE = 48000; // Opus codec sampling rate, 48KHz. - private const int OPUS_CHANNELS = 2; // Opus codec number of channels. + private const int G722_BIT_RATE = 64000; // G722 sampling rate is 16KHz with bits per sample of 16. + private const int OPUS_SAMPLE_RATE = 48000; // Opus codec sampling rate, 48KHz. + private const int OPUS_CHANNELS = 2; // Opus codec number of channels. - /// - /// The max frame size that the OPUS encoder will accept is 2880 bytes (see IOpusEncoder.Encode). - /// 2880 corresponds to a sample size of 30ms for a single channel at 48Khz with 16 bit PCM. Therefore - /// the max sample size supported by OPUS is 30ms. - /// - private const int OPUS_MAXIMUM_INPUT_SAMPLES_PER_CHANNEL = 2880; + /// + /// The max frame size that the OPUS encoder will accept is 2880 bytes (see IOpusEncoder.Encode). + /// 2880 corresponds to a sample size of 30ms for a single channel at 48Khz with 16 bit PCM. Therefore + /// the max sample size supported by OPUS is 30ms. + /// + private const int OPUS_MAXIMUM_INPUT_SAMPLES_PER_CHANNEL = 2880; - /// - /// OPUS max encode size (see IOpusEncoder.Encode). - /// - private const int OPUS_MAXIMUM_ENCODED_FRAME_SIZE = 1275; + /// + /// OPUS max encode size (see IOpusEncoder.Encode). + /// + private const int OPUS_MAXIMUM_ENCODED_FRAME_SIZE = 1275; - private static readonly ILogger logger = LogFactory.CreateLogger(); + private static readonly ILogger logger = LogFactory.CreateLogger(); - private bool _disposedValue = false; + private bool _disposedValue; - private G722Codec _g722Codec; - private G722CodecState _g722CodecState; - private G722Codec _g722Decoder; - private G722CodecState _g722DecoderState; + private G722Codec? _g722Codec; + private G722CodecState? _g722CodecState; + private G722Codec? _g722Decoder; + private G722CodecState? _g722DecoderState; - private G729Encoder _g729Encoder; - private G729Decoder _g729Decoder; + private G729Encoder? _g729Encoder; + private G729Decoder? _g729Decoder; - private IOpusDecoder _opusDecoder; - private IOpusEncoder _opusEncoder; + private IOpusDecoder? _opusDecoder; + private IOpusEncoder? _opusEncoder; - private List _linearFormats = new List - { - new AudioFormat(AudioCodecsEnum.L16, 117, 16000), - new AudioFormat(AudioCodecsEnum.L16, 118, 8000), + private static readonly ReadOnlyMemory _linearFormats = new AudioFormat[] + { + new AudioFormat(AudioCodecsEnum.L16, 117, 16000), + new AudioFormat(AudioCodecsEnum.L16, 118, 8000), - // Not recommended due to very, very crude up-sampling in AudioEncoder class. PR's welcome :). - //new AudioFormat(121, "L16", "L16/48000", null), - }; + // Not recommended due to very, very crude up-sampling in AudioEncoder class. PR's welcome :). + //new AudioFormat(121, "L16", "L16/48000", null), + }; - private List _supportedFormats = new List - { - new AudioFormat(SDPWellKnownMediaFormatsEnum.PCMU), - new AudioFormat(SDPWellKnownMediaFormatsEnum.PCMA), - new AudioFormat(SDPWellKnownMediaFormatsEnum.G722), - new AudioFormat(SDPWellKnownMediaFormatsEnum.G729), + private List _supportedFormats = new List + { + new AudioFormat(SDPWellKnownMediaFormatsEnum.PCMU), + new AudioFormat(SDPWellKnownMediaFormatsEnum.PCMA), + new AudioFormat(SDPWellKnownMediaFormatsEnum.G722), + new AudioFormat(SDPWellKnownMediaFormatsEnum.G729), - // Need more testing before adding OPUS by default. 24 Dec 2024 AC. - //new AudioFormat(111, AudioCodecsEnum.OPUS.ToString(), OPUS_SAMPLE_RATE, OPUS_CHANNELS, "useinbandfec=1") - // AudioCommonlyUsedFormats.OpusWebRTC - }; + // Need more testing befoer adding OPUS by default. 24 Dec 2024 AC. + //new AudioFormat(111, nameof(AudioCodecsEnum.OPUS), OPUS_SAMPLE_RATE, OPUS_CHANNELS, "useinbandfec=1") + // AudioCommonlyUsedFormats.OpusWebRTC + }; - public List SupportedFormats - { - get => _supportedFormats; - } + public List SupportedFormats + { + get => _supportedFormats; + } - /// - /// Creates a new audio encoder instance. - /// - /// If set to true the linear audio formats will be added - /// to the list of supported formats. The reason they are only included if explicitly requested - /// is they are not very popular for other VoIP systems and therefore needlessly pollute the SDP. - public AudioEncoder(bool includeLinearFormats = false, bool includeOpus = false) + /// + /// Creates a new audio encoder instance. + /// + /// If set to true the linear audio formats will be added + /// to the list of supported formats. The reason they are only included if explicitly requested + /// is they are not very popular for other VoIP systems and therefore needlessly pollute the SDP. + public AudioEncoder(bool includeLinearFormats = false, bool includeOpus = false) + { + if (includeLinearFormats) { - if (includeLinearFormats) - { - _supportedFormats.AddRange(_linearFormats); - } - - if(includeOpus) - { - _supportedFormats.Add(AudioCommonlyUsedFormats.OpusWebRTC); - } + _supportedFormats.AddRange(_linearFormats.Span); } - public AudioEncoder(params AudioFormat[] supportedFormats) + if (includeOpus) { - _supportedFormats = supportedFormats.ToList(); + _supportedFormats.Add(AudioCommonlyUsedFormats.OpusWebRTC); } + } - public byte[] EncodeAudio(short[] pcm, AudioFormat format) + public AudioEncoder(params AudioFormat[] supportedFormats) + { + _supportedFormats = [.. supportedFormats]; + } + + public void EncodeAudio(ReadOnlySpan pcm, AudioFormat format, IBufferWriter destination) + { + switch (format.Codec) { - if (format.Codec == AudioCodecsEnum.G722) - { - if (_g722Codec == null) + case AudioCodecsEnum.G722: { - _g722Codec = new G722Codec(); - _g722CodecState = new G722CodecState(G722_BIT_RATE, G722Flags.None); + if (_g722Codec is null) + { + _g722Codec = new G722Codec(); + _g722CodecState = new G722CodecState(G722_BIT_RATE, G722Flags.None); + } + Debug.Assert(_g722CodecState is { }); + var outputBufferSize = pcm.Length / 2; + var encodedSpan = destination.GetSpan(outputBufferSize); + var res = _g722Codec.Encode(_g722CodecState, encodedSpan, pcm); + destination.Advance(res); } - - int outputBufferSize = pcm.Length / 2; - byte[] encodedSample = new byte[outputBufferSize]; - int res = _g722Codec.Encode(_g722CodecState, encodedSample, pcm, pcm.Length); - - return encodedSample; - } - else if (format.Codec == AudioCodecsEnum.G729) - { - if (_g729Encoder == null) + break; + case AudioCodecsEnum.G729: { - _g729Encoder = new G729Encoder(); + if (_g729Encoder is null) + { + _g729Encoder = new G729Encoder(); + } + Debug.Assert(_g729Encoder is { }); + var speech = MemoryMarshal.AsBytes(pcm); + _g729Encoder.Process(speech, destination); } - - byte[] pcmBytes = new byte[pcm.Length * sizeof(short)]; - Buffer.BlockCopy(pcm, 0, pcmBytes, 0, pcmBytes.Length); - return _g729Encoder.Process(pcmBytes); - } - else if (format.Codec == AudioCodecsEnum.PCMA) - { - return pcm.Select(x => ALawEncoder.LinearToALawSample(x)).ToArray(); - } - else if (format.Codec == AudioCodecsEnum.PCMU) - { - return pcm.Select(x => MuLawEncoder.LinearToMuLawSample(x)).ToArray(); - } - else if (format.Codec == AudioCodecsEnum.L16) - { - // When netstandard2.1 can be used. - //return MemoryMarshal.Cast(pcm) - - // Put on the wire in network byte order (big endian). - return pcm.SelectMany(x => new byte[] { (byte)(x >> 8), (byte)(x) }).ToArray(); - } - else if (format.Codec == AudioCodecsEnum.PCM_S16LE) - { - // Put on the wire as little endian. - return pcm.SelectMany(x => new byte[] { (byte)(x), (byte)(x >> 8) }).ToArray(); - } - else if (format.Codec == AudioCodecsEnum.OPUS) - { - if (_opusEncoder == null) + break; + case AudioCodecsEnum.PCMA: { - var channelCount = format.ChannelCount > 0 ? format.ChannelCount : OPUS_CHANNELS; - _opusEncoder = OpusCodecFactory.CreateEncoder(format.ClockRate, channelCount, OpusApplication.OPUS_APPLICATION_VOIP); + var encoded = destination.GetSpan(pcm.Length); + for (var i = 0; i < pcm.Length; i++) + { + encoded[i] = ALawEncoder.LinearToALawSample(pcm[i]); + } + destination.Advance(encoded.Length); } - - if (pcm.Length > _opusEncoder.NumChannels * OPUS_MAXIMUM_INPUT_SAMPLES_PER_CHANNEL) + break; + case AudioCodecsEnum.PCMU: { - logger.LogWarning("{audioEncoder} input sample of length {inputSize} supplied to OPUS encoder exceeded maximum limit of {maxLimit}. Reduce sampling period.", nameof(AudioEncoder), pcm.Length, _opusEncoder.NumChannels * OPUS_MAXIMUM_INPUT_SAMPLES_PER_CHANNEL); - return []; + var encoded = destination.GetSpan(pcm.Length); + for (var i = 0; i < pcm.Length; i++) + { + encoded[i] = MuLawEncoder.LinearToMuLawSample(pcm[i]); + } + destination.Advance(encoded.Length); } - else + break; + case AudioCodecsEnum.L16: { - Span encodedSample = stackalloc byte[OPUS_MAXIMUM_ENCODED_FRAME_SIZE]; - int encodedLength = _opusEncoder.Encode(pcm, pcm.Length / _opusEncoder.NumChannels, encodedSample, encodedSample.Length); - return encodedSample.Slice(0, encodedLength).ToArray(); + // Put on the wire in network byte order (big endian). + var encoded = MemoryMarshal.Cast(pcm); + encoded.CopyTo(destination.GetSpan(encoded.Length)); + destination.Advance(encoded.Length); } - } - else - { - throw new ApplicationException($"Audio format {format.Codec} cannot be encoded."); - } + break; + case AudioCodecsEnum.PCM_S16LE: + { + // Put on the wire as little endian. + var length = pcm.Length / 2; + var encoded = destination.GetSpan(length); + MemoryOperations.ToLittleEndianBytes(pcm, encoded); + destination.Advance(length); + } + break; + case AudioCodecsEnum.OPUS: + { + if (_opusEncoder is null) + { + var channelCount = format.ChannelCount > 0 ? format.ChannelCount : OPUS_CHANNELS; + _opusEncoder = OpusCodecFactory.CreateEncoder(format.ClockRate, channelCount, OpusApplication.OPUS_APPLICATION_VOIP); + } + + Debug.Assert(_opusEncoder is { }); + + if (pcm.Length > _opusEncoder.NumChannels * OPUS_MAXIMUM_INPUT_SAMPLES_PER_CHANNEL) + { + logger.LogSettingAudioFormatWarning(nameof(AudioEncoder), pcm.Length, _opusEncoder.NumChannels * OPUS_MAXIMUM_INPUT_SAMPLES_PER_CHANNEL); + } + else + { + var encodedSample = destination.GetSpan(OPUS_MAXIMUM_ENCODED_FRAME_SIZE); + var encodedLength = _opusEncoder.Encode(pcm, pcm.Length / _opusEncoder.NumChannels, encodedSample, encodedSample.Length); + destination.Advance(encodedLength); + } + } + break; + default: + throw new SipSorceryException($"Audio format {format.Codec} cannot be encoded."); } + } - /// - /// Event handler for receiving RTP packets from the remote party. - /// - /// Data received from an RTP socket. - /// The audio format of the encoded packets. - public short[] DecodeAudio(byte[] encodedSample, AudioFormat format) + /// + /// Decodes to 16bit signed PCM samples. + /// + /// The span containing the encoded sample. + /// The audio format of the encoded sample. + /// A of to receive the decoded PCM samples. + public void DecodeAudio(ReadOnlySpan encodedSample, AudioFormat format, IBufferWriter destination) + { + switch (format.Codec) { - if (format.Codec == AudioCodecsEnum.G722) - { - if (_g722Decoder == null) + case AudioCodecsEnum.G722: { - _g722Decoder = new G722Codec(); - _g722DecoderState = new G722CodecState(G722_BIT_RATE, G722Flags.None); - } + if (_g722Decoder is null) + { + _g722Decoder = new G722Codec(); + _g722DecoderState = new G722CodecState(G722_BIT_RATE, G722Flags.None); + } - short[] decodedPcm = new short[encodedSample.Length * 2]; - int decodedSampleCount = _g722Decoder.Decode(_g722DecoderState, decodedPcm, encodedSample, encodedSample.Length); + Debug.Assert(_g722DecoderState is { }); - return decodedPcm.Take(decodedSampleCount).ToArray(); - } - if (format.Codec == AudioCodecsEnum.G729) - { - if (_g729Decoder == null) - { - _g729Decoder = new G729Decoder(); + // Use the new IBufferWriter-based decode method directly + _g722Decoder.Decode(_g722DecoderState, destination, encodedSample); } - byte[] decodedBytes = _g729Decoder.Process(encodedSample); - short[] decodedPcm = new short[decodedBytes.Length / sizeof(short)]; - Buffer.BlockCopy(decodedBytes, 0, decodedPcm, 0, decodedBytes.Length); - return decodedPcm; - } - else if (format.Codec == AudioCodecsEnum.PCMA) - { - return encodedSample.Select(x => ALawDecoder.ALawToLinearSample(x)).ToArray(); - } - else if (format.Codec == AudioCodecsEnum.PCMU) - { - return encodedSample.Select(x => MuLawDecoder.MuLawToLinearSample(x)).ToArray(); - } - else if (format.Codec == AudioCodecsEnum.L16) - { - // Samples are on the wire as big endian. - return encodedSample.Where((x, i) => i % 2 == 0).Select((y, i) => (short)(encodedSample[i * 2] << 8 | encodedSample[i * 2 + 1])).ToArray(); - } - else if (format.Codec == AudioCodecsEnum.PCM_S16LE) - { - // Samples are on the wire as little endian (well unlikely to be on the wire in this case but when they - // arrive from somewhere like the SkypeBot SDK they will be in little endian format). - return encodedSample.Where((x, i) => i % 2 == 0).Select((y, i) => (short)(encodedSample[i * 2 + 1] << 8 | encodedSample[i * 2])).ToArray(); - } - else if (format.Codec == AudioCodecsEnum.OPUS) - { - if (_opusDecoder == null) + break; + case AudioCodecsEnum.G729: { - var channelCount = format.ChannelCount > 0 ? format.ChannelCount : OPUS_CHANNELS; - _opusDecoder = OpusCodecFactory.CreateDecoder(format.ClockRate, channelCount); + if (_g729Decoder is null) + { + _g729Decoder = new G729Decoder(); + } + + // Use the new span-based decode method directly + using var buffer = new ArrayPoolBufferWriter(8192); + _g729Decoder.Process(encodedSample, buffer); + destination.Write(MemoryMarshal.Cast(buffer.WrittenSpan)); } - int maxSamples = OPUS_MAXIMUM_INPUT_SAMPLES_PER_CHANNEL * _opusDecoder.NumChannels; - float[] floatBuf = new float[maxSamples]; + break; + case AudioCodecsEnum.PCMA: + { + var outputSpan = destination.GetSpan(encodedSample.Length); + for (var i = 0; i < encodedSample.Length; i++) + { + outputSpan[i] = ALawDecoder.ALawToLinearSample(encodedSample[i]); + } + destination.Advance(encodedSample.Length); + } - // Decode returns the number of samples per channel. - int samplesPerChannel = _opusDecoder.Decode( - encodedSample, - floatBuf, - floatBuf.Length, - false); + break; + case AudioCodecsEnum.PCMU: + { + var outputSpan = destination.GetSpan(encodedSample.Length); + for (var i = 0; i < encodedSample.Length; i++) + { + outputSpan[i] = MuLawDecoder.MuLawToLinearSample(encodedSample[i]); + } + destination.Advance(encodedSample.Length); + } - int totalFloats = samplesPerChannel * _opusDecoder.NumChannels; + break; + case AudioCodecsEnum.L16: + { + // Samples are on the wire as big endian. + var sampleCount = encodedSample.Length / 2; + var outputSpan = destination.GetSpan(sampleCount); + for (var i = 0; i < sampleCount; i++) + { + var byteIndex = i * 2; + outputSpan[i] = (short)(encodedSample[byteIndex] << 8 | encodedSample[byteIndex + 1]); + } + destination.Advance(sampleCount); + } - // Convert to 16-bit interleaved PCM. - short[] pcm16 = new short[totalFloats]; - for (int i = 0; i < totalFloats; i++) + break; + case AudioCodecsEnum.PCM_S16LE: { - var f = ClampToFloat(floatBuf[i], -1.0f, 1.0f); - pcm16[i] = (short)(f * 32767); + // Samples are on the wire as little endian. + var sampleCount = encodedSample.Length / 2; + var outputSpan = destination.GetSpan(sampleCount); + for (var i = 0; i < sampleCount; i++) + { + var byteIndex = i * 2; + outputSpan[i] = (short)(encodedSample[byteIndex + 1] << 8 | encodedSample[byteIndex]); + } + destination.Advance(sampleCount); } - return pcm16; - } - else - { - throw new ApplicationException($"Audio format {format.Codec} cannot be decoded."); - } - } + break; + case AudioCodecsEnum.OPUS: + { + if (_opusDecoder is null) + { + var channelCount = format.ChannelCount > 0 ? format.ChannelCount : OPUS_CHANNELS; + _opusDecoder = OpusCodecFactory.CreateDecoder(format.ClockRate, channelCount); + } - [Obsolete("No longer used. Use SIPSorcery.Media.PcmResampler.Resample instead.")] - public short[] Resample(short[] pcm, int inRate, int outRate) - { - return PcmResampler.Resample(pcm, inRate, outRate); - } + var maxSamples = OPUS_MAXIMUM_INPUT_SAMPLES_PER_CHANNEL * _opusDecoder.NumChannels; - private float ClampToFloat(float value, float min, float max) - { - if (value < min) { return min; } - if (value > max) { return max; } - return value; - } + var outputSpan = destination.GetSpan(maxSamples); - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - (_opusEncoder as IDisposable)?.Dispose(); - (_opusDecoder as IDisposable)?.Dispose(); - (_g729Encoder as IDisposable)?.Dispose(); - (_g729Decoder as IDisposable)?.Dispose(); + var samplesPerChannel = _opusDecoder.Decode( + encodedSample, + outputSpan, + maxSamples, + false); + + var totalSamples = samplesPerChannel * _opusDecoder.NumChannels; + + if (totalSamples > 0) + { + destination.Advance(totalSamples); + } } - _disposedValue = true; - } + break; + default: + throw new SipSorceryException($"Audio format {format.Codec} cannot be decoded."); } + } - public void Dispose() + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - Dispose(disposing: true); - GC.SuppressFinalize(this); + if (disposing) + { + (_opusEncoder as IDisposable)?.Dispose(); + (_opusDecoder as IDisposable)?.Dispose(); + (_g729Encoder as IDisposable)?.Dispose(); + (_g729Decoder as IDisposable)?.Dispose(); + } + + _disposedValue = true; } } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } diff --git a/src/SIPSorcery/app/Media/Codecs/G711Codecs.cs b/src/SIPSorcery/app/Media/Codecs/G711Codecs.cs index 1583356018..76ade2d8c1 100644 --- a/src/SIPSorcery/app/Media/Codecs/G711Codecs.cs +++ b/src/SIPSorcery/app/Media/Codecs/G711Codecs.cs @@ -16,237 +16,236 @@ // MS_PL Microsoft Public License. //----------------------------------------------------------------------------- -namespace SIPSorcery.Media +namespace SIPSorcery.Media; + +/// +/// mu-law encoder +/// based on code from: +/// http://hazelware.luggle.com/tutorials/mulawcompression.html +/// +public static class MuLawEncoder { + private const int cBias = 0x84; + private const int cClip = 32635; + + private static readonly byte[] MuLawCompressTable = new byte[256] + { + 0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3, + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7 + }; + /// - /// mu-law encoder - /// based on code from: - /// http://hazelware.luggle.com/tutorials/mulawcompression.html + /// Encodes a single 16 bit sample to mu-law /// - public static class MuLawEncoder + /// 16 bit PCM sample + /// mu-law encoded byte + public static byte LinearToMuLawSample(short sample) { - private const int cBias = 0x84; - private const int cClip = 32635; - - private static readonly byte[] MuLawCompressTable = new byte[256] + int sign = (sample >> 8) & 0x80; + if (sign != 0) { - 0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3, - 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, - 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, - 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, - 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, - 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, - 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, - 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7 - }; - - /// - /// Encodes a single 16 bit sample to mu-law - /// - /// 16 bit PCM sample - /// mu-law encoded byte - public static byte LinearToMuLawSample(short sample) + sample = (short)-sample; + } + if (sample > cClip) { - int sign = (sample >> 8) & 0x80; - if (sign != 0) - { - sample = (short)-sample; - } - if (sample > cClip) - { - sample = cClip; - } - sample = (short)(sample + cBias); - int exponent = (int)MuLawCompressTable[(sample >> 7) & 0xFF]; - int mantissa = (sample >> (exponent + 3)) & 0x0F; - int compressedByte = ~(sign | (exponent << 4) | mantissa); - - return (byte)compressedByte; + sample = cClip; } + sample = (short)(sample + cBias); + int exponent = (int)MuLawCompressTable[(sample >> 7) & 0xFF]; + int mantissa = (sample >> (exponent + 3)) & 0x0F; + int compressedByte = ~(sign | (exponent << 4) | mantissa); + + return (byte)compressedByte; } +} + +/// +/// A-law encoder +/// +public static class ALawEncoder +{ + private const int cBias = 0x84; + private const int cClip = 32635; + private static readonly byte[] ALawCompressTable = new byte[128] + { + 1,1,2,2,3,3,3,3, + 4,4,4,4,4,4,4,4, + 5,5,5,5,5,5,5,5, + 5,5,5,5,5,5,5,5, + 6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6, + 6,6,6,6,6,6,6,6, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7, + 7,7,7,7,7,7,7,7 + }; /// - /// A-law encoder + /// Encodes a single 16 bit sample to a-law /// - public static class ALawEncoder + /// 16 bit PCM sample + /// a-law encoded byte + public static byte LinearToALawSample(short sample) { - private const int cBias = 0x84; - private const int cClip = 32635; - private static readonly byte[] ALawCompressTable = new byte[128] - { - 1,1,2,2,3,3,3,3, - 4,4,4,4,4,4,4,4, - 5,5,5,5,5,5,5,5, - 5,5,5,5,5,5,5,5, - 6,6,6,6,6,6,6,6, - 6,6,6,6,6,6,6,6, - 6,6,6,6,6,6,6,6, - 6,6,6,6,6,6,6,6, - 7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7, - 7,7,7,7,7,7,7,7 - }; + int sign; + int exponent; + int mantissa; + byte compressedByte; - /// - /// Encodes a single 16 bit sample to a-law - /// - /// 16 bit PCM sample - /// a-law encoded byte - public static byte LinearToALawSample(short sample) + sign = ((~sample) >> 8) & 0x80; + if (sign == 0) { - int sign; - int exponent; - int mantissa; - byte compressedByte; - - sign = ((~sample) >> 8) & 0x80; - if (sign == 0) - { - sample = (short)-sample; - } - if (sample > cClip) - { - sample = cClip; - } - if (sample >= 256) - { - exponent = (int)ALawCompressTable[(sample >> 8) & 0x7F]; - mantissa = (sample >> (exponent + 3)) & 0x0F; - compressedByte = (byte)((exponent << 4) | mantissa); - } - else - { - compressedByte = (byte)(sample >> 4); - } - compressedByte ^= (byte)(sign ^ 0x55); - return compressedByte; + sample = (short)-sample; + } + if (sample > cClip) + { + sample = cClip; + } + if (sample >= 256) + { + exponent = (int)ALawCompressTable[(sample >> 8) & 0x7F]; + mantissa = (sample >> (exponent + 3)) & 0x0F; + compressedByte = (byte)((exponent << 4) | mantissa); } + else + { + compressedByte = (byte)(sample >> 4); + } + compressedByte ^= (byte)(sign ^ 0x55); + return compressedByte; } +} +/// +/// a-law decoder +/// based on code from: +/// http://hazelware.luggle.com/tutorials/mulawcompression.html +/// +public static class ALawDecoder +{ /// - /// a-law decoder - /// based on code from: - /// http://hazelware.luggle.com/tutorials/mulawcompression.html + /// only 512 bytes required, so just use a lookup /// - public class ALawDecoder + private static readonly short[] ALawDecompressTable = new short[256] { - /// - /// only 512 bytes required, so just use a lookup - /// - private static readonly short[] ALawDecompressTable = new short[256] - { - -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, - -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, - -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368, - -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, - -22016,-20992,-24064,-23040,-17920,-16896,-19968,-18944, - -30208,-29184,-32256,-31232,-26112,-25088,-28160,-27136, - -11008,-10496,-12032,-11520,-8960, -8448, -9984, -9472, - -15104,-14592,-16128,-15616,-13056,-12544,-14080,-13568, - -344, -328, -376, -360, -280, -264, -312, -296, - -472, -456, -504, -488, -408, -392, -440, -424, - -88, -72, -120, -104, -24, -8, -56, -40, - -216, -200, -248, -232, -152, -136, -184, -168, - -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184, - -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, - -688, -656, -752, -720, -560, -528, -624, -592, - -944, -912, -1008, -976, -816, -784, -880, -848, - 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, - 7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784, - 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, - 3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392, - 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, - 30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136, - 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, - 15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568, - 344, 328, 376, 360, 280, 264, 312, 296, - 472, 456, 504, 488, 408, 392, 440, 424, - 88, 72, 120, 104, 24, 8, 56, 40, - 216, 200, 248, 232, 152, 136, 184, 168, - 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, - 1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696, - 688, 656, 752, 720, 560, 528, 624, 592, - 944, 912, 1008, 976, 816, 784, 880, 848 - }; + -5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, + -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, + -2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368, + -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, + -22016,-20992,-24064,-23040,-17920,-16896,-19968,-18944, + -30208,-29184,-32256,-31232,-26112,-25088,-28160,-27136, + -11008,-10496,-12032,-11520,-8960, -8448, -9984, -9472, + -15104,-14592,-16128,-15616,-13056,-12544,-14080,-13568, + -344, -328, -376, -360, -280, -264, -312, -296, + -472, -456, -504, -488, -408, -392, -440, -424, + -88, -72, -120, -104, -24, -8, -56, -40, + -216, -200, -248, -232, -152, -136, -184, -168, + -1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184, + -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, + -688, -656, -752, -720, -560, -528, -624, -592, + -944, -912, -1008, -976, -816, -784, -880, -848, + 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, + 7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784, + 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, + 3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392, + 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, + 30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136, + 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, + 15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568, + 344, 328, 376, 360, 280, 264, 312, 296, + 472, 456, 504, 488, 408, 392, 440, 424, + 88, 72, 120, 104, 24, 8, 56, 40, + 216, 200, 248, 232, 152, 136, 184, 168, + 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, + 1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696, + 688, 656, 752, 720, 560, 528, 624, 592, + 944, 912, 1008, 976, 816, 784, 880, 848 + }; - /// - /// Converts an a-law encoded byte to a 16 bit linear sample - /// - /// a-law encoded byte - /// Linear sample - public static short ALawToLinearSample(byte aLaw) - { - return ALawDecompressTable[aLaw]; - } + /// + /// Converts an a-law encoded byte to a 16 bit linear sample + /// + /// a-law encoded byte + /// Linear sample + public static short ALawToLinearSample(byte aLaw) + { + return ALawDecompressTable[aLaw]; } +} +/// +/// mu-law decoder +/// based on code from: +/// http://hazelware.luggle.com/tutorials/mulawcompression.html +/// +public static class MuLawDecoder +{ /// - /// mu-law decoder - /// based on code from: - /// http://hazelware.luggle.com/tutorials/mulawcompression.html + /// only 512 bytes required, so just use a lookup /// - public static class MuLawDecoder + private static readonly short[] MuLawDecompressTable = new short[256] { - /// - /// only 512 bytes required, so just use a lookup - /// - private static readonly short[] MuLawDecompressTable = new short[256] - { - -32124,-31100,-30076,-29052,-28028,-27004,-25980,-24956, - -23932,-22908,-21884,-20860,-19836,-18812,-17788,-16764, - -15996,-15484,-14972,-14460,-13948,-13436,-12924,-12412, - -11900,-11388,-10876,-10364, -9852, -9340, -8828, -8316, - -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, - -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, - -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, - -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, - -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, - -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, - -876, -844, -812, -780, -748, -716, -684, -652, - -620, -588, -556, -524, -492, -460, -428, -396, - -372, -356, -340, -324, -308, -292, -276, -260, - -244, -228, -212, -196, -180, -164, -148, -132, - -120, -112, -104, -96, -88, -80, -72, -64, - -56, -48, -40, -32, -24, -16, -8, -1, - 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, - 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, - 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, - 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, - 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, - 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, - 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, - 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, - 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, - 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, - 876, 844, 812, 780, 748, 716, 684, 652, - 620, 588, 556, 524, 492, 460, 428, 396, - 372, 356, 340, 324, 308, 292, 276, 260, - 244, 228, 212, 196, 180, 164, 148, 132, - 120, 112, 104, 96, 88, 80, 72, 64, - 56, 48, 40, 32, 24, 16, 8, 0 - }; + -32124,-31100,-30076,-29052,-28028,-27004,-25980,-24956, + -23932,-22908,-21884,-20860,-19836,-18812,-17788,-16764, + -15996,-15484,-14972,-14460,-13948,-13436,-12924,-12412, + -11900,-11388,-10876,-10364, -9852, -9340, -8828, -8316, + -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, + -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, + -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, + -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, + -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, + -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, + -876, -844, -812, -780, -748, -716, -684, -652, + -620, -588, -556, -524, -492, -460, -428, -396, + -372, -356, -340, -324, -308, -292, -276, -260, + -244, -228, -212, -196, -180, -164, -148, -132, + -120, -112, -104, -96, -88, -80, -72, -64, + -56, -48, -40, -32, -24, -16, -8, -1, + 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, + 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, + 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, + 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, + 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, + 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, + 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, + 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, + 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, + 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, + 876, 844, 812, 780, 748, 716, 684, 652, + 620, 588, 556, 524, 492, 460, 428, 396, + 372, 356, 340, 324, 308, 292, 276, 260, + 244, 228, 212, 196, 180, 164, 148, 132, + 120, 112, 104, 96, 88, 80, 72, 64, + 56, 48, 40, 32, 24, 16, 8, 0 + }; - /// - /// Converts a mu-law encoded byte to a 16 bit linear sample - /// - /// mu-law encoded byte - /// Linear sample - public static short MuLawToLinearSample(byte muLaw) - { - return MuLawDecompressTable[muLaw]; - } + /// + /// Converts a mu-law encoded byte to a 16 bit linear sample + /// + /// mu-law encoded byte + /// Linear sample + public static short MuLawToLinearSample(byte muLaw) + { + return MuLawDecompressTable[muLaw]; } } diff --git a/src/SIPSorcery/app/Media/Codecs/G722Codec.cs b/src/SIPSorcery/app/Media/Codecs/G722Codec.cs index 9b98e1412b..c39a044988 100644 --- a/src/SIPSorcery/app/Media/Codecs/G722Codec.cs +++ b/src/SIPSorcery/app/Media/Codecs/G722Codec.cs @@ -1,703 +1,776 @@ using System; - -namespace SIPSorcery.Media +using System.Buffers; + +namespace SIPSorcery.Media; + +/// +/// SpanDSP - a series of DSP components for telephony +/// +/// g722_decode.c - The ITU G.722 codec, decode part. +/// +/// Written by Steve Underwood <steveu@coppice.org> +/// +/// Copyright (C) 2005 Steve Underwood +/// Ported to C# by Mark Heath 2011 +/// +/// Despite my general liking of the GPL, I place my own contributions +/// to this code in the public domain for the benefit of all mankind - +/// even the slimy ones who might try to proprietize my work and use it +/// to my detriment. +/// +/// Based in part on a single channel G.722 codec which is: +/// Copyright (c) CMU 1993 +/// Computer Science, Speech Group +/// Chengxiang Lu and Alex Hauptmann +/// +public class G722Codec { /// - /// SpanDSP - a series of DSP components for telephony - /// - /// g722_decode.c - The ITU G.722 codec, decode part. - /// - /// Written by Steve Underwood <steveu@coppice.org> - /// - /// Copyright (C) 2005 Steve Underwood - /// Ported to C# by Mark Heath 2011 - /// - /// Despite my general liking of the GPL, I place my own contributions - /// to this code in the public domain for the benefit of all mankind - - /// even the slimy ones who might try to proprietize my work and use it - /// to my detriment. - /// - /// Based in part on a single channel G.722 codec which is: - /// Copyright (c) CMU 1993 - /// Computer Science, Speech Group - /// Chengxiang Lu and Alex Hauptmann + /// hard limits to 16 bit samples /// - public class G722Codec + private static short Saturate(int amp) { - /// - /// hard limits to 16 bit samples - /// - static short Saturate(int amp) + short amp16; + + // Hopefully this is optimised for the common case - not clipping + amp16 = (short)amp; + if (amp == amp16) { - short amp16; + return amp16; + } - // Hopefully this is optimised for the common case - not clipping - amp16 = (short)amp; - if (amp == amp16) - { - return amp16; - } + if (amp > short.MaxValue) + { + return short.MaxValue; + } - if (amp > Int16.MaxValue) - { - return Int16.MaxValue; - } + return short.MinValue; + } + + private static void Block4(G722CodecState s, int band, int d) + { + int wd1; + int wd2; + int wd3; + int i; - return Int16.MinValue; + // Block 4, RECONS + s.Band[band].d[0] = d; + s.Band[band].r[0] = Saturate(s.Band[band].s + d); + + // Block 4, PARREC + s.Band[band].p[0] = Saturate(s.Band[band].sz + d); + + // Block 4, UPPOL2 + for (i = 0; i < 3; i++) + { + s.Band[band].sg[i] = s.Band[band].p[i] >> 15; } + wd1 = Saturate(s.Band[band].a[1] << 2); - static void Block4(G722CodecState s, int band, int d) + wd2 = (s.Band[band].sg[0] == s.Band[band].sg[1]) ? -wd1 : wd1; + if (wd2 > 32767) { - int wd1; - int wd2; - int wd3; - int i; + wd2 = 32767; + } - // Block 4, RECONS - s.Band[band].d[0] = d; - s.Band[band].r[0] = Saturate(s.Band[band].s + d); + wd3 = (s.Band[band].sg[0] == s.Band[band].sg[2]) ? 128 : -128; + wd3 += (wd2 >> 7); + wd3 += (s.Band[band].a[2] * 32512) >> 15; + if (wd3 > 12288) + { + wd3 = 12288; + } + else if (wd3 < -12288) + { + wd3 = -12288; + } - // Block 4, PARREC - s.Band[band].p[0] = Saturate(s.Band[band].sz + d); + s.Band[band].ap[2] = wd3; - // Block 4, UPPOL2 - for (i = 0; i < 3; i++) - { - s.Band[band].sg[i] = s.Band[band].p[i] >> 15; - } - wd1 = Saturate(s.Band[band].a[1] << 2); + // Block 4, UPPOL1 + s.Band[band].sg[0] = s.Band[band].p[0] >> 15; + s.Band[band].sg[1] = s.Band[band].p[1] >> 15; + wd1 = (s.Band[band].sg[0] == s.Band[band].sg[1]) ? 192 : -192; + wd2 = (s.Band[band].a[1] * 32640) >> 15; - wd2 = (s.Band[band].sg[0] == s.Band[band].sg[1]) ? -wd1 : wd1; - if (wd2 > 32767) - { - wd2 = 32767; - } + s.Band[band].ap[1] = Saturate(wd1 + wd2); + wd3 = Saturate(15360 - s.Band[band].ap[2]); + if (s.Band[band].ap[1] > wd3) + { + s.Band[band].ap[1] = wd3; + } + else if (s.Band[band].ap[1] < -wd3) + { + s.Band[band].ap[1] = -wd3; + } - wd3 = (s.Band[band].sg[0] == s.Band[band].sg[2]) ? 128 : -128; - wd3 += (wd2 >> 7); - wd3 += (s.Band[band].a[2] * 32512) >> 15; - if (wd3 > 12288) - { - wd3 = 12288; - } - else if (wd3 < -12288) - { - wd3 = -12288; - } + // Block 4, UPZERO + wd1 = (d == 0) ? 0 : 128; + s.Band[band].sg[0] = d >> 15; + for (i = 1; i < 7; i++) + { + s.Band[band].sg[i] = s.Band[band].d[i] >> 15; + wd2 = (s.Band[band].sg[i] == s.Band[band].sg[0]) ? wd1 : -wd1; + wd3 = (s.Band[band].b[i] * 32640) >> 15; + s.Band[band].bp[i] = Saturate(wd2 + wd3); + } - s.Band[band].ap[2] = wd3; + // Block 4, DELAYA + for (i = 6; i > 0; i--) + { + s.Band[band].d[i] = s.Band[band].d[i - 1]; + s.Band[band].b[i] = s.Band[band].bp[i]; + } + + for (i = 2; i > 0; i--) + { + s.Band[band].r[i] = s.Band[band].r[i - 1]; + s.Band[band].p[i] = s.Band[band].p[i - 1]; + s.Band[band].a[i] = s.Band[band].ap[i]; + } + + // Block 4, FILTEP + wd1 = Saturate(s.Band[band].r[1] + s.Band[band].r[1]); + wd1 = (s.Band[band].a[1] * wd1) >> 15; + wd2 = Saturate(s.Band[band].r[2] + s.Band[band].r[2]); + wd2 = (s.Band[band].a[2] * wd2) >> 15; + s.Band[band].sp = Saturate(wd1 + wd2); + + // Block 4, FILTEZ + s.Band[band].sz = 0; + for (i = 6; i > 0; i--) + { + wd1 = Saturate(s.Band[band].d[i] + s.Band[band].d[i]); + s.Band[band].sz += (s.Band[band].b[i] * wd1) >> 15; + } + s.Band[band].sz = Saturate(s.Band[band].sz); + + // Block 4, PREDIC + s.Band[band].s = Saturate(s.Band[band].sp + s.Band[band].sz); + } - // Block 4, UPPOL1 - s.Band[band].sg[0] = s.Band[band].p[0] >> 15; - s.Band[band].sg[1] = s.Band[band].p[1] >> 15; - wd1 = (s.Band[band].sg[0] == s.Band[band].sg[1]) ? 192 : -192; - wd2 = (s.Band[band].a[1] * 32640) >> 15; + private static readonly int[] wl = { -60, -30, 58, 172, 334, 538, 1198, 3042 }; + private static readonly int[] rl42 = { 0, 7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1, 0 }; + private static readonly int[] ilb = { 2048, 2093, 2139, 2186, 2233, 2282, 2332, 2383, 2435, 2489, 2543, 2599, 2656, 2714, 2774, 2834, 2896, 2960, 3025, 3091, 3158, 3228, 3298, 3371, 3444, 3520, 3597, 3676, 3756, 3838, 3922, 4008 }; + private static readonly int[] wh = { 0, -214, 798 }; + private static readonly int[] rh2 = { 2, 1, 2, 1 }; + private static readonly int[] qm2 = { -7408, -1616, 7408, 1616 }; + private static readonly int[] qm4 = { 0, -20456, -12896, -8968, -6288, -4240, -2584, -1200, 20456, 12896, 8968, 6288, 4240, 2584, 1200, 0 }; + private static readonly int[] qm5 = { -280, -280, -23352, -17560, -14120, -11664, -9752, -8184, -6864, -5712, -4696, -3784, -2960, -2208, -1520, -880, 23352, 17560, 14120, 11664, 9752, 8184, 6864, 5712, 4696, 3784, 2960, 2208, 1520, 880, 280, -280 }; + private static readonly int[] qm6 = { -136, -136, -136, -136, -24808, -21904, -19008, -16704, -14984, -13512, -12280, -11192, -10232, -9360, -8576, -7856, -7192, -6576, -6000, -5456, -4944, -4464, -4008, -3576, -3168, -2776, -2400, -2032, -1688, -1360, -1040, -728, 24808, 21904, 19008, 16704, 14984, 13512, 12280, 11192, 10232, 9360, 8576, 7856, 7192, 6576, 6000, 5456, 4944, 4464, 4008, 3576, 3168, 2776, 2400, 2032, 1688, 1360, 1040, 728, 432, 136, -432, -136 }; + private static readonly int[] qmf_coeffs = { 3, -11, 12, 32, -210, 951, 3876, -805, 362, -156, 53, -11, }; + private static readonly int[] q6 = { 0, 35, 72, 110, 150, 190, 233, 276, 323, 370, 422, 473, 530, 587, 650, 714, 786, 858, 940, 1023, 1121, 1219, 1339, 1458, 1612, 1765, 1980, 2195, 2557, 2919, 0, 0 }; + private static readonly int[] iln = { 0, 63, 62, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 0 }; + private static readonly int[] ilp = { 0, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 0 }; + private static readonly int[] ihn = { 0, 1, 0 }; + private static readonly int[] ihp = { 0, 3, 2 }; - s.Band[band].ap[1] = Saturate(wd1 + wd2); - wd3 = Saturate(15360 - s.Band[band].ap[2]); - if (s.Band[band].ap[1] > wd3) + /// + /// Decodes a buffer of G722 using IBufferWriter for efficient memory management + /// + /// Codec state + /// IBufferWriter to receive decoded PCM samples + /// Input G722 data span + /// Number of samples written into output buffer + public int Decode(G722CodecState state, IBufferWriter destination, ReadOnlySpan inputG722Data) + { + int dlowt; + int rlow; + int ihigh; + int dhigh; + int rhigh; + int xout1; + int xout2; + int wd1; + int wd2; + int wd3; + int code; + int outlen; + int i; + int j; + + outlen = 0; + rhigh = 0; + + // Estimate output size - G722 typically expands by factor of 2 + var estimatedOutputSize = inputG722Data.Length * 2; + var outputSpan = destination.GetSpan(estimatedOutputSize); + var currentSpanIndex = 0; + + for (j = 0; j < inputG722Data.Length;) + { + if (state.Packed) { - s.Band[band].ap[1] = wd3; + // Unpack the code bits + if (state.InBits < state.BitsPerSample) + { + state.InBuffer |= (uint)(inputG722Data[j++] << state.InBits); + state.InBits += 8; + } + code = (int)state.InBuffer & ((1 << state.BitsPerSample) - 1); + state.InBuffer >>= state.BitsPerSample; + state.InBits -= state.BitsPerSample; } - else if (s.Band[band].ap[1] < -wd3) + else { - s.Band[band].ap[1] = -wd3; + code = inputG722Data[j++]; } - // Block 4, UPZERO - wd1 = (d == 0) ? 0 : 128; - s.Band[band].sg[0] = d >> 15; - for (i = 1; i < 7; i++) + switch (state.BitsPerSample) { - s.Band[band].sg[i] = s.Band[band].d[i] >> 15; - wd2 = (s.Band[band].sg[i] == s.Band[band].sg[0]) ? wd1 : -wd1; - wd3 = (s.Band[band].b[i] * 32640) >> 15; - s.Band[band].bp[i] = Saturate(wd2 + wd3); + default: + case 8: + wd1 = code & 0x3F; + ihigh = (code >> 6) & 0x03; + wd2 = qm6[wd1]; + wd1 >>= 2; + break; + case 7: + wd1 = code & 0x1F; + ihigh = (code >> 5) & 0x03; + wd2 = qm5[wd1]; + wd1 >>= 1; + break; + case 6: + wd1 = code & 0x0F; + ihigh = (code >> 4) & 0x03; + wd2 = qm4[wd1]; + break; } - // Block 4, DELAYA - for (i = 6; i > 0; i--) + // Block 5L, LOW BAND INVQBL + wd2 = (state.Band[0].det * wd2) >> 15; + + // Block 5L, RECONS + rlow = state.Band[0].s + wd2; + + // Block 6L, LIMIT + if (rlow > 16383) { - s.Band[band].d[i] = s.Band[band].d[i - 1]; - s.Band[band].b[i] = s.Band[band].bp[i]; + rlow = 16383; } - - for (i = 2; i > 0; i--) + else if (rlow < -16384) { - s.Band[band].r[i] = s.Band[band].r[i - 1]; - s.Band[band].p[i] = s.Band[band].p[i - 1]; - s.Band[band].a[i] = s.Band[band].ap[i]; + rlow = -16384; } - // Block 4, FILTEP - wd1 = Saturate(s.Band[band].r[1] + s.Band[band].r[1]); - wd1 = (s.Band[band].a[1] * wd1) >> 15; - wd2 = Saturate(s.Band[band].r[2] + s.Band[band].r[2]); - wd2 = (s.Band[band].a[2] * wd2) >> 15; - s.Band[band].sp = Saturate(wd1 + wd2); + // Block 2L, INVQAL + wd2 = qm4[wd1]; + dlowt = (state.Band[0].det * wd2) >> 15; - // Block 4, FILTEZ - s.Band[band].sz = 0; - for (i = 6; i > 0; i--) + // Block 3L, LOGSCL + wd2 = rl42[wd1]; + wd1 = (state.Band[0].nb * 127) >> 7; + wd1 += wl[wd2]; + if (wd1 < 0) { - wd1 = Saturate(s.Band[band].d[i] + s.Band[band].d[i]); - s.Band[band].sz += (s.Band[band].b[i] * wd1) >> 15; + wd1 = 0; + } + else if (wd1 > 18432) + { + wd1 = 18432; } - s.Band[band].sz = Saturate(s.Band[band].sz); - // Block 4, PREDIC - s.Band[band].s = Saturate(s.Band[band].sp + s.Band[band].sz); - } + state.Band[0].nb = wd1; - static readonly int[] wl = { -60, -30, 58, 172, 334, 538, 1198, 3042 }; - static readonly int[] rl42 = { 0, 7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1, 0 }; - static readonly int[] ilb = { 2048, 2093, 2139, 2186, 2233, 2282, 2332, 2383, 2435, 2489, 2543, 2599, 2656, 2714, 2774, 2834, 2896, 2960, 3025, 3091, 3158, 3228, 3298, 3371, 3444, 3520, 3597, 3676, 3756, 3838, 3922, 4008 }; - static readonly int[] wh = { 0, -214, 798 }; - static readonly int[] rh2 = { 2, 1, 2, 1 }; - static readonly int[] qm2 = { -7408, -1616, 7408, 1616 }; - static readonly int[] qm4 = { 0, -20456, -12896, -8968, -6288, -4240, -2584, -1200, 20456, 12896, 8968, 6288, 4240, 2584, 1200, 0 }; - static readonly int[] qm5 = { -280, -280, -23352, -17560, -14120, -11664, -9752, -8184, -6864, -5712, -4696, -3784, -2960, -2208, -1520, -880, 23352, 17560, 14120, 11664, 9752, 8184, 6864, 5712, 4696, 3784, 2960, 2208, 1520, 880, 280, -280 }; - static readonly int[] qm6 = { -136, -136, -136, -136, -24808, -21904, -19008, -16704, -14984, -13512, -12280, -11192, -10232, -9360, -8576, -7856, -7192, -6576, -6000, -5456, -4944, -4464, -4008, -3576, -3168, -2776, -2400, -2032, -1688, -1360, -1040, -728, 24808, 21904, 19008, 16704, 14984, 13512, 12280, 11192, 10232, 9360, 8576, 7856, 7192, 6576, 6000, 5456, 4944, 4464, 4008, 3576, 3168, 2776, 2400, 2032, 1688, 1360, 1040, 728, 432, 136, -432, -136 }; - static readonly int[] qmf_coeffs = { 3, -11, 12, 32, -210, 951, 3876, -805, 362, -156, 53, -11, }; - static readonly int[] q6 = { 0, 35, 72, 110, 150, 190, 233, 276, 323, 370, 422, 473, 530, 587, 650, 714, 786, 858, 940, 1023, 1121, 1219, 1339, 1458, 1612, 1765, 1980, 2195, 2557, 2919, 0, 0 }; - static readonly int[] iln = { 0, 63, 62, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 0 }; - static readonly int[] ilp = { 0, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 0 }; - static readonly int[] ihn = { 0, 1, 0 }; - static readonly int[] ihp = { 0, 3, 2 }; - - /// - /// Decodes a buffer of G722 - /// - /// Codec state - /// Output buffer (to contain decompressed PCM samples) - /// - /// Number of bytes in input G722 data to decode - /// Number of samples written into output buffer - public int Decode(G722CodecState state, short[] outputBuffer, byte[] inputG722Data, int inputLength) - { - int dlowt; - int rlow; - int ihigh; - int dhigh; - int rhigh; - int xout1; - int xout2; - int wd1; - int wd2; - int wd3; - int code; - int outlen; - int i; - int j; - - outlen = 0; - rhigh = 0; - for (j = 0; j < inputLength;) - { - if (state.Packed) - { - // Unpack the code bits - if (state.InBits < state.BitsPerSample) - { - state.InBuffer |= (uint)(inputG722Data[j++] << state.InBits); - state.InBits += 8; - } - code = (int)state.InBuffer & ((1 << state.BitsPerSample) - 1); - state.InBuffer >>= state.BitsPerSample; - state.InBits -= state.BitsPerSample; - } - else - { - code = inputG722Data[j++]; - } + // Block 3L, SCALEL + wd1 = (state.Band[0].nb >> 6) & 31; + wd2 = 8 - (state.Band[0].nb >> 11); + wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2); + state.Band[0].det = wd3 << 2; - switch (state.BitsPerSample) - { - default: - case 8: - wd1 = code & 0x3F; - ihigh = (code >> 6) & 0x03; - wd2 = qm6[wd1]; - wd1 >>= 2; - break; - case 7: - wd1 = code & 0x1F; - ihigh = (code >> 5) & 0x03; - wd2 = qm5[wd1]; - wd1 >>= 1; - break; - case 6: - wd1 = code & 0x0F; - ihigh = (code >> 4) & 0x03; - wd2 = qm4[wd1]; - break; - } + Block4(state, 0, dlowt); - // Block 5L, LOW BAND INVQBL - wd2 = (state.Band[0].det * wd2) >> 15; + if (!state.EncodeFrom8000Hz) + { + // Block 2H, INVQAH + wd2 = qm2[ihigh]; + dhigh = (state.Band[1].det * wd2) >> 15; - // Block 5L, RECONS - rlow = state.Band[0].s + wd2; + // Block 5H, RECONS + rhigh = dhigh + state.Band[1].s; - // Block 6L, LIMIT - if (rlow > 16383) + // Block 6H, LIMIT + if (rhigh > 16383) { - rlow = 16383; + rhigh = 16383; } - else if (rlow < -16384) + else if (rhigh < -16384) { - rlow = -16384; + rhigh = -16384; } - // Block 2L, INVQAL - wd2 = qm4[wd1]; - dlowt = (state.Band[0].det * wd2) >> 15; - - // Block 3L, LOGSCL - wd2 = rl42[wd1]; - wd1 = (state.Band[0].nb * 127) >> 7; - wd1 += wl[wd2]; + // Block 2H, INVQAH + wd2 = rh2[ihigh]; + wd1 = (state.Band[1].nb * 127) >> 7; + wd1 += wh[wd2]; if (wd1 < 0) { wd1 = 0; } - else if (wd1 > 18432) + else if (wd1 > 22528) { - wd1 = 18432; + wd1 = 22528; } - state.Band[0].nb = wd1; + state.Band[1].nb = wd1; - // Block 3L, SCALEL - wd1 = (state.Band[0].nb >> 6) & 31; - wd2 = 8 - (state.Band[0].nb >> 11); + // Block 3H, SCALEH + wd1 = (state.Band[1].nb >> 6) & 31; + wd2 = 10 - (state.Band[1].nb >> 11); wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2); - state.Band[0].det = wd3 << 2; + state.Band[1].det = wd3 << 2; - Block4(state, 0, dlowt); + Block4(state, 1, dhigh); + } - if (!state.EncodeFrom8000Hz) + if (state.ItuTestMode) + { + // Ensure we have space for 2 samples + if (currentSpanIndex + 1 >= outputSpan.Length) { - // Block 2H, INVQAH - wd2 = qm2[ihigh]; - dhigh = (state.Band[1].det * wd2) >> 15; - - // Block 5H, RECONS - rhigh = dhigh + state.Band[1].s; + // Commit current samples and get a new span + destination.Advance(currentSpanIndex); + outlen += currentSpanIndex; + outputSpan = destination.GetSpan(Math.Max(2, estimatedOutputSize)); + currentSpanIndex = 0; + } - // Block 6H, LIMIT - if (rhigh > 16383) + if (currentSpanIndex + 1 < outputSpan.Length) + { + outputSpan[currentSpanIndex++] = (short)(rlow << 1); + outputSpan[currentSpanIndex++] = (short)(rhigh << 1); + } + else + { + break; // Output buffer full + } + } + else + { + if (state.EncodeFrom8000Hz) + { + // Ensure we have space for 1 sample + if (currentSpanIndex >= outputSpan.Length) { - rhigh = 16383; + // Commit current samples and get a new span + destination.Advance(currentSpanIndex); + outlen += currentSpanIndex; + outputSpan = destination.GetSpan(Math.Max(1, estimatedOutputSize)); + currentSpanIndex = 0; } - else if (rhigh < -16384) + + if (currentSpanIndex < outputSpan.Length) { - rhigh = -16384; + outputSpan[currentSpanIndex++] = (short)(rlow << 1); } - - // Block 2H, INVQAH - wd2 = rh2[ihigh]; - wd1 = (state.Band[1].nb * 127) >> 7; - wd1 += wh[wd2]; - if (wd1 < 0) + else { - wd1 = 0; + break; // Output buffer full } - else if (wd1 > 22528) + } + else + { + // Apply the receive QMF + for (i = 0; i < 22; i++) { - wd1 = 22528; + state.QmfSignalHistory[i] = state.QmfSignalHistory[i + 2]; } + state.QmfSignalHistory[22] = rlow + rhigh; + state.QmfSignalHistory[23] = rlow - rhigh; - state.Band[1].nb = wd1; - - // Block 3H, SCALEH - wd1 = (state.Band[1].nb >> 6) & 31; - wd2 = 10 - (state.Band[1].nb >> 11); - wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2); - state.Band[1].det = wd3 << 2; + xout1 = 0; + xout2 = 0; + for (i = 0; i < 12; i++) + { + xout2 += state.QmfSignalHistory[2 * i] * qmf_coeffs[i]; + xout1 += state.QmfSignalHistory[2 * i + 1] * qmf_coeffs[11 - i]; + } - Block4(state, 1, dhigh); - } + // Ensure we have space for 2 samples + if (currentSpanIndex + 1 >= outputSpan.Length) + { + // Commit current samples and get a new span + destination.Advance(currentSpanIndex); + outlen += currentSpanIndex; + outputSpan = destination.GetSpan(Math.Max(2, estimatedOutputSize)); + currentSpanIndex = 0; + } - if (state.ItuTestMode) - { - outputBuffer[outlen++] = (short)(rlow << 1); - outputBuffer[outlen++] = (short)(rhigh << 1); - } - else - { - if (state.EncodeFrom8000Hz) + if (currentSpanIndex + 1 < outputSpan.Length) { - outputBuffer[outlen++] = (short)(rlow << 1); + outputSpan[currentSpanIndex++] = (short)(xout1 >> 11); + outputSpan[currentSpanIndex++] = (short)(xout2 >> 11); } else { - // Apply the receive QMF - for (i = 0; i < 22; i++) - { - state.QmfSignalHistory[i] = state.QmfSignalHistory[i + 2]; - } - state.QmfSignalHistory[22] = rlow + rhigh; - state.QmfSignalHistory[23] = rlow - rhigh; - - xout1 = 0; - xout2 = 0; - for (i = 0; i < 12; i++) - { - xout2 += state.QmfSignalHistory[2 * i] * qmf_coeffs[i]; - xout1 += state.QmfSignalHistory[2 * i + 1] * qmf_coeffs[11 - i]; - } - outputBuffer[outlen++] = (short)(xout1 >> 11); - outputBuffer[outlen++] = (short)(xout2 >> 11); + break; // Output buffer full } } } - return outlen; } - /// - /// Encodes a buffer of G722 - /// - /// Codec state - /// Output buffer (to contain encoded G722) - /// PCM 16 bit samples to encode - /// Number of samples in the input buffer to encode - /// Number of encoded bytes written into output buffer - public int Encode(G722CodecState state, byte[] outputBuffer, short[] inputBuffer, int inputBufferCount) + // Commit any remaining samples + if (currentSpanIndex > 0) { - int dlow; - int dhigh; - int el; - int wd; - int wd1; - int ril; - int wd2; - int il4; - int ih2; - int wd3; - int eh; - int mih; - int i; - int j; - // Low and high band PCM from the QMF - int xlow; - int xhigh; - int g722_bytes; - // Even and odd tap accumulators - int sumeven; - int sumodd; - int ihigh; - int ilow; - int code; - - g722_bytes = 0; - xhigh = 0; - for (j = 0; j < inputBufferCount;) + destination.Advance(currentSpanIndex); + outlen += currentSpanIndex; + } + + return outlen; + } + + /// + /// Encodes a buffer of G722 + /// + /// Codec state + /// Output buffer (to contain encoded G722) + /// PCM 16 bit samples to encode + /// Number of encoded bytes written into output buffer + public int Encode(G722CodecState state, Span outputBuffer, ReadOnlySpan inputBuffer) + { + int dlow; + int dhigh; + int el; + int wd; + int wd1; + int ril; + int wd2; + int il4; + int ih2; + int wd3; + int eh; + int mih; + int i; + int j; + // Low and high band PCM from the QMF + int xlow; + int xhigh; + int g722_bytes; + // Even and odd tap accumulators + int sumeven; + int sumodd; + int ihigh; + int ilow; + int code; + + g722_bytes = 0; + xhigh = 0; + for (j = 0; j < inputBuffer.Length;) + { + if (state.ItuTestMode) + { + xlow = + xhigh = inputBuffer[j++] >> 1; + } + else { - if (state.ItuTestMode) + if (state.EncodeFrom8000Hz) { - xlow = - xhigh = inputBuffer[j++] >> 1; + xlow = inputBuffer[j++] >> 1; } else { - if (state.EncodeFrom8000Hz) + // Apply the transmit QMF + // Shuffle the buffer down + for (i = 0; i < 22; i++) { - xlow = inputBuffer[j++] >> 1; + state.QmfSignalHistory[i] = state.QmfSignalHistory[i + 2]; } - else - { - // Apply the transmit QMF - // Shuffle the buffer down - for (i = 0; i < 22; i++) - { - state.QmfSignalHistory[i] = state.QmfSignalHistory[i + 2]; - } - state.QmfSignalHistory[22] = inputBuffer[j++]; - if (j < inputBufferCount) - { - state.QmfSignalHistory[23] = inputBuffer[j++]; - } - else - { - //Duplicate the last sample - fix odd shorts issue - state.QmfSignalHistory[23] = state.QmfSignalHistory[22]; - } - - // Discard every other QMF output - sumeven = 0; - sumodd = 0; - for (i = 0; i < 12; i++) - { - sumodd += state.QmfSignalHistory[2 * i] * qmf_coeffs[i]; - sumeven += state.QmfSignalHistory[2 * i + 1] * qmf_coeffs[11 - i]; - } - xlow = (sumeven + sumodd) >> 14; - xhigh = (sumeven - sumodd) >> 14; - } - } - // Block 1L, SUBTRA - el = Saturate(xlow - state.Band[0].s); - // Block 1L, QUANTL - wd = (el >= 0) ? el : -(el + 1); + state.QmfSignalHistory[22] = inputBuffer[j++]; - for (i = 1; i < 30; i++) - { - wd1 = (q6[i] * state.Band[0].det) >> 12; - if (wd < wd1) + if (j < inputBuffer.Length) { - break; + state.QmfSignalHistory[23] = inputBuffer[j++]; } - } - ilow = (el < 0) ? iln[i] : ilp[i]; - - // Block 2L, INVQAL - ril = ilow >> 2; - wd2 = qm4[ril]; - dlow = (state.Band[0].det * wd2) >> 15; - - // Block 3L, LOGSCL - il4 = rl42[ril]; - wd = (state.Band[0].nb * 127) >> 7; - state.Band[0].nb = wd + wl[il4]; - if (state.Band[0].nb < 0) - { - state.Band[0].nb = 0; - } - else if (state.Band[0].nb > 18432) - { - state.Band[0].nb = 18432; - } - - // Block 3L, SCALEL - wd1 = (state.Band[0].nb >> 6) & 31; - wd2 = 8 - (state.Band[0].nb >> 11); - wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2); - state.Band[0].det = wd3 << 2; - - Block4(state, 0, dlow); - - if (state.EncodeFrom8000Hz) - { - // Just leave the high bits as zero - code = (0xC0 | ilow) >> (8 - state.BitsPerSample); - } - else - { - // Block 1H, SUBTRA - eh = Saturate(xhigh - state.Band[1].s); - - // Block 1H, QUANTH - wd = (eh >= 0) ? eh : -(eh + 1); - wd1 = (564 * state.Band[1].det) >> 12; - mih = (wd >= wd1) ? 2 : 1; - ihigh = (eh < 0) ? ihn[mih] : ihp[mih]; - - // Block 2H, INVQAH - wd2 = qm2[ihigh]; - dhigh = (state.Band[1].det * wd2) >> 15; - - // Block 3H, LOGSCH - ih2 = rh2[ihigh]; - wd = (state.Band[1].nb * 127) >> 7; - state.Band[1].nb = wd + wh[ih2]; - if (state.Band[1].nb < 0) + else { - state.Band[1].nb = 0; + // Duplicate the last sample - fix odd shorts issue + state.QmfSignalHistory[23] = state.QmfSignalHistory[22]; } - else if (state.Band[1].nb > 22528) + + // Discard every other QMF output + sumeven = 0; + sumodd = 0; + for (i = 0; i < 12; i++) { - state.Band[1].nb = 22528; + sumodd += state.QmfSignalHistory[2 * i] * qmf_coeffs[i]; + sumeven += state.QmfSignalHistory[2 * i + 1] * qmf_coeffs[11 - i]; } - // Block 3H, SCALEH - wd1 = (state.Band[1].nb >> 6) & 31; - wd2 = 10 - (state.Band[1].nb >> 11); - wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2); - state.Band[1].det = wd3 << 2; - - Block4(state, 1, dhigh); - code = ((ihigh << 6) | ilow) >> (8 - state.BitsPerSample); + xlow = (sumeven + sumodd) >> 14; + xhigh = (sumeven - sumodd) >> 14; } + } - if (state.Packed) - { - // Pack the code bits - state.OutBuffer |= (uint)(code << state.OutBits); - state.OutBits += state.BitsPerSample; - if (state.OutBits >= 8) - { - outputBuffer[g722_bytes++] = (byte)(state.OutBuffer & 0xFF); - state.OutBits -= 8; - state.OutBuffer >>= 8; - } - } - else + // Block 1L, SUBTRA + el = Saturate(xlow - state.Band[0].s); + + // Block 1L, QUANTL + wd = (el >= 0) ? el : -(el + 1); + + for (i = 1; i < 30; i++) + { + wd1 = (q6[i] * state.Band[0].det) >> 12; + if (wd < wd1) { - outputBuffer[g722_bytes++] = (byte)code; + break; } } - return g722_bytes; - } - } - /// - /// Stores state to be used between calls to Encode or Decode - /// - public class G722CodecState - { - /// - /// ITU Test Mode - /// TRUE if the operating in the special ITU test mode, with the band split filters disabled. - /// - public bool ItuTestMode { get; set; } - - /// - /// TRUE if the G.722 data is packed - /// - public bool Packed { get; private set; } - - /// - /// 8kHz Sampling - /// TRUE if encode from 8k samples/second - /// - public bool EncodeFrom8000Hz { get; private set; } - - /// - /// Bits Per Sample - /// 6 for 48000kbps, 7 for 56000kbps, or 8 for 64000kbps. - /// - public int BitsPerSample { get; private set; } - - /// - /// Signal history for the QMF (x) - /// - public int[] QmfSignalHistory { get; private set; } - - /// - /// Band - /// - public Band[] Band { get; private set; } - - /// - /// In bit buffer - /// - public uint InBuffer { get; internal set; } - - /// - /// Number of bits in InBuffer - /// - public int InBits { get; internal set; } - - /// - /// Out bit buffer - /// - public uint OutBuffer { get; internal set; } - - /// - /// Number of bits in OutBuffer - /// - public int OutBits { get; internal set; } - - /// - /// Creates a new instance of G722 Codec State for a - /// new encode or decode session - /// - /// Bitrate (typically 64000) - /// Special options - public G722CodecState(int rate, G722Flags options) - { - this.Band = new Band[2] { new Band(), new Band() }; - this.QmfSignalHistory = new int[24]; - this.ItuTestMode = false; + ilow = (el < 0) ? iln[i] : ilp[i]; + + // Block 2L, INVQAL + ril = ilow >> 2; + wd2 = qm4[ril]; + dlow = (state.Band[0].det * wd2) >> 15; + + il4 = rl42[ril]; + wd = (state.Band[0].nb * 127) >> 7; + state.Band[0].nb = wd + wl[il4]; - if (rate == 48000) + if (state.Band[0].nb < 0) { - this.BitsPerSample = 6; + state.Band[0].nb = 0; } - else if (rate == 56000) + else if (state.Band[0].nb > 18432) { - this.BitsPerSample = 7; + state.Band[0].nb = 18432; } - else if (rate == 64000) + + + wd1 = (state.Band[0].nb >> 6) & 31; + wd2 = 8 - (state.Band[0].nb >> 11); + wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2); + state.Band[0].det = wd3 << 2; + + Block4(state, 0, dlow); + + if (state.EncodeFrom8000Hz) { - this.BitsPerSample = 8; + // Just leave the high bits as zero + code = (0xC0 | ilow) >> (8 - state.BitsPerSample); } else { - throw new ArgumentException("Invalid rate, should be 48000, 56000 or 64000"); - } + // Block 1H, SUBTRA + eh = Saturate(xhigh - state.Band[1].s); - if ((options & G722Flags.SampleRate8000) == G722Flags.SampleRate8000) - { - this.EncodeFrom8000Hz = true; + // Block 1H, QUANTH + wd = (eh >= 0) ? eh : -(eh + 1); + wd1 = (564 * state.Band[1].det) >> 12; + mih = (wd >= wd1) ? 2 : 1; + ihigh = (eh < 0) ? ihn[mih] : ihp[mih]; + + // Block 2H, INVQAH + wd2 = qm2[ihigh]; + dhigh = (state.Band[1].det * wd2) >> 15; + + // Block 3H, LOGSCH + ih2 = rh2[ihigh]; + wd = (state.Band[1].nb * 127) >> 7; + state.Band[1].nb = wd + wh[ih2]; + + if (state.Band[1].nb < 0) + { + state.Band[1].nb = 0; + } + else if (state.Band[1].nb > 22528) + { + state.Band[1].nb = 22528; + } + + // Block 3H, SCALEH + wd1 = (state.Band[1].nb >> 6) & 31; + wd2 = 10 - (state.Band[1].nb >> 11); + wd3 = (wd2 < 0) ? (ilb[wd1] << -wd2) : (ilb[wd1] >> wd2); + state.Band[1].det = wd3 << 2; + + Block4(state, 1, dhigh); + + code = ((ihigh << 6) | ilow) >> (8 - state.BitsPerSample); } - if (((options & G722Flags.Packed) == G722Flags.Packed) && this.BitsPerSample != 8) + if (state.Packed) { - this.Packed = true; + // Pack the code bits + state.OutBuffer |= (uint)(code << state.OutBits); + state.OutBits += state.BitsPerSample; + + if (state.OutBits >= 8) + { + outputBuffer[g722_bytes++] = (byte)(state.OutBuffer & 0xFF); + state.OutBits -= 8; + state.OutBuffer >>= 8; + } } else { - this.Packed = false; + outputBuffer[g722_bytes++] = (byte)code; } - - this.Band[0].det = 32; - this.Band[1].det = 8; } + + return g722_bytes; } +} +/// +/// Stores state to be used between calls to Encode or Decode +/// +public class G722CodecState +{ /// - /// Band data for G722 Codec + /// ITU Test Mode + /// TRUE if the operating in the special ITU test mode, with the band split filters disabled. /// - public class Band - { - /// s - public int s; - /// sp - public int sp; - /// sz - public int sz; - /// r - public int[] r = new int[3]; - /// a - public int[] a = new int[3]; - /// ap - public int[] ap = new int[3]; - /// p - public int[] p = new int[3]; - /// d - public int[] d = new int[7]; - /// b - public int[] b = new int[7]; - /// bp - public int[] bp = new int[7]; - /// sg - public int[] sg = new int[7]; - /// nb - public int nb; - /// det - public int det; - } + public bool ItuTestMode { get; set; } + + /// + /// TRUE if the G.722 data is packed + /// + public bool Packed { get; private set; } /// - /// G722 Flags + /// 8kHz Sampling + /// TRUE if encode from 8k samples/second /// - [Flags] - public enum G722Flags + public bool EncodeFrom8000Hz { get; private set; } + + /// + /// Bits Per Sample + /// 6 for 48000kbps, 7 for 56000kbps, or 8 for 64000kbps. + /// + public int BitsPerSample { get; private set; } + + /// + /// Signal history for the QMF (x) + /// + public int[] QmfSignalHistory { get; private set; } + + /// + /// Band + /// + public Band[] Band { get; private set; } + + /// + /// In bit buffer + /// + public uint InBuffer { get; internal set; } + + /// + /// Number of bits in InBuffer + /// + public int InBits { get; internal set; } + + /// + /// Out bit buffer + /// + public uint OutBuffer { get; internal set; } + + /// + /// Number of bits in OutBuffer + /// + public int OutBits { get; internal set; } + + /// + /// Creates a new instance of G722 Codec State for a + /// new encode or decode session + /// + /// Bitrate (typically 64000) + /// Special options + public G722CodecState(int rate, G722Flags options) { - /// - /// None - /// - None = 0, - /// - /// Using a G722 sample rate of 8000 - /// - SampleRate8000 = 0x0001, - /// - /// Packed - /// - Packed = 0x0002 + this.Band = new Band[2] { new Band(), new Band() }; + this.QmfSignalHistory = new int[24]; + this.ItuTestMode = false; + + if (rate == 48000) + { + this.BitsPerSample = 6; + } + else if (rate == 56000) + { + this.BitsPerSample = 7; + } + else if (rate == 64000) + { + this.BitsPerSample = 8; + } + else + { + throw new ArgumentException("Invalid rate, should be 48000, 56000 or 64000"); + } + + if ((options & G722Flags.SampleRate8000) == G722Flags.SampleRate8000) + { + this.EncodeFrom8000Hz = true; + } + + if (((options & G722Flags.Packed) == G722Flags.Packed) && this.BitsPerSample != 8) + { + this.Packed = true; + } + else + { + this.Packed = false; + } + + this.Band[0].det = 32; + this.Band[1].det = 8; } } + +/// +/// Band data for G722 Codec +/// +public class Band +{ + /// s + public int s; + /// sp + public int sp; + /// sz + public int sz; + /// r + public int[] r = new int[3]; + /// a + public int[] a = new int[3]; + /// ap + public int[] ap = new int[3]; + /// p + public int[] p = new int[3]; + /// d + public int[] d = new int[7]; + /// b + public int[] b = new int[7]; + /// bp + public int[] bp = new int[7]; + /// sg + public int[] sg = new int[7]; + /// nb + public int nb; + /// det + public int det; +} + +/// +/// G722 Flags +/// +[Flags] +public enum G722Flags +{ + /// + /// None + /// + None = 0, + /// + /// Using a G722 sample rate of 8000 + /// + SampleRate8000 = 0x0001, + /// + /// Packed + /// + Packed = 0x0002 +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/AcelpCo.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/AcelpCo.cs index d00012b715..4d69f3ec2f 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/AcelpCo.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/AcelpCo.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,241 +24,297 @@ * * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal class AcelpCo { - internal class AcelpCo - { - private int extra; + private int extra; - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : ACELP_CO.C - Used for the floating point version of G.729 main body - (not for G.729A) + /* +File : ACELP_CO.C +Used for the floating point version of G.729 main body +(not for G.729A) */ - /** - * - * @param x (i) :Target vector - * @param h (i) :Impulse response of filters - * @param t0 (i) :Pitch lag - * @param pitch_sharp (i) :Last quantized pitch gain - * @param i_subfr (i) :Indicator of 1st subframe, - * @param code (o) :Innovative codebook - * @param y (o) :Filtered innovative codebook - * @param sign (o) :Signs of 4 pulses - * @return index of pulses positions - */ + /** +* +* @param x (i) :Target vector +* @param h (i) :Impulse response of filters +* @param t0 (i) :Pitch lag +* @param pitch_sharp (i) :Last quantized pitch gain +* @param i_subfr (i) :Indicator of 1st subframe, +* @param code (o) :Innovative codebook +* @param y (o) :Filtered innovative codebook +* @param sign (o) :Signs of 4 pulses +* @return index of pulses positions +*/ - public int ACELP_codebook( - float[] x, - float[] h, - int t0, - float pitch_sharp, - int i_subfr, - float[] code, - float[] y, - IntReference sign - ) - { - var DIM_RR = Ld8k.DIM_RR; - var L_SUBFR = Ld8k.L_SUBFR; + public int ACELP_codebook( + float[] x, + float[] h, + int t0, + float pitch_sharp, + int i_subfr, + float[] code, + float[] y, + IntReference sign + ) + { + var DIM_RR = Ld8k.DIM_RR; + var L_SUBFR = Ld8k.L_SUBFR; - int i, index; - var dn = new float[L_SUBFR]; - var rr = new float[DIM_RR]; + int i, index; + var dn = new float[L_SUBFR]; + var rr = new float[DIM_RR]; - /*----------------------------------------------------------------* - * Include fixed-gain pitch contribution into impulse resp. h[] * - * Find correlations of h[] needed for the codebook search. * - *-----------------------------------------------------------------*/ + /*----------------------------------------------------------------* +* Include fixed-gain pitch contribution into impulse resp. h[] * +* Find correlations of h[] needed for the codebook search. * +*-----------------------------------------------------------------*/ - if (t0 < L_SUBFR) - for (i = t0; i < L_SUBFR; i++) - h[i] += pitch_sharp * h[i - t0]; + if (t0 < L_SUBFR) + { + for (i = t0; i < L_SUBFR; i++) + { + h[i] += pitch_sharp * h[i - t0]; + } + } - cor_h(h, rr); + cor_h(h, rr); - /*----------------------------------------------------------------* - * Compute correlation of target vector with impulse response. * - *-----------------------------------------------------------------*/ + /*----------------------------------------------------------------* +* Compute correlation of target vector with impulse response. * +*-----------------------------------------------------------------*/ - CorFunc.cor_h_x(h, x, dn); /* backward filtered target vector dn */ + CorFunc.cor_h_x(h, x, dn); /* backward filtered target vector dn */ - /*----------------------------------------------------------------* - * Find innovative codebook. * - *-----------------------------------------------------------------*/ + /*----------------------------------------------------------------* +* Find innovative codebook. * +*-----------------------------------------------------------------*/ - index = d4i40_17(dn, rr, h, code, y, sign, i_subfr); + index = d4i40_17(dn, rr, h, code, y, sign, i_subfr); - /*------------------------------------------------------* - * - Add the fixed-gain pitch contribution to code[]. * - *-------------------------------------------------------*/ + /*------------------------------------------------------* +* - Add the fixed-gain pitch contribution to code[]. * +*-------------------------------------------------------*/ - if (t0 < L_SUBFR) - for (i = t0; i < L_SUBFR; i++) - code[i] += pitch_sharp * code[i - t0]; + if (t0 < L_SUBFR) + { + for (i = t0; i < L_SUBFR; i++) + { + code[i] += pitch_sharp * code[i - t0]; + } + } + + return index; + } - return index; + /** +* Compute correlations of h[] needed for the codebook search. +* +* @param h (i) :Impulse response of filters +* @param rr (o) :Correlations of H[] +*/ + private void cor_h( + float[] h, + float[] rr + ) + { + var MSIZE = Ld8k.MSIZE; + var NB_POS = Ld8k.NB_POS; + var STEP = Ld8k.STEP; + + int rri0i0, rri1i1, rri2i2, rri3i3, rri4i4; + int rri0i1, rri0i2, rri0i3, rri0i4; + int rri1i2, rri1i3, rri1i4; + int rri2i3, rri2i4; + + int p0, p1, p2, p3, p4; + + int ptr_hd, ptr_hf, ptr_h1, ptr_h2; + float cor; + int i, k, ldec, l_fin_sup, l_fin_inf; + + /* Init pointers */ + rri0i0 = 0; + rri1i1 = rri0i0 + NB_POS; + rri2i2 = rri1i1 + NB_POS; + rri3i3 = rri2i2 + NB_POS; + rri4i4 = rri3i3 + NB_POS; + rri0i1 = rri4i4 + NB_POS; + rri0i2 = rri0i1 + MSIZE; + rri0i3 = rri0i2 + MSIZE; + rri0i4 = rri0i3 + MSIZE; + rri1i2 = rri0i4 + MSIZE; + rri1i3 = rri1i2 + MSIZE; + rri1i4 = rri1i3 + MSIZE; + rri2i3 = rri1i4 + MSIZE; + rri2i4 = rri2i3 + MSIZE; + + /*------------------------------------------------------------* +* Compute rri0i0[], rri1i1[], rri2i2[], rri3i3 and rri4i4[] * +*------------------------------------------------------------*/ + + p0 = rri0i0 + NB_POS - 1; /* Init pointers to last position of rrixix[] */ + p1 = rri1i1 + NB_POS - 1; + p2 = rri2i2 + NB_POS - 1; + p3 = rri3i3 + NB_POS - 1; + p4 = rri4i4 + NB_POS - 1; + + ptr_h1 = 0; + cor = 0.0f; + for (i = 0; i < NB_POS; i++) + { + cor += h[ptr_h1] * h[ptr_h1]; + ptr_h1++; + rr[p4] = cor; + p4--; + + cor += h[ptr_h1] * h[ptr_h1]; + ptr_h1++; + rr[p3] = cor; + p3--; + + cor += h[ptr_h1] * h[ptr_h1]; + ptr_h1++; + rr[p2] = cor; + p2--; + + cor += h[ptr_h1] * h[ptr_h1]; + ptr_h1++; + rr[p1] = cor; + p1--; + + cor += h[ptr_h1] * h[ptr_h1]; + ptr_h1++; + rr[p0] = cor; + p0--; } - /** - * Compute correlations of h[] needed for the codebook search. - * - * @param h (i) :Impulse response of filters - * @param rr (o) :Correlations of H[] - */ - private void cor_h( - float[] h, - float[] rr - ) + /*-----------------------------------------------------------------* +* Compute elements of: rri2i3[], rri1i2[], rri0i1[] and rri0i4[] * +*-----------------------------------------------------------------*/ + + l_fin_sup = MSIZE - 1; + l_fin_inf = l_fin_sup - 1; + ldec = NB_POS + 1; + + ptr_hd = 0; + ptr_hf = ptr_hd + 1; + + for (k = 0; k < NB_POS; k++) { - var MSIZE = Ld8k.MSIZE; - var NB_POS = Ld8k.NB_POS; - var STEP = Ld8k.STEP; - - int rri0i0, rri1i1, rri2i2, rri3i3, rri4i4; - int rri0i1, rri0i2, rri0i3, rri0i4; - int rri1i2, rri1i3, rri1i4; - int rri2i3, rri2i4; - - int p0, p1, p2, p3, p4; - - int ptr_hd, ptr_hf, ptr_h1, ptr_h2; - float cor; - int i, k, ldec, l_fin_sup, l_fin_inf; - - /* Init pointers */ - rri0i0 = 0; - rri1i1 = rri0i0 + NB_POS; - rri2i2 = rri1i1 + NB_POS; - rri3i3 = rri2i2 + NB_POS; - rri4i4 = rri3i3 + NB_POS; - rri0i1 = rri4i4 + NB_POS; - rri0i2 = rri0i1 + MSIZE; - rri0i3 = rri0i2 + MSIZE; - rri0i4 = rri0i3 + MSIZE; - rri1i2 = rri0i4 + MSIZE; - rri1i3 = rri1i2 + MSIZE; - rri1i4 = rri1i3 + MSIZE; - rri2i3 = rri1i4 + MSIZE; - rri2i4 = rri2i3 + MSIZE; - - /*------------------------------------------------------------* - * Compute rri0i0[], rri1i1[], rri2i2[], rri3i3 and rri4i4[] * - *------------------------------------------------------------*/ - - p0 = rri0i0 + NB_POS - 1; /* Init pointers to last position of rrixix[] */ - p1 = rri1i1 + NB_POS - 1; - p2 = rri2i2 + NB_POS - 1; - p3 = rri3i3 + NB_POS - 1; - p4 = rri4i4 + NB_POS - 1; - - ptr_h1 = 0; + + p3 = rri2i3 + l_fin_sup; + p2 = rri1i2 + l_fin_sup; + p1 = rri0i1 + l_fin_sup; + p0 = rri0i4 + l_fin_inf; cor = 0.0f; - for (i = 0; i < NB_POS; i++) + ptr_h1 = ptr_hd; + ptr_h2 = ptr_hf; + + for (i = k + 1; i < NB_POS; i++) { - cor += h[ptr_h1] * h[ptr_h1]; - ptr_h1++; - rr[p4] = cor; - p4--; - cor += h[ptr_h1] * h[ptr_h1]; + cor += h[ptr_h1] * h[ptr_h2]; ptr_h1++; + ptr_h2++; + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; rr[p3] = cor; - p3--; - cor += h[ptr_h1] * h[ptr_h1]; + cor += h[ptr_h1] * h[ptr_h2]; ptr_h1++; + ptr_h2++; rr[p2] = cor; - p2--; - cor += h[ptr_h1] * h[ptr_h1]; + cor += h[ptr_h1] * h[ptr_h2]; ptr_h1++; + ptr_h2++; rr[p1] = cor; - p1--; - cor += h[ptr_h1] * h[ptr_h1]; + cor += h[ptr_h1] * h[ptr_h2]; ptr_h1++; + ptr_h2++; rr[p0] = cor; - p0--; - } - /*-----------------------------------------------------------------* - * Compute elements of: rri2i3[], rri1i2[], rri0i1[] and rri0i4[] * - *-----------------------------------------------------------------*/ + p3 -= ldec; + p2 -= ldec; + p1 -= ldec; + p0 -= ldec; + } - l_fin_sup = MSIZE - 1; - l_fin_inf = l_fin_sup - 1; - ldec = NB_POS + 1; + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p3] = cor; + + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p2] = cor; + + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p1] = cor; + + l_fin_sup -= NB_POS; + l_fin_inf--; + ptr_hf += STEP; + } - ptr_hd = 0; - ptr_hf = ptr_hd + 1; + /*---------------------------------------------------------------------* +* Compute elements of: rri2i4[], rri1i3[], rri0i2[], rri1i4[], rri0i3 * +*---------------------------------------------------------------------*/ - for (k = 0; k < NB_POS; k++) - { - - p3 = rri2i3 + l_fin_sup; - p2 = rri1i2 + l_fin_sup; - p1 = rri0i1 + l_fin_sup; - p0 = rri0i4 + l_fin_inf; - cor = 0.0f; - ptr_h1 = ptr_hd; - ptr_h2 = ptr_hf; + ptr_hd = 0; + ptr_hf = ptr_hd + 2; + l_fin_sup = MSIZE - 1; + l_fin_inf = l_fin_sup - 1; + for (k = 0; k < NB_POS; k++) + { - for (i = k + 1; i < NB_POS; i++) - { + p4 = rri2i4 + l_fin_sup; + p3 = rri1i3 + l_fin_sup; + p2 = rri0i2 + l_fin_sup; + p1 = rri1i4 + l_fin_inf; + p0 = rri0i3 + l_fin_inf; - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p3] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p2] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p1] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p0] = cor; - - p3 -= ldec; - p2 -= ldec; - p1 -= ldec; - p0 -= ldec; - } + cor = 0.0f; + ptr_h1 = ptr_hd; + ptr_h2 = ptr_hf; + for (i = k + 1; i < NB_POS; i++) + { cor += h[ptr_h1] * h[ptr_h2]; ptr_h1++; ptr_h2++; + rr[p4] = cor; + cor += h[ptr_h1] * h[ptr_h2]; ptr_h1++; ptr_h2++; @@ -274,65 +330,60 @@ float[] rr ptr_h2++; rr[p1] = cor; - l_fin_sup -= NB_POS; - l_fin_inf--; - ptr_hf += STEP; + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p0] = cor; + + p4 -= ldec; + p3 -= ldec; + p2 -= ldec; + p1 -= ldec; + p0 -= ldec; } - /*---------------------------------------------------------------------* - * Compute elements of: rri2i4[], rri1i3[], rri0i2[], rri1i4[], rri0i3 * - *---------------------------------------------------------------------*/ + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p4] = cor; - ptr_hd = 0; - ptr_hf = ptr_hd + 2; - l_fin_sup = MSIZE - 1; - l_fin_inf = l_fin_sup - 1; - for (k = 0; k < NB_POS; k++) - { + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p3] = cor; - p4 = rri2i4 + l_fin_sup; - p3 = rri1i3 + l_fin_sup; - p2 = rri0i2 + l_fin_sup; - p1 = rri1i4 + l_fin_inf; - p0 = rri0i3 + l_fin_inf; + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p2] = cor; - cor = 0.0f; - ptr_h1 = ptr_hd; - ptr_h2 = ptr_hf; - for (i = k + 1; i < NB_POS; i++) - { + l_fin_sup -= NB_POS; + l_fin_inf--; + ptr_hf += STEP; + } - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p4] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p3] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p2] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p1] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p0] = cor; - - p4 -= ldec; - p3 -= ldec; - p2 -= ldec; - p1 -= ldec; - p0 -= ldec; - } + /*----------------------------------------------------------------------* +* Compute elements of: rri1i4[], rri0i3[], rri2i4[], rri1i3[], rri0i2 * +*----------------------------------------------------------------------*/ + + ptr_hd = 0; + ptr_hf = ptr_hd + 3; + l_fin_sup = MSIZE - 1; + l_fin_inf = l_fin_sup - 1; + for (k = 0; k < NB_POS; k++) + { + + p4 = rri1i4 + l_fin_sup; + p3 = rri0i3 + l_fin_sup; + p2 = rri2i4 + l_fin_inf; + p1 = rri1i3 + l_fin_inf; + p0 = rri0i2 + l_fin_inf; + + ptr_h1 = ptr_hd; + ptr_h2 = ptr_hf; + cor = 0.0f; + for (i = k + 1; i < NB_POS; i++) + { cor += h[ptr_h1] * h[ptr_h2]; ptr_h1++; @@ -349,269 +400,243 @@ float[] rr ptr_h2++; rr[p2] = cor; - l_fin_sup -= NB_POS; - l_fin_inf--; - ptr_hf += STEP; + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p1] = cor; + + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p0] = cor; + + p4 -= ldec; + p3 -= ldec; + p2 -= ldec; + p1 -= ldec; + p0 -= ldec; } - /*----------------------------------------------------------------------* - * Compute elements of: rri1i4[], rri0i3[], rri2i4[], rri1i3[], rri0i2 * - *----------------------------------------------------------------------*/ + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p4] = cor; - ptr_hd = 0; - ptr_hf = ptr_hd + 3; - l_fin_sup = MSIZE - 1; - l_fin_inf = l_fin_sup - 1; - for (k = 0; k < NB_POS; k++) - { + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p3] = cor; + + l_fin_sup -= NB_POS; + l_fin_inf--; + ptr_hf += STEP; + } - p4 = rri1i4 + l_fin_sup; - p3 = rri0i3 + l_fin_sup; - p2 = rri2i4 + l_fin_inf; - p1 = rri1i3 + l_fin_inf; - p0 = rri0i2 + l_fin_inf; + /*----------------------------------------------------------------------* +* Compute elements of: rri0i4[], rri2i3[], rri1i2[], rri0i1[] * +*----------------------------------------------------------------------*/ - ptr_h1 = ptr_hd; - ptr_h2 = ptr_hf; - cor = 0.0f; - for (i = k + 1; i < NB_POS; i++) - { + ptr_hd = 0; + ptr_hf = ptr_hd + 4; + l_fin_sup = MSIZE - 1; + l_fin_inf = l_fin_sup - 1; + for (k = 0; k < NB_POS; k++) + { - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p4] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p3] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p2] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p1] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p0] = cor; - - p4 -= ldec; - p3 -= ldec; - p2 -= ldec; - p1 -= ldec; - p0 -= ldec; - } + p3 = rri0i4 + l_fin_sup; + p2 = rri2i3 + l_fin_inf; + p1 = rri1i2 + l_fin_inf; + p0 = rri0i1 + l_fin_inf; + + ptr_h1 = ptr_hd; + ptr_h2 = ptr_hf; + cor = 0.0f; + for (i = k + 1; i < NB_POS; i++) + { cor += h[ptr_h1] * h[ptr_h2]; ptr_h1++; ptr_h2++; - rr[p4] = cor; + rr[p3] = cor; cor += h[ptr_h1] * h[ptr_h2]; ptr_h1++; ptr_h2++; - rr[p3] = cor; + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p2] = cor; - l_fin_sup -= NB_POS; - l_fin_inf--; - ptr_hf += STEP; - } + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p1] = cor; - /*----------------------------------------------------------------------* - * Compute elements of: rri0i4[], rri2i3[], rri1i2[], rri0i1[] * - *----------------------------------------------------------------------*/ + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p0] = cor; - ptr_hd = 0; - ptr_hf = ptr_hd + 4; - l_fin_sup = MSIZE - 1; - l_fin_inf = l_fin_sup - 1; - for (k = 0; k < NB_POS; k++) - { + p3 -= ldec; + p2 -= ldec; + p1 -= ldec; + p0 -= ldec; + } - p3 = rri0i4 + l_fin_sup; - p2 = rri2i3 + l_fin_inf; - p1 = rri1i2 + l_fin_inf; - p0 = rri0i1 + l_fin_inf; + cor += h[ptr_h1] * h[ptr_h2]; + ptr_h1++; + ptr_h2++; + rr[p3] = cor; - ptr_h1 = ptr_hd; - ptr_h2 = ptr_hf; - cor = 0.0f; - for (i = k + 1; i < NB_POS; i++) - { + l_fin_sup -= NB_POS; + l_fin_inf--; + ptr_hf += STEP; + } + } - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p3] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p2] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p1] = cor; - - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p0] = cor; - - p3 -= ldec; - p2 -= ldec; - p1 -= ldec; - p0 -= ldec; - } + /** +* Algebraic codebook search 17 bits; 4 pulses 40 sampleframe +* +* @param dn (i) : backward filtered target vector +* @param rr (i) : autocorrelations of impulse response h[] +* @param h (i) : impulse response of filters +* @param cod (o) : selected algebraic codeword +* @param y (o) : output: selected algebraic codeword +* @param signs (o) : signs of 4 pulses +* @param i_subfr (i) subframe flag +* @return pulse positions +*/ + private int d4i40_17( + float[] dn, + float[] rr, + float[] h, + float[] cod, + float[] y, + IntReference signs, + int i_subfr + ) + { + var L_SUBFR = Ld8k.L_SUBFR; + var MAX_TIME = Ld8k.MAX_TIME; + var MSIZE = Ld8k.MSIZE; + var NB_POS = Ld8k.NB_POS; + var STEP = Ld8k.STEP; + var THRESHFCB = Ld8k.THRESHFCB; - cor += h[ptr_h1] * h[ptr_h2]; - ptr_h1++; - ptr_h2++; - rr[p3] = cor; + /* +* The code length is 40, containing 4 nonzero pulses i0, i1, i2, i3. +* with pulse spacings of step = 5 +* Each pulses can have 8 possible positions (positive or negative): +* +* i0 (+-1) : 0, 5, 10, 15, 20, 25, 30, 35 +* i1 (+-1) : 1, 6, 11, 16, 21, 26, 31, 36 +* i2 (+-1) : 2, 7, 12, 17, 22, 27, 32, 37 +* i3 (+-1) : 3, 8, 13, 18, 23, 28, 33, 38 +* 4, 9, 14, 19, 24, 29, 34, 39 +*--------------------------------------------------------------------------- +*/ + int i0, i1, i2, i3, ip0, ip1, ip2, ip3; + int i, j, time; + float ps0, ps1, ps2, ps3, alp0, alp1, alp2, alp3; + float ps3c, psc, alpha; + float average, max0, max1, max2, thres; + var p_sign = new float[L_SUBFR]; + + int rri0i0, rri1i1, rri2i2, rri3i3, rri4i4; + int rri0i1, rri0i2, rri0i3, rri0i4; + int rri1i2, rri1i3, rri1i4; + int rri2i3, rri2i4; + + int ptr_ri0i0, ptr_ri1i1, ptr_ri2i2, ptr_ri3i3, ptr_ri4i4; + int ptr_ri0i1, ptr_ri0i2, ptr_ri0i3, ptr_ri0i4; + int ptr_ri1i2, ptr_ri1i3, ptr_ri1i4; + int ptr_ri2i3, ptr_ri2i4; + + /* Init pointers */ + + rri0i0 = 0; + rri1i1 = rri0i0 + NB_POS; + rri2i2 = rri1i1 + NB_POS; + rri3i3 = rri2i2 + NB_POS; + rri4i4 = rri3i3 + NB_POS; + + rri0i1 = rri4i4 + NB_POS; + rri0i2 = rri0i1 + MSIZE; + rri0i3 = rri0i2 + MSIZE; + rri0i4 = rri0i3 + MSIZE; + rri1i2 = rri0i4 + MSIZE; + rri1i3 = rri1i2 + MSIZE; + rri1i4 = rri1i3 + MSIZE; + rri2i3 = rri1i4 + MSIZE; + rri2i4 = rri2i3 + MSIZE; + + /*-----------------------------------------------------------------------* +* Reset max_time for 1st subframe. * +*-----------------------------------------------------------------------* +*/ + if (i_subfr == 0) + { + extra = 30; + } - l_fin_sup -= NB_POS; - l_fin_inf--; - ptr_hf += STEP; + /*----------------------------------------------------------------------* +* Chose the signs of the impulses. * +*-----------------------------------------------------------------------*/ + + for (i = 0; i < L_SUBFR; i++) + { + if (dn[i] >= 0.0f) + { + p_sign[i] = 1.0f; + } + else + { + p_sign[i] = -1.0f; + dn[i] = -dn[i]; } } - /** - * Algebraic codebook search 17 bits; 4 pulses 40 sampleframe - * - * @param dn (i) : backward filtered target vector - * @param rr (i) : autocorrelations of impulse response h[] - * @param h (i) : impulse response of filters - * @param cod (o) : selected algebraic codeword - * @param y (o) : output: selected algebraic codeword - * @param signs (o) : signs of 4 pulses - * @param i_subfr (i) subframe flag - * @return pulse positions - */ - private int d4i40_17( - float[] dn, - float[] rr, - float[] h, - float[] cod, - float[] y, - IntReference signs, - int i_subfr - ) + /*-------------------------------------------------------------------* + * - Compute the search threshold after three pulses * + *-------------------------------------------------------------------*/ + + average = dn[0] + dn[1] + dn[2]; + max0 = dn[0]; + max1 = dn[1]; + max2 = dn[2]; + for (i = 5; i < L_SUBFR; i += STEP) { - var L_SUBFR = Ld8k.L_SUBFR; - var MAX_TIME = Ld8k.MAX_TIME; - var MSIZE = Ld8k.MSIZE; - var NB_POS = Ld8k.NB_POS; - var STEP = Ld8k.STEP; - var THRESHFCB = Ld8k.THRESHFCB; - - /* - * The code length is 40, containing 4 nonzero pulses i0, i1, i2, i3. - * with pulse spacings of step = 5 - * Each pulses can have 8 possible positions (positive or negative): - * - * i0 (+-1) : 0, 5, 10, 15, 20, 25, 30, 35 - * i1 (+-1) : 1, 6, 11, 16, 21, 26, 31, 36 - * i2 (+-1) : 2, 7, 12, 17, 22, 27, 32, 37 - * i3 (+-1) : 3, 8, 13, 18, 23, 28, 33, 38 - * 4, 9, 14, 19, 24, 29, 34, 39 - *--------------------------------------------------------------------------- - */ - int i0, i1, i2, i3, ip0, ip1, ip2, ip3; - int i, j, time; - float ps0, ps1, ps2, ps3, alp0, alp1, alp2, alp3; - float ps3c, psc, alpha; - float average, max0, max1, max2, thres; - var p_sign = new float[L_SUBFR]; - - int rri0i0, rri1i1, rri2i2, rri3i3, rri4i4; - int rri0i1, rri0i2, rri0i3, rri0i4; - int rri1i2, rri1i3, rri1i4; - int rri2i3, rri2i4; - - int ptr_ri0i0, ptr_ri1i1, ptr_ri2i2, ptr_ri3i3, ptr_ri4i4; - int ptr_ri0i1, ptr_ri0i2, ptr_ri0i3, ptr_ri0i4; - int ptr_ri1i2, ptr_ri1i3, ptr_ri1i4; - int ptr_ri2i3, ptr_ri2i4; - - /* Init pointers */ - - rri0i0 = 0; - rri1i1 = rri0i0 + NB_POS; - rri2i2 = rri1i1 + NB_POS; - rri3i3 = rri2i2 + NB_POS; - rri4i4 = rri3i3 + NB_POS; - - rri0i1 = rri4i4 + NB_POS; - rri0i2 = rri0i1 + MSIZE; - rri0i3 = rri0i2 + MSIZE; - rri0i4 = rri0i3 + MSIZE; - rri1i2 = rri0i4 + MSIZE; - rri1i3 = rri1i2 + MSIZE; - rri1i4 = rri1i3 + MSIZE; - rri2i3 = rri1i4 + MSIZE; - rri2i4 = rri2i3 + MSIZE; - - /*-----------------------------------------------------------------------* - * Reset max_time for 1st subframe. * - *-----------------------------------------------------------------------* - */ - if (i_subfr == 0) extra = 30; - - /*----------------------------------------------------------------------* - * Chose the signs of the impulses. * - *-----------------------------------------------------------------------*/ - - for (i = 0; i < L_SUBFR; i++) - if (dn[i] >= 0.0f) - { - p_sign[i] = 1.0f; - } - else - { - p_sign[i] = -1.0f; - dn[i] = -dn[i]; - } - - /*-------------------------------------------------------------------* - * - Compute the search threshold after three pulses * - *-------------------------------------------------------------------*/ - - average = dn[0] + dn[1] + dn[2]; - max0 = dn[0]; - max1 = dn[1]; - max2 = dn[2]; - for (i = 5; i < L_SUBFR; i += STEP) + average += dn[i] + dn[i + 1] + dn[i + 2]; + if (dn[i] > max0) + { + max0 = dn[i]; + } + + if (dn[i + 1] > max1) + { + max1 = dn[i + 1]; + } + + if (dn[i + 2] > max2) { - average += dn[i] + dn[i + 1] + dn[i + 2]; - if (dn[i] > max0) max0 = dn[i]; - if (dn[i + 1] > max1) max1 = dn[i + 1]; - if (dn[i + 2] > max2) max2 = dn[i + 2]; + max2 = dn[i + 2]; } + } - max0 += max1 + max2; - average *= 0.125f; /* 1/8 */ - thres = average + (max0 - average) * THRESHFCB; + max0 += max1 + max2; + average *= 0.125f; /* 1/8 */ + thres = average + (max0 - average) * THRESHFCB; - /*-------------------------------------------------------------------* - * Modification of rrixiy to take into account signs. * - *-------------------------------------------------------------------*/ - ptr_ri0i1 = rri0i1; - ptr_ri0i2 = rri0i2; - ptr_ri0i3 = rri0i3; - ptr_ri0i4 = rri0i4; + /*-------------------------------------------------------------------* +* Modification of rrixiy to take into account signs. * +*-------------------------------------------------------------------*/ + ptr_ri0i1 = rri0i1; + ptr_ri0i2 = rri0i2; + ptr_ri0i3 = rri0i3; + ptr_ri0i4 = rri0i4; - for (i0 = 0; i0 < L_SUBFR; i0 += STEP) + for (i0 = 0; i0 < L_SUBFR; i0 += STEP) + { for (i1 = 1; i1 < L_SUBFR; i1 += STEP) { rr[ptr_ri0i1] *= p_sign[i0] * p_sign[i1]; @@ -623,12 +648,14 @@ int i_subfr rr[ptr_ri0i4] *= p_sign[i0] * p_sign[i1 + 3]; ptr_ri0i4++; } + } - ptr_ri1i2 = rri1i2; - ptr_ri1i3 = rri1i3; - ptr_ri1i4 = rri1i4; + ptr_ri1i2 = rri1i2; + ptr_ri1i3 = rri1i3; + ptr_ri1i4 = rri1i4; - for (i1 = 1; i1 < L_SUBFR; i1 += STEP) + for (i1 = 1; i1 < L_SUBFR; i1 += STEP) + { for (i2 = 2; i2 < L_SUBFR; i2 += STEP) { rr[ptr_ri1i2] *= p_sign[i1] * p_sign[i2]; @@ -638,11 +665,13 @@ int i_subfr rr[ptr_ri1i4] *= p_sign[i1] * p_sign[i2 + 2]; ptr_ri1i4++; } + } - ptr_ri2i3 = rri2i3; - ptr_ri2i4 = rri2i4; + ptr_ri2i3 = rri2i3; + ptr_ri2i4 = rri2i4; - for (i2 = 2; i2 < L_SUBFR; i2 += STEP) + for (i2 = 2; i2 < L_SUBFR; i2 += STEP) + { for (i3 = 3; i3 < L_SUBFR; i3 += STEP) { rr[ptr_ri2i3] *= p_sign[i2] * p_sign[i3]; @@ -650,205 +679,262 @@ int i_subfr rr[ptr_ri2i4] *= p_sign[i2] * p_sign[i3 + 1]; ptr_ri2i4++; } + } - /*-------------------------------------------------------------------* - * Search the optimum positions of the four pulses which maximize * - * square(correlation) / energy * - * The search is performed in four nested loops. At each loop, one * - * pulse contribution is added to the correlation and energy. * - * * - * The fourth loop is entered only if the correlation due to the * - * contribution of the first three pulses exceeds the preset * - * threshold. * - *-------------------------------------------------------------------*/ - - /* Default values */ - - ip0 = 0; - ip1 = 1; - ip2 = 2; - ip3 = 3; - psc = 0.0f; - alpha = 1000000.0f; - time = MAX_TIME + extra; - - /* Four loops to search innovation code. */ - ptr_ri0i0 = rri0i0; /* Init. pointers that depend on first loop */ - ptr_ri0i1 = rri0i1; - ptr_ri0i2 = rri0i2; - ptr_ri0i3 = rri0i3; - ptr_ri0i4 = rri0i4; - - for (i0 = 0; i0 < L_SUBFR; i0 += STEP) /* first pulse loop */ + /*-------------------------------------------------------------------* +* Search the optimum positions of the four pulses which maximize * +* square(correlation) / energy * +* The search is performed in four nested loops. At each loop, one * +* pulse contribution is added to the correlation and energy. * +* * +* The fourth loop is entered only if the correlation due to the * +* contribution of the first three pulses exceeds the preset * +* threshold. * +*-------------------------------------------------------------------*/ + + /* Default values */ + + ip0 = 0; + ip1 = 1; + ip2 = 2; + ip3 = 3; + psc = 0.0f; + alpha = 1000000.0f; + time = MAX_TIME + extra; + + /* Four loops to search innovation code. */ + ptr_ri0i0 = rri0i0; /* Init. pointers that depend on first loop */ + ptr_ri0i1 = rri0i1; + ptr_ri0i2 = rri0i2; + ptr_ri0i3 = rri0i3; + ptr_ri0i4 = rri0i4; + + for (i0 = 0; i0 < L_SUBFR; i0 += STEP) /* first pulse loop */ + { + ps0 = dn[i0]; + alp0 = rr[ptr_ri0i0]; + ptr_ri0i0++; + + ptr_ri1i1 = rri1i1; /* Init. pointers that depend on second loop */ + ptr_ri1i2 = rri1i2; + ptr_ri1i3 = rri1i3; + ptr_ri1i4 = rri1i4; + + for (i1 = 1; i1 < L_SUBFR; i1 += STEP) /* second pulse loop */ { - ps0 = dn[i0]; - alp0 = rr[ptr_ri0i0]; - ptr_ri0i0++; + ps1 = ps0 + dn[i1]; + alp1 = alp0 + rr[ptr_ri1i1] + 2.0f * rr[ptr_ri0i1]; + ptr_ri1i1++; + ptr_ri0i1++; - ptr_ri1i1 = rri1i1; /* Init. pointers that depend on second loop */ - ptr_ri1i2 = rri1i2; - ptr_ri1i3 = rri1i3; - ptr_ri1i4 = rri1i4; + ptr_ri2i2 = rri2i2; /* Init. pointers that depend on third loop */ + ptr_ri2i3 = rri2i3; + ptr_ri2i4 = rri2i4; - for (i1 = 1; i1 < L_SUBFR; i1 += STEP) /* second pulse loop */ + for (i2 = 2; i2 < L_SUBFR; i2 += STEP) { - ps1 = ps0 + dn[i1]; - alp1 = alp0 + rr[ptr_ri1i1] + 2.0f * rr[ptr_ri0i1]; - ptr_ri1i1++; - ptr_ri0i1++; + ps2 = ps1 + dn[i2]; + alp2 = alp1 + rr[ptr_ri2i2] + 2.0f * (rr[ptr_ri0i2] + rr[ptr_ri1i2]); + ptr_ri2i2++; + ptr_ri0i2++; + ptr_ri1i2++; - ptr_ri2i2 = rri2i2; /* Init. pointers that depend on third loop */ - ptr_ri2i3 = rri2i3; - ptr_ri2i4 = rri2i4; - - for (i2 = 2; i2 < L_SUBFR; i2 += STEP) + if (ps2 > thres) { - ps2 = ps1 + dn[i2]; - alp2 = alp1 + rr[ptr_ri2i2] + 2.0f * (rr[ptr_ri0i2] + rr[ptr_ri1i2]); - ptr_ri2i2++; - ptr_ri0i2++; - ptr_ri1i2++; + ptr_ri3i3 = rri3i3; /* Init. pointers that depend on 4th loop */ - if (ps2 > thres) + for (i3 = 3; i3 < L_SUBFR; i3 += STEP) { - ptr_ri3i3 = rri3i3; /* Init. pointers that depend on 4th loop */ - - for (i3 = 3; i3 < L_SUBFR; i3 += STEP) + ps3 = ps2 + dn[i3]; + alp3 = alp2 + rr[ptr_ri3i3] + 2.0f * (rr[ptr_ri1i3] + rr[ptr_ri0i3] + rr[ptr_ri2i3]); + ptr_ri3i3++; + ptr_ri1i3++; + ptr_ri0i3++; + ptr_ri2i3++; + + ps3c = ps3 * ps3; + if (ps3c * alpha > psc * alp3) { - ps3 = ps2 + dn[i3]; - alp3 = alp2 + rr[ptr_ri3i3] + 2.0f * (rr[ptr_ri1i3] + rr[ptr_ri0i3] + rr[ptr_ri2i3]); - ptr_ri3i3++; - ptr_ri1i3++; - ptr_ri0i3++; - ptr_ri2i3++; - - ps3c = ps3 * ps3; - if (ps3c * alpha > psc * alp3) - { - psc = ps3c; - alpha = alp3; - ip0 = i0; - ip1 = i1; - ip2 = i2; - ip3 = i3; - } - } /* end of for i3 = */ - - ptr_ri0i3 -= NB_POS; - ptr_ri1i3 -= NB_POS; - - ptr_ri4i4 = rri4i4; /* Init. pointers that depend on 4th loop */ - - for (i3 = 4; i3 < L_SUBFR; i3 += STEP) + psc = ps3c; + alpha = alp3; + ip0 = i0; + ip1 = i1; + ip2 = i2; + ip3 = i3; + } + } /* end of for i3 = */ + + ptr_ri0i3 -= NB_POS; + ptr_ri1i3 -= NB_POS; + + ptr_ri4i4 = rri4i4; /* Init. pointers that depend on 4th loop */ + + for (i3 = 4; i3 < L_SUBFR; i3 += STEP) + { + ps3 = ps2 + dn[i3]; + alp3 = alp2 + rr[ptr_ri4i4] + 2.0f * (rr[ptr_ri1i4] + rr[ptr_ri0i4] + rr[ptr_ri2i4]); + ptr_ri4i4++; + ptr_ri1i4++; + ptr_ri0i4++; + ptr_ri2i4++; + + ps3c = ps3 * ps3; + if (ps3c * alpha > psc * alp3) { - ps3 = ps2 + dn[i3]; - alp3 = alp2 + rr[ptr_ri4i4] + 2.0f * (rr[ptr_ri1i4] + rr[ptr_ri0i4] + rr[ptr_ri2i4]); - ptr_ri4i4++; - ptr_ri1i4++; - ptr_ri0i4++; - ptr_ri2i4++; - - ps3c = ps3 * ps3; - if (ps3c * alpha > psc * alp3) - { - psc = ps3c; - alpha = alp3; - ip0 = i0; - ip1 = i1; - ip2 = i2; - ip3 = i3; - } - } /* end of for i3 = */ - - ptr_ri0i4 -= NB_POS; - ptr_ri1i4 -= NB_POS; - - time--; - if (time <= 0) goto end_search; /* Maximum time finish */ - - } /* end of if >thres */ - else + psc = ps3c; + alpha = alp3; + ip0 = i0; + ip1 = i1; + ip2 = i2; + ip3 = i3; + } + } /* end of for i3 = */ + + ptr_ri0i4 -= NB_POS; + ptr_ri1i4 -= NB_POS; + + time--; + if (time <= 0) { - ptr_ri2i3 += NB_POS; - ptr_ri2i4 += NB_POS; + goto end_search; /* Maximum time finish */ } + } /* end of if >thres */ + else + { + ptr_ri2i3 += NB_POS; + ptr_ri2i4 += NB_POS; + } - } /* end of for i2 = */ + } /* end of for i2 = */ - ptr_ri0i2 -= NB_POS; - ptr_ri1i3 += NB_POS; - ptr_ri1i4 += NB_POS; + ptr_ri0i2 -= NB_POS; + ptr_ri1i3 += NB_POS; + ptr_ri1i4 += NB_POS; - } /* end of for i1 = */ + } /* end of for i1 = */ - ptr_ri0i2 += NB_POS; - ptr_ri0i3 += NB_POS; - ptr_ri0i4 += NB_POS; + ptr_ri0i2 += NB_POS; + ptr_ri0i3 += NB_POS; + ptr_ri0i4 += NB_POS; - } /* end of for i0 = */ + } /* end of for i0 = */ - end_search: + end_search: - extra = time; + extra = time; - /* Find the codeword corresponding to the selected positions */ + /* Find the codeword corresponding to the selected positions */ - for (i = 0; i < L_SUBFR; i++) cod[i] = 0.0f; - cod[ip0] = p_sign[ip0]; - cod[ip1] = p_sign[ip1]; - cod[ip2] = p_sign[ip2]; - cod[ip3] = p_sign[ip3]; + for (i = 0; i < L_SUBFR; i++) + { + cod[i] = 0.0f; + } - /* find the filtered codeword */ + cod[ip0] = p_sign[ip0]; + cod[ip1] = p_sign[ip1]; + cod[ip2] = p_sign[ip2]; + cod[ip3] = p_sign[ip3]; - for (i = 0; i < L_SUBFR; i++) y[i] = 0.0f; + /* find the filtered codeword */ - if (p_sign[ip0] > 0.0f) - for (i = ip0, j = 0; i < L_SUBFR; i++, j++) - y[i] = h[j]; - else - for (i = ip0, j = 0; i < L_SUBFR; i++, j++) - y[i] = -h[j]; + for (i = 0; i < L_SUBFR; i++) + { + y[i] = 0.0f; + } - if (p_sign[ip1] > 0.0f) - for (i = ip1, j = 0; i < L_SUBFR; i++, j++) - y[i] = y[i] + h[j]; - else - for (i = ip1, j = 0; i < L_SUBFR; i++, j++) - y[i] = y[i] - h[j]; + if (p_sign[ip0] > 0.0f) + { + for (i = ip0, j = 0; i < L_SUBFR; i++, j++) + { + y[i] = h[j]; + } + } + else + { + for (i = ip0, j = 0; i < L_SUBFR; i++, j++) + { + y[i] = -h[j]; + } + } - if (p_sign[ip2] > 0.0f) - for (i = ip2, j = 0; i < L_SUBFR; i++, j++) - y[i] = y[i] + h[j]; - else - for (i = ip2, j = 0; i < L_SUBFR; i++, j++) - y[i] = y[i] - h[j]; + if (p_sign[ip1] > 0.0f) + { + for (i = ip1, j = 0; i < L_SUBFR; i++, j++) + { + y[i] = y[i] + h[j]; + } + } + else + { + for (i = ip1, j = 0; i < L_SUBFR; i++, j++) + { + y[i] = y[i] - h[j]; + } + } - if (p_sign[ip3] > 0.0f) - for (i = ip3, j = 0; i < L_SUBFR; i++, j++) - y[i] = y[i] + h[j]; - else - for (i = ip3, j = 0; i < L_SUBFR; i++, j++) - y[i] = y[i] - h[j]; + if (p_sign[ip2] > 0.0f) + { + for (i = ip2, j = 0; i < L_SUBFR; i++, j++) + { + y[i] = y[i] + h[j]; + } + } + else + { + for (i = ip2, j = 0; i < L_SUBFR; i++, j++) + { + y[i] = y[i] - h[j]; + } + } + + if (p_sign[ip3] > 0.0f) + { + for (i = ip3, j = 0; i < L_SUBFR; i++, j++) + { + y[i] = y[i] + h[j]; + } + } + else + { + for (i = ip3, j = 0; i < L_SUBFR; i++, j++) + { + y[i] = y[i] - h[j]; + } + } - /* find codebook index; 4 bit signs + 13 bit positions */ + /* find codebook index; 4 bit signs + 13 bit positions */ - i = 0; - if (p_sign[ip0] > 0.0f) i += 1; - if (p_sign[ip1] > 0.0f) i += 2; - if (p_sign[ip2] > 0.0f) i += 4; - if (p_sign[ip3] > 0.0f) i += 8; - signs.value = i; + i = 0; + if (p_sign[ip0] > 0.0f) + { + i += 1; + } - ip0 = ip0 / 5; - ip1 = ip1 / 5; - ip2 = ip2 / 5; - j = ip3 % 5 - 3; - ip3 = ((ip3 / 5) << 1) + j; + if (p_sign[ip1] > 0.0f) + { + i += 2; + } - i = ip0 + (ip1 << 3) + (ip2 << 6) + (ip3 << 9); + if (p_sign[ip2] > 0.0f) + { + i += 4; + } - return i; + if (p_sign[ip3] > 0.0f) + { + i += 8; } + + signs.value = i; + + ip0 = ip0 / 5; + ip1 = ip1 / 5; + ip2 = ip2 / 5; + j = ip3 % 5 - 3; + ip3 = ((ip3 / 5) << 1) + j; + + i = ip0 + (ip1 << 3) + (ip2 << 6) + (ip3 << 9); + + return i; } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Bits.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Bits.cs index 35efe410db..38abdddea7 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Bits.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Bits.cs @@ -19,10 +19,12 @@ * SIPRO Lab Telecom. */ +using System; + /** * Bit stream manipulation routines. *

prm2bits_ld8k -converts encoder parameter vector into vector of serial bits

- *

bits2prm_ld8k - converts serial received bits to encoder parameter vector

+ *

bits2prm_ld8k - converts serial received bits to encoder parameter vector

*
  * The transmitted parameters for 8000 bits/sec are:
  *
@@ -45,164 +47,105 @@
  *
  * @author Lubomir Marinov (translation of ITU-T C source code to Java)
  */
-namespace SIPSorcery.Media.G729Codec
+namespace SIPSorcery.Media.G729Codec;
+
+internal static class Bits
 {
-    internal class Bits
-    {
 
-        /* ITU-T G.729 Software Package Release 2 (November 2006) */
-        /*
-   ITU-T G.729 Annex C - Reference C code for floating point
-                         implementation of G.729
-                         Version 1.01 of 15.September.98
+    /* ITU-T G.729 Software Package Release 2 (November 2006) */
+    /*
+ITU-T G.729 Annex C - Reference C code for floating point
+                     implementation of G.729
+                     Version 1.01 of 15.September.98
 */
 
-        /*
+    /*
 ----------------------------------------------------------------------
-                    COPYRIGHT NOTICE
+                COPYRIGHT NOTICE
 ----------------------------------------------------------------------
-   ITU-T G.729 Annex C ANSI C source code
-   Copyright (C) 1998, AT&T, France Telecom, NTT, University of
-   Sherbrooke.  All rights reserved.
+ITU-T G.729 Annex C ANSI C source code
+Copyright (C) 1998, AT&T, France Telecom, NTT, University of
+Sherbrooke.  All rights reserved.
 
 ----------------------------------------------------------------------
 */
 
-        /*
- File : BITS.C
- Used for the floating point version of both
- G.729 main body and G.729A
+    /*
+File : BITS.C
+Used for the floating point version of both
+G.729 main body and G.729A
 */
 
-        /**
- * Converts encoder parameter vector into vector of serial bits.
- *
- * @param prm        input : encoded parameters
- * @param bits       output: serial bits
- */
+    /// 
+    /// Converts encoder parameter vector into vector of serial bits using spans.
+    /// 
+    /// Input: encoded parameters as ReadOnlySpan
+    /// Output: serial bits as Span
+    public static void prm2bits_ld8k(
+        ReadOnlySpan prm,
+        Span bits
+    )
+    {
+        var j = 0;
+        bits[j++] = Ld8k.SYNC_WORD; // At receiver this bit indicates BFI
+        bits[j++] = Ld8k.SIZE_WORD; // Number of bits in this frame
 
-        public static void prm2bits_ld8k(
-            int[] prm,
-            short[] bits
-        )
+        for (var i = 0; i < Ld8k.PRM_SIZE; i++)
         {
-            var PRM_SIZE = Ld8k.PRM_SIZE;
-            var SIZE_WORD = Ld8k.SIZE_WORD;
-            var SYNC_WORD = Ld8k.SYNC_WORD;
-            var bitsno = TabLd8k.bitsno;
-
-            int j = 0, i;
-            bits[j] = SYNC_WORD; /* At receiver this bit indicates BFI */
-            j++;
-            bits[j] = SIZE_WORD; /* Number of bits in this frame       */
-            j++;
-
-            for (i = 0; i < PRM_SIZE; i++)
-            {
-                int2bin(prm[i], bitsno[i], bits, j);
-                j += bitsno[i];
-            }
+            int2bin(prm[i], TabLd8k.bitsno[i], bits, j);
+            j += TabLd8k.bitsno[i];
         }
 
-        /**
- * Convert integer to binary and write the bits bitstream array.
- *
- * @param value             input : decimal value
- * @param no_of_bits        input : number of bits to use
- * @param bitstream         output: bitstream
- * @param bitstream_offset  input: bitstream offset
- */
-        private static void int2bin(
+        static void int2bin(
             int value,
             int no_of_bits,
-            short[] bitstream,
+            Span bitstream,
             int bitstream_offset
         )
         {
-            var BIT_0 = Ld8k.BIT_0;
-            var BIT_1 = Ld8k.BIT_1;
-
-            int pt_bitstream;
-            int i, bit;
-
-            pt_bitstream = bitstream_offset + no_of_bits;
-
-            for (i = 0; i < no_of_bits; i++)
+            var pt_bitstream = bitstream_offset + no_of_bits;
+            for (var i = 0; i < no_of_bits; i++)
             {
-                bit = value & 0x0001; /* get lsb */
-                if (bit == 0)
-                    bitstream[--pt_bitstream] = BIT_0;
-                else
-                    bitstream[--pt_bitstream] = BIT_1;
+                var bit = value & 0x0001; // get lsb
+                bitstream[--pt_bitstream] = bit == 0 ? Ld8k.BIT_0 : Ld8k.BIT_1;
                 value >>= 1;
             }
         }
+    }
 
-        /**
- * Converts serial received bits to  encoder parameter vector.
- *
- * @param bits  input : serial bits
- * @param prm   output: decoded parameters
- */
-        private static void bits2prm_ld8k(short[] bits, int[] prm)
-        {
-            bits2prm_ld8k(bits, 0, prm, 0);
-        }
-
-        /**
- * Converts serial received bits to  encoder parameter vector.
- *
- * @param bits           input : serial bits
- * @param bits_offset    input : serial bits offset
- * @param prm            output: decoded parameters
- * @param prm_offset     input: decoded parameters offset
- */
-        public static void bits2prm_ld8k(
-            short[] bits,
-            int bits_offset,
-            int[] prm,
-            int prm_offset
-        )
+    /// 
+    /// Span-based version: Converts serial received bits to encoder parameter vector.
+    /// 
+    /// Input: serial bits as ReadOnlySpan
+    /// Input: serial bits offset
+    /// Output: decoded parameters
+    /// Input: decoded parameters offset
+    public static void bits2prm_ld8k(
+        ReadOnlySpan bits,
+        int bits_offset,
+        Span prm,
+        int prm_offset
+    )
+    {
+        for (var i = 0; i < Ld8k.PRM_SIZE; i++)
         {
-            var PRM_SIZE = Ld8k.PRM_SIZE;
-            var bitsno = TabLd8k.bitsno;
-
-            int i;
-            for (i = 0; i < PRM_SIZE; i++)
-            {
-                prm[i + prm_offset] = bin2int(bitsno[i], bits, bits_offset);
-                bits_offset += bitsno[i];
-            }
+            var bitCount = TabLd8k.bitsno[i];
+            prm[i + prm_offset] = bin2int(bits.Slice(bits_offset, bitCount));
+            bits_offset += bitCount;
         }
 
-        /**
- * Read specified bits from bit array  and convert to integer value.
- *
- * @param no_of_bits        input : number of bits to read
- * @param bitstream         input : array containing bits
- * @param bitstream_offset  input : array offset
- * @return                   decimal value of bit pattern
- */
-        private static int bin2int(
-            int no_of_bits,
-            short[] bitstream,
-            int bitstream_offset
-        )
+        static int bin2int(ReadOnlySpan bitstream)
         {
-            var BIT_1 = Ld8k.BIT_1;
-
-            int value, i;
-            int bit;
-
-            value = 0;
-            for (i = 0; i < no_of_bits; i++)
+            var value = 0;
+            for (var i = 0; i < bitstream.Length; i++)
             {
                 value <<= 1;
-                bit = bitstream[bitstream_offset++];
-                if (bit == BIT_1) value += 1;
+                if (bitstream[i] == Ld8k.BIT_1)
+                {
+                    value |= 1;
+                }
             }
-
             return value;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/CodLd8k.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/CodLd8k.cs
index f6be7031ef..346bebc8b4 100644
--- a/src/SIPSorcery/app/Media/Codecs/G729Codec/CodLd8k.cs
+++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/CodLd8k.cs
@@ -19,6 +19,9 @@
  * SIPRO Lab Telecom.
  */
 
+
+using System.Diagnostics;
+
 /**
  * Functions coder_ld8k and init_coder_ld8k
  *    Coder constant parameters (defined in "ld8k.h")
@@ -37,510 +40,541 @@
  *
  * @author Lubomir Marinov (translation of ITU-T C source code to Java)
  */
-namespace SIPSorcery.Media.G729Codec
-{
-    internal class CodLd8k : Ld8k
-    {
+namespace SIPSorcery.Media.G729Codec;
 
-        private readonly AcelpCo acelpCo = new AcelpCo();
+internal sealed class CodLd8k : Ld8k
+{
+    private readonly AcelpCo acelpCo = new AcelpCo();
 
-        /* Zero vector */
+    /* Zero vector */
 
-        private readonly float[] ai_zero = new float[L_SUBFR + MP1];
+    private readonly float[] ai_zero = new float[L_SUBFR + MP1];
 
-        private float[] error;
+    private float[]? error;
 
-        private int error_offset;
+    private int error_offset;
 
-        private float[] exc;
+    private float[]? exc;
 
-        private int exc_offset;
+    private int exc_offset;
 
-        /* Lsp (Line spectral pairs) */
-        private readonly float[ /* M */] lsp_old =
-        {
-            0.9595f,
-            0.8413f,
-            0.6549f,
-            0.4154f,
-            0.1423f,
-            -0.1423f,
-            -0.4154f,
-            -0.6549f,
-            -0.8413f,
-            -0.9595f
-        };
+    /* Lsp (Line spectral pairs) */
+    private readonly float[ /* M */] lsp_old =
+    {
+        0.9595f,
+        0.8413f,
+        0.6549f,
+        0.4154f,
+        0.1423f,
+        -0.1423f,
+        -0.4154f,
+        -0.6549f,
+        -0.8413f,
+        -0.9595f
+    };
 
-        private readonly float[] lsp_old_q = new float[M];
+    private readonly float[] lsp_old_q = new float[M];
 
-        private readonly float[] mem_err = new float[M + L_SUBFR];
+    private readonly float[] mem_err = new float[M + L_SUBFR];
 
-        /* Filter's memory */
+    /* Filter's memory */
 
-        private readonly float[] mem_syn = new float[M];
+    private readonly float[] mem_syn = new float[M];
 
-        private readonly float[] mem_w = new float[M];
+    private readonly float[] mem_w = new float[M];
 
-        private readonly float[] mem_w0 = new float[M];
+    private readonly float[] mem_w0 = new float[M];
 
-        public float[] new_speech;
+    public float[]? new_speech;
 
-        public int new_speech_offset;
+    public int new_speech_offset;
 
-        /* Excitation vector */
+    /* Excitation vector */
 
-        private readonly float[] old_exc = new float[L_FRAME + PIT_MAX + L_INTERPOL];
+    private readonly float[] old_exc = new float[L_FRAME + PIT_MAX + L_INTERPOL];
 
-        /* ITU-T G.729 Software Package Release 2 (November 2006) */
-        /*
-   ITU-T G.729 Annex C - Reference C code for floating point
-                         implementation of G.729
-                         Version 1.01 of 15.September.98
+    /* ITU-T G.729 Software Package Release 2 (November 2006) */
+    /*
+ITU-T G.729 Annex C - Reference C code for floating point
+                     implementation of G.729
+                     Version 1.01 of 15.September.98
 */
 
-        /*
+    /*
 ----------------------------------------------------------------------
-                    COPYRIGHT NOTICE
+                COPYRIGHT NOTICE
 ----------------------------------------------------------------------
-   ITU-T G.729 Annex C ANSI C source code
-   Copyright (C) 1998, AT&T, France Telecom, NTT, University of
-   Sherbrooke.  All rights reserved.
+ITU-T G.729 Annex C ANSI C source code
+Copyright (C) 1998, AT&T, France Telecom, NTT, University of
+Sherbrooke.  All rights reserved.
 
 ----------------------------------------------------------------------
 */
 
-        /*
- File : COD_LD8K.C
- Used for the floating point version of G.729 main body
- (not for G.729A)
+    /*
+File : COD_LD8K.C
+Used for the floating point version of G.729 main body
+(not for G.729A)
 */
 
-        /*--------------------------------------------------------*
-  *         Static memory allocation.                      *
-  *--------------------------------------------------------*/
+    /*--------------------------------------------------------*
+*         Static memory allocation.                      *
+*--------------------------------------------------------*/
 
-        /* Speech vector */
-        private readonly float[] old_speech = new float[L_TOTAL];
+    /* Speech vector */
+    private readonly float[] old_speech = new float[L_TOTAL];
 
-        /* Weighted speech vector */
+    /* Weighted speech vector */
 
-        private readonly float[] old_wsp = new float[L_FRAME + PIT_MAX];
+    private readonly float[] old_wsp = new float[L_FRAME + PIT_MAX];
 
-        private float[] p_window;
+    private float[]? p_window;
 
-        private int p_window_offset;
+    private int p_window_offset;
 
-        private readonly Pwf pwf = new Pwf();
+    private readonly Pwf pwf = new Pwf();
 
-        private readonly QuaGain quaGain = new QuaGain();
+    private readonly QuaGain quaGain = new QuaGain();
 
-        private readonly QuaLsp quaLsp = new QuaLsp();
+    private readonly QuaLsp quaLsp = new QuaLsp();
 
-        private float sharp;
+    private float sharp;
 
-        private float[] speech;
+    private float[]? speech;
 
-        private int speech_offset;
+    private int speech_offset;
 
-        private readonly Taming taming = new Taming();
+    private readonly Taming taming = new Taming();
 
-        private float[] wsp;
+    private float[]? wsp;
 
-        private int wsp_offset;
+    private int wsp_offset;
 
-        private float[] zero;
+    private float[]? zero;
 
-        private int zero_offset;
+    private int zero_offset;
 
-        /**
- * Initialization of variables for the encoder.
- * Initialize pointers to speech vector.
- *
 
- *   |   <------------  LPC analysis window (L_WINDOW)  ----------->
- *   |   |               <-- present frame (L_FRAME) -->
- * old_speech            |              <-- new speech (L_FRAME) -->
- *     p_wind            |              |
- *                     speech           |
- *                             new_speech
- * ]]>
- */ + /** +* Initialization of variables for the encoder. +* Initialize pointers to speech vector. +*
 
+*   |   <------------  LPC analysis window (L_WINDOW)  ----------->
+*   |   |               <-- present frame (L_FRAME) -->
+* old_speech            |              <-- new speech (L_FRAME) -->
+*     p_wind            |              |
+*                     speech           |
+*                             new_speech
+* ]]>
+*/ - public void init_coder_ld8k() - { + public void init_coder_ld8k() + { - new_speech = old_speech; - new_speech_offset = L_TOTAL - L_FRAME; /* New speech */ - speech = new_speech; /* Present frame */ - speech_offset = new_speech_offset - L_NEXT; - p_window = old_speech; - p_window_offset = L_TOTAL - L_WINDOW; /* For LPC window */ - - /* Initialize static pointers */ - - wsp = old_wsp; - wsp_offset = PIT_MAX; - exc = old_exc; - exc_offset = PIT_MAX + L_INTERPOL; - zero = ai_zero; - zero_offset = MP1; - error = mem_err; - error_offset = M; - - /* Static vectors to zero */ - Util.set_zero(old_speech, L_TOTAL); - Util.set_zero(old_exc, PIT_MAX + L_INTERPOL); - Util.set_zero(old_wsp, PIT_MAX); - Util.set_zero(mem_syn, M); - Util.set_zero(mem_w, M); - Util.set_zero(mem_w0, M); - Util.set_zero(mem_err, M); - Util.set_zero(zero, zero_offset, L_SUBFR); - sharp = SHARPMIN; - - /* Initialize lsp_old_q[] */ - Util.copy(lsp_old, lsp_old_q, M); - - quaLsp.lsp_encw_reset(); - taming.init_exc_err(); - } + new_speech = old_speech; + new_speech_offset = L_TOTAL - L_FRAME; /* New speech */ + speech = new_speech; /* Present frame */ + speech_offset = new_speech_offset - L_NEXT; + p_window = old_speech; + p_window_offset = L_TOTAL - L_WINDOW; /* For LPC window */ + + /* Initialize static pointers */ + + wsp = old_wsp; + wsp_offset = PIT_MAX; + exc = old_exc; + exc_offset = PIT_MAX + L_INTERPOL; + zero = ai_zero; + zero_offset = MP1; + error = mem_err; + error_offset = M; + + /* Static vectors to zero */ + Util.set_zero(old_speech, L_TOTAL); + Util.set_zero(old_exc, PIT_MAX + L_INTERPOL); + Util.set_zero(old_wsp, PIT_MAX); + Util.set_zero(mem_syn, M); + Util.set_zero(mem_w, M); + Util.set_zero(mem_w0, M); + Util.set_zero(mem_err, M); + Util.set_zero(zero, zero_offset, L_SUBFR); + sharp = SHARPMIN; + + /* Initialize lsp_old_q[] */ + Util.copy(lsp_old, lsp_old_q, M); + + quaLsp.lsp_encw_reset(); + taming.init_exc_err(); + } - /** - * Encoder routine ( speech data should be in new_speech ). - * - * @param ana output: analysis parameters - */ + /** +* Encoder routine ( speech data should be in new_speech ). +* +* @param ana output: analysis parameters +*/ - public void coder_ld8k( - int[] ana - ) + public void coder_ld8k( + int[] ana + ) + { + /* LPC coefficients */ + var r = new float[MP1]; /* Autocorrelations low and hi */ + var A_t = new float[MP1 * 2]; /* A(z) unquantized for the 2 subframes */ + var Aq_t = new float[MP1 * 2]; /* A(z) quantized for the 2 subframes */ + var Ap1 = new float[MP1]; /* A(z) with spectral expansion */ + var Ap2 = new float[MP1]; /* A(z) with spectral expansion */ + float[] A, Aq; /* Pointer on A_t and Aq_t */ + int A_offset, Aq_offset; + + /* LSP coefficients */ + float[] lsp_new = new float[M], lsp_new_q = new float[M]; /* LSPs at 2th subframe */ + var lsf_int = new float[M]; /* Interpolated LSF 1st subframe. */ + var lsf_new = new float[M]; + + /* Variable added for adaptive gamma1 and gamma2 of the PWF */ + + var rc = new float[M]; /* Reflection coefficients */ + var gamma1 = new float[2]; /* Gamma1 for 1st and 2nd subframes */ + var gamma2 = new float[2]; /* Gamma2 for 1st and 2nd subframes */ + + /* Other vectors */ + var synth = new float[L_FRAME]; /* Buffer for synthesis speech */ + var h1 = new float[L_SUBFR]; /* Impulse response h1[] */ + var xn = new float[L_SUBFR]; /* Target vector for pitch search */ + var xn2 = new float[L_SUBFR]; /* Target vector for codebook search */ + var code = new float[L_SUBFR]; /* Fixed codebook excitation */ + var y1 = new float[L_SUBFR]; /* Filtered adaptive excitation */ + var y2 = new float[L_SUBFR]; /* Filtered fixed codebook excitation */ + var g_coeff = new float[5]; /* Correlations between xn, y1, & y2: + , , , ,*/ + + /* Scalars */ + + int i, j, i_gamma, i_subfr; + var iRef = new IntReference(); + int T_op, t0; + IntReference t0_min = new IntReference(), t0_max = new IntReference(), t0_frac = new IntReference(); + int index, taming; + float gain_pit, gain_code = 0.0f; + FloatReference _gain_pit = new FloatReference(), _gain_code = new FloatReference(); + + var ana_offset = 0; + + /*------------------------------------------------------------------------* +* - Perform LPC analysis: * +* * autocorrelation + lag windowing * +* * Levinson-durbin algorithm to find a[] * +* * convert a[] to lsp[] * +* * quantize and code the LSPs * +* * find the interpolated LSPs and convert to a[] for the 2 * +* subframes (both quantized and unquantized) * +*------------------------------------------------------------------------*/ + + /* LP analysis */ + + Debug.Assert(p_window is { }); + + Lpc.autocorr(p_window, p_window_offset, M, r); /* Autocorrelations */ + Lpc.lag_window(M, r); /* Lag windowing */ + Lpc.levinson(r, A_t, MP1, rc); /* Levinson Durbin */ + Lpc.az_lsp(A_t, MP1, lsp_new, lsp_old); /* From A(z) to lsp */ + + /* LSP quantization */ + + quaLsp.qua_lsp(lsp_new, lsp_new_q, ana); + ana_offset += 2; /* Advance analysis parameters pointer */ + + /*--------------------------------------------------------------------* +* Find interpolated LPC parameters in all subframes (both quantized * +* and unquantized). * +* The interpolated parameters are in array A_t[] of size (M+1)*4 * +* and the quantized interpolated parameters are in array Aq_t[] * +*--------------------------------------------------------------------*/ + + Lpcfunc.int_lpc(lsp_old, lsp_new, lsf_int, lsf_new, A_t); + Lpcfunc.int_qlpc(lsp_old_q, lsp_new_q, Aq_t); + + /* update the LSPs for the next frame */ + + for (i = 0; i < M; i++) { - /* LPC coefficients */ - var r = new float[MP1]; /* Autocorrelations low and hi */ - var A_t = new float[MP1 * 2]; /* A(z) unquantized for the 2 subframes */ - var Aq_t = new float[MP1 * 2]; /* A(z) quantized for the 2 subframes */ - var Ap1 = new float[MP1]; /* A(z) with spectral expansion */ - var Ap2 = new float[MP1]; /* A(z) with spectral expansion */ - float[] A, Aq; /* Pointer on A_t and Aq_t */ - int A_offset, Aq_offset; - - /* LSP coefficients */ - float[] lsp_new = new float[M], lsp_new_q = new float[M]; /* LSPs at 2th subframe */ - var lsf_int = new float[M]; /* Interpolated LSF 1st subframe. */ - var lsf_new = new float[M]; - - /* Variable added for adaptive gamma1 and gamma2 of the PWF */ - - var rc = new float[M]; /* Reflection coefficients */ - var gamma1 = new float[2]; /* Gamma1 for 1st and 2nd subframes */ - var gamma2 = new float[2]; /* Gamma2 for 1st and 2nd subframes */ - - /* Other vectors */ - var synth = new float[L_FRAME]; /* Buffer for synthesis speech */ - var h1 = new float[L_SUBFR]; /* Impulse response h1[] */ - var xn = new float[L_SUBFR]; /* Target vector for pitch search */ - var xn2 = new float[L_SUBFR]; /* Target vector for codebook search */ - var code = new float[L_SUBFR]; /* Fixed codebook excitation */ - var y1 = new float[L_SUBFR]; /* Filtered adaptive excitation */ - var y2 = new float[L_SUBFR]; /* Filtered fixed codebook excitation */ - var g_coeff = new float[5]; /* Correlations between xn, y1, & y2: - , , , ,*/ - - /* Scalars */ - - int i, j, i_gamma, i_subfr; - var iRef = new IntReference(); - int T_op, t0; - IntReference t0_min = new IntReference(), t0_max = new IntReference(), t0_frac = new IntReference(); - int index, taming; - float gain_pit, gain_code = 0.0f; - FloatReference _gain_pit = new FloatReference(), _gain_code = new FloatReference(); - - var ana_offset = 0; - - /*------------------------------------------------------------------------* - * - Perform LPC analysis: * - * * autocorrelation + lag windowing * - * * Levinson-durbin algorithm to find a[] * - * * convert a[] to lsp[] * - * * quantize and code the LSPs * - * * find the interpolated LSPs and convert to a[] for the 2 * - * subframes (both quantized and unquantized) * - *------------------------------------------------------------------------*/ - - /* LP analysis */ - - Lpc.autocorr(p_window, p_window_offset, M, r); /* Autocorrelations */ - Lpc.lag_window(M, r); /* Lag windowing */ - Lpc.levinson(r, A_t, MP1, rc); /* Levinson Durbin */ - Lpc.az_lsp(A_t, MP1, lsp_new, lsp_old); /* From A(z) to lsp */ + lsp_old[i] = lsp_new[i]; + lsp_old_q[i] = lsp_new_q[i]; + } - /* LSP quantization */ + /*----------------------------------------------------------------------* +* - Find the weighting factors * +*----------------------------------------------------------------------*/ - quaLsp.qua_lsp(lsp_new, lsp_new_q, ana); - ana_offset += 2; /* Advance analysis parameters pointer */ + pwf.perc_var(gamma1, gamma2, lsf_int, lsf_new, rc); - /*--------------------------------------------------------------------* - * Find interpolated LPC parameters in all subframes (both quantized * - * and unquantized). * - * The interpolated parameters are in array A_t[] of size (M+1)*4 * - * and the quantized interpolated parameters are in array Aq_t[] * - *--------------------------------------------------------------------*/ + /*----------------------------------------------------------------------* +* - Find the weighted input speech w_sp[] for the whole speech frame * +* - Find the open-loop pitch delay for the whole speech frame * +* - Set the range for searching closed-loop pitch in 1st subframe * +*----------------------------------------------------------------------*/ - Lpcfunc.int_lpc(lsp_old, lsp_new, lsf_int, lsf_new, A_t); - Lpcfunc.int_qlpc(lsp_old_q, lsp_new_q, Aq_t); + Debug.Assert(speech is { }); + Debug.Assert(wsp is { }); - /* update the LSPs for the next frame */ + Lpcfunc.weight_az(A_t, 0, gamma1[0], M, Ap1); + Lpcfunc.weight_az(A_t, 0, gamma2[0], M, Ap2); + Filter.residu(Ap1, 0, speech, speech_offset, wsp, wsp_offset, L_SUBFR); + Filter.syn_filt(Ap2, 0, wsp, wsp_offset, wsp, wsp_offset, L_SUBFR, mem_w, 0, 1); - for (i = 0; i < M; i++) - { - lsp_old[i] = lsp_new[i]; - lsp_old_q[i] = lsp_new_q[i]; - } + Lpcfunc.weight_az(A_t, MP1, gamma1[1], M, Ap1); + Lpcfunc.weight_az(A_t, MP1, gamma2[1], M, Ap2); + Filter.residu(Ap1, 0, speech, speech_offset + L_SUBFR, wsp, wsp_offset + L_SUBFR, L_SUBFR); + Filter.syn_filt(Ap2, 0, wsp, wsp_offset + L_SUBFR, wsp, wsp_offset + L_SUBFR, L_SUBFR, mem_w, 0, 1); - /*----------------------------------------------------------------------* - * - Find the weighting factors * - *----------------------------------------------------------------------*/ + /* Find open loop pitch lag for whole speech frame */ - pwf.perc_var(gamma1, gamma2, lsf_int, lsf_new, rc); + T_op = Pitch.pitch_ol(wsp, wsp_offset, PIT_MIN, PIT_MAX, L_FRAME); - /*----------------------------------------------------------------------* - * - Find the weighted input speech w_sp[] for the whole speech frame * - * - Find the open-loop pitch delay for the whole speech frame * - * - Set the range for searching closed-loop pitch in 1st subframe * - *----------------------------------------------------------------------*/ + /* range for closed loop pitch search in 1st subframe */ - Lpcfunc.weight_az(A_t, 0, gamma1[0], M, Ap1); - Lpcfunc.weight_az(A_t, 0, gamma2[0], M, Ap2); - Filter.residu(Ap1, 0, speech, speech_offset, wsp, wsp_offset, L_SUBFR); - Filter.syn_filt(Ap2, 0, wsp, wsp_offset, wsp, wsp_offset, L_SUBFR, mem_w, 0, 1); + t0_min.value = T_op - 3; + if (t0_min.value < PIT_MIN) + { + t0_min.value = PIT_MIN; + } - Lpcfunc.weight_az(A_t, MP1, gamma1[1], M, Ap1); - Lpcfunc.weight_az(A_t, MP1, gamma2[1], M, Ap2); - Filter.residu(Ap1, 0, speech, speech_offset + L_SUBFR, wsp, wsp_offset + L_SUBFR, L_SUBFR); - Filter.syn_filt(Ap2, 0, wsp, wsp_offset + L_SUBFR, wsp, wsp_offset + L_SUBFR, L_SUBFR, mem_w, 0, 1); + t0_max.value = t0_min.value + 6; + if (t0_max.value > PIT_MAX) + { + t0_max.value = PIT_MAX; + t0_min.value = t0_max.value - 6; + } - /* Find open loop pitch lag for whole speech frame */ + /*------------------------------------------------------------------------* +* Loop for every subframe in the analysis frame * +*------------------------------------------------------------------------* +* To find the pitch and innovation parameters. The subframe size is * +* L_SUBFR and the loop is repeated L_FRAME/L_SUBFR times. * +* - find the weighted LPC coefficients * +* - find the LPC residual signal * +* - compute the target signal for pitch search * +* - compute impulse response of weighted synthesis filter (h1[]) * +* - find the closed-loop pitch parameters * +* - encode the pitch delay * +* - update the impulse response h1[] by including fixed-gain pitch * +* - find target vector for codebook search * +* - codebook search * +* - encode codebook address * +* - VQ of pitch and codebook gains * +* - find synthesis speech * +* - update states of weighting filter * +*------------------------------------------------------------------------*/ + + A = A_t; /* pointer to interpolated LPC parameters */ + A_offset = 0; + Aq = Aq_t; /* pointer to interpolated quantized LPC parameters */ + Aq_offset = 0; + + i_gamma = 0; + + for (i_subfr = 0; i_subfr < L_FRAME; i_subfr += L_SUBFR) + { + /*---------------------------------------------------------------* +* Find the weighted LPC coefficients for the weighting filter. * +*---------------------------------------------------------------*/ - T_op = Pitch.pitch_ol(wsp, wsp_offset, PIT_MIN, PIT_MAX, L_FRAME); + Lpcfunc.weight_az(A, A_offset, gamma1[i_gamma], M, Ap1); + Lpcfunc.weight_az(A, A_offset, gamma2[i_gamma], M, Ap2); + i_gamma++; - /* range for closed loop pitch search in 1st subframe */ + /*---------------------------------------------------------------* +* Compute impulse response, h1[], of weighted synthesis filter * +*---------------------------------------------------------------*/ - t0_min.value = T_op - 3; - if (t0_min.value < PIT_MIN) t0_min.value = PIT_MIN; - t0_max.value = t0_min.value + 6; - if (t0_max.value > PIT_MAX) + for (i = 0; i <= M; i++) { - t0_max.value = PIT_MAX; - t0_min.value = t0_max.value - 6; + ai_zero[i] = Ap1[i]; } - /*------------------------------------------------------------------------* - * Loop for every subframe in the analysis frame * - *------------------------------------------------------------------------* - * To find the pitch and innovation parameters. The subframe size is * - * L_SUBFR and the loop is repeated L_FRAME/L_SUBFR times. * - * - find the weighted LPC coefficients * - * - find the LPC residual signal * - * - compute the target signal for pitch search * - * - compute impulse response of weighted synthesis filter (h1[]) * - * - find the closed-loop pitch parameters * - * - encode the pitch delay * - * - update the impulse response h1[] by including fixed-gain pitch * - * - find target vector for codebook search * - * - codebook search * - * - encode codebook address * - * - VQ of pitch and codebook gains * - * - find synthesis speech * - * - update states of weighting filter * - *------------------------------------------------------------------------*/ - - A = A_t; /* pointer to interpolated LPC parameters */ - A_offset = 0; - Aq = Aq_t; /* pointer to interpolated quantized LPC parameters */ - Aq_offset = 0; - - i_gamma = 0; - - for (i_subfr = 0; i_subfr < L_FRAME; i_subfr += L_SUBFR) - { - /*---------------------------------------------------------------* - * Find the weighted LPC coefficients for the weighting filter. * - *---------------------------------------------------------------*/ - - Lpcfunc.weight_az(A, A_offset, gamma1[i_gamma], M, Ap1); - Lpcfunc.weight_az(A, A_offset, gamma2[i_gamma], M, Ap2); - i_gamma++; - - /*---------------------------------------------------------------* - * Compute impulse response, h1[], of weighted synthesis filter * - *---------------------------------------------------------------*/ - - for (i = 0; i <= M; i++) ai_zero[i] = Ap1[i]; - Filter.syn_filt(Aq, Aq_offset, ai_zero, 0, h1, 0, L_SUBFR, zero, zero_offset, 0); - Filter.syn_filt(Ap2, 0, h1, 0, h1, 0, L_SUBFR, zero, zero_offset, 0); - - /*------------------------------------------------------------------------* - * * - * Find the target vector for pitch search: * - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * - * * - * |------| res[n] * - * speech[n]---| A(z) |-------- * - * |------| | |--------| error[n] |------| * - * zero -- (-)--| 1/A(z) |-----------| W(z) |-- target * - * exc |--------| |------| * - * * - * Instead of subtracting the zero-input response of filters from * - * the weighted input speech, the above configuration is used to * - * compute the target vector. This configuration gives better performance * - * with fixed-point implementation. The memory of 1/A(z) is updated by * - * filtering (res[n]-exc[n]) through 1/A(z), or simply by subtracting * - * the synthesis speech from the input speech: * - * error[n] = speech[n] - syn[n]. * - * The memory of W(z) is updated by filtering error[n] through W(z), * - * or more simply by subtracting the filtered adaptive and fixed * - * codebook excitations from the target: * - * target[n] - gain_pit*y1[n] - gain_code*y2[n] * - * as these signals are already available. * - * * - *------------------------------------------------------------------------*/ - - Filter.residu( - Aq, - Aq_offset, - speech, - speech_offset + i_subfr, - exc, - exc_offset + i_subfr, - L_SUBFR); /* LPC residual */ - - Filter.syn_filt(Aq, Aq_offset, exc, exc_offset + i_subfr, error, error_offset, L_SUBFR, mem_err, 0, 0); - - Filter.residu(Ap1, 0, error, error_offset, xn, 0, L_SUBFR); - - Filter.syn_filt(Ap2, 0, xn, 0, xn, 0, L_SUBFR, mem_w0, 0, 0); /* target signal xn[]*/ - - /*----------------------------------------------------------------------* - * Closed-loop fractional pitch search * - *----------------------------------------------------------------------*/ - - t0 = Pitch.pitch_fr3( - exc, - exc_offset + i_subfr, - xn, - h1, - L_SUBFR, - t0_min.value, - t0_max.value, - i_subfr, - t0_frac); - - index = Pitch.enc_lag3(t0, t0_frac.value, t0_min, t0_max, PIT_MIN, PIT_MAX, i_subfr); - - ana[ana_offset] = index; - ana_offset++; - if (i_subfr == 0) - { - ana[ana_offset] = PParity.parity_pitch(index); - ana_offset++; - } - - /*-----------------------------------------------------------------* - * - find unity gain pitch excitation (adaptive codebook entry) * - * with fractional interpolation. * - * - find filtered pitch exc. y1[]=exc[] convolve with h1[]) * - * - compute pitch gain and limit between 0 and 1.2 * - * - update target vector for codebook search * - * - find LTP residual. * - *-----------------------------------------------------------------*/ + Debug.Assert(zero is { }); - PredLt3.pred_lt_3(exc, exc_offset + i_subfr, t0, t0_frac.value, L_SUBFR); + Filter.syn_filt(Aq, Aq_offset, ai_zero, 0, h1, 0, L_SUBFR, zero, zero_offset, 0); + Filter.syn_filt(Ap2, 0, h1, 0, h1, 0, L_SUBFR, zero, zero_offset, 0); - Filter.convolve(exc, exc_offset + i_subfr, h1, y1, L_SUBFR); - - gain_pit = Pitch.g_pitch(xn, y1, g_coeff, L_SUBFR); - - /* clip pitch gain if taming is necessary */ - taming = this.taming.test_err(t0, t0_frac.value); - - if (taming == 1) - if (gain_pit > GPCLIP) - gain_pit = GPCLIP; + /*------------------------------------------------------------------------* +* * +* Find the target vector for pitch search: * +* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * +* * +* |------| res[n] * +* speech[n]---| A(z) |-------- * +* |------| | |--------| error[n] |------| * +* zero -- (-)--| 1/A(z) |-----------| W(z) |-- target * +* exc |--------| |------| * +* * +* Instead of subtracting the zero-input response of filters from * +* the weighted input speech, the above configuration is used to * +* compute the target vector. This configuration gives better performance * +* with fixed-point implementation. The memory of 1/A(z) is updated by * +* filtering (res[n]-exc[n]) through 1/A(z), or simply by subtracting * +* the synthesis speech from the input speech: * +* error[n] = speech[n] - syn[n]. * +* The memory of W(z) is updated by filtering error[n] through W(z), * +* or more simply by subtracting the filtered adaptive and fixed * +* codebook excitations from the target: * +* target[n] - gain_pit*y1[n] - gain_code*y2[n] * +* as these signals are already available. * +* * +*------------------------------------------------------------------------*/ + + Debug.Assert(exc is { }); + Debug.Assert(error is { }); + + Filter.residu( + Aq, + Aq_offset, + speech, + speech_offset + i_subfr, + exc, + exc_offset + i_subfr, + L_SUBFR); /* LPC residual */ + + Filter.syn_filt(Aq, Aq_offset, exc, exc_offset + i_subfr, error, error_offset, L_SUBFR, mem_err, 0, 0); + + Filter.residu(Ap1, 0, error, error_offset, xn, 0, L_SUBFR); + + Filter.syn_filt(Ap2, 0, xn, 0, xn, 0, L_SUBFR, mem_w0, 0, 0); /* target signal xn[]*/ - for (i = 0; i < L_SUBFR; i++) - xn2[i] = xn[i] - y1[i] * gain_pit; + /*----------------------------------------------------------------------* +* Closed-loop fractional pitch search * +*----------------------------------------------------------------------*/ + + t0 = Pitch.pitch_fr3( + exc, + exc_offset + i_subfr, + xn, + h1, + L_SUBFR, + t0_min.value, + t0_max.value, + i_subfr, + t0_frac); + + index = Pitch.enc_lag3(t0, t0_frac.value, t0_min, t0_max, PIT_MIN, PIT_MAX, i_subfr); + + ana[ana_offset] = index; + ana_offset++; + if (i_subfr == 0) + { + ana[ana_offset] = PParity.parity_pitch(index); + ana_offset++; + } - /*-----------------------------------------------------* - * - Innovative codebook search. * - *-----------------------------------------------------*/ + /*-----------------------------------------------------------------* +* - find unity gain pitch excitation (adaptive codebook entry) * +* with fractional interpolation. * +* - find filtered pitch exc. y1[]=exc[] convolve with h1[]) * +* - compute pitch gain and limit between 0 and 1.2 * +* - update target vector for codebook search * +* - find LTP residual. * +*-----------------------------------------------------------------*/ - iRef.value = i; - index = acelpCo.ACELP_codebook(xn2, h1, t0, sharp, i_subfr, code, y2, iRef); - i = iRef.value; - ana[ana_offset] = index; /* Positions index */ - ana_offset++; - ana[ana_offset] = i; /* Signs index */ - ana_offset++; + PredLt3.pred_lt_3(exc, exc_offset + i_subfr, t0, t0_frac.value, L_SUBFR); - /*-----------------------------------------------------* - * - Quantization of gains. * - *-----------------------------------------------------*/ - CorFunc.corr_xy2(xn, y1, y2, g_coeff); + Filter.convolve(exc, exc_offset + i_subfr, h1, y1, L_SUBFR); - _gain_pit.value = gain_pit; - _gain_code.value = gain_code; - ana[ana_offset] = quaGain.qua_gain(code, g_coeff, L_SUBFR, _gain_pit, _gain_code, taming); - gain_pit = _gain_pit.value; - gain_code = _gain_code.value; - ana_offset++; + gain_pit = Pitch.g_pitch(xn, y1, g_coeff, L_SUBFR); - /*------------------------------------------------------------* - * - Update pitch sharpening "sharp" with quantized gain_pit * - *------------------------------------------------------------*/ + /* clip pitch gain if taming is necessary */ + taming = this.taming.test_err(t0, t0_frac.value); - sharp = gain_pit; - if (sharp > SHARPMAX) sharp = SHARPMAX; - if (sharp < SHARPMIN) sharp = SHARPMIN; - /*------------------------------------------------------* - * - Find the total excitation * - * - find synthesis speech corresponding to exc[] * - * - update filters' memories for finding the target * - * vector in the next subframe * - * (update error[-m..-1] and mem_w0[]) * - * update error function for taming process * - *------------------------------------------------------*/ + if (taming == 1) + { + if (gain_pit > GPCLIP) + { + gain_pit = GPCLIP; + } + } - for (i = 0; i < L_SUBFR; i++) - exc[exc_offset + i + i_subfr] = gain_pit * exc[exc_offset + i + i_subfr] + gain_code * code[i]; + for (i = 0; i < L_SUBFR; i++) + { + xn2[i] = xn[i] - y1[i] * gain_pit; + } - this.taming.update_exc_err(gain_pit, t0); + /*-----------------------------------------------------* +* - Innovative codebook search. * +*-----------------------------------------------------*/ + + iRef.value = i; + index = acelpCo.ACELP_codebook(xn2, h1, t0, sharp, i_subfr, code, y2, iRef); + i = iRef.value; + ana[ana_offset] = index; /* Positions index */ + ana_offset++; + ana[ana_offset] = i; /* Signs index */ + ana_offset++; + + /*-----------------------------------------------------* +* - Quantization of gains. * +*-----------------------------------------------------*/ + CorFunc.corr_xy2(xn, y1, y2, g_coeff); + + _gain_pit.value = gain_pit; + _gain_code.value = gain_code; + ana[ana_offset] = quaGain.qua_gain(code, g_coeff, L_SUBFR, _gain_pit, _gain_code, taming); + gain_pit = _gain_pit.value; + gain_code = _gain_code.value; + ana_offset++; + + /*------------------------------------------------------------* +* - Update pitch sharpening "sharp" with quantized gain_pit * +*------------------------------------------------------------*/ + + sharp = gain_pit; + if (sharp > SHARPMAX) + { + sharp = SHARPMAX; + } - Filter.syn_filt(Aq, Aq_offset, exc, exc_offset + i_subfr, synth, i_subfr, L_SUBFR, mem_syn, 0, 1); + if (sharp < SHARPMIN) + { + sharp = SHARPMIN; + } + /*------------------------------------------------------* +* - Find the total excitation * +* - find synthesis speech corresponding to exc[] * +* - update filters' memories for finding the target * +* vector in the next subframe * +* (update error[-m..-1] and mem_w0[]) * +* update error function for taming process * +*------------------------------------------------------*/ + + for (i = 0; i < L_SUBFR; i++) + { + exc[exc_offset + i + i_subfr] = gain_pit * exc[exc_offset + i + i_subfr] + gain_code * code[i]; + } - for (i = L_SUBFR - M, j = 0; i < L_SUBFR; i++, j++) - { - mem_err[j] = speech[speech_offset + i_subfr + i] - synth[i_subfr + i]; - mem_w0[j] = xn[i] - gain_pit * y1[i] - gain_code * y2[i]; - } + this.taming.update_exc_err(gain_pit, t0); - A_offset += MP1; /* interpolated LPC parameters for next subframe */ - Aq_offset += MP1; + Filter.syn_filt(Aq, Aq_offset, exc, exc_offset + i_subfr, synth, i_subfr, L_SUBFR, mem_syn, 0, 1); + for (i = L_SUBFR - M, j = 0; i < L_SUBFR; i++, j++) + { + mem_err[j] = speech[speech_offset + i_subfr + i] - synth[i_subfr + i]; + mem_w0[j] = xn[i] - gain_pit * y1[i] - gain_code * y2[i]; } - /*--------------------------------------------------* - * Update signal for next frame. * - * -> shift to the left by L_FRAME: * - * speech[], wsp[] and exc[] * - *--------------------------------------------------*/ + A_offset += MP1; /* interpolated LPC parameters for next subframe */ + Aq_offset += MP1; - Util.copy(old_speech, L_FRAME, old_speech, L_TOTAL - L_FRAME); - Util.copy(old_wsp, L_FRAME, old_wsp, PIT_MAX); - Util.copy(old_exc, L_FRAME, old_exc, PIT_MAX + L_INTERPOL); } + + /*--------------------------------------------------* +* Update signal for next frame. * +* -> shift to the left by L_FRAME: * +* speech[], wsp[] and exc[] * +*--------------------------------------------------*/ + + Util.copy(old_speech, L_FRAME, old_speech, L_TOTAL - L_FRAME); + Util.copy(old_wsp, L_FRAME, old_wsp, PIT_MAX); + Util.copy(old_exc, L_FRAME, old_exc, PIT_MAX + L_INTERPOL); } } diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/CorFunc.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/CorFunc.cs index 2d5466af34..61319810bb 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/CorFunc.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/CorFunc.cs @@ -24,95 +24,109 @@ * * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal static class CorFunc { - internal class CorFunc - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : COR_FUNC.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : COR_FUNC.C +Used for the floating point version of both +G.729 main body and G.729A */ - /** - * Compute the correlation products needed for gain computation. - * - * @param xn input : target vector x[0:l_subfr] - * @param y1 input : filtered adaptive codebook vector - * @param y2 input : filtered 1st codebook innovation - * @param g_coeff , -2 , and 2 ]]> - */ + /** +* Compute the correlation products needed for gain computation. +* +* @param xn input : target vector x[0:l_subfr] +* @param y1 input : filtered adaptive codebook vector +* @param y2 input : filtered 1st codebook innovation +* @param g_coeff , -2 , and 2 ]]> +*/ - public static void corr_xy2( - float[] xn, - float[] y1, - float[] y2, - float[] g_coeff - ) - { - var L_SUBFR = Ld8k.L_SUBFR; + public static void corr_xy2( + float[] xn, + float[] y1, + float[] y2, + float[] g_coeff + ) + { + var L_SUBFR = Ld8k.L_SUBFR; - float y2y2, xny2, y1y2; - int i; + float y2y2, xny2, y1y2; + int i; - y2y2 = 0.01f; - for (i = 0; i < L_SUBFR; i++) y2y2 += y2[i] * y2[i]; - g_coeff[2] = y2y2; + y2y2 = 0.01f; + for (i = 0; i < L_SUBFR; i++) + { + y2y2 += y2[i] * y2[i]; + } - xny2 = 0.01f; - for (i = 0; i < L_SUBFR; i++) xny2 += xn[i] * y2[i]; - g_coeff[3] = -2.0f * xny2; + g_coeff[2] = y2y2; - y1y2 = 0.01f; - for (i = 0; i < L_SUBFR; i++) y1y2 += y1[i] * y2[i]; - g_coeff[4] = 2.0f * y1y2; + xny2 = 0.01f; + for (i = 0; i < L_SUBFR; i++) + { + xny2 += xn[i] * y2[i]; } - /** - * Compute correlations of input response h[] with the target vector X[]. - * - * @param h (i) :Impulse response of filters - * @param x (i) :Target vector - * @param d (o) :Correlations between h[] and x[] - */ + g_coeff[3] = -2.0f * xny2; - public static void cor_h_x( - float[] h, - float[] x, - float[] d - ) + y1y2 = 0.01f; + for (i = 0; i < L_SUBFR; i++) { - var L_SUBFR = Ld8k.L_SUBFR; + y1y2 += y1[i] * y2[i]; + } + + g_coeff[4] = 2.0f * y1y2; + } + + /** +* Compute correlations of input response h[] with the target vector X[]. +* +* @param h (i) :Impulse response of filters +* @param x (i) :Target vector +* @param d (o) :Correlations between h[] and x[] +*/ + + public static void cor_h_x( + float[] h, + float[] x, + float[] d + ) + { + var L_SUBFR = Ld8k.L_SUBFR; - int i, j; - float s; + int i, j; + float s; - for (i = 0; i < L_SUBFR; i++) + for (i = 0; i < L_SUBFR; i++) + { + s = 0.0f; + for (j = i; j < L_SUBFR; j++) { - s = 0.0f; - for (j = i; j < L_SUBFR; j++) - s += x[j] * h[j - i]; - d[i] = s; + s += x[j] * h[j - i]; } + + d[i] = s; } } } diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/DeAcelp.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/DeAcelp.cs index 2cada2c882..eb824b0ea8 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/DeAcelp.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/DeAcelp.cs @@ -22,90 +22,96 @@ /** * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal static class DeAcelp { - internal class DeAcelp - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : DE_ACELP.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : DE_ACELP.C +Used for the floating point version of both +G.729 main body and G.729A */ - /** - * Algebraic codebook decoder. - * - * @param sign input : signs of 4 pulses - * @param index input : positions of 4 pulses - * @param cod output: innovative codevector - */ + /** +* Algebraic codebook decoder. +* +* @param sign input : signs of 4 pulses +* @param index input : positions of 4 pulses +* @param cod output: innovative codevector +*/ - public static void decod_ACELP( - int sign, - int index, - float[] cod - ) - { - var L_SUBFR = Ld8k.L_SUBFR; + public static void decod_ACELP( + int sign, + int index, + float[] cod + ) + { + var L_SUBFR = Ld8k.L_SUBFR; - var pos = new int[4]; - int i, j; + var pos = new int[4]; + int i, j; - /* decode the positions of 4 pulses */ + /* decode the positions of 4 pulses */ - i = index & 7; - pos[0] = i * 5; + i = index & 7; + pos[0] = i * 5; - index >>= 3; - i = index & 7; - pos[1] = i * 5 + 1; + index >>= 3; + i = index & 7; + pos[1] = i * 5 + 1; - index >>= 3; - i = index & 7; - pos[2] = i * 5 + 2; + index >>= 3; + i = index & 7; + pos[2] = i * 5 + 2; - index >>= 3; - j = index & 1; - index >>= 1; - i = index & 7; - pos[3] = i * 5 + 3 + j; + index >>= 3; + j = index & 1; + index >>= 1; + i = index & 7; + pos[3] = i * 5 + 3 + j; - /* find the algebraic codeword */ + /* find the algebraic codeword */ - for (i = 0; i < L_SUBFR; i++) cod[i] = 0; + for (i = 0; i < L_SUBFR; i++) + { + cod[i] = 0; + } - /* decode the signs of 4 pulses */ + /* decode the signs of 4 pulses */ - for (j = 0; j < 4; j++) - { + for (j = 0; j < 4; j++) + { - i = sign & 1; - sign >>= 1; + i = sign & 1; + sign >>= 1; - if (i != 0) - cod[pos[j]] = 1.0f; - else - cod[pos[j]] = -1.0f; + if (i != 0) + { + cod[pos[j]] = 1.0f; + } + else + { + cod[pos[j]] = -1.0f; } } } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/DecGain.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/DecGain.cs index e8c9fe6da6..4ecd21dec3 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/DecGain.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/DecGain.cs @@ -22,114 +22,117 @@ /** * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal class DecGain { - internal class DecGain - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : DEC_GAIN.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : DEC_GAIN.C +Used for the floating point version of both +G.729 main body and G.729A +*/ + private readonly float[ /* 4 */] past_qua_en = + { + -14.0f, + -14.0f, + -14.0f, + -14.0f + }; + + /** +* Decode the adaptive and fixed codebook gains. +* +* @param index input : quantizer index +* @param code input : fixed code book vector +* @param l_subfr input : subframe size +* @param bfi input : bad frame indicator good = 0 +* @param gain_pit output: quantized acb gain +* @param gain_code output: quantized fcb gain */ - private readonly float[ /* 4 */] past_qua_en = - { - -14.0f, - -14.0f, - -14.0f, - -14.0f - }; - - /** - * Decode the adaptive and fixed codebook gains. - * - * @param index input : quantizer index - * @param code input : fixed code book vector - * @param l_subfr input : subframe size - * @param bfi input : bad frame indicator good = 0 - * @param gain_pit output: quantized acb gain - * @param gain_code output: quantized fcb gain - */ - public void dec_gain( - int index, - float[] code, - int l_subfr, - int bfi, - FloatReference gain_pit, - FloatReference gain_code - ) - { - var NCODE2 = Ld8k.NCODE2; - var gbk1 = TabLd8k.gbk1; - var gbk2 = TabLd8k.gbk2; - var imap1 = TabLd8k.imap1; - var imap2 = TabLd8k.imap2; + public void dec_gain( + int index, + float[] code, + int l_subfr, + int bfi, + FloatReference gain_pit, + FloatReference gain_code + ) + { + var NCODE2 = Ld8k.NCODE2; + var gbk1 = TabLd8k.gbk1; + var gbk2 = TabLd8k.gbk2; + var imap1 = TabLd8k.imap1; + var imap2 = TabLd8k.imap2; - int index1, index2; - float gcode0, g_code; + int index1, index2; + float gcode0, g_code; - /*----------------- Test erasure ---------------*/ - if (bfi != 0) + /*----------------- Test erasure ---------------*/ + if (bfi != 0) + { + gain_pit.value *= 0.9f; + if (gain_pit.value > 0.9f) { - gain_pit.value *= 0.9f; - if (gain_pit.value > 0.9f) gain_pit.value = 0.9f; - gain_code.value *= 0.98f; + gain_pit.value = 0.9f; + } - /*----------------------------------------------* - * update table of past quantized energies * - * (frame erasure) * - *----------------------------------------------*/ - Gainpred.gain_update_erasure(past_qua_en); + gain_code.value *= 0.98f; - return; - } + /*----------------------------------------------* + * update table of past quantized energies * + * (frame erasure) * + *----------------------------------------------*/ + Gainpred.gain_update_erasure(past_qua_en); - /*-------------- Decode pitch gain ---------------*/ + return; + } - index1 = imap1[index / NCODE2]; - index2 = imap2[index % NCODE2]; - gain_pit.value = gbk1[index1][0] + gbk2[index2][0]; + /*-------------- Decode pitch gain ---------------*/ - /*-------------- Decode codebook gain ---------------*/ + index1 = imap1[index / NCODE2]; + index2 = imap2[index % NCODE2]; + gain_pit.value = gbk1[index1][0] + gbk2[index2][0]; - /*---------------------------------------------------* - *- energy due to innovation -* - *- predicted energy -* - *- predicted codebook gain => gcode0[exp_gcode0] -* - *---------------------------------------------------*/ + /*-------------- Decode codebook gain ---------------*/ - gcode0 = Gainpred.gain_predict(past_qua_en, code, l_subfr); + /*---------------------------------------------------* +*- energy due to innovation -* +*- predicted energy -* +*- predicted codebook gain => gcode0[exp_gcode0] -* +*---------------------------------------------------*/ - /*-----------------------------------------------------------------* - * *gain_code = (gbk1[indice1][1]+gbk2[indice2][1]) * gcode0; * - *-----------------------------------------------------------------*/ + gcode0 = Gainpred.gain_predict(past_qua_en, code, l_subfr); - g_code = gbk1[index1][1] + gbk2[index2][1]; - gain_code.value = g_code * gcode0; + /*-----------------------------------------------------------------* +* *gain_code = (gbk1[indice1][1]+gbk2[indice2][1]) * gcode0; * +*-----------------------------------------------------------------*/ - /*----------------------------------------------* - * update table of past quantized energies * - *----------------------------------------------*/ + g_code = gbk1[index1][1] + gbk2[index2][1]; + gain_code.value = g_code * gcode0; - Gainpred.gain_update(past_qua_en, g_code); - } + /*----------------------------------------------* +* update table of past quantized energies * +*----------------------------------------------*/ + + Gainpred.gain_update(past_qua_en, g_code); } } \ No newline at end of file diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/DecLag3.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/DecLag3.cs index c4c8bf979a..b147caaf85 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/DecLag3.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/DecLag3.cs @@ -22,96 +22,97 @@ /** * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal static class DecLag3 { - internal class DecLag3 - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : DEC_LAG3.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : DEC_LAG3.C +Used for the floating point version of both +G.729 main body and G.729A */ - /** - * Decoding of fractional pitch lag with 1/3 resolution. - * See the source for more details about the encoding procedure. - * - * @param index input : received pitch index - * @param pit_min input : minimum pitch lag - * @param pit_max input : maximum pitch lag - * @param i_subfr input : subframe flag - * @param T0 output: integer part of pitch lag - * @param T0_frac output: fractional part of pitch lag - */ + /** +* Decoding of fractional pitch lag with 1/3 resolution. +* See the source for more details about the encoding procedure. +* +* @param index input : received pitch index +* @param pit_min input : minimum pitch lag +* @param pit_max input : maximum pitch lag +* @param i_subfr input : subframe flag +* @param T0 output: integer part of pitch lag +* @param T0_frac output: fractional part of pitch lag +*/ - public static void dec_lag3( - int index, - int pit_min, - int pit_max, - int i_subfr, - IntReference T0, - IntReference T0_frac - ) - { - int i; - int _T0 = T0.value, _T0_frac = T0_frac.value; - int T0_min, T0_max; + public static void dec_lag3( + int index, + int pit_min, + int pit_max, + int i_subfr, + IntReference T0, + IntReference T0_frac + ) + { + int i; + int _T0 = T0.value, _T0_frac = T0_frac.value; + int T0_min, T0_max; - if (i_subfr == 0) /* if 1st subframe */ + if (i_subfr == 0) /* if 1st subframe */ + { + if (index < 197) { - if (index < 197) - { - _T0 = (index + 2) / 3 + 19; - _T0_frac = index - _T0 * 3 + 58; - } - else - { - _T0 = index - 112; - _T0_frac = 0; - } + _T0 = (index + 2) / 3 + 19; + _T0_frac = index - _T0 * 3 + 58; } - - else /* second subframe */ + else { - /* find T0_min and T0_max for 2nd subframe */ + _T0 = index - 112; + _T0_frac = 0; + } + } - T0_min = _T0 - 5; - if (T0_min < pit_min) - T0_min = pit_min; + else /* second subframe */ + { + /* find T0_min and T0_max for 2nd subframe */ - T0_max = T0_min + 9; - if (T0_max > pit_max) - { - T0_max = pit_max; - T0_min = T0_max - 9; - } + T0_min = _T0 - 5; + if (T0_min < pit_min) + { + T0_min = pit_min; + } - i = (index + 2) / 3 - 1; - _T0 = i + T0_min; - _T0_frac = index - 2 - i * 3; + T0_max = T0_min + 9; + if (T0_max > pit_max) + { + T0_max = pit_max; + T0_min = T0_max - 9; } - T0.value = _T0; - T0_frac.value = _T0_frac; + i = (index + 2) / 3 - 1; + _T0 = i + T0_min; + _T0_frac = index - 2 - i * 3; } + + T0.value = _T0; + T0_frac.value = _T0_frac; } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/DecLd8k.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/DecLd8k.cs index 895b86792b..f55bbe5f1b 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/DecLd8k.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/DecLd8k.cs @@ -19,6 +19,9 @@ * SIPRO Lab Telecom. */ + +using System.Diagnostics; + /** * Functions init_decod_ld8k and decod_ld8k. *
@@ -35,318 +38,343 @@
  *
  * @author Lubomir Marinov (translation of ITU-T C source code to Java)
  */
-namespace SIPSorcery.Media.G729Codec
+namespace SIPSorcery.Media.G729Codec;
+
+internal sealed class DecLd8k : Ld8k
 {
-    internal class DecLd8k : Ld8k
-    {
 
-        private readonly DecGain decGain = new DecGain();
+    private readonly DecGain decGain = new DecGain();
 
-        /**
- * Excitation vector
- */
-        private float[] exc;
+    /**
+* Excitation vector
+*/
+    private float[]? exc;
 
-        /**
- * Excitation vector offset
- */
-        private int exc_offset;
+    /**
+* Excitation vector offset
+*/
+    private int exc_offset;
 
-        /**
- * fixed codebook gain
- */
-        private readonly FloatReference gain_code = new FloatReference();
+    /**
+* fixed codebook gain
+*/
+    private readonly FloatReference gain_code = new FloatReference();
 
-        /**
- * adaptive codebook gain
- */
-        private readonly FloatReference gain_pitch = new FloatReference();
+    /**
+* adaptive codebook gain
+*/
+    private readonly FloatReference gain_pitch = new FloatReference();
 
-        /**
- * Lsp (Line spectral pairs)
- */
-        private readonly float[ /* M */] lsp_old =
-        {
-            0.9595f,
-            0.8413f,
-            0.6549f,
-            0.4154f,
-            0.1423f,
-            -0.1423f,
-            -0.4154f,
-            -0.6549f,
-            -0.8413f,
-            -0.9595f
-        };
-
-        private readonly Lspdec lspdec = new Lspdec();
-
-        /**
- * Filter's memory
- */
-        private readonly float[] mem_syn = new float[M];
+    /**
+* Lsp (Line spectral pairs)
+*/
+    private readonly float[ /* M */] lsp_old =
+    {
+        0.9595f,
+        0.8413f,
+        0.6549f,
+        0.4154f,
+        0.1423f,
+        -0.1423f,
+        -0.4154f,
+        -0.6549f,
+        -0.8413f,
+        -0.9595f
+    };
+
+    private readonly Lspdec lspdec = new Lspdec();
+
+    /**
+* Filter's memory
+*/
+    private readonly float[] mem_syn = new float[M];
 
-        /* ITU-T G.729 Software Package Release 2 (November 2006) */
-        /*
-   ITU-T G.729 Annex C - Reference C code for floating point
-                         implementation of G.729
-                         Version 1.01 of 15.September.98
+    /* ITU-T G.729 Software Package Release 2 (November 2006) */
+    /*
+ITU-T G.729 Annex C - Reference C code for floating point
+                     implementation of G.729
+                     Version 1.01 of 15.September.98
 */
 
-        /*
+    /*
 ----------------------------------------------------------------------
-                    COPYRIGHT NOTICE
+                COPYRIGHT NOTICE
 ----------------------------------------------------------------------
-   ITU-T G.729 Annex C ANSI C source code
-   Copyright (C) 1998, AT&T, France Telecom, NTT, University of
-   Sherbrooke.  All rights reserved.
+ITU-T G.729 Annex C ANSI C source code
+Copyright (C) 1998, AT&T, France Telecom, NTT, University of
+Sherbrooke.  All rights reserved.
 
 ----------------------------------------------------------------------
 */
 
-        /*
- File : DEC_LD8K.C
- Used for the floating point version of G.729 main body
- (not for G.729A)
+    /*
+File : DEC_LD8K.C
+Used for the floating point version of G.729 main body
+(not for G.729A)
 */
 
-        /*--------------------------------------------------------*
- *         Static memory allocation.                      *
- *--------------------------------------------------------*/
+    /*--------------------------------------------------------*
+*         Static memory allocation.                      *
+*--------------------------------------------------------*/
 
-        /**
- * Excitation vector
- */
-        private readonly float[] old_exc = new float[L_FRAME + PIT_MAX + L_INTERPOL];
+    /**
+* Excitation vector
+*/
+    private readonly float[] old_exc = new float[L_FRAME + PIT_MAX + L_INTERPOL];
 
-        /**
+    /**
 * integer delay of previous frame
 */
-        private int old_t0;
+    private int old_t0;
 
-        /**
- * pitch sharpening of previous fr
- */
-        private float sharp;
+    /**
+* pitch sharpening of previous fr
+*/
+    private float sharp;
 
-        /**
- * Initialization of variables for the decoder section.
- */
+    /**
+* Initialization of variables for the decoder section.
+*/
 
-        public void init_decod_ld8k()
-        {
-            /* Initialize static pointer */
-            exc = old_exc;
-            exc_offset = PIT_MAX + L_INTERPOL;
+    public void init_decod_ld8k()
+    {
+        /* Initialize static pointer */
+        exc = old_exc;
+        exc_offset = PIT_MAX + L_INTERPOL;
 
-            /* Static vectors to zero */
-            Util.set_zero(old_exc, PIT_MAX + L_INTERPOL);
-            Util.set_zero(mem_syn, M);
+        /* Static vectors to zero */
+        Util.set_zero(old_exc, PIT_MAX + L_INTERPOL);
+        Util.set_zero(mem_syn, M);
 
-            sharp = SHARPMIN;
-            old_t0 = 60;
-            gain_code.value = 0.0f;
-            gain_pitch.value = 0.0f;
+        sharp = SHARPMIN;
+        old_t0 = 60;
+        gain_code.value = 0.0f;
+        gain_pitch.value = 0.0f;
 
-            lspdec.lsp_decw_reset();
-        }
+        lspdec.lsp_decw_reset();
+    }
 
-        /**
- * Decoder
- *
- * @param parm          input : synthesis parameters (parm[0] = bfi)
- * @param voicing       input : voicing decision from previous frame
- * @param synth         output: synthesized speech
- * @param synth_offset  input : synthesized speech offset
- * @param A_t           output: two sets of A(z) coefficients length=2*MP1
- * @return              output: integer delay of first subframe
- */
+    /**
+* Decoder
+*
+* @param parm          input : synthesis parameters (parm[0] = bfi)
+* @param voicing       input : voicing decision from previous frame
+* @param synth         output: synthesized speech
+* @param synth_offset  input : synthesized speech offset
+* @param A_t           output: two sets of A(z) coefficients length=2*MP1
+* @return              output: integer delay of first subframe
+*/
 
-        public int decod_ld8k(
-            int[] parm,
-            int voicing,
-            float[] synth,
-            int synth_offset,
-            float[] A_t
-        )
-        {
-            var parm_offset = 0;
+    public int decod_ld8k(
+        int[] parm,
+        int voicing,
+        float[] synth,
+        int synth_offset,
+        float[] A_t
+    )
+    {
+        var parm_offset = 0;
 
-            var t0_first = 0; /* output: integer delay of first subframe            */
-            float[] Az; /* Pointer to A_t (LPC coefficients)  */
-            int Az_offset;
-            var lsp_new = new float[M]; /* LSPs                               */
-            var code = new float[L_SUBFR]; /* algebraic codevector               */
+        var t0_first = 0; /* output: integer delay of first subframe            */
+        float[] Az; /* Pointer to A_t (LPC coefficients)  */
+        int Az_offset;
+        var lsp_new = new float[M]; /* LSPs                               */
+        var code = new float[L_SUBFR]; /* algebraic codevector               */
 
-            /* Scalars */
-            int i, i_subfr;
-            IntReference t0 = new IntReference(), t0_frac = new IntReference();
-            int index;
+        /* Scalars */
+        int i, i_subfr;
+        IntReference t0 = new IntReference(), t0_frac = new IntReference();
+        int index;
 
-            int bfi;
-            int bad_pitch;
+        int bfi;
+        int bad_pitch;
 
-            /* Test bad frame indicator (bfi) */
+        /* Test bad frame indicator (bfi) */
 
-            bfi = parm[parm_offset];
-            parm_offset++;
+        bfi = parm[parm_offset];
+        parm_offset++;
 
-            /* Decode the LSPs */
+        /* Decode the LSPs */
 
-            lspdec.d_lsp(parm, parm_offset, lsp_new, bfi);
-            parm_offset += 2; /* Advance synthesis parameters pointer */
+        lspdec.d_lsp(parm, parm_offset, lsp_new, bfi);
+        parm_offset += 2; /* Advance synthesis parameters pointer */
 
-            /* Interpolation of LPC for the 2 subframes */
+        /* Interpolation of LPC for the 2 subframes */
 
-            Lpcfunc.int_qlpc(lsp_old, lsp_new, A_t);
+        Lpcfunc.int_qlpc(lsp_old, lsp_new, A_t);
 
-            /* update the LSFs for the next frame */
+        /* update the LSFs for the next frame */
 
-            Util.copy(lsp_new, lsp_old, M);
+        Util.copy(lsp_new, lsp_old, M);
 
-            /*------------------------------------------------------------------------*
- *          Loop for every subframe in the analysis frame                 *
- *------------------------------------------------------------------------*
- * The subframe size is L_SUBFR and the loop is repeated L_FRAME/L_SUBFR  *
- *  times                                                                 *
- *     - decode the pitch delay                                           *
- *     - decode algebraic code                                            *
- *     - decode pitch and codebook gains                                  *
- *     - find the excitation and compute synthesis speech                 *
- *------------------------------------------------------------------------*/
+        /*------------------------------------------------------------------------*
+*          Loop for every subframe in the analysis frame                 *
+*------------------------------------------------------------------------*
+* The subframe size is L_SUBFR and the loop is repeated L_FRAME/L_SUBFR  *
+*  times                                                                 *
+*     - decode the pitch delay                                           *
+*     - decode algebraic code                                            *
+*     - decode pitch and codebook gains                                  *
+*     - find the excitation and compute synthesis speech                 *
+*------------------------------------------------------------------------*/
 
-            Az = A_t; /* pointer to interpolated LPC parameters */
-            Az_offset = 0;
+        Az = A_t; /* pointer to interpolated LPC parameters */
+        Az_offset = 0;
 
-            for (i_subfr = 0; i_subfr < L_FRAME; i_subfr += L_SUBFR)
-            {
+        for (i_subfr = 0; i_subfr < L_FRAME; i_subfr += L_SUBFR)
+        {
 
-                index = parm[parm_offset]; /* pitch index */
-                parm_offset++;
+            index = parm[parm_offset]; /* pitch index */
+            parm_offset++;
 
-                if (i_subfr == 0)
+            if (i_subfr == 0)
+            {
+                /* if first subframe */
+                i = parm[parm_offset]; /* get parity check result */
+                parm_offset++;
+                bad_pitch = bfi + i;
+                if (bad_pitch == 0)
                 {
-                    /* if first subframe */
-                    i = parm[parm_offset]; /* get parity check result */
-                    parm_offset++;
-                    bad_pitch = bfi + i;
-                    if (bad_pitch == 0)
-                    {
-                        DecLag3.dec_lag3(index, PIT_MIN, PIT_MAX, i_subfr, t0, t0_frac);
-                        old_t0 = t0.value;
-                    }
-                    else /* Bad frame, or parity error */
+                    DecLag3.dec_lag3(index, PIT_MIN, PIT_MAX, i_subfr, t0, t0_frac);
+                    old_t0 = t0.value;
+                }
+                else /* Bad frame, or parity error */
+                {
+                    t0.value = old_t0;
+                    t0_frac.value = 0;
+                    old_t0++;
+                    if (old_t0 > PIT_MAX)
                     {
-                        t0.value = old_t0;
-                        t0_frac.value = 0;
-                        old_t0++;
-                        if (old_t0 > PIT_MAX)
-                            old_t0 = PIT_MAX;
+                        old_t0 = PIT_MAX;
                     }
+                }
 
-                    t0_first = t0.value; /* If first frame */
+                t0_first = t0.value; /* If first frame */
+            }
+            else /* second subframe */
+            {
+                if (bfi == 0)
+                {
+                    DecLag3.dec_lag3(index, PIT_MIN, PIT_MAX, i_subfr, t0, t0_frac);
+                    old_t0 = t0.value;
                 }
-                else /* second subframe */
+                else
                 {
-                    if (bfi == 0)
+                    t0.value = old_t0;
+                    t0_frac.value = 0;
+                    old_t0++;
+                    if (old_t0 > PIT_MAX)
                     {
-                        DecLag3.dec_lag3(index, PIT_MIN, PIT_MAX, i_subfr, t0, t0_frac);
-                        old_t0 = t0.value;
-                    }
-                    else
-                    {
-                        t0.value = old_t0;
-                        t0_frac.value = 0;
-                        old_t0++;
-                        if (old_t0 > PIT_MAX)
-                            old_t0 = PIT_MAX;
+                        old_t0 = PIT_MAX;
                     }
                 }
+            }
 
-                /*-------------------------------------------------*
-    *  - Find the adaptive codebook vector.            *
-    *--------------------------------------------------*/
+            /*-------------------------------------------------*
+*  - Find the adaptive codebook vector.            *
+*--------------------------------------------------*/
 
-                PredLt3.pred_lt_3(exc, exc_offset + i_subfr, t0.value, t0_frac.value, L_SUBFR);
+            Debug.Assert(exc is { });
 
-                /*-------------------------------------------------------*
-    * - Decode innovative codebook.                         *
-    * - Add the fixed-gain pitch contribution to code[].    *
-    *-------------------------------------------------------*/
+            PredLt3.pred_lt_3(exc, exc_offset + i_subfr, t0.value, t0_frac.value, L_SUBFR);
 
-                if (bfi != 0)
-                {
-                    /* Bad Frame Error Concealment */
-                    parm[parm_offset + 0] = Util.random_g729() & 0x1fff; /* 13 bits random*/
-                    parm[parm_offset + 1] = Util.random_g729() & 0x000f; /*  4 bits random */
-                }
+            /*-------------------------------------------------------*
+* - Decode innovative codebook.                         *
+* - Add the fixed-gain pitch contribution to code[].    *
+*-------------------------------------------------------*/
 
-                DeAcelp.decod_ACELP(parm[parm_offset + 1], parm[parm_offset + 0], code);
-                parm_offset += 2;
-                for (i = t0.value; i < L_SUBFR; i++) code[i] += sharp * code[i - t0.value];
+            if (bfi != 0)
+            {
+                /* Bad Frame Error Concealment */
+                parm[parm_offset + 0] = Util.random_g729() & 0x1fff; /* 13 bits random*/
+                parm[parm_offset + 1] = Util.random_g729() & 0x000f; /*  4 bits random */
+            }
 
-                /*-------------------------------------------------*
-    * - Decode pitch and codebook gains.              *
-    *-------------------------------------------------*/
+            DeAcelp.decod_ACELP(parm[parm_offset + 1], parm[parm_offset + 0], code);
+            parm_offset += 2;
+            for (i = t0.value; i < L_SUBFR; i++)
+            {
+                code[i] += sharp * code[i - t0.value];
+            }
 
-                index = parm[parm_offset]; /* index of energy VQ */
-                parm_offset++;
-                decGain.dec_gain(index, code, L_SUBFR, bfi, gain_pitch, gain_code);
+            /*-------------------------------------------------*
+* - Decode pitch and codebook gains.              *
+*-------------------------------------------------*/
 
-                /*-------------------------------------------------------------*
-    * - Update pitch sharpening "sharp" with quantized gain_pitch *
-    *-------------------------------------------------------------*/
+            index = parm[parm_offset]; /* index of energy VQ */
+            parm_offset++;
+            decGain.dec_gain(index, code, L_SUBFR, bfi, gain_pitch, gain_code);
+
+            /*-------------------------------------------------------------*
+* - Update pitch sharpening "sharp" with quantized gain_pitch *
+*-------------------------------------------------------------*/
+
+            sharp = gain_pitch.value;
+            if (sharp > SHARPMAX)
+            {
+                sharp = SHARPMAX;
+            }
 
-                sharp = gain_pitch.value;
-                if (sharp > SHARPMAX) sharp = SHARPMAX;
-                if (sharp < SHARPMIN) sharp = SHARPMIN;
+            if (sharp < SHARPMIN)
+            {
+                sharp = SHARPMIN;
+            }
 
-                /*-------------------------------------------------------*
-    * - Find the total excitation.                          *
-    *-------------------------------------------------------*/
+            /*-------------------------------------------------------*
+* - Find the total excitation.                          *
+*-------------------------------------------------------*/
 
-                if (bfi != 0)
+            if (bfi != 0)
+            {
+                if (voicing == 0)
                 {
-                    if (voicing == 0)
-                        for (i = 0; i < L_SUBFR; i++)
-                            exc[exc_offset + i + i_subfr] = gain_code.value * code[i];
-                    else
-                        for (i = 0; i < L_SUBFR; i++)
-                            exc[exc_offset + i + i_subfr] = gain_pitch.value * exc[exc_offset + i + i_subfr];
+                    for (i = 0; i < L_SUBFR; i++)
+                    {
+                        exc[exc_offset + i + i_subfr] = gain_code.value * code[i];
+                    }
                 }
                 else
                 {
-                    /* No frame errors */
                     for (i = 0; i < L_SUBFR; i++)
-                        exc[exc_offset + i + i_subfr] =
-                            gain_pitch.value * exc[exc_offset + i + i_subfr] + gain_code.value * code[i];
+                    {
+                        exc[exc_offset + i + i_subfr] = gain_pitch.value * exc[exc_offset + i + i_subfr];
+                    }
+                }
+            }
+            else
+            {
+                /* No frame errors */
+                for (i = 0; i < L_SUBFR; i++)
+                {
+                    exc[exc_offset + i + i_subfr] =
+                        gain_pitch.value * exc[exc_offset + i + i_subfr] + gain_code.value * code[i];
                 }
-
-                /*-------------------------------------------------------*
-     * - Find synthesis speech corresponding to exc[].       *
-     *-------------------------------------------------------*/
-
-                Filter.syn_filt(
-                    Az,
-                    Az_offset,
-                    exc,
-                    exc_offset + i_subfr,
-                    synth,
-                    synth_offset + i_subfr,
-                    L_SUBFR,
-                    mem_syn,
-                    0,
-                    1);
-
-                Az_offset += MP1; /* interpolated LPC parameters for next subframe */
             }
 
-            /*--------------------------------------------------*
-    * Update signal for next frame.                    *
-    * -> shift to the left by L_FRAME  exc[]           *
-    *--------------------------------------------------*/
-            Util.copy(old_exc, L_FRAME, old_exc, PIT_MAX + L_INTERPOL);
-            return t0_first;
+            /*-------------------------------------------------------*
+ * - Find synthesis speech corresponding to exc[].       *
+ *-------------------------------------------------------*/
+
+            Filter.syn_filt(
+                Az,
+                Az_offset,
+                exc,
+                exc_offset + i_subfr,
+                synth,
+                synth_offset + i_subfr,
+                L_SUBFR,
+                mem_syn,
+                0,
+                1);
+
+            Az_offset += MP1; /* interpolated LPC parameters for next subframe */
         }
+
+        /*--------------------------------------------------*
+* Update signal for next frame.                    *
+* -> shift to the left by L_FRAME  exc[]           *
+*--------------------------------------------------*/
+        Util.copy(old_exc, L_FRAME, old_exc, PIT_MAX + L_INTERPOL);
+        return t0_first;
     }
-}
\ No newline at end of file
+}
diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Filter.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Filter.cs
index 3b48d50456..5b84310727 100644
--- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Filter.cs
+++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Filter.cs
@@ -24,159 +24,176 @@
  *
  * @author Lubomir Marinov (translation of ITU-T C source code to Java)
  */
-namespace SIPSorcery.Media.G729Codec
+namespace SIPSorcery.Media.G729Codec;
+
+internal static class Filter
 {
-    internal class Filter
-    {
 
-        /* ITU-T G.729 Software Package Release 2 (November 2006) */
-        /*
-   ITU-T G.729 Annex C - Reference C code for floating point
-                         implementation of G.729
-                         Version 1.01 of 15.September.98
+    /* ITU-T G.729 Software Package Release 2 (November 2006) */
+    /*
+ITU-T G.729 Annex C - Reference C code for floating point
+                     implementation of G.729
+                     Version 1.01 of 15.September.98
 */
 
-        /*
+    /*
 ----------------------------------------------------------------------
-                    COPYRIGHT NOTICE
+                COPYRIGHT NOTICE
 ----------------------------------------------------------------------
-   ITU-T G.729 Annex C ANSI C source code
-   Copyright (C) 1998, AT&T, France Telecom, NTT, University of
-   Sherbrooke.  All rights reserved.
+ITU-T G.729 Annex C ANSI C source code
+Copyright (C) 1998, AT&T, France Telecom, NTT, University of
+Sherbrooke.  All rights reserved.
 
 ----------------------------------------------------------------------
 */
 
-        /*
- File : FILTER.C
- Used for the floating point version of both
- G.729 main body and G.729A
+    /*
+File : FILTER.C
+Used for the floating point version of both
+G.729 main body and G.729A
 */
 
-        /**
- * Convolve vectors x and h and put result in y.
- *
- * @param x         input : input vector x[0:l]
- * @param x_offset  input : input vector offset
- * @param h         input : impulse response or second input h[0:l]
- * @param y         output: x convolved with h , y[0:l]
- * @param l         input : dimension of all vectors
- */
+    /**
+* Convolve vectors x and h and put result in y.
+*
+* @param x         input : input vector x[0:l]
+* @param x_offset  input : input vector offset
+* @param h         input : impulse response or second input h[0:l]
+* @param y         output: x convolved with h , y[0:l]
+* @param l         input : dimension of all vectors
+*/
 
-        public static void convolve(
-            float[] x,
-            int x_offset,
-            float[] h,
-            float[] y,
-            int l
-        )
-        {
-            float temp;
-            int i, n;
+    public static void convolve(
+        float[] x,
+        int x_offset,
+        float[] h,
+        float[] y,
+        int l
+    )
+    {
+        float temp;
+        int i, n;
 
-            for (n = 0; n < l; n++)
+        for (n = 0; n < l; n++)
+        {
+            temp = 0.0f;
+            for (i = 0; i <= n; i++)
             {
-                temp = 0.0f;
-                for (i = 0; i <= n; i++)
-                    temp += x[x_offset + i] * h[n - i];
-                y[n] = temp;
+                temp += x[x_offset + i] * h[n - i];
             }
+
+            y[n] = temp;
         }
+    }
 
-        /**
- * Filter with synthesis filter 1/A(z).
- *
- * @param a          input : predictor coefficients a[0:m]
- * @param a_offset   input : predictor coefficients a offset
- * @param x          input : excitation signal
- * @param x_offset   input : excitation signal offset
- * @param y          output: filtered output signal
- * @param y_offset   output: filtered output signal offset
- * @param l          input : vector dimension
- * @param mem        in/out: filter memory
- * @param mem_offset input : filter memory ofset
- * @param update     input : 0 = no memory update, 1 = update
- */
+    /**
+* Filter with synthesis filter 1/A(z).
+*
+* @param a          input : predictor coefficients a[0:m]
+* @param a_offset   input : predictor coefficients a offset
+* @param x          input : excitation signal
+* @param x_offset   input : excitation signal offset
+* @param y          output: filtered output signal
+* @param y_offset   output: filtered output signal offset
+* @param l          input : vector dimension
+* @param mem        in/out: filter memory
+* @param mem_offset input : filter memory ofset
+* @param update     input : 0 = no memory update, 1 = update
+*/
 
-        public static void syn_filt(
-            float[] a,
-            int a_offset,
-            float[] x,
-            int x_offset,
-            float[] y,
-            int y_offset,
-            int l,
-            float[] mem,
-            int mem_offset,
-            int update
-        )
+    public static void syn_filt(
+        float[] a,
+        int a_offset,
+        float[] x,
+        int x_offset,
+        float[] y,
+        int y_offset,
+        int l,
+        float[] mem,
+        int mem_offset,
+        int update
+    )
+    {
+        var L_SUBFR = Ld8k.L_SUBFR;
+        var M = Ld8k.M;
+
+        int i, j;
+
+        /* This is usually done by memory allocation (l+m) */
+        var yy_b = new float[L_SUBFR + M];
+        float s;
+        int yy, py, pa;
+        /* Copy mem[] to yy[] */
+        yy = 0; //index instead of pointer
+        for (i = 0; i < M; i++)
         {
-            var L_SUBFR = Ld8k.L_SUBFR;
-            var M = Ld8k.M;
-
-            int i, j;
-
-            /* This is usually done by memory allocation (l+m) */
-            var yy_b = new float[L_SUBFR + M];
-            float s;
-            int yy, py, pa;
-            /* Copy mem[] to yy[] */
-            yy = 0; //index instead of pointer
-            for (i = 0; i < M; i++) yy_b[yy++] = mem[mem_offset++];
+            yy_b[yy++] = mem[mem_offset++];
+        }
 
-            /* Filtering */
+        /* Filtering */
 
-            for (i = 0; i < l; i++)
+        for (i = 0; i < l; i++)
+        {
+            py = yy;
+            pa = 0; //index instead of pointer
+            s = x[x_offset++];
+            for (j = 0; j < M; j++)
             {
-                py = yy;
-                pa = 0; //index instead of pointer
-                s = x[x_offset++];
-                for (j = 0; j < M; j++) s -= a[a_offset + ++pa] * yy_b[--py];
-                yy_b[yy++] = s;
-                y[y_offset++] = s;
+                s -= a[a_offset + ++pa] * yy_b[--py];
             }
 
-            /* Update memory if required */
-
-            if (update != 0)
-                for (i = 0; i < M; i++)
-                    mem[--mem_offset] = yy_b[--yy];
+            yy_b[yy++] = s;
+            y[y_offset++] = s;
         }
 
-        /**
- * Filter input vector with all-zero filter A(Z).
- *
- * @param a         input : prediction coefficients a[0:m+1], a[0]=1.
- * @param a_offset  input : prediction coefficients a offset
- * @param x         input : input signal x[0:l-1], x[-1:m] are needed
- * @param x_offset  input : input signal x offset
- * @param y         output: output signal y[0:l-1].
- *                  NOTE: x[] and y[] cannot point to same array
- * @param y_offset  input : output signal y offset
- * @param l         input : dimension of x and y
- */
+        /* Update memory if required */
 
-        public static void residu(
-            float[] a,
-            int a_offset,
-            float[] x,
-            int x_offset,
-            float[] y,
-            int y_offset,
-            int l
-        )
+        if (update != 0)
         {
-            var M = Ld8k.M;
+            for (i = 0; i < M; i++)
+            {
+                mem[--mem_offset] = yy_b[--yy];
+            }
+        }
+    }
+
+    /**
+* Filter input vector with all-zero filter A(Z).
+*
+* @param a         input : prediction coefficients a[0:m+1], a[0]=1.
+* @param a_offset  input : prediction coefficients a offset
+* @param x         input : input signal x[0:l-1], x[-1:m] are needed
+* @param x_offset  input : input signal x offset
+* @param y         output: output signal y[0:l-1].
+*                  NOTE: x[] and y[] cannot point to same array
+* @param y_offset  input : output signal y offset
+* @param l         input : dimension of x and y
+*/
 
-            float s;
-            int i, j;
+    public static void residu(
+        float[] a,
+        int a_offset,
+        float[] x,
+        int x_offset,
+        float[] y,
+        int y_offset,
+        int l
+    )
+    {
+        var M = Ld8k.M;
 
-            for (i = 0; i < l; i++)
+        float s;
+        int i, j;
+
+        for (i = 0; i < l; i++)
+        {
+            s = x[x_offset + i];
+            for (j = 1; j <= M; j++)
             {
-                s = x[x_offset + i];
-                for (j = 1; j <= M; j++) s += a[a_offset + j] * x[x_offset + i - j];
-                y[y_offset + i] = s;
+                s += a[a_offset + j] * x[x_offset + i - j];
             }
+
+            y[y_offset + i] = s;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/FloatReference.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/FloatReference.cs
index 5ff8065aad..a346b13adc 100644
--- a/src/SIPSorcery/app/Media/Codecs/G729Codec/FloatReference.cs
+++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/FloatReference.cs
@@ -17,10 +17,9 @@
 /**
  * @author Lubomir Marinov
  */
-namespace SIPSorcery.Media.G729Codec
+namespace SIPSorcery.Media.G729Codec;
+
+internal sealed class FloatReference
 {
-    internal class FloatReference
-    {
-        public float value;
-    }
-}
\ No newline at end of file
+    public float value;
+}
diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Gainpred.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Gainpred.cs
index 871c062528..e52a752d15 100644
--- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Gainpred.cs
+++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Gainpred.cs
@@ -24,127 +24,144 @@
  */
 using System;
 
-namespace SIPSorcery.Media.G729Codec
+namespace SIPSorcery.Media.G729Codec;
+
+internal static class Gainpred
 {
-    internal class Gainpred
-    {
 
-        /* ITU-T G.729 Software Package Release 2 (November 2006) */
-        /*
-   ITU-T G.729 Annex C - Reference C code for floating point
-                         implementation of G.729
-                         Version 1.01 of 15.September.98
+    /* ITU-T G.729 Software Package Release 2 (November 2006) */
+    /*
+ITU-T G.729 Annex C - Reference C code for floating point
+                     implementation of G.729
+                     Version 1.01 of 15.September.98
 */
 
-        /*
+    /*
 ----------------------------------------------------------------------
-                    COPYRIGHT NOTICE
+                COPYRIGHT NOTICE
 ----------------------------------------------------------------------
-   ITU-T G.729 Annex C ANSI C source code
-   Copyright (C) 1998, AT&T, France Telecom, NTT, University of
-   Sherbrooke.  All rights reserved.
+ITU-T G.729 Annex C ANSI C source code
+Copyright (C) 1998, AT&T, France Telecom, NTT, University of
+Sherbrooke.  All rights reserved.
 
 ----------------------------------------------------------------------
 */
 
-        /*
- File : GAINPRED.C
- Used for the floating point version of both
- G.729 main body and G.729A
+    /*
+File : GAINPRED.C
+Used for the floating point version of both
+G.729 main body and G.729A
 */
 
-        /**
- * MA prediction is performed on the innovation energy (in dB with mean
- * removed).
- *
- * @param past_qua_en       (i)     :Past quantized energies
- * @param code              (i)     :Innovative vector.
- * @param l_subfr           (i)     :Subframe length.
- * @return                  Predicted codebook gain
- */
+    /**
+* MA prediction is performed on the innovation energy (in dB with mean
+* removed).
+*
+* @param past_qua_en       (i)     :Past quantized energies
+* @param code              (i)     :Innovative vector.
+* @param l_subfr           (i)     :Subframe length.
+* @return                  Predicted codebook gain
+*/
+
+    public static float gain_predict(
+        float[] past_qua_en,
+        float[] code,
+        int l_subfr
+    )
+    {
+        var MEAN_ENER = Ld8k.MEAN_ENER;
+        var pred = TabLd8k.pred;
+
+        float ener_code, pred_code;
+        int i;
+        float gcode0; /* (o)     :Predicted codebook gain        */
 
-        public static float gain_predict(
-            float[] past_qua_en,
-            float[] code,
-            int l_subfr
-        )
+        pred_code = MEAN_ENER;
+
+        /* innovation energy */
+        ener_code = 0.01f;
+        for (i = 0; i < l_subfr; i++)
         {
-            var MEAN_ENER = Ld8k.MEAN_ENER;
-            var pred = TabLd8k.pred;
+            ener_code += code[i] * code[i];
+        }
 
-            float ener_code, pred_code;
-            int i;
-            float gcode0; /* (o)     :Predicted codebook gain        */
+        ener_code = 10.0f * (float)Math.Log10(ener_code / l_subfr);
 
-            pred_code = MEAN_ENER;
+        pred_code -= ener_code;
 
-            /* innovation energy */
-            ener_code = 0.01f;
-            for (i = 0; i < l_subfr; i++)
-                ener_code += code[i] * code[i];
-            ener_code = 10.0f * (float)Math.Log10(ener_code / l_subfr);
+        /* predicted energy */
+        for (i = 0; i < 4; i++)
+        {
+            pred_code += pred[i] * past_qua_en[i];
+        }
 
-            pred_code -= ener_code;
+        /* predicted codebook gain */
+        gcode0 = pred_code;
+        gcode0 = (float)Math.Pow(10.0, gcode0 / 20.0); /* predicted gain */
 
-            /* predicted energy */
-            for (i = 0; i < 4; i++) pred_code += pred[i] * past_qua_en[i];
+        return gcode0;
+    }
+
+    /**
+* Update table of past quantized energies.
+*
+* @param past_qua_en        input/output :Past quantized energies
+* @param g_code             input: gbk1[indice1][1]+gbk2[indice2][1]
+*/
 
-            /* predicted codebook gain */
-            gcode0 = pred_code;
-            gcode0 = (float)Math.Pow(10.0, gcode0 / 20.0); /* predicted gain */
+    public static void gain_update(
+        float[] past_qua_en,
+        float g_code
+    )
+    {
+        int i;
 
-            return gcode0;
+        /* update table of past quantized energies */
+        for (i = 3; i > 0; i--)
+        {
+            past_qua_en[i] = past_qua_en[i - 1];
         }
 
-        /**
- * Update table of past quantized energies.
- *
- * @param past_qua_en        input/output :Past quantized energies
- * @param g_code             input: gbk1[indice1][1]+gbk2[indice2][1]
- */
+        past_qua_en[0] = 20.0f * (float)Math.Log10(g_code);
+    }
 
-        public static void gain_update(
-            float[] past_qua_en,
-            float g_code
-        )
-        {
-            int i;
+    /**
+* Update table of past quantized energies (frame erasure).
+* 
 
+* +* @param past_qua_en input/output:Past quantized energies +*/ - /* update table of past quantized energies */ - for (i = 3; i > 0; i--) - past_qua_en[i] = past_qua_en[i - 1]; - past_qua_en[0] = 20.0f * (float)Math.Log10(g_code); + public static void gain_update_erasure( + float[] past_qua_en + ) + { + int i; + float av_pred_en; + + av_pred_en = 0.0f; + for (i = 0; i < 4; i++) + { + av_pred_en += past_qua_en[i]; } - /** - * Update table of past quantized energies (frame erasure). - *
 
- * - * @param past_qua_en input/output:Past quantized energies - */ + av_pred_en = av_pred_en * 0.25f - 4.0f; + if (av_pred_en < -14.0f) + { + av_pred_en = -14.0f; + } - public static void gain_update_erasure( - float[] past_qua_en - ) + for (i = 3; i > 0; i--) { - int i; - float av_pred_en; - - av_pred_en = 0.0f; - for (i = 0; i < 4; i++) - av_pred_en += past_qua_en[i]; - av_pred_en = av_pred_en * 0.25f - 4.0f; - if (av_pred_en < -14.0f) av_pred_en = -14.0f; - - for (i = 3; i > 0; i--) - past_qua_en[i] = past_qua_en[i - 1]; - past_qua_en[0] = av_pred_en; + past_qua_en[i] = past_qua_en[i - 1]; } + + past_qua_en[0] = av_pred_en; } } diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/IntReference.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/IntReference.cs index 38b4517c3c..15356fd8b2 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/IntReference.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/IntReference.cs @@ -17,10 +17,9 @@ /** * @author Lubomir Marinov */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal sealed class IntReference { - internal class IntReference - { - public int value; - } -} \ No newline at end of file + public int value; +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Ld8k.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Ld8k.cs index 34f6aa981d..9c2ad8ffa9 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Ld8k.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Ld8k.cs @@ -1,482 +1,480 @@ /* - * Copyright @ 2015 Atlassian Pty Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ /* - * WARNING: The use of G.729 may require a license fee and/or royalty fee in - * some countries and is licensed by - * SIPRO Lab Telecom. - */ + * WARNING: The use of G.729 may require a license fee and/or royalty fee in + * some countries and is licensed by + * SIPRO Lab Telecom. + */ /** - * @author Lubomir Marinov (translation of ITU-T C source code to Java) - */ -namespace SIPSorcery.Media.G729Codec + * @author Lubomir Marinov (translation of ITU-T C source code to Java) + */ +namespace SIPSorcery.Media.G729Codec; + +public class Ld8k { - public class Ld8k - { - /** - * gain adjustment factor - */ - public static float AGC_FAC = 0.9875f; + /** + * gain adjustment factor + */ + public const float AGC_FAC = 0.9875f; + + /** + * gain adjustment factor + */ + public const float AGC_FAC1 = 1.0f - AGC_FAC; + + public const float ALPHA = -6.0f; + + public const float BETA = 1.0f; - /** - * gain adjustment factor - */ - public static float AGC_FAC1 = 1.0f - AGC_FAC; + /** + * Definition of zero-bit in bit-stream. + */ + public const short BIT_0 = 0x007f; - public static float ALPHA = -6.0f; + /*---------------------------------------------------------------------------* + * Constants for bitstream packing * + *---------------------------------------------------------------------------*/ + /** + * Definition of one-bit in bit-stream. + */ + public const short BIT_1 = 0x0081; - public static float BETA = 1.0f; + public const float CONST12 = 1.2f; - /** - * Definition of zero-bit in bit-stream. - */ - public static short BIT_0 = 0x007f; + /*---------------------------------------------------------------------------* + * Constants for fixed codebook. * + *---------------------------------------------------------------------------*/ + /** + * Size of correlation matrix + */ + public const int DIM_RR = 616; - /*---------------------------------------------------------------------------* - * Constants for bitstream packing * - *---------------------------------------------------------------------------*/ - /** - * Definition of one-bit in bit-stream. - */ - public static short BIT_1 = 0x0081; + /** + * resolution for fractionnal delay + */ + public const int F_UP_PST = 8; - public static float CONST12 = 1.2f; + public const int L_INTER4 = 4; - /*---------------------------------------------------------------------------* - * Constants for fixed codebook. * - *---------------------------------------------------------------------------*/ - /** - * Size of correlation matrix - */ - public static int DIM_RR = 616; + public const int UP_SAMP = 3; - /** - * resolution for fractionnal delay - */ - public static int F_UP_PST = 8; + public const int FIR_SIZE_ANA = UP_SAMP * L_INTER4 + 1; - public static int L_INTER4 = 4; + public const int L_INTER10 = 10; - public static int UP_SAMP = 3; + public const int FIR_SIZE_SYN = UP_SAMP * L_INTER10 + 1; - public static int FIR_SIZE_ANA = UP_SAMP * L_INTER4 + 1; + /** + * Largest floating point number + */ + public const float FLT_MAX_G729 = float.MaxValue; - public static int L_INTER10 = 10; - - public static int FIR_SIZE_SYN = UP_SAMP * L_INTER10 + 1; + /** + * Largest floating point number + */ + public const float FLT_MIN_G729 = -FLT_MAX_G729; - /** - * Largest floating point number - */ - public static float FLT_MAX_G729 = float.MaxValue; + /** + * maximum adaptive codebook gain + */ + public const float GAIN_PIT_MAX = 1.2f; - /** - * Largest floating point number - */ - public static float FLT_MIN_G729 = -FLT_MAX_G729; + /** + * LT weighting factor + */ + public const float GAMMA_G = 0.5f; + + public const float GAMMA1_0 = 0.98f; + + public const float GAMMA1_1 = 0.94f; + + /*--------------------------------------------------------------------------- + * Constants for postfilter. + *--------------------------------------------------------------------------- + */ + /* short term pst parameters : */ + /** + * denominator weighting factor + */ + public const float GAMMA1_PST = 0.7f; + + public const float GAMMA2_0_H = 0.7f; + + public const float GAMMA2_0_L = 0.4f; + + public const float GAMMA2_1 = 0.6f; + + /** + * numerator weighting factor + */ + public const float GAMMA2_PST = 0.55f; + + /** + * tilt weighting factor when k1 < 0 + */ + public const float GAMMA3_MINUS = 0.9f; + + /** + * tilt weighting factor when k1 > 0 + */ + public const float GAMMA3_PLUS = 0.2f; + + public const float GAP1 = 0.0012f; - /** - * maximum adaptive codebook gain - */ - public static float GAIN_PIT_MAX = 1.2f; - - /** - * LT weighting factor - */ - public static float GAMMA_G = 0.5f; - - public static float GAMMA1_0 = 0.98f; - - public static float GAMMA1_1 = 0.94f; - - /*--------------------------------------------------------------------------- - * Constants for postfilter. - *--------------------------------------------------------------------------- - */ - /* short term pst parameters : */ - /** - * denominator weighting factor - */ - public static float GAMMA1_PST = 0.7f; - - public static float GAMMA2_0_H = 0.7f; - - public static float GAMMA2_0_L = 0.4f; - - public static float GAMMA2_1 = 0.6f; - - /** - * numerator weighting factor - */ - public static float GAMMA2_PST = 0.55f; - - /** - * tilt weighting factor when k1 < 0 - */ - public static float GAMMA3_MINUS = 0.9f; - - /** - * tilt weighting factor when k1 > 0 - */ - public static float GAMMA3_PLUS = 0.2f; - - public static float GAP1 = 0.0012f; - - public static float GAP2 = 0.0006f; - - public static float GAP3 = 0.0392f; - - /** - * Maximum pitch gain if taming is needed - */ - public static float GP0999 = 0.9999f; - - /*--------------------------------------------------------------------------* - * Constants for taming procedure. * - *--------------------------------------------------------------------------*/ - /** - * Maximum pitch gain if taming is needed - */ - public static float GPCLIP = 0.95f; + public const float GAP2 = 0.0006f; + + public const float GAP3 = 0.0392f; + + /** + * Maximum pitch gain if taming is needed + */ + public const float GP0999 = 0.9999f; + + /*--------------------------------------------------------------------------* + * Constants for taming procedure. * + *--------------------------------------------------------------------------*/ + /** + * Maximum pitch gain if taming is needed + */ + public const float GPCLIP = 0.95f; - /** - * Maximum pitch gain if taming is needed - */ - public static float GPCLIP2 = 0.94f; - - /** - * Resolution of lsp search. - */ - public static int GRID_POINTS = 60; + /** + * Maximum pitch gain if taming is needed + */ + public const float GPCLIP2 = 0.94f; + + /** + * Resolution of lsp search. + */ + public const int GRID_POINTS = 60; - public static float INV_COEF = -0.032623f; + public const float INV_COEF = -0.032623f; - public static - int L_SUBFR = 40; + public const int L_SUBFR = 40; - public static float INV_L_SUBFR = 1.0f / L_SUBFR; /* =0.025 */ + public const float INV_L_SUBFR = 1.0f / L_SUBFR; /* =0.025 */ - /** - * LPC update frame size - */ - public static int L_FRAME = 80; - /** - * Length for pitch interpolation - */ - - /** - * upsampling ration for pitch search - */ - - /** - * Length of filter for interpolation. - */ - public static int L_INTERPOL = 10 + 1; - - public static float L_LIMIT = 0.005f; - - /** - * Samples of next frame needed for LPC ana. - */ - public static int L_NEXT = 40; - /** - * Sub-frame size - */ - - /* long term pst parameters : */ - /** - * Sub-frame size + 1 - */ - public static int L_SUBFRP1 = L_SUBFR + 1; - - /** - * Total size of speech buffer - */ - public static int L_TOTAL = 240; - - /*---------------------------------------------------------------------------* - * Constants for lpc analysis and lsp quantizer. * - *---------------------------------------------------------------------------*/ - /** - * LPC analysis window size. - */ - public static int L_WINDOW = 240; - - public static int LH2_L = 16; - - public static int LH_UP_L = LH2_L / 2; - - public static int LH2_S = 4; - - public static int LH_UP_S = LH2_S / 2; - /** - * length of long interp. subfilters - */ - - public static int LH2_L_P1 = LH2_L + 1; - /** - * length of short interp. subfilters - */ - - /** - * impulse response length - */ - public static int LONG_H_ST = 20; - - /** - * LPC order. - */ - public static int M = 10; - - public static float M_LIMIT = 3.135f; - - /** - * MA prediction order for LSP. - */ - public static int MA_NP = 4; - - public static int MAX_TIME = 75; - - /*------------------------------------------------------------------------- - * gain quantizer constants - *------------------------------------------------------------------------- - */ - /** - * Average innovation energy - */ - public static float MEAN_ENER = 36.0f; - - /* Array sizes */ - public static int PIT_MAX = 143; - - public static int MEM_RES2 = PIT_MAX + 1 + LH_UP_L; - - /** - * LT gain minimum - */ - public static float MIN_GPLT = 1.0f / (1.0f + GAMMA_G); - - /** - * Number of modes for MA prediction. - */ - public static int MODE = 2; - - /** - * LPC order+1. - */ - public static int MP1 = M + 1; - - /** - * Size of vectors for cross-correlation between 2 pulses - */ - public static int MSIZE = 64; - - /** - * Number of positions for each pulse - */ - public static int NB_POS = 8; - - /** - * LPC order / 2. - */ - public static int NC = M / 2; - - /** - * Number of entries in first stage. - */ - public static int NC0_B = 7; - - public static int NC0 = 1 << NC0_B; - /** - * Number of bits in first stage. - */ - - /** - * Number of entries in second stage. - */ - public static int NC1_B = 5; - - public static int NC1 = 1 << NC1_B; - /** - * Number of bits in second stage. - */ - - /** - * Pre-selecting order for #1 - */ - public static int NCAN1 = 4; - - /** - * Pre-selecting order for #2 - */ - public static int NCAN2 = 8; - - /** - * Codebook 1 size - */ - public static int NCODE1_B = 3; - - public static int NCODE1 = 1 << NCODE1_B; - /** - * Number of Codebook-bit - */ - - /** - * Codebook 2 size - */ - public static int NCODE2_B = 4; - - public static int NCODE2 = 1 << NCODE2_B; - /** - * Number of Codebook-bit - */ - - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /** + * LPC update frame size + */ + public const int L_FRAME = 80; + /** + * Length for pitch interpolation + */ + + /** + * upsampling ration for pitch search + */ + + /** + * Length of filter for interpolation. + */ + public const int L_INTERPOL = 10 + 1; + + public const float L_LIMIT = 0.005f; + + /** + * Samples of next frame needed for LPC ana. + */ + public const int L_NEXT = 40; + /** + * Sub-frame size + */ + + /* long term pst parameters : */ + /** + * Sub-frame size + 1 + */ + public const int L_SUBFRP1 = L_SUBFR + 1; + + /** + * Total size of speech buffer + */ + public const int L_TOTAL = 240; + + /*---------------------------------------------------------------------------* + * Constants for lpc analysis and lsp quantizer. * + *---------------------------------------------------------------------------*/ + /** + * LPC analysis window size. + */ + public const int L_WINDOW = 240; + + public const int LH2_L = 16; + + public const int LH_UP_L = LH2_L / 2; + + public const int LH2_S = 4; + + public const int LH_UP_S = LH2_S / 2; + /** + * length of long interp. subfilters + */ + + public const int LH2_L_P1 = LH2_L + 1; + /** + * length of short interp. subfilters + */ + + /** + * impulse response length + */ + public const int LONG_H_ST = 20; + + /** + * LPC order. + */ + public const int M = 10; + + public const float M_LIMIT = 3.135f; + + /** + * MA prediction order for LSP. + */ + public const int MA_NP = 4; + + public const int MAX_TIME = 75; + + /*------------------------------------------------------------------------- + * gain quantizer constants + *------------------------------------------------------------------------- + */ + /** + * Average innovation energy + */ + public const float MEAN_ENER = 36.0f; + + /* Array sizes */ + public const int PIT_MAX = 143; + + public const int MEM_RES2 = PIT_MAX + 1 + LH_UP_L; + + /** + * LT gain minimum + */ + public const float MIN_GPLT = 1.0f / (1.0f + GAMMA_G); + + /** + * Number of modes for MA prediction. + */ + public const int MODE = 2; + + /** + * LPC order+1. + */ + public const int MP1 = M + 1; + + /** + * Size of vectors for cross-correlation between 2 pulses + */ + public const int MSIZE = 64; + + /** + * Number of positions for each pulse + */ + public const int NB_POS = 8; + + /** + * LPC order / 2. + */ + public const int NC = M / 2; + + /** + * Number of entries in first stage. + */ + public const int NC0_B = 7; + + public const int NC0 = 1 << NC0_B; + /** + * Number of bits in first stage. + */ + + /** + * Number of entries in second stage. + */ + public const int NC1_B = 5; + + public const int NC1 = 1 << NC1_B; + /** + * Number of bits in second stage. + */ + + /** + * Pre-selecting order for #1 + */ + public const int NCAN1 = 4; + + /** + * Pre-selecting order for #2 + */ + public const int NCAN2 = 8; + + /** + * Codebook 1 size + */ + public const int NCODE1_B = 3; + + public const int NCODE1 = 1 << NCODE1_B; + /** + * Number of Codebook-bit + */ + + /** + * Codebook 2 size + */ + public const int NCODE2_B = 4; + + public const int NCODE2 = 1 << NCODE2_B; + /** + * Number of Codebook-bit + */ + + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : LD8K.H - Used for the floating point version of G.729 main body - (not for G.729A) + /* +File : LD8K.H +Used for the floating point version of G.729 main body +(not for G.729A) */ - /*--------------------------------------------------------------------------- - * ld8k.h - include file for all ITU-T 8 kb/s CELP coder routines - *--------------------------------------------------------------------------- - */ - - public static float PI = 3.14159265358979323846f; - - /** - * pi*0.04 - */ - public static float PI04 = PI * 0.04f; - - /** - * pi*0.92 - */ - public static float PI92 = PI * 0.92f; - /** - * Maximum pitch lag in samples - */ - - /*---------------------------------------------------------------------------- - * Constants for long-term predictor - *---------------------------------------------------------------------------- - */ - /** - * Minimum pitch lag in samples - */ - public static int PIT_MIN = 20; - - /** - * Number of parameters per 10 ms frame. - */ - public static int PRM_SIZE = 11; - - /** - * Bits per frame. - */ - public static int SERIAL_SIZE = 82; - - /** - * Maximum value of pitch sharpening - */ - public static float SHARPMAX = 0.7945f; - - /** - * minimum value of pitch sharpening - */ - public static float SHARPMIN = 0.2f; - - public static int SIZ_RES2 = MEM_RES2 + L_SUBFR; - - public static int SIZ_TAB_HUP_L = (F_UP_PST - 1) * LH2_L; - - public static int SIZ_TAB_HUP_S = (F_UP_PST - 1) * LH2_S; - - public static int SIZ_Y_UP = (F_UP_PST - 1) * L_SUBFRP1; - - /** - * Size of bitstream frame. - */ - public static short SIZE_WORD = 80; - - /** - * Step betweem position of the same pulse. - */ - public static int STEP = 5; - - /** - * Definition of frame erasure flag. - */ - public static short SYNC_WORD = 0x6b21; - - /** - * threshold LT pst switch off - */ - public static float THRESCRIT = 0.5f; - - /** - * Error threshold taming - */ - public static float THRESH_ERR = 60000.0f; - - public static float THRESH_H1 = 0.65f; - - public static float THRESH_H2 = 0.43f; - - /*------------------------------------------------------------------------- - * pwf constants - *------------------------------------------------------------------------- - */ - - public static float THRESH_L1 = -1.74f; - - public static float THRESH_L2 = -1.52f; - - /*--------------------------------------------------------------------------* - * Example values for threshold and approximated worst case complexity: * - * * - * threshold=0.40 maxtime= 75 extra=30 Mips = 6.0 * - *--------------------------------------------------------------------------*/ - public static float THRESHFCB = 0.40f; - - /** - * Threshold to favor smaller pitch lags - */ - public static float THRESHPIT = 0.85f; - - /** - * resolution of fractional delays - */ - } + /*--------------------------------------------------------------------------- + * ld8k.h - include file for all ITU-T 8 kb/s CELP coder routines + *--------------------------------------------------------------------------- + */ + + public const float PI = 3.14159265358979323846f; + + /** + * pi*0.04 + */ + public const float PI04 = PI * 0.04f; + + /** + * pi*0.92 + */ + public const float PI92 = PI * 0.92f; + /** + * Maximum pitch lag in samples + */ + + /*---------------------------------------------------------------------------- + * Constants for long-term predictor + *---------------------------------------------------------------------------- + */ + /** + * Minimum pitch lag in samples + */ + public const int PIT_MIN = 20; + + /** + * Number of parameters per 10 ms frame. + */ + public const int PRM_SIZE = 11; + + /** + * Bits per frame. + */ + public const int SERIAL_SIZE = 82; + + /** + * Maximum value of pitch sharpening + */ + public const float SHARPMAX = 0.7945f; + + /** + * minimum value of pitch sharpening + */ + public const float SHARPMIN = 0.2f; + + public const int SIZ_RES2 = MEM_RES2 + L_SUBFR; + + public const int SIZ_TAB_HUP_L = (F_UP_PST - 1) * LH2_L; + + public const int SIZ_TAB_HUP_S = (F_UP_PST - 1) * LH2_S; + + public const int SIZ_Y_UP = (F_UP_PST - 1) * L_SUBFRP1; + + /** + * Size of bitstream frame. + */ + public const short SIZE_WORD = 80; + + /** + * Step betweem position of the same pulse. + */ + public const int STEP = 5; + + /** + * Definition of frame erasure flag. + */ + public const short SYNC_WORD = 0x6b21; + + /** + * threshold LT pst switch off + */ + public const float THRESCRIT = 0.5f; + + /** + * Error threshold taming + */ + public const float THRESH_ERR = 60000.0f; + + public const float THRESH_H1 = 0.65f; + + public const float THRESH_H2 = 0.43f; + + /*------------------------------------------------------------------------- + * pwf constants + *------------------------------------------------------------------------- + */ + + public const float THRESH_L1 = -1.74f; + + public const float THRESH_L2 = -1.52f; + + /*--------------------------------------------------------------------------* + * Example values for threshold and approximated worst case complexity: * + * * + * threshold=0.40 maxtime= 75 extra=30 Mips = 6.0 * + *--------------------------------------------------------------------------*/ + public const float THRESHFCB = 0.40f; + + /** + * Threshold to favor smaller pitch lags + */ + public const float THRESHPIT = 0.85f; + + /** + * resolution of fractional delays + */ } diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Lpc.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Lpc.cs index 5bc1f3e0d1..f7f6c02d9b 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Lpc.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Lpc.cs @@ -26,297 +26,315 @@ * * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal static class Lpc { - internal class Lpc - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : LPC.C - Used for the floating point version of G.729 main body - (not for G.729A) + /* +File : LPC.C +Used for the floating point version of G.729 main body +(not for G.729A) */ - /** - * Compute the auto-correlations of windowed speech signal - * - * @param x (i) input signal x[0:L_WINDOW] - * @param x_offset (i) input signal offset - * @param m (i) LPC order - * @param r (o) auto-correlation vector r[0:M] - */ + /** +* Compute the auto-correlations of windowed speech signal +* +* @param x (i) input signal x[0:L_WINDOW] +* @param x_offset (i) input signal offset +* @param m (i) LPC order +* @param r (o) auto-correlation vector r[0:M] +*/ - public static void autocorr( - float[] x, - int x_offset, - int m, - float[] r - ) - { - var L_WINDOW = Ld8k.L_WINDOW; - var hamwindow = TabLd8k.hamwindow; + public static void autocorr( + float[] x, + int x_offset, + int m, + float[] r + ) + { + var L_WINDOW = Ld8k.L_WINDOW; + var hamwindow = TabLd8k.hamwindow; - var y = new float[L_WINDOW]; - float sum; - int i, j; + var y = new float[L_WINDOW]; + float sum; + int i, j; - for (i = 0; i < L_WINDOW; i++) - y[i] = x[x_offset + i] * hamwindow[i]; + for (i = 0; i < L_WINDOW; i++) + { + y[i] = x[x_offset + i] * hamwindow[i]; + } - for (i = 0; i <= m; i++) + for (i = 0; i <= m; i++) + { + sum = 0.0f; + for (j = 0; j < L_WINDOW - i; j++) { - sum = 0.0f; - for (j = 0; j < L_WINDOW - i; j++) - sum += y[j] * y[j + i]; - r[i] = sum; + sum += y[j] * y[j + i]; } - if (r[0] < 1.0f) r[0] = 1.0f; + r[i] = sum; } - /** - * Lag windowing of the autocorrelations - * - * @param m (i) LPC order - * @param r (i/o) correlation - */ - - public static void lag_window( - int m, - float[] r - ) + if (r[0] < 1.0f) { - var lwindow = TabLd8k.lwindow; + r[0] = 1.0f; + } + } - int i; + /** +* Lag windowing of the autocorrelations +* +* @param m (i) LPC order +* @param r (i/o) correlation +*/ - for (i = 1; i <= m; i++) - r[i] *= lwindow[i - 1]; - } + public static void lag_window( + int m, + float[] r + ) + { + var lwindow = TabLd8k.lwindow; - /** - * Levinson-Durbin recursion to compute LPC parameters. - * - * @param r (i) auto correlation coefficients r[0:M] - * @param a (o) lpc coefficients a[0] = 1 - * @param a_offset (i) lpc coefficients offset - * @param rc (o) reflection coefficients rc[0:M-1] - * @return prediction error (energy) - */ + int i; - public static float levinson( - float[] r, - float[] a, - int a_offset, - float[] rc - ) + for (i = 1; i <= m; i++) { - var M = Ld8k.M; + r[i] *= lwindow[i - 1]; + } + } + + /** +* Levinson-Durbin recursion to compute LPC parameters. +* +* @param r (i) auto correlation coefficients r[0:M] +* @param a (o) lpc coefficients a[0] = 1 +* @param a_offset (i) lpc coefficients offset +* @param rc (o) reflection coefficients rc[0:M-1] +* @return prediction error (energy) +*/ + + public static float levinson( + float[] r, + float[] a, + int a_offset, + float[] rc + ) + { + var M = Ld8k.M; - float s, at, err; - int i, j, l; + float s, at, err; + int i, j, l; - rc[0] = -r[1] / r[0]; - a[a_offset + 0] = 1.0f; - a[a_offset + 1] = rc[0]; - err = r[0] + r[1] * rc[0]; - for (i = 2; i <= M; i++) + rc[0] = -r[1] / r[0]; + a[a_offset + 0] = 1.0f; + a[a_offset + 1] = rc[0]; + err = r[0] + r[1] * rc[0]; + for (i = 2; i <= M; i++) + { + s = 0.0f; + for (j = 0; j < i; j++) { - s = 0.0f; - for (j = 0; j < i; j++) - s += r[i - j] * a[a_offset + j]; - rc[i - 1] = -s / err; - for (j = 1; j <= i / 2; j++) - { - l = i - j; - at = a[a_offset + j] + rc[i - 1] * a[a_offset + l]; - a[a_offset + l] += rc[i - 1] * a[a_offset + j]; - a[a_offset + j] = at; - } + s += r[i - j] * a[a_offset + j]; + } - a[a_offset + i] = rc[i - 1]; - err += rc[i - 1] * s; - if (err <= 0.0f) - err = 0.001f; + rc[i - 1] = -s / err; + for (j = 1; j <= i / 2; j++) + { + l = i - j; + at = a[a_offset + j] + rc[i - 1] * a[a_offset + l]; + a[a_offset + l] += rc[i - 1] * a[a_offset + j]; + a[a_offset + j] = at; } - return err; + a[a_offset + i] = rc[i - 1]; + err += rc[i - 1] * s; + if (err <= 0.0f) + { + err = 0.001f; + } } - /** * - * Compute the LSPs from the LP coefficients a[] using Chebyshev - * polynomials. The found LSPs are in the cosine domain with values - * in the range from 1 down to -1. - * The table grid[] contains the points (in the cosine domain) at - * which the polynomials are evaluated. The table corresponds to - * NO_POINTS frequencies uniformly spaced between 0 and pi. - * - * @param a (i) LP filter coefficients - * @param a_offset (i) LP filter coefficients offset - * @param lsp (o) Line spectral pairs (in the cosine domain) - * @param old_lsp (i) LSP vector from past frame - */ + return err; + } + + /** * +* Compute the LSPs from the LP coefficients a[] using Chebyshev +* polynomials. The found LSPs are in the cosine domain with values +* in the range from 1 down to -1. +* The table grid[] contains the points (in the cosine domain) at +* which the polynomials are evaluated. The table corresponds to +* NO_POINTS frequencies uniformly spaced between 0 and pi. +* +* @param a (i) LP filter coefficients +* @param a_offset (i) LP filter coefficients offset +* @param lsp (o) Line spectral pairs (in the cosine domain) +* @param old_lsp (i) LSP vector from past frame +*/ - public static void az_lsp( - float[] a, - int a_offset, - float[] lsp, - float[] old_lsp - ) + public static void az_lsp( + float[] a, + int a_offset, + float[] lsp, + float[] old_lsp + ) + { + var GRID_POINTS = Ld8k.GRID_POINTS; + var M = Ld8k.M; + var NC = Ld8k.NC; + var grid = TabLd8k.grid; + + int i, j, nf, ip; + float xlow, ylow, xhigh, yhigh, xmid, ymid, xint; + float[] coef; + + float[] f1 = new float[NC + 1], f2 = new float[NC + 1]; + + /*-------------------------------------------------------------* +* find the sum and diff polynomials F1(z) and F2(z) * +* F1(z) = [A(z) + z^11 A(z^-1)]/(1+z^-1) * +* F2(z) = [A(z) - z^11 A(z^-1)]/(1-z^-1) * +*-------------------------------------------------------------*/ + + f1[0] = 1.0f; + f2[0] = 1.0f; + for (i = 1, j = a_offset + M; i <= NC; i++, j--) { - var GRID_POINTS = Ld8k.GRID_POINTS; - var M = Ld8k.M; - var NC = Ld8k.NC; - var grid = TabLd8k.grid; - - int i, j, nf, ip; - float xlow, ylow, xhigh, yhigh, xmid, ymid, xint; - float[] coef; - - float[] f1 = new float[NC + 1], f2 = new float[NC + 1]; - - /*-------------------------------------------------------------* - * find the sum and diff polynomials F1(z) and F2(z) * - * F1(z) = [A(z) + z^11 A(z^-1)]/(1+z^-1) * - * F2(z) = [A(z) - z^11 A(z^-1)]/(1-z^-1) * - *-------------------------------------------------------------*/ - - f1[0] = 1.0f; - f2[0] = 1.0f; - for (i = 1, j = a_offset + M; i <= NC; i++, j--) - { - var ai = a[a_offset + i]; - var aj = a[j]; - f1[i] = ai + aj - f1[i - 1]; - f2[i] = ai - aj + f2[i - 1]; - } + var ai = a[a_offset + i]; + var aj = a[j]; + f1[i] = ai + aj - f1[i - 1]; + f2[i] = ai - aj + f2[i - 1]; + } + + /*---------------------------------------------------------------------* +* Find the LSPs (roots of F1(z) and F2(z) ) using the * +* Chebyshev polynomial evaluation. * +* The roots of F1(z) and F2(z) are alternatively searched. * +* We start by finding the first root of F1(z) then we switch * +* to F2(z) then back to F1(z) and so on until all roots are found. * +* * +* - Evaluate Chebyshev pol. at grid points and check for sign change.* +* - If sign change track the root by subdividing the interval * +* NO_ITER times and ckecking sign change. * +*---------------------------------------------------------------------*/ - /*---------------------------------------------------------------------* - * Find the LSPs (roots of F1(z) and F2(z) ) using the * - * Chebyshev polynomial evaluation. * - * The roots of F1(z) and F2(z) are alternatively searched. * - * We start by finding the first root of F1(z) then we switch * - * to F2(z) then back to F1(z) and so on until all roots are found. * - * * - * - Evaluate Chebyshev pol. at grid points and check for sign change.* - * - If sign change track the root by subdividing the interval * - * NO_ITER times and ckecking sign change. * - *---------------------------------------------------------------------*/ + nf = 0; /* number of found frequencies */ + ip = 0; /* flag to first polynomial */ - nf = 0; /* number of found frequencies */ - ip = 0; /* flag to first polynomial */ + coef = f1; /* start with F1(z) */ - coef = f1; /* start with F1(z) */ + xlow = grid[0]; + ylow = chebyshev(xlow, coef, NC); - xlow = grid[0]; + j = 0; + while (nf < M && j < GRID_POINTS) + { + j++; + xhigh = xlow; + yhigh = ylow; + xlow = grid[j]; ylow = chebyshev(xlow, coef, NC); - j = 0; - while (nf < M && j < GRID_POINTS) + if (ylow * yhigh <= 0.0f) /* if sign change new root exists */ { - j++; - xhigh = xlow; - yhigh = ylow; - xlow = grid[j]; - ylow = chebyshev(xlow, coef, NC); + j--; - if (ylow * yhigh <= 0.0f) /* if sign change new root exists */ - { - j--; - - /* divide the interval of sign change by 4 */ + /* divide the interval of sign change by 4 */ - for (i = 0; i < 4; i++) + for (i = 0; i < 4; i++) + { + xmid = 0.5f * (xlow + xhigh); + ymid = chebyshev(xmid, coef, NC); + if (ylow * ymid <= 0.0f) + { + yhigh = ymid; + xhigh = xmid; + } + else { - xmid = 0.5f * (xlow + xhigh); - ymid = chebyshev(xmid, coef, NC); - if (ylow * ymid <= 0.0f) - { - yhigh = ymid; - xhigh = xmid; - } - else - { - ylow = ymid; - xlow = xmid; - } + ylow = ymid; + xlow = xmid; } + } - /* linear interpolation for evaluating the root */ + /* linear interpolation for evaluating the root */ - xint = xlow - ylow * (xhigh - xlow) / (yhigh - ylow); + xint = xlow - ylow * (xhigh - xlow) / (yhigh - ylow); - lsp[nf] = xint; /* new root */ - nf++; + lsp[nf] = xint; /* new root */ + nf++; - ip = 1 - ip; /* flag to other polynomial */ - coef = ip != 0 ? f2 : f1; /* pointer to other polynomial */ + ip = 1 - ip; /* flag to other polynomial */ + coef = ip != 0 ? f2 : f1; /* pointer to other polynomial */ - xlow = xint; - ylow = chebyshev(xlow, coef, NC); - } + xlow = xint; + ylow = chebyshev(xlow, coef, NC); } + } - /* Check if M roots found */ - /* if not use the LSPs from previous frame */ + /* Check if M roots found */ + /* if not use the LSPs from previous frame */ - if (nf < M) - for (i = 0; i < M; i++) - lsp[i] = old_lsp[i]; + if (nf < M) + { + for (i = 0; i < M; i++) + { + lsp[i] = old_lsp[i]; + } } + } - /** - * Evaluates the Chebyshev polynomial series. - * - * The polynomial order is - * n = m/2 (m is the prediction order) - * The polynomial is given by - * C(x) = T_n(x) + f(1)T_n-1(x) + ... +f(n-1)T_1(x) + f(n)/2 - * - * @param x (i) value of evaluation; x=cos(freq) - * @param f (i) coefficients of sum or diff polynomial - * @param n (i) order of polynomial - * @return the value of the polynomial C(x) - */ - private static float chebyshev( - float x, - float[] f, - int n - ) + /** +* Evaluates the Chebyshev polynomial series. +* +* The polynomial order is +* n = m/2 (m is the prediction order) +* The polynomial is given by +* C(x) = T_n(x) + f(1)T_n-1(x) + ... +f(n-1)T_1(x) + f(n)/2 +* +* @param x (i) value of evaluation; x=cos(freq) +* @param f (i) coefficients of sum or diff polynomial +* @param n (i) order of polynomial +* @return the value of the polynomial C(x) +*/ + private static float chebyshev( + float x, + float[] f, + int n + ) + { + float b1, b2, b0, x2; + int i; /* for the special case of 10th order */ + /* filter (n=5) */ + x2 = 2.0f * x; /* x2 = 2.0*x; */ + b2 = 1.0f; /* f[0] */ /* */ + b1 = x2 + f[1]; /* b1 = x2 + f[1]; */ + for (i = 2; i < n; i++) { - float b1, b2, b0, x2; - int i; /* for the special case of 10th order */ - /* filter (n=5) */ - x2 = 2.0f * x; /* x2 = 2.0*x; */ - b2 = 1.0f; /* f[0] */ /* */ - b1 = x2 + f[1]; /* b1 = x2 + f[1]; */ - for (i = 2; i < n; i++) - { - /* */ - b0 = x2 * b1 - b2 + f[i]; /* b0 = x2 * b1 - 1. + f[2]; */ - b2 = b1; /* b2 = x2 * b0 - b1 + f[3]; */ - b1 = b0; /* b1 = x2 * b2 - b0 + f[4]; */ - } /* */ + /* */ + b0 = x2 * b1 - b2 + f[i]; /* b0 = x2 * b1 - 1. + f[2]; */ + b2 = b1; /* b2 = x2 * b0 - b1 + f[3]; */ + b1 = b0; /* b1 = x2 * b2 - b0 + f[4]; */ + } /* */ - return x * b1 - b2 + 0.5f * f[n]; /* return (x*b1 - b2 + 0.5*f[5]); */ - } + return x * b1 - b2 + 0.5f * f[n]; /* return (x*b1 - b2 + 0.5*f[5]); */ } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Lpcfunc.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Lpcfunc.cs index 8887b7f584..93ffabae12 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Lpcfunc.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Lpcfunc.cs @@ -24,226 +24,236 @@ */ using System; -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal static class Lpcfunc { - internal class Lpcfunc - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : LPCFUNC.C - Used for the floating point version of G.729 main body - (not for G.729A) + /* +File : LPCFUNC.C +Used for the floating point version of G.729 main body +(not for G.729A) */ - /** - * Convert LSPs to predictor coefficients a[] - * - * @param lsp input : lsp[0:M-1] - * @param a output: predictor coeffs a[0:M], a[0] = 1. - * @param a_offset input: predictor coeffs a offset. - */ - private static void lsp_az( - float[] lsp, - float[] a, - int a_offset - ) - { - var M = Ld8k.M; - var NC = Ld8k.NC; - - float[] f1 = new float[NC + 1], f2 = new float[NC + 1]; - int i, j; + /** +* Convert LSPs to predictor coefficients a[] +* +* @param lsp input : lsp[0:M-1] +* @param a output: predictor coeffs a[0:M], a[0] = 1. +* @param a_offset input: predictor coeffs a offset. +*/ + private static void lsp_az( + float[] lsp, + float[] a, + int a_offset + ) + { + var M = Ld8k.M; + var NC = Ld8k.NC; - get_lsp_pol(lsp, 0, f1); - get_lsp_pol(lsp, 1, f2); + float[] f1 = new float[NC + 1], f2 = new float[NC + 1]; + int i, j; - for (i = NC; i > 0; i--) - { - f1[i] += f1[i - 1]; - f2[i] -= f2[i - 1]; - } + get_lsp_pol(lsp, 0, f1); + get_lsp_pol(lsp, 1, f2); - a[a_offset + 0] = 1.0f; - for (i = 1, j = M; i <= NC; i++, j--) - { - a[a_offset + i] = 0.5f * (f1[i] + f2[i]); - a[a_offset + j] = 0.5f * (f1[i] - f2[i]); - } + for (i = NC; i > 0; i--) + { + f1[i] += f1[i - 1]; + f2[i] -= f2[i - 1]; } - /** - * Find the polynomial F1(z) or F2(z) from the LSFs - * - * @param lsp input : line spectral freq. (cosine domain) - * @param lsp_offset input : line spectral freq offset - * @param f output: the coefficients of F1 or F2 - */ - private static void get_lsp_pol( - float[] lsp, - int lsp_offset, - float[] f - ) + a[a_offset + 0] = 1.0f; + for (i = 1, j = M; i <= NC; i++, j--) { - var NC = Ld8k.NC; + a[a_offset + i] = 0.5f * (f1[i] + f2[i]); + a[a_offset + j] = 0.5f * (f1[i] - f2[i]); + } + } + + /** +* Find the polynomial F1(z) or F2(z) from the LSFs +* +* @param lsp input : line spectral freq. (cosine domain) +* @param lsp_offset input : line spectral freq offset +* @param f output: the coefficients of F1 or F2 +*/ + private static void get_lsp_pol( + float[] lsp, + int lsp_offset, + float[] f + ) + { + var NC = Ld8k.NC; - float b; - int i, j; + float b; + int i, j; - f[0] = 1.0f; - b = -2.0f * lsp[lsp_offset + 0]; - f[1] = b; - for (i = 2; i <= NC; i++) + f[0] = 1.0f; + b = -2.0f * lsp[lsp_offset + 0]; + f[1] = b; + for (i = 2; i <= NC; i++) + { + b = -2.0f * lsp[lsp_offset + 2 * i - 2]; + f[i] = b * f[i - 1] + 2.0f * f[i - 2]; + for (j = i - 1; j > 1; j--) { - b = -2.0f * lsp[lsp_offset + 2 * i - 2]; - f[i] = b * f[i - 1] + 2.0f * f[i - 2]; - for (j = i - 1; j > 1; j--) - f[j] += b * f[j - 1] + f[j - 2]; - f[1] += b; + f[j] += b * f[j - 1] + f[j - 2]; } - } - /** - * Convert from lsf[0..M-1 to lsp[0..M-1] - * - * @param lsf input : lsf - * @param lsp output: lsp - * @param m input : length - */ - private static void lsf_lsp( - float[] lsf, - float[] lsp, - int m - ) - { - int i; - for (i = 0; i < m; i++) - lsp[i] = (float)Math.Cos(lsf[i]); + f[1] += b; } + } - /** - * Convert from lsp[0..M-1 to lsf[0..M-1] - * - * @param lsp input : lsp coefficients - * @param lsf output: lsf (normalized frequencies - * @param m input: length - */ - private static void lsp_lsf( - float[] lsp, - float[] lsf, - int m - ) + /** +* Convert from lsf[0..M-1 to lsp[0..M-1] +* +* @param lsf input : lsf +* @param lsp output: lsp +* @param m input : length +*/ + private static void lsf_lsp( + float[] lsf, + float[] lsp, + int m + ) + { + int i; + for (i = 0; i < m; i++) { - int i; - - for (i = 0; i < m; i++) - lsf[i] = (float)Math.Acos(lsp[i]); + lsp[i] = (float)Math.Cos(lsf[i]); } + } - /** - * Weighting of LPC coefficients ap[i] = a[i] * (gamma ** i) - * - * @param a input : lpc coefficients a[0:m] - * @param a_offset input : lpc coefficients offset - * @param gamma input : weighting factor - * @param m input : filter order - * @param ap output: weighted coefficients ap[0:m] - */ + /** +* Convert from lsp[0..M-1 to lsf[0..M-1] +* +* @param lsp input : lsp coefficients +* @param lsf output: lsf (normalized frequencies +* @param m input: length +*/ + private static void lsp_lsf( + float[] lsp, + float[] lsf, + int m + ) + { + int i; - public static void weight_az( - float[] a, - int a_offset, - float gamma, - int m, - float[] ap - ) + for (i = 0; i < m; i++) { - float fac; - int i; - - ap[0] = a[a_offset + 0]; - fac = gamma; - for (i = 1; i < m; i++) - { - ap[i] = fac * a[a_offset + i]; - fac *= gamma; - } - - ap[m] = fac * a[a_offset + m]; + lsf[i] = (float)Math.Acos(lsp[i]); } + } - /** - * Interpolated M LSP parameters and convert to M+1 LPC coeffs - * - * @param lsp_old input : LSPs for past frame (0:M-1) - * @param lsp_new input : LSPs for present frame (0:M-1) - * @param az output: filter parameters in 2 subfr (dim 2(m+1)) - */ + /** +* Weighting of LPC coefficients ap[i] = a[i] * (gamma ** i) +* +* @param a input : lpc coefficients a[0:m] +* @param a_offset input : lpc coefficients offset +* @param gamma input : weighting factor +* @param m input : filter order +* @param ap output: weighted coefficients ap[0:m] +*/ + + public static void weight_az( + float[] a, + int a_offset, + float gamma, + int m, + float[] ap + ) + { + float fac; + int i; - public static void int_qlpc( - float[] lsp_old, - float[] lsp_new, - float[] az - ) + ap[0] = a[a_offset + 0]; + fac = gamma; + for (i = 1; i < m; i++) { - var M = Ld8k.M; + ap[i] = fac * a[a_offset + i]; + fac *= gamma; + } - int i; - var lsp = new float[M]; + ap[m] = fac * a[a_offset + m]; + } - for (i = 0; i < M; i++) - lsp[i] = lsp_old[i] * 0.5f + lsp_new[i] * 0.5f; + /** +* Interpolated M LSP parameters and convert to M+1 LPC coeffs +* +* @param lsp_old input : LSPs for past frame (0:M-1) +* @param lsp_new input : LSPs for present frame (0:M-1) +* @param az output: filter parameters in 2 subfr (dim 2(m+1)) +*/ - lsp_az(lsp, az, 0); - lsp_az(lsp_new, az, M + 1); - } - /** - * Interpolated M LSP parameters and convert to M+1 LPC coeffs - * - * @param lsp_old input : LSPs for past frame (0:M-1) - * @param lsp_new input : LSPs for present frame (0:M-1) - * @param lsf_int output: interpolated lsf coefficients - * @param lsf_new input : LSFs for present frame (0:M-1) - * @param az output: filter parameters in 2 subfr (dim 2(m+1)) - */ + public static void int_qlpc( + float[] lsp_old, + float[] lsp_new, + float[] az + ) + { + var M = Ld8k.M; + + int i; + var lsp = new float[M]; - public static void int_lpc( - float[] lsp_old, - float[] lsp_new, - float[] lsf_int, - float[] lsf_new, - float[] az - ) + for (i = 0; i < M; i++) { - var M = Ld8k.M; + lsp[i] = lsp_old[i] * 0.5f + lsp_new[i] * 0.5f; + } - int i; - var lsp = new float[M]; + lsp_az(lsp, az, 0); + lsp_az(lsp_new, az, M + 1); + } + /** +* Interpolated M LSP parameters and convert to M+1 LPC coeffs +* +* @param lsp_old input : LSPs for past frame (0:M-1) +* @param lsp_new input : LSPs for present frame (0:M-1) +* @param lsf_int output: interpolated lsf coefficients +* @param lsf_new input : LSFs for present frame (0:M-1) +* @param az output: filter parameters in 2 subfr (dim 2(m+1)) +*/ - for (i = 0; i < M; i++) - lsp[i] = lsp_old[i] * 0.5f + lsp_new[i] * 0.5f; + public static void int_lpc( + float[] lsp_old, + float[] lsp_new, + float[] lsf_int, + float[] lsf_new, + float[] az + ) + { + var M = Ld8k.M; - lsp_az(lsp, az, 0); + int i; + var lsp = new float[M]; - lsp_lsf(lsp, lsf_int, M); - lsp_lsf(lsp_new, lsf_new, M); + for (i = 0; i < M; i++) + { + lsp[i] = lsp_old[i] * 0.5f + lsp_new[i] * 0.5f; } + + lsp_az(lsp, az, 0); + + lsp_lsf(lsp, lsf_int, M); + lsp_lsf(lsp_new, lsf_new, M); } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Lspdec.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Lspdec.cs index 83e97276d2..5b4badd3f8 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Lspdec.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Lspdec.cs @@ -24,196 +24,204 @@ */ using System; -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal sealed class Lspdec { - internal class Lspdec - { - /** - * Previous LSP vector(init) - */ - private static readonly float[ /* M */] FREQ_PREV_RESET = - { - 0.285599f, - 0.571199f, - 0.856798f, - 1.142397f, - 1.427997f, - 1.713596f, - 1.999195f, - 2.284795f, - 2.570394f, - 2.855993f - }; /* PI*(float)(j+1)/(float)(M+1) */ - - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /** +* Previous LSP vector(init) +*/ + private static readonly float[ /* M */] FREQ_PREV_RESET = + { + 0.285599f, + 0.571199f, + 0.856798f, + 1.142397f, + 1.427997f, + 1.713596f, + 1.999195f, + 2.284795f, + 2.570394f, + 2.855993f + }; /* PI*(float)(j+1)/(float)(M+1) */ + + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : LSPDEC.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : LSPDEC.C +Used for the floating point version of both +G.729 main body and G.729A */ - private static readonly int M = Ld8k.M; + private static readonly int M = Ld8k.M; - private static readonly int MA_NP = Ld8k.MA_NP; + private static readonly int MA_NP = Ld8k.MA_NP; - /* static memory */ - /** - * Previous LSP vector - */ - private readonly float[][] freq_prev = new float[MA_NP][ /* M */]; + /* static memory */ + /** +* Previous LSP vector +*/ + private readonly float[][] freq_prev = new float[MA_NP][ /* M */]; - /** - * Previous LSP vector - */ - private readonly float[] prev_lsp = new float[M]; + /** +* Previous LSP vector +*/ + private readonly float[] prev_lsp = new float[M]; - /* static memory for frame erase operation */ - /** - * Previous MA prediction coef - */ - private int prev_ma; + /* static memory for frame erase operation */ + /** +* Previous MA prediction coef +*/ + private int prev_ma; - public Lspdec() + public Lspdec() + { + // need this to initialize freq_prev + for (var i = 0; i < freq_prev.Length; i++) { - // need this to initialize freq_prev - for (var i = 0; i < freq_prev.Length; i++) - freq_prev[i] = new float[M]; + freq_prev[i] = new float[M]; } + } - /** - * Set the previous LSP vectors. - */ + /** +* Set the previous LSP vectors. +*/ + + public void lsp_decw_reset() + { + int i; - public void lsp_decw_reset() + for (i = 0; i < MA_NP; i++) { - int i; + Util.copy(FREQ_PREV_RESET, freq_prev[i], M); + } - for (i = 0; i < MA_NP; i++) - Util.copy(FREQ_PREV_RESET, freq_prev[i], M); + prev_ma = 0; - prev_ma = 0; + Util.copy(FREQ_PREV_RESET, prev_lsp, M); + } - Util.copy(FREQ_PREV_RESET, prev_lsp, M); + private static int ZFRS(int i, int j) + { + var maskIt = i < 0; + i = i >> j; + if (maskIt) + { + i &= 0x7FFFFFFF; } - private static int ZFRS(int i, int j) + return i; + } + + /** +* LSP main quantization routine +* +* @param prm input : codes of the selected LSP +* @param prm_offset input : codes offset +* @param lsp_q output: Quantized LSP parameters +* @param erase input : frame erase information +*/ + private void lsp_iqua_cs( + int[] prm, + int prm_offset, + float[] lsp_q, + int erase + ) + { + var NC0 = Ld8k.NC0; + var NC0_B = Ld8k.NC0_B; + var NC1 = Ld8k.NC1; + var NC1_B = Ld8k.NC1_B; + var fg = TabLd8k.fg; + var fg_sum = TabLd8k.fg_sum; + var fg_sum_inv = TabLd8k.fg_sum_inv; + var lspcb1 = TabLd8k.lspcb1; + var lspcb2 = TabLd8k.lspcb2; + + int mode_index; + int code0; + int code1; + int code2; + var buf = new float[M]; + + if (erase == 0) /* Not frame erasure */ { - var maskIt = i < 0; - i = i >> j; - if (maskIt) - i &= 0x7FFFFFFF; - return i; + mode_index = ZFRS(prm[prm_offset + 0], NC0_B) & 1; + code0 = prm[prm_offset + 0] & (short)(NC0 - 1); + code1 = ZFRS(prm[prm_offset + 1], NC1_B) & (short)(NC1 - 1); + code2 = prm[prm_offset + 1] & (short)(NC1 - 1); + + Lspgetq.lsp_get_quant( + lspcb1, + lspcb2, + code0, + code1, + code2, + fg[mode_index], + freq_prev, + lsp_q, + fg_sum[mode_index]); + + Util.copy(lsp_q, prev_lsp, M); + prev_ma = mode_index; } - - /** - * LSP main quantization routine - * - * @param prm input : codes of the selected LSP - * @param prm_offset input : codes offset - * @param lsp_q output: Quantized LSP parameters - * @param erase input : frame erase information - */ - private void lsp_iqua_cs( - int[] prm, - int prm_offset, - float[] lsp_q, - int erase - ) + else /* Frame erased */ { - var NC0 = Ld8k.NC0; - var NC0_B = Ld8k.NC0_B; - var NC1 = Ld8k.NC1; - var NC1_B = Ld8k.NC1_B; - var fg = TabLd8k.fg; - var fg_sum = TabLd8k.fg_sum; - var fg_sum_inv = TabLd8k.fg_sum_inv; - var lspcb1 = TabLd8k.lspcb1; - var lspcb2 = TabLd8k.lspcb2; - - int mode_index; - int code0; - int code1; - int code2; - var buf = new float[M]; - - if (erase == 0) /* Not frame erasure */ - { - mode_index = ZFRS(prm[prm_offset + 0], NC0_B) & 1; - code0 = prm[prm_offset + 0] & (short)(NC0 - 1); - code1 = ZFRS(prm[prm_offset + 1], NC1_B) & (short)(NC1 - 1); - code2 = prm[prm_offset + 1] & (short)(NC1 - 1); - - Lspgetq.lsp_get_quant( - lspcb1, - lspcb2, - code0, - code1, - code2, - fg[mode_index], - freq_prev, - lsp_q, - fg_sum[mode_index]); - - Util.copy(lsp_q, prev_lsp, M); - prev_ma = mode_index; - } - else /* Frame erased */ - { - Util.copy(prev_lsp, lsp_q, M); - - /* update freq_prev */ - Lspgetq.lsp_prev_extract( - prev_lsp, - buf, - fg[prev_ma], - freq_prev, - fg_sum_inv[prev_ma]); - Lspgetq.lsp_prev_update(buf, freq_prev); - } + Util.copy(prev_lsp, lsp_q, M); + + /* update freq_prev */ + Lspgetq.lsp_prev_extract( + prev_lsp, + buf, + fg[prev_ma], + freq_prev, + fg_sum_inv[prev_ma]); + Lspgetq.lsp_prev_update(buf, freq_prev); } + } - /** - * Decode lsp parameters - * - * @param index input : indexes - * @param index_offset input : indexes offset - * @param lsp_q output: decoded lsp - * @param bfi input : frame erase information - */ + /** +* Decode lsp parameters +* +* @param index input : indexes +* @param index_offset input : indexes offset +* @param lsp_q output: decoded lsp +* @param bfi input : frame erase information +*/ - public void d_lsp( - int[] index, - int index_offset, - float[] lsp_q, - int bfi - ) - { - int i; + public void d_lsp( + int[] index, + int index_offset, + float[] lsp_q, + int bfi + ) + { + int i; - lsp_iqua_cs(index, index_offset, lsp_q, bfi); /* decode quantized information */ + lsp_iqua_cs(index, index_offset, lsp_q, bfi); /* decode quantized information */ - /* Convert LSFs to LSPs */ + /* Convert LSFs to LSPs */ - for (i = 0; i < M; i++) - lsp_q[i] = (float)Math.Cos(lsp_q[i]); + for (i = 0; i < M; i++) + { + lsp_q[i] = (float)Math.Cos(lsp_q[i]); } } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Lspgetq.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Lspgetq.cs index 4f71b45b1e..16ba6f5a2b 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Lspgetq.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Lspgetq.cs @@ -22,301 +22,320 @@ /** * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal static class Lspgetq { - internal class Lspgetq - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : LSPGETQ.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : LSPGETQ.C +Used for the floating point version of both +G.729 main body and G.729A */ - /** - * Reconstruct quantized LSP parameter and check the stabilty - * - * @param lspcb1 input : first stage LSP codebook - * @param lspcb2 input : Second stage LSP codebook - * @param code0 input : selected code of first stage - * @param code1 input : selected code of second stage - * @param code2 input : selected code of second stage - * @param fg input : MA prediction coef. - * @param freq_prev input : previous LSP vector - * @param lspq output: quantized LSP parameters - * @param fg_sum input : present MA prediction coef. - */ + /** +* Reconstruct quantized LSP parameter and check the stabilty +* +* @param lspcb1 input : first stage LSP codebook +* @param lspcb2 input : Second stage LSP codebook +* @param code0 input : selected code of first stage +* @param code1 input : selected code of second stage +* @param code2 input : selected code of second stage +* @param fg input : MA prediction coef. +* @param freq_prev input : previous LSP vector +* @param lspq output: quantized LSP parameters +* @param fg_sum input : present MA prediction coef. +*/ - public static void lsp_get_quant( - float[][ /* M */] lspcb1, - float[][ /* M */] lspcb2, - int code0, - int code1, - int code2, - float[][ /* M */] fg, - float[][ /* M */] freq_prev, - float[] lspq, - float[] fg_sum - ) - { - var GAP1 = Ld8k.GAP1; - var GAP2 = Ld8k.GAP2; - var M = Ld8k.M; - var NC = Ld8k.NC; + public static void lsp_get_quant( + float[][ /* M */] lspcb1, + float[][ /* M */] lspcb2, + int code0, + int code1, + int code2, + float[][ /* M */] fg, + float[][ /* M */] freq_prev, + float[] lspq, + float[] fg_sum + ) + { + var GAP1 = Ld8k.GAP1; + var GAP2 = Ld8k.GAP2; + var M = Ld8k.M; + var NC = Ld8k.NC; - int j; - var buf = new float[M]; + int j; + var buf = new float[M]; + + for (j = 0; j < NC; j++) + { + buf[j] = lspcb1[code0][j] + lspcb2[code1][j]; + } - for (j = 0; j < NC; j++) - buf[j] = lspcb1[code0][j] + lspcb2[code1][j]; - for (j = NC; j < M; j++) - buf[j] = lspcb1[code0][j] + lspcb2[code2][j]; + for (j = NC; j < M; j++) + { + buf[j] = lspcb1[code0][j] + lspcb2[code2][j]; + } - /* check */ - lsp_expand_1_2(buf, GAP1); - lsp_expand_1_2(buf, GAP2); + /* check */ + lsp_expand_1_2(buf, GAP1); + lsp_expand_1_2(buf, GAP2); - /* reconstruct quantized LSP parameters */ - lsp_prev_compose(buf, lspq, fg, freq_prev, fg_sum); + /* reconstruct quantized LSP parameters */ + lsp_prev_compose(buf, lspq, fg, freq_prev, fg_sum); - lsp_prev_update(buf, freq_prev); + lsp_prev_update(buf, freq_prev); - lsp_stability(lspq); /* check the stabilty */ - } + lsp_stability(lspq); /* check the stabilty */ + } - /** - * Check for lower (0-4) - * - * @param buf in/out: lsp vectors - * @param gap input : gap - */ + /** +* Check for lower (0-4) +* +* @param buf in/out: lsp vectors +* @param gap input : gap +*/ - public static void lsp_expand_1( - float[] buf, /* */ - float gap - ) - { - var NC = Ld8k.NC; + public static void lsp_expand_1( + float[] buf, /* */ + float gap + ) + { + var NC = Ld8k.NC; - int j; - float diff, tmp; + int j; + float diff, tmp; - for (j = 1; j < NC; j++) + for (j = 1; j < NC; j++) + { + diff = buf[j - 1] - buf[j]; + tmp = (diff + gap) * 0.5f; + if (tmp > 0) { - diff = buf[j - 1] - buf[j]; - tmp = (diff + gap) * 0.5f; - if (tmp > 0) - { - buf[j - 1] -= tmp; - buf[j] += tmp; - } + buf[j - 1] -= tmp; + buf[j] += tmp; } } + } - /** - * Check for higher (5-9) - * - * @param buf in/out: lsp vectors - * @param gap input : gap - */ + /** +* Check for higher (5-9) +* +* @param buf in/out: lsp vectors +* @param gap input : gap +*/ - public static void lsp_expand_2( - float[] buf, - float gap - ) - { - var M = Ld8k.M; - var NC = Ld8k.NC; + public static void lsp_expand_2( + float[] buf, + float gap + ) + { + var M = Ld8k.M; + var NC = Ld8k.NC; - int j; - float diff, tmp; + int j; + float diff, tmp; - for (j = NC; j < M; j++) + for (j = NC; j < M; j++) + { + diff = buf[j - 1] - buf[j]; + tmp = (diff + gap) * 0.5f; + if (tmp > 0) { - diff = buf[j - 1] - buf[j]; - tmp = (diff + gap) * 0.5f; - if (tmp > 0) - { - buf[j - 1] -= tmp; - buf[j] += tmp; - } + buf[j - 1] -= tmp; + buf[j] += tmp; } } + } - /** - * - * @param buf in/out: LSP parameters - * @param gap input: gap - */ + /** +* +* @param buf in/out: LSP parameters +* @param gap input: gap +*/ - public static void lsp_expand_1_2( - float[] buf, - float gap - ) - { - var M = Ld8k.M; + public static void lsp_expand_1_2( + float[] buf, + float gap + ) + { + var M = Ld8k.M; - int j; - float diff, tmp; + int j; + float diff, tmp; - for (j = 1; j < M; j++) + for (j = 1; j < M; j++) + { + diff = buf[j - 1] - buf[j]; + tmp = (diff + gap) * 0.5f; + if (tmp > 0) { - diff = buf[j - 1] - buf[j]; - tmp = (diff + gap) * 0.5f; - if (tmp > 0) - { - buf[j - 1] -= tmp; - buf[j] += tmp; - } + buf[j - 1] -= tmp; + buf[j] += tmp; } } + } - /* - Functions which use previous LSP parameter (freq_prev). + /* +Functions which use previous LSP parameter (freq_prev). */ - /** - * Compose LSP parameter from elementary LSP with previous LSP. - * - * @param lsp_ele (i) Q13 : LSP vectors - * @param lsp (o) Q13 : quantized LSP parameters - * @param fg (i) Q15 : MA prediction coef. - * @param freq_prev (i) Q13 : previous LSP vector - * @param fg_sum (i) Q15 : present MA prediction coef. - */ - private static void lsp_prev_compose( - float[] lsp_ele, - float[] lsp, - float[][ /* M */] fg, - float[][ /* M */] freq_prev, - float[] fg_sum - ) - { - var M = Ld8k.M; - var MA_NP = Ld8k.MA_NP; + /** +* Compose LSP parameter from elementary LSP with previous LSP. +* +* @param lsp_ele (i) Q13 : LSP vectors +* @param lsp (o) Q13 : quantized LSP parameters +* @param fg (i) Q15 : MA prediction coef. +* @param freq_prev (i) Q13 : previous LSP vector +* @param fg_sum (i) Q15 : present MA prediction coef. +*/ + private static void lsp_prev_compose( + float[] lsp_ele, + float[] lsp, + float[][ /* M */] fg, + float[][ /* M */] freq_prev, + float[] fg_sum + ) + { + var M = Ld8k.M; + var MA_NP = Ld8k.MA_NP; - int j, k; + int j, k; - for (j = 0; j < M; j++) + for (j = 0; j < M; j++) + { + lsp[j] = lsp_ele[j] * fg_sum[j]; + for (k = 0; k < MA_NP; k++) { - lsp[j] = lsp_ele[j] * fg_sum[j]; - for (k = 0; k < MA_NP; k++) lsp[j] += freq_prev[k][j] * fg[k][j]; + lsp[j] += freq_prev[k][j] * fg[k][j]; } } + } - /** - * Extract elementary LSP from composed LSP with previous LSP - * - * @param lsp (i) Q13 : unquantized LSP parameters - * @param lsp_ele (o) Q13 : target vector - * @param fg (i) Q15 : MA prediction coef. - * @param freq_prev (i) Q13 : previous LSP vector - * @param fg_sum_inv (i) Q12 : inverse previous LSP vector - */ + /** +* Extract elementary LSP from composed LSP with previous LSP +* +* @param lsp (i) Q13 : unquantized LSP parameters +* @param lsp_ele (o) Q13 : target vector +* @param fg (i) Q15 : MA prediction coef. +* @param freq_prev (i) Q13 : previous LSP vector +* @param fg_sum_inv (i) Q12 : inverse previous LSP vector +*/ - public static void lsp_prev_extract( - float[ /* M */] lsp, - float[ /* M */] lsp_ele, - float[ /* MA_NP */][ /* M */] fg, - float[ /* MA_NP */][ /* M */] freq_prev, - float[ /* M */] fg_sum_inv - ) - { - var M = Ld8k.M; - var MA_NP = Ld8k.MA_NP; + public static void lsp_prev_extract( + float[ /* M */] lsp, + float[ /* M */] lsp_ele, + float[ /* MA_NP */][ /* M */] fg, + float[ /* MA_NP */][ /* M */] freq_prev, + float[ /* M */] fg_sum_inv + ) + { + var M = Ld8k.M; + var MA_NP = Ld8k.MA_NP; - int j, k; + int j, k; - /*----- compute target vectors for each MA coef.-----*/ - for (j = 0; j < M; j++) + /*----- compute target vectors for each MA coef.-----*/ + for (j = 0; j < M; j++) + { + lsp_ele[j] = lsp[j]; + for (k = 0; k < MA_NP; k++) { - lsp_ele[j] = lsp[j]; - for (k = 0; k < MA_NP; k++) - lsp_ele[j] -= freq_prev[k][j] * fg[k][j]; - lsp_ele[j] *= fg_sum_inv[j]; + lsp_ele[j] -= freq_prev[k][j] * fg[k][j]; } - } - /** - * Update previous LSP parameter - * - * @param lsp_ele input : LSP vectors - * @param freq_prev input/output: previous LSP vectors - */ + lsp_ele[j] *= fg_sum_inv[j]; + } + } - public static void lsp_prev_update( - float[ /* M */] lsp_ele, - float[ /* MA_NP */][ /* M */] freq_prev - ) - { - var M = Ld8k.M; - var MA_NP = Ld8k.MA_NP; + /** +* Update previous LSP parameter +* +* @param lsp_ele input : LSP vectors +* @param freq_prev input/output: previous LSP vectors +*/ - int k; + public static void lsp_prev_update( + float[ /* M */] lsp_ele, + float[ /* MA_NP */][ /* M */] freq_prev + ) + { + var M = Ld8k.M; + var MA_NP = Ld8k.MA_NP; - for (k = MA_NP - 1; k > 0; k--) - Util.copy(freq_prev[k - 1], freq_prev[k], M); + int k; - Util.copy(lsp_ele, freq_prev[0], M); + for (k = MA_NP - 1; k > 0; k--) + { + Util.copy(freq_prev[k - 1], freq_prev[k], M); } - /** - * Check stability of lsp coefficients - * - * @param buf in/out: LSP parameters - */ - private static void lsp_stability( - float[] buf - ) - { - var GAP3 = Ld8k.GAP3; - var L_LIMIT = Ld8k.L_LIMIT; - var M = Ld8k.M; - var M_LIMIT = Ld8k.M_LIMIT; + Util.copy(lsp_ele, freq_prev[0], M); + } + + /** +* Check stability of lsp coefficients +* +* @param buf in/out: LSP parameters +*/ + private static void lsp_stability( + float[] buf + ) + { + var GAP3 = Ld8k.GAP3; + var L_LIMIT = Ld8k.L_LIMIT; + var M = Ld8k.M; + var M_LIMIT = Ld8k.M_LIMIT; - int j; - float diff, tmp; + int j; + float diff, tmp; - for (j = 0; j < M - 1; j++) + for (j = 0; j < M - 1; j++) + { + diff = buf[j + 1] - buf[j]; + if (diff < 0.0f) { - diff = buf[j + 1] - buf[j]; - if (diff < 0.0f) - { - tmp = buf[j + 1]; - buf[j + 1] = buf[j]; - buf[j] = tmp; - } + tmp = buf[j + 1]; + buf[j + 1] = buf[j]; + buf[j] = tmp; } + } - if (buf[0] < L_LIMIT) - buf[0] = L_LIMIT; - for (j = 0; j < M - 1; j++) + if (buf[0] < L_LIMIT) + { + buf[0] = L_LIMIT; + } + + for (j = 0; j < M - 1; j++) + { + diff = buf[j + 1] - buf[j]; + if (diff < GAP3) { - diff = buf[j + 1] - buf[j]; - if (diff < GAP3) - buf[j + 1] = buf[j] + GAP3; + buf[j + 1] = buf[j] + GAP3; } + } - if (buf[M - 1] > M_LIMIT) - buf[M - 1] = M_LIMIT; + if (buf[M - 1] > M_LIMIT) + { + buf[M - 1] = M_LIMIT; } } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/PParity.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/PParity.cs index bd270143b3..2d35951d00 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/PParity.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/PParity.cs @@ -22,89 +22,88 @@ /** * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal static class PParity { - internal class PParity - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : P_PARITY.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : P_PARITY.C +Used for the floating point version of both +G.729 main body and G.729A */ - /** - * Compute parity bit for first 6 MSBs - * - * @param pitch_index input : index for which parity is computed - * @return parity bit (XOR of 6 MSB bits) - */ - - public static int parity_pitch( - int pitch_index - ) - { - int temp, sum, i, bit; + /** +* Compute parity bit for first 6 MSBs +* +* @param pitch_index input : index for which parity is computed +* @return parity bit (XOR of 6 MSB bits) +*/ - temp = pitch_index >> 1; + public static int parity_pitch( + int pitch_index + ) + { + int temp, sum, i, bit; - sum = 1; - for (i = 0; i <= 5; i++) - { - temp >>= 1; - bit = temp & 1; - sum = sum + bit; - } + temp = pitch_index >> 1; - sum = sum & 1; - return sum; + sum = 1; + for (i = 0; i <= 5; i++) + { + temp >>= 1; + bit = temp & 1; + sum = sum + bit; } - /** - * Check parity of index with transmitted parity - * - * @param pitch_index input : index of parameter - * @param parity input : parity bit - * @return 0 = no error, 1= error - */ + sum = sum & 1; + return sum; + } - public static int check_parity_pitch( - int pitch_index, - int parity - ) - { - int temp, sum, i, bit; - temp = pitch_index >> 1; + /** +* Check parity of index with transmitted parity +* +* @param pitch_index input : index of parameter +* @param parity input : parity bit +* @return 0 = no error, 1= error +*/ - sum = 1; - for (i = 0; i <= 5; i++) - { - temp >>= 1; - bit = temp & 1; - sum = sum + bit; - } + public static int check_parity_pitch( + int pitch_index, + int parity + ) + { + int temp, sum, i, bit; + temp = pitch_index >> 1; - sum += parity; - sum = sum & 1; - return sum; + sum = 1; + for (i = 0; i <= 5; i++) + { + temp >>= 1; + bit = temp & 1; + sum = sum + bit; } + + sum += parity; + sum = sum & 1; + return sum; } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Pitch.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Pitch.cs index e077fa84d7..258fd1cd54 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Pitch.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Pitch.cs @@ -26,507 +26,545 @@ */ using System; -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal static class Pitch { - internal class Pitch - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : PITCH.C - Used for the floating point version of G.729 main body - (not for G.729A) + /* +File : PITCH.C +Used for the floating point version of G.729 main body +(not for G.729A) */ - /** - * Compute the open loop pitch lag. - * - * @param signal input : signal to compute pitch - * s[-PIT_MAX : l_frame-1] - * @param signal_offset input : signal offset - * @param pit_min input : minimum pitch lag - * @param pit_max input : maximum pitch lag - * @param l_frame input : error minimization window - * @return open-loop pitch lag - */ + /** +* Compute the open loop pitch lag. +* +* @param signal input : signal to compute pitch +* s[-PIT_MAX : l_frame-1] +* @param signal_offset input : signal offset +* @param pit_min input : minimum pitch lag +* @param pit_max input : maximum pitch lag +* @param l_frame input : error minimization window +* @return open-loop pitch lag +*/ - public static int pitch_ol( - float[] signal, - int signal_offset, - int pit_min, - int pit_max, - int l_frame - ) + public static int pitch_ol( + float[] signal, + int signal_offset, + int pit_min, + int pit_max, + int l_frame + ) + { + var THRESHPIT = Ld8k.THRESHPIT; + + float max1, max2, max3; + int p_max1, p_max2, p_max3; + + /*--------------------------------------------------------------------* +* The pitch lag search is divided in three sections. * +* Each section cannot have a pitch multiple. * +* We find a maximum for each section. * +* We compare the maxima of each section by favoring small lag. * +* * +* First section: lag delay = PIT_MAX to 80 * +* Second section: lag delay = 79 to 40 * +* Third section: lag delay = 39 to 20 * +*--------------------------------------------------------------------*/ + + var maxRef = new FloatReference(); + p_max1 = lag_max(signal, signal_offset, l_frame, pit_max, 80, maxRef); + max1 = maxRef.value; + p_max2 = lag_max(signal, signal_offset, l_frame, 79, 40, maxRef); + max2 = maxRef.value; + p_max3 = lag_max(signal, signal_offset, l_frame, 39, pit_min, maxRef); + max3 = maxRef.value; + + /*--------------------------------------------------------------------* +* Compare the 3 sections maxima, and favor small lag. * +*--------------------------------------------------------------------*/ + + if (max1 * THRESHPIT < max2) { - var THRESHPIT = Ld8k.THRESHPIT; - - float max1, max2, max3; - int p_max1, p_max2, p_max3; - - /*--------------------------------------------------------------------* - * The pitch lag search is divided in three sections. * - * Each section cannot have a pitch multiple. * - * We find a maximum for each section. * - * We compare the maxima of each section by favoring small lag. * - * * - * First section: lag delay = PIT_MAX to 80 * - * Second section: lag delay = 79 to 40 * - * Third section: lag delay = 39 to 20 * - *--------------------------------------------------------------------*/ - - var maxRef = new FloatReference(); - p_max1 = lag_max(signal, signal_offset, l_frame, pit_max, 80, maxRef); - max1 = maxRef.value; - p_max2 = lag_max(signal, signal_offset, l_frame, 79, 40, maxRef); - max2 = maxRef.value; - p_max3 = lag_max(signal, signal_offset, l_frame, 39, pit_min, maxRef); - max3 = maxRef.value; - - /*--------------------------------------------------------------------* - * Compare the 3 sections maxima, and favor small lag. * - *--------------------------------------------------------------------*/ - - if (max1 * THRESHPIT < max2) - { - max1 = max2; - p_max1 = p_max2; - } - - if (max1 * THRESHPIT < max3) p_max1 = p_max3; - - return p_max1; + max1 = max2; + p_max1 = p_max2; } - /** - * Find the lag that has maximum correlation - * - * @param signal input : Signal to compute the open loop pitch - * signal[-142:-1] should be known. - * @param signal_offset input : signal offset - * @param l_frame input : Length of frame to compute pitch - * @param lagmax input : maximum lag - * @param lagmin input : minimum lag - * @param cor_max input : normalized correlation of selected lag - * @return lag found - */ - private static int lag_max( - float[] signal, - int signal_offset, - int l_frame, - int lagmax, - int lagmin, - FloatReference cor_max - ) + if (max1 * THRESHPIT < max3) { - var FLT_MIN_G729 = Ld8k.FLT_MIN_G729; - - int i, j; - int p, p1; - float max, t0; - var p_max = 0; - - max = FLT_MIN_G729; + p_max1 = p_max3; + } - for (i = lagmax; i >= lagmin; i--) - { - p = signal_offset; - p1 = signal_offset - i; - t0 = 0.0f; + return p_max1; + } - for (j = 0; j < l_frame; j++, p++, p1++) - t0 += signal[p] * signal[p1]; + /** +* Find the lag that has maximum correlation +* +* @param signal input : Signal to compute the open loop pitch +* signal[-142:-1] should be known. +* @param signal_offset input : signal offset +* @param l_frame input : Length of frame to compute pitch +* @param lagmax input : maximum lag +* @param lagmin input : minimum lag +* @param cor_max input : normalized correlation of selected lag +* @return lag found +*/ + private static int lag_max( + float[] signal, + int signal_offset, + int l_frame, + int lagmax, + int lagmin, + FloatReference cor_max + ) + { + var FLT_MIN_G729 = Ld8k.FLT_MIN_G729; - if (t0 >= max) - { - max = t0; - p_max = i; - } - } + int i, j; + int p, p1; + float max, t0; + var p_max = 0; - /* compute energy */ + max = FLT_MIN_G729; - t0 = 0.01f; /* to avoid division by zero */ - p = signal_offset - p_max; - for (i = 0; i < l_frame; i++, p++) - t0 += signal[p] * signal[p]; - t0 = inv_sqrt(t0); /* 1/sqrt(energy) */ + for (i = lagmax; i >= lagmin; i--) + { + p = signal_offset; + p1 = signal_offset - i; + t0 = 0.0f; - cor_max.value = max * t0; /* max/sqrt(energy) */ + for (j = 0; j < l_frame; j++, p++, p1++) + { + t0 += signal[p] * signal[p1]; + } - return p_max; + if (t0 >= max) + { + max = t0; + p_max = i; + } } - /** - * Find the pitch period with 1/3 subsample resolution - * - * @param exc input : excitation buffer - * @param exc_offset input : excitation buffer offset - * @param xn input : target vector - * @param h input : impulse response of filters. - * @param l_subfr input : Length of frame to compute pitch - * @param t0_min input : minimum value in the searched range - * @param t0_max input : maximum value in the searched range - * @param i_subfr input : indicator for first subframe - * @param pit_frac output: chosen fraction - * @return integer part of pitch period - */ + /* compute energy */ - public static int pitch_fr3( - float[] exc, /* */ - int exc_offset, - float[] xn, /* */ - float[] h, /* */ - int l_subfr, /* */ - int t0_min, /* */ - int t0_max, /* */ - int i_subfr, /* */ - IntReference pit_frac /* */ - ) + t0 = 0.01f; /* to avoid division by zero */ + p = signal_offset - p_max; + for (i = 0; i < l_frame; i++, p++) { - var L_INTER4 = Ld8k.L_INTER4; + t0 += signal[p] * signal[p]; + } - int i, frac; - int lag, t_min, t_max; - float max; - float corr_int; - var corr_v = new float[10 + 2 * L_INTER4]; /* size: 2*L_INTER4+t0_max-t0_min+1 */ - float[] corr; - int corr_offset; + t0 = inv_sqrt(t0); /* 1/sqrt(energy) */ - /* Find interval to compute normalized correlation */ + cor_max.value = max * t0; /* max/sqrt(energy) */ - t_min = t0_min - L_INTER4; - t_max = t0_max + L_INTER4; + return p_max; + } - corr = corr_v; /* corr[t_min:t_max] */ - corr_offset = -t_min; + /** +* Find the pitch period with 1/3 subsample resolution +* +* @param exc input : excitation buffer +* @param exc_offset input : excitation buffer offset +* @param xn input : target vector +* @param h input : impulse response of filters. +* @param l_subfr input : Length of frame to compute pitch +* @param t0_min input : minimum value in the searched range +* @param t0_max input : maximum value in the searched range +* @param i_subfr input : indicator for first subframe +* @param pit_frac output: chosen fraction +* @return integer part of pitch period +*/ - /* Compute normalized correlation between target and filtered excitation */ + public static int pitch_fr3( + float[] exc, /* */ + int exc_offset, + float[] xn, /* */ + float[] h, /* */ + int l_subfr, /* */ + int t0_min, /* */ + int t0_max, /* */ + int i_subfr, /* */ + IntReference pit_frac /* */ + ) + { + var L_INTER4 = Ld8k.L_INTER4; - norm_corr(exc, exc_offset, xn, h, l_subfr, t_min, t_max, corr, corr_offset); + int i, frac; + int lag, t_min, t_max; + float max; + float corr_int; + var corr_v = new float[10 + 2 * L_INTER4]; /* size: 2*L_INTER4+t0_max-t0_min+1 */ + float[] corr; + int corr_offset; - /* find integer pitch */ + /* Find interval to compute normalized correlation */ - max = corr[corr_offset + t0_min]; - lag = t0_min; + t_min = t0_min - L_INTER4; + t_max = t0_max + L_INTER4; - for (i = t0_min + 1; i <= t0_max; i++) - if (corr[corr_offset + i] >= max) - { - max = corr[corr_offset + i]; - lag = i; - } + corr = corr_v; /* corr[t_min:t_max] */ + corr_offset = -t_min; - /* If first subframe and lag > 84 do not search fractionnal pitch */ + /* Compute normalized correlation between target and filtered excitation */ - if (i_subfr == 0 && lag > 84) - { - pit_frac.value = 0; - return lag; - } + norm_corr(exc, exc_offset, xn, h, l_subfr, t_min, t_max, corr, corr_offset); - /* test the fractions around lag and choose the one which maximizes - the interpolated normalized correlation */ - corr_offset += lag; - max = interpol_3(corr, corr_offset, -2); - frac = -2; + /* find integer pitch */ - for (i = -1; i <= 2; i++) + max = corr[corr_offset + t0_min]; + lag = t0_min; + + for (i = t0_min + 1; i <= t0_max; i++) + { + if (corr[corr_offset + i] >= max) { - corr_int = interpol_3(corr, corr_offset, i); - if (corr_int > max) - { - max = corr_int; - frac = i; - } + max = corr[corr_offset + i]; + lag = i; } + } - /* limit the fraction value in the interval [-1,0,1] */ + /* If first subframe and lag > 84 do not search fractionnal pitch */ - if (frac == -2) - { - frac = 1; - lag -= 1; - } + if (i_subfr == 0 && lag > 84) + { + pit_frac.value = 0; + return lag; + } + + /* test the fractions around lag and choose the one which maximizes + the interpolated normalized correlation */ + corr_offset += lag; + max = interpol_3(corr, corr_offset, -2); + frac = -2; - if (frac == 2) + for (i = -1; i <= 2; i++) + { + corr_int = interpol_3(corr, corr_offset, i); + if (corr_int > max) { - frac = -1; - lag += 1; + max = corr_int; + frac = i; } + } - pit_frac.value = frac; + /* limit the fraction value in the interval [-1,0,1] */ - return lag; + if (frac == -2) + { + frac = 1; + lag -= 1; } - /** - * Find the normalized correlation between the target vector and - * the filtered past excitation. - * - * @param exc input : excitation buffer - * @param exc_offset input : excitation buffer offset - * @param xn input : target vector - * @param h input : imp response of synth and weighting flt - * @param l_subfr input : Length of frame to compute pitch - * @param t_min input : minimum value of searched range - * @param t_max input : maximum value of search range - * @param corr_norm output: normalized correlation (correlation - * between target and filtered excitation divided - * by the square root of energy of filtered - * excitation) - * @param corr_norm_offset input: normalized correlation offset - */ - private static void norm_corr( - float[] exc, - int exc_offset, - float[] xn, - float[] h, - int l_subfr, - int t_min, - int t_max, - float[] corr_norm, - int corr_norm_offset - ) + if (frac == 2) { - var L_SUBFR = Ld8k.L_SUBFR; + frac = -1; + lag += 1; + } - int i, j, k; - var excf = new float[L_SUBFR]; /* filtered past excitation */ - float alp, s, norm; + pit_frac.value = frac; - k = exc_offset - t_min; + return lag; + } - /* compute the filtered excitation for the first delay t_min */ + /** +* Find the normalized correlation between the target vector and +* the filtered past excitation. +* +* @param exc input : excitation buffer +* @param exc_offset input : excitation buffer offset +* @param xn input : target vector +* @param h input : imp response of synth and weighting flt +* @param l_subfr input : Length of frame to compute pitch +* @param t_min input : minimum value of searched range +* @param t_max input : maximum value of search range +* @param corr_norm output: normalized correlation (correlation +* between target and filtered excitation divided +* by the square root of energy of filtered +* excitation) +* @param corr_norm_offset input: normalized correlation offset +*/ + private static void norm_corr( + float[] exc, + int exc_offset, + float[] xn, + float[] h, + int l_subfr, + int t_min, + int t_max, + float[] corr_norm, + int corr_norm_offset + ) + { + var L_SUBFR = Ld8k.L_SUBFR; - Filter.convolve(exc, k, h, excf, l_subfr); + int i, j, k; + var excf = new float[L_SUBFR]; /* filtered past excitation */ + float alp, s, norm; - /* loop for every possible period */ + k = exc_offset - t_min; - for (i = t_min; i <= t_max; i++) - { - /* Compute 1/sqrt(energie of excf[]) */ + /* compute the filtered excitation for the first delay t_min */ - alp = 0.01f; - for (j = 0; j < l_subfr; j++) - alp += excf[j] * excf[j]; + Filter.convolve(exc, k, h, excf, l_subfr); - norm = inv_sqrt(alp); + /* loop for every possible period */ - /* Compute correlation between xn[] and excf[] */ + for (i = t_min; i <= t_max; i++) + { + /* Compute 1/sqrt(energie of excf[]) */ - s = 0.0f; - for (j = 0; j < l_subfr; j++) s += xn[j] * excf[j]; + alp = 0.01f; + for (j = 0; j < l_subfr; j++) + { + alp += excf[j] * excf[j]; + } + + norm = inv_sqrt(alp); + + /* Compute correlation between xn[] and excf[] */ + + s = 0.0f; + for (j = 0; j < l_subfr; j++) + { + s += xn[j] * excf[j]; + } - /* Normalize correlation = correlation * (1/sqrt(energie)) */ + /* Normalize correlation = correlation * (1/sqrt(energie)) */ - corr_norm[corr_norm_offset + i] = s * norm; + corr_norm[corr_norm_offset + i] = s * norm; - /* modify the filtered excitation excf[] for the next iteration */ + /* modify the filtered excitation excf[] for the next iteration */ - if (i != t_max) + if (i != t_max) + { + k--; + for (j = l_subfr - 1; j > 0; j--) { - k--; - for (j = l_subfr - 1; j > 0; j--) - excf[j] = excf[j - 1] + exc[k] * h[j]; - excf[0] = exc[k]; + excf[j] = excf[j - 1] + exc[k] * h[j]; } + + excf[0] = exc[k]; } } + } - /** , -2 - * - * @param xn input : target vector - * @param y1 input : filtered adaptive codebook vector - * @param g_coeff output: and -2 - * @param l_subfr input : vector dimension - * @return pitch gain - * ]]> - */ + /** , -2 +* +* @param xn input : target vector +* @param y1 input : filtered adaptive codebook vector +* @param g_coeff output: and -2 +* @param l_subfr input : vector dimension +* @return pitch gain +* ]]> +*/ + + public static float g_pitch( + float[] xn, + float[] y1, + float[] g_coeff, + int l_subfr + ) + { + var GAIN_PIT_MAX = Ld8k.GAIN_PIT_MAX; - public static float g_pitch( - float[] xn, - float[] y1, - float[] g_coeff, - int l_subfr - ) + float xy, yy, gain; + int i; + + xy = 0.0f; + for (i = 0; i < l_subfr; i++) { - var GAIN_PIT_MAX = Ld8k.GAIN_PIT_MAX; + xy += xn[i] * y1[i]; + } - float xy, yy, gain; - int i; + yy = 0.01f; + for (i = 0; i < l_subfr; i++) + { + yy += y1[i] * y1[i]; /* energy of filtered excitation */ + } - xy = 0.0f; - for (i = 0; i < l_subfr; i++) - xy += xn[i] * y1[i]; - yy = 0.01f; - for (i = 0; i < l_subfr; i++) - yy += y1[i] * y1[i]; /* energy of filtered excitation */ - g_coeff[0] = yy; - g_coeff[1] = -2.0f * xy + 0.01f; + g_coeff[0] = yy; + g_coeff[1] = -2.0f * xy + 0.01f; - /* find pitch gain and bound it by [0,1.2] */ + /* find pitch gain and bound it by [0,1.2] */ - gain = xy / yy; + gain = xy / yy; - if (gain < 0.0f) gain = 0.0f; - if (gain > GAIN_PIT_MAX) gain = GAIN_PIT_MAX; + if (gain < 0.0f) + { + gain = 0.0f; + } - return gain; + if (gain > GAIN_PIT_MAX) + { + gain = GAIN_PIT_MAX; } - /** - * Function enc_lag3() - * Encoding of fractional pitch lag with 1/3 resolution. - *
- * The pitch range for the first subframe is divided as follows:
- *   19 1/3  to   84 2/3   resolution 1/3
- *   85      to   143      resolution 1
- *
- * The period in the first subframe is encoded with 8 bits.
- * For the range with fractions:
- *   index = (T-19)*3 + frac - 1;   where T=[19..85] and frac=[-1,0,1]
- * and for the integer only range
- *   index = (T - 85) + 197;        where T=[86..143]
- *----------------------------------------------------------------------
- * For the second subframe a resolution of 1/3 is always used, and the
- * search range is relative to the lag in the first subframe.
- * If t0 is the lag in the first subframe then
- *  t_min=t0-5   and  t_max=t0+4   and  the range is given by
- *       t_min - 2/3   to  t_max + 2/3
- *
- * The period in the 2nd subframe is encoded with 5 bits:
- *   index = (T-(t_min-1))*3 + frac - 1;    where T[t_min-1 .. t_max+1]
- * 
- * - * @param T0 input : Pitch delay - * @param T0_frac input : Fractional pitch delay - * @param T0_min in/out: Minimum search delay - * @param T0_max in/out: Maximum search delay - * @param pit_min input : Minimum pitch delay - * @param pit_max input : Maximum pitch delay - * @param pit_flag input : Flag for 1st subframe - * @return Return index of encoding - */ + return gain; + } + + /** +* Function enc_lag3() +* Encoding of fractional pitch lag with 1/3 resolution. +*
+* The pitch range for the first subframe is divided as follows:
+*   19 1/3  to   84 2/3   resolution 1/3
+*   85      to   143      resolution 1
+*
+* The period in the first subframe is encoded with 8 bits.
+* For the range with fractions:
+*   index = (T-19)*3 + frac - 1;   where T=[19..85] and frac=[-1,0,1]
+* and for the integer only range
+*   index = (T - 85) + 197;        where T=[86..143]
+*----------------------------------------------------------------------
+* For the second subframe a resolution of 1/3 is always used, and the
+* search range is relative to the lag in the first subframe.
+* If t0 is the lag in the first subframe then
+*  t_min=t0-5   and  t_max=t0+4   and  the range is given by
+*       t_min - 2/3   to  t_max + 2/3
+*
+* The period in the 2nd subframe is encoded with 5 bits:
+*   index = (T-(t_min-1))*3 + frac - 1;    where T[t_min-1 .. t_max+1]
+* 
+* +* @param T0 input : Pitch delay +* @param T0_frac input : Fractional pitch delay +* @param T0_min in/out: Minimum search delay +* @param T0_max in/out: Maximum search delay +* @param pit_min input : Minimum pitch delay +* @param pit_max input : Maximum pitch delay +* @param pit_flag input : Flag for 1st subframe +* @return Return index of encoding +*/ + + public static int enc_lag3( + int T0, + int T0_frac, + IntReference T0_min, + IntReference T0_max, + int pit_min, + int pit_max, + int pit_flag + ) + { + int index; + int _T0_min = T0_min.value, _T0_max = T0_max.value; - public static int enc_lag3( - int T0, - int T0_frac, - IntReference T0_min, - IntReference T0_max, - int pit_min, - int pit_max, - int pit_flag - ) + if (pit_flag == 0) /* if 1st subframe */ { - int index; - int _T0_min = T0_min.value, _T0_max = T0_max.value; + /* encode pitch delay (with fraction) */ - if (pit_flag == 0) /* if 1st subframe */ + if (T0 <= 85) { - /* encode pitch delay (with fraction) */ - - if (T0 <= 85) - index = T0 * 3 - 58 + T0_frac; - else - index = T0 + 112; + index = T0 * 3 - 58 + T0_frac; + } + else + { + index = T0 + 112; + } - /* find T0_min and T0_max for second subframe */ + /* find T0_min and T0_max for second subframe */ - _T0_min = T0 - 5; - if (_T0_min < pit_min) _T0_min = pit_min; - _T0_max = _T0_min + 9; - if (_T0_max > pit_max) - { - _T0_max = pit_max; - _T0_min = _T0_max - 9; - } + _T0_min = T0 - 5; + if (_T0_min < pit_min) + { + _T0_min = pit_min; } - else /* second subframe */ + _T0_max = _T0_min + 9; + if (_T0_max > pit_max) { - index = T0 - _T0_min; - index = index * 3 + 2 + T0_frac; + _T0_max = pit_max; + _T0_min = _T0_max - 9; } - - T0_min.value = _T0_min; - T0_max.value = _T0_max; - return index; } - /** - * For interpolating the normalized correlation - * - * @param x input : function to be interpolated - * @param x_offset input : function offset - * @param frac input : fraction value to evaluate - * @return interpolated value - */ - private static float interpol_3( - float[] x, - int x_offset, - int frac - ) + else /* second subframe */ { - var L_INTER4 = Ld8k.L_INTER4; - var UP_SAMP = Ld8k.UP_SAMP; - var inter_3 = TabLd8k.inter_3; - - int i; - float s; - int x1, x2, c1, c2; + index = T0 - _T0_min; + index = index * 3 + 2 + T0_frac; + } - if (frac < 0) - { - frac += UP_SAMP; - x_offset--; - } + T0_min.value = _T0_min; + T0_max.value = _T0_max; + return index; + } - x1 = x_offset; - x2 = x_offset + 1; - c1 = frac; - c2 = UP_SAMP - frac; + /** +* For interpolating the normalized correlation +* +* @param x input : function to be interpolated +* @param x_offset input : function offset +* @param frac input : fraction value to evaluate +* @return interpolated value +*/ + private static float interpol_3( + float[] x, + int x_offset, + int frac + ) + { + var L_INTER4 = Ld8k.L_INTER4; + var UP_SAMP = Ld8k.UP_SAMP; + var inter_3 = TabLd8k.inter_3; - s = 0.0f; - for (i = 0; i < L_INTER4; i++, c1 += UP_SAMP, c2 += UP_SAMP) - { - s += x[x1] * inter_3[c1] + x[x2] * inter_3[c2]; - x1--; - x2++; - } + int i; + float s; + int x1, x2, c1, c2; - return s; + if (frac < 0) + { + frac += UP_SAMP; + x_offset--; } - /** - * Compute y = 1 / sqrt(x) - * - * @param x input : value of x - * @return output: 1/sqrt(x) - */ - private static float inv_sqrt( - float x - ) + x1 = x_offset; + x2 = x_offset + 1; + c1 = frac; + c2 = UP_SAMP - frac; + + s = 0.0f; + for (i = 0; i < L_INTER4; i++, c1 += UP_SAMP, c2 += UP_SAMP) { - return 1.0f / (float)Math.Sqrt(x); + s += x[x1] * inter_3[c1] + x[x2] * inter_3[c2]; + x1--; + x2++; } + + return s; + } + + /** +* Compute y = 1 / sqrt(x) +* +* @param x input : value of x +* @return output: 1/sqrt(x) +*/ + private static float inv_sqrt( + float x + ) + { + return 1.0f / (float)Math.Sqrt(x); } } diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/PostPro.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/PostPro.cs index a2514bc39e..decf3584e5 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/PostPro.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/PostPro.cs @@ -35,86 +35,85 @@ * * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal sealed class PostPro { - internal class PostPro - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : POST_PRO.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : POST_PRO.C +Used for the floating point version of both +G.729 main body and G.729A */ - /** - * High-pass fir memory - */ - private float x0, x1; + /** +* High-pass fir memory +*/ + private float x0, x1; - /** - * High-pass iir memory - */ - private float y1, y2; + /** +* High-pass iir memory +*/ + private float y1, y2; - /** - * Init Post Process. - */ + /** +* Init Post Process. +*/ - public void init_post_process() - { - x0 = x1 = 0.0f; - y2 = y1 = 0.0f; - } + public void init_post_process() + { + x0 = x1 = 0.0f; + y2 = y1 = 0.0f; + } - /** - * Post Process - * - * @param signal (i/o) : signal - * @param lg (i) : lenght of signal - */ + /** +* Post Process +* +* @param signal (i/o) : signal +* @param lg (i) : lenght of signal +*/ - public void post_process( - float[] signal, - int lg - ) - { - var a100 = TabLd8k.a100; - var b100 = TabLd8k.b100; + public void post_process( + float[] signal, + int lg + ) + { + var a100 = TabLd8k.a100; + var b100 = TabLd8k.b100; - int i; - float x2; - float y0; + int i; + float x2; + float y0; - for (i = 0; i < lg; i++) - { - x2 = x1; - x1 = x0; - x0 = signal[i]; + for (i = 0; i < lg; i++) + { + x2 = x1; + x1 = x0; + x0 = signal[i]; - y0 = y1 * a100[1] + y2 * a100[2] + x0 * b100[0] + x1 * b100[1] + x2 * b100[2]; + y0 = y1 * a100[1] + y2 * a100[2] + x0 * b100[0] + x1 * b100[1] + x2 * b100[2]; - signal[i] = y0; - y2 = y1; - y1 = y0; - } + signal[i] = y0; + y2 = y1; + y1 = y0; } } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Postfil.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Postfil.cs index bd730c858d..fe679298b6 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Postfil.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Postfil.cs @@ -46,864 +46,954 @@ * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ using System; +using System.Diagnostics; -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal sealed class Postfil : Ld8k { - internal class Postfil : Ld8k - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : POSTFIL.C - Used for the floating point version of G.729 main body - (not for G.729A) + /* +File : POSTFIL.C +Used for the floating point version of G.729 main body +(not for G.729A) */ - /* Static arrays and variables */ - /** - * s.t. numerator coeff. - */ - private readonly float[] apond2 = new float[LONG_H_ST]; + /* Static arrays and variables */ + /** +* s.t. numerator coeff. +*/ + private readonly float[] apond2 = new float[LONG_H_ST]; - /** - * for gain adjustment - */ - private float gain_prec; + /** +* for gain adjustment +*/ + private float gain_prec; - /** - * s.t. postfilter memory - */ - private readonly float[] mem_stp = new float[M]; + /** +* s.t. postfilter memory +*/ + private readonly float[] mem_stp = new float[M]; - /** - * null memory to compute h_st - */ - private readonly float[] mem_zero = new float[M]; + /** +* null memory to compute h_st +*/ + private readonly float[] mem_zero = new float[M]; - private float[] ptr_mem_stp; + private float[]? ptr_mem_stp; - private int ptr_mem_stp_offset; + private int ptr_mem_stp_offset; - /** - * A(gamma2) residual - */ - private readonly float[] res2 = new float[SIZ_RES2]; + /** +* A(gamma2) residual +*/ + private readonly float[] res2 = new float[SIZ_RES2]; - /* Static pointers */ - private int res2_ptr; + /* Static pointers */ + private int res2_ptr; - /** - * Initialize postfilter functions - */ + /** +* Initialize postfilter functions +*/ - public void init_post_filter() - { - int i; + public void init_post_filter() + { + int i; - /* Initialize arrays and pointers */ + /* Initialize arrays and pointers */ - /* A(gamma2) residual */ - for (i = 0; i < MEM_RES2; i++) res2[i] = 0.0f; - res2_ptr = MEM_RES2; + /* A(gamma2) residual */ + for (i = 0; i < MEM_RES2; i++) + { + res2[i] = 0.0f; + } - /* 1/A(gamma1) memory */ - for (i = 0; i < M; i++) mem_stp[i] = 0.0f; - ptr_mem_stp = mem_stp; - ptr_mem_stp_offset = M - 1; + res2_ptr = MEM_RES2; - /* fill apond2[M+1->LONG_H_ST-1] with zeroes */ - for (i = MP1; i < LONG_H_ST; i++) apond2[i] = 0.0f; + /* 1/A(gamma1) memory */ + for (i = 0; i < M; i++) + { + mem_stp[i] = 0.0f; + } - /* null memory to compute i.r. of A(gamma2)/A(gamma1) */ - for (i = 0; i < M; i++) mem_zero[i] = 0.0f; + ptr_mem_stp = mem_stp; + ptr_mem_stp_offset = M - 1; - /* for gain adjustment */ - gain_prec = 1.0f; + /* fill apond2[M+1->LONG_H_ST-1] with zeroes */ + for (i = MP1; i < LONG_H_ST; i++) + { + apond2[i] = 0.0f; } - /** - * Adaptive postfilter main function - * - * @param t0 input : pitch delay given by coder - * @param signal_ptr input : input signal (pointer to current subframe) - * @param signal_ptr_offset input : input signal offset - * @param coeff input : LPC coefficients for current subframe - * @param coeff_offset input : LPC coefficients offset - * @param sig_out output: postfiltered output - * @param sig_out_offset input: postfiltered output offset - * @return voicing decision 0 = uv, > 0 delay - */ - - public int post( - int t0, - float[] signal_ptr, - int signal_ptr_offset, - float[] coeff, - int coeff_offset, - float[] sig_out, - int sig_out_offset - ) + /* null memory to compute i.r. of A(gamma2)/A(gamma1) */ + for (i = 0; i < M; i++) { - int vo; /* output: voicing decision 0 = uv, > 0 delay */ + mem_zero[i] = 0.0f; + } + + /* for gain adjustment */ + gain_prec = 1.0f; + } + + /** +* Adaptive postfilter main function +* +* @param t0 input : pitch delay given by coder +* @param signal_ptr input : input signal (pointer to current subframe) +* @param signal_ptr_offset input : input signal offset +* @param coeff input : LPC coefficients for current subframe +* @param coeff_offset input : LPC coefficients offset +* @param sig_out output: postfiltered output +* @param sig_out_offset input: postfiltered output offset +* @return voicing decision 0 = uv, > 0 delay +*/ + + public int post( + int t0, + float[] signal_ptr, + int signal_ptr_offset, + float[] coeff, + int coeff_offset, + float[] sig_out, + int sig_out_offset + ) + { + int vo; /* output: voicing decision 0 = uv, > 0 delay */ - var apond1 = new float[MP1]; /* s.t. denominator coeff. */ - var sig_ltp = new float[L_SUBFRP1]; /* H0 output signal */ - int sig_ltp_ptr; - float parcor0; + var apond1 = new float[MP1]; /* s.t. denominator coeff. */ + var sig_ltp = new float[L_SUBFRP1]; /* H0 output signal */ + int sig_ltp_ptr; + float parcor0; - /* Compute weighted LPC coefficients */ - Lpcfunc.weight_az(coeff, coeff_offset, GAMMA1_PST, M, apond1); - Lpcfunc.weight_az(coeff, coeff_offset, GAMMA2_PST, M, apond2); + /* Compute weighted LPC coefficients */ + Lpcfunc.weight_az(coeff, coeff_offset, GAMMA1_PST, M, apond1); + Lpcfunc.weight_az(coeff, coeff_offset, GAMMA2_PST, M, apond2); - /* Compute A(gamma2) residual */ - Filter.residu(apond2, 0, signal_ptr, signal_ptr_offset, res2, res2_ptr, L_SUBFR); + /* Compute A(gamma2) residual */ + Filter.residu(apond2, 0, signal_ptr, signal_ptr_offset, res2, res2_ptr, L_SUBFR); - /* Harmonic filtering */ - sig_ltp_ptr = 1; - vo = pst_ltp(t0, res2, res2_ptr, sig_ltp, sig_ltp_ptr); + /* Harmonic filtering */ + sig_ltp_ptr = 1; + vo = pst_ltp(t0, res2, res2_ptr, sig_ltp, sig_ltp_ptr); - /* Save last output of 1/A(gamma1) */ - /* (from preceding subframe) */ - sig_ltp[0] = ptr_mem_stp[ptr_mem_stp_offset]; + /* Save last output of 1/A(gamma1) */ + /* (from preceding subframe) */ + Debug.Assert(ptr_mem_stp is { } && ptr_mem_stp.Length > ptr_mem_stp_offset); + sig_ltp[0] = ptr_mem_stp[ptr_mem_stp_offset]; - /* Control short term pst filter gain and compute parcor0 */ - parcor0 = calc_st_filt(apond2, apond1, sig_ltp, sig_ltp_ptr); + /* Control short term pst filter gain and compute parcor0 */ + parcor0 = calc_st_filt(apond2, apond1, sig_ltp, sig_ltp_ptr); - /* 1/A(gamma1) filtering, mem_stp is updated */ - Filter.syn_filt(apond1, 0, sig_ltp, sig_ltp_ptr, sig_ltp, sig_ltp_ptr, L_SUBFR, mem_stp, 0, 1); + /* 1/A(gamma1) filtering, mem_stp is updated */ + Filter.syn_filt(apond1, 0, sig_ltp, sig_ltp_ptr, sig_ltp, sig_ltp_ptr, L_SUBFR, mem_stp, 0, 1); - /* (1 + mu z-1) tilt filtering */ - filt_mu(sig_ltp, sig_out, sig_out_offset, parcor0); + /* (1 + mu z-1) tilt filtering */ + filt_mu(sig_ltp, sig_out, sig_out_offset, parcor0); - /* gain control */ - gain_prec = scale_st(signal_ptr, signal_ptr_offset, sig_out, sig_out_offset, gain_prec); + /* gain control */ + gain_prec = scale_st(signal_ptr, signal_ptr_offset, sig_out, sig_out_offset, gain_prec); - /* Update for next frame */ - Util.copy(res2, L_SUBFR, res2, MEM_RES2); + /* Update for next frame */ + Util.copy(res2, L_SUBFR, res2, MEM_RES2); - return vo; + return vo; + } + + /** +* Harmonic postfilter +* +* @param t0 input : pitch delay given by coder +* @param ptr_sig_in input : postfilter input filter (residu2) +* @param ptr_sig_in_offset input : postfilter input filter offset +* @param ptr_sig_pst0 output: harmonic postfilter output +* @param ptr_sig_pst0_offset input: harmonic postfilter offset +* @return voicing decision 0 = uv, > 0 delay +*/ + private int pst_ltp( + int t0, + float[] ptr_sig_in, + int ptr_sig_in_offset, + float[] ptr_sig_pst0, + int ptr_sig_pst0_offset + ) + { + int vo; + + /* Declare variables */ + int ltpdel, phase; + float num_gltp, den_gltp; + float num2_gltp, den2_gltp; + float gain_plt; + var y_up = new float[SIZ_Y_UP]; + float[] ptr_y_up; + int ptr_y_up_offset; + int off_yup; + + /* Sub optimal delay search */ + var _ltpdel = new IntReference(); + var _phase = new IntReference(); + var _num_gltp = new FloatReference(); + var _den_gltp = new FloatReference(); + var _off_yup = new IntReference(); + search_del( + t0, + ptr_sig_in, + ptr_sig_in_offset, + _ltpdel, + _phase, + _num_gltp, + _den_gltp, + y_up, + _off_yup); + ltpdel = _ltpdel.value; + phase = _phase.value; + num_gltp = _num_gltp.value; + den_gltp = _den_gltp.value; + off_yup = _off_yup.value; + + vo = ltpdel; + + if (num_gltp == 0.0f) + { + Util.copy(ptr_sig_in, ptr_sig_in_offset, ptr_sig_pst0, ptr_sig_pst0_offset, L_SUBFR); } + else + { - /** - * Harmonic postfilter - * - * @param t0 input : pitch delay given by coder - * @param ptr_sig_in input : postfilter input filter (residu2) - * @param ptr_sig_in_offset input : postfilter input filter offset - * @param ptr_sig_pst0 output: harmonic postfilter output - * @param ptr_sig_pst0_offset input: harmonic postfilter offset - * @return voicing decision 0 = uv, > 0 delay - */ - private int pst_ltp( - int t0, - float[] ptr_sig_in, - int ptr_sig_in_offset, - float[] ptr_sig_pst0, - int ptr_sig_pst0_offset - ) - { - int vo; - - /* Declare variables */ - int ltpdel, phase; - float num_gltp, den_gltp; - float num2_gltp, den2_gltp; - float gain_plt; - var y_up = new float[SIZ_Y_UP]; - float[] ptr_y_up; - int ptr_y_up_offset; - int off_yup; - - /* Sub optimal delay search */ - var _ltpdel = new IntReference(); - var _phase = new IntReference(); - var _num_gltp = new FloatReference(); - var _den_gltp = new FloatReference(); - var _off_yup = new IntReference(); - search_del( - t0, - ptr_sig_in, - ptr_sig_in_offset, - _ltpdel, - _phase, - _num_gltp, - _den_gltp, - y_up, - _off_yup); - ltpdel = _ltpdel.value; - phase = _phase.value; - num_gltp = _num_gltp.value; - den_gltp = _den_gltp.value; - off_yup = _off_yup.value; - - vo = ltpdel; - - if (num_gltp == 0.0f) + if (phase == 0) { - Util.copy(ptr_sig_in, ptr_sig_in_offset, ptr_sig_pst0, ptr_sig_pst0_offset, L_SUBFR); + ptr_y_up = ptr_sig_in; + ptr_y_up_offset = ptr_sig_in_offset - ltpdel; } + else { + /* Filtering with long filter */ + var _num2_gltp = new FloatReference(); + var _den2_gltp = new FloatReference(); + compute_ltp_l( + ptr_sig_in, + ptr_sig_in_offset, + ltpdel, + phase, + ptr_sig_pst0, + ptr_sig_pst0_offset, + _num2_gltp, + _den2_gltp); + num2_gltp = _num2_gltp.value; + den2_gltp = _den2_gltp.value; - if (phase == 0) + if (select_ltp(num_gltp, den_gltp, num2_gltp, den2_gltp) == 1) { - ptr_y_up = ptr_sig_in; - ptr_y_up_offset = ptr_sig_in_offset - ltpdel; - } + /* select short filter */ + ptr_y_up = y_up; + ptr_y_up_offset = (phase - 1) * L_SUBFRP1 + off_yup; + } else { - /* Filtering with long filter */ - var _num2_gltp = new FloatReference(); - var _den2_gltp = new FloatReference(); - compute_ltp_l( - ptr_sig_in, - ptr_sig_in_offset, - ltpdel, - phase, - ptr_sig_pst0, - ptr_sig_pst0_offset, - _num2_gltp, - _den2_gltp); - num2_gltp = _num2_gltp.value; - den2_gltp = _den2_gltp.value; - - if (select_ltp(num_gltp, den_gltp, num2_gltp, den2_gltp) == 1) - { - - /* select short filter */ - ptr_y_up = y_up; - ptr_y_up_offset = (phase - 1) * L_SUBFRP1 + off_yup; - } - else - { - /* select long filter */ - num_gltp = num2_gltp; - den_gltp = den2_gltp; - ptr_y_up = ptr_sig_pst0; - ptr_y_up_offset = ptr_sig_pst0_offset; - } + /* select long filter */ + num_gltp = num2_gltp; + den_gltp = den2_gltp; + ptr_y_up = ptr_sig_pst0; + ptr_y_up_offset = ptr_sig_pst0_offset; } + } - if (num_gltp > den_gltp) - gain_plt = MIN_GPLT; - else - gain_plt = den_gltp / (den_gltp + GAMMA_G * num_gltp); - - /* filtering by H0(z) (harmonic filter) */ - filt_plt( - ptr_sig_in, - ptr_sig_in_offset, - ptr_y_up, - ptr_y_up_offset, - ptr_sig_pst0, - ptr_sig_pst0_offset, - gain_plt); + if (num_gltp > den_gltp) + { + gain_plt = MIN_GPLT; + } + else + { + gain_plt = den_gltp / (den_gltp + GAMMA_G * num_gltp); } - return vo; + /* filtering by H0(z) (harmonic filter) */ + filt_plt( + ptr_sig_in, + ptr_sig_in_offset, + ptr_y_up, + ptr_y_up_offset, + ptr_sig_pst0, + ptr_sig_pst0_offset, + gain_plt); } - /** - * Computes best (shortest) integer LTP delay + fine search - * - * @param t0 input : pitch delay given by coder - * @param ptr_sig_in input : input signal (with delay line) - * @param ptr_sig_in_offset input : input signal offset - * @param ltpdel output: delay = *ltpdel - *phase / f_up - * @param phase output: phase - * @param num_gltp output: numerator of LTP gain - * @param den_gltp output: denominator of LTP gain - * @param y_up - * @param off_yup - */ - private void search_del( - int t0, - float[] ptr_sig_in, - int ptr_sig_in_offset, - IntReference ltpdel, - IntReference phase, - FloatReference num_gltp, - FloatReference den_gltp, - float[] y_up, - IntReference off_yup - ) - { - var tab_hup_s = TabLd8k.tab_hup_s; - - /* pointers on tables of constants */ - int ptr_h; - - /* Variables and local arrays */ - float[] tab_den0 = new float[F_UP_PST - 1], tab_den1 = new float[F_UP_PST - 1]; - int ptr_den0, ptr_den1; - int ptr_sig_past, ptr_sig_past0; - int ptr1; - - int i, n, ioff, i_max; - float ener, num, numsq, den0, den1; - float den_int, num_int; - float den_max, num_max, numsq_max; - int phi_max; - int lambda, phi; - float temp0, temp1; - int ptr_y_up; - - /* Compute current signal energy */ - ener = 0.0f; - for (i = 0; i < L_SUBFR; i++) - ener += ptr_sig_in[ptr_sig_in_offset + i] * ptr_sig_in[ptr_sig_in_offset + i]; - if (ener < 0.1f) - { - num_gltp.value = 0.0f; - den_gltp.value = 1.0f; - ltpdel.value = 0; - phase.value = 0; - return; - } + return vo; + } - /* Selects best of 3 integer delays */ - /* Maximum of 3 numerators around t0 */ - /* coder LTP delay */ + /** +* Computes best (shortest) integer LTP delay + fine search +* +* @param t0 input : pitch delay given by coder +* @param ptr_sig_in input : input signal (with delay line) +* @param ptr_sig_in_offset input : input signal offset +* @param ltpdel output: delay = *ltpdel - *phase / f_up +* @param phase output: phase +* @param num_gltp output: numerator of LTP gain +* @param den_gltp output: denominator of LTP gain +* @param y_up +* @param off_yup +*/ + private void search_del( + int t0, + float[] ptr_sig_in, + int ptr_sig_in_offset, + IntReference ltpdel, + IntReference phase, + FloatReference num_gltp, + FloatReference den_gltp, + float[] y_up, + IntReference off_yup + ) + { + var tab_hup_s = TabLd8k.tab_hup_s; + + /* pointers on tables of constants */ + int ptr_h; + + /* Variables and local arrays */ + float[] tab_den0 = new float[F_UP_PST - 1], tab_den1 = new float[F_UP_PST - 1]; + int ptr_den0, ptr_den1; + int ptr_sig_past, ptr_sig_past0; + int ptr1; + + int i, n, ioff, i_max; + float ener, num, numsq, den0, den1; + float den_int, num_int; + float den_max, num_max, numsq_max; + int phi_max; + int lambda, phi; + float temp0, temp1; + int ptr_y_up; + + /* Compute current signal energy */ + ener = 0.0f; + for (i = 0; i < L_SUBFR; i++) + { + ener += ptr_sig_in[ptr_sig_in_offset + i] * ptr_sig_in[ptr_sig_in_offset + i]; + } - lambda = t0 - 1; + if (ener < 0.1f) + { + num_gltp.value = 0.0f; + den_gltp.value = 1.0f; + ltpdel.value = 0; + phase.value = 0; + return; + } - ptr_sig_past = ptr_sig_in_offset - lambda; + /* Selects best of 3 integer delays */ + /* Maximum of 3 numerators around t0 */ + /* coder LTP delay */ - num_int = -1.0e30f; + lambda = t0 - 1; - /* initialization used only to suppress Microsoft Visual C++ warnings */ - i_max = 0; - for (i = 0; i < 3; i++) - { - num = 0.0f; - for (n = 0; n < L_SUBFR; n++) - num += ptr_sig_in[ptr_sig_in_offset + n] * ptr_sig_in[ptr_sig_past + n]; - if (num > num_int) - { - i_max = i; - num_int = num; - } + ptr_sig_past = ptr_sig_in_offset - lambda; - ptr_sig_past--; - } + num_int = -1.0e30f; - if (num_int <= 0.0f) + /* initialization used only to suppress Microsoft Visual C++ warnings */ + i_max = 0; + for (i = 0; i < 3; i++) + { + num = 0.0f; + for (n = 0; n < L_SUBFR; n++) { - num_gltp.value = 0.0f; - den_gltp.value = 1.0f; - ltpdel.value = 0; - phase.value = 0; - return; + num += ptr_sig_in[ptr_sig_in_offset + n] * ptr_sig_in[ptr_sig_past + n]; } - /* Calculates denominator for lambda_max */ - lambda += i_max; - ptr_sig_past = ptr_sig_in_offset - lambda; - den_int = 0.0f; - for (n = 0; n < L_SUBFR; n++) - den_int += ptr_sig_in[ptr_sig_past + n] * ptr_sig_in[ptr_sig_past + n]; - if (den_int < 0.1f) + if (num > num_int) { - num_gltp.value = 0.0f; - den_gltp.value = 1.0f; - ltpdel.value = 0; - phase.value = 0; - return; + i_max = i; + num_int = num; } - /* Select best phase around lambda */ - - /* Compute y_up & denominators */ - ptr_y_up = 0; - den_max = den_int; - ptr_den0 = 0; - ptr_den1 = 0; - ptr_h = 0; - ptr_sig_past0 = ptr_sig_in_offset + LH_UP_S - 1 - lambda; /* points on lambda_max+1 */ - - /* loop on phase */ - for (phi = 1; phi < F_UP_PST; phi++) - { - - /* Computes criterion for (lambda_max+1) - phi/F_UP_PST */ - /* and lambda_max - phi/F_UP_PST */ - ptr_sig_past = ptr_sig_past0; - /* computes y_up[n] */ - for (n = 0; n <= L_SUBFR; n++) - { - ptr1 = ptr_sig_past++; - temp0 = 0.0f; - for (i = 0; i < LH2_S; i++) - temp0 += tab_hup_s[ptr_h + i] * ptr_sig_in[ptr1 - i]; - y_up[ptr_y_up + n] = temp0; - } - /* recursive computation of den0 (lambda_max+1) and den1 (lambda_max) */ + ptr_sig_past--; + } - /* common part to den0 and den1 */ - temp0 = 0.0f; - for (n = 1; n < L_SUBFR; n++) - temp0 += y_up[ptr_y_up + n] * y_up[ptr_y_up + n]; + if (num_int <= 0.0f) + { + num_gltp.value = 0.0f; + den_gltp.value = 1.0f; + ltpdel.value = 0; + phase.value = 0; + return; + } - /* den0 */ - den0 = temp0 + y_up[ptr_y_up + 0] * y_up[ptr_y_up + 0]; - tab_den0[ptr_den0] = den0; - ptr_den0++; + /* Calculates denominator for lambda_max */ + lambda += i_max; + ptr_sig_past = ptr_sig_in_offset - lambda; + den_int = 0.0f; + for (n = 0; n < L_SUBFR; n++) + { + den_int += ptr_sig_in[ptr_sig_past + n] * ptr_sig_in[ptr_sig_past + n]; + } - /* den1 */ - den1 = temp0 + y_up[ptr_y_up + L_SUBFR] * y_up[ptr_y_up + L_SUBFR]; - tab_den1[ptr_den1] = den1; - ptr_den1++; + if (den_int < 0.1f) + { + num_gltp.value = 0.0f; + den_gltp.value = 1.0f; + ltpdel.value = 0; + phase.value = 0; + return; + } + /* Select best phase around lambda */ + + /* Compute y_up & denominators */ + ptr_y_up = 0; + den_max = den_int; + ptr_den0 = 0; + ptr_den1 = 0; + ptr_h = 0; + ptr_sig_past0 = ptr_sig_in_offset + LH_UP_S - 1 - lambda; /* points on lambda_max+1 */ + + /* loop on phase */ + for (phi = 1; phi < F_UP_PST; phi++) + { - if (Math.Abs(y_up[ptr_y_up + 0]) > Math.Abs(y_up[ptr_y_up + L_SUBFR])) - { - if (den0 > den_max) - den_max = den0; - } - else + /* Computes criterion for (lambda_max+1) - phi/F_UP_PST */ + /* and lambda_max - phi/F_UP_PST */ + ptr_sig_past = ptr_sig_past0; + /* computes y_up[n] */ + for (n = 0; n <= L_SUBFR; n++) + { + ptr1 = ptr_sig_past++; + temp0 = 0.0f; + for (i = 0; i < LH2_S; i++) { - if (den1 > den_max) - den_max = den1; + temp0 += tab_hup_s[ptr_h + i] * ptr_sig_in[ptr1 - i]; } - ptr_y_up += L_SUBFRP1; - ptr_h += LH2_S; + y_up[ptr_y_up + n] = temp0; } - if (den_max < 0.1f) + /* recursive computation of den0 (lambda_max+1) and den1 (lambda_max) */ + + /* common part to den0 and den1 */ + temp0 = 0.0f; + for (n = 1; n < L_SUBFR; n++) { - num_gltp.value = 0.0f; - den_gltp.value = 1.0f; - ltpdel.value = 0; - phase.value = 0; - return; + temp0 += y_up[ptr_y_up + n] * y_up[ptr_y_up + n]; } - /* Computation of the numerators */ - /* and selection of best num*num/den */ - /* for non null phases */ - - /* Initialize with null phase */ - num_max = num_int; - den_max = den_int; - numsq_max = num_max * num_max; - phi_max = 0; - ioff = 1; - - ptr_den0 = 0; - ptr_den1 = 0; - ptr_y_up = 0; - - /* if den_max = 0 : will be selected and declared unvoiced */ - /* if num!=0 & den=0 : will be selected and declared unvoiced */ - /* degenerated seldom cases, switch off LT is OK */ - - /* Loop on phase */ - for (phi = 1; phi < F_UP_PST; phi++) - { - /* computes num for lambda_max+1 - phi/F_UP_PST */ - num = 0.0f; - for (n = 0; n < L_SUBFR; n++) - num += ptr_sig_in[n] * y_up[ptr_y_up + n]; - if (num < 0.0f) num = 0.0f; - numsq = num * num; - - /* selection if num/sqrt(den0) max */ - den0 = tab_den0[ptr_den0]; - ptr_den0++; - temp0 = numsq * den_max; - temp1 = numsq_max * den0; - if (temp0 > temp1) + /* den0 */ + den0 = temp0 + y_up[ptr_y_up + 0] * y_up[ptr_y_up + 0]; + tab_den0[ptr_den0] = den0; + ptr_den0++; + + /* den1 */ + den1 = temp0 + y_up[ptr_y_up + L_SUBFR] * y_up[ptr_y_up + L_SUBFR]; + tab_den1[ptr_den1] = den1; + ptr_den1++; + + if (Math.Abs(y_up[ptr_y_up + 0]) > Math.Abs(y_up[ptr_y_up + L_SUBFR])) + { + if (den0 > den_max) { - num_max = num; - numsq_max = numsq; den_max = den0; - ioff = 0; - phi_max = phi; } - - /* computes num for lambda_max - phi/F_UP_PST */ - ptr_y_up++; - num = 0.0f; - for (n = 0; n < L_SUBFR; n++) - num += ptr_sig_in[n] * y_up[ptr_y_up + n]; - if (num < 0.0f) num = 0.0f; - numsq = num * num; - - /* selection if num/sqrt(den1) max */ - den1 = tab_den1[ptr_den1]; - ptr_den1++; - temp0 = numsq * den_max; - temp1 = numsq_max * den1; - if (temp0 > temp1) + } + else + { + if (den1 > den_max) { - num_max = num; - numsq_max = numsq; den_max = den1; - ioff = 1; - phi_max = phi; } + } + + ptr_y_up += L_SUBFRP1; + ptr_h += LH2_S; + } + + if (den_max < 0.1f) + { + num_gltp.value = 0.0f; + den_gltp.value = 1.0f; + ltpdel.value = 0; + phase.value = 0; + return; + } + /* Computation of the numerators */ + /* and selection of best num*num/den */ + /* for non null phases */ + + /* Initialize with null phase */ + num_max = num_int; + den_max = den_int; + numsq_max = num_max * num_max; + phi_max = 0; + ioff = 1; + + ptr_den0 = 0; + ptr_den1 = 0; + ptr_y_up = 0; + + /* if den_max = 0 : will be selected and declared unvoiced */ + /* if num!=0 & den=0 : will be selected and declared unvoiced */ + /* degenerated seldom cases, switch off LT is OK */ + + /* Loop on phase */ + for (phi = 1; phi < F_UP_PST; phi++) + { - ptr_y_up += L_SUBFR; + /* computes num for lambda_max+1 - phi/F_UP_PST */ + num = 0.0f; + for (n = 0; n < L_SUBFR; n++) + { + num += ptr_sig_in[n] * y_up[ptr_y_up + n]; } - /* test if normalised crit0[iopt] > THRESCRIT */ + if (num < 0.0f) + { + num = 0.0f; + } + + numsq = num * num; - if (num_max == 0.0f || den_max <= 0.1f) + /* selection if num/sqrt(den0) max */ + den0 = tab_den0[ptr_den0]; + ptr_den0++; + temp0 = numsq * den_max; + temp1 = numsq_max * den0; + if (temp0 > temp1) { - num_gltp.value = 0.0f; - den_gltp.value = 1.0f; - ltpdel.value = 0; - phase.value = 0; - return; + num_max = num; + numsq_max = numsq; + den_max = den0; + ioff = 0; + phi_max = phi; } - /* comparison num * num */ - /* with ener * den x THRESCRIT */ - temp1 = den_max * ener * THRESCRIT; - if (numsq_max >= temp1) + /* computes num for lambda_max - phi/F_UP_PST */ + ptr_y_up++; + num = 0.0f; + for (n = 0; n < L_SUBFR; n++) { - ltpdel.value = lambda + 1 - ioff; - off_yup.value = ioff; - phase.value = phi_max; - num_gltp.value = num_max; - den_gltp.value = den_max; + num += ptr_sig_in[n] * y_up[ptr_y_up + n]; } - else + + if (num < 0.0f) + { + num = 0.0f; + } + + numsq = num * num; + + /* selection if num/sqrt(den1) max */ + den1 = tab_den1[ptr_den1]; + ptr_den1++; + temp0 = numsq * den_max; + temp1 = numsq_max * den1; + if (temp0 > temp1) { - num_gltp.value = 0.0f; - den_gltp.value = 1.0f; - ltpdel.value = 0; - phase.value = 0; + num_max = num; + numsq_max = numsq; + den_max = den1; + ioff = 1; + phi_max = phi; } + + ptr_y_up += L_SUBFR; } - /** - * Ltp postfilter - * - * @param s_in input : input signal with past - * @param s_in_offset input : input signal offset - * @param s_ltp input : filtered signal with gain 1 - * @param s_ltp_offset input : filtered signal offset - * @param s_out output: output signal - * @param s_out_offset input: output signal offset - * @param gain_plt input : filter gain - */ - private void filt_plt( - float[] s_in, - int s_in_offset, - float[] s_ltp, - int s_ltp_offset, - float[] s_out, - int s_out_offset, - float gain_plt - ) + /* test if normalised crit0[iopt] > THRESCRIT */ + + if (num_max == 0.0f || den_max <= 0.1f) { + num_gltp.value = 0.0f; + den_gltp.value = 1.0f; + ltpdel.value = 0; + phase.value = 0; + return; + } - /* Local variables */ - int n; - float temp; - float gain_plt_1; + /* comparison num * num */ + /* with ener * den x THRESCRIT */ + temp1 = den_max * ener * THRESCRIT; + if (numsq_max >= temp1) + { + ltpdel.value = lambda + 1 - ioff; + off_yup.value = ioff; + phase.value = phi_max; + num_gltp.value = num_max; + den_gltp.value = den_max; + } + else + { + num_gltp.value = 0.0f; + den_gltp.value = 1.0f; + ltpdel.value = 0; + phase.value = 0; + } + } - gain_plt_1 = 1.0f - gain_plt; + /** +* Ltp postfilter +* +* @param s_in input : input signal with past +* @param s_in_offset input : input signal offset +* @param s_ltp input : filtered signal with gain 1 +* @param s_ltp_offset input : filtered signal offset +* @param s_out output: output signal +* @param s_out_offset input: output signal offset +* @param gain_plt input : filter gain +*/ + private void filt_plt( + float[] s_in, + int s_in_offset, + float[] s_ltp, + int s_ltp_offset, + float[] s_out, + int s_out_offset, + float gain_plt + ) + { - for (n = 0; n < L_SUBFR; n++) + /* Local variables */ + int n; + float temp; + float gain_plt_1; + + gain_plt_1 = 1.0f - gain_plt; + + for (n = 0; n < L_SUBFR; n++) + { + /* s_out(n) = gain_plt x s_in(n) + gain_plt_1 x s_ltp(n) */ + temp = gain_plt * s_in[s_in_offset + n]; + temp += gain_plt_1 * s_ltp[s_ltp_offset + n]; + s_out[s_out_offset + n] = temp; + } + } + + /** +* Compute delayed signal, +* num & den of gain for fractional delay +* with long interpolation filter +* +* @param s_in input signal with past +* @param s_in_offset input signal with past +* @param ltpdel delay factor +* @param phase phase factor +* @param y_up delayed signal +* @param y_up_offset delayed signal offset +* @param num numerator of LTP gain +* @param den denominator of LTP gain +*/ + private void compute_ltp_l( + float[] s_in, + int s_in_offset, + int ltpdel, + int phase, + float[] y_up, + int y_up_offset, + FloatReference num, + FloatReference den + ) + { + var tab_hup_l = TabLd8k.tab_hup_l; + + /* Pointer on table of constants */ + int ptr_h; + + /* Local variables */ + int i; + int ptr2; + float temp; + + /* Filtering with long filter */ + ptr_h = (phase - 1) * LH2_L; + ptr2 = s_in_offset - ltpdel + LH_UP_L; + + /* Compute y_up */ + for (int n = y_up_offset, toIndex = y_up_offset + L_SUBFR; n < toIndex; n++) + { + temp = 0.0f; + for (i = 0; i < LH2_L; i++) { - /* s_out(n) = gain_plt x s_in(n) + gain_plt_1 x s_ltp(n) */ - temp = gain_plt * s_in[s_in_offset + n]; - temp += gain_plt_1 * s_ltp[s_ltp_offset + n]; - s_out[s_out_offset + n] = temp; + temp += tab_hup_l[ptr_h + i] * s_in[ptr2]; + ptr2--; } + + y_up[n] = temp; + ptr2 += LH2_L_P1; } - /** - * Compute delayed signal, - * num & den of gain for fractional delay - * with long interpolation filter - * - * @param s_in input signal with past - * @param s_in_offset input signal with past - * @param ltpdel delay factor - * @param phase phase factor - * @param y_up delayed signal - * @param y_up_offset delayed signal offset - * @param num numerator of LTP gain - * @param den denominator of LTP gain - */ - private void compute_ltp_l( - float[] s_in, - int s_in_offset, - int ltpdel, - int phase, - float[] y_up, - int y_up_offset, - FloatReference num, - FloatReference den - ) - { - var tab_hup_l = TabLd8k.tab_hup_l; - - /* Pointer on table of constants */ - int ptr_h; - - /* Local variables */ - int i; - int ptr2; - float temp; - - /* Filtering with long filter */ - ptr_h = (phase - 1) * LH2_L; - ptr2 = s_in_offset - ltpdel + LH_UP_L; - - /* Compute y_up */ - for (int n = y_up_offset, toIndex = y_up_offset + L_SUBFR; n < toIndex; n++) - { - temp = 0.0f; - for (i = 0; i < LH2_L; i++) - { - temp += tab_hup_l[ptr_h + i] * s_in[ptr2]; - ptr2--; - } + var _num = 0.0f; + /* Compute num */ + for (var n = 0; n < L_SUBFR; n++) + { + _num += y_up[y_up_offset + n] * s_in[s_in_offset + n]; + } - y_up[n] = temp; - ptr2 += LH2_L_P1; - } + if (_num < 0.0f) + { + _num = 0.0f; + } - var _num = 0.0f; - /* Compute num */ - for (var n = 0; n < L_SUBFR; n++) - _num += y_up[y_up_offset + n] * s_in[s_in_offset + n]; - if (_num < 0.0f) _num = 0.0f; - num.value = _num; + num.value = _num; - var _den = 0.0f; - /* Compute den */ - for (int n = y_up_offset, toIndex = y_up_offset + L_SUBFR; n < toIndex; n++) - _den += y_up[n] * y_up[n]; - den.value = _den; + var _den = 0.0f; + /* Compute den */ + for (int n = y_up_offset, toIndex = y_up_offset + L_SUBFR; n < toIndex; n++) + { + _den += y_up[n] * y_up[n]; } - /** - * Selects best of (gain1, gain2) - * with gain1 = num1 / den1 - * and gain2 = num2 / den2 - * - * @param num1 input : numerator of gain1 - * @param den1 input : denominator of gain1 - * @param num2 input : numerator of gain2 - * @param den2 input : denominator of gain2 - * @return 1 = 1st gain, 2 = 2nd gain - */ - private int select_ltp( - float num1, - float den1, - float num2, - float den2 - ) - { - if (den2 == 0.0f) - return 1; - if (num2 * num2 * den1 > num1 * num1 * den2) - return 2; + den.value = _den; + } + + /** +* Selects best of (gain1, gain2) +* with gain1 = num1 / den1 +* and gain2 = num2 / den2 +* +* @param num1 input : numerator of gain1 +* @param den1 input : denominator of gain1 +* @param num2 input : numerator of gain2 +* @param den2 input : denominator of gain2 +* @return 1 = 1st gain, 2 = 2nd gain +*/ + private int select_ltp( + float num1, + float den1, + float num2, + float den2 + ) + { + if (den2 == 0.0f) + { return 1; } - /** - * Computes impulse response of A(gamma2) / A(gamma1). - * controls gain : computation of energy impulse response as - * SUMn (abs (h[n])) and computes parcor0 - * - * @param apond2 input : coefficients of numerator - * @param apond1 input : coefficients of denominator - * @param sig_ltp_ptr in/out: input of 1/A(gamma1) : scaled by 1/g0 - * @param sig_ltp_ptr_offset input : input of 1/A(gamma1) ... offset - * @return 1st parcor calcul. on composed filter - */ - private float calc_st_filt( - float[] apond2, - float[] apond1, - float[] sig_ltp_ptr, - int sig_ltp_ptr_offset - ) + if (num2 * num2 * den1 > num1 * num1 * den2) { - var h = new float[LONG_H_ST]; - float parcor0; /* output: 1st parcor calcul. on composed filter */ - float g0, temp; + return 2; + } - /* computes impulse response of apond1 / apond2 */ - Filter.syn_filt(apond1, 0, apond2, 0, h, 0, LONG_H_ST, mem_zero, 0, 0); + return 1; + } - /* computes 1st parcor */ - parcor0 = calc_rc0_h(h); + /** +* Computes impulse response of A(gamma2) / A(gamma1). +* controls gain : computation of energy impulse response as +* SUMn (abs (h[n])) and computes parcor0 +* +* @param apond2 input : coefficients of numerator +* @param apond1 input : coefficients of denominator +* @param sig_ltp_ptr in/out: input of 1/A(gamma1) : scaled by 1/g0 +* @param sig_ltp_ptr_offset input : input of 1/A(gamma1) ... offset +* @return 1st parcor calcul. on composed filter +*/ + private float calc_st_filt( + float[] apond2, + float[] apond1, + float[] sig_ltp_ptr, + int sig_ltp_ptr_offset + ) + { + var h = new float[LONG_H_ST]; + float parcor0; /* output: 1st parcor calcul. on composed filter */ + float g0, temp; - /* computes gain g0 */ - g0 = 0.0f; - for (var i = 0; i < LONG_H_ST; i++) - g0 += Math.Abs(h[i]); + /* computes impulse response of apond1 / apond2 */ + Filter.syn_filt(apond1, 0, apond2, 0, h, 0, LONG_H_ST, mem_zero, 0, 0); + + /* computes 1st parcor */ + parcor0 = calc_rc0_h(h); - /* Scale signal input of 1/A(gamma1) */ - if (g0 > 1.0f) + /* computes gain g0 */ + g0 = 0.0f; + for (var i = 0; i < LONG_H_ST; i++) + { + g0 += Math.Abs(h[i]); + } + + /* Scale signal input of 1/A(gamma1) */ + if (g0 > 1.0f) + { + temp = 1.0f / g0; + for (int i = sig_ltp_ptr_offset, toIndex = sig_ltp_ptr_offset + L_SUBFR; i < toIndex; i++) { - temp = 1.0f / g0; - for (int i = sig_ltp_ptr_offset, toIndex = sig_ltp_ptr_offset + L_SUBFR; i < toIndex; i++) - sig_ltp_ptr[i] = sig_ltp_ptr[i] * temp; + sig_ltp_ptr[i] = sig_ltp_ptr[i] * temp; } + } + + return parcor0; + } - return parcor0; + /** +* Computes 1st parcor from composed filter impulse response. +* +* @param h input : impulse response of composed filter +* @return 1st parcor +*/ + private float calc_rc0_h( + float[] h + ) + { + float acf0, acf1; + float temp, temp2; + int ptrs; + int i; + + /* computation of the autocorrelation function acf */ + temp = 0.0f; + for (i = 0; i < LONG_H_ST; i++) + { + temp += h[i] * h[i]; } - /** - * Computes 1st parcor from composed filter impulse response. - * - * @param h input : impulse response of composed filter - * @return 1st parcor - */ - private float calc_rc0_h( - float[] h - ) + acf0 = temp; + + temp = 0.0f; + ptrs = 0; + for (i = 0; i < LONG_H_ST - 1; i++) { - float acf0, acf1; - float temp, temp2; - int ptrs; - int i; + temp2 = h[ptrs]; + ptrs++; + temp += temp2 * h[ptrs]; + } - /* computation of the autocorrelation function acf */ - temp = 0.0f; - for (i = 0; i < LONG_H_ST; i++) - temp += h[i] * h[i]; - acf0 = temp; + acf1 = temp; - temp = 0.0f; - ptrs = 0; - for (i = 0; i < LONG_H_ST - 1; i++) - { - temp2 = h[ptrs]; - ptrs++; - temp += temp2 * h[ptrs]; - } + /* Initialisation of the calculation */ + if (acf0 == 0.0f) + { + return 0.0f; /* output: 1st parcor */ + } - acf1 = temp; + /* Compute 1st parcor */ + if (acf0 < Math.Abs(acf1)) + { + return 0.0f; /* output: 1st parcor */ + } - /* Initialisation of the calculation */ - if (acf0 == 0.0f) - return 0.0f; /* output: 1st parcor */ + return -acf1 / acf0; /* output: 1st parcor */ + } - /* Compute 1st parcor */ - if (acf0 < Math.Abs(acf1)) - return 0.0f; /* output: 1st parcor */ - return -acf1 / acf0; /* output: 1st parcor */ + /** +* Tilt filtering with : (1 + mu z-1) * (1/1-|mu|). +* computes y[n] = (1/1-|mu|) (x[n]+mu*x[n-1]) +* +* @param sig_in input : input signal (beginning at sample -1) +* @param sig_out output: output signal +* @param sig_out_offset input: output signal offset +* @param parcor0 input : parcor0 (mu = parcor0 * gamma3) +*/ + private void filt_mu( + float[] sig_in, + float[] sig_out, + int sig_out_offset, + float parcor0 + ) + { + int n; + float mu, ga, temp; + int ptrs; + + if (parcor0 > 0.0f) + { + mu = parcor0 * GAMMA3_PLUS; + } + else + { + mu = parcor0 * GAMMA3_MINUS; } - /** - * Tilt filtering with : (1 + mu z-1) * (1/1-|mu|). - * computes y[n] = (1/1-|mu|) (x[n]+mu*x[n-1]) - * - * @param sig_in input : input signal (beginning at sample -1) - * @param sig_out output: output signal - * @param sig_out_offset input: output signal offset - * @param parcor0 input : parcor0 (mu = parcor0 * gamma3) - */ - private void filt_mu( - float[] sig_in, - float[] sig_out, - int sig_out_offset, - float parcor0 - ) - { - int n; - float mu, ga, temp; - int ptrs; - - if (parcor0 > 0.0f) - mu = parcor0 * GAMMA3_PLUS; - else - mu = parcor0 * GAMMA3_MINUS; - ga = 1.0f / (1.0f - Math.Abs(mu)); + ga = 1.0f / (1.0f - Math.Abs(mu)); - ptrs = 0; /* points on sig_in(-1) */ - for (n = 0; n < L_SUBFR; n++) - { - temp = mu * sig_in[ptrs]; - ptrs++; - temp += sig_in[ptrs]; - sig_out[sig_out_offset + n] = ga * temp; - } + ptrs = 0; /* points on sig_in(-1) */ + for (n = 0; n < L_SUBFR; n++) + { + temp = mu * sig_in[ptrs]; + ptrs++; + temp += sig_in[ptrs]; + sig_out[sig_out_offset + n] = ga * temp; } + } - /** - * Control of the subframe gain. - * gain[n] = AGC_FAC * gain[n-1] + (1 - AGC_FAC) g_in/g_out - * - * @param sig_in input : postfilter input signal - * @param sig_in_offset input : postfilter input signal offset - * @param sig_out in/out: postfilter output signal - * @param sig_out_offset input: postfilter output signal offset - * @param gain_prec input : last value of gain for subframe - * @return gain_prec last value of gain for subframe - */ - private float scale_st( - float[] sig_in, - int sig_in_offset, - float[] sig_out, - int sig_out_offset, - float gain_prec - ) - { - float gain_in, gain_out; - float g0; - - /* compute input gain */ - gain_in = 0.0f; - for (int i = sig_in_offset, toIndex = sig_in_offset + L_SUBFR; i < toIndex; i++) - gain_in += Math.Abs(sig_in[i]); - if (gain_in == 0.0f) - { - g0 = 0.0f; - } - else - { + /** +* Control of the subframe gain. +* gain[n] = AGC_FAC * gain[n-1] + (1 - AGC_FAC) g_in/g_out +* +* @param sig_in input : postfilter input signal +* @param sig_in_offset input : postfilter input signal offset +* @param sig_out in/out: postfilter output signal +* @param sig_out_offset input: postfilter output signal offset +* @param gain_prec input : last value of gain for subframe +* @return gain_prec last value of gain for subframe +*/ + private float scale_st( + float[] sig_in, + int sig_in_offset, + float[] sig_out, + int sig_out_offset, + float gain_prec + ) + { + float gain_in, gain_out; + float g0; - /* Compute output gain */ - gain_out = 0.0f; - for (int i = sig_out_offset, toIndex = sig_out_offset + L_SUBFR; i < toIndex; i++) - gain_out += Math.Abs(sig_out[i]); - if (gain_out == 0.0f) - { - gain_prec = 0.0f; - return gain_prec; - } + /* compute input gain */ + gain_in = 0.0f; + for (int i = sig_in_offset, toIndex = sig_in_offset + L_SUBFR; i < toIndex; i++) + { + gain_in += Math.Abs(sig_in[i]); + } - g0 = gain_in / gain_out; - g0 *= AGC_FAC1; - } + if (gain_in == 0.0f) + { + g0 = 0.0f; + } + else + { - /* compute gain(n) = AGC_FAC gain(n-1) + (1-AGC_FAC)gain_in/gain_out */ - /* sig_out(n) = gain(n) sig_out(n) */ + /* Compute output gain */ + gain_out = 0.0f; for (int i = sig_out_offset, toIndex = sig_out_offset + L_SUBFR; i < toIndex; i++) { - gain_prec *= AGC_FAC; - gain_prec += g0; - sig_out[i] *= gain_prec; + gain_out += Math.Abs(sig_out[i]); } - return gain_prec; + if (gain_out == 0.0f) + { + gain_prec = 0.0f; + return gain_prec; + } + + g0 = gain_in / gain_out; + g0 *= AGC_FAC1; } + + /* compute gain(n) = AGC_FAC gain(n-1) + (1-AGC_FAC)gain_in/gain_out */ + /* sig_out(n) = gain(n) sig_out(n) */ + for (int i = sig_out_offset, toIndex = sig_out_offset + L_SUBFR; i < toIndex; i++) + { + gain_prec *= AGC_FAC; + gain_prec += g0; + sig_out[i] *= gain_prec; + } + + return gain_prec; } } diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/PreProc.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/PreProc.cs index 3e4bb80fb8..d1b1fb5102 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/PreProc.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/PreProc.cs @@ -1,4 +1,5 @@ -/* +using System; +/* * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,87 +38,84 @@ * * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal sealed class PreProc { - internal class PreProc - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : PRE_PROC.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : PRE_PROC.C +Used for the floating point version of both +G.729 main body and G.729A */ - /** - * High-pass fir memory - */ - private float x0, x1; - - /** - * High-pass iir memory - */ - private float y1, y2; + /** +* High-pass fir memory +*/ + private float x0, x1; - /** - * Init Pre Process - */ + /** +* High-pass iir memory +*/ + private float y1, y2; - public void init_pre_process() - { - x0 = x1 = 0.0f; - y2 = y1 = 0.0f; - } + /** +* Init Pre Process +*/ - /** - * Pre Process - * - * @param signal (i/o) : signal - * @param signal_offset (input) : signal offset - * @param lg (i) : length of signal - */ + public void init_pre_process() + { + x0 = x1 = 0.0f; + y2 = y1 = 0.0f; + } - public void pre_process( - float[] signal, - int signal_offset, - int lg - ) - { - var a140 = TabLd8k.a140; - var b140 = TabLd8k.b140; + /// + /// Pre Process + /// + /// Signal buffer to process (in-place). + /// Offset in the signal buffer. + /// Number of samples to process. + public void pre_process( + Span signal, + int signalOffset, + int length + ) + { + var a140 = TabLd8k.a140; + var b140 = TabLd8k.b140; - float x2; - float y0; + float x2; + float y0; - for (int i = signal_offset, toIndex = lg + signal_offset; i < toIndex; i++) - { - x2 = x1; - x1 = x0; - x0 = signal[i]; + for (int i = signalOffset, toIndex = length + signalOffset; i < toIndex; i++) + { + x2 = x1; + x1 = x0; + x0 = signal[i]; - y0 = y1 * a140[1] + y2 * a140[2] + x0 * b140[0] + x1 * b140[1] + x2 * b140[2]; + y0 = y1 * a140[1] + y2 * a140[2] + x0 * b140[0] + x1 * b140[1] + x2 * b140[2]; - signal[i] = y0; - y2 = y1; - y1 = y0; - } + signal[i] = y0; + y2 = y1; + y1 = y0; } } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/PredLt3.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/PredLt3.cs index 74530fe629..1fc14d3ce5 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/PredLt3.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/PredLt3.cs @@ -22,88 +22,89 @@ /** * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal static class PredLt3 { - internal class PredLt3 - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : PRED_LT3.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : PRED_LT3.C +Used for the floating point version of both +G.729 main body and G.729A */ - /** - * Compute the result of long term prediction with fractional - * interpolation of resolution 1/3. - * - * On return exc[0..L_subfr-1] contains the interpolated signal - * (adaptive codebook excitation) - * - * @param exc in/out: excitation vector, exc[0:l_sub-1] = out - * @param exc_offset input: excitation vector offset - * @param t0 input : pitch lag - * @param frac input : Fraction of pitch lag (-1, 0, 1) / 3 - * @param l_subfr input : length of subframe. - */ + /** +* Compute the result of long term prediction with fractional +* interpolation of resolution 1/3. +* +* On return exc[0..L_subfr-1] contains the interpolated signal +* (adaptive codebook excitation) +* +* @param exc in/out: excitation vector, exc[0:l_sub-1] = out +* @param exc_offset input: excitation vector offset +* @param t0 input : pitch lag +* @param frac input : Fraction of pitch lag (-1, 0, 1) / 3 +* @param l_subfr input : length of subframe. +*/ - public static void pred_lt_3( - float[] exc, - int exc_offset, - int t0, - int frac, - int l_subfr - ) - { - var L_INTER10 = Ld8k.L_INTER10; - var UP_SAMP = Ld8k.UP_SAMP; - var inter_3l = TabLd8k.inter_3l; + public static void pred_lt_3( + float[] exc, + int exc_offset, + int t0, + int frac, + int l_subfr + ) + { + var L_INTER10 = Ld8k.L_INTER10; + var UP_SAMP = Ld8k.UP_SAMP; + var inter_3l = TabLd8k.inter_3l; - int i, j, k; - float s; - int x0, x1, x2, c1, c2; + int i, j, k; + float s; + int x0, x1, x2, c1, c2; - x0 = exc_offset - t0; + x0 = exc_offset - t0; - frac = -frac; - if (frac < 0) - { - frac += UP_SAMP; - x0--; - } - - for (j = 0; j < l_subfr; j++) - { - x1 = x0; - x0++; - x2 = x0; - c1 = frac; - c2 = UP_SAMP - frac; + frac = -frac; + if (frac < 0) + { + frac += UP_SAMP; + x0--; + } - s = 0.0f; - for (i = 0, k = 0; i < L_INTER10; i++, k += UP_SAMP) - s += exc[x1 - i] * inter_3l[c1 + k] + exc[x2 + i] * inter_3l[c2 + k]; + for (j = 0; j < l_subfr; j++) + { + x1 = x0; + x0++; + x2 = x0; + c1 = frac; + c2 = UP_SAMP - frac; - exc[exc_offset + j] = s; + s = 0.0f; + for (i = 0, k = 0; i < L_INTER10; i++, k += UP_SAMP) + { + s += exc[x1 - i] * inter_3l[c1 + k] + exc[x2 + i] * inter_3l[c2 + k]; } + + exc[exc_offset + j] = s; } } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Pwf.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Pwf.cs index 6fee4a228b..49d4e6df99 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Pwf.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Pwf.cs @@ -24,147 +24,170 @@ */ using System; -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal sealed class Pwf { - internal class Pwf + private readonly float[ /* 2 */] lar_old = { - private readonly float[ /* 2 */] lar_old = - { - 0.0f, - 0.0f - }; - - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + 0.0f, + 0.0f + }; + + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : PWF.C - Used for the floating point version of G.729 main body - (not for G.729A) + /* +File : PWF.C +Used for the floating point version of G.729 main body +(not for G.729A) */ - private int smooth = 1; + private int smooth = 1; - /** - * Adaptive bandwidth expansion for perceptual weighting filter - * - * @param gamma1 output: gamma1 value - * @param gamma2 output: gamma2 value - * @param lsfint input : Interpolated lsf vector : 1st subframe - * @param lsfnew input : lsf vector : 2nd subframe - * @param r_c input : Reflection coefficients - */ + /** +* Adaptive bandwidth expansion for perceptual weighting filter +* +* @param gamma1 output: gamma1 value +* @param gamma2 output: gamma2 value +* @param lsfint input : Interpolated lsf vector : 1st subframe +* @param lsfnew input : lsf vector : 2nd subframe +* @param r_c input : Reflection coefficients +*/ + + public void perc_var( + float[] gamma1, + float[] gamma2, + float[] lsfint, + float[] lsfnew, + float[] r_c + ) + { + var ALPHA = Ld8k.ALPHA; + var BETA = Ld8k.BETA; + var GAMMA1_0 = Ld8k.GAMMA1_0; + var GAMMA1_1 = Ld8k.GAMMA1_1; + var GAMMA2_0_H = Ld8k.GAMMA2_0_H; + var GAMMA2_0_L = Ld8k.GAMMA2_0_L; + var GAMMA2_1 = Ld8k.GAMMA2_1; + var M = Ld8k.M; + var THRESH_H1 = Ld8k.THRESH_H1; + var THRESH_H2 = Ld8k.THRESH_H2; + var THRESH_L1 = Ld8k.THRESH_L1; + var THRESH_L2 = Ld8k.THRESH_L2; + + var lar = new float[4]; + float[] lsf; + float critlar0, critlar1; + float d_min, temp; + int i, k; + + var lar_new = lar; + var lar_new_offset = 2; + + /* reflection coefficients --> lar */ + for (i = 0; i < 2; i++) + { + lar_new[lar_new_offset + i] = (float)Math.Log10((1.0f + r_c[i]) / (1.0f - r_c[i])); + } + + /* Interpolation of lar for the 1st subframe */ + for (i = 0; i < 2; i++) + { + lar[i] = 0.5f * (lar_new[lar_new_offset + i] + lar_old[i]); + lar_old[i] = lar_new[lar_new_offset + i]; + } - public void perc_var( - float[] gamma1, - float[] gamma2, - float[] lsfint, - float[] lsfnew, - float[] r_c - ) + for (k = 0; k < 2; k++) { - var ALPHA = Ld8k.ALPHA; - var BETA = Ld8k.BETA; - var GAMMA1_0 = Ld8k.GAMMA1_0; - var GAMMA1_1 = Ld8k.GAMMA1_1; - var GAMMA2_0_H = Ld8k.GAMMA2_0_H; - var GAMMA2_0_L = Ld8k.GAMMA2_0_L; - var GAMMA2_1 = Ld8k.GAMMA2_1; - var M = Ld8k.M; - var THRESH_H1 = Ld8k.THRESH_H1; - var THRESH_H2 = Ld8k.THRESH_H2; - var THRESH_L1 = Ld8k.THRESH_L1; - var THRESH_L2 = Ld8k.THRESH_L2; - - var lar = new float[4]; - float[] lsf; - float critlar0, critlar1; - float d_min, temp; - int i, k; - - var lar_new = lar; - var lar_new_offset = 2; - - /* reflection coefficients --> lar */ - for (i = 0; i < 2; i++) - lar_new[lar_new_offset + i] = (float)Math.Log10((1.0f + r_c[i]) / (1.0f - r_c[i])); - - /* Interpolation of lar for the 1st subframe */ - for (i = 0; i < 2; i++) + /* LOOP : gamma2 for 1st to 2nd subframes */ + + /* ----------------------------------------------------- */ + /* First criterion based on the first two lars */ + /* */ + /* smooth == 1 ==> gamma2 is set to 0.6 */ + /* gamma1 is set to 0.94 */ + /* */ + /* smooth == 0 ==> gamma2 can vary from 0.4 to 0.7 */ + /* (gamma2 = -6.0 dmin + 1.0) */ + /* gamma1 is set to 0.98 */ + /* ----------------------------------------------------- */ + critlar0 = lar[2 * k]; + critlar1 = lar[2 * k + 1]; + + if (smooth != 0) { - lar[i] = 0.5f * (lar_new[lar_new_offset + i] + lar_old[i]); - lar_old[i] = lar_new[lar_new_offset + i]; + if (critlar0 < THRESH_L1 && critlar1 > THRESH_H1) + { + smooth = 0; + } + } + else + { + if (critlar0 > THRESH_L2 || critlar1 < THRESH_H2) + { + smooth = 1; + } } - for (k = 0; k < 2; k++) + if (smooth == 0) { - /* LOOP : gamma2 for 1st to 2nd subframes */ - - /* ----------------------------------------------------- */ - /* First criterion based on the first two lars */ - /* */ - /* smooth == 1 ==> gamma2 is set to 0.6 */ - /* gamma1 is set to 0.94 */ - /* */ - /* smooth == 0 ==> gamma2 can vary from 0.4 to 0.7 */ - /* (gamma2 = -6.0 dmin + 1.0) */ - /* gamma1 is set to 0.98 */ - /* ----------------------------------------------------- */ - critlar0 = lar[2 * k]; - critlar1 = lar[2 * k + 1]; - - if (smooth != 0) + /* ------------------------------------------------------ */ + /* Second criterion based on the minimum distance between */ + /* two successives lsfs */ + /* ------------------------------------------------------ */ + gamma1[k] = GAMMA1_0; + if (k == 0) { - if (critlar0 < THRESH_L1 && critlar1 > THRESH_H1) smooth = 0; + lsf = lsfint; } else { - if (critlar0 > THRESH_L2 || critlar1 < THRESH_H2) smooth = 1; + lsf = lsfnew; } - if (smooth == 0) + d_min = lsf[1] - lsf[0]; + for (i = 1; i < M - 1; i++) { - /* ------------------------------------------------------ */ - /* Second criterion based on the minimum distance between */ - /* two successives lsfs */ - /* ------------------------------------------------------ */ - gamma1[k] = GAMMA1_0; - if (k == 0) lsf = lsfint; - else lsf = lsfnew; - d_min = lsf[1] - lsf[0]; - for (i = 1; i < M - 1; i++) + temp = lsf[i + 1] - lsf[i]; + if (temp < d_min) { - temp = lsf[i + 1] - lsf[i]; - if (temp < d_min) d_min = temp; + d_min = temp; } + } - gamma2[k] = ALPHA * d_min + BETA; + gamma2[k] = ALPHA * d_min + BETA; - if (gamma2[k] > GAMMA2_0_H) gamma2[k] = GAMMA2_0_H; - if (gamma2[k] < GAMMA2_0_L) gamma2[k] = GAMMA2_0_L; + if (gamma2[k] > GAMMA2_0_H) + { + gamma2[k] = GAMMA2_0_H; } - else + + if (gamma2[k] < GAMMA2_0_L) { - gamma1[k] = GAMMA1_1; - gamma2[k] = GAMMA2_1; - ; + gamma2[k] = GAMMA2_0_L; } } + else + { + gamma1[k] = GAMMA1_1; + gamma2[k] = GAMMA2_1; + } } } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/QuaGain.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/QuaGain.cs index a0d0c9f531..ce39844849 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/QuaGain.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/QuaGain.cs @@ -22,148 +22,135 @@ /** * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal sealed class QuaGain { - internal class QuaGain - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : QUA_GAIN.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : QUA_GAIN.C +Used for the floating point version of both +G.729 main body and G.729A */ - /* gain quantizer routines */ + /* gain quantizer routines */ - private readonly float[ /* 4 */] past_qua_en = - { - -14.0f, - -14.0f, - -14.0f, - -14.0f - }; + private readonly float[ /* 4 */] past_qua_en = + { + -14.0f, + -14.0f, + -14.0f, + -14.0f + }; - /** - * Quantization of pitch and codebook gains - * - * @param code input : fixed codebook vector - * @param g_coeff input : correlation factors - * @param l_subfr input : fcb vector length - * @param gain_pit output: quantized acb gain - * @param gain_code output: quantized fcb gain - * @param tameflag input : flag set to 1 if taming is needed - * @return quantizer index - */ + /** +* Quantization of pitch and codebook gains +* +* @param code input : fixed codebook vector +* @param g_coeff input : correlation factors +* @param l_subfr input : fcb vector length +* @param gain_pit output: quantized acb gain +* @param gain_code output: quantized fcb gain +* @param tameflag input : flag set to 1 if taming is needed +* @return quantizer index +*/ - public int qua_gain( - float[] code, - float[] g_coeff, - int l_subfr, - FloatReference gain_pit, - FloatReference gain_code, - int tameflag - ) - { - var FLT_MAX_G729 = Ld8k.FLT_MAX_G729; - var GP0999 = Ld8k.GP0999; - var GPCLIP2 = Ld8k.GPCLIP2; - var NCAN1 = Ld8k.NCAN1; - var NCAN2 = Ld8k.NCAN2; - var NCODE2 = Ld8k.NCODE2; - var gbk1 = TabLd8k.gbk1; - var gbk2 = TabLd8k.gbk2; - var map1 = TabLd8k.map1; - var map2 = TabLd8k.map2; + public int qua_gain( + float[] code, + float[] g_coeff, + int l_subfr, + FloatReference gain_pit, + FloatReference gain_code, + int tameflag + ) + { + var FLT_MAX_G729 = Ld8k.FLT_MAX_G729; + var GP0999 = Ld8k.GP0999; + var GPCLIP2 = Ld8k.GPCLIP2; + var NCAN1 = Ld8k.NCAN1; + var NCAN2 = Ld8k.NCAN2; + var NCODE2 = Ld8k.NCODE2; + var gbk1 = TabLd8k.gbk1; + var gbk2 = TabLd8k.gbk2; + var map1 = TabLd8k.map1; + var map2 = TabLd8k.map2; - /* - * MA prediction is performed on the innovation energy (in dB with mean * - * removed). * - * An initial predicted gain, g_0, is first determined and the correction * - * factor alpha = gain / g_0 is quantized. * - * The pitch gain and the correction factor are vector quantized and the * - * mean-squared weighted error criterion is used in the quantizer search. * - * CS Codebook , fast pre-selection version * - */ + /* +* MA prediction is performed on the innovation energy (in dB with mean * +* removed). * +* An initial predicted gain, g_0, is first determined and the correction * +* factor alpha = gain / g_0 is quantized. * +* The pitch gain and the correction factor are vector quantized and the * +* mean-squared weighted error criterion is used in the quantizer search. * +* CS Codebook , fast pre-selection version * +*/ - int i, j, index1 = 0, index2 = 0; - int cand1, cand2; - float gcode0; - float dist, dist_min, g_pitch, g_code; - var best_gain = new float[2]; - float tmp; + int i, j, index1 = 0, index2 = 0; + int cand1, cand2; + float gcode0; + float dist, dist_min, g_pitch, g_code; + var best_gain = new float[2]; + float tmp; - /*---------------------------------------------------* - *- energy due to innovation -* - *- predicted energy -* - *- predicted codebook gain => gcode0[exp_gcode0] -* - *---------------------------------------------------*/ + /*---------------------------------------------------* +*- energy due to innovation -* +*- predicted energy -* +*- predicted codebook gain => gcode0[exp_gcode0] -* +*---------------------------------------------------*/ - gcode0 = Gainpred.gain_predict(past_qua_en, code, l_subfr); + gcode0 = Gainpred.gain_predict(past_qua_en, code, l_subfr); - /*-- pre-selection --*/ - tmp = -1.0f / (4.0f * g_coeff[0] * g_coeff[2] - g_coeff[4] * g_coeff[4]); - best_gain[0] = (2.0f * g_coeff[2] * g_coeff[1] - g_coeff[3] * g_coeff[4]) * tmp; - best_gain[1] = (2.0f * g_coeff[0] * g_coeff[3] - g_coeff[1] * g_coeff[4]) * tmp; + /*-- pre-selection --*/ + tmp = -1.0f / (4.0f * g_coeff[0] * g_coeff[2] - g_coeff[4] * g_coeff[4]); + best_gain[0] = (2.0f * g_coeff[2] * g_coeff[1] - g_coeff[3] * g_coeff[4]) * tmp; + best_gain[1] = (2.0f * g_coeff[0] * g_coeff[3] - g_coeff[1] * g_coeff[4]) * tmp; - if (tameflag == 1) - if (best_gain[0] > GPCLIP2) - best_gain[0] = GPCLIP2; - /*----------------------------------------------* - * - presearch for gain codebook - * - *----------------------------------------------*/ + if (tameflag == 1) + { + if (best_gain[0] > GPCLIP2) + { + best_gain[0] = GPCLIP2; + } + } + /*----------------------------------------------* +* - presearch for gain codebook - * +*----------------------------------------------*/ - var cand1Ref = new IntReference(); - var cand2Ref = new IntReference(); - gbk_presel(best_gain, cand1Ref, cand2Ref, gcode0); - cand1 = cand1Ref.value; - cand2 = cand2Ref.value; + var cand1Ref = new IntReference(); + var cand2Ref = new IntReference(); + gbk_presel(best_gain, cand1Ref, cand2Ref, gcode0); + cand1 = cand1Ref.value; + cand2 = cand2Ref.value; - /*-- selection --*/ - dist_min = FLT_MAX_G729; - if (tameflag == 1) - for (i = 0; i < NCAN1; i++) - for (j = 0; j < NCAN2; j++) - { - g_pitch = gbk1[cand1 + i][0] + gbk2[cand2 + j][0]; - if (g_pitch < GP0999) - { - g_code = gcode0 * (gbk1[cand1 + i][1] + gbk2[cand2 + j][1]); - dist = g_pitch * g_pitch * g_coeff[0] - + g_pitch * g_coeff[1] - + g_code * g_code * g_coeff[2] - + g_code * g_coeff[3] - + g_pitch * g_code * g_coeff[4]; - if (dist < dist_min) - { - dist_min = dist; - index1 = cand1 + i; - index2 = cand2 + j; - } - } - } - else - for (i = 0; i < NCAN1; i++) + /*-- selection --*/ + dist_min = FLT_MAX_G729; + if (tameflag == 1) + { + for (i = 0; i < NCAN1; i++) + { for (j = 0; j < NCAN2; j++) + { + g_pitch = gbk1[cand1 + i][0] + gbk2[cand2 + j][0]; + if (g_pitch < GP0999) { - g_pitch = gbk1[cand1 + i][0] + gbk2[cand2 + j][0]; g_code = gcode0 * (gbk1[cand1 + i][1] + gbk2[cand2 + j][1]); dist = g_pitch * g_pitch * g_coeff[0] + g_pitch * g_coeff[1] @@ -177,94 +164,142 @@ int tameflag index2 = cand2 + j; } } + } + } + } + else + { + for (i = 0; i < NCAN1; i++) + { + for (j = 0; j < NCAN2; j++) + { + g_pitch = gbk1[cand1 + i][0] + gbk2[cand2 + j][0]; + g_code = gcode0 * (gbk1[cand1 + i][1] + gbk2[cand2 + j][1]); + dist = g_pitch * g_pitch * g_coeff[0] + + g_pitch * g_coeff[1] + + g_code * g_code * g_coeff[2] + + g_code * g_coeff[3] + + g_pitch * g_code * g_coeff[4]; + if (dist < dist_min) + { + dist_min = dist; + index1 = cand1 + i; + index2 = cand2 + j; + } + } + } + } - gain_pit.value = gbk1[index1][0] + gbk2[index2][0]; - g_code = gbk1[index1][1] + gbk2[index2][1]; - gain_code.value = g_code * gcode0; - /*----------------------------------------------* - * update table of past quantized energies * - *----------------------------------------------*/ - Gainpred.gain_update(past_qua_en, g_code); + gain_pit.value = gbk1[index1][0] + gbk2[index2][0]; + g_code = gbk1[index1][1] + gbk2[index2][1]; + gain_code.value = g_code * gcode0; + /*----------------------------------------------* +* update table of past quantized energies * +*----------------------------------------------*/ + Gainpred.gain_update(past_qua_en, g_code); - return map1[index1] * NCODE2 + map2[index2]; - } + return map1[index1] * NCODE2 + map2[index2]; + } - /** - * Presearch for gain codebook - * - * @param best_gain input : [0] unquantized pitch gain - * [1] unquantized code gain - * @param cand1 output: index of best 1st stage vector - * @param cand2 output: index of best 2nd stage vector - * @param gcode0 input : presearch for gain codebook - */ - private void gbk_presel( - float[] best_gain, - IntReference cand1, - IntReference cand2, - float gcode0 - ) - { - var INV_COEF = Ld8k.INV_COEF; - var NCAN1 = Ld8k.NCAN1; - var NCAN2 = Ld8k.NCAN2; - var NCODE1 = Ld8k.NCODE1; - var NCODE2 = Ld8k.NCODE2; - var coef = TabLd8k.coef; - var thr1 = TabLd8k.thr1; - var thr2 = TabLd8k.thr2; + /** +* Presearch for gain codebook +* +* @param best_gain input : [0] unquantized pitch gain +* [1] unquantized code gain +* @param cand1 output: index of best 1st stage vector +* @param cand2 output: index of best 2nd stage vector +* @param gcode0 input : presearch for gain codebook +*/ + private void gbk_presel( + float[] best_gain, + IntReference cand1, + IntReference cand2, + float gcode0 + ) + { + var INV_COEF = Ld8k.INV_COEF; + var NCAN1 = Ld8k.NCAN1; + var NCAN2 = Ld8k.NCAN2; + var NCODE1 = Ld8k.NCODE1; + var NCODE2 = Ld8k.NCODE2; + var coef = TabLd8k.coef; + var thr1 = TabLd8k.thr1; + var thr2 = TabLd8k.thr2; - int _cand1 = cand1.value, _cand2 = cand2.value; + int _cand1 = cand1.value, _cand2 = cand2.value; - float x, y; + float x, y; - x = (best_gain[1] - (coef[0][0] * best_gain[0] + coef[1][1]) * gcode0) * INV_COEF; - y = (coef[1][0] * (-coef[0][1] + best_gain[0] * coef[0][0]) * gcode0 - - coef[0][0] * best_gain[1]) * INV_COEF; + x = (best_gain[1] - (coef[0][0] * best_gain[0] + coef[1][1]) * gcode0) * INV_COEF; + y = (coef[1][0] * (-coef[0][1] + best_gain[0] * coef[0][0]) * gcode0 + - coef[0][0] * best_gain[1]) * INV_COEF; - if (gcode0 > 0.0f) + if (gcode0 > 0.0f) + { + /* pre select codebook #1 */ + _cand1 = 0; + do { - /* pre select codebook #1 */ - _cand1 = 0; - do + if (y > thr1[_cand1] * gcode0) { - if (y > thr1[_cand1] * gcode0) _cand1++; - else break; + _cand1++; } - while (_cand1 < NCODE1 - NCAN1); + else + { + break; + } + } + while (_cand1 < NCODE1 - NCAN1); - /* pre select codebook #2 */ - _cand2 = 0; - do + /* pre select codebook #2 */ + _cand2 = 0; + do + { + if (x > thr2[_cand2] * gcode0) + { + _cand2++; + } + else { - if (x > thr2[_cand2] * gcode0) _cand2++; - else break; + break; } - while (_cand2 < NCODE2 - NCAN2); } - else + while (_cand2 < NCODE2 - NCAN2); + } + else + { + /* pre select codebook #1 */ + _cand1 = 0; + do { - /* pre select codebook #1 */ - _cand1 = 0; - do + if (y < thr1[_cand1] * gcode0) { - if (y < thr1[_cand1] * gcode0) _cand1++; - else break; + _cand1++; } - while (_cand1 < NCODE1 - NCAN1); - - /* pre select codebook #2 */ - _cand2 = 0; - do + else { - if (x < thr2[_cand2] * gcode0) _cand2++; - else break; + break; } - while (_cand2 < NCODE2 - NCAN2); } + while (_cand1 < NCODE1 - NCAN1); - cand1.value = _cand1; - cand2.value = _cand2; + /* pre select codebook #2 */ + _cand2 = 0; + do + { + if (x < thr2[_cand2] * gcode0) + { + _cand2++; + } + else + { + break; + } + } + while (_cand2 < NCODE2 - NCAN2); } + + cand1.value = _cand1; + cand2.value = _cand2; } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/QuaLsp.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/QuaLsp.cs index f2fa501ff2..8986a216ae 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/QuaLsp.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/QuaLsp.cs @@ -26,466 +26,504 @@ */ using System; -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal sealed class QuaLsp { - internal class QuaLsp - { - /** - * previous LSP vector(init) - */ - private static readonly float[ /* M */] FREQ_PREV_RESET = - { - 0.285599f, - 0.571199f, - 0.856798f, - 1.142397f, - 1.427997f, - 1.713596f, - 1.999195f, - 2.284795f, - 2.570394f, - 2.855993f - }; /* PI*(float)(j+1)/(float)(M+1) */ - - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /** +* previous LSP vector(init) +*/ + private static readonly float[ /* M */] FREQ_PREV_RESET = + { + 0.285599f, + 0.571199f, + 0.856798f, + 1.142397f, + 1.427997f, + 1.713596f, + 1.999195f, + 2.284795f, + 2.570394f, + 2.855993f + }; /* PI*(float)(j+1)/(float)(M+1) */ + + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : QUA_LSP.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : QUA_LSP.C +Used for the floating point version of both +G.729 main body and G.729A */ - /* static memory */ - /** - * previous LSP vector - */ - private readonly float[][] freq_prev = new float[Ld8k.MA_NP][ /* Ld8k.M */]; + /* static memory */ + /** +* previous LSP vector +*/ + private readonly float[][] freq_prev = new float[Ld8k.MA_NP][ /* Ld8k.M */]; - public QuaLsp() + public QuaLsp() + { + // need this to initialize freq_prev + for (var i = 0; i < freq_prev.Length; i++) { - // need this to initialize freq_prev - for (var i = 0; i < freq_prev.Length; i++) - freq_prev[i] = new float[Ld8k.M]; + freq_prev[i] = new float[Ld8k.M]; } + } - /** - * @param lsp (i) : Unquantized LSP - * @param lsp_q (o) : Quantized LSP - * @param ana (o) : indexes - */ + /** +* @param lsp (i) : Unquantized LSP +* @param lsp_q (o) : Quantized LSP +* @param ana (o) : indexes +*/ - public void qua_lsp( - float[] lsp, - float[] lsp_q, - int[] ana - ) - { - var M = Ld8k.M; + public void qua_lsp( + float[] lsp, + float[] lsp_q, + int[] ana + ) + { + var M = Ld8k.M; - int i; - float[] lsf = new float[M], lsf_q = new float[M]; /* domain 0.0<= lsf 0.0f) wegt[0] = 1.0f; - else wegt[0] = tmp * tmp * 10.0f + 1.0f; + tmp = flsp[1] - PI04 - 1.0f; + if (tmp > 0.0f) + { + wegt[0] = 1.0f; + } + else + { + wegt[0] = tmp * tmp * 10.0f + 1.0f; + } - for (i = 1; i < M - 1; i++) + for (i = 1; i < M - 1; i++) + { + tmp = flsp[i + 1] - flsp[i - 1] - 1.0f; + if (tmp > 0.0f) { - tmp = flsp[i + 1] - flsp[i - 1] - 1.0f; - if (tmp > 0.0f) wegt[i] = 1.0f; - else wegt[i] = tmp * tmp * 10.0f + 1.0f; + wegt[i] = 1.0f; } + else + { + wegt[i] = tmp * tmp * 10.0f + 1.0f; + } + } - tmp = PI92 - flsp[M - 2] - 1.0f; - if (tmp > 0.0f) wegt[M - 1] = 1.0f; - else wegt[M - 1] = tmp * tmp * 10.0f + 1.0f; - - wegt[4] *= CONST12; - wegt[5] *= CONST12; + tmp = PI92 - flsp[M - 2] - 1.0f; + if (tmp > 0.0f) + { + wegt[M - 1] = 1.0f; } + else + { + wegt[M - 1] = tmp * tmp * 10.0f + 1.0f; + } + + wegt[4] *= CONST12; + wegt[5] *= CONST12; } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/TabLd8k.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/TabLd8k.cs index 9b1e1fc7c7..3ac2706ab2 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/TabLd8k.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/TabLd8k.cs @@ -22,3323 +22,3322 @@ /** * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +public static class TabLd8k { - public class TabLd8k + + public static readonly float[ /* 3 */] a100 = { + 1.00000000E+00f, + 0.19330735E+01f, + -0.93589199E+00f + }; - public static float[ /* 3 */] a100 = - { - 1.00000000E+00f, - 0.19330735E+01f, - -0.93589199E+00f - }; + public static readonly float[ /* 3 */] a140 = + { + 1.00000000E+00f, + 0.19059465E+01f, + -0.91140240E+00f + }; - public static float[ /* 3 */] a140 = - { - 1.00000000E+00f, - 0.19059465E+01f, - -0.91140240E+00f - }; + /* filter coefficients (fc = 100 Hz ) */ - /* filter coefficients (fc = 100 Hz ) */ + public static readonly float[ /* 3 */] b100 = + { + 0.93980581E+00f, + -0.18795834E+01f, + 0.93980581E+00f + }; - public static float[ /* 3 */] b100 = - { - 0.93980581E+00f, - -0.18795834E+01f, - 0.93980581E+00f - }; + /* filter coefficients (fc = 140 Hz) */ - /* filter coefficients (fc = 140 Hz) */ + public static readonly float[ /* 3 */] b140 = + { + 0.92727435E+00f, + -0.18544941E+01f, + 0.92727435E+00f + }; - public static float[ /* 3 */] b140 = + public static readonly int[ /* PRM_SIZE */] bitsno = + { + 1 + Ld8k.NC0_B, /* MA + 1st stage */ + Ld8k.NC1_B * 2, /* 2nd stage */ + 8, + 1, + 13, + 4, + 7, /* first subframe */ + 5, + 13, + 4, + 7 + }; /* second subframe */ + + public static readonly float[ /* 2 */][ /* 2 */] coef = + { + new[] { - 0.92727435E+00f, - -0.18544941E+01f, - 0.92727435E+00f - }; + 31.134575f, + 1.612322f + }, - public static int[ /* PRM_SIZE */] bitsno = + new[] { - 1 + Ld8k.NC0_B, /* MA + 1st stage */ - Ld8k.NC1_B * 2, /* 2nd stage */ - 8, - 1, - 13, - 4, - 7, /* first subframe */ - 5, - 13, - 4, - 7 - }; /* second subframe */ + 0.481389f, + 0.053056f + } + }; - public static float[ /* 2 */][ /* 2 */] coef = + /*MA prediction coef. */ + + public static readonly float[ /* MODE */][ /* MA_NP */][ /* M */] fg = + { + new[] { new[] { - 31.134575f, - 1.612322f + 0.2570f, + 0.2780f, + 0.2800f, + 0.2736f, + 0.2757f, + 0.2764f, + 0.2675f, + 0.2678f, + 0.2779f, + 0.2647f }, new[] { - 0.481389f, - 0.053056f - } - }; - - /*MA prediction coef. */ + 0.2142f, + 0.2194f, + 0.2331f, + 0.2230f, + 0.2272f, + 0.2252f, + 0.2148f, + 0.2123f, + 0.2115f, + 0.2096f + }, - public static float[ /* MODE */][ /* MA_NP */][ /* M */] fg = - { new[] { - new[] - { - 0.2570f, - 0.2780f, - 0.2800f, - 0.2736f, - 0.2757f, - 0.2764f, - 0.2675f, - 0.2678f, - 0.2779f, - 0.2647f - }, - - new[] - { - 0.2142f, - 0.2194f, - 0.2331f, - 0.2230f, - 0.2272f, - 0.2252f, - 0.2148f, - 0.2123f, - 0.2115f, - 0.2096f - }, - - new[] - { - 0.1670f, - 0.1523f, - 0.1567f, - 0.1580f, - 0.1601f, - 0.1569f, - 0.1589f, - 0.1555f, - 0.1474f, - 0.1571f - }, - - new[] - { - 0.1238f, - 0.0925f, - 0.0798f, - 0.0923f, - 0.0890f, - 0.0828f, - 0.1010f, - 0.0988f, - 0.0872f, - 0.1060f - } + 0.1670f, + 0.1523f, + 0.1567f, + 0.1580f, + 0.1601f, + 0.1569f, + 0.1589f, + 0.1555f, + 0.1474f, + 0.1571f }, new[] { - new[] - { - 0.2360f, - 0.2405f, - 0.2499f, - 0.2495f, - 0.2517f, - 0.2591f, - 0.2636f, - 0.2625f, - 0.2551f, - 0.2310f - }, - - new[] - { - 0.1285f, - 0.0925f, - 0.0779f, - 0.1060f, - 0.1183f, - 0.1176f, - 0.1277f, - 0.1268f, - 0.1193f, - 0.1211f - }, - - new[] - { - 0.0981f, - 0.0589f, - 0.0401f, - 0.0654f, - 0.0761f, - 0.0728f, - 0.0841f, - 0.0826f, - 0.0776f, - 0.0891f - }, - - new[] - { - 0.0923f, - 0.0486f, - 0.0287f, - 0.0498f, - 0.0526f, - 0.0482f, - 0.0621f, - 0.0636f, - 0.0584f, - 0.0794f - } + 0.1238f, + 0.0925f, + 0.0798f, + 0.0923f, + 0.0890f, + 0.0828f, + 0.1010f, + 0.0988f, + 0.0872f, + 0.1060f } + }, - }; - - /*present MA prediction coef.*/ - - public static float[ /* MODE */][ /* M */] fg_sum = + new[] { new[] { - 0.2380000054836f, - 0.2578000128269f, - 0.2504000067711f, - 0.2531000375748f, - 0.2480000108480f, - 0.2587000429630f, - 0.2577999532223f, - 0.2656000256538f, - 0.2760000228882f, - 0.2625999450684f + 0.2360f, + 0.2405f, + 0.2499f, + 0.2495f, + 0.2517f, + 0.2591f, + 0.2636f, + 0.2625f, + 0.2551f, + 0.2310f }, new[] { - 0.4451000094414f, - 0.5595000386238f, - 0.6034000515938f, - 0.5292999744415f, - 0.5012999176979f, - 0.5023000240326f, - 0.4625000357628f, - 0.4645000100136f, - 0.4895999729633f, - 0.4793999791145f - } - }; - - /*inverse coef. */ + 0.1285f, + 0.0925f, + 0.0779f, + 0.1060f, + 0.1183f, + 0.1176f, + 0.1277f, + 0.1268f, + 0.1193f, + 0.1211f + }, - public static float[ /* MODE */][ /* M */] fg_sum_inv = - { new[] { - 4.2016806602478f, - 3.8789758682251f, - 3.9936101436615f, - 3.9510068893433f, - 4.0322580337524f, - 3.8654806613922f, - 3.8789765834808f, - 3.7650599479675f, - 3.6231880187988f, - 3.8080739974976f + 0.0981f, + 0.0589f, + 0.0401f, + 0.0654f, + 0.0761f, + 0.0728f, + 0.0841f, + 0.0826f, + 0.0776f, + 0.0891f }, new[] { - 2.2466859817505f, - 1.7873100042343f, - 1.6572753190994f, - 1.8892878293991f, - 1.9948137998581f, - 1.9908419847488f, - 2.1621620655060f, - 2.1528525352478f, - 2.0424838066101f, - 2.0859408378601f + 0.0923f, + 0.0486f, + 0.0287f, + 0.0498f, + 0.0526f, + 0.0482f, + 0.0621f, + 0.0636f, + 0.0584f, + 0.0794f } - }; + } + + }; + + /*present MA prediction coef.*/ - public static float[ /* NCODE1 */][ /* 2 */] gbk1 = + public static readonly float[ /* MODE */][ /* M */] fg_sum = + { + new[] { - new[] - { - 0.000010f, - 0.185084f - }, + 0.2380000054836f, + 0.2578000128269f, + 0.2504000067711f, + 0.2531000375748f, + 0.2480000108480f, + 0.2587000429630f, + 0.2577999532223f, + 0.2656000256538f, + 0.2760000228882f, + 0.2625999450684f + }, + + new[] + { + 0.4451000094414f, + 0.5595000386238f, + 0.6034000515938f, + 0.5292999744415f, + 0.5012999176979f, + 0.5023000240326f, + 0.4625000357628f, + 0.4645000100136f, + 0.4895999729633f, + 0.4793999791145f + } + }; + + /*inverse coef. */ + + public static readonly float[ /* MODE */][ /* M */] fg_sum_inv = + { + new[] + { + 4.2016806602478f, + 3.8789758682251f, + 3.9936101436615f, + 3.9510068893433f, + 4.0322580337524f, + 3.8654806613922f, + 3.8789765834808f, + 3.7650599479675f, + 3.6231880187988f, + 3.8080739974976f + }, + + new[] + { + 2.2466859817505f, + 1.7873100042343f, + 1.6572753190994f, + 1.8892878293991f, + 1.9948137998581f, + 1.9908419847488f, + 2.1621620655060f, + 2.1528525352478f, + 2.0424838066101f, + 2.0859408378601f + } + }; + + public static readonly float[ /* NCODE1 */][ /* 2 */] gbk1 = + { + new[] + { + 0.000010f, + 0.185084f + }, - new[] - { - 0.094719f, - 0.296035f - }, + new[] + { + 0.094719f, + 0.296035f + }, - new[] - { - 0.111779f, - 0.613122f - }, + new[] + { + 0.111779f, + 0.613122f + }, - new[] - { - 0.003516f, - 0.659780f - }, + new[] + { + 0.003516f, + 0.659780f + }, - new[] - { - 0.117258f, - 1.134277f - }, + new[] + { + 0.117258f, + 1.134277f + }, - new[] - { - 0.197901f, - 1.214512f - }, + new[] + { + 0.197901f, + 1.214512f + }, - new[] - { - 0.021772f, - 1.801288f - }, + new[] + { + 0.021772f, + 1.801288f + }, - new[] - { - 0.163457f, - 3.315700f - } - }; + new[] + { + 0.163457f, + 3.315700f + } + }; - public static float[ /* NCODE2*/][ /* 2 */] gbk2 = + public static readonly float[ /* NCODE2*/][ /* 2 */] gbk2 = + { + new[] { - new[] - { - 0.050466f, - 0.244769f - }, + 0.050466f, + 0.244769f + }, - new[] - { - 0.121711f, - 0.000010f - }, + new[] + { + 0.121711f, + 0.000010f + }, - new[] - { - 0.313871f, - 0.072357f - }, + new[] + { + 0.313871f, + 0.072357f + }, - new[] - { - 0.375977f, - 0.292399f - }, + new[] + { + 0.375977f, + 0.292399f + }, - new[] - { - 0.493870f, - 0.593410f - }, + new[] + { + 0.493870f, + 0.593410f + }, - new[] - { - 0.556641f, - 0.064087f - }, + new[] + { + 0.556641f, + 0.064087f + }, - new[] - { - 0.645363f, - 0.362118f - }, + new[] + { + 0.645363f, + 0.362118f + }, - new[] - { - 0.706138f, - 0.146110f - }, + new[] + { + 0.706138f, + 0.146110f + }, - new[] - { - 0.809357f, - 0.397579f - }, + new[] + { + 0.809357f, + 0.397579f + }, - new[] - { - 0.866379f, - 0.199087f - }, + new[] + { + 0.866379f, + 0.199087f + }, - new[] - { - 0.923602f, - 0.599938f - }, + new[] + { + 0.923602f, + 0.599938f + }, - new[] - { - 0.925376f, - 1.742757f - }, + new[] + { + 0.925376f, + 1.742757f + }, - new[] - { - 0.942028f, - 0.029027f - }, + new[] + { + 0.942028f, + 0.029027f + }, - new[] - { - 0.983459f, - 0.414166f - }, + new[] + { + 0.983459f, + 0.414166f + }, - new[] - { - 1.055892f, - 0.227186f - }, + new[] + { + 1.055892f, + 0.227186f + }, - new[] - { - 1.158039f, - 0.724592f - } - }; - - public static float[ /* GRID_POINTS+1 */] grid = - { - 0.9997559f, - 0.9986295f, - 0.9945219f, - 0.9876884f, - 0.9781476f, - 0.9659258f, - 0.9510565f, - 0.9335804f, - 0.9135454f, - 0.8910065f, - 0.8660254f, - 0.8386706f, - 0.8090170f, - 0.7771460f, - 0.7431448f, - 0.7071068f, - 0.6691306f, - 0.6293204f, - 0.5877852f, - 0.5446391f, - 0.5000000f, - 0.4539905f, - 0.4067366f, - 0.3583679f, - 0.3090170f, - 0.2588190f, - 0.2079117f, - 0.1564345f, - 0.1045285f, - 0.0523360f, - 0.0000000f, - -0.0523360f, - -0.1045285f, - -0.1564345f, - -0.2079117f, - -0.2588190f, - -0.3090170f, - -0.3583679f, - -0.4067366f, - -0.4539905f, - -0.5000000f, - -0.5446391f, - -0.5877852f, - -0.6293204f, - -0.6691306f, - -0.7071068f, - -0.7431448f, - -0.7771460f, - -0.8090170f, - -0.8386706f, - -0.8660254f, - -0.8910065f, - -0.9135454f, - -0.9335804f, - -0.9510565f, - -0.9659258f, - -0.9781476f, - -0.9876884f, - -0.9945219f, - -0.9986295f, - -0.9997559f - }; - - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + new[] + { + 1.158039f, + 0.724592f + } + }; + + public static readonly float[ /* GRID_POINTS+1 */] grid = + { + 0.9997559f, + 0.9986295f, + 0.9945219f, + 0.9876884f, + 0.9781476f, + 0.9659258f, + 0.9510565f, + 0.9335804f, + 0.9135454f, + 0.8910065f, + 0.8660254f, + 0.8386706f, + 0.8090170f, + 0.7771460f, + 0.7431448f, + 0.7071068f, + 0.6691306f, + 0.6293204f, + 0.5877852f, + 0.5446391f, + 0.5000000f, + 0.4539905f, + 0.4067366f, + 0.3583679f, + 0.3090170f, + 0.2588190f, + 0.2079117f, + 0.1564345f, + 0.1045285f, + 0.0523360f, + 0.0000000f, + -0.0523360f, + -0.1045285f, + -0.1564345f, + -0.2079117f, + -0.2588190f, + -0.3090170f, + -0.3583679f, + -0.4067366f, + -0.4539905f, + -0.5000000f, + -0.5446391f, + -0.5877852f, + -0.6293204f, + -0.6691306f, + -0.7071068f, + -0.7431448f, + -0.7771460f, + -0.8090170f, + -0.8386706f, + -0.8660254f, + -0.8910065f, + -0.9135454f, + -0.9335804f, + -0.9510565f, + -0.9659258f, + -0.9781476f, + -0.9876884f, + -0.9945219f, + -0.9986295f, + -0.9997559f + }; + + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : TAB_LD8K.H - Used for the floating point version of G.729 main body - (not for G.729A) + /* +File : TAB_LD8K.H +Used for the floating point version of G.729 main body +(not for G.729A) */ - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. u---------------------------------------------------------------------- */ - /* - File : TAB_LD8K.C - Used for the floating point version of G.729 main body - (not for G.729A) + /* +File : TAB_LD8K.C +Used for the floating point version of G.729 main body +(not for G.729A) */ - public static float[ /* L_WINDOW */] hamwindow = - { - 0.08000000f, - 0.08005703f, - 0.08022812f, - 0.08051321f, - 0.08091225f, - 0.08142514f, - 0.08205172f, - 0.08279188f, - 0.08364540f, - 0.08461212f, - 0.08569173f, - 0.08688401f, - 0.08818865f, - 0.08960532f, - 0.09113365f, - 0.09277334f, - 0.09452391f, - 0.09638494f, - 0.09835598f, - 0.10043652f, - 0.10262608f, - 0.10492408f, - 0.10732999f, - 0.10984316f, - 0.11246302f, - 0.11518890f, - 0.11802010f, - 0.12095598f, - 0.12399574f, - 0.12713866f, - 0.13038395f, - 0.13373083f, - 0.13717847f, - 0.14072597f, - 0.14437246f, - 0.14811710f, - 0.15195890f, - 0.15589692f, - 0.15993017f, - 0.16405767f, - 0.16827843f, - 0.17259133f, - 0.17699537f, - 0.18148938f, - 0.18607232f, - 0.19074300f, - 0.19550033f, - 0.20034306f, - 0.20527001f, - 0.21027996f, - 0.21537170f, - 0.22054392f, - 0.22579536f, - 0.23112471f, - 0.23653066f, - 0.24201185f, - 0.24756692f, - 0.25319457f, - 0.25889328f, - 0.26466170f, - 0.27049842f, - 0.27640197f, - 0.28237087f, - 0.28840363f, - 0.29449883f, - 0.30065489f, - 0.30687031f, - 0.31314352f, - 0.31947297f, - 0.32585713f, - 0.33229437f, - 0.33878314f, - 0.34532180f, - 0.35190874f, - 0.35854232f, - 0.36522087f, - 0.37194279f, - 0.37870640f, - 0.38550997f, - 0.39235184f, - 0.39923036f, - 0.40614375f, - 0.41309035f, - 0.42006844f, - 0.42707625f, - 0.43411207f, - 0.44117412f, - 0.44826069f, - 0.45537004f, - 0.46250033f, - 0.46964988f, - 0.47681686f, - 0.48399949f, - 0.49119604f, - 0.49840465f, - 0.50562358f, - 0.51285106f, - 0.52008528f, - 0.52732444f, - 0.53456670f, - 0.54181033f, - 0.54905349f, - 0.55629444f, - 0.56353134f, - 0.57076240f, - 0.57798582f, - 0.58519983f, - 0.59240264f, - 0.59959245f, - 0.60676748f, - 0.61392599f, - 0.62106609f, - 0.62818617f, - 0.63528436f, - 0.64235890f, - 0.64940804f, - 0.65643007f, - 0.66342324f, - 0.67038584f, - 0.67731601f, - 0.68421221f, - 0.69107264f, - 0.69789559f, - 0.70467937f, - 0.71142232f, - 0.71812278f, - 0.72477907f, - 0.73138952f, - 0.73795253f, - 0.74446648f, - 0.75092971f, - 0.75734061f, - 0.76369762f, - 0.76999915f, - 0.77624369f, - 0.78242958f, - 0.78855544f, - 0.79461962f, - 0.80062068f, - 0.80655706f, - 0.81242740f, - 0.81823015f, - 0.82396388f, - 0.82962728f, - 0.83521879f, - 0.84073710f, - 0.84618086f, - 0.85154873f, - 0.85683930f, - 0.86205131f, - 0.86718345f, - 0.87223446f, - 0.87720311f, - 0.88208807f, - 0.88688827f, - 0.89160240f, - 0.89622939f, - 0.90076804f, - 0.90521723f, - 0.90957582f, - 0.91384280f, - 0.91801709f, - 0.92209762f, - 0.92608339f, - 0.92997342f, - 0.93376678f, - 0.93746245f, - 0.94105959f, - 0.94455731f, - 0.94795465f, - 0.95125085f, - 0.95444512f, - 0.95753652f, - 0.96052444f, - 0.96340811f, - 0.96618676f, - 0.96885973f, - 0.97142631f, - 0.97388595f, - 0.97623801f, - 0.97848189f, - 0.98061699f, - 0.98264289f, - 0.98455900f, - 0.98636484f, - 0.98806006f, - 0.98964417f, - 0.99111670f, - 0.99247742f, - 0.99372596f, - 0.99486196f, - 0.99588519f, - 0.99679530f, - 0.99759221f, - 0.99827564f, - 0.99884540f, - 0.99930143f, - 0.99964350f, - 0.99987161f, - 0.99998569f, - 1.00000000f, - 0.99921930f, - 0.99687845f, - 0.99298108f, - 0.98753333f, - 0.98054361f, - 0.97202289f, - 0.96198452f, - 0.95044410f, - 0.93741965f, - 0.92293155f, - 0.90700239f, - 0.88965708f, - 0.87092263f, - 0.85082841f, - 0.82940567f, - 0.80668795f, - 0.78271067f, - 0.75751126f, - 0.73112911f, - 0.70360541f, - 0.67498308f, - 0.64530689f, - 0.61462307f, - 0.58297962f, - 0.55042595f, - 0.51701277f, - 0.48279238f, - 0.44781810f, - 0.41214463f, - 0.37582767f, - 0.33892387f, - 0.30149087f, - 0.26358715f, - 0.22527184f, - 0.18660481f, - 0.14764643f, - 0.10845750f, - 0.06909923f, - 0.02963307f - }; - - public static int[ /* NCODE1 */] imap1 = - { - 5, - 1, - 7, - 4, - 2, - 0, - 6, - 3 - }; - - public static int[ /* NCODE2 */] imap2 = - { - 2, - 14, - 3, - 13, - 0, - 15, - 1, - 12, - 6, - 10, - 7, - 9, - 4, - 11, - 5, - 8 - }; - - public static float[ /* FIR_SIZE_ANA */] inter_3 = - { - 0.900839f, - 0.760084f, - 0.424082f, - 0.084078f, - -0.105570f, - -0.121120f, - -0.047624f, - 0.016285f, - 0.031217f, - 0.015738f, - 0.000000f, - -0.005925f, - 0.000000f - }; - - public static float[ /* FIR_SIZE_SYN */] inter_3l = - { - 0.898517f, - 0.769271f, - 0.448635f, - 0.095915f, - -0.134333f, - -0.178528f, - -0.084919f, - 0.036952f, - 0.095533f, - 0.068936f, - -0.000000f, - -0.050404f, - -0.050835f, - -0.014169f, - 0.023083f, - 0.033543f, - 0.016774f, - -0.007466f, - -0.019340f, - -0.013755f, - 0.000000f, - 0.009400f, - 0.009029f, - 0.002381f, - -0.003658f, - -0.005027f, - -0.002405f, - 0.001050f, - 0.002780f, - 0.002145f, - 0.000000f - }; - - /*First Stage Codebook */ - public static float[ /* NC0 */][ /* M */] lspcb1 = + public static readonly float[ /* L_WINDOW */] hamwindow = + { + 0.08000000f, + 0.08005703f, + 0.08022812f, + 0.08051321f, + 0.08091225f, + 0.08142514f, + 0.08205172f, + 0.08279188f, + 0.08364540f, + 0.08461212f, + 0.08569173f, + 0.08688401f, + 0.08818865f, + 0.08960532f, + 0.09113365f, + 0.09277334f, + 0.09452391f, + 0.09638494f, + 0.09835598f, + 0.10043652f, + 0.10262608f, + 0.10492408f, + 0.10732999f, + 0.10984316f, + 0.11246302f, + 0.11518890f, + 0.11802010f, + 0.12095598f, + 0.12399574f, + 0.12713866f, + 0.13038395f, + 0.13373083f, + 0.13717847f, + 0.14072597f, + 0.14437246f, + 0.14811710f, + 0.15195890f, + 0.15589692f, + 0.15993017f, + 0.16405767f, + 0.16827843f, + 0.17259133f, + 0.17699537f, + 0.18148938f, + 0.18607232f, + 0.19074300f, + 0.19550033f, + 0.20034306f, + 0.20527001f, + 0.21027996f, + 0.21537170f, + 0.22054392f, + 0.22579536f, + 0.23112471f, + 0.23653066f, + 0.24201185f, + 0.24756692f, + 0.25319457f, + 0.25889328f, + 0.26466170f, + 0.27049842f, + 0.27640197f, + 0.28237087f, + 0.28840363f, + 0.29449883f, + 0.30065489f, + 0.30687031f, + 0.31314352f, + 0.31947297f, + 0.32585713f, + 0.33229437f, + 0.33878314f, + 0.34532180f, + 0.35190874f, + 0.35854232f, + 0.36522087f, + 0.37194279f, + 0.37870640f, + 0.38550997f, + 0.39235184f, + 0.39923036f, + 0.40614375f, + 0.41309035f, + 0.42006844f, + 0.42707625f, + 0.43411207f, + 0.44117412f, + 0.44826069f, + 0.45537004f, + 0.46250033f, + 0.46964988f, + 0.47681686f, + 0.48399949f, + 0.49119604f, + 0.49840465f, + 0.50562358f, + 0.51285106f, + 0.52008528f, + 0.52732444f, + 0.53456670f, + 0.54181033f, + 0.54905349f, + 0.55629444f, + 0.56353134f, + 0.57076240f, + 0.57798582f, + 0.58519983f, + 0.59240264f, + 0.59959245f, + 0.60676748f, + 0.61392599f, + 0.62106609f, + 0.62818617f, + 0.63528436f, + 0.64235890f, + 0.64940804f, + 0.65643007f, + 0.66342324f, + 0.67038584f, + 0.67731601f, + 0.68421221f, + 0.69107264f, + 0.69789559f, + 0.70467937f, + 0.71142232f, + 0.71812278f, + 0.72477907f, + 0.73138952f, + 0.73795253f, + 0.74446648f, + 0.75092971f, + 0.75734061f, + 0.76369762f, + 0.76999915f, + 0.77624369f, + 0.78242958f, + 0.78855544f, + 0.79461962f, + 0.80062068f, + 0.80655706f, + 0.81242740f, + 0.81823015f, + 0.82396388f, + 0.82962728f, + 0.83521879f, + 0.84073710f, + 0.84618086f, + 0.85154873f, + 0.85683930f, + 0.86205131f, + 0.86718345f, + 0.87223446f, + 0.87720311f, + 0.88208807f, + 0.88688827f, + 0.89160240f, + 0.89622939f, + 0.90076804f, + 0.90521723f, + 0.90957582f, + 0.91384280f, + 0.91801709f, + 0.92209762f, + 0.92608339f, + 0.92997342f, + 0.93376678f, + 0.93746245f, + 0.94105959f, + 0.94455731f, + 0.94795465f, + 0.95125085f, + 0.95444512f, + 0.95753652f, + 0.96052444f, + 0.96340811f, + 0.96618676f, + 0.96885973f, + 0.97142631f, + 0.97388595f, + 0.97623801f, + 0.97848189f, + 0.98061699f, + 0.98264289f, + 0.98455900f, + 0.98636484f, + 0.98806006f, + 0.98964417f, + 0.99111670f, + 0.99247742f, + 0.99372596f, + 0.99486196f, + 0.99588519f, + 0.99679530f, + 0.99759221f, + 0.99827564f, + 0.99884540f, + 0.99930143f, + 0.99964350f, + 0.99987161f, + 0.99998569f, + 1.00000000f, + 0.99921930f, + 0.99687845f, + 0.99298108f, + 0.98753333f, + 0.98054361f, + 0.97202289f, + 0.96198452f, + 0.95044410f, + 0.93741965f, + 0.92293155f, + 0.90700239f, + 0.88965708f, + 0.87092263f, + 0.85082841f, + 0.82940567f, + 0.80668795f, + 0.78271067f, + 0.75751126f, + 0.73112911f, + 0.70360541f, + 0.67498308f, + 0.64530689f, + 0.61462307f, + 0.58297962f, + 0.55042595f, + 0.51701277f, + 0.48279238f, + 0.44781810f, + 0.41214463f, + 0.37582767f, + 0.33892387f, + 0.30149087f, + 0.26358715f, + 0.22527184f, + 0.18660481f, + 0.14764643f, + 0.10845750f, + 0.06909923f, + 0.02963307f + }; + + public static readonly int[ /* NCODE1 */] imap1 = + { + 5, + 1, + 7, + 4, + 2, + 0, + 6, + 3 + }; + + public static readonly int[ /* NCODE2 */] imap2 = + { + 2, + 14, + 3, + 13, + 0, + 15, + 1, + 12, + 6, + 10, + 7, + 9, + 4, + 11, + 5, + 8 + }; + + public static readonly float[ /* FIR_SIZE_ANA */] inter_3 = + { + 0.900839f, + 0.760084f, + 0.424082f, + 0.084078f, + -0.105570f, + -0.121120f, + -0.047624f, + 0.016285f, + 0.031217f, + 0.015738f, + 0.000000f, + -0.005925f, + 0.000000f + }; + + public static readonly float[ /* FIR_SIZE_SYN */] inter_3l = + { + 0.898517f, + 0.769271f, + 0.448635f, + 0.095915f, + -0.134333f, + -0.178528f, + -0.084919f, + 0.036952f, + 0.095533f, + 0.068936f, + -0.000000f, + -0.050404f, + -0.050835f, + -0.014169f, + 0.023083f, + 0.033543f, + 0.016774f, + -0.007466f, + -0.019340f, + -0.013755f, + 0.000000f, + 0.009400f, + 0.009029f, + 0.002381f, + -0.003658f, + -0.005027f, + -0.002405f, + 0.001050f, + 0.002780f, + 0.002145f, + 0.000000f + }; + + /*First Stage Codebook */ + public static readonly float[ /* NC0 */][ /* M */] lspcb1 = + { + new[] { - new[] - { - 0.1814f, - 0.2647f, - 0.4580f, - 1.1077f, - 1.4813f, - 1.7022f, - 2.1953f, - 2.3405f, - 2.5867f, - 2.6636f - }, - - new[] - { - 0.2113f, - 0.3223f, - 0.4212f, - 0.5946f, - 0.7479f, - 0.9615f, - 1.9097f, - 2.1750f, - 2.4773f, - 2.6737f - }, - - new[] - { - 0.1915f, - 0.2755f, - 0.3770f, - 0.5950f, - 1.3505f, - 1.6349f, - 2.2348f, - 2.3552f, - 2.5768f, - 2.6540f - }, - - new[] - { - 0.2116f, - 0.3067f, - 0.4099f, - 0.5748f, - 0.8518f, - 1.2569f, - 2.0782f, - 2.1920f, - 2.3371f, - 2.4842f - }, - - new[] - { - 0.2129f, - 0.2974f, - 0.4039f, - 1.0659f, - 1.2735f, - 1.4658f, - 1.9061f, - 2.0312f, - 2.6074f, - 2.6750f - }, - - new[] - { - 0.2181f, - 0.2893f, - 0.4117f, - 0.5519f, - 0.8295f, - 1.5825f, - 2.1575f, - 2.3179f, - 2.5458f, - 2.6417f - }, - - new[] - { - 0.1991f, - 0.2971f, - 0.4104f, - 0.7725f, - 1.3073f, - 1.4665f, - 1.6208f, - 1.6973f, - 2.3732f, - 2.5743f - }, - - new[] - { - 0.1818f, - 0.2886f, - 0.4018f, - 0.7630f, - 1.1264f, - 1.2699f, - 1.6899f, - 1.8650f, - 2.1633f, - 2.6186f - }, - - new[] - { - 0.2282f, - 0.3093f, - 0.4243f, - 0.5329f, - 1.1173f, - 1.7717f, - 1.9420f, - 2.0780f, - 2.5160f, - 2.6137f - }, - - new[] - { - 0.2528f, - 0.3693f, - 0.5290f, - 0.7146f, - 0.9528f, - 1.1269f, - 1.2936f, - 1.9589f, - 2.4548f, - 2.6653f - }, - - new[] - { - 0.2332f, - 0.3263f, - 0.4174f, - 0.5202f, - 1.3633f, - 1.8447f, - 2.0236f, - 2.1474f, - 2.3572f, - 2.4738f - }, - - new[] - { - 0.1393f, - 0.2216f, - 0.3204f, - 0.5644f, - 0.7929f, - 1.1705f, - 1.7051f, - 2.0054f, - 2.3623f, - 2.5985f - }, - - new[] - { - 0.2677f, - 0.3871f, - 0.5746f, - 0.7091f, - 1.3311f, - 1.5260f, - 1.7288f, - 1.9122f, - 2.5787f, - 2.6598f - }, - - new[] - { - 0.1570f, - 0.2328f, - 0.3111f, - 0.4216f, - 1.1688f, - 1.4605f, - 1.9505f, - 2.1173f, - 2.4038f, - 2.7460f - }, - - new[] - { - 0.2346f, - 0.3321f, - 0.5621f, - 0.8160f, - 1.4042f, - 1.5860f, - 1.7518f, - 1.8631f, - 2.0749f, - 2.5380f - }, - - new[] - { - 0.2505f, - 0.3368f, - 0.4758f, - 0.6405f, - 0.8104f, - 1.2533f, - 1.9329f, - 2.0526f, - 2.2155f, - 2.6459f - }, - - new[] - { - 0.2196f, - 0.3049f, - 0.6857f, - 1.3976f, - 1.6100f, - 1.7958f, - 2.0813f, - 2.2211f, - 2.4789f, - 2.5857f - }, - - new[] - { - 0.1232f, - 0.2011f, - 0.3527f, - 0.6969f, - 1.1647f, - 1.5081f, - 1.8593f, - 2.2576f, - 2.5594f, - 2.6896f - }, - - new[] - { - 0.3682f, - 0.4632f, - 0.6600f, - 0.9118f, - 1.5245f, - 1.7071f, - 1.8712f, - 1.9939f, - 2.4356f, - 2.5380f - }, - - new[] - { - 0.2690f, - 0.3711f, - 0.4635f, - 0.6644f, - 1.4633f, - 1.6495f, - 1.8227f, - 1.9983f, - 2.1797f, - 2.2954f - }, - - new[] - { - 0.3555f, - 0.5240f, - 0.9751f, - 1.1685f, - 1.4114f, - 1.6168f, - 1.7769f, - 2.0178f, - 2.4420f, - 2.5724f - }, - - new[] - { - 0.3493f, - 0.4404f, - 0.7231f, - 0.8587f, - 1.1272f, - 1.4715f, - 1.6760f, - 2.2042f, - 2.4735f, - 2.5604f - }, - - new[] - { - 0.3747f, - 0.5263f, - 0.7284f, - 0.8994f, - 1.4017f, - 1.5502f, - 1.7468f, - 1.9816f, - 2.2380f, - 2.3404f - }, - - new[] - { - 0.2972f, - 0.4470f, - 0.5941f, - 0.7078f, - 1.2675f, - 1.4310f, - 1.5930f, - 1.9126f, - 2.3026f, - 2.4208f - }, - - new[] - { - 0.2467f, - 0.3180f, - 0.4712f, - 1.1281f, - 1.6206f, - 1.7876f, - 1.9544f, - 2.0873f, - 2.3521f, - 2.4721f - }, - - new[] - { - 0.2292f, - 0.3430f, - 0.4383f, - 0.5747f, - 1.3497f, - 1.5187f, - 1.9070f, - 2.0958f, - 2.2902f, - 2.4301f - }, - - new[] - { - 0.2573f, - 0.3508f, - 0.4484f, - 0.7079f, - 1.6577f, - 1.7929f, - 1.9456f, - 2.0847f, - 2.3060f, - 2.4208f - }, - - new[] - { - 0.1968f, - 0.2789f, - 0.3594f, - 0.4361f, - 1.0034f, - 1.7040f, - 1.9439f, - 2.1044f, - 2.2696f, - 2.4558f - }, - - new[] - { - 0.2955f, - 0.3853f, - 0.7986f, - 1.2470f, - 1.4723f, - 1.6522f, - 1.8684f, - 2.0084f, - 2.2849f, - 2.4268f - }, - - new[] - { - 0.2036f, - 0.3189f, - 0.4314f, - 0.6393f, - 1.2834f, - 1.4278f, - 1.5796f, - 2.0506f, - 2.2044f, - 2.3656f - }, - - new[] - { - 0.2916f, - 0.3684f, - 0.5907f, - 1.1394f, - 1.3933f, - 1.5540f, - 1.8341f, - 1.9835f, - 2.1301f, - 2.2800f - }, - - new[] - { - 0.2289f, - 0.3402f, - 0.5166f, - 0.7716f, - 1.0614f, - 1.2389f, - 1.4386f, - 2.0769f, - 2.2715f, - 2.4366f - }, - - new[] - { - 0.0829f, - 0.1723f, - 0.5682f, - 0.9773f, - 1.3973f, - 1.6174f, - 1.9242f, - 2.2128f, - 2.4855f, - 2.6327f - }, - - new[] - { - 0.2244f, - 0.3169f, - 0.4368f, - 0.5625f, - 0.6897f, - 1.3763f, - 1.7524f, - 1.9393f, - 2.5121f, - 2.6556f - }, - - new[] - { - 0.1591f, - 0.2387f, - 0.2924f, - 0.4056f, - 1.4677f, - 1.6802f, - 1.9389f, - 2.2067f, - 2.4635f, - 2.5919f - }, - - new[] - { - 0.1756f, - 0.2566f, - 0.3251f, - 0.4227f, - 1.0167f, - 1.2649f, - 1.6801f, - 2.1055f, - 2.4088f, - 2.7276f - }, - - new[] - { - 0.1050f, - 0.2325f, - 0.7445f, - 0.9491f, - 1.1982f, - 1.4658f, - 1.8093f, - 2.0397f, - 2.4155f, - 2.5797f - }, - - new[] - { - 0.2043f, - 0.3324f, - 0.4522f, - 0.7477f, - 0.9361f, - 1.1533f, - 1.6703f, - 1.7631f, - 2.5071f, - 2.6528f - }, - - new[] - { - 0.1522f, - 0.2258f, - 0.3543f, - 0.5504f, - 0.8815f, - 1.5516f, - 1.8110f, - 1.9915f, - 2.3603f, - 2.7735f - }, - - new[] - { - 0.1862f, - 0.2759f, - 0.4715f, - 0.6908f, - 0.8963f, - 1.4341f, - 1.6322f, - 1.7630f, - 2.2027f, - 2.6043f - }, - - new[] - { - 0.1460f, - 0.2254f, - 0.3790f, - 0.8622f, - 1.3394f, - 1.5754f, - 1.8084f, - 2.0798f, - 2.4319f, - 2.7632f - }, - - new[] - { - 0.2621f, - 0.3792f, - 0.5463f, - 0.7948f, - 1.0043f, - 1.1921f, - 1.3409f, - 1.4845f, - 2.3159f, - 2.6002f - }, - - new[] - { - 0.1935f, - 0.2937f, - 0.3656f, - 0.4927f, - 1.4015f, - 1.6086f, - 1.7724f, - 1.8837f, - 2.4374f, - 2.5971f - }, - - new[] - { - 0.2171f, - 0.3282f, - 0.4412f, - 0.5713f, - 1.1554f, - 1.3506f, - 1.5227f, - 1.9923f, - 2.4100f, - 2.5391f - }, - - new[] - { - 0.2274f, - 0.3157f, - 0.4263f, - 0.8202f, - 1.4293f, - 1.5884f, - 1.7535f, - 1.9688f, - 2.3939f, - 2.4934f - }, - - new[] - { - 0.1704f, - 0.2633f, - 0.3259f, - 0.4134f, - 1.2948f, - 1.4802f, - 1.6619f, - 2.0393f, - 2.3165f, - 2.6083f - }, - - new[] - { - 0.1763f, - 0.2585f, - 0.4012f, - 0.7609f, - 1.1503f, - 1.5847f, - 1.8309f, - 1.9352f, - 2.0982f, - 2.6681f - }, - - new[] - { - 0.2447f, - 0.3535f, - 0.4618f, - 0.5979f, - 0.7530f, - 0.8908f, - 1.5393f, - 2.0075f, - 2.3557f, - 2.6203f - }, - - new[] - { - 0.1826f, - 0.3496f, - 0.7764f, - 0.9888f, - 1.3915f, - 1.7421f, - 1.9412f, - 2.1620f, - 2.4999f, - 2.6931f - }, - - new[] - { - 0.3033f, - 0.3802f, - 0.6981f, - 0.8664f, - 1.0254f, - 1.5401f, - 1.7180f, - 1.8124f, - 2.5068f, - 2.6119f - }, - - new[] - { - 0.2960f, - 0.4001f, - 0.6465f, - 0.7672f, - 1.3782f, - 1.5751f, - 1.9559f, - 2.1373f, - 2.3601f, - 2.4760f - }, - - new[] - { - 0.3132f, - 0.4613f, - 0.6544f, - 0.8532f, - 1.0721f, - 1.2730f, - 1.7566f, - 1.9217f, - 2.1693f, - 2.6531f - }, - - new[] - { - 0.3329f, - 0.4131f, - 0.8073f, - 1.1297f, - 1.2869f, - 1.4937f, - 1.7885f, - 1.9150f, - 2.4505f, - 2.5760f - }, - - new[] - { - 0.2340f, - 0.3605f, - 0.7659f, - 0.9874f, - 1.1854f, - 1.3337f, - 1.5128f, - 2.0062f, - 2.4427f, - 2.5859f - }, - - new[] - { - 0.4131f, - 0.5330f, - 0.6530f, - 0.9360f, - 1.3648f, - 1.5388f, - 1.6994f, - 1.8707f, - 2.4294f, - 2.5335f - }, - - new[] - { - 0.3754f, - 0.5229f, - 0.7265f, - 0.9301f, - 1.1724f, - 1.3440f, - 1.5118f, - 1.7098f, - 2.5218f, - 2.6242f - }, - - new[] - { - 0.2138f, - 0.2998f, - 0.6283f, - 1.2166f, - 1.4187f, - 1.6084f, - 1.7992f, - 2.0106f, - 2.5377f, - 2.6558f - }, - - new[] - { - 0.1761f, - 0.2672f, - 0.4065f, - 0.8317f, - 1.0900f, - 1.4814f, - 1.7672f, - 1.8685f, - 2.3969f, - 2.5079f - }, - - new[] - { - 0.2801f, - 0.3535f, - 0.4969f, - 0.9809f, - 1.4934f, - 1.6378f, - 1.8021f, - 2.1200f, - 2.3135f, - 2.4034f - }, - - new[] - { - 0.2365f, - 0.3246f, - 0.5618f, - 0.8176f, - 1.1073f, - 1.5702f, - 1.7331f, - 1.8592f, - 1.9589f, - 2.3044f - }, - - new[] - { - 0.2529f, - 0.3251f, - 0.5147f, - 1.1530f, - 1.3291f, - 1.5005f, - 1.7028f, - 1.8200f, - 2.3482f, - 2.4831f - }, - - new[] - { - 0.2125f, - 0.3041f, - 0.4259f, - 0.9935f, - 1.1788f, - 1.3615f, - 1.6121f, - 1.7930f, - 2.5509f, - 2.6742f - }, - - new[] - { - 0.2685f, - 0.3518f, - 0.5707f, - 1.0410f, - 1.2270f, - 1.3927f, - 1.7622f, - 1.8876f, - 2.0985f, - 2.5144f - }, - - new[] - { - 0.2373f, - 0.3648f, - 0.5099f, - 0.7373f, - 0.9129f, - 1.0421f, - 1.7312f, - 1.8984f, - 2.1512f, - 2.6342f - }, - - new[] - { - 0.2229f, - 0.3876f, - 0.8621f, - 1.1986f, - 1.5655f, - 1.8861f, - 2.2376f, - 2.4239f, - 2.6648f, - 2.7359f - }, - - new[] - { - 0.3009f, - 0.3719f, - 0.5887f, - 0.7297f, - 0.9395f, - 1.8797f, - 2.0423f, - 2.1541f, - 2.5132f, - 2.6026f - }, - - new[] - { - 0.3114f, - 0.4142f, - 0.6476f, - 0.8448f, - 1.2495f, - 1.7192f, - 2.2148f, - 2.3432f, - 2.5246f, - 2.6046f - }, - - new[] - { - 0.3666f, - 0.4638f, - 0.6496f, - 0.7858f, - 0.9667f, - 1.4213f, - 1.9300f, - 2.0564f, - 2.2119f, - 2.3170f - }, - - new[] - { - 0.4218f, - 0.5075f, - 0.8348f, - 1.0009f, - 1.2057f, - 1.5032f, - 1.9416f, - 2.0540f, - 2.4352f, - 2.5504f - }, - - new[] - { - 0.3726f, - 0.4602f, - 0.5971f, - 0.7093f, - 0.8517f, - 1.2361f, - 1.8052f, - 1.9520f, - 2.4137f, - 2.5518f - }, - - new[] - { - 0.4482f, - 0.5318f, - 0.7114f, - 0.8542f, - 1.0328f, - 1.4751f, - 1.7278f, - 1.8237f, - 2.3496f, - 2.4931f - }, - - new[] - { - 0.3316f, - 0.4498f, - 0.6404f, - 0.8162f, - 1.0332f, - 1.2209f, - 1.5130f, - 1.7250f, - 1.9715f, - 2.4141f - }, - - new[] - { - 0.2375f, - 0.3221f, - 0.5042f, - 0.9760f, - 1.7503f, - 1.9014f, - 2.0822f, - 2.2225f, - 2.4689f, - 2.5632f - }, - - new[] - { - 0.2813f, - 0.3575f, - 0.5032f, - 0.5889f, - 0.6885f, - 1.6040f, - 1.9318f, - 2.0677f, - 2.4546f, - 2.5701f - }, - - new[] - { - 0.2198f, - 0.3072f, - 0.4090f, - 0.6371f, - 1.6365f, - 1.9468f, - 2.1507f, - 2.2633f, - 2.5063f, - 2.5943f - }, - - new[] - { - 0.1754f, - 0.2716f, - 0.3361f, - 0.5550f, - 1.1789f, - 1.3728f, - 1.8527f, - 1.9919f, - 2.1349f, - 2.3359f - }, - - new[] - { - 0.2832f, - 0.3540f, - 0.6080f, - 0.8467f, - 1.0259f, - 1.6467f, - 1.8987f, - 1.9875f, - 2.4744f, - 2.5527f - }, - - new[] - { - 0.2670f, - 0.3564f, - 0.5628f, - 0.7172f, - 0.9021f, - 1.5328f, - 1.7131f, - 2.0501f, - 2.5633f, - 2.6574f - }, - - new[] - { - 0.2729f, - 0.3569f, - 0.6252f, - 0.7641f, - 0.9887f, - 1.6589f, - 1.8726f, - 1.9947f, - 2.1884f, - 2.4609f - }, - - new[] - { - 0.2155f, - 0.3221f, - 0.4580f, - 0.6995f, - 0.9623f, - 1.2339f, - 1.6642f, - 1.8823f, - 2.0518f, - 2.2674f - }, - - new[] - { - 0.4224f, - 0.7009f, - 1.1714f, - 1.4334f, - 1.7595f, - 1.9629f, - 2.2185f, - 2.3304f, - 2.5446f, - 2.6369f - }, - - new[] - { - 0.4560f, - 0.5403f, - 0.7568f, - 0.8989f, - 1.1292f, - 1.7687f, - 1.9575f, - 2.0784f, - 2.4260f, - 2.5484f - }, - - new[] - { - 0.4299f, - 0.5833f, - 0.8408f, - 1.0596f, - 1.5524f, - 1.7484f, - 1.9471f, - 2.2034f, - 2.4617f, - 2.5812f - }, - - new[] - { - 0.2614f, - 0.3624f, - 0.8381f, - 0.9829f, - 1.2220f, - 1.6064f, - 1.8083f, - 1.9362f, - 2.1397f, - 2.2773f - }, - - new[] - { - 0.5064f, - 0.7481f, - 1.1021f, - 1.3271f, - 1.5486f, - 1.7096f, - 1.9503f, - 2.1006f, - 2.3911f, - 2.5141f - }, - - new[] - { - 0.5375f, - 0.6552f, - 0.8099f, - 1.0219f, - 1.2407f, - 1.4160f, - 1.8266f, - 1.9936f, - 2.1951f, - 2.2911f - }, - - new[] - { - 0.4994f, - 0.6575f, - 0.8365f, - 1.0706f, - 1.4116f, - 1.6224f, - 1.9200f, - 2.0667f, - 2.3262f, - 2.4539f - }, - - new[] - { - 0.3353f, - 0.4426f, - 0.6469f, - 0.9161f, - 1.2528f, - 1.3956f, - 1.6080f, - 1.8909f, - 2.0600f, - 2.1380f - }, - - new[] - { - 0.2745f, - 0.4341f, - 1.0424f, - 1.2928f, - 1.5461f, - 1.7940f, - 2.0161f, - 2.1758f, - 2.4742f, - 2.5937f - }, - - new[] - { - 0.1562f, - 0.2393f, - 0.4786f, - 0.9513f, - 1.2395f, - 1.8010f, - 2.0320f, - 2.2143f, - 2.5243f, - 2.6204f - }, - - new[] - { - 0.2979f, - 0.4242f, - 0.8224f, - 1.0564f, - 1.4881f, - 1.7808f, - 2.0898f, - 2.1882f, - 2.3328f, - 2.4389f - }, - - new[] - { - 0.2294f, - 0.3070f, - 0.5490f, - 0.9244f, - 1.2229f, - 1.8248f, - 1.9704f, - 2.0627f, - 2.2458f, - 2.3653f - }, - - new[] - { - 0.3423f, - 0.4502f, - 0.9144f, - 1.2313f, - 1.3694f, - 1.5517f, - 1.9907f, - 2.1326f, - 2.4509f, - 2.5789f - }, - - new[] - { - 0.2470f, - 0.3275f, - 0.4729f, - 1.0093f, - 1.2519f, - 1.4216f, - 1.8540f, - 2.0877f, - 2.3151f, - 2.4156f - }, - - new[] - { - 0.3447f, - 0.4401f, - 0.7099f, - 1.0493f, - 1.2312f, - 1.4001f, - 2.0225f, - 2.1317f, - 2.2894f, - 2.4263f - }, - - new[] - { - 0.3481f, - 0.4494f, - 0.6446f, - 0.9336f, - 1.1198f, - 1.2620f, - 1.8264f, - 1.9712f, - 2.1435f, - 2.2552f - }, - - new[] - { - 0.1646f, - 0.3229f, - 0.7112f, - 1.0725f, - 1.2964f, - 1.5663f, - 1.9843f, - 2.2363f, - 2.5798f, - 2.7572f - }, - - new[] - { - 0.2614f, - 0.3707f, - 0.5241f, - 0.7425f, - 0.9269f, - 1.2976f, - 2.0945f, - 2.2014f, - 2.6204f, - 2.6959f - }, - - new[] - { - 0.1963f, - 0.2900f, - 0.4131f, - 0.8397f, - 1.2171f, - 1.3705f, - 2.0665f, - 2.1546f, - 2.4640f, - 2.5782f - }, - - new[] - { - 0.3387f, - 0.4415f, - 0.6121f, - 0.8005f, - 0.9507f, - 1.0937f, - 2.0836f, - 2.2342f, - 2.3849f, - 2.5076f - }, - - new[] - { - 0.2362f, - 0.5876f, - 0.7574f, - 0.8804f, - 1.0961f, - 1.4240f, - 1.9519f, - 2.1742f, - 2.4935f, - 2.6493f - }, - - new[] - { - 0.2793f, - 0.4282f, - 0.6149f, - 0.8352f, - 1.0106f, - 1.1766f, - 1.8392f, - 2.0119f, - 2.6433f, - 2.7117f - }, - - new[] - { - 0.3603f, - 0.4604f, - 0.5955f, - 0.9251f, - 1.1006f, - 1.2572f, - 1.7688f, - 1.8607f, - 2.4687f, - 2.5623f - }, - - new[] - { - 0.3975f, - 0.5849f, - 0.8059f, - 0.9182f, - 1.0552f, - 1.1850f, - 1.6356f, - 1.9627f, - 2.3318f, - 2.4719f - }, - - new[] - { - 0.2231f, - 0.3192f, - 0.4256f, - 0.7373f, - 1.4831f, - 1.6874f, - 1.9765f, - 2.1097f, - 2.6152f, - 2.6906f - }, - - new[] - { - 0.1221f, - 0.2081f, - 0.3665f, - 0.7734f, - 1.0341f, - 1.2818f, - 1.8162f, - 2.0727f, - 2.4446f, - 2.7377f - }, - - new[] - { - 0.2010f, - 0.2791f, - 0.3796f, - 0.8845f, - 1.4030f, - 1.5615f, - 2.0538f, - 2.1567f, - 2.3171f, - 2.4686f - }, - - new[] - { - 0.2086f, - 0.3053f, - 0.4047f, - 0.8224f, - 1.0656f, - 1.2115f, - 1.9641f, - 2.0871f, - 2.2430f, - 2.4313f - }, - - new[] - { - 0.3203f, - 0.4285f, - 0.5467f, - 0.6891f, - 1.2039f, - 1.3569f, - 1.8578f, - 2.2055f, - 2.3906f, - 2.4881f - }, - - new[] - { - 0.3074f, - 0.4192f, - 0.5772f, - 0.7799f, - 0.9866f, - 1.1335f, - 1.6068f, - 2.2441f, - 2.4194f, - 2.5089f - }, - - new[] - { - 0.2108f, - 0.2910f, - 0.4993f, - 0.7695f, - 0.9528f, - 1.5681f, - 1.7838f, - 2.1495f, - 2.3522f, - 2.4636f - }, - - new[] - { - 0.3492f, - 0.4560f, - 0.5906f, - 0.7379f, - 0.8855f, - 1.0257f, - 1.7128f, - 1.9997f, - 2.2019f, - 2.3694f - }, - - new[] - { - 0.5185f, - 0.7316f, - 0.9708f, - 1.1954f, - 1.5066f, - 1.7887f, - 2.1396f, - 2.2918f, - 2.5429f, - 2.6489f - }, - - new[] - { - 0.4276f, - 0.4946f, - 0.6934f, - 0.8308f, - 0.9944f, - 1.4582f, - 2.0324f, - 2.1294f, - 2.4891f, - 2.6324f - }, - - new[] - { - 0.3847f, - 0.5973f, - 0.7202f, - 0.8787f, - 1.3938f, - 1.5959f, - 1.8463f, - 2.1574f, - 2.5050f, - 2.6687f - }, - - new[] - { - 0.4835f, - 0.5919f, - 0.7235f, - 0.8862f, - 1.0756f, - 1.2853f, - 1.9118f, - 2.0215f, - 2.2213f, - 2.4638f - }, - - new[] - { - 0.5492f, - 0.8062f, - 0.9810f, - 1.1293f, - 1.3189f, - 1.5415f, - 1.9385f, - 2.1378f, - 2.4439f, - 2.5691f - }, - - new[] - { - 0.5190f, - 0.6764f, - 0.8123f, - 1.0154f, - 1.2085f, - 1.4266f, - 1.8433f, - 2.0866f, - 2.5113f, - 2.6474f - }, - - new[] - { - 0.4602f, - 0.6503f, - 0.9602f, - 1.1427f, - 1.3043f, - 1.4427f, - 1.6676f, - 1.8758f, - 2.2868f, - 2.4271f - }, - - new[] - { - 0.3764f, - 0.4845f, - 0.7627f, - 0.9914f, - 1.1961f, - 1.3421f, - 1.5129f, - 1.6707f, - 2.1836f, - 2.3322f - }, - - new[] - { - 0.3334f, - 0.5701f, - 0.8622f, - 1.1232f, - 1.3851f, - 1.6767f, - 2.0600f, - 2.2946f, - 2.5375f, - 2.7295f - }, - - new[] - { - 0.1449f, - 0.2719f, - 0.5783f, - 0.8807f, - 1.1746f, - 1.5422f, - 1.8804f, - 2.1934f, - 2.4734f, - 2.8728f - }, - - new[] - { - 0.2333f, - 0.3024f, - 0.4780f, - 1.2327f, - 1.4180f, - 1.5815f, - 1.9804f, - 2.0921f, - 2.3524f, - 2.5304f - }, - - new[] - { - 0.2154f, - 0.3075f, - 0.4746f, - 0.8477f, - 1.1170f, - 1.5369f, - 1.9847f, - 2.0733f, - 2.1880f, - 2.2504f - }, - - new[] - { - 0.1709f, - 0.4486f, - 0.8705f, - 1.0643f, - 1.3047f, - 1.5269f, - 1.9175f, - 2.1621f, - 2.4073f, - 2.5718f - }, - - new[] - { - 0.2835f, - 0.3752f, - 0.5234f, - 0.9898f, - 1.1484f, - 1.2974f, - 1.9363f, - 2.0378f, - 2.4065f, - 2.6214f - }, - - new[] - { - 0.3211f, - 0.4077f, - 0.5809f, - 1.0206f, - 1.2542f, - 1.3835f, - 1.5723f, - 2.1209f, - 2.3464f, - 2.4336f - }, - - new[] - { - 0.2101f, - 0.3146f, - 0.6779f, - 0.8783f, - 1.0561f, - 1.3045f, - 1.8395f, - 2.0695f, - 2.2831f, - 2.4328f - } - }; - - /*Second Stage Codebook*/ - - public static float[ /* NC1 */][ /* M */] lspcb2 = + 0.1814f, + 0.2647f, + 0.4580f, + 1.1077f, + 1.4813f, + 1.7022f, + 2.1953f, + 2.3405f, + 2.5867f, + 2.6636f + }, + + new[] + { + 0.2113f, + 0.3223f, + 0.4212f, + 0.5946f, + 0.7479f, + 0.9615f, + 1.9097f, + 2.1750f, + 2.4773f, + 2.6737f + }, + + new[] + { + 0.1915f, + 0.2755f, + 0.3770f, + 0.5950f, + 1.3505f, + 1.6349f, + 2.2348f, + 2.3552f, + 2.5768f, + 2.6540f + }, + + new[] + { + 0.2116f, + 0.3067f, + 0.4099f, + 0.5748f, + 0.8518f, + 1.2569f, + 2.0782f, + 2.1920f, + 2.3371f, + 2.4842f + }, + + new[] + { + 0.2129f, + 0.2974f, + 0.4039f, + 1.0659f, + 1.2735f, + 1.4658f, + 1.9061f, + 2.0312f, + 2.6074f, + 2.6750f + }, + + new[] + { + 0.2181f, + 0.2893f, + 0.4117f, + 0.5519f, + 0.8295f, + 1.5825f, + 2.1575f, + 2.3179f, + 2.5458f, + 2.6417f + }, + + new[] + { + 0.1991f, + 0.2971f, + 0.4104f, + 0.7725f, + 1.3073f, + 1.4665f, + 1.6208f, + 1.6973f, + 2.3732f, + 2.5743f + }, + + new[] + { + 0.1818f, + 0.2886f, + 0.4018f, + 0.7630f, + 1.1264f, + 1.2699f, + 1.6899f, + 1.8650f, + 2.1633f, + 2.6186f + }, + + new[] + { + 0.2282f, + 0.3093f, + 0.4243f, + 0.5329f, + 1.1173f, + 1.7717f, + 1.9420f, + 2.0780f, + 2.5160f, + 2.6137f + }, + + new[] + { + 0.2528f, + 0.3693f, + 0.5290f, + 0.7146f, + 0.9528f, + 1.1269f, + 1.2936f, + 1.9589f, + 2.4548f, + 2.6653f + }, + + new[] + { + 0.2332f, + 0.3263f, + 0.4174f, + 0.5202f, + 1.3633f, + 1.8447f, + 2.0236f, + 2.1474f, + 2.3572f, + 2.4738f + }, + + new[] + { + 0.1393f, + 0.2216f, + 0.3204f, + 0.5644f, + 0.7929f, + 1.1705f, + 1.7051f, + 2.0054f, + 2.3623f, + 2.5985f + }, + + new[] + { + 0.2677f, + 0.3871f, + 0.5746f, + 0.7091f, + 1.3311f, + 1.5260f, + 1.7288f, + 1.9122f, + 2.5787f, + 2.6598f + }, + + new[] + { + 0.1570f, + 0.2328f, + 0.3111f, + 0.4216f, + 1.1688f, + 1.4605f, + 1.9505f, + 2.1173f, + 2.4038f, + 2.7460f + }, + + new[] + { + 0.2346f, + 0.3321f, + 0.5621f, + 0.8160f, + 1.4042f, + 1.5860f, + 1.7518f, + 1.8631f, + 2.0749f, + 2.5380f + }, + + new[] + { + 0.2505f, + 0.3368f, + 0.4758f, + 0.6405f, + 0.8104f, + 1.2533f, + 1.9329f, + 2.0526f, + 2.2155f, + 2.6459f + }, + + new[] + { + 0.2196f, + 0.3049f, + 0.6857f, + 1.3976f, + 1.6100f, + 1.7958f, + 2.0813f, + 2.2211f, + 2.4789f, + 2.5857f + }, + + new[] + { + 0.1232f, + 0.2011f, + 0.3527f, + 0.6969f, + 1.1647f, + 1.5081f, + 1.8593f, + 2.2576f, + 2.5594f, + 2.6896f + }, + + new[] + { + 0.3682f, + 0.4632f, + 0.6600f, + 0.9118f, + 1.5245f, + 1.7071f, + 1.8712f, + 1.9939f, + 2.4356f, + 2.5380f + }, + + new[] + { + 0.2690f, + 0.3711f, + 0.4635f, + 0.6644f, + 1.4633f, + 1.6495f, + 1.8227f, + 1.9983f, + 2.1797f, + 2.2954f + }, + + new[] + { + 0.3555f, + 0.5240f, + 0.9751f, + 1.1685f, + 1.4114f, + 1.6168f, + 1.7769f, + 2.0178f, + 2.4420f, + 2.5724f + }, + + new[] + { + 0.3493f, + 0.4404f, + 0.7231f, + 0.8587f, + 1.1272f, + 1.4715f, + 1.6760f, + 2.2042f, + 2.4735f, + 2.5604f + }, + + new[] + { + 0.3747f, + 0.5263f, + 0.7284f, + 0.8994f, + 1.4017f, + 1.5502f, + 1.7468f, + 1.9816f, + 2.2380f, + 2.3404f + }, + + new[] + { + 0.2972f, + 0.4470f, + 0.5941f, + 0.7078f, + 1.2675f, + 1.4310f, + 1.5930f, + 1.9126f, + 2.3026f, + 2.4208f + }, + + new[] + { + 0.2467f, + 0.3180f, + 0.4712f, + 1.1281f, + 1.6206f, + 1.7876f, + 1.9544f, + 2.0873f, + 2.3521f, + 2.4721f + }, + + new[] + { + 0.2292f, + 0.3430f, + 0.4383f, + 0.5747f, + 1.3497f, + 1.5187f, + 1.9070f, + 2.0958f, + 2.2902f, + 2.4301f + }, + + new[] + { + 0.2573f, + 0.3508f, + 0.4484f, + 0.7079f, + 1.6577f, + 1.7929f, + 1.9456f, + 2.0847f, + 2.3060f, + 2.4208f + }, + + new[] + { + 0.1968f, + 0.2789f, + 0.3594f, + 0.4361f, + 1.0034f, + 1.7040f, + 1.9439f, + 2.1044f, + 2.2696f, + 2.4558f + }, + + new[] + { + 0.2955f, + 0.3853f, + 0.7986f, + 1.2470f, + 1.4723f, + 1.6522f, + 1.8684f, + 2.0084f, + 2.2849f, + 2.4268f + }, + + new[] + { + 0.2036f, + 0.3189f, + 0.4314f, + 0.6393f, + 1.2834f, + 1.4278f, + 1.5796f, + 2.0506f, + 2.2044f, + 2.3656f + }, + + new[] + { + 0.2916f, + 0.3684f, + 0.5907f, + 1.1394f, + 1.3933f, + 1.5540f, + 1.8341f, + 1.9835f, + 2.1301f, + 2.2800f + }, + + new[] + { + 0.2289f, + 0.3402f, + 0.5166f, + 0.7716f, + 1.0614f, + 1.2389f, + 1.4386f, + 2.0769f, + 2.2715f, + 2.4366f + }, + + new[] + { + 0.0829f, + 0.1723f, + 0.5682f, + 0.9773f, + 1.3973f, + 1.6174f, + 1.9242f, + 2.2128f, + 2.4855f, + 2.6327f + }, + + new[] + { + 0.2244f, + 0.3169f, + 0.4368f, + 0.5625f, + 0.6897f, + 1.3763f, + 1.7524f, + 1.9393f, + 2.5121f, + 2.6556f + }, + + new[] + { + 0.1591f, + 0.2387f, + 0.2924f, + 0.4056f, + 1.4677f, + 1.6802f, + 1.9389f, + 2.2067f, + 2.4635f, + 2.5919f + }, + + new[] + { + 0.1756f, + 0.2566f, + 0.3251f, + 0.4227f, + 1.0167f, + 1.2649f, + 1.6801f, + 2.1055f, + 2.4088f, + 2.7276f + }, + + new[] + { + 0.1050f, + 0.2325f, + 0.7445f, + 0.9491f, + 1.1982f, + 1.4658f, + 1.8093f, + 2.0397f, + 2.4155f, + 2.5797f + }, + + new[] + { + 0.2043f, + 0.3324f, + 0.4522f, + 0.7477f, + 0.9361f, + 1.1533f, + 1.6703f, + 1.7631f, + 2.5071f, + 2.6528f + }, + + new[] + { + 0.1522f, + 0.2258f, + 0.3543f, + 0.5504f, + 0.8815f, + 1.5516f, + 1.8110f, + 1.9915f, + 2.3603f, + 2.7735f + }, + + new[] + { + 0.1862f, + 0.2759f, + 0.4715f, + 0.6908f, + 0.8963f, + 1.4341f, + 1.6322f, + 1.7630f, + 2.2027f, + 2.6043f + }, + + new[] + { + 0.1460f, + 0.2254f, + 0.3790f, + 0.8622f, + 1.3394f, + 1.5754f, + 1.8084f, + 2.0798f, + 2.4319f, + 2.7632f + }, + + new[] { - new[] - { - -0.0532f, - -0.0995f, - -0.0906f, - 0.1261f, - -0.0633f, - 0.0711f, - -0.1467f, - 0.1012f, - 0.0106f, - 0.0470f - }, - - new[] - { - -0.1017f, - -0.1088f, - 0.0566f, - -0.0010f, - -0.1528f, - 0.1771f, - 0.0089f, - -0.0282f, - 0.1055f, - 0.0808f - }, - - new[] - { - -0.1247f, - 0.0283f, - -0.0374f, - 0.0393f, - -0.0269f, - -0.0200f, - -0.0643f, - -0.0921f, - -0.1994f, - 0.0327f - }, - - new[] - { - 0.0070f, - -0.0242f, - -0.0415f, - -0.0041f, - -0.1793f, - 0.0700f, - 0.0972f, - -0.0207f, - -0.0771f, - 0.0997f - }, - - new[] - { - 0.0209f, - -0.0428f, - 0.0359f, - 0.2027f, - 0.0554f, - 0.0634f, - 0.0356f, - 0.0195f, - -0.0782f, - -0.1583f - }, - - new[] - { - -0.0856f, - -0.1028f, - -0.0071f, - 0.1160f, - 0.1089f, - 0.1892f, - 0.0874f, - 0.0644f, - -0.0872f, - -0.0236f - }, - - new[] - { - 0.0713f, - 0.0039f, - -0.0353f, - 0.0435f, - -0.0407f, - -0.0558f, - 0.0748f, - -0.0346f, - -0.1686f, - -0.0905f - }, - - new[] - { - -0.0134f, - -0.0987f, - 0.0283f, - 0.0095f, - -0.0107f, - -0.0420f, - 0.1638f, - 0.1328f, - -0.0799f, - -0.0695f - }, - - new[] - { - -0.1049f, - 0.1510f, - 0.0672f, - 0.1043f, - 0.0872f, - -0.0663f, - -0.2139f, - -0.0239f, - -0.0120f, - -0.0338f - }, - - new[] - { - -0.1071f, - -0.1165f, - -0.1524f, - -0.0365f, - 0.0260f, - -0.0288f, - -0.0889f, - 0.1159f, - 0.1852f, - 0.1093f - }, - - new[] - { - -0.0094f, - 0.0420f, - -0.0758f, - 0.0932f, - 0.0505f, - 0.0614f, - -0.0443f, - -0.1172f, - -0.0590f, - 0.1693f - }, - - new[] - { - -0.0384f, - -0.0375f, - -0.0313f, - -0.1539f, - -0.0524f, - 0.0550f, - -0.0569f, - -0.0133f, - 0.1233f, - 0.2714f - }, - - new[] - { - 0.0869f, - 0.0847f, - 0.0637f, - 0.0794f, - 0.1594f, - -0.0035f, - -0.0462f, - 0.0909f, - -0.1227f, - 0.0294f - }, - - new[] - { - -0.0137f, - -0.0332f, - -0.0611f, - 0.1156f, - 0.2116f, - 0.0332f, - -0.0019f, - 0.1110f, - -0.0317f, - 0.2061f - }, - - new[] - { - 0.0703f, - -0.0013f, - -0.0572f, - -0.0243f, - 0.1345f, - -0.1235f, - 0.0710f, - -0.0065f, - -0.0912f, - 0.1072f - }, - - new[] - { - 0.0178f, - -0.0349f, - -0.1563f, - -0.0487f, - 0.0044f, - -0.0609f, - -0.1682f, - 0.0023f, - -0.0542f, - 0.1811f - }, - - new[] - { - -0.1384f, - -0.1020f, - 0.1649f, - 0.1568f, - -0.0116f, - 0.1240f, - -0.0271f, - 0.0541f, - 0.0455f, - -0.0433f - }, - - new[] - { - -0.1782f, - -0.1511f, - 0.0509f, - -0.0261f, - 0.0570f, - 0.0817f, - 0.0805f, - 0.2003f, - 0.1138f, - 0.0653f - }, - - new[] - { - -0.0019f, - 0.0081f, - 0.0572f, - 0.1245f, - -0.0914f, - 0.1691f, - -0.0223f, - -0.1108f, - -0.0881f, - -0.0320f - }, - - new[] - { - -0.0413f, - 0.0181f, - 0.1764f, - 0.0092f, - -0.0928f, - 0.0695f, - 0.1523f, - 0.0412f, - 0.0508f, - -0.0148f - }, - - new[] - { - 0.0476f, - 0.0292f, - 0.1915f, - 0.1198f, - 0.0139f, - 0.0451f, - -0.1225f, - -0.0619f, - -0.0717f, - -0.1104f - }, - - new[] - { - -0.0382f, - -0.0120f, - 0.1159f, - 0.0039f, - 0.1348f, - 0.0088f, - -0.0173f, - 0.1789f, - 0.0078f, - -0.0959f - }, - - new[] - { - 0.1376f, - 0.0713f, - 0.1020f, - 0.0339f, - -0.1415f, - 0.0254f, - 0.0368f, - -0.1077f, - 0.0143f, - -0.0494f - }, - - new[] - { - 0.0658f, - -0.0140f, - 0.1046f, - -0.0603f, - 0.0273f, - -0.1114f, - 0.0761f, - -0.0093f, - 0.0338f, - -0.0538f - }, - - new[] - { - 0.2683f, - 0.2853f, - 0.1549f, - 0.0819f, - 0.0372f, - -0.0327f, - -0.0642f, - 0.0172f, - 0.1077f, - -0.0170f - }, - - new[] - { - -0.1949f, - 0.0672f, - 0.0978f, - -0.0557f, - -0.0069f, - -0.0851f, - 0.1057f, - 0.1294f, - 0.0505f, - 0.0545f - }, - - new[] - { - 0.1409f, - 0.0724f, - -0.0094f, - 0.1511f, - -0.0039f, - 0.0710f, - -0.1266f, - -0.1093f, - 0.0817f, - 0.0363f - }, - - new[] - { - 0.0485f, - 0.0682f, - 0.0248f, - -0.0974f, - -0.1122f, - 0.0004f, - 0.0845f, - -0.0357f, - 0.1282f, - 0.0955f - }, - - new[] - { - 0.0408f, - 0.1801f, - 0.0772f, - -0.0098f, - 0.0059f, - -0.1296f, - -0.0591f, - 0.0443f, - -0.0729f, - -0.1041f - }, - - new[] - { - -0.0666f, - -0.0403f, - -0.0524f, - -0.0831f, - 0.1384f, - -0.1443f, - -0.0909f, - 0.1636f, - 0.0320f, - 0.0077f - }, + 0.2621f, + 0.3792f, + 0.5463f, + 0.7948f, + 1.0043f, + 1.1921f, + 1.3409f, + 1.4845f, + 2.3159f, + 2.6002f + }, + + new[] + { + 0.1935f, + 0.2937f, + 0.3656f, + 0.4927f, + 1.4015f, + 1.6086f, + 1.7724f, + 1.8837f, + 2.4374f, + 2.5971f + }, + + new[] + { + 0.2171f, + 0.3282f, + 0.4412f, + 0.5713f, + 1.1554f, + 1.3506f, + 1.5227f, + 1.9923f, + 2.4100f, + 2.5391f + }, + + new[] + { + 0.2274f, + 0.3157f, + 0.4263f, + 0.8202f, + 1.4293f, + 1.5884f, + 1.7535f, + 1.9688f, + 2.3939f, + 2.4934f + }, + + new[] + { + 0.1704f, + 0.2633f, + 0.3259f, + 0.4134f, + 1.2948f, + 1.4802f, + 1.6619f, + 2.0393f, + 2.3165f, + 2.6083f + }, + + new[] + { + 0.1763f, + 0.2585f, + 0.4012f, + 0.7609f, + 1.1503f, + 1.5847f, + 1.8309f, + 1.9352f, + 2.0982f, + 2.6681f + }, + + new[] + { + 0.2447f, + 0.3535f, + 0.4618f, + 0.5979f, + 0.7530f, + 0.8908f, + 1.5393f, + 2.0075f, + 2.3557f, + 2.6203f + }, + + new[] + { + 0.1826f, + 0.3496f, + 0.7764f, + 0.9888f, + 1.3915f, + 1.7421f, + 1.9412f, + 2.1620f, + 2.4999f, + 2.6931f + }, + + new[] + { + 0.3033f, + 0.3802f, + 0.6981f, + 0.8664f, + 1.0254f, + 1.5401f, + 1.7180f, + 1.8124f, + 2.5068f, + 2.6119f + }, + + new[] + { + 0.2960f, + 0.4001f, + 0.6465f, + 0.7672f, + 1.3782f, + 1.5751f, + 1.9559f, + 2.1373f, + 2.3601f, + 2.4760f + }, + + new[] + { + 0.3132f, + 0.4613f, + 0.6544f, + 0.8532f, + 1.0721f, + 1.2730f, + 1.7566f, + 1.9217f, + 2.1693f, + 2.6531f + }, + + new[] + { + 0.3329f, + 0.4131f, + 0.8073f, + 1.1297f, + 1.2869f, + 1.4937f, + 1.7885f, + 1.9150f, + 2.4505f, + 2.5760f + }, + + new[] + { + 0.2340f, + 0.3605f, + 0.7659f, + 0.9874f, + 1.1854f, + 1.3337f, + 1.5128f, + 2.0062f, + 2.4427f, + 2.5859f + }, + + new[] + { + 0.4131f, + 0.5330f, + 0.6530f, + 0.9360f, + 1.3648f, + 1.5388f, + 1.6994f, + 1.8707f, + 2.4294f, + 2.5335f + }, + + new[] + { + 0.3754f, + 0.5229f, + 0.7265f, + 0.9301f, + 1.1724f, + 1.3440f, + 1.5118f, + 1.7098f, + 2.5218f, + 2.6242f + }, + + new[] + { + 0.2138f, + 0.2998f, + 0.6283f, + 1.2166f, + 1.4187f, + 1.6084f, + 1.7992f, + 2.0106f, + 2.5377f, + 2.6558f + }, + + new[] + { + 0.1761f, + 0.2672f, + 0.4065f, + 0.8317f, + 1.0900f, + 1.4814f, + 1.7672f, + 1.8685f, + 2.3969f, + 2.5079f + }, + + new[] + { + 0.2801f, + 0.3535f, + 0.4969f, + 0.9809f, + 1.4934f, + 1.6378f, + 1.8021f, + 2.1200f, + 2.3135f, + 2.4034f + }, + + new[] + { + 0.2365f, + 0.3246f, + 0.5618f, + 0.8176f, + 1.1073f, + 1.5702f, + 1.7331f, + 1.8592f, + 1.9589f, + 2.3044f + }, + + new[] + { + 0.2529f, + 0.3251f, + 0.5147f, + 1.1530f, + 1.3291f, + 1.5005f, + 1.7028f, + 1.8200f, + 2.3482f, + 2.4831f + }, + + new[] + { + 0.2125f, + 0.3041f, + 0.4259f, + 0.9935f, + 1.1788f, + 1.3615f, + 1.6121f, + 1.7930f, + 2.5509f, + 2.6742f + }, + + new[] + { + 0.2685f, + 0.3518f, + 0.5707f, + 1.0410f, + 1.2270f, + 1.3927f, + 1.7622f, + 1.8876f, + 2.0985f, + 2.5144f + }, + + new[] + { + 0.2373f, + 0.3648f, + 0.5099f, + 0.7373f, + 0.9129f, + 1.0421f, + 1.7312f, + 1.8984f, + 2.1512f, + 2.6342f + }, + + new[] + { + 0.2229f, + 0.3876f, + 0.8621f, + 1.1986f, + 1.5655f, + 1.8861f, + 2.2376f, + 2.4239f, + 2.6648f, + 2.7359f + }, + + new[] + { + 0.3009f, + 0.3719f, + 0.5887f, + 0.7297f, + 0.9395f, + 1.8797f, + 2.0423f, + 2.1541f, + 2.5132f, + 2.6026f + }, + + new[] + { + 0.3114f, + 0.4142f, + 0.6476f, + 0.8448f, + 1.2495f, + 1.7192f, + 2.2148f, + 2.3432f, + 2.5246f, + 2.6046f + }, + + new[] + { + 0.3666f, + 0.4638f, + 0.6496f, + 0.7858f, + 0.9667f, + 1.4213f, + 1.9300f, + 2.0564f, + 2.2119f, + 2.3170f + }, + + new[] + { + 0.4218f, + 0.5075f, + 0.8348f, + 1.0009f, + 1.2057f, + 1.5032f, + 1.9416f, + 2.0540f, + 2.4352f, + 2.5504f + }, + + new[] + { + 0.3726f, + 0.4602f, + 0.5971f, + 0.7093f, + 0.8517f, + 1.2361f, + 1.8052f, + 1.9520f, + 2.4137f, + 2.5518f + }, + + new[] + { + 0.4482f, + 0.5318f, + 0.7114f, + 0.8542f, + 1.0328f, + 1.4751f, + 1.7278f, + 1.8237f, + 2.3496f, + 2.4931f + }, + + new[] + { + 0.3316f, + 0.4498f, + 0.6404f, + 0.8162f, + 1.0332f, + 1.2209f, + 1.5130f, + 1.7250f, + 1.9715f, + 2.4141f + }, + + new[] + { + 0.2375f, + 0.3221f, + 0.5042f, + 0.9760f, + 1.7503f, + 1.9014f, + 2.0822f, + 2.2225f, + 2.4689f, + 2.5632f + }, + + new[] + { + 0.2813f, + 0.3575f, + 0.5032f, + 0.5889f, + 0.6885f, + 1.6040f, + 1.9318f, + 2.0677f, + 2.4546f, + 2.5701f + }, + + new[] + { + 0.2198f, + 0.3072f, + 0.4090f, + 0.6371f, + 1.6365f, + 1.9468f, + 2.1507f, + 2.2633f, + 2.5063f, + 2.5943f + }, + + new[] + { + 0.1754f, + 0.2716f, + 0.3361f, + 0.5550f, + 1.1789f, + 1.3728f, + 1.8527f, + 1.9919f, + 2.1349f, + 2.3359f + }, + + new[] + { + 0.2832f, + 0.3540f, + 0.6080f, + 0.8467f, + 1.0259f, + 1.6467f, + 1.8987f, + 1.9875f, + 2.4744f, + 2.5527f + }, + + new[] + { + 0.2670f, + 0.3564f, + 0.5628f, + 0.7172f, + 0.9021f, + 1.5328f, + 1.7131f, + 2.0501f, + 2.5633f, + 2.6574f + }, + + new[] + { + 0.2729f, + 0.3569f, + 0.6252f, + 0.7641f, + 0.9887f, + 1.6589f, + 1.8726f, + 1.9947f, + 2.1884f, + 2.4609f + }, + + new[] + { + 0.2155f, + 0.3221f, + 0.4580f, + 0.6995f, + 0.9623f, + 1.2339f, + 1.6642f, + 1.8823f, + 2.0518f, + 2.2674f + }, + + new[] + { + 0.4224f, + 0.7009f, + 1.1714f, + 1.4334f, + 1.7595f, + 1.9629f, + 2.2185f, + 2.3304f, + 2.5446f, + 2.6369f + }, + + new[] + { + 0.4560f, + 0.5403f, + 0.7568f, + 0.8989f, + 1.1292f, + 1.7687f, + 1.9575f, + 2.0784f, + 2.4260f, + 2.5484f + }, + + new[] + { + 0.4299f, + 0.5833f, + 0.8408f, + 1.0596f, + 1.5524f, + 1.7484f, + 1.9471f, + 2.2034f, + 2.4617f, + 2.5812f + }, + + new[] + { + 0.2614f, + 0.3624f, + 0.8381f, + 0.9829f, + 1.2220f, + 1.6064f, + 1.8083f, + 1.9362f, + 2.1397f, + 2.2773f + }, + + new[] + { + 0.5064f, + 0.7481f, + 1.1021f, + 1.3271f, + 1.5486f, + 1.7096f, + 1.9503f, + 2.1006f, + 2.3911f, + 2.5141f + }, + + new[] + { + 0.5375f, + 0.6552f, + 0.8099f, + 1.0219f, + 1.2407f, + 1.4160f, + 1.8266f, + 1.9936f, + 2.1951f, + 2.2911f + }, + + new[] + { + 0.4994f, + 0.6575f, + 0.8365f, + 1.0706f, + 1.4116f, + 1.6224f, + 1.9200f, + 2.0667f, + 2.3262f, + 2.4539f + }, + + new[] + { + 0.3353f, + 0.4426f, + 0.6469f, + 0.9161f, + 1.2528f, + 1.3956f, + 1.6080f, + 1.8909f, + 2.0600f, + 2.1380f + }, + + new[] + { + 0.2745f, + 0.4341f, + 1.0424f, + 1.2928f, + 1.5461f, + 1.7940f, + 2.0161f, + 2.1758f, + 2.4742f, + 2.5937f + }, + + new[] + { + 0.1562f, + 0.2393f, + 0.4786f, + 0.9513f, + 1.2395f, + 1.8010f, + 2.0320f, + 2.2143f, + 2.5243f, + 2.6204f + }, + + new[] + { + 0.2979f, + 0.4242f, + 0.8224f, + 1.0564f, + 1.4881f, + 1.7808f, + 2.0898f, + 2.1882f, + 2.3328f, + 2.4389f + }, + + new[] + { + 0.2294f, + 0.3070f, + 0.5490f, + 0.9244f, + 1.2229f, + 1.8248f, + 1.9704f, + 2.0627f, + 2.2458f, + 2.3653f + }, + + new[] + { + 0.3423f, + 0.4502f, + 0.9144f, + 1.2313f, + 1.3694f, + 1.5517f, + 1.9907f, + 2.1326f, + 2.4509f, + 2.5789f + }, + + new[] + { + 0.2470f, + 0.3275f, + 0.4729f, + 1.0093f, + 1.2519f, + 1.4216f, + 1.8540f, + 2.0877f, + 2.3151f, + 2.4156f + }, + + new[] + { + 0.3447f, + 0.4401f, + 0.7099f, + 1.0493f, + 1.2312f, + 1.4001f, + 2.0225f, + 2.1317f, + 2.2894f, + 2.4263f + }, + + new[] + { + 0.3481f, + 0.4494f, + 0.6446f, + 0.9336f, + 1.1198f, + 1.2620f, + 1.8264f, + 1.9712f, + 2.1435f, + 2.2552f + }, + + new[] + { + 0.1646f, + 0.3229f, + 0.7112f, + 1.0725f, + 1.2964f, + 1.5663f, + 1.9843f, + 2.2363f, + 2.5798f, + 2.7572f + }, + + new[] + { + 0.2614f, + 0.3707f, + 0.5241f, + 0.7425f, + 0.9269f, + 1.2976f, + 2.0945f, + 2.2014f, + 2.6204f, + 2.6959f + }, + + new[] + { + 0.1963f, + 0.2900f, + 0.4131f, + 0.8397f, + 1.2171f, + 1.3705f, + 2.0665f, + 2.1546f, + 2.4640f, + 2.5782f + }, + + new[] + { + 0.3387f, + 0.4415f, + 0.6121f, + 0.8005f, + 0.9507f, + 1.0937f, + 2.0836f, + 2.2342f, + 2.3849f, + 2.5076f + }, + + new[] + { + 0.2362f, + 0.5876f, + 0.7574f, + 0.8804f, + 1.0961f, + 1.4240f, + 1.9519f, + 2.1742f, + 2.4935f, + 2.6493f + }, + + new[] + { + 0.2793f, + 0.4282f, + 0.6149f, + 0.8352f, + 1.0106f, + 1.1766f, + 1.8392f, + 2.0119f, + 2.6433f, + 2.7117f + }, + + new[] + { + 0.3603f, + 0.4604f, + 0.5955f, + 0.9251f, + 1.1006f, + 1.2572f, + 1.7688f, + 1.8607f, + 2.4687f, + 2.5623f + }, + + new[] + { + 0.3975f, + 0.5849f, + 0.8059f, + 0.9182f, + 1.0552f, + 1.1850f, + 1.6356f, + 1.9627f, + 2.3318f, + 2.4719f + }, + + new[] + { + 0.2231f, + 0.3192f, + 0.4256f, + 0.7373f, + 1.4831f, + 1.6874f, + 1.9765f, + 2.1097f, + 2.6152f, + 2.6906f + }, + + new[] + { + 0.1221f, + 0.2081f, + 0.3665f, + 0.7734f, + 1.0341f, + 1.2818f, + 1.8162f, + 2.0727f, + 2.4446f, + 2.7377f + }, + + new[] + { + 0.2010f, + 0.2791f, + 0.3796f, + 0.8845f, + 1.4030f, + 1.5615f, + 2.0538f, + 2.1567f, + 2.3171f, + 2.4686f + }, + + new[] + { + 0.2086f, + 0.3053f, + 0.4047f, + 0.8224f, + 1.0656f, + 1.2115f, + 1.9641f, + 2.0871f, + 2.2430f, + 2.4313f + }, + + new[] + { + 0.3203f, + 0.4285f, + 0.5467f, + 0.6891f, + 1.2039f, + 1.3569f, + 1.8578f, + 2.2055f, + 2.3906f, + 2.4881f + }, + + new[] + { + 0.3074f, + 0.4192f, + 0.5772f, + 0.7799f, + 0.9866f, + 1.1335f, + 1.6068f, + 2.2441f, + 2.4194f, + 2.5089f + }, + + new[] + { + 0.2108f, + 0.2910f, + 0.4993f, + 0.7695f, + 0.9528f, + 1.5681f, + 1.7838f, + 2.1495f, + 2.3522f, + 2.4636f + }, + + new[] + { + 0.3492f, + 0.4560f, + 0.5906f, + 0.7379f, + 0.8855f, + 1.0257f, + 1.7128f, + 1.9997f, + 2.2019f, + 2.3694f + }, + + new[] + { + 0.5185f, + 0.7316f, + 0.9708f, + 1.1954f, + 1.5066f, + 1.7887f, + 2.1396f, + 2.2918f, + 2.5429f, + 2.6489f + }, + + new[] + { + 0.4276f, + 0.4946f, + 0.6934f, + 0.8308f, + 0.9944f, + 1.4582f, + 2.0324f, + 2.1294f, + 2.4891f, + 2.6324f + }, + + new[] + { + 0.3847f, + 0.5973f, + 0.7202f, + 0.8787f, + 1.3938f, + 1.5959f, + 1.8463f, + 2.1574f, + 2.5050f, + 2.6687f + }, + + new[] + { + 0.4835f, + 0.5919f, + 0.7235f, + 0.8862f, + 1.0756f, + 1.2853f, + 1.9118f, + 2.0215f, + 2.2213f, + 2.4638f + }, + + new[] + { + 0.5492f, + 0.8062f, + 0.9810f, + 1.1293f, + 1.3189f, + 1.5415f, + 1.9385f, + 2.1378f, + 2.4439f, + 2.5691f + }, + + new[] + { + 0.5190f, + 0.6764f, + 0.8123f, + 1.0154f, + 1.2085f, + 1.4266f, + 1.8433f, + 2.0866f, + 2.5113f, + 2.6474f + }, + + new[] + { + 0.4602f, + 0.6503f, + 0.9602f, + 1.1427f, + 1.3043f, + 1.4427f, + 1.6676f, + 1.8758f, + 2.2868f, + 2.4271f + }, + + new[] + { + 0.3764f, + 0.4845f, + 0.7627f, + 0.9914f, + 1.1961f, + 1.3421f, + 1.5129f, + 1.6707f, + 2.1836f, + 2.3322f + }, + + new[] + { + 0.3334f, + 0.5701f, + 0.8622f, + 1.1232f, + 1.3851f, + 1.6767f, + 2.0600f, + 2.2946f, + 2.5375f, + 2.7295f + }, + + new[] + { + 0.1449f, + 0.2719f, + 0.5783f, + 0.8807f, + 1.1746f, + 1.5422f, + 1.8804f, + 2.1934f, + 2.4734f, + 2.8728f + }, + + new[] + { + 0.2333f, + 0.3024f, + 0.4780f, + 1.2327f, + 1.4180f, + 1.5815f, + 1.9804f, + 2.0921f, + 2.3524f, + 2.5304f + }, + + new[] + { + 0.2154f, + 0.3075f, + 0.4746f, + 0.8477f, + 1.1170f, + 1.5369f, + 1.9847f, + 2.0733f, + 2.1880f, + 2.2504f + }, + + new[] + { + 0.1709f, + 0.4486f, + 0.8705f, + 1.0643f, + 1.3047f, + 1.5269f, + 1.9175f, + 2.1621f, + 2.4073f, + 2.5718f + }, + + new[] + { + 0.2835f, + 0.3752f, + 0.5234f, + 0.9898f, + 1.1484f, + 1.2974f, + 1.9363f, + 2.0378f, + 2.4065f, + 2.6214f + }, + + new[] + { + 0.3211f, + 0.4077f, + 0.5809f, + 1.0206f, + 1.2542f, + 1.3835f, + 1.5723f, + 2.1209f, + 2.3464f, + 2.4336f + }, + + new[] + { + 0.2101f, + 0.3146f, + 0.6779f, + 0.8783f, + 1.0561f, + 1.3045f, + 1.8395f, + 2.0695f, + 2.2831f, + 2.4328f + } + }; + + /*Second Stage Codebook*/ + + public static readonly float[ /* NC1 */][ /* M */] lspcb2 = + { + new[] + { + -0.0532f, + -0.0995f, + -0.0906f, + 0.1261f, + -0.0633f, + 0.0711f, + -0.1467f, + 0.1012f, + 0.0106f, + 0.0470f + }, + + new[] + { + -0.1017f, + -0.1088f, + 0.0566f, + -0.0010f, + -0.1528f, + 0.1771f, + 0.0089f, + -0.0282f, + 0.1055f, + 0.0808f + }, + + new[] + { + -0.1247f, + 0.0283f, + -0.0374f, + 0.0393f, + -0.0269f, + -0.0200f, + -0.0643f, + -0.0921f, + -0.1994f, + 0.0327f + }, + + new[] + { + 0.0070f, + -0.0242f, + -0.0415f, + -0.0041f, + -0.1793f, + 0.0700f, + 0.0972f, + -0.0207f, + -0.0771f, + 0.0997f + }, + + new[] + { + 0.0209f, + -0.0428f, + 0.0359f, + 0.2027f, + 0.0554f, + 0.0634f, + 0.0356f, + 0.0195f, + -0.0782f, + -0.1583f + }, + + new[] + { + -0.0856f, + -0.1028f, + -0.0071f, + 0.1160f, + 0.1089f, + 0.1892f, + 0.0874f, + 0.0644f, + -0.0872f, + -0.0236f + }, + + new[] + { + 0.0713f, + 0.0039f, + -0.0353f, + 0.0435f, + -0.0407f, + -0.0558f, + 0.0748f, + -0.0346f, + -0.1686f, + -0.0905f + }, + + new[] + { + -0.0134f, + -0.0987f, + 0.0283f, + 0.0095f, + -0.0107f, + -0.0420f, + 0.1638f, + 0.1328f, + -0.0799f, + -0.0695f + }, + + new[] + { + -0.1049f, + 0.1510f, + 0.0672f, + 0.1043f, + 0.0872f, + -0.0663f, + -0.2139f, + -0.0239f, + -0.0120f, + -0.0338f + }, + + new[] + { + -0.1071f, + -0.1165f, + -0.1524f, + -0.0365f, + 0.0260f, + -0.0288f, + -0.0889f, + 0.1159f, + 0.1852f, + 0.1093f + }, + + new[] + { + -0.0094f, + 0.0420f, + -0.0758f, + 0.0932f, + 0.0505f, + 0.0614f, + -0.0443f, + -0.1172f, + -0.0590f, + 0.1693f + }, + + new[] + { + -0.0384f, + -0.0375f, + -0.0313f, + -0.1539f, + -0.0524f, + 0.0550f, + -0.0569f, + -0.0133f, + 0.1233f, + 0.2714f + }, + + new[] + { + 0.0869f, + 0.0847f, + 0.0637f, + 0.0794f, + 0.1594f, + -0.0035f, + -0.0462f, + 0.0909f, + -0.1227f, + 0.0294f + }, + + new[] + { + -0.0137f, + -0.0332f, + -0.0611f, + 0.1156f, + 0.2116f, + 0.0332f, + -0.0019f, + 0.1110f, + -0.0317f, + 0.2061f + }, + + new[] + { + 0.0703f, + -0.0013f, + -0.0572f, + -0.0243f, + 0.1345f, + -0.1235f, + 0.0710f, + -0.0065f, + -0.0912f, + 0.1072f + }, + + new[] + { + 0.0178f, + -0.0349f, + -0.1563f, + -0.0487f, + 0.0044f, + -0.0609f, + -0.1682f, + 0.0023f, + -0.0542f, + 0.1811f + }, + + new[] + { + -0.1384f, + -0.1020f, + 0.1649f, + 0.1568f, + -0.0116f, + 0.1240f, + -0.0271f, + 0.0541f, + 0.0455f, + -0.0433f + }, + + new[] + { + -0.1782f, + -0.1511f, + 0.0509f, + -0.0261f, + 0.0570f, + 0.0817f, + 0.0805f, + 0.2003f, + 0.1138f, + 0.0653f + }, + + new[] + { + -0.0019f, + 0.0081f, + 0.0572f, + 0.1245f, + -0.0914f, + 0.1691f, + -0.0223f, + -0.1108f, + -0.0881f, + -0.0320f + }, + + new[] + { + -0.0413f, + 0.0181f, + 0.1764f, + 0.0092f, + -0.0928f, + 0.0695f, + 0.1523f, + 0.0412f, + 0.0508f, + -0.0148f + }, + + new[] + { + 0.0476f, + 0.0292f, + 0.1915f, + 0.1198f, + 0.0139f, + 0.0451f, + -0.1225f, + -0.0619f, + -0.0717f, + -0.1104f + }, + + new[] + { + -0.0382f, + -0.0120f, + 0.1159f, + 0.0039f, + 0.1348f, + 0.0088f, + -0.0173f, + 0.1789f, + 0.0078f, + -0.0959f + }, + + new[] + { + 0.1376f, + 0.0713f, + 0.1020f, + 0.0339f, + -0.1415f, + 0.0254f, + 0.0368f, + -0.1077f, + 0.0143f, + -0.0494f + }, + + new[] + { + 0.0658f, + -0.0140f, + 0.1046f, + -0.0603f, + 0.0273f, + -0.1114f, + 0.0761f, + -0.0093f, + 0.0338f, + -0.0538f + }, + + new[] + { + 0.2683f, + 0.2853f, + 0.1549f, + 0.0819f, + 0.0372f, + -0.0327f, + -0.0642f, + 0.0172f, + 0.1077f, + -0.0170f + }, + + new[] + { + -0.1949f, + 0.0672f, + 0.0978f, + -0.0557f, + -0.0069f, + -0.0851f, + 0.1057f, + 0.1294f, + 0.0505f, + 0.0545f + }, + + new[] + { + 0.1409f, + 0.0724f, + -0.0094f, + 0.1511f, + -0.0039f, + 0.0710f, + -0.1266f, + -0.1093f, + 0.0817f, + 0.0363f + }, + + new[] + { + 0.0485f, + 0.0682f, + 0.0248f, + -0.0974f, + -0.1122f, + 0.0004f, + 0.0845f, + -0.0357f, + 0.1282f, + 0.0955f + }, + + new[] + { + 0.0408f, + 0.1801f, + 0.0772f, + -0.0098f, + 0.0059f, + -0.1296f, + -0.0591f, + 0.0443f, + -0.0729f, + -0.1041f + }, + + new[] + { + -0.0666f, + -0.0403f, + -0.0524f, + -0.0831f, + 0.1384f, + -0.1443f, + -0.0909f, + 0.1636f, + 0.0320f, + 0.0077f + }, + + new[] + { + 0.1612f, + 0.1010f, + -0.0486f, + -0.0704f, + 0.0417f, + -0.0945f, + -0.0590f, + -0.1523f, + -0.0086f, + 0.0120f + }, + + new[] + { + -0.0199f, + 0.0823f, + -0.0014f, + -0.1082f, + 0.0649f, + -0.1374f, + -0.0324f, + -0.0296f, + 0.0885f, + 0.1141f + } + }; + + public static readonly float[ /* M */] lwindow = + { + 0.99879038f, + 0.99546894f, + 0.98995779f, + 0.98229335f, + 0.97252620f, + 0.96072035f, + 0.94695264f, + 0.93131180f, + 0.91389754f, + 0.89481964f + }; + + public static readonly int[ /* NCODE1 */] map1 = + { + 5, + 1, + 4, + 7, + 3, + 0, + 6, + 2 + }; + + public static readonly int[ /* NCODE2 */] map2 = + { + 4, + 6, + 0, + 2, + 12, + 14, + 8, + 10, + 15, + 11, + 9, + 13, + 7, + 3, + 1, + 5 + }; + + public static readonly float[ /* 4 */] pred = + { + 0.68f, + 0.58f, + 0.34f, + 0.19f + }; - new[] - { - 0.1612f, - 0.1010f, - -0.0486f, - -0.0704f, - 0.0417f, - -0.0945f, - -0.0590f, - -0.1523f, - -0.0086f, - 0.0120f - }, + public static readonly float[ /* SIZ_TAB_HUP_L */] tab_hup_l = + { + -0.001246f, + 0.002200f, + -0.004791f, + 0.009621f, + -0.017685f, + 0.031212f, + -0.057225f, + 0.135470f, + 0.973955f, + -0.103495f, + 0.048663f, + -0.027090f, + 0.015280f, + -0.008160f, + 0.003961f, + -0.001827f, + -0.002388f, + 0.004479f, + -0.009715f, + 0.019261f, + -0.035118f, + 0.061945f, + -0.115187f, + 0.294161f, + 0.898322f, + -0.170283f, + 0.083211f, + -0.046645f, + 0.026210f, + -0.013854f, + 0.006641f, + -0.003099f, + -0.003277f, + 0.006456f, + -0.013906f, + 0.027229f, + -0.049283f, + 0.086990f, + -0.164590f, + 0.464041f, + 0.780309f, + -0.199879f, + 0.100795f, + -0.056792f, + 0.031761f, + -0.016606f, + 0.007866f, + -0.003740f, + -0.003770f, + 0.007714f, + -0.016462f, + 0.031849f, + -0.057272f, + 0.101294f, + -0.195755f, + 0.630993f, + 0.630993f, + -0.195755f, + 0.101294f, + -0.057272f, + 0.031849f, + -0.016462f, + 0.007714f, + -0.003770f, + -0.003740f, + 0.007866f, + -0.016606f, + 0.031761f, + -0.056792f, + 0.100795f, + -0.199879f, + 0.780309f, + 0.464041f, + -0.164590f, + 0.086990f, + -0.049283f, + 0.027229f, + -0.013906f, + 0.006456f, + -0.003277f, + -0.003099f, + 0.006641f, + -0.013854f, + 0.026210f, + -0.046645f, + 0.083211f, + -0.170283f, + 0.898322f, + 0.294161f, + -0.115187f, + 0.061945f, + -0.035118f, + 0.019261f, + -0.009715f, + 0.004479f, + -0.002388f, + -0.001827f, + 0.003961f, + -0.008160f, + 0.015280f, + -0.027090f, + 0.048663f, + -0.103495f, + 0.973955f, + 0.135470f, + -0.057225f, + 0.031212f, + -0.017685f, + 0.009621f, + -0.004791f, + 0.002200f, + -0.001246f + }; + + public static readonly float[ /* SIZ_TAB_HUP_S */] tab_hup_s = + { + -0.005772f, + 0.087669f, + 0.965882f, + -0.048753f, + -0.014793f, + 0.214886f, + 0.868791f, + -0.065537f, + -0.028507f, + 0.374334f, + 0.723418f, + -0.060834f, + -0.045567f, + 0.550847f, + 0.550847f, + -0.045567f, + -0.060834f, + 0.723418f, + 0.374334f, + -0.028507f, + -0.065537f, + 0.868791f, + 0.214886f, + -0.014793f, + -0.048753f, + 0.965882f, + 0.087669f, + -0.005772f + }; + + public static readonly float[ /* NCODE1-NCAN1 */] thr1 = + { + 0.659681f, + 0.755274f, + 1.207205f, + 1.987740f + }; - new[] - { - -0.0199f, - 0.0823f, - -0.0014f, - -0.1082f, - 0.0649f, - -0.1374f, - -0.0324f, - -0.0296f, - 0.0885f, - 0.1141f - } - }; - - public static float[ /* M */] lwindow = - { - 0.99879038f, - 0.99546894f, - 0.98995779f, - 0.98229335f, - 0.97252620f, - 0.96072035f, - 0.94695264f, - 0.93131180f, - 0.91389754f, - 0.89481964f - }; - - public static int[ /* NCODE1 */] map1 = - { - 5, - 1, - 4, - 7, - 3, - 0, - 6, - 2 - }; - - public static int[ /* NCODE2 */] map2 = - { - 4, - 6, - 0, - 2, - 12, - 14, - 8, - 10, - 15, - 11, - 9, - 13, - 7, - 3, - 1, - 5 - }; - - public static float[ /* 4 */] pred = - { - 0.68f, - 0.58f, - 0.34f, - 0.19f - }; - - public static float[ /* SIZ_TAB_HUP_L */] tab_hup_l = - { - -0.001246f, - 0.002200f, - -0.004791f, - 0.009621f, - -0.017685f, - 0.031212f, - -0.057225f, - 0.135470f, - 0.973955f, - -0.103495f, - 0.048663f, - -0.027090f, - 0.015280f, - -0.008160f, - 0.003961f, - -0.001827f, - -0.002388f, - 0.004479f, - -0.009715f, - 0.019261f, - -0.035118f, - 0.061945f, - -0.115187f, - 0.294161f, - 0.898322f, - -0.170283f, - 0.083211f, - -0.046645f, - 0.026210f, - -0.013854f, - 0.006641f, - -0.003099f, - -0.003277f, - 0.006456f, - -0.013906f, - 0.027229f, - -0.049283f, - 0.086990f, - -0.164590f, - 0.464041f, - 0.780309f, - -0.199879f, - 0.100795f, - -0.056792f, - 0.031761f, - -0.016606f, - 0.007866f, - -0.003740f, - -0.003770f, - 0.007714f, - -0.016462f, - 0.031849f, - -0.057272f, - 0.101294f, - -0.195755f, - 0.630993f, - 0.630993f, - -0.195755f, - 0.101294f, - -0.057272f, - 0.031849f, - -0.016462f, - 0.007714f, - -0.003770f, - -0.003740f, - 0.007866f, - -0.016606f, - 0.031761f, - -0.056792f, - 0.100795f, - -0.199879f, - 0.780309f, - 0.464041f, - -0.164590f, - 0.086990f, - -0.049283f, - 0.027229f, - -0.013906f, - 0.006456f, - -0.003277f, - -0.003099f, - 0.006641f, - -0.013854f, - 0.026210f, - -0.046645f, - 0.083211f, - -0.170283f, - 0.898322f, - 0.294161f, - -0.115187f, - 0.061945f, - -0.035118f, - 0.019261f, - -0.009715f, - 0.004479f, - -0.002388f, - -0.001827f, - 0.003961f, - -0.008160f, - 0.015280f, - -0.027090f, - 0.048663f, - -0.103495f, - 0.973955f, - 0.135470f, - -0.057225f, - 0.031212f, - -0.017685f, - 0.009621f, - -0.004791f, - 0.002200f, - -0.001246f - }; - - public static float[ /* SIZ_TAB_HUP_S */] tab_hup_s = - { - -0.005772f, - 0.087669f, - 0.965882f, - -0.048753f, - -0.014793f, - 0.214886f, - 0.868791f, - -0.065537f, - -0.028507f, - 0.374334f, - 0.723418f, - -0.060834f, - -0.045567f, - 0.550847f, - 0.550847f, - -0.045567f, - -0.060834f, - 0.723418f, - 0.374334f, - -0.028507f, - -0.065537f, - 0.868791f, - 0.214886f, - -0.014793f, - -0.048753f, - 0.965882f, - 0.087669f, - -0.005772f - }; - - public static float[ /* NCODE1-NCAN1 */] thr1 = - { - 0.659681f, - 0.755274f, - 1.207205f, - 1.987740f - }; - - public static float[ /* NCODE2-NCAN2 */] thr2 = - { - 0.429912f, - 0.494045f, - 0.618737f, - 0.650676f, - 0.717949f, - 0.770050f, - 0.850628f, - 0.932089f - }; - } -} \ No newline at end of file + public static readonly float[ /* NCODE2-NCAN2 */] thr2 = + { + 0.429912f, + 0.494045f, + 0.618737f, + 0.650676f, + 0.717949f, + 0.770050f, + 0.850628f, + 0.932089f + }; +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Taming.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Taming.cs index a8a09711da..fa601a299b 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Taming.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Taming.cs @@ -24,131 +24,159 @@ * * @author Lubomir Marinov (translation of ITU-T C source code to Java) */ -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +internal sealed class Taming { - internal class Taming - { - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : TAMING.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : TAMING.C +Used for the floating point version of both +G.729 main body and G.729A */ - private readonly float[] exc_err = new float[4]; + private readonly float[] exc_err = new float[4]; - public void init_exc_err() + public void init_exc_err() + { + int i; + for (i = 0; i < 4; i++) { - int i; - for (i = 0; i < 4; i++) exc_err[i] = 1.0f; + exc_err[i] = 1.0f; } + } - /** - * Computes the accumulated potential error in the - * adaptive codebook contribution - * - * @param t0 (i) integer part of pitch delay - * @param t0_frac (i) fractional part of pitch delay - * @return flag set to 1 if taming is necessary - */ + /** +* Computes the accumulated potential error in the +* adaptive codebook contribution +* +* @param t0 (i) integer part of pitch delay +* @param t0_frac (i) fractional part of pitch delay +* @return flag set to 1 if taming is necessary +*/ - public int test_err( - int t0, - int t0_frac - ) - { - var INV_L_SUBFR = Ld8k.INV_L_SUBFR; - var L_INTER10 = Ld8k.L_INTER10; - var L_SUBFR = Ld8k.L_SUBFR; - var THRESH_ERR = Ld8k.THRESH_ERR; + public int test_err( + int t0, + int t0_frac + ) + { + var INV_L_SUBFR = Ld8k.INV_L_SUBFR; + var L_INTER10 = Ld8k.L_INTER10; + var L_SUBFR = Ld8k.L_SUBFR; + var THRESH_ERR = Ld8k.THRESH_ERR; - int i, t1, zone1, zone2, flag; - float maxloc; + int i, t1, zone1, zone2, flag; + float maxloc; - t1 = t0_frac > 0 ? t0 + 1 : t0; + t1 = t0_frac > 0 ? t0 + 1 : t0; - i = t1 - L_SUBFR - L_INTER10; - if (i < 0) i = 0; - zone1 = (int)(i * INV_L_SUBFR); + i = t1 - L_SUBFR - L_INTER10; + if (i < 0) + { + i = 0; + } - i = t1 + L_INTER10 - 2; - zone2 = (int)(i * INV_L_SUBFR); + zone1 = (int)(i * INV_L_SUBFR); - maxloc = -1.0f; - flag = 0; - for (i = zone2; i >= zone1; i--) - if (exc_err[i] > maxloc) - maxloc = exc_err[i]; - if (maxloc > THRESH_ERR) - flag = 1; - return flag; - } + i = t1 + L_INTER10 - 2; + zone2 = (int)(i * INV_L_SUBFR); - /** - * Maintains the memory used to compute the error - * function due to an adaptive codebook mismatch between encoder and - * decoder - * - * @param gain_pit (i) pitch gain - * @param t0 (i) integer part of pitch delay - */ + maxloc = -1.0f; + flag = 0; + for (i = zone2; i >= zone1; i--) + { + if (exc_err[i] > maxloc) + { + maxloc = exc_err[i]; + } + } - public void update_exc_err( - float gain_pit, - int t0 - ) + if (maxloc > THRESH_ERR) { - var INV_L_SUBFR = Ld8k.INV_L_SUBFR; - var L_SUBFR = Ld8k.L_SUBFR; + flag = 1; + } - int i, zone1, zone2, n; - float worst, temp; + return flag; + } - worst = (float)-1.0; + /** +* Maintains the memory used to compute the error +* function due to an adaptive codebook mismatch between encoder and +* decoder +* +* @param gain_pit (i) pitch gain +* @param t0 (i) integer part of pitch delay +*/ - n = t0 - L_SUBFR; - if (n < 0) + public void update_exc_err( + float gain_pit, + int t0 + ) + { + var INV_L_SUBFR = Ld8k.INV_L_SUBFR; + var L_SUBFR = Ld8k.L_SUBFR; + + int i, zone1, zone2, n; + float worst, temp; + + worst = (float)-1.0; + + n = t0 - L_SUBFR; + if (n < 0) + { + temp = 1.0f + gain_pit * exc_err[0]; + if (temp > worst) { - temp = 1.0f + gain_pit * exc_err[0]; - if (temp > worst) worst = temp; - temp = 1.0f + gain_pit * temp; - if (temp > worst) worst = temp; + worst = temp; } - else + temp = 1.0f + gain_pit * temp; + if (temp > worst) { - zone1 = (int)(n * INV_L_SUBFR); + worst = temp; + } + } - i = t0 - 1; - zone2 = (int)(i * INV_L_SUBFR); + else + { + zone1 = (int)(n * INV_L_SUBFR); + + i = t0 - 1; + zone2 = (int)(i * INV_L_SUBFR); - for (i = zone1; i <= zone2; i++) + for (i = zone1; i <= zone2; i++) + { + temp = 1.0f + gain_pit * exc_err[i]; + if (temp > worst) { - temp = 1.0f + gain_pit * exc_err[i]; - if (temp > worst) worst = temp; + worst = temp; } } + } - for (i = 3; i >= 1; i--) exc_err[i] = exc_err[i - 1]; - exc_err[0] = worst; + for (i = 3; i >= 1; i--) + { + exc_err[i] = exc_err[i - 1]; } + + exc_err[0] = worst; } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Codec/Util.cs b/src/SIPSorcery/app/Media/Codecs/G729Codec/Util.cs index 5b4aa554a4..f37729d820 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Codec/Util.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Codec/Util.cs @@ -26,189 +26,144 @@ */ using System.IO; -namespace SIPSorcery.Media.G729Codec +namespace SIPSorcery.Media.G729Codec; + +public static class Util { - public class Util - { - /* Random generator */ - private static short seed = 21845; + /* Random generator */ + private static short seed = 21845; - /* ITU-T G.729 Software Package Release 2 (November 2006) */ - /* - ITU-T G.729 Annex C - Reference C code for floating point - implementation of G.729 - Version 1.01 of 15.September.98 + /* ITU-T G.729 Software Package Release 2 (November 2006) */ + /* +ITU-T G.729 Annex C - Reference C code for floating point + implementation of G.729 + Version 1.01 of 15.September.98 */ - /* + /* ---------------------------------------------------------------------- - COPYRIGHT NOTICE + COPYRIGHT NOTICE ---------------------------------------------------------------------- - ITU-T G.729 Annex C ANSI C source code - Copyright (C) 1998, AT&T, France Telecom, NTT, University of - Sherbrooke. All rights reserved. +ITU-T G.729 Annex C ANSI C source code +Copyright (C) 1998, AT&T, France Telecom, NTT, University of +Sherbrooke. All rights reserved. ---------------------------------------------------------------------- */ - /* - File : UTIL.C - Used for the floating point version of both - G.729 main body and G.729A + /* +File : UTIL.C +Used for the floating point version of both +G.729 main body and G.729A */ - /** - * Assigns the value zero to element of the specified array of floats. - * The number of components set to zero equal to the length argument. - * - * @param x (o) : vector to clear - * @param L (i) : length of vector - */ - public static void set_zero( - float[] x, - int L - ) - { - set_zero(x, 0, L); - } - - /** - * Assigns the value zero to element of the specified array of floats. - * The number of components set to zero equal to the length argument. - * The components at positions offset through offset+length-1 in the - * array are set to zero. - * - * @param x (o) : vector to clear - * @param offset (i) : offset of vector - * @param length (i) : length of vector - */ - public static void set_zero(float[] x, int offset, int length) - { - for (int i = offset, toIndex = offset + length; i < toIndex; i++) - x[i] = 0.0f; - } - - /** - * Copies an array from the specified x array, to the specified y array. - * The number of components copied is equal to the length argument. - * - * @param x (i) : input vector - * @param y (o) : output vector - * @param L (i) : vector length - */ - public static void copy( - float[] x, - float[] y, - int L - ) - { - copy(x, 0, y, L); - } - - /** - * Copies an array from the specified source array, - * beginning at the specified destination array. - * A subsequence of array components are copied from the source array referenced - * by x to the destination array referenced by y. - * The number of components copied is equal to the length argument. - * The components at positions x_offset through x_offset+length-1 in the source - * array are copied into positions 0 through length-1, - * respectively, of the destination array. - * - * @param x (i) : input vector - * @param x_offset (i) : input vector offset - * @param y (o) : output vector - * @param L (i) : vector length - */ - public static void copy(float[] x, int x_offset, float[] y, int L) - { - copy(x, x_offset, y, 0, L); - } + /** +* Assigns the value zero to element of the specified array of floats. +* The number of components set to zero equal to the length argument. +* +* @param x (o) : vector to clear +* @param L (i) : length of vector +*/ + public static void set_zero( + float[] x, + int L + ) + { + set_zero(x, 0, L); + } - /** - * Copies an array from the specified source array, - * beginning at the specified position, - * to the specified position of the destination array. - * A subsequence of array components are copied from the source array referenced - * by x to the destination array referenced by y. - * The number of components copied is equal to the length argument. - * The components at positions x_offset through x_offset+length-1 in the source - * array are copied into positions y_offset through y_offset+length-1, - * respectively, of the destination array. - * - * @param x (i) : input vector - * @param x_offset (i) : input vector offset - * @param y (o) : output vector - * @param y_offset (i) : output vector offset - * @param L (i) : vector length - */ - public static void copy(float[] x, int x_offset, float[] y, int y_offset, int L) + /** +* Assigns the value zero to element of the specified array of floats. +* The number of components set to zero equal to the length argument. +* The components at positions offset through offset+length-1 in the +* array are set to zero. +* +* @param x (o) : vector to clear +* @param offset (i) : offset of vector +* @param length (i) : length of vector +*/ + public static void set_zero(float[] x, int offset, int length) + { + for (int i = offset, toIndex = offset + length; i < toIndex; i++) { - int i; - - for (i = 0; i < L; i++) - y[y_offset + i] = x[x_offset + i]; + x[i] = 0.0f; } + } - /** - * Return random short. - * - * @return random short - */ - public static short random_g729() - { - seed = (short)(seed * 31821L + 13849L); + /** +* Copies an array from the specified x array, to the specified y array. +* The number of components copied is equal to the length argument. +* +* @param x (i) : input vector +* @param y (o) : output vector +* @param L (i) : vector length +*/ + public static void copy( + float[] x, + float[] y, + int L + ) + { + copy(x, 0, y, L); + } - return seed; - } + /** +* Copies an array from the specified source array, +* beginning at the specified destination array. +* A subsequence of array components are copied from the source array referenced +* by x to the destination array referenced by y. +* The number of components copied is equal to the length argument. +* The components at positions x_offset through x_offset+length-1 in the source +* array are copied into positions 0 through length-1, +* respectively, of the destination array. +* +* @param x (i) : input vector +* @param x_offset (i) : input vector offset +* @param y (o) : output vector +* @param L (i) : vector length +*/ + public static void copy(float[] x, int x_offset, float[] y, int L) + { + copy(x, x_offset, y, 0, L); + } - /** - * Write data in fp - * - * @param data - * @param length - * @param fp - * @throws java.io.IOException - */ - public static void fwrite(short[] data, int length, Stream fp) + /** +* Copies an array from the specified source array, +* beginning at the specified position, +* to the specified position of the destination array. +* A subsequence of array components are copied from the source array referenced +* by x to the destination array referenced by y. +* The number of components copied is equal to the length argument. +* The components at positions x_offset through x_offset+length-1 in the source +* array are copied into positions y_offset through y_offset+length-1, +* respectively, of the destination array. +* +* @param x (i) : input vector +* @param x_offset (i) : input vector offset +* @param y (o) : output vector +* @param y_offset (i) : output vector offset +* @param L (i) : vector length +*/ + public static void copy(float[] x, int x_offset, float[] y, int y_offset, int L) + { + int i; + for (i = 0; i < L; i++) { - var bytes = new byte[2]; - - for (var i = 0; i < length; i++) - { - int value = data[i]; - bytes[0] = (byte)(value & 0xFF); - bytes[1] = (byte)(value >> 8); - fp.Write(bytes, 0, bytes.Length); - } + y[y_offset + i] = x[x_offset + i]; } + } - /** - * Read data from fp. - * - * @param data - * @param length - * @param fp - * @return length of resulting data array - * @throws java.io.IOException - */ - public static int fread(short[] data, int length, Stream fp) + /** +* Return random short. +* +* @return random short +*/ + public static short random_g729() + { + seed = (short)(seed * 31821L + 13849L); - { - var bytes = new byte[2]; - var readLength = 0; - - for (var i = 0; i < length; i++) - { - if (fp.Read(bytes, 0, bytes.Length) != 2) - break; - data[i] = (short)((bytes[1] << 8) | (bytes[0] & 0x00FF)); - readLength++; - } - - return readLength; - } + return seed; } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Decoder.cs b/src/SIPSorcery/app/Media/Codecs/G729Decoder.cs index bd8af07991..bbf50685a1 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Decoder.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Decoder.cs @@ -43,211 +43,235 @@ Sherbrooke. All rights reserved. */ using System; -using System.IO; +using System.Buffers; +using System.Runtime.InteropServices; +using CommunityToolkit.HighPerformance.Buffers; using SIPSorcery.Media.G729Codec; -namespace SIPSorcery.Media -{ - public class G729Decoder : Ld8k - { - - /** - * Synthesis parameters + BFI - */ - private readonly float[] Az_dec = new float[2 * MP1]; - - /** - * DecLd8k reference - */ - private readonly DecLd8k decLd8k = new DecLd8k(); - - /** - * Synthesis parameters + BFI - */ - private readonly int[] parm = new int[PRM_SIZE + 1]; +namespace SIPSorcery.Media; - /** - * Postfil reference - */ - private readonly Postfil postfil = new Postfil(); - - /** - * PostPro reference - */ - private readonly PostPro postPro = new PostPro(); - - /** - * postfilter output - */ - private readonly float[] pst_out = new float[L_FRAME]; - - /** - * Synthesis - */ - private readonly float[] synth; +public class G729Decoder : Ld8k +{ - /** - * Synthesis - */ - private readonly float[] synth_buf = new float[L_FRAME + M]; + /** + * Synthesis parameters + BFI + */ + private readonly float[] Az_dec = new float[2 * MP1]; + + /** + * DecLd8k reference + */ + private readonly DecLd8k decLd8k = new DecLd8k(); + + /** + * Synthesis parameters + BFI + */ + private readonly int[] parm = new int[PRM_SIZE + 1]; + + /** + * Postfil reference + */ + private readonly Postfil postfil = new Postfil(); + + /** + * PostPro reference + */ + private readonly PostPro postPro = new PostPro(); + + /** + * postfilter output + */ + private readonly float[] pst_out = new float[L_FRAME]; + + /** + * Synthesis + */ + private readonly float[] synth; + + /** + * Synthesis + */ + private readonly float[] synth_buf = new float[L_FRAME + M]; + + /** + * Synthesis + */ + private readonly int synth_offset; + + /** + * voicing for previous subframe + */ + private int voicing; + + /** + * Initialization of decoder + */ + + public G729Decoder() + { - /** - * Synthesis - */ - private readonly int synth_offset; + synth = synth_buf; + synth_offset = M; - /** - * voicing for previous subframe - */ - private int voicing; + decLd8k.init_decod_ld8k(); + postfil.init_post_filter(); + postPro.init_post_process(); - /** - * Initialization of decoder - */ + voicing = 60; + } - public G729Decoder() + /** + * Converts floats array into shorts span. + * + * @param floats + * @param shorts + */ + private static void floats2shorts(ReadOnlySpan floats, Span shorts) + { + for (var i = 0; i < floats.Length && i < shorts.Length; i++) { + /* round and convert to int */ + var f = floats[i]; + if (f >= 0.0f) + { + f += 0.5f; + } + else + { + f -= 0.5f; + } - synth = synth_buf; - synth_offset = M; - - decLd8k.init_decod_ld8k(); - postfil.init_post_filter(); - postPro.init_post_process(); - - voicing = 60; - } + if (f > 32767.0f) + { + f = 32767.0f; + } - /** - * Converts floats array into shorts array. - * - * @param floats - * @param shorts - */ - private static void floats2shorts(float[] floats, short[] shorts) - { - for (var i = 0; i < floats.Length; i++) + if (f < -32768.0f) { - /* round and convert to int */ - var f = floats[i]; - if (f >= 0.0f) - f += 0.5f; - else - f -= 0.5f; - if (f > 32767.0f) - f = 32767.0f; - if (f < -32768.0f) - f = -32768.0f; - shorts[i] = (short)f; + f = -32768.0f; } + + shorts[i] = (short)f; } + } - private void depacketize(byte[] inFrame, int inFrameOffset, short[] serial) + private void depacketize(ReadOnlySpan inFrame, int inFrameOffset, Span serial) + { + serial[0] = SYNC_WORD; + serial[1] = SIZE_WORD; + for (var s = 0; s < L_FRAME; s++) { - serial[0] = SYNC_WORD; - serial[1] = SIZE_WORD; - for (var s = 0; s < L_FRAME; s++) - { - int in_ = inFrame[inFrameOffset + s / 8]; + int in_ = inFrame[inFrameOffset + s / 8]; - in_ &= 1 << (7 - s % 8); - serial[2 + s] = 0 != in_ ? BIT_1 : BIT_0; - } + in_ &= 1 << (7 - s % 8); + serial[2 + s] = 0 != in_ ? BIT_1 : BIT_0; } + } + + /** + * Process SERIAL_SIZE short of speech using spans. + * + * @param serial input : serial array encoded in bits_ld8k + * @param sp16 output : speech short span + */ + private void ProcessPacket(ReadOnlySpan serial, Span sp16) + { + Bits.bits2prm_ld8k(serial, 2, parm, 1); - /** - * Process SERIAL_SIZE short of speech. - * - * @param serial input : serial array encoded in bits_ld8k - * @param sp16 output : speech short array + /* the hardware detects frame erasures by checking if all bits + * are set to zero */ - private void ProcessPacket(short[] serial, short[] sp16) + parm[0] = 0; /* No frame erasure */ + for (var i = 2; i < SERIAL_SIZE; i++) { - Bits.bits2prm_ld8k(serial, 2, parm, 1); + if (serial[i] == 0) + { + parm[0] = 1; /* frame erased */ + } + } - /* the hardware detects frame erasures by checking if all bits - * are set to zero - */ - parm[0] = 0; /* No frame erasure */ - for (var i = 2; i < SERIAL_SIZE; i++) - if (serial[i] == 0) - parm[0] = 1; /* frame erased */ + /* check parity and put 1 in parm[4] if parity error */ - /* check parity and put 1 in parm[4] if parity error */ + parm[4] = PParity.check_parity_pitch(parm[3], parm[4]); - parm[4] = PParity.check_parity_pitch(parm[3], parm[4]); + var t0_first = decLd8k.decod_ld8k(parm, voicing, synth, synth_offset, Az_dec); /* Decoder */ - var t0_first = decLd8k.decod_ld8k(parm, voicing, synth, synth_offset, Az_dec); /* Decoder */ + /* Post-filter and decision on voicing parameter */ + voicing = 0; - /* Post-filter and decision on voicing parameter */ - voicing = 0; + var ptr_Az = Az_dec; /* Decoded Az for post-filter */ + var ptr_Az_offset = 0; - var ptr_Az = Az_dec; /* Decoded Az for post-filter */ - var ptr_Az_offset = 0; + for (var i = 0; i < L_FRAME; i += L_SUBFR) + { + int sf_voic; /* voicing for subframe */ - for (var i = 0; i < L_FRAME; i += L_SUBFR) + sf_voic = postfil.post(t0_first, synth, synth_offset + i, ptr_Az, ptr_Az_offset, pst_out, i); + if (sf_voic != 0) { - int sf_voic; /* voicing for subframe */ - - sf_voic = postfil.post(t0_first, synth, synth_offset + i, ptr_Az, ptr_Az_offset, pst_out, i); - if (sf_voic != 0) - voicing = sf_voic; - ptr_Az_offset += MP1; + voicing = sf_voic; } + ptr_Az_offset += MP1; + } - Util.copy(synth_buf, L_FRAME, synth_buf, M); + Util.copy(synth_buf, L_FRAME, synth_buf, M); - postPro.post_process(pst_out, L_FRAME); + postPro.post_process(pst_out, L_FRAME); - floats2shorts(pst_out, sp16); - } + floats2shorts(pst_out, sp16); + } - /** - * Main decoder routine - * Usage :Decoder bitstream_file outputspeech_file - * - * Format for bitstream_file: - * One (2-byte) synchronization word - * One (2-byte) size word, - * 80 words (2-byte) containing 80 bits. - * - * Format for outputspeech_file: - * Synthesis is written to a binary file of 16 bits data. - * - * @param args bitstream_file outputspeech_file - * @throws java.io.IOException - */ - public byte[] Process(byte[] source) - { - var serial = new short[SERIAL_SIZE]; /* Serial stream */ - var sp16 = new short[L_FRAME]; /* Buffer to write 16 bits speech */ - var speech = new byte[L_FRAME * 2]; - var output = new MemoryStream(); + /// + /// Decodes G729 audio using spans and IBufferWriter for efficient memory usage. + /// + /// Input encoded data + /// IBufferWriter to receive decoded PCM samples + /// Number of samples written + /// + /// Main decoder routine + /// Usage :Decoder bitstream_file outputspeech_file + /// + /// Format for bitstream_file: + /// One(2-byte) synchronization word + /// One(2-byte) size word, + /// 80 words(2-byte) containing 80 bits. + /// + /// Format for outputspeech_file: + /// Synthesis is written to a binary file of 16 bits data. + /// + /// @param args bitstream_file outputspeech_file + /// @throws java.io.IOException + /// + public int Process(ReadOnlySpan source, IBufferWriter destination) + { + // Use stackalloc for the serial buffer since it's very small (82 shorts = 164 bytes) + Span serial = stackalloc short[SERIAL_SIZE]; + var samplesWritten = 0; + Span sp16 = stackalloc short[L_FRAME]; - /*-----------------------------------------------------------------* - * Loop for each "L_FRAME" speech data * - *-----------------------------------------------------------------*/ + /*-----------------------------------------------------------------* + * Loop for each "L_FRAME" speech data * + *-----------------------------------------------------------------*/ - var frame = 0; - try - { - // Iterate over each frame - int i; - for (i = 0; i <= source.Length - L_FRAME / 8 /* must have a complete frame left */; i += L_FRAME / 8) - { - frame++; - depacketize(source, i, serial); - ProcessPacket(serial, sp16); - Buffer.BlockCopy(sp16, 0, speech, 0, speech.Length); - output.Write(speech, 0, speech.Length); - } - } - catch (Exception) + try + { + // Iterate over each frame + int i; + for (i = 0; i <= source.Length - L_FRAME / 8 /* must have a complete frame left */; i += L_FRAME / 8) { - // No logging as we could get huge log files if any issues arises decoding - } + depacketize(source, i, serial); - return output.ToArray(); + // Get span for this frame's output + ProcessPacket(serial, sp16); + destination.Write(MemoryMarshal.AsBytes(sp16)); + samplesWritten += L_FRAME; + } } + catch (Exception) + { + // No logging as we could get huge log files if any issues arises decoding + } + + return samplesWritten; } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/Codecs/G729Encoder.cs b/src/SIPSorcery/app/Media/Codecs/G729Encoder.cs index 20167d1eb1..e01833e698 100644 --- a/src/SIPSorcery/app/Media/Codecs/G729Encoder.cs +++ b/src/SIPSorcery/app/Media/Codecs/G729Encoder.cs @@ -43,163 +43,225 @@ Sherbrooke. All rights reserved. */ using System; -using System.IO; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using CommunityToolkit.HighPerformance.Buffers; using SIPSorcery.Media.G729Codec; -namespace SIPSorcery.Media +namespace SIPSorcery.Media; + +public class G729Encoder : Ld8k { - public class G729Encoder : Ld8k - { - /** - * Initialization of the coder. - */ + /** + * Initialization of the coder. + */ - private byte[] _leftover = new byte[0]; + private readonly ArrayPoolBufferWriter _leftover = new(); + private int _leftoverOffset; - /** - * Init the Ld8k Coder - */ - private readonly CodLd8k codLd8k = new CodLd8k(); + /** + * Init the Ld8k Coder + */ + private readonly CodLd8k codLd8k = new CodLd8k(); - /** - * Init the PreProc - */ - private readonly PreProc preProc = new PreProc(); + /** + * Init the PreProc + */ + private readonly PreProc preProc = new PreProc(); - /** - * Transmitted parameters - */ - private readonly int[] prm = new int[PRM_SIZE]; + /** + * Transmitted parameters + */ + private readonly int[] prm = new int[PRM_SIZE]; - public G729Encoder() + public G729Encoder() + { + preProc.init_pre_process(); + codLd8k.init_coder_ld8k(); /* Initialize the coder */ + } + + /// + /// Fills a span with a specified value from start to end index. + /// + private static void Fill(Span span, int start, int end, T value) + { + for (var i = start; i < end; i++) { - preProc.init_pre_process(); - codLd8k.init_coder_ld8k(); /* Initialize the coder */ + span[i] = value; } + } + + /// + /// Writes the encoded serial bits to the output packet using spans. + /// + /// Input serial bits as Span. + /// Output packet as Span. + private void packetize(ReadOnlySpan serial, Span outFrame) + { + Fill(outFrame, 0, L_FRAME / 8, (byte)0); - private static void Fill(T[] array, int start, int end, T value) + for (var s = 0; s < L_FRAME; s++) { - for (var i = start; i < end; i++) - array[i] = value; + if (BIT_1 == serial[2 + s]) + { + var o = s / 8; + int out_ = outFrame[o]; + + out_ |= 1 << (7 - s % 8); + outFrame[o] = (byte)(out_ & 0xFF); + } } + } + + /** + * Process L_FRAME short of speech. + * + * @param sp16 input : speach short array + * @param serial output : serial array encoded in bits_ld8k + */ + private void ProcessPacket(ReadOnlySpan sp16, Span serial) + { + var new_speech = codLd8k.new_speech; /* Pointer to new speech data */ + Debug.Assert(new_speech is { }); + var new_speech_offset = codLd8k.new_speech_offset; - private void packetize(short[] serial, byte[] outFrame, int outFrameOffset) + for (var i = 0; i < L_FRAME; i++) { - Fill(outFrame, outFrameOffset, outFrameOffset + L_FRAME / 8, (byte)0); + new_speech[new_speech_offset + i] = sp16[i]; + } - for (var s = 0; s < L_FRAME; s++) - if (BIT_1 == serial[2 + s]) - { - var o = outFrameOffset + s / 8; - int out_ = outFrame[o]; + preProc.pre_process(new_speech.AsSpan(), new_speech_offset, L_FRAME); - out_ |= 1 << (7 - s % 8); - outFrame[o] = (byte)(out_ & 0xFF); - } - } + codLd8k.coder_ld8k(prm); - /** - * Process L_FRAME short of speech. - * - * @param sp16 input : speach short array - * @param serial output : serial array encoded in bits_ld8k + Bits.prm2bits_ld8k(prm, serial); + } + + /// + /// Processes speech data using spans and writes output to an of . + /// + /// Input speech data as of . + /// Output buffer writer for encoded bytes. + /// + /// Format for speech_file: + /// Speech is read form a binary file of 16 bits data. + /// + /// Format for bitstream_file: + /// One word (2-bytes) to indicate erasure. + /// One word (2 bytes) to indicate bit rate + /// 80 words (2-bytes) containing 80 bits. + /// + [SkipLocalsInit] + public void Process(ReadOnlySpan speech, IBufferWriter output) + { + const int frameSizeInBytes = L_FRAME * 2; + Span frameBytes = stackalloc byte[frameSizeInBytes]; + Span serial = stackalloc short[SERIAL_SIZE]; + const int packetLength = L_FRAME / 8; + Span packet = stackalloc byte[packetLength]; + + // Combine leftover and new speech + _leftover.Write(speech); + var totalLength = _leftover.WrittenCount - _leftoverOffset; + var framesToProcess = totalLength / frameSizeInBytes; + var processedBytes = 0; + + /*-------------------------------------------------------------------------* + * Loop for every analysis/transmission frame. * + * -New L_FRAME data are read. (L_FRAME = number of speech data per frame) * + * -Conversion of the speech data from 16 bit integer to real * + * -Call cod_ld8k to encode the speech. * + * -The compressed serial output stream is written to a file. * + * -The synthesis speech is written to a file * + *-------------------------------------------------------------------------* */ - private void ProcessPacket(short[] sp16, short[] serial) + + for (var frame = 0; frame < framesToProcess; frame++) { - var new_speech = codLd8k.new_speech; /* Pointer to new speech data */ - var new_speech_offset = codLd8k.new_speech_offset; + _leftover.WrittenSpan.Slice(_leftoverOffset + processedBytes, frameSizeInBytes).CopyTo(frameBytes); + processedBytes += frameSizeInBytes; - for (var i = 0; i < L_FRAME; i++) - new_speech[new_speech_offset + i] = sp16[i]; + var sp16 = MemoryMarshal.Cast(frameBytes); - preProc.pre_process(new_speech, new_speech_offset, L_FRAME); - codLd8k.coder_ld8k(prm); + ProcessPacket(sp16, serial); + packet.Clear(); + packetize(serial, packet); + output.GetSpan(packetLength).Slice(0, packetLength).CopyTo(packet); + output.Advance(packetLength); + } - Bits.prm2bits_ld8k(prm, serial); + // Keep only unprocessed bytes in the leftover buffer. + _leftoverOffset += processedBytes; + CompactLeftoverIfNeeded(); + } - } + /// + /// Flushes any remaining buffered audio, encoding and writing to the provided output buffer. + /// + /// Output buffer writer for encoded bytes. + [SkipLocalsInit] + public void Flush(IBufferWriter output) + { + var leftoverLength = _leftover.WrittenCount - _leftoverOffset; - /** - * Usage : coder speech_file bitstream_file - * - * Format for speech_file: - * Speech is read form a binary file of 16 bits data. - * - * Format for bitstream_file: - * One word (2-bytes) to indicate erasure. - * One word (2 bytes) to indicate bit rate - * 80 words (2-bytes) containing 80 bits. - * - * @param args speech_file bitstream_file - * @throws java.io.IOException - */ - public byte[] Process(byte[] speech) + if (leftoverLength > 0) { - var sp16 = new short[L_FRAME]; /* Buffer to read 16 bits speech */ - var serial = new short[SERIAL_SIZE]; /* Output bit stream buffer */ - var packet = new byte[L_FRAME / 8]; - var output = new MemoryStream(); - var buffer = new MemoryStream(); - - buffer.Write(_leftover, 0, _leftover.Length); - buffer.Write(speech, 0, speech.Length); - var input = buffer.ToArray(); - - /*-------------------------------------------------------------------------* - * Loop for every analysis/transmission frame. * - * -New L_FRAME data are read. (L_FRAME = number of speech data per frame) * - * -Conversion of the speech data from 16 bit integer to real * - * -Call cod_ld8k to encode the speech. * - * -The compressed serial output stream is written to a file. * - * -The synthesis speech is written to a file * - *-------------------------------------------------------------------------* - */ - - var frame = 0; - try - { - // Iterate over each frame - int i; - for (i = 0; i <= input.Length - L_FRAME * 2 /* must have a complete frame left */; i += L_FRAME * 2) - { - frame++; - Buffer.BlockCopy(input, i, sp16, 0, L_FRAME * 2); - ProcessPacket(sp16, serial); - packetize(serial, packet, 0); - output.Write(packet, 0, packet.Length); - } - - _leftover = new byte[input.Length - i]; - Array.Copy(input, i, _leftover, 0, _leftover.Length); - } - catch (Exception) + const int frameSizeInBytes = L_FRAME * 2; + Span frameBytes = stackalloc byte[frameSizeInBytes]; + Debug.Assert(leftoverLength <= frameSizeInBytes); + // Copy leftover bytes into frameBytes + _leftover.WrittenSpan.Slice(_leftoverOffset, leftoverLength).CopyTo(frameBytes.Slice(0, leftoverLength)); + // Zero-fill any missing bytes + if (leftoverLength < frameSizeInBytes) { - // No logging as we could get huge log files if any issues arises decoding + frameBytes.Slice(leftoverLength, frameSizeInBytes - leftoverLength).Clear(); } + var sp16 = MemoryMarshal.Cast(frameBytes); + Span serial = stackalloc short[SERIAL_SIZE]; + Span packet = stackalloc byte[L_FRAME / 8]; + ProcessPacket(sp16, serial); + packet.Clear(); + packetize(serial, packet); + output.GetSpan(packet.Length).Slice(0, packet.Length).CopyTo(packet); + output.Advance(packet.Length); - return output.ToArray(); + _leftover.Clear(); + _leftoverOffset = 0; } + } - // TODO: pad out _leftover with silence, and return one last frame - public byte[] Flush() + private void CompactLeftoverIfNeeded() + { + var remaining = _leftover.WrittenCount - _leftoverOffset; + + if (remaining <= 0) { - var output = new MemoryStream(); - if (_leftover.Length > 0) - { - var sp16 = new short[L_FRAME]; /* Buffer to read 16 bits speech */ - var serial = new short[SERIAL_SIZE]; /* Output bit stream buffer */ - var packet = new byte[L_FRAME / 8]; - - Buffer.BlockCopy(_leftover, 0, sp16, 0, _leftover.Length); - Fill(sp16, _leftover.Length / 2, sp16.Length, (short)0); - ProcessPacket(sp16, serial); - packetize(serial, packet, 0); - output.Write(packet, 0, packet.Length); - } + _leftover.Clear(); + _leftoverOffset = 0; + return; + } - return output.ToArray(); + if (_leftoverOffset <= 0) + { + return; + } + + var rented = ArrayPool.Shared.Rent(remaining); + + try + { + _leftover.WrittenSpan.Slice(_leftoverOffset, remaining).CopyTo(rented.AsSpan(0, remaining)); + _leftover.Clear(); + _leftover.Write(rented.AsSpan(0, remaining)); + _leftoverOffset = 0; + } + finally + { + ArrayPool.Shared.Return(rented); } } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/Media/EchoMediaSession.cs b/src/SIPSorcery/app/Media/EchoMediaSession.cs index c1874b6a9b..da42e28bd6 100644 --- a/src/SIPSorcery/app/Media/EchoMediaSession.cs +++ b/src/SIPSorcery/app/Media/EchoMediaSession.cs @@ -19,6 +19,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; +using CommunityToolkit.HighPerformance.Buffers; using Microsoft.Extensions.Logging; using SIPSorcery.Net; using SIPSorcery.SIP.App; @@ -54,7 +55,7 @@ private void AudioFormatsNegotiated(List audoFormats) var audioFormat = audoFormats.First(); logger.LogDebug("{session} setting audio source format to {FormatID}:{Codec} {ClockRate} (RTP clock rate {RtpClockRate}).", nameof(EchoMediaSession), audioFormat.FormatID, audioFormat.Codec, audioFormat.ClockRate, audioFormat.RtpClockRate); - if (AudioStream != null && AudioStream.LocalTrack.NoDtmfSupport == false) + if (AudioStream?.LocalTrack?.NoDtmfSupport == false) { logger.LogDebug("Audio track negotiated DTMF payload ID {AudioStreamNegotiatedRtpEventPayloadID}.", AudioStream.NegotiatedRtpEventPayloadID); } @@ -64,7 +65,7 @@ private void AudioFormatsNegotiated(List audoFormats) AudioStream.OnAudioFrameReceived += (audioFrame) => { // Echo the received audio frame back to the sender. - AudioStream?.SendAudio(audioFrame.DurationMilliSeconds, audioFrame.EncodedAudio); + AudioStream?.SendAudio(audioFrame.DurationMilliSeconds, audioFrame.EncodedAudio.Span); }; } } @@ -81,7 +82,7 @@ private void VideoFormatsNegotiated(List videoFormats) if (VideoStream != null && VideoStream.RemoteTrack != null && VideoStream.LocalTrack != null) { - VideoStream.OnVideoFrameReceivedByIndex += (int index, IPEndPoint from, uint ts, byte[] payload, VideoFormat format) => + VideoStream.OnVideoFrameReceivedByIndex += (index, from, ts, payload, format) => { // TODO. logger.LogWarning("Video frame received, echoing not yet implemented."); @@ -98,7 +99,7 @@ private void TextFormatsNegotiated(List textFormats) { TextStream.OnRtpPacketReceivedByIndex += (int index, IPEndPoint from, SDPMediaTypesEnum mediaType, RTPPacket pkt) => { - TextStream.SendText(pkt.Payload); + TextStream.SendText(pkt.Payload.Span); }; } } @@ -127,7 +128,9 @@ private void EncodeAndSend(short[] pcm, AudioFormat audioFormat) { if (pcm.Length > 0) { - byte[] encodedSample = _audioEncoder.EncodeAudio(pcm, audioFormat); + using var buffer = new ArrayPoolBufferWriter(8192); + _audioEncoder.EncodeAudio(pcm, audioFormat, buffer); + var encodedSample = buffer.WrittenSpan; uint rtpUnits = RtpTimestampExtensions.ToRtpUnits(SILENCE_SAMPLE_PERIOD_MILLISECONDS, audioFormat.RtpClockRate); diff --git a/src/SIPSorcery/app/Media/IMediaSession.cs b/src/SIPSorcery/app/Media/IMediaSession.cs index 2514d6af1b..47f0ec2aba 100644 --- a/src/SIPSorcery/app/Media/IMediaSession.cs +++ b/src/SIPSorcery/app/Media/IMediaSession.cs @@ -19,139 +19,138 @@ using System.Threading.Tasks; using SIPSorcery.Net; -namespace SIPSorcery.SIP.App +namespace SIPSorcery.SIP.App; + +/// +/// The type of the SDP packet being set. +/// +public enum SdpType +{ + answer = 0, + offer = 1 +} + +/// +/// Offering and Answering SDP messages so that it can be +/// signaled to the other party using the SIPUserAgent. +/// +/// The implementing class is responsible for ensuring that the client +/// can send media to the other party including creating and managing +/// the RTP streams and processing the audio and video. +/// +public interface IMediaSession { /// - /// The type of the SDP packet being set. + /// Indicates whether the session supports real time text. + /// + bool HasText { get; } + + /// + /// Indicates whether the session supports audio. + /// + bool HasAudio { get; } + + /// + /// Indicates whether the session supports video. + /// + bool HasVideo { get; } + + /// + /// Indicates whether the session has been closed. + /// + bool IsClosed { get; } + + /// + /// The SDP description from the remote party describing + /// their audio/video sending and receive capabilities. + /// + SDP? RemoteDescription { get; } + + /// + /// Set if the session has been bound to a specific IP address. + /// Normally not required but some esoteric call or network set ups may need. + /// + IPAddress RtpBindAddress { get; } + + /// + /// Fired when the RTP channel is closed. /// - public enum SdpType - { - answer = 0, - offer = 1 - } - - /// - /// Offering and Answering SDP messages so that it can be - /// signaled to the other party using the SIPUserAgent. - /// - /// The implementing class is responsible for ensuring that the client - /// can send media to the other party including creating and managing - /// the RTP streams and processing the audio and video. + event Action? OnRtpClosed; + + /// + /// Fired when an RTP event (typically representing a DTMF tone) is + /// detected. + /// + event Action OnRtpEvent; + + /// + /// Fired when no RTP or RTCP packets are received for a pre-defined period (typically 30s). + /// + event Action OnTimeout; + + /// + /// Creates a new SDP offer based on the local media tracks in the session. + /// Calling this method does NOT change the state of the media tracks. It is + /// safe to call at any time if a session description of the local media state is + /// required. + /// + /// Optional. If set this address will be used + /// as the Connection address in the SDP offer. If not set an attempt will be + /// made to determine the best matching address. + /// A new SDP offer representing the session's local media tracks. + SDP? CreateOffer(IPAddress? connectionAddress = default); + + /// + /// Sets the remote description. Calling this method can result in the local + /// media tracks being disabled if not supported or setting the RTP/RTCP end points + /// if they are. + /// + /// Whether the SDP being set is an offer or answer. + /// The SDP description from the remote party. + /// If successful an OK enum result. If not an enum result indicating the + /// failure cause. + SetDescriptionResultEnum SetRemoteDescription(SdpType sdpType, SDP sessionDescription); + + /// + /// Generates an SDP answer to an offer based on the local media tracks. Calling + /// this method does NOT result in any changes to the local tracks. To apply the + /// changes the SetRemoteDescription method must be called. + /// + /// Optional. If set this address will be used as + /// the SDP Connection address. If not specified the Operating System routing table + /// will be used to lookup the address used to connect to the SDP connection address + /// from the remote offer. + /// An SDP answer matching the offer and the local media tracks contained + /// in the session. + SDP? CreateAnswer(IPAddress? connectionAddress = default); + + /// + /// Needs to be called prior to sending media. Performs any set up tasks such as + /// starting audio/video capture devices and starting RTCP reporting. + /// + Task Start(); + + /// + /// Sets the stream status on all local audio or all video media track. + /// + /// The type of the media track. Must be audio or video. + /// The stream status for the media track. + void SetMediaStreamStatus(SDPMediaTypesEnum kind, MediaStreamStatusEnum status); + + /// + /// Attempts to send a DTMF tone to the remote party. + /// + /// The digit representing the DTMF tone to send. + /// A cancellation token that should be set if the DTMF send should be + /// cancelled before completing. Depending on the duration a DTMF send can require + /// multiple RTP packets. This token can be used to cancel any further RTP packets + /// being sent for the tone. + Task SendDtmf(byte tone, CancellationToken ct); + + /// + /// Closes the session. This will stop any audio/video capturing and rendering devices as + /// well as the RTP and RTCP sessions and sockets. /// - public interface IMediaSession - { - /// - /// Indicates whether the session supports real time text. - /// - bool HasText { get; } - - /// - /// Indicates whether the session supports audio. - /// - bool HasAudio { get; } - - /// - /// Indicates whether the session supports video. - /// - bool HasVideo { get; } - - /// - /// Indicates whether the session has been closed. - /// - bool IsClosed { get; } - - /// - /// The SDP description from the remote party describing - /// their audio/video sending and receive capabilities. - /// - SDP RemoteDescription { get; } - - /// - /// Set if the session has been bound to a specific IP address. - /// Normally not required but some esoteric call or network set ups may need. - /// - IPAddress RtpBindAddress { get; } - - /// - /// Fired when the RTP channel is closed. - /// - event Action OnRtpClosed; - - /// - /// Fired when an RTP event (typically representing a DTMF tone) is - /// detected. - /// - event Action OnRtpEvent; - - /// - /// Fired when no RTP or RTCP packets are received for a pre-defined period (typically 30s). - /// - event Action OnTimeout; - - /// - /// Creates a new SDP offer based on the local media tracks in the session. - /// Calling this method does NOT change the state of the media tracks. It is - /// safe to call at any time if a session description of the local media state is - /// required. - /// - /// Optional. If set this address will be used - /// as the Connection address in the SDP offer. If not set an attempt will be - /// made to determine the best matching address. - /// A new SDP offer representing the session's local media tracks. - SDP CreateOffer(IPAddress connectionAddress = null); - - /// - /// Sets the remote description. Calling this method can result in the local - /// media tracks being disabled if not supported or setting the RTP/RTCP end points - /// if they are. - /// - /// Whether the SDP being set is an offer or answer. - /// The SDP description from the remote party. - /// If successful an OK enum result. If not an enum result indicating the - /// failure cause. - SetDescriptionResultEnum SetRemoteDescription(SdpType sdpType, SDP sessionDescription); - - /// - /// Generates an SDP answer to an offer based on the local media tracks. Calling - /// this method does NOT result in any changes to the local tracks. To apply the - /// changes the SetRemoteDescription method must be called. - /// - /// Optional. If set this address will be used as - /// the SDP Connection address. If not specified the Operating System routing table - /// will be used to lookup the address used to connect to the SDP connection address - /// from the remote offer. - /// An SDP answer matching the offer and the local media tracks contained - /// in the session. - SDP CreateAnswer(IPAddress connectionAddress = null); - - /// - /// Needs to be called prior to sending media. Performs any set up tasks such as - /// starting audio/video capture devices and starting RTCP reporting. - /// - Task Start(); - - /// - /// Sets the stream status on all local audio or all video media track. - /// - /// The type of the media track. Must be audio or video. - /// The stream status for the media track. - void SetMediaStreamStatus(SDPMediaTypesEnum kind, MediaStreamStatusEnum status); - - /// - /// Attempts to send a DTMF tone to the remote party. - /// - /// The digit representing the DTMF tone to send. - /// A cancellation token that should be set if the DTMF send should be - /// cancelled before completing. Depending on the duration a DTMF send can require - /// multiple RTP packets. This token can be used to cancel any further RTP packets - /// being sent for the tone. - Task SendDtmf(byte tone, CancellationToken ct); - - /// - /// Closes the session. This will stop any audio/video capturing and rendering devices as - /// well as the RTP and RTCP sessions and sockets. - /// - /// Optional. A descriptive reason for closing the session. - void Close(string reason); - } + /// Optional. A descriptive reason for closing the session. + void Close(string reason); } diff --git a/src/SIPSorcery/app/Media/MediaLoggingExtensions.cs b/src/SIPSorcery/app/Media/MediaLoggingExtensions.cs new file mode 100644 index 0000000000..57e32a4082 --- /dev/null +++ b/src/SIPSorcery/app/Media/MediaLoggingExtensions.cs @@ -0,0 +1,229 @@ +using System; +using System.Net; +using Microsoft.Extensions.Logging; +using SIPSorceryMedia.Abstractions; + +namespace SIPSorcery.Media; + +internal static partial class MediaLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "SettingAudioSourceFormat", + Level = LogLevel.Debug, + Message = "Setting audio source format to {AudioFormatID}:{AudioFormatCodec}.")] + public static partial void LogSettingAudioSourceFormat( + this ILogger logger, + int audioFormatID, + AudioCodecsEnum audioFormatCodec); + + [LoggerMessage( + EventId = 0, + EventName = "MusicFileNotSetOrFound", + Level = LogLevel.Warning, + Message = "Music file not set or not found, using default music resource.")] + public static partial void LogMusicFileNotSetOrFound( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SendingAudioSteamLength", + Level = LogLevel.Debug, + Message = "Sending audio stream length {AudioStreamLength}.")] + public static partial void LogSendingAudioSteamLength( + this ILogger logger, + long audioStreamLength); + + [LoggerMessage( + EventId = 0, + EventName = "RtpAudioPacketReceived", + Level = LogLevel.Trace, + Message = "audio RTP packet received from {RemoteEndPoint} ssrc {SyncSource} seqnum {SequenceNumber} timestamp {Timestamp} payload type {PayloadType}.")] + public static partial void LogRtpAudioPacketReceived( + this ILogger logger, + IPEndPoint remoteEndPoint, + uint syncSource, + ushort sequenceNumber, + uint timestamp, + int payloadType); + + [LoggerMessage( + EventId = 0, + EventName = "RtpTextPacketReceived", + Level = LogLevel.Trace, + Message = "RtpMediaPacketReceived text RTP packet received from {RemoteEndPoint} ssrc {SyncSource} seqnum {SequenceNumber} timestamp {Timestamp} payload type {PayloadType}.")] + public static partial void LogRtpTextPacketReceived( + this ILogger logger, + IPEndPoint remoteEndPoint, + uint syncSource, + ushort sequenceNumber, + uint timestamp, + int payloadType); + + [LoggerMessage( + EventId = 0, + EventName = "SendAudioFromStreamCompleted", + Level = LogLevel.Debug, + Message = "Send audio from stream completed.")] + public static partial void LogSendAudioFromStreamCompleted( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SettingAudioFormat", + Level = LogLevel.Debug, + Message = "Setting audio source format to {FormatID}:{Codec} {ClockRate} (RTP clock rate {RtpClockRate}).")] + public static partial void LogSettingAudioFormat( + this ILogger logger, + int formatID, + AudioCodecsEnum codec, + int clockRate, + int rtpClockRate); + + [LoggerMessage( + EventId = 0, + EventName = "SettingVideoFormat", + Level = LogLevel.Debug, + Message = "Setting video sink and source format to {VideoFormatID}:{VideoCodec}.")] + public static partial void LogSettingVideoFormat( + this ILogger logger, + int videoFormatID, + VideoCodecsEnum videoCodec); + + [LoggerMessage( + EventId = 0, + EventName = "TextFormatNegotiated", + Level = LogLevel.Debug, + Message = "Setting text sink and source format to {TextFormatID}:{TextCodec}")] + public static partial void LogTextFormatNegotiated( + this ILogger logger, + int textFormatID, + TextCodecsEnum textCodec); + + [LoggerMessage( + EventId = 0, + EventName = "AudioTrackDtmfNegotiated", + Level = LogLevel.Debug, + Message = "Audio track negotiated DTMF payload ID {AudioStreamNegotiatedRtpEventPayloadID}.")] + public static partial void LogAudioTrackDtmfNegotiated( + this ILogger logger, + int audioStreamNegotiatedRtpEventPayloadID); + + [LoggerMessage( + EventId = 0, + EventName = "VideoCaptureDeviceFailure", + Level = LogLevel.Warning, + Message = "Video source for capture device failure. {errorMessage}")] + public static partial void LogVideoCaptureDeviceFailure( + this ILogger logger, + string errorMessage); + + [LoggerMessage( + EventId = 0, + EventName = "WebcamVideoSourceFailed", + Level = LogLevel.Warning, + Message = "Webcam video source failed before start, switching to test pattern source.")] + public static partial void LogWebcamFailedSwitchingToPattern( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "StreamClosedWarning", + Level = LogLevel.Warning, + Message = "Stream Closed.")] + public static partial void LogStreamClosedWarning( + this ILogger logger, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "AudioStreamReadError", + Level = LogLevel.Warning, + Message = "Failed to read from audio stream source.")] + public static partial void LogAudioStreamReadError( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "AudioStreamNullError", + Level = LogLevel.Warning, + Message = "Failed to read from audio stream source, stream null or closed.")] + public static partial void LogAudioStreamNullError( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "UnhandledStreamException", + Level = LogLevel.Warning, + Message = "Caught unhandled exception")] + public static partial void LogUnhandledStreamException( + this ILogger logger, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StreamReaderCloseError", + Level = LogLevel.Warning, + Message = "Error occurred whilst trying to close the stream source reader.")] + public static partial void LogStreamReaderCloseError( + this ILogger logger, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "FrameRateError", + Level = LogLevel.Warning, + Message = "{framesPerSecond} fames per second not in the allowed range of {minimumFramesPerSecond} to {maximumFramesPerSecond}, ignoring.")] + public static partial void LogFrameRateError( + this ILogger logger, + int framesPerSecond, + int minimumFramesPerSecond, + int maximumFramesPerSecond); + + [LoggerMessage( + EventId = 0, + EventName = "SilenceSampleError", + Level = LogLevel.Error, + Message = "Exception sending silence sample")] + public static partial void LogSendingSilenceSampleError( + this ILogger logger, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "SignalGeneratorError", + Level = LogLevel.Error, + Message = "Exception sending signal generator sample")] + public static partial void LogSignalGeneratorError( + this ILogger logger, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "MediaEngineStartError", + Level = LogLevel.Error, + Message = "Error starting media engine. {ErrorMessage}")] + public static partial void LogMediaEngineStartError( + this ILogger logger, + string errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "MediaEngineStop", + Level = LogLevel.Debug, + Message = "Media engine stopped")] + public static partial void LogMediaEngineStop( + this ILogger logger); + + [LoggerMessage( + EventId = 1, + EventName = "SettingAudioFormatWarning", + Level = LogLevel.Warning, + Message = "{audioEncoder} input sample of length {inputSize} supplied to OPUS encoder exceeded maximum limit of {maxLimit}. Reduce sampling period.")] + public static partial void LogSettingAudioFormatWarning( + this ILogger logger, + string audioEncoder, + int inputSize, + int maxLimit); +} diff --git a/src/SIPSorcery/app/Media/Sources/AudioExtrasSource.cs b/src/SIPSorcery/app/Media/Sources/AudioExtrasSource.cs index f07533aa49..956dfa0c35 100644 --- a/src/SIPSorcery/app/Media/Sources/AudioExtrasSource.cs +++ b/src/SIPSorcery/app/Media/Sources/AudioExtrasSource.cs @@ -20,6 +20,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.IO; @@ -27,6 +29,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.HighPerformance.Buffers; using Microsoft.Extensions.Logging; using SIPSorcery.Net; using SIPSorceryMedia.Abstractions; @@ -157,7 +160,7 @@ public int AudioSamplePeriodMilliseconds get => _audioSamplePeriodMilliseconds; set { - if (value < AUDIO_SAMPLE_PERIOD_MILLISECONDS_MIN || value > AUDIO_SAMPLE_PERIOD_MILLISECONDS_MAX) + if (value is < AUDIO_SAMPLE_PERIOD_MILLISECONDS_MIN or > AUDIO_SAMPLE_PERIOD_MILLISECONDS_MAX) { throw new ApplicationException($"Invalid value for the audio sample period. Must be between {AUDIO_SAMPLE_PERIOD_MILLISECONDS_MIN} and {AUDIO_SAMPLE_PERIOD_MILLISECONDS_MAX}ms."); } @@ -324,9 +327,9 @@ public void SetSource(AudioSourceOptions sourceOptions) _sendSampleTimer = new Timer(SendSilenceSample); _sendSampleTimer.Change(0, _audioSamplePeriodMilliseconds); } - else if (_audioOpts.AudioSource == AudioSourcesEnum.PinkNoise || - _audioOpts.AudioSource == AudioSourcesEnum.WhiteNoise || - _audioOpts.AudioSource == AudioSourcesEnum.SineWave) + else if (_audioOpts.AudioSource is AudioSourcesEnum.PinkNoise or + AudioSourcesEnum.WhiteNoise or + AudioSourcesEnum.SineWave) { _signalGenerator = new SignalGenerator(_audioFormatManager.SelectedFormat.ClockRate, 1); @@ -567,7 +570,9 @@ private void EncodeAndSend(short[] pcm, int pcmSampleRate) pcm = PcmResampler.Resample(pcm, pcmSampleRate, _audioFormatManager.SelectedFormat.ClockRate); } - byte[] encodedSample = _audioEncoder.EncodeAudio(pcm, _audioFormatManager.SelectedFormat); + using var buffer = new ArrayPoolBufferWriter(8192); + _audioEncoder.EncodeAudio(pcm, _audioFormatManager.SelectedFormat, buffer); + var encodedSample = buffer.WrittenMemory; uint rtpUnits = RtpTimestampExtensions.ToRtpUnits(_audioSamplePeriodMilliseconds, _audioFormatManager.SelectedFormat.RtpClockRate); diff --git a/src/SIPSorcery/app/Media/Sources/VideoTestPatternSource.cs b/src/SIPSorcery/app/Media/Sources/VideoTestPatternSource.cs index 9578dfe888..5bc73cc98b 100644 --- a/src/SIPSorcery/app/Media/Sources/VideoTestPatternSource.cs +++ b/src/SIPSorcery/app/Media/Sources/VideoTestPatternSource.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.Reflection; @@ -129,7 +131,7 @@ public Task InitialiseVideoSourceDevice() => public void SetFrameRate(int framesPerSecond) { - if (framesPerSecond < MINIMUM_FRAMES_PER_SECOND || framesPerSecond > MAXIMUM_FRAMES_PER_SECOND) + if (framesPerSecond is < MINIMUM_FRAMES_PER_SECOND or > MAXIMUM_FRAMES_PER_SECOND) { logger.LogWarning("{FramesPerSecond} fames per second not in the allowed range of {MinimumFramesPerSecond} to {MaximumFramesPerSecond}, ignoring.", framesPerSecond, MINIMUM_FRAMES_PER_SECOND, MAXIMUM_FRAMES_PER_SECOND); } @@ -246,8 +248,8 @@ private void GenerateTestPattern(object state) if (encodedBuffer != null) { - uint fps = (_frameSpacing > 0) ? 1000 / (uint)_frameSpacing : DEFAULT_FRAMES_PER_SECOND; - uint durationRtpTS = VIDEO_SAMPLING_RATE / fps; + var fps = (_frameSpacing > 0) ? 1000 / (uint)_frameSpacing : DEFAULT_FRAMES_PER_SECOND; + var durationRtpTS = VIDEO_SAMPLING_RATE / fps; // Use ?.Invoke so the null-check and the call are // a single atomic delegate read. Without this a // subscriber unsubscribing on another thread @@ -283,12 +285,12 @@ private void GenerateRawSample(int width, int height, byte[] i420Buffer) public static void StampI420Buffer(byte[] i420Buffer, int width, int height, int frameNumber) { // Draws a varying grey scale square in the bottom right corner on the base I420 buffer. - int startX = width - STAMP_BOX_SIZE - STAMP_BOX_PADDING; - int startY = height - STAMP_BOX_SIZE - STAMP_BOX_PADDING; + var startX = width - STAMP_BOX_SIZE - STAMP_BOX_PADDING; + var startY = height - STAMP_BOX_SIZE - STAMP_BOX_PADDING; - for (int y = startY; y < startY + STAMP_BOX_SIZE; y++) + for (var y = startY; y < startY + STAMP_BOX_SIZE; y++) { - for (int x = startX; x < startX + STAMP_BOX_SIZE; x++) + for (var x = startX; x < startX + STAMP_BOX_SIZE; x++) { i420Buffer[y * width + x] = (byte)(frameNumber % 255); } diff --git a/src/SIPSorcery/app/Media/VoIPMediaSession.cs b/src/SIPSorcery/app/Media/VoIPMediaSession.cs index 7c603c67ef..65df030fa7 100644 --- a/src/SIPSorcery/app/Media/VoIPMediaSession.cs +++ b/src/SIPSorcery/app/Media/VoIPMediaSession.cs @@ -17,6 +17,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.Linq; @@ -162,7 +164,7 @@ public VoIPMediaSession(VoIPMediaSessionConfig config) if (Media.AudioSink != null) { - base.OnAudioFrameReceived += Media.AudioSink.GotEncodedMediaFrame; + base.OnAudioFrameReceived += Media.AudioSink.GotEncodedMediaFrame; } if (Media.TextSink != null) @@ -339,7 +341,10 @@ protected void RtpMediaPacketReceived(IPEndPoint remoteEndPoint, SDPMediaTypesEn { logger.LogTrace(nameof(RtpMediaPacketReceived) + " text RTP packet received from {RemoteEndPoint} ssrc {SyncSource} seqnum {SequenceNumber} timestamp {Timestamp} payload type {PayloadType}.", remoteEndPoint, hdr.SyncSource, hdr.SequenceNumber, hdr.Timestamp, hdr.PayloadType); - Media.TextSink.GotTextRtp(remoteEndPoint, hdr.SyncSource, hdr.SequenceNumber, hdr.Timestamp, hdr.PayloadType, hdr.MarkerBit, rtpPacket.GetPayloadBytes()); + var rtpPacketBytes = new byte[rtpPacket.GetByteCount()]; + rtpPacket.WriteBytes(rtpPacketBytes); + + Media.TextSink.GotTextRtp(remoteEndPoint, hdr.SyncSource, hdr.SequenceNumber, hdr.Timestamp, hdr.PayloadType, hdr.MarkerBit, rtpPacketBytes); } else { diff --git a/src/SIPSorcery/app/Media/VoIPMediaSessionConfig.cs b/src/SIPSorcery/app/Media/VoIPMediaSessionConfig.cs index f96a9c698c..1710a61945 100644 --- a/src/SIPSorcery/app/Media/VoIPMediaSessionConfig.cs +++ b/src/SIPSorcery/app/Media/VoIPMediaSessionConfig.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System.Net; using SIPSorcery.Net; using SIPSorcery.Sys; @@ -36,4 +38,4 @@ public sealed class VoIPMediaSessionConfig public IAudioEncoder AudioExtrasEncoder { get; set; } = new AudioEncoder(); } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/app/SIPPacketMangler.cs b/src/SIPSorcery/app/SIPPacketMangler.cs index 80e6c57b0b..2a0667d234 100644 --- a/src/SIPSorcery/app/SIPPacketMangler.cs +++ b/src/SIPSorcery/app/SIPPacketMangler.cs @@ -16,6 +16,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using System.Net; using System.Net.Sockets; diff --git a/src/SIPSorcery/app/SIPRequestAuthoriser/SIPRequestAuthenticationResult.cs b/src/SIPSorcery/app/SIPRequestAuthoriser/SIPRequestAuthenticationResult.cs index c272e62572..dd1a2e17a4 100644 --- a/src/SIPSorcery/app/SIPRequestAuthoriser/SIPRequestAuthenticationResult.cs +++ b/src/SIPSorcery/app/SIPRequestAuthoriser/SIPRequestAuthenticationResult.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + namespace SIPSorcery.SIP.App { public class SIPRequestAuthenticationResult diff --git a/src/SIPSorcery/app/SIPRequestAuthoriser/SIPRequestAuthenticator.cs b/src/SIPSorcery/app/SIPRequestAuthoriser/SIPRequestAuthenticator.cs index 5d307309f9..13bdc0ca57 100644 --- a/src/SIPSorcery/app/SIPRequestAuthoriser/SIPRequestAuthenticator.cs +++ b/src/SIPSorcery/app/SIPRequestAuthoriser/SIPRequestAuthenticator.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Linq; using Microsoft.Extensions.Logging; diff --git a/src/SIPSorcery/app/SIPUserAgents/ISIPAccount.cs b/src/SIPSorcery/app/SIPUserAgents/ISIPAccount.cs index 8180e4e0a0..2cfe8162c2 100644 --- a/src/SIPSorcery/app/SIPUserAgents/ISIPAccount.cs +++ b/src/SIPSorcery/app/SIPUserAgents/ISIPAccount.cs @@ -17,6 +17,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; namespace SIPSorcery.SIP.App diff --git a/src/SIPSorcery/app/SIPUserAgents/ISIPClientUserAgent.cs b/src/SIPSorcery/app/SIPUserAgents/ISIPClientUserAgent.cs index 3600b2efab..abecf46d69 100644 --- a/src/SIPSorcery/app/SIPUserAgents/ISIPClientUserAgent.cs +++ b/src/SIPSorcery/app/SIPUserAgents/ISIPClientUserAgent.cs @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + namespace SIPSorcery.SIP.App { public delegate void SIPCallResponseDelegate(ISIPClientUserAgent uac, SIPResponse sipResponse); diff --git a/src/SIPSorcery/app/SIPUserAgents/ISIPServerUserAgent.cs b/src/SIPSorcery/app/SIPUserAgents/ISIPServerUserAgent.cs index e13ae16aad..da26f984df 100644 --- a/src/SIPSorcery/app/SIPUserAgents/ISIPServerUserAgent.cs +++ b/src/SIPSorcery/app/SIPUserAgents/ISIPServerUserAgent.cs @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + namespace SIPSorcery.SIP.App { public delegate void SIPUASDelegate(ISIPServerUserAgent uas); diff --git a/src/SIPSorcery/app/SIPUserAgents/SIPB2BUserAgent.cs b/src/SIPSorcery/app/SIPUserAgents/SIPB2BUserAgent.cs index 22d54d30f1..86f1f9adce 100644 --- a/src/SIPSorcery/app/SIPUserAgents/SIPB2BUserAgent.cs +++ b/src/SIPSorcery/app/SIPUserAgents/SIPB2BUserAgent.cs @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using Microsoft.Extensions.Logging; using SIPSorcery.Sys; diff --git a/src/SIPSorcery/app/SIPUserAgents/SIPCallDescriptor.cs b/src/SIPSorcery/app/SIPUserAgents/SIPCallDescriptor.cs index d610db82a9..7c191afaff 100644 --- a/src/SIPSorcery/app/SIPUserAgents/SIPCallDescriptor.cs +++ b/src/SIPSorcery/app/SIPUserAgents/SIPCallDescriptor.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using System.Collections.Generic; using System.Net; @@ -310,11 +312,11 @@ public void ParseCallOptions(string options) //{ // RedirectMode = SIPCallRedirectModesEnum.Replace; //} - if (redirectMode == "n" || redirectMode == "N") + if (redirectMode is "n" or "N") { RedirectMode = SIPCallRedirectModesEnum.NewDialPlan; } - else if (redirectMode == "m" || redirectMode == "M") + else if (redirectMode is "m" or "M") { RedirectMode = SIPCallRedirectModesEnum.Manual; } @@ -360,15 +362,15 @@ public void ParseCallOptions(string options) if (transferMatch.Success) { string transferMode = transferMatch.Result("${transfermode}"); - if (transferMode == "n" || transferMode == "N") + if (transferMode is "n" or "N") { TransferMode = SIPDialogueTransferModesEnum.NotAllowed; } - else if (transferMode == "p" || transferMode == "P") + else if (transferMode is "p" or "P") { TransferMode = SIPDialogueTransferModesEnum.PassThru; } - else if (transferMode == "c" || transferMode == "C") + else if (transferMode is "c" or "C") { TransferMode = SIPDialogueTransferModesEnum.BlindPlaceCall; } @@ -458,13 +460,13 @@ public static List ParseCustomHeaders(string customHeaders) //string headerValue = (customHeader.Length > colonIndex) ? customHeader.Substring(colonIndex + 1).Trim() : String.Empty; var trimmedCustomHeader = customHeader.AsSpan().Trim(); - if (trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_VIA, StringComparison.OrdinalIgnoreCase) || - trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_FROM, StringComparison.OrdinalIgnoreCase) || - trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CONTACT, StringComparison.OrdinalIgnoreCase) || - trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CSEQ, StringComparison.OrdinalIgnoreCase) || - trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CALLID, StringComparison.OrdinalIgnoreCase) || - trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_MAXFORWARDS, StringComparison.OrdinalIgnoreCase) || - trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CONTENTLENGTH, StringComparison.OrdinalIgnoreCase)) + if (SIPHeaders.SIP_HEADER_VIA.Equals(trimmedCustomHeader, StringComparison.OrdinalIgnoreCase) || + SIPHeaders.SIP_HEADER_FROM.Equals(trimmedCustomHeader, StringComparison.OrdinalIgnoreCase) || + SIPHeaders.SIP_HEADER_CONTACT.Equals(trimmedCustomHeader, StringComparison.OrdinalIgnoreCase) || + SIPHeaders.SIP_HEADER_CSEQ.Equals(trimmedCustomHeader, StringComparison.OrdinalIgnoreCase) || + SIPHeaders.SIP_HEADER_CALLID.Equals(trimmedCustomHeader, StringComparison.OrdinalIgnoreCase) || + SIPHeaders.SIP_HEADER_MAXFORWARDS.Equals(trimmedCustomHeader, StringComparison.OrdinalIgnoreCase) || + SIPHeaders.SIP_HEADER_CONTENTLENGTH.Equals(trimmedCustomHeader, StringComparison.OrdinalIgnoreCase)) { logger.LogWarning("ParseCustomHeaders skipping custom header due to an non-permitted string in header name, {CustomHeader}.", customHeader); continue; diff --git a/src/SIPSorcery/app/SIPUserAgents/SIPClientUserAgent.cs b/src/SIPSorcery/app/SIPUserAgents/SIPClientUserAgent.cs index ffda6e1313..1b87999436 100644 --- a/src/SIPSorcery/app/SIPUserAgents/SIPClientUserAgent.cs +++ b/src/SIPSorcery/app/SIPUserAgents/SIPClientUserAgent.cs @@ -16,6 +16,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.Linq; @@ -410,7 +412,7 @@ private Task ServerFinalResponseReceived(SIPEndPoint localSIPEndPoi #endregion } - else if (sipResponse.Status == SIPResponseStatusCodesEnum.ProxyAuthenticationRequired || sipResponse.Status == SIPResponseStatusCodesEnum.Unauthorised) + else if (sipResponse.Status is SIPResponseStatusCodesEnum.ProxyAuthenticationRequired or SIPResponseStatusCodesEnum.Unauthorised) { #region Authenticate client call to third party server. @@ -453,7 +455,7 @@ private Task ServerFinalResponseReceived(SIPEndPoint localSIPEndPoi } else { - if (sipResponse.StatusCode >= 200 && sipResponse.StatusCode <= 299) + if (sipResponse.StatusCode is >= 200 and <= 299) { m_sipDialogue = new SIPDialogue(m_serverTransaction); m_sipDialogue.CallDurationLimit = m_sipCallDescriptor.CallDurationLimit; @@ -482,7 +484,7 @@ private Task ServerInformationResponseReceived(SIPEndPoint localSIP } else { - if (sipResponse.Status == SIPResponseStatusCodesEnum.Ringing || sipResponse.Status == SIPResponseStatusCodesEnum.SessionProgress) + if (sipResponse.Status is SIPResponseStatusCodesEnum.Ringing or SIPResponseStatusCodesEnum.SessionProgress) { CallRinging?.Invoke(this, sipResponse); } @@ -527,7 +529,7 @@ private Task ByeServerFinalResponseReceived(SIPEndPoint localSIPEnd SIPNonInviteTransaction transaction = sipTransaction as SIPNonInviteTransaction; transaction.NonInviteTransactionFinalResponseReceived -= ByeServerFinalResponseReceived; - if (sipResponse.Status == SIPResponseStatusCodesEnum.ProxyAuthenticationRequired || sipResponse.Status == SIPResponseStatusCodesEnum.Unauthorised) + if (sipResponse.Status is SIPResponseStatusCodesEnum.ProxyAuthenticationRequired or SIPResponseStatusCodesEnum.Unauthorised) { var username = string.IsNullOrWhiteSpace(m_sipCallDescriptor.AuthUsername) ? m_sipCallDescriptor.Username : m_sipCallDescriptor.AuthUsername; var authRequest = transaction.TransactionRequest.DuplicateAndAuthenticate(sipResponse.Header.AuthenticationHeaders, diff --git a/src/SIPSorcery/app/SIPUserAgents/SIPNonInviteClientUserAgent.cs b/src/SIPSorcery/app/SIPUserAgents/SIPNonInviteClientUserAgent.cs index ca85518964..b874001b6e 100644 --- a/src/SIPSorcery/app/SIPUserAgents/SIPNonInviteClientUserAgent.cs +++ b/src/SIPSorcery/app/SIPUserAgents/SIPNonInviteClientUserAgent.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using System.Net.Sockets; using System.Threading; @@ -75,7 +77,7 @@ private Task ServerResponseReceived(SIPEndPoint localSIPEndPoint, S string reasonPhrase = (sipResponse.ReasonPhrase.IsNullOrBlank()) ? sipResponse.Status.ToString() : sipResponse.ReasonPhrase; logger.LogDebug("Server response {StatusCode} {ReasonPhrase} received for {Method} to {Uri}.", sipResponse.StatusCode, reasonPhrase, sipTransaction.TransactionRequest.Method, m_callDescriptor.Uri); - if (sipResponse.Status == SIPResponseStatusCodesEnum.ProxyAuthenticationRequired || sipResponse.Status == SIPResponseStatusCodesEnum.Unauthorised) + if (sipResponse.Status is SIPResponseStatusCodesEnum.ProxyAuthenticationRequired or SIPResponseStatusCodesEnum.Unauthorised) { if (sipResponse.Header.HasAuthenticationHeader) { diff --git a/src/SIPSorcery/app/SIPUserAgents/SIPNonInviteServerUserAgent.cs b/src/SIPSorcery/app/SIPUserAgents/SIPNonInviteServerUserAgent.cs index 86ff19044c..af1c10b440 100644 --- a/src/SIPSorcery/app/SIPUserAgents/SIPNonInviteServerUserAgent.cs +++ b/src/SIPSorcery/app/SIPUserAgents/SIPNonInviteServerUserAgent.cs @@ -16,6 +16,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; diff --git a/src/SIPSorcery/app/SIPUserAgents/SIPNotifierClient.cs b/src/SIPSorcery/app/SIPUserAgents/SIPNotifierClient.cs index 7661689d6d..167c9d225c 100644 --- a/src/SIPSorcery/app/SIPUserAgents/SIPNotifierClient.cs +++ b/src/SIPSorcery/app/SIPUserAgents/SIPNotifierClient.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using System.Collections.Generic; using System.Net.Sockets; @@ -328,7 +330,7 @@ private Task SubscribeTransactionFinalResponseReceived(SIPEndPoint SubscriptionFailed?.Invoke(m_resourceURI, sipResponse.Status, $"Subscribe failed with response {sipResponse.StatusCode} {sipResponse.ReasonPhrase}."); m_waitForSubscribeResponse.Set(); } - else if (sipResponse.Status == SIPResponseStatusCodesEnum.ProxyAuthenticationRequired || sipResponse.Status == SIPResponseStatusCodesEnum.Unauthorised) + else if (sipResponse.Status is SIPResponseStatusCodesEnum.ProxyAuthenticationRequired or SIPResponseStatusCodesEnum.Unauthorised) { if (m_authUsername.IsNullOrBlank() || m_authPassword.IsNullOrBlank()) { @@ -377,7 +379,7 @@ private Task SubscribeTransactionFinalResponseReceived(SIPEndPoint m_waitForSubscribeResponse.Set(); } } - else if (sipResponse.StatusCode >= 200 && sipResponse.StatusCode <= 299) + else if (sipResponse.StatusCode is >= 200 and <= 299) { logger.LogDebug("Authenticating subscribe request for event package {EventPackage} and {ResourceURI} was successful.", m_sipEventPackage, m_resourceURI); diff --git a/src/SIPSorcery/app/SIPUserAgents/SIPRegistrationUserAgent.cs b/src/SIPSorcery/app/SIPUserAgents/SIPRegistrationUserAgent.cs index 469861dca9..786d6e45cc 100644 --- a/src/SIPSorcery/app/SIPUserAgents/SIPRegistrationUserAgent.cs +++ b/src/SIPSorcery/app/SIPUserAgents/SIPRegistrationUserAgent.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using System.Collections.Generic; using System.Linq; @@ -150,7 +152,7 @@ public SIPRegistrationUserAgent( m_authUsername = username; m_password = password; m_registrarHost = server; - m_expiry = (expiry >= REGISTER_MINIMUM_EXPIRY && expiry <= MAX_EXPIRY) ? expiry : DEFAULT_REGISTER_EXPIRY; + m_expiry = (expiry is >= REGISTER_MINIMUM_EXPIRY and <= MAX_EXPIRY) ? expiry : DEFAULT_REGISTER_EXPIRY; m_originalExpiry = m_expiry; m_callID = Guid.NewGuid().ToString(); m_maxRegistrationAttemptTimeout = maxRegistrationAttemptTimeout; @@ -191,7 +193,7 @@ public SIPRegistrationUserAgent( m_realm = realm; m_registrarHost = registrarHost; m_contactURI = contactURI; - m_expiry = (expiry >= REGISTER_MINIMUM_EXPIRY && expiry <= MAX_EXPIRY) ? expiry : DEFAULT_REGISTER_EXPIRY; + m_expiry = (expiry is >= REGISTER_MINIMUM_EXPIRY and <= MAX_EXPIRY) ? expiry : DEFAULT_REGISTER_EXPIRY; m_originalExpiry = m_expiry; m_customHeaders = customHeaders; m_callID = CallProperties.CreateNewCallId(); @@ -314,7 +316,7 @@ IEnumerable splitMethodsString(string methodsString) /// The new expiry value. public void SetExpiry(int expiry) { - int newExpiry = (expiry >= REGISTER_MINIMUM_EXPIRY && expiry <= MAX_EXPIRY) ? expiry : DEFAULT_REGISTER_EXPIRY; + int newExpiry = (expiry is >= REGISTER_MINIMUM_EXPIRY and <= MAX_EXPIRY) ? expiry : DEFAULT_REGISTER_EXPIRY; if (newExpiry != m_expiry) { @@ -434,7 +436,7 @@ private void ServerResponseReceived(SIPEndPoint localSIPEndPoint, SIPEndPoint re { logger.LogDebug("Server response {SipResponseStatus} received for {SipAccountAOR}.", sipResponse.Status, m_sipAccountAOR); - if (sipResponse.Status == SIPResponseStatusCodesEnum.ProxyAuthenticationRequired || sipResponse.Status == SIPResponseStatusCodesEnum.Unauthorised) + if (sipResponse.Status is SIPResponseStatusCodesEnum.ProxyAuthenticationRequired or SIPResponseStatusCodesEnum.Unauthorised) { if (sipResponse.Header.HasAuthenticationHeader) { @@ -515,7 +517,7 @@ private void ServerResponseReceived(SIPEndPoint localSIPEndPoint, SIPEndPoint re m_waitForRegistrationMRE.Set(); } - else if (sipResponse.Status == SIPResponseStatusCodesEnum.Forbidden || sipResponse.Status == SIPResponseStatusCodesEnum.NotFound) + else if (sipResponse.Status is SIPResponseStatusCodesEnum.Forbidden or SIPResponseStatusCodesEnum.NotFound) { // SIP account does not appear to exist. m_exit = m_exitOnUnequivocalFailure; @@ -578,7 +580,7 @@ private void AuthResponseReceived(SIPEndPoint localSIPEndPoint, SIPEndPoint remo logger.LogDebug("Registration for {SIPAccountAOR} had a too short expiry, updated to {Expiry} and trying again.", m_sipAccountAOR, m_expiry); SendInitialRegister(); } - else if (sipResponse.Status == SIPResponseStatusCodesEnum.Forbidden || sipResponse.Status == SIPResponseStatusCodesEnum.NotFound || sipResponse.Status == SIPResponseStatusCodesEnum.PaymentRequired) + else if (sipResponse.Status is SIPResponseStatusCodesEnum.Forbidden or SIPResponseStatusCodesEnum.NotFound or SIPResponseStatusCodesEnum.PaymentRequired) { // SIP account does not appear to exist. m_exit = m_exitOnUnequivocalFailure; @@ -589,7 +591,7 @@ private void AuthResponseReceived(SIPEndPoint localSIPEndPoint, SIPEndPoint remo m_waitForRegistrationMRE.Set(); } - else if (sipResponse.Status == SIPResponseStatusCodesEnum.ProxyAuthenticationRequired || sipResponse.Status == SIPResponseStatusCodesEnum.Unauthorised) + else if (sipResponse.Status is SIPResponseStatusCodesEnum.ProxyAuthenticationRequired or SIPResponseStatusCodesEnum.Unauthorised) { // SIP account credentials failed. m_exit = m_exitOnUnequivocalFailure; diff --git a/src/SIPSorcery/app/SIPUserAgents/SIPServerUserAgent.cs b/src/SIPSorcery/app/SIPUserAgents/SIPServerUserAgent.cs index be8b72252e..0c9fb2a32d 100644 --- a/src/SIPSorcery/app/SIPUserAgents/SIPServerUserAgent.cs +++ b/src/SIPSorcery/app/SIPUserAgents/SIPServerUserAgent.cs @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Linq; using System.Net.Sockets; diff --git a/src/SIPSorcery/app/SIPUserAgents/SIPUserAgent.cs b/src/SIPSorcery/app/SIPUserAgents/SIPUserAgent.cs index 22c9aa8b2b..cdce9a19c6 100644 --- a/src/SIPSorcery/app/SIPUserAgents/SIPUserAgent.cs +++ b/src/SIPSorcery/app/SIPUserAgents/SIPUserAgent.cs @@ -22,6 +22,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.Linq; @@ -153,8 +155,8 @@ public bool IsCalling { if (!IsCallActive && m_uac != null && m_uac.ServerTransaction != null) { - return m_uac.ServerTransaction.TransactionState == SIPTransactionStatesEnum.Calling || - m_uac.ServerTransaction.TransactionState == SIPTransactionStatesEnum.Trying; + return m_uac.ServerTransaction.TransactionState is SIPTransactionStatesEnum.Calling or + SIPTransactionStatesEnum.Trying; } else { @@ -1751,7 +1753,7 @@ private async void ClientCallAnsweredHandler(ISIPClientUserAgent uac, SIPRespons { _ringTimeout?.Dispose(); - if (sipResponse.StatusCode >= 200 && sipResponse.StatusCode <= 299) + if (sipResponse.StatusCode is >= 200 and <= 299) { if (sipResponse.Body == null && ((MediaSession as RTPSession)?.IsAudioStarted ?? false)) { diff --git a/src/SIPSorcery/core/CallProperties.cs b/src/SIPSorcery/core/CallProperties.cs index 9eb91d3daf..cde0c65868 100644 --- a/src/SIPSorcery/core/CallProperties.cs +++ b/src/SIPSorcery/core/CallProperties.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: CallProperties.cs // // Description: Helper functions for setting SIP headers. @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using SIPSorcery.Sys; diff --git a/src/SIPSorcery/core/DNS/SIPDns.cs b/src/SIPSorcery/core/DNS/SIPDns.cs index c14028550a..d732c4b775 100644 --- a/src/SIPSorcery/core/DNS/SIPDns.cs +++ b/src/SIPSorcery/core/DNS/SIPDns.cs @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Linq; using System.Net; @@ -64,7 +66,7 @@ private class SipDNS { } public const int DNS_RETRIES_PER_SERVER = 1; public const int CACHE_FAILED_RESULTS_DURATION = 10; // Cache failed DNS responses for this duration in seconds. - private static readonly ILogger logger = LogFactory.CreateLogger(); + private static readonly ILogger logger = LogFactory.CreateLogger(typeof(SIPDns).FullName!); /// /// Don't use IN_ANY queries by default. These are useful if a DNS server supports them as they can diff --git a/src/SIPSorcery/core/SIP/Channels/SIPChannel.cs b/src/SIPSorcery/core/SIP/Channels/SIPChannel.cs index 967f2326f3..c9121ffe2b 100644 --- a/src/SIPSorcery/core/SIP/Channels/SIPChannel.cs +++ b/src/SIPSorcery/core/SIP/Channels/SIPChannel.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.Linq; @@ -77,7 +79,7 @@ public abstract class SIPChannel : IDisposable { private static int _lastUsedChannelID = 0; - protected ILogger logger = LogFactory.CreateLogger(); + protected static readonly ILogger logger = LogFactory.CreateLogger(); /// /// A unique ID for the channel. Useful for ensuring a transmission can occur diff --git a/src/SIPSorcery/core/SIP/Channels/SIPClientWebSocketChannel.cs b/src/SIPSorcery/core/SIP/Channels/SIPClientWebSocketChannel.cs index a1e46904e5..8ae509a331 100644 --- a/src/SIPSorcery/core/SIP/Channels/SIPClientWebSocketChannel.cs +++ b/src/SIPSorcery/core/SIP/Channels/SIPClientWebSocketChannel.cs @@ -17,6 +17,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Concurrent; using System.Linq; @@ -283,7 +285,7 @@ public override bool IsAddressFamilySupported(AddressFamily addresFamily) public override bool IsProtocolSupported(SIPProtocolsEnum protocol) { // We can establish client web sockets to both ws and wss servers. - return protocol == SIPProtocolsEnum.ws || protocol == SIPProtocolsEnum.wss; + return protocol is SIPProtocolsEnum.ws or SIPProtocolsEnum.wss; } /// diff --git a/src/SIPSorcery/core/SIP/Channels/SIPStreamConnection.cs b/src/SIPSorcery/core/SIP/Channels/SIPStreamConnection.cs index 37270e54d5..6b79152a78 100644 --- a/src/SIPSorcery/core/SIP/Channels/SIPStreamConnection.cs +++ b/src/SIPSorcery/core/SIP/Channels/SIPStreamConnection.cs @@ -17,6 +17,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using System.Net.Security; diff --git a/src/SIPSorcery/core/SIP/Channels/SIPStreamWrapper.cs b/src/SIPSorcery/core/SIP/Channels/SIPStreamWrapper.cs index 2a72207fa4..3892dc1647 100644 --- a/src/SIPSorcery/core/SIP/Channels/SIPStreamWrapper.cs +++ b/src/SIPSorcery/core/SIP/Channels/SIPStreamWrapper.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: SIPStreamWrapper.cs // // Description: Helper class for Stream with a thread-safe function WriteAsync. @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.IO; @@ -111,4 +113,4 @@ public void SetStatus(Task task) } } } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/core/SIP/Channels/SIPTCPChannel.cs b/src/SIPSorcery/core/SIP/Channels/SIPTCPChannel.cs index 8e5a11f2e4..7102522695 100644 --- a/src/SIPSorcery/core/SIP/Channels/SIPTCPChannel.cs +++ b/src/SIPSorcery/core/SIP/Channels/SIPTCPChannel.cs @@ -42,6 +42,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/src/SIPSorcery/core/SIP/Channels/SIPTLSChannel.cs b/src/SIPSorcery/core/SIP/Channels/SIPTLSChannel.cs index dccf0203ab..bbc03d7ed0 100644 --- a/src/SIPSorcery/core/SIP/Channels/SIPTLSChannel.cs +++ b/src/SIPSorcery/core/SIP/Channels/SIPTLSChannel.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.IO; using System.Net; diff --git a/src/SIPSorcery/core/SIP/Channels/SIPUDPChannel.cs b/src/SIPSorcery/core/SIP/Channels/SIPUDPChannel.cs index 4e6aa5c5d5..b61f22707e 100644 --- a/src/SIPSorcery/core/SIP/Channels/SIPUDPChannel.cs +++ b/src/SIPSorcery/core/SIP/Channels/SIPUDPChannel.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: SIPUDPChannel.cs // // Description: SIP transport for UDP. @@ -22,6 +22,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Concurrent; using System.Linq; diff --git a/src/SIPSorcery/core/SIP/Channels/SIPWebSocketChannel.cs b/src/SIPSorcery/core/SIP/Channels/SIPWebSocketChannel.cs index 6a7bde9bc9..e2036d77e5 100644 --- a/src/SIPSorcery/core/SIP/Channels/SIPWebSocketChannel.cs +++ b/src/SIPSorcery/core/SIP/Channels/SIPWebSocketChannel.cs @@ -23,6 +23,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Concurrent; using System.Linq; @@ -205,7 +207,7 @@ public SIPWebSocketChannel( m_webSocketServer.AddWebSocketService("/", (behaviour) => { behaviour.Channel = this; - behaviour.Logger = this.logger; + behaviour.Logger = logger; behaviour.OnClientClose += (id) => m_ingressConnections.TryRemove(id, out _); }); diff --git a/src/SIPSorcery/core/SIP/SIPAuthChallenge.cs b/src/SIPSorcery/core/SIP/SIPAuthChallenge.cs index 3846c10699..d39133ea97 100644 --- a/src/SIPSorcery/core/SIP/SIPAuthChallenge.cs +++ b/src/SIPSorcery/core/SIP/SIPAuthChallenge.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: SIPAuthChallenge.cs // // Description: Common logic when having to add auth to SIP Requests @@ -11,6 +11,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System.Collections.Generic; using System.Linq; using SIPSorcery.SIP; diff --git a/src/SIPSorcery/core/SIP/SIPAuthorisationDigest.cs b/src/SIPSorcery/core/SIP/SIPAuthorisationDigest.cs index 5b7a3fb051..ec59fa4e54 100644 --- a/src/SIPSorcery/core/SIP/SIPAuthorisationDigest.cs +++ b/src/SIPSorcery/core/SIP/SIPAuthorisationDigest.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Security.Cryptography; using System.Text; diff --git a/src/SIPSorcery/core/SIP/SIPConstants.cs b/src/SIPSorcery/core/SIP/SIPConstants.cs index 79c3bb696e..ed376f62bb 100644 --- a/src/SIPSorcery/core/SIP/SIPConstants.cs +++ b/src/SIPSorcery/core/SIP/SIPConstants.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.Reflection; diff --git a/src/SIPSorcery/core/SIP/SIPDialogue.cs b/src/SIPSorcery/core/SIP/SIPDialogue.cs index 03fe079d16..d72c3b47e8 100644 --- a/src/SIPSorcery/core/SIP/SIPDialogue.cs +++ b/src/SIPSorcery/core/SIP/SIPDialogue.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using Microsoft.Extensions.Logging; diff --git a/src/SIPSorcery/core/SIP/SIPEndPoint.cs b/src/SIPSorcery/core/SIP/SIPEndPoint.cs index efd9ae5af1..054134e3cb 100644 --- a/src/SIPSorcery/core/SIP/SIPEndPoint.cs +++ b/src/SIPSorcery/core/SIP/SIPEndPoint.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using System.Net.Sockets; diff --git a/src/SIPSorcery/core/SIP/SIPHeader.cs b/src/SIPSorcery/core/SIP/SIPHeader.cs index 52aaa924aa..e0ac23e0cd 100644 --- a/src/SIPSorcery/core/SIP/SIPHeader.cs +++ b/src/SIPSorcery/core/SIP/SIPHeader.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.Net; @@ -757,7 +759,7 @@ public static bool AreEqual(SIPContactHeader contact1, SIPContactHeader contact2 { foreach (string key in contact1Keys) { - if (key == EXPIRES_PARAMETER_KEY || key == QVALUE_PARAMETER_KEY) + if (key is EXPIRES_PARAMETER_KEY or QVALUE_PARAMETER_KEY) { continue; } @@ -775,7 +777,7 @@ public static bool AreEqual(SIPContactHeader contact1, SIPContactHeader contact2 { foreach (string key in contact2Keys) { - if (key == EXPIRES_PARAMETER_KEY || key == QVALUE_PARAMETER_KEY) + if (key is EXPIRES_PARAMETER_KEY or QVALUE_PARAMETER_KEY) { continue; } @@ -1590,7 +1592,7 @@ private void Initialise(List contact, SIPFromHeader from, SIPT Contact = contact; CallId = callId; - if (cseq >= 0 && cseq < Int32.MaxValue) + if (cseq is >= 0 and < int.MaxValue) { CSeq = cseq; } diff --git a/src/SIPSorcery/core/SIP/SIPMessageBase.cs b/src/SIPSorcery/core/SIP/SIPMessageBase.cs index 234a2da331..e6fe9384d2 100644 --- a/src/SIPSorcery/core/SIP/SIPMessageBase.cs +++ b/src/SIPSorcery/core/SIP/SIPMessageBase.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: SIPMessageBase.cs // // Description: Common base class for SIPRequest and SIPResponse classes. @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Text; using Microsoft.Extensions.Logging; diff --git a/src/SIPSorcery/core/SIP/SIPMessageBuffer.cs b/src/SIPSorcery/core/SIP/SIPMessageBuffer.cs index e694a38202..05bf4e1bfa 100644 --- a/src/SIPSorcery/core/SIP/SIPMessageBuffer.cs +++ b/src/SIPSorcery/core/SIP/SIPMessageBuffer.cs @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Text; using Microsoft.Extensions.Logging; @@ -309,7 +311,7 @@ public static int GetContentLength(byte[] buffer, int start, int end,Encoding si contentLengthValueStartPosn = index + 1; break; } - else if (buffer[index] == ' ' || buffer[index] == '\t') + else if (buffer[index] is (byte)' ' or (byte)'\t') { // Skip any whitespace between the header and the colon. continue; @@ -364,7 +366,7 @@ public static int GetContentLength(byte[] buffer, int start, int end,Encoding si // Skip any whitespace at the start of the header value. continue; } - else if (buffer[index] >= '0' && buffer[index] <= '9') + else if (buffer[index] is >= (byte)'0' and <= (byte)'9') { contentLengthValue += ((char)buffer[index]).ToString(); } diff --git a/src/SIPSorcery/core/SIP/SIPParameterlessURI.cs b/src/SIPSorcery/core/SIP/SIPParameterlessURI.cs index 22e89b0d64..0f57fb29d0 100644 --- a/src/SIPSorcery/core/SIP/SIPParameterlessURI.cs +++ b/src/SIPSorcery/core/SIP/SIPParameterlessURI.cs @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Runtime.Serialization; using Microsoft.Extensions.Logging; diff --git a/src/SIPSorcery/core/SIP/SIPParameters.cs b/src/SIPSorcery/core/SIP/SIPParameters.cs index 6924686b43..2d3c6c1fab 100644 --- a/src/SIPSorcery/core/SIP/SIPParameters.cs +++ b/src/SIPSorcery/core/SIP/SIPParameters.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/src/SIPSorcery/core/SIP/SIPReplacesParameter.cs b/src/SIPSorcery/core/SIP/SIPReplacesParameter.cs index b885e56571..60b44342ff 100644 --- a/src/SIPSorcery/core/SIP/SIPReplacesParameter.cs +++ b/src/SIPSorcery/core/SIP/SIPReplacesParameter.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: SIPReplacesParameter.cs // // Description: Represents the Replaces parameter on a Refer-To header. The Replaces parameter @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System.Text.RegularExpressions; namespace SIPSorcery.SIP diff --git a/src/SIPSorcery/core/SIP/SIPRequest.cs b/src/SIPSorcery/core/SIP/SIPRequest.cs index 16d3663f6e..94adc12f81 100644 --- a/src/SIPSorcery/core/SIP/SIPRequest.cs +++ b/src/SIPSorcery/core/SIP/SIPRequest.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.Linq; diff --git a/src/SIPSorcery/core/SIP/SIPResponse.cs b/src/SIPSorcery/core/SIP/SIPResponse.cs index 14be4b8940..f4541b3762 100644 --- a/src/SIPSorcery/core/SIP/SIPResponse.cs +++ b/src/SIPSorcery/core/SIP/SIPResponse.cs @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Text; using Microsoft.Extensions.Logging; @@ -52,7 +54,7 @@ public class SIPResponse : SIPMessageBase /// public bool IsSuccessStatusCode { - get { return (StatusCode >= 200) && (StatusCode <= 204); } + get { return StatusCode is >= 200 and <= 204; } } /// diff --git a/src/SIPSorcery/core/SIP/SIPTransport.cs b/src/SIPSorcery/core/SIP/SIPTransport.cs index a49e1b9316..5b07da4aca 100644 --- a/src/SIPSorcery/core/SIP/SIPTransport.cs +++ b/src/SIPSorcery/core/SIP/SIPTransport.cs @@ -18,6 +18,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -1004,8 +1006,8 @@ private Task SIPMessageReceived( } else if (sipRequest.Method == SIPMethodsEnum.ACK) { - if (requestTransaction.TransactionState == SIPTransactionStatesEnum.Completed || - requestTransaction.TransactionState == SIPTransactionStatesEnum.Cancelled) + if (requestTransaction.TransactionState is SIPTransactionStatesEnum.Completed or + SIPTransactionStatesEnum.Cancelled) { sipRequest.Header.Vias.UpateTopViaHeader(remoteEndPoint.GetIPEndPoint()); requestTransaction.ACKReceived(localEndPoint, remoteEndPoint, sipRequest); diff --git a/src/SIPSorcery/core/SIP/SIPURI.cs b/src/SIPSorcery/core/SIP/SIPURI.cs index 6406c8b7fd..bb7ed7969e 100644 --- a/src/SIPSorcery/core/SIP/SIPURI.cs +++ b/src/SIPSorcery/core/SIP/SIPURI.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using System.Runtime.Serialization; diff --git a/src/SIPSorcery/core/SIP/SIPUserAgentsRolesEnum.cs b/src/SIPSorcery/core/SIP/SIPUserAgentsRolesEnum.cs index 19a4179339..729941cb80 100644 --- a/src/SIPSorcery/core/SIP/SIPUserAgentsRolesEnum.cs +++ b/src/SIPSorcery/core/SIP/SIPUserAgentsRolesEnum.cs @@ -16,6 +16,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; namespace SIPSorcery.SIP diff --git a/src/SIPSorcery/core/SIP/SIPUserField.cs b/src/SIPSorcery/core/SIP/SIPUserField.cs index 1335c85964..eb4a4f38f6 100644 --- a/src/SIPSorcery/core/SIP/SIPUserField.cs +++ b/src/SIPSorcery/core/SIP/SIPUserField.cs @@ -16,6 +16,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Runtime.Serialization; using Microsoft.Extensions.Logging; diff --git a/src/SIPSorcery/core/SIP/SIPValidationException.cs b/src/SIPSorcery/core/SIP/SIPValidationException.cs index 0a50b6d8ab..463378e2f0 100644 --- a/src/SIPSorcery/core/SIP/SIPValidationException.cs +++ b/src/SIPSorcery/core/SIP/SIPValidationException.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: SIPValidationException.cs // // Description: Exception class for SIP validation errors. @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; namespace SIPSorcery.SIP diff --git a/src/SIPSorcery/core/SIPCDR/SIPCDR.cs b/src/SIPSorcery/core/SIPCDR/SIPCDR.cs index 96a9b39a66..36c9c292fd 100644 --- a/src/SIPSorcery/core/SIPCDR/SIPCDR.cs +++ b/src/SIPSorcery/core/SIPCDR/SIPCDR.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Runtime.Serialization; using Microsoft.Extensions.Logging; diff --git a/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialog.cs b/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialog.cs index 6dd52ca983..31108a9cca 100644 --- a/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialog.cs +++ b/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialog.cs @@ -1,4 +1,4 @@ -// ============================================================================ +// ============================================================================ // FileName: SIPEventDialog.cs // // Description: @@ -16,6 +16,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using System.Xml.Linq; using SIPSorcery.Sys; diff --git a/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialogInfo.cs b/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialogInfo.cs index 8cb134e216..fac386febe 100644 --- a/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialogInfo.cs +++ b/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialogInfo.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using System.Collections.Generic; using System.Text; diff --git a/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialogParticipant.cs b/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialogParticipant.cs index ad61209866..f5fb6befe1 100644 --- a/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialogParticipant.cs +++ b/src/SIPSorcery/core/SIPEvents/Dialog/SIPEventDialogParticipant.cs @@ -1,4 +1,4 @@ -// ============================================================================ +// ============================================================================ // FileName: SIPEventDialog.cs // // Description: @@ -16,6 +16,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using System.Xml.Linq; using SIPSorcery.Sys; diff --git a/src/SIPSorcery/core/SIPEvents/Presence/SIPEventPresence.cs b/src/SIPSorcery/core/SIPEvents/Presence/SIPEventPresence.cs index 3dee5fba86..e836001589 100644 --- a/src/SIPSorcery/core/SIPEvents/Presence/SIPEventPresence.cs +++ b/src/SIPSorcery/core/SIPEvents/Presence/SIPEventPresence.cs @@ -1,4 +1,4 @@ -// ============================================================================ +// ============================================================================ // FileName: SIPEventPresence.cs // // Description: @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using System.Collections.Generic; using System.Text; diff --git a/src/SIPSorcery/core/SIPEvents/Presence/SIPEventPresenceTuple.cs b/src/SIPSorcery/core/SIPEvents/Presence/SIPEventPresenceTuple.cs index 45bf637648..e936dbe91c 100644 --- a/src/SIPSorcery/core/SIPEvents/Presence/SIPEventPresenceTuple.cs +++ b/src/SIPSorcery/core/SIPEvents/Presence/SIPEventPresenceTuple.cs @@ -1,4 +1,4 @@ -// ============================================================================ +// ============================================================================ // FileName: SIPEventPresenceTuple.cs // // Description: @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using System.Xml.Linq; diff --git a/src/SIPSorcery/core/SIPEvents/SIPEventPackages.cs b/src/SIPSorcery/core/SIPEvents/SIPEventPackages.cs index 88c9bfc8d2..5bd49bac39 100644 --- a/src/SIPSorcery/core/SIPEvents/SIPEventPackages.cs +++ b/src/SIPSorcery/core/SIPEvents/SIPEventPackages.cs @@ -16,6 +16,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. // ============================================================================ +#nullable disable + using System; using SIPSorcery.Sys; diff --git a/src/SIPSorcery/core/SIPTransactions/SIPNonInviteTransaction.cs b/src/SIPSorcery/core/SIPTransactions/SIPNonInviteTransaction.cs index ff47e33bdc..a7417ccfc3 100644 --- a/src/SIPSorcery/core/SIPTransactions/SIPNonInviteTransaction.cs +++ b/src/SIPSorcery/core/SIPTransactions/SIPNonInviteTransaction.cs @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System.Net.Sockets; using System.Threading.Tasks; diff --git a/src/SIPSorcery/core/SIPTransactions/SIPTransaction.cs b/src/SIPSorcery/core/SIPTransactions/SIPTransaction.cs index de81ab0563..9c929aa7d1 100644 --- a/src/SIPSorcery/core/SIPTransactions/SIPTransaction.cs +++ b/src/SIPSorcery/core/SIPTransactions/SIPTransaction.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net.Sockets; using System.Threading.Tasks; @@ -291,14 +293,14 @@ public static string GetRequestTransactionId(string branchId, SIPMethodsEnum met public Task GotResponse(SIPEndPoint localSIPEndPoint, SIPEndPoint remoteEndPoint, SIPResponse sipResponse) { - if (TransactionState == SIPTransactionStatesEnum.Completed || TransactionState == SIPTransactionStatesEnum.Confirmed) + if (TransactionState is SIPTransactionStatesEnum.Completed or SIPTransactionStatesEnum.Confirmed) { TransactionTraceMessage?.Invoke(this, $"Transaction received duplicate response {localSIPEndPoint}<-{remoteEndPoint}: {sipResponse.ShortDescription}"); TransactionDuplicateResponse?.Invoke(localSIPEndPoint, remoteEndPoint, this, sipResponse); if (sipResponse.Header.CSeqMethod == SIPMethodsEnum.INVITE) { - if (sipResponse.StatusCode > 100 && sipResponse.StatusCode <= 199) + if (sipResponse.StatusCode is > 100 and <= 199) { return ResendPrackRequest(); } @@ -316,7 +318,7 @@ public Task GotResponse(SIPEndPoint localSIPEndPoint, SIPEndPoint r { TransactionTraceMessage?.Invoke(this, $"Transaction received Response {localSIPEndPoint}<-{remoteEndPoint}: {sipResponse.ShortDescription}"); - if (sipResponse.StatusCode >= 100 && sipResponse.StatusCode <= 199) + if (sipResponse.StatusCode is >= 100 and <= 199) { UpdateTransactionState(SIPTransactionStatesEnum.Proceeding); return TransactionInformationResponseReceived(localSIPEndPoint, remoteEndPoint, this, sipResponse); @@ -344,9 +346,9 @@ protected void UpdateTransactionState(SIPTransactionStatesEnum transactionState) { m_transactionState = transactionState; - if (transactionState == SIPTransactionStatesEnum.Confirmed || - transactionState == SIPTransactionStatesEnum.Terminated || - transactionState == SIPTransactionStatesEnum.Cancelled) + if (transactionState is SIPTransactionStatesEnum.Confirmed or + SIPTransactionStatesEnum.Terminated or + SIPTransactionStatesEnum.Cancelled) { if (transactionState == SIPTransactionStatesEnum.Cancelled && CancelledAt == DateTime.MinValue) { @@ -395,7 +397,7 @@ protected virtual Task SendProvisionalResponse(SIPResponse sipRespo UnreliableProvisionalResponse = sipResponse; return m_sipTransport.SendResponseAsync(sipResponse); } - else if (sipResponse.StatusCode > 100 && sipResponse.StatusCode <= 199) + else if (sipResponse.StatusCode is > 100 and <= 199) { UpdateTransactionState(SIPTransactionStatesEnum.Proceeding); diff --git a/src/SIPSorcery/core/SIPTransactions/SIPTransactionEngine.cs b/src/SIPSorcery/core/SIPTransactions/SIPTransactionEngine.cs index 9e877f0737..25cfd84e30 100644 --- a/src/SIPSorcery/core/SIPTransactions/SIPTransactionEngine.cs +++ b/src/SIPSorcery/core/SIPTransactions/SIPTransactionEngine.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -480,7 +482,7 @@ private void ProcessPendingTransactions() break; } - if (sendResult != SocketError.Success && sendResult != SocketError.InProgress) + if (sendResult is not SocketError.Success and not SocketError.InProgress) { logger.LogWarning("SIP transaction send failed in state {TransactionState} with error {SendResult}.", transaction.TransactionState, sendResult); @@ -644,7 +646,7 @@ private void RemoveExpiredTransactions() foreach (var (_, transaction) in m_pendingTransactions) { - if (transaction.TransactionType == SIPTransactionTypesEnum.InviteClient || transaction.TransactionType == SIPTransactionTypesEnum.InviteServer) + if (transaction.TransactionType is SIPTransactionTypesEnum.InviteClient or SIPTransactionTypesEnum.InviteServer) { if (transaction.TransactionState == SIPTransactionStatesEnum.Confirmed) { @@ -706,8 +708,8 @@ private void RemoveExpiredTransactions() // - Calling: it means no response of any kind (provisional or final) was received from the server in time. // - Trying: it means all we got was a "100 Trying" response without any follow up progress indications or final response. - if (transaction.TransactionState == SIPTransactionStatesEnum.Calling || - transaction.TransactionState == SIPTransactionStatesEnum.Trying) + if (transaction.TransactionState is SIPTransactionStatesEnum.Calling or + SIPTransactionStatesEnum.Trying) { transaction.Expire(now); expiredTransactionIds.Add(transaction.TransactionId); @@ -725,9 +727,9 @@ private void RemoveExpiredTransactions() } else if (now.Subtract(transaction.Created).TotalMilliseconds >= m_t6) { - if (transaction.TransactionState == SIPTransactionStatesEnum.Calling || - transaction.TransactionState == SIPTransactionStatesEnum.Trying || - transaction.TransactionState == SIPTransactionStatesEnum.Proceeding) + if (transaction.TransactionState is SIPTransactionStatesEnum.Calling or + SIPTransactionStatesEnum.Trying or + SIPTransactionStatesEnum.Proceeding) { //logger.LogWarning("Timed out transaction in SIPTransactionEngine, should have been timed out in the SIP Transport layer. " + transaction.TransactionRequest.Method + "."); transaction.Expire(now); diff --git a/src/SIPSorcery/core/SIPTransactions/UACInviteTransaction.cs b/src/SIPSorcery/core/SIPTransactions/UACInviteTransaction.cs index 5547ba282b..e331fa683c 100644 --- a/src/SIPSorcery/core/SIPTransactions/UACInviteTransaction.cs +++ b/src/SIPSorcery/core/SIPTransactions/UACInviteTransaction.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using System.Net.Sockets; @@ -103,7 +105,7 @@ private async Task UACInviteTransaction_TransactionInformationRespo { try { - if (sipResponse.StatusCode > 100 && sipResponse.StatusCode <= 199) + if (sipResponse.StatusCode is > 100 and <= 199) { if (!_disablePrackSupport && sipResponse.Header.RSeq > 0) { @@ -145,7 +147,7 @@ private async Task UACInviteTransaction_TransactionFinalResponseRec base.UpdateTransactionState(SIPTransactionStatesEnum.Confirmed); // BranchId for 2xx responses needs to be a new one, non-2xx final responses use same one as original request. - if (sipResponse.StatusCode >= 200 && sipResponse.StatusCode < 299) + if (sipResponse.StatusCode is >= 200 and < 299) { if (_sendOkAckManually == false) { diff --git a/src/SIPSorcery/core/SIPTransactions/UASInviteTransaction.cs b/src/SIPSorcery/core/SIPTransactions/UASInviteTransaction.cs index 5371f51528..c32366ea7e 100644 --- a/src/SIPSorcery/core/SIPTransactions/UASInviteTransaction.cs +++ b/src/SIPSorcery/core/SIPTransactions/UASInviteTransaction.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net.Sockets; using System.Threading.Tasks; @@ -123,7 +125,7 @@ private Task UASInviteTransaction_TransactionResponseReceived(SIPEn /// A socket error with the result of the cancel. public void CancelCall(SIPRequest sipCancelRequest = null) { - if (TransactionState == SIPTransactionStatesEnum.Calling || TransactionState == SIPTransactionStatesEnum.Trying || TransactionState == SIPTransactionStatesEnum.Proceeding) + if (TransactionState is SIPTransactionStatesEnum.Calling or SIPTransactionStatesEnum.Trying or SIPTransactionStatesEnum.Proceeding) { base.UpdateTransactionState(SIPTransactionStatesEnum.Cancelled); UASInviteTransactionCancelled?.Invoke(this, sipCancelRequest); diff --git a/src/SIPSorcery/core/SIPTransportConfig.cs b/src/SIPSorcery/core/SIPTransportConfig.cs index e4f746b999..e7cc469f5a 100644 --- a/src/SIPSorcery/core/SIPTransportConfig.cs +++ b/src/SIPSorcery/core/SIPTransportConfig.cs @@ -14,6 +14,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.Net; diff --git a/src/SIPSorcery/core/TWCCBitrateController.cs b/src/SIPSorcery/core/TWCCBitrateController.cs index 19365a2c32..6ac50902fc 100644 --- a/src/SIPSorcery/core/TWCCBitrateController.cs +++ b/src/SIPSorcery/core/TWCCBitrateController.cs @@ -13,6 +13,8 @@ * 2025-03-05 Initial creation. */ +#nullable disable + using System; using SIPSorcery.Net; using SIPSorceryMedia.Abstractions; diff --git a/src/SIPSorcery/net/DtlsSrtp/DtlsSrtpTransport.cs b/src/SIPSorcery/net/DtlsSrtp/DtlsSrtpTransport.cs index 0eb3515a4e..d0fac8f74e 100644 --- a/src/SIPSorcery/net/DtlsSrtp/DtlsSrtpTransport.cs +++ b/src/SIPSorcery/net/DtlsSrtp/DtlsSrtpTransport.cs @@ -19,159 +19,171 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using Org.BouncyCastle.Tls; using SIPSorcery.Net.SharpSRTP.DTLS; using SIPSorcery.Net.SharpSRTP.DTLSSRTP; using SIPSorcery.Net.SharpSRTP.SRTP; +using SIPSorcery.SIP.App; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public delegate void OnDataReadyEvent(byte[] data); +public delegate void OnDtlsAlertEvent(TlsAlertLevelsEnum alertLevel, TlsAlertTypesEnum alertType, string alertDescription); + +public class DtlsSrtpTransport : DatagramTransport { - public delegate void OnDataReadyEvent(byte[] data); - public delegate void OnDtlsAlertEvent(TlsAlertLevelsEnum alertLevel, TlsAlertTypesEnum alertType, string alertDescription); + public const int MAXIMUM_MTU = 1472; // 1500 - 20 (IP) - 8 (UDP) + public const int DTLS_RETRANSMISSION_CODE = -1; - public class DtlsSrtpTransport : DatagramTransport - { - public const int MAXIMUM_MTU = 1472; // 1500 - 20 (IP) - 8 (UDP) - public const int DTLS_RETRANSMISSION_CODE = -1; + private IDtlsSrtpPeer _connection; - private IDtlsSrtpPeer _connection; + private ConcurrentQueue _data = new ConcurrentQueue(); + private Certificate? _peerCertificate; - private ConcurrentQueue _data = new ConcurrentQueue(); - private Certificate _peerCertificate; + public DatagramTransport? Transport { get; internal set; } + public bool IsClient { get { return _connection is DtlsSrtpClient; } } + public SrtpKeys? Keys { get; private set; } - public DatagramTransport Transport { get; internal set; } - public bool IsClient { get { return _connection is DtlsSrtpClient; } } - public SrtpKeys Keys { get; private set; } + public SrtpSessionContext? Context { get; private set; } - public SrtpSessionContext Context { get; private set; } + public int TimeoutMilliseconds { get { return _connection.TimeoutMilliseconds; } set { _connection.TimeoutMilliseconds = value; } } - public int TimeoutMilliseconds { get { return _connection.TimeoutMilliseconds; } set { _connection.TimeoutMilliseconds = value; } } + public event OnDataReadyEvent? OnDataReady; - public event OnDataReadyEvent OnDataReady; + public event OnDtlsAlertEvent? OnAlert; - public event OnDtlsAlertEvent OnAlert; + public DtlsSrtpTransport(IDtlsSrtpPeer connection) + { + this._connection = connection; + this._connection.OnSessionStarted += DtlsSrtpTransport_OnSessionStarted; + this._connection.OnAlert += DtlsSrtpTransport_OnAlert; + } - public DtlsSrtpTransport(IDtlsSrtpPeer connection) - { - this._connection = connection; - this._connection.OnSessionStarted += DtlsSrtpTransport_OnSessionStarted; - this._connection.OnAlert += DtlsSrtpTransport_OnAlert; - } + private void DtlsSrtpTransport_OnSessionStarted(object? sender, DtlsSessionStartedEventArgs e) + { + this._peerCertificate = e.PeerCertificate; + this.Context = e.Context; + } - private void DtlsSrtpTransport_OnSessionStarted(object sender, DtlsSessionStartedEventArgs e) - { - this._peerCertificate = e.PeerCertificate; - this.Context = e.Context; - } + private void DtlsSrtpTransport_OnAlert(object? sender, DtlsAlertEventArgs args) + { + OnAlert?.Invoke(args.Level, args.AlertType, args.Description); + } - private void DtlsSrtpTransport_OnAlert(object sender, DtlsAlertEventArgs args) - { - OnAlert?.Invoke(args.Level, args.AlertType, args.Description); - } + public bool DoHandshake(out string? handshakeError) + { + var transport = _connection.DoHandshake(out handshakeError, this, null); + Transport = transport; + return string.IsNullOrEmpty(handshakeError); + } - public bool DoHandshake(out string handshakeError) - { - DtlsTransport transport = _connection.DoHandshake(out handshakeError, this, null); - Transport = transport; - return string.IsNullOrEmpty(handshakeError); - } + public bool IsHandshakeComplete() + { + return Transport is not null; + } - public bool IsHandshakeComplete() - { - return Transport != null; - } + public int ProtectRTP(byte[] payload, int length, out int outputBufferLength) + { + Debug.Assert(Context is not null); + return Context.ProtectRtp(payload, length, out outputBufferLength); + } - public int ProtectRTP(byte[] payload, int length, out int outputBufferLength) - { - return Context.ProtectRtp(payload, length, out outputBufferLength); - } + public int UnprotectRTP(byte[] payload, int length, out int outputBufferLength) + { + Debug.Assert(Context is not null); + return Context.UnprotectRtp(payload, length, out outputBufferLength); + } - public int UnprotectRTP(byte[] payload, int length, out int outputBufferLength) - { - return Context.UnprotectRtp(payload, length, out outputBufferLength); - } + public int ProtectRTCP(byte[] payload, int length, out int outputBufferLength) + { + Debug.Assert(Context is not null); + return Context.ProtectRtcp(payload, length, out outputBufferLength); + } - public int ProtectRTCP(byte[] payload, int length, out int outputBufferLength) - { - return Context.ProtectRtcp(payload, length, out outputBufferLength); - } + public int UnprotectRTCP(byte[] payload, int length, out int outputBufferLength) + { + Debug.Assert(Context is not null); + return Context.UnprotectRtcp(payload, length, out outputBufferLength); + } - public int UnprotectRTCP(byte[] payload, int length, out int outputBufferLength) - { - return Context.UnprotectRtcp(payload, length, out outputBufferLength); - } + public Certificate? GetRemoteCertificate() + { + return _peerCertificate; + } - public Certificate GetRemoteCertificate() - { - return _peerCertificate; - } + public int GetReceiveLimit() => MAXIMUM_MTU; - public int GetReceiveLimit() => MAXIMUM_MTU; + public int GetSendLimit() => MAXIMUM_MTU; - public int GetSendLimit() => MAXIMUM_MTU; + // TODO: Optimize to avoid array copy. + public void WriteToRecvStream(byte[] buffer) // remoteEndPoint = "127.0.0.1:80" + { + _data.Enqueue(buffer); + } - public void WriteToRecvStream(byte[] buffer) + public void Close() + { + var transport = Transport; + if (transport != null) { - _data.Enqueue(buffer); + Transport = null; + transport.Close(); } + } - public void Close() + public int Receive(byte[] buf, int off, int len, int waitMillis) + { + var t = 0L; + while(true) { - var transport = Transport; - if (transport != null) + if (_data.TryDequeue(out var data)) { - Transport = null; - transport.Close(); + Buffer.BlockCopy(data, 0, buf, off, data.Length); + return data.Length; } - } - - public int Receive(byte[] buf, int off, int len, int waitMillis) - { - long t = 0; - while(true) + else { - if (_data.TryDequeue(out var data)) - { - Buffer.BlockCopy(data, 0, buf, off, data.Length); - return data.Length; - } - else + System.Threading.Thread.Sleep(25); + t += 25; + if (t > waitMillis) { - System.Threading.Thread.Sleep(25); - t += 25; - if (t > waitMillis) - { - return -1; - } + return -1; } } } + } - public void Send(byte[] buf, int off, int len) + public void Send(byte[] buf, int off, int len) + { + if (OnDataReady is not { } onDataReady) { - if (off != 0 || len < buf.Length) - { - buf = buf.AsSpan(off, len).ToArray(); - } - OnDataReady?.Invoke(buf); + return; } -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - public int Receive(Span buffer, int waitMillis) + if (off != 0 || len < buf.Length) { - byte[] buff = buffer.ToArray(); - int len = Receive(buff, 0, buff.Length, waitMillis); - if (len > 0) - { - buff.AsSpan(0, len).CopyTo(buffer); - } - return len; + buf = buf.AsSpan(off, len).ToArray(); } - public void Send(ReadOnlySpan buffer) + onDataReady(buf); + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public int Receive(Span buffer, int waitMillis) + { + byte[] buff = buffer.ToArray(); + int len = Receive(buff, 0, buff.Length, waitMillis); + if (len > 0) { - Send(buffer.ToArray(), 0, buffer.Length); + buff.AsSpan(0, len).CopyTo(buffer); } -#endif + return len; } + + public void Send(ReadOnlySpan buffer) + { + Send(buffer.ToArray(), 0, buffer.Length); + } +#endif } diff --git a/src/SIPSorcery/net/DtlsSrtp/DtlsUtils.cs b/src/SIPSorcery/net/DtlsSrtp/DtlsUtils.cs index e20cd6455c..015d6518e2 100644 --- a/src/SIPSorcery/net/DtlsSrtp/DtlsUtils.cs +++ b/src/SIPSorcery/net/DtlsSrtp/DtlsUtils.cs @@ -47,33 +47,32 @@ using Org.BouncyCastle.Tls.Crypto; using SIPSorcery.Net.SharpSRTP.DTLS; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class DtlsUtils { - public class DtlsUtils + public static (Certificate certificate, AsymmetricKeyParameter privateKey) CreateSelfSignedTlsCert(BcTlsCrypto crypto, bool useRsa = false) { - public static (Certificate certificate, AsymmetricKeyParameter privateKey) CreateSelfSignedTlsCert(BcTlsCrypto crypto, bool useRsa = false) - { - return DtlsCertificateUtils.GenerateCertificate("WebRTC", DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddDays(30), useRsa); - } + return DtlsCertificateUtils.GenerateCertificate("WebRTC", DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddDays(30), useRsa); + } - public static RTCDtlsFingerprint Fingerprint(string algorithm, TlsCertificate value) - { - return Fingerprint(new X509Certificate(value.GetEncoded()), algorithm); - } + public static RTCDtlsFingerprint Fingerprint(string algorithm, TlsCertificate value) + { + return Fingerprint(new X509Certificate(value.GetEncoded()), algorithm); + } - public static RTCDtlsFingerprint Fingerprint(Certificate certificate) - { - return Fingerprint(new X509Certificate(certificate.GetCertificateAt(0).GetEncoded())); - } + public static RTCDtlsFingerprint Fingerprint(Certificate certificate) + { + return Fingerprint(new X509Certificate(certificate.GetCertificateAt(0).GetEncoded())); + } - public static RTCDtlsFingerprint Fingerprint(X509Certificate x509Certificate, string algorithm = "sha-256") - { - return new RTCDtlsFingerprint { algorithm = algorithm, value = DtlsCertificateUtils.Fingerprint(x509Certificate.CertificateStructure, algorithm) }; - } + public static RTCDtlsFingerprint Fingerprint(X509Certificate x509Certificate, string algorithm = "sha-256") + { + return new RTCDtlsFingerprint { algorithm = algorithm, value = DtlsCertificateUtils.Fingerprint(x509Certificate.CertificateStructure, algorithm) }; + } - public static bool IsHashSupported(string algStr) - { - return DtlsCertificateUtils.IsHashSupported(algStr); - } + public static bool IsHashSupported(string algStr) + { + return DtlsCertificateUtils.IsHashSupported(algStr); } } diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsCertificateUtils.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsCertificateUtils.cs index a548b3db8b..b3c254269c 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsCertificateUtils.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsCertificateUtils.cs @@ -19,23 +19,24 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System; +using System.Collections.Generic; +using System.Text; +using Org.BouncyCastle.Asn1; using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Asn1.X9; -using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Operators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Prng; -using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Security; -using Org.BouncyCastle.Tls.Crypto.Impl.BC; -using Org.BouncyCastle.Tls.Crypto; using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; using Org.BouncyCastle.Utilities.Encoders; using Org.BouncyCastle.X509; -using System.Collections.Generic; -using System.Text; -using System; +using SIPSorcery.Sys; namespace SIPSorcery.Net.SharpSRTP.DTLS { @@ -180,16 +181,20 @@ public static string Fingerprint(X509CertificateStructure c, string algorithm = byte[] der = c.GetEncoded(); byte[] hash = DigestUtilities.CalculateDigest(algorithm, der); byte[] hexBytes = Hex.Encode(hash); - string hex = Encoding.ASCII.GetString(hexBytes).ToUpperInvariant(); + var hex = Encoding.ASCII.GetString(hexBytes).AsSpan(); - StringBuilder fp = new StringBuilder(); - int i = 0; - fp.Append(hex.Substring(i, 2)); - while ((i += 2) < hex.Length) + using var fp = new ValueStringBuilder((hash.Length * 3) - 1); + for (int i = 0; i < hash.Length; i += 2) { - fp.Append(':'); - fp.Append(hex.Substring(i, 2)); + if (i > 0) + { + fp.Append(':'); + } + + fp.Append(char.ToUpperInvariant(hex[i])); + fp.Append(char.ToUpperInvariant(hex[i])); } + return fp.ToString(); } @@ -200,7 +205,7 @@ public static bool IsHashSupported(string algStr) throw new ArgumentNullException(nameof(algStr)); } - IDigest digest = null; + IDigest? digest = null; try { diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsClient.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsClient.cs index 2eaae06132..7af4f1b660 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsClient.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsClient.cs @@ -19,6 +19,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Tls; @@ -26,405 +29,378 @@ using Org.BouncyCastle.Tls.Crypto.Impl.BC; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.Encoders; -using System; -using System.Collections.Generic; -namespace SIPSorcery.Net.SharpSRTP.DTLS +namespace SIPSorcery.Net.SharpSRTP.DTLS; + +public class DtlsClient : DefaultTlsClient, IDtlsPeer { - public class DtlsClient : DefaultTlsClient, IDtlsPeer + private readonly object _syncRoot = new object(); + private static readonly ILogger logger = LogFactory.CreateLogger(); + + protected DatagramTransport? _clientDatagramTransport; // valid only for the current session + + private TlsSession? _session; + + public bool AutogenerateCertificate { get; set; } = true; + + public int TimeoutMilliseconds { get; set; } = 20000; + public Certificate? Certificate { get; private set; } + public AsymmetricKeyParameter? CertificatePrivateKey { get; private set; } + public short CertificateSignatureAlgorithm { get; private set; } + public short CertificateHashAlgorithm { get; private set; } + + public bool ForceUseExtendedMasterSecret { get; set; } = true; + public TlsServerCertificate? RemoteCertificate { get; private set; } + public Certificate? PeerCertificate { get { return RemoteCertificate?.Certificate; } } + + public event EventHandler? OnHandshakeCompleted; + public event EventHandler? OnAlert; + + public DtlsClient( + TlsSession? session = null, + Certificate? certificate = null, + AsymmetricKeyParameter? privateKey = null, + short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, + short certificateHashAlgorithm = HashAlgorithm.sha256) + : this( + new BcTlsCrypto(), + session, + certificate, + privateKey, + certificateSignatureAlgorithm, + certificateHashAlgorithm) + { } + + public DtlsClient( + TlsCrypto crypto, + TlsSession? session = null, + Certificate? certificate = null, + AsymmetricKeyParameter? privateKey = null, + short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, + short certificateHashAlgorithm = HashAlgorithm.sha256) : base(crypto) { - private readonly object _syncRoot = new object(); - protected DatagramTransport _clientDatagramTransport = null; - private TlsSession _session; - - public bool AutogenerateCertificate { get; set; } = true; - - public int TimeoutMilliseconds { get; set; } = 20000; - public Certificate Certificate { get; private set; } - public AsymmetricKeyParameter CertificatePrivateKey { get; private set; } - public short CertificateSignatureAlgorithm { get; private set; } - public short CertificateHashAlgorithm { get; private set; } + this._session = session; + SetCertificate(certificate, privateKey, certificateSignatureAlgorithm, certificateHashAlgorithm); + } - public bool ForceUseExtendedMasterSecret { get; set; } = true; - public TlsServerCertificate RemoteCertificate { get; private set; } - public Certificate PeerCertificate { get { return RemoteCertificate?.Certificate; } } + public virtual void SetCertificate(Certificate? certificate, AsymmetricKeyParameter? privateKey, short signatureAlgorithm, short hashAlgorithm) + { + Certificate = certificate; + CertificatePrivateKey = privateKey; + CertificateSignatureAlgorithm = signatureAlgorithm; + CertificateHashAlgorithm = hashAlgorithm; + } - public event EventHandler OnHandshakeCompleted; - public event EventHandler OnAlert; + public virtual void AutogenerateClientCertificate(bool isRsa) + { + var cert = DtlsCertificateUtils.GenerateCertificate(GetCertificateCommonName(), DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddDays(30), isRsa); + SetCertificate(cert.Certificate, cert.PrivateKey, isRsa ? SignatureAlgorithm.rsa : SignatureAlgorithm.ecdsa, HashAlgorithm.sha256); + } - public DtlsClient(TlsSession session = null, Certificate certificate = null, AsymmetricKeyParameter privateKey = null, short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, short certificateHashAlgorithm = HashAlgorithm.sha256) - : this(new BcTlsCrypto(), session, certificate, privateKey, certificateSignatureAlgorithm, certificateHashAlgorithm) - { } + protected virtual string GetCertificateCommonName() + { + return "DTLS"; + } - public DtlsClient(TlsCrypto crypto, TlsSession session = null, Certificate certificate = null, AsymmetricKeyParameter privateKey = null, short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, short certificateHashAlgorithm = HashAlgorithm.sha256) : base(crypto) - { - this._session = session; - SetCertificate(certificate, privateKey, certificateSignatureAlgorithm, certificateHashAlgorithm); - } + public override bool RequiresExtendedMasterSecret() + { + return ForceUseExtendedMasterSecret; + } - public virtual void SetCertificate(Certificate certificate, AsymmetricKeyParameter privateKey, short signatureAlgorithm, short hashAlgorithm) - { - Certificate = certificate; - CertificatePrivateKey = privateKey; - CertificateSignatureAlgorithm = signatureAlgorithm; - CertificateHashAlgorithm = hashAlgorithm; - } + protected override ProtocolVersion[] GetSupportedVersions() + { + //return ProtocolVersion.DTLSv13.DownTo(ProtocolVersion.DTLSv12); + return ProtocolVersion.DTLSv12.Only(); // ProtocolVersion.IsSupportedDtlsVersionClient currently does not support DTLS 1.3 + } - public virtual void AutogenerateClientCertificate(bool isRsa) + protected override int[] GetSupportedCipherSuites() + { + // TODO: review + if (CertificateSignatureAlgorithm == SignatureAlgorithm.rsa) { - var cert = DtlsCertificateUtils.GenerateCertificate(GetCertificateCommonName(), DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddDays(30), isRsa); - SetCertificate(cert.Certificate, cert.PrivateKey, isRsa ? SignatureAlgorithm.rsa : SignatureAlgorithm.ecdsa, HashAlgorithm.sha256); + return new int[] + { + // TLS 1.3 cpihers + //CipherSuite.TLS_AES_256_GCM_SHA384, + //CipherSuite.TLS_AES_128_GCM_SHA256, + //CipherSuite.TLS_CHACHA20_POLY1305_SHA256, + + // TLS 1.2 ciphers: + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + }; } - - protected virtual string GetCertificateCommonName() + else if (CertificateSignatureAlgorithm == SignatureAlgorithm.ecdsa) { - return "DTLS"; + // ECDSA certificates require matching cipher suites + return new int[] + { + // TLS 1.3 cpihers + //CipherSuite.TLS_AES_256_GCM_SHA384, + //CipherSuite.TLS_AES_128_GCM_SHA256, + //CipherSuite.TLS_CHACHA20_POLY1305_SHA256, + + // TLS 1.2 ciphers: + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + }; } - - public override bool RequiresExtendedMasterSecret() + else { - return ForceUseExtendedMasterSecret; + throw new NotSupportedException(); } + } - protected override ProtocolVersion[] GetSupportedVersions() + public virtual DtlsTransport? DoHandshake(out string? handshakeError, DatagramTransport datagramTransport, DtlsRequest? request = null) + { + lock (_syncRoot) { - //return ProtocolVersion.DTLSv13.DownTo(ProtocolVersion.DTLSv12); - return ProtocolVersion.DTLSv12.Only(); // ProtocolVersion.IsSupportedDtlsVersionClient currently does not support DTLS 1.3 - } + DtlsTransport? transport = null; - protected override int[] GetSupportedCipherSuites() - { - // TODO: review - if (CertificateSignatureAlgorithm == SignatureAlgorithm.rsa) + try { - return new int[] - { - // TLS 1.3 cpihers - //CipherSuite.TLS_AES_256_GCM_SHA384, - //CipherSuite.TLS_AES_128_GCM_SHA256, - //CipherSuite.TLS_CHACHA20_POLY1305_SHA256, - - // TLS 1.2 ciphers: - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, - CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - }; + var clientProtocol = new DtlsClientProtocol(); + _clientDatagramTransport = datagramTransport; + transport = clientProtocol.Connect(this, datagramTransport); + _clientDatagramTransport = null; } - else if(CertificateSignatureAlgorithm == SignatureAlgorithm.ecdsa) + catch (Exception ex) { - // ECDSA certificates require matching cipher suites - return new int[] - { - // TLS 1.3 cpihers - //CipherSuite.TLS_AES_256_GCM_SHA384, - //CipherSuite.TLS_AES_128_GCM_SHA256, - //CipherSuite.TLS_CHACHA20_POLY1305_SHA256, - - // TLS 1.2 ciphers: - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, - CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - }; + handshakeError = ex.Message; + return null; } - else - { - throw new NotSupportedException(); - } - } - public virtual DtlsTransport DoHandshake(out string handshakeError, DatagramTransport datagramTransport, DtlsRequest request = null) - { - lock (_syncRoot) - { - DtlsTransport transport = null; + handshakeError = null; + return transport; + } + } - try - { - DtlsClientProtocol clientProtocol = new DtlsClientProtocol(); - _clientDatagramTransport = datagramTransport; - transport = clientProtocol.Connect(this, datagramTransport); - _clientDatagramTransport = null; - } - catch (Exception ex) - { - handshakeError = ex.Message; - return null; - } + public override TlsSession? GetSessionToResume() + { + return this._session; + } - handshakeError = null; - return transport; - } - } + public override int GetHandshakeTimeoutMillis() => TimeoutMilliseconds; - public override TlsSession GetSessionToResume() - { - return this._session; - } + public override void NotifyAlertRaised(short alertLevel, short alertDescription, string message, Exception cause) + { + logger.LogDtlsClientAlertRaised(alertLevel, alertDescription, message, cause); + } - public override int GetHandshakeTimeoutMillis() => TimeoutMilliseconds; + public override void NotifyAlertReceived(short level, short alertDescription) + { + logger.LogDtlsClientAlertReceived(level, alertDescription); - public override void NotifyAlertRaised(short alertLevel, short alertDescription, string message, Exception cause) + TlsAlertTypesEnum alertType = TlsAlertTypesEnum.Unassigned; + if (Enum.IsDefined(typeof(TlsAlertTypesEnum), (int)alertDescription)) { - if (Log.DebugEnabled) - { - Log.Debug("DTLS client raised alert: " + AlertLevel.GetText(alertLevel) + ", " + AlertDescription.GetText(alertDescription)); - } - if (message != null) - { - if (Log.DebugEnabled) - { - Log.Debug("> " + message); - } - } - if (cause != null) - { - if (Log.DebugEnabled) - { - Log.Debug("", cause); - } - } + alertType = (TlsAlertTypesEnum)alertDescription; } - public override void NotifyAlertReceived(short level, short alertDescription) + TlsAlertLevelsEnum alertLevel = TlsAlertLevelsEnum.Warn; + if (Enum.IsDefined(typeof(TlsAlertLevelsEnum), (int)alertLevel)) { - if (Log.DebugEnabled) - { - Log.Debug("DTLS client received alert: " + AlertLevel.GetText(level) + ", " + AlertDescription.GetText(alertDescription)); - } + alertLevel = (TlsAlertLevelsEnum)level; + } - TlsAlertTypesEnum alertType = TlsAlertTypesEnum.Unassigned; - if (Enum.IsDefined(typeof(TlsAlertTypesEnum), (int)alertDescription)) - { - alertType = (TlsAlertTypesEnum)alertDescription; - } + OnAlert?.Invoke(this, new DtlsAlertEventArgs(alertLevel, alertType, AlertDescription.GetText(alertDescription))); + } - TlsAlertLevelsEnum alertLevel = TlsAlertLevelsEnum.Warn; - if (Enum.IsDefined(typeof(TlsAlertLevelsEnum), (int)alertLevel)) - { - alertLevel = (TlsAlertLevelsEnum)level; - } + public override void NotifyServerVersion(ProtocolVersion serverVersion) + { + base.NotifyServerVersion(serverVersion); - OnAlert?.Invoke(this, new DtlsAlertEventArgs(alertLevel, alertType, AlertDescription.GetText(alertDescription))); - } + logger.LogDtlsClientNegotiated(serverVersion); + } - public override void NotifyServerVersion(ProtocolVersion serverVersion) - { - base.NotifyServerVersion(serverVersion); + public override TlsAuthentication GetAuthentication() + { + return new DTlsAuthentication(m_context, this); + } - if (Log.DebugEnabled) - { - Log.Debug("DTLS client negotiated " + serverVersion); - } - } + public override void NotifyHandshakeComplete() + { + base.NotifyHandshakeComplete(); - public override TlsAuthentication GetAuthentication() + ProtocolName protocolName = m_context.SecurityParameters.ApplicationProtocol; + if (protocolName != null) { - return new DTlsAuthentication(m_context, this); + logger.LogDtlsClientAlpn(protocolName.GetUtf8Decoding()); } - public override void NotifyHandshakeComplete() + TlsSession newSession = m_context.Session; + if (newSession != null) { - base.NotifyHandshakeComplete(); - - ProtocolName protocolName = m_context.SecurityParameters.ApplicationProtocol; - if (protocolName != null) + if (newSession.IsResumable) { - if (Log.DebugEnabled) - { - Log.Debug("Client ALPN: " + protocolName.GetUtf8Decoding()); - } - } + byte[] newSessionID = newSession.SessionID; + string hex = ToHexString(newSessionID); - TlsSession newSession = m_context.Session; - if (newSession != null) - { - if (newSession.IsResumable) + if (_session != null && Arrays.AreEqual(_session.SessionID, newSessionID)) { - byte[] newSessionID = newSession.SessionID; - string hex = ToHexString(newSessionID); - - if (_session != null && Arrays.AreEqual(_session.SessionID, newSessionID)) - { - if (Log.DebugEnabled) - { - Log.Debug("Client resumed session: " + hex); - } - } - else - { - if (Log.DebugEnabled) - { - Log.Debug("Client established session: " + hex); - } - } - - this._session = newSession; + logger.LogDtlsClientSessionResumed(hex); } - - byte[] tlsServerEndPoint = m_context.ExportChannelBinding(ChannelBinding.tls_server_end_point); - if (null != tlsServerEndPoint) + else { - if (Log.DebugEnabled) - { - Log.Debug("Client 'tls-server-end-point': " + ToHexString(tlsServerEndPoint)); - } + logger.LogDtlsClientSessionEstablished(hex); } - byte[] tlsUnique = m_context.ExportChannelBinding(ChannelBinding.tls_unique); - if (Log.DebugEnabled) - { - Log.Debug("Client 'tls-unique': " + ToHexString(tlsUnique)); - } + this._session = newSession; } - OnHandshakeCompleted?.Invoke(this, new DtlsHandshakeCompletedEventArgs(m_context.SecurityParameters)); - } - - public override IDictionary GetClientExtensions() - { - if (m_context.SecurityParameters.ClientRandom == null) + byte[] tlsServerEndPoint = m_context.ExportChannelBinding(ChannelBinding.tls_server_end_point); + if (null != tlsServerEndPoint) { - throw new TlsFatalAlert(AlertDescription.internal_error); + logger.LogDtlsClientTlsServerEndPoint(ToHexString(tlsServerEndPoint)); } - return base.GetClientExtensions(); + byte[] tlsUnique = m_context.ExportChannelBinding(ChannelBinding.tls_unique); + logger.LogDtlsClientTlsUnique(ToHexString(tlsUnique)); } - public override void ProcessServerExtensions(IDictionary serverExtensions) + OnHandshakeCompleted?.Invoke(this, new DtlsHandshakeCompletedEventArgs(m_context.SecurityParameters)); + } + + public override IDictionary GetClientExtensions() + { + if (m_context.SecurityParameters.ClientRandom == null) { - if (m_context.SecurityParameters.ServerRandom == null) - { - throw new TlsFatalAlert(AlertDescription.internal_error); - } + throw new TlsFatalAlert(AlertDescription.internal_error); + } + + return base.GetClientExtensions(); + } - base.ProcessServerExtensions(serverExtensions); + public override void ProcessServerExtensions(IDictionary serverExtensions) + { + if (m_context.SecurityParameters.ServerRandom == null) + { + throw new TlsFatalAlert(AlertDescription.internal_error); } - protected virtual string ToHexString(byte[] data) + base.ProcessServerExtensions(serverExtensions); + } + + protected virtual string ToHexString(byte[] data) + { + return data == null ? "(null)" : Hex.ToHexString(data); + } + + internal class DTlsAuthentication : TlsAuthentication + { + private readonly TlsContext _context; + private readonly DtlsClient _client; + + public DTlsAuthentication(TlsContext context, DtlsClient client) { - return data == null ? "(null)" : Hex.ToHexString(data); + this._client = client ?? throw new ArgumentNullException(nameof(client)); + this._context = context; } - internal class DTlsAuthentication : TlsAuthentication + public void NotifyServerCertificate(TlsServerCertificate serverCertificate) { - private readonly TlsContext _context; - private readonly DtlsClient _client; + TlsCertificate[] chain = serverCertificate.Certificate.GetCertificateList(); - public DTlsAuthentication(TlsContext context, DtlsClient client) + logger.LogDtlsClientServerCertificateChainReceived(chain.Length); + for (int i = 0; i != chain.Length; i++) { - this._client = client ?? throw new ArgumentNullException(nameof(client)); - this._context = context; + var entry = X509CertificateStructure.GetInstance(chain[i].GetEncoded()); + logger.LogDtlsClientServerCertificateFingerprint(DtlsCertificateUtils.Fingerprint(entry), entry.Subject.ToString()); } - public void NotifyServerCertificate(TlsServerCertificate serverCertificate) - { - TlsCertificate[] chain = serverCertificate.Certificate.GetCertificateList(); + bool isEmpty = serverCertificate == null || serverCertificate.Certificate == null || serverCertificate.Certificate.IsEmpty; - if (Log.DebugEnabled) - { - Log.Debug("DTLS client received server certificate chain of length " + chain.Length); - } - for (int i = 0; i != chain.Length; i++) - { - X509CertificateStructure entry = X509CertificateStructure.GetInstance(chain[i].GetEncoded()); - if (Log.DebugEnabled) - { - Log.Debug("DTLS client fingerprint:SHA-256 " + DtlsCertificateUtils.Fingerprint(entry) + " (" + entry.Subject + ")"); - } - } - - bool isEmpty = serverCertificate == null || serverCertificate.Certificate == null || serverCertificate.Certificate.IsEmpty; + if (isEmpty) + { + throw new TlsFatalAlert(AlertDescription.bad_certificate); + } - if (isEmpty) - { - throw new TlsFatalAlert(AlertDescription.bad_certificate); - } + TlsCertificate[] certPath = chain; - TlsCertificate[] certPath = chain; + // store the certificate for further fingerprint validation + _client.RemoteCertificate = serverCertificate; - // store the certificate for further fingerprint validation - _client.RemoteCertificate = serverCertificate; + TlsUtilities.CheckPeerSigAlgs(_context, certPath); + } - TlsUtilities.CheckPeerSigAlgs(_context, certPath); + public TlsCredentials? GetClientCredentials(CertificateRequest certificateRequest) + { + short[] certificateTypes = certificateRequest.CertificateTypes; + if (certificateTypes == null || (!Arrays.Contains(certificateTypes, ClientCertificateType.rsa_sign) && !Arrays.Contains(certificateTypes, ClientCertificateType.ecdsa_sign))) + { + return null; } - public TlsCredentials GetClientCredentials(CertificateRequest certificateRequest) + if (_client.Certificate == null || _client.CertificatePrivateKey == null) { - short[] certificateTypes = certificateRequest.CertificateTypes; - if (certificateTypes == null || (!Arrays.Contains(certificateTypes, ClientCertificateType.rsa_sign) && !Arrays.Contains(certificateTypes, ClientCertificateType.ecdsa_sign))) + if (_client.AutogenerateCertificate) { - return null; + bool isRsa = IsServerCertificateRsa(_client.RemoteCertificate); + _client.AutogenerateClientCertificate(isRsa); } - - if(_client.Certificate == null || _client.CertificatePrivateKey == null) + else { - if (_client.AutogenerateCertificate) - { - bool isRsa = IsServerCertificateRsa(_client.RemoteCertificate); - _client.AutogenerateClientCertificate(isRsa); - } - else - { - // no client certificate - return null; - } + // no client certificate + return null; } + } - var clientSigAlgs = _context.SecurityParameters.ClientSigAlgs; + var clientSigAlgs = _context.SecurityParameters.ClientSigAlgs; - SignatureAndHashAlgorithm signatureAndHashAlgorithm = null; + SignatureAndHashAlgorithm? signatureAndHashAlgorithm = null; - foreach (SignatureAndHashAlgorithm alg in clientSigAlgs) - { - if (alg.Signature == _client.CertificateSignatureAlgorithm && alg.Hash == _client.CertificateHashAlgorithm) - { - signatureAndHashAlgorithm = alg; - break; - } - } - - if(signatureAndHashAlgorithm == null) + foreach (SignatureAndHashAlgorithm alg in clientSigAlgs) + { + if (alg.Signature == _client.CertificateSignatureAlgorithm && alg.Hash == _client.CertificateHashAlgorithm) { - throw new InvalidOperationException("DTLS Client does not support the selected certificate algorithm!"); + signatureAndHashAlgorithm = alg; + break; } + } - return new BcDefaultTlsCredentialedSigner(new TlsCryptoParameters(_context), (BcTlsCrypto)_context.Crypto, _client.CertificatePrivateKey, _client.Certificate, signatureAndHashAlgorithm); + if (signatureAndHashAlgorithm == null) + { + throw new InvalidOperationException("DTLS Client does not support the selected certificate algorithm!"); } + + return new BcDefaultTlsCredentialedSigner(new TlsCryptoParameters(_context), (BcTlsCrypto)_context.Crypto, _client.CertificatePrivateKey, _client.Certificate, signatureAndHashAlgorithm); } + } - public static bool IsServerCertificateRsa(TlsServerCertificate serverCertificate) + public static bool IsServerCertificateRsa(TlsServerCertificate? serverCertificate) + { + if (serverCertificate == null || serverCertificate.Certificate == null || serverCertificate.Certificate.IsEmpty) { - if (serverCertificate == null || serverCertificate.Certificate == null || serverCertificate.Certificate.IsEmpty) - { - throw new ArgumentNullException(nameof(serverCertificate)); - } + throw new ArgumentNullException(nameof(serverCertificate)); + } - var certList = serverCertificate.Certificate.GetCertificateList(); - if (certList == null || certList.Length == 0) - { - throw new ArgumentException("Server certificate chain is empty.", nameof(serverCertificate)); - } + var certList = serverCertificate.Certificate.GetCertificateList(); + if (certList == null || certList.Length == 0) + { + throw new ArgumentException("Server certificate chain is empty.", nameof(serverCertificate)); + } - var firstCertificate = X509CertificateStructure.GetInstance(certList[0].GetEncoded()); - var algOid = firstCertificate.SubjectPublicKeyInfo.Algorithm.Algorithm; + var firstCertificate = X509CertificateStructure.GetInstance(certList[0].GetEncoded()); + var algOid = firstCertificate.SubjectPublicKeyInfo.Algorithm.Algorithm; - if (algOid.Equals(Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers.RsaEncryption)) - { - return true; - } + if (algOid.Equals(Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers.RsaEncryption)) + { + return true; + } - // Fallback: decode the public key and check its runtime type - var pubKey = Org.BouncyCastle.Security.PublicKeyFactory.CreateKey(firstCertificate.SubjectPublicKeyInfo); - if (pubKey is Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters) - { - return true; - } - - return false; + // Fallback: decode the public key and check its runtime type + var pubKey = Org.BouncyCastle.Security.PublicKeyFactory.CreateKey(firstCertificate.SubjectPublicKeyInfo); + if (pubKey is Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters) + { + return true; } + + return false; } } diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsServer.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsServer.cs index 6b1b91262e..5ba7c5e0c6 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsServer.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/DtlsServer.cs @@ -19,359 +19,335 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Tls; using Org.BouncyCastle.Tls.Crypto; using Org.BouncyCastle.Tls.Crypto.Impl.BC; using Org.BouncyCastle.Utilities.Encoders; -using System; -using System.Collections.Generic; -namespace SIPSorcery.Net.SharpSRTP.DTLS +namespace SIPSorcery.Net.SharpSRTP.DTLS; + +public class DtlsServer : DefaultTlsServer, IDtlsPeer { - public class DtlsServer : DefaultTlsServer, IDtlsPeer + private readonly object _syncRoot = new object(); + protected DatagramTransport? _clientDatagramTransport; // valid only for the current session + + public int TimeoutMilliseconds { get; set; } = 20000; + + public Certificate? Certificate { get; private set; } + public AsymmetricKeyParameter? CertificatePrivateKey { get; private set; } + public short CertificateSignatureAlgorithm { get; private set; } + public short CertificateHashAlgorithm { get; private set; } + + public bool ForceUseExtendedMasterSecret { get; set; } = true; + public event EventHandler? OnHandshakeCompleted; + public event EventHandler? OnAlert; + + public DtlsServer( + Certificate? certificate = null, + AsymmetricKeyParameter? privateKey = null, + short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, + short certificateHashAlgorithm = HashAlgorithm.sha256) : + this( + new BcTlsCrypto(), + certificate, + privateKey, + certificateSignatureAlgorithm, + certificateHashAlgorithm) + { } + + public DtlsServer( + TlsCrypto crypto, + Certificate? certificate = null, + AsymmetricKeyParameter? privateKey = null, + short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, + short certificateHashAlgorithm = HashAlgorithm.sha256) : base(crypto) { - private readonly object _syncRoot = new object(); - protected DatagramTransport _clientDatagramTransport = null; - - public int TimeoutMilliseconds { get; set; } = 20000; - - public Certificate Certificate { get; private set; } - public AsymmetricKeyParameter CertificatePrivateKey { get; private set; } - public short CertificateSignatureAlgorithm { get; private set; } - public short CertificateHashAlgorithm { get; private set; } - - public bool ForceUseExtendedMasterSecret { get; set; } = true; - public event EventHandler OnHandshakeCompleted; - public event EventHandler OnAlert; - - public DtlsServer(Certificate certificate = null, AsymmetricKeyParameter privateKey = null, short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, short certificateHashAlgorithm = HashAlgorithm.sha256) : - this(new BcTlsCrypto(), certificate, privateKey, certificateSignatureAlgorithm, certificateHashAlgorithm) - { } - - public DtlsServer(TlsCrypto crypto, Certificate certificate = null, AsymmetricKeyParameter privateKey = null, short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, short certificateHashAlgorithm = HashAlgorithm.sha256) : base(crypto) + if (certificate == null || privateKey == null) { - if (certificate == null || privateKey == null) - { - // generate default self-signed certificate - SRTP_AEAD_AES_256_GCM requires ECDsa - AutogenerateClientCertificate(false); - } - else - { - SetCertificate(certificate, privateKey, certificateSignatureAlgorithm, certificateHashAlgorithm); - } + // generate default self-signed certificate - SRTP_AEAD_AES_256_GCM requires ECDsa + AutogenerateClientCertificate(false); } - - public virtual void SetCertificate(Certificate certificate, AsymmetricKeyParameter privateKey, short signatureAlgorithm, short hashAlgorithm) + else { - Certificate = certificate; - CertificatePrivateKey = privateKey; - CertificateSignatureAlgorithm = signatureAlgorithm; - CertificateHashAlgorithm = hashAlgorithm; + SetCertificate(certificate, privateKey, certificateSignatureAlgorithm, certificateHashAlgorithm); } + } - public virtual void AutogenerateClientCertificate(bool isRsa) - { - var cert = DtlsCertificateUtils.GenerateCertificate(GetCertificateCommonName(), DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddDays(30), isRsa); - SetCertificate(cert.Certificate, cert.PrivateKey, isRsa ? SignatureAlgorithm.rsa : SignatureAlgorithm.ecdsa, HashAlgorithm.sha256); - } + public virtual void SetCertificate(Certificate certificate, AsymmetricKeyParameter privateKey, short signatureAlgorithm, short hashAlgorithm) + { + Certificate = certificate; + CertificatePrivateKey = privateKey; + CertificateSignatureAlgorithm = signatureAlgorithm; + CertificateHashAlgorithm = hashAlgorithm; + } - protected virtual string GetCertificateCommonName() - { - return "DTLS"; - } + public virtual void AutogenerateClientCertificate(bool isRsa) + { + var cert = DtlsCertificateUtils.GenerateCertificate(GetCertificateCommonName(), DateTime.UtcNow.AddDays(-1), DateTime.UtcNow.AddDays(30), isRsa); + SetCertificate(cert.Certificate, cert.PrivateKey, isRsa ? SignatureAlgorithm.rsa : SignatureAlgorithm.ecdsa, HashAlgorithm.sha256); + } - public override bool RequiresExtendedMasterSecret() - { - return ForceUseExtendedMasterSecret; - } + protected virtual string GetCertificateCommonName() + { + return "DTLS"; + } - protected override ProtocolVersion[] GetSupportedVersions() - { - //return ProtocolVersion.DTLSv13.DownTo(ProtocolVersion.DTLSv12); - return ProtocolVersion.DTLSv12.Only(); // ProtocolVersion.IsSupportedDtlsVersionServer currently does not support DTLS 1.3 - } + public override bool RequiresExtendedMasterSecret() + { + return ForceUseExtendedMasterSecret; + } - protected override int[] GetSupportedCipherSuites() + protected override ProtocolVersion[] GetSupportedVersions() + { + //return ProtocolVersion.DTLSv13.DownTo(ProtocolVersion.DTLSv12); + return ProtocolVersion.DTLSv12.Only(); // ProtocolVersion.IsSupportedDtlsVersionServer currently does not support DTLS 1.3 + } + + protected override int[] GetSupportedCipherSuites() + { + if (CertificateSignatureAlgorithm == SignatureAlgorithm.rsa) { - if (CertificateSignatureAlgorithm == SignatureAlgorithm.rsa) + return new int[] { - return new int[] - { - // TLS 1.2 ciphers: - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, - CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - }; - } - else if (CertificateSignatureAlgorithm == SignatureAlgorithm.ecdsa) - { - // ECDSA certificates require matching cipher suites - return new int[] - { - // TLS 1.3 ciphers: - //CipherSuite.TLS_AES_256_GCM_SHA384, - //CipherSuite.TLS_AES_128_GCM_SHA256, - //CipherSuite.TLS_CHACHA20_POLY1305_SHA256, - - // TLS 1.2 ciphers: - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, - CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - }; - } - else - { - throw new InvalidOperationException($"DTLS server certificate algorithm {CertificateSignatureAlgorithm} not supported!"); - } + // TLS 1.2 ciphers: + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + }; } - - public virtual DtlsTransport DoHandshake(out string handshakeError, DatagramTransport datagramTransport, DtlsRequest request = null) + else if (CertificateSignatureAlgorithm == SignatureAlgorithm.ecdsa) { - lock (_syncRoot) + // ECDSA certificates require matching cipher suites + return new int[] { - if (datagramTransport == null) - { - throw new ArgumentNullException(nameof(datagramTransport)); - } - - DtlsTransport transport = null; - - try - { - DtlsServerProtocol serverProtocol = new DtlsServerProtocol(); - _clientDatagramTransport = datagramTransport; - transport = serverProtocol.Accept(this, datagramTransport, request); - _clientDatagramTransport = null; - } - catch (Exception ex) - { - handshakeError = ex.Message; - return null; - } - - handshakeError = null; - return transport; - } + // TLS 1.3 ciphers: + //CipherSuite.TLS_AES_256_GCM_SHA384, + //CipherSuite.TLS_AES_128_GCM_SHA256, + //CipherSuite.TLS_CHACHA20_POLY1305_SHA256, + + // TLS 1.2 ciphers: + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + }; } - - public override void NotifyAlertRaised(short alertLevel, short alertDescription, string message, Exception cause) + else { - if (Log.DebugEnabled) - { - Log.Debug("DTLS server raised alert: " + AlertLevel.GetText(alertLevel) + ", " + AlertDescription.GetText(alertDescription)); - } - - if (message != null) - { - if (Log.DebugEnabled) - { - Log.Debug("> " + message); - } - } - if (cause != null) - { - if (Log.DebugEnabled) - { - Log.Debug("", cause); - } - } + throw new InvalidOperationException($"DTLS server certificate algorithm {CertificateSignatureAlgorithm} not supported!"); } + } - public override void NotifyAlertReceived(short level, short alertDescription) + public virtual DtlsTransport? DoHandshake(out string? handshakeError, DatagramTransport datagramTransport, DtlsRequest? request = null) + { + lock (_syncRoot) { - if (Log.DebugEnabled) + if (datagramTransport == null) { - Log.Debug("DTLS server received alert: " + AlertLevel.GetText(level) + ", " + AlertDescription.GetText(alertDescription)); + throw new ArgumentNullException(nameof(datagramTransport)); } - TlsAlertTypesEnum alertType = TlsAlertTypesEnum.Unassigned; - if (Enum.IsDefined(typeof(TlsAlertTypesEnum), (int)alertDescription)) + DtlsTransport? transport = null; + + try { - alertType = (TlsAlertTypesEnum)alertDescription; + DtlsServerProtocol serverProtocol = new DtlsServerProtocol(); + _clientDatagramTransport = datagramTransport; + transport = serverProtocol.Accept(this, datagramTransport, request); + _clientDatagramTransport = null; } - - TlsAlertLevelsEnum alertLevel = TlsAlertLevelsEnum.Warn; - if (Enum.IsDefined(typeof(TlsAlertLevelsEnum), (int)alertLevel)) + catch (Exception ex) { - alertLevel = (TlsAlertLevelsEnum)level; + handshakeError = ex.Message; + return null; } - OnAlert?.Invoke(this, new DtlsAlertEventArgs(alertLevel, alertType, AlertDescription.GetText(alertDescription))); + handshakeError = null; + return transport; } + } - public override ProtocolVersion GetServerVersion() - { - ProtocolVersion serverVersion = base.GetServerVersion(); - if (Log.DebugEnabled) - { - Log.Debug("DTLS server negotiated " + serverVersion); - } - return serverVersion; - } + public override void NotifyAlertRaised(short alertLevel, short alertDescription, string message, Exception cause) + { + Log.Logger.LogDtlsServerAlertRaised(alertLevel, alertDescription, message, cause); + } + + public override void NotifyAlertReceived(short level, short alertDescription) + { + Log.Logger.LogDtlsServerAlertReceived(level, alertDescription); - public override int GetHandshakeTimeoutMillis() + TlsAlertTypesEnum alertType = TlsAlertTypesEnum.Unassigned; + if (Enum.IsDefined(typeof(TlsAlertTypesEnum), (int)alertDescription)) { - return TimeoutMilliseconds; + alertType = (TlsAlertTypesEnum)alertDescription; } - public override CertificateRequest GetCertificateRequest() + TlsAlertLevelsEnum alertLevel = TlsAlertLevelsEnum.Warn; + if (Enum.IsDefined(typeof(TlsAlertLevelsEnum), (int)alertLevel)) { - short[] certificateTypes = new short[]{ ClientCertificateType.ecdsa_sign, ClientCertificateType.rsa_sign }; - - IList serverSigAlgs = null; - if (TlsUtilities.IsSignatureAlgorithmsExtensionAllowed(m_context.ServerVersion)) - { - serverSigAlgs = TlsUtilities.GetDefaultSupportedSignatureAlgorithms(m_context); - } - - return new CertificateRequest(certificateTypes, serverSigAlgs, null); + alertLevel = (TlsAlertLevelsEnum)level; } - public override void NotifyClientCertificate(Certificate clientCertificate) - { - TlsCertificate[] chain = clientCertificate.GetCertificateList(); + OnAlert?.Invoke(this, new DtlsAlertEventArgs(alertLevel, alertType, AlertDescription.GetText(alertDescription))); + } - if (Log.DebugEnabled) - { - Log.Debug("DTLS server received client certificate chain of length " + chain.Length); - } + public override ProtocolVersion GetServerVersion() + { + ProtocolVersion serverVersion = base.GetServerVersion(); + Log.Logger.LogDtlsServerNegotiated(serverVersion); + return serverVersion; + } - for (int i = 0; i != chain.Length; i++) - { - X509CertificateStructure entry = X509CertificateStructure.GetInstance(chain[i].GetEncoded()); - if (Log.DebugEnabled) - { - Log.Debug(" fingerprint:SHA-256 " + DtlsCertificateUtils.Fingerprint(entry) + " (" + entry.Subject + ")"); - } - } - } + public override int GetHandshakeTimeoutMillis() + { + return TimeoutMilliseconds; + } + + public override CertificateRequest GetCertificateRequest() + { + short[] certificateTypes = new short[] { ClientCertificateType.ecdsa_sign, ClientCertificateType.rsa_sign }; - public override void NotifyHandshakeComplete() + IList? serverSigAlgs = null; + if (TlsUtilities.IsSignatureAlgorithmsExtensionAllowed(m_context.ServerVersion)) { - base.NotifyHandshakeComplete(); + serverSigAlgs = TlsUtilities.GetDefaultSupportedSignatureAlgorithms(m_context); + } - ProtocolName protocolName = m_context.SecurityParameters.ApplicationProtocol; - if (protocolName != null) - { - if (Log.DebugEnabled) - { - Log.Debug("Server ALPN: " + protocolName.GetUtf8Decoding()); - } - } + return new CertificateRequest(certificateTypes, serverSigAlgs, null); + } - byte[] tlsServerEndPoint = m_context.ExportChannelBinding(ChannelBinding.tls_server_end_point); - if (Log.DebugEnabled) - { - Log.Debug("Server 'tls-server-end-point': " + ToHexString(tlsServerEndPoint)); - } + public override void NotifyClientCertificate(Certificate clientCertificate) + { + TlsCertificate[] chain = clientCertificate.GetCertificateList(); - byte[] tlsUnique = m_context.ExportChannelBinding(ChannelBinding.tls_unique); - if (Log.DebugEnabled) - { - Log.Debug("Server 'tls-unique': " + ToHexString(tlsUnique)); - } + Log.Logger.LogDtlsServerCertificateChainReceived(chain.Length); - OnHandshakeCompleted?.Invoke(this, new DtlsHandshakeCompletedEventArgs(m_context.SecurityParameters)); + for (int i = 0; i != chain.Length; i++) + { + X509CertificateStructure entry = X509CertificateStructure.GetInstance(chain[i].GetEncoded()); + Log.Logger.LogDtlsServerCertificateFingerprint(DtlsCertificateUtils.Fingerprint(entry), entry.Subject.ToString()); } + } - public override void ProcessClientExtensions(IDictionary clientExtensions) - { - if (m_context.SecurityParameters.ClientRandom == null) - { - throw new TlsFatalAlert(AlertDescription.internal_error); - } + public override void NotifyHandshakeComplete() + { + base.NotifyHandshakeComplete(); - base.ProcessClientExtensions(clientExtensions); + ProtocolName protocolName = m_context.SecurityParameters.ApplicationProtocol; + if (protocolName != null) + { + Log.Logger.LogDtlsServerAlpn(protocolName.GetUtf8Decoding()); } - public override IDictionary GetServerExtensions() - { - if (m_context.SecurityParameters.ServerRandom == null) - { - throw new TlsFatalAlert(AlertDescription.internal_error); - } + byte[] tlsServerEndPoint = m_context.ExportChannelBinding(ChannelBinding.tls_server_end_point); + Log.Logger.LogDtlsServerTlsServerEndPoint(ToHexString(tlsServerEndPoint)); - return base.GetServerExtensions(); - } + byte[] tlsUnique = m_context.ExportChannelBinding(ChannelBinding.tls_unique); + Log.Logger.LogDtlsServerTlsUnique(ToHexString(tlsUnique)); - public override void GetServerExtensionsForConnection(IDictionary serverExtensions) - { - if (m_context.SecurityParameters.ServerRandom == null) - { - throw new TlsFatalAlert(AlertDescription.internal_error); - } + OnHandshakeCompleted?.Invoke(this, new DtlsHandshakeCompletedEventArgs(m_context.SecurityParameters)); + } - base.GetServerExtensionsForConnection(serverExtensions); + public override void ProcessClientExtensions(IDictionary clientExtensions) + { + if (m_context.SecurityParameters.ClientRandom == null) + { + throw new TlsFatalAlert(AlertDescription.internal_error); } - protected virtual string ToHexString(byte[] data) + base.ProcessClientExtensions(clientExtensions); + } + + public override IDictionary GetServerExtensions() + { + if (m_context.SecurityParameters.ServerRandom == null) { - return data == null ? "(null)" : Hex.ToHexString(data); + throw new TlsFatalAlert(AlertDescription.internal_error); } - public override int GetSelectedCipherSuite() + return base.GetServerExtensions(); + } + + public override void GetServerExtensionsForConnection(IDictionary serverExtensions) + { + if (m_context.SecurityParameters.ServerRandom == null) { - return base.GetSelectedCipherSuite(); + throw new TlsFatalAlert(AlertDescription.internal_error); } - protected override TlsCredentialedSigner GetECDsaSignerCredentials() - { - IList clientSigAlgs = m_context.SecurityParameters.ClientSigAlgs; - SignatureAndHashAlgorithm signatureAndHashAlgorithm = null; + base.GetServerExtensionsForConnection(serverExtensions); + } - if (Certificate == null || CertificatePrivateKey == null) - { - throw new InvalidOperationException("DTLS server ECDsa certificate not set!"); - } + protected virtual string ToHexString(byte[] data) + { + return data == null ? "(null)" : Hex.ToHexString(data); + } - foreach (SignatureAndHashAlgorithm alg in clientSigAlgs) - { - if (alg.Signature == CertificateSignatureAlgorithm && alg.Hash == CertificateHashAlgorithm) - { - signatureAndHashAlgorithm = alg; - break; - } - } + public override int GetSelectedCipherSuite() + { + return base.GetSelectedCipherSuite(); + } - if (signatureAndHashAlgorithm == null) - { - throw new InvalidOperationException("DTLS Client does not support the selected certificate algorithm!"); - } + protected override TlsCredentialedSigner GetECDsaSignerCredentials() + { + IList clientSigAlgs = m_context.SecurityParameters.ClientSigAlgs; + SignatureAndHashAlgorithm? signatureAndHashAlgorithm = null; - return new BcDefaultTlsCredentialedSigner(new TlsCryptoParameters(m_context), (BcTlsCrypto)m_context.Crypto, CertificatePrivateKey, Certificate, signatureAndHashAlgorithm); + if (Certificate == null || CertificatePrivateKey == null) + { + throw new InvalidOperationException("DTLS server ECDsa certificate not set!"); } - protected override TlsCredentialedSigner GetRsaSignerCredentials() + foreach (SignatureAndHashAlgorithm alg in clientSigAlgs) { - IList clientSigAlgs = m_context.SecurityParameters.ClientSigAlgs; - SignatureAndHashAlgorithm signatureAndHashAlgorithm = null; - - if (Certificate == null || CertificatePrivateKey == null) + if (alg.Signature == CertificateSignatureAlgorithm && alg.Hash == CertificateHashAlgorithm) { - throw new InvalidOperationException("DTLS server RSA certificate not set!"); + signatureAndHashAlgorithm = alg; + break; } + } - foreach (SignatureAndHashAlgorithm alg in clientSigAlgs) - { - if (alg.Signature == CertificateSignatureAlgorithm && alg.Hash == CertificateHashAlgorithm) - { - signatureAndHashAlgorithm = alg; - break; - } - } + if (signatureAndHashAlgorithm == null) + { + throw new InvalidOperationException("DTLS Client does not support the selected certificate algorithm!"); + } + + return new BcDefaultTlsCredentialedSigner(new TlsCryptoParameters(m_context), (BcTlsCrypto)m_context.Crypto, CertificatePrivateKey, Certificate, signatureAndHashAlgorithm); + } - if(signatureAndHashAlgorithm == null) + protected override TlsCredentialedSigner GetRsaSignerCredentials() + { + IList clientSigAlgs = m_context.SecurityParameters.ClientSigAlgs; + SignatureAndHashAlgorithm? signatureAndHashAlgorithm = null; + + if (Certificate == null || CertificatePrivateKey == null) + { + throw new InvalidOperationException("DTLS server RSA certificate not set!"); + } + + foreach (SignatureAndHashAlgorithm alg in clientSigAlgs) + { + if (alg.Signature == CertificateSignatureAlgorithm && alg.Hash == CertificateHashAlgorithm) { - throw new InvalidOperationException("DTLS Client does not support the selected certificate algorithm!"); + signatureAndHashAlgorithm = alg; + break; } + } - return new BcDefaultTlsCredentialedSigner(new TlsCryptoParameters(m_context), (BcTlsCrypto)m_context.Crypto, CertificatePrivateKey, Certificate, signatureAndHashAlgorithm); + if (signatureAndHashAlgorithm == null) + { + throw new InvalidOperationException("DTLS Client does not support the selected certificate algorithm!"); } + + return new BcDefaultTlsCredentialedSigner(new TlsCryptoParameters(m_context), (BcTlsCrypto)m_context.Crypto, CertificatePrivateKey, Certificate, signatureAndHashAlgorithm); } } diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/IDtlsPeer.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/IDtlsPeer.cs index e971927887..cf0555663d 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/IDtlsPeer.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLS/IDtlsPeer.cs @@ -23,106 +23,105 @@ using Org.BouncyCastle.Tls; using System; -namespace SIPSorcery.Net.SharpSRTP.DTLS +namespace SIPSorcery.Net.SharpSRTP.DTLS; + +public enum TlsAlertLevelsEnum { - public enum TlsAlertLevelsEnum - { - Warn = 1, - Fatal = 2 - } + Warn = 1, + Fatal = 2 +} - // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-6 - public enum TlsAlertTypesEnum - { - CloseNotify = 0, - // 1-9 unassigned - UnexpectedMessage = 10, - // 11-19 unassigned - BadRecordMac = 20, - DecryptionFailedReserved = 21, // Used in TLS versions prior to 1.3. - RecordOverflow = 22, - // 23-29 unassigned - DecompressionFailureReserved = 30, // Used in TLS versions prior to 1.3. - // 31-39 unassigned - HandshakeFailure = 40, - NoCertificateReserved = 41, // Used in SSLv3 but not in TLS. - BadCertificate = 42, - UnsupportedCertificate = 43, - CertificateRevoked = 44, - CertificateExpired = 45, - CertificateUnknown = 46, - IllegalParameter = 47, - UnknownCA = 48, - AccessDenied = 49, - DecodeError = 50, - DecryptError = 51, - TooManyCidsRequested = 52, - // 53-59 unassigned - ExportRestrictionReserved = 60, // Used in TLS 1.0 but not TLS 1.1 or later. - ProtocolVersion = 70, - InsufficientSecurity = 71, - // 72-79 unassigned - InternalEror = 80, - // 81-85 unassigned - InappropriateFallback = 86, - // 87-89 unassigned - UserCanceled = 90, - // 91-99 unassigned - NoRenegotiationReserved = 100, // Used in TLS versions prior to 1.3. - // 101-108 unassigned - MissingExtension = 109, - UnsupportedExtension = 110, - CertificateUnobtainableReserved = 111, // Used in TLS versions prior to 1.3. - UnrecognizedName = 112, - BadCertificateStatusResponse = 113, - BadCertificateHashValueReserved = 114, // Used in TLS versions prior to 1.3. - UnknownPskIdentity = 115, - CertificateRequired = 116, - GeneralError = 117, - // 118-119 unassigned - NoApplicationProtocol = 120, - EchRequired = 121, - // 122-255 unassigned - Unassigned = 255 - } +// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-6 +public enum TlsAlertTypesEnum +{ + CloseNotify = 0, + // 1-9 unassigned + UnexpectedMessage = 10, + // 11-19 unassigned + BadRecordMac = 20, + DecryptionFailedReserved = 21, // Used in TLS versions prior to 1.3. + RecordOverflow = 22, + // 23-29 unassigned + DecompressionFailureReserved = 30, // Used in TLS versions prior to 1.3. + // 31-39 unassigned + HandshakeFailure = 40, + NoCertificateReserved = 41, // Used in SSLv3 but not in TLS. + BadCertificate = 42, + UnsupportedCertificate = 43, + CertificateRevoked = 44, + CertificateExpired = 45, + CertificateUnknown = 46, + IllegalParameter = 47, + UnknownCA = 48, + AccessDenied = 49, + DecodeError = 50, + DecryptError = 51, + TooManyCidsRequested = 52, + // 53-59 unassigned + ExportRestrictionReserved = 60, // Used in TLS 1.0 but not TLS 1.1 or later. + ProtocolVersion = 70, + InsufficientSecurity = 71, + // 72-79 unassigned + InternalEror = 80, + // 81-85 unassigned + InappropriateFallback = 86, + // 87-89 unassigned + UserCanceled = 90, + // 91-99 unassigned + NoRenegotiationReserved = 100, // Used in TLS versions prior to 1.3. + // 101-108 unassigned + MissingExtension = 109, + UnsupportedExtension = 110, + CertificateUnobtainableReserved = 111, // Used in TLS versions prior to 1.3. + UnrecognizedName = 112, + BadCertificateStatusResponse = 113, + BadCertificateHashValueReserved = 114, // Used in TLS versions prior to 1.3. + UnknownPskIdentity = 115, + CertificateRequired = 116, + GeneralError = 117, + // 118-119 unassigned + NoApplicationProtocol = 120, + EchRequired = 121, + // 122-255 unassigned + Unassigned = 255 +} - public class DtlsAlertEventArgs : EventArgs - { - public TlsAlertLevelsEnum Level { get; } - public TlsAlertTypesEnum AlertType { get; } - public string Description { get; } +public class DtlsAlertEventArgs : EventArgs +{ + public TlsAlertLevelsEnum Level { get; } + public TlsAlertTypesEnum AlertType { get; } + public string Description { get; } - public DtlsAlertEventArgs(TlsAlertLevelsEnum level, TlsAlertTypesEnum type, string description) - { - this.Level = level; - this.AlertType = type; - this.Description = description; - } + public DtlsAlertEventArgs(TlsAlertLevelsEnum level, TlsAlertTypesEnum type, string description) + { + this.Level = level; + this.AlertType = type; + this.Description = description; } +} - public class DtlsHandshakeCompletedEventArgs : EventArgs - { - public SecurityParameters SecurityParameters { get; } +public class DtlsHandshakeCompletedEventArgs : EventArgs +{ + public SecurityParameters SecurityParameters { get; } - public DtlsHandshakeCompletedEventArgs(SecurityParameters securityParameters) - { - SecurityParameters = securityParameters; - } + public DtlsHandshakeCompletedEventArgs(SecurityParameters securityParameters) + { + SecurityParameters = securityParameters; } +} - public interface IDtlsPeer - { - event EventHandler OnAlert; - event EventHandler OnHandshakeCompleted; - int TimeoutMilliseconds { get; set; } - bool ForceUseExtendedMasterSecret { get; set; } +public interface IDtlsPeer +{ + event EventHandler? OnAlert; + event EventHandler? OnHandshakeCompleted; + int TimeoutMilliseconds { get; set; } + bool ForceUseExtendedMasterSecret { get; set; } - Certificate Certificate { get; } - AsymmetricKeyParameter CertificatePrivateKey { get; } - short CertificateSignatureAlgorithm { get; } - short CertificateHashAlgorithm { get; } - void SetCertificate(Certificate certificate, AsymmetricKeyParameter privateKey, short signatureAlgorithm, short hashAlgorithm); + Certificate? Certificate { get; } + AsymmetricKeyParameter? CertificatePrivateKey { get; } + short CertificateSignatureAlgorithm { get; } + short CertificateHashAlgorithm { get; } + void SetCertificate(Certificate certificate, AsymmetricKeyParameter privateKey, short signatureAlgorithm, short hashAlgorithm); - DtlsTransport DoHandshake(out string handshakeError, DatagramTransport datagramTransport, DtlsRequest request = null); - } + DtlsTransport? DoHandshake(out string? handshakeError, DatagramTransport datagramTransport, DtlsRequest? request = null); } diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpClient.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpClient.cs index aa24b8e9c7..a0c581868c 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpClient.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpClient.cs @@ -27,135 +27,159 @@ using SIPSorcery.Net.SharpSRTP.SRTP; using System; using System.Collections.Generic; +using System.Diagnostics; -namespace SIPSorcery.Net.SharpSRTP.DTLSSRTP +namespace SIPSorcery.Net.SharpSRTP.DTLSSRTP; + +public class DtlsSrtpClient : DtlsClient, IDtlsSrtpPeer { - public class DtlsSrtpClient : DtlsClient, IDtlsSrtpPeer + private UseSrtpData _srtpData; + + public event EventHandler? OnSessionStarted; + public int MkiLength { get; private set; } = 0; + + public DtlsSrtpClient( + Certificate? certificate = null, + AsymmetricKeyParameter? privateKey = null, + short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, + short certificateHashAlgorithm = HashAlgorithm.sha256, + TlsSession? session = null) : + this( + new BcTlsCrypto(), + certificate, + privateKey, + certificateSignatureAlgorithm, + certificateHashAlgorithm, + session) + { } + + public DtlsSrtpClient( + TlsCrypto crypto, + Certificate? certificate = null, + AsymmetricKeyParameter? privateKey = null, + short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, + short certificateHashAlgorithm = HashAlgorithm.sha256, + TlsSession? session = null) : + base( + crypto, + session, + certificate, + privateKey, + certificateSignatureAlgorithm, + certificateHashAlgorithm) { - private UseSrtpData _srtpData; - - public event EventHandler OnSessionStarted; - public int MkiLength { get; private set; } = 0; - - public DtlsSrtpClient(Certificate certificate = null, AsymmetricKeyParameter privateKey = null, short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, short certificateHashAlgorithm = HashAlgorithm.sha256, TlsSession session = null) : - this(new BcTlsCrypto(), certificate, privateKey, certificateSignatureAlgorithm, certificateHashAlgorithm, session) - { } + int[] protectionProfiles = GetSupportedProtectionProfiles(); - public DtlsSrtpClient(TlsCrypto crypto, Certificate certificate = null, AsymmetricKeyParameter privateKey = null, short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, short certificateHashAlgorithm = HashAlgorithm.sha256, TlsSession session = null) : - base(crypto, session, certificate, privateKey, certificateSignatureAlgorithm, certificateHashAlgorithm) - { - int[] protectionProfiles = GetSupportedProtectionProfiles(); + byte[] mki = DtlsSrtpProtocol.GenerateMki(MkiLength); + this._srtpData = new UseSrtpData(protectionProfiles, mki); - byte[] mki = DtlsSrtpProtocol.GenerateMki(MkiLength); - this._srtpData = new UseSrtpData(protectionProfiles, mki); + this.OnHandshakeCompleted += DtlsSrtpClient_OnHandshakeCompleted; + } - this.OnHandshakeCompleted += DtlsSrtpClient_OnHandshakeCompleted; - } + private void DtlsSrtpClient_OnHandshakeCompleted(object? sender, DtlsHandshakeCompletedEventArgs e) + { + SrtpSessionContext context = CreateSessionContext(e.SecurityParameters); + Certificate peerCertificate = e.SecurityParameters.PeerCertificate; + Debug.Assert(base._clientDatagramTransport is not null); + OnSessionStarted?.Invoke(this, new DtlsSessionStartedEventArgs(context, peerCertificate, base._clientDatagramTransport)); + } - private void DtlsSrtpClient_OnHandshakeCompleted(object sender, DtlsHandshakeCompletedEventArgs e) + public void SetMKI(byte[] mki) + { + if (mki == null) { - SrtpSessionContext context = CreateSessionContext(e.SecurityParameters); - Certificate peerCertificate = e.SecurityParameters.PeerCertificate; - OnSessionStarted?.Invoke(this, new DtlsSessionStartedEventArgs(context, peerCertificate, base._clientDatagramTransport)); + MkiLength = 0; + mki = new byte[0]; } - - public void SetMKI(byte[] mki) + else { - if (mki == null) - { - MkiLength = 0; - mki = new byte[0]; - } - else + if (mki.Length > 255) { - if (mki.Length > 255) - { - throw new ArgumentOutOfRangeException(nameof(mki)); - } - - MkiLength = mki.Length; + throw new ArgumentOutOfRangeException(nameof(mki)); } - this._srtpData = new UseSrtpData(_srtpData.ProtectionProfiles, mki); - } - - protected virtual int[] GetSupportedProtectionProfiles() - { - return new int[] - { - ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM, - ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM, - ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_256_GCM, - ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_256_GCM, - ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_128_GCM, - ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_128_GCM, - ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_80, - ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80, - ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_80, - ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32, - ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_32, - ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_32, - - // do not offer NULL profiles to make sure these do not get selected by accident - //ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_80, - //ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_32 - }; + MkiLength = mki.Length; } - protected override string GetCertificateCommonName() - { - return "WebRTC"; - } + this._srtpData = new UseSrtpData(_srtpData.ProtectionProfiles, mki); + } - public override void ProcessServerExtensions(IDictionary serverExtensions) + protected virtual int[] GetSupportedProtectionProfiles() + { + return new int[] { - base.ProcessServerExtensions(serverExtensions); - - // https://www.rfc-editor.org/rfc/rfc5764#section-4.1 - UseSrtpData serverSrtpExtension = TlsSrtpUtilities.GetUseSrtpExtension(serverExtensions); + ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM, + ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM, + ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_256_GCM, + ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_256_GCM, + ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_128_GCM, + ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_128_GCM, + ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_80, + ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80, + ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_80, + ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32, + ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_32, + ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_32, + + // do not offer NULL profiles to make sure these do not get selected by accident + //ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_80, + //ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_32 + }; + } - // verify that the server has selected exactly 1 profile - int[] clientSupportedProfiles = GetSupportedProtectionProfiles(); - if (serverSrtpExtension.ProtectionProfiles.Length != 1) - { - throw new TlsFatalAlert(AlertDescription.internal_error); - } + protected override string GetCertificateCommonName() + { + return "WebRTC"; + } - // verify that the server has selected a profile we support - int selectedProfile = serverSrtpExtension.ProtectionProfiles[0]; - if (Array.IndexOf(clientSupportedProfiles, selectedProfile) < 0) - { - throw new TlsFatalAlert(AlertDescription.internal_error); - } + public override void ProcessServerExtensions(IDictionary serverExtensions) + { + base.ProcessServerExtensions(serverExtensions); - // verify the mki sent by the server matches our mki - if (_srtpData.Mki != null && serverSrtpExtension.Mki != null && !_srtpData.Mki.AsSpan().SequenceEqual(serverSrtpExtension.Mki)) - { - throw new TlsFatalAlert(AlertDescription.illegal_parameter); - } + // https://www.rfc-editor.org/rfc/rfc5764#section-4.1 + UseSrtpData serverSrtpExtension = TlsSrtpUtilities.GetUseSrtpExtension(serverExtensions); - // store the server extension as it contains the selected profile - _srtpData = serverSrtpExtension; + // verify that the server has selected exactly 1 profile + int[] clientSupportedProfiles = GetSupportedProtectionProfiles(); + if (serverSrtpExtension.ProtectionProfiles.Length != 1) + { + throw new TlsFatalAlert(AlertDescription.internal_error); } - public override IDictionary GetClientExtensions() + // verify that the server has selected a profile we support + int selectedProfile = serverSrtpExtension.ProtectionProfiles[0]; + if (Array.IndexOf(clientSupportedProfiles, selectedProfile) < 0) { - var extensions = base.GetClientExtensions(); - TlsSrtpUtilities.AddUseSrtpExtension(extensions, _srtpData); - return extensions; + throw new TlsFatalAlert(AlertDescription.internal_error); } - public virtual SrtpSessionContext CreateSessionContext(SecurityParameters securityParameters) + // verify the mki sent by the server matches our mki + if (_srtpData.Mki != null && serverSrtpExtension.Mki != null && !_srtpData.Mki.AsSpan().SequenceEqual(serverSrtpExtension.Mki)) { - // this should only be called from OnHandshakeCompleted so we should still have _srtpData from the connection - if (m_context == null) - { - throw new InvalidOperationException(); - } + throw new TlsFatalAlert(AlertDescription.illegal_parameter); + } - int selectedProtectionProfile = _srtpData.ProtectionProfiles[0]; - DtlsSrtpKeys keys = DtlsSrtpProtocol.CreateMasterKeys(selectedProtectionProfile, _srtpData.Mki, securityParameters, ForceUseExtendedMasterSecret); - return DtlsSrtpProtocol.CreateSrtpClientSessionContext(keys); + // store the server extension as it contains the selected profile + _srtpData = serverSrtpExtension; + } + + public override IDictionary GetClientExtensions() + { + var extensions = base.GetClientExtensions(); + TlsSrtpUtilities.AddUseSrtpExtension(extensions, _srtpData); + return extensions; + } + + public virtual SrtpSessionContext CreateSessionContext(SecurityParameters securityParameters) + { + // this should only be called from OnHandshakeCompleted so we should still have _srtpData from the connection + if (m_context == null) + { + throw new InvalidOperationException(); } + + int selectedProtectionProfile = _srtpData.ProtectionProfiles[0]; + DtlsSrtpKeys keys = DtlsSrtpProtocol.CreateMasterKeys(selectedProtectionProfile, _srtpData.Mki, securityParameters, ForceUseExtendedMasterSecret); + return DtlsSrtpProtocol.CreateSrtpClientSessionContext(keys); } } diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpKeys.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpKeys.cs index b029bf288a..cf448b69da 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpKeys.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpKeys.cs @@ -22,44 +22,43 @@ using System; using SIPSorcery.Net.SharpSRTP.SRTP; -namespace SIPSorcery.Net.SharpSRTP.DTLSSRTP -{ - public class DtlsSrtpKeys - { - public SrtpProtectionProfileConfiguration ProtectionProfile { get; } - public ReadOnlyMemory Mki { get; } +namespace SIPSorcery.Net.SharpSRTP.DTLSSRTP; - public ReadOnlyMemory ClientWriteMasterKey { get; } - public ReadOnlyMemory ClientWriteMasterSalt { get; } - public ReadOnlyMemory ServerWriteMasterKey { get; } - public ReadOnlyMemory ServerWriteMasterSalt { get; } +public class DtlsSrtpKeys +{ + public SrtpProtectionProfileConfiguration ProtectionProfile { get; } + public ReadOnlyMemory Mki { get; } - public DtlsSrtpKeys( - SrtpProtectionProfileConfiguration protectionProfile, - ReadOnlyMemory clientWriteMasterKey, - ReadOnlyMemory clientWriteMasterSalt, - ReadOnlyMemory serverWriteMasterKey, - ReadOnlyMemory serverWriteMasterSalt, - ReadOnlyMemory mki = default) - { - this.ProtectionProfile = protectionProfile ?? throw new ArgumentNullException(nameof(protectionProfile)); - this.Mki = mki; + public ReadOnlyMemory ClientWriteMasterKey { get; } + public ReadOnlyMemory ClientWriteMasterSalt { get; } + public ReadOnlyMemory ServerWriteMasterKey { get; } + public ReadOnlyMemory ServerWriteMasterSalt { get; } - int cipherKeyLen = protectionProfile.CipherKeyLength >> 3; - int cipherSaltLen = protectionProfile.CipherSaltLength >> 3; + public DtlsSrtpKeys( + SrtpProtectionProfileConfiguration protectionProfile, + ReadOnlyMemory clientWriteMasterKey, + ReadOnlyMemory clientWriteMasterSalt, + ReadOnlyMemory serverWriteMasterKey, + ReadOnlyMemory serverWriteMasterSalt, + ReadOnlyMemory mki = default) + { + this.ProtectionProfile = protectionProfile ?? throw new ArgumentNullException(nameof(protectionProfile)); + this.Mki = mki; - if (clientWriteMasterKey.Length != cipherKeyLen - || clientWriteMasterSalt.Length != cipherSaltLen - || serverWriteMasterKey.Length != cipherKeyLen - || serverWriteMasterSalt.Length != cipherSaltLen) - { - throw new ArgumentException(); - } + int cipherKeyLen = protectionProfile.CipherKeyLength >> 3; + int cipherSaltLen = protectionProfile.CipherSaltLength >> 3; - this.ClientWriteMasterKey = clientWriteMasterKey; - this.ClientWriteMasterSalt = clientWriteMasterSalt; - this.ServerWriteMasterKey = serverWriteMasterKey; - this.ServerWriteMasterSalt = serverWriteMasterSalt; + if (clientWriteMasterKey.Length != cipherKeyLen + || clientWriteMasterSalt.Length != cipherSaltLen + || serverWriteMasterKey.Length != cipherKeyLen + || serverWriteMasterSalt.Length != cipherSaltLen) + { + throw new ArgumentException(); } + + this.ClientWriteMasterKey = clientWriteMasterKey; + this.ClientWriteMasterSalt = clientWriteMasterSalt; + this.ServerWriteMasterKey = serverWriteMasterKey; + this.ServerWriteMasterSalt = serverWriteMasterSalt; } } diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpProtocol.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpProtocol.cs index b7b0414fb6..31ae88c2b9 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpProtocol.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpProtocol.cs @@ -23,216 +23,215 @@ using SIPSorcery.Net.SharpSRTP.SRTP; using System; using System.Collections.Generic; -using System.Linq; -namespace SIPSorcery.Net.SharpSRTP.DTLSSRTP +namespace SIPSorcery.Net.SharpSRTP.DTLSSRTP; + +/// +/// Currently registered DTLS-SRTP profiles: +/// https://www.iana.org/assignments/srtp-protection/srtp-protection.xhtml#srtp-protection-1 +/// +public abstract class ExtendedSrtpProtectionProfile : SrtpProtectionProfile +{ + // TODO: Remove this once BouncyCastle adds the constants + public const int DRAFT_SRTP_AES256_CM_SHA1_80 = 0x0003; + public const int DRAFT_SRTP_AES256_CM_SHA1_32 = 0x0004; + public const int DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM = 0x0009; + public const int DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM = 0x000A; + public const int SRTP_ARIA_128_CTR_HMAC_SHA1_80 = 0x000B; + public const int SRTP_ARIA_128_CTR_HMAC_SHA1_32 = 0x000C; + public const int SRTP_ARIA_256_CTR_HMAC_SHA1_80 = 0x000D; + public const int SRTP_ARIA_256_CTR_HMAC_SHA1_32 = 0x000E; + public const int SRTP_AEAD_ARIA_128_GCM = 0x000F; + public const int SRTP_AEAD_ARIA_256_GCM = 0x0010; +} + +public static class DtlsSrtpProtocol { - /// - /// Currently registered DTLS-SRTP profiles: https://www.iana.org/assignments/srtp-protection/srtp-protection.xhtml#srtp-protection-1 - /// - public abstract class ExtendedSrtpProtectionProfile : SrtpProtectionProfile + public static readonly Dictionary DtlsProtectionProfiles; + + static DtlsSrtpProtocol() { - // TODO: Remove this once BouncyCastle adds the constants - public const int DRAFT_SRTP_AES256_CM_SHA1_80 = 0x0003; - public const int DRAFT_SRTP_AES256_CM_SHA1_32 = 0x0004; - public const int DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM = 0x0009; - public const int DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM = 0x000A; - public const int SRTP_ARIA_128_CTR_HMAC_SHA1_80 = 0x000B; - public const int SRTP_ARIA_128_CTR_HMAC_SHA1_32 = 0x000C; - public const int SRTP_ARIA_256_CTR_HMAC_SHA1_80 = 0x000D; - public const int SRTP_ARIA_256_CTR_HMAC_SHA1_32 = 0x000E; - public const int SRTP_AEAD_ARIA_128_GCM = 0x000F; - public const int SRTP_AEAD_ARIA_256_GCM = 0x0010; + // see https://www.iana.org/assignments/srtp-protection/srtp-protection.xhtml#srtp-protection-1 + DtlsProtectionProfiles = new Dictionary() + { + // https://www.rfc-editor.org/rfc/rfc8723.txt + { ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM, 512, 192, int.MaxValue, SrtpAuth.NONE, 0, 256) }, + { ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM, 256, 192, int.MaxValue, SrtpAuth.NONE, 0, 256) }, + + // https://datatracker.ietf.org/doc/html/rfc8269 + { ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_256_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.AEAD_ARIA_256_GCM, 256, 96, int.MaxValue, SrtpAuth.NONE, 0, 128) }, + { ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_128_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.AEAD_ARIA_128_GCM, 128, 96, int.MaxValue, SrtpAuth.NONE, 0, 128) }, + { ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_80, new SrtpProtectionProfileConfiguration(SrtpCiphers.ARIA_256_CTR, 256, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 80) }, + { ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_32, new SrtpProtectionProfileConfiguration(SrtpCiphers.ARIA_256_CTR, 256, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 32) }, + { ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_80, new SrtpProtectionProfileConfiguration(SrtpCiphers.ARIA_128_CTR, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 80) }, + { ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_32, new SrtpProtectionProfileConfiguration(SrtpCiphers.ARIA_128_CTR, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 32) }, + + // https://datatracker.ietf.org/doc/html/rfc7714 + { ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_256_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.AEAD_AES_256_GCM, 256, 96, int.MaxValue, SrtpAuth.NONE, 0, 128) }, + { ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_128_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.AEAD_AES_128_GCM, 128, 96, int.MaxValue, SrtpAuth.NONE, 0, 128) }, + + // AES256 CM is specified in RFC 6188, but not included in IANA DTLS-SRTP registry https://www.iana.org/assignments/srtp-protection/srtp-protection.xhtml#srtp-protection-1 + // https://www.rfc-editor.org/rfc/rfc6188 + // AES192 CM is not supported in DTLS-SRTP + // AES256 CM was removed in Draft 4 of RFC 5764 + // https://author-tools.ietf.org/iddiff?url1=draft-ietf-avt-dtls-srtp-04&url2=draft-ietf-avt-dtls-srtp-03&difftype=--html + { ExtendedSrtpProtectionProfile.DRAFT_SRTP_AES256_CM_SHA1_80, new SrtpProtectionProfileConfiguration(SrtpCiphers.AES_256_CM, 256, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 80) }, + { ExtendedSrtpProtectionProfile.DRAFT_SRTP_AES256_CM_SHA1_32, new SrtpProtectionProfileConfiguration(SrtpCiphers.AES_256_CM, 256, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 32) }, + + // https://datatracker.ietf.org/doc/html/rfc5764#section-9 + { ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80, new SrtpProtectionProfileConfiguration(SrtpCiphers.AES_128_CM, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 80) }, + { ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32, new SrtpProtectionProfileConfiguration(SrtpCiphers.AES_128_CM, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 32) }, + + // for NULL we still need the keys (K_a) for auth, so we use the same key lengths as AES128 CM in order to derive non-zero master keys + { ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_80, new SrtpProtectionProfileConfiguration(SrtpCiphers.NULL, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 80) }, + { ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_32, new SrtpProtectionProfileConfiguration(SrtpCiphers.NULL, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 32) }, + }; } - public static class DtlsSrtpProtocol + public static DtlsSrtpKeys CreateMasterKeys(int protectionProfile, byte[] mki, SecurityParameters dtlsSecurityParameters, bool requireExtendedMasterSecret = true) { - public static readonly Dictionary DtlsProtectionProfiles; - - static DtlsSrtpProtocol() + // verify that we have extended master secret before computing the keys + if (!dtlsSecurityParameters.IsExtendedMasterSecret && requireExtendedMasterSecret) { - // see https://www.iana.org/assignments/srtp-protection/srtp-protection.xhtml#srtp-protection-1 - DtlsProtectionProfiles = new Dictionary() - { - // https://www.rfc-editor.org/rfc/rfc8723.txt - { ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM, 512, 192, int.MaxValue, SrtpAuth.NONE, 0, 256) }, - { ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM, 256, 192, int.MaxValue, SrtpAuth.NONE, 0, 256) }, - - // https://datatracker.ietf.org/doc/html/rfc8269 - { ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_256_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.AEAD_ARIA_256_GCM, 256, 96, int.MaxValue, SrtpAuth.NONE, 0, 128) }, - { ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_128_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.AEAD_ARIA_128_GCM, 128, 96, int.MaxValue, SrtpAuth.NONE, 0, 128) }, - { ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_80, new SrtpProtectionProfileConfiguration(SrtpCiphers.ARIA_256_CTR, 256, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 80) }, - { ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_32, new SrtpProtectionProfileConfiguration(SrtpCiphers.ARIA_256_CTR, 256, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 32) }, - { ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_80, new SrtpProtectionProfileConfiguration(SrtpCiphers.ARIA_128_CTR, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 80) }, - { ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_32, new SrtpProtectionProfileConfiguration(SrtpCiphers.ARIA_128_CTR, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 32) }, - - // https://datatracker.ietf.org/doc/html/rfc7714 - { ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_256_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.AEAD_AES_256_GCM, 256, 96, int.MaxValue, SrtpAuth.NONE, 0, 128) }, - { ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_128_GCM, new SrtpProtectionProfileConfiguration(SrtpCiphers.AEAD_AES_128_GCM, 128, 96, int.MaxValue, SrtpAuth.NONE, 0, 128) }, - - // AES256 CM is specified in RFC 6188, but not included in IANA DTLS-SRTP registry https://www.iana.org/assignments/srtp-protection/srtp-protection.xhtml#srtp-protection-1 - // https://www.rfc-editor.org/rfc/rfc6188 - // AES192 CM is not supported in DTLS-SRTP - // AES256 CM was removed in Draft 4 of RFC 5764 - // https://author-tools.ietf.org/iddiff?url1=draft-ietf-avt-dtls-srtp-04&url2=draft-ietf-avt-dtls-srtp-03&difftype=--html - { ExtendedSrtpProtectionProfile.DRAFT_SRTP_AES256_CM_SHA1_80, new SrtpProtectionProfileConfiguration(SrtpCiphers.AES_256_CM, 256, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 80) }, - { ExtendedSrtpProtectionProfile.DRAFT_SRTP_AES256_CM_SHA1_32, new SrtpProtectionProfileConfiguration(SrtpCiphers.AES_256_CM, 256, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 32) }, - - // https://datatracker.ietf.org/doc/html/rfc5764#section-9 - { ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80, new SrtpProtectionProfileConfiguration(SrtpCiphers.AES_128_CM, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 80) }, - { ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32, new SrtpProtectionProfileConfiguration(SrtpCiphers.AES_128_CM, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 32) }, - - // for NULL we still need the keys (K_a) for auth, so we use the same key lengths as AES128 CM in order to derive non-zero master keys - { ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_80, new SrtpProtectionProfileConfiguration(SrtpCiphers.NULL, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 80) }, - { ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_32, new SrtpProtectionProfileConfiguration(SrtpCiphers.NULL, 128, 112, int.MaxValue, SrtpAuth.HMAC_SHA1, 160, 32) }, - }; + throw new InvalidOperationException(); } - public static DtlsSrtpKeys CreateMasterKeys(int protectionProfile, byte[] mki, SecurityParameters dtlsSecurityParameters, bool requireExtendedMasterSecret = true) + // SRTP key derivation as described here https://datatracker.ietf.org/doc/html/rfc5764 + var srtpSecurityParams = DtlsProtectionProfiles[protectionProfile]; + + // 2 * (SRTPSecurityParams.master_key_len + SRTPSecurityParams.master_salt_len) bytes of data + int sharedSecretLength = (2 * (srtpSecurityParams.CipherKeyLength + srtpSecurityParams.CipherSaltLength)) >> 3; + + // EXTRACTOR-dtls_srtp https://datatracker.ietf.org/doc/html/rfc5705 + + // TODO: If context is provided, it computes: + /* + PRF(SecurityParameters.master_secret, label, + SecurityParameters.client_random + + SecurityParameters.server_random + + context_value_length + context_value + )[length] + */ + + // derive shared secret + /* + PRF(SecurityParameters.master_secret, label, + SecurityParameters.client_random + + SecurityParameters.server_random + )[length] + */ + byte[] prfSeed = GC.AllocateUninitializedArray(dtlsSecurityParameters.ClientRandom.Length + dtlsSecurityParameters.ServerRandom.Length); + Buffer.BlockCopy(dtlsSecurityParameters.ClientRandom, 0, prfSeed, 0, dtlsSecurityParameters.ClientRandom.Length); + Buffer.BlockCopy(dtlsSecurityParameters.ServerRandom, 0, prfSeed, dtlsSecurityParameters.ClientRandom.Length, dtlsSecurityParameters.ServerRandom.Length); + byte[] sharedSecret = TlsUtilities.Prf( + dtlsSecurityParameters, + dtlsSecurityParameters.MasterSecret, + ExporterLabel.dtls_srtp, // The exporter label for this usage is "EXTRACTOR-dtls_srtp" + prfSeed, + sharedSecretLength + ).Extract(); + + return CreateMasterKeys(protectionProfile, mki, sharedSecret); + } + + public static DtlsSrtpKeys CreateMasterKeys(int protectionProfile, byte[] mki, byte[] sharedSecret) + { + var srtpSecurityParams = DtlsProtectionProfiles[protectionProfile]; + + if (sharedSecret == null) { - // verify that we have extended master secret before computing the keys - if (!dtlsSecurityParameters.IsExtendedMasterSecret && requireExtendedMasterSecret) - { - throw new InvalidOperationException(); - } - - // SRTP key derivation as described here https://datatracker.ietf.org/doc/html/rfc5764 - var srtpSecurityParams = DtlsProtectionProfiles[protectionProfile]; - - // 2 * (SRTPSecurityParams.master_key_len + SRTPSecurityParams.master_salt_len) bytes of data - int sharedSecretLength = (2 * (srtpSecurityParams.CipherKeyLength + srtpSecurityParams.CipherSaltLength)) >> 3; - - // EXTRACTOR-dtls_srtp https://datatracker.ietf.org/doc/html/rfc5705 - - // TODO: If context is provided, it computes: - /* - PRF(SecurityParameters.master_secret, label, - SecurityParameters.client_random + - SecurityParameters.server_random + - context_value_length + context_value - )[length] - */ - - // derive shared secret - /* - PRF(SecurityParameters.master_secret, label, - SecurityParameters.client_random + - SecurityParameters.server_random - )[length] - */ - byte[] prfSeed = GC.AllocateUninitializedArray(dtlsSecurityParameters.ClientRandom.Length + dtlsSecurityParameters.ServerRandom.Length); - Buffer.BlockCopy(dtlsSecurityParameters.ClientRandom, 0, prfSeed, 0, dtlsSecurityParameters.ClientRandom.Length); - Buffer.BlockCopy(dtlsSecurityParameters.ServerRandom, 0, prfSeed, dtlsSecurityParameters.ClientRandom.Length, dtlsSecurityParameters.ServerRandom.Length); - byte[] sharedSecret = TlsUtilities.Prf( - dtlsSecurityParameters, - dtlsSecurityParameters.MasterSecret, - ExporterLabel.dtls_srtp, // The exporter label for this usage is "EXTRACTOR-dtls_srtp" - prfSeed, - sharedSecretLength - ).Extract(); - - return CreateMasterKeys(protectionProfile, mki, sharedSecret); + throw new ArgumentNullException(nameof(sharedSecret)); } - public static DtlsSrtpKeys CreateMasterKeys(int protectionProfile, byte[] mki, byte[] sharedSecret) + int sharedSecretLength = (2 * (srtpSecurityParams.CipherKeyLength + srtpSecurityParams.CipherSaltLength)) >> 3; + if (sharedSecret.Length < sharedSecretLength) { - var srtpSecurityParams = DtlsProtectionProfiles[protectionProfile]; - - if (sharedSecret == null) - { - throw new ArgumentNullException(nameof(sharedSecret)); - } - - int sharedSecretLength = (2 * (srtpSecurityParams.CipherKeyLength + srtpSecurityParams.CipherSaltLength)) >> 3; - if (sharedSecret.Length < sharedSecretLength) - { - throw new ArgumentException("Invalid shared secret length.", nameof(sharedSecret)); - } - - var cipherKeyLen = srtpSecurityParams.CipherKeyLength >> 3; - var cipherSaltLen = srtpSecurityParams.CipherSaltLength >> 3; - - - ReadOnlyMemory clientWriteMasterKey, clientWriteMasterSalt, serverWriteMasterKey, serverWriteMasterSalt; - - if (srtpSecurityParams.Cipher >= SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM) - { - // we have to maintain separation of the inner and outer keys according to RFC8723 - // | - int halfKeyLen = cipherKeyLen / 2; - int halfSaltLen = cipherSaltLen / 2; - int halfSecret = sharedSecretLength / 2; - - // ClientWriteMasterKey: inner + outer - var clientKey = new byte[cipherKeyLen]; - Buffer.BlockCopy(sharedSecret, 0, clientKey, 0, halfKeyLen); - Buffer.BlockCopy(sharedSecret, halfSecret, clientKey, halfKeyLen, halfKeyLen); - clientWriteMasterKey = clientKey.AsMemory(); - - // ServerWriteMasterKey: inner + outer - var serverKey = new byte[cipherKeyLen]; - Buffer.BlockCopy(sharedSecret, halfKeyLen, serverKey, 0, halfKeyLen); - Buffer.BlockCopy(sharedSecret, halfSecret + halfKeyLen, serverKey, halfKeyLen, halfKeyLen); - serverWriteMasterKey = serverKey.AsMemory(); - - // ClientWriteMasterSalt: inner + outer - var clientSalt = new byte[cipherSaltLen]; - Buffer.BlockCopy(sharedSecret, 2 * halfKeyLen, clientSalt, 0, halfSaltLen); - Buffer.BlockCopy(sharedSecret, halfSecret + 2 * halfKeyLen, clientSalt, halfSaltLen, halfSaltLen); - clientWriteMasterSalt = clientSalt.AsMemory(); - - // ServerWriteMasterSalt: inner + outer - var serverSalt = new byte[cipherSaltLen]; - Buffer.BlockCopy(sharedSecret, 2 * halfKeyLen + halfSaltLen, serverSalt, 0, halfSaltLen); - Buffer.BlockCopy(sharedSecret, halfSecret + 2 * halfKeyLen + halfSaltLen, serverSalt, halfSaltLen, halfSaltLen); - serverWriteMasterSalt = serverSalt.AsMemory(); - } - else - { - // - int offset = 0; - clientWriteMasterKey = sharedSecret.AsMemory(offset, cipherKeyLen); - offset += cipherKeyLen; - serverWriteMasterKey = sharedSecret.AsMemory(offset, cipherKeyLen); - offset += cipherKeyLen; - clientWriteMasterSalt = sharedSecret.AsMemory(offset, cipherSaltLen); - offset += cipherSaltLen; - serverWriteMasterSalt = sharedSecret.AsMemory(offset, cipherSaltLen); - } - - var k = new DtlsSrtpKeys( - srtpSecurityParams, - clientWriteMasterKey, - clientWriteMasterSalt, - serverWriteMasterKey, - serverWriteMasterSalt, - mki == null ? default : mki.AsMemory()); - return k; + throw new ArgumentException("Invalid shared secret length.", nameof(sharedSecret)); } - public static byte[] GenerateMki(int length) + var cipherKeyLen = srtpSecurityParams.CipherKeyLength >> 3; + var cipherSaltLen = srtpSecurityParams.CipherSaltLength >> 3; + + + ReadOnlyMemory clientWriteMasterKey, clientWriteMasterSalt, serverWriteMasterKey, serverWriteMasterSalt; + + if (srtpSecurityParams.Cipher >= SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM) { - return SrtpProtocol.GenerateMki(length); + // we have to maintain separation of the inner and outer keys according to RFC8723 + // | + int halfKeyLen = cipherKeyLen / 2; + int halfSaltLen = cipherSaltLen / 2; + int halfSecret = sharedSecretLength / 2; + + // ClientWriteMasterKey: inner + outer + var clientKey = new byte[cipherKeyLen]; + Buffer.BlockCopy(sharedSecret, 0, clientKey, 0, halfKeyLen); + Buffer.BlockCopy(sharedSecret, halfSecret, clientKey, halfKeyLen, halfKeyLen); + clientWriteMasterKey = clientKey.AsMemory(); + + // ServerWriteMasterKey: inner + outer + var serverKey = new byte[cipherKeyLen]; + Buffer.BlockCopy(sharedSecret, halfKeyLen, serverKey, 0, halfKeyLen); + Buffer.BlockCopy(sharedSecret, halfSecret + halfKeyLen, serverKey, halfKeyLen, halfKeyLen); + serverWriteMasterKey = serverKey.AsMemory(); + + // ClientWriteMasterSalt: inner + outer + var clientSalt = new byte[cipherSaltLen]; + Buffer.BlockCopy(sharedSecret, 2 * halfKeyLen, clientSalt, 0, halfSaltLen); + Buffer.BlockCopy(sharedSecret, halfSecret + 2 * halfKeyLen, clientSalt, halfSaltLen, halfSaltLen); + clientWriteMasterSalt = clientSalt.AsMemory(); + + // ServerWriteMasterSalt: inner + outer + var serverSalt = new byte[cipherSaltLen]; + Buffer.BlockCopy(sharedSecret, 2 * halfKeyLen + halfSaltLen, serverSalt, 0, halfSaltLen); + Buffer.BlockCopy(sharedSecret, halfSecret + 2 * halfKeyLen + halfSaltLen, serverSalt, halfSaltLen, halfSaltLen); + serverWriteMasterSalt = serverSalt.AsMemory(); } - - public static SrtpSessionContext CreateSrtpServerSessionContext(DtlsSrtpKeys keys) + else { - var encodeRtpContext = new SrtpContext(SrtpContextType.RTP, keys.ProtectionProfile, keys.ServerWriteMasterKey, keys.ServerWriteMasterSalt, keys.Mki); - var encodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, keys.ProtectionProfile, keys.ServerWriteMasterKey, keys.ServerWriteMasterSalt, keys.Mki); - var decodeRtpContext = new SrtpContext(SrtpContextType.RTP, keys.ProtectionProfile, keys.ClientWriteMasterKey, keys.ClientWriteMasterSalt, keys.Mki); - var decodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, keys.ProtectionProfile, keys.ClientWriteMasterKey, keys.ClientWriteMasterSalt, keys.Mki); - - return new SrtpSessionContext(encodeRtpContext, decodeRtpContext, encodeRtcpContext, decodeRtcpContext); + // + int offset = 0; + clientWriteMasterKey = sharedSecret.AsMemory(offset, cipherKeyLen); + offset += cipherKeyLen; + serverWriteMasterKey = sharedSecret.AsMemory(offset, cipherKeyLen); + offset += cipherKeyLen; + clientWriteMasterSalt = sharedSecret.AsMemory(offset, cipherSaltLen); + offset += cipherSaltLen; + serverWriteMasterSalt = sharedSecret.AsMemory(offset, cipherSaltLen); } - public static SrtpSessionContext CreateSrtpClientSessionContext(DtlsSrtpKeys keys) - { - var encodeRtpContext = new SrtpContext(SrtpContextType.RTP, keys.ProtectionProfile, keys.ClientWriteMasterKey, keys.ClientWriteMasterSalt, keys.Mki); - var encodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, keys.ProtectionProfile, keys.ClientWriteMasterKey, keys.ClientWriteMasterSalt, keys.Mki); - var decodeRtpContext = new SrtpContext(SrtpContextType.RTP, keys.ProtectionProfile, keys.ServerWriteMasterKey, keys.ServerWriteMasterSalt, keys.Mki); - var decodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, keys.ProtectionProfile, keys.ServerWriteMasterKey, keys.ServerWriteMasterSalt, keys.Mki); + var k = new DtlsSrtpKeys( + srtpSecurityParams, + clientWriteMasterKey, + clientWriteMasterSalt, + serverWriteMasterKey, + serverWriteMasterSalt, + mki == null ? default : mki.AsMemory()); + return k; + } - return new SrtpSessionContext(encodeRtpContext, decodeRtpContext, encodeRtcpContext, decodeRtcpContext); - } + public static byte[] GenerateMki(int length) + { + return SrtpProtocol.GenerateMki(length); + } + + public static SrtpSessionContext CreateSrtpServerSessionContext(DtlsSrtpKeys keys) + { + var encodeRtpContext = new SrtpContext(SrtpContextType.RTP, keys.ProtectionProfile, keys.ServerWriteMasterKey, keys.ServerWriteMasterSalt, keys.Mki); + var encodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, keys.ProtectionProfile, keys.ServerWriteMasterKey, keys.ServerWriteMasterSalt, keys.Mki); + var decodeRtpContext = new SrtpContext(SrtpContextType.RTP, keys.ProtectionProfile, keys.ClientWriteMasterKey, keys.ClientWriteMasterSalt, keys.Mki); + var decodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, keys.ProtectionProfile, keys.ClientWriteMasterKey, keys.ClientWriteMasterSalt, keys.Mki); + + return new SrtpSessionContext(encodeRtpContext, decodeRtpContext, encodeRtcpContext, decodeRtcpContext); + } + + public static SrtpSessionContext CreateSrtpClientSessionContext(DtlsSrtpKeys keys) + { + var encodeRtpContext = new SrtpContext(SrtpContextType.RTP, keys.ProtectionProfile, keys.ClientWriteMasterKey, keys.ClientWriteMasterSalt, keys.Mki); + var encodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, keys.ProtectionProfile, keys.ClientWriteMasterKey, keys.ClientWriteMasterSalt, keys.Mki); + var decodeRtpContext = new SrtpContext(SrtpContextType.RTP, keys.ProtectionProfile, keys.ServerWriteMasterKey, keys.ServerWriteMasterSalt, keys.Mki); + var decodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, keys.ProtectionProfile, keys.ServerWriteMasterKey, keys.ServerWriteMasterSalt, keys.Mki); + + return new SrtpSessionContext(encodeRtpContext, decodeRtpContext, encodeRtcpContext, decodeRtcpContext); } } diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpServer.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpServer.cs index f3ea4bf98a..0909513423 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpServer.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/DtlsSrtpServer.cs @@ -27,116 +27,136 @@ using SIPSorcery.Net.SharpSRTP.SRTP; using System; using System.Collections.Generic; +using System.Diagnostics; -namespace SIPSorcery.Net.SharpSRTP.DTLSSRTP +namespace SIPSorcery.Net.SharpSRTP.DTLSSRTP; + +// Useful link for troubleshooting WebRTC in Chrome/Edge: https://learn.microsoft.com/en-us/azure/communication-services/resources/troubleshooting/voice-video-calling/references/how-to-collect-browser-verbose-log +public class DtlsSrtpServer : DtlsServer, IDtlsSrtpPeer { - // Useful link for troubleshooting WebRTC in Chrome/Edge: https://learn.microsoft.com/en-us/azure/communication-services/resources/troubleshooting/voice-video-calling/references/how-to-collect-browser-verbose-log - public class DtlsSrtpServer : DtlsServer, IDtlsSrtpPeer + /// + /// Used in WebRTC to tell the server to not use MKI even if the client requested it. + /// + /// + /// RFC 8827 states: An SRTP Master Key Identifier (MKI) MUST NOT be used. + /// + public bool ForceDisableMKI { get; set; } = false; + + private UseSrtpData? _srtpData; + + public event EventHandler? OnSessionStarted; + + public DtlsSrtpServer( + Certificate? certificate = null, + AsymmetricKeyParameter? privateKey = null, + short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, + short certificateHashAlgorithm = HashAlgorithm.sha256) + : this( + new BcTlsCrypto(), + certificate, + privateKey, + certificateSignatureAlgorithm, + certificateHashAlgorithm) + { } + + public DtlsSrtpServer( + TlsCrypto crypto, + Certificate? certificate = null, + AsymmetricKeyParameter? privateKey = null, + short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, + short certificateHashAlgorithm = HashAlgorithm.sha256) + : base( + crypto, + certificate, + privateKey, + certificateSignatureAlgorithm, + certificateHashAlgorithm) { - /// - /// Used in WebRTC to tell the server to not use MKI even if the client requested it. - /// - /// - /// RFC 8827 states: An SRTP Master Key Identifier (MKI) MUST NOT be used. - /// - public bool ForceDisableMKI { get; set; } = false; - - private UseSrtpData _srtpData; - - public event EventHandler OnSessionStarted; + this.OnHandshakeCompleted += DtlsSrtpServer_OnHandshakeCompleted; + } - public DtlsSrtpServer(Certificate certificate = null, AsymmetricKeyParameter privateKey = null, short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, short certificateHashAlgorithm = HashAlgorithm.sha256) - : this(new BcTlsCrypto(), certificate, privateKey, certificateSignatureAlgorithm, certificateHashAlgorithm) - { } + private void DtlsSrtpServer_OnHandshakeCompleted(object? sender, DtlsHandshakeCompletedEventArgs e) + { + SrtpSessionContext context = CreateSessionContext(e.SecurityParameters); + Certificate peerCertificate = e.SecurityParameters.PeerCertificate; + OnSessionStarted?.Invoke(this, new DtlsSessionStartedEventArgs(context, peerCertificate, base._clientDatagramTransport)); + } - public DtlsSrtpServer(TlsCrypto crypto, Certificate certificate = null, AsymmetricKeyParameter privateKey = null, short certificateSignatureAlgorithm = SignatureAlgorithm.ecdsa, short certificateHashAlgorithm = HashAlgorithm.sha256) - : base(crypto, certificate, privateKey, certificateSignatureAlgorithm, certificateHashAlgorithm) + protected virtual int[] GetSupportedProtectionProfiles() + { + return new int[] { - this.OnHandshakeCompleted += DtlsSrtpServer_OnHandshakeCompleted; - } + ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM, + ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM, + ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_256_GCM, + ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_256_GCM, + ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_128_GCM, + ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_128_GCM, + ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_80, + ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80, + ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_80, + ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_32, + ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32, + ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_32, + + // do not offer NULL profiles to make sure these do not get selected by accident + //ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_80, + //ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_32, + }; + } - private void DtlsSrtpServer_OnHandshakeCompleted(object sender, DtlsHandshakeCompletedEventArgs e) - { - SrtpSessionContext context = CreateSessionContext(e.SecurityParameters); - Certificate peerCertificate = e.SecurityParameters.PeerCertificate; - OnSessionStarted?.Invoke(this, new DtlsSessionStartedEventArgs(context, peerCertificate, base._clientDatagramTransport)); - } + protected override string GetCertificateCommonName() + { + return "WebRTC"; + } - protected virtual int[] GetSupportedProtectionProfiles() - { - return new int[] - { - ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM, - ExtendedSrtpProtectionProfile.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM, - ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_256_GCM, - ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_256_GCM, - ExtendedSrtpProtectionProfile.SRTP_AEAD_AES_128_GCM, - ExtendedSrtpProtectionProfile.SRTP_AEAD_ARIA_128_GCM, - ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_80, - ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80, - ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_80, - ExtendedSrtpProtectionProfile.SRTP_ARIA_256_CTR_HMAC_SHA1_32, - ExtendedSrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32, - ExtendedSrtpProtectionProfile.SRTP_ARIA_128_CTR_HMAC_SHA1_32, - - // do not offer NULL profiles to make sure these do not get selected by accident - //ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_80, - //ExtendedSrtpProtectionProfile.SRTP_NULL_HMAC_SHA1_32, - }; - } + public override void ProcessClientExtensions(IDictionary clientExtensions) + { + base.ProcessClientExtensions(clientExtensions); - protected override string GetCertificateCommonName() - { - return "WebRTC"; - } + UseSrtpData clientSrtpExtension = TlsSrtpUtilities.GetUseSrtpExtension(clientExtensions); - public override void ProcessClientExtensions(IDictionary clientExtensions) + // Choose the highest priority profile supported by the server + int[] serverSupportedProfiles = GetSupportedProtectionProfiles(); + bool found = false; + int selectedProfile = int.MinValue; + int minIndex = int.MaxValue; + for (int i = 0; i < clientSrtpExtension.ProtectionProfiles.Length; i++) { - base.ProcessClientExtensions(clientExtensions); - - UseSrtpData clientSrtpExtension = TlsSrtpUtilities.GetUseSrtpExtension(clientExtensions); - - // Choose the highest priority profile supported by the server - int[] serverSupportedProfiles = GetSupportedProtectionProfiles(); - bool found = false; - int selectedProfile = int.MinValue; - int minIndex = int.MaxValue; - for (int i = 0; i < clientSrtpExtension.ProtectionProfiles.Length; i++) + int val = clientSrtpExtension.ProtectionProfiles[i]; + int idx = Array.IndexOf(serverSupportedProfiles, val); + if (idx >= 0 && idx < minIndex) { - int val = clientSrtpExtension.ProtectionProfiles[i]; - int idx = Array.IndexOf(serverSupportedProfiles, val); - if (idx >= 0 && idx < minIndex) - { - minIndex = idx; - selectedProfile = val; - found = true; - } + minIndex = idx; + selectedProfile = val; + found = true; } - if (!found) - { - throw new TlsFatalAlert(AlertDescription.internal_error); - } - _srtpData = new UseSrtpData(new int[] { selectedProfile }, ForceDisableMKI ? Array.Empty() : clientSrtpExtension.Mki); // Server must return only a single selected profile } - - public override IDictionary GetServerExtensions() + if (!found) { - var extensions = base.GetServerExtensions(); - TlsSrtpUtilities.AddUseSrtpExtension(extensions, _srtpData); - return extensions; + throw new TlsFatalAlert(AlertDescription.internal_error); } + _srtpData = new UseSrtpData(new int[] { selectedProfile }, ForceDisableMKI ? Array.Empty() : clientSrtpExtension.Mki); // Server must return only a single selected profile + } - public virtual SrtpSessionContext CreateSessionContext(SecurityParameters securityParameters) - { - // this should only be called from OnHandshakeCompleted so we should still have _srtpData from the connection - if (m_context == null) - { - throw new InvalidOperationException(); - } + public override IDictionary GetServerExtensions() + { + var extensions = base.GetServerExtensions(); + TlsSrtpUtilities.AddUseSrtpExtension(extensions, _srtpData); + return extensions; + } - int selectedProtectionProfile = _srtpData.ProtectionProfiles[0]; - DtlsSrtpKeys keys = DtlsSrtpProtocol.CreateMasterKeys(_srtpData.ProtectionProfiles[0], _srtpData.Mki, securityParameters, ForceUseExtendedMasterSecret); - return DtlsSrtpProtocol.CreateSrtpServerSessionContext(keys); + public virtual SrtpSessionContext CreateSessionContext(SecurityParameters securityParameters) + { + // this should only be called from OnHandshakeCompleted so we should still have _srtpData from the connection + if (m_context == null) + { + throw new InvalidOperationException(); } + + Debug.Assert(_srtpData is not null); + int selectedProtectionProfile = _srtpData.ProtectionProfiles[0]; + DtlsSrtpKeys keys = DtlsSrtpProtocol.CreateMasterKeys(_srtpData.ProtectionProfiles[0], _srtpData.Mki, securityParameters, ForceUseExtendedMasterSecret); + return DtlsSrtpProtocol.CreateSrtpServerSessionContext(keys); } } diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/IDtlsSrtpPeer.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/IDtlsSrtpPeer.cs index b5cab6526f..fd9b725ee3 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/IDtlsSrtpPeer.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/DTLSSRTP/IDtlsSrtpPeer.cs @@ -24,25 +24,24 @@ using SIPSorcery.Net.SharpSRTP.SRTP; using System; -namespace SIPSorcery.Net.SharpSRTP.DTLSSRTP -{ - public class DtlsSessionStartedEventArgs : EventArgs - { - public SrtpSessionContext Context { get; private set; } - public Certificate PeerCertificate { get; private set; } - public DatagramTransport ClientDatagramTransport { get; private set; } +namespace SIPSorcery.Net.SharpSRTP.DTLSSRTP; - public DtlsSessionStartedEventArgs(SrtpSessionContext context, Certificate peerCertificate, DatagramTransport clientDatagramTransport) - { - this.Context = context ?? throw new ArgumentNullException(nameof(context)); - this.PeerCertificate = peerCertificate ?? throw new ArgumentNullException(nameof(peerCertificate)); - this.ClientDatagramTransport = clientDatagramTransport ?? throw new ArgumentNullException(nameof(clientDatagramTransport)); - } - } +public class DtlsSessionStartedEventArgs : EventArgs +{ + public SrtpSessionContext Context { get; private set; } + public Certificate PeerCertificate { get; private set; } + public DatagramTransport? ClientDatagramTransport { get; private set; } - public interface IDtlsSrtpPeer : IDtlsPeer + public DtlsSessionStartedEventArgs(SrtpSessionContext context, Certificate peerCertificate, DatagramTransport? clientDatagramTransport) { - event EventHandler OnSessionStarted; - SrtpSessionContext CreateSessionContext(SecurityParameters securityParameters); + this.Context = context ?? throw new ArgumentNullException(nameof(context)); + this.PeerCertificate = peerCertificate ?? throw new ArgumentNullException(nameof(peerCertificate)); + this.ClientDatagramTransport = clientDatagramTransport ?? throw new ArgumentNullException(nameof(clientDatagramTransport)); } } + +public interface IDtlsSrtpPeer : IDtlsPeer +{ + event EventHandler? OnSessionStarted; + SrtpSessionContext CreateSessionContext(SecurityParameters securityParameters); +} diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/Log.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/Log.cs index 765fa095f7..4269c36ac9 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/Log.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/Log.cs @@ -20,52 +20,294 @@ // SOFTWARE. using System; +using Microsoft.Extensions.Logging; +using Org.BouncyCastle.Tls; -namespace SIPSorcery.Net.SharpSRTP +namespace SIPSorcery.Net.SharpSRTP; + +public static class Log { - public static class Log - { - public static bool WarnEnabled { get; set; } = true; - public static void Warn(string message, Exception ex = null) - { - SinkWarn(message, ex); - } - - public static bool ErrorEnabled { get; set; } = true; - public static void Error(string message, Exception ex = null) - { - SinkError(message, ex); - } - - public static bool TraceEnabled { get; set; } = true; - public static void Trace(string message, Exception ex = null) - { - SinkTrace(message, ex); - } - - public static bool DebugEnabled { get; set; } -#if DEBUG - = true; -#endif - - public static void Debug(string message, Exception ex = null) - { - SinkDebug(message, ex); - } - - public static bool InfoEnabled { get; set; } -#if DEBUG - = true; -#endif - public static void Info(string message, Exception ex = null) - { - SinkInfo(message, ex); - } - - public static Action SinkWarn = new Action((m, ex) => { System.Diagnostics.Debug.WriteLine(m); }); - public static Action SinkError = new Action((m, ex) => { System.Diagnostics.Debug.WriteLine(m); }); - public static Action SinkTrace = new Action((m, ex) => { System.Diagnostics.Debug.WriteLine(m); }); - public static Action SinkDebug = new Action((m, ex) => { System.Diagnostics.Debug.WriteLine(m); }); - public static Action SinkInfo = new Action((m, ex) => { System.Diagnostics.Debug.WriteLine(m); }); + // wire up the sipsorcery's logger + public static ILogger Logger { get; } = LogFactory.CreateLogger(typeof(Log).Namespace!); + + public static void LogDtlsServerAlertRaised(this ILogger logger, short alertLevel, short alertDescription, string message, Exception cause) + { + SharpSrtpLoggingExtensions.LogDtlsServerAlertRaisedImpl( + logger, + AlertLevel.GetText(alertLevel), + AlertDescription.GetText(alertDescription), + message, + cause); + } + + public static void LogDtlsServerAlertReceived(this ILogger logger, short level, short alertDescription) + { + SharpSrtpLoggingExtensions.LogDtlsServerAlertReceivedImpl( + logger, + AlertLevel.GetText(level), + AlertDescription.GetText(alertDescription)); + } + + public static void LogDtlsServerNegotiated(this ILogger logger, ProtocolVersion serverVersion) + { + SharpSrtpLoggingExtensions.LogDtlsServerNegotiatedImpl(logger, serverVersion.ToString()); + } + + public static void LogDtlsServerCertificateChainReceived(this ILogger logger, int chainLength) + { + SharpSrtpLoggingExtensions.LogDtlsServerCertificateChainReceivedImpl(logger, chainLength); + } + + public static void LogDtlsServerCertificateFingerprint(this ILogger logger, string fingerprint, string subject) + { + SharpSrtpLoggingExtensions.LogDtlsServerCertificateFingerprintImpl(logger, fingerprint, subject); + } + + public static void LogDtlsServerAlpn(this ILogger logger, string alpnProtocol) + { + SharpSrtpLoggingExtensions.LogDtlsServerAlpnImpl(logger, alpnProtocol); + } + + public static void LogDtlsServerTlsServerEndPoint(this ILogger logger, string tlsServerEndPoint) + { + SharpSrtpLoggingExtensions.LogDtlsServerTlsServerEndPointImpl(logger, tlsServerEndPoint); + } + + public static void LogDtlsServerTlsUnique(this ILogger logger, string tlsUnique) + { + SharpSrtpLoggingExtensions.LogDtlsServerTlsUniqueImpl(logger, tlsUnique); + } + + public static void LogDtlsClientAlertRaised(this ILogger logger, short alertLevel, short alertDescription, string message, Exception cause) + { + SharpSrtpLoggingExtensions.LogDtlsClientAlertRaisedImpl( + logger, + AlertLevel.GetText(alertLevel), + AlertDescription.GetText(alertDescription), + message, + cause); + } + + public static void LogDtlsClientAlertReceived(this ILogger logger, short level, short alertDescription) + { + SharpSrtpLoggingExtensions.LogDtlsClientAlertReceivedImpl( + logger, + AlertLevel.GetText(level), + AlertDescription.GetText(alertDescription)); + } + + public static void LogDtlsClientNegotiated(this ILogger logger, ProtocolVersion serverVersion) + { + SharpSrtpLoggingExtensions.LogDtlsClientNegotiatedImpl(logger, serverVersion.ToString()); + } + + public static void LogDtlsClientAlpn(this ILogger logger, string alpnProtocol) + { + SharpSrtpLoggingExtensions.LogDtlsClientAlpnImpl(logger, alpnProtocol); + } + + public static void LogDtlsClientSessionResumed(this ILogger logger, string sessionId) + { + SharpSrtpLoggingExtensions.LogDtlsClientSessionResumedImpl(logger, sessionId); + } + + public static void LogDtlsClientSessionEstablished(this ILogger logger, string sessionId) + { + SharpSrtpLoggingExtensions.LogDtlsClientSessionEstablishedImpl(logger, sessionId); } + + public static void LogDtlsClientTlsServerEndPoint(this ILogger logger, string tlsServerEndPoint) + { + SharpSrtpLoggingExtensions.LogDtlsClientTlsServerEndPointImpl(logger, tlsServerEndPoint); + } + + public static void LogDtlsClientTlsUnique(this ILogger logger, string tlsUnique) + { + SharpSrtpLoggingExtensions.LogDtlsClientTlsUniqueImpl(logger, tlsUnique); + } + + public static void LogDtlsClientServerCertificateChainReceived(this ILogger logger, int chainLength) + { + SharpSrtpLoggingExtensions.LogDtlsClientServerCertificateChainReceivedImpl(logger, chainLength); + } + + public static void LogDtlsClientServerCertificateFingerprint(this ILogger logger, string fingerprint, string subject) + { + SharpSrtpLoggingExtensions.LogDtlsClientServerCertificateFingerprintImpl(logger, fingerprint, subject); + } +} + +internal static partial class SharpSrtpLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "DtlsServerAlertRaised", + Level = LogLevel.Debug, + Message = "DTLS server raised alert: {AlertLevel}, {AlertDescription}> {Message}")] + internal static partial void LogDtlsServerAlertRaisedImpl( + ILogger logger, + string alertLevel, + string alertDescription, + string message, + Exception cause); + + [LoggerMessage( + EventId = 1, + EventName = "DtlsServerAlertReceived", + Level = LogLevel.Debug, + Message = "DTLS server received alert: {AlertLevel}, {AlertDescription}")] + internal static partial void LogDtlsServerAlertReceivedImpl( + ILogger logger, + string alertLevel, + string alertDescription); + + [LoggerMessage( + EventId = 2, + EventName = "DtlsServerNegotiated", + Level = LogLevel.Debug, + Message = "DTLS server negotiated {ServerVersion}")] + internal static partial void LogDtlsServerNegotiatedImpl( + ILogger logger, + string serverVersion); + + [LoggerMessage( + EventId = 3, + EventName = "DtlsServerCertificateChainReceived", + Level = LogLevel.Debug, + Message = "DTLS server received client certificate chain of length {ChainLength}")] + internal static partial void LogDtlsServerCertificateChainReceivedImpl( + ILogger logger, + int chainLength); + + [LoggerMessage( + EventId = 4, + EventName = "DtlsServerCertificateFingerprint", + Level = LogLevel.Debug, + Message = " fingerprint:SHA-256 {Fingerprint} ({Subject})")] + internal static partial void LogDtlsServerCertificateFingerprintImpl( + ILogger logger, + string fingerprint, + string subject); + + [LoggerMessage( + EventId = 5, + EventName = "DtlsServerAlpn", + Level = LogLevel.Debug, + Message = "Server ALPN: {AlpnProtocol}")] + internal static partial void LogDtlsServerAlpnImpl( + ILogger logger, + string alpnProtocol); + + [LoggerMessage( + EventId = 6, + EventName = "DtlsServerTlsServerEndPoint", + Level = LogLevel.Debug, + Message = "Server 'tls-server-end-point': {TlsServerEndPoint}")] + internal static partial void LogDtlsServerTlsServerEndPointImpl( + ILogger logger, + string tlsServerEndPoint); + + [LoggerMessage( + EventId = 7, + EventName = "DtlsServerTlsUnique", + Level = LogLevel.Debug, + Message = "Server 'tls-unique': {TlsUnique}")] + internal static partial void LogDtlsServerTlsUniqueImpl( + ILogger logger, + string tlsUnique); + + [LoggerMessage( + EventId = 8, + EventName = "DtlsClientAlertRaised", + Level = LogLevel.Debug, + Message = "DTLS client raised alert: {AlertLevel}, {AlertDescription}> {Message}")] + internal static partial void LogDtlsClientAlertRaisedImpl( + ILogger logger, + string alertLevel, + string alertDescription, + string message, + Exception cause); + + [LoggerMessage( + EventId = 9, + EventName = "DtlsClientAlertReceived", + Level = LogLevel.Debug, + Message = "DTLS client received alert: {AlertLevel}, {AlertDescription}")] + internal static partial void LogDtlsClientAlertReceivedImpl( + ILogger logger, + string alertLevel, + string alertDescription); + + [LoggerMessage( + EventId = 10, + EventName = "DtlsClientNegotiated", + Level = LogLevel.Debug, + Message = "DTLS client negotiated {ServerVersion}")] + internal static partial void LogDtlsClientNegotiatedImpl( + ILogger logger, + string serverVersion); + + [LoggerMessage( + EventId = 11, + EventName = "DtlsClientAlpn", + Level = LogLevel.Debug, + Message = "Client ALPN: {AlpnProtocol}")] + internal static partial void LogDtlsClientAlpnImpl( + ILogger logger, + string alpnProtocol); + + [LoggerMessage( + EventId = 12, + EventName = "DtlsClientSessionResumed", + Level = LogLevel.Debug, + Message = "Client resumed session: {SessionId}")] + internal static partial void LogDtlsClientSessionResumedImpl( + ILogger logger, + string sessionId); + + [LoggerMessage( + EventId = 13, + EventName = "DtlsClientSessionEstablished", + Level = LogLevel.Debug, + Message = "Client established session: {SessionId}")] + internal static partial void LogDtlsClientSessionEstablishedImpl( + ILogger logger, + string sessionId); + + [LoggerMessage( + EventId = 14, + EventName = "DtlsClientTlsServerEndPoint", + Level = LogLevel.Debug, + Message = "Client 'tls-server-end-point': {TlsServerEndPoint}")] + internal static partial void LogDtlsClientTlsServerEndPointImpl( + ILogger logger, + string tlsServerEndPoint); + + [LoggerMessage( + EventId = 15, + EventName = "DtlsClientTlsUnique", + Level = LogLevel.Debug, + Message = "Client 'tls-unique': {TlsUnique}")] + internal static partial void LogDtlsClientTlsUniqueImpl( + ILogger logger, + string tlsUnique); + + [LoggerMessage( + EventId = 16, + EventName = "DtlsClientServerCertificateChainReceived", + Level = LogLevel.Debug, + Message = "DTLS client received server certificate chain of length {ChainLength}")] + internal static partial void LogDtlsClientServerCertificateChainReceivedImpl( + ILogger logger, + int chainLength); + + [LoggerMessage( + EventId = 17, + EventName = "DtlsClientServerCertificateFingerprint", + Level = LogLevel.Debug, + Message = "DTLS client fingerprint:SHA-256 {Fingerprint} ({Subject})")] + internal static partial void LogDtlsClientServerCertificateFingerprintImpl( + ILogger logger, + string fingerprint, + string subject); } diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/AEAD.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/AEAD.cs index efad8bbd9f..6214cdc3bf 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/AEAD.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/AEAD.cs @@ -23,6 +23,8 @@ using Org.BouncyCastle.Crypto.Parameters; using System; using System.Buffers.Binary; +using SIPSorcery.Sys; + #if NET8_0_OR_GREATER using ReadOnlyBytes = System.ReadOnlySpan; using Bytes = System.Span; diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/CTR.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/CTR.cs index d49baadb79..d760eeb39a 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/CTR.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/CTR.cs @@ -20,6 +20,7 @@ // SOFTWARE. using Org.BouncyCastle.Crypto; +using SIPSorcery.Sys; using System; using System.Buffers; using System.Buffers.Binary; diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/F8.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/F8.cs index 2e48968c93..ccca8baca3 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/F8.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/Encryption/F8.cs @@ -21,6 +21,7 @@ using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; +using SIPSorcery.Sys; using System; using System.Buffers; using System.Buffers.Binary; @@ -71,7 +72,7 @@ public static byte[] GenerateRtcpMessageKeyIV(IBlockCipher engine, byte[] k_e, b #endif GenerateRtcpIV(iv, rtcpPacket, index); - byte[] iv2 = new byte[BLOCK_SIZE]; + var iv2 = new byte[BLOCK_SIZE]; GenerateIV2(engine, k_e, k_s, iv, iv2); @@ -108,14 +109,14 @@ private static void GenerateIV2(IBlockCipher engine, byte[] k_e, byte[] k_s, Rea public static void Encrypt(IBlockCipher aes, ReadOnlySpan input, Span output, ReadOnlySpan iv) { - int payloadSize = input.Length; - int blockCount = (payloadSize + BLOCK_SIZE - 1) / BLOCK_SIZE; - byte[] cipher = ArrayPool.Shared.Rent(blockCount * BLOCK_SIZE); + var payloadSize = input.Length; + var blockCount = (payloadSize + BLOCK_SIZE - 1) / BLOCK_SIZE; + var cipher = ArrayPool.Shared.Rent(blockCount * BLOCK_SIZE); try { - int blockNo = 0; - byte[] iv2 = GC.AllocateUninitializedArray(iv.Length); + var blockNo = 0; + var iv2 = GC.AllocateUninitializedArray(iv.Length); for (uint j = 0; j < blockCount; j++) { iv.CopyTo(iv2); diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpContext.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpContext.cs index 72cde1d4e5..b937453591 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpContext.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpContext.cs @@ -26,11 +26,14 @@ using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Parameters; using SIPSorcery.Net.SharpSRTP.SRTP.Readers; +using SIPSorcery.Sys; using System; using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; using System.Threading; +using System.Diagnostics; + #if NET8_0_OR_GREATER using ReadOnlyBytes = System.ReadOnlySpan; using Bytes = System.Span; @@ -202,7 +205,7 @@ public void SetSequence(uint sequenceNumber) /// /// Sender only -- current rollover counter (ROC) for this SSRC. /// Per RFC 3711 section 3.2.1 each SRTP stream maintains its own ROC; - /// SSRCs that share the same SrtpContext (e.g. audio + video + /// SSRCs that share the same (e.g. audio + video /// bundled on the same DTLS-SRTP transport) MUST track ROC /// independently. Incremented from 0 each time the 16-bit /// RTP sequence number wraps from 0xFFFF to 0x0000 on this @@ -228,30 +231,33 @@ public class SrtpContext : ISrtpContext public const uint E_FLAG = 0x80000000; private readonly SrtpContextType _contextType; - public SrtpContextType ContextType { get { return _contextType; } } + public SrtpContextType? ContextType { get { return _contextType; } } - public event EventHandler OnRekeyingRequested; + public event EventHandler? OnRekeyingRequested; - public HMac HMAC { get; private set; } - public IBlockCipher PayloadCTR { get; private set; } - public IBlockCipher PayloadF8 { get; private set; } - public IAeadBlockCipher PayloadAEAD { get; private set; } + public HMac? HMAC { get; private set; } + public IBlockCipher? PayloadCTR { get; private set; } + public IBlockCipher? PayloadF8 { get; private set; } + public IAeadBlockCipher? PayloadAEAD { get; private set; } public byte[] Iv12 { get; } = new byte[Encryption.AEAD.BLOCK_SIZE]; public byte[] Iv16 { get; } = new byte[Encryption.CTR.BLOCK_SIZE]; - public IBlockCipher HeaderCTR { get; private set; } - public IBlockCipher HeaderF8 { get; private set; } + public IBlockCipher? HeaderCTR { get; private set; } + public IBlockCipher? HeaderF8 { get; private set; } - public SrtpProtectionProfileConfiguration ProtectionProfile { get; set; } + public SrtpProtectionProfileConfiguration? ProtectionProfile { get; set; } public SrtpCiphers Cipher { get; set; } - public SrtpAuth Auth { get; set; } + public SrtpAuth? Auth { get; set; } public ReadOnlyMemory MasterKey { get; set; } public ReadOnlyMemory MasterSalt { get; set; } /// - /// Rollover counter. + /// DEPRECATED. RFC 3711 section 3.2.1 specifies that the rollover counter is per-SSRC, not per-SrtpContext. Use + /// on the per-SSRC context returned from + /// instead. This property is retained as a settable field for binary + /// compatibility but is no longer read or written by ProtectRtp or UnprotectRtp. /// /// /// DEPRECATED. RFC 3711 section 3.2.1 specifies that the rollover @@ -295,7 +301,7 @@ public class SrtpContext : ISrtpContext /// /// Session key for encryption. /// - public byte[] K_e { get; set; } + public byte[]? K_e { get; set; } /// /// The byte-length of k_s. @@ -305,23 +311,26 @@ public class SrtpContext : ISrtpContext /// /// Session salting key. /// - public byte[] K_s { get; set; } + public byte[]? K_s { get; set; } /// /// Session key for RTP header encyption. Not used in RTCP. /// - public byte[] K_he { get; set; } + public byte[]? K_he { get; set; } /// /// Session salt for header encryption. /// - public byte[] K_hs { get; set; } + public byte[]? K_hs { get; set; } /// /// Gets or sets the encryption mask applied to RTP header extensions. /// - /// The encryption mask is used to protect the contents of RTP header extensions. If set to null, header extensions will not be encrypted. - public byte[] RtpHeaderExtensionsEncryptionMask { get; set; } = null; + /// + /// The encryption mask is used to protect the contents of RTP header extensions. If set to null, header + /// extensions will not be encrypted. + /// + public byte[]? RtpHeaderExtensionsEncryptionMask { get; set; } /// /// The byte-length of the session keys for authentication. @@ -331,7 +340,7 @@ public class SrtpContext : ISrtpContext /// /// The session message authentication key. /// - public byte[] K_a { get; set; } + public byte[]? K_a { get; set; } /// /// The byte-length of the output authentication tag. @@ -411,11 +420,11 @@ public virtual void DeriveSessionKeys(ulong index = 0) this.PayloadF8 = AesUtilities.CreateEngine(); this.HeaderF8 = AesUtilities.CreateEngine(); } - else if (Cipher == SrtpCiphers.AEAD_AES_128_GCM || Cipher == SrtpCiphers.AEAD_AES_256_GCM) + else if (Cipher is SrtpCiphers.AEAD_AES_128_GCM or SrtpCiphers.AEAD_AES_256_GCM) { this.PayloadAEAD = new GcmBlockCipher(AesUtilities.CreateEngine()); } - else if (Cipher == SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM || Cipher == SrtpCiphers.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM) + else if (Cipher is SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM or SrtpCiphers.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM) { this.PayloadAEAD = new GcmBlockCipher(AesUtilities.CreateEngine()); } @@ -442,7 +451,7 @@ public virtual void DeriveSessionKeys(ulong index = 0) ariaHeader.Init(true, new KeyParameter(K_he)); this.HeaderCTR = ariaHeader; - if (Cipher == SrtpCiphers.AEAD_ARIA_128_GCM || Cipher == SrtpCiphers.AEAD_ARIA_256_GCM) + if (Cipher is SrtpCiphers.AEAD_ARIA_128_GCM or SrtpCiphers.AEAD_ARIA_256_GCM) { this.PayloadAEAD = new GcmBlockCipher(new AriaEngine()); } @@ -596,8 +605,7 @@ public virtual int ProtectRtp(ReadOnlyBytes input, Bytes output, out int outputB // context-wide Roc field caused all streams sharing the // context to have their keystream desynchronise from the // receiver whenever ANY stream's sequence number wrapped. - SsrcSrtpContext outboundCtx; - if (!context.ReplayProtection.TryGetValue(ssrc, out outboundCtx)) + if (!context.ReplayProtection.TryGetValue(ssrc, out var outboundCtx)) { outboundCtx = new SsrcSrtpContext(); context.ReplayProtection.Add(ssrc, outboundCtx); @@ -635,6 +643,10 @@ public virtual int ProtectRtp(ReadOnlyBytes input, Bytes output, out int outputB case SrtpCiphers.AES_128_F8: { + Debug.Assert(context.PayloadF8 is not null); + Debug.Assert(context.K_e is not null); + Debug.Assert(context.K_s is not null); + Debug.Assert(context.PayloadCTR is not null); SRTP.Encryption.F8.GenerateRtpMessageKeyIV(context.PayloadF8, context.K_e, context.K_s, input, roc, context.Iv16); SRTP.Encryption.F8.Encrypt(context.PayloadCTR, input.Slice(offset, length - offset), output.Slice(offset, length - offset), context.Iv16); } @@ -647,6 +659,7 @@ public virtual int ProtectRtp(ReadOnlyBytes input, Bytes output, out int outputB case SrtpCiphers.ARIA_256_CTR: case SrtpCiphers.SEED_128_CTR: { + Debug.Assert(context.PayloadCTR is not null); SRTP.Encryption.CTR.GenerateMessageKeyIV(context.K_s, ssrc, index, context.Iv16); SRTP.Encryption.CTR.Encrypt(context.PayloadCTR, input.Slice(offset, length - offset), output.Slice(offset, length - offset), context.Iv16); } @@ -659,6 +672,8 @@ public virtual int ProtectRtp(ReadOnlyBytes input, Bytes output, out int outputB case SrtpCiphers.SEED_128_CCM: case SrtpCiphers.SEED_128_GCM: { + Debug.Assert(context.PayloadAEAD is not null); + Debug.Assert(context.K_e is not null); SRTP.Encryption.AEAD.GenerateMessageKeyIV(context.K_s, ssrc, index, context.Iv12); SRTP.Encryption.AEAD.Encrypt(context.PayloadAEAD, true, input.Slice(offset, length - offset), output.Slice(offset), context.Iv12, context.K_e, context.N_tag, output.Slice(0, offset)); length += context.N_tag; @@ -686,6 +701,9 @@ public virtual int ProtectRtp(ReadOnlyBytes input, Bytes output, out int outputB input.Slice(offset, length - offset).CopyTo(syntheticRtpPacket.AsSpan(rtpHeaderLength, length - offset)); // apply inner cryptographic algorithm + Debug.Assert(context.PayloadAEAD is not null); + Debug.Assert(context.K_e is not null); + Debug.Assert(context.K_s is not null); var innerK_e = KeyParameter.Create(context.K_e.Slice(0, context.K_e.Length / 2)); var innerK_s = context.K_s.AsSpan(0, context.K_s.Length / 2); SRTP.Encryption.AEAD.GenerateMessageKeyIV(innerK_s, ssrc, index, context.Iv12); @@ -722,9 +740,11 @@ public virtual int ProtectRtp(ReadOnlyBytes input, Bytes output, out int outputB } } - byte[] auth = null; + byte[]? auth = null; if (context.Auth != SrtpAuth.NONE) { + Debug.Assert(context.HMAC is not null); + BinaryPrimitives.WriteUInt32BigEndian(output.Slice(length, 4), roc); auth = SRTP.Authentication.HMAC.GenerateAuthTag(context.HMAC, output.Slice(0, length + 4)); @@ -772,6 +792,10 @@ public int ProtectUnprotectRtpHeaderExtensions(ReadOnlySpan payload, Span< case SrtpCiphers.AES_128_F8: { + Debug.Assert(context.HeaderF8 is not null); + Debug.Assert(context.K_he is not null); + Debug.Assert(context.K_hs is not null); + Debug.Assert(context.HeaderCTR is not null); SRTP.Encryption.F8.GenerateRtpMessageKeyIV(context.HeaderF8, context.K_he, context.K_hs, payload, roc, context.Iv16); SRTP.Encryption.F8.Encrypt(context.HeaderCTR, rtpExtensions, rtpExtensionsEncrypted.AsSpan(0, rtpExtensions.Length), context.Iv16); } @@ -790,6 +814,7 @@ public int ProtectUnprotectRtpHeaderExtensions(ReadOnlySpan payload, Span< case SrtpCiphers.SEED_128_CCM: case SrtpCiphers.SEED_128_GCM: { + Debug.Assert(context.HeaderCTR is not null); SRTP.Encryption.CTR.GenerateMessageKeyIV(context.K_hs, ssrc, index, context.Iv16); SRTP.Encryption.CTR.Encrypt(context.HeaderCTR, rtpExtensions, rtpExtensionsEncrypted.AsSpan(0, rtpExtensions.Length), context.Iv16); } @@ -798,6 +823,8 @@ public int ProtectUnprotectRtpHeaderExtensions(ReadOnlySpan payload, Span< case SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM: case SrtpCiphers.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM: { + Debug.Assert(context.K_hs is not null); + Debug.Assert(context.HeaderCTR is not null); var outerK_hs = context.K_hs.AsSpan(context.K_hs.Length / 2); SRTP.Encryption.CTR.GenerateMessageKeyIV(outerK_hs, ssrc, index, context.Iv16); SRTP.Encryption.CTR.Encrypt(context.HeaderCTR, rtpExtensions, rtpExtensionsEncrypted.AsSpan(0, rtpExtensions.Length), context.Iv16); @@ -856,8 +883,7 @@ public virtual int UnprotectRtp(ReadOnlyBytes input, Bytes output, out int outpu var ssrc = RtpReader.ReadSsrc(input); var sequenceNumber = RtpReader.ReadSequenceNumber(input); - SsrcSrtpContext ssrcContext; - if (context.ReplayProtection.TryGetValue(ssrc, out ssrcContext) == false) + if (context.ReplayProtection.TryGetValue(ssrc, out var ssrcContext) == false) { ssrcContext = new SsrcSrtpContext(); context.ReplayProtection.Add(ssrc, ssrcContext); @@ -881,6 +907,7 @@ public virtual int UnprotectRtp(ReadOnlyBytes input, Bytes output, out int outpu input.Slice(0, authenticatedLen).CopyTo(msgAuth.AsSpan(0, authenticatedLen)); BinaryPrimitives.WriteUInt32BigEndian(msgAuth.AsSpan(authenticatedLen, 4), roc); + Debug.Assert(context.HMAC is not null); var auth = SRTP.Authentication.HMAC.GenerateAuthTag(context.HMAC, msgAuth.Slice(0, authenticatedLen + 4)); for (var i = 0; i < context.N_tag; i++) { @@ -901,154 +928,164 @@ public virtual int UnprotectRtp(ReadOnlyBytes input, Bytes output, out int outpu // Read-only replay check. The window/ROC state is NOT advanced here; that only happens once // the packet has authenticated (UpdateReplayWindow below). RFC 3711 section 3.3. - if (!ssrcContext.CheckReplayWindow(index)) - { + if (!ssrcContext.CheckReplayWindow(index)) { outputBufferLength = 0; return ERROR_REPLAY_CHECK_FAILED; } try { - switch (context.Cipher) - { - case SrtpCiphers.NULL: - { - var dataLen = length - mki.Length - context.N_tag; - input.Slice(0, dataLen).CopyTo(output.Slice(0, dataLen)); - outputBufferLength = dataLen; - } - break; - - case SrtpCiphers.AES_128_F8: - { - SRTP.Encryption.F8.GenerateRtpMessageKeyIV(context.PayloadF8, context.K_e, context.K_s, input, roc, context.Iv16); - var decLen = length - mki.Length - context.N_tag; - input.Slice(0, offset).CopyTo(output.Slice(0, offset)); - SRTP.Encryption.F8.Encrypt(context.PayloadCTR, input.Slice(offset, decLen - offset), output.Slice(offset, decLen - offset), context.Iv16); - outputBufferLength = decLen; - } - break; - - case SrtpCiphers.AES_128_CM: - case SrtpCiphers.AES_192_CM: - case SrtpCiphers.AES_256_CM: - case SrtpCiphers.ARIA_128_CTR: - case SrtpCiphers.ARIA_256_CTR: - case SrtpCiphers.SEED_128_CTR: - { - SRTP.Encryption.CTR.GenerateMessageKeyIV(context.K_s, ssrc, index, context.Iv16); - var decLen = length - mki.Length - context.N_tag; - input.Slice(0, offset).CopyTo(output.Slice(0, offset)); - SRTP.Encryption.CTR.Encrypt(context.PayloadCTR, input.Slice(offset, decLen - offset), output.Slice(offset, decLen - offset), context.Iv16); - outputBufferLength = decLen; - } - break; - - case SrtpCiphers.AEAD_AES_128_GCM: - case SrtpCiphers.AEAD_AES_256_GCM: - case SrtpCiphers.AEAD_ARIA_128_GCM: - case SrtpCiphers.AEAD_ARIA_256_GCM: - case SrtpCiphers.SEED_128_CCM: - case SrtpCiphers.SEED_128_GCM: - { - SRTP.Encryption.AEAD.GenerateMessageKeyIV(context.K_s, ssrc, index, context.Iv12); - input.Slice(0, offset).CopyTo(output.Slice(0, offset)); - SRTP.Encryption.AEAD.Encrypt(context.PayloadAEAD, false, input.Slice(offset, length - mki.Length - offset), output.Slice(offset), context.Iv12, context.K_e, context.N_tag, input.Slice(0, offset)); - outputBufferLength = length - mki.Length - context.N_tag; - } - break; - - case SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM: - case SrtpCiphers.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM: - { - // apply outer cryptographic algorithm - var outerK_e = KeyParameter.Create(context.K_e.Slice(context.K_e.Length / 2)); - var outerK_s = context.K_s.AsSpan(context.K_s.Length / 2); - SRTP.Encryption.AEAD.GenerateMessageKeyIV(outerK_s, ssrc, index, context.Iv12); - SRTP.Encryption.AEAD.Encrypt(context.PayloadAEAD, false, input.Slice(offset, length - mki.Length - offset), output.Slice(offset), context.Iv12, outerK_e, context.N_tag / 2, input.Slice(0, offset)); - - // copy header from input to output - input.Slice(0, offset).CopyTo(output.Slice(0, offset)); + switch (context.Cipher) + { + case SrtpCiphers.NULL: + { + var dataLen = length - mki.Length - context.N_tag; + input.Slice(0, dataLen).CopyTo(output.Slice(0, dataLen)); + outputBufferLength = dataLen; + } + break; - // calculate OHB size - it can now be larger than 1 byte if it was modified - var lastOhbByteIndex = length - mki.Length - context.N_tag / 2 - 1; - var ohbConfig = output[lastOhbByteIndex]; - var ohbLength = 1; - if ((ohbConfig & 0x01) == 0x01) + case SrtpCiphers.AES_128_F8: { - ohbLength += 2; + Debug.Assert(context.PayloadF8 is not null); + Debug.Assert(context.K_e is not null); + Debug.Assert(context.K_s is not null); + Debug.Assert(context.PayloadCTR is not null); + SRTP.Encryption.F8.GenerateRtpMessageKeyIV(context.PayloadF8, context.K_e, context.K_s, input, roc, context.Iv16); + var decLen = length - mki.Length - context.N_tag; + input.Slice(0, offset).CopyTo(output.Slice(0, offset)); + SRTP.Encryption.F8.Encrypt(context.PayloadCTR, input.Slice(offset, decLen - offset), output.Slice(offset, decLen - offset), context.Iv16); + outputBufferLength = decLen; } - if ((ohbConfig & 0x02) == 0x02) + break; + + case SrtpCiphers.AES_128_CM: + case SrtpCiphers.AES_192_CM: + case SrtpCiphers.AES_256_CM: + case SrtpCiphers.ARIA_128_CTR: + case SrtpCiphers.ARIA_256_CTR: + case SrtpCiphers.SEED_128_CTR: { - ohbLength += 1; + Debug.Assert(context.PayloadCTR is not null); + SRTP.Encryption.CTR.GenerateMessageKeyIV(context.K_s, ssrc, index, context.Iv16); + var decLen = length - mki.Length - context.N_tag; + input.Slice(0, offset).CopyTo(output.Slice(0, offset)); + SRTP.Encryption.CTR.Encrypt(context.PayloadCTR, input.Slice(offset, decLen - offset), output.Slice(offset, decLen - offset), context.Iv16); + outputBufferLength = decLen; } + break; - // form a synthetic RTP packet - var rtpHeaderLength = RtpReader.ReadHeaderLenWithoutExtensions(output); - var rtpExtensionsLength = RtpReader.ReadExtensionsLength(output); - var syntheticRtpPacketLen = length - rtpExtensionsLength - (context.N_tag / 2) - ohbLength; - var syntheticRtpPacket = ArrayPool.Shared.Rent(syntheticRtpPacketLen); + case SrtpCiphers.AEAD_AES_128_GCM: + case SrtpCiphers.AEAD_AES_256_GCM: + case SrtpCiphers.AEAD_ARIA_128_GCM: + case SrtpCiphers.AEAD_ARIA_256_GCM: + case SrtpCiphers.SEED_128_CCM: + case SrtpCiphers.SEED_128_GCM: + { + Debug.Assert(context.PayloadAEAD is not null); + Debug.Assert(context.K_e is not null); + SRTP.Encryption.AEAD.GenerateMessageKeyIV(context.K_s, ssrc, index, context.Iv12); + input.Slice(0, offset).CopyTo(output.Slice(0, offset)); + SRTP.Encryption.AEAD.Encrypt(context.PayloadAEAD, false, input.Slice(offset, length - mki.Length - offset), output.Slice(offset), context.Iv12, context.K_e, context.N_tag, input.Slice(0, offset)); + outputBufferLength = length - mki.Length - context.N_tag; + } + break; - try + case SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM: + case SrtpCiphers.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM: { - // copy header without extensions - output.Slice(0, rtpHeaderLength).CopyTo(syntheticRtpPacket.AsSpan(0, rtpHeaderLength)); + Debug.Assert(context.K_e is not null); + Debug.Assert(context.K_s is not null); + Debug.Assert(context.PayloadAEAD is not null); + // apply outer cryptographic algorithm + var outerK_e = KeyParameter.Create(context.K_e.Slice(context.K_e.Length / 2)); + var outerK_s = context.K_s.AsSpan(context.K_s.Length / 2); + SRTP.Encryption.AEAD.GenerateMessageKeyIV(outerK_s, ssrc, index, context.Iv12); + SRTP.Encryption.AEAD.Encrypt(context.PayloadAEAD, false, input.Slice(offset, length - mki.Length - offset), output.Slice(offset), context.Iv12, outerK_e, context.N_tag / 2, input.Slice(0, offset)); - // set X bit to 0 - syntheticRtpPacket[0] &= 0xEF; + // copy header from input to output + input.Slice(0, offset).CopyTo(output.Slice(0, offset)); - // restore original header values from the OHB + // calculate OHB size - it can now be larger than 1 byte if it was modified + var lastOhbByteIndex = length - mki.Length - context.N_tag / 2 - 1; + var ohbConfig = output[lastOhbByteIndex]; + var ohbLength = 1; if ((ohbConfig & 0x01) == 0x01) { - syntheticRtpPacket[2] = output[lastOhbByteIndex - ohbLength - 1]; - syntheticRtpPacket[3] = output[lastOhbByteIndex - ohbLength]; + ohbLength += 2; } if ((ohbConfig & 0x02) == 0x02) { - var pt = output[lastOhbByteIndex - ohbLength]; - syntheticRtpPacket[1] = (byte)((syntheticRtpPacket[1] & 0x80) | (pt & 0x7F)); + ohbLength += 1; } - if ((ohbConfig & 0x04) == 0x04) - { - var markerBit = (ohbConfig & 0x08) == 0x08; - syntheticRtpPacket[1] = (byte)((markerBit ? 0x80 : 0x00) | (syntheticRtpPacket[1] & 0x7F)); - } - - // copy the payload including the inner authentication tag - output.Slice(offset, length - offset - mki.Length - context.N_tag / 2 - ohbLength).CopyTo(syntheticRtpPacket.AsSpan(rtpHeaderLength, length - offset - mki.Length - context.N_tag / 2 - ohbLength)); - - var innerSsrc = RtpReader.ReadSsrc(syntheticRtpPacket); - var innerSequenceNumber = RtpReader.ReadSequenceNumber(syntheticRtpPacket); - var innerIndex = SrtpContext.DetermineRtpIndex(lastSeq, innerSequenceNumber, lastRoc); - - // apply inner cryptographic algorithm - var innerK_e = KeyParameter.Create(context.K_e.Slice(0, context.K_e.Length / 2)); - var innerK_s = context.K_s.AsSpan(0, context.K_s.Length / 2); - SRTP.Encryption.AEAD.GenerateMessageKeyIV(innerK_s, innerSsrc, innerIndex, context.Iv12); - SRTP.Encryption.AEAD.Encrypt(context.PayloadAEAD, false, syntheticRtpPacket.Slice(rtpHeaderLength, syntheticRtpPacketLen - rtpHeaderLength), syntheticRtpPacket.Slice(rtpHeaderLength, syntheticRtpPacketLen - rtpHeaderLength), context.Iv12, innerK_e, context.N_tag / 2, syntheticRtpPacket.Slice(0, rtpHeaderLength)); - // copy the unprotected payload back to the output buffer - syntheticRtpPacket.AsSpan(rtpHeaderLength, syntheticRtpPacketLen - rtpHeaderLength - context.N_tag / 2).CopyTo(output.Slice(offset, syntheticRtpPacketLen - rtpHeaderLength - context.N_tag / 2)); + // form a synthetic RTP packet + var rtpHeaderLength = RtpReader.ReadHeaderLenWithoutExtensions(output); + var rtpExtensionsLength = RtpReader.ReadExtensionsLength(output); + var syntheticRtpPacketLen = length - rtpExtensionsLength - (context.N_tag / 2) - ohbLength; + var syntheticRtpPacket = ArrayPool.Shared.Rent(syntheticRtpPacketLen); - // copy the synthetic header back to the output buffer - syntheticRtpPacket.AsSpan(0, rtpHeaderLength).CopyTo(output.Slice(0, rtpHeaderLength)); - - // update the output buffer length - outputBufferLength = offset + syntheticRtpPacketLen - rtpHeaderLength - context.N_tag / 2; + try + { + // copy header without extensions + output.Slice(0, rtpHeaderLength).CopyTo(syntheticRtpPacket.AsSpan(0, rtpHeaderLength)); + + // set X bit to 0 + syntheticRtpPacket[0] &= 0xEF; + + // restore original header values from the OHB + if ((ohbConfig & 0x01) == 0x01) + { + syntheticRtpPacket[2] = output[lastOhbByteIndex - ohbLength - 1]; + syntheticRtpPacket[3] = output[lastOhbByteIndex - ohbLength]; + } + if ((ohbConfig & 0x02) == 0x02) + { + var pt = output[lastOhbByteIndex - ohbLength]; + syntheticRtpPacket[1] = (byte)((syntheticRtpPacket[1] & 0x80) | (pt & 0x7F)); + } + if ((ohbConfig & 0x04) == 0x04) + { + var markerBit = (ohbConfig & 0x08) == 0x08; + syntheticRtpPacket[1] = (byte)((markerBit ? 0x80 : 0x00) | (syntheticRtpPacket[1] & 0x7F)); + } + + // copy the payload including the inner authentication tag + output.Slice(offset, length - offset - mki.Length - context.N_tag / 2 - ohbLength).CopyTo(syntheticRtpPacket.AsSpan(rtpHeaderLength, length - offset - mki.Length - context.N_tag / 2 - ohbLength)); + + var innerSsrc = RtpReader.ReadSsrc(syntheticRtpPacket); + var innerSequenceNumber = RtpReader.ReadSequenceNumber(syntheticRtpPacket); + var innerIndex = SrtpContext.DetermineRtpIndex(lastSeq, innerSequenceNumber, lastRoc); + + // apply inner cryptographic algorithm + var innerK_e = KeyParameter.Create(context.K_e.Slice(0, context.K_e.Length / 2)); + var innerK_s = context.K_s.AsSpan(0, context.K_s.Length / 2); + SRTP.Encryption.AEAD.GenerateMessageKeyIV(innerK_s, innerSsrc, innerIndex, context.Iv12); + SRTP.Encryption.AEAD.Encrypt(context.PayloadAEAD, false, syntheticRtpPacket.Slice(rtpHeaderLength, syntheticRtpPacketLen - rtpHeaderLength), syntheticRtpPacket.Slice(rtpHeaderLength, syntheticRtpPacketLen - rtpHeaderLength), context.Iv12, innerK_e, context.N_tag / 2, syntheticRtpPacket.Slice(0, rtpHeaderLength)); + + // copy the unprotected payload back to the output buffer + syntheticRtpPacket.AsSpan(rtpHeaderLength, syntheticRtpPacketLen - rtpHeaderLength - context.N_tag / 2).CopyTo(output.Slice(offset, syntheticRtpPacketLen - rtpHeaderLength - context.N_tag / 2)); + + // copy the synthetic header back to the output buffer + syntheticRtpPacket.AsSpan(0, rtpHeaderLength).CopyTo(output.Slice(0, rtpHeaderLength)); + + // update the output buffer length + outputBufferLength = offset + syntheticRtpPacketLen - rtpHeaderLength - context.N_tag / 2; + } + finally + { + ArrayPool.Shared.Return(syntheticRtpPacket); + } } - finally + break; + + default: { - ArrayPool.Shared.Return(syntheticRtpPacket); + outputBufferLength = 0; + return ERROR_UNSUPPORTED_CIPHER; } - } - break; + } - default: - { - outputBufferLength = 0; - return ERROR_UNSUPPORTED_CIPHER; - } - } } catch (Org.BouncyCastle.Crypto.InvalidCipherTextException) { @@ -1120,8 +1157,7 @@ public int ProtectRtcp(ReadOnlyBytes input, Bytes output, out int outputBufferLe var ssrc = RtcpReader.ReadSsrc(input); var offset = RtcpReader.GetHeaderLen(); - SsrcSrtpContext ssrcContext; - if (context.ReplayProtection.TryGetValue(ssrc, out ssrcContext) == false) + if (context.ReplayProtection.TryGetValue(ssrc, out var ssrcContext) == false) { ssrcContext = new SsrcSrtpContext(); context.ReplayProtection.Add(ssrc, ssrcContext); @@ -1137,6 +1173,10 @@ public int ProtectRtcp(ReadOnlyBytes input, Bytes output, out int outputBufferLe case SrtpCiphers.AES_128_F8: { + Debug.Assert(context.PayloadF8 is not null); + Debug.Assert(context.K_e is not null); + Debug.Assert(context.K_s is not null); + Debug.Assert(context.PayloadCTR is not null); var iv = SRTP.Encryption.F8.GenerateRtcpMessageKeyIV(context.PayloadF8, context.K_e, context.K_s, input, index); input.Slice(0, offset).CopyTo(output.Slice(0, offset)); SRTP.Encryption.F8.Encrypt(context.PayloadCTR, input.Slice(offset, length - offset), output.Slice(offset, length - offset), iv); @@ -1150,6 +1190,7 @@ public int ProtectRtcp(ReadOnlyBytes input, Bytes output, out int outputBufferLe case SrtpCiphers.ARIA_256_CTR: case SrtpCiphers.SEED_128_CTR: { + Debug.Assert(context.PayloadCTR is not null); SRTP.Encryption.CTR.GenerateMessageKeyIV(context.K_s, ssrc, ssrcContext.S_l, context.Iv16); input.Slice(0, offset).CopyTo(output.Slice(0, offset)); SRTP.Encryption.CTR.Encrypt(context.PayloadCTR, input.Slice(offset, length - offset), output.Slice(offset, length - offset), context.Iv16); @@ -1163,6 +1204,8 @@ public int ProtectRtcp(ReadOnlyBytes input, Bytes output, out int outputBufferLe case SrtpCiphers.SEED_128_CCM: case SrtpCiphers.SEED_128_GCM: { + Debug.Assert(context.PayloadAEAD is not null); + Debug.Assert(context.K_e is not null); SRTP.Encryption.AEAD.GenerateMessageKeyIV(context.K_s, ssrc, ssrcContext.S_l, context.Iv12); var associatedDataRented = ArrayPool.Shared.Rent(offset + 4); try @@ -1183,6 +1226,9 @@ public int ProtectRtcp(ReadOnlyBytes input, Bytes output, out int outputBufferLe case SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM: case SrtpCiphers.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM: { + Debug.Assert(context.K_e is not null); + Debug.Assert(context.K_s is not null); + Debug.Assert(context.PayloadAEAD is not null); // RTCP under Double AEAD is protected only with the outer layer var outerK_e = KeyParameter.Create(context.K_e.Slice(context.K_e.Length / 2)); var outerK_s = context.K_s.AsSpan(context.K_s.Length / 2); @@ -1222,6 +1268,7 @@ public int ProtectRtcp(ReadOnlyBytes input, Bytes output, out int outputBufferLe if (context.Auth != SrtpAuth.NONE) { + Debug.Assert(context.HMAC is not null); var auth = SRTP.Authentication.HMAC.GenerateAuthTag(context.HMAC, output.Slice(0, length)); auth.AsSpan(0, context.N_tag).CopyTo(output.Slice(length, context.N_tag)); length += context.N_tag; @@ -1266,8 +1313,7 @@ public virtual int UnprotectRtcp(ReadOnlyBytes input, Bytes output, out int outp var ssrc = RtcpReader.ReadSsrc(input); var offset = RtcpReader.GetHeaderLen(); - SsrcSrtpContext ssrcContext; - if (context.ReplayProtection.TryGetValue(ssrc, out ssrcContext) == false) + if (context.ReplayProtection.TryGetValue(ssrc, out var ssrcContext) == false) { ssrcContext = new SsrcSrtpContext(); context.ReplayProtection.Add(ssrc, ssrcContext); @@ -1285,6 +1331,7 @@ public virtual int UnprotectRtcp(ReadOnlyBytes input, Bytes output, out int outp if (context.Auth != SrtpAuth.NONE) { + Debug.Assert(context.HMAC is not null); var auth = SRTP.Authentication.HMAC.GenerateAuthTag(context.HMAC, input.Slice(0, length - context.N_tag - mki.Length)); for (var i = 0; i < context.N_tag; i++) { @@ -1316,6 +1363,10 @@ public virtual int UnprotectRtcp(ReadOnlyBytes input, Bytes output, out int outp case SrtpCiphers.AES_128_F8: { + Debug.Assert(context.PayloadF8 is not null); + Debug.Assert(context.K_e is not null); + Debug.Assert(context.K_s is not null); + Debug.Assert(context.PayloadCTR is not null); var decLen = length - 4 - context.N_tag - mki.Length; var iv = SRTP.Encryption.F8.GenerateRtcpMessageKeyIV(context.PayloadF8, context.K_e, context.K_s, input, originalIndex); input.Slice(0, offset).CopyTo(output.Slice(0, offset)); @@ -1331,6 +1382,7 @@ public virtual int UnprotectRtcp(ReadOnlyBytes input, Bytes output, out int outp case SrtpCiphers.ARIA_256_CTR: case SrtpCiphers.SEED_128_CTR: { + Debug.Assert(context.PayloadCTR is not null); var decLen = length - 4 - context.N_tag - mki.Length; SRTP.Encryption.CTR.GenerateMessageKeyIV(context.K_s, ssrc, ssrcContext.S_l, context.Iv16); input.Slice(0, offset).CopyTo(output.Slice(0, offset)); @@ -1346,6 +1398,8 @@ public virtual int UnprotectRtcp(ReadOnlyBytes input, Bytes output, out int outp case SrtpCiphers.SEED_128_CCM: case SrtpCiphers.SEED_128_GCM: { + Debug.Assert(context.PayloadAEAD is not null); + Debug.Assert(context.K_e is not null); SRTP.Encryption.AEAD.GenerateMessageKeyIV(context.K_s, ssrc, ssrcContext.S_l, context.Iv12); var associatedDataRented = ArrayPool.Shared.Rent(offset + 4); try @@ -1366,6 +1420,9 @@ public virtual int UnprotectRtcp(ReadOnlyBytes input, Bytes output, out int outp case SrtpCiphers.DOUBLE_AEAD_AES_128_GCM_AEAD_AES_128_GCM: case SrtpCiphers.DOUBLE_AEAD_AES_256_GCM_AEAD_AES_256_GCM: { + Debug.Assert(context.K_e is not null); + Debug.Assert(context.K_s is not null); + Debug.Assert(context.PayloadAEAD is not null); // RTCP under Double AEAD is protected only with the outer layer var outerK_e = KeyParameter.Create(context.K_e.Slice(context.K_e.Length / 2)); var outerK_s = context.K_s.AsSpan(context.K_s.Length / 2); diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpKeys.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpKeys.cs index 5772601f00..64daf299b8 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpKeys.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpKeys.cs @@ -32,19 +32,13 @@ public class SrtpKeys public ReadOnlyMemory MasterSalt { get; } public ReadOnlyMemory MasterKeySalt { get; } - public SrtpKeys(SrtpProtectionProfileConfiguration protectionProfile, byte[] masterKeySalt, byte[] mki = default) + public SrtpKeys(SrtpProtectionProfileConfiguration protectionProfile, byte[] masterKeySalt, byte[]? mki = default) { -#if NET8_0_OR_GREATER ArgumentNullException.ThrowIfNull(protectionProfile); this.ProtectionProfile = protectionProfile; ArgumentNullException.ThrowIfNull(masterKeySalt); this.MasterKeySalt = masterKeySalt.AsMemory(); -#else - this.ProtectionProfile = protectionProfile ?? throw new ArgumentNullException(nameof(protectionProfile)); - - this.MasterKeySalt = (masterKeySalt ?? throw new ArgumentNullException(nameof(masterKeySalt))).AsMemory(); -#endif if (masterKeySalt.Length != (protectionProfile.CipherKeyLength + protectionProfile.CipherSaltLength) >> 3) { diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpProtocol.cs b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpProtocol.cs index caeee92de5..c0390c89b3 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpProtocol.cs +++ b/src/SIPSorcery/net/DtlsSrtp/Lib/SRTP/SrtpProtocol.cs @@ -88,7 +88,7 @@ static SrtpProtocol() }; } - public static SrtpKeys CreateMasterKeys(string cryptoSuite, byte[] mki = null, byte[] useMasterKeySalt = null) + public static SrtpKeys CreateMasterKeys(string cryptoSuite, byte[]? mki = null, byte[]? useMasterKeySalt = null) { var srtpSecurityParams = SrtpCryptoSuites[cryptoSuite]; int masterKeyLen = srtpSecurityParams.CipherKeyLength >> 3; diff --git a/src/SIPSorcery/net/DtlsSrtp/NetDtlsSrtpLoggingExtensions.cs b/src/SIPSorcery/net/DtlsSrtp/NetDtlsSrtpLoggingExtensions.cs new file mode 100644 index 0000000000..15db8a5aa7 --- /dev/null +++ b/src/SIPSorcery/net/DtlsSrtp/NetDtlsSrtpLoggingExtensions.cs @@ -0,0 +1,263 @@ +using System; +using System.Text; +using Microsoft.Extensions.Logging; +using Org.BouncyCastle.Tls; +using SIPSorcery.Net.SharpSRTP.DTLSSRTP; + +namespace SIPSorcery.Net; + +internal static partial class NetDtlsSrtpLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "DtlsHandshakeStart", + Level = LogLevel.Debug, + Message = "DTLS commencing handshake as {Role}.")] + public static partial void LogDtlsHandshakeStartUnchecked( + this ILogger logger, + string role); + + [LoggerMessage( + EventId = 0, + EventName = "DtlsHandshakeTimeout", + Level = LogLevel.Warning, + Message = "DTLS handshake as {Role} timed out waiting for handshake to complete.")] + public static partial void LogDtlsHandshakeTimeout( + this ILogger logger, + string role, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "DtlsHandshakeFailed", + Level = LogLevel.Warning, + Message = "DTLS handshake as {Role} failed. {ErrorMessage}")] + public static partial void LogDtlsHandshakeFailed( + this ILogger logger, + string role, + string errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "DtlsCloseNotification", + Level = LogLevel.Debug, + Message = "DTLS client raised close notification: {AlertMessage}")] + private static partial void LogDtlsCloseNotificationUnchecked( + this ILogger logger, + string alertMessage, + Exception exception); + + public static void LogDtlsCloseNotification( + this ILogger logger, + short alertLevel, + short alertDescription, + string message, + Exception cause) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + var alertMessage = BuildAlertMessage(alertLevel, alertDescription, message, cause); + + logger.LogDtlsCloseNotificationUnchecked(alertMessage.ToString(), cause); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "DtlsUnexpectedAlert", + Level = LogLevel.Warning, + Message = "DTLS client raised unexpected alert: {AlertMessage}")] + private static partial void LogDtlsUnexpectedAlertUnchecked( + this ILogger logger, + string alertMessage, + Exception exception); + + public static void LogDtlsUnexpectedAlert( + this ILogger logger, + short alertLevel, + short alertDescription, + string message, + Exception cause) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + var alertMessage = BuildAlertMessage(alertLevel, alertDescription, message, cause); + + logger.LogDtlsUnexpectedAlertUnchecked(alertMessage.ToString(), cause); + } + } + + private static StringBuilder BuildAlertMessage(short alertLevel, short alertDescription, string message, Exception cause) + { + var description = new StringBuilder(); + if (!string.IsNullOrEmpty(message)) + { + description.Append(message); + } + if (cause is { }) + { + description.Append(cause); + } + + var alertMessage = new StringBuilder(); + alertMessage.Append(AlertLevel.GetText(alertLevel)); + alertMessage.Append(", "); + alertMessage.Append(AlertDescription.GetText(alertDescription)); + if (description.Length > 0) + { + alertMessage.Append(", "); + alertMessage.Append(description); + } + alertMessage.Append('.'); + return alertMessage; + } + + [LoggerMessage( + EventId = 0, + EventName = "DtlsReceivedCloseNotification", + Level = LogLevel.Debug, + Message = "DTLS client received close notification: {AlertLevel}, {Description}")] + private static partial void LogDtlsReceivedCloseUnchecked( + this ILogger logger, + string alertLevel, + string description); + + public static void LogDtlsReceivedClose( + this ILogger logger, + short alertLevel, + string description) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDtlsReceivedCloseUnchecked(AlertLevel.GetText(alertLevel), description); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "DtlsReceivedUnexpectedAlert", + Level = LogLevel.Warning, + Message = "DTLS client received unexpected alert: {AlertLevel}, {Description}")] + private static partial void LogDtlsReceivedUnexpectedAlertUnchecked( + this ILogger logger, + string alertLevel, + string description); + + public static void LogDtlsReceivedUnexpectedAlert( + this ILogger logger, + short alertLevel, + string description) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + logger.LogDtlsReceivedUnexpectedAlertUnchecked(AlertLevel.GetText(alertLevel), description); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "DtlsServerNoMatchingCipherSuite", + Level = LogLevel.Warning, + Message = "DTLS server no matching cipher suite. Most likely issue is the client not supporting the server certificate's digital signature algorithm of {SignatureAlgorithm}.")] + public static partial void LogDtlsServerNoMatchingCipherSuite( + this ILogger logger, + string signatureAlgorithm); + + [LoggerMessage( + EventId = 1, + EventName = "DtlsNoRenegotiation", + Level = LogLevel.Warning, + Message = "DTLS server received a client handshake without renegotiation support.")] + public static partial void LogDtlsNoRenegotiation( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SrtpSetupLocalCryptoFailed", + Level = LogLevel.Error, + Message = "Setup local crypto failed. No crypto attribute in {MessageType}.")] + public static partial void LogSrtpSetupLocalCryptoFailedUnchecked( + this ILogger logger, + string messageType); + + [LoggerMessage( + EventId = 0, + EventName = "SrtpSetupRemoteCryptoFailed", + Level = LogLevel.Error, + Message = "Setup remote crypto failed. No crypto attribute in {MessageType}.")] + public static partial void LogSrtpSetupRemoteCryptoFailedUnchecked( + this ILogger logger, + string messageType); + + [LoggerMessage( + EventId = 0, + EventName = "DtlsHandshakeTimedOut", + Level = LogLevel.Warning, + Message = "DTLS transport timed out after {TimeoutMilliseconds}ms waiting for handshake from remote {Role}.")] + public static partial void LogDtlsHandshakeTimedOut( + this ILogger logger, + int timeoutMilliseconds, + string role); + + [LoggerMessage( + EventId = 0, + EventName = "DtlsNoCertificate", + Level = LogLevel.Warning, + Message = "No certificate was set for " + nameof(DtlsSrtpServer) + ".")] + public static partial void LogDtlsNoCertificate( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "DtlsSelectedCipherSuite", + Level = LogLevel.Debug, + Message = "Selected cipher suite: {CipherSuiteName}. Using {SignatureAlgorithm} certificate with fingerprint {Fingerprint}.")] + public static partial void LogDtlsSelectedCipherSuite( + this ILogger logger, + string cipherSuiteName, + string signatureAlgorithm, + RTCDtlsFingerprint fingerprint); + + [LoggerMessage( + EventId = 0, + EventName = "DtlsServerReceivedClose", + Level = LogLevel.Debug, + Message = "DTLS server received close notification: {AlertMsg}")] + private static partial void LogDtlsServerReceivedCloseUnchecked( + this ILogger logger, + string alertMsg); + + public static void LogDtlsServerReceivedClose( + this ILogger logger, + short alertLevel, + string description) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + var msg = $"{AlertLevel.GetText(alertLevel)}{((!string.IsNullOrEmpty(description)) ? $", {description}." : ".")}"; + logger.LogDtlsServerReceivedCloseUnchecked(msg); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "DtlsServerReceivedUnexpectedAlert", + Level = LogLevel.Warning, + Message = "DTLS server received unexpected alert: {AlertMsg}")] + private static partial void LogDtlsServerReceivedUnexpectedAlertUnchecked( + this ILogger logger, + string alertMsg); + + public static void LogDtlsServerReceivedUnexpectedAlert( + this ILogger logger, + short alertLevel, + string description) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + var msg = $"{AlertLevel.GetText(alertLevel)}{((!string.IsNullOrEmpty(description)) ? $", {description}." : ".")}"; + logger.LogDtlsServerReceivedUnexpectedAlertUnchecked(msg); + } + } +} diff --git a/src/SIPSorcery/net/DtlsSrtp/SecureContext.cs b/src/SIPSorcery/net/DtlsSrtp/SecureContext.cs index a680b2b4b6..9b46e62711 100644 --- a/src/SIPSorcery/net/DtlsSrtp/SecureContext.cs +++ b/src/SIPSorcery/net/DtlsSrtp/SecureContext.cs @@ -13,22 +13,21 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class SecureContext { - public class SecureContext - { - public ProtectRtpPacket ProtectRtpPacket { get; private set; } - public ProtectRtpPacket ProtectRtcpPacket { get; private set; } + public ProtectRtpPacket ProtectRtpPacket { get; private set; } + public ProtectRtpPacket ProtectRtcpPacket { get; private set; } - public ProtectRtpPacket UnprotectRtpPacket { get; private set; } - public ProtectRtpPacket UnprotectRtcpPacket { get; private set; } + public ProtectRtpPacket UnprotectRtpPacket { get; private set; } + public ProtectRtpPacket UnprotectRtcpPacket { get; private set; } - public SecureContext(ProtectRtpPacket protectRtpPacket, ProtectRtpPacket unprotectRtpPacket, ProtectRtpPacket protectRtcpPacket, ProtectRtpPacket unprotectRtcpPacket) - { - ProtectRtpPacket = protectRtpPacket; - ProtectRtcpPacket = protectRtcpPacket; - UnprotectRtpPacket = unprotectRtpPacket; - UnprotectRtcpPacket = unprotectRtcpPacket; - } + public SecureContext(ProtectRtpPacket protectRtpPacket, ProtectRtpPacket unprotectRtpPacket, ProtectRtpPacket protectRtcpPacket, ProtectRtpPacket unprotectRtcpPacket) + { + ProtectRtpPacket = protectRtpPacket; + ProtectRtcpPacket = protectRtcpPacket; + UnprotectRtpPacket = unprotectRtpPacket; + UnprotectRtcpPacket = unprotectRtcpPacket; } } diff --git a/src/SIPSorcery/net/DtlsSrtp/SrtpHandler.cs b/src/SIPSorcery/net/DtlsSrtp/SrtpHandler.cs index ef05d02403..d88215a51b 100644 --- a/src/SIPSorcery/net/DtlsSrtp/SrtpHandler.cs +++ b/src/SIPSorcery/net/DtlsSrtp/SrtpHandler.cs @@ -16,139 +16,180 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using SIPSorcery.Net.SharpSRTP.SRTP; using SIPSorcery.SIP.App; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public sealed class SrtpHandler { - public class SrtpHandler + private List? _localSecurityDescriptions; + private List? _remoteSecurityDescriptions; + + public SrtpSessionContext? Context { get; private set; } + + public bool IsNegotiationComplete { get; private set; } + public SDPSecurityDescription? LocalSecurityDescription { get; private set; } + public SDPSecurityDescription? RemoteSecurityDescription { get; private set; } + + public SrtpHandler() { - private List _localSecurityDescriptions; - private List _remoteSecurityDescriptions; + } - public SrtpSessionContext Context { get; private set; } + public int ProtectRTP(byte[] payload, int length, out int outputBufferLength) + { + Debug.Assert(Context is not null); + return Context.ProtectRtp(payload, length, out outputBufferLength); + } - public bool IsNegotiationComplete { get; private set; } - public SDPSecurityDescription LocalSecurityDescription { get; private set; } - public SDPSecurityDescription RemoteSecurityDescription { get; private set; } + public int UnprotectRTP(byte[] payload, int length, out int outputBufferLength) + { + Debug.Assert(Context is not null); + return Context.UnprotectRtp(payload, length, out outputBufferLength); + } - public SrtpHandler() - { } + public int ProtectRTCP(byte[] payload, int length, out int outputBufferLength) + { + Debug.Assert(Context is not null); + return Context.ProtectRtcp(payload, length, out outputBufferLength); + } + + public int UnprotectRTCP(byte[] payload, int length, out int outputBufferLength) + { + Debug.Assert(Context is not null); + return Context.UnprotectRtcp(payload, length, out outputBufferLength); + } - public int ProtectRTP(byte[] payload, int length, out int outputBufferLength) + public bool RemoteSecurityDescriptionUnchanged(List securityDescriptions) + { + if (LocalSecurityDescription is null || RemoteSecurityDescription is null) { - return Context.ProtectRtp(payload, length, out outputBufferLength); + return false; } - public int UnprotectRTP(byte[] payload, int length, out int outputBufferLength) + var remoteCryptoSuite = FindSecurityDescriptionByCryptoSuite(securityDescriptions, LocalSecurityDescription.CryptoSuite); + + if (remoteCryptoSuite is null) { - return Context.UnprotectRtp(payload, length, out outputBufferLength); + return false; } - public int ProtectRTCP(byte[] payload, int length, out int outputBufferLength) + return remoteCryptoSuite.Equals(RemoteSecurityDescription); + + // Local method for finding security description by crypto suite + static SDPSecurityDescription? FindSecurityDescriptionByCryptoSuite(List descriptions, SDPSecurityDescription.CryptoSuites cryptoSuite) { - return Context.ProtectRtcp(payload, length, out outputBufferLength); + foreach (var description in descriptions) + { + if (description.CryptoSuite == cryptoSuite) + { + return description; + } + } + return null; } + } - public int UnprotectRTCP(byte[] payload, int length, out int outputBufferLength) + public bool SetupLocal(List securityDescriptions, SdpType sdpType) + { + _localSecurityDescriptions = securityDescriptions; + + if (sdpType == SdpType.offer) { - return Context.UnprotectRtcp(payload, length, out outputBufferLength); + IsNegotiationComplete = false; + return true; } - public bool RemoteSecurityDescriptionUnchanged(List securityDescriptions) - { - if (LocalSecurityDescription == null || RemoteSecurityDescription == null) - { - return false; - } + Debug.Assert(_remoteSecurityDescriptions is { }); - var remoteCryptoSuite = securityDescriptions.FirstOrDefault(x => x.CryptoSuite == LocalSecurityDescription.CryptoSuite); - return remoteCryptoSuite.ToString() == RemoteSecurityDescription.ToString(); + if (_remoteSecurityDescriptions.Count == 0) + { + throw new SipSorceryException("Setup local crypto failed. No crypto attribute in offer."); } - public bool SetupLocal(List securityDescriptions, SdpType sdpType) + if (_localSecurityDescriptions.Count == 0) { - _localSecurityDescriptions = securityDescriptions; + throw new SipSorceryException("Setup local crypto failed. No crypto attribute in answer."); + } - if (sdpType == SdpType.offer) - { - IsNegotiationComplete = false; - return true; - } + var localSecurityDescription = LocalSecurityDescription = _localSecurityDescriptions[0]; + var remoteSecurityDescription = RemoteSecurityDescription = GetFirstMatchingSecurityDescription(localSecurityDescription); - if (_remoteSecurityDescriptions.Count == 0) - { - throw new ApplicationException("Setup local crypto failed. No cryto attribute in offer."); - } + if (remoteSecurityDescription is { } && remoteSecurityDescription.Tag == localSecurityDescription.Tag) + { + IsNegotiationComplete = true; - if (_localSecurityDescriptions.Count == 0) - { - throw new ApplicationException("Setup local crypto failed. No crypto attribute in answer."); - } + Context = CreateSessionContext(localSecurityDescription, remoteSecurityDescription); - var localSecurityDescription = LocalSecurityDescription = _localSecurityDescriptions.First(); - var remoteSecurityDescription = RemoteSecurityDescription = _remoteSecurityDescriptions.FirstOrDefault(x => x.CryptoSuite == localSecurityDescription.CryptoSuite); + return true; + } - if (remoteSecurityDescription != null && remoteSecurityDescription.Tag == localSecurityDescription.Tag) - { - IsNegotiationComplete = true; + return false; + } - Context = CreateSessionContext(localSecurityDescription, remoteSecurityDescription); + public bool SetupRemote(List securityDescriptions, SdpType sdpType) + { + _remoteSecurityDescriptions = securityDescriptions; - return true; - } + if (sdpType == SdpType.offer) + { + IsNegotiationComplete = false; + return true; + } - return false; + Debug.Assert(_localSecurityDescriptions is { }); + + if (_localSecurityDescriptions.Count == 0) + { + throw new SipSorceryException("Setup remote crypto failed. No crypto attribute in offer."); } - public bool SetupRemote(List securityDescriptions, SdpType sdpType) + if (_remoteSecurityDescriptions.Count == 0) { - _remoteSecurityDescriptions = securityDescriptions; + throw new SipSorceryException("Setup remote crypto failed. No crypto attribute in answer."); + } - if (sdpType == SdpType.offer) - { - IsNegotiationComplete = false; - return true; - } + var remoteSecurityDescription = RemoteSecurityDescription = _remoteSecurityDescriptions[0]; + var localSecurityDescription = LocalSecurityDescription = GetFirstMatchingSecurityDescription(remoteSecurityDescription); - if (_localSecurityDescriptions.Count == 0) - { - throw new ApplicationException("Setup remote crypto failed. No cryto attribute in offer."); - } + if (localSecurityDescription is { } && localSecurityDescription.Tag == remoteSecurityDescription.Tag) + { + IsNegotiationComplete = true; - if (_remoteSecurityDescriptions.Count == 0) - { - throw new ApplicationException("Setup remote crypto failed. No cryto attribute in answer."); - } + Context = CreateSessionContext(localSecurityDescription, remoteSecurityDescription); - var remoteSecurityDescription = RemoteSecurityDescription = _remoteSecurityDescriptions.First(); - var localSecurityDescription = LocalSecurityDescription = _localSecurityDescriptions.FirstOrDefault(x => x.CryptoSuite == remoteSecurityDescription.CryptoSuite); + return true; + } - if (localSecurityDescription != null && localSecurityDescription.Tag == remoteSecurityDescription.Tag) - { - IsNegotiationComplete = true; + return false; + } - Context = CreateSessionContext(localSecurityDescription, remoteSecurityDescription); + private SrtpSessionContext CreateSessionContext(SDPSecurityDescription localSecurityDescription, SDPSecurityDescription remoteSecurityDescription, byte[]? mki = null) + { + // TODO: not tested + var localProtectionProfile = SrtpProtocol.SrtpCryptoSuites[localSecurityDescription.CryptoSuite.ToStringFast()]; + var remoteProtectionProfile = SrtpProtocol.SrtpCryptoSuites[remoteSecurityDescription.CryptoSuite.ToStringFast()]; - return true; - } + var encodeRtpContext = new SrtpContext(SrtpContextType.RTP, localProtectionProfile, localSecurityDescription.KeyParams[0].Key, localSecurityDescription.KeyParams[0].Salt, mki); + var encodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, localProtectionProfile, localSecurityDescription.KeyParams[0].Key, localSecurityDescription.KeyParams[0].Salt, mki); + var decodeRtpContext = new SrtpContext(SrtpContextType.RTP, remoteProtectionProfile, remoteSecurityDescription.KeyParams[0].Key, remoteSecurityDescription.KeyParams[0].Salt, mki); + var decodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, remoteProtectionProfile, remoteSecurityDescription.KeyParams[0].Key, remoteSecurityDescription.KeyParams[0].Salt, mki); - return false; - } + return new SrtpSessionContext(encodeRtpContext, decodeRtpContext, encodeRtcpContext, decodeRtcpContext); + } - private SrtpSessionContext CreateSessionContext(SDPSecurityDescription localSecurityDescription, SDPSecurityDescription remoteSecurityDescription, byte[] mki = null) + private SDPSecurityDescription? GetFirstMatchingSecurityDescription(SDPSecurityDescription other) + { + Debug.Assert(_remoteSecurityDescriptions is { }); + foreach (var desc in _remoteSecurityDescriptions) { - // TODO: not tested - var localProtectionProfile = SrtpProtocol.SrtpCryptoSuites[localSecurityDescription.CryptoSuite.ToString()]; - var remoteProtectionProfile = SrtpProtocol.SrtpCryptoSuites[remoteSecurityDescription.CryptoSuite.ToString()]; - - var encodeRtpContext = new SrtpContext(SrtpContextType.RTP, localProtectionProfile, localSecurityDescription.KeyParams[0].Key, localSecurityDescription.KeyParams[0].Salt, mki); - var encodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, localProtectionProfile, localSecurityDescription.KeyParams[0].Key, localSecurityDescription.KeyParams[0].Salt, mki); - var decodeRtpContext = new SrtpContext(SrtpContextType.RTP, remoteProtectionProfile, remoteSecurityDescription.KeyParams[0].Key, remoteSecurityDescription.KeyParams[0].Salt, mki); - var decodeRtcpContext = new SrtpContext(SrtpContextType.RTCP, remoteProtectionProfile, remoteSecurityDescription.KeyParams[0].Key, remoteSecurityDescription.KeyParams[0].Salt, mki); - - return new SrtpSessionContext(encodeRtpContext, decodeRtpContext, encodeRtcpContext, decodeRtcpContext); + if (desc.CryptoSuite == other.CryptoSuite) + { + return desc; + } } + + return null; } } diff --git a/src/SIPSorcery/net/HEP/HepPacket.cs b/src/SIPSorcery/net/HEP/HepPacket.cs index 3d023d9eec..c994355d85 100644 --- a/src/SIPSorcery/net/HEP/HepPacket.cs +++ b/src/SIPSorcery/net/HEP/HepPacket.cs @@ -137,7 +137,7 @@ public static byte[] GetBytes(ChunkTypeEnum chunkType, byte[] payload) /// public static byte[] GetBytes(ChunkTypeEnum chunkType, IPAddress address) { - if (chunkType == ChunkTypeEnum.IPv4SourceAddress || chunkType == ChunkTypeEnum.IPv4DesinationAddress) + if (chunkType is ChunkTypeEnum.IPv4SourceAddress or ChunkTypeEnum.IPv4DesinationAddress) { if (address.AddressFamily != AddressFamily.InterNetwork) { @@ -148,7 +148,7 @@ public static byte[] GetBytes(ChunkTypeEnum chunkType, IPAddress address) Buffer.BlockCopy(address.GetAddressBytes(), 0, buf, MINIMUM_CHUNK_LENGTH, 4); return buf; } - else if (chunkType == ChunkTypeEnum.IPv6SourceAddress || chunkType == ChunkTypeEnum.IPv6DesinationAddress) + else if (chunkType is ChunkTypeEnum.IPv6SourceAddress or ChunkTypeEnum.IPv6DesinationAddress) { if (address.AddressFamily != AddressFamily.InterNetworkV6) { diff --git a/src/SIPSorcery/net/ICE/IPAddressHelper.cs b/src/SIPSorcery/net/ICE/IPAddressHelper.cs index ff01772684..ba77825701 100644 --- a/src/SIPSorcery/net/ICE/IPAddressHelper.cs +++ b/src/SIPSorcery/net/ICE/IPAddressHelper.cs @@ -17,136 +17,139 @@ using System.Net; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public static class IPAddressHelper { - public static class IPAddressHelper - { - // Prefixes used for categorizing IPv6 addresses. - static byte[] kV4MappedPrefix = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 0 }; - static byte[] k6To4Prefix = new byte[] { 0x20, 0x02, 0 }; - static byte[] kTeredoPrefix = new byte[] { 0x20, 0x01, 0x00, 0x00 }; - static byte[] kV4CompatibilityPrefix = new byte[] { 0 }; - static byte[] k6BonePrefix = new byte[] { 0x3f, 0xfe, 0 }; - static byte[] kPrivateNetworkPrefix = new byte[] { 0xFD }; + // Prefixes used for categorizing IPv6 addresses. + private static readonly byte[] kV4MappedPrefix = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 0 }; + private static readonly byte[] k6To4Prefix = new byte[] { 0x20, 0x02, 0 }; + private static readonly byte[] kTeredoPrefix = new byte[] { 0x20, 0x01, 0x00, 0x00 }; + private static readonly byte[] kV4CompatibilityPrefix = new byte[] { 0 }; + private static readonly byte[] k6BonePrefix = new byte[] { 0x3f, 0xfe, 0 }; + private static readonly byte[] kPrivateNetworkPrefix = new byte[] { 0xFD }; - public static uint IPAddressPrecedence(IPAddress ip) + public static uint IPAddressPrecedence(IPAddress ip) + { + try { - try + // Precedence values from RFC 3484-bis. Prefers native v4 over 6to4/Teredo. + if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) { - // Precedence values from RFC 3484-bis. Prefers native v4 over 6to4/Teredo. - if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + return 30; + } + else if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + { + if (IPAddress.IsLoopback(ip)) + { + return 60; + } + // A unique local address (ULA) is an IPv6 address in the block fc00::/7, defined in RFC 4193. + // It is the IPv6 counterpart of the IPv4 private address. + // Unique local addresses are available for use in private networks, e.g. inside a single site + // or organisation, or spanning a limited number of sites or organisations. + // They are not routable in the global IPv6 Internet. + else if (ip.IsIPv6SiteLocal) + { + return 50; + } + else if (ip.IsIPv4MappedToIPv6) { return 30; } - else if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + else if (IPIs6To4(ip)) { - if (IPAddress.IsLoopback(ip)) - { - return 60; - } - // A unique local address (ULA) is an IPv6 address in the block fc00::/7, defined in RFC 4193. - // It is the IPv6 counterpart of the IPv4 private address. - // Unique local addresses are available for use in private networks, e.g. inside a single site - // or organisation, or spanning a limited number of sites or organisations. - // They are not routable in the global IPv6 Internet. - else if (ip.IsIPv6SiteLocal) - { - return 50; - } - else if (ip.IsIPv4MappedToIPv6) - { - return 30; - } - else if (IPIs6To4(ip)) - { - return 20; - } - // In computer networking, Teredo is a transition technology that gives full IPv6 - // connectivity for IPv6-capable hosts which are on the IPv4 Internet but which have - // no direct native connection to an IPv6 network. Compared to other similar protocols - // its distinguishing feature is that it is able to perform its function even from behind - // network address translation (NAT) devices such as home routers. - else if (ip.IsIPv6Teredo) - { - return 10; - } - else if (IPIsV4Compatibility(ip) || IPIsSiteLocal(ip) || IPIs6Bone(ip)) - { - return 1; - } - else - { - // A 'normal' IPv6 address. - return 40; - } + return 20; } - } - catch { } - - return 0; - } - - public static bool IPIsLinkLocalV4(IPAddress ip) - { - try - { - if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + // In computer networking, Teredo is a transition technology that gives full IPv6 + // connectivity for IPv6-capable hosts which are on the IPv4 Internet but which have + // no direct native connection to an IPv6 network. Compared to other similar protocols + // its distinguishing feature is that it is able to perform its function even from behind + // network address translation (NAT) devices such as home routers. + else if (ip.IsIPv6Teredo) + { + return 10; + } + else if (IPIsV4Compatibility(ip) || IPIsSiteLocal(ip) || IPIs6Bone(ip)) + { + return 1; + } + else { - long address = BitConverter.ToInt64(ip.GetAddressBytes(), 0); - long ip_in_host_order = IPAddress.NetworkToHostOrder(address); - return ((ip_in_host_order >> 16) == ((169 << 8) | 254)); + // A 'normal' IPv6 address. + return 40; } } - catch { } - return false; } + catch { } - public static bool IPIs6Bone(IPAddress ip) { - return IPIsHelper(ip, k6BonePrefix, 16); - } + return 0; + } - public static bool IPIsSiteLocal(IPAddress ip) { - try + public static bool IPIsLinkLocalV4(IPAddress ip) + { + try + { + if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) { - // Can't use the helper because the prefix is 10 bits. - ip = ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 ? ip : ip.MapToIPv6(); - byte[] addr = ip.GetAddressBytes(); - return addr[0] == 0xFE && (addr[1] & 0xC0) == 0xC0; + long address = BitConverter.ToInt64(ip.GetAddressBytes(), 0); + long ip_in_host_order = IPAddress.NetworkToHostOrder(address); + return ((ip_in_host_order >> 16) == ((169 << 8) | 254)); } - catch { } - return false; } + catch { } + return false; + } - public static bool IPIsV4Compatibility (IPAddress ip) { - return IPIsHelper(ip, kV4CompatibilityPrefix, 96); - } + public static bool IPIs6Bone(IPAddress ip) { + return IPIsHelper(ip, k6BonePrefix, 16); + } - public static bool IPIs6To4(IPAddress ip) { - return IPIsHelper(ip, k6To4Prefix, 16); + public static bool IPIsSiteLocal(IPAddress ip) { + try + { + // Can't use the helper because the prefix is 10 bits. + ip = ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 ? ip : ip.MapToIPv6(); + byte[] addr = ip.GetAddressBytes(); + return addr[0] == 0xFE && (addr[1] & 0xC0) == 0xC0; } + catch { } + return false; + } + + public static bool IPIsV4Compatibility (IPAddress ip) { + return IPIsHelper(ip, kV4CompatibilityPrefix, 96); + } + + public static bool IPIs6To4(IPAddress ip) { + return IPIsHelper(ip, k6To4Prefix, 16); + } - static bool IPIsHelper(IPAddress ip, byte[] tomatch, int lengthInBits) + private static bool IPIsHelper(IPAddress ip, byte[] tomatch, int lengthInBits) + { + try { - try - { - // Helper method for checking IP prefix matches (but only on whole byte - // lengths). Length is in bits. - byte[] addr = ip.GetAddressBytes(); - var bytesToCompare = (lengthInBits >> 3); + // Helper method for checking IP prefix matches (but only on whole byte + // lengths). Length is in bits. + byte[] addr = ip.GetAddressBytes(); + var bytesToCompare = (lengthInBits >> 3); - if (addr == null || addr.Length < bytesToCompare || tomatch == null || tomatch.Length < bytesToCompare) - return false; + if (addr is null || addr.Length < bytesToCompare || tomatch is null || tomatch.Length < bytesToCompare) + { + return false; + } - for (int i = 0; i < bytesToCompare; i++) + for (int i = 0; i < bytesToCompare; i++) + { + if (addr[i] != tomatch[i]) { - if (addr[i] != tomatch[i]) - return false; + return false; } - return true; } - catch { } - - return false; + return true; } + catch { } + + return false; } } diff --git a/src/SIPSorcery/net/ICE/IRTCIceCandidate.cs b/src/SIPSorcery/net/ICE/IRTCIceCandidate.cs index 88d211402b..39fd1b1702 100644 --- a/src/SIPSorcery/net/ICE/IRTCIceCandidate.cs +++ b/src/SIPSorcery/net/ICE/IRTCIceCandidate.cs @@ -13,243 +13,248 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- -namespace SIPSorcery.Net +using System.Text.Json; +using System.Text.Json.Serialization; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; + +/// +/// The ICE set up roles that a peer can be in. The role determines how the DTLS +/// handshake is performed, i.e. which peer is the client and which is the server. +/// +public enum IceImplementationEnum +{ + full, + lite +} + +/// +/// The ICE set up roles that a peer can be in. The role determines how the DTLS +/// handshake is performed, i.e. which peer is the client and which is the server. +/// +public enum IceRolesEnum +{ + actpass = 0, + passive = 1, + active = 2 +} + +/// +/// The gathering states an ICE session transitions through. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#dom-rtcicegatheringstate. +/// +public enum RTCIceGatheringState +{ + @new, + gathering, + complete +} + +/// +/// The states an ICE session transitions through. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#rtciceconnectionstate-enum. +/// +public enum RTCIceConnectionState { /// - /// The ICE set up roles that a peer can be in. The role determines how the DTLS - /// handshake is performed, i.e. which peer is the client and which is the server. + /// The connection has been closed. All checks stop. /// - public enum IceImplementationEnum - { - full, - lite - } + closed, /// - /// The ICE set up roles that a peer can be in. The role determines how the DTLS - /// handshake is performed, i.e. which peer is the client and which is the server. + /// The connection attempt has failed or connection checks on an established + /// connection have failed. /// - public enum IceRolesEnum - { - actpass = 0, - passive = 1, - active = 2 - } + failed, /// - /// The gathering states an ICE session transitions through. + /// Connection attempts on an established connection have failed. Attempts + /// will continue until the state transitions to failure. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcicegatheringstate. - /// - public enum RTCIceGatheringState - { - @new, - gathering, - complete - } + disconnected, /// - /// The states an ICE session transitions through. + /// The initial state. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#rtciceconnectionstate-enum. - /// - public enum RTCIceConnectionState - { - /// - /// The connection has been closed. All checks stop. - /// - closed, - - /// - /// The connection attempt has failed or connection checks on an established - /// connection have failed. - /// - failed, - - /// - /// Connection attempts on an established connection have failed. Attempts - /// will continue until the state transitions to failure. - /// - disconnected, - - /// - /// The initial state. - /// - @new, - - /// - /// Checks are being carried out in an attempt to establish a connection. - /// - checking, - - /// - /// What is this state for? - /// - //completed, - - /// - /// The checks have been successful and the connection has been established. - /// - connected - } + @new, + + /// + /// Checks are being carried out in an attempt to establish a connection. + /// + checking, + + /// + /// What is this state for? + /// + //completed, /// - /// Represents an ICE candidate and associated properties that link it to the SDP. + /// The checks have been successful and the connection has been established. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcicecandidateinit. - /// - public class RTCIceCandidateInit + connected +} + +/// +/// Represents an ICE candidate and associated properties that link it to the SDP. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#dom-rtcicecandidateinit. +/// +public class RTCIceCandidateInit +{ + [JsonPropertyName("candidate")] + public string? candidate { get; set; } + [JsonPropertyName("sdpMid")] + public string? sdpMid { get; set; } + [JsonPropertyName("sdpMLineIndex")] + public ushort sdpMLineIndex { get; set; } + [JsonPropertyName("usernameFragment")] + public string? usernameFragment { get; set; } + + public string toJSON() + { + //return "{" + + // $" \"sdpMid\": \"{sdpMid ?? sdpMLineIndex.ToString()}\"," + + // $" \"sdpMLineIndex\": {sdpMLineIndex}," + + // $" \"usernameFragment\": \"{usernameFragment}\"," + + // $" \"candidate\": \"{candidate}\"" + + // "}"; + + return JsonSerializer.Serialize(this, SipSorceryJsonSerializerContext.Default.RTCIceCandidateInit); + } + + public static bool TryParse(string json, out RTCIceCandidateInit? init) { - public string candidate { get; set; } - public string sdpMid { get; set; } - public ushort sdpMLineIndex { get; set; } - public string usernameFragment { get; set; } + init = null; - public string toJSON() + if (string.IsNullOrWhiteSpace(json)) { - //return "{" + - // $" \"sdpMid\": \"{sdpMid ?? sdpMLineIndex.ToString()}\"," + - // $" \"sdpMLineIndex\": {sdpMLineIndex}," + - // $" \"usernameFragment\": \"{usernameFragment}\"," + - // $" \"candidate\": \"{candidate}\"" + - // "}"; - - return TinyJson.JSONWriter.ToJson(this); + return false; } - - public static bool TryParse(string json, out RTCIceCandidateInit init) + else { - //init = JsonSerializer.Deserialize< RTCIceCandidateInit>(json); - - init = null; - - if (string.IsNullOrWhiteSpace(json)) - { - return false; - } - else - { - init = TinyJson.JSONParser.FromJson(json); - - // To qualify as parsed all required fields must be set. - return init != null && - init.candidate != null && - init.sdpMid != null; - } + init = JsonSerializer.Deserialize(json, SipSorceryJsonSerializerContext.Default.RTCIceCandidateInit); + + // To qualify as parsed all required fields must be set. + return init is { } && + init.candidate is { } && + init.sdpMid is { }; } } +} + +/// +/// +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#dom-rtcicecomponent. +/// +public enum RTCIceComponent +{ + rtp = 1, + rtcp = 2 +} + +/// +/// The transport protocol types for an ICE candidate. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#rtciceprotocol-enum. +/// +public enum RTCIceProtocol +{ + udp, + tcp +} +/// +/// The RTCIceTcpCandidateType represents the type of the ICE TCP candidate. +/// +/// +/// As defined in https://www.w3.org/TR/webrtc/#rtcicetcpcandidatetype-enum. +/// +public enum RTCIceTcpCandidateType +{ /// - /// + /// An active TCP candidate is one for which the transport will attempt to + /// open an outbound connection but will not receive incoming connection requests. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcicecomponent. - /// - public enum RTCIceComponent - { - rtp = 1, - rtcp = 2 - } + active, /// - /// The transport protocol types for an ICE candidate. + /// A passive TCP candidate is one for which the transport will receive incoming + /// connection attempts but not attempt a connection. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#rtciceprotocol-enum. - /// - public enum RTCIceProtocol - { - udp, - tcp - } + passive, /// - /// The RTCIceTcpCandidateType represents the type of the ICE TCP candidate. + /// An so candidate is one for which the transport will attempt to open a connection + /// simultaneously with its peer. /// - /// - /// As defined in https://www.w3.org/TR/webrtc/#rtcicetcpcandidatetype-enum. - /// - public enum RTCIceTcpCandidateType - { - /// - /// An active TCP candidate is one for which the transport will attempt to - /// open an outbound connection but will not receive incoming connection requests. - /// - active, - - /// - /// A passive TCP candidate is one for which the transport will receive incoming - /// connection attempts but not attempt a connection. - /// - passive, - - /// - /// An so candidate is one for which the transport will attempt to open a connection - /// simultaneously with its peer. - /// - so - } + so +} +/// +/// The RTCIceCandidateType represents the type of the ICE candidate. +/// +/// +/// As defined in https://www.w3.org/TR/webrtc/#rtcicecandidatetype-enum. +/// +public enum RTCIceCandidateType +{ /// - /// The RTCIceCandidateType represents the type of the ICE candidate. + /// A host candidate, locally gathered. /// - /// - /// As defined in https://www.w3.org/TR/webrtc/#rtcicecandidatetype-enum. - /// - public enum RTCIceCandidateType - { - /// - /// A host candidate, locally gathered. - /// - host, - - /// - /// A peer reflexive candidate, obtained as a result of a connectivity check - /// (e.g. STUN request from a previously unknown address). - /// - prflx, - - /// - /// A server reflexive candidate, obtained from STUN and/or TURN (non-relay TURN). - /// - srflx, - - /// - /// A relay candidate, TURN (relay). - /// - relay - } + host, - /// - /// As defined in: https://www.w3.org/TR/webrtc/#rtcicecandidate-interface - /// - /// Rhe 'priority` field was adjusted from ulong to uint due to an issue that - /// occurred with the STUN PRIORITY attribute being rejected for not being 4 bytes. - /// The ICE and WebRTC specifications are contradictory so went with the same as - /// libwebrtc which is 4 bytes. - /// See https://github.com/sipsorcery/sipsorcery/issues/350. - /// - public interface IRTCIceCandidate - { - //constructor(optional RTCIceCandidateInit candidateInitDict = { }); - string candidate { get; } - string sdpMid { get; } - ushort sdpMLineIndex { get; } - string foundation { get; } - RTCIceComponent component { get; } - uint priority { get; } - string address { get; } - RTCIceProtocol protocol { get; } - ushort port { get; } - RTCIceCandidateType type { get; } - RTCIceTcpCandidateType tcpType { get; } - string relatedAddress { get; } - ushort relatedPort { get; } - string usernameFragment { get; } - //RTCIceCandidateInit toJSON(); - string toJSON(); - } + /// + /// A peer reflexive candidate, obtained as a result of a connectivity check + /// (e.g. STUN request from a previously unknown address). + /// + prflx, + + /// + /// A server reflexive candidate, obtained from STUN and/or TURN (non-relay TURN). + /// + srflx, + + /// + /// A relay candidate, TURN (relay). + /// + relay +} + +/// +/// As defined in: https://www.w3.org/TR/webrtc/#rtcicecandidate-interface +/// +/// Rhe 'priority` field was adjusted from ulong to uint due to an issue that +/// occurred with the STUN PRIORITY attribute being rejected for not being 4 bytes. +/// The ICE and WebRTC specifications are contradictory so went with the same as +/// libwebrtc which is 4 bytes. +/// See https://github.com/sipsorcery/sipsorcery/issues/350. +/// +public interface IRTCIceCandidate +{ + //constructor(optional RTCIceCandidateInit candidateInitDict = { }); + string candidate { get; } + string? sdpMid { get; } + ushort sdpMLineIndex { get; } + string? foundation { get; } + RTCIceComponent component { get; } + uint priority { get; } + string? address { get; } + RTCIceProtocol protocol { get; } + ushort port { get; } + RTCIceCandidateType type { get; } + RTCIceTcpCandidateType tcpType { get; } + string? relatedAddress { get; } + ushort relatedPort { get; } + string? usernameFragment { get; } + //RTCIceCandidateInit toJSON(); + string toJSON(); } diff --git a/src/SIPSorcery/net/ICE/IceChecklistEntry.cs b/src/SIPSorcery/net/ICE/IceChecklistEntry.cs index f34bdf909c..0a9e11892b 100644 --- a/src/SIPSorcery/net/ICE/IceChecklistEntry.cs +++ b/src/SIPSorcery/net/ICE/IceChecklistEntry.cs @@ -14,185 +14,187 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Net; using System.Text; using Microsoft.Extensions.Logging; -using System.Buffers.Binary; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// List of state conditions for a check list entry as the connectivity checks are +/// carried out. +/// +public enum ChecklistEntryState { /// - /// List of state conditions for a check list entry as the connectivity checks are - /// carried out. + /// A check has not been sent for this pair, but the pair is not Frozen. /// - public enum ChecklistEntryState - { - /// - /// A check has not been sent for this pair, but the pair is not Frozen. - /// - Waiting, - - /// - /// A check has been sent for this pair, but the transaction is in progress. - /// - InProgress, - - /// - /// A check has been sent for this pair, and it produced a successful result. - /// - Succeeded, - - /// - /// A check has been sent for this pair, and it failed (a response to the - /// check was never received, or a failure response was received). - /// - Failed, - - /// - /// A check for this pair has not been sent, and it cannot be sent until the - /// pair is unfrozen and moved into the Waiting state. - /// - Frozen - } + Waiting, + + /// + /// A check has been sent for this pair, but the transaction is in progress. + /// + InProgress, + + /// + /// A check has been sent for this pair, and it produced a successful result. + /// + Succeeded, + + /// + /// A check has been sent for this pair, and it failed (a response to the + /// check was never received, or a failure response was received). + /// + Failed, + + /// + /// A check for this pair has not been sent, and it cannot be sent until the + /// pair is unfrozen and moved into the Waiting state. + /// + Frozen +} + +/// +/// Represents the state of the ICE checks for a checklist. +/// +/// +/// As specified in https://tools.ietf.org/html/rfc8445#section-6.1.2.1. +/// +internal enum ChecklistState +{ + /// + /// The checklist is neither Completed nor Failed yet. + /// Checklists are initially set to the Running state. + /// + Running, + + /// + /// The checklist contains a nominated pair for each + /// component of the data stream. + /// + Completed, + + /// + /// The checklist does not have a valid pair for each component + /// of the data stream, and all of the candidate pairs in the + /// checklist are in either the Failed or the Succeeded state. In + /// other words, at least one component of the checklist has candidate + /// pairs that are all in the Failed state, which means the component + /// has failed, which means the checklist has failed. + /// + Failed +} + +/// +/// A check list entry represents an ICE candidate pair (local candidate + remote candidate) +/// that is being checked for connectivity. If the overall ICE session does succeed it will +/// be due to one of these checklist entries successfully completing the ICE checks. +/// +public class ChecklistEntry : IComparable +{ + private static readonly ILogger logger = LogFactory.CreateLogger(); + + //Previous RequestIds + protected List _cachedRequestTransactionIDs = new List(); + + public RTCIceCandidate LocalCandidate; + public RTCIceCandidate RemoteCandidate; /// - /// Represents the state of the ICE checks for a checklist. + /// The current state of this checklist entry. Indicates whether a STUN check has been + /// sent, responded to, timed out etc. /// /// - /// As specified in https://tools.ietf.org/html/rfc8445#section-6.1.2.1. + /// See https://tools.ietf.org/html/rfc8445#section-6.1.2.6 for the state + /// transition diagram for a check list entry. /// - internal enum ChecklistState - { - /// - /// The checklist is neither Completed nor Failed yet. - /// Checklists are initially set to the Running state. - /// - Running, - - /// - /// The checklist contains a nominated pair for each - /// component of the data stream. - /// - Completed, - - /// - /// The checklist does not have a valid pair for each component - /// of the data stream, and all of the candidate pairs in the - /// checklist are in either the Failed or the Succeeded state. In - /// other words, at least one component of the checklist has candidate - /// pairs that are all in the Failed state, which means the component - /// has failed, which means the checklist has failed. - /// - Failed - } + public ChecklistEntryState State = ChecklistEntryState.Frozen; + + /// + /// The candidate pairs whose local and remote candidates are both the + /// default candidates for a particular component is called the "default + /// candidate pair" for that component. This is the pair that would be + /// used to transmit data if both agents had not been ICE aware. + /// + public bool Default; + + /// + /// Gets set to true when the connectivity checks for the candidate pair are + /// successful. Valid entries are eligible to be set as nominated. + /// + public bool Valid; + + /// + /// Gets set to true if this entry is selected as the single nominated entry to be + /// used for the session communications. Setting a check list entry as nominated + /// indicates the ICE checks have been successful and the application can begin + /// normal communications. + /// + public bool Nominated { get; set; } + + public uint LocalPriority { get; private set; } + + public uint RemotePriority { get; private set; } /// - /// A check list entry represents an ICE candidate pair (local candidate + remote candidate) - /// that is being checked for connectivity. If the overall ICE session does succeed it will - /// be due to one of these checklist entries successfully completing the ICE checks. + /// The priority for the candidate pair: + /// - Let G be the priority for the candidate provided by the controlling agent. + /// - Let D be the priority for the candidate provided by the controlled agent. + /// Pair Priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0) /// - public class ChecklistEntry : IComparable + /// + /// See https://tools.ietf.org/html/rfc8445#section-6.1.2.3. + /// + public ulong Priority { - private static readonly ILogger logger = LogFactory.CreateLogger(); - - //Previous RequestIds - protected List _cachedRequestTransactionIDs = new List(); - - public RTCIceCandidate LocalCandidate; - public RTCIceCandidate RemoteCandidate; - - /// - /// The current state of this checklist entry. Indicates whether a STUN check has been - /// sent, responded to, timed out etc. - /// - /// - /// See https://tools.ietf.org/html/rfc8445#section-6.1.2.6 for the state - /// transition diagram for a check list entry. - /// - public ChecklistEntryState State = ChecklistEntryState.Frozen; - - /// - /// The candidate pairs whose local and remote candidates are both the - /// default candidates for a particular component is called the "default - /// candidate pair" for that component. This is the pair that would be - /// used to transmit data if both agents had not been ICE aware. - /// - public bool Default; - - /// - /// Gets set to true when the connectivity checks for the candidate pair are - /// successful. Valid entries are eligible to be set as nominated. - /// - public bool Valid; - - /// - /// Gets set to true if this entry is selected as the single nominated entry to be - /// used for the session communications. Setting a check list entry as nominated - /// indicates the ICE checks have been successful and the application can begin - /// normal communications. - /// - public bool Nominated { get; set; } - - public uint LocalPriority { get; private set; } - - public uint RemotePriority { get; private set; } - - /// - /// The priority for the candidate pair: - /// - Let G be the priority for the candidate provided by the controlling agent. - /// - Let D be the priority for the candidate provided by the controlled agent. - /// Pair Priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0) - /// - /// - /// See https://tools.ietf.org/html/rfc8445#section-6.1.2.3. - /// - public ulong Priority + get { - get - { - ulong priority = Math.Min(LocalPriority, RemotePriority); - priority = priority << 32; - priority += 2u * (ulong)Math.Max(LocalPriority, RemotePriority) + (ulong)((IsLocalController) ? LocalPriority > RemotePriority ? 1 : 0 - : RemotePriority > LocalPriority ? 1 : 0); + ulong priority = Math.Min(LocalPriority, RemotePriority); + priority = priority << 32; + priority += 2u * (ulong)Math.Max(LocalPriority, RemotePriority) + (ulong)((IsLocalController) ? LocalPriority > RemotePriority ? 1 : 0 + : RemotePriority > LocalPriority ? 1 : 0); - return priority; - } + return priority; } + } - /// - /// Timestamp the first connectivity check (STUN binding request) was sent at. - /// - public DateTime FirstCheckSentAt = DateTime.MinValue; - - /// - /// Timestamp the last connectivity check (STUN binding request) was sent at. - /// - public DateTime LastCheckSentAt = DateTime.MinValue; - - /// - /// The number of checks that have been sent without a response. - /// - public int ChecksSent; - - /// - /// The transaction ID that was set in the last STUN request connectivity check. - /// - public string RequestTransactionID - { - get - { - return _cachedRequestTransactionIDs?.Count > 0 ? _cachedRequestTransactionIDs[0] : null; - } - set + /// + /// Timestamp the first connectivity check (STUN binding request) was sent at. + /// + public DateTime FirstCheckSentAt = DateTime.MinValue; + + /// + /// Timestamp the last connectivity check (STUN binding request) was sent at. + /// + public DateTime LastCheckSentAt = DateTime.MinValue; + + /// + /// The number of checks that have been sent without a response. + /// + public int ChecksSent; + + /// + /// The transaction ID that was set in the last STUN request connectivity check. + /// + public string? RequestTransactionID + { + get + { + return _cachedRequestTransactionIDs?.Count > 0 ? _cachedRequestTransactionIDs[0] : null; + } + set + { + if (value is { }) { - var currentValue = _cachedRequestTransactionIDs?.Count > 0 ? _cachedRequestTransactionIDs[0] : null; - if (value != currentValue) + var currentValue = RequestTransactionID; + if (value != currentValue && _cachedRequestTransactionIDs is { }) { const int MAX_CACHED_REQUEST_IDS = 30; - while (_cachedRequestTransactionIDs.Count >= MAX_CACHED_REQUEST_IDS && _cachedRequestTransactionIDs.Count > 0) + while (_cachedRequestTransactionIDs.Count is >= MAX_CACHED_REQUEST_IDS and > 0) { _cachedRequestTransactionIDs.RemoveAt(_cachedRequestTransactionIDs.Count - 1); } @@ -204,122 +206,130 @@ public string RequestTransactionID } } } + } - /// - /// Before a remote peer will be able to use the relay it's IP address needs - /// to be authorised by sending a Create Permissions request to the TURN server. - /// This field records the number of Create Permissions requests that have been - /// sent for this entry. - /// - public int TurnPermissionsRequestSent { get; set; } = 0; - - /// - /// This field records the time a Create Permissions response was received. - /// - public DateTime TurnPermissionsResponseAt { get; set; } = DateTime.MinValue; - - /// - /// If a candidate has been nominated this field records the time the last - /// STUN binding response was received from the remote peer. - /// - public DateTime LastConnectedResponseAt { get; set; } - - public bool IsLocalController { get; private set; } - - /// - /// Timestamp for the most recent binding request received from the remote peer. - /// - public DateTime LastBindingRequestReceivedAt { get; set; } - - /// - /// Creates a new entry for the ICE session checklist. - /// - /// The local candidate for the checklist pair. - /// The remote candidate for the checklist pair. - /// True if we are acting as the controlling agent in the ICE session. - public ChecklistEntry(RTCIceCandidate localCandidate, RTCIceCandidate remoteCandidate, bool isLocalController) - { - LocalCandidate = localCandidate; - RemoteCandidate = remoteCandidate; - IsLocalController = isLocalController; + /// + /// Before a remote peer will be able to use the relay it's IP address needs + /// to be authorised by sending a Create Permissions request to the TURN server. + /// This field records the number of Create Permissions requests that have been + /// sent for this entry. + /// + public int TurnPermissionsRequestSent { get; set; } - LocalPriority = localCandidate.priority; - RemotePriority = remoteCandidate.priority; - } + /// + /// This field records the time a Create Permissions response was received. + /// + public DateTime TurnPermissionsResponseAt { get; set; } = DateTime.MinValue; - public bool IsTransactionIDMatch(string id) - { - var index = _cachedRequestTransactionIDs.IndexOf(id); + /// + /// If a candidate has been nominated this field records the time the last + /// STUN binding response was received from the remote peer. + /// + public DateTime LastConnectedResponseAt { get; set; } - if (index >= 1) - { - logger.LogInformation("Received transaction id from a previous cached RequestTransactionID {Id} Index: {Index}", id, index); - } + public bool IsLocalController { get; private set; } + + /// + /// Timestamp for the most recent binding request received from the remote peer. + /// + public DateTime LastBindingRequestReceivedAt { get; set; } + + /// + /// Creates a new entry for the ICE session checklist. + /// + /// The local candidate for the checklist pair. + /// The remote candidate for the checklist pair. + /// True if we are acting as the controlling agent in the ICE session. + public ChecklistEntry(RTCIceCandidate localCandidate, RTCIceCandidate remoteCandidate, bool isLocalController) + { + LocalCandidate = localCandidate; + RemoteCandidate = remoteCandidate; + IsLocalController = isLocalController; + + LocalPriority = localCandidate.priority; + RemotePriority = remoteCandidate.priority; + } - return index >= 0; + public bool IsTransactionIDMatch(string id) + { + var index = _cachedRequestTransactionIDs.IndexOf(id); + + if (index >= 1) + { + logger.LogIceChecklistEntryTxIdMatch(id, index); } - /// - /// Compare method to allow the checklist to be sorted in priority order. - /// - public int CompareTo(Object other) + return index >= 0; + } + + /// + /// Compare method to allow the checklist to be sorted in priority order. + /// + public int CompareTo(object? other) + { + if (other is ChecklistEntry checklistEntry) { - if (other is ChecklistEntry) - { - //return Priority.CompareTo((other as ChecklistEntry).Priority); - return (other as ChecklistEntry).Priority.CompareTo(Priority); - } - else - { - throw new ApplicationException("CompareTo is not implemented for ChecklistEntry and arbitrary types."); - } + return checklistEntry.Priority.CompareTo(Priority); } + else + { + throw new SipSorceryException("CompareTo is not implemented for ChecklistEntry and arbitrary types."); + } + } - internal void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoint) + internal void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoint) + { + var retry = false; + if (stunResponse.Header.MessageClass == STUNClassTypesEnum.ErrorResponse) { - bool retry = false; - var msgType = stunResponse.Header.MessageClass; - if (msgType == STUNClassTypesEnum.ErrorResponse) + if (stunResponse.Attributes is { }) { - if (stunResponse.Attributes.Any(x => x.AttributeType == STUNAttributeTypesEnum.ErrorCode)) + foreach (var attr in stunResponse.Attributes) { - var errCodeAttribute = - stunResponse.Attributes.First(x => x.AttributeType == STUNAttributeTypesEnum.ErrorCode) as - STUNErrorCodeAttribute; - if (errCodeAttribute.ErrorCode == IceServer.STUN_UNAUTHORISED_ERROR_CODE || - errCodeAttribute.ErrorCode == IceServer.STUN_STALE_NONCE_ERROR_CODE) + if (attr.AttributeType == STUNAttributeTypesEnum.ErrorCode) { - if (LocalCandidate.IceServer == null) + if (attr is STUNErrorCodeAttribute errCodeAttribute && + (errCodeAttribute.ErrorCode == IceServer.STUN_UNAUTHORISED_ERROR_CODE || + errCodeAttribute.ErrorCode == IceServer.STUN_STALE_NONCE_ERROR_CODE)) { - logger.LogWarning("A STUN error response was received on an ICE candidate without a corresponding ICE server, ignoring."); - } - else - { - LocalCandidate.IceServer.SetAuthenticationFields(stunResponse); - LocalCandidate.IceServer.GenerateNewTransactionID(); - retry = true; + if (LocalCandidate.IceServer is null) + { + logger.LogIceStunNoAuthServer(); + } + else + { + LocalCandidate.IceServer.SetAuthenticationFields(stunResponse); + LocalCandidate.IceServer.GenerateNewTransactionID(); + retry = true; + } } + break; } } - } + } - if (stunResponse.Header.MessageType == STUNMessageTypesEnum.RefreshSuccessResponse) - { - var lifetime = stunResponse.Attributes.FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.Lifetime); - - if (lifetime != null) + switch (stunResponse.Header.MessageType) + { + case STUNMessageTypesEnum.RefreshSuccessResponse: + if (stunResponse.Attributes is { }) { - LocalCandidate.IceServer.TurnTimeToExpiry = DateTime.Now + - TimeSpan.FromSeconds(BinaryPrimitives.ReadUInt32BigEndian(lifetime.Value)); + foreach (var attr in stunResponse.Attributes) + { + if (attr.AttributeType == STUNAttributeTypesEnum.Lifetime) + { + Debug.Assert(LocalCandidate?.IceServer is { }); + LocalCandidate.IceServer.TurnTimeToExpiry = + DateTime.Now + TimeSpan.FromSeconds(BinaryPrimitives.ReadUInt32BigEndian(attr.Value.Span)); + break; + } + } } - } - else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.RefreshErrorResponse) - { - logger.LogError("Cannot refresh TURN allocation"); - } - else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.BindingSuccessResponse) - { + break; + case STUNMessageTypesEnum.RefreshErrorResponse: + logger.LogIceServerRefreshError(); + break; + case STUNMessageTypesEnum.BindingSuccessResponse: if (Nominated) { // If the candidate has been nominated then this is a response to a periodic @@ -333,16 +343,14 @@ internal void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoin ChecksSent = 0; //LastCheckSentAt = DateTime.MinValue; } - } - else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.BindingErrorResponse) - { - logger.LogWarning("ICE RTP channel a STUN binding error response was received from {RemoteEndPoint}.", remoteEndPoint); - logger.LogWarning("ICE RTP channel check list entry set to failed: {RemoteCandidate}.", RemoteCandidate); + break; + case STUNMessageTypesEnum.BindingErrorResponse: + logger.LogIceStunBindingErrorResponse(remoteEndPoint); + logger.LogIceChecklistEntryFailed(RemoteCandidate); State = ChecklistEntryState.Failed; - } - else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.CreatePermissionSuccessResponse) - { - logger.LogDebug("A TURN Create Permission success response was received from {RemoteEndPoint} (TxID: {TransactionId}).", remoteEndPoint, Encoding.ASCII.GetString(stunResponse.Header.TransactionId)); + break; + case STUNMessageTypesEnum.CreatePermissionSuccessResponse: + logger.LogIceTurnPermissionResponse(remoteEndPoint, Encoding.ASCII.GetString(stunResponse.Header.TransactionId)); TurnPermissionsRequestSent = 1; TurnPermissionsResponseAt = DateTime.Now; @@ -353,17 +361,15 @@ internal void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoin //Clear CheckSentAt Time to force send it again FirstCheckSentAt = DateTime.MinValue; } - } - else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.CreatePermissionErrorResponse) - { - logger.LogWarning("ICE RTP channel TURN Create Permission error response was received from {RemoteEndPoint}.", remoteEndPoint); + break; + case STUNMessageTypesEnum.CreatePermissionErrorResponse: + logger.LogIceTurnCreatePermissionsError(remoteEndPoint); TurnPermissionsResponseAt = DateTime.Now; State = retry ? State : ChecklistEntryState.Failed; - } - else - { - logger.LogWarning("ICE RTP channel received an unexpected STUN response {MessageType} from {RemoteEndPoint}.", stunResponse.Header.MessageType, remoteEndPoint); - } + break; + default: + logger.LogIceUnexpectedStunResponse(stunResponse.Header.MessageType, remoteEndPoint); + break; } } } diff --git a/src/SIPSorcery/net/ICE/IceServer.cs b/src/SIPSorcery/net/ICE/IceServer.cs index 3eb5afb8d7..876c187ec7 100644 --- a/src/SIPSorcery/net/ICE/IceServer.cs +++ b/src/SIPSorcery/net/ICE/IceServer.cs @@ -14,582 +14,717 @@ //----------------------------------------------------------------------------- using System; -using System.Linq; +using System.Buffers.Binary; +using System.Diagnostics; using System.Net; +using System.Net.Security; using System.Net.Sockets; -using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Org.BouncyCastle.Crypto.Digests; using SIPSorcery.Sys; -using System.Buffers.Binary; -[assembly: InternalsVisibleToAttribute("SIPSorcery.UnitTests")] +namespace SIPSorcery.Net; -namespace SIPSorcery.Net +/// +/// If ICE servers (STUN or TURN) are being used with the session this class is used to track +/// the connection state for each server that gets used. +/// +public class IceServer { + private static readonly ILogger logger = LogFactory.CreateLogger(); + + /// + /// A magic cookie to use as the prefix for STUN requests generated for ICE servers. + /// Allows quick matching of responses for ICE servers compared to responses for + /// ICE candidate connectivity checks; + /// + internal const string ICE_SERVER_TXID_PREFIX = "91245"; + + /// + /// The length of the magic cookie of server ID that are used as the prefix for + /// each ICE server transaction ID. + /// + internal const int ICE_SERVER_TXID_PREFIX_LENGTH = 6; + /// - /// If ICE servers (STUN or TURN) are being used with the session this class is used to track - /// the connection state for each server that gets used. + /// The minimum ICE server ID that can be set. /// - public class IceServer + internal const int MINIMUM_ICE_SERVER_ID = 0; + + /// + /// The maximum ICE server ID that can be set. Means the number of ICE servers per + /// session is limited to 10. Checking 10 ICE servers when attempting to establish + /// a peer connection seems very, very high. It would generally be expected that only + /// 1 or 2 ICE servers would ever be used. + /// + internal const int MAXIMUM_ICE_SERVER_ID = 9; + + /// + /// The maximum number of requests to send to an ICE server without getting + /// a response. + /// + internal const int MAX_REQUESTS = 25; + + /// + /// The maximum number of error responses before failing the ICE server checks. + /// A success response will reset the count. + /// + internal const int MAX_ERRORS = 3; + + /// + /// Time to wait for a DNS lookup of an ICE server to complete. + /// + internal const int DNS_LOOKUP_TIMEOUT_SECONDS = 3; + + /// + /// The period at which to refresh a successful STUN binding. If the ICE + /// server did not get used as the nominated candidate the ICE server + /// checks timer will be stopped. + /// + internal const int STUN_BINDING_REQUEST_REFRESH_SECONDS = 180; + + /// + /// The STUN error code response indicating an authenticated request is required. + /// + internal const int STUN_UNAUTHORISED_ERROR_CODE = 401; + + /// + /// The STUN error code response indicating a stale nonce + /// + internal const int STUN_STALE_NONCE_ERROR_CODE = 438; + + public STUNUri Uri { get; } + + internal ReadOnlyMemory Username { get; } + + internal ReadOnlyMemory Password { get; } + + /// + /// An incrementing number that needs to be unique for each server in the session. + /// + internal int Id { get; } + + /// + /// The end point for this STUN or TURN server. Will be set asynchronously once + /// any required DNS lookup completes. + /// + public IPEndPoint? ServerEndPoint { get; set; } + + /// + /// The transaction ID to use in STUN requests. It is used to match responses + /// with connection checks for this ICE serve entry. + /// + internal string? TransactionID { get; private set; } + + /// + /// The timestamp that the DNS lookup for this ICE server was sent at. + /// + internal DateTime DnsLookupSentAt { get; set; } = DateTime.MinValue; + + /// + /// The number of requests that have been sent to the server without + /// a response. + /// + internal int OutstandingRequestsSent { get; set; } + + /// + /// The timestamp the most recent binding request was sent at. + /// + internal DateTime LastRequestSentAt { get; set; } + + /// + /// The timestamp of the most recent response received from the ICE server. + /// + internal DateTime LastResponseReceivedAt { get; set; } = DateTime.MinValue; + + /// + /// This field records the time when allocation expires + /// + public DateTime TurnTimeToExpiry { get; set; } = DateTime.MinValue; + + /// + /// Records the failure message if there was an error configuring or contacting + /// the STUN or TURN server. + /// + internal SocketError Error { get; set; } = SocketError.Success; + + /// + /// If the initial Binding (for STUN) or Allocate (for TURN) connection check is successful + /// this will hold the resultant server reflexive transport address. + /// + public IPEndPoint? ServerReflexiveEndPoint { get; set; } + + /// + /// If the ICE server being checked is a TURN one and the Allocate request is successful this + /// will hold the relay transport address. + /// + internal IPEndPoint? RelayEndPoint { get; set; } + + /// + /// If requests to the server need to be authenticated this is the nonce to set. + /// Normally the nonce will come from the server in a 401 Unauthorized response. + /// + internal ReadOnlyMemory Nonce { get; set; } + + /// + /// If requests to the server need to be authenticated this is the realm to set. + /// The realm may be known in advance or can come from the server in a 401 + /// Unauthorized response. + /// + internal ReadOnlyMemory Realm { get; set; } + + /// + /// Count of the number of error responses received without a success response. + /// + internal int ErrorResponseCount; + + public ProtocolType Protocol => Uri.Protocol; + + /// + /// Task that completes when this server is done (resolved or timed out). + /// + internal Task? DnsResolutionTask { get; set; } + + internal SslClientAuthenticationOptions? SslClientAuthenticationOptions { get; set; } + + internal ReadOnlyMemory MessageIntegrityKey { get; private set; } + + /// + /// Default constructor. + /// + /// The STUN or TURN server URI the connection is being attempted to. + /// Needs to be set uniquely for each ICE server used in this session. Gets added to the + /// transaction ID to facilitate quick matching of STUN requests and responses. Needs to be between + /// 0 and 9. + /// Optional. If authentication is required the username to use. + /// Optional. If authentication is required the password to use. + internal IceServer(STUNUri uri, int id, string? username, string? password) { - private static readonly ILogger logger = LogFactory.CreateLogger(); - - /// - /// A magic cookie to use as the prefix for STUN requests generated for ICE servers. - /// Allows quick matching of responses for ICE servers compared to responses for - /// ICE candidate connectivity checks; - /// - internal const string ICE_SERVER_TXID_PREFIX = "91245"; - - /// - /// The length of the magic cookie of server ID that are used as the prefix for - /// each ICE server transaction ID. - /// - internal const int ICE_SERVER_TXID_PREFIX_LENGTH = 6; - - /// - /// The minimum ICE server ID that can be set. - /// - internal const int MINIMUM_ICE_SERVER_ID = 0; - - /// - /// The maximum ICE server ID that can be set. Means the number of ICE servers per - /// session is limited to 10. Checking 10 ICE servers when attempting to establish - /// a peer connection seems very, very high. It would generally be expected that only - /// 1 or 2 ICE servers would ever be used. - /// - internal const int MAXIMUM_ICE_SERVER_ID = 9; - - /// - /// The maximum number of requests to send to an ICE server without getting - /// a response. - /// - internal const int MAX_REQUESTS = 25; - - /// - /// The maximum number of error responses before failing the ICE server checks. - /// A success response will reset the count. - /// - internal const int MAX_ERRORS = 3; - - /// - /// Time to wait for a DNS lookup of an ICE server to complete. - /// - internal const int DNS_LOOKUP_TIMEOUT_SECONDS = 3; - - /// - /// The period at which to refresh a successful STUN binding. If the ICE - /// server did not get used as the nominated candidate the ICE server - /// checks timer will be stopped. - /// - internal const int STUN_BINDING_REQUEST_REFRESH_SECONDS = 180; - - /// - /// The STUN error code response indicating an authenticated request is required. - /// - internal const int STUN_UNAUTHORISED_ERROR_CODE = 401; - - /// - /// The STUN error code response indicating a stale nonce - /// - internal const int STUN_STALE_NONCE_ERROR_CODE = 438; - - internal STUNUri _uri; - internal string _username; - internal string _password; - - /// - /// An incrementing number that needs to be unique for each server in the session. - /// - internal int _id; - - /// - /// The end point for this STUN or TURN server. Will be set asynchronously once - /// any required DNS lookup completes. - /// - public IPEndPoint ServerEndPoint { get; set; } - - /// - /// The transaction ID to use in STUN requests. It is used to match responses - /// with connection checks for this ICE serve entry. - /// - internal string TransactionID { get; private set; } - - /// - /// The timestamp that the DNS lookup for this ICE server was sent at. - /// - internal DateTime DnsLookupSentAt { get; set; } = DateTime.MinValue; - - /// - /// The number of requests that have been sent to the server without - /// a response. - /// - internal int OutstandingRequestsSent { get; set; } - - /// - /// The timestamp the most recent binding request was sent at. - /// - internal DateTime LastRequestSentAt { get; set; } - - /// - /// The timestamp of the most recent response received from the ICE server. - /// - internal DateTime LastResponseReceivedAt { get; set; } = DateTime.MinValue; - - /// - /// This field records the time when allocation expires - /// - public DateTime TurnTimeToExpiry { get; set; } = DateTime.MinValue; - - /// - /// Records the failure message if there was an error configuring or contacting - /// the STUN or TURN server. - /// - internal SocketError Error { get; set; } = SocketError.Success; - - /// - /// If the initial Binding (for STUN) or Allocate (for TURN) connection check is successful - /// this will hold the resultant server reflexive transport address. - /// - public IPEndPoint ServerReflexiveEndPoint { get; set; } - - /// - /// If the ICE server being checked is a TURN one and the Allocate request is successful this - /// will hold the relay transport address. - /// - internal IPEndPoint RelayEndPoint { get; set; } - - /// - /// If requests to the server need to be authenticated this is the nonce to set. - /// Normally the nonce will come from the server in a 401 Unauthorized response. - /// - internal byte[] Nonce { get; set; } - - /// - /// If requests to the server need to be authenticated this is the realm to set. - /// The realm may be known in advance or can come from the server in a 401 - /// Unauthorized response. - /// - internal byte[] Realm { get; set; } - - /// - /// Count of the number of error responses received without a success response. - /// - internal int ErrorResponseCount = 0; - - public ProtocolType Protocol { get { return _uri.Protocol; } } - - public STUNUri Uri { get { return _uri; } } - - /// - /// Task that completes when this server is done (resolved or timed out). - /// - internal Task DnsResolutionTask { get; set; } - - /// - /// Default constructor. - /// - /// The STUN or TURN server URI the connection is being attempted to. - /// Needs to be set uniquely for each ICE server used in this session. Gets added to the - /// transaction ID to facilitate quick matching of STUN requests and responses. Needs to be between - /// 0 and 9. - /// Optional. If authentication is required the username to use. - /// Optional. If authentication is required the password to use. - internal IceServer(STUNUri uri, int id, string username, string password) - { - _uri = uri; - _id = id; - _username = username; - _password = password; - GenerateNewTransactionID(); - } + Uri = uri; + Id = id; + Username = string.IsNullOrEmpty(username) ? default : Encoding.UTF8.GetBytes(username); + Password = string.IsNullOrEmpty(password) ? default : Encoding.UTF8.GetBytes(password); - /// - /// Parses a semicolon-delimited ICE server string into an RTCIceServer instance. - /// Expected format: - /// urls[;username[;credential]] - /// Examples: - /// "stun:stun.example.com:3478" - /// "turn:turn.example.com?transport=tcp;user1;pass1" - /// "stun:stun1.example.com,stun:stun2.example.com" - /// Notes: - /// - Whitespace is trimmed. - /// - Surrounding quotes are removed from fields. - /// - If multiple URLs are provided in the first field (comma or whitespace separated), - /// the first non-empty URL is used. - /// - If the URL lacks a scheme (e.g. "example.com:3478"), a stun: scheme will be assumed. - /// - /// The ICE server string to parse. Format: "urls[;username[;credential]]". - /// An IceServer configured with the parsed values. - /// Thrown if iceServer is null. - /// Thrown if iceServer is empty or the URL is invalid. - public static IceServer ParseIceServer(string iceServer) - { - if (iceServer == null) - { - throw new ArgumentNullException(nameof(iceServer)); - } + GenerateNewTransactionID(); + } - iceServer = iceServer.Trim(); - if (iceServer.Length == 0) - { - throw new ArgumentException("ICE server string cannot be empty.", nameof(iceServer)); - } + /// + /// Parses a semicolon-delimited ICE server span into an IceServer instance. + /// Expected format: + /// urls[;username[;credential]] + /// Examples: + /// "stun:stun.example.com:3478" + /// "turn:turn.example.com?transport=tcp;user1;pass1" + /// "stun:stun1.example.com,stun:stun2.example.com" + /// Notes: + /// - Whitespace is trimmed. + /// - Surrounding quotes are removed from fields. + /// - If multiple URLs are provided in the first field (comma or whitespace separated), + /// the first non-empty URL is used. + /// - If the URL lacks a scheme (e.g. "example.com:3478"), a stun: scheme will be assumed. + /// + /// The ICE server span to parse. Format: "urls[;username[;credential]]". + /// An IceServer configured with the parsed values. + /// Thrown if iceServer is empty or the URL is invalid. + public static IceServer ParseIceServer(ReadOnlySpan iceServer) + { + ArgumentException.ThrowIfEmptyWhiteSpace(iceServer); - var fields = iceServer.Split([';'], StringSplitOptions.None); + // Trim the input + iceServer = iceServer.Trim(); - string Unquote(string s) - { - if (string.IsNullOrEmpty(s)) - { - return s; - } + // Extract fields on demand without creating a list + var urlsFieldRaw = UnquoteSpan(ExtractField(iceServer, 0)); + if (urlsFieldRaw.IsEmpty) + { + throw new ArgumentException("ICE server value must include a STUN/TURN URL in the first field.", nameof(iceServer)); + } - s = s.Trim(); - if (s.Length >= 2) + // If multiple URLs are provided, take the first non-empty candidate. + // Split on comma or whitespace and return early on first match. + ReadOnlySpan selectedUrl = default; + var start = 0; + for (var i = 0; i <= urlsFieldRaw.Length; i++) + { + if (i == urlsFieldRaw.Length || urlsFieldRaw[i] == ',' || urlsFieldRaw[i] == ' ') + { + if (i > start) { - if ((s[0] == '"' && s[s.Length - 1] == '"') || - (s[0] == '\'' && s[s.Length - 1] == '\'')) + var candidate = urlsFieldRaw.Slice(start, i - start).Trim(); + if (!candidate.IsEmpty) { - return s.Substring(1, s.Length - 2); + selectedUrl = candidate; + break; } } - return s; + start = i + 1; } + } + + if (selectedUrl.IsEmpty) + { + selectedUrl = urlsFieldRaw.Trim(); + } - // urls (required) - string urlsFieldRaw = fields.Length > 0 ? Unquote(fields[0]) : null; - if (string.IsNullOrWhiteSpace(urlsFieldRaw)) + // Try validate; if it fails, try auto-prefixing stun: + if (!STUNUri.TryParse(selectedUrl, out var stunUri)) + { + selectedUrl = $"stun:{selectedUrl.ToString()}"; + if (!STUNUri.TryParse(selectedUrl, out stunUri)) { - throw new ArgumentException("ICE server value must include a STUN/TURN URL in the first field.", nameof(iceServer)); + throw new ArgumentException( + $"Invalid ICE server URL: '{selectedUrl.ToString()}'. Expected a STUN/TURN URI such as 'stun:example.org:3478' or 'turn:example.org?transport=tcp'.", + nameof(iceServer)); } + } + + // username (optional) + var username = UnquoteSpan(ExtractField(iceServer, 1)); - // If multiple URLs are provided, take the first non-empty candidate. - // Split on comma or whitespace. - var urlCandidates = urlsFieldRaw.Split([ ',', ' '], StringSplitOptions.RemoveEmptyEntries) - .Select(u => u.Trim()) - .ToArray(); + // credential (optional) + var credential = UnquoteSpan(ExtractField(iceServer, 2)); - string selectedUrl = urlCandidates.Length > 0 ? urlCandidates[0] : urlsFieldRaw.Trim(); + return new IceServer( + stunUri, + 0, + username.IsEmpty ? null : username.ToString(), + credential.IsEmpty ? null : credential.ToString()); + + static ReadOnlySpan ExtractField(ReadOnlySpan input, int fieldIndex) + { + var fieldCount = 0; + var start = 0; - // Try validate; if it fails, try auto-prefixing stun: - bool isValid = STUNUri.TryParse(selectedUrl, out var stunUri); - if (!isValid) + for (var i = 0; i <= input.Length; i++) { - var withScheme = $"stun:{selectedUrl}"; - if (STUNUri.TryParse(withScheme, out var _)) + if (i == input.Length || input[i] == ';') { - selectedUrl = withScheme; - isValid = true; + if (fieldCount == fieldIndex) + { + return input.Slice(start, i - start).Trim(); + } + fieldCount++; + start = i + 1; } } - if (!isValid) - { - throw new ArgumentException( - $"Invalid ICE server URL: '{selectedUrl}'. Expected a STUN/TURN URI such as 'stun:example.org:3478' or 'turn:example.org?transport=tcp'.", - nameof(iceServer)); - } + return ReadOnlySpan.Empty; + } - // username (optional) - string username = fields.Length > 1 ? Unquote(fields[1]) : null; - if (string.IsNullOrWhiteSpace(username)) + static ReadOnlySpan UnquoteSpan(ReadOnlySpan s) + { + if (s.IsEmpty) { - username = null; + return ReadOnlySpan.Empty; } - // credential (optional) - string credential = fields.Length > 2 ? Unquote(fields[2]) : null; - if (string.IsNullOrWhiteSpace(credential)) + s = s.Trim(); + if (s.Length >= 2) { - credential = null; + if ((s[0] == '"' && s[s.Length - 1] == '"') || + (s[0] == '\'' && s[s.Length - 1] == '\'')) + { + return s.Slice(1, s.Length - 2).Trim(); + } } - - return new IceServer - ( - stunUri, - 0, - username, - credential - ); + return s; } + } - /// - /// Gets an ICE candidate for this ICE server once the required server responses have been received. - /// Note the related address and port are deliberately not set to avoid leaking information about - /// internal network configuration. - /// - /// The initialisation parameters for the ICE candidate (mainly local username). - /// The type of ICE candidate to get, must be srflx or relay. - /// An ICE candidate that can be sent to the remote peer. - internal RTCIceCandidate GetCandidate(RTCIceCandidateInit init, RTCIceCandidateType type) + /// + /// Gets an ICE candidate for this ICE server once the required server responses have been received. + /// Note the related address and port are deliberately not set to avoid leaking information about + /// internal network configuration. + /// + /// The initialisation parameters for the ICE candidate (mainly local username). + /// The type of ICE candidate to get, must be srflx or relay. + /// An ICE candidate that can be sent to the remote peer. + internal RTCIceCandidate? GetCandidate(RTCIceCandidateInit init, RTCIceCandidateType type) + { + if (type == RTCIceCandidateType.srflx && ServerReflexiveEndPoint is { }) { - RTCIceCandidate candidate = new RTCIceCandidate(init); + // TODO: Currently implementation always use UDP candidates as we will only support TURN TCP Transport. + //var srflxProtocol = _uri.Protocol == ProtocolType.Tcp ? RTCIceProtocol.tcp : RTCIceProtocol.udp; + var srflxProtocol = RTCIceProtocol.udp; - if (type == RTCIceCandidateType.srflx && ServerReflexiveEndPoint != null) - { - // TODO: Currently implementation always use UDP candidates as we will only support TURN TCP Transport. - //var srflxProtocol = _uri.Protocol == ProtocolType.Tcp ? RTCIceProtocol.tcp : RTCIceProtocol.udp; - var srflxProtocol = RTCIceProtocol.udp; - candidate.SetAddressProperties(srflxProtocol, ServerReflexiveEndPoint.Address, (ushort)ServerReflexiveEndPoint.Port, - type, null, 0); - candidate.IceServer = this; - - return candidate; - } - else if (type == RTCIceCandidateType.relay && RelayEndPoint != null) - { - // TODO: Currently implementation always use UDP candidates as we will only support TURN TCP Transport. - //var relayProtocol = _uri.Protocol == ProtocolType.Tcp ? RTCIceProtocol.tcp : RTCIceProtocol.udp; - var relayProtocol = RTCIceProtocol.udp; + var candidate = new RTCIceCandidate(init); - candidate.SetAddressProperties(relayProtocol, RelayEndPoint.Address, (ushort)RelayEndPoint.Port, - type, null, 0); - candidate.IceServer = this; + candidate.SetAddressProperties(srflxProtocol, ServerReflexiveEndPoint.Address, (ushort)ServerReflexiveEndPoint.Port, + type, null, 0); + candidate.IceServer = this; - return candidate; - } - else - { - logger.LogWarning("Could not get ICE server candidate for {Uri} and type {Type}.", _uri, type); - return null; - } + return candidate; } + else if (type == RTCIceCandidateType.relay && RelayEndPoint is { }) + { + // TODO: Currently implementation always use UDP candidates as we will only support TURN TCP Transport. + //var relayProtocol = _uri.Protocol == ProtocolType.Tcp ? RTCIceProtocol.tcp : RTCIceProtocol.udp; + var relayProtocol = RTCIceProtocol.udp; + + var candidate = new RTCIceCandidate(init); + + candidate.SetAddressProperties(relayProtocol, RelayEndPoint.Address, (ushort)RelayEndPoint.Port, + type, null, 0); + candidate.IceServer = this; - /// - /// A new transaction ID is needed for each request. - /// - internal void GenerateNewTransactionID() + return candidate; + } + else { - TransactionID = ICE_SERVER_TXID_PREFIX + _id.ToString() - + Crypto.GetRandomString(STUNHeader.TRANSACTION_ID_LENGTH - ICE_SERVER_TXID_PREFIX_LENGTH); + logger.LogIceServerCandidateUnavailable(Uri, type); + return null; } + } + + /// + /// A new transaction ID is needed for each request. + /// + internal void GenerateNewTransactionID() + { + TransactionID = ICE_SERVER_TXID_PREFIX + Id.ToString() + + Crypto.GetRandomString(STUNHeader.TRANSACTION_ID_LENGTH - ICE_SERVER_TXID_PREFIX_LENGTH); + } - /// - /// Checks whether a STUN response transaction ID belongs to a request that was sent for - /// this ICE server entry. - /// - /// The transaction ID from the STUN response. - /// True if it dos match. False if not. - internal bool IsTransactionIDMatch(string responseTxID) + /// + /// Checks whether a STUN response transaction ID belongs to a request that was sent for + /// this ICE server entry. + /// + /// The transaction ID from the STUN response. + /// True if it dos match. False if not. + internal bool IsTransactionIDMatch(string responseTxID) + { + if (responseTxID.Length < ICE_SERVER_TXID_PREFIX.Length + || !responseTxID.StartsWith(ICE_SERVER_TXID_PREFIX, StringComparison.Ordinal)) { - return responseTxID.StartsWith(ICE_SERVER_TXID_PREFIX + _id.ToString()); + return false; } - /// - /// Handler for a STUN response received in response to an ICE server connectivity check. - /// Note that no STUN requests are expected to be received from an ICE server during the initial - /// connection to an ICE server. Requests will only arrive if a TURN relay is used and data - /// indications arrive but this will be at a later stage. - /// - /// The STUN response received. - /// The remote end point the STUN response was received from. - /// True if the STUN response resulted in new ICE candidates being available (which - /// will be either a "server reflexive" or "relay" candidate. - internal bool GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoint) + var idPart = responseTxID.AsSpan(ICE_SERVER_TXID_PREFIX.Length); + return idPart.StartsWith(Id.ToString().AsSpan(), StringComparison.Ordinal); + } + + /// + /// Handler for a STUN response received in response to an ICE server connectivity check. + /// Note that no STUN requests are expected to be received from an ICE server during the initial + /// connection to an ICE server. Requests will only arrive if a TURN relay is used and data + /// indications arrive but this will be at a later stage. + /// + /// The STUN response received. + /// The remote end point the STUN response was received from. + /// True if the STUN response resulted in new ICE candidates being available (which + /// will be either a "server reflexive" or "relay" candidate. + internal bool GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoint) + { + var candidatesAvailable = false; + + // Ignore responses to old requests on the assumption they are retransmits. + if (!Encoding.ASCII.Equals(TransactionID, stunResponse.Header.TransactionId)) { - bool candidatesAvailable = false; + return candidatesAvailable; + } - string txID = Encoding.ASCII.GetString(stunResponse.Header.TransactionId); + // The STUN response is for a check sent to an ICE server. + LastResponseReceivedAt = DateTime.Now; + OutstandingRequestsSent = 0; - // Ignore responses to old requests on the assumption they are retransmits. - if (TransactionID == txID) - { - // The STUN response is for a check sent to an ICE server. - LastResponseReceivedAt = DateTime.Now; - OutstandingRequestsSent = 0; + switch (stunResponse.Header.MessageType) + { + case STUNMessageTypesEnum.AllocateSuccessResponse: + ErrorResponseCount = 0; - if (stunResponse.Header.MessageType == STUNMessageTypesEnum.AllocateSuccessResponse) + // If the relay end point is set then this connection check has already been completed. + if (RelayEndPoint is null) { - ErrorResponseCount = 0; + logger.LogIceAllocationSucceeded(Uri); - // If the relay end point is set then this connection check has already been completed. - if (RelayEndPoint == null) - { - logger.LogDebug("TURN allocate success response received for ICE server check to {Uri}.", _uri); - - var mappedAddrAttr = stunResponse.Attributes.Where(x => x.AttributeType == STUNAttributeTypesEnum.XORMappedAddress).FirstOrDefault(); + STUNXORAddressAttribute? mappedAddressAttribute = null; + STUNXORAddressAttribute? relayedAddressAttribute = null; + STUNAttribute? lifetimeAttribute = null; - if (mappedAddrAttr != null) + foreach (var attr in stunResponse.Attributes) + { + if (mappedAddressAttribute is null && attr.AttributeType == STUNAttributeTypesEnum.XORMappedAddress) { - ServerReflexiveEndPoint = (mappedAddrAttr as STUNXORAddressAttribute).GetIPEndPoint(); + mappedAddressAttribute = attr as STUNXORAddressAttribute; } - - var mappedRelayAddrAttr = stunResponse.Attributes.Where(x => x.AttributeType == STUNAttributeTypesEnum.XORRelayedAddress).FirstOrDefault(); - - if (mappedRelayAddrAttr != null) + else if (relayedAddressAttribute is null && attr.AttributeType == STUNAttributeTypesEnum.XORRelayedAddress) { - RelayEndPoint = (mappedRelayAddrAttr as STUNXORAddressAttribute).GetIPEndPoint(); + relayedAddressAttribute = attr as STUNXORAddressAttribute; } - - var lifetime = stunResponse.Attributes.FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.Lifetime); - - if (lifetime != null) + else if (lifetimeAttribute is null && attr.AttributeType == STUNAttributeTypesEnum.Lifetime) { - TurnTimeToExpiry = DateTime.Now + - TimeSpan.FromSeconds(BinaryPrimitives.ReadUInt32BigEndian(lifetime.Value)); + lifetimeAttribute = attr; } - else + + if (mappedAddressAttribute is { } && relayedAddressAttribute is { } && lifetimeAttribute is { }) { - TurnTimeToExpiry = DateTime.Now + - TimeSpan.FromSeconds(3600); + break; } - - candidatesAvailable = true; } - } - else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.AllocateErrorResponse) - { - logger.LogWarning("ICE session received an error response for an Allocate request to {Uri} from {remoteEP}.", _uri, remoteEndPoint); - ErrorResponseCount++; - - if (stunResponse.Attributes.Any(x => x.AttributeType == STUNAttributeTypesEnum.ErrorCode)) + if (mappedAddressAttribute is { }) { - STUNErrorCodeAttribute errCodeAttribute = stunResponse.Attributes.FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.ErrorCode) as STUNErrorCodeAttribute; - STUNAddressAttribute alternateServerAttribute = alternateServerAttribute = stunResponse.Attributes.FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.AlternateServer) as STUNAddressAttribute; + ServerReflexiveEndPoint = mappedAddressAttribute.GetIPEndPoint(); + } - if (errCodeAttribute.ErrorCode == STUN_UNAUTHORISED_ERROR_CODE || errCodeAttribute.ErrorCode == STUN_STALE_NONCE_ERROR_CODE) - { - // Set the authentication properties authenticate. - SetAuthenticationFields(stunResponse); + if (relayedAddressAttribute is { }) + { + RelayEndPoint = relayedAddressAttribute.GetIPEndPoint(); + } - // Set a new transaction ID. - GenerateNewTransactionID(); + if (lifetimeAttribute is { }) + { + TurnTimeToExpiry = + DateTime.Now + TimeSpan.FromSeconds(BinaryPrimitives.ReadUInt32BigEndian(lifetimeAttribute.Value.Span)); + } + else + { + TurnTimeToExpiry = DateTime.Now + + TimeSpan.FromSeconds(3600); + } - ErrorResponseCount = 1; - } - else if (alternateServerAttribute != null) - { - ServerEndPoint = new IPEndPoint(alternateServerAttribute.Address, alternateServerAttribute.Port); + candidatesAvailable = true; + } + break; + case STUNMessageTypesEnum.AllocateErrorResponse: + ErrorResponseCount++; - logger.LogWarning("ICE session received an alternate respose for an Allocate request to {Uri}, changed server url to {ServerEndPoint}.", _uri, ServerEndPoint); + STUNErrorCodeAttribute? allocateErrorCodeAttribute = null; + STUNAddressAttribute? allocateAlternateServerAttribute = null; + foreach (var attr in stunResponse.Attributes) + { + if (allocateErrorCodeAttribute is null && attr.AttributeType == STUNAttributeTypesEnum.ErrorCode) + { + allocateErrorCodeAttribute = attr as STUNErrorCodeAttribute; + break; // Stop as soon as the first ErrorCode is found + } + else if (allocateAlternateServerAttribute is null && attr.AttributeType == STUNAttributeTypesEnum.AlternateServer) + { + allocateAlternateServerAttribute = attr as STUNAddressAttribute; + } - // Set a new transaction ID. - GenerateNewTransactionID(); + if (allocateErrorCodeAttribute is { } && allocateAlternateServerAttribute is { }) + { + break; // Stop as soon as both attributes are found + } + } - ErrorResponseCount = 1; + if (allocateErrorCodeAttribute is { }) + { + if (allocateErrorCodeAttribute.ErrorCode is STUN_UNAUTHORISED_ERROR_CODE or STUN_STALE_NONCE_ERROR_CODE) + { + if (allocateErrorCodeAttribute.ErrorCode is STUN_UNAUTHORISED_ERROR_CODE) + { + logger.LogStunUnauthorisedError(remoteEndPoint); } else { - logger.LogWarning("ICE session received an error response for an Allocate request to {Uri}, error {ErrorCode} {ReasonPhrase}.", _uri, errCodeAttribute.ErrorCode, errCodeAttribute.ReasonPhrase); + logger.LogStunStaleNonceError(remoteEndPoint); } + + // Set the authentication properties authenticate. + SetAuthenticationFields(stunResponse); + + // Set a new transaction ID. + GenerateNewTransactionID(); + + ErrorResponseCount = 1; + } + else if (allocateAlternateServerAttribute is { }) + { + Debug.Assert(allocateAlternateServerAttribute.Address is { }); + ServerEndPoint = new IPEndPoint(allocateAlternateServerAttribute.Address, allocateAlternateServerAttribute.Port); + + logger.LogIceStunAlternateServer(Uri, ServerEndPoint); + + // Set a new transaction ID. + GenerateNewTransactionID(); + + ErrorResponseCount = 1; } else { - logger.LogWarning("ICE session received an error response for an Allocate request to {Uri}.", _uri); + logger.LogIceAllocateRequestErrorResponseWithCode(Uri, allocateErrorCodeAttribute.ErrorCode, allocateErrorCodeAttribute.ReasonPhrase); } } - else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.BindingSuccessResponse) + else { - ErrorResponseCount = 0; + logger.LogIceStunAllocateError(Uri); + } + break; + case STUNMessageTypesEnum.BindingSuccessResponse: + ErrorResponseCount = 0; - // If the server reflexive end point is set then this connection check has already been completed. - if (ServerReflexiveEndPoint == null) + // If the server reflexive end point is set then this connection check has already been completed. + if (ServerReflexiveEndPoint is null) + { + logger.LogIceStunBindingSuccess(Uri); + foreach (var attr in stunResponse.Attributes) { - logger.LogDebug("STUN binding success response received for ICE server check to {Uri}.", _uri); - var mappedAddrAttr = stunResponse.Attributes.Where(x => x.AttributeType == STUNAttributeTypesEnum.XORMappedAddress).FirstOrDefault(); - - if (mappedAddrAttr != null) + if (attr.AttributeType == STUNAttributeTypesEnum.XORMappedAddress) { - ServerReflexiveEndPoint = (mappedAddrAttr as STUNXORAddressAttribute).GetIPEndPoint(); + ServerReflexiveEndPoint = ((STUNXORAddressAttribute)attr).GetIPEndPoint(); candidatesAvailable = true; + break; } } } - else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.BindingErrorResponse) - { - ErrorResponseCount++; + break; + case STUNMessageTypesEnum.BindingErrorResponse: + ErrorResponseCount++; - if (stunResponse.Attributes.Any(x => x.AttributeType == STUNAttributeTypesEnum.ErrorCode)) + STUNErrorCodeAttribute? bindErrorCodeAttribute = null; + foreach (var attr in stunResponse.Attributes) + { + if (attr.AttributeType == STUNAttributeTypesEnum.ErrorCode) { - var errCodeAttribute = stunResponse.Attributes.First(x => x.AttributeType == STUNAttributeTypesEnum.ErrorCode) as STUNErrorCodeAttribute; + bindErrorCodeAttribute = attr as STUNErrorCodeAttribute; + break; + } + } - if (errCodeAttribute.ErrorCode == STUN_UNAUTHORISED_ERROR_CODE || errCodeAttribute.ErrorCode == STUN_STALE_NONCE_ERROR_CODE) - { - SetAuthenticationFields(stunResponse); + if (bindErrorCodeAttribute is { }) + { + if (bindErrorCodeAttribute.ErrorCode is STUN_UNAUTHORISED_ERROR_CODE or STUN_STALE_NONCE_ERROR_CODE) + { + SetAuthenticationFields(stunResponse); - // Set a new transaction ID. - GenerateNewTransactionID(); - } - else - { - logger.LogWarning("ICE session received an error response for a Binding request to {Uri}, error {ErrorCode} {ReasonPhrase}.", _uri, errCodeAttribute.ErrorCode, errCodeAttribute.ReasonPhrase); - } + // Set a new transaction ID. + GenerateNewTransactionID(); } else { - logger.LogWarning("STUN binding error response received for ICE server check to {Uri}.", _uri); - // The STUN response is for a check sent to an ICE server. - Error = SocketError.ConnectionRefused; + logger.LogIceBindingRequestErrorResponseWithCode(Uri, bindErrorCodeAttribute.ErrorCode, bindErrorCodeAttribute.ReasonPhrase); } } - else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.RefreshSuccessResponse) + else { - ErrorResponseCount = 0; - - logger.LogDebug("STUN binding success response received for ICE server check to {Uri}.", _uri); + logger.LogIceStunBindingError(Uri); + // The STUN response is for a check sent to an ICE server. + Error = SocketError.ConnectionRefused; + } + break; + case STUNMessageTypesEnum.RefreshSuccessResponse: + ErrorResponseCount = 0; - var lifetime = stunResponse.Attributes.FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.Lifetime); + logger.LogIceStunBindingSuccess(Uri); - if (lifetime != null) + STUNAttribute? refreshLifetimeAttr = null; + foreach (var attr in stunResponse.Attributes) + { + if (attr.AttributeType == STUNAttributeTypesEnum.Lifetime) { - TurnTimeToExpiry = DateTime.Now + - TimeSpan.FromSeconds(BinaryPrimitives.ReadUInt32BigEndian(lifetime.Value)); + refreshLifetimeAttr = attr; + break; } } - else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.RefreshErrorResponse) + if (refreshLifetimeAttr is { }) { - ErrorResponseCount++; + TurnTimeToExpiry = DateTime.Now + + TimeSpan.FromSeconds(BinaryPrimitives.ReadUInt32BigEndian(refreshLifetimeAttr.Value.Span)); + } - if (stunResponse.Attributes.Any(x => x.AttributeType == STUNAttributeTypesEnum.ErrorCode)) + break; + case STUNMessageTypesEnum.RefreshErrorResponse: + ErrorResponseCount++; + + STUNErrorCodeAttribute? refreshErrorCodeAttribute = null; + foreach (var attr in stunResponse.Attributes) + { + if (attr.AttributeType == STUNAttributeTypesEnum.ErrorCode) { - var errCodeAttribute = stunResponse.Attributes.First(x => x.AttributeType == STUNAttributeTypesEnum.ErrorCode) as STUNErrorCodeAttribute; + refreshErrorCodeAttribute = attr as STUNErrorCodeAttribute; + break; + } + } - if (errCodeAttribute.ErrorCode == STUN_UNAUTHORISED_ERROR_CODE || errCodeAttribute.ErrorCode == STUN_STALE_NONCE_ERROR_CODE) - { - SetAuthenticationFields(stunResponse); + if (refreshErrorCodeAttribute is { }) + { + if (refreshErrorCodeAttribute.ErrorCode is STUN_UNAUTHORISED_ERROR_CODE or STUN_STALE_NONCE_ERROR_CODE) + { + SetAuthenticationFields(stunResponse); - // Set a new transaction ID. - GenerateNewTransactionID(); - } - else - { - logger.LogWarning("ICE session received an error response for a Refresh request to {Uri}, error {ErrorCode} {ReasonPhrase}.", _uri, errCodeAttribute.ErrorCode, errCodeAttribute.ReasonPhrase); - } + // Set a new transaction ID. + GenerateNewTransactionID(); } else { - logger.LogWarning("STUN binding error response received for ICE server check to {Uri}.", _uri); - // The STUN response is for a check sent to an ICE server. - Error = SocketError.ConnectionRefused; + logger.LogIceRefreshRequestErrorResponseWithCode(Uri, refreshErrorCodeAttribute.ErrorCode, refreshErrorCodeAttribute.ReasonPhrase); } } else { - logger.LogWarning("An unrecognised STUN {MessageType} response for an ICE server check was received from {RemoteEndPoint}.", stunResponse.Header.MessageType, remoteEndPoint); - ErrorResponseCount++; + logger.LogIceStunBindingError(Uri); + // The STUN response is for a check sent to an ICE server. + Error = SocketError.ConnectionRefused; } + break; + default: + logger.LogIceUnrecognisedStunResponse(stunResponse.Header.MessageType, remoteEndPoint); + ErrorResponseCount++; + break; + } + + return candidatesAvailable; + } + + /// + /// Extracts the fields required for authentication from a STUN error response. + /// + /// The STUN authentication required error response. + internal void SetAuthenticationFields(STUNMessage stunResponse) + { + // Set the authentication properties authenticate. + + var computeMessageIntegrityKey = false; + + foreach (var attr in stunResponse.Attributes) + { + if (attr.AttributeType == STUNAttributeTypesEnum.Nonce) + { + Nonce = attr.Value.ToArray(); + computeMessageIntegrityKey = true; + } + else if (attr.AttributeType == STUNAttributeTypesEnum.Realm) + { + Realm = attr.Value.ToArray(); + computeMessageIntegrityKey = true; } - return candidatesAvailable; + if (!Nonce.IsEmpty && !Realm.IsEmpty) + { + break; + } } - /// - /// Extracts the fields required for authentication from a STUN error response. - /// - /// The STUN authentication required error response. - internal void SetAuthenticationFields(STUNMessage stunResponse) + if (computeMessageIntegrityKey && !Realm.IsEmpty && !Nonce.IsEmpty && !Username.IsEmpty && !Password.IsEmpty) { - // Set the authentication properties authenticate. - var nonceAttribute = stunResponse.Attributes.FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.Nonce); - Nonce = nonceAttribute?.Value; + var messageIntegrityKeySource = new byte[Username.Length + Realm.Length + Password.Length + 2]; + var messageIntegrityKeySourceSpan = messageIntegrityKeySource.AsSpan(); + + var offset = Username.Length; + Username.Span.CopyTo(messageIntegrityKeySourceSpan); + messageIntegrityKeySourceSpan[offset++] = (byte)':'; + + Realm.Span.CopyTo(messageIntegrityKeySourceSpan.Slice(offset)); + offset += Realm.Length; + messageIntegrityKeySourceSpan[offset++] = (byte)':'; + + Password.Span.CopyTo(messageIntegrityKeySourceSpan.Slice(offset)); + + + var md5Digest = new MD5Digest(); + var md5DigestLength = md5Digest.GetDigestSize(); + + var messageIntegrityKey = new byte[md5DigestLength]; + + md5Digest.BlockUpdate(messageIntegrityKeySource); + md5Digest.DoFinal(messageIntegrityKey, 0); - var realmAttribute = stunResponse.Attributes.FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.Realm); - Realm = realmAttribute?.Value; + MessageIntegrityKey = messageIntegrityKey; } } } diff --git a/src/SIPSorcery/net/ICE/IceServerResolver.cs b/src/SIPSorcery/net/ICE/IceServerResolver.cs index 6f8bf3a8ff..9e7b2ba4ea 100644 --- a/src/SIPSorcery/net/ICE/IceServerResolver.cs +++ b/src/SIPSorcery/net/ICE/IceServerResolver.cs @@ -15,12 +15,12 @@ //----------------------------------------------------------------------------- using System; -using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; +using System.Diagnostics; using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; @@ -31,12 +31,12 @@ public class IceServerResolver { private static readonly ILogger logger = LogFactory.CreateLogger(); - private ConcurrentDictionary _iceServers = new(); + private FrozenDictionary _iceServers = FrozenDictionary.Empty; - public IReadOnlyDictionary IceServers => new ReadOnlyDictionary(_iceServers); + public FrozenDictionary IceServers => Volatile.Read(ref _iceServers); public IceServerResolver() - { } + { } /// /// Initialises the ICE servers if any were provided in the initial configuration. @@ -46,64 +46,81 @@ public IceServerResolver() /// /// See https://tools.ietf.org/html/rfc8445#section-5.1.1.2 public void InitialiseIceServers( - List iceServers, + IEnumerable iceServers, RTCIceTransportPolicy policy) { - if(iceServers == null || iceServers.Count == 0) + if (iceServers is { }) { - logger.LogDebug("{caller} no ICE servers provided.", nameof(IceServerResolver)); - return; - } - - int iceServerID = IceServer.MINIMUM_ICE_SERVER_ID; - _iceServers.Clear(); + var iceServerID = IceServer.MINIMUM_ICE_SERVER_ID; - foreach (var cfg in iceServers) - { - foreach (var rawUrl in cfg.urls.Split([ ',' ], IceServer.MAXIMUM_ICE_SERVER_ID + 1, StringSplitOptions.RemoveEmptyEntries)) - { - if (!STUNUri.TryParse(rawUrl.Trim(), out var stunUri)) - { - logger.LogWarning("{caller} could not parse ICE server URL {url}", nameof(IceServerResolver), rawUrl); - continue; - } + var iceServersDictionary = new Dictionary(); - // Filter out policy excluded entries (TURNS/STUNS are now supported via TLS) - if (policy == RTCIceTransportPolicy.relay && stunUri.Scheme == STUNSchemesEnum.stun) - { - logger.LogWarning("{caller} ignoring ICE server {stunUri} (scheme {scheme})", nameof(IceServerResolver), stunUri, stunUri.Scheme); - continue; - } + Span ranges = stackalloc Range[IceServer.MAXIMUM_ICE_SERVER_ID + 1]; - // Avoid deplicates. - if (_iceServers.ContainsKey(stunUri)) - { - continue; - } - - var server = new IceServer(stunUri, iceServerID++, cfg.username, cfg.credential); + foreach (var cfg in iceServers) + { + var urls = cfg.urls.AsSpan(); + var rangesCount = urls.Split(ranges, ',', StringSplitOptions.RemoveEmptyEntries); - // immediate bind if it’s already an IP - if (IPAddress.TryParse(stunUri.Host, out var ip)) + for (var r = 0; r < rangesCount && iceServerID <= IceServer.MAXIMUM_ICE_SERVER_ID; r++) { - server.ServerEndPoint = new IPEndPoint(ip, stunUri.Port); - logger.LogDebug("{caller} bound {Uri} -> {EndPoint}", nameof(IceServerResolver), stunUri, server.ServerEndPoint); + var rawUrl = urls[ranges[r]].Trim(); + + if (!STUNUri.TryParse(rawUrl, out var stunUri)) + { + logger.LogIceServerUrlParseError(rawUrl.ToString()); + continue; + } + + // Filter out TLS or policy excluded entries + if (stunUri.Scheme is STUNSchemesEnum.stuns || + (stunUri.Scheme is STUNSchemesEnum.turns && stunUri.Transport == STUNProtocolsEnum.udp) || + (policy == RTCIceTransportPolicy.relay && stunUri.Scheme == STUNSchemesEnum.stun)) + { + logger.LogIcePolicyStunWarning(stunUri); + continue; + } + + // Avoid deplicates. + if (iceServersDictionary.ContainsKey(stunUri)) + { + continue; + } + + var server = new IceServer(stunUri, iceServerID++, cfg.username, cfg.credential) + { + SslClientAuthenticationOptions = cfg.SslClientAuthenticationOptions, + }; + + // immediate bind if it’s already an IP + if (IPAddress.TryParse(stunUri.Host, out var ip)) + { + server.ServerEndPoint = new IPEndPoint(ip, stunUri.Port); + logger.LogIceServerEndPointSet(stunUri, server.ServerEndPoint); + } + + iceServersDictionary[stunUri] = server; + + if (server.ServerEndPoint is null) + { + // Kick off DNS in background, passing the key so we can update the map. + ScheduleDnsLookup(stunUri, server); + } + + if (iceServerID > IceServer.MAXIMUM_ICE_SERVER_ID) + { + logger.LogMaxServers(); + break; + } } + } - _iceServers[stunUri] = server; - - if (server.ServerEndPoint == null) - { - // Kick off DNS in background, passing the key so we can update the map. - ScheduleDnsLookup(stunUri, server); - } + _iceServers = iceServersDictionary.ToFrozenDictionary(); + } - if (iceServerID > IceServer.MAXIMUM_ICE_SERVER_ID) - { - logger.LogWarning("{caller} reached max ICE server count", nameof(IceServerResolver)); - break; - } - } + if (_iceServers.Count == 0) + { + logger.LogIceServerNotAcquired(); } } @@ -114,69 +131,78 @@ private void ScheduleDnsLookup(STUNUri key, IceServer server) return; } - if (_iceServers.ContainsKey(key)) + if (_iceServers.TryGetValue(key, out var iceServer)) { // NOTE: must use DateTime.Now (not UtcNow) because RtpIceChannel.CheckIceServers // measures the elapsed time with DateTime.Now.Subtract(DnsLookupSentAt). Using // UtcNow here makes the timeout fire ~immediately in any non-UTC timezone (the // sign of the local offset determines whether the channel gives up before DNS // resolves or never times out at all). - _iceServers[key].DnsLookupSentAt = DateTime.Now; + iceServer.DnsLookupSentAt = DateTime.UtcNow; } - logger.LogDebug("{caller} starting DNS lookup for ICE server {Uri}", nameof(IceServerResolver), key); + logger.LogIceServerDnsLookup(key); server.DnsResolutionTask = Task.Run(async () => { try { - var resolveTask = STUNDns.Resolve(key); - var timeout = Task.Delay(TimeSpan.FromSeconds(IceServer.DNS_LOOKUP_TIMEOUT_SECONDS)); - var winner = await Task.WhenAny(resolveTask, timeout).ConfigureAwait(false); + var ep = await STUNDns.Resolve(key).WaitAsync(TimeSpan.FromSeconds(IceServer.DNS_LOOKUP_TIMEOUT_SECONDS)).ConfigureAwait(false); - if (winner == resolveTask) - { - var ep = await resolveTask.ConfigureAwait(false); - server.ServerEndPoint = ep; - logger.LogDebug("{caller} resolved {Uri} -> {EndPoint}", nameof(IceServerResolver), key, ep); - } - else - { - server.Error = SocketError.TimedOut; - logger.LogWarning("{caller} DNS lookup timed out for {Uri}", nameof(IceServerResolver), key); - } + Debug.Assert(ep is { }); + server.ServerEndPoint = ep; + logger.LogIceServerResolved(key, ep); + } + catch (TimeoutException) + { + server.Error = SocketError.TimedOut; + logger.LogIceServerConnectionTimeout(key, 0); // RequestsSent not tracked here } catch (Exception ex) { server.Error = SocketError.HostNotFound; - logger.LogWarning(ex, "{caller} DNS resolution failed for {Uri}", nameof(IceServerResolver), key); + logger.LogIceServerResolutionFailed(key, ex); } - _iceServers[key] = server; + var iceServers = Volatile.Read(ref _iceServers); + var iceServersDictionary = iceServers.Count > 1 ? new Dictionary(_iceServers) : new Dictionary(_iceServers); + iceServersDictionary[key] = server; + Volatile.Write(ref _iceServers, iceServersDictionary.ToFrozenDictionary()); }); } /// /// Wait until all ICE servers have resolved or timed out. Optional timeout. /// - public async Task WaitForAllIceServersAsync(TimeSpan? timeout = null) + public Task WaitForAllIceServersAsync(TimeSpan? timeout = null) { - var tasks = _iceServers.Values - .Select(s => s.DnsResolutionTask ?? Task.CompletedTask) - .ToArray(); + var iceServers = Volatile.Read(ref _iceServers); + var dnsResolutionTasks = new List(iceServers.Count); + foreach (var server in iceServers.Values) + { + if (server.DnsResolutionTask is { }) + { + dnsResolutionTasks.Add(server.DnsResolutionTask); + } + } + + return dnsResolutionTasks.Count == 0 + ? Task.CompletedTask + : WaitForAllIceServersCoreAsync(dnsResolutionTasks.ToArray(), timeout); - var all = Task.WhenAll(tasks); - if (timeout.HasValue) + static async Task WaitForAllIceServersCoreAsync(Task[] dnsResolutionTasks, TimeSpan? timeout) { - if (await Task.WhenAny(all, Task.Delay(timeout.Value)).ConfigureAwait(false) != all) + // propagate any resolution exception + try + { + await Task.WhenAny(dnsResolutionTasks).WaitAsync(timeout).ConfigureAwait(false); + } + catch (TimeoutException ex) { throw new TimeoutException( - $"Timed out waiting {timeout.Value} for ICE server DNS resolutions"); + $"Timed out waiting {timeout.GetValueOrDefault()} for ICE server DNS resolutions", ex); } } - - // propagate any resolution exception - await all.ConfigureAwait(false); } } diff --git a/src/SIPSorcery/net/ICE/MdnsResolver.cs b/src/SIPSorcery/net/ICE/MdnsResolver.cs index c096f176a4..46ac743f74 100644 --- a/src/SIPSorcery/net/ICE/MdnsResolver.cs +++ b/src/SIPSorcery/net/ICE/MdnsResolver.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: MdnsResolver.cs // // Description: Multicast DNS (RFC 6762) hostname resolver used by @@ -35,6 +35,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -73,15 +74,23 @@ private MdnsResolver() { } // group on every NIC every time -- wasteful and also racey on // Windows where the rebind can take a few hundred ms. private static readonly object s_lock = new object(); - private static MulticastService s_mdns; + private static MulticastService? s_mdns; private static bool s_startFailed; - private static MulticastService GetService() + private static MulticastService? GetService() { - if (s_mdns != null || s_startFailed) return s_mdns; + if (s_mdns is not null || s_startFailed) + { + return s_mdns; + } + lock (s_lock) { - if (s_mdns != null || s_startFailed) return s_mdns; + if (s_mdns is not null || s_startFailed) + { + return s_mdns; + } + try { var mdns = new MulticastService(); @@ -111,7 +120,7 @@ public static async Task ResolveAsync(string hostname, Cancellation } var mdns = GetService(); - if (mdns == null) + if (mdns is null) { return Array.Empty(); } @@ -133,11 +142,14 @@ public static async Task ResolveAsync(string hostname, Cancellation continue; } - IPAddress addr = null; - if (rr is ARecord a) addr = a.Address; - else if (rr is AAAARecord aaaa) addr = aaaa.Address; + var addr = rr switch + { + ARecord a => a.Address, + AAAARecord aaaa => aaaa.Address, + _ => null, + }; - if (addr != null) + if (addr is not null) { lock (addressesLock) { diff --git a/src/SIPSorcery/net/ICE/NetICeLoggingExtensions.cs b/src/SIPSorcery/net/ICE/NetICeLoggingExtensions.cs new file mode 100644 index 0000000000..c7ce374c11 --- /dev/null +++ b/src/SIPSorcery/net/ICE/NetICeLoggingExtensions.cs @@ -0,0 +1,1299 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using Microsoft.Extensions.Logging; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; + +internal static partial class NetIceLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "IceServerDnsLookup", + Level = LogLevel.Debug, + Message = "Attempting to resolve STUN server URI {Uri}.")] + public static partial void LogIceServerDnsLookup( + this ILogger logger, + STUNUri uri); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerDnsResolutionFailed", + Level = LogLevel.Warning, + Message = "ICE server DNS resolution failed for {Uri}.")] + public static partial void LogIceServerDnsResolutionFailed( + this ILogger logger, + STUNUri uri); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerConnectionTimeout", + Level = LogLevel.Warning, + Message = "Connection attempt to ICE server {Uri} timed out after {RequestsSent} requests.")] + public static partial void LogIceServerConnectionTimeout( + this ILogger logger, + STUNUri uri, + int requestsSent); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerErrorResponses", + Level = LogLevel.Warning, + Message = "Connection attempt to ICE server {Uri} cancelled after {ErrorResponseCount} error responses.")] + public static partial void LogIceServerErrorResponses( + this ILogger logger, + STUNUri uri, + int errorResponseCount); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunBindingSuccess", + Level = LogLevel.Debug, + Message = "STUN binding success response received for ICE server check to {Uri}.")] + public static partial void LogIceStunBindingSuccess( + this ILogger logger, + STUNUri uri); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunBindingError", + Level = LogLevel.Warning, + Message = "STUN binding error response received for ICE server check to {Uri}.")] + public static partial void LogIceStunBindingError( + this ILogger logger, + STUNUri uri); + + [LoggerMessage( + EventId = 0, + EventName = "IceTurnRefreshRequest", + Level = LogLevel.Debug, + Message = "Sending TURN refresh request to ICE server {Uri}.")] + public static partial void LogIceTurnRefreshRequest( + this ILogger logger, + STUNUri uri); + + [LoggerMessage( + EventId = 0, + EventName = "IceTurnPermissionsFailed", + Level = LogLevel.Warning, + Message = "ICE RTP channel failed to get a Create Permissions response from {IceServerUri} after {TurnPermissionsRequestSent} attempts.")] + public static partial void LogIceTurnPermissionsFailed( + this ILogger logger, + STUNUri? iceServerUri, + int turnPermissionsRequestSent); + + [LoggerMessage( + EventId = 0, + EventName = "IceTurnPermissionsRequest", + Level = LogLevel.Debug, + Message = "ICE RTP channel sending TURN permissions request {TurnPermissionsRequestSent} to server {IceServerUri} for peer {RemoteCandidate} (TxID: {RequestTransactionID}).")] + public static partial void LogIceTurnPermissionsRequest( + this ILogger logger, + int turnPermissionsRequestSent, + STUNUri? iceServerUri, + IPEndPoint? remoteCandidate, + string requestTransactionID); + + [LoggerMessage( + EventId = 0, + EventName = "IceChecksTimerStopped", + Level = LogLevel.Debug, + Message = "ICE RTP channel stopping connectivity checks in connection state {IceConnectionState}.")] + public static partial void LogIceChecksTimerStopped( + this ILogger logger, + RTCIceConnectionState iceConnectionState); + + [LoggerMessage( + EventId = 0, + EventName = "IceChannelLocalCandidates", + Level = LogLevel.Debug, + Message = "RTP ICE Channel discovered {CandidateCount} local candidates.")] + public static partial void LogIceChannelLocalCandidates( + this ILogger logger, + int candidateCount); + + [LoggerMessage( + EventId = 0, + EventName = "IceChecklistEntryTxIdMatch", + Level = LogLevel.Information, + Message = "Received transaction id from a previous cached RequestTransactionID {Id} Index: {Index}")] + public static partial void LogIceChecklistEntryTxIdMatch( + this ILogger logger, + string id, + int index); + + [LoggerMessage( + EventId = 0, + EventName = "IceTurnPermissionResponse", + Level = LogLevel.Debug, + Message = "A TURN Create Permission success response was received from {RemoteEndPoint} (TxID: {TransactionId}).")] + public static partial void LogIceTurnPermissionResponse( + this ILogger logger, + IPEndPoint remoteEndPoint, + string transactionId); + + [LoggerMessage( + EventId = 0, + EventName = "IceChecklistEntryFailed", + Level = LogLevel.Warning, + Message = "ICE RTP channel check list entry set to failed: {RemoteCandidate}.")] + public static partial void LogIceChecklistEntryFailed( + this ILogger logger, + RTCIceCandidate remoteCandidate); + + [LoggerMessage( + EventId = 0, + EventName = "IceChannelConnected", + Level = LogLevel.Debug, + Message = "ICE RTP channel connected after {Duration:0.##}ms {LocalCandidate}->{RemoteCandidate}.", + SkipEnabledCheck = true)] + private static partial void LogIceChannelConnectedUnchecked( + this ILogger logger, + double duration, + string? localCandidate, + string? remoteCandidate); + + public static void LogIceChannelConnected( + this ILogger logger, + long duration, + RTCIceCandidate? localCandidate, + RTCIceCandidate? remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceChannelConnectedUnchecked(duration, localCandidate?.ToShortString(), remoteCandidate?.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceChannelNominatedChanged", + Level = LogLevel.Debug, + Message = "ICE RTP channel remote nominated candidate changed from {OldCandidate} to {NewCandidate}.", + SkipEnabledCheck = true)] + private static partial void LogIceChannelNominatedChangedUnchecked( + this ILogger logger, + string? oldCandidate, + string? newCandidate); + + public static void LogIceChannelNominatedChanged( + this ILogger logger, + RTCIceCandidate? oldCandidate, + RTCIceCandidate? newCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceChannelNominatedChangedUnchecked(oldCandidate?.ToShortString(), newCandidate?.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceChannelFailed", + Level = LogLevel.Warning, + Message = "ICE RTP channel failed to connect as no checklist entries became available within {ElapsedSeconds}s.")] + private static partial void LogIceChannelFailedUnchecked( + this ILogger logger, + double elapsedSeconds); + + public static void LogIceChannelFailed( + this ILogger logger, + DateTime checklistStartedAt) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceChannelFailedUnchecked(DateTime.Now.Subtract(checklistStartedAt).TotalSeconds); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceMdnsResolutionFailed", + Level = LogLevel.Warning, + Message = "RTP ICE channel MDNS resolver failed to resolve {RemoteCandidateAddress}.")] + public static partial void LogIceMdnsResolutionFailed( + this ILogger logger, + string remoteCandidateAddress); + + [LoggerMessage( + EventId = 0, + EventName = "IceMdnsResolutionSuccess", + Level = LogLevel.Debug, + Message = "RTP ICE channel resolved MDNS hostname {RemoteCandidateAddress} to {RemoteCandidateIPAddr}.")] + public static partial void LogIceMdnsResolutionSuccess( + this ILogger logger, + string remoteCandidateAddress, + IPAddress remoteCandidateIPAddr); + + [LoggerMessage( + EventId = 0, + EventName = "IceTcpStarted", + Level = LogLevel.Debug, + Message = "RTPIceChannel TCP for {LocalEndPoint} started.")] + public static partial void LogTcpStarted( + this ILogger logger, + EndPoint? localEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceChecklistNominatedBinding", + Level = LogLevel.Debug, + Message = "ICE RTP channel remote peer nominated entry from binding request: {RemoteCandidate}.", + SkipEnabledCheck = true)] + private static partial void LogIceChecklistNominatedBindingUnchecked( + this ILogger logger, + string remoteCandidate); + + public static void LogIceChecklistNominatedBinding( + this ILogger logger, + RTCIceCandidate remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceChecklistNominatedBindingUnchecked(remoteCandidate.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceChecklistBindingResponse", + Level = LogLevel.Debug, + Message = "ICE RTP channel binding response state {State} as Controller for {RemoteCandidate}", + SkipEnabledCheck = true)] + private static partial void LogIceChecklistBindingResponseUnchecked( + this ILogger logger, + ChecklistEntryState state, + string? remoteCandidate); + + public static void LogIceChecklistBindingResponse( + this ILogger logger, + ChecklistEntryState state, + RTCIceCandidate? remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceChecklistBindingResponseUnchecked(state, remoteCandidate?.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceChecklistNominatedResponse", + Level = LogLevel.Debug, + Message = "ICE RTP channel remote peer nominated entry from binding response {RemoteCandidate}", + SkipEnabledCheck = true)] + private static partial void LogIceChecklistNominatedResponseUnchecked( + this ILogger logger, + string? remoteCandidate); + + public static void LogIceChecklistNominatedResponse( + this ILogger logger, + RTCIceCandidate? remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceChecklistNominatedResponseUnchecked(remoteCandidate?.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceChannelDisconnected", + Level = LogLevel.Warning, + Message = "ICE RTP channel disconnected after {Duration:0.##}s {LocalCandidate}->{RemoteCandidate}.", + SkipEnabledCheck = true)] + private static partial void LogIceChannelDisconnectedUnchecked( + this ILogger logger, + double duration, + string? localCandidate, + string? remoteCandidate); + + public static void LogIceChannelDisconnected( + this ILogger logger, + double duration, + RTCIceCandidate? localCandidate, + RTCIceCandidate? remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + logger.LogIceChannelDisconnectedUnchecked(duration, localCandidate?.ToShortString(), remoteCandidate?.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceChannelReconnected", + Level = LogLevel.Debug, + Message = "ICE RTP channel has re-connected {LocalCandidate}->{RemoteCandidate}.", + SkipEnabledCheck = true)] + private static partial void LogIceChannelReconnectedUnchecked( + this ILogger logger, + string? localCandidate, + string? remoteCandidate); + + public static void LogIceChannelReconnected( + this ILogger logger, + RTCIceCandidate? localCandidate, + RTCIceCandidate? remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceChannelReconnectedUnchecked(localCandidate?.ToShortString(), remoteCandidate?.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceConnectivityCheckSending", + Level = LogLevel.Debug, + Message = "ICE RTP channel sending connectivity check for {LocalCandidate}->{RemoteCandidate} from {LocalEndPoint} to {RemoteEndPoint} (use candidate {SetUseCandidate}).", + SkipEnabledCheck = true)] + private static partial void LogIceConnectivityCheckUnchecked( + this ILogger logger, + string? localCandidate, + string? remoteCandidate, + IPEndPoint? localEndPoint, + IPEndPoint? remoteEndPoint, + bool setUseCandidate); + + public static void LogIceConnectivityCheck( + this ILogger logger, + RTCIceCandidate? localCandidate, + RTCIceCandidate? remoteCandidate, + IPEndPoint localEndPoint, + IPEndPoint? remoteEndPoint, + bool setUseCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceConnectivityCheckUnchecked(localCandidate?.ToShortString(), remoteCandidate?.ToShortString(), localEndPoint, remoteEndPoint, setUseCandidate); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceRelayCheck", + Level = LogLevel.Debug, + Message = "ICE RTP channel sending connectivity check for {LocalCandidate}->{RemoteCandidate} from {LocalEndPoint} to relay at {RelayServerEndPoint} (use candidate {SetUseCandidate}).", + SkipEnabledCheck = true)] + private static partial void LogIceRelayCheckUnchecked( + this ILogger logger, + string? localCandidate, + string? remoteCandidate, + IPEndPoint localEndPoint, + IPEndPoint? relayServerEndPoint, + bool setUseCandidate); + + public static void LogIceRelayCheck( + this ILogger logger, + RTCIceCandidate? localCandidate, + RTCIceCandidate? remoteCandidate, + IPEndPoint localEndPoint, + IPEndPoint? relayServerEndPoint, + bool setUseCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceRelayCheckUnchecked(localCandidate?.ToShortString(), remoteCandidate?.ToShortString(), localEndPoint, relayServerEndPoint, setUseCandidate); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceStunBindingRequestFailed", + Level = LogLevel.Warning, + Message = "ICE RTP channel STUN binding request from {RemoteEndPoint} failed an integrity check, rejecting.")] + public static partial void LogIceStunBindingRequestFailed( + this ILogger logger, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceUnrecognisedStunResponse", + Level = LogLevel.Warning, + Message = "An unrecognised STUN {MessageType} response for an ICE server check was received from {RemoteEndPoint}.")] + public static partial void LogIceUnrecognisedStunResponse( + this ILogger logger, + STUNMessageTypesEnum messageType, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerRefreshError", + Level = LogLevel.Error, + Message = "Cannot refresh TURN allocation")] + public static partial void LogIceServerRefreshError( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunRequestMismatch", + Level = LogLevel.Warning, + Message = "ICE RTP channel STUN request matched a remote candidate but NOT a checklist entry.")] + public static partial void LogIceStunRequestMismatch( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceBindingRequestRejected", + Level = LogLevel.Warning, + Message = "ICE RTP channel rejecting non-relayed STUN binding request from {RemoteEndPoint}.")] + public static partial void LogIceBindingRequestRejected( + this ILogger logger, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunServerBindingSendError", + Level = LogLevel.Warning, + Message = "Error sending STUN server binding request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.")] + public static partial void LogIceStunServerBindingSendError( + this ILogger logger, + int outstandingRequestsSent, + STUNUri uri, + IPEndPoint serverEndPoint, + SocketError sendResult); + + [LoggerMessage( + EventId = 0, + EventName = "IceTurnAllocateRequestSendError", + Level = LogLevel.Warning, + Message = "Error sending TURN Allocate request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.")] + public static partial void LogIceTurnAllocateRequestSendError( + this ILogger logger, + int outstandingRequestsSent, + STUNUri uri, + IPEndPoint serverEndPoint, + SocketError sendResult); + + [LoggerMessage( + EventId = 0, + EventName = "IceTurnRefreshRequestSendError", + Level = LogLevel.Warning, + Message = "Error sending TURN Refresh request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.")] + public static partial void LogIceTurnRefreshRequestSendError( + this ILogger logger, + int outstandingRequestsSent, + STUNUri uri, + IPEndPoint serverEndPoint, + SocketError sendResult); + + [LoggerMessage( + EventId = 0, + EventName = "IceTurnCreatePermissionsRequestSendError", + Level = LogLevel.Warning, + Message = "Error sending TURN Create Permissions request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.")] + public static partial void LogIceTurnCreatePermissionsRequestSendError( + this ILogger logger, + int outstandingRequestsSent, + STUNUri uri, + IPEndPoint serverEndPoint, + SocketError sendResult); + + [LoggerMessage( + EventId = 0, + EventName = "IcePeerReflexAdded", + Level = LogLevel.Debug, + Message = "Adding peer reflex ICE candidate for {RemoteEndPoint}.")] + public static partial void LogIcePeerReflexAdded( + this ILogger logger, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceClosed", + Level = LogLevel.Debug, + Message = "RtpIceChannel for {LocalEndPoint} closed.")] + public static partial void LogIceClosed( + this ILogger logger, + IPEndPoint localEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceConnCheckEmptyPair", + Level = LogLevel.Warning, + Message = "RTP ICE channel was requested to send a connectivity check on an empty candidate pair.")] + public static partial void LogIceConnCheckEmptyPair( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunBindingErrorResponse", + Level = LogLevel.Warning, + Message = "ICE RTP channel a STUN binding error response was received from {RemoteEndPoint}.")] + public static partial void LogIceStunBindingErrorResponse( + this ILogger logger, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceTurnCreatePermissionsError", + Level = LogLevel.Warning, + Message = "ICE RTP channel TURN Create Permission error response was received from {RemoteEndPoint}.")] + public static partial void LogIceTurnCreatePermissionsError( + this ILogger logger, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceUnexpectedStunResponse", + Level = LogLevel.Warning, + Message = "ICE RTP channel received an unexpected STUN response {MessageType} from {RemoteEndPoint}.")] + public static partial void LogIceUnexpectedStunResponse( + this ILogger logger, + STUNMessageTypesEnum messageType, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunNoAuthServer", + Level = LogLevel.Warning, + Message = "A STUN error response was received on an ICE candidate without a corresponding ICE server, ignoring.")] + public static partial void LogIceStunNoAuthServer( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunRequestTxIdMismatch", + Level = LogLevel.Warning, + Message = "ICE RTP channel received a STUN {MessageType} with a transaction ID that did not match a checklist entry.")] + public static partial void LogIceStunRequestTxIdMismatch( + this ILogger logger, + STUNMessageTypesEnum messageType); + + [LoggerMessage( + EventId = 0, + EventName = "IceAllocationSucceeded", + Level = LogLevel.Debug, + Message = "TURN allocate success response received for ICE server check to {Uri}.")] + public static partial void LogIceAllocationSucceeded( + this ILogger logger, + STUNUri uri); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerCandidateUnavailable", + Level = LogLevel.Warning, + Message = "Could not get ICE server candidate for {Uri} and type {Type}.")] + public static partial void LogIceServerCandidateUnavailable( + this ILogger logger, + STUNUri uri, + RTCIceCandidateType type); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunAllocateError", + Level = LogLevel.Warning, + Message = "ICE session received an error response for an Allocate request to {Uri}.")] + public static partial void LogIceStunAllocateError( + this ILogger logger, + STUNUri uri); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunRefreshError", + Level = LogLevel.Warning, + Message = "ICE session received an error response for a Refresh request to {Uri}.")] + public static partial void LogIceStunRefreshError( + this ILogger logger, + STUNUri uri); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunAlternateServer", + Level = LogLevel.Warning, + Message = "ICE session received an alternate response for an Allocate request to {Uri}, changed server url to {ServerEndPoint}.")] + public static partial void LogIceStunAlternateServer( + this ILogger logger, + STUNUri uri, + IPEndPoint serverEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceAllocateRequestErrorResponseWithCode", + Level = LogLevel.Warning, + Message = "ICE session received an error response for an Allocate request to {Uri}, error {ErrorCode} {ReasonPhrase}.")] + public static partial void LogIceAllocateRequestErrorResponseWithCode( + this ILogger logger, + STUNUri uri, + int errorCode, + string reasonPhrase); + + [LoggerMessage( + EventId = 0, + EventName = "IceBindingRequestErrorResponseWithCode", + Level = LogLevel.Warning, + Message = "ICE session received an error response for a Binding request to {Uri}, error {ErrorCode} {ReasonPhrase}.")] + public static partial void LogIceBindingRequestErrorResponseWithCode( + this ILogger logger, + STUNUri uri, + int errorCode, + string reasonPhrase); + + [LoggerMessage( + EventId = 0, + EventName = "IceRefreshRequestErrorResponseWithCode", + Level = LogLevel.Warning, + Message = "ICE session received an error response for a Refresh request to {Uri}, error {ErrorCode} {ReasonPhrase}.")] + public static partial void LogIceRefreshRequestErrorResponseWithCode( + this ILogger logger, + STUNUri uri, + int errorCode, + string reasonPhrase); + + [LoggerMessage( + EventId = 0, + EventName = "DtlsUnexpectedStunMessage", + Level = LogLevel.Warning, + Message = "ICE RTP channel received an unexpected STUN message {MessageType} from {RemoteEndPoint}.\nJson: {StunMessage}")] + public static partial void LogDtlsUnexpectedStunMessage( + this ILogger logger, + STUNMessageTypesEnum messageType, + IPEndPoint remoteEndPoint, + STUNMessage stunMessage); + + [LoggerMessage( + EventId = 0, + EventName = "TcpSendToSocketError", + Level = LogLevel.Warning, + Message = "RTPIceChannel Send over TCP error: {SocketErrorCode}")] + public static partial void LogTcpSendToSocketError( + this ILogger logger, + SocketError socketErrorCode); + + [LoggerMessage( + EventId = 0, + EventName = "TcpSendToError", + Level = LogLevel.Error, + Message = "Exception RTPIceChannel EndSendToTCP. {ErrorMessage}")] + public static partial void LogTcpSendToError( + this ILogger logger, + string errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "IceStunBindingSendError", + Level = LogLevel.Warning, + Message = "Error sending STUN server binding request to {RemoteEndPoint}. {SendResult}.")] + public static partial void LogStunBindingSendError( + this ILogger logger, + IPEndPoint remoteEndPoint, + SocketError sendResult); + + [LoggerMessage( + EventId = 0, + EventName = "IceTurnRelayError", + Level = LogLevel.Warning, + Message = "Error sending TURN relay request to TURN server at {RelayEndPoint}. {SendResult}.")] + public static partial void LogTurnRelayError( + this ILogger logger, + IPEndPoint relayEndPoint, + SocketError sendResult); + + [LoggerMessage( + EventId = 0, + EventName = "IceRemoteCandidate", + Level = LogLevel.Debug, + Message = "RTP ICE Channel received remote candidate: {Candidate}")] + public static partial void LogRemoteCandidate( + this ILogger logger, + RTCIceCandidate candidate); + + [LoggerMessage( + EventId = 0, + EventName = "IceRemoteCredentials", + Level = LogLevel.Debug, + Message = "RTP ICE Channel remote credentials set.")] + public static partial void LogRemoteCredentialsSet( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "TcpDisconnectRequest", + Level = LogLevel.Debug, + Message = "SendOverTCP request disconnect.")] + public static partial void LogTcpDisconnectRequest( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceSocketReceiveError", + Level = LogLevel.Error, + Message = "Exception IceTcpReceiver.BeginReceiveFrom. {ErrorMessage}")] + public static partial void LogIceSocketReceiveError( + this ILogger logger, + string errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "TcpSendStatus", + Level = LogLevel.Debug, + Message = "SendOverTCP status: {Connected} endpoint: {EndPoint}")] + public static partial void LogTcpSendStatus( + this ILogger logger, + bool connected, + IPEndPoint endPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceTcpError", + Level = LogLevel.Error, + Message = "Exception RTPChannel.Close. {ErrorMessage}")] + public static partial void LogTcpError( + this ILogger logger, + string errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "IceSocketWarning", + Level = LogLevel.Warning, + Message = "Socket error {SocketErrorCode} in IceTcpReceiver.BeginReceiveFrom. {ErrorMessage}")] + public static partial void LogIceSocketWarning( + this ILogger logger, + SocketError socketErrorCode, + string errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "TcpAddress", + Level = LogLevel.Debug, + Message = "RTP ICE channel has no MDNS resolver set, and the system can not resolve remote candidate with MDNS hostname {CandidateAddress}.")] + public static partial void LogTcpAddress( + this ILogger logger, + string candidateAddress); + + [LoggerMessage( + EventId = 0, + EventName = "IceServersChecksFailed", + Level = LogLevel.Debug, + Message = "RTP ICE Channel all ICE server connection checks failed, stopping ICE servers timer.")] + public static partial void LogIceServersChecksFailed( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceStopsProcessing", + Level = LogLevel.Debug, + Message = "ICE RTP channel stopping ICE server checks in gathering state {IceGatheringState} and connection state {IceConnectionState}.")] + public static partial void LogIceStopsProcessing( + this ILogger logger, + RTCIceGatheringState iceGatheringState, + RTCIceConnectionState iceConnectionState); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerNotAcquired", + Level = LogLevel.Debug, + Message = "RTP ICE Channel was not able to acquire an active ICE server, stopping ICE servers timer.")] + public static partial void LogIceServerNotAcquired( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceActiveServerNotSet", + Level = LogLevel.Debug, + Message = "RTP ICE Channel was not able to set an active ICE server, stopping ICE servers timer.")] + public static partial void LogIceActiveServerNotSet( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "ReceivedStunResponse", + Level = LogLevel.Debug, + Message = "Received STUN response from remote endpoint {RemoteEndPoint}, local port {LocalPort}: {StunMessage}")] + public static partial void LogReceivedStunResponse( + this ILogger logger, + IPEndPoint remoteEndPoint, + int localPort, + STUNMessage stunMessage); + + [LoggerMessage( + EventId = 0, + EventName = "SendStunBindingRequest", + Level = LogLevel.Debug, + Message = "Sending STUN binding request to ICE server {Uri} with address {EndPoint}: {StunMessage}")] + public static partial void LogSendStunBindingRequest( + this ILogger logger, + STUNUri uri, + IPEndPoint endPoint, + STUNMessage stunMessage); + + [LoggerMessage( + EventId = 0, + EventName = "SendTurnAllocateRequest", + Level = LogLevel.Debug, + Message = "Sending TURN allocate request to ICE server {Uri} with address {EndPoint}: {StunMessage}")] + public static partial void LogSendTurnAllocateRequest( + this ILogger logger, + STUNUri uri, + IPEndPoint endPoint, + STUNMessage stunMessage); + + [LoggerMessage( + EventId = 0, + EventName = "SendTurnRefreshRequest", + Level = LogLevel.Debug, + Message = "Sending TURN refresh request to ICE server {Uri} with address {EndPoint}: {StunMessage}")] + public static partial void LogSendTurnRefreshRequest( + this ILogger logger, + STUNUri uri, + IPEndPoint endPoint, + STUNMessage stunMessage); + + [LoggerMessage( + EventId = 0, + EventName = "SendTurnCreatePermissionsRequest", + Level = LogLevel.Debug, + Message = "Sending TURN create permissions request to ICE server {Uri} with address {EndPoint}: {StunMessage}")] + public static partial void LogSendTurnCreatePermissionsRequest( + this ILogger logger, + STUNUri uri, + IPEndPoint endPoint, + STUNMessage stunMessage); + + [LoggerMessage( + EventId = 0, + EventName = "IceUnexpectedState", + Level = LogLevel.Warning, + Message = "The active ICE server reached an unexpected state {Uri}.")] + public static partial void LogIceUnexpectedState( + this ILogger logger, + STUNUri uri); + + [LoggerMessage( + EventId = 0, + EventName = "IceNominatedNewCandidate", + Level = LogLevel.Debug, + Message = "ICE RTP channel remote peer nominated a new candidate: {RemoteCandidate}.", + SkipEnabledCheck = true)] + private static partial void LogIceNominatedNewCandidateUnchecked( + this ILogger logger, + string remoteCandidate); + + public static void LogIceNominatedNewCandidate( + this ILogger logger, + RTCIceCandidate remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceNominatedNewCandidateUnchecked(remoteCandidate.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IcePeerReflexAddingCandidate", + Level = LogLevel.Debug, + Message = "Adding server reflex ICE candidate for ICE server {Uri} and {EndPoint}.")] + public static partial void LogIcePeerReflexAddingCandidate( + this ILogger logger, + STUNUri uri, + IPEndPoint endPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceRelayAddingCandidate", + Level = LogLevel.Debug, + Message = "Adding relay ICE candidate for ICE server {Uri} and {EndPoint}.")] + public static partial void LogIceRelayAddingCandidate( + this ILogger logger, + STUNUri uri, + IPEndPoint endPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceDnsResolutionSuccess", + Level = LogLevel.Warning, + Message = "RTP ICE channel resolved remote candidate {RemoteCandidateAddress} to {RemoteCandidateIPAddr}.")] + public static partial void LogIceDnsResolutionSuccess( + this ILogger logger, + string remoteCandidateAddress, + IPAddress? remoteCandidateIPAddr); + + [LoggerMessage( + EventId = 0, + EventName = "IceDnsResolutionFailed", + Level = LogLevel.Debug, + Message = "RTP ICE channel failed to resolve remote candidate {RemoteCandidateAddress}.")] + public static partial void LogIceDnsResolutionFailed( + this ILogger logger, + string remoteCandidateAddress); + + [LoggerMessage( + EventId = 0, + EventName = "IceNewChecklistEntry", + Level = LogLevel.Debug, + Message = "Adding new candidate pair to checklist for: {LocalCandidate}->{RemoteCandidate}")] + private static partial void LogIceNewChecklistEntryUnchecked( + this ILogger logger, + string? localCandidate, + string? remoteCandidate); + + public static void LogIceNewChecklistEntry( + this ILogger logger, + RTCIceCandidate? localCandidate, + RTCIceCandidate? remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceNewChecklistEntryUnchecked(localCandidate?.ToShortString(), remoteCandidate?.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceChecklistEntryTimeout", + Level = LogLevel.Debug, + Message = "ICE RTP channel checks for checklist entry have timed out, state being set to failed: {LocalCandidate}->{RemoteCandidate}.", + SkipEnabledCheck = true)] + private static partial void LogIceChecklistEntryTimeoutUnchecked( + this ILogger logger, + string? localCandidate, + string? remoteCandidate); + + public static void LogIceChecklistEntryTimeout( + this ILogger logger, + RTCIceCandidate? localCandidate, + RTCIceCandidate? remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceChecklistEntryTimeoutUnchecked(localCandidate?.ToShortString(), remoteCandidate?.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceChecklistEntrySucceededTimeout", + Level = LogLevel.Debug, + Message = "ICE RTP channel checks for succeded checklist entry have timed out, state being set to failed: {LocalCandidate}->{RemoteCandidate}.")] + private static partial void LogIceChecklistEntrySucceededTimeoutUnchecked( + this ILogger logger, + string? localCandidate, + string? remoteCandidate); + + public static void LogIceChecklistEntrySucceededTimeout( + this ILogger logger, + RTCIceCandidate? localCandidate, + RTCIceCandidate? remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogIceChecklistEntrySucceededTimeoutUnchecked(localCandidate?.ToShortString(), remoteCandidate?.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceChecklistEntryLowerPriority", + Level = LogLevel.Debug, + Message = "Removing lower priority entry and adding candidate pair to checklist for: {RemoteCandidate}")] + public static partial void LogIceChecklistEntryLowerPriority( + this ILogger logger, + RTCIceCandidate remoteCandidate); + + [LoggerMessage( + EventId = 0, + EventName = "IceChecklistEntryHigherPriority", + Level = LogLevel.Debug, + Message = "Existing checklist entry has higher priority, NOT adding entry for: {RemoteCandidate}")] + public static partial void LogIceChecklistEntryHigherPriority( + this ILogger logger, + RTCIceCandidate remoteCandidate); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerAdded", + Level = LogLevel.Debug, + Message = "Adding ICE server for {Uri}.")] + public static partial void LogIceServerAdded( + this ILogger logger, + STUNUri uri); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerEndPointSet", + Level = LogLevel.Debug, + Message = "ICE server end point for {Uri} set to {EndPoint}.")] + public static partial void LogIceServerEndPointSet( + this ILogger logger, + STUNUri uri, + IPEndPoint endPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerResolved", + Level = LogLevel.Debug, + Message = "ICE server {Uri} successfully resolved to {Result}.")] + public static partial void LogIceServerResolved( + this ILogger logger, + STUNUri uri, + IPEndPoint result); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerResolutionFailed", + Level = LogLevel.Warning, + Message = "Unable to resolve ICE server end point for {Uri}.")] + public static partial void LogIceServerResolutionFailed( + this ILogger logger, + STUNUri uri, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "IceFailedNoRemoteCredentials", + Level = LogLevel.Warning, + Message = "ICE RTP channel checklist processing cannot occur as either the remote ICE user or password are not set.")] + public static partial void LogIceFailedNoRemoteCredentials( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceChannelFailedTimeout", + Level = LogLevel.Warning, + Message = "ICE RTP channel failed after {Duration:0.##}s {LocalCandidate}->{RemoteCandidate}.", + SkipEnabledCheck = true)] + private static partial void LogIceChannelFailedTimeoutUnchecked( + this ILogger logger, + int duration, + string localCandidate, + string remoteCandidate); + + public static void LogIceChannelFailedTimeout( + this ILogger logger, + int duration, + RTCIceCandidate localCandidate, + RTCIceCandidate remoteCandidate) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + logger.LogIceChannelFailedTimeoutUnchecked(duration, localCandidate.ToShortString(), remoteCandidate.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "IceLocalCandidateUpdateChecklistError", + Level = LogLevel.Error, + Message = "UpdateChecklist the local candidate supplied to UpdateChecklist was null.")] + public static partial void LogIceLocalCandidateUpdateChecklistError( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceRemoteCandidateUpdateChecklistError", + Level = LogLevel.Error, + Message = "UpdateChecklist the remote candidate supplied to UpdateChecklist was null.")] + public static partial void LogIceRemoteCandidateUpdateChecklistError( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IcePolicyStunWarning", + Level = LogLevel.Warning, + Message = "ICE channel policy is relay only, ignoring STUN server {stunUri}.")] + public static partial void LogIcePolicyStunWarning( + this ILogger logger, + STUNUri stunUri); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerUrlParseError", + Level = LogLevel.Warning, + Message = "RTP ICE Channel could not parse ICE server URL {url}.")] + public static partial void LogIceServerUrlParseError( + this ILogger logger, + string url); + + [LoggerMessage( + EventId = 0, + EventName = "MdnsResolutionError", + Level = LogLevel.Warning, + Message = "Error resolving mDNS hostname {HostName}")] + public static partial void LogMdnsResolutionError( + this ILogger logger, + string hostName, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "MdnsResolutionNoAnswers", + Level = LogLevel.Warning, + Message = "RTP ICE channel mDNS resolver returned no answers for {CandidateAddress} within the timeout.")] + public static partial void LogMdnsResolutionNoAnswers( + this ILogger logger, + string candidateAddress); + + [LoggerMessage( + EventId = 0, + EventName = "StunServerTlsNotSupported", + Level = LogLevel.Warning, + Message = "ICE channel does not currently support TLS for STUN and TURN servers, not checking {stunUri}.")] + public static partial void LogStunServerTlsNotSupported( + this ILogger logger, + STUNUri stunUri); + + [LoggerMessage( + EventId = 0, + EventName = "MaxServers", + Level = LogLevel.Warning, + Message = "The maximum number of ICE servers for the session has been reached.")] + public static partial void LogMaxServers( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "TcpEndReceiveError", + Level = LogLevel.Error, + Message = "Exception IceTcpReceiver.EndReceiveFrom. {ErrorMessage}")] + public static partial void LogTcpEndReceiveError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RefreshError", + Level = LogLevel.Error, + Message = "Exception RefreshTurn. {ErrorMessage}")] + public static partial void LogRefreshError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "UpdateChecklistError", + Level = LogLevel.Error, + Message = "Exception UpdateChecklist. {ErrorMessage}")] + public static partial void LogUpdateChecklistError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "DstAddressError", + Level = LogLevel.Error, + Message = "The destination address for Send in RTPChannel cannot be {Address}.")] + public static partial void LogDstAddressError( + this ILogger logger, + IPAddress address); + + [LoggerMessage( + EventId = 0, + EventName = "SendTcpError", + Level = LogLevel.Error, + Message = "Exception RTPIceChannel.SendOverTCP. {ErrorMessage}")] + public static partial void LogSendTcpError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpCandidatesUnavailable", + Level = LogLevel.Warning, + Message = "RTP ICE Channel could not create a check list entry for a remote candidate with no destination end point, {RemoteCandidate}.")] + public static partial void LogRtpCandidatesUnavailable( + this ILogger logger, + RTCIceCandidate remoteCandidate); + + [LoggerMessage( + EventId = 0, + EventName = "IceSocketEndReceiveError", + Level = LogLevel.Warning, + Message = "SocketException IceTcpReceiver.EndReceiveFrom ({SocketErrorCode}). {ErrorMessage}")] + public static partial void LogIceSocketEndReceiveError( + this ILogger logger, + SocketError socketErrorCode, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "IceChannelBothFuncs", + Level = LogLevel.Warning, + Message = "RTP ICE channel has both " + nameof(RtpIceChannel.MdnsGetAddresses) + " and " + nameof(RtpIceChannel.MdnsResolve) + " set.Only " + nameof(RtpIceChannel.MdnsGetAddresses) + " will be used.")] + public static partial void LogIceMdnsBothSet( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceAgentStartGathering", + Level = LogLevel.Debug, + Message = "ICE agent starting gathering.")] + public static partial void LogIceAgentStartGathering( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerSocket", + Level = LogLevel.Debug, + Message = "ICE server socket for component {Component} created local end point {LocalEndPoint}.")] + public static partial void LogIceServerSocket( + this ILogger logger, + RTCIceComponent component, + IPEndPoint localEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceGatheringTimeout", + Level = LogLevel.Warning, + Message = "ICE gathering timed out after {TimeoutMilliseconds}ms.")] + public static partial void LogIceGatheringTimeout( + this ILogger logger, + int timeoutMilliseconds); + + [LoggerMessage( + EventId = 0, + EventName = "IceServerControlEndPoint", + Level = LogLevel.Debug, + Message = "ICE server control socket for component {Component} created local end point {LocalEndPoint}.")] + public static partial void LogIceServerControlEndPoint( + this ILogger logger, + RTCIceComponent component, + IPEndPoint localEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "IceAgentNoCandidates", + Level = LogLevel.Warning, + Message = "No ICE candidates were gathered.")] + public static partial void LogIceAgentNoCandidates( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "IceGatheringStart", + Level = LogLevel.Debug, + Message = "ICE gathering connecting to STUN server at {StunServer}.")] + public static partial void LogIceGatheringStart( + this ILogger logger, + string stunServer); + + [LoggerMessage( + EventId = 0, + EventName = "StunUnauthorisedError", + Level = LogLevel.Debug, + Message = "Received STUN Unauthorized error from {RemoteEndPoint}.")] + public static partial void LogStunUnauthorisedError( + this ILogger logger, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "StunStaleNonceError", + Level = LogLevel.Error, + Message = "Received STUN NONCE error from {RemoteEndPoint}.")] + public static partial void LogStunStaleNonceError( + this ILogger logger, + IPEndPoint remoteEndPoint); +} diff --git a/src/SIPSorcery/net/ICE/NetIceActivitySource.cs b/src/SIPSorcery/net/ICE/NetIceActivitySource.cs new file mode 100644 index 0000000000..92b5fa6d79 --- /dev/null +++ b/src/SIPSorcery/net/ICE/NetIceActivitySource.cs @@ -0,0 +1,110 @@ +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Text; +using SIPSorcery.Net; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; + +internal static class NetIceActivitySource +{ + private static readonly ActivitySource _activitySource = new("sipsorcery.net.ice"); + + public static Activity? StartStunMessageSentActivity( + STUNMessage stunMessage, + IPEndPoint dstEndPoint, + STUNUri? uri = null, + ReadOnlyMemory realm = default, + ReadOnlyMemory nonce = default, + bool isRelayed = false) + { + if (!_activitySource.HasListeners()) + { + return null; + } + + var transactionId = stunMessage.Header.TransactionId; + var messageTypeText = stunMessage.Header.MessageType.ToStringFast(); + var transactionIdText = transactionId.HexStr(); + if (_activitySource.StartActivity($"Send {(isRelayed ? " relayed" : "")} STUN {messageTypeText} message {transactionIdText}", ActivityKind.Client) is { } activity) + { + activity + .SetTag("sipsorcery.net.ice.stun.message.type", messageTypeText) + .SetTag("sipsorcery.net.ice.stun.transaction.id", transactionIdText) + .SetTag("sipsorcery.net.ice.stun.uri", uri) + .SetTag("sipsorcery.net.ice.dst.endpoint", dstEndPoint) + .SetTag("sipsorcery.net.ice.relayed", isRelayed.ToString()) + ; + + if (!realm.IsEmpty) + { + activity + .SetTag("sipsorcery.net.ice.stun.realm", Encoding.UTF8.GetString(realm.Span)) + ; + } + + if (!nonce.IsEmpty) + { + activity + .SetTag("sipsorcery.net.ice.stun.nonce", nonce.Span.HexStr()) + ; + } + + SetStunMessageCustomProperty(activity, stunMessage); + + return activity; + } + + return null; + } + + public static Activity? StartStunMessageReceivedActivity(STUNMessage stunMessage, IPEndPoint srcEndPoint, int localPort, bool isRelayed) + { + if (!_activitySource.HasListeners()) + { + return null; + } + + var transactionId = stunMessage.Header.TransactionId; + var messageTypeText = stunMessage.Header.MessageType.ToStringFast(); + var transactionIdText = transactionId.HexStr(); + if (_activitySource.StartActivity($"Received{(isRelayed ? " relayed" : "")} STUN {messageTypeText} message {transactionIdText}", ActivityKind.Client) is { } activity) + { + activity + .SetTag("sipsorcery.net.ice.stun.message.type", messageTypeText) + .SetTag("sipsorcery.net.ice.stun.transaction.id", transactionIdText) + .SetTag("sipsorcery.net.ice.src.endpoint", srcEndPoint) + .SetTag("sipsorcery.net.ice.dst.port", localPort.ToString()) + .SetTag("sipsorcery.net.ice.relayed", isRelayed.ToString()) + ; + + SetStunMessageCustomProperty(activity, stunMessage); + + return activity; + } + + return null; + } + + public static Activity SetRelayEndpoint(this Activity activity, IPEndPoint relayEndPoint) + { + activity + .SetTag("sipsorcery.net.ice.relay.endpoint", relayEndPoint) + ; + + return activity; + } + + private static void SetStunMessageCustomProperty(Activity activity, STUNMessage stunMessage) + { + activity.SetCustomProperty("StunMessage", stunMessage); + } + + private static bool IsStunMessageActivity(Activity activity) + { + return activity.GetCustomProperty("StunMessage") is { }; + } +} diff --git a/src/SIPSorcery/net/ICE/RTCIceCandidate.cs b/src/SIPSorcery/net/ICE/RTCIceCandidate.cs index 8c869775a3..b1e60e3dfb 100644 --- a/src/SIPSorcery/net/ICE/RTCIceCandidate.cs +++ b/src/SIPSorcery/net/ICE/RTCIceCandidate.cs @@ -20,418 +20,540 @@ using System; using System.Net; +using System.Net.Sockets; +using System.Diagnostics.CodeAnalysis; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class RTCIceCandidate : IRTCIceCandidate, IEquatable { - public class RTCIceCandidate : IRTCIceCandidate + public const string m_CRLF = "\r\n"; + public const string TCP_TYPE_KEY = "tcpType"; + public const string REMOTE_ADDRESS_KEY = "raddr"; + public const string REMOTE_PORT_KEY = "rport"; + public const string CANDIDATE_PREFIX = "candidate:"; + + /// + /// The ICE server (STUN or TURN) the candidate was generated from. + /// Will be null for non-ICE server candidates. + /// + public IceServer? IceServer { get; internal set; } + + public string candidate => ToString(); + + public string? sdpMid { get; set; } + + public ushort sdpMLineIndex { get; set; } + + /// + /// Composed of 1 to 32 chars. It is an + /// identifier that is equivalent for two candidates that are of the + /// same type, share the same base, and come from the same STUN + /// server. + /// + /// + /// See https://tools.ietf.org/html/rfc8445#section-5.1.1.3. + /// + public string? foundation { get; set; } + + /// + /// Is a positive integer between 1 and 256 (inclusive) + /// that identifies the specific component of the data stream for + /// which this is a candidate. + /// + public RTCIceComponent component { get; set; } = RTCIceComponent.rtp; + + /// + /// A positive integer between 1 and (2**31 - 1) inclusive. + /// This priority will be used by ICE to determine the order of the + /// connectivity checks and the relative preference for candidates. + /// Higher-priority values give more priority over lower values. + /// + /// + /// See specification at https://tools.ietf.org/html/rfc8445#section-5.1.2. + /// + public uint priority { get; set; } + + /// + /// The address or hostname for the candidate. + /// + public string? address { get; set; } + + /// + /// The transport protocol for the candidate, supported options are UDP and TCP. + /// + public RTCIceProtocol protocol { get; set; } + + /// + /// The local port the candidate is listening on. + /// + public ushort port { get; set; } + + /// + /// The type of ICE candidate, host, srflx etc. + /// + public RTCIceCandidateType type { get; set; } + + /// + /// For TCP candidates the role they are fulfilling (client, server or both). + /// + public RTCIceTcpCandidateType tcpType { get; set; } + + public string? relatedAddress { get; set; } + + public ushort relatedPort { get; set; } + + public string? usernameFragment { get; set; } + + /// + /// This is the end point to use for a remote candidate. The address supplied for an ICE + /// candidate could be a hostname or IP address. This field will be set before the candidate + /// is used. + /// + public IPEndPoint? DestinationEndPoint { get; private set; } + + private RTCIceCandidate() + { } + + public RTCIceCandidate(RTCIceCandidateInit init) { - public const string m_CRLF = "\r\n"; - public const string TCP_TYPE_KEY = "tcpType"; - public const string REMOTE_ADDRESS_KEY = "raddr"; - public const string REMOTE_PORT_KEY = "rport"; - public const string CANDIDATE_PREFIX = "candidate"; - - /// - /// The ICE server (STUN or TURN) the candidate was generated from. - /// Will be null for non-ICE server candidates. - /// - public IceServer IceServer { get; internal set; } - - public string candidate => ToString(); - - public string sdpMid { get; set; } - - public ushort sdpMLineIndex { get; set; } - - /// - /// Composed of 1 to 32 chars. It is an - /// identifier that is equivalent for two candidates that are of the - /// same type, share the same base, and come from the same STUN - /// server. - /// - /// - /// See https://tools.ietf.org/html/rfc8445#section-5.1.1.3. - /// - public string foundation { get; set; } - - /// - /// Is a positive integer between 1 and 256 (inclusive) - /// that identifies the specific component of the data stream for - /// which this is a candidate. - /// - public RTCIceComponent component { get; set; } = RTCIceComponent.rtp; - - /// - /// A positive integer between 1 and (2**31 - 1) inclusive. - /// This priority will be used by ICE to determine the order of the - /// connectivity checks and the relative preference for candidates. - /// Higher-priority values give more priority over lower values. - /// - /// - /// See specification at https://tools.ietf.org/html/rfc8445#section-5.1.2. - /// - public uint priority { get; set; } - - /// - /// The address or hostname for the candidate. - /// - public string address { get; set; } - - /// - /// The transport protocol for the candidate, supported options are UDP and TCP. - /// - public RTCIceProtocol protocol { get; set; } - - /// - /// The local port the candidate is listening on. - /// - public ushort port { get; set; } - - /// - /// The type of ICE candidate, host, srflx etc. - /// - public RTCIceCandidateType type { get; set; } - - /// - /// For TCP candidates the role they are fulfilling (client, server or both). - /// - public RTCIceTcpCandidateType tcpType { get; set; } - - public string relatedAddress { get; set; } - - public ushort relatedPort { get; set; } - - public string usernameFragment { get; set; } - - /// - /// This is the end point to use for a remote candidate. The address supplied for an ICE - /// candidate could be a hostname or IP address. This field will be set before the candidate - /// is used. - /// - public IPEndPoint DestinationEndPoint { get; private set; } - - private RTCIceCandidate() - { } - - public RTCIceCandidate(RTCIceCandidateInit init) - { - sdpMid = init.sdpMid; - sdpMLineIndex = init.sdpMLineIndex; - usernameFragment = init.usernameFragment; + sdpMid = init.sdpMid; + sdpMLineIndex = init.sdpMLineIndex; + usernameFragment = init.usernameFragment; - if (!String.IsNullOrEmpty(init.candidate)) - { - var iceCandidate = Parse(init.candidate); - foundation = iceCandidate.foundation; - priority = iceCandidate.priority; - component = iceCandidate.component; - address = iceCandidate.address; - port = iceCandidate.port; - type = iceCandidate.type; - tcpType = iceCandidate.tcpType; - relatedAddress = iceCandidate.relatedAddress; - relatedPort = iceCandidate.relatedPort; - } + if (!string.IsNullOrEmpty(init.candidate)) + { + var iceCandidate = Parse(init.candidate.AsSpan()); + foundation = iceCandidate.foundation; + priority = iceCandidate.priority; + component = iceCandidate.component; + address = iceCandidate.address; + port = iceCandidate.port; + type = iceCandidate.type; + tcpType = iceCandidate.tcpType; + relatedAddress = iceCandidate.relatedAddress; + relatedPort = iceCandidate.relatedPort; } + } + + /// + /// Convenience constructor for cases when the application wants + /// to create an ICE candidate, + /// + public RTCIceCandidate( + RTCIceProtocol cProtocol, + IPAddress cAddress, + ushort cPort, + RTCIceCandidateType cType) + { + SetAddressProperties(cProtocol, cAddress, cPort, cType, null, 0); + } - /// - /// Convenience constructor for cases when the application wants - /// to create an ICE candidate, - /// - public RTCIceCandidate( - RTCIceProtocol cProtocol, - IPAddress cAddress, - ushort cPort, - RTCIceCandidateType cType) + public void SetAddressProperties( + RTCIceProtocol cProtocol, + IPAddress cAddress, + ushort cPort, + RTCIceCandidateType cType, + IPAddress? cRelatedAddress, + ushort cRelatedPort) + { + protocol = cProtocol; + address = cAddress.ToString(); + port = cPort; + type = cType; + relatedAddress = cRelatedAddress?.ToString(); + relatedPort = cRelatedPort; + + foundation = GetFoundation(); + priority = GetPriority(); + } + + public static RTCIceCandidate Parse(ReadOnlySpan candidateLine) + { + ArgumentOutOfRangeException.ThrowIfEmptyWhiteSpace(candidateLine); + + if (!TryParse(candidateLine, out var candidate)) { - SetAddressProperties(cProtocol, cAddress, cPort, cType, null, 0); + throw new FormatException("The ICE candidate line was not in the correct format."); } - public void SetAddressProperties( - RTCIceProtocol cProtocol, - IPAddress cAddress, - ushort cPort, - RTCIceCandidateType cType, - IPAddress cRelatedAddress, - ushort cRelatedPort) + return candidate; + } + + /// + /// Attempts to parse an ICE candidate line. + /// + /// The candidate line to parse. + /// The parsed candidate, or null if parsing failed. + /// True if parsing succeeded, false otherwise. + public static bool TryParse(ReadOnlySpan candidateLine, [NotNullWhen(true)] out RTCIceCandidate? candidate) + { + candidate = null; + + if (candidateLine.IsEmptyOrWhiteSpace()) { - protocol = cProtocol; - address = cAddress.ToString(); - port = cPort; - type = cType; - relatedAddress = cRelatedAddress?.ToString(); - relatedPort = cRelatedPort; - - foundation = GetFoundation(); - priority = GetPriority(); + return false; } - public static RTCIceCandidate Parse(string candidateLine) + if (candidateLine.StartsWith(CANDIDATE_PREFIX.AsSpan(), StringComparison.Ordinal)) { - if (string.IsNullOrEmpty(candidateLine)) - { - throw new ArgumentNullException("Cant parse ICE candidate from empty string.", candidateLine); - } - else - { - candidateLine = candidateLine.Replace("candidate:", ""); + candidateLine = candidateLine.Slice(CANDIDATE_PREFIX.Length); + } - RTCIceCandidate candidate = new RTCIceCandidate(); + Span ranges = stackalloc Range[13]; + var rangesCount = candidateLine.Split(ranges, ' '); + if (rangesCount < 8) + { + return false; + } - string[] candidateFields = candidateLine.Trim().Split(' '); + var cand = new RTCIceCandidate(); - candidate.foundation = candidateFields[0]; + cand.foundation = candidateLine[ranges[0]].ToString(); - if (Enum.TryParse(candidateFields[1], out var candidateComponent)) - { - candidate.component = candidateComponent; - } + if (RTCIceComponentExtensions.TryParse(candidateLine[ranges[1]], out var candidateComponent)) + { + cand.component = candidateComponent; + } - if (Enum.TryParse(candidateFields[2], out var candidateProtocol)) - { - candidate.protocol = candidateProtocol; - } + if (RTCIceProtocolExtensions.TryParse(candidateLine[ranges[2]], out var candidateProtocol)) + { + cand.protocol = candidateProtocol; + } - if (uint.TryParse(candidateFields[3], out var candidatePriority)) - { - candidate.priority = candidatePriority; - } + if (uint.TryParse(candidateLine[ranges[3]], out var candidatePriority)) + { + cand.priority = candidatePriority; + } - candidate.address = candidateFields[4]; - candidate.port = Convert.ToUInt16(candidateFields[5]); + cand.address = candidateLine[ranges[4]].ToString(); - if (Enum.TryParse(candidateFields[7], out var candidateType)) - { - candidate.type = candidateType; - } + if (!ushort.TryParse(candidateLine[ranges[5]], out var port)) + { + return false; + } + cand.port = port; - // TCP Candidates require extra steps to be parsed - // {"candidate":"candidate:4 1 TCP 2105458943 10.0.1.16 9 typ host tcptype active","sdpMid":"sdparta_0","sdpMLineIndex":0} - var parseIndex = 8; - if (candidate.protocol == RTCIceProtocol.tcp) - { - if (candidateFields.Length > parseIndex && candidateFields[parseIndex] == TCP_TYPE_KEY) - { - candidate.relatedAddress = candidateFields[parseIndex + 1]; - } - parseIndex += 2; - } + if (RTCIceCandidateTypeExtensions.TryParse(candidateLine[ranges[7]], out var candidateType)) + { + cand.type = candidateType; + } - if (candidateFields.Length > parseIndex && candidateFields[parseIndex] == REMOTE_ADDRESS_KEY) - { - candidate.relatedAddress = candidateFields[parseIndex+1]; - } + // TCP Candidates require extra steps to be parsed + // {"candidate":"candidate:4 1 TCP 2105458943 10.0.1.16 9 typ host tcptype active","sdpMid":"sdparta_0","sdpMLineIndex":0} + var parseIndex = 8; + if (cand.protocol == RTCIceProtocol.tcp) + { + if (rangesCount > parseIndex + 1 && candidateLine[ranges[parseIndex]].Equals(TCP_TYPE_KEY.AsSpan(), StringComparison.Ordinal)) + { + cand.relatedAddress = candidateLine[ranges[parseIndex + 1]].ToString(); + } - if (candidateFields.Length > parseIndex+2 && candidateFields[parseIndex+2] == REMOTE_PORT_KEY) - { - candidate.relatedPort = Convert.ToUInt16(candidateFields[parseIndex+3]); - } + parseIndex += 2; + } - return candidate; - } + if (rangesCount > parseIndex && candidateLine[ranges[parseIndex]].Equals(REMOTE_ADDRESS_KEY.AsSpan(), StringComparison.Ordinal)) + { + cand.relatedAddress = candidateLine[ranges[parseIndex + 1]].ToString(); } - /// - /// Serialises an ICE candidate to a string that's suitable for inclusion in an SDP session - /// description payload. - /// - /// - /// The specification regarding how an ICE candidate should be serialised in SDP is at - /// https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1. - /// - /// A string representing the ICE candidate suitable for inclusion in an SDP session - /// description. - public override string ToString() + if (rangesCount > parseIndex + 3 && candidateLine[ranges[parseIndex + 2]].Equals(REMOTE_PORT_KEY.AsSpan(), StringComparison.Ordinal)) { - if (type == RTCIceCandidateType.host || type == RTCIceCandidateType.prflx) + if (!ushort.TryParse(candidateLine[ranges[parseIndex + 3]], out var relatedPort)) { - string candidateStr; - if (protocol == RTCIceProtocol.tcp) - { - candidateStr = $"{foundation} {component.GetHashCode()} tcp {priority} {address} {port} typ {type} tcptype {tcpType} generation 0"; - } - else - { - candidateStr = $"{foundation} {component.GetHashCode()} udp {priority} {address} {port} typ {type} generation 0"; - } - - return candidateStr; + return false; } - else - { - string relAddr = relatedAddress; - - if (string.IsNullOrWhiteSpace(relAddr)) - { - relAddr = IPAddress.Any.ToString(); - } + cand.relatedPort = relatedPort; + } - string candidateStr; - if (protocol == RTCIceProtocol.tcp) - { - candidateStr = $"{foundation} {component.GetHashCode()} tcp {priority} {address} {port} typ {type} tcptype {tcpType} raddr {relAddr} rport {relatedPort} generation 0"; - } - else - { - candidateStr = $"{foundation} {component.GetHashCode()} udp {priority} {address} {port} typ {type} raddr {relAddr} rport {relatedPort} generation 0"; - } + candidate = cand; + return true; + } - return candidateStr; - } + /// + /// Serialises an ICE candidate to a string that's suitable for inclusion in an SDP session + /// description payload. + /// + /// + /// The specification regarding how an ICE candidate should be serialised in SDP is at + /// https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1. + /// + /// A string representing the ICE candidate suitable for inclusion in an SDP session + /// description. + public override string ToString() + { + var sb = new ValueStringBuilder(stackalloc char[256]); + try + { + ToString(ref sb); + return sb.ToString(); + } + finally + { + sb.Dispose(); } + } + + /// + /// Appends the candidate to a ValueStringBuilder. + /// + /// The ValueStringBuilder to append to. + internal void ToString(ref ValueStringBuilder builder) + { + builder.Append(foundation); + builder.Append(' '); + builder.Append((int)component); + builder.Append(' '); - /// - /// Sets the remote end point for a remote candidate. - /// - /// The resolved end point for the candidate. - public void SetDestinationEndPoint(IPEndPoint destinationEP) + if (protocol == RTCIceProtocol.tcp) { - DestinationEndPoint = destinationEP; + builder.Append("tcp "); } - - private string GetFoundation() + else { - var serverProtocol = IceServer != null ? IceServer.Protocol.ToString().ToLower() : "udp"; - var builder = new System.Text.StringBuilder(); - builder = builder.Append(type).Append(address).Append(protocol).Append(serverProtocol); - byte[] bytes = System.Text.Encoding.ASCII.GetBytes(builder.ToString()); - return UpdateCrc32(0, bytes).ToString(); - - /*int addressVal = !String.IsNullOrEmpty(address) ? Crypto.GetSHAHash(address).Sum(x => (byte)x) : 0; - int svrVal = (type == RTCIceCandidateType.relay || type == RTCIceCandidateType.srflx) ? - Crypto.GetSHAHash(IceServer != null ? IceServer._uri.ToString() : "").Sum(x => (byte)x) : 0; - return (type.GetHashCode() + addressVal + svrVal + protocol.GetHashCode()).ToString();*/ + builder.Append("udp "); } - private uint GetPriority() + builder.Append(priority); + builder.Append(' '); + builder.Append(address); + builder.Append(' '); + builder.Append(port); + builder.Append(" typ "); + builder.Append(type.ToStringFast()); + + if (protocol == RTCIceProtocol.tcp) { - uint localPreference = 0; - IPAddress addr; + builder.Append(" tcptype "); + builder.Append(tcpType.ToStringFast()); + } - //Calculate our LocalPreference Priority - if (IPAddress.TryParse(address, out addr)) - { - uint addrPref = IPAddressHelper.IPAddressPrecedence(addr); + if (type is not RTCIceCandidateType.host and not RTCIceCandidateType.prflx) + { + var relAddr = string.IsNullOrWhiteSpace(relatedAddress) ? IPAddress.Any.ToString() : relatedAddress; + builder.Append(" raddr "); + builder.Append(relAddr); + builder.Append(" rport "); + builder.Append(relatedPort); + } - // relay_preference in original code was sorted with params: - // UDP == 2 - // TCP == 1 - // TLS == 0 - uint relayPreference = protocol == RTCIceProtocol.udp ? 2u : 1u; + builder.Append(" generation 0"); + } - // TODO: Original implementation consider network adapter preference as strength of wifi - // We will ignore it as its seems to not be a trivial implementation for use in net-standard 2.0 - uint networkAdapterPreference = 0; + /// + /// Sets the remote end point for a remote candidate. + /// + /// The resolved end point for the candidate. + public void SetDestinationEndPoint(IPEndPoint destinationEP) + { + DestinationEndPoint = destinationEP; + } - localPreference = ((networkAdapterPreference << 8) | addrPref) + relayPreference; - } + private string GetFoundation() + { + var serverProtocol = (IceServer?.Protocol ?? ProtocolType.Udp).ToLowerString(); + var builder = new System.Text.StringBuilder(); + builder = builder.Append(type).Append(address).Append(protocol).Append(serverProtocol); + var bytes = System.Text.Encoding.ASCII.GetBytes(builder.ToString()); + return UpdateCrc32(0, bytes).ToString(); + } - // RTC 5245 Define priority for RTCIceCandidateType - // https://datatracker.ietf.org/doc/html/rfc5245 - uint typePreference = 0; - switch (type) - { - case RTCIceCandidateType.host: - typePreference = 126; - break; - case RTCIceCandidateType.prflx: - typePreference = 110; - break; - case RTCIceCandidateType.srflx: - typePreference = 100; - break; - } + private uint GetPriority() + { + uint localPreference = 0; + + //Calculate our LocalPreference Priority + if (IPAddress.TryParse(address, out var addr)) + { + var addrPref = IPAddressHelper.IPAddressPrecedence(addr); + + // relay_preference in original code was sorted with params: + // UDP == 2 + // TCP == 1 + // TLS == 0 + var relayPreference = protocol == RTCIceProtocol.udp ? 2u : 1u; + + // TODO: Original implementation consider network adapter preference as strength of wifi + // We will ignore it as its seems to not be a trivial implementation for use in net-standard 2.0 + uint networkAdapterPreference = 0; + + localPreference = ((networkAdapterPreference << 8) | addrPref) + relayPreference; + } - //Use formula found in RFC 5245 to define candidate priority - return (uint)((typePreference << 24) | (localPreference << 8) | (256u - component.GetHashCode())); + // RTC 5245 Define priority for RTCIceCandidateType + // https://datatracker.ietf.org/doc/html/rfc5245 + uint typePreference = 0; + switch (type) + { + case RTCIceCandidateType.host: + typePreference = 126; + break; + case RTCIceCandidateType.prflx: + typePreference = 110; + break; + case RTCIceCandidateType.srflx: + typePreference = 100; + break; } - public string toJSON() + //Use formula found in RFC 5245 to define candidate priority + return (uint)((typePreference << 24) | (localPreference << 8) | (256u - component.GetHashCode())); + } + + public string toJSON() + { + var sb = new ValueStringBuilder(stackalloc char[256]); + try { + sb.Append(CANDIDATE_PREFIX); + ToString(ref sb); + var rtcCandInit = new RTCIceCandidateInit { sdpMid = sdpMid ?? sdpMLineIndex.ToString(), sdpMLineIndex = sdpMLineIndex, usernameFragment = usernameFragment, - candidate = $"{CANDIDATE_PREFIX}:{this}" + candidate = sb.ToString(), }; return rtcCandInit.toJSON(); } - - /// - /// Checks the candidate to identify whether it is equivalent to the specified - /// protocol and IP end point. Primary use case is to check whether a candidate - /// is a match for a remote end point that a message has been received from. - /// - /// The protocol to check equivalence for. - /// The IP end point to check equivalence for. - /// True if the candidate is deemed equivalent or false if not. - public bool IsEquivalentEndPoint(RTCIceProtocol epPotocol, IPEndPoint ep) + finally { - if (protocol == epPotocol && DestinationEndPoint != null && - ep.Address.Equals(DestinationEndPoint.Address) && DestinationEndPoint.Port == ep.Port) - { - return true; - } - else - { - return false; - } + sb.Dispose(); } + } - /// - /// Gets a short description for the candidate that's helpful for log messages. - /// - /// A short string describing the key properties of the candidate. - public string ToShortString() + /// + /// Checks the candidate to identify whether it is equivalent to the specified + /// protocol and IP end point. Primary use case is to check whether a candidate + /// is a match for a remote end point that a message has been received from. + /// + /// The protocol to check equivalence for. + /// The IP end point to check equivalence for. + /// True if the candidate is deemed equivalent or false if not. + public bool IsEquivalentEndPoint(RTCIceProtocol epPotocol, IPEndPoint ep) + { + if (protocol == epPotocol && DestinationEndPoint is { } && + ep.Address.Equals(DestinationEndPoint.Address) && DestinationEndPoint.Port == ep.Port) { - string epDescription = $"{address}:{port}"; - if (IPAddress.TryParse(address, out var ipAddress)) - { - IPEndPoint ep = new IPEndPoint(ipAddress, port); - epDescription = ep.ToString(); - } + return true; + } + else + { + return false; + } + } - return $"{protocol}:{epDescription} ({type})"; + /// + /// Gets a short description for the candidate that's helpful for log messages. + /// + /// A short string describing the key properties of the candidate. + public string ToShortString() + { + string epDescription; + if (IPAddress.TryParse(address, out var ipAddress)) + { + var ep = new IPEndPoint(ipAddress, port); + epDescription = ep.ToString(); } + else + { + epDescription = $"{address}:{port}"; + } + + return $"{protocol}:{epDescription} ({type.ToStringFast()})"; + } - //CRC32 implementation from C++ to calculate foundation - const uint kCrc32Polynomial = 0xEDB88320; - private static uint[] LoadCrc32Table() + //CRC32 implementation from C++ to calculate foundation + private const uint kCrc32Polynomial = 0xEDB88320; + private static uint[] LoadCrc32Table() + { + var kCrc32Table = new uint[256]; + for (uint i = 0; i < kCrc32Table.Length; ++i) { - uint[] kCrc32Table = new uint[256]; - for (uint i = 0; i < kCrc32Table.Length; ++i) + var c = i; + for (var j = 0; j < 8; ++j) { - uint c = i; - for (int j = 0; j < 8; ++j) + if ((c & 1) != 0) { - if ((c & 1) != 0) - { - c = kCrc32Polynomial ^ (c >> 1); - } - else - { - c >>= 1; - } + c = kCrc32Polynomial ^ (c >> 1); + } + else + { + c >>= 1; } - kCrc32Table[i] = c; } - return kCrc32Table; + kCrc32Table[i] = c; } + return kCrc32Table; + } - private uint UpdateCrc32(uint start, byte[] buf) + private uint UpdateCrc32(uint start, byte[] buf) + { + var kCrc32Table = LoadCrc32Table(); + + long c = (int)(start ^ 0xFFFFFFFF); + var u = buf; + for (var i = 0; i < buf.Length; ++i) { - var kCrc32Table = LoadCrc32Table(); + c = kCrc32Table[(c ^ u[i]) & 0xFF] ^ (c >> 8); + } + return (uint)(c ^ 0xFFFFFFFF); + } - long c = (int)(start ^ 0xFFFFFFFF); - byte[] u = buf; - for (int i = 0; i < buf.Length; ++i) - { - c = kCrc32Table[(c ^ u[i]) & 0xFF] ^ (c >> 8); - } - return (uint)(c ^ 0xFFFFFFFF); + /// + /// Determines whether the specified RTCIceCandidate is equal to the current RTCIceCandidate. + /// Equality is based on all fields used in the SDP string representation. + /// + /// The RTCIceCandidate to compare with the current candidate. + /// true if the specified candidate is equal to the current candidate; otherwise, false. + public bool Equals(RTCIceCandidate? other) + { + if (other is null) + { + return false; } + return string.Equals(foundation, other.foundation, StringComparison.Ordinal) + && component == other.component + && protocol == other.protocol + && priority == other.priority + && string.Equals(address, other.address, StringComparison.Ordinal) + && port == other.port + && type == other.type + && tcpType == other.tcpType + && string.Equals(relatedAddress, other.relatedAddress, StringComparison.Ordinal) + && relatedPort == other.relatedPort; + } + /// + /// Determines whether the specified object is equal to the current RTCIceCandidate. + /// + /// The object to compare with the current candidate. + /// true if the specified object is equal to the current candidate; otherwise, false. + public override bool Equals(object? obj) + { + return obj is RTCIceCandidate candidate && Equals(candidate); + } + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current candidate. + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(foundation); + hash.Add(component); + hash.Add(protocol); + hash.Add(priority); + hash.Add(address); + hash.Add(port); + hash.Add(type); + hash.Add(tcpType); + hash.Add(relatedAddress); + hash.Add(relatedPort); + return hash.ToHashCode(); } } diff --git a/src/SIPSorcery/net/ICE/RtpIceChannel.cs b/src/SIPSorcery/net/ICE/RtpIceChannel.cs index 9c1a8b962b..d050226cf6 100644 --- a/src/SIPSorcery/net/ICE/RtpIceChannel.cs +++ b/src/SIPSorcery/net/ICE/RtpIceChannel.cs @@ -65,1341 +65,1080 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Security; using System.Net.Sockets; -using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Org.BouncyCastle.Crypto.Digests; +using SIPSorcery.Net; +using SIPSorcery.SIP; using SIPSorcery.Sys; -[assembly: InternalsVisibleToAttribute("SIPSorcery.UnitTests")] - -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// An RTP ICE Channel carries out connectivity checks with a remote peer in an attempt to determine the best +/// destination end point to communicate with the remote party. +/// +/// +/// Local server reflexive candidates don't get added to the checklist since they are just local "host" candidates with +/// an extra NAT address mapping. The NAT address mapping is needed for the remote ICE peer but locally a server +/// reflexive candidate is always going to be represented by a "host" candidate. Limitations: - To reduce complexity +/// only a single checklist is used. This is based on the main webrtc use case where RTP (audio and video) and RTCP are +/// all multiplexed on a single socket pair. Therefore there only needs to be a single component and single data stream. +/// If an additional use case occurs then multiple checklists could be added. Developer Notes: There are 4 main tasks +/// occurring during the ICE checks: - Local candidates: ICE server checks (which can take seconds) are being carried +/// out to gather "server reflexive" and "relay" candidates. - Remote candidates: the remote peer should be trickling in +/// its candidates which need to be validated and if accepted new entries added to the checklist. - Checklist +/// connectivity checks: the candidate pairs in the checklist need to have connectivity checks sent. - Match STUN +/// messages: STUN requests and responses are being received and need to be matched to either an ICE server check or a +/// checklist entry check. After matching action needs to be taken to update the status of the ICE server or checklist +/// entry check. +/// +public partial class RtpIceChannel : RTPChannel { + private const int ICE_UFRAG_LENGTH = 4; + private const int ICE_PASSWORD_LENGTH = 24; + private const int MAX_CHECKLIST_ENTRIES = 25; // Maximum number of entries that can be added to the checklist of candidate pairs. + private const string MDNS_TLD = ".local"; // Top Level Domain name for multicast lookups as per RFC6762. + private const int CONNECTED_CHECK_PERIOD = 3; // The period in seconds to send STUN connectivity checks once connected. + public const string SDP_MID = "0"; + public const int SDP_MLINE_INDEX = 0; + + private static DnsClient.LookupClient? _dnsLookupClient; + + public static List? DefaultNameServers { get; set; } + /// - /// An RTP ICE Channel carries out connectivity checks with a remote peer in an - /// attempt to determine the best destination end point to communicate with the - /// remote party. + /// ICE transaction spacing interval in milliseconds. /// /// - /// Local server reflexive candidates don't get added to the checklist since they are just local - /// "host" candidates with an extra NAT address mapping. The NAT address mapping is needed for the - /// remote ICE peer but locally a server reflexive candidate is always going to be represented by - /// a "host" candidate. - /// - /// Limitations: - /// - To reduce complexity only a single checklist is used. This is based on the main - /// webrtc use case where RTP (audio and video) and RTCP are all multiplexed on a - /// single socket pair. Therefore there only needs to be a single component and single - /// data stream. If an additional use case occurs then multiple checklists could be added. - /// - /// Developer Notes: - /// There are 4 main tasks occurring during the ICE checks: - /// - Local candidates: ICE server checks (which can take seconds) are being carried out to - /// gather "server reflexive" and "relay" candidates. - /// - Remote candidates: the remote peer should be trickling in its candidates which need to - /// be validated and if accepted new entries added to the checklist. - /// - Checklist connectivity checks: the candidate pairs in the checklist need to have - /// connectivity checks sent. - /// - Match STUN messages: STUN requests and responses are being received and need to be - /// matched to either an ICE server check or a checklist entry check. After matching - /// action needs to be taken to update the status of the ICE server or checklist entry - /// check. + /// See https://tools.ietf.org/html/rfc8445#section-14. /// - public class RtpIceChannel : RTPChannel - { - private const int ICE_UFRAG_LENGTH = 4; - private const int ICE_PASSWORD_LENGTH = 24; - private const int MAX_CHECKLIST_ENTRIES = 25; // Maximum number of entries that can be added to the checklist of candidate pairs. - private const string MDNS_TLD = ".local"; // Top Level Domain name for multicast lookups as per RFC6762. - private const int CONNECTED_CHECK_PERIOD = 3; // The period in seconds to send STUN connectivity checks once connected. - public const string SDP_MID = "0"; - public const int SDP_MLINE_INDEX = 0; + private const int Ta = 50; - private static DnsClient.LookupClient _dnsLookupClient; + private static readonly ILogger logger = LogFactory.CreateLogger(); - public static List DefaultNameServers { get; set; } + /// + /// The period in seconds after which a connection will be flagged as disconnected. + /// + public static int DISCONNECTED_TIMEOUT_PERIOD = 8; - public class IceTcpReceiver : UdpReceiver - { - protected const int REVEIVE_TCP_BUFFER_SIZE = RECEIVE_BUFFER_SIZE * 2; + /// + /// The period in seconds after which a connection will be flagged as failed. + /// + public static int FAILED_TIMEOUT_PERIOD = 16; - protected int m_recvOffset; - public IceTcpReceiver(Socket socket, int mtu = REVEIVE_TCP_BUFFER_SIZE) : base(socket, mtu) - { - m_recvOffset = 0; - } + /// + /// The period in seconds after which a CreatePermission will be sent. + /// + public static int REFRESH_PERMISSION_PERIOD = 240; - /// - /// Starts the receive. This method returns immediately. An event will be fired in the corresponding "End" event to - /// return any data received. - /// - public override void BeginReceiveFrom() - { - //Prevent call BeginReceiveFrom if it is already running or into invalid state - if ((m_isClosed || !m_socket.Connected) && m_isRunningReceive) - { - m_isRunningReceive = false; - } - if (m_isRunningReceive || m_isClosed || !m_socket.Connected) - { - return; - } + /// + /// The lifetime value used in refresh request. + /// + public static uint ALLOCATION_TIME_TO_EXPIRY_VALUE = 600; - try - { - m_isRunningReceive = true; - EndPoint recvEndPoint = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); - var recvLength = m_recvBuffer.Length - m_recvOffset; - //Discard fragmentation buffer as seems that we will have an incorrect result based in cached values - if (recvLength <= 0 || m_recvOffset < 0) - { - m_recvOffset = 0; - recvLength = m_recvBuffer.Length; - } - m_socket.BeginReceiveFrom(m_recvBuffer, m_recvOffset, recvLength, SocketFlags.None, ref recvEndPoint, EndReceiveFrom, null); - } - // Thrown when socket is closed. Can be safely ignored. - // This exception can be thrown in response to an ICMP packet. The problem is the ICMP packet can be a false positive. - // For example if the remote RTP socket has not yet been opened the remote host could generate an ICMP packet for the - // initial RTP packets. Experience has shown that it's not safe to close an RTP connection based solely on ICMP packets. - catch (ObjectDisposedException) - { - m_isRunningReceive = false; - } - catch (SocketException sockExcp) - { - m_isRunningReceive = false; - logger.LogWarning(sockExcp, "Socket error {SocketErrorCode} in IceTcpReceiver.BeginReceiveFrom. {ErrorMessage}", sockExcp.SocketErrorCode, sockExcp.Message); - //Close(sockExcp.Message); - } - catch (Exception excp) - { - m_isRunningReceive = false; - // From https://github.com/dotnet/corefx/blob/e99ec129cfd594d53f4390bf97d1d736cff6f860/src/System.Net.Sockets/src/System/Net/Sockets/Socket.cs#L3262 - // the BeginReceiveFrom will only throw if there is an problem with the arguments or the socket has been disposed of. In that - // case the socket can be considered to be unusable and there's no point trying another receive. - logger.LogError(excp, "Exception IceTcpReceiver.BeginReceiveFrom. {ErrorMessage}", excp.Message); - Close(excp.Message); - } - } + private readonly IPAddress? _bindAddress; + private readonly RTCIceServer[] _iceServers; + private readonly RTCIceTransportPolicy _policy; - /// - /// Handler for end of the begin receive call. - /// - /// Contains the results of the receive. - protected override void EndReceiveFrom(IAsyncResult ar) - { - try - { - EndPoint remoteEP = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); - // When socket is closed the object will be disposed of in the middle of a receive. - if (!m_isClosed) - { - int bytesRead = m_socket.EndReceiveFrom(ar, ref remoteEP); + private DateTime _startedGatheringAt = DateTime.MinValue; + private DateTime _connectedAt = DateTime.MinValue; - if (bytesRead > 0) - { - ProcessRawBuffer(bytesRead + m_recvOffset, remoteEP as IPEndPoint); - } - } - else - { - m_socket.EndReceiveFromClosed(ar, ref remoteEP); - } + internal IceServerResolver _iceServerResolver = new IceServerResolver(); - // If there is still data available it should be read now. This is more efficient than calling - // BeginReceiveFrom which will incur the overhead of creating the callback and then immediately firing it. - // It also avoids the situation where if the application cannot keep up with the network then BeginReceiveFrom - // will be called synchronously (if data is available it calls the callback method immediately) which can - // create a very nasty stack. - if (!m_isClosed && m_socket.Available > 0) - { - while (!m_isClosed && m_socket.Available > 0) - { - remoteEP = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); - var recvLength = m_recvBuffer.Length - m_recvOffset; - //Discard fragmentation buffer as seems that we will have an incorrect result based in cached values - if (recvLength <= 0 || m_recvOffset < 0) - { - m_recvOffset = 0; - recvLength = m_recvBuffer.Length; - } - int bytesReadSync = m_socket.ReceiveFrom(m_recvBuffer, m_recvOffset, recvLength, SocketFlags.None, ref remoteEP); + private IceServer? _activeIceServer; - if (bytesReadSync > 0) - { - if (ProcessRawBuffer(bytesReadSync + m_recvOffset, remoteEP as IPEndPoint) == 0) - { - break; - } - } - else - { - break; - } - } - } - } - catch (SocketException resetSockExcp) when (resetSockExcp.SocketErrorCode == SocketError.ConnectionReset) - { - // ConnectionReset is raised when the OS receives an ICMP "port unreachable" message. - // On a UDP socket this commonly occurs when: - // - The remote party has not yet opened its RTP socket (e.g. during call setup), - // - The remote endpoint changed (hold, transfer) and the old port is no longer listening, - // - The remote process terminated and the OS rejected a subsequent outgoing packet. - // In all cases the local socket is still perfectly usable — the error relates to a - // single outbound send, not to the health of the receive path. The receive loop must - // continue so that packets arriving from the (possibly new) remote endpoint are not lost. - logger.LogWarning(resetSockExcp, "SocketException RtpIceChannel.EndReceiveFrom ({SocketErrorCode}). {ErrorMessage}", resetSockExcp.SocketErrorCode, resetSockExcp.Message); - } - catch (SocketException sockExcp) - { - // Other socket errors are also non-fatal for a UDP receive path. The same transient - // scenarios described above apply. - logger.LogWarning(sockExcp, "SocketException RtpIceChannel.EndReceiveFrom ({SocketErrorCode}). {ErrorMessage}", sockExcp.SocketErrorCode, sockExcp.Message); - } - catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored. - { } - catch (Exception excp) - { - logger.LogError(excp, "Exception IceTcpReceiver.EndReceiveFrom. {ErrorMessage}", excp.Message); - Close(excp.Message); - } - finally - { - m_isRunningReceive = false; - if (!m_isClosed) - { - BeginReceiveFrom(); - } - } - } + public RTCIceComponent Component { get; private set; } - // TODO: If we miss any package because slow internet connection - // and initial byte in buffer is not a STUNHeader (starts with 0x00 0x00) - // and our receive buffer is full, we need a way to discard whole buffer - // or check for 0x00 0x00 start again. - protected virtual int ProcessRawBuffer(int bytesRead, IPEndPoint remoteEP) - { - var extractCount = 0; - if (bytesRead > 0) - { - // During experiments IPPacketInformation wasn't getting set on Linux. Without it the local IP address - // cannot be determined when a listener was bound to IPAddress.Any (or IPv6 equivalent). If the caller - // is relying on getting the local IP address on Linux then something may fail. - //if (packetInfo != null && packetInfo.Address != null) - //{ - // localEndPoint = new IPEndPoint(packetInfo.Address, localEndPoint.Port); - //} - - //Try extract all StunMessages from current receive buffer - var isFragmented = true; - var recvRemainingSegment = new ArraySegment(m_recvBuffer, 0, bytesRead); - - while (recvRemainingSegment.Count > STUNHeader.STUN_HEADER_LENGTH) - { - isFragmented = false; - STUNHeader header = null; - try - { - header = STUNHeader.ParseSTUNHeader(recvRemainingSegment); - } - catch - { - header = null; - } - if (header != null) - { - int stunMsgBytes = STUNHeader.STUN_HEADER_LENGTH + header.MessageLength; - if (stunMsgBytes % 4 != 0) - { - stunMsgBytes = stunMsgBytes - (stunMsgBytes % 4) + 4; - } + public RTCIceGatheringState IceGatheringState { get; private set; } = RTCIceGatheringState.@new; - //We have the packet count all inside current receiving buffer - if (recvRemainingSegment.Count >= stunMsgBytes) - { - extractCount++; - m_recvOffset = recvRemainingSegment.Offset + recvRemainingSegment.Count; + public RTCIceConnectionState IceConnectionState { get; private set; } = RTCIceConnectionState.@new; - byte[] packetBuffer = new byte[stunMsgBytes]; - Buffer.BlockCopy(recvRemainingSegment.Array, recvRemainingSegment.Offset, packetBuffer, 0, stunMsgBytes); + /// + /// True if we are the "controlling" ICE agent (we initiated the communications) or false if we are the "controlled" + /// agent. + /// + public bool IsController { get; internal set; } - CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEP, packetBuffer); + /// + /// Optional hook to normalize the source endpoint of received STUN binding requests before they're matched against + /// the remote candidate list / checklist. Used to reconcile hairpin scenarios where a peer reaches this agent via a + /// TURN relay running on the same machine — the observed source IP is a local interface address but the remote + /// candidate was advertised with the relay's public IP. The delegate returns the translated endpoint, or + /// null / the input unchanged when no translation applies. + /// forwards its value here. + /// + public Func? RemoteEndpointTranslator { get; set; } - var newOffset = recvRemainingSegment.Offset + stunMsgBytes; - var newCount = recvRemainingSegment.Count - stunMsgBytes; - if (newCount > STUNHeader.STUN_HEADER_LENGTH && newOffset >= 0) - { - recvRemainingSegment = new ArraySegment(recvRemainingSegment.Array, newOffset, newCount); - } - else - { - if (newCount > 0 && newOffset >= 0) - { - recvRemainingSegment = new ArraySegment(recvRemainingSegment.Array, newOffset, newCount); - isFragmented = true; - } - else - { - recvRemainingSegment = new ArraySegment(); - isFragmented = false; - } - break; - } - } - //We have a fragmentation but the header is intact, we need to cache the fragmentation for the next receive cycle - else - { - isFragmented = true; - break; - } - } - //Save Remaining Buffer in start of m_recvBuffer - else - { - isFragmented = true; - break; - } - } - if (isFragmented) - { - m_recvOffset = recvRemainingSegment.Count; - Buffer.BlockCopy(recvRemainingSegment.Array, recvRemainingSegment.Offset, m_recvBuffer, 0, recvRemainingSegment.Count); - } - else + /// + /// The list of host ICE candidates that have been gathered for this peer. + /// + public List Candidates + { + get + { + return new List(_candidates); + } + } + + private ConcurrentBag _candidates = new ConcurrentBag(); + internal ConcurrentBag _remoteCandidates = new ConcurrentBag(); + + /// + /// A queue of remote ICE candidates that have been added to the session and that are waiting to be processed to + /// determine if they will create a new checklist entry. + /// + private ConcurrentQueue _pendingRemoteCandidates = new ConcurrentQueue(); + + /// + /// The state of the checklist as the ICE checks are carried out. + /// + internal ChecklistState _checklistState = ChecklistState.Running; + + /// + /// The checklist of local and remote candidate pairs + /// + internal List _checklist = new List(); + + /// + /// Lock to co-ordinate access to the _checklist. + /// + private readonly object _checklistLock = new object(); + + /// + /// For local candidates this implementation takes a shortcut to reduce complexity. The RTP socket will always be + /// bound to one of: - IPAddress.IPv6Any [::], - IPAddress.Any 0.0.0.0, or, - a specific single IP address. As such + /// it's only necessary to create a single checklist entry to cover all local Host type candidates. Host candidates + /// must still be generated, based on all local IP addresses, and will need to be transmitted to the remote peer but + /// they don't need to be used when populating the checklist. + /// + internal readonly RTCIceCandidate _localChecklistCandidate; + + /// + /// If a TURN server is being used for this session and has received a successful response to the allocate request + /// then this field will hold the candidate to use in the checklist. + /// + internal RTCIceCandidate? _relayChecklistCandidate; + + /// + /// If the connectivity checks are successful this will hold the entry that was nominated by the connection check + /// process. + /// + public ChecklistEntry? NominatedEntry { get; private set; } + + /// + /// Retransmission timer for STUN transactions, measured in milliseconds. + /// + /// + /// As specified in https://tools.ietf.org/html/rfc8445#section-14. + /// + internal int RTO + { + get + { + if (IceGatheringState == RTCIceGatheringState.gathering) + { + var rto = 500; + foreach (var candidate in Candidates) + { + if (candidate.type is RTCIceCandidateType.srflx or RTCIceCandidateType.relay) { - m_recvOffset = 0; + rto += Ta; } } - - return extractCount; + return rto; } - - /// - /// Closes the socket and stops any new receives from being initiated. - /// - public override void Close(string reason) + else { - if (!m_isClosed) + lock (_checklistLock) { - if (m_socket != null && m_socket.Connected) + var rto = 500; + foreach (var entry in _checklist) { - m_socket?.Disconnect(false); + if (entry.State is ChecklistEntryState.Waiting or ChecklistEntryState.InProgress) + { + rto += Ta; + } } - base.Close(reason); + return rto; } } } + } - /// - /// ICE transaction spacing interval in milliseconds. - /// - /// - /// See https://tools.ietf.org/html/rfc8445#section-14. - /// - private const int Ta = 50; - - private static readonly ILogger logger = LogFactory.CreateLogger(); - - /// - /// The period in seconds after which a connection will be flagged as disconnected. - /// - public static int DISCONNECTED_TIMEOUT_PERIOD = 8; + public readonly string LocalIceUser; + public readonly string LocalIcePassword; + public string? RemoteIceUser { get; private set; } + public string? RemoteIcePassword { get; private set; } - /// - /// The period in seconds after which a connection will be flagged as failed. - /// - public static int FAILED_TIMEOUT_PERIOD = 16; + private bool _closed; + private Timer? _connectivityChecksTimer; + private Timer? _processIceServersTimer; + private Timer? _refreshTurnTimer; + private DateTime _checklistStartedAt = DateTime.MinValue; + private bool _includeAllInterfaceAddresses; + private ulong _iceTiebreaker; - /// - /// The period in seconds after which a CreatePermission will be sent. - /// - public static int REFRESH_PERMISSION_PERIOD = 240; + public event Action? OnIceCandidate; + public event Action? OnIceConnectionStateChange; + public event Action? OnIceGatheringStateChange; + public event Action? OnIceCandidateError; - /// - /// The lifetime value used in refresh request. - /// - public static uint ALLOCATION_TIME_TO_EXPIRY_VALUE = 600; + /// + /// This event gets fired when a STUN message is sent by this channel. The event is for diagnostic purposes only. + /// Parameters: - STUNMessage: The STUN message that was sent. - IPEndPoint: The remote end point the STUN message + /// was sent to. - bool: True if the message was sent via a TURN server relay. + /// + public event Action? OnStunMessageSent; - private IPAddress _bindAddress; - private List _iceServers; - private RTCIceTransportPolicy _policy; + /// + /// An optional callback function to resolve remote ICE candidates with MDNS hostnames. + /// + /// + /// The order is , then . If both are null system + /// DNS resolver will be used. + /// + public Func>? MdnsResolve; - private DateTime _startedGatheringAt = DateTime.MinValue; - private DateTime _connectedAt = DateTime.MinValue; + /// + /// An optional callback function to resolve remote ICE candidates with MDNS hostnames. + /// + /// + /// The order is , then . If both are null system + /// DNS resolver will be used. + /// + public Func>? MdnsGetAddresses; - internal IceServerResolver _iceServerResolver = new IceServerResolver(); + private readonly FrozenDictionary m_iceServerConnections; - // Tracks one SslStream per TURNS/STUNS server URI so subsequent sends reuse the TLS session. - private ConcurrentDictionary _tlsStreams = new ConcurrentDictionary(); + private bool m_tcpRtpReceiverStarted; - // Per-URI write gate. SslStream.Write is NOT thread-safe and throws - // "another write operation is pending" if two threads (e.g. ICE - // connectivity checks + TURN permission refreshes + framed SCTP data) - // race. We also use this to make the lazy handshake atomic. - private ConcurrentDictionary _tlsWriteLocks = new ConcurrentDictionary(); + /// + /// Creates a new instance of an RTP ICE channel to provide RTP channel functions with ICE connectivity checks. + /// + public RtpIceChannel() : + this(null, RTCIceComponent.rtp) + { } - private IceServer _activeIceServer; + /// + /// Creates a new instance of an RTP ICE channel to provide RTP channel functions with ICE connectivity checks. + /// + /// + /// Optional. If this is not set then the default is to bind to the IPv6 wildcard address in dual mode to the IPv4 + /// wildcard address if IPv6 is not available. + /// + /// + /// The component (RTP or RTCP) the channel is being used for. Note for cases where RTP and RTCP are multiplexed the + /// component is set to RTP. + /// + /// A list of STUN or TURN servers that can be used by this ICE agent. + /// Determines which ICE candidates can be used in this RTP ICE Channel. + /// + /// If set to true then IP addresses from ALL local interfaces will be used for host ICE candidates. If left as the + /// default false value host candidates will be restricted to the single interface that the OS routing table matches + /// to the destination address or the Internet facing interface if the destination is not known. The restrictive + /// behaviour is as per the recommendation at: + /// https://tools.ietf.org/html/draft-ietf-rtcweb-ip-handling-12#section-5.2. + /// + public RtpIceChannel( + IPAddress? bindAddress, + RTCIceComponent component, + IEnumerable? iceServers = null, + RTCIceTransportPolicy policy = RTCIceTransportPolicy.all, + bool includeAllInterfaceAddresses = false, + int bindPort = 0, + PortRange? rtpPortRange = null) : + base(false, bindAddress, bindPort, rtpPortRange) + { + _bindAddress = bindAddress; + Component = component; + _iceServers = iceServers is null ? Array.Empty() : System.Linq.Enumerable.ToArray(iceServers); + _policy = policy; + _includeAllInterfaceAddresses = includeAllInterfaceAddresses; + _iceTiebreaker = Crypto.GetRandomULong(); - public RTCIceComponent Component { get; private set; } + LocalIceUser = Crypto.GetRandomString(ICE_UFRAG_LENGTH); + LocalIcePassword = Crypto.GetRandomString(ICE_PASSWORD_LENGTH); - public RTCIceGatheringState IceGatheringState { get; private set; } = RTCIceGatheringState.@new; + base.OnStunMessageReceived += (stunMessage, remoteEndPoint, wasRelayed) => + { + _ = ProcessStunMessage(stunMessage, remoteEndPoint, wasRelayed); + }; - public RTCIceConnectionState IceConnectionState { get; private set; } = RTCIceConnectionState.@new; + _localChecklistCandidate = new RTCIceCandidate(new RTCIceCandidateInit + { + sdpMid = SDP_MID, + sdpMLineIndex = SDP_MLINE_INDEX, + usernameFragment = LocalIceUser + }); + + _localChecklistCandidate.SetAddressProperties( + RTCIceProtocol.udp, + base.RTPLocalEndPoint.Address, + (ushort)base.RTPLocalEndPoint.Port, + RTCIceCandidateType.host, + null, + 0); + + if (_iceServers is { Length: > 0 }) + { + var iceServerConnections = new Dictionary(); - /// - /// True if we are the "controlling" ICE agent (we initiated the communications) or - /// false if we are the "controlled" agent. - /// - public bool IsController { get; internal set; } + _iceServerResolver.InitialiseIceServers(_iceServers, _policy); - /// - /// Optional hook to normalize the source endpoint of received STUN binding requests - /// before they're matched against the remote candidate list / checklist. Used to - /// reconcile hairpin scenarios where a peer reaches this agent via a TURN relay - /// running on the same machine — the observed source IP is a local interface address - /// but the remote candidate was advertised with the relay's public IP. The delegate - /// returns the translated endpoint, or null / the input unchanged when no - /// translation applies. - /// forwards its value here. - /// - public Func RemoteEndpointTranslator { get; set; } + var resolvedIceServers = _iceServerResolver.IceServers; - /// - /// The list of host ICE candidates that have been gathered for this peer. - /// - public List Candidates - { - get + foreach (var (uri, iceServer) in resolvedIceServers) { - return _candidates.ToList(); - } - } - - private ConcurrentBag _candidates = new ConcurrentBag(); - internal ConcurrentBag _remoteCandidates = new ConcurrentBag(); - - /// - /// A queue of remote ICE candidates that have been added to the session and that - /// are waiting to be processed to determine if they will create a new checklist entry. - /// - private ConcurrentQueue _pendingRemoteCandidates = new ConcurrentQueue(); - - /// - /// The state of the checklist as the ICE checks are carried out. - /// - internal ChecklistState _checklistState = ChecklistState.Running; - - /// - /// The checklist of local and remote candidate pairs - /// - internal List _checklist = new List(); - - /// - /// Lock to co-ordinate access to the _checklist. - /// - private readonly object _checklistLock = new object(); - - /// - /// For local candidates this implementation takes a shortcut to reduce complexity. - /// The RTP socket will always be bound to one of: - /// - IPAddress.IPv6Any [::], - /// - IPAddress.Any 0.0.0.0, or, - /// - a specific single IP address. - /// As such it's only necessary to create a single checklist entry to cover all local - /// Host type candidates. - /// Host candidates must still be generated, based on all local IP addresses, and - /// will need to be transmitted to the remote peer but they don't need to - /// be used when populating the checklist. - /// - internal readonly RTCIceCandidate _localChecklistCandidate; - - /// - /// If a TURN server is being used for this session and has received a successful - /// response to the allocate request then this field will hold the candidate to - /// use in the checklist. - /// - internal RTCIceCandidate _relayChecklistCandidate; - - /// - /// If the connectivity checks are successful this will hold the entry that was - /// nominated by the connection check process. - /// - public ChecklistEntry NominatedEntry { get; private set; } - - /// - /// Retransmission timer for STUN transactions, measured in milliseconds. - /// - /// - /// As specified in https://tools.ietf.org/html/rfc8445#section-14. - /// - internal int RTO - { - get - { - if (IceGatheringState == RTCIceGatheringState.gathering) - { - return Math.Max(500, Ta * Candidates.Count(x => x.type == RTCIceCandidateType.srflx || x.type == RTCIceCandidateType.relay)); - } - else + switch (iceServer.Protocol) { - lock (_checklistLock) - { - return Math.Max(500, Ta * (_checklist.Count(x => x.State == ChecklistEntryState.Waiting) + _checklist.Count(x => x.State == ChecklistEntryState.InProgress))); - } + case ProtocolType.Udp: + Debug.Assert(RtpConnection is not null); + iceServerConnections[uri] = RtpConnection; + break; + case ProtocolType.Tcp when iceServer.Uri.Scheme is STUNSchemesEnum.turn: + { + NetServices.CreateRtpSocket( + false, + ProtocolType.Tcp, + bindAddress, + bindPort, + rtpPortRange, + true, + true, + out var rtpTcpSocket, + out _); + + Debug.Assert(rtpTcpSocket is { }); + var iceServerConnection = new SocketTcpConnection(rtpTcpSocket); + iceServerConnection.OnPacketReceived += OnRTPPacketReceived; + iceServerConnection.OnClosed += reason => CloseIceServerTcpConnection(iceServerConnection, reason); + iceServerConnections[uri] = iceServerConnection; + break; + } + + case ProtocolType.Tcp when iceServer.Uri.Scheme is STUNSchemesEnum.turns: + { + NetServices.CreateRtpSocket( + false, + ProtocolType.Tcp, + bindAddress, + bindPort, + rtpPortRange, + true, + true, + out var rtpTcpSocket, + out _); + + Debug.Assert(rtpTcpSocket is { }); + var iceServerConnection = new SocketTlsConnection(rtpTcpSocket, iceServer.Uri.Host, iceServer.SslClientAuthenticationOptions); + iceServerConnection.OnPacketReceived += OnRTPPacketReceived; + iceServerConnection.OnClosed += reason => CloseIceServerTcpConnection(iceServerConnection, reason); + iceServerConnections[uri] = iceServerConnection; + break; + } } } - } - public readonly string LocalIceUser; - public readonly string LocalIcePassword; - public string RemoteIceUser { get; private set; } - public string RemoteIcePassword { get; private set; } - - private bool _closed = false; - private Timer _connectivityChecksTimer; - private Timer _processIceServersTimer; - private Timer _refreshTurnTimer; - private DateTime _checklistStartedAt = DateTime.MinValue; - private bool _includeAllInterfaceAddresses = false; - private ulong _iceTiebreaker; - - public event Action OnIceCandidate; - public event Action OnIceConnectionStateChange; - public event Action OnIceGatheringStateChange; - public event Action OnIceCandidateError; - - /// - /// This event gets fired when a STUN message is sent by this channel. - /// The event is for diagnostic purposes only. - /// Parameters: - /// - STUNMessage: The STUN message that was sent. - /// - IPEndPoint: The remote end point the STUN message was sent to. - /// - bool: True if the message was sent via a TURN server relay. - /// - public event Action OnStunMessageSent; - - /// - /// An optional callback function to resolve remote ICE candidates with MDNS hostnames. - /// - /// - /// The order is , then . - /// If both are null system DNS resolver will be used. - /// - public Func> MdnsResolve; - - /// - /// An optional callback function to resolve remote ICE candidates with MDNS hostnames. - /// - /// - /// The order is , then . - /// If both are null system DNS resolver will be used. - /// - public Func> MdnsGetAddresses; - - public Dictionary RtpTcpSocketByUri { get; private set; } = new Dictionary(); - - protected Dictionary m_rtpTcpReceiverByUri = new Dictionary(); - - private bool m_tcpRtpReceiverStarted = false; - - /// - /// Creates a new instance of an RTP ICE channel to provide RTP channel functions - /// with ICE connectivity checks. - /// - public RtpIceChannel() : - this(null, RTCIceComponent.rtp) - { } - - /// - /// Creates a new instance of an RTP ICE channel to provide RTP channel functions - /// with ICE connectivity checks. - /// - /// Optional. If this is not set then the default is to - /// bind to the IPv6 wildcard address in dual mode to the IPv4 wildcard address if - /// IPv6 is not available. - /// The component (RTP or RTCP) the channel is being used for. Note - /// for cases where RTP and RTCP are multiplexed the component is set to RTP. - /// A list of STUN or TURN servers that can be used by this ICE agent. - /// Determines which ICE candidates can be used in this RTP ICE Channel. - /// If set to true then IP addresses from ALL local - /// interfaces will be used for host ICE candidates. If left as the default false value host - /// candidates will be restricted to the single interface that the OS routing table matches to - /// the destination address or the Internet facing interface if the destination is not known. - /// The restrictive behaviour is as per the recommendation at: - /// https://tools.ietf.org/html/draft-ietf-rtcweb-ip-handling-12#section-5.2. - /// - public RtpIceChannel( - IPAddress bindAddress, - RTCIceComponent component, - List iceServers = null, - RTCIceTransportPolicy policy = RTCIceTransportPolicy.all, - bool includeAllInterfaceAddresses = false, - int bindPort = 0, - PortRange rtpPortRange = null) : - base(false, bindAddress, bindPort, rtpPortRange) - { - _bindAddress = bindAddress; - Component = component; - _iceServers = iceServers != null ? new List(iceServers) : null; - _policy = policy; - _includeAllInterfaceAddresses = includeAllInterfaceAddresses; - _iceTiebreaker = Crypto.GetRandomULong(); - - LocalIceUser = Crypto.GetRandomString(ICE_UFRAG_LENGTH); - LocalIcePassword = Crypto.GetRandomString(ICE_PASSWORD_LENGTH); - - base.OnStunMessageReceived += (stunMessage, remoteEndPoint, wasRelayed) => - { - _ = ProcessStunMessage(stunMessage, remoteEndPoint, wasRelayed); - }; + m_iceServerConnections = iceServerConnections.ToFrozenDictionary(); + } + else + { + m_iceServerConnections = FrozenDictionary.Empty; + } - _localChecklistCandidate = new RTCIceCandidate(new RTCIceCandidateInit - { - sdpMid = SDP_MID, - sdpMLineIndex = SDP_MLINE_INDEX, - usernameFragment = LocalIceUser - }); - - _localChecklistCandidate.SetAddressProperties( - RTCIceProtocol.udp, - base.RTPLocalEndPoint.Address, - (ushort)base.RTPLocalEndPoint.Port, - RTCIceCandidateType.host, - null, - 0); - - // For TURN servers reached over TCP/TLS (turn:...?transport=tcp or turns:), open a TCP socket. - // The whole client<->TURN-server leg rides this single connection: Allocate/Refresh, - // CreatePermission, the ICE connectivity-check Binding requests, AND the relayed media itself - // (carried as Send/Data indications or ChannelData) - it's not just signalling. - // The relay candidate produced is still a UDP candidate, because the allocation requests UDP - // transport (REQUESTED-TRANSPORT=UDP), so the TURN-server<->peer leg is always UDP regardless - // of how we reached the server. - // - // Classify TCP ICE servers by the parsed transport protocol, not by string-matching the raw - // URL. This correctly includes secure schemes that imply TCP without an explicit - // "?transport=" parameter (e.g. "turns:host:443", which defaults to TLS over TCP per - // RFC 7064/7065). String-matching for "transport=tcp"/"transport=tls" would miss those. - var tcpIceServers = _iceServers != null ? - _iceServers.FindAll(a => - a != null && - a.urls != null && - STUNUri.TryParse(a.urls, out var tcpUri) && - tcpUri.Protocol == ProtocolType.Tcp) : - new List(); - var supportTcp = tcpIceServers != null && tcpIceServers.Count > 0; - if (supportTcp) - { - // Init one TCP Socket per IceServer as we need to connect to properly use a TcpSocket (unfortunately). - RtpTcpSocketByUri = new Dictionary(); - foreach (var tcpIceServer in tcpIceServers) - { - var serverUrl = tcpIceServer.urls; - STUNUri.TryParse(serverUrl, out STUNUri uri); - if (uri != null && !RtpTcpSocketByUri.ContainsKey(uri)) - { + if (DefaultNameServers is { }) + { + _dnsLookupClient = new DnsClient.LookupClient(DefaultNameServers.ToArray()); + } + else + { + _dnsLookupClient = new DnsClient.LookupClient(); + } + } - if (uri != null) - { - NetServices.CreateRtpSocket(false, ProtocolType.Tcp, bindAddress, bindPort, rtpPortRange, true, true, out var rtpTcpSocket, out _); + /// + /// We've been given the green light to start the ICE candidate gathering process. This could include contacting + /// external STUN and TURN servers. Events will be fired as each ICE is identified and as the gathering state + /// machine changes state. + /// + public void StartGathering() + { + if (!_closed && IceGatheringState == RTCIceGatheringState.@new) + { + _startedGatheringAt = DateTime.Now; - if (rtpTcpSocket == null) - { - throw new ApplicationException("The RTP channel was not able to create an RTP socket."); - } + // Start listening on the UDP socket. + base.Start(); + StartTcpRtpReceiver(); + IceGatheringState = RTCIceGatheringState.gathering; + OnIceGatheringStateChange?.Invoke(IceGatheringState); - RtpTcpSocketByUri.Add(uri, rtpTcpSocket); - } - } + if (_policy == RTCIceTransportPolicy.all) + { + _candidates = new ConcurrentBag(); + foreach (var iceCandidate in GetHostCandidates()) + { + _candidates.Add(iceCandidate); } } - if (_iceServers != null && _iceServers.Count > 0) - { - _iceServerResolver.InitialiseIceServers(_iceServers, _policy); - } + logger.LogIceChannelLocalCandidates(_candidates.Count); - if (DefaultNameServers != null) + if (_iceServerResolver.IceServers is { Count: > 0 }) { - _dnsLookupClient = new DnsClient.LookupClient(DefaultNameServers.ToArray()); + _processIceServersTimer = new Timer(CheckIceServers, null, Timeout.Infinite, Timeout.Infinite); + _processIceServersTimer.Change(0, Ta); } else { - _dnsLookupClient = new DnsClient.LookupClient(); + // If there are no ICE servers then gathering has finished. + IceGatheringState = RTCIceGatheringState.complete; + OnIceGatheringStateChange?.Invoke(IceGatheringState); } + + _connectivityChecksTimer = new Timer(DoConnectivityCheck, null, Timeout.Infinite, Timeout.Infinite); + _connectivityChecksTimer.Change(0, Ta); } + } - /// - /// We've been given the green light to start the ICE candidate gathering process. - /// This could include contacting external STUN and TURN servers. Events will - /// be fired as each ICE is identified and as the gathering state machine changes - /// state. - /// - public void StartGathering() + protected void StartTcpRtpReceiver() + { + if (!m_tcpRtpReceiverStarted) { - if (!_closed && IceGatheringState == RTCIceGatheringState.@new) - { - _startedGatheringAt = DateTime.Now; + m_tcpRtpReceiverStarted = true; - // Start listening on the UDP socket. - base.Start(); - StartTcpRtpReceiver(); - - IceGatheringState = RTCIceGatheringState.gathering; - OnIceGatheringStateChange?.Invoke(IceGatheringState); + foreach (var (uri, iceServerConnection) in m_iceServerConnections) + { + //iceServerConnection.OnClosed += reason => CloseIceServerConnection(receiver, reason); + iceServerConnection.BeginReceiveFrom(); + } - if (_policy == RTCIceTransportPolicy.all) - { - _candidates = new ConcurrentBag(); - foreach (var iceCandidate in GetHostCandidates()) - { - _candidates.Add(iceCandidate); - } - } + Debug.Assert(RtpConnection is { }); - logger.LogDebug("RTP ICE Channel discovered {CandidateCount} local candidates.", _candidates.Count); + logger.LogTcpStarted(RtpConnection.Socket.LocalEndPoint); - if (_iceServerResolver.IceServers?.Count > 0) - { - _processIceServersTimer = new Timer(CheckIceServers); - _processIceServersTimer.Change(0, Ta); - } - else - { - // If there are no ICE servers then gathering has finished. - IceGatheringState = RTCIceGatheringState.complete; - OnIceGatheringStateChange?.Invoke(IceGatheringState); - } + OnClosed -= CloseIceServerTcpConnections; + OnClosed += CloseIceServerTcpConnections; + } + } - _connectivityChecksTimer = new Timer(DoConnectivityCheck); - _connectivityChecksTimer.Change(0, Ta); + protected void CloseIceServerTcpConnections(string reason) + { + foreach (var (uri, iceServerConnection) in m_iceServerConnections) + { + if (iceServerConnection is SocketTcpConnection iceServerTcpConnection) + { + CloseIceServerTcpConnection(iceServerTcpConnection, reason); } } + } - protected void StartTcpRtpReceiver() + protected internal void CloseIceServerTcpConnection(SocketTcpConnection target, string? reason) + { + try { - if (!m_tcpRtpReceiverStarted && RtpTcpSocketByUri != null && RtpTcpSocketByUri.Count > 0) - { - m_tcpRtpReceiverStarted = true; + target.Close(reason); + } + catch (Exception excp) + { + logger.LogTcpError(excp.Message, excp); + } + } - // Create TCP Receivers by Tcp Sockets - m_rtpTcpReceiverByUri = new Dictionary(); - foreach (var pair in RtpTcpSocketByUri) - { - var stunUri = pair.Key; - var tcpSocket = pair.Value; + /// + /// Set the ICE credentials that have been supplied by the remote peer. Once these are set the connectivity checks + /// should be able to commence. + /// + /// The remote peer's ICE username. + /// The remote peer's ICE password. + public void SetRemoteCredentials(string username, string password) + { + logger.LogRemoteCredentialsSet(); - // TURNS/STUNS use an SslStream owned by SendOverTCP/StartTlsReadLoop, so the - // plain TCP receiver loop must not also try to read from the raw socket. - if (stunUri != null && (stunUri.Scheme == STUNSchemesEnum.turns || stunUri.Scheme == STUNSchemesEnum.stuns)) - { - continue; - } + RemoteIceUser = username; + RemoteIcePassword = password; - if (stunUri != null && !m_rtpTcpReceiverByUri.ContainsKey(stunUri) && tcpSocket != null) - { - var rtpTcpReceiver = new IceTcpReceiver(tcpSocket); + if (IceConnectionState == RTCIceConnectionState.@new) + { + // A potential race condition exists here. The remote peer can send a binding request that + // results in the ICE channel connecting BEFORE the remote credentials get set. Since the goal + // is to connect ICE as quickly as possible it does not seem sensible to force a wait for the + // remote credentials to be set. The credentials will still be used on STUN binding requests + // sent on the connected ICE channel. In the case of WebRTC transport confidentiality is still + // preserved since the DTLS negotiation will sill need to check the certificate fingerprint in + // supplied by the remote offer. + + _checklistStartedAt = DateTime.Now; + + // Once the remote party's ICE credentials are known connection checking can + // commence immediately as candidates trickle in. + IceConnectionState = RTCIceConnectionState.checking; + OnIceConnectionStateChange?.Invoke(IceConnectionState); + } + } - Action onClose = (reason) => - { - CloseTcp(rtpTcpReceiver, reason); - }; - rtpTcpReceiver.OnPacketReceived += OnRTPPacketReceived; - rtpTcpReceiver.OnClosed += onClose; - rtpTcpReceiver.BeginReceiveFrom(); + /// + /// Closes the RTP ICE Channel and stops any further connectivity checks. + /// + public void Close() + { + if (!_closed) + { + logger.LogIceClosed(base.RTPLocalEndPoint); + _closed = true; + _connectivityChecksTimer?.Dispose(); + _processIceServersTimer?.Dispose(); + _refreshTurnTimer?.Dispose(); + } + } - m_rtpTcpReceiverByUri.Add(stunUri, rtpTcpReceiver); - } - } + /// + /// Adds a remote ICE candidate to the RTP ICE Channel. + /// + /// An ICE candidate from the remote party. + public void AddRemoteCandidate(RTCIceCandidate candidate) + { + if (candidate is null || string.IsNullOrWhiteSpace(candidate.address)) + { + // Note that the way ICE signals the end of the gathering stage is to send + // an empty candidate or "end-of-candidates" SDP attribute. + OnIceCandidateError?.Invoke(candidate, "Remote ICE candidate was empty."); + } + else if (candidate.component != Component) + { + // This occurs if the remote party made an offer and assumed we couldn't multiplex the audio and video streams. + // It will offer the same ICE candidates separately for the audio and video announcements. + OnIceCandidateError?.Invoke(candidate, "Remote ICE candidate has unsupported component."); + } + else if (candidate.sdpMLineIndex != 0) + { + // This implementation currently only supports audio and video multiplexed on a single channel. + OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate only supports multiplexed media, excluding remote candidate with non-zero sdpMLineIndex of {candidate.sdpMLineIndex}."); + } + else if (candidate.protocol != RTCIceProtocol.udp) + { + // This implementation currently only supports UDP for RTP communications. + OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate has an unsupported transport protocol {candidate.protocol}."); + } + else if (IPAddress.TryParse(candidate.address, out var addr) && + (IPAddress.Any.Equals(addr) || IPAddress.IPv6Any.Equals(addr))) + { + OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate had a wildcard IP address {candidate.address}."); + } + else if (candidate.port is <= 0 or > IPEndPoint.MaxPort) + { + OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate had an invalid port {candidate.port}."); + } + else if (IPAddress.TryParse(candidate.address, out var addrIPv6) && + addrIPv6.AddressFamily == AddressFamily.InterNetworkV6 && + !Socket.OSSupportsIPv6 && + NetServices.HasActiveIPv6Address()) + { + OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate was for IPv6 but OS does not support {candidate.address}."); + } + else + { + // Have a remote candidate. Connectivity checks can start. Note because we support ICE trickle + // we may also still be gathering candidates. Connectivity checks and gathering can be done in parallel. - logger.LogDebug("RTPIceChannel TCP for {LocalEndPoint} started.", RtpSocket.LocalEndPoint); + logger.LogRemoteCandidate(candidate); - OnClosed -= CloseTcp; - OnClosed += CloseTcp; - } + _remoteCandidates.Add(candidate); + _pendingRemoteCandidates.Enqueue(candidate); } + } - protected void CloseTcp(string reason) + /// + /// Restarts the ICE gathering and connection checks for this RTP ICE Channel. + /// + public void Restart() + { + // Reset the session state. + _connectivityChecksTimer?.Dispose(); + _processIceServersTimer?.Dispose(); + _refreshTurnTimer?.Dispose(); + _candidates = new ConcurrentBag(); + lock (_checklistLock) { - if (m_rtpTcpReceiverByUri != null) - { - foreach (var pair in m_rtpTcpReceiverByUri) - { - CloseTcp(pair.Value, reason); - } - } + _checklist?.Clear(); } + _iceServerResolver.InitialiseIceServers(_iceServers, _policy); + IceGatheringState = RTCIceGatheringState.@new; + IceConnectionState = RTCIceConnectionState.@new; + + StartGathering(); + } + + /// + /// Acquires an ICE candidate for each IP address that this host has except for: - Loopback addresses must not be + /// included. - Deprecated IPv4-compatible IPv6 addresses and IPv6 site-local unicast addresses must not be + /// included, - IPv4-mapped IPv6 address should not be included. - If a non-location tracking IPv6 address is + /// available use it and do not included location tracking enabled IPv6 addresses (i.e. prefer temporary IPv6 + /// addresses over permanent addresses), see RFC6724. SECURITY NOTE: + /// https://tools.ietf.org/html/draft-ietf-rtcweb-ip-handling-12#section-5.2 Makes recommendations about how host IP + /// address information should be exposed. Of particular relevance are: Mode 1: Enumerate all addresses: WebRTC MUST + /// use all network interfaces to attempt communication with STUN servers, TURN servers, or peers.This will converge + /// on the best media path, and is ideal when media performance is the highest priority, but it discloses the most + /// information. Mode 2: Default route + associated local addresses: WebRTC MUST follow the kernel routing table + /// rules, which will typically cause media packets to take the same route as the application's HTTP traffic. If an + /// enterprise TURN server is present, the preferred route MUST be through this TURN server.Once an interface has + /// been chosen, the private IPv4 and IPv6 addresses associated with this interface MUST be discovered and provided + /// to the application as host candidates.This ensures that direct connections can still be established in this + /// mode. This implementation implements Mode 2. + /// + /// + /// See https://tools.ietf.org/html/rfc8445#section-5.1.1.1 See https://tools.ietf.org/html/rfc6874 for a + /// recommendation on how scope or zone ID's should be represented as strings in IPv6 link local addresses. Due to + /// parsing issues in at least two other WebRTC stacks (as of Feb 2021) any zone ID is removed from an ICE candidate + /// string. + /// + /// A list of "host" ICE candidates for the local machine. + private List GetHostCandidates() + { + var hostCandidates = new List(); + var init = new RTCIceCandidateInit { usernameFragment = LocalIceUser }; + + // RFC8445 states that loopback addresses should not be included in + // host candidates. If the provided bind address is a loopback + // address it means no host candidates will be gathered. To avoid this + // set the desired interface address to the Internet facing address + // in the event a loopback address was specified. - protected void CloseTcp(IceTcpReceiver target, string reason) + var rtpBindAddress = base.RTPLocalEndPoint.Address; + + Debug.Assert(RtpConnection is { }); + + // We get a list of local addresses that can be used with the address the RTP socket is bound on. + IEnumerable localAddresses; + if (IPAddress.IPv6Any.Equals(rtpBindAddress)) { - try + if (RtpConnection.Socket.DualMode) { - if (target != null && !target.IsClosed) + // IPv6 dual mode listening on [::] means we can use all valid local addresses. + var list = new List(); + foreach (var x in NetServices.GetLocalAddressesOnInterface(_bindAddress, _includeAllInterfaceAddresses)) { - target?.Close(null); + if (!IPAddress.IsLoopback(x) && !x.IsIPv4MappedToIPv6 && !x.IsIPv6SiteLocal && !x.IsIPv6LinkLocal) + { + list.Add(x); + } } + localAddresses = list; } - catch (Exception excp) + else { - logger.LogError(excp, "Exception RTPChannel.Close. {ErrorMessage}", excp.Message); + // IPv6 but not dual mode on [::] means can use all valid local IPv6 addresses. + var list = new List(); + foreach (var x in NetServices.GetLocalAddressesOnInterface(_bindAddress, _includeAllInterfaceAddresses)) + { + if (x.AddressFamily == AddressFamily.InterNetworkV6 && !IPAddress.IsLoopback(x) && !x.IsIPv4MappedToIPv6 && !x.IsIPv6SiteLocal && !x.IsIPv6LinkLocal) + { + list.Add(x); + } + } + localAddresses = list; } } - - /// - /// Set the ICE credentials that have been supplied by the remote peer. Once these - /// are set the connectivity checks should be able to commence. - /// - /// The remote peer's ICE username. - /// The remote peer's ICE password. - public void SetRemoteCredentials(string username, string password) + else if (IPAddress.Any.Equals(rtpBindAddress)) { - logger.LogDebug("RTP ICE Channel remote credentials set."); - - RemoteIceUser = username; - RemoteIcePassword = password; - - if (IceConnectionState == RTCIceConnectionState.@new) + // IPv4 on 0.0.0.0 means can use all valid local IPv4 addresses. + var list = new List(); + foreach (var x in NetServices.GetLocalAddressesOnInterface(_bindAddress, _includeAllInterfaceAddresses)) { - // A potential race condition exists here. The remote peer can send a binding request that - // results in the ICE channel connecting BEFORE the remote credentials get set. Since the goal - // is to connect ICE as quickly as possible it does not seem sensible to force a wait for the - // remote credentials to be set. The credentials will still be used on STUN binding requests - // sent on the connected ICE channel. In the case of WebRTC transport confidentiality is still - // preserved since the DTLS negotiation will sill need to check the certificate fingerprint in - // supplied by the remote offer. - - _checklistStartedAt = DateTime.Now; - - // Once the remote party's ICE credentials are known connection checking can - // commence immediately as candidates trickle in. - IceConnectionState = RTCIceConnectionState.checking; - OnIceConnectionStateChange?.Invoke(IceConnectionState); + if (x.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(x)) + { + list.Add(x); + } } + localAddresses = list; + } + else + { + // If not bound on a [::] or 0.0.0.0 means we're only listening on a specific IP address + // and that's the only one that can be used for the host candidate. + localAddresses = new IPAddress[] { rtpBindAddress }; } - /// - /// Closes the RTP ICE Channel and stops any further connectivity checks. - /// - public void Close() + foreach (var localAddress in localAddresses) { - if (!_closed) + var hostCandidate = new RTCIceCandidate(init); + hostCandidate.SetAddressProperties(RTCIceProtocol.udp, localAddress, (ushort)base.RTPPort, RTCIceCandidateType.host, null, 0); + + // We currently only support a single multiplexed connection for all data streams and RTCP. + if (hostCandidate.component == RTCIceComponent.rtp && hostCandidate.sdpMLineIndex == SDP_MLINE_INDEX) { - logger.LogDebug("RtpIceChannel for {RTPLocalEndPoint} closed.", base.RTPLocalEndPoint); - _closed = true; - _connectivityChecksTimer?.Dispose(); - _processIceServersTimer?.Dispose(); - _refreshTurnTimer?.Dispose(); + hostCandidates.Add(hostCandidate); + + OnIceCandidate?.Invoke(hostCandidate); } } - /// - /// Adds a remote ICE candidate to the RTP ICE Channel. - /// - /// An ICE candidate from the remote party. - public void AddRemoteCandidate(RTCIceCandidate candidate) + return hostCandidates; + } + + private void RefreshTurn(object? state) + { + try { - if (candidate == null || string.IsNullOrWhiteSpace(candidate.address)) - { - // Note that the way ICE signals the end of the gathering stage is to send - // an empty candidate or "end-of-candidates" SDP attribute. - OnIceCandidateError?.Invoke(candidate, "Remote ICE candidate was empty."); - } - else if (candidate.component != Component) + if (_closed) { - // This occurs if the remote party made an offer and assumed we couldn't multiplex the audio and video streams. - // It will offer the same ICE candidates separately for the audio and video announcements. - OnIceCandidateError?.Invoke(candidate, "Remote ICE candidate has unsupported component."); + return; } - else if (candidate.sdpMLineIndex != 0) + + if (NominatedEntry is null || _activeIceServer is null) { - // This implementation currently only supports audio and video multiplexed on a single channel. - OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate only supports multiplexed media, excluding remote candidate with non-zero sdpMLineIndex of {candidate.sdpMLineIndex}."); + return; } - else if (candidate.protocol != RTCIceProtocol.udp) + if (_activeIceServer.Uri.Scheme != STUNSchemesEnum.turn || NominatedEntry.LocalCandidate.IceServer is null) { - // This implementation currently only supports UDP for RTP communications. - OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate has an unsupported transport protocol {candidate.protocol}."); + _refreshTurnTimer?.Dispose(); + _refreshTurnTimer = new Timer(RefreshTurn, null, Timeout.Infinite, Timeout.Infinite); + _refreshTurnTimer.Change(0, 2000); + return; } - else if (IPAddress.TryParse(candidate.address, out var addr) && - (IPAddress.Any.Equals(addr) || IPAddress.IPv6Any.Equals(addr))) + if (_activeIceServer.TurnTimeToExpiry.Subtract(DateTime.Now) <= TimeSpan.FromMinutes(1)) { - OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate had a wildcard IP address {candidate.address}."); + logger.LogIceTurnRefreshRequest(_activeIceServer.Uri); + _activeIceServer.Error = SendTurnRefreshRequest(_activeIceServer); } - else if (candidate.port <= 0 || candidate.port > IPEndPoint.MaxPort) + + if (NominatedEntry.TurnPermissionsRequestSent >= IceServer.MAX_REQUESTS) { - OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate had an invalid port {candidate.port}."); + logger.LogIceTurnPermissionsFailed(NominatedEntry.LocalCandidate.IceServer.Uri, NominatedEntry.TurnPermissionsRequestSent); } - else if(IPAddress.TryParse(candidate.address, out var addrIPv6) && - addrIPv6.AddressFamily == AddressFamily.InterNetworkV6 && - !Socket.OSSupportsIPv6 && - NetServices.HasActiveIPv6Address()) + else if (NominatedEntry.TurnPermissionsRequestSent != 1 || NominatedEntry.TurnPermissionsResponseAt == DateTime.MinValue || DateTime.Now.Subtract(NominatedEntry.TurnPermissionsResponseAt).TotalSeconds > + REFRESH_PERMISSION_PERIOD) { - OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate was for IPv6 but OS does not support {candidate.address}."); + // Send Create Permissions request to TURN server for remote candidate. + var turnPermissionsRequestSent = ++NominatedEntry.TurnPermissionsRequestSent; + var iceServer = NominatedEntry.LocalCandidate.IceServer; + var destinationEndPoint = NominatedEntry.RemoteCandidate.DestinationEndPoint; + var requestTransactionID = NominatedEntry.RequestTransactionID; + Debug.Assert(iceServer is { }); + Debug.Assert(destinationEndPoint is { }); + Debug.Assert(requestTransactionID is { }); + logger.LogIceTurnPermissionsRequest(turnPermissionsRequestSent, iceServer.Uri, destinationEndPoint, requestTransactionID); + SendTurnCreatePermissionsRequest(requestTransactionID, iceServer, destinationEndPoint); } - else - { - // Have a remote candidate. Connectivity checks can start. Note because we support ICE trickle - // we may also still be gathering candidates. Connectivity checks and gathering can be done in parallel. + } + catch (Exception excp) + { + logger.LogRefreshError(excp.Message, excp); + } + } - logger.LogDebug("RTP ICE Channel received remote candidate: {candidate}", candidate); + /// + /// Checks the list of ICE servers to perform STUN binding or TURN reservation requests. Only one of the ICE server + /// entries should end up being used. If at least one TURN server is provided it will take precedence as it can + /// potentially supply both Server Reflexive and Relay candidates. + /// + private void CheckIceServers(object? state) + { + if (_closed || IceGatheringState == RTCIceGatheringState.complete || + !(IceConnectionState == RTCIceConnectionState.@new || IceConnectionState == RTCIceConnectionState.checking)) + { + logger.LogIceStopsProcessing(IceGatheringState, IceConnectionState); + _refreshTurnTimer?.Dispose(); + _refreshTurnTimer = new Timer(RefreshTurn, null, Timeout.Infinite, Timeout.Infinite); + _refreshTurnTimer.Change(0, 2000); - _remoteCandidates.Add(candidate); - _pendingRemoteCandidates.Enqueue(candidate); - } + Debug.Assert(_processIceServersTimer is { }); + _processIceServersTimer.Dispose(); + return; } - /// - /// Restarts the ICE gathering and connection checks for this RTP ICE Channel. - /// - public void Restart() + // The lock is to ensure the timer callback doesn't run multiple instances in parallel. + if (Monitor.TryEnter(_iceServerResolver)) { - // Reset the session state. - _connectivityChecksTimer?.Dispose(); - _processIceServersTimer?.Dispose(); - _refreshTurnTimer?.Dispose(); - _candidates = new ConcurrentBag(); - lock (_checklistLock) + try { - _checklist?.Clear(); - } - _iceServerResolver.InitialiseIceServers(_iceServers, _policy); - IceGatheringState = RTCIceGatheringState.@new; - IceConnectionState = RTCIceConnectionState.@new; - - StartGathering(); - } - - /// - /// Acquires an ICE candidate for each IP address that this host has except for: - /// - Loopback addresses must not be included. - /// - Deprecated IPv4-compatible IPv6 addresses and IPv6 site-local unicast addresses - /// must not be included, - /// - IPv4-mapped IPv6 address should not be included. - /// - If a non-location tracking IPv6 address is available use it and do not included - /// location tracking enabled IPv6 addresses (i.e. prefer temporary IPv6 addresses over - /// permanent addresses), see RFC6724. - /// - /// SECURITY NOTE: https://tools.ietf.org/html/draft-ietf-rtcweb-ip-handling-12#section-5.2 - /// Makes recommendations about how host IP address information should be exposed. - /// Of particular relevance are: - /// - /// Mode 1: Enumerate all addresses: WebRTC MUST use all network - /// interfaces to attempt communication with STUN servers, TURN - /// servers, or peers.This will converge on the best media - /// path, and is ideal when media performance is the highest - /// priority, but it discloses the most information. - /// - /// Mode 2: Default route + associated local addresses: WebRTC MUST - /// follow the kernel routing table rules, which will typically - /// cause media packets to take the same route as the - /// application's HTTP traffic. If an enterprise TURN server is - /// present, the preferred route MUST be through this TURN - /// server.Once an interface has been chosen, the private IPv4 - /// and IPv6 addresses associated with this interface MUST be - /// discovered and provided to the application as host - /// candidates.This ensures that direct connections can still - /// be established in this mode. - /// - /// This implementation implements Mode 2. - /// - /// - /// See https://tools.ietf.org/html/rfc8445#section-5.1.1.1 - /// See https://tools.ietf.org/html/rfc6874 for a recommendation on how scope or zone ID's - /// should be represented as strings in IPv6 link local addresses. Due to parsing - /// issues in at least two other WebRTC stacks (as of Feb 2021) any zone ID is removed - /// from an ICE candidate string. - /// - /// A list of "host" ICE candidates for the local machine. - private List GetHostCandidates() - { - List hostCandidates = new List(); - RTCIceCandidateInit init = new RTCIceCandidateInit { usernameFragment = LocalIceUser }; - - // RFC8445 states that loopback addresses should not be included in - // host candidates. If the provided bind address is a loopback - // address it means no host candidates will be gathered. To avoid this - // set the desired interface address to the Internet facing address - // in the event a loopback address was specified. - //if (_bindAddress != null && - // (IPAddress.IsLoopback(_bindAddress) || - // IPAddress.Any.Equals(_bindAddress) || - // IPAddress.IPv6Any.Equals(_bindAddress))) - //{ - // // By setting to null means the default Internet facing interface will be used. - // signallingDstAddress = null; - //} - - var rtpBindAddress = base.RTPLocalEndPoint.Address; - - // We get a list of local addresses that can be used with the address the RTP socket is bound on. - List localAddresses = null; - if (IPAddress.IPv6Any.Equals(rtpBindAddress)) - { - if (base.RtpSocket.DualMode) + if (_activeIceServer is null || _activeIceServer.Error != SocketError.Success) { - // IPv6 dual mode listening on [::] means we can use all valid local addresses. - localAddresses = NetServices.GetLocalAddressesOnInterface(_bindAddress, _includeAllInterfaceAddresses) - .Where(x => !IPAddress.IsLoopback(x) && !x.IsIPv4MappedToIPv6 && !x.IsIPv6SiteLocal && !x.IsIPv6LinkLocal).ToList(); - } - else - { - // IPv6 but not dual mode on [::] means can use all valid local IPv6 addresses. - localAddresses = NetServices.GetLocalAddressesOnInterface(_bindAddress, _includeAllInterfaceAddresses) - .Where(x => x.AddressFamily == AddressFamily.InterNetworkV6 - && !IPAddress.IsLoopback(x) && !x.IsIPv4MappedToIPv6 && !x.IsIPv6SiteLocal && !x.IsIPv6LinkLocal).ToList(); - } - } - else if (IPAddress.Any.Equals(rtpBindAddress)) - { - // IPv4 on 0.0.0.0 means can use all valid local IPv4 addresses. - localAddresses = NetServices.GetLocalAddressesOnInterface(_bindAddress, _includeAllInterfaceAddresses) - .Where(x => x.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(x)).ToList(); - } - else - { - // If not bound on a [::] or 0.0.0.0 means we're only listening on a specific IP address - // and that's the only one that can be used for the host candidate. - localAddresses = new List { rtpBindAddress }; - } + // Select the next server to check. - foreach (var localAddress in localAddresses) - { - var hostCandidate = new RTCIceCandidate(init); - hostCandidate.SetAddressProperties(RTCIceProtocol.udp, localAddress, (ushort)base.RTPPort, RTCIceCandidateType.host, null, 0); + foreach (var (uri, iceServer) in _iceServerResolver.IceServers) + { + if (iceServer.Error != SocketError.Success) + { + continue; + } + + var iceServerScheme = iceServer.Uri.Scheme; + + if (iceServerScheme == STUNSchemesEnum.turns) + { + _activeIceServer = iceServer; + break; + } + + if (iceServerScheme == STUNSchemesEnum.turn || _activeIceServer is null) + { + _activeIceServer = iceServer; + } + } - // We currently only support a single multiplexed connection for all data streams and RTCP. - if (hostCandidate.component == RTCIceComponent.rtp && hostCandidate.sdpMLineIndex == SDP_MLINE_INDEX) - { - hostCandidates.Add(hostCandidate); + if (_activeIceServer is null) + { + // no server found. - OnIceCandidate?.Invoke(hostCandidate); + logger.LogIceServersChecksFailed(); + Debug.Assert(_processIceServersTimer is { }); + _processIceServersTimer.Dispose(); + } } - } - return hostCandidates; - } + // Run a state machine on the active ICE server. - private void RefreshTurn(Object state) - { - try - { - if (_closed) + // Something went wrong. An active server could not be set. + if (_activeIceServer is null) { + logger.LogIceServerNotAcquired(); + Debug.Assert(_processIceServersTimer is { }); + _processIceServersTimer.Dispose(); return; } - if (NominatedEntry == null || _activeIceServer == null) + var activeIceServer = _activeIceServer; + var activeIceServerUri = activeIceServer.Uri; + + if (activeIceServerUri.Scheme is STUNSchemesEnum.turn or STUNSchemesEnum.turns && activeIceServer.RelayEndPoint is { } || + activeIceServerUri.Scheme is STUNSchemesEnum.stun && activeIceServer.ServerReflexiveEndPoint is { }) { - return; + // Successfully set up the ICE server. Do nothing. } - if ((_activeIceServer._uri.Scheme != STUNSchemesEnum.turn && _activeIceServer._uri.Scheme != STUNSchemesEnum.turns) || NominatedEntry.LocalCandidate.IceServer is null) + // If the ICE server hasn't yet been resolved skip and retyr again when teh next ICE server checnk runs. + if (activeIceServer.ServerEndPoint is null && + DateTime.Now.Subtract(activeIceServer.DnsLookupSentAt).TotalSeconds < IceServer.DNS_LOOKUP_TIMEOUT_SECONDS) { - _refreshTurnTimer?.Dispose(); - return; + // Do nothing. } - if (_activeIceServer.TurnTimeToExpiry.Subtract(DateTime.Now) <= TimeSpan.FromMinutes(1)) + // DNS lookup for ICE server host has timed out. + else if (activeIceServer.ServerEndPoint is null) { - logger.LogDebug("Sending TURN refresh request to ICE server {Uri}.", _activeIceServer._uri); - _activeIceServer.Error = SendTurnRefreshRequest(_activeIceServer); + logger.LogIceServerDnsResolutionFailed(activeIceServerUri); + activeIceServer.Error = SocketError.TimedOut; } - - if (NominatedEntry.TurnPermissionsRequestSent >= IceServer.MAX_REQUESTS) + // Maximum number of requests have been sent to the ICE server without a response. + else if (activeIceServer.OutstandingRequestsSent >= IceServer.MAX_REQUESTS && activeIceServer.LastResponseReceivedAt == DateTime.MinValue) + { + logger.LogIceServerConnectionTimeout(activeIceServerUri, activeIceServer.OutstandingRequestsSent); + activeIceServer.Error = SocketError.TimedOut; + } + // Maximum number of error response have been received for the requests sent to this ICE server. + else if (activeIceServer.ErrorResponseCount >= IceServer.MAX_ERRORS) + { + logger.LogIceServerErrorResponses(activeIceServerUri, activeIceServer.ErrorResponseCount); + activeIceServer.Error = SocketError.TimedOut; + } + // Send STUN binding request. + else if (activeIceServer.ServerReflexiveEndPoint is null && activeIceServerUri.Scheme is STUNSchemesEnum.stun) + { + activeIceServer.Error = SendStunBindingRequest(_activeIceServer); + } + // Send TURN binding request. + else if (activeIceServer.ServerReflexiveEndPoint is null && activeIceServerUri.Scheme is STUNSchemesEnum.turn or STUNSchemesEnum.turns) { - logger.LogWarning("ICE RTP channel failed to get a Create Permissions response from {IceServerUri} after {TurnPermissionsRequestSent} attempts.", NominatedEntry.LocalCandidate.IceServer._uri, NominatedEntry.TurnPermissionsRequestSent); + activeIceServer.Error = SendTurnAllocateRequest(_activeIceServer); } - else if (NominatedEntry.TurnPermissionsRequestSent != 1 || NominatedEntry.TurnPermissionsResponseAt == DateTime.MinValue || DateTime.Now.Subtract(NominatedEntry.TurnPermissionsResponseAt).TotalSeconds > - REFRESH_PERMISSION_PERIOD) + else { - // Send Create Permissions request to TURN server for remote candidate. - NominatedEntry.TurnPermissionsRequestSent++; - logger.LogDebug("ICE RTP channel sending TURN permissions request {TurnPermissionsRequestSent} to server {IceServerUri} for peer {RemoteCandidate} (TxID: {RequestTransactionID}).", - NominatedEntry.TurnPermissionsRequestSent, NominatedEntry.LocalCandidate.IceServer._uri, NominatedEntry.RemoteCandidate.DestinationEndPoint, NominatedEntry.RequestTransactionID); - SendTurnCreatePermissionsRequest(NominatedEntry.RequestTransactionID, NominatedEntry.LocalCandidate.IceServer, NominatedEntry.RemoteCandidate.DestinationEndPoint); + logger.LogIceUnexpectedState(activeIceServerUri); } } - catch (Exception excp) + finally { - logger.LogError(excp, "Exception in {Method}.", nameof(RefreshTurn)); + Monitor.Exit(_iceServerResolver); } } + } - /// - /// Checks the list of ICE servers to perform STUN binding or TURN reservation requests. - /// Only one of the ICE server entries should end up being used. If at least one TURN server - /// is provided it will take precedence as it can potentially supply both Server Reflexive - /// and Relay candidates. - /// - private void CheckIceServers(Object state) + /// + /// Adds candidates and updates the checklist for an ICE server that has completed the initial connectivity checks. + /// + /// + /// The ICE server that the initial checks have been completed for. + /// + private async Task AddCandidatesForIceServer(IceServer iceServer) + { + var init = new RTCIceCandidateInit { - if (_closed || IceGatheringState == RTCIceGatheringState.complete || - !(IceConnectionState == RTCIceConnectionState.@new || IceConnectionState == RTCIceConnectionState.checking)) - { - logger.LogDebug("ICE RTP channel stopping ICE server checks in gathering state {IceGatheringState} and connection state {IceConnectionState}.", IceGatheringState, IceConnectionState); - _refreshTurnTimer?.Dispose(); - _refreshTurnTimer = new Timer(RefreshTurn); - _refreshTurnTimer.Change(0, 2000); - _processIceServersTimer.Dispose(); - return; - } - - // The lock is to ensure the timer callback doesn't run multiple instances in parallel. - if (Monitor.TryEnter(_iceServerResolver)) - { - try - { - if (_activeIceServer == null || _activeIceServer.Error != SocketError.Success) - { - if (_iceServerResolver.IceServers.Count(x => x.Value.Error == SocketError.Success) == 0) - { - logger.LogDebug("RTP ICE Channel all ICE server connection checks failed, stopping ICE servers timer."); - _processIceServersTimer.Dispose(); - } - else - { - // Select the next server to check. - var entry = _iceServerResolver.IceServers - .Where(x => x.Value.Error == SocketError.Success) - .OrderByDescending(x => x.Value._uri.Scheme) // TURN serves take priority. - .FirstOrDefault(); + usernameFragment = LocalIceUser, + sdpMid = SDP_MID, + sdpMLineIndex = SDP_MLINE_INDEX, + }; - if (!entry.Equals(default(KeyValuePair))) - { - _activeIceServer = entry.Value; - } - else - { - logger.LogDebug("RTP ICE Channel was not able to set an active ICE server, stopping ICE servers timer."); - _processIceServersTimer.Dispose(); - } - } - } + if (iceServer.ServerReflexiveEndPoint is { }) + { + var svrRflxCandidate = iceServer.GetCandidate(init, RTCIceCandidateType.srflx); - // Run a state machine on the active ICE server. + if (_policy == RTCIceTransportPolicy.all && svrRflxCandidate is { }) + { + logger.LogIcePeerReflexAddingCandidate(iceServer.Uri, iceServer.ServerReflexiveEndPoint); - // Something went wrong. An active server could not be set. - if (_activeIceServer == null) - { - logger.LogDebug("RTP ICE Channel was not able to acquire an active ICE server, stopping ICE servers timer."); - _processIceServersTimer.Dispose(); - } - else if (((_activeIceServer._uri.Scheme == STUNSchemesEnum.turn || _activeIceServer._uri.Scheme == STUNSchemesEnum.turns) && _activeIceServer.RelayEndPoint != null) || - ((_activeIceServer._uri.Scheme == STUNSchemesEnum.stun || _activeIceServer._uri.Scheme == STUNSchemesEnum.stuns) && _activeIceServer.ServerReflexiveEndPoint != null)) - { - // Successfully set up the ICE server. Do nothing. - } - // If the ICE server hasn't yet been resolved skip and retry again when the next ICE server check runs. - if (_activeIceServer.ServerEndPoint == null && - DateTime.Now.Subtract(_activeIceServer.DnsLookupSentAt).TotalSeconds < IceServer.DNS_LOOKUP_TIMEOUT_SECONDS) - { - // Do nothing. - } - // DNS lookup for ICE server host has timed out. - else if (_activeIceServer.ServerEndPoint == null) - { - logger.LogWarning("ICE server DNS resolution failed for {Uri}.", _activeIceServer._uri); - _activeIceServer.Error = SocketError.TimedOut; - } - // Maximum number of requests have been sent to the ICE server without a response. - else if (_activeIceServer.OutstandingRequestsSent >= IceServer.MAX_REQUESTS && _activeIceServer.LastResponseReceivedAt == DateTime.MinValue) - { - logger.LogWarning("Connection attempt to ICE server {Uri} timed out after {RequestsSent} requests.", _activeIceServer._uri, _activeIceServer.OutstandingRequestsSent); - _activeIceServer.Error = SocketError.TimedOut; - } - // Maximum number of error response have been received for the requests sent to this ICE server. - else if (_activeIceServer.ErrorResponseCount >= IceServer.MAX_ERRORS) - { - logger.LogWarning("Connection attempt to ICE server {Uri} cancelled after {ErrorResponseCount} error responses.", _activeIceServer._uri, _activeIceServer.ErrorResponseCount); - _activeIceServer.Error = SocketError.TimedOut; - } - // Send STUN binding request. - else if (_activeIceServer.ServerReflexiveEndPoint == null && (_activeIceServer._uri.Scheme == STUNSchemesEnum.stun || _activeIceServer._uri.Scheme == STUNSchemesEnum.stuns)) - { - logger.LogDebug("Sending STUN binding request to ICE server {Uri} with address {EndPoint}.", _activeIceServer._uri, _activeIceServer.ServerEndPoint); - _activeIceServer.Error = SendStunBindingRequest(_activeIceServer); - } - // Send TURN binding request. - else if (_activeIceServer.ServerReflexiveEndPoint == null && (_activeIceServer._uri.Scheme == STUNSchemesEnum.turn || _activeIceServer._uri.Scheme == STUNSchemesEnum.turns)) - { - logger.LogDebug("Sending TURN allocate request to ICE server {Uri} with address {EndPoint}.", _activeIceServer._uri, _activeIceServer.ServerEndPoint); - _activeIceServer.Error = SendTurnAllocateRequest(_activeIceServer); - } - else - { - logger.LogWarning("The active ICE server reached an unexpected state {Uri}.", _activeIceServer._uri); - } - } - finally - { - Monitor.Exit(_iceServerResolver); - } + // Note server reflexive candidates don't update the checklist pairs since it's merely an + // alternative way to represent an existing host candidate. + _candidates.Add(svrRflxCandidate); + OnIceCandidate?.Invoke(svrRflxCandidate); } } - /// - /// Adds candidates and updates the checklist for an ICE server that has completed - /// the initial connectivity checks. - /// - /// The ICE server that the initial checks have been completed - /// for. - private async Task AddCandidatesForIceServer(IceServer iceServer) + if (_relayChecklistCandidate is null && iceServer.RelayEndPoint is { }) { - RTCIceCandidateInit init = new RTCIceCandidateInit - { - usernameFragment = LocalIceUser, - sdpMid = SDP_MID, - sdpMLineIndex = SDP_MLINE_INDEX, - }; + var relayCandidate = iceServer.GetCandidate(init, RTCIceCandidateType.relay); + Debug.Assert(relayCandidate is { }); + relayCandidate.SetDestinationEndPoint(iceServer.RelayEndPoint); - if (iceServer.ServerReflexiveEndPoint != null) - { - RTCIceCandidate svrRflxCandidate = iceServer.GetCandidate(init, RTCIceCandidateType.srflx); + // A local relay candidate is stored so it can be pared with any remote candidates + // that arrive after the checklist update carried out in this method. + _relayChecklistCandidate = relayCandidate; - if (_policy == RTCIceTransportPolicy.all && svrRflxCandidate != null) - { - logger.LogDebug("Adding server reflex ICE candidate for ICE server {Uri} and {EndPoint}.", iceServer._uri, iceServer.ServerReflexiveEndPoint); + if (relayCandidate is { }) + { + logger.LogIceRelayAddingCandidate(iceServer.Uri, iceServer.RelayEndPoint); - // Note server reflexive candidates don't update the checklist pairs since it's merely an - // alternative way to represent an existing host candidate. - _candidates.Add(svrRflxCandidate); - OnIceCandidate?.Invoke(svrRflxCandidate); - } + _candidates.Add(relayCandidate); + OnIceCandidate?.Invoke(relayCandidate); } - if (_relayChecklistCandidate == null && iceServer.RelayEndPoint != null) + foreach (var remoteCandidate in _remoteCandidates) { - RTCIceCandidate relayCandidate = iceServer.GetCandidate(init, RTCIceCandidateType.relay); - relayCandidate.SetDestinationEndPoint(iceServer.RelayEndPoint); - - // A local relay candidate is stored so it can be pared with any remote candidates - // that arrive after the checklist update carried out in this method. - _relayChecklistCandidate = relayCandidate; - - if (relayCandidate != null) - { - logger.LogDebug("Adding relay ICE candidate for ICE server {Uri} and {EndPoint}.", iceServer._uri, iceServer.RelayEndPoint); - - _candidates.Add(relayCandidate); - OnIceCandidate?.Invoke(relayCandidate); - } - - foreach (var remoteCandidate in _remoteCandidates) - { - await UpdateChecklist(_relayChecklistCandidate, remoteCandidate).ConfigureAwait(false); - } + await UpdateChecklist(_relayChecklistCandidate, remoteCandidate).ConfigureAwait(false); } - - IceGatheringState = RTCIceGatheringState.complete; - OnIceGatheringStateChange?.Invoke(IceGatheringState); } - /// - /// Updates the checklist with new candidate pairs. - /// - /// - /// From https://tools.ietf.org/html/rfc8445#section-6.1.2.2: - /// IPv6 link-local addresses MUST NOT be paired with other than link-local addresses. - /// - /// The local candidate for the checklist entry. - /// The remote candidate to attempt to create a new checklist - /// entry for. - private async Task UpdateChecklist(RTCIceCandidate localCandidate, RTCIceCandidate remoteCandidate) + IceGatheringState = RTCIceGatheringState.complete; + OnIceGatheringStateChange?.Invoke(IceGatheringState); + } + + /// + /// Updates the checklist with new candidate pairs. + /// + /// + /// From https://tools.ietf.org/html/rfc8445#section-6.1.2.2: IPv6 link-local addresses MUST NOT be paired with + /// other than link-local addresses. + /// + /// The local candidate for the checklist entry. + /// + /// The remote candidate to attempt to create a new checklist entry for. + /// + private async Task UpdateChecklist(RTCIceCandidate localCandidate, RTCIceCandidate remoteCandidate) + { + if (localCandidate is null) { - if (localCandidate == null) - { - logger.LogError("{Method} the local candidate supplied to UpdateChecklist was null.", nameof(UpdateChecklist)); - return; - } - else if (remoteCandidate == null) - { - logger.LogError("{Method} the remote candidate supplied to UpdateChecklist was null.", nameof(UpdateChecklist)); - return; - } + logger.LogIceLocalCandidateUpdateChecklistError(); + return; + } + else if (remoteCandidate is null) + { + logger.LogIceRemoteCandidateUpdateChecklistError(); + return; + } - // This method is called in a fire and forget fashion so any exceptions need to be handled here. - try + // This method is called in a fire and forget fashion so any exceptions need to be handled here. + try + { + // Attempt to resolve the remote candidate address. + if (!IPAddress.TryParse(remoteCandidate.address, out var remoteCandidateIPAddr)) { - // Attempt to resolve the remote candidate address. - if (!IPAddress.TryParse(remoteCandidate.address, out var remoteCandidateIPAddr)) + Debug.Assert(remoteCandidate.address is { }); + if (remoteCandidate.address.EndsWith(MDNS_TLD, StringComparison.OrdinalIgnoreCase)) { - if (remoteCandidate.address.EndsWith(MDNS_TLD, StringComparison.OrdinalIgnoreCase)) + var addresses = await ResolveMdnsName(remoteCandidate).ConfigureAwait(false); + if (addresses.Length == 0) { - var addresses = await ResolveMdnsName(remoteCandidate).ConfigureAwait(false); - if (addresses.Length == 0) - { - logger.LogWarning("RTP ICE channel MDNS resolver failed to resolve {RemoteCandidateAddress}.", remoteCandidate.address); - } - else - { - remoteCandidateIPAddr = addresses[0]; - logger.LogDebug("RTP ICE channel resolved MDNS hostname {RemoteCandidateAddress} to {RemoteCandidateIPAddr}.", remoteCandidate.address, remoteCandidateIPAddr); - - var remoteEP = new IPEndPoint(remoteCandidateIPAddr, remoteCandidate.port); - remoteCandidate.SetDestinationEndPoint(remoteEP); - } + logger.LogIceMdnsResolutionFailed(remoteCandidate.address); } else { - // The candidate string can be a hostname or an IP address. - var lookupResult = await _dnsLookupClient.QueryAsync(remoteCandidate.address, DnsClient.QueryType.A).ConfigureAwait(false); - - if (lookupResult.Answers.Count > 0) - { - remoteCandidateIPAddr = lookupResult.Answers.AddressRecords().FirstOrDefault()?.Address; - logger.LogWarning("RTP ICE channel resolved remote candidate {RemoteCandidateAddress} to {RemoteCandidateIPAddr}.", remoteCandidate.address, remoteCandidateIPAddr); - } - else - { - logger.LogDebug("RTP ICE channel failed to resolve remote candidate {RemoteCandidateAddress}.", remoteCandidate.address); - } + remoteCandidateIPAddr = addresses[0]; + logger.LogIceMdnsResolutionSuccess(remoteCandidate.address, remoteCandidateIPAddr); - if (remoteCandidateIPAddr != null) - { - var remoteEP = new IPEndPoint(remoteCandidateIPAddr, remoteCandidate.port); - remoteCandidate.SetDestinationEndPoint(remoteEP); - } + var remoteEP = new IPEndPoint(remoteCandidateIPAddr, remoteCandidate.port); + remoteCandidate.SetDestinationEndPoint(remoteEP); } } else { - var remoteEP = new IPEndPoint(remoteCandidateIPAddr, remoteCandidate.port); - remoteCandidate.SetDestinationEndPoint(remoteEP); - } - - // If the remote candidate is resolvable create a new checklist entry. - if (remoteCandidate.DestinationEndPoint != null) - { - bool supportsIPv4 = true; - bool supportsIPv6 = false; + Debug.Assert(_dnsLookupClient is { }); + // The candidate string can be a hostname or an IP address. + var lookupResult = await _dnsLookupClient.QueryAsync(remoteCandidate.address, DnsClient.QueryType.A).ConfigureAwait(false); - if (localCandidate.type == RTCIceCandidateType.relay) + if (lookupResult.Answers.Count > 0) { - supportsIPv4 = localCandidate.DestinationEndPoint.AddressFamily == AddressFamily.InterNetwork; - supportsIPv6 = localCandidate.DestinationEndPoint.AddressFamily == AddressFamily.InterNetworkV6; + remoteCandidateIPAddr = null; + foreach (var rr in lookupResult.Answers) + { + if (rr is DnsClient.Protocol.ARecord a) + { + remoteCandidateIPAddr = a.Address; + break; + } + } + logger.LogIceDnsResolutionSuccess(remoteCandidate.address, remoteCandidateIPAddr); } else { - supportsIPv4 = base.RtpSocket.AddressFamily == AddressFamily.InterNetwork || base.IsDualMode; - supportsIPv6 = base.RtpSocket.AddressFamily == AddressFamily.InterNetworkV6 || base.IsDualMode; + logger.LogIceDnsResolutionFailed(remoteCandidate.address); + } + + if (remoteCandidateIPAddr is { }) + { + var remoteEP = new IPEndPoint(remoteCandidateIPAddr, remoteCandidate.port); + remoteCandidate.SetDestinationEndPoint(remoteEP); } + } + } + else + { + var remoteEP = new IPEndPoint(remoteCandidateIPAddr, remoteCandidate.port); + remoteCandidate.SetDestinationEndPoint(remoteEP); + } + + // If the remote candidate is resolvable create a new checklist entry. + if (remoteCandidate.DestinationEndPoint is { }) + { + var supportsIPv4 = true; + var supportsIPv6 = false; + + if (localCandidate.type == RTCIceCandidateType.relay) + { + Debug.Assert(localCandidate.DestinationEndPoint is { }); + var addressFamily = localCandidate.DestinationEndPoint.AddressFamily; + supportsIPv4 = addressFamily == AddressFamily.InterNetwork; + supportsIPv6 = addressFamily == AddressFamily.InterNetworkV6; + } + else + { + Debug.Assert(RtpConnection is { }); + var addressFamily = RtpConnection.Socket.AddressFamily; + supportsIPv4 = addressFamily == AddressFamily.InterNetwork || base.IsDualMode; + supportsIPv6 = addressFamily == AddressFamily.InterNetworkV6 || base.IsDualMode; + } + + if (remoteCandidateIPAddr is { }) + { lock (_checklistLock) { - if (remoteCandidateIPAddr.AddressFamily == AddressFamily.InterNetwork && supportsIPv4 || - remoteCandidateIPAddr.AddressFamily == AddressFamily.InterNetworkV6 && supportsIPv6) + Debug.Assert(remoteCandidateIPAddr is { }); + var addressFamily = remoteCandidateIPAddr.AddressFamily; + if (addressFamily == AddressFamily.InterNetwork && supportsIPv4 || + addressFamily == AddressFamily.InterNetworkV6 && supportsIPv6) { - ChecklistEntry entry = new ChecklistEntry(localCandidate, remoteCandidate, IsController); + var entry = new ChecklistEntry(localCandidate, remoteCandidate, IsController); // Because only ONE checklist is currently supported each candidate pair can be set to // a "waiting" state. If an additional checklist is ever added then only one candidate @@ -1421,1384 +1160,1444 @@ private async Task UpdateChecklist(RTCIceCandidate localCandidate, RTCIceCandida } } } - else - { - logger.LogWarning("RTP ICE Channel could not create a check list entry for a remote candidate with no destination end point, {RemoteCandidate}.", remoteCandidate); - } } - catch (Exception excp) + else { - logger.LogError(excp, "Exception in {Method}.", nameof(UpdateChecklist)); + logger.LogRtpCandidatesUnavailable(remoteCandidate); } } - - /// - /// Attempts to add a checklist entry. If there is already an equivalent entry in the checklist - /// the entry may not be added or may replace an existing entry. - /// - /// The new entry to attempt to add to the checklist. - private void AddChecklistEntry(ChecklistEntry entry) + catch (Exception excp) { - // Check if there is already an entry that matches the remote candidate. - // Note: The implementation in this class relies binding the socket used for all - // local candidates on a SINGLE address (typically 0.0.0.0 or [::]). Consequently - // there is no need to check the local candidate when determining duplicates. As long - // as there is one checklist entry with each remote candidate the connectivity check will - // work. To put it another way the local candidate information is not used on the - // "Nominated" pair. - - var entryRemoteEP = entry.RemoteCandidate.DestinationEndPoint; + logger.LogUpdateChecklistError(excp.Message, excp); + } + } - lock (_checklistLock) + /// + /// Attempts to add a checklist entry. If there is already an equivalent entry in the checklist the entry may not be + /// added or may replace an existing entry. + /// + /// The new entry to attempt to add to the checklist. + private void AddChecklistEntry(ChecklistEntry entry) + { + // Check if there is already an entry that matches the remote candidate. + // Note: The implementation in this class relies binding the socket used for all + // local candidates on a SINGLE address (typically 0.0.0.0 or [::]). Consequently + // there is no need to check the local candidate when determining duplicates. As long + // as there is one checklist entry with each remote candidate the connectivity check will + // work. To put it another way the local candidate information is not used on the + // "Nominated" pair. + + lock (_checklistLock) + { + if (FindMatchingChecklistEntry(entry) is { } existingEntry) { - var existingEntry = _checklist.Where(x => - x.LocalCandidate.type == entry.LocalCandidate.type - && x.RemoteCandidate.DestinationEndPoint != null - && x.RemoteCandidate.DestinationEndPoint.Address.Equals(entryRemoteEP.Address) - && x.RemoteCandidate.DestinationEndPoint.Port == entryRemoteEP.Port - && x.RemoteCandidate.protocol == entry.RemoteCandidate.protocol).SingleOrDefault(); - - if (existingEntry != null) + // Don't replace an existing checklist entry if it's already acting as the nominated entry. + if (!existingEntry.Nominated) { - // Don't replace an existing checklist entry if it's already acting as the nominated entry. - if (!existingEntry.Nominated) + if (entry.Priority > existingEntry.Priority) { - if (entry.Priority > existingEntry.Priority) - { - logger.LogDebug("Removing lower priority entry and adding candidate pair to checklist for: {RemoteCandidate}", entry.RemoteCandidate); - _checklist.Remove(existingEntry); - _checklist.Add(entry); - } - else - { - logger.LogDebug("Existing checklist entry has higher priority, NOT adding entry for: {RemoteCandidate}", entry.RemoteCandidate); - } + logger.LogIceChecklistEntryLowerPriority(entry.RemoteCandidate); + _checklist.Remove(existingEntry); + _checklist.Add(entry); + } + else + { + logger.LogIceChecklistEntryHigherPriority(entry.RemoteCandidate); } - } - else - { - // No existing entry. - logger.LogDebug("Adding new candidate pair to checklist for: {LocalCandidate}->{RemoteCandidate}", entry.LocalCandidate.ToShortString(), entry.RemoteCandidate.ToShortString()); - _checklist.Add(entry); } } - } - - /// - /// The periodic logic to run to establish or monitor an ICE connection. - /// - private void DoConnectivityCheck(Object stateInfo) - { - if (_closed) + else { - return; + // No existing entry. + logger.LogIceNewChecklistEntry(entry.LocalCandidate, entry.RemoteCandidate); + _checklist.Add(entry); } - switch (IceConnectionState) + ChecklistEntry? FindMatchingChecklistEntry(ChecklistEntry targetEntry) { - case RTCIceConnectionState.@new: - case RTCIceConnectionState.checking: - ProcessChecklist(); - break; + var entryRemoteEP = targetEntry.RemoteCandidate.DestinationEndPoint; + Debug.Assert(entryRemoteEP is { }); + + foreach (var x in _checklist) + { + var remoteCandidate = x.RemoteCandidate; + var destinationEndPoint = remoteCandidate.DestinationEndPoint; + if (x.LocalCandidate.type == targetEntry.LocalCandidate.type + && destinationEndPoint is { } + && destinationEndPoint.Address.Equals(entryRemoteEP.Address) + && destinationEndPoint.Port == entryRemoteEP.Port + && remoteCandidate.protocol == targetEntry.RemoteCandidate.protocol) + { + return x; + } + } - case RTCIceConnectionState.connected: - case RTCIceConnectionState.disconnected: - // Periodic checks on the nominated peer. - SendCheckOnConnectedPair(NominatedEntry); - break; + return null; - case RTCIceConnectionState.failed: - case RTCIceConnectionState.closed: - logger.LogDebug("ICE RTP channel stopping connectivity checks in connection state {IceConnectionState}.", IceConnectionState); - _connectivityChecksTimer?.Dispose(); - break; } } + } + + /// + /// The periodic logic to run to establish or monitor an ICE connection. + /// + private void DoConnectivityCheck(object? stateInfo) + { + if (_closed) + { + return; + } + + switch (IceConnectionState) + { + case RTCIceConnectionState.@new: + case RTCIceConnectionState.checking: + ProcessChecklist(); + break; + + case RTCIceConnectionState.connected: + case RTCIceConnectionState.disconnected: + // Periodic checks on the nominated peer. + Debug.Assert(NominatedEntry is { }); + SendCheckOnConnectedPair(NominatedEntry); + break; + + case RTCIceConnectionState.failed: + case RTCIceConnectionState.closed: + logger.LogIceChecksTimerStopped(IceConnectionState); + _connectivityChecksTimer?.Dispose(); + break; + } + } - /// - /// Processes the checklist and sends any required STUN requests to perform connectivity checks. - /// - /// - /// The scheduling mechanism for ICE is specified in https://tools.ietf.org/html/rfc8445#section-6.1.4. - /// - private async void ProcessChecklist() + /// + /// Processes the checklist and sends any required STUN requests to perform connectivity checks. + /// + /// + /// The scheduling mechanism for ICE is specified in https://tools.ietf.org/html/rfc8445#section-6.1.4. + /// + private async void ProcessChecklist() + { + if (!_closed && (IceConnectionState == RTCIceConnectionState.@new || + IceConnectionState == RTCIceConnectionState.checking)) { - if (!_closed && (IceConnectionState == RTCIceConnectionState.@new || - IceConnectionState == RTCIceConnectionState.checking)) + while (_pendingRemoteCandidates.TryDequeue(out var candidate)) { - while (_pendingRemoteCandidates.Count() > 0) + if (_policy != RTCIceTransportPolicy.relay) { - if (_pendingRemoteCandidates.TryDequeue(out var candidate)) - { - if (_policy != RTCIceTransportPolicy.relay) - { - // The reason not to wait for this operation is that the ICE candidate can - // contain a hostname and require a DNS lookup. There's nothing that can be done - // if the DNS lookup fails so initiate the task and then keep going with - // adding any other pending candidates and move on with processing the check list. - _ = UpdateChecklist(_localChecklistCandidate, candidate); - } + // The reason not to wait for this operation is that the ICE candidate can + // contain a hostname and require a DNS lookup. There's nothing that can be done + // if the DNS lookup fails so initiate the task and then keep going with + // adding any other pending candidates and move on with processing the check list. + _ = UpdateChecklist(_localChecklistCandidate, candidate); + } - // If a relay server is available add a checklist entry for it as well. - if (_relayChecklistCandidate != null) - { - // The local relay candidate has already been checked and any hostnames - // resolved when the ICE servers were checked. - await UpdateChecklist(_relayChecklistCandidate, candidate).ConfigureAwait(false); - } - } + // If a relay server is available add a checklist entry for it as well. + if (_relayChecklistCandidate is { }) + { + // The local relay candidate has already been checked and any hostnames + // resolved when the ICE servers were checked. + await UpdateChecklist(_relayChecklistCandidate, candidate).ConfigureAwait(false); } + } - // The connection state will be set to checking when the remote ICE user and password are available. - // Until that happens there is no work to do. - if (IceConnectionState == RTCIceConnectionState.checking) + // The connection state will be set to checking when the remote ICE user and password are available. + // Until that happens there is no work to do. + if (IceConnectionState == RTCIceConnectionState.checking) + { + lock (_checklistLock) { - lock (_checklistLock) + if (_checklist.Count > 0) { - if (_checklist.Count > 0) + if (RemoteIceUser is null || RemoteIcePassword is null) { - if (RemoteIceUser == null || RemoteIcePassword == null) - { - logger.LogWarning("ICE RTP channel checklist processing cannot occur as either the remote ICE user or password are not set."); - IceConnectionState = RTCIceConnectionState.failed; - } - else + logger.LogIceFailedNoRemoteCredentials(); + IceConnectionState = RTCIceConnectionState.failed; + } + else + { + // The checklist gets sorted into priority order whenever a remote candidate and its corresponding candidate pairs + // are added. At this point it can be relied upon that the checklist is correctly sorted by candidate pair priority. + + // Do a check for any timed out entries. + var now = DateTime.Now; + var rto = RTO; + ChecklistEntry? nextEntry = null; + ChecklistEntry? retransmitEntry = null; + foreach (var entry in _checklist) { - // The checklist gets sorted into priority order whenever a remote candidate and its corresponding candidate pairs - // are added. At this point it can be relied upon that the checklist is correctly sorted by candidate pair priority. - - // Do a check for any timed out entries. - var failedEntries = _checklist.Where(x => x.State == ChecklistEntryState.InProgress - && DateTime.Now.Subtract(x.FirstCheckSentAt).TotalSeconds > FAILED_TIMEOUT_PERIOD).ToList(); - - foreach (var failedEntry in failedEntries) + if (entry.State == ChecklistEntryState.InProgress + && now.Subtract(entry.FirstCheckSentAt).TotalSeconds > FAILED_TIMEOUT_PERIOD) { - logger.LogDebug("ICE RTP channel checks for checklist entry have timed out, state being set to failed: {LocalCandidate}->{RemoteCandidate}.", failedEntry.LocalCandidate.ToShortString(), failedEntry.RemoteCandidate.ToShortString()); - failedEntry.State = ChecklistEntryState.Failed; + logger.LogIceChecklistEntryTimeout(entry.LocalCandidate, entry.RemoteCandidate); + entry.State = ChecklistEntryState.Failed; } - // Move on to checking for checklist entries that need an initial check sent. - var nextEntry = _checklist.Where(x => x.State == ChecklistEntryState.Waiting).FirstOrDefault(); - - if (nextEntry != null) + // Capture the first waiting entry for connectivity check + if (nextEntry is null && entry.State == ChecklistEntryState.Waiting) { - SendConnectivityCheck(nextEntry, false); - return; + nextEntry = entry; } - var rto = RTO; - // No waiting entries so check for ones requiring a retransmit. - var retransmitEntry = _checklist.Where(x => x.State == ChecklistEntryState.InProgress - && DateTime.Now.Subtract(x.LastCheckSentAt).TotalMilliseconds > rto).FirstOrDefault(); - - if (retransmitEntry != null) + // Capture the first retransmit candidate + if (retransmitEntry is null + && entry.State == ChecklistEntryState.InProgress + && now.Subtract(entry.LastCheckSentAt).TotalMilliseconds > rto) { - SendConnectivityCheck(retransmitEntry, false); - return; + retransmitEntry = entry; } - if (IceGatheringState == RTCIceGatheringState.complete) + } + + // Move on to checking for checklist entries that need an initial check sent. + if (nextEntry is { }) + { + SendConnectivityCheck(nextEntry, false); + return; + } + + // No waiting entries so check for ones requiring a retransmit. + if (retransmitEntry is { }) + { + SendConnectivityCheck(retransmitEntry, false); + return; + } + + if (IceGatheringState == RTCIceGatheringState.complete) + { + //Try force finalize process as probably we lost any RtpPacketResponse during process and we are unable to finalize process + if (NominatedEntry is null) { - //Try force finalize process as probably we lost any RtpPacketResponse during process and we are unable to finalize process - if (NominatedEntry == null) + // Do a check for any timed out that has + var requireReprocess = false; + foreach (var entry in _checklist) { - // Do a check for any timed out that has succeded - var failedNominatedEntries = _checklist.Where(x => - x.State == ChecklistEntryState.Succeeded - && x.LastCheckSentAt > System.DateTime.MinValue - && DateTime.Now.Subtract(x.LastCheckSentAt).TotalSeconds > FAILED_TIMEOUT_PERIOD).ToList(); - - var requireReprocess = false; - foreach (var failedNominatedEntry in failedNominatedEntries) + if (entry.State == ChecklistEntryState.Succeeded + && entry.LastCheckSentAt > DateTime.MinValue + && now.Subtract(entry.LastCheckSentAt).TotalSeconds > FAILED_TIMEOUT_PERIOD) { - //Recalculate logic when we lost a nominated entry - if (failedNominatedEntry.Nominated) + if (entry.Nominated) { requireReprocess = true; } - failedNominatedEntry.State = ChecklistEntryState.Failed; - failedNominatedEntry.Nominated = false; + entry.State = ChecklistEntryState.Failed; + entry.Nominated = false; - logger.LogDebug("ICE RTP channel checks for succeded checklist entry have timed out, state being set to failed: {LocalCandidate}->{RemoteCandidate}.", failedNominatedEntry.LocalCandidate.ToShortString(), failedNominatedEntry.RemoteCandidate.ToShortString()); - } - - //Try nominate another entry - if (requireReprocess) - { - ProcessNominateLogicAsController(null); + logger.LogIceChecklistEntrySucceededTimeout(entry.LocalCandidate, entry.RemoteCandidate); } } - // If this point is reached and all entries are in a failed state then the overall result - // of the ICE check is a failure. - if (_checklist.All(x => x.State == ChecklistEntryState.Failed)) + //Try nominate another entry + if (requireReprocess) { - _checklistState = ChecklistState.Failed; - IceConnectionState = RTCIceConnectionState.failed; - OnIceConnectionStateChange?.Invoke(IceConnectionState); + ProcessNominateLogicAsController(null); } } - } - } - else if (_checklistStartedAt != DateTime.MinValue && - DateTime.Now.Subtract(_checklistStartedAt).TotalSeconds > FAILED_TIMEOUT_PERIOD) - { - // No checklist entries were made available before the failed timeout. - logger.LogWarning("ICE RTP channel failed to connect as no checklist entries became available within {ElapsedSeconds}s.", DateTime.Now.Subtract(_checklistStartedAt).TotalSeconds); - _checklistState = ChecklistState.Failed; - //IceConnectionState = RTCIceConnectionState.disconnected; - // No point going to and ICE disconnected state as there was never a connection and therefore - // nothing to monitor for a re-connection. - IceConnectionState = RTCIceConnectionState.failed; - OnIceConnectionStateChange?.Invoke(IceConnectionState); + // If this point is reached and all entries are in a failed state then the overall result + // of the ICE check is a failure. + if (_checklist.TrueForAll(static x => x.State == ChecklistEntryState.Failed)) + { + _checklistState = ChecklistState.Failed; + IceConnectionState = RTCIceConnectionState.failed; + OnIceConnectionStateChange?.Invoke(IceConnectionState); + } + } } } + else if (_checklistStartedAt != DateTime.MinValue && + DateTime.Now.Subtract(_checklistStartedAt).TotalSeconds > FAILED_TIMEOUT_PERIOD) + { + // No checklist entries were made available before the failed timeout. + logger.LogIceChannelFailed(_checklistStartedAt); + + _checklistState = ChecklistState.Failed; + //IceConnectionState = RTCIceConnectionState.disconnected; + // No point going to and ICE disconnected state as there was never a connection and therefore + // nothing to monitor for a re-connection. + IceConnectionState = RTCIceConnectionState.failed; + OnIceConnectionStateChange?.Invoke(IceConnectionState); + } } } } + } + + /// + /// Sets the nominated checklist entry. This action completes the checklist processing and indicates the connection + /// checks were successful. + /// + /// The checklist entry that was nominated. + private void SetNominatedEntry(ChecklistEntry entry) + { + if (NominatedEntry is null) + { + _connectedAt = DateTime.Now; + var duration = (int)_connectedAt.Subtract(_startedGatheringAt).TotalMilliseconds; + + logger.LogIceChannelConnected(duration, entry.LocalCandidate, entry.RemoteCandidate); + + entry.Nominated = true; + entry.LastConnectedResponseAt = DateTime.Now; + _checklistState = ChecklistState.Completed; + Debug.Assert(_connectivityChecksTimer is { }); + _connectivityChecksTimer.Change(CONNECTED_CHECK_PERIOD * 1000, CONNECTED_CHECK_PERIOD * 1000); + NominatedEntry = entry; + IceConnectionState = RTCIceConnectionState.connected; + OnIceConnectionStateChange?.Invoke(RTCIceConnectionState.connected); + } + else + { + // The nominated entry has been changed. + logger.LogIceChannelNominatedChanged(NominatedEntry.RemoteCandidate, entry.RemoteCandidate); + + entry.Nominated = true; + entry.LastConnectedResponseAt = DateTime.Now; + NominatedEntry = entry; + OnIceConnectionStateChange?.Invoke(RTCIceConnectionState.connected); + } + } + + /// + /// Performs a connectivity check for a single candidate pair entry. + /// + /// The candidate pair to perform a connectivity check for. + /// + /// If true indicates we are acting as the "controlling" ICE agent and are nominating this candidate as the chosen + /// one. + /// + /// + /// As specified in https://tools.ietf.org/html/rfc8445#section-7.2.4. Relay candidates are a special (and more + /// difficult) case. The extra steps required to send packets via a TURN server are: - A Channel Bind request needs + /// to be sent for each peer end point the channel will be used to communicate with. - Packets need to be sent and + /// received as TURN Channel Data messages. + /// + private void SendConnectivityCheck(ChecklistEntry candidatePair, bool setUseCandidate) + { + if (_closed) + { + return; + } - /// - /// Sets the nominated checklist entry. This action completes the checklist processing and - /// indicates the connection checks were successful. - /// - /// The checklist entry that was nominated. - private void SetNominatedEntry(ChecklistEntry entry) + if (candidatePair.FirstCheckSentAt == DateTime.MinValue) { - if (NominatedEntry == null) - { - _connectedAt = DateTime.Now; - int duration = (int)_connectedAt.Subtract(_startedGatheringAt).TotalMilliseconds; + candidatePair.FirstCheckSentAt = DateTime.Now; + candidatePair.State = ChecklistEntryState.InProgress; + } - logger.LogDebug("ICE RTP channel connected after {Duration:0.##}ms {LocalCandidate}->{RemoteCandidate}.", duration, entry.LocalCandidate.ToShortString(), entry.RemoteCandidate.ToShortString()); + candidatePair.LastCheckSentAt = DateTime.Now; + candidatePair.ChecksSent++; + candidatePair.RequestTransactionID = Crypto.GetRandomString(STUNHeader.TRANSACTION_ID_LENGTH); - entry.Nominated = true; - entry.LastConnectedResponseAt = DateTime.Now; - _checklistState = ChecklistState.Completed; - _connectivityChecksTimer.Change(CONNECTED_CHECK_PERIOD * 1000, CONNECTED_CHECK_PERIOD * 1000); - NominatedEntry = entry; - IceConnectionState = RTCIceConnectionState.connected; - OnIceConnectionStateChange?.Invoke(RTCIceConnectionState.connected); + var localCandidate = candidatePair.LocalCandidate; + var isRelayCheck = localCandidate.type == RTCIceCandidateType.relay; + + if (isRelayCheck && candidatePair.TurnPermissionsResponseAt == DateTime.MinValue) + { + if (candidatePair.TurnPermissionsRequestSent >= IceServer.MAX_REQUESTS) + { + logger.LogIceTurnPermissionsFailed(localCandidate.IceServer?.Uri, candidatePair.TurnPermissionsRequestSent); + candidatePair.State = ChecklistEntryState.Failed; } else { - // The nominated entry has been changed. - logger.LogDebug("ICE RTP channel remote nominated candidate changed from {OldCandidate} to {NewCandidate}.", NominatedEntry.RemoteCandidate.ToShortString(), entry.RemoteCandidate.ToShortString()); + // Send Create Permissions request to TURN server for remote candidate. + candidatePair.TurnPermissionsRequestSent++; + + var requestTransactionID = candidatePair.RequestTransactionID; + Debug.Assert(requestTransactionID is { }); - entry.Nominated = true; - entry.LastConnectedResponseAt = DateTime.Now; - NominatedEntry = entry; - OnIceConnectionStateChange?.Invoke(RTCIceConnectionState.connected); + logger.LogIceTurnPermissionsRequest( + candidatePair.TurnPermissionsRequestSent, + localCandidate.IceServer?.Uri, + candidatePair.RemoteCandidate.DestinationEndPoint, + requestTransactionID); + + Debug.Assert(localCandidate?.IceServer is { }); + Debug.Assert(candidatePair?.RemoteCandidate?.DestinationEndPoint is { }); + SendTurnCreatePermissionsRequest(requestTransactionID, localCandidate.IceServer, candidatePair.RemoteCandidate.DestinationEndPoint); } } - - /// - /// Performs a connectivity check for a single candidate pair entry. - /// - /// The candidate pair to perform a connectivity check for. - /// If true indicates we are acting as the "controlling" ICE agent - /// and are nominating this candidate as the chosen one. - /// As specified in https://tools.ietf.org/html/rfc8445#section-7.2.4. - /// - /// Relay candidates are a special (and more difficult) case. The extra steps required to send packets via - /// a TURN server are: - /// - A Channel Bind request needs to be sent for each peer end point the channel will be used to - /// communicate with. - /// - Packets need to be sent and received as TURN Channel Data messages. - /// - /// - private void SendConnectivityCheck(ChecklistEntry candidatePair, bool setUseCandidate) + else { - if (_closed) + if (localCandidate.type == RTCIceCandidateType.relay) { - return; + logger.LogIceConnectivityCheck( + localCandidate, + candidatePair.RemoteCandidate, + base.RTPLocalEndPoint, + localCandidate.IceServer?.ServerEndPoint, + setUseCandidate); } - - if (candidatePair.FirstCheckSentAt == DateTime.MinValue) + else { - candidatePair.FirstCheckSentAt = DateTime.Now; - candidatePair.State = ChecklistEntryState.InProgress; + logger.LogIceRelayCheck( + localCandidate, + candidatePair.RemoteCandidate, + base.RTPLocalEndPoint, + candidatePair.RemoteCandidate.DestinationEndPoint, + setUseCandidate); } - candidatePair.LastCheckSentAt = DateTime.Now; - candidatePair.ChecksSent++; - candidatePair.RequestTransactionID = Crypto.GetRandomString(STUNHeader.TRANSACTION_ID_LENGTH); - - bool isRelayCheck = candidatePair.LocalCandidate.type == RTCIceCandidateType.relay; - //bool isTcpProtocol = candidatePair.LocalCandidate.IceServer?.Protocol == ProtocolType.Tcp; + SendStunBindingRequest(candidatePair, setUseCandidate); + } + } - if (isRelayCheck && candidatePair.TurnPermissionsResponseAt == DateTime.MinValue) + /// + /// Builds and sends a STUN binding request to a remote peer based on the candidate pair properties. + /// + /// + /// The candidate pair identifying the remote peer to send the STUN Binding Request to. + /// + /// Set to true to add a "UseCandidate" attribute to the STUN request. + private void SendStunBindingRequest(ChecklistEntry candidatePair, bool setUseCandidate) + { + var requestTransactionID = candidatePair.RequestTransactionID; + Debug.Assert(requestTransactionID is { }); + var stunRequest = new STUNMessage(STUNMessageTypesEnum.BindingRequest) + { + Header = { - if (candidatePair.TurnPermissionsRequestSent >= IceServer.MAX_REQUESTS) - { - logger.LogWarning("ICE RTP channel failed to get a Create Permissions response from {IceServerUri} after {TurnPermissionsRequestSent} attempts.", candidatePair.LocalCandidate.IceServer._uri, candidatePair.TurnPermissionsRequestSent); - candidatePair.State = ChecklistEntryState.Failed; - } - else - { - // Send Create Permissions request to TURN server for remote candidate. - candidatePair.TurnPermissionsRequestSent++; - - logger.LogDebug("ICE RTP channel sending TURN permissions request {TurnPermissionsRequestSent} to server {IceServerUri} for peer {RemoteCandidate} (TxID: {RequestTransactionID}).", candidatePair.TurnPermissionsRequestSent, candidatePair.LocalCandidate.IceServer._uri, candidatePair.RemoteCandidate.DestinationEndPoint, candidatePair.RequestTransactionID); - SendTurnCreatePermissionsRequest(candidatePair.RequestTransactionID, candidatePair.LocalCandidate.IceServer, candidatePair.RemoteCandidate.DestinationEndPoint); - } - } - else + TransactionId = Encoding.ASCII.GetBytes(requestTransactionID) + }, + Attributes = { - if (candidatePair.LocalCandidate.type == RTCIceCandidateType.relay) - { - IPEndPoint relayServerEP = candidatePair.LocalCandidate.IceServer.ServerEndPoint; - logger.LogDebug("ICE RTP channel sending connectivity check for {LocalCandidate}->{RemoteCandidate} from {LocalEndPoint} to relay at {RelayServerEndPoint} (use candidate {SetUseCandidate}).", candidatePair.LocalCandidate.ToShortString(), candidatePair.RemoteCandidate.ToShortString(), base.RTPLocalEndPoint, relayServerEP, setUseCandidate); - } - else - { - IPEndPoint remoteEndPoint = candidatePair.RemoteCandidate.DestinationEndPoint; - logger.LogDebug("ICE RTP channel sending connectivity check for {LocalCandidate}->{RemoteCandidate} from {LocalEndPoint} to {RemoteEndPoint} (use candidate {SetUseCandidate}).", candidatePair.LocalCandidate.ToShortString(), candidatePair.RemoteCandidate.ToShortString(), base.RTPLocalEndPoint, remoteEndPoint, setUseCandidate); - } - SendSTUNBindingRequest(candidatePair, setUseCandidate); - } + new STUNAttribute(STUNAttributeTypesEnum.Priority, candidatePair.LocalPriority) + }, + }; + + stunRequest.AddUsernameAttribute($"{RemoteIceUser}:{LocalIceUser}"); + + if (IsController) + { + stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.IceControlling, _iceTiebreaker)); + } + else + { + stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.IceControlled, _iceTiebreaker)); } - /// - /// Builds and sends a STUN binding request to a remote peer based on the candidate pair properties. - /// - /// The candidate pair identifying the remote peer to send the STUN Binding Request - /// to. - /// Set to true to add a "UseCandidate" attribute to the STUN request. - private void SendSTUNBindingRequest(ChecklistEntry candidatePair, bool setUseCandidate) + if (setUseCandidate) { - STUNMessage stunRequest = new STUNMessage(STUNMessageTypesEnum.BindingRequest); - stunRequest.Header.TransactionId = Encoding.ASCII.GetBytes(candidatePair.RequestTransactionID); - stunRequest.AddUsernameAttribute($"{RemoteIceUser}:{LocalIceUser}"); - stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Priority, BitConverter.GetBytes(candidatePair.LocalPriority))); + stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.UseCandidate, ReadOnlyMemory.Empty)); + } - if (IsController) - { - stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.IceControlling, NetConvert.GetBytes(_iceTiebreaker))); - } - else - { - stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.IceControlled, NetConvert.GetBytes(_iceTiebreaker))); - } + //logger.LogSendStunBindingRequest(activeIceServerUri, remoteCandidateEndPoint, stunRequest); - if (setUseCandidate) - { - stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.UseCandidate, null)); - } + var remoteCandidateEndPoint = candidatePair.RemoteCandidate.DestinationEndPoint; + + var bufferSize = stunRequest.GetByteBufferSizeStringKey(RemoteIcePassword, addFingerprint: true); + var rentedBuffer = ArrayPool.Shared.Rent(bufferSize); + + try + { + stunRequest.WriteToBufferStringKey(rentedBuffer.AsSpan(0, bufferSize), RemoteIcePassword, addFingerprint: true); - byte[] stunReqBytes = stunRequest.ToByteBufferStringKey(RemoteIcePassword, true); + Debug.Assert(remoteCandidateEndPoint is { }); if (candidatePair.LocalCandidate.type == RTCIceCandidateType.relay) { - IPEndPoint relayServerEP = candidatePair.LocalCandidate.IceServer.ServerEndPoint; + Debug.Assert(candidatePair?.LocalCandidate?.IceServer is { }); + var relayServerEP = candidatePair.LocalCandidate.IceServer.ServerEndPoint; var protocol = candidatePair.LocalCandidate.IceServer.Protocol; - SendRelay(protocol, candidatePair.RemoteCandidate.DestinationEndPoint, stunReqBytes, relayServerEP, candidatePair.LocalCandidate.IceServer); + + Debug.Assert(relayServerEP is { }); + + using var activity = NetIceActivitySource.StartStunMessageSentActivity( + stunRequest, + relayServerEP, + null); + + SendRelay( + protocol, + remoteCandidateEndPoint, + rentedBuffer.AsMemory(0, bufferSize), + null, + relayServerEP, + candidatePair.LocalCandidate.IceServer); } else { - IPEndPoint remoteEndPoint = candidatePair.RemoteCandidate.DestinationEndPoint; - var sendResult = base.Send(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunReqBytes); + using var activity = NetIceActivitySource.StartStunMessageSentActivity( + stunRequest, + remoteCandidateEndPoint, + null); + + var sendResult = base.Send(RTPChannelSocketsEnum.RTP, remoteCandidateEndPoint, rentedBuffer.AsMemory(0, bufferSize)); if (sendResult != SocketError.Success) { - logger.LogWarning("Error sending STUN server binding request to {RemoteEndPoint}. {SendResult}.", remoteEndPoint, sendResult); + logger.LogStunBindingSendError(remoteCandidateEndPoint, sendResult); } else { - OnStunMessageSent?.Invoke(stunRequest, remoteEndPoint, false); + OnStunMessageSent?.Invoke(stunRequest, remoteCandidateEndPoint, false); } } } + finally + { + ArrayPool.Shared.Return(rentedBuffer); + } + } - /// - /// Builds and sends the connectivity check on a candidate pair that is set - /// as the current nominated, connected pair. - /// - /// The pair to send the connectivity check on. - private void SendCheckOnConnectedPair(ChecklistEntry candidatePair) + /// + /// Builds and sends the connectivity check on a candidate pair that is set as the current nominated, connected + /// pair. + /// + /// The pair to send the connectivity check on. + private void SendCheckOnConnectedPair(ChecklistEntry candidatePair) + { + if (candidatePair is null) + { + logger.LogIceConnCheckEmptyPair(); + } + else { - if (candidatePair == null) + if (DateTime.Now.Subtract(candidatePair.LastConnectedResponseAt).TotalSeconds > FAILED_TIMEOUT_PERIOD && + DateTime.Now.Subtract(candidatePair.LastBindingRequestReceivedAt).TotalSeconds > FAILED_TIMEOUT_PERIOD) { - logger.LogWarning("RTP ICE channel was requested to send a connectivity check on an empty candidate pair."); + var duration = (int)DateTime.Now.Subtract(candidatePair.LastConnectedResponseAt).TotalSeconds; + logger.LogIceChannelFailedTimeout(duration, candidatePair.LocalCandidate, candidatePair.RemoteCandidate); + + IceConnectionState = RTCIceConnectionState.failed; + OnIceConnectionStateChange?.Invoke(IceConnectionState); + + _connectivityChecksTimer?.Dispose(); } else { - if (DateTime.Now.Subtract(candidatePair.LastConnectedResponseAt).TotalSeconds > FAILED_TIMEOUT_PERIOD && - DateTime.Now.Subtract(candidatePair.LastBindingRequestReceivedAt).TotalSeconds > FAILED_TIMEOUT_PERIOD) - { - int duration = (int)DateTime.Now.Subtract(candidatePair.LastConnectedResponseAt).TotalSeconds; - logger.LogWarning("ICE RTP channel failed after {Duration:0.##}s {LocalCandidate}->{RemoteCandidate}.", duration, candidatePair.LocalCandidate.ToShortString(), candidatePair.RemoteCandidate.ToShortString()); - - IceConnectionState = RTCIceConnectionState.failed; - OnIceConnectionStateChange?.Invoke(IceConnectionState); - - _connectivityChecksTimer?.Dispose(); - } - else + if (DateTime.Now.Subtract(candidatePair.LastConnectedResponseAt).TotalSeconds > DISCONNECTED_TIMEOUT_PERIOD && + DateTime.Now.Subtract(candidatePair.LastBindingRequestReceivedAt).TotalSeconds > DISCONNECTED_TIMEOUT_PERIOD) { - if (DateTime.Now.Subtract(candidatePair.LastConnectedResponseAt).TotalSeconds > DISCONNECTED_TIMEOUT_PERIOD && - DateTime.Now.Subtract(candidatePair.LastBindingRequestReceivedAt).TotalSeconds > DISCONNECTED_TIMEOUT_PERIOD) - { - if (IceConnectionState == RTCIceConnectionState.connected) - { - int duration = (int)DateTime.Now.Subtract(candidatePair.LastConnectedResponseAt).TotalSeconds; - logger.LogWarning("ICE RTP channel disconnected after {Duration:0.##}s {LocalCandidate}->{RemoteCandidate}.", duration, candidatePair.LocalCandidate.ToShortString(), candidatePair.RemoteCandidate.ToShortString()); - - IceConnectionState = RTCIceConnectionState.disconnected; - OnIceConnectionStateChange?.Invoke(IceConnectionState); - } - } - else if (IceConnectionState != RTCIceConnectionState.connected) + if (IceConnectionState == RTCIceConnectionState.connected) { - logger.LogDebug("ICE RTP channel has re-connected {LocalCandidate}->{RemoteCandidate}.", candidatePair.LocalCandidate.ToShortString(), candidatePair.RemoteCandidate.ToShortString()); + var duration = (int)DateTime.Now.Subtract(candidatePair.LastConnectedResponseAt).TotalSeconds; + logger.LogIceChannelDisconnected(duration, candidatePair.LocalCandidate, candidatePair.RemoteCandidate); - // Re-connected. - IceConnectionState = RTCIceConnectionState.connected; + IceConnectionState = RTCIceConnectionState.disconnected; OnIceConnectionStateChange?.Invoke(IceConnectionState); } + } + else if (IceConnectionState != RTCIceConnectionState.connected) + { + logger.LogIceChannelReconnected(candidatePair.LocalCandidate, candidatePair.RemoteCandidate); - candidatePair.RequestTransactionID = candidatePair.RequestTransactionID ?? Crypto.GetRandomString(STUNHeader.TRANSACTION_ID_LENGTH); - candidatePair.LastCheckSentAt = DateTime.Now; - candidatePair.ChecksSent++; - - SendSTUNBindingRequest(candidatePair, false); + // Re-connected. + IceConnectionState = RTCIceConnectionState.connected; + OnIceConnectionStateChange?.Invoke(IceConnectionState); } + + candidatePair.RequestTransactionID = candidatePair.RequestTransactionID ?? Crypto.GetRandomString(STUNHeader.TRANSACTION_ID_LENGTH); + candidatePair.LastCheckSentAt = DateTime.Now; + candidatePair.ChecksSent++; + + SendStunBindingRequest(candidatePair, false); } } + } - /// - /// Processes a received STUN request or response. - /// - /// - /// Actions to take on a successful STUN response https://tools.ietf.org/html/rfc8445#section-7.2.5.3 - /// - Discover peer reflexive remote candidates as per https://tools.ietf.org/html/rfc8445#section-7.2.5.3.1. - /// - Construct a valid pair which means match a candidate pair in the check list and mark it as valid (since a successful STUN exchange - /// has now taken place on it). A new entry may need to be created for this pair for a peer reflexive candidate. - /// - Update state of candidate pair that generated the check to Succeeded. - /// - If the controlling candidate set the USE_CANDIDATE attribute then the ICE agent that receives the successful response sets the nominated - /// flag of the pair to true. Once the nominated flag is set it concludes the ICE processing for that component. - /// - /// The STUN message received. - /// The remote end point the STUN packet was received from. - public async Task ProcessStunMessage(STUNMessage stunMessage, IPEndPoint remoteEndPoint, bool wasRelayed) + /// + /// Processes a received STUN request or response. + /// + /// + /// Actions to take on a successful STUN response https://tools.ietf.org/html/rfc8445#section-7.2.5.3 - Discover + /// peer reflexive remote candidates as per https://tools.ietf.org/html/rfc8445#section-7.2.5.3.1. - Construct a + /// valid pair which means match a candidate pair in the check list and mark it as valid (since a successful STUN + /// exchange has now taken place on it). A new entry may need to be created for this pair for a peer reflexive + /// candidate. - Update state of candidate pair that generated the check to Succeeded. - If the controlling + /// candidate set the USE_CANDIDATE attribute then the ICE agent that receives the successful response sets the + /// nominated flag of the pair to true. Once the nominated flag is set it concludes the ICE processing for that + /// component. + /// + /// The STUN message received. + /// The remote end point the STUN packet was received from. + public async Task ProcessStunMessage(STUNMessage stunMessage, IPEndPoint remoteEndPoint, bool wasRelayed) + { + if (_closed) { - if (_closed) - { - return; - } + return; + } - remoteEndPoint = (!remoteEndPoint.Address.IsIPv4MappedToIPv6) ? remoteEndPoint : new IPEndPoint(remoteEndPoint.Address.MapToIPv4(), remoteEndPoint.Port); + remoteEndPoint = (!remoteEndPoint.Address.IsIPv4MappedToIPv6) ? remoteEndPoint : new IPEndPoint(remoteEndPoint.Address.MapToIPv4(), remoteEndPoint.Port); - // Check if the STUN message is for an ICE server check. - var iceServer = GetIceServerForTransactionID(stunMessage.Header.TransactionId); - if (iceServer != null) + // Check if the STUN message is for an ICE server check. + var iceServer = GetIceServerForTransactionID(stunMessage.Header.TransactionId); + if (iceServer is { }) + { + var candidatesAvailable = iceServer.GotStunResponse(stunMessage, remoteEndPoint); + if (candidatesAvailable) { - bool candidatesAvailable = iceServer.GotStunResponse(stunMessage, remoteEndPoint); - if (candidatesAvailable) - { - // Safe to wait here as the candidates from an ICE server will always be IP addresses only, - // no DNS lookups required. - await AddCandidatesForIceServer(iceServer).ConfigureAwait(false); - } + // Safe to wait here as the candidates from an ICE server will always be IP addresses only, + // no DNS lookups required. + await AddCandidatesForIceServer(iceServer).ConfigureAwait(false); } - else + } + else + { + // If the STUN message isn't for an ICE server then it needs to be matched against a remote + // candidate and a checklist entry and if no match a "peer reflexive" candidate may need to + // be created. + if (stunMessage.Header.MessageType == STUNMessageTypesEnum.BindingRequest) + { + GotStunBindingRequest(stunMessage, remoteEndPoint, wasRelayed); + } + else if (stunMessage.Header.MessageClass is STUNClassTypesEnum.ErrorResponse or STUNClassTypesEnum.SuccessResponse) { - // If the STUN message isn't for an ICE server then it needs to be matched against a remote - // candidate and a checklist entry and if no match a "peer reflexive" candidate may need to - // be created. - if (stunMessage.Header.MessageType == STUNMessageTypesEnum.BindingRequest) + // Correlate with request using transaction ID as per https://tools.ietf.org/html/rfc8445#section-7.2.5. + var matchingChecklistEntry = GetChecklistEntryForStunResponse(stunMessage.Header.TransactionId); + + if (matchingChecklistEntry is null) { - GotStunBindingRequest(stunMessage, remoteEndPoint, wasRelayed); + if (IceConnectionState != RTCIceConnectionState.connected) + { + // If the channel is connected a mismatched txid can result if the connection is very busy, i.e. streaming 1080p video, + // it's likely to only be transient and does not impact the connection state. + logger.LogIceStunRequestTxIdMismatch(stunMessage.Header.MessageType); + } } - else if (stunMessage.Header.MessageClass == STUNClassTypesEnum.ErrorResponse || - stunMessage.Header.MessageClass == STUNClassTypesEnum.SuccessResponse) + else { - // Correlate with request using transaction ID as per https://tools.ietf.org/html/rfc8445#section-7.2.5. - var matchingChecklistEntry = GetChecklistEntryForStunResponse(stunMessage.Header.TransactionId); + matchingChecklistEntry.GotStunResponse(stunMessage, remoteEndPoint); - if (matchingChecklistEntry == null) + if (_checklistState == ChecklistState.Running && + stunMessage.Header.MessageType == STUNMessageTypesEnum.BindingSuccessResponse) { - if (IceConnectionState != RTCIceConnectionState.connected) + if (matchingChecklistEntry.Nominated) { - // If the channel is connected a mismatched txid can result if the connection is very busy, i.e. streaming 1080p video, - // it's likely to only be transient and does not impact the connection state. - logger.LogWarning("ICE RTP channel received a STUN {MessageType} with a transaction ID that did not match a checklist entry.", stunMessage.Header.MessageType); - } - } - else - { - matchingChecklistEntry.GotStunResponse(stunMessage, remoteEndPoint); + logger.LogIceChecklistNominatedResponse(matchingChecklistEntry.RemoteCandidate); - if (_checklistState == ChecklistState.Running && - stunMessage.Header.MessageType == STUNMessageTypesEnum.BindingSuccessResponse) + // This is the response to a connectivity check that had the "UseCandidate" attribute set. + SetNominatedEntry(matchingChecklistEntry); + } + else if (IsController) { - if (matchingChecklistEntry.Nominated) - { - logger.LogDebug("ICE RTP channel remote peer nominated entry from binding response {RemoteCandidate}", matchingChecklistEntry.RemoteCandidate.ToShortString()); - - // This is the response to a connectivity check that had the "UseCandidate" attribute set. - SetNominatedEntry(matchingChecklistEntry); - } - else if (IsController) - { - logger.LogDebug("ICE RTP channel binding response state {State} as Controller for {RemoteCandidate}", matchingChecklistEntry.State, matchingChecklistEntry.RemoteCandidate.ToShortString()); - ProcessNominateLogicAsController(matchingChecklistEntry); - } + logger.LogIceChecklistBindingResponse(matchingChecklistEntry.State, matchingChecklistEntry.RemoteCandidate); + ProcessNominateLogicAsController(matchingChecklistEntry); } } } - else - { - logger.LogWarning("ICE RTP channel received an unexpected STUN message {MessageType} from {RemoteEndPoint}.\nJson: {StunMessage}", stunMessage.Header.MessageType, remoteEndPoint, stunMessage); - } + } + else + { + logger.LogDtlsUnexpectedStunMessage(stunMessage.Header.MessageType, remoteEndPoint, stunMessage); } } + } - /// - /// Handles Nominate logic when Agent is the controller - /// - /// Optional initial ChecklistEntry. - private void ProcessNominateLogicAsController(ChecklistEntry possibleMatchingCheckEntry) + /// + /// Handles Nominate logic when Agent is the controller + /// + /// Optional initial ChecklistEntry. + private void ProcessNominateLogicAsController(ChecklistEntry? possibleMatchingCheckEntry) + { + if (IsController && (NominatedEntry is null || !NominatedEntry.Nominated || NominatedEntry.State != ChecklistEntryState.Succeeded)) { - if (IsController && (NominatedEntry == null || !NominatedEntry.Nominated || NominatedEntry.State != ChecklistEntryState.Succeeded)) + lock (_checklistLock) { - lock (_checklistLock) + _checklist.Sort(); + + var findBetterOptionOrWait = possibleMatchingCheckEntry is null; //|| possibleMatchingCheckEntry.RemoteCandidate.type == RTCIceCandidateType.relay; + var nominatedCandidate = _checklist.Find( + x => x.Nominated + && x.State == ChecklistEntryState.Succeeded + && (x.LastCheckSentAt == DateTime.MinValue || + DateTime.Now.Subtract(x.LastCheckSentAt).TotalSeconds <= FAILED_TIMEOUT_PERIOD)); + + //We already have a good candidate, discard our succeded candidate + if (nominatedCandidate is { } /*&& nominatedCandidate.RemoteCandidate.type != RTCIceCandidateType.relay*/) { - _checklist.Sort(); + possibleMatchingCheckEntry = null; + findBetterOptionOrWait = false; + } - var findBetterOptionOrWait = possibleMatchingCheckEntry == null; //|| possibleMatchingCheckEntry.RemoteCandidate.type == RTCIceCandidateType.relay; - var nominatedCandidate = _checklist.Find( - x => x.Nominated - && x.State == ChecklistEntryState.Succeeded - && (x.LastCheckSentAt == DateTime.MinValue || - DateTime.Now.Subtract(x.LastCheckSentAt).TotalSeconds <= FAILED_TIMEOUT_PERIOD)); + if (findBetterOptionOrWait) + { + //Search for another succeded non-nominated entries with better priority over our current object. + var betterOptionEntry = _checklist.Find(x => + x.State == ChecklistEntryState.Succeeded && + !x.Nominated && + (possibleMatchingCheckEntry is null || + (x.Priority > possibleMatchingCheckEntry.Priority /*&& x.RemoteCandidate.type != RTCIceCandidateType.relay*/) || + possibleMatchingCheckEntry.State != ChecklistEntryState.Succeeded)); - //We already have a good candidate, discard our succeded candidate - if (nominatedCandidate != null /*&& nominatedCandidate.RemoteCandidate.type != RTCIceCandidateType.relay*/) + if (betterOptionEntry is { }) { - possibleMatchingCheckEntry = null; - findBetterOptionOrWait = false; + possibleMatchingCheckEntry = betterOptionEntry; + findBetterOptionOrWait = false; //possibleMatchingCheckEntry.RemoteCandidate.type == RTCIceCandidateType.relay; } + //if we still need to find a better option, we will search for matching entries with high priority that still processing if (findBetterOptionOrWait) { - //Search for another succeded non-nominated entries with better priority over our current object. - var betterOptionEntry = _checklist.Find(x => - x.State == ChecklistEntryState.Succeeded && - !x.Nominated && - (possibleMatchingCheckEntry == null || - (x.Priority > possibleMatchingCheckEntry.Priority /*&& x.RemoteCandidate.type != RTCIceCandidateType.relay*/) || - possibleMatchingCheckEntry.State != ChecklistEntryState.Succeeded)); - - if (betterOptionEntry != null) - { - possibleMatchingCheckEntry = betterOptionEntry; - findBetterOptionOrWait = false; //possibleMatchingCheckEntry.RemoteCandidate.type == RTCIceCandidateType.relay; - } + var waitOptionEntry = _checklist.Find(x => + (x.State == ChecklistEntryState.InProgress || x.State == ChecklistEntryState.Waiting) && + (possibleMatchingCheckEntry is null || + (x.Priority > possibleMatchingCheckEntry.Priority /*&& x.RemoteCandidate.type != RTCIceCandidateType.relay*/) || + possibleMatchingCheckEntry.State != ChecklistEntryState.Succeeded)); - //if we still need to find a better option, we will search for matching entries with high priority that still processing - if (findBetterOptionOrWait) + if (waitOptionEntry is { }) { - var waitOptionEntry = _checklist.Find(x => - (x.State == ChecklistEntryState.InProgress || x.State == ChecklistEntryState.Waiting) && - (possibleMatchingCheckEntry == null || - (x.Priority > possibleMatchingCheckEntry.Priority /*&& x.RemoteCandidate.type != RTCIceCandidateType.relay*/) || - possibleMatchingCheckEntry.State != ChecklistEntryState.Succeeded)); - - if (waitOptionEntry != null) - { - possibleMatchingCheckEntry = null; - } + possibleMatchingCheckEntry = null; } } } - - //Nominate Candidate if we pass in all heuristic checks from previous algorithm - if (possibleMatchingCheckEntry != null && possibleMatchingCheckEntry.State == ChecklistEntryState.Succeeded) - { - possibleMatchingCheckEntry.Nominated = true; - SendConnectivityCheck(possibleMatchingCheckEntry, true); - } } - /*if (IsController && !_checklist.Any(x => x.Nominated)) + //Nominate Candidate if we pass in all heuristic checks from previous algorithm + if (possibleMatchingCheckEntry is { State: ChecklistEntryState.Succeeded }) { - // If we are the controlling ICE agent it's up to us to decide when to nominate a candidate pair to use for the connection. - // For the lack of a more sophisticated approach use whichever pair gets the first successful STUN exchange. If needs be - // the selection algorithm can improve over time. - - //Find high priority succeded event - _checklist.Sort(); - var matchingCheckEntry = _checklist.Find(x => x.State == ChecklistEntryState.Succeeded); + possibleMatchingCheckEntry.Nominated = true; + SendConnectivityCheck(possibleMatchingCheckEntry, true); + } + } + } - //We can nominate this entry (if exists) - if (matchingCheckEntry != null) - { - matchingCheckEntry.Nominated = true; - SendConnectivityCheck(matchingCheckEntry, true); - } - }*/ + /// + /// Handles STUN binding requests received from remote candidates as part of the ICE connectivity checks. + /// + /// The binding request received. + /// The end point the request was received from. + /// + /// True of the request was relayed via the TURN server in use by this ICE channel (i.e. the ICE server that this + /// channel is acting as the client with). + /// + private void GotStunBindingRequest(STUNMessage bindingRequest, IPEndPoint remoteEndPoint, bool wasRelayed) + { + if (_closed) + { + return; } - /// - /// Handles STUN binding requests received from remote candidates as part of the ICE connectivity checks. - /// - /// The binding request received. - /// The end point the request was received from. - /// True of the request was relayed via the TURN server in use - /// by this ICE channel (i.e. the ICE server that this channel is acting as the client with). - private void GotStunBindingRequest(STUNMessage bindingRequest, IPEndPoint remoteEndPoint, bool wasRelayed) + if (_policy == RTCIceTransportPolicy.relay && !wasRelayed) { - if (_closed) + // If the policy is "relay only" then direct binding requests are not accepted. + logger.LogIceBindingRequestRejected(remoteEndPoint); + + var stunErrResponse = new STUNMessage(STUNMessageTypesEnum.BindingErrorResponse) { - return; - } + Header = { TransactionId = bindingRequest.Header.TransactionId } + }; + + var bufferSize = stunErrResponse.GetByteBufferSize(ReadOnlySpan.Empty, addFingerprint: false); + var rentedBuffer = MemoryPool.Shared.Rent(bufferSize); + var memory = rentedBuffer.Memory.Slice(0, bufferSize); + + stunErrResponse.WriteToBuffer(memory.Span, ReadOnlySpan.Empty, addFingerprint: false); + Send(RTPChannelSocketsEnum.RTP, remoteEndPoint, memory, rentedBuffer); - if (_policy == RTCIceTransportPolicy.relay && !wasRelayed) + OnStunMessageSent?.Invoke(stunErrResponse, remoteEndPoint, false); + } + else + { + var result = bindingRequest.CheckIntegrity(Encoding.UTF8.GetBytes(LocalIcePassword)); + + if (!result) { - // If the policy is "relay only" then direct binding requests are not accepted. - logger.LogWarning("ICE RTP channel rejecting non-relayed STUN binding request from {RemoteEndPoint}.", remoteEndPoint); + // Send STUN error response. + logger.LogIceStunBindingRequestFailed(remoteEndPoint); + + var stunErrResponse = new STUNMessage(STUNMessageTypesEnum.BindingErrorResponse) + { + Header = { TransactionId = bindingRequest.Header.TransactionId } + }; + + var bufferSize = stunErrResponse.GetByteBufferSize(ReadOnlySpan.Empty, addFingerprint: false); + var rentedBuffer = MemoryPool.Shared.Rent(bufferSize); + var memory = rentedBuffer.Memory.Slice(0, bufferSize); - STUNMessage stunErrResponse = new STUNMessage(STUNMessageTypesEnum.BindingErrorResponse); - stunErrResponse.Header.TransactionId = bindingRequest.Header.TransactionId; - Send(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunErrResponse.ToByteBuffer(null, false)); + stunErrResponse.WriteToBuffer(memory.Span, ReadOnlySpan.Empty, addFingerprint: false); + Send(RTPChannelSocketsEnum.RTP, remoteEndPoint, memory, rentedBuffer); OnStunMessageSent?.Invoke(stunErrResponse, remoteEndPoint, false); } else { - bool result = bindingRequest.CheckIntegrity(Encoding.UTF8.GetBytes(LocalIcePassword)); + ChecklistEntry? matchingChecklistEntry = null; - if (!result) + // Apply the source translator if one is set. This reconciles hairpin + // scenarios where a peer reaches us through a TURN relay running on the + // same machine: the observed source IP is a local interface address but + // the corresponding remote candidate was advertised with the relay's + // public IP. Without translation we'd treat the address as a new prflx + // and end up with a phantom candidate that doesn't have a return path. + var canonicalEndPoint = RemoteEndpointTranslator?.Invoke(remoteEndPoint) ?? remoteEndPoint; + + // Find the checklist entry for this remote candidate and update its status. + lock (_checklistLock) { - // Send STUN error response. - logger.LogWarning("ICE RTP channel STUN binding request from {RemoteEndPoint} failed an integrity check, rejecting.", remoteEndPoint); - STUNMessage stunErrResponse = new STUNMessage(STUNMessageTypesEnum.BindingErrorResponse); - stunErrResponse.Header.TransactionId = bindingRequest.Header.TransactionId; - Send(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunErrResponse.ToByteBuffer(null, false)); + // The matching checklist entry is chosen as: + // - The entry that has a remote candidate with an end point that matches the endpoint this STUN request came from, + // - And if the STUN request was relayed through a TURN server then only match is the checklist local candidate is + // also a relay type. It is possible for the same remote end point to send STUN requests directly and via a TURN server. + foreach (var entry in _checklist) + { + if ((entry.RemoteCandidate.IsEquivalentEndPoint(RTCIceProtocol.udp, remoteEndPoint) + || entry.RemoteCandidate.IsEquivalentEndPoint(RTCIceProtocol.udp, canonicalEndPoint)) + && (!wasRelayed || entry.LocalCandidate.type == RTCIceCandidateType.relay)) + { + matchingChecklistEntry = entry; + break; + } + } + } - OnStunMessageSent?.Invoke(stunErrResponse, remoteEndPoint, false); + bool IsUnknownRemoteCandidate(IPEndPoint remoteEndPoint, IPEndPoint canonicalEndPoint) + { + foreach (var candidate in _remoteCandidates) + { + if (candidate.IsEquivalentEndPoint(RTCIceProtocol.udp, remoteEndPoint) + || candidate.IsEquivalentEndPoint(RTCIceProtocol.udp, canonicalEndPoint)) + { + return false; + } + } + return true; } - else + + if (matchingChecklistEntry is null + && ((_remoteCandidates is null) || IsUnknownRemoteCandidate(remoteEndPoint, canonicalEndPoint))) { - ChecklistEntry matchingChecklistEntry = null; + // This STUN request has come from a socket not in the remote ICE candidates list. + // Add a new remote peer reflexive candidate. + var peerRflxCandidate = new RTCIceCandidate(new RTCIceCandidateInit()); + peerRflxCandidate.SetAddressProperties(RTCIceProtocol.udp, remoteEndPoint.Address, (ushort)remoteEndPoint.Port, RTCIceCandidateType.prflx, null, 0); + peerRflxCandidate.SetDestinationEndPoint(remoteEndPoint); + logger.LogIcePeerReflexAdded(remoteEndPoint); - // Apply the source translator if one is set. This reconciles hairpin - // scenarios where a peer reaches us through a TURN relay running on the - // same machine: the observed source IP is a local interface address but - // the corresponding remote candidate was advertised with the relay's - // public IP. Without translation we'd treat the address as a new prflx - // and end up with a phantom candidate that doesn't have a return path. - var canonicalEndPoint = RemoteEndpointTranslator?.Invoke(remoteEndPoint) ?? remoteEndPoint; + Debug.Assert(_remoteCandidates is { }); + _remoteCandidates.Add(peerRflxCandidate); - // Find the checklist entry for this remote candidate and update its status. - lock (_checklistLock) + // Add a new entry to the check list for the new peer reflexive candidate. + var localCandidate = wasRelayed ? _relayChecklistCandidate : _localChecklistCandidate; + Debug.Assert(localCandidate is { }); + var entry = new ChecklistEntry( + localCandidate, + peerRflxCandidate, + IsController); + entry.State = ChecklistEntryState.Waiting; + + if (wasRelayed) { - // The matching checklist entry is chosen as: - // - The entry that has a remote candidate with an end point that matches the endpoint this STUN request came from, - // - And if the STUN request was relayed through a TURN server then only match is the checklist local candidate is - // also a relay type. It is possible for the same remote end point to send STUN requests directly and via a TURN server. - matchingChecklistEntry = _checklist.Where(x => - (x.RemoteCandidate.IsEquivalentEndPoint(RTCIceProtocol.udp, remoteEndPoint) || - x.RemoteCandidate.IsEquivalentEndPoint(RTCIceProtocol.udp, canonicalEndPoint)) && - (!wasRelayed || x.LocalCandidate.type == RTCIceCandidateType.relay) - ).FirstOrDefault(); + // No need to send a TURN permissions request given this request was already successfully relayed. + entry.TurnPermissionsRequestSent = 1; + entry.TurnPermissionsResponseAt = DateTime.Now; } - if (matchingChecklistEntry == null && - (_remoteCandidates == null || - (!_remoteCandidates.Any(x => x.IsEquivalentEndPoint(RTCIceProtocol.udp, remoteEndPoint)) && - !_remoteCandidates.Any(x => x.IsEquivalentEndPoint(RTCIceProtocol.udp, canonicalEndPoint))))) - { - // This STUN request has come from a socket not in the remote ICE candidates list. - // Add a new remote peer reflexive candidate. - RTCIceCandidate peerRflxCandidate = new RTCIceCandidate(new RTCIceCandidateInit()); - peerRflxCandidate.SetAddressProperties(RTCIceProtocol.udp, remoteEndPoint.Address, (ushort)remoteEndPoint.Port, RTCIceCandidateType.prflx, null, 0); - peerRflxCandidate.SetDestinationEndPoint(remoteEndPoint); - logger.LogDebug("Adding peer reflex ICE candidate for {RemoteEndPoint}.", remoteEndPoint); - _remoteCandidates.Add(peerRflxCandidate); - - // Add a new entry to the check list for the new peer reflexive candidate. - ChecklistEntry entry = new ChecklistEntry(wasRelayed ? _relayChecklistCandidate : _localChecklistCandidate, - peerRflxCandidate, IsController); - entry.State = ChecklistEntryState.Waiting; + AddChecklistEntry(entry); - if (wasRelayed) + matchingChecklistEntry = entry; + } + else if (matchingChecklistEntry == null && !ReferenceEquals(canonicalEndPoint, remoteEndPoint)) + { + // The canonical endpoint (post-translation) matches a remote candidate + // but no checklist entry exists for it yet against this local candidate. + // Find or create one so the nomination/connectivity check has somewhere + // to live without inventing a prflx. + RTCIceCandidate GetCanonicalCandidateOrThrow(IPEndPoint endPoint) + { + foreach (var candidate in _remoteCandidates) { - // No need to send a TURN permissions request given this request was already successfully relayed. - entry.TurnPermissionsRequestSent = 1; - entry.TurnPermissionsResponseAt = DateTime.Now; + if (candidate.IsEquivalentEndPoint(RTCIceProtocol.udp, endPoint)) + { + return candidate; + } } - AddChecklistEntry(entry); - - matchingChecklistEntry = entry; + throw new InvalidOperationException("No matching canonical remote candidate was found."); } - else if (matchingChecklistEntry == null && !ReferenceEquals(canonicalEndPoint, remoteEndPoint)) + + var canonicalCandidate = GetCanonicalCandidateOrThrow(canonicalEndPoint); + + ChecklistEntry? entry = null; + lock (_checklistLock) { - // The canonical endpoint (post-translation) matches a remote candidate - // but no checklist entry exists for it yet against this local candidate. - // Find or create one so the nomination/connectivity check has somewhere - // to live without inventing a prflx. - var canonicalCandidate = _remoteCandidates.First( - x => x.IsEquivalentEndPoint(RTCIceProtocol.udp, canonicalEndPoint)); - - ChecklistEntry entry; - lock (_checklistLock) - { - entry = _checklist.FirstOrDefault( - x => ReferenceEquals(x.RemoteCandidate, canonicalCandidate) - && (!wasRelayed || x.LocalCandidate.type == RTCIceCandidateType.relay)); - } - if (entry == null) + foreach (var checklistEntry in _checklist) { - entry = new ChecklistEntry(wasRelayed ? _relayChecklistCandidate : _localChecklistCandidate, - canonicalCandidate, IsController); - entry.State = ChecklistEntryState.Waiting; - if (wasRelayed) + if (ReferenceEquals(checklistEntry.RemoteCandidate, canonicalCandidate) + && (!wasRelayed || checklistEntry.LocalCandidate.type == RTCIceCandidateType.relay)) { - entry.TurnPermissionsRequestSent = 1; - entry.TurnPermissionsResponseAt = DateTime.Now; + entry = checklistEntry; + break; } - AddChecklistEntry(entry); } - matchingChecklistEntry = entry; } - if (matchingChecklistEntry == null) + if (entry == null) { - logger.LogWarning("ICE RTP channel STUN request matched a remote candidate but NOT a checklist entry."); - STUNMessage stunErrResponse = new STUNMessage(STUNMessageTypesEnum.BindingErrorResponse); - stunErrResponse.Header.TransactionId = bindingRequest.Header.TransactionId; - Send(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunErrResponse.ToByteBuffer(null, false)); - - OnStunMessageSent?.Invoke(stunErrResponse, remoteEndPoint, false); + var localCandidate = wasRelayed ? _relayChecklistCandidate : _localChecklistCandidate; + Debug.Assert(localCandidate is { }); + entry = new ChecklistEntry(localCandidate, canonicalCandidate, IsController); + entry.State = ChecklistEntryState.Waiting; + if (wasRelayed) + { + entry.TurnPermissionsRequestSent = 1; + entry.TurnPermissionsResponseAt = DateTime.Now; + } + AddChecklistEntry(entry); } - else + matchingChecklistEntry = entry; + } + + if (matchingChecklistEntry is null) + { + logger.LogIceStunRequestMismatch(); + + var stunErrResponse = new STUNMessage(STUNMessageTypesEnum.BindingErrorResponse) + { + Header = { TransactionId = bindingRequest.Header.TransactionId } + }; + + var bufferSize = stunErrResponse.GetByteBufferSize(ReadOnlySpan.Empty, addFingerprint: false); + var rentedBuffer = MemoryPool.Shared.Rent(bufferSize); + var memory = rentedBuffer.Memory.Slice(0, bufferSize); + + stunErrResponse.WriteToBuffer(memory.Span, ReadOnlySpan.Empty, addFingerprint: false); + Send(RTPChannelSocketsEnum.RTP, remoteEndPoint, memory, rentedBuffer); + + OnStunMessageSent?.Invoke(stunErrResponse, remoteEndPoint, false); + } + else + { + // The UseCandidate attribute is only meant to be set by the "Controller" peer. This implementation + // will accept it irrespective of the peer roles. If the remote peer wants us to use a certain remote + // end point then so be it. + if (bindingRequest.Attributes.Exists(static x => x.AttributeType == STUNAttributeTypesEnum.UseCandidate)) { - // The UseCandidate attribute is only meant to be set by the "Controller" peer. This implementation - // will accept it irrespective of the peer roles. If the remote peer wants us to use a certain remote - // end point then so be it. - if (bindingRequest.Attributes.Any(x => x.AttributeType == STUNAttributeTypesEnum.UseCandidate)) + if (IceConnectionState != RTCIceConnectionState.connected) { - if (IceConnectionState != RTCIceConnectionState.connected) - { - // If we are the "controlled" agent and get a "use candidate" attribute that sets the matching candidate as nominated - // as per https://tools.ietf.org/html/rfc8445#section-7.3.1.5. - logger.LogDebug("ICE RTP channel remote peer nominated entry from binding request: {RemoteCandidate}.", matchingChecklistEntry.RemoteCandidate.ToShortString()); - SetNominatedEntry(matchingChecklistEntry); - } - else if (matchingChecklistEntry.RemoteCandidate.ToString() != NominatedEntry.RemoteCandidate.ToString()) + // If we are the "controlled" agent and get a "use candidate" attribute that sets the matching candidate as nominated + // as per https://tools.ietf.org/html/rfc8445#section-7.3.1.5. + logger.LogIceChecklistNominatedBinding(matchingChecklistEntry.RemoteCandidate); + + SetNominatedEntry(matchingChecklistEntry); + } + else + { + Debug.Assert(NominatedEntry is { }); + if (!matchingChecklistEntry.RemoteCandidate.Equals(NominatedEntry.RemoteCandidate)) { // The remote peer is changing the nominated candidate. - logger.LogDebug("ICE RTP channel remote peer nominated a new candidate: {RemoteCandidate}.", matchingChecklistEntry.RemoteCandidate.ToShortString()); + logger.LogIceNominatedNewCandidate(matchingChecklistEntry.RemoteCandidate); SetNominatedEntry(matchingChecklistEntry); } } + } + + matchingChecklistEntry.LastBindingRequestReceivedAt = DateTime.Now; - matchingChecklistEntry.LastBindingRequestReceivedAt = DateTime.Now; + var stunResponse = new STUNMessage(STUNMessageTypesEnum.BindingSuccessResponse) + { + Header = { TransactionId = bindingRequest.Header.TransactionId } + }; + + stunResponse.AddXORMappedAddressAttribute(remoteEndPoint.Address, remoteEndPoint.Port); + + var bufferSize = stunResponse.GetByteBufferSizeStringKey(LocalIcePassword, addFingerprint: true); + var rentedBuffer = ArrayPool.Shared.Rent(bufferSize); - STUNMessage stunResponse = new STUNMessage(STUNMessageTypesEnum.BindingSuccessResponse); - stunResponse.Header.TransactionId = bindingRequest.Header.TransactionId; - stunResponse.AddXORMappedAddressAttribute(remoteEndPoint.Address, remoteEndPoint.Port); - byte[] stunRespBytes = stunResponse.ToByteBufferStringKey(LocalIcePassword, true); + try + { + stunResponse.WriteToBufferStringKey(rentedBuffer.AsSpan(0, bufferSize), LocalIcePassword, addFingerprint: true); + var stunRespMemory = rentedBuffer.AsMemory(0, bufferSize); if (wasRelayed) { + using var activity = NetIceActivitySource.StartStunMessageSentActivity( + stunResponse, + remoteEndPoint, + null); + + Debug.Assert(matchingChecklistEntry?.LocalCandidate?.IceServer is { }); + Debug.Assert(matchingChecklistEntry?.LocalCandidate?.IceServer.ServerEndPoint is { }); var protocol = matchingChecklistEntry.LocalCandidate.IceServer.Protocol; - SendRelay(protocol, remoteEndPoint, stunRespBytes, matchingChecklistEntry.LocalCandidate.IceServer.ServerEndPoint, matchingChecklistEntry.LocalCandidate.IceServer); + SendRelay( + protocol, + remoteEndPoint, + stunRespMemory, + null, + matchingChecklistEntry.LocalCandidate.IceServer.ServerEndPoint, + matchingChecklistEntry.LocalCandidate.IceServer); + OnStunMessageSent?.Invoke(stunResponse, remoteEndPoint, true); } else { - Send(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunRespBytes); + using var activity = NetIceActivitySource.StartStunMessageSentActivity( + stunResponse, + remoteEndPoint, + null); + + Send(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunRespMemory, null); OnStunMessageSent?.Invoke(stunResponse, remoteEndPoint, false); } } - } - } - } - - /// - /// Attempts to get the matching checklist entry for the transaction ID in a STUN response. - /// - /// The STUN response transaction ID. - /// A checklist entry or null if there was no match. - private ChecklistEntry GetChecklistEntryForStunResponse(byte[] transactionID) - { - string txID = Encoding.ASCII.GetString(transactionID); - ChecklistEntry matchingChecklistEntry = null; - - lock (_checklistLock) - { - matchingChecklistEntry = _checklist.Where(x => x.IsTransactionIDMatch(txID)).FirstOrDefault(); + finally + { + ArrayPool.Shared.Return(rentedBuffer); + } + } } - - return matchingChecklistEntry; } + } + + /// + /// Attempts to get the matching checklist entry for the transaction ID in a STUN response. + /// + /// The STUN response transaction ID. + /// A checklist entry or null if there was no match. + private ChecklistEntry? GetChecklistEntryForStunResponse(byte[] transactionID) + { + var txID = Encoding.ASCII.GetString(transactionID); - /// - /// Checks a STUN response transaction ID to determine if it matches a check being carried - /// out for an ICE server. - /// - /// The transaction ID from the STUN response. - /// If found a matching state object or null if not. - private IceServer GetIceServerForTransactionID(byte[] transactionID) + lock (_checklistLock) { - if (_iceServerResolver.IceServers.Count() == 0) - { - return null; - } - else + foreach (var entry in _checklist) { - string txID = Encoding.ASCII.GetString(transactionID); - - var entry = _iceServerResolver.IceServers - .Where(x => x.Value.IsTransactionIDMatch(txID)) - .SingleOrDefault(); - - if (!entry.Equals(default(KeyValuePair))) - { - return entry.Value; - } - else + if (entry.IsTransactionIDMatch(txID)) { - return null; + return entry; } } } - /// - /// Sends a STUN binding request to an ICE server. - /// - /// The ICE server to send the request to. - /// The result of the send attempt. Note this is the return code from the - /// socket send call and not the result code from the STUN response. - private SocketError SendStunBindingRequest(IceServer iceServer) - { - iceServer.OutstandingRequestsSent += 1; - iceServer.LastRequestSentAt = DateTime.Now; + return null; + } - // Send a STUN binding request. - STUNMessage stunRequest = new STUNMessage(STUNMessageTypesEnum.BindingRequest); - stunRequest.Header.TransactionId = Encoding.ASCII.GetBytes(iceServer.TransactionID); + /// + /// Checks a STUN response transaction ID to determine if it matches a check being carried out for an ICE server. + /// + /// The transaction ID from the STUN response. + /// If found a matching state object or null if not. + private IceServer? GetIceServerForTransactionID(byte[] transactionID) + { + if (_iceServerResolver.IceServers.Count == 0) + { + return null; + } - byte[] stunReqBytes = null; + var txID = Encoding.ASCII.GetString(transactionID); - if (iceServer.Nonce != null && iceServer.Realm != null && iceServer._username != null && iceServer._password != null) - { - stunReqBytes = GetAuthenticatedStunRequest(stunRequest, iceServer._username, iceServer.Realm, iceServer._password, iceServer.Nonce); - } - else + foreach (var (_, iceServer) in _iceServerResolver.IceServers) + { + if (iceServer.IsTransactionIDMatch(txID)) { - stunReqBytes = stunRequest.ToByteBuffer(null, false); + return iceServer; } + } - var sendResult = iceServer.Protocol == ProtocolType.Tcp ? - SendOverTCP(iceServer, stunReqBytes) : - base.Send(RTPChannelSocketsEnum.RTP, iceServer.ServerEndPoint, stunReqBytes); + return null; + } - if (sendResult != SocketError.Success) - { - logger.LogWarning("Error sending STUN server binding request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.", - iceServer.OutstandingRequestsSent, iceServer._uri, iceServer.ServerEndPoint, sendResult); - } - else - { - OnStunMessageSent?.Invoke(stunRequest, iceServer.ServerEndPoint, false); - } + /// + /// Sends a STUN binding request to an ICE server. + /// + /// The ICE server to send the request to. + /// + /// The result of the send attempt. Note this is the return code from the socket send call and not the result code + /// from the STUN response. + /// + private SocketError SendStunBindingRequest(IceServer iceServer) + { + iceServer.OutstandingRequestsSent += 1; + iceServer.LastRequestSentAt = DateTime.Now; - return sendResult; - } + Debug.Assert(iceServer.TransactionID is { }); - /// - /// Sends an allocate request to a TURN server. - /// - /// The TURN server to send the request to. - /// The result from the socket send (not the response code from the TURN server). - private SocketError SendTurnAllocateRequest(IceServer iceServer) + // Send a STUN binding request. + var stunRequest = new STUNMessage(STUNMessageTypesEnum.BindingRequest) { - iceServer.OutstandingRequestsSent += 1; - iceServer.LastRequestSentAt = DateTime.Now; + Header = + { + TransactionId = Encoding.ASCII.GetBytes(iceServer.TransactionID), + }, + }; - STUNMessage allocateRequest = new STUNMessage(STUNMessageTypesEnum.Allocate); - allocateRequest.Header.TransactionId = Encoding.ASCII.GetBytes(iceServer.TransactionID); - allocateRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.RequestedTransport, STUNAttributeConstants.UdpTransportType)); - allocateRequest.Attributes.Add( - new STUNAttribute(STUNAttributeTypesEnum.RequestedAddressFamily, - iceServer.ServerEndPoint.AddressFamily == AddressFamily.InterNetwork ? - STUNAttributeConstants.IPv4AddressFamily : STUNAttributeConstants.IPv6AddressFamily)); + var iceServerUri = iceServer.Uri; + var iceServerEndPoint = iceServer.ServerEndPoint; - byte[] allocateReqBytes = null; + Debug.Assert(iceServerEndPoint is { }); - if (iceServer.Nonce != null && iceServer.Realm != null && iceServer._username != null && iceServer._password != null) - { - allocateReqBytes = GetAuthenticatedStunRequest(allocateRequest, iceServer._username, iceServer.Realm, iceServer._password, iceServer.Nonce); - } - else - { - allocateReqBytes = allocateRequest.ToByteBuffer(null, false); - } + using var activity = NetIceActivitySource.StartStunMessageSentActivity( + stunRequest, + iceServerEndPoint, + iceServerUri, + iceServer.Realm, + iceServer.Nonce); - var sendResult = iceServer.Protocol == ProtocolType.Tcp ? - SendOverTCP(iceServer, allocateReqBytes) : - base.Send(RTPChannelSocketsEnum.RTP, iceServer.ServerEndPoint, allocateReqBytes); + logger.LogSendStunBindingRequest(iceServerUri, iceServerEndPoint, stunRequest); - if (sendResult != SocketError.Success) - { - logger.LogWarning("Error sending TURN Allocate request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.", - iceServer.OutstandingRequestsSent, iceServer._uri, iceServer.ServerEndPoint, sendResult); - } - else - { - OnStunMessageSent?.Invoke(allocateRequest, iceServer.ServerEndPoint, false); - } + var sendResult = SendStunMessage(stunRequest, iceServer); - return sendResult; - } + if (sendResult != SocketError.Success) + { + activity?.SetStatus(ActivityStatusCode.Error, sendResult.ToStringFast()); - /// - /// Sends an allocate request to a TURN server. - /// - /// The TURN server to send the request to. - /// The result from the socket send (not the response code from the TURN server). - private SocketError SendTurnRefreshRequest(IceServer iceServer) + logger.LogIceStunServerBindingSendError(iceServer.OutstandingRequestsSent, iceServerUri, iceServerEndPoint, sendResult); + } + else { - iceServer.OutstandingRequestsSent += 1; - iceServer.LastRequestSentAt = DateTime.Now; + OnStunMessageSent?.Invoke(stunRequest, iceServerEndPoint, false); + } - STUNMessage allocateRequest = new STUNMessage(STUNMessageTypesEnum.Refresh); - allocateRequest.Header.TransactionId = Encoding.ASCII.GetBytes(iceServer.TransactionID); - //allocateRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Lifetime, 3600)); - allocateRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Lifetime, ALLOCATION_TIME_TO_EXPIRY_VALUE)); + return sendResult; + } - allocateRequest.Attributes.Add( - new STUNAttribute(STUNAttributeTypesEnum.RequestedAddressFamily, - iceServer.ServerEndPoint.AddressFamily == AddressFamily.InterNetwork ? - STUNAttributeConstants.IPv4AddressFamily : STUNAttributeConstants.IPv6AddressFamily)); + /// + /// Sends an allocate request to a TURN server. + /// + /// The TURN server to send the request to. + /// The result from the socket send (not the response code from the TURN server). + private SocketError SendTurnAllocateRequest(IceServer iceServer) + { + iceServer.OutstandingRequestsSent += 1; + iceServer.LastRequestSentAt = DateTime.Now; - byte[] allocateReqBytes = null; + Debug.Assert(iceServer.TransactionID is { }); + Debug.Assert(iceServer.ServerEndPoint is { }); - if (iceServer.Nonce != null && iceServer.Realm != null && iceServer._username != null && iceServer._password != null) + var allocateRequest = new STUNMessage(STUNMessageTypesEnum.Allocate) + { + Header = { - allocateReqBytes = GetAuthenticatedStunRequest(allocateRequest, iceServer._username, iceServer.Realm, iceServer._password, iceServer.Nonce); - } - else + TransactionId = Encoding.ASCII.GetBytes(iceServer.TransactionID), + }, + Attributes = { - allocateReqBytes = allocateRequest.ToByteBuffer(null, false); - } + new STUNAttribute(STUNAttributeTypesEnum.RequestedTransport, STUNAttributeConstants.UdpTransportType), + new STUNAttribute( + STUNAttributeTypesEnum.RequestedAddressFamily, + iceServer.ServerEndPoint.AddressFamily == AddressFamily.InterNetwork + ? STUNAttributeConstants.IPv4AddressFamily + : STUNAttributeConstants.IPv6AddressFamily), + }, + }; - var sendResult = iceServer.Protocol == ProtocolType.Tcp ? - SendOverTCP(iceServer, allocateReqBytes) : - base.Send(RTPChannelSocketsEnum.RTP, iceServer.ServerEndPoint, allocateReqBytes); + var iceServerUri = iceServer.Uri; + var iceServerEndPoint = iceServer.ServerEndPoint; - if (sendResult != SocketError.Success) - { - logger.LogWarning("Error sending TURN Refresh request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.", - iceServer.OutstandingRequestsSent, iceServer._uri, iceServer.ServerEndPoint, sendResult); - } - else - { - OnStunMessageSent?.Invoke(allocateRequest, iceServer.ServerEndPoint, false); - } + using var activity = NetIceActivitySource.StartStunMessageSentActivity( + allocateRequest, + iceServerEndPoint, + iceServerUri, + iceServer.Realm, + iceServer.Nonce); - return sendResult; - } + logger.LogSendTurnAllocateRequest(iceServerUri, iceServerEndPoint, allocateRequest); + + var sendResult = SendStunMessage(allocateRequest, iceServer); - /// - /// Sends a create permissions request to a TURN server for a peer end point. - /// - /// The transaction ID to set on the request. This - /// gets used to match responses back to the sender. - /// The ICE server to send the request to. - /// The peer end point to request the channel bind for. - /// The result from the socket send (not the response code from the TURN server). - private SocketError SendTurnCreatePermissionsRequest(string transactionID, IceServer iceServer, IPEndPoint peerEndPoint) + if (sendResult != SocketError.Success) { - STUNMessage permissionsRequest = new STUNMessage(STUNMessageTypesEnum.CreatePermission); - permissionsRequest.Header.TransactionId = Encoding.ASCII.GetBytes(transactionID); - permissionsRequest.Attributes.Add(new STUNXORAddressAttribute(STUNAttributeTypesEnum.XORPeerAddress, peerEndPoint.Port, peerEndPoint.Address, permissionsRequest.Header.TransactionId)); + activity?.SetStatus(ActivityStatusCode.Error, sendResult.ToStringFast()); - byte[] createPermissionReqBytes = null; + logger.LogIceTurnAllocateRequestSendError( + iceServer.OutstandingRequestsSent, + iceServer.Uri, + iceServer.ServerEndPoint, + sendResult); + } + else + { + OnStunMessageSent?.Invoke(allocateRequest, iceServer.ServerEndPoint, false); + } - if (iceServer.Nonce != null && iceServer.Realm != null && iceServer._username != null && iceServer._password != null) - { - createPermissionReqBytes = GetAuthenticatedStunRequest(permissionsRequest, iceServer._username, iceServer.Realm, iceServer._password, iceServer.Nonce); - } - else - { - createPermissionReqBytes = permissionsRequest.ToByteBuffer(null, false); - } + return sendResult; + } + + /// + /// Sends an allocate request to a TURN server. + /// + /// The TURN server to send the request to. + /// The result from the socket send (not the response code from the TURN server). + private SocketError SendTurnRefreshRequest(IceServer iceServer) + { + iceServer.OutstandingRequestsSent += 1; + iceServer.LastRequestSentAt = DateTime.Now; - var sendResult = iceServer.Protocol == ProtocolType.Tcp ? - SendOverTCP(iceServer, createPermissionReqBytes) : - base.Send(RTPChannelSocketsEnum.RTP, iceServer.ServerEndPoint, createPermissionReqBytes); + Debug.Assert(iceServer.TransactionID is { }); + Debug.Assert(iceServer.ServerEndPoint is { }); - if (sendResult != SocketError.Success) + var refreshRequest = new STUNMessage(STUNMessageTypesEnum.Refresh) + { + Header = { - logger.LogWarning("Error sending TURN Create Permissions request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.", - iceServer.OutstandingRequestsSent, iceServer._uri, iceServer.ServerEndPoint, sendResult); - } - else + TransactionId = Encoding.ASCII.GetBytes(iceServer.TransactionID), + }, + Attributes = { - OnStunMessageSent?.Invoke(permissionsRequest, iceServer.ServerEndPoint, false); + new STUNAttribute(STUNAttributeTypesEnum.Lifetime, ALLOCATION_TIME_TO_EXPIRY_VALUE), + new STUNAttribute( + STUNAttributeTypesEnum.RequestedAddressFamily, + iceServer.ServerEndPoint.AddressFamily == AddressFamily.InterNetwork + ? STUNAttributeConstants.IPv4AddressFamily + : STUNAttributeConstants.IPv6AddressFamily), } + }; - return sendResult; - } + var iceServerUri = iceServer.Uri; + var iceServerEndPoint = iceServer.ServerEndPoint; + + using var activity = NetIceActivitySource.StartStunMessageSentActivity( + refreshRequest, + iceServerEndPoint, + iceServerUri, + iceServer.Realm, + iceServer.Nonce); - protected virtual SocketError SendOverTCP(IceServer iceServer, byte[] buffer) + logger.LogSendTurnRefreshRequest(iceServerUri, iceServerEndPoint, refreshRequest); + + var sendResult = SendStunMessage(refreshRequest, iceServer); + + if (sendResult != SocketError.Success) { - IPEndPoint dstEndPoint = iceServer?.ServerEndPoint; - if (IsClosed) - { - return SocketError.Disconnecting; - } - else if (dstEndPoint == null) - { - throw new ArgumentException("dstEndPoint", "An empty destination was specified to Send in RTPChannel."); - } - else if (buffer == null || buffer.Length == 0) - { - throw new ArgumentException("buffer", "The buffer must be set and non empty for Send in RTPChannel."); - } - else if (IPAddress.Any.Equals(dstEndPoint.Address) || IPAddress.IPv6Any.Equals(dstEndPoint.Address)) - { - logger.LogWarning("The destination address for Send in RTPChannel cannot be {Address}.", dstEndPoint.Address); - return SocketError.DestinationAddressRequired; - } - else - { - try - { - //Connect to destination - RtpTcpSocketByUri.TryGetValue(iceServer?._uri, out Socket sendSocket); - //LastRtpDestination = dstEndPoint; + activity?.SetStatus(ActivityStatusCode.Error, sendResult.ToStringFast()); - if (sendSocket == null) - { - return SocketError.Fault; - } + logger.LogIceTurnRefreshRequestSendError( + iceServer.OutstandingRequestsSent, + iceServer.Uri, + iceServer.ServerEndPoint, + sendResult); + } + else + { + OnStunMessageSent?.Invoke(refreshRequest, iceServer.ServerEndPoint, false); + } - //Prevent Send to IPV4 while socket is IPV6 (Mono Error) - if (dstEndPoint.AddressFamily == AddressFamily.InterNetwork && sendSocket.AddressFamily != dstEndPoint.AddressFamily) - { - dstEndPoint = new IPEndPoint(dstEndPoint.Address.MapToIPv6(), dstEndPoint.Port); - } + return sendResult; + } - Func equals = (IPEndPoint e1, IPEndPoint e2) => - { - return e1.Port == e2.Port && e1.Address.Equals(e2.Address); - }; + /// + /// Sends a create permissions request to a TURN server for a peer end point. + /// + /// + /// The transaction ID to set on the request. This gets used to match responses back to the sender. + /// + /// The ICE server to send the request to. + /// The peer end point to request the channel bind for. + /// The result from the socket send (not the response code from the TURN server). + private SocketError SendTurnCreatePermissionsRequest(string transactionID, IceServer iceServer, IPEndPoint peerEndPoint) + { + var transactionId = Encoding.ASCII.GetBytes(transactionID); - bool isTls = iceServer._uri.Scheme == STUNSchemesEnum.turns || iceServer._uri.Scheme == STUNSchemesEnum.stuns; - if (isTls) - { - // --- TLS PATH (TURNS / STUNS) --- - // SslStream.Write is not thread-safe, and the lazy handshake/connect - // must also be atomic so two threads don't both create a stream. - var writeLock = _tlsWriteLocks.GetOrAdd(iceServer._uri, _ => new SemaphoreSlim(1, 1)); - writeLock.Wait(); - try - { - if (!_tlsStreams.TryGetValue(iceServer._uri, out SslStream sslStream)) - { - // Connect the raw socket if needed - if (!sendSocket.Connected) - { - sendSocket.Connect(dstEndPoint); - } + var permissionsRequest = new STUNMessage(STUNMessageTypesEnum.CreatePermission) + { + Header = + { + TransactionId = transactionId, + }, + Attributes = + { + new STUNXORAddressAttribute( + STUNAttributeTypesEnum.XORPeerAddress, + peerEndPoint.Port, + peerEndPoint.Address, + transactionId) + } + }; - // Wrap in SslStream. Validation is left permissive here; tighten if needed. - sslStream = new SslStream(new NetworkStream(sendSocket, false), false, - (sender, cert, chain, errors) => true, null); + var iceServerUri = iceServer.Uri; + var iceServerEndPoint = iceServer.ServerEndPoint; - try - { - // Perform TLS handshake using the hostname from the URI for SNI/cert match. - sslStream.AuthenticateAsClient(iceServer._uri.Host); + Debug.Assert(iceServerEndPoint is { }); - _tlsStreams.TryAdd(iceServer._uri, sslStream); - logger.LogDebug("TLS handshake successful for {Uri}", iceServer._uri); + using var activity = NetIceActivitySource.StartStunMessageSentActivity( + permissionsRequest, + iceServerEndPoint, + iceServerUri, + iceServer.Realm, + iceServer.Nonce); - // Start a dedicated read loop for this SSL stream - _ = StartTlsReadLoop(iceServer._uri, sslStream, dstEndPoint); - } - catch (Exception tlsEx) - { - logger.LogError(tlsEx, "TLS handshake failed for {Uri}: {Message}", iceServer._uri, tlsEx.Message); - return SocketError.SocketError; - } - } + logger.LogSendTurnCreatePermissionsRequest(iceServerUri, iceServerEndPoint, permissionsRequest); - // Write to the SSL stream (serialised by writeLock). - sslStream.Write(buffer); - return SocketError.Success; - } - finally - { - writeLock.Release(); - } - } - else - { - if (!sendSocket.Connected || !(sendSocket.RemoteEndPoint is IPEndPoint) || !equals(sendSocket.RemoteEndPoint as IPEndPoint, dstEndPoint)) - { - if (sendSocket.Connected) - { - logger.LogDebug("SendOverTCP request disconnect."); - sendSocket.Disconnect(true); - } - sendSocket.Connect(dstEndPoint); + var sendResult = SendStunMessage(permissionsRequest, iceServer); - logger.LogDebug("SendOverTCP status: {Status} endpoint: {EndPoint}", sendSocket.Connected, dstEndPoint); - } + Debug.Assert(iceServer.ServerEndPoint is { }); - //Fix ReceiveFrom logic if any previous exception happens - m_rtpTcpReceiverByUri.TryGetValue(iceServer?._uri, out IceTcpReceiver rtpTcpReceiver); - if (rtpTcpReceiver != null && !rtpTcpReceiver.IsRunningReceive && !rtpTcpReceiver.IsClosed) - { - rtpTcpReceiver.BeginReceiveFrom(); - } + if (sendResult != SocketError.Success) + { + activity?.SetStatus(ActivityStatusCode.Error, sendResult.ToStringFast()); - sendSocket.BeginSendTo(buffer, 0, buffer.Length, SocketFlags.None, dstEndPoint, EndSendToTCP, sendSocket); - return SocketError.Success; - } - } - catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored. - { - return SocketError.Disconnecting; - } - catch (SocketException sockExcp) - { - return sockExcp.SocketErrorCode; - } - catch (Exception excp) - { - logger.LogError(excp, "Exception RTPIceChannel.SendOverTCP. {ErrorMessage}", excp.Message); - return SocketError.Fault; - } - } + logger.LogIceTurnCreatePermissionsRequestSendError( + iceServer.OutstandingRequestsSent, + iceServer.Uri, + iceServer.ServerEndPoint, + sendResult); } - - /// - /// Reads framed STUN / TURN traffic off a TLS stream (TURNS / STUNS). STUN and TURN - /// allocation messages share a 20-byte header where bytes 2-3 are the body length in - /// big-endian; TURN channel-data messages instead use a 4-byte header in the - /// 0x4000-0x7FFF channel-number range. We accumulate bytes until we have a full - /// message and then dispatch it via OnRTPPacketReceived (inherited from RTPChannel). - /// - private async Task StartTlsReadLoop(STUNUri uri, SslStream sslStream, IPEndPoint remoteEndPoint) + else { - byte[] receiveBuffer = new byte[4096]; - List streamBuffer = new List(); + OnStunMessageSent?.Invoke(permissionsRequest, iceServer.ServerEndPoint, false); + } - logger.LogDebug("Starting TLS read loop for {Uri}", uri); + return sendResult; + } - try - { - while (!IsClosed && sslStream.CanRead) - { - int bytesRead = await sslStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length).ConfigureAwait(false); + /// + /// Sends a packet via a TURN relay server. + /// + /// The peer destination end point. + /// The data to send to the peer. + /// The owner of the buffer memory, if any. + /// The TURN server end point to send the relayed request to. + /// + private SocketError SendRelay(ProtocolType protocol, IPEndPoint dstEndPoint, ReadOnlyMemory buffer, IDisposable? memoryOwner, IPEndPoint relayEndPoint, IceServer iceServer) + { + using (memoryOwner) + { + var sendReq = new STUNMessage(STUNMessageTypesEnum.SendIndication); + sendReq.AddXORPeerAddressAttribute(dstEndPoint.Address, dstEndPoint.Port); + sendReq.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Data, buffer)); - if (bytesRead == 0) - { - logger.LogWarning("TLS stream closed remotely for {Uri}", uri); - break; - } + using var activity = NetIceActivitySource.StartStunMessageSentActivity(sendReq, dstEndPoint, isRelayed: true); + activity?.SetRelayEndpoint(relayEndPoint); - streamBuffer.AddRange(new ArraySegment(receiveBuffer, 0, bytesRead)); + var bufferSize = sendReq.GetByteBufferSize(default, addFingerprint: false); + var rentedStunBufferOwner = MemoryPool.Shared.Rent(bufferSize); + var memory = rentedStunBufferOwner.Memory.Slice(0, bufferSize); - // Process complete packets from the stream buffer - while (streamBuffer.Count >= 4) // Minimum header size - { - int bodyLength = (streamBuffer[2] << 8) | streamBuffer[3]; - int totalPacketLength = 20 + bodyLength; // STUN/TURN header (20) + body + sendReq.WriteToBuffer(memory.Span, ReadOnlySpan.Empty, addFingerprint: false); - // TURN Channel Data has a 4-byte header; channel range is 0x4000 -> 0x7FFF - if (streamBuffer[0] >= 0x40 && streamBuffer[0] <= 0x7F) - { - totalPacketLength = 4 + bodyLength; - } + var sendResult = GetIceServerConection(iceServer).SendTo(relayEndPoint, memory, rentedStunBufferOwner); - if (streamBuffer.Count >= totalPacketLength) - { - byte[] packetBytes = streamBuffer.GetRange(0, totalPacketLength).ToArray(); - streamBuffer.RemoveRange(0, totalPacketLength); - OnRTPPacketReceived(null, 0, remoteEndPoint, packetBytes); - } - else - { - // Not enough data yet, wait for the next ReadAsync. - break; - } - } - } - } - catch (Exception ex) + if (sendResult != SocketError.Success) { - if (!IsClosed) - { - logger.LogError(ex, "TLS read loop exception for {Uri}: {Message}", uri, ex.Message); - } + logger.LogTurnRelayError(relayEndPoint, sendResult); } - finally + else { - _tlsStreams.TryRemove(uri, out _); - if (_tlsWriteLocks.TryRemove(uri, out var removedLock)) - { - removedLock.Dispose(); - } + OnStunMessageSent?.Invoke(sendReq, relayEndPoint, true); } + + return sendResult; } + } - protected virtual void EndSendToTCP(IAsyncResult ar) + /// + /// Sends the STUN request. Optionally adds the authentication fields. + /// + private SocketError SendStunMessage(STUNMessage stunMessage, IceServer iceServer) + { + if (!iceServer.MessageIntegrityKey.IsEmpty) { - try - { - Socket sendSocket = (Socket)ar.AsyncState; - int bytesSent = sendSocket.EndSendTo(ar); - } - catch (SocketException sockExcp) - { - // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during - // normal RTP operation. For example: - // - the RTP connection may start sending before the remote socket starts listening, - // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old - // or new socket during the transition. - logger.LogWarning(sockExcp, "SocketException RTPIceChannel EndSendToTCP ({SocketErrorCode}). {ErrorMessage}", sockExcp.SocketErrorCode, sockExcp.Message); - } - catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored. - { } - catch (Exception excp) - { - logger.LogError(excp, "Exception RTPIceChannel EndSendToTCP. {ErrorMessage}", excp.Message); - } + // https://tools.ietf.org/html/rfc5389#section-15.4 + + stunMessage.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Nonce, iceServer.Nonce)); + stunMessage.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Realm, iceServer.Realm)); + stunMessage.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Username, iceServer.Username)); } - /// - /// Adds the authentication fields to a STUN request. - /// - /// The serialised STUN request. - private byte[] GetAuthenticatedStunRequest(STUNMessage stunRequest, string username, byte[] realm, string password, byte[] nonce) - { - stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Nonce, nonce)); - stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Realm, realm)); - stunRequest.AddUsernameAttribute(username); + var messageIntegrityKeySpan = iceServer.MessageIntegrityKey.Span; - // See https://tools.ietf.org/html/rfc5389#section-15.4 - string key = $"{username}:{Encoding.UTF8.GetString(realm)}:{password}"; - var buffer = Encoding.UTF8.GetBytes(key); - var md5Digest = new MD5Digest(); - var hash = new byte[md5Digest.GetDigestSize()]; + var bufferSize = stunMessage.GetByteBufferSize(messageIntegrityKeySpan, addFingerprint: true); + var rentedMemory = MemoryPool.Shared.Rent(bufferSize); - md5Digest.BlockUpdate(buffer, 0, buffer.Length); - md5Digest.DoFinal(hash, 0); + var requestBytes = rentedMemory.Memory.Slice(0, bufferSize); + stunMessage.WriteToBuffer(requestBytes.Span, messageIntegrityKeySpan, addFingerprint: true); + Debug.Assert(iceServer.ServerEndPoint is { }); + return GetIceServerConection(iceServer).SendTo(iceServer.ServerEndPoint, requestBytes, rentedMemory); + } - return stunRequest.ToByteBuffer(hash, true); + private SocketConnection GetIceServerConection(IceServer iceServer) + { + if (!m_iceServerConnections.TryGetValue(iceServer.Uri, out var iceServerConnection)) + { + throw new InvalidOperationException($"Invalid ICE server: {iceServer.Uri}"); } - /// - /// Sends a packet via a TURN relay server. - /// - /// The peer destination end point. - /// The data to send to the peer. - /// The TURN server end point to send the relayed request to. - /// - private SocketError SendRelay(ProtocolType protocol, IPEndPoint dstEndPoint, byte[] buffer, IPEndPoint relayEndPoint, IceServer iceServer) - { - STUNMessage sendReq = new STUNMessage(STUNMessageTypesEnum.SendIndication); - sendReq.AddXORPeerAddressAttribute(dstEndPoint.Address, dstEndPoint.Port); - sendReq.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Data, buffer)); + return iceServerConnection; + } - var request = sendReq.ToByteBuffer(null, false); - var sendResult = protocol == ProtocolType.Tcp ? - SendOverTCP(iceServer, request) : - base.Send(RTPChannelSocketsEnum.RTP, relayEndPoint, request); + private async Task ResolveMdnsName(RTCIceCandidate candidate) + { + Debug.Assert(candidate.address is { }); - if (sendResult != SocketError.Success) - { - logger.LogWarning("Error sending TURN relay request to TURN server at {RelayEndPoint}. {SendResult}.", relayEndPoint, sendResult); - } - else + if (MdnsGetAddresses is { } mdnsGetAddresses) + { + if (MdnsResolve is { }) { - OnStunMessageSent?.Invoke(sendReq, relayEndPoint, true); + logger.LogIceMdnsBothSet(); } - return sendResult; + return await mdnsGetAddresses(candidate.address).ConfigureAwait(false); } - private async Task ResolveMdnsName(RTCIceCandidate candidate) + if (MdnsResolve is { } mdsnResolve) { - if (MdnsGetAddresses != null) - { - if (MdnsResolve != null) - { - logger.LogWarning("RTP ICE channel has both {PrimaryResolver} and {SecondaryResolver} set. Only {SelectedResolver} will be used.", - nameof(MdnsGetAddresses), nameof(MdnsResolve), nameof(MdnsGetAddresses)); - } - return await MdnsGetAddresses(candidate.address).ConfigureAwait(false); - } - if (MdnsResolve != null) - { - var address = await MdnsResolve(candidate.address).ConfigureAwait(false); - return address != null ? new IPAddress[] { address } : Array.Empty(); - } + var address = await mdsnResolve(candidate.address).ConfigureAwait(false); + return address is { } ? new IPAddress[] { address } : Array.Empty(); + } - // Default fallback: do an actual mDNS query via Makaretu.Dns.Multicast. - // Dns.GetHostAddressesAsync delegates to the OS resolver which on Windows - // does not reliably handle .local lookups (depends on whether the OS-level - // mDNS resolver is enabled, the multicast packet round-trips through the - // firewall in time, etc.). Without a real mDNS query Chrome-published - // .local candidates almost never resolve and ICE never gets to a - // succeeded pair. - IPAddress[] addresses; - try - { - addresses = await MdnsResolver.ResolveAsync(candidate.address).ConfigureAwait(false); - } - catch (Exception e) - { - logger.LogError(e, "Error resolving mDNS hostname {Name}", candidate.address); - return Array.Empty(); - } + // Default fallback: do an actual mDNS query via Makaretu.Dns.Multicast. + // Dns.GetHostAddressesAsync delegates to the OS resolver which on Windows + // does not reliably handle .local lookups (depends on whether the OS-level + // mDNS resolver is enabled, the multicast packet round-trips through the + // firewall in time, etc.). Without a real mDNS query Chrome-published + // .local candidates almost never resolve and ICE never gets to a + // succeeded pair. + IPAddress[] addresses; + try + { + addresses = await MdnsResolver.ResolveAsync(candidate.address).ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogMdnsResolutionError(candidate.address, e); + return Array.Empty(); + } - if (addresses.Length == 0) - { - logger.LogWarning("RTP ICE channel mDNS resolver returned no answers for {CandidateAddress} within the timeout.", candidate.address); - OnIceCandidateError?.Invoke(candidate, $"mDNS hostname {candidate.address} did not resolve within the query timeout."); - } - return addresses; - } - - /// - /// The send method for the RTP ICE channel. The sole purpose of this overload is to package up - /// sends that need to be relayed via a TURN server. If the connected channel is not a relay then - /// the send can be passed straight through to the underlying RTP channel. - /// - /// The socket to send on. Can be the RTP or Control socket. - /// The destination end point to send to. - /// The data to send. - /// The result of initiating the send. This result does not reflect anything about - /// whether the remote party received the packet or not. - public override SocketError Send(RTPChannelSocketsEnum sendOn, IPEndPoint dstEndPoint, byte[] buffer) - { - if (NominatedEntry != null && NominatedEntry.LocalCandidate.type == RTCIceCandidateType.relay && - NominatedEntry.LocalCandidate.IceServer != null && - NominatedEntry.RemoteCandidate.DestinationEndPoint.Address.Equals(dstEndPoint.Address) && - NominatedEntry.RemoteCandidate.DestinationEndPoint.Port == dstEndPoint.Port) - { - // A TURN relay channel is being used to communicate with the remote peer. - var protocol = NominatedEntry.LocalCandidate.IceServer.Protocol; - var serverEndPoint = NominatedEntry.LocalCandidate.IceServer.ServerEndPoint; - return SendRelay(protocol, dstEndPoint, buffer, serverEndPoint, NominatedEntry.LocalCandidate.IceServer); - } - else + if (addresses.Length == 0) + { + logger.LogMdnsResolutionNoAnswers(candidate.address); + OnIceCandidateError?.Invoke(candidate, $"mDNS hostname {candidate.address} did not resolve within the query timeout."); + } + return addresses; + } + + /// + /// The send method for the RTP ICE channel. The sole purpose of this overload is to package up sends that need to + /// be relayed via a TURN server. If the connected channel is not a relay then the send can be passed straight + /// through to the underlying RTP channel. + /// + /// The socket to send on. Can be the RTP or Control socket. + /// The destination end point to send to. + /// The data to send. + /// The onwer of the memory. + /// + /// The result of initiating the send. This result does not reflect anything about whether the remote party received + /// the packet or not. + /// + public override SocketError Send(RTPChannelSocketsEnum sendOn, IPEndPoint dstEndPoint, ReadOnlyMemory buffer, IDisposable? memoryOwner = null) + { + if (NominatedEntry is { - return base.Send(sendOn, dstEndPoint, buffer); - } + LocalCandidate: + { + type: RTCIceCandidateType.relay, + IceServer: { } iceServer + }, + RemoteCandidate: + { + DestinationEndPoint: { } remoteEndPoint + } + } && + remoteEndPoint.Port == dstEndPoint.Port && + remoteEndPoint.Address.Equals(dstEndPoint.Address)) + { + // A TURN relay channel is being used to communicate with the remote peer. + Debug.Assert(iceServer.ServerEndPoint is { }); + return SendRelay( + iceServer.Protocol, + dstEndPoint, + buffer, + memoryOwner, + iceServer.ServerEndPoint, + iceServer); + } + else + { + return base.Send(sendOn, dstEndPoint, buffer, memoryOwner); } } } diff --git a/src/SIPSorcery/net/RTCP/NetRtcpLoggingExtensions.cs b/src/SIPSorcery/net/RTCP/NetRtcpLoggingExtensions.cs new file mode 100644 index 0000000000..85aaf7df3d --- /dev/null +++ b/src/SIPSorcery/net/RTCP/NetRtcpLoggingExtensions.cs @@ -0,0 +1,144 @@ +using System; +using Microsoft.Extensions.Logging; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; + +internal static partial class NetRtcpLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "RtcpSessionStart", + Level = LogLevel.Debug, + Message = "Starting RTCP session for {CNameOrSsrc}.", + SkipEnabledCheck = true)] + private static partial void LogRtcpSessionStartUnchecked( + this ILogger logger, + string cnameOrSsrc); + + public static void LogRtcpSessionStart( + this ILogger logger, + string cname, + uint ssrc) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + LogRtcpSessionStartUnchecked(logger, !string.IsNullOrWhiteSpace(cname) ? cname : ssrc.ToString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "RtcpSessionRemovingReport", + Level = LogLevel.Debug, + Message = "RTCP session removing reception report for remote ssrc {Ssrc}.")] + public static partial void LogRtcpSessionRemovingReport( + this ILogger logger, + uint ssrc); + + [LoggerMessage( + EventId = 0, + EventName = "RtcpSessionNoActivity", + Level = LogLevel.Warning, + Message = "RTCP session for local ssrc {Ssrc} has not had any activity for over {NoActivityTimeoutSeconds} seconds.", + SkipEnabledCheck = true)] + private static partial void LogRtcpSessionNoActivityUnchecked( + this ILogger logger, + uint ssrc, + int noActivityTimeoutSeconds); + + public static void LogRtcpSessionNoActivity( + this ILogger logger, + uint ssrc, + int noActivityTimeoutMilliseconds) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + LogRtcpSessionNoActivityUnchecked(logger, ssrc, noActivityTimeoutMilliseconds / 1000); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "RtcpSessionAlreadyStarted", + Level = LogLevel.Warning, + Message = "Start was called on RTCP session for {CNameOrSsrc} but it has already been started.", + SkipEnabledCheck = true)] + private static partial void LogRtcpSessionAlreadyStartedUnchecked( + this ILogger logger, + string cnameOrSsrc); + + public static void LogRtcpSessionAlreadyStarted( + this ILogger logger, + string cname, + uint ssrc) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + LogRtcpSessionAlreadyStartedUnchecked(logger, !string.IsNullOrWhiteSpace(cname) ? cname : ssrc.ToString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "RtcpSessionSendReportError", + Level = LogLevel.Error, + Message = "Exception RTCPSession.SendReportTimerCallback. {errorMessage}")] + public static partial void LogRtcpSessionSendReportError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtcpSessionReportReceiveError", + Level = LogLevel.Error, + Message = "Exception RTCPSession.ReportReceived. {errorMessage}")] + public static partial void LogRtcpSessionReportReceiveError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RTCPCompoundPacketUnrecognizedType", + Level = LogLevel.Warning, + Message = "RTCPCompoundPacket did not recognise packet type ID {packetTypeId}. {packet}", + SkipEnabledCheck = true)] + private static partial void LogRtcpCompoundPacketUnrecognizedTypeUnchecked( + this ILogger logger, + byte packetTypeId, + string packet); + + public static void LogRtcpCompoundPacketUnrecognizedType( + this ILogger logger, + byte packetTypeId, + byte[] packet) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + logger.LogRtcpCompoundPacketUnrecognizedTypeUnchecked(packetTypeId, packet.HexStr()); + } + } + + public static void LogRtcpCompoundPacketUnrecognizedType( + this ILogger logger, + byte packetTypeId, + ReadOnlySpan packet) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + logger.LogRtcpCompoundPacketUnrecognizedTypeUnchecked(packetTypeId, packet.HexStr()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "RtcpFeedbackSerializationNotImplemented", + Level = LogLevel.Debug, + Message = "Serialization for feedback report {PacketType} and message type {FeedbackMessageType} not yet implemented.")] + public static partial void LogRtcpFeedbackSerializationNotImplemented( + this ILogger logger, + RTCPReportTypesEnum packetType, + RTCPFeedbackTypesEnum feedbackMessageType); +} diff --git a/src/SIPSorcery/net/RTCP/RTCPBye.cs b/src/SIPSorcery/net/RTCP/RTCPBye.cs index 450de07eac..d8b2e9d353 100644 --- a/src/SIPSorcery/net/RTCP/RTCPBye.cs +++ b/src/SIPSorcery/net/RTCP/RTCPBye.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: RTCPBye.cs // // Description: RTCP Goodbye packet as defined in RFC3550. @@ -29,122 +29,129 @@ using System; using System.Buffers.Binary; using System.Text; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; -namespace SIPSorcery.Net +/// +/// RTCP Goodbye packet as defined in RFC3550. The BYE packet indicates +/// that one or more sources are no longer active. +/// +public partial class RTCPBye : IByteSerializable { + public const int MAX_REASON_BYTES = 255; + public const int SSRC_SIZE = 4; // 4 bytes for the SSRC. + public const int MIN_PACKET_SIZE = RTCPHeader.HEADER_BYTES_LENGTH + SSRC_SIZE; + + public RTCPHeader Header; + public uint SSRC { get; private set; } + public string? Reason { get; private set; } + /// - /// RTCP Goodbye packet as defined in RFC3550. The BYE packet indicates - /// that one or more sources are no longer active. + /// Creates a new RTCP Bye payload. /// - public class RTCPBye + /// The synchronisation source of the RTP stream being closed. + /// Optional reason for closing. Maximum length is 255 bytes + /// (note bytes not characters). + public RTCPBye(uint ssrc, string? reason) { - public const int MAX_REASON_BYTES = 255; - public const int SSRC_SIZE = 4; // 4 bytes for the SSRC. - public const int MIN_PACKET_SIZE = RTCPHeader.HEADER_BYTES_LENGTH + SSRC_SIZE; - - public RTCPHeader Header; - public uint SSRC { get; private set; } - public string Reason { get; private set; } - - /// - /// Creates a new RTCP Bye payload. - /// - /// The synchronisation source of the RTP stream being closed. - /// Optional reason for closing. Maximum length is 255 bytes - /// (note bytes not characters). - public RTCPBye(uint ssrc, string reason) + Header = new RTCPHeader(RTCPReportTypesEnum.BYE, 1); + SSRC = ssrc; + + if (reason is { }) { - Header = new RTCPHeader(RTCPReportTypesEnum.BYE, 1); - SSRC = ssrc; + Reason = (reason.Length > MAX_REASON_BYTES) ? reason.Substring(0, MAX_REASON_BYTES) : reason; - if (reason != null) + // Need to take account of multi-byte characters. + while (Encoding.UTF8.GetBytes(Reason).Length > MAX_REASON_BYTES) { - Reason = (reason.Length > MAX_REASON_BYTES) ? reason.Substring(0, MAX_REASON_BYTES) : reason; - - // Need to take account of multi-byte characters. - while (Encoding.UTF8.GetBytes(Reason).Length > MAX_REASON_BYTES) - { - Reason = Reason.Substring(0, Reason.Length - 1); - } + Reason = Reason.Substring(0, Reason.Length - 1); } } + } - /// - /// Create a new RTCP Goodbye packet from a serialised byte array. - /// - /// The byte array holding the Goodbye packet. - public RTCPBye(byte[] packet) + /// + /// Create a new RTCP Goodbye packet from a serialised byte array. + /// + /// The byte array holding the Goodbye packet. + public RTCPBye(ReadOnlySpan packet) + { + if (packet.Length < MIN_PACKET_SIZE) { - if (packet.Length < MIN_PACKET_SIZE) - { - throw new ApplicationException("The packet did not contain the minimum number of bytes for an RTCP Goodbye packet."); - } + throw new SipSorceryException("The packet did not contain the minimum number of bytes for an RTCP Goodbye packet."); + } - Header = new RTCPHeader(packet); + Header = new RTCPHeader(packet); - SSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(4)); + SSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(4)); - if (packet.Length > MIN_PACKET_SIZE) - { - int reasonLength = packet[8]; + if (packet.Length > MIN_PACKET_SIZE) + { + int reasonLength = packet[8]; - if (packet.Length - MIN_PACKET_SIZE - 1 >= reasonLength) - { - Reason = Encoding.UTF8.GetString(packet, 9, reasonLength); - } + if (packet.Length - MIN_PACKET_SIZE - 1 >= reasonLength) + { + Reason = Encoding.UTF8.GetString(packet.Slice(9, reasonLength)); } } + } + + /// + public int GetByteCount() => RTCPHeader.HEADER_BYTES_LENGTH + GetPaddedLength(((Reason is { }) ? Encoding.UTF8.GetByteCount(Reason) : 0)); - /// - /// Gets the raw bytes for the Goodbye packet. - /// - /// A byte array. - public byte[] GetBytes() + /// + public int WriteBytes(Span buffer) + { + var size = GetByteCount(); + + if (buffer.Length < size) { - byte[] reasonBytes = (Reason != null) ? Encoding.UTF8.GetBytes(Reason) : null; - int reasonLength = (reasonBytes != null) ? reasonBytes.Length : 0; - byte[] buffer = new byte[RTCPHeader.HEADER_BYTES_LENGTH + GetPaddedLength(reasonLength)]; - Header.SetLength((ushort)(buffer.Length / 4 - 1)); + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); + } - Buffer.BlockCopy(Header.GetBytes(), 0, buffer, 0, RTCPHeader.HEADER_BYTES_LENGTH); - int payloadIndex = RTCPHeader.HEADER_BYTES_LENGTH; + WriteBytesCore(buffer.Slice(0, size)); - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(payloadIndex), SSRC); + return size; + } - if (reasonLength > 0) - { - buffer[payloadIndex + 4] = (byte)reasonLength; - Buffer.BlockCopy(reasonBytes, 0, buffer, payloadIndex + 5, reasonBytes.Length); - } + private void WriteBytesCore(Span buffer) + { + Header.SetLength((ushort)(buffer.Length / 4 - 1)); + _ = Header.WriteBytes(buffer); + + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH), SSRC); - return buffer; + if (Reason is { }) + { + buffer[RTCPHeader.HEADER_BYTES_LENGTH + 4] = + (byte)Encoding.UTF8.GetBytes(Reason.AsSpan(), buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 5)); } + } - /// - /// The packet has to finish on a 4 byte boundary. This method calculates the minimum - /// packet length for the Goodbye fields to fit within a 4 byte boundary. - /// - /// The length of the optional reason string, can be 0. - /// The minimum length for the full packet to be able to fit within a 4 byte - /// boundary. - private int GetPaddedLength(int reasonLength) + /// + /// The packet has to finish on a 4 byte boundary. This method calculates the minimum + /// packet length for the Goodbye fields to fit within a 4 byte boundary. + /// + /// The length of the optional reason string, can be 0. + /// The minimum length for the full packet to be able to fit within a 4 byte + /// boundary. + private int GetPaddedLength(int reasonLength) + { + // Plus one is for the reason length field. + if (reasonLength > 0) { - // Plus one is for the reason length field. - if (reasonLength > 0) - { - reasonLength += 1; - } + reasonLength += 1; + } - int nonPaddedSize = reasonLength + SSRC_SIZE; + int nonPaddedSize = reasonLength + SSRC_SIZE; - if (nonPaddedSize % 4 == 0) - { - return nonPaddedSize; - } - else - { - return nonPaddedSize + 4 - (nonPaddedSize % 4); - } + if (nonPaddedSize % 4 == 0) + { + return nonPaddedSize; + } + else + { + return nonPaddedSize + 4 - (nonPaddedSize % 4); } } } diff --git a/src/SIPSorcery/net/RTCP/RTCPCompoundPacket.cs b/src/SIPSorcery/net/RTCP/RTCPCompoundPacket.cs index 40c1dfc312..9ba2ec73cf 100644 --- a/src/SIPSorcery/net/RTCP/RTCPCompoundPacket.cs +++ b/src/SIPSorcery/net/RTCP/RTCPCompoundPacket.cs @@ -15,292 +15,334 @@ //----------------------------------------------------------------------------- using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// Represents an RTCP compound packet consisting of 1 or more +/// RTCP packets combined together in a single buffer. According to RFC3550 RTCP +/// transmissions should always have at least 2 RTCP packets (a sender/receiver report +/// and an SDES report). This implementation does not enforce that constraint for +/// received reports but does for sends. +/// +public partial class RTCPCompoundPacket : IByteSerializable { + private static readonly ILogger logger = LogFactory.CreateLogger(); + + public RTCPSenderReport? SenderReport { get; private set; } + public RTCPReceiverReport? ReceiverReport { get; private set; } + public RTCPSDesReport? SDesReport { get; private set; } + public RTCPBye? Bye { get; set; } + public RTCPFeedback? Feedback { get; set; } + public RTCPTWCCFeedback? TWCCFeedback { get; set; } + + protected internal RTCPCompoundPacket() + { + } + + public RTCPCompoundPacket(RTCPSenderReport senderReport, RTCPSDesReport sdesReport) + { + SenderReport = senderReport; + SDesReport = sdesReport; + } + + public RTCPCompoundPacket(RTCPReceiverReport receiverReport, RTCPSDesReport sdesReport) + { + ReceiverReport = receiverReport; + SDesReport = sdesReport; + } + /// - /// Represents an RTCP compound packet consisting of 1 or more - /// RTCP packets combined together in a single buffer. According to RFC3550 RTCP - /// transmissions should always have at least 2 RTCP packets (a sender/receiver report - /// and an SDES report). This implementation does not enforce that constraint for - /// received reports but does for sends. + /// Creates a new RTCP compound packet from a serialised buffer. /// - public class RTCPCompoundPacket + /// The serialised RTCP compound packet to parse. + public RTCPCompoundPacket(ReadOnlySpan packet) + { + var offset = 0; + while (offset < packet.Length) + { + if (packet.Length - offset < RTCPHeader.HEADER_BYTES_LENGTH) + { + // Not enough bytes left for a RTCP header. + break; + } + else + { + var buffer = packet.Slice(offset); + + // The payload type field is the second byte in the RTCP header. + var packetTypeID = buffer[1]; + switch (packetTypeID) + { + case (byte)RTCPReportTypesEnum.SR: + SenderReport = new RTCPSenderReport(buffer); + var srLength = SenderReport.GetByteCount(); + offset += srLength; + break; + case (byte)RTCPReportTypesEnum.RR: + ReceiverReport = new RTCPReceiverReport(buffer); + var rrLength = ReceiverReport.GetByteCount(); + offset += rrLength; + break; + case (byte)RTCPReportTypesEnum.SDES: + SDesReport = new RTCPSDesReport(buffer); + var sdesLength = SDesReport.GetByteCount(); + offset += sdesLength; + break; + case (byte)RTCPReportTypesEnum.BYE: + Bye = new RTCPBye(buffer); + var byeLength = Bye.GetByteCount(); + offset += byeLength; + break; + case (byte)RTCPReportTypesEnum.RTPFB: + var typ = RTCPHeader.ParseFeedbackType(buffer); + switch (typ) + { + case RTCPFeedbackTypesEnum.TWCC: + TWCCFeedback = new RTCPTWCCFeedback(buffer); + var twccFeedbackLength = (TWCCFeedback.Header.Length + 1) * 4; + offset += twccFeedbackLength; + break; + default: + Feedback = new RTCPFeedback(buffer); + var rtpfbFeedbackLength = Feedback.GetByteCount(); + offset += rtpfbFeedbackLength; + break; + } + break; + case (byte)RTCPReportTypesEnum.PSFB: + // TODO: Interpret Payload specific feedback reports. + Feedback = new RTCPFeedback(buffer); + var psfbFeedbackLength = Feedback.GetByteCount(); + offset += psfbFeedbackLength; + //var psfbHeader = new RTCPHeader(buffer); + //offset += psfbHeader.Length * 4 + 4; + break; + default: + offset = int.MaxValue; + logger.LogRtcpCompoundPacketUnrecognizedType(packetTypeID, buffer); + break; + } + } + } + } + + // TODO: optimize this + /// + public int GetByteCount() { - private static readonly ILogger logger = LogFactory.CreateLogger(); + Debug.Assert(SDesReport is { }); - public RTCPSenderReport SenderReport { get; private set; } - public RTCPReceiverReport ReceiverReport { get; private set; } - public RTCPSDesReport SDesReport { get; private set; } - public RTCPBye Bye { get; set; } - public RTCPFeedback Feedback { get; set; } - public RTCPTWCCFeedback TWCCFeedback { get; set; } + return (SenderReport?.GetByteCount()).GetValueOrDefault() + + (ReceiverReport?.GetByteCount()).GetValueOrDefault() + + SDesReport.GetByteCount() + + (Bye?.GetByteCount()).GetValueOrDefault(); + } - protected internal RTCPCompoundPacket() + /// + public int WriteBytes(Span buffer) + { + if (SenderReport is null && ReceiverReport is null) { + throw new InvalidOperationException("An RTCP compound packet must have either a Sender or Receiver report set."); } - - public RTCPCompoundPacket(RTCPSenderReport senderReport, RTCPSDesReport sdesReport) + else if (SDesReport is null) { - SenderReport = senderReport; - SDesReport = sdesReport; + throw new InvalidOperationException("An RTCP compound packet must have an SDES report set."); } - public RTCPCompoundPacket(RTCPReceiverReport receiverReport, RTCPSDesReport sdesReport) + var size = GetByteCount(); + + if (buffer.Length < size) { - ReceiverReport = receiverReport; - SDesReport = sdesReport; + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); } - /// - /// Creates a new RTCP compound packet from a serialised buffer. - /// - /// The serialised RTCP compound packet to parse. - public RTCPCompoundPacket(byte[] packet) - { - int offset = 0; - while (offset < packet.Length) - { - if (packet.Length - offset < RTCPHeader.HEADER_BYTES_LENGTH) - { - // Not enough bytes left for a RTCP header. - break; - } - else - { - var buffer = packet.Skip(offset).ToArray(); + WriteBytesCore(buffer.Slice(0, size)); - // The payload type field is the second byte in the RTCP header. - byte packetTypeID = buffer[1]; - switch (packetTypeID) - { - case (byte)RTCPReportTypesEnum.SR: - SenderReport = new RTCPSenderReport(buffer); - int srLength = (SenderReport != null) ? SenderReport.GetBytes().Length : Int32.MaxValue; - offset += srLength; - break; - case (byte)RTCPReportTypesEnum.RR: - ReceiverReport = new RTCPReceiverReport(buffer); - int rrLength = (ReceiverReport != null) ? ReceiverReport.GetBytes().Length : Int32.MaxValue; - offset += rrLength; - break; - case (byte)RTCPReportTypesEnum.SDES: - SDesReport = new RTCPSDesReport(buffer); - int sdesLength = (SDesReport != null) ? SDesReport.GetBytes().Length : Int32.MaxValue; - offset += sdesLength; - break; - case (byte)RTCPReportTypesEnum.BYE: - Bye = new RTCPBye(buffer); - int byeLength = (Bye != null) ? Bye.GetBytes().Length : Int32.MaxValue; - offset += byeLength; - break; - case (byte)RTCPReportTypesEnum.RTPFB: - var typ = RTCPHeader.ParseFeedbackType(buffer); - switch (typ) { - case RTCPFeedbackTypesEnum.TWCC: - TWCCFeedback = new RTCPTWCCFeedback(buffer); - int twccFeedbackLength = (TWCCFeedback.Header.Length + 1) * 4; - offset += twccFeedbackLength; - break; - default: - Feedback = new RTCPFeedback(buffer); - int rtpfbFeedbackLength = Feedback.GetBytes().Length; - offset += rtpfbFeedbackLength; - break; - } - break; - case (byte)RTCPReportTypesEnum.PSFB: - // TODO: Interpret Payload specific feedback reports. - Feedback = new RTCPFeedback(buffer); - int psfbFeedbackLength = (Feedback != null) ? Feedback.GetBytes().Length : Int32.MaxValue; - offset += psfbFeedbackLength; - //var psfbHeader = new RTCPHeader(buffer); - //offset += psfbHeader.Length * 4 + 4; - break; - default: - offset = Int32.MaxValue; - logger.LogWarning("RTCPCompoundPacket did not recognise packet type ID {PacketTypeID}. {Packet}", packetTypeID, buffer.HexStr()); - break; - } - } - } + return size; + } + + private void WriteBytesCore(Span buffer) + { + if (SenderReport is { }) + { + var bytesWritten = SenderReport.WriteBytes(buffer); + buffer = buffer.Slice(bytesWritten); + } + else + { + Debug.Assert(ReceiverReport is { }); + var bytesWritten = ReceiverReport.WriteBytes(buffer); + buffer = buffer.Slice(bytesWritten); } - /// - /// Serialises a compound RTCP packet to a byte array ready for transmission. - /// - /// A byte array representing a serialised compound RTCP packet. - public byte[] GetBytes() { - if (SenderReport == null && ReceiverReport == null) - { - throw new ApplicationException("An RTCP compound packet must have either a Sender or Receiver report set."); - } - else if (SDesReport == null) - { - throw new ApplicationException("An RTCP compound packet must have an SDES report set."); - } + Debug.Assert(SDesReport is { }); + var bytesWritten = SDesReport.WriteBytes(buffer); + buffer = buffer.Slice(bytesWritten); + } - List compoundBuffer = new List(); - compoundBuffer.AddRange((SenderReport != null) ? SenderReport.GetBytes() : ReceiverReport.GetBytes()); - compoundBuffer.AddRange(SDesReport.GetBytes()); + if (Bye is { }) + { + var bytesWritten = Bye.WriteBytes(buffer); + } + } - if (Bye != null) - { - compoundBuffer.AddRange(Bye.GetBytes()); - } + public string GetDebugSummary() + { + StringBuilder sb = new StringBuilder(); - return compoundBuffer.ToArray(); + if (Bye != null) + { + sb.AppendLine("BYE"); } - public string GetDebugSummary() + if (SDesReport != null) { - StringBuilder sb = new StringBuilder(); - - if (Bye != null) - { - sb.AppendLine("BYE"); - } - - if (SDesReport != null) - { - sb.AppendLine($"SDES: SSRC={SDesReport.SSRC}, CNAME={SDesReport.CNAME}"); - } + sb.AppendLine($"SDES: SSRC={SDesReport.SSRC}, CNAME={SDesReport.CNAME}"); + } - if (SenderReport != null) + if (SenderReport != null) + { + var sr = SenderReport; + sb.AppendLine($"Sender: SSRC={sr.SSRC}, PKTS={sr.PacketCount}, BYTES={sr.OctetCount}"); + if (sr.ReceptionReports != null) { - var sr = SenderReport; - sb.AppendLine($"Sender: SSRC={sr.SSRC}, PKTS={sr.PacketCount}, BYTES={sr.OctetCount}"); - if (sr.ReceptionReports != null) + foreach (var rr in sr.ReceptionReports) { - foreach (var rr in sr.ReceptionReports) - { - sb.AppendLine($" RR: SSRC={rr.SSRC}, LOST={rr.PacketsLost}, JITTER={rr.Jitter}"); - } + sb.AppendLine($" RR: SSRC={rr.SSRC}, LOST={rr.PacketsLost}, JITTER={rr.Jitter}"); } } + } - if (ReceiverReport != null) + if (ReceiverReport != null) + { + var recv = ReceiverReport; + sb.AppendLine($"Receiver: SSRC={recv.SSRC}"); + if (recv.ReceptionReports != null) { - var recv = ReceiverReport; - sb.AppendLine($"Receiver: SSRC={recv.SSRC}"); - if (recv.ReceptionReports != null) + foreach (var rr in recv.ReceptionReports) { - foreach (var rr in recv.ReceptionReports) - { - sb.AppendLine($" RR: SSRC={rr.SSRC}, LOST={rr.PacketsLost}, JITTER={rr.Jitter}"); - } + sb.AppendLine($" RR: SSRC={rr.SSRC}, LOST={rr.PacketsLost}, JITTER={rr.Jitter}"); } } - - return sb.ToString().TrimEnd('\n'); } - public static bool TryParse( - ReadOnlySpan packet, - out RTCPCompoundPacket rtcpCompoundPacket, - out int consumed) + return sb.ToString().TrimEnd('\n'); + } + + /// + /// Creates a new RTCP compound packet from a serialised buffer. + /// + /// + /// + /// + /// The amount read from the packet + public static bool TryParse( + ReadOnlySpan packet, + RTCPCompoundPacket rtcpCompoundPacket, + out int consumed) + { + if (rtcpCompoundPacket is null) { rtcpCompoundPacket = new RTCPCompoundPacket(); - return TryParse(packet.ToArray(), rtcpCompoundPacket, out consumed); } - /// - /// Creates a new RTCP compound packet from a serialised buffer. - /// - /// - /// - /// - /// The amount read from the packet - public static bool TryParse( - byte[] packet, - RTCPCompoundPacket rtcpCompoundPacket, - out int consumed) + var offset = 0; + + while (offset < packet.Length) { - if (rtcpCompoundPacket == null) + if (packet.Length - offset < RTCPHeader.HEADER_BYTES_LENGTH) { - rtcpCompoundPacket = new RTCPCompoundPacket(); + break; } - int offset = 0; - while (offset < packet.Length) + else { - if (packet.Length - offset < RTCPHeader.HEADER_BYTES_LENGTH) - { - // Not enough bytes left for a RTCP header. - break; - } - else - { - var buffer = packet.Skip(offset).ToArray(); + var buffer = packet.Slice(offset); + var packetTypeID = buffer[1]; - // The payload type field is the second byte in the RTCP header. - byte packetTypeID = buffer[1]; - switch (packetTypeID) - { - case (byte)RTCPReportTypesEnum.SR: - rtcpCompoundPacket.SenderReport = new RTCPSenderReport(buffer); - int srLength = (rtcpCompoundPacket.SenderReport != null) ? rtcpCompoundPacket.SenderReport.GetBytes().Length : Int32.MaxValue; - offset += srLength; + switch (packetTypeID) + { + case (byte)RTCPReportTypesEnum.SR: + { + var report = new RTCPSenderReport(buffer); + rtcpCompoundPacket.SenderReport = report; + var length = report?.GetByteCount() ?? int.MaxValue; + offset += length; break; - case (byte)RTCPReportTypesEnum.RR: - rtcpCompoundPacket.ReceiverReport = new RTCPReceiverReport(buffer); - int rrLength = (rtcpCompoundPacket.ReceiverReport != null) ? rtcpCompoundPacket.ReceiverReport.GetBytes().Length : Int32.MaxValue; - offset += rrLength; + } + case (byte)RTCPReportTypesEnum.RR: + { + var report = new RTCPReceiverReport(buffer); + rtcpCompoundPacket.ReceiverReport = report; + var length = report?.GetByteCount() ?? int.MaxValue; + offset += length; break; - case (byte)RTCPReportTypesEnum.SDES: - rtcpCompoundPacket.SDesReport = new RTCPSDesReport(buffer); - int sdesLength = (rtcpCompoundPacket.SDesReport != null) ? rtcpCompoundPacket.SDesReport.GetBytes().Length : Int32.MaxValue; - offset += sdesLength; + } + case (byte)RTCPReportTypesEnum.SDES: + { + var report = new RTCPSDesReport(buffer); + rtcpCompoundPacket.SDesReport = report; + var length = report?.GetByteCount() ?? int.MaxValue; + offset += length; break; - case (byte)RTCPReportTypesEnum.BYE: - rtcpCompoundPacket.Bye = new RTCPBye(buffer); - int byeLength = (rtcpCompoundPacket.Bye != null) ? rtcpCompoundPacket.Bye.GetBytes().Length : Int32.MaxValue; - offset += byeLength; + } + case (byte)RTCPReportTypesEnum.BYE: + { + var report = new RTCPBye(buffer); + rtcpCompoundPacket.Bye = report; + var length = report?.GetByteCount() ?? int.MaxValue; + offset += length; break; - case (byte)RTCPReportTypesEnum.RTPFB: + } + case (byte)RTCPReportTypesEnum.RTPFB: + { var typ = RTCPHeader.ParseFeedbackType(buffer); switch (typ) { default: { - rtcpCompoundPacket.Feedback = new RTCPFeedback(buffer); - int rtpfbFeedbackLength = (rtcpCompoundPacket.Feedback != null) ? rtcpCompoundPacket.Feedback.GetBytes().Length : Int32.MaxValue; - offset += rtpfbFeedbackLength; + var feedback = new RTCPFeedback(buffer); + rtcpCompoundPacket.Feedback = feedback; + var length = feedback?.GetByteCount() ?? int.MaxValue; + offset += length; + break; } - break; case RTCPFeedbackTypesEnum.TWCC: { - rtcpCompoundPacket.TWCCFeedback = new RTCPTWCCFeedback(buffer); - int twccFeedbackLength = (rtcpCompoundPacket.TWCCFeedback != null) ? rtcpCompoundPacket.TWCCFeedback.GetBytes().Length : Int32.MaxValue; - offset += twccFeedbackLength; + var feedback = new RTCPTWCCFeedback(buffer); + rtcpCompoundPacket.TWCCFeedback = feedback; + var length = feedback?.GetByteCount() ?? int.MaxValue; + offset += length; + break; } - break; } break; - case (byte)RTCPReportTypesEnum.PSFB: - // TODO: Interpret Payload specific feedback reports. - rtcpCompoundPacket.Feedback = new RTCPFeedback(buffer); - int psfbFeedbackLength = (rtcpCompoundPacket.Feedback != null) ? rtcpCompoundPacket.Feedback.GetBytes().Length : Int32.MaxValue; - offset += psfbFeedbackLength; - //var psfbHeader = new RTCPHeader(buffer); - //offset += psfbHeader.Length * 4 + 4; + } + case (byte)RTCPReportTypesEnum.PSFB: + { + var feedback = new RTCPFeedback(buffer); + rtcpCompoundPacket.Feedback = feedback; + var length = feedback?.GetByteCount() ?? int.MaxValue; + offset += length; break; - default: - offset = Int32.MaxValue; - logger.LogWarning("RTCPCompoundPacket did not recognise packet type ID {PacketTypeID}. {Packet}", packetTypeID, packet.HexStr()); + } + default: + { + offset = int.MaxValue; + logger.LogRtcpCompoundPacketUnrecognizedType(packetTypeID, packet.ToArray()); break; - } + } } } - - consumed = offset; - return true; } + + consumed = offset; + return true; } } diff --git a/src/SIPSorcery/net/RTCP/RTCPFeedback.cs b/src/SIPSorcery/net/RTCP/RTCPFeedback.cs index bb1eaef8b4..0c68fc48b2 100644 --- a/src/SIPSorcery/net/RTCP/RTCPFeedback.cs +++ b/src/SIPSorcery/net/RTCP/RTCPFeedback.cs @@ -28,319 +28,329 @@ using System; using System.Buffers.Binary; +using System.Diagnostics; +using System.Text; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// The different types of Feedback Message Types. (RFC4585) https://tools.ietf.org/html/rfc4585#page-35 +/// +public enum RTCPFeedbackTypesEnum : int { - /// - /// The different types of Feedback Message Types. (RFC4585) - /// https://tools.ietf.org/html/rfc4585#page-35 - /// - public enum RTCPFeedbackTypesEnum : int + unassigned = 0, // Unassigned + NACK = 1, // Generic NACK Generic negative acknowledgment [RFC4585] + // reserved = 2 // Reserved [RFC5104] + TMMBR = 3, // Temporary Maximum Media Stream Bit Rate Request [RFC5104] + TMMBN = 4, // Temporary Maximum Media Stream Bit Rate Notification [RFC5104] + RTCP_SR_REQ = 5, // RTCP Rapid Resynchronisation Request [RFC6051] + RAMS = 6, // Rapid Acquisition of Multicast Sessions [RFC6285] + TLLEI = 7, // Transport-Layer Third-Party Loss Early Indication [RFC6642] + RTCP_ECN_FB = 8, // RTCP ECN Feedback [RFC6679] + PAUSE_RESUME = 9, // Media Pause/Resume [RFC7728] + DBI = 10, // Delay Budget Information (DBI) [3GPP TS 26.114 v16.3.0][Ozgur_Oyman] + TWCC = 15, // Transport-Wide Congestion Control [RFC8888] + // 11-30 // Unassigned + // Extension = 31 // Reserved for future extensions [RFC4585] +} + +/// +/// The different types of Feedback Message Types. (RFC4585) https://tools.ietf.org/html/rfc4585#page-35 +/// +public enum PSFBFeedbackTypesEnum : byte +{ + unassigned = 0, // Unassigned + PLI = 1, // Picture Loss Indication [RFC4585] + SLI = 2, // Slice Loss Indication [RFC4585] + RPSI = 3, // Reference Picture Selection Indication [RFC4585] + FIR = 4, // Full Intra Request Command [RFC5104] + TSTR = 5, // Temporal-Spatial Trade-off Request [RFC5104] + TSTN = 6, // Temporal-Spatial Trade-off Notification [RFC5104] + VBCM = 7, // Video Back Channel Message [RFC5104] + PSLEI = 8, // Payload-Specific Third-Party Loss Early Indication [RFC6642] + ROI = 9, // Video region-of-interest (ROI) [3GPP TS 26.114 v16.3.0][Ozgur_Oyman] + LRR = 10, // Layer Refresh Request Command [RFC-ietf-avtext-lrr-07] + // 11-14 // Unassigned + AFB = 15 // Application Layer Feedback [RFC4585] + // 16-30 // Unassigned + // Extension = 31 //Extension Reserved for future extensions [RFC4585] +} + +public enum FeedbackProtocol +{ + RTCP = 0, + PSFB = 1 +} + +public partial class RTCPFeedback : IByteSerializable +{ + private static readonly ILogger logger = LogFactory.CreateLogger(); + + public int SENDER_PAYLOAD_SIZE = 20; + public int MIN_PACKET_SIZE; + + public RTCPHeader Header; + public uint SenderSSRC; // Packet Sender + public uint MediaSSRC; + public ushort PID; // Packet ID (PID): 16 bits to specify a lost packet, the RTP sequence number of the lost packet. + public ushort BLP; // bitmask of following lost packets (BLP): 16 bits + public uint FCI; // Feedback Control Information (FCI) + + // REMB Parameters + // TODO: Maybe we need to separate RTCPFeedback into specialized classes to better implement different kind of messages + public string? UniqueID; + public byte NumSsrcs; + public byte BitrateExp; // Bitrate Expoent + public uint BitrateMantissa; //Bits per Second + public uint FeedbackSSRC //Packet sender { - unassigned = 0, // Unassigned - NACK = 1, // Generic NACK Generic negative acknowledgment [RFC4585] - // reserved = 2 // Reserved [RFC5104] - TMMBR = 3, // Temporary Maximum Media Stream Bit Rate Request [RFC5104] - TMMBN = 4, // Temporary Maximum Media Stream Bit Rate Notification [RFC5104] - RTCP_SR_REQ = 5, // RTCP Rapid Resynchronisation Request [RFC6051] - RAMS = 6, // Rapid Acquisition of Multicast Sessions [RFC6285] - TLLEI = 7, // Transport-Layer Third-Party Loss Early Indication [RFC6642] - RTCP_ECN_FB = 8, // RTCP ECN Feedback [RFC6679] - PAUSE_RESUME = 9, // Media Pause/Resume [RFC7728] - DBI = 10, // Delay Budget Information (DBI) [3GPP TS 26.114 v16.3.0][Ozgur_Oyman] - TWCC = 15, // Transport-Wide Congestion Control [RFC8888] - // 11-30 // Unassigned - // Extension = 31 // Reserved for future extensions [RFC4585] + get => FeedbackSSRCs is { Length: > 0 } ? FeedbackSSRCs[0] : 0; + + internal set + { + if (FeedbackSSRCs == null || FeedbackSSRCs?.Length == 0) + { + FeedbackSSRCs = new uint[Math.Max((int)NumSsrcs, 1)]; + } + + Debug.Assert(FeedbackSSRCs is not null); + FeedbackSSRCs[0] = value; + } + } + + public uint[] FeedbackSSRCs = [0]; // Packet Senders + + + public RTCPFeedback(uint senderSsrc, uint mediaSsrc, RTCPFeedbackTypesEnum feedbackMessageType, ushort sequenceNo, ushort bitMask) + { + Header = new RTCPHeader(feedbackMessageType); + SENDER_PAYLOAD_SIZE = 12; + MIN_PACKET_SIZE = RTCPHeader.HEADER_BYTES_LENGTH + SENDER_PAYLOAD_SIZE; + SenderSSRC = senderSsrc; + MediaSSRC = mediaSsrc; + PID = sequenceNo; + BLP = bitMask; } /// - /// The different types of Feedback Message Types. (RFC4585) - /// https://tools.ietf.org/html/rfc4585#page-35 + /// Constructor for RTP feedback reports that do not require any additional feedback control indication parameters + /// (e.g. RTCP Rapid Resynchronisation Request). /// - public enum PSFBFeedbackTypesEnum : byte + /// The payload specific feedback type. + public RTCPFeedback(uint senderSsrc, uint mediaSsrc, RTCPFeedbackTypesEnum feedbackMessageType) { - unassigned = 0, // Unassigned - PLI = 1, // Picture Loss Indication [RFC4585] - SLI = 2, // Slice Loss Indication [RFC4585] - RPSI = 3, // Reference Picture Selection Indication [RFC4585] - FIR = 4, // Full Intra Request Command [RFC5104] - TSTR = 5, // Temporal-Spatial Trade-off Request [RFC5104] - TSTN = 6, // Temporal-Spatial Trade-off Notification [RFC5104] - VBCM = 7, // Video Back Channel Message [RFC5104] - PSLEI = 8, // Payload-Specific Third-Party Loss Early Indication [RFC6642] - ROI = 9, // Video region-of-interest (ROI) [3GPP TS 26.114 v16.3.0][Ozgur_Oyman] - LRR = 10, // Layer Refresh Request Command [RFC-ietf-avtext-lrr-07] - // 11-14 // Unassigned - AFB = 15 // Application Layer Feedback [RFC4585] - // 16-30 // Unassigned - // Extension = 31 //Extension Reserved for future extensions [RFC4585] + Header = new RTCPHeader(feedbackMessageType); + SenderSSRC = senderSsrc; + MediaSSRC = mediaSsrc; + SENDER_PAYLOAD_SIZE = 8; } - public enum FeedbackProtocol + /// + /// Constructor for payload feedback reports that do not require any additional feedback control indication + /// parameters (e.g. Picture Loss Indication reports). + /// + /// The payload specific feedback type. + public RTCPFeedback(uint senderSsrc, uint mediaSsrc, PSFBFeedbackTypesEnum feedbackMessageType) { - RTCP = 0, - PSFB = 1 + Header = new RTCPHeader(feedbackMessageType); + SenderSSRC = senderSsrc; + MediaSSRC = mediaSsrc; + SENDER_PAYLOAD_SIZE = 8; } - public class RTCPFeedback + /// + /// Create a new RTCP Report from a serialised byte array. + /// + /// The byte array holding the serialised feedback report. + public RTCPFeedback(ReadOnlySpan packet) { - private static readonly ILogger logger = LogFactory.CreateLogger(); - - public int SENDER_PAYLOAD_SIZE = 20; - public int MIN_PACKET_SIZE = 0; - - public RTCPHeader Header; - public uint SenderSSRC; // Packet Sender - public uint MediaSSRC; - public ushort PID; // Packet ID (PID): 16 bits to specify a lost packet, the RTP sequence number of the lost packet. - public ushort BLP; // bitmask of following lost packets (BLP): 16 bits - public uint FCI; // Feedback Control Information (FCI) - - // REMB Parameters - // TODO: Maybe we need to separate RTCPFeedback into specialized classes to better implement different kind of messages - public string UniqueID = null; - public byte NumSsrcs = 0; - public byte BitrateExp = 0; // Bitrate Expoent - public uint BitrateMantissa = 0; //Bits per Second - public uint FeedbackSSRC //Packet sender - { - get => FeedbackSSRCs==null || FeedbackSSRCs.Length == 0 ? 0 : FeedbackSSRCs[0]; - - set - { - if(FeedbackSSRCs==null || FeedbackSSRCs?.Length==0) - { - FeedbackSSRCs = new uint[Math.Max((int)NumSsrcs,1)]; - } - FeedbackSSRCs[0] = value; - } - } + Header = new RTCPHeader(packet); - public uint[] FeedbackSSRCs=[0]; // Packet Senders - + var payloadIndex = RTCPHeader.HEADER_BYTES_LENGTH; + SenderSSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(payloadIndex)); + MediaSSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(payloadIndex + 4)); - public RTCPFeedback(uint senderSsrc, uint mediaSsrc, RTCPFeedbackTypesEnum feedbackMessageType, ushort sequenceNo, ushort bitMask) + switch (Header) { - Header = new RTCPHeader(feedbackMessageType); - SENDER_PAYLOAD_SIZE = 12; - MIN_PACKET_SIZE = RTCPHeader.HEADER_BYTES_LENGTH + SENDER_PAYLOAD_SIZE; - SenderSSRC = senderSsrc; - MediaSSRC = mediaSsrc; - PID = sequenceNo; - BLP = bitMask; - } + case { PacketType: RTCPReportTypesEnum.RTPFB, FeedbackMessageType: RTCPFeedbackTypesEnum.RTCP_SR_REQ }: + SENDER_PAYLOAD_SIZE = 8; + // PLI feedback reports do no have any additional parameters. + break; + case { PacketType: RTCPReportTypesEnum.RTPFB }: + SENDER_PAYLOAD_SIZE = 12; + PID = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(payloadIndex + 8)); + BLP = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(payloadIndex + 10)); + break; + + case { PacketType: RTCPReportTypesEnum.PSFB, PayloadFeedbackMessageType: PSFBFeedbackTypesEnum.PLI }: + SENDER_PAYLOAD_SIZE = 8; + break; + + // We have a lot of different kind of extension messages + // In case below we will handle the specific REMB Message https://datatracker.ietf.org/doc/html/draft-alvestrand-rmcat-remb-03#page-3 + case { PacketType: RTCPReportTypesEnum.PSFB, PayloadFeedbackMessageType: PSFBFeedbackTypesEnum.AFB }: + + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //| Unique identifier 'R' 'E' 'M' 'B' | + //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //| Num SSRC | BR Exp | BR Mantissa | + //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //| SSRC feedback | + + SENDER_PAYLOAD_SIZE = 8 + 12; // 8 bytes from (SenderSSRC + MediaSSRC) + extra 12 bytes from REMB Definition + + var currentCounter = payloadIndex + 8; + UniqueID = Encoding.ASCII.GetString(packet.Slice(currentCounter, 4)); + currentCounter += 4; + + if (string.Equals(UniqueID, "REMB", StringComparison.CurrentCultureIgnoreCase)) + { + // Read first 8 bits + NumSsrcs = packet[currentCounter]; + currentCounter++; - /// - /// Constructor for RTP feedback reports that do not require any additional feedback control - /// indication parameters (e.g. RTCP Rapid Resynchronisation Request). - /// - /// The payload specific feedback type. - public RTCPFeedback(uint senderSsrc, uint mediaSsrc, RTCPFeedbackTypesEnum feedbackMessageType) - { - Header = new RTCPHeader(feedbackMessageType); - SenderSSRC = senderSsrc; - MediaSSRC = mediaSsrc; - SENDER_PAYLOAD_SIZE = 8; - } + // Now read next 6 bits + BitrateExp = (byte)(packet[currentCounter] >> 2); - /// - /// Constructor for payload feedback reports that do not require any additional feedback control - /// indication parameters (e.g. Picture Loss Indication reports). - /// - /// The payload specific feedback type. - public RTCPFeedback(uint senderSsrc, uint mediaSsrc, PSFBFeedbackTypesEnum feedbackMessageType) - { - Header = new RTCPHeader(feedbackMessageType); - SenderSSRC = senderSsrc; - MediaSSRC = mediaSsrc; - SENDER_PAYLOAD_SIZE = 8; - } + // Now read next 18 bits + BitrateMantissa = + ((((uint)packet[currentCounter++]) & 0b11U) << 16) | + (((uint)packet[currentCounter++]) << 8) | + ((uint)packet[currentCounter++]); - /// - /// Create a new RTCP Report from a serialised byte array. - /// - /// The byte array holding the serialised feedback report. - public RTCPFeedback(byte[] packet) - { - Header = new RTCPHeader(packet); + FeedbackSSRCs = new uint[NumSsrcs]; + for (var i = 0; i < NumSsrcs; i++) + { + FeedbackSSRCs[i] = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(currentCounter)); - int payloadIndex = RTCPHeader.HEADER_BYTES_LENGTH; - SenderSSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(payloadIndex)); - MediaSSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(payloadIndex + 4)); + if (i < FeedbackSSRCs.Length - 1) + { + currentCounter += 4; + } + } - switch (Header) - { - case var h when h.PacketType == RTCPReportTypesEnum.RTPFB && h.FeedbackMessageType == RTCPFeedbackTypesEnum.RTCP_SR_REQ: - SENDER_PAYLOAD_SIZE = 8; - // PLI feedback reports do no have any additional parameters. - break; - case var h when h.PacketType == RTCPReportTypesEnum.RTPFB: - SENDER_PAYLOAD_SIZE = 12; - PID = BinaryPrimitives.ReadUInt16BigEndian(packet.AsSpan(payloadIndex + 8)); - BLP = BinaryPrimitives.ReadUInt16BigEndian(packet.AsSpan(payloadIndex + 10)); - break; - - case var h when h.PacketType == RTCPReportTypesEnum.PSFB && h.PayloadFeedbackMessageType == PSFBFeedbackTypesEnum.PLI: - SENDER_PAYLOAD_SIZE = 8; - break; - - // We have a lot of different kind of extension messages - // In case below we will handle the specific REMB Message https://datatracker.ietf.org/doc/html/draft-alvestrand-rmcat-remb-03#page-3 - case var h when h.PacketType == RTCPReportTypesEnum.PSFB && h.PayloadFeedbackMessageType == PSFBFeedbackTypesEnum.AFB: + //var additionalFeedbacksIgnored = NumSsrcs - 1; + //currentCounter += additionalFeedbacksIgnored * 4; + SENDER_PAYLOAD_SIZE = currentCounter; + } - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - //| Unique identifier 'R' 'E' 'M' 'B' | - //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - //| Num SSRC | BR Exp | BR Mantissa | - //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - //| SSRC feedback | + break; - SENDER_PAYLOAD_SIZE = 8 + 12; // 8 bytes from (SenderSSRC + MediaSSRC) + extra 12 bytes from REMB Definition + //default: + // throw new NotImplementedException($"Deserialisation for feedback report {Header.PacketType} not yet implemented."); + } + } - var currentCounter = payloadIndex + 8; - UniqueID = System.Text.ASCIIEncoding.ASCII.GetString(packet, currentCounter, 4); - currentCounter += 4; + /// + public int GetByteCount() => RTCPHeader.HEADER_BYTES_LENGTH + SENDER_PAYLOAD_SIZE; - if (string.Equals(UniqueID,"REMB", StringComparison.CurrentCultureIgnoreCase)) - { - // Read first 8 bits - NumSsrcs = packet[currentCounter]; - currentCounter++; - - // Now read next 6 bits - BitrateExp = (byte)(packet[currentCounter] >> 2); - - // Now read next 18 bits - var remaininMantissaBytes = new byte[] { (byte)0, (byte)(packet[currentCounter] & 3), packet[currentCounter + 1], packet[currentCounter + 2] }; - BitrateMantissa = BinaryPrimitives.ReadUInt32BigEndian(remaininMantissaBytes); - - currentCounter += 3; - FeedbackSSRCs=new uint[NumSsrcs]; - for (int i = 0; i < NumSsrcs; i++) - { - FeedbackSSRCs[i] = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(currentCounter)); + /// + public int WriteBytes(Span buffer) + { + //0 1 2 3 + //0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //|V=2|P| FMT | PT | length | + //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //| SSRC of packet sender | + //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //| SSRC of media source | + //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //: Feedback Control Information(FCI) : + //: : + var size = GetByteCount(); + + if (buffer.Length < size) + { + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); + } - if (i < FeedbackSSRCs.Length - 1) - { - currentCounter += 4; - } - } + WriteBytesCore(buffer.Slice(0, size)); - //var additionalFeedbacksIgnored = NumSsrcs - 1; - //currentCounter += additionalFeedbacksIgnored * 4; - SENDER_PAYLOAD_SIZE = currentCounter; - } + return size; + } - break; + private void WriteBytesCore(Span buffer) + { + Header.SetLength((ushort)(buffer.Length / 4 - 1)); + _ = Header.WriteBytes(buffer); - //default: - // throw new NotImplementedException($"Deserialisation for feedback report {Header.PacketType} not yet implemented."); - } - } + // All feedback packets require the Sender and Media SSRC's to be set. + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH), SenderSSRC); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 4), MediaSSRC); - //0 1 2 3 - //0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - //|V=2|P| FMT | PT | length | - //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - //| SSRC of packet sender | - //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - //| SSRC of media source | - //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - //: Feedback Control Information(FCI) : - //: : - public byte[] GetBytes() + switch (Header) { - byte[] buffer = new byte[RTCPHeader.HEADER_BYTES_LENGTH + SENDER_PAYLOAD_SIZE]; - Header.SetLength((ushort)(buffer.Length / 4 - 1)); + case { PacketType: RTCPReportTypesEnum.RTPFB, FeedbackMessageType: RTCPFeedbackTypesEnum.RTCP_SR_REQ }: + // PLI feedback reports do no have any additional parameters. + break; + case { PacketType: RTCPReportTypesEnum.RTPFB }: + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 8), PID); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 10), BLP); + break; + + case { PacketType: RTCPReportTypesEnum.PSFB, PayloadFeedbackMessageType: PSFBFeedbackTypesEnum.PLI }: + break; + case { PacketType: RTCPReportTypesEnum.PSFB, PayloadFeedbackMessageType: PSFBFeedbackTypesEnum.AFB }: + + // There's have a lot of different kind of extension messages + // In case below we will handle the specific REMB Message https://datatracker.ietf.org/doc/html/draft-alvestrand-rmcat-remb-03#page-3 - Buffer.BlockCopy(Header.GetBytes(), 0, buffer, 0, RTCPHeader.HEADER_BYTES_LENGTH); - int payloadIndex = RTCPHeader.HEADER_BYTES_LENGTH; + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //| Unique identifier 'R' 'E' 'M' 'B' | + //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //| Num SSRC | BR Exp | BR Mantissa | + //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //| SSRC feedback | + + //Fix UniqueID Size + Span uniqueIdBytes = stackalloc char[4]; + uniqueIdBytes.Clear(); + if (UniqueID is not null) + { + var length = Math.Min(4, UniqueID.Length); + UniqueID.AsSpan(0, length).CopyTo(uniqueIdBytes); + } - // All feedback packets require the Sender and Media SSRC's to be set. - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(payloadIndex), SenderSSRC); - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(payloadIndex + 4), MediaSSRC); + var currentCounter = RTCPHeader.HEADER_BYTES_LENGTH + 8; + Encoding.ASCII.GetBytes(uniqueIdBytes, buffer.Slice(currentCounter)); + currentCounter += 4; - switch (Header) - { - case var h when h.PacketType == RTCPReportTypesEnum.RTPFB && h.FeedbackMessageType == RTCPFeedbackTypesEnum.RTCP_SR_REQ: - // PLI feedback reports do no have any additional parameters. - break; - case var h when h.PacketType == RTCPReportTypesEnum.RTPFB: - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(payloadIndex + 8), PID); - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(payloadIndex + 10), BLP); - break; - - case var h when h.PacketType == RTCPReportTypesEnum.PSFB && h.PayloadFeedbackMessageType == PSFBFeedbackTypesEnum.PLI: - break; - case var h when h.PacketType == RTCPReportTypesEnum.PSFB && h.PayloadFeedbackMessageType == PSFBFeedbackTypesEnum.AFB: - - // There's have a lot of different kind of extension messages - // In case below we will handle the specific REMB Message https://datatracker.ietf.org/doc/html/draft-alvestrand-rmcat-remb-03#page-3 - - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - //| Unique identifier 'R' 'E' 'M' 'B' | - //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - //| Num SSRC | BR Exp | BR Mantissa | - //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - //| SSRC feedback | - var currentCounter = payloadIndex + 8; - - //Fix UniqueID Size - if(string.IsNullOrEmpty(UniqueID)) - { - UniqueID = System.Text.ASCIIEncoding.ASCII.GetString(new byte[4]); - } - while (UniqueID.Length < 4) - { - UniqueID += (char)0; - } + if (string.Equals(UniqueID, "REMB", StringComparison.OrdinalIgnoreCase)) + { + // Copy NumSsrcs + buffer[currentCounter] = NumSsrcs; + currentCounter++; - Buffer.BlockCopy(System.Text.ASCIIEncoding.ASCII.GetBytes(UniqueID), 0, buffer, currentCounter, 4); - currentCounter += 4; + var temp = BitrateMantissa; + buffer[currentCounter + 2] = (byte)temp; + buffer[currentCounter + 1] = (byte)(temp >>= 8); + buffer[currentCounter] = (byte)((BitrateExp << 2) | (byte)((temp >>= 8) & 0b11)); + currentCounter += 3; - if (string.Equals(UniqueID, "REMB", StringComparison.CurrentCultureIgnoreCase)) + for (var i = 0; i < FeedbackSSRCs.Length; i++) { - // Copy NumSsrcs - buffer[currentCounter] = NumSsrcs; - currentCounter++; - - - byte[] remaininMantissaBytes; - remaininMantissaBytes = new byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(remaininMantissaBytes, BitrateMantissa); - buffer[currentCounter] = (byte)((BitrateExp << 2) | (remaininMantissaBytes[1] & 3)); - buffer[currentCounter + 1] = remaininMantissaBytes[2]; - buffer[currentCounter + 2] = remaininMantissaBytes[3]; - - currentCounter += 3; - - for (int i = 0; i < FeedbackSSRCs.Length; i++) - { - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(currentCounter), FeedbackSSRCs[i]); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(currentCounter), FeedbackSSRCs[i]); - if (i < FeedbackSSRCs.Length - 1) - { - currentCounter += 4; - } + if (i < FeedbackSSRCs.Length - 1) + { + currentCounter += 4; } - } + } - break; - default: - logger?.LogDebug("Serialization for feedback report {PacketType} and message type {FeedbackMessageType} not yet implemented.", Header.PacketType, Header.FeedbackMessageType); - break; - //throw new NotImplementedException($"Serialisation for feedback report {Header.PacketType} and message type " - //+ $"{Header.FeedbackMessageType} not yet implemented."); - } - return buffer; + break; + default: + logger?.LogRtcpFeedbackSerializationNotImplemented(Header.PacketType, Header.FeedbackMessageType); + break; + //throw new NotImplementedException($"Serialisation for feedback report {Header.PacketType} and message type " + //+ $"{Header.FeedbackMessageType} not yet implemented."); } } } @@ -348,47 +358,47 @@ public byte[] GetBytes() /* 6. Format of RTCP Feedback Messages - This section defines the format of the low-delay RTCP feedback - messages.These messages are classified into three categories as - follows: - - - Transport layer FB messages - - Payload-specific FB messages - - Application layer FB messages - - Transport layer FB messages are intended to transmit general purpose - feedback information, i.e., information independent of the particular - codec or the application in use.The information is expected to be - generated and processed at the transport/RTP layer. Currently, only - a generic negative acknowledgement (NACK) message is defined. - - Payload-specific FB messages transport information that is specific - to a certain payload type and will be generated and acted upon at the - codec "layer". This document defines a common header to be used in - conjunction with all payload-specific FB messages.The definition of - specific messages is left either to RTP payload format specifications - or to additional feedback format documents. - - Application layer FB messages provide a means to transparently convey - feedback from the receiver's to the sender's application. The - information contained in such a message is not expected to be acted - upon at the transport/RTP or the codec layer.The data to be - exchanged between two application instances is usually defined in the - application protocol specification and thus can be identified by the - application so that there is no need for additional external - information.Hence, this document defines only a common header to be - used along with all application layer FB messages. From a protocol - point of view, an application layer FB message is treated as a - special case of a payload-specific FB message. - - Note: Proper processing of some FB messages at the media sender - side may require the sender to know which payload type the FB - message refers to.Most of the time, this knowledge can likely be - derived from a media stream using only a single payload type. - However, if several codecs are used simultaneously (e.g., with - audio and DTMF) or when codec changes occur, the payload type - information may need to be conveyed explicitly as part of the FB - message.This applies to all +This section defines the format of the low-delay RTCP feedback +messages.These messages are classified into three categories as +follows: + +- Transport layer FB messages +- Payload-specific FB messages +- Application layer FB messages + +Transport layer FB messages are intended to transmit general purpose +feedback information, i.e., information independent of the particular +codec or the application in use.The information is expected to be +generated and processed at the transport/RTP layer. Currently, only +a generic negative acknowledgement (NACK) message is defined. + +Payload-specific FB messages transport information that is specific +to a certain payload type and will be generated and acted upon at the +codec "layer". This document defines a common header to be used in +conjunction with all payload-specific FB messages.The definition of +specific messages is left either to RTP payload format specifications +or to additional feedback format documents. + +Application layer FB messages provide a means to transparently convey +feedback from the receiver's to the sender's application. The +information contained in such a message is not expected to be acted +upon at the transport/RTP or the codec layer.The data to be +exchanged between two application instances is usually defined in the +application protocol specification and thus can be identified by the +application so that there is no need for additional external +information.Hence, this document defines only a common header to be +used along with all application layer FB messages. From a protocol +point of view, an application layer FB message is treated as a +special case of a payload-specific FB message. + + Note: Proper processing of some FB messages at the media sender + side may require the sender to know which payload type the FB + message refers to.Most of the time, this knowledge can likely be + derived from a media stream using only a single payload type. + However, if several codecs are used simultaneously (e.g., with + audio and DTMF) or when codec changes occur, the payload type + information may need to be conveyed explicitly as part of the FB + message.This applies to all @@ -398,97 +408,97 @@ message.This applies to all RFC 4585 RTP/AVPF July 2006 - payload-specific as well as application layer FB messages. It is - up to the specification of an FB message to define how payload - type information is transmitted. + payload-specific as well as application layer FB messages. It is + up to the specification of an FB message to define how payload + type information is transmitted. - This document defines two transport layer and three (video) payload- - specific FB messages as well as a single container for application - layer FB messages. Additional transport layer and payload-specific - FB messages MAY be defined in other documents and MUST be registered - through IANA (see Section 9, "IANA Considerations"). +This document defines two transport layer and three (video) payload- +specific FB messages as well as a single container for application +layer FB messages. Additional transport layer and payload-specific +FB messages MAY be defined in other documents and MUST be registered +through IANA (see Section 9, "IANA Considerations"). - The general syntax and semantics for the above RTCP FB message types - are described in the following subsections. +The general syntax and semantics for the above RTCP FB message types +are described in the following subsections. 6.1. Common Packet Format for Feedback Messages - All FB messages MUST use a common packet format that is depicted in - Figure 3: +All FB messages MUST use a common packet format that is depicted in +Figure 3: - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |V=2|P| FMT | PT | length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | SSRC of packet sender | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | SSRC of media source | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - : Feedback Control Information(FCI) : - : : +0 1 2 3 +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +|V=2|P| FMT | PT | length | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| SSRC of packet sender | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| SSRC of media source | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +: Feedback Control Information(FCI) : +: : - Figure 3: Common Packet Format for Feedback Messages + Figure 3: Common Packet Format for Feedback Messages - The fields V, P, SSRC, and length are defined in the RTP - specification[2], the respective meaning being summarized below: +The fields V, P, SSRC, and length are defined in the RTP +specification[2], the respective meaning being summarized below: - version(V) : 2 bits - This field identifies the RTP version.The current version is 2. +version(V) : 2 bits + This field identifies the RTP version.The current version is 2. - padding(P) : 1 bit - If set, the padding bit indicates that the packet contains - additional padding octets at the end that are not part of the - control information but are included in the length field. +padding(P) : 1 bit + If set, the padding bit indicates that the packet contains + additional padding octets at the end that are not part of the + control information but are included in the length field. Ott, et al.Standards Track[Page 32] RFC 4585 RTP/AVPF July 2006 - Feedback message type (FMT): 5 bits - This field identifies the type of the FB message and is - interpreted relative to the type (transport layer, payload- - specific, or application layer feedback). The values for each of - the three feedback types are defined in the respective sections - below. - - Payload type (PT): 8 bits - This is the RTCP packet type that identifies the packet as being - an RTCP FB message.Two values are defined by the IANA: - - Name | Value | Brief Description - ----------+-------+------------------------------------ - RTPFB | 205 | Transport layer FB message - PSFB | 206 | Payload-specific FB message - - Length: 16 bits - The length of this packet in 32-bit words minus one, including the - header and any padding. This is in line with the definition of - the length field used in RTCP sender and receiver reports[3]. - - SSRC of packet sender: 32 bits - The synchronization source identifier for the originator of this - packet. - - SSRC of media source: 32 bits - The synchronization source identifier of the media source that - this piece of feedback information is related to. - - Feedback Control Information (FCI): variable length - The following three sections define which additional information - MAY be included in the FB message for each type of feedback: - transport layer, payload-specific, or application layer feedback. - Note that further FCI contents MAY be specified in further - documents. - - Each RTCP feedback packet MUST contain at least one FB message in the - FCI field.Sections 6.2 and 6.3 define for each FCI type, whether or - not multiple FB messages MAY be compressed into a single FCI field. - If this is the case, they MUST be of the same type, i.e., same FMT. - If multiple types of feedback messages, i.e., several FMTs, need to - be conveyed, then several RTCP FB messages MUST be generated and - SHOULD be concatenated in the same compound RTCP packet. +Feedback message type (FMT): 5 bits + This field identifies the type of the FB message and is + interpreted relative to the type (transport layer, payload- + specific, or application layer feedback). The values for each of + the three feedback types are defined in the respective sections + below. + +Payload type (PT): 8 bits + This is the RTCP packet type that identifies the packet as being + an RTCP FB message.Two values are defined by the IANA: + + Name | Value | Brief Description + ----------+-------+------------------------------------ + RTPFB | 205 | Transport layer FB message + PSFB | 206 | Payload-specific FB message + +Length: 16 bits + The length of this packet in 32-bit words minus one, including the + header and any padding. This is in line with the definition of + the length field used in RTCP sender and receiver reports[3]. + +SSRC of packet sender: 32 bits + The synchronization source identifier for the originator of this + packet. + +SSRC of media source: 32 bits + The synchronization source identifier of the media source that + this piece of feedback information is related to. + +Feedback Control Information (FCI): variable length + The following three sections define which additional information + MAY be included in the FB message for each type of feedback: + transport layer, payload-specific, or application layer feedback. + Note that further FCI contents MAY be specified in further + documents. + +Each RTCP feedback packet MUST contain at least one FB message in the +FCI field.Sections 6.2 and 6.3 define for each FCI type, whether or +not multiple FB messages MAY be compressed into a single FCI field. +If this is the case, they MUST be of the same type, i.e., same FMT. +If multiple types of feedback messages, i.e., several FMTs, need to +be conveyed, then several RTCP FB messages MUST be generated and +SHOULD be concatenated in the same compound RTCP packet. Ott, et al. Standards Track [Page 33] @@ -497,52 +507,52 @@ RFC 4585 RTP/AVPF July 2006 6.2. Transport Layer Feedback Messages - Transport layer FB messages are identified by the value RTPFB as RTCP - message type. +Transport layer FB messages are identified by the value RTPFB as RTCP +message type. - A single general purpose transport layer FB message is defined in - this document: Generic NACK. It is identified by means of the FMT - parameter as follows: +A single general purpose transport layer FB message is defined in +this document: Generic NACK. It is identified by means of the FMT +parameter as follows: - 0: unassigned - 1: Generic NACK - 2-30: unassigned - 31: reserved for future expansion of the identifier number space +0: unassigned +1: Generic NACK +2-30: unassigned +31: reserved for future expansion of the identifier number space - The following subsection defines the formats of the FCI field for - this type of FB message. Further generic feedback messages MAY be - defined in the future. +The following subsection defines the formats of the FCI field for +this type of FB message. Further generic feedback messages MAY be +defined in the future. 6.2.1. Generic NACK - The Generic NACK message is identified by PT= RTPFB and FMT = 1. +The Generic NACK message is identified by PT= RTPFB and FMT = 1. - The FCI field MUST contain at least one and MAY contain more than one - Generic NACK. +The FCI field MUST contain at least one and MAY contain more than one +Generic NACK. - The Generic NACK is used to indicate the loss of one or more RTP - packets.The lost packet(s) are identified by the means of a packet - identifier and a bit mask. +The Generic NACK is used to indicate the loss of one or more RTP +packets.The lost packet(s) are identified by the means of a packet +identifier and a bit mask. - Generic NACK feedback SHOULD NOT be used if the underlying transport - protocol is capable of providing similar feedback information to the - sender (as may be the case, e.g., with DCCP). +Generic NACK feedback SHOULD NOT be used if the underlying transport +protocol is capable of providing similar feedback information to the +sender (as may be the case, e.g., with DCCP). - The Feedback Control Information(FCI) field has the following Syntax - (Figure 4) : +The Feedback Control Information(FCI) field has the following Syntax +(Figure 4) : - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | PID | BLP | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0 1 2 3 +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| PID | BLP | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - Figure 4: Syntax for the Generic NACK message + Figure 4: Syntax for the Generic NACK message - Packet ID(PID): 16 bits - The PID field is used to specify a lost packet.The PID field +Packet ID(PID): 16 bits + The PID field is used to specify a lost packet.The PID field - refers to the RTP sequence number of the lost packet. + refers to the RTP sequence number of the lost packet. Ott, et al. Standards Track [Page 34] @@ -551,59 +561,59 @@ RFC 4585 RTP/AVPF July 2006 - bitmask of following lost packets (BLP): 16 bits - The BLP allows for reporting losses of any of the 16 RTP packets +bitmask of following lost packets (BLP): 16 bits + The BLP allows for reporting losses of any of the 16 RTP packets - immediately following the RTP packet indicated by the PID.The - BLP's definition is identical to that given in [6]. Denoting the + immediately following the RTP packet indicated by the PID.The + BLP's definition is identical to that given in [6]. Denoting the - BLP's least significant bit as bit 1, and its most significant bit - as bit 16, then bit i of the bit mask is set to 1 if the receiver + BLP's least significant bit as bit 1, and its most significant bit + as bit 16, then bit i of the bit mask is set to 1 if the receiver - has not received RTP packet number (PID+i) (modulo 2^16) and - indicates this packet is lost; bit i is set to 0 otherwise.Note - that the sender MUST NOT assume that a receiver has received a - packet because its bit mask was set to 0. For example, the least + has not received RTP packet number (PID+i) (modulo 2^16) and + indicates this packet is lost; bit i is set to 0 otherwise.Note + that the sender MUST NOT assume that a receiver has received a + packet because its bit mask was set to 0. For example, the least - significant bit of the BLP would be set to 1 if the packet + significant bit of the BLP would be set to 1 if the packet - corresponding to the PID and the following packet have been lost. - However, the sender cannot infer that packets PID+2 through PID+16 + corresponding to the PID and the following packet have been lost. + However, the sender cannot infer that packets PID+2 through PID+16 - have been received simply because bits 2 through 15 of the BLP are - 0; all the sender knows is that the receiver has not reported them - as lost at this time. + have been received simply because bits 2 through 15 of the BLP are + 0; all the sender knows is that the receiver has not reported them + as lost at this time. - The length of the FB message MUST be set to 2+n, with n being the +The length of the FB message MUST be set to 2+n, with n being the - number of Generic NACKs contained in the FCI field. +number of Generic NACKs contained in the FCI field. - The Generic NACK message implicitly references the payload type - through the sequence number(s). +The Generic NACK message implicitly references the payload type +through the sequence number(s). 6.3. Payload-Specific Feedback Messages - Payload-Specific FB messages are identified by the value PT= PSFB as - RTCP message type. +Payload-Specific FB messages are identified by the value PT= PSFB as +RTCP message type. - Three payload-specific FB messages are defined so far plus an - application layer FB message.They are identified by means of the - FMT parameter as follows: +Three payload-specific FB messages are defined so far plus an +application layer FB message.They are identified by means of the +FMT parameter as follows: - 0: unassigned - 1: Picture Loss Indication (PLI) - 2: Slice Loss Indication (SLI) - 3: Reference Picture Selection Indication (RPSI) - 4-14: unassigned - 15: Application layer FB (AFB) message - 16-30: unassigned - 31: reserved for future expansion of the sequence number space + 0: unassigned + 1: Picture Loss Indication (PLI) + 2: Slice Loss Indication (SLI) + 3: Reference Picture Selection Indication (RPSI) + 4-14: unassigned + 15: Application layer FB (AFB) message + 16-30: unassigned + 31: reserved for future expansion of the sequence number space - The following subsections define the FCI formats for the payload- +The following subsections define the FCI formats for the payload- - specific FB messages, Section 6.4 defines FCI format for the - application layer FB message. +specific FB messages, Section 6.4 defines FCI format for the +application layer FB message. Ott, et al. Standards Track [Page 35] @@ -613,58 +623,58 @@ RFC 4585 RTP/AVPF July 2006 6.3.1. Picture Loss Indication (PLI) - The PLI FB message is identified by PT= PSFB and FMT = 1. +The PLI FB message is identified by PT= PSFB and FMT = 1. - There MUST be exactly one PLI contained in the FCI field. +There MUST be exactly one PLI contained in the FCI field. 6.3.1.1. Semantics - With the Picture Loss Indication message, a decoder informs the +With the Picture Loss Indication message, a decoder informs the - encoder about the loss of an undefined amount of coded video data +encoder about the loss of an undefined amount of coded video data - belonging to one or more pictures. When used in conjunction with any - video coding scheme that is based on inter-picture prediction, an - encoder that receives a PLI becomes aware that the prediction chain +belonging to one or more pictures. When used in conjunction with any +video coding scheme that is based on inter-picture prediction, an +encoder that receives a PLI becomes aware that the prediction chain - may be broken.The sender MAY react to a PLI by transmitting an +may be broken.The sender MAY react to a PLI by transmitting an - intra-picture to achieve resynchronization (making this message - effectively similar to the FIR message as defined in [6]); however, - the sender MUST consider congestion control as outlined in Section 7, - which MAY restrict its ability to send an intra frame. +intra-picture to achieve resynchronization (making this message +effectively similar to the FIR message as defined in [6]); however, +the sender MUST consider congestion control as outlined in Section 7, +which MAY restrict its ability to send an intra frame. - Other RTP payload specifications such as RFC 2032 [6] +Other RTP payload specifications such as RFC 2032 [6] already define - a feedback mechanism for some for certain codecs. An application - supporting both schemes MUST use the feedback mechanism defined in - this specification when sending feedback. For backward compatibility - reasons, such an application SHOULD also be capable to receive and - react to the feedback scheme defined in the respective RTP payload - format, if this is required by that payload format. +a feedback mechanism for some for certain codecs. An application +supporting both schemes MUST use the feedback mechanism defined in +this specification when sending feedback. For backward compatibility +reasons, such an application SHOULD also be capable to receive and +react to the feedback scheme defined in the respective RTP payload +format, if this is required by that payload format. 6.3.1.2. Message Format - PLI does not require parameters. Therefore, the length field MUST be - 2, and there MUST NOT be any Feedback Control Information. +PLI does not require parameters. Therefore, the length field MUST be +2, and there MUST NOT be any Feedback Control Information. - The semantics of this FB message is independent of the payload type. +The semantics of this FB message is independent of the payload type. 6.3.1.3. Timing Rules - The timing follows the rules outlined in Section 3. In systems that - employ both PLI and other types of feedback, it may be advisable to - follow the Regular RTCP RR timing rules for PLI, since PLI is not as - delay critical as other FB types. +The timing follows the rules outlined in Section 3. In systems that +employ both PLI and other types of feedback, it may be advisable to +follow the Regular RTCP RR timing rules for PLI, since PLI is not as +delay critical as other FB types. 6.3.1.4. Remarks - PLI messages typically trigger the sending of full intra-pictures. - Intra-pictures are several times larger then predicted (inter-) - pictures. Their size is independent of the time they are generated. - In most environments, especially when employing bandwidth-limited - links, the use of an intra-picture implies an allowed delay that is a +PLI messages typically trigger the sending of full intra-pictures. +Intra-pictures are several times larger then predicted (inter-) +pictures. Their size is independent of the time they are generated. +In most environments, especially when employing bandwidth-limited +links, the use of an intra-picture implies an allowed delay that is a @@ -673,54 +683,54 @@ pictures. Their size is independent of the time they are generated. RFC 4585 RTP/AVPF July 2006 - significant multitude of the typical frame duration. An example: If - the sending frame rate is 10 fps, and an intra-picture is assumed to - be 10 times as big as an inter-picture, then a full second of latency - has to be accepted. In such an environment, there is no need for a - particular short delay in sending the FB message. Hence, waiting for - the next possible time slot allowed by RTCP timing rules as per [2] +significant multitude of the typical frame duration. An example: If +the sending frame rate is 10 fps, and an intra-picture is assumed to +be 10 times as big as an inter-picture, then a full second of latency +has to be accepted. In such an environment, there is no need for a +particular short delay in sending the FB message. Hence, waiting for +the next possible time slot allowed by RTCP timing rules as per [2] with Tmin=0 does not have a negative impact on the system - performance. +performance. 6.3.2. Slice Loss Indication (SLI) - The SLI FB message is identified by PT=PSFB and FMT=2. +The SLI FB message is identified by PT=PSFB and FMT=2. - The FCI field MUST contain at least one and MAY contain more than one - SLI. +The FCI field MUST contain at least one and MAY contain more than one +SLI. 6.3.2.1. Semantics - With the Slice Loss Indication, a decoder can inform an encoder that - it has detected the loss or corruption of one or several consecutive - macroblock(s) in scan order (see below). This FB message MUST NOT be - used for video codecs with non-uniform, dynamically changeable - macroblock sizes such as H.263 with enabled Annex Q. In such a case, - an encoder cannot always identify the corrupted spatial region. +With the Slice Loss Indication, a decoder can inform an encoder that +it has detected the loss or corruption of one or several consecutive +macroblock(s) in scan order (see below). This FB message MUST NOT be +used for video codecs with non-uniform, dynamically changeable +macroblock sizes such as H.263 with enabled Annex Q. In such a case, +an encoder cannot always identify the corrupted spatial region. 6.3.2.2. Format - The Slice Loss Indication uses one additional FCI field, the content - of which is depicted in Figure 6. The length of the FB message MUST - be set to 2 + n, with n being the number of SLIs contained in the FCI - field. +The Slice Loss Indication uses one additional FCI field, the content +of which is depicted in Figure 6. The length of the FB message MUST +be set to 2 + n, with n being the number of SLIs contained in the FCI +field. - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - + -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | First | Number | PictureID | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0 1 2 3 +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++ -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| First | Number | PictureID | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - Figure 6: Syntax of the Slice Loss Indication(SLI) + Figure 6: Syntax of the Slice Loss Indication(SLI) - First: 13 bits - The macroblock(MB) address of the first lost macroblock. The MB - numbering is done such that the macroblock in the upper left - corner of the picture is considered macroblock number 1 and the - number for each macroblock increases from left to right and then - from top to bottom in raster - scan order(such that if there is a - total of N macroblocks in a picture, the bottom right macroblock - is considered macroblock number N). +First: 13 bits + The macroblock(MB) address of the first lost macroblock. The MB + numbering is done such that the macroblock in the upper left + corner of the picture is considered macroblock number 1 and the + number for each macroblock increases from left to right and then + from top to bottom in raster - scan order(such that if there is a + total of N macroblocks in a picture, the bottom right macroblock + is considered macroblock number N). @@ -730,33 +740,33 @@ corner of the picture is considered macroblock number 1 and the RFC 4585 RTP / AVPF July 2006 - Number: 13 bits - The number of lost macroblocks, in scan order as discussed above. +Number: 13 bits + The number of lost macroblocks, in scan order as discussed above. - PictureID: 6 bits - The six least significant bits of the codec - specific identifier - that is used to reference the picture in which the loss of the - macroblock(s) has occurred. For many video codecs, the PictureID - is identical to the Temporal Reference. +PictureID: 6 bits + The six least significant bits of the codec - specific identifier + that is used to reference the picture in which the loss of the + macroblock(s) has occurred. For many video codecs, the PictureID + is identical to the Temporal Reference. - The applicability of this FB message is limited to a small set of - video codecs; therefore, no explicit payload type information is - provided. +The applicability of this FB message is limited to a small set of +video codecs; therefore, no explicit payload type information is +provided. 6.3.2.3.Timing Rules - The efficiency of algorithms using the Slice Loss Indication is - reduced greatly when the Indication is not transmitted in a timely - fashion.Motion compensation propagates corrupted pixels that are - not reported as being corrupted.Therefore, the use of the algorithm +The efficiency of algorithms using the Slice Loss Indication is +reduced greatly when the Indication is not transmitted in a timely +fashion.Motion compensation propagates corrupted pixels that are +not reported as being corrupted.Therefore, the use of the algorithm discussed in Section 3 is highly recommended. 6.3.2.4.Remarks - The term Slice is defined and used here in the sense of MPEG-1-- a - consecutive number of macroblocks in scan order. More recent video - coding standards sometimes have a different understanding of the term - Slice.In H.263(1998), for example, a concept known as "rectangular +The term Slice is defined and used here in the sense of MPEG-1-- a +consecutive number of macroblocks in scan order. More recent video +coding standards sometimes have a different understanding of the term +Slice.In H.263(1998), for example, a concept known as "rectangular slice" exists. The loss of one rectangular slice may lead to the @@ -805,38 +815,38 @@ Algorithms were reported that keep track of the regions affected by (see H.263(2000) Appendix I[17] and[15]).Although the timing of the FB is less critical when those algorithms are used than if they - are not, it has to be observed that those algorithms correct large - parts of the picture and, therefore, have to transmit much higher - data volume in case of delayed FBs. +are not, it has to be observed that those algorithms correct large +parts of the picture and, therefore, have to transmit much higher +data volume in case of delayed FBs. 6.3.3.Reference Picture Selection Indication(RPSI) - The RPSI FB message is identified by PT = PSFB and FMT = 3. +The RPSI FB message is identified by PT = PSFB and FMT = 3. - There MUST be exactly one RPSI contained in the FCI field. +There MUST be exactly one RPSI contained in the FCI field. 6.3.3.1.Semantics - Modern video coding standards such as MPEG - 4 visual version 2[16] or - H.263 version 2[17] allow using older reference pictures than the - most recent one for predictive coding. Typically, a first -in-first - - out queue of reference pictures is maintained.If an encoder has - learned about a loss of encoder - decoder synchronicity, a known -as- - correct reference picture can be used.As this reference picture is - temporally further away then usual, the resulting predictively coded - picture will use more bits. - - Both MPEG - 4 and H.263 define a binary format for the "payload" of an - - RPSI message that includes information such as the temporal ID of the - - damaged picture and the size of the damaged region.This bit string - is typically small (a couple of dozen bits), of variable length, and - self - contained, i.e., contains all information that is necessary to - perform reference picture selection. - - Both MPEG-4 and H.263 allow the use of RPSI with positive feedback - information as well.That is, pictures(or Slices) are reported that +Modern video coding standards such as MPEG - 4 visual version 2[16] or +H.263 version 2[17] allow using older reference pictures than the +most recent one for predictive coding. Typically, a first -in-first - +out queue of reference pictures is maintained.If an encoder has +learned about a loss of encoder - decoder synchronicity, a known -as- +correct reference picture can be used.As this reference picture is +temporally further away then usual, the resulting predictively coded +picture will use more bits. + +Both MPEG - 4 and H.263 define a binary format for the "payload" of an + + RPSI message that includes information such as the temporal ID of the + + damaged picture and the size of the damaged region.This bit string + is typically small (a couple of dozen bits), of variable length, and +self - contained, i.e., contains all information that is necessary to +perform reference picture selection. + +Both MPEG-4 and H.263 allow the use of RPSI with positive feedback +information as well.That is, pictures(or Slices) are reported that were decoded without error.Note that any form of positive feedback MUST NOT be used when in a multiparty session(reporting positive @@ -852,47 +862,47 @@ RFC 4585 RTP / AVPF July 2006 6.3.3.2.Format - The FCI for the RPSI message follows the format depicted in Figure 7: +The FCI for the RPSI message follows the format depicted in Figure 7: - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - + -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | PB | 0 | Payload Type | Native RPSI bit string | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | defined per codec... | Padding(0) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +0 1 2 3 +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++ -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| PB | 0 | Payload Type | Native RPSI bit string | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| defined per codec... | Padding(0) | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - Figure 7: Syntax of the Reference Picture Selection Indication(RPSI) +Figure 7: Syntax of the Reference Picture Selection Indication(RPSI) - PB: 8 bits - The number of unused bits required to pad the length of the RPSI - message to a multiple of 32 bits. +PB: 8 bits + The number of unused bits required to pad the length of the RPSI + message to a multiple of 32 bits. - 0: 1 bit - MUST be set to zero upon transmission and ignored upon reception. +0: 1 bit + MUST be set to zero upon transmission and ignored upon reception. - Payload Type: 7 bits - Indicates the RTP payload type in the context of which the native - RPSI bit string MUST be interpreted. +Payload Type: 7 bits + Indicates the RTP payload type in the context of which the native + RPSI bit string MUST be interpreted. - Native RPSI bit string: variable length - The RPSI information as natively defined by the video codec. +Native RPSI bit string: variable length + The RPSI information as natively defined by the video codec. - Padding: #PB bits - A number of bits set to zero to fill up the contents of the RPSI - message to the next 32 - bit boundary.The number of padding bits - MUST be indicated by the PB field. +Padding: #PB bits + A number of bits set to zero to fill up the contents of the RPSI + message to the next 32 - bit boundary.The number of padding bits + MUST be indicated by the PB field. 6.3.3.3.Timing Rules - RPSI is even more critical to delay than algorithms using SLI.This - is because the older the RPSI message is, the more bits the encoder - has to spend to re-establish encoder - decoder synchronicity.See[15] - for some information about the overhead of RPSI for certain bit - rate / frame rate / loss rate scenarios. +RPSI is even more critical to delay than algorithms using SLI.This +is because the older the RPSI message is, the more bits the encoder +has to spend to re-establish encoder - decoder synchronicity.See[15] +for some information about the overhead of RPSI for certain bit +rate / frame rate / loss rate scenarios. - Therefore, RPSI messages should typically be sent as soon as - possible, employing the algorithm of Section 3. +Therefore, RPSI messages should typically be sent as soon as +possible, employing the algorithm of Section 3. */ diff --git a/src/SIPSorcery/net/RTCP/RTCPHeader.cs b/src/SIPSorcery/net/RTCP/RTCPHeader.cs index 9ea82cbef9..f9c244991b 100644 --- a/src/SIPSorcery/net/RTCP/RTCPHeader.cs +++ b/src/SIPSorcery/net/RTCP/RTCPHeader.cs @@ -34,192 +34,201 @@ using System; using System.Buffers.Binary; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// The different types of RTCP packets as defined in RFC3550. +/// +public enum RTCPReportTypesEnum : byte +{ + SR = 200, // Send Report. + RR = 201, // Receiver Report. + SDES = 202, // Session Description. + BYE = 203, // Goodbye. + APP = 204, // Application-defined. + + // From RFC5760: https://tools.ietf.org/html/rfc5760 + // "RTP Control Protocol (RTCP) Extensions for + // Single-Source Multicast Sessions with Unicast Feedback" + + RTPFB = 205, // Generic RTP feedback + PSFB = 206, // Payload-specific feedback + XR = 207, // RTCP Extension +} + +public static class RTCPReportTypes +{ + public static RTCPReportTypesEnum GetRTCPReportTypeForId(ushort rtcpReportTypeId) + { + return (RTCPReportTypesEnum)Enum.Parse(typeof(RTCPReportTypesEnum), rtcpReportTypeId.ToString(), true); + } +} + +/// +/// RTCP Header as defined in RFC3550. +/// +public partial class RTCPHeader : IByteSerializable { + public const int HEADER_BYTES_LENGTH = 4; + public const int MAX_RECEPTIONREPORT_COUNT = 32; + public const int RTCP_VERSION = 2; + + public int Version { get; private set; } = RTCP_VERSION; // 2 bits. + public int PaddingFlag { get; private set; } // 1 bit. + public int ReceptionReportCount { get; private set; } // 5 bits. + public RTCPReportTypesEnum PacketType { get; private set; } // 8 bits. + public ushort Length { get; private set; } // 16 bits. + + /// + /// The Feedback Message Type is used for RFC4585 transport layer feedback reports. + /// When used this field gets set in place of the Reception Report Counter field. + /// + public RTCPFeedbackTypesEnum FeedbackMessageType { get; private set; } = RTCPFeedbackTypesEnum.unassigned; + /// - /// The different types of RTCP packets as defined in RFC3550. + /// The Payload Feedback Message Type is used for RFC4585 payload layer feedback reports. + /// When used this field gets set in place of the Reception Report Counter field. /// - public enum RTCPReportTypesEnum : byte + public PSFBFeedbackTypesEnum PayloadFeedbackMessageType { get; private set; } = PSFBFeedbackTypesEnum.unassigned; + + public RTCPHeader(RTCPFeedbackTypesEnum feedbackType) { - SR = 200, // Send Report. - RR = 201, // Receiver Report. - SDES = 202, // Session Description. - BYE = 203, // Goodbye. - APP = 204, // Application-defined. - - // From RFC5760: https://tools.ietf.org/html/rfc5760 - // "RTP Control Protocol (RTCP) Extensions for - // Single-Source Multicast Sessions with Unicast Feedback" - - RTPFB = 205, // Generic RTP feedback - PSFB = 206, // Payload-specific feedback - XR = 207, // RTCP Extension + PacketType = RTCPReportTypesEnum.RTPFB; + FeedbackMessageType = feedbackType; } - public class RTCPReportTypes + public RTCPHeader(PSFBFeedbackTypesEnum feedbackType) { - public static RTCPReportTypesEnum GetRTCPReportTypeForId(ushort rtcpReportTypeId) - { - return (RTCPReportTypesEnum)Enum.Parse(typeof(RTCPReportTypesEnum), rtcpReportTypeId.ToString(), true); - } + PacketType = RTCPReportTypesEnum.PSFB; + PayloadFeedbackMessageType = feedbackType; + } + + public RTCPHeader(RTCPReportTypesEnum packetType, int reportCount) + { + PacketType = packetType; + ReceptionReportCount = reportCount; } /// - /// RTCP Header as defined in RFC3550. + /// Identifies whether an RTCP header is for a standard RTCP packet or for an + /// RTCP feedback report. /// - public class RTCPHeader + /// True if the header is for an RTCP feedback report or false if not. + public bool IsFeedbackReport() { - public const int HEADER_BYTES_LENGTH = 4; - public const int MAX_RECEPTIONREPORT_COUNT = 32; - public const int RTCP_VERSION = 2; - - public int Version { get; private set; } = RTCP_VERSION; // 2 bits. - public int PaddingFlag { get; private set; } = 0; // 1 bit. - public int ReceptionReportCount { get; private set; } = 0; // 5 bits. - public RTCPReportTypesEnum PacketType { get; private set; } // 8 bits. - public UInt16 Length { get; private set; } // 16 bits. - - /// - /// The Feedback Message Type is used for RFC4585 transport layer feedback reports. - /// When used this field gets set in place of the Reception Report Counter field. - /// - public RTCPFeedbackTypesEnum FeedbackMessageType { get; private set; } = RTCPFeedbackTypesEnum.unassigned; - - /// - /// The Payload Feedback Message Type is used for RFC4585 payload layer feedback reports. - /// When used this field gets set in place of the Reception Report Counter field. - /// - public PSFBFeedbackTypesEnum PayloadFeedbackMessageType { get; private set; } = PSFBFeedbackTypesEnum.unassigned; - - public RTCPHeader(RTCPFeedbackTypesEnum feedbackType) + if (PacketType is RTCPReportTypesEnum.RTPFB or + RTCPReportTypesEnum.PSFB) { - PacketType = RTCPReportTypesEnum.RTPFB; - FeedbackMessageType = feedbackType; + return true; } - - public RTCPHeader(PSFBFeedbackTypesEnum feedbackType) + else { - PacketType = RTCPReportTypesEnum.PSFB; - PayloadFeedbackMessageType = feedbackType; + return false; } + } - public RTCPHeader(RTCPReportTypesEnum packetType, int reportCount) + public static RTCPFeedbackTypesEnum ParseFeedbackType(ReadOnlySpan packet) + { + if (packet.Length < HEADER_BYTES_LENGTH) { - PacketType = packetType; - ReceptionReportCount = reportCount; + throw new SipSorceryException("The packet did not contain the minimum number of bytes for an RTCP header packet."); } - /// - /// Identifies whether an RTCP header is for a standard RTCP packet or for an - /// RTCP feedback report. - /// - /// True if the header is for an RTCP feedback report or false if not. - public bool IsFeedbackReport() - { - if (PacketType == RTCPReportTypesEnum.RTPFB || - PacketType == RTCPReportTypesEnum.PSFB) - { - return true; - } - else - { - return false; - } - } + var firstWord = BinaryPrimitives.ReadUInt16BigEndian(packet); - public static RTCPFeedbackTypesEnum ParseFeedbackType(byte[] packet) - { - if (packet.Length < HEADER_BYTES_LENGTH) - { - throw new ApplicationException("The packet did not contain the minimum number of bytes for an RTCP header packet."); - } - UInt16 firstWord = BinaryPrimitives.ReadUInt16BigEndian(packet); - return (RTCPFeedbackTypesEnum)((firstWord >> 8) & 0x1f); - } + return (RTCPFeedbackTypesEnum)((firstWord >> 8) & 0x1f); + } - /// - /// Extract and load the RTCP header from an RTCP packet. - /// - /// - public RTCPHeader(byte[] packet) + public RTCPHeader(ReadOnlySpan packet) + { + if (packet.Length < HEADER_BYTES_LENGTH) { - if (packet.Length < HEADER_BYTES_LENGTH) - { - throw new ApplicationException("The packet did not contain the minimum number of bytes for an RTCP header packet."); - } + throw new SipSorceryException("The packet did not contain the minimum number of bytes for an RTCP header packet."); + } - UInt16 firstWord = BinaryPrimitives.ReadUInt16BigEndian(packet); - Length = BinaryPrimitives.ReadUInt16BigEndian(packet.AsSpan(2)); + var firstWord = BinaryPrimitives.ReadUInt16BigEndian(packet); + Length = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(2)); - Version = Convert.ToInt32(firstWord >> 14); - PaddingFlag = Convert.ToInt32((firstWord >> 13) & 0x1); - PacketType = (RTCPReportTypesEnum)(firstWord & 0x00ff); + Version = Convert.ToInt32(firstWord >> 14); + PaddingFlag = Convert.ToInt32((firstWord >> 13) & 0x1); + PacketType = (RTCPReportTypesEnum)(firstWord & 0x00ff); - if (IsFeedbackReport()) + if (IsFeedbackReport()) + { + if (PacketType == RTCPReportTypesEnum.RTPFB) { - if (PacketType == RTCPReportTypesEnum.RTPFB) - { - FeedbackMessageType = (RTCPFeedbackTypesEnum)((firstWord >> 8) & 0x1f); - } - else - { - PayloadFeedbackMessageType = (PSFBFeedbackTypesEnum)((firstWord >> 8) & 0x1f); - } + FeedbackMessageType = (RTCPFeedbackTypesEnum)((firstWord >> 8) & 0x1f); } else { - ReceptionReportCount = Convert.ToInt32((firstWord >> 8) & 0x1f); + PayloadFeedbackMessageType = (PSFBFeedbackTypesEnum)((firstWord >> 8) & 0x1f); } } - - public byte[] GetHeader(int receptionReportCount, UInt16 length) + else { - if (receptionReportCount > MAX_RECEPTIONREPORT_COUNT) - { - throw new ApplicationException($"The Reception Report Count value cannot be larger than {MAX_RECEPTIONREPORT_COUNT}."); - } + ReceptionReportCount = Convert.ToInt32((firstWord >> 8) & 0x1f); + } + } - ReceptionReportCount = receptionReportCount; - Length = length; + /// + /// The length of this RTCP packet in 32-bit words minus one, + /// including the header and any padding. + /// + public void SetLength(ushort length) + { + Length = length; + } - return GetBytes(); + public void SetReceptionReportCount(int receptionReportCount) + { + if (receptionReportCount > MAX_RECEPTIONREPORT_COUNT) + { + throw new SipSorceryException("The Reception Report Count value cannot be larger than " + MAX_RECEPTIONREPORT_COUNT + "."); } - /// - /// The length of this RTCP packet in 32-bit words minus one, - /// including the header and any padding. - /// - public void SetLength(ushort length) + ReceptionReportCount = receptionReportCount; + } + + /// + public int GetByteCount() => 4; + + /// + public int WriteBytes(Span buffer) + { + var size = GetByteCount(); + + if (buffer.Length < size) { - Length = length; + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); } - public byte[] GetBytes() - { - byte[] header = new byte[4]; + WriteBytesCore(buffer.Slice(0, size)); + + return size; + } - UInt32 firstWord = ((uint)Version << 30) + ((uint)PaddingFlag << 29) + ((uint)PacketType << 16) + Length; + private void WriteBytesCore(Span buffer) + { + var firstWord = ((uint)Version << 30) + ((uint)PaddingFlag << 29) + ((uint)PacketType << 16) + Length; - if (IsFeedbackReport()) + if (IsFeedbackReport()) + { + if (PacketType == RTCPReportTypesEnum.RTPFB) { - if (PacketType == RTCPReportTypesEnum.RTPFB) - { - firstWord += (uint)FeedbackMessageType << 24; - } - else - { - firstWord += (uint)PayloadFeedbackMessageType << 24; - } + firstWord += (uint)FeedbackMessageType << 24; } else { - firstWord += (uint)ReceptionReportCount << 24; + firstWord += (uint)PayloadFeedbackMessageType << 24; } - - BinaryPrimitives.WriteUInt32BigEndian(header, firstWord); - - return header; } + else + { + firstWord += (uint)ReceptionReportCount << 24; + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, firstWord); } } diff --git a/src/SIPSorcery/net/RTCP/RTCPReceiverReport.cs b/src/SIPSorcery/net/RTCP/RTCPReceiverReport.cs index 5986860fad..b87031ba29 100644 --- a/src/SIPSorcery/net/RTCP/RTCPReceiverReport.cs +++ b/src/SIPSorcery/net/RTCP/RTCPReceiverReport.cs @@ -47,83 +47,92 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public partial class RTCPReceiverReport : IByteSerializable { - public class RTCPReceiverReport + public const int MIN_PACKET_SIZE = RTCPHeader.HEADER_BYTES_LENGTH + 4; + + public RTCPHeader Header; + public uint SSRC; + public List? ReceptionReports; + + /// + /// Creates a new RTCP Reception Report payload. + /// + /// The synchronisation source of the RTP packet being sent. Can be zero + /// if there are none being sent. + /// A list of the reception reports to include. Can be empty. + public RTCPReceiverReport(uint ssrc, List? receptionReports) + { + Header = new RTCPHeader(RTCPReportTypesEnum.RR, receptionReports is { } ? receptionReports.Count : 0); + SSRC = ssrc; + ReceptionReports = receptionReports; + } + + /// + /// Create a new RTCP Receiver Report from a serialised byte array. + /// + /// The byte array holding the serialised receiver report. + public RTCPReceiverReport(ReadOnlySpan packet) { - public const int MIN_PACKET_SIZE = RTCPHeader.HEADER_BYTES_LENGTH + 4; - - public RTCPHeader Header; - public uint SSRC; - public List ReceptionReports; - - /// - /// Creates a new RTCP Reception Report payload. - /// - /// The synchronisation source of the RTP packet being sent. Can be zero - /// if there are none being sent. - /// A list of the reception reports to include. Can be empty. - public RTCPReceiverReport(uint ssrc, List receptionReports) + if (packet.Length < MIN_PACKET_SIZE) { - Header = new RTCPHeader(RTCPReportTypesEnum.RR, receptionReports != null ? receptionReports.Count : 0); - SSRC = ssrc; - ReceptionReports = receptionReports; + throw new ArgumentException("The packet did not contain the minimum number of bytes for an RTCPReceiverReport packet.", nameof(packet)); } - /// - /// Create a new RTCP Receiver Report from a serialised byte array. - /// - /// The byte array holding the serialised receiver report. - public RTCPReceiverReport(byte[] packet) + Header = new RTCPHeader(packet); + ReceptionReports = new List(); + + SSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(4, 4)); + + var rrIndex = 8; + for (var i = 0; i < Header.ReceptionReportCount; i++) { - if (packet.Length < MIN_PACKET_SIZE) + var pkt = packet.Slice(rrIndex + i * ReceptionReportSample.PAYLOAD_SIZE); + if (pkt.Length >= ReceptionReportSample.PAYLOAD_SIZE) { - throw new ApplicationException("The packet did not contain the minimum number of bytes for an RTCPReceiverReport packet."); + var rr = new ReceptionReportSample(pkt); + ReceptionReports.Add(rr); } + } + } - Header = new RTCPHeader(packet); - ReceptionReports = new List(); + /// + public int GetByteCount() => RTCPHeader.HEADER_BYTES_LENGTH + 4 + (ReceptionReports?.Count).GetValueOrDefault() * ReceptionReportSample.PAYLOAD_SIZE; - SSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(4)); + /// + public int WriteBytes(Span buffer) + { + var size = GetByteCount(); - int rrIndex = 8; - for (int i = 0; i < Header.ReceptionReportCount; i++) - { - var pkt = packet.Skip(rrIndex + i * ReceptionReportSample.PAYLOAD_SIZE).ToArray(); - if (pkt.Length >= ReceptionReportSample.PAYLOAD_SIZE) - { - var rr = new ReceptionReportSample(pkt); - ReceptionReports.Add(rr); - } - } + if (buffer.Length < size) + { + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); } - /// - /// Gets the serialised bytes for this Receiver Report. - /// - /// A byte array. - public byte[] GetBytes() - { - int rrCount = (ReceptionReports != null) ? ReceptionReports.Count : 0; - byte[] buffer = new byte[RTCPHeader.HEADER_BYTES_LENGTH + 4 + rrCount * ReceptionReportSample.PAYLOAD_SIZE]; - Header.SetLength((ushort)(buffer.Length / 4 - 1)); + WriteBytesCore(buffer.Slice(0, size)); - Buffer.BlockCopy(Header.GetBytes(), 0, buffer, 0, RTCPHeader.HEADER_BYTES_LENGTH); - int payloadIndex = RTCPHeader.HEADER_BYTES_LENGTH; + return size; + } + + private void WriteBytesCore(Span buffer) + { + Header.SetLength((ushort)(buffer.Length / 4 - 1)); + _ = Header.WriteBytes(buffer); - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(payloadIndex), SSRC); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH), SSRC); - int bufferIndex = payloadIndex + 4; - for (int i = 0; i < rrCount; i++) + if (ReceptionReports is { Count: > 0 } receptionReports) + { + buffer = buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 4); + for (var i = 0; i < receptionReports.Count; i++) { - var receptionReportBytes = ReceptionReports[i].GetBytes(); - Buffer.BlockCopy(receptionReportBytes, 0, buffer, bufferIndex, ReceptionReportSample.PAYLOAD_SIZE); - bufferIndex += ReceptionReportSample.PAYLOAD_SIZE; + _ = receptionReports[i].WriteBytes(buffer); + buffer = buffer.Slice(ReceptionReportSample.PAYLOAD_SIZE); } - - return buffer; } } } diff --git a/src/SIPSorcery/net/RTCP/RTCPSdesReport.cs b/src/SIPSorcery/net/RTCP/RTCPSdesReport.cs index 5008ed79c7..c3a6653662 100644 --- a/src/SIPSorcery/net/RTCP/RTCPSdesReport.cs +++ b/src/SIPSorcery/net/RTCP/RTCPSdesReport.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: RTCPSDesReport.cs // // Description: RTCP Source Description (SDES) report as defined in RFC3550. @@ -52,131 +52,140 @@ using System; using System.Buffers.Binary; +using System.Diagnostics; using System.Text; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; -namespace SIPSorcery.Net +/// +/// RTCP Source Description (SDES) report as defined in RFC3550. +/// Only the mandatory CNAME item is supported. +/// +public partial class RTCPSDesReport : IByteSerializable { + public const int PACKET_SIZE_WITHOUT_CNAME = 6; // 4 byte SSRC, 1 byte CNAME ID, 1 byte CNAME length. + public const int MAX_CNAME_BYTES = 255; + public const byte CNAME_ID = 0x01; + public const int MIN_PACKET_SIZE = RTCPHeader.HEADER_BYTES_LENGTH + PACKET_SIZE_WITHOUT_CNAME; + + public RTCPHeader Header; + public uint SSRC { get; private set; } + public string? CNAME { get; private set; } + /// - /// RTCP Source Description (SDES) report as defined in RFC3550. - /// Only the mandatory CNAME item is supported. + /// Creates a new RTCP SDES payload that can be included in an RTCP packet. /// - public class RTCPSDesReport + /// The synchronisation source of the SDES. + /// Canonical End-Point Identifier SDES item. This should be a + /// unique string common to all RTP streams in use by the application. Maximum + /// length is 255 bytes (note bytes not characters). + public RTCPSDesReport(uint ssrc, string cname) { - public const int PACKET_SIZE_WITHOUT_CNAME = 6; // 4 byte SSRC, 1 byte CNAME ID, 1 byte CNAME length. - public const int MAX_CNAME_BYTES = 255; - public const byte CNAME_ID = 0x01; - public const int MIN_PACKET_SIZE = RTCPHeader.HEADER_BYTES_LENGTH + PACKET_SIZE_WITHOUT_CNAME; - - public RTCPHeader Header; - public uint SSRC { get; private set; } - public string CNAME { get; private set; } - - /// - /// Creates a new RTCP SDES payload that can be included in an RTCP packet. - /// - /// The synchronisation source of the SDES. - /// Canonical End-Point Identifier SDES item. This should be a - /// unique string common to all RTP streams in use by the application. Maximum - /// length is 255 bytes (note bytes not characters). - public RTCPSDesReport(uint ssrc, string cname) + ArgumentNullException.ThrowIfNullOrEmpty(cname); + + Header = new RTCPHeader(RTCPReportTypesEnum.SDES, 1); + SSRC = ssrc; + CNAME = (cname.Length > MAX_CNAME_BYTES) ? cname.Substring(0, MAX_CNAME_BYTES) : cname; + + // Need to take account of multi-byte characters. + while (Encoding.UTF8.GetBytes(CNAME).Length > MAX_CNAME_BYTES) { - if (String.IsNullOrEmpty(cname)) - { - throw new ArgumentNullException("cname"); - } + CNAME = CNAME.Substring(0, CNAME.Length - 1); + } + } - Header = new RTCPHeader(RTCPReportTypesEnum.SDES, 1); - SSRC = ssrc; - CNAME = (cname.Length > MAX_CNAME_BYTES) ? cname.Substring(0, MAX_CNAME_BYTES) : cname; + /// + /// Create a new RTCP SDES item from a serialised byte array. + /// + /// The byte array holding the SDES report. + public RTCPSDesReport(ReadOnlySpan packet) + { + // if (packet.Length < MIN_PACKET_SIZE) + // { + // throw new SipSorceryException("The packet did not contain the minimum number of bytes for an RTCP SDES packet."); + // } + // else if (packet[8] != CNAME_ID) + // { + // throw new SipSorceryException("The RTCP report packet did not have the required CNAME type field set correctly."); + // } + + Header = new RTCPHeader(packet); + if (Header.Length <= 0) + { + return; + } - // Need to take account of multi-byte characters. - while (Encoding.UTF8.GetBytes(CNAME).Length > MAX_CNAME_BYTES) - { - CNAME = CNAME.Substring(0, CNAME.Length - 1); - } + if (packet.Length >= RTCPHeader.HEADER_BYTES_LENGTH + 4) + { + SSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(RTCPHeader.HEADER_BYTES_LENGTH, 4)); } - /// - /// Create a new RTCP SDES item from a serialised byte array. - /// - /// The byte array holding the SDES report. - public RTCPSDesReport(byte[] packet) + if (packet.Length >= MIN_PACKET_SIZE) { - // if (packet.Length < MIN_PACKET_SIZE) - // { - // throw new ApplicationException("The packet did not contain the minimum number of bytes for an RTCP SDES packet."); - // } - // else if (packet[8] != CNAME_ID) - // { - // throw new ApplicationException("The RTCP report packet did not have the required CNAME type field set correctly."); - // } - - Header = new RTCPHeader(packet); - if (Header.Length <= 0) + int cnameLength = packet[9]; + if (cnameLength <= 0 || packet.Length < RTCPHeader.HEADER_BYTES_LENGTH + 4 + cnameLength) { + CNAME = string.Empty; return; } + CNAME = Encoding.UTF8.GetString(packet.Slice(10, cnameLength)); + } + } - if (packet.Length >= RTCPHeader.HEADER_BYTES_LENGTH+4) - { - SSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(RTCPHeader.HEADER_BYTES_LENGTH)); - } + /// + public int GetByteCount() + { + Debug.Assert(CNAME is { }); + return RTCPHeader.HEADER_BYTES_LENGTH + GetPaddedLength(Encoding.UTF8.GetByteCount(CNAME)); + } - if (packet.Length >= MIN_PACKET_SIZE) - { - int cnameLength = packet[9]; - if (cnameLength <= 0 || packet.Length < RTCPHeader.HEADER_BYTES_LENGTH + 4 + cnameLength) - { - CNAME = string.Empty; - return; - } - CNAME = Encoding.UTF8.GetString(packet, 10, cnameLength); - } - } + /// + public int WriteBytes(Span buffer) + { + var size = GetByteCount(); - /// - /// Gets the raw bytes for the SDES item. This packet is ready to be included - /// directly in an RTCP packet. - /// - /// A byte array containing the serialised SDES item. - public byte[] GetBytes() + if (buffer.Length < size) { - byte[] cnameBytes = CNAME == null ? Array.Empty() : Encoding.UTF8.GetBytes(CNAME); - byte[] buffer = new byte[RTCPHeader.HEADER_BYTES_LENGTH + GetPaddedLength(cnameBytes.Length)]; // Array automatically initialised with 0x00 values. - Header.SetLength((ushort)(buffer.Length / 4 - 1)); + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); + } - Buffer.BlockCopy(Header.GetBytes(), 0, buffer, 0, RTCPHeader.HEADER_BYTES_LENGTH); - int payloadIndex = RTCPHeader.HEADER_BYTES_LENGTH; + WriteBytesCore(buffer.Slice(0, size)); - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(payloadIndex), SSRC); + return size; + } - buffer[payloadIndex + 4] = CNAME_ID; - buffer[payloadIndex + 5] = (byte)cnameBytes.Length; - Buffer.BlockCopy(cnameBytes, 0, buffer, payloadIndex + 6, cnameBytes.Length); + private void WriteBytesCore(Span buffer) + { + Header.SetLength((ushort)(buffer.Length / 4 - 1)); + _ = Header.WriteBytes(buffer); - return buffer; - } + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH), SSRC); - /// - /// The packet has to finish on a 4 byte boundary. This method calculates the minimum - /// packet length for the SDES fields to fit within a 4 byte boundary. - /// - /// The length of the cname string. - /// The minimum length for the full packet to be able to fit within a 4 byte - /// boundary. - private int GetPaddedLength(int cnameLength) - { - // Plus one is for the 0x00 items termination byte. - int nonPaddedSize = cnameLength + PACKET_SIZE_WITHOUT_CNAME + 1; + buffer[RTCPHeader.HEADER_BYTES_LENGTH + 4] = CNAME_ID; + buffer[RTCPHeader.HEADER_BYTES_LENGTH + 5] = + (byte)Encoding.UTF8.GetBytes(CNAME.AsSpan(), buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 6)); + } - if (nonPaddedSize % 4 == 0) - { - return nonPaddedSize; - } - else - { - return nonPaddedSize + 4 - (nonPaddedSize % 4); - } + /// + /// The packet has to finish on a 4 byte boundary. This method calculates the minimum + /// packet length for the SDES fields to fit within a 4 byte boundary. + /// + /// The length of the cname string. + /// The minimum length for the full packet to be able to fit within a 4 byte + /// boundary. + private int GetPaddedLength(int cnameLength) + { + // Plus one is for the 0x00 items termination byte. + int nonPaddedSize = cnameLength + PACKET_SIZE_WITHOUT_CNAME + 1; + + if (nonPaddedSize % 4 == 0) + { + return nonPaddedSize; + } + else + { + return nonPaddedSize + 4 - (nonPaddedSize % 4); } } } diff --git a/src/SIPSorcery/net/RTCP/RTCPSenderReport.cs b/src/SIPSorcery/net/RTCP/RTCPSenderReport.cs index a6ab62f419..4467f34f5e 100644 --- a/src/SIPSorcery/net/RTCP/RTCPSenderReport.cs +++ b/src/SIPSorcery/net/RTCP/RTCPSenderReport.cs @@ -47,103 +47,115 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// An RTCP sender report is for use by active RTP senders. +/// +/// +/// From https://tools.ietf.org/html/rfc3550#section-6.4: +/// "The only difference between the +/// sender report(SR) and receiver report(RR) forms, besides the packet +/// type code, is that the sender report includes a 20-byte sender +/// information section for use by active senders.The SR is issued if a +/// site has sent any data packets during the interval since issuing the +/// last report or the previous one, otherwise the RR is issued." +/// +public partial class RTCPSenderReport : IByteSerializable { + public const int SENDER_PAYLOAD_SIZE = 20; + public const int MIN_PACKET_SIZE = RTCPHeader.HEADER_BYTES_LENGTH + 4 + SENDER_PAYLOAD_SIZE; + + public RTCPHeader Header; + public uint SSRC; + public ulong NtpTimestamp; + public uint RtpTimestamp; + public uint PacketCount; + public uint OctetCount; + public List? ReceptionReports; + + public RTCPSenderReport(uint ssrc, ulong ntpTimestamp, uint rtpTimestamp, uint packetCount, uint octetCount, List? receptionReports) + { + Header = new RTCPHeader(RTCPReportTypesEnum.SR, (receptionReports is { }) ? receptionReports.Count : 0); + SSRC = ssrc; + NtpTimestamp = ntpTimestamp; + RtpTimestamp = rtpTimestamp; + PacketCount = packetCount; + OctetCount = octetCount; + ReceptionReports = receptionReports; + } + /// - /// An RTCP sender report is for use by active RTP senders. + /// Create a new RTCP Sender Report from a serialised byte array. /// - /// - /// From https://tools.ietf.org/html/rfc3550#section-6.4: - /// "The only difference between the - /// sender report(SR) and receiver report(RR) forms, besides the packet - /// type code, is that the sender report includes a 20-byte sender - /// information section for use by active senders.The SR is issued if a - /// site has sent any data packets during the interval since issuing the - /// last report or the previous one, otherwise the RR is issued." - /// - public class RTCPSenderReport + /// The byte array holding the serialised sender report. + public RTCPSenderReport(ReadOnlySpan packet) { - public const int SENDER_PAYLOAD_SIZE = 20; - public const int MIN_PACKET_SIZE = RTCPHeader.HEADER_BYTES_LENGTH + 4 + SENDER_PAYLOAD_SIZE; - - public RTCPHeader Header; - public uint SSRC; - public ulong NtpTimestamp; - public uint RtpTimestamp; - public uint PacketCount; - public uint OctetCount; - public List ReceptionReports; - - public RTCPSenderReport(uint ssrc, ulong ntpTimestamp, uint rtpTimestamp, uint packetCount, uint octetCount, List receptionReports) + if (packet.Length < MIN_PACKET_SIZE) { - Header = new RTCPHeader(RTCPReportTypesEnum.SR, (receptionReports != null) ? receptionReports.Count : 0); - SSRC = ssrc; - NtpTimestamp = ntpTimestamp; - RtpTimestamp = rtpTimestamp; - PacketCount = packetCount; - OctetCount = octetCount; - ReceptionReports = receptionReports; + throw new SipSorceryException("The packet did not contain the minimum number of bytes for an RTCPSenderReport packet."); } - /// - /// Create a new RTCP Sender Report from a serialised byte array. - /// - /// The byte array holding the serialised sender report. - public RTCPSenderReport(byte[] packet) + Header = new RTCPHeader(packet); + ReceptionReports = new List(); + + SSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(RTCPHeader.HEADER_BYTES_LENGTH, 4)); + NtpTimestamp = BinaryPrimitives.ReadUInt64BigEndian(packet.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 4, 8)); + RtpTimestamp = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 12, 4)); + PacketCount = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 16, 4)); + OctetCount = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 20, 4)); + + var rrIndex = 28; + for (var i = 0; i < Header.ReceptionReportCount; i++) { - if (packet.Length < MIN_PACKET_SIZE) + var pkt = packet.Slice(rrIndex + i * ReceptionReportSample.PAYLOAD_SIZE); + if (pkt.Length >= ReceptionReportSample.PAYLOAD_SIZE) { - throw new ApplicationException("The packet did not contain the minimum number of bytes for an RTCPSenderReport packet."); + var rr = new ReceptionReportSample(pkt); + ReceptionReports.Add(rr); } + } + } - Header = new RTCPHeader(packet); - ReceptionReports = new List(); + /// + public int GetByteCount() => RTCPHeader.HEADER_BYTES_LENGTH + 4 + SENDER_PAYLOAD_SIZE + (ReceptionReports?.Count).GetValueOrDefault() * ReceptionReportSample.PAYLOAD_SIZE; - SSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(4)); - NtpTimestamp = BinaryPrimitives.ReadUInt64BigEndian(packet.AsSpan(8)); - RtpTimestamp = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(16)); - PacketCount = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(20)); - OctetCount = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(24)); + /// + public int WriteBytes(Span buffer) + { + var size = GetByteCount(); - int rrIndex = 28; - for (int i = 0; i < Header.ReceptionReportCount; i++) - { - var pkt = packet.Skip(rrIndex + i * ReceptionReportSample.PAYLOAD_SIZE).ToArray(); - if (pkt.Length >= ReceptionReportSample.PAYLOAD_SIZE) - { - var rr = new ReceptionReportSample(pkt); - ReceptionReports.Add(rr); - } - - } + if (buffer.Length < size) + { + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); } - public byte[] GetBytes() - { - int rrCount = (ReceptionReports != null) ? ReceptionReports.Count : 0; - byte[] buffer = new byte[RTCPHeader.HEADER_BYTES_LENGTH + 4 + SENDER_PAYLOAD_SIZE + rrCount * ReceptionReportSample.PAYLOAD_SIZE]; - Header.SetLength((ushort)(buffer.Length / 4 - 1)); + WriteBytesCore(buffer.Slice(0, size)); - Buffer.BlockCopy(Header.GetBytes(), 0, buffer, 0, RTCPHeader.HEADER_BYTES_LENGTH); - int payloadIndex = RTCPHeader.HEADER_BYTES_LENGTH; + return size; + } + + private void WriteBytesCore(Span buffer) + { + Header.SetLength((ushort)(buffer.Length / 4 - 1)); + _ = Header.WriteBytes(buffer); - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(payloadIndex), SSRC); - BinaryPrimitives.WriteUInt64BigEndian(buffer.AsSpan(payloadIndex + 4), NtpTimestamp); - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(payloadIndex + 12), RtpTimestamp); - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(payloadIndex + 16), PacketCount); - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(payloadIndex + 20), OctetCount); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH), SSRC); + BinaryPrimitives.WriteUInt64BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 4), NtpTimestamp); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 12), RtpTimestamp); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 16), PacketCount); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 20), OctetCount); - int bufferIndex = payloadIndex + 24; - for (int i = 0; i < rrCount; i++) + if (ReceptionReports is { Count: > 0 } receptionReports) + { + buffer = buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH + 24); + for (var i = 0; i < receptionReports.Count; i++) { - var receptionReportBytes = ReceptionReports[i].GetBytes(); - Buffer.BlockCopy(receptionReportBytes, 0, buffer, bufferIndex, ReceptionReportSample.PAYLOAD_SIZE); - bufferIndex += ReceptionReportSample.PAYLOAD_SIZE; + _ = receptionReports[i].WriteBytes(buffer); + buffer = buffer.Slice(ReceptionReportSample.PAYLOAD_SIZE); } - - return buffer; } } } diff --git a/src/SIPSorcery/net/RTCP/RTCPSession.cs b/src/SIPSorcery/net/RTCP/RTCPSession.cs index bcb11be063..714c11e7ef 100644 --- a/src/SIPSorcery/net/RTCP/RTCPSession.cs +++ b/src/SIPSorcery/net/RTCP/RTCPSession.cs @@ -28,435 +28,442 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net; using System.Threading; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// Represents an RTCP session intended to be used in conjunction with an +/// RTP session. This class needs to get notified of all RTP sends and receives +/// and will take care of RTCP reporting. +/// +/// +/// RTCP Design Decisions: +/// - Minimum Report Period set to 5s as per RFC3550: 6.2 RTCP Transmission Interval (page 24). +/// - Delay for initial report transmission set to 2.5s (0.5 * minimum report period) as per RFC3550: 6.2 RTCP Transmission Interval (page 26). +/// - Randomisation factor to apply to report intervals to attempt to ensure RTCP reports amongst participants don't become synchronised +/// [0.5 * interval, 1.5 * interval] as per RFC3550: 6.2 RTCP Transmission Interval (page 26). +/// - Timeout period during which if no RTP or RTCP packets received a participant is assumed to have dropped +/// 5 x minimum report period as per RFC3550: 6.2.1 (page 27) and 6.3.5 (page 31). +/// - All RTCP composite reports must satisfy (this includes when a BYE is sent): +/// - First RTCP packet must be a SR or RR, +/// - Must contain an SDES packet. +/// +public class RTCPSession { + public const string NO_ACTIVITY_TIMEOUT_REASON = "No activity timeout."; + private const int RTCP_MINIMUM_REPORT_PERIOD_MILLISECONDS = 5000; + private const float RTCP_INTERVAL_LOW_RANDOMISATION_FACTOR = 0.5F; + private const float RTCP_INTERVAL_HIGH_RANDOMISATION_FACTOR = 1.5F; + private const int NO_ACTIVITY_TIMEOUT_FACTOR = 6; + + private static readonly ILogger logger = LogFactory.CreateLogger(); + + private static DateTime UtcEpoch2036 = new DateTime(2036, 2, 7, 6, 28, 16, DateTimeKind.Utc); + private static DateTime UtcEpoch1900 = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// The media type this report session is measuring. + /// + public SDPMediaTypesEnum MediaType { get; private set; } + + /// + /// The SSRC number of the RTP packets we are sending. + /// + public uint Ssrc { get; set; } + + /// + /// Timestamp that the RTCP session was created at. + /// + public DateTime CreatedAt { get; private set; } + + /// + /// Timestamp that the RTCP session sender report scheduler was started at. + /// + public DateTime StartedAt { get; private set; } = DateTime.MinValue; + + /// + /// Timestamp that the last RTP or RTCP packet for was received at. + /// + public DateTime LastActivityAt { get; private set; } = DateTime.MinValue; + + /// + /// Sets the timeout threshold for this session. If no RTP or RTCP packets are received + /// in this time period, a hangup is invoked. It's checked every 5 seconds so the timeout + /// is best set to an integer multiple of that. + /// + public int NoActivityTimeoutMilliseconds { get; set; } = NO_ACTIVITY_TIMEOUT_FACTOR * RTCP_MINIMUM_REPORT_PERIOD_MILLISECONDS; + + + /// + /// Indicates whether the session is currently in a timed out state. This + /// occurs if no RTP or RTCP packets have been received during an expected + /// interval. + /// + public bool IsTimedOut { get; private set; } + + /// + /// Number of RTP packets sent to the remote party. + /// + public uint PacketsSentCount { get; private set; } + + /// + /// Number of RTP bytes sent to the remote party. + /// + public uint OctetsSentCount { get; private set; } + + /// + /// The last RTP sequence number sent by us. + /// + public ushort LastSeqNum { get; private set; } + + /// + /// The last RTP timestamp sent by us. + /// + public uint LastRtpTimestampSent { get; private set; } + + /// + /// The last NTP timestamp corresponding to the last RTP timestamp sent by us. + /// + public ulong LastNtpTimestampSent { get; private set; } + + /// + /// Number of RTP packets received from the remote party. + /// + public uint PacketsReceivedCount { get; private set; } + /// - /// Represents an RTCP session intended to be used in conjunction with an - /// RTP session. This class needs to get notified of all RTP sends and receives - /// and will take care of RTCP reporting. + /// Number of RTP bytes received from the remote party. /// - /// - /// RTCP Design Decisions: - /// - Minimum Report Period set to 5s as per RFC3550: 6.2 RTCP Transmission Interval (page 24). - /// - Delay for initial report transmission set to 2.5s (0.5 * minimum report period) as per RFC3550: 6.2 RTCP Transmission Interval (page 26). - /// - Randomisation factor to apply to report intervals to attempt to ensure RTCP reports amongst participants don't become synchronised - /// [0.5 * interval, 1.5 * interval] as per RFC3550: 6.2 RTCP Transmission Interval (page 26). - /// - Timeout period during which if no RTP or RTCP packets received a participant is assumed to have dropped - /// 5 x minimum report period as per RFC3550: 6.2.1 (page 27) and 6.3.5 (page 31). - /// - All RTCP composite reports must satisfy (this includes when a BYE is sent): - /// - First RTCP packet must be a SR or RR, - /// - Must contain an SDES packet. - /// - public class RTCPSession + public uint OctetsReceivedCount { get; private set; } + + /// + /// Unique common name field for use in SDES packets. + /// + public string Cname { get; private set; } + + /// + /// The reception report to keep track of the RTP statistics + /// from packets received from the remote call party. + /// + public ReceptionReport? ReceptionReport { get; private set; } + + /// + /// Indicates whether the RTCP session has been closed. + /// An RTCP BYE request will typically trigger an close. + /// + public bool IsClosed { get; private set; } + + /// + /// Indicates the sample rate for RTP media data. + /// + public int PayloadSampleRateHz { get; set; } + + /// + /// Time to schedule the delivery of RTCP reports. + /// + private Timer? m_rtcpReportTimer; + + private ReceptionReport? m_receptionReport; + private uint m_previousPacketsSentCount; // Used to track whether we have sent any packets since the last report was sent. + + /// + /// Event handler for sending RTCP reports. + /// + public event Action? OnReportReadyToSend; + + /// + /// Fires when the connection is classified as timed out due to not + /// receiving any RTP or RTCP packets within the given period. + /// + public event Action? OnTimeout; + + /// + /// Default constructor. + /// + /// The media type this reporting session will be measuring. + /// The SSRC of the RTP stream being sent. + public RTCPSession(SDPMediaTypesEnum mediaType, uint ssrc) { - public const string NO_ACTIVITY_TIMEOUT_REASON = "No activity timeout."; - private const int RTCP_MINIMUM_REPORT_PERIOD_MILLISECONDS = 5000; - private const float RTCP_INTERVAL_LOW_RANDOMISATION_FACTOR = 0.5F; - private const float RTCP_INTERVAL_HIGH_RANDOMISATION_FACTOR = 1.5F; - private const int NO_ACTIVITY_TIMEOUT_FACTOR = 6; - - private static readonly ILogger logger = LogFactory.CreateLogger(); - - private static DateTime UtcEpoch2036 = new DateTime(2036, 2, 7, 6, 28, 16, DateTimeKind.Utc); - private static DateTime UtcEpoch1900 = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - /// - /// The media type this report session is measuring. - /// - public SDPMediaTypesEnum MediaType { get; private set; } - - /// - /// The SSRC number of the RTP packets we are sending. - /// - public uint Ssrc { get; set; } - - /// - /// Timestamp that the RTCP session was created at. - /// - public DateTime CreatedAt { get; private set; } - - /// - /// Timestamp that the RTCP session sender report scheduler was started at. - /// - public DateTime StartedAt { get; private set; } = DateTime.MinValue; - - /// - /// Timestamp that the last RTP or RTCP packet for was received at. - /// - public DateTime LastActivityAt { get; private set; } = DateTime.MinValue; - - /// - /// Sets the timeout threshold for this session. If no RTP or RTCP packets are received - /// in this time period, a hangup is invoked. It's checked every 5 seconds so the timeout - /// is best set to an integer multiple of that. - /// - public int NoActivityTimeoutMilliseconds { get; set; } = NO_ACTIVITY_TIMEOUT_FACTOR * RTCP_MINIMUM_REPORT_PERIOD_MILLISECONDS; - - - /// - /// Indicates whether the session is currently in a timed out state. This - /// occurs if no RTP or RTCP packets have been received during an expected - /// interval. - /// - public bool IsTimedOut { get; private set; } = false; - - /// - /// Number of RTP packets sent to the remote party. - /// - public uint PacketsSentCount { get; private set; } - - /// - /// Number of RTP bytes sent to the remote party. - /// - public uint OctetsSentCount { get; private set; } - - /// - /// The last RTP sequence number sent by us. - /// - public ushort LastSeqNum { get; private set; } - - /// - /// The last RTP timestamp sent by us. - /// - public uint LastRtpTimestampSent { get; private set; } - - /// - /// The last NTP timestamp corresponding to the last RTP timestamp sent by us. - /// - public ulong LastNtpTimestampSent { get; private set; } - - /// - /// Number of RTP packets received from the remote party. - /// - public uint PacketsReceivedCount { get; private set; } - - /// - /// Number of RTP bytes received from the remote party. - /// - public uint OctetsReceivedCount { get; private set; } - - /// - /// Unique common name field for use in SDES packets. - /// - public string Cname { get; private set; } - - /// - /// The reception report to keep track of the RTP statistics - /// from packets received from the remote call party. - /// - public ReceptionReport ReceptionReport { get; private set; } - - /// - /// Indicates whether the RTCP session has been closed. - /// An RTCP BYE request will typically trigger an close. - /// - public bool IsClosed { get; private set; } = false; - - /// - /// Indicates the sample rate for RTP media data. - /// - public int PayloadSampleRateHz { get; set; } = 0; - - /// - /// Time to schedule the delivery of RTCP reports. - /// - private Timer m_rtcpReportTimer; - - private ReceptionReport m_receptionReport; - private uint m_previousPacketsSentCount = 0; // Used to track whether we have sent any packets since the last report was sent. - - /// - /// Event handler for sending RTCP reports. - /// - public event Action OnReportReadyToSend; - - /// - /// Fires when the connection is classified as timed out due to not - /// receiving any RTP or RTCP packets within the given period. - /// - public event Action OnTimeout; - - /// - /// Default constructor. - /// - /// The media type this reporting session will be measuring. - /// The SSRC of the RTP stream being sent. - public RTCPSession(SDPMediaTypesEnum mediaType, uint ssrc) + MediaType = mediaType; + Ssrc = ssrc; + CreatedAt = DateTime.Now; + Cname = Guid.NewGuid().ToString(); + } + + public void Start() + { + if (StartedAt != DateTime.MinValue) { - MediaType = mediaType; - Ssrc = ssrc; - CreatedAt = DateTime.Now; - Cname = Guid.NewGuid().ToString(); + logger.LogRtcpSessionAlreadyStarted(Cname, Ssrc); } - - public void Start() + else { - if (StartedAt != DateTime.MinValue) - { - logger.LogWarning("Start was called on RTCP session for {CNameOrSsrc} but it has already been started.", !string.IsNullOrWhiteSpace(Cname) ? Cname : Ssrc.ToString()); - } - else - { - logger.LogDebug("Starting RTCP session for {CNameOrSsrc}.", !string.IsNullOrWhiteSpace(Cname) ? Cname : Ssrc.ToString()); + logger.LogRtcpSessionStart(Cname, Ssrc); - StartedAt = DateTime.Now; + StartedAt = DateTime.Now; - // Schedule an immediate sender report. - var interval = GetNextRtcpInterval(RTCP_MINIMUM_REPORT_PERIOD_MILLISECONDS); - m_rtcpReportTimer = new Timer(SendReportTimerCallback); + // Schedule an immediate sender report. + var interval = GetNextRtcpInterval(RTCP_MINIMUM_REPORT_PERIOD_MILLISECONDS); + m_rtcpReportTimer = new Timer(SendReportTimerCallback, null, Timeout.Infinite, Timeout.Infinite); m_rtcpReportTimer.Change(interval, Timeout.Infinite); - } } + } - public void Close(string reason) + public void Close(string? reason) + { + if (!IsClosed) { - if (!IsClosed) - { - IsClosed = true; - m_rtcpReportTimer?.Dispose(); + IsClosed = true; + m_rtcpReportTimer?.Dispose(); - var byeReport = GetRtcpReport(); - byeReport.Bye = new RTCPBye(Ssrc, reason); - OnReportReadyToSend?.Invoke(MediaType, byeReport); - } + var byeReport = GetRtcpReport(); + byeReport.Bye = new RTCPBye(Ssrc, reason); + OnReportReadyToSend?.Invoke(MediaType, byeReport); } + } - /// - /// Event handler for an RTP packet being received by the RTP session. - /// Used for measuring transmission statistics. - /// - public void RecordRtpPacketReceived(RTPPacket rtpPacket) + /// + /// Event handler for an RTP packet being received by the RTP session. + /// Used for measuring transmission statistics. + /// + public void RecordRtpPacketReceived(RTPPacket rtpPacket) + { + LastActivityAt = DateTime.Now; + IsTimedOut = false; + PacketsReceivedCount++; + OctetsReceivedCount += (uint)rtpPacket.Payload.Length; + + if (m_receptionReport is null) { - LastActivityAt = DateTime.Now; - IsTimedOut = false; - PacketsReceivedCount++; - OctetsReceivedCount += rtpPacket.GetPayloadLength(); + m_receptionReport = new ReceptionReport(rtpPacket.Header.SyncSource); + } - if (m_receptionReport == null) - { - m_receptionReport = new ReceptionReport(rtpPacket.Header.SyncSource); - } + var arrivalTimestamp = PayloadSampleRateHz == 0 ? DateTimeToNtpTimestamp32(DateTime.Now) : (uint)((DateTime.Now - CreatedAt).TotalSeconds * PayloadSampleRateHz); + m_receptionReport.RtpPacketReceived(rtpPacket.Header.SequenceNumber, rtpPacket.Header.Timestamp, arrivalTimestamp); + } - var arrivalTimestamp = PayloadSampleRateHz == 0 ? DateTimeToNtpTimestamp32(DateTime.Now) : (uint)((DateTime.Now - CreatedAt).TotalSeconds * PayloadSampleRateHz); - m_receptionReport.RtpPacketReceived(rtpPacket.Header.SequenceNumber, rtpPacket.Header.Timestamp, arrivalTimestamp); + /// + /// Removes the reception report when the remote party indicates no more RTP packets + /// for that SSRC will be received by sending an RTCP BYE. + /// + /// The SSRC of the reception report being closed. Typically this + /// should be the SSRC received in the RTCP BYE. + public void RemoveReceptionReport(uint ssrc) + { + if (m_receptionReport is { } && m_receptionReport.SSRC == ssrc) + { + logger.LogRtcpSessionRemovingReport(ssrc); + m_receptionReport = null; } + } - /// - /// Removes the reception report when the remote party indicates no more RTP packets - /// for that SSRC will be received by sending an RTCP BYE. - /// - /// The SSRC of the reception report being closed. Typically this - /// should be the SSRC received in the RTCP BYE. - public void RemoveReceptionReport(uint ssrc) + /// + /// Event handler for an RTP packet being sent by the RTP session. + /// Used for measuring transmission statistics. + /// + public void RecordRtpPacketSend(RTPPacket rtpPacket) + { + if (StartedAt == DateTime.MinValue) { - if (m_receptionReport != null && m_receptionReport.SSRC == ssrc) - { - logger.LogDebug("RTCP session removing reception report for remote ssrc {Ssrc}.", ssrc); - m_receptionReport = null; - } + Start(); } - /// - /// Event handler for an RTP packet being sent by the RTP session. - /// Used for measuring transmission statistics. - /// - public void RecordRtpPacketSend(RTPPacket rtpPacket) + PacketsSentCount++; + OctetsSentCount += (uint)rtpPacket.Payload.Length; + LastSeqNum = rtpPacket.Header.SequenceNumber; + LastRtpTimestampSent = rtpPacket.Header.Timestamp; + LastNtpTimestampSent = DateTimeToNtpTimestamp(DateTime.Now); + } + + /// + /// Event handler for an RTCP packet being received from the remote party. + /// + /// The end point the packet was received from. + /// The data received. + public void ReportReceived(IPEndPoint remoteEndPoint, RTCPCompoundPacket rtcpCompoundPacket) + { + try { - if(StartedAt == DateTime.MinValue) + if (StartedAt == DateTime.MinValue) { Start(); } - PacketsSentCount++; - OctetsSentCount += rtpPacket.GetPayloadLength(); - LastSeqNum = rtpPacket.Header.SequenceNumber; - LastRtpTimestampSent = rtpPacket.Header.Timestamp; - LastNtpTimestampSent = DateTimeToNtpTimestamp(DateTime.Now); - } + LastActivityAt = DateTime.Now; + IsTimedOut = false; - /// - /// Event handler for an RTCP packet being received from the remote party. - /// - /// The end point the packet was received from. - /// The data received. - public void ReportReceived(IPEndPoint remoteEndPoint, RTCPCompoundPacket rtcpCompoundPacket) - { - try + if (rtcpCompoundPacket is { }) { - if (StartedAt == DateTime.MinValue) + if (rtcpCompoundPacket.SenderReport is { } && m_receptionReport is { }) { - Start(); + m_receptionReport.RtcpSenderReportReceived(rtcpCompoundPacket.SenderReport.NtpTimestamp); } - LastActivityAt = DateTime.Now; - IsTimedOut = false; - - if (rtcpCompoundPacket != null) - { - if (rtcpCompoundPacket.SenderReport != null && m_receptionReport != null) - { - m_receptionReport.RtcpSenderReportReceived(rtcpCompoundPacket.SenderReport.NtpTimestamp); - } - - // TODO: Apply information from report. - //if (rtcpCompoundPacket.SenderReport != null) - //{ - // if (m_receptionReport == null) - // { - // m_receptionReport = new ReceptionReport(rtcpCompoundPacket.SenderReport.SSRC); - // } + // TODO: Apply information from report. + //if (rtcpCompoundPacket.SenderReport is { }) + //{ + // if (m_receptionReport is null) + // { + // m_receptionReport = new ReceptionReport(rtcpCompoundPacket.SenderReport.SSRC); + // } - // m_receptionReport.RtcpSenderReportReceived(rtcpCompoundPacket.SenderReport.NtpTimestamp); + // m_receptionReport.RtcpSenderReportReceived(rtcpCompoundPacket.SenderReport.NtpTimestamp); - // var sr = rtcpCompoundPacket.SenderReport; - //} + // var sr = rtcpCompoundPacket.SenderReport; + //} - //if (rtcpCompoundPacket.ReceiverReport != null) - //{ - // var rr = rtcpCompoundPacket.ReceiverReport.ReceptionReports.First(); - //} - } - } - catch (Exception excp) - { - logger.LogError(excp, "Exception RTCPSession.ReportReceived. {ErrorMessage}", excp.Message); + //if (rtcpCompoundPacket.ReceiverReport is { }) + //{ + // var rr = rtcpCompoundPacket.ReceiverReport.ReceptionReports.First(); + //} } } + catch (Exception excp) + { + logger.LogRtcpSessionReportReceiveError(excp.Message, excp); + } + } - /// - /// Callback function for the RTCP report timer. - /// - /// Not used. - private void SendReportTimerCallback(Object stateInfo) + /// + /// Callback function for the RTCP report timer. + /// + /// Not used. + private void SendReportTimerCallback(object? stateInfo) + { + try { - try + if (!IsClosed) { - if (!IsClosed) + Debug.Assert(m_rtcpReportTimer is { }); + lock (m_rtcpReportTimer) { - lock (m_rtcpReportTimer) + if ((LastActivityAt != DateTime.MinValue && DateTime.Now.Subtract(LastActivityAt).TotalMilliseconds > NoActivityTimeoutMilliseconds) || + (LastActivityAt == DateTime.MinValue && DateTime.Now.Subtract(CreatedAt).TotalMilliseconds > NoActivityTimeoutMilliseconds)) { - if ((LastActivityAt != DateTime.MinValue && DateTime.Now.Subtract(LastActivityAt).TotalMilliseconds > NoActivityTimeoutMilliseconds) || - (LastActivityAt == DateTime.MinValue && DateTime.Now.Subtract(CreatedAt).TotalMilliseconds > NoActivityTimeoutMilliseconds)) + if (!IsTimedOut) { - if (!IsTimedOut) - { - logger.LogWarning("RTCP session for local ssrc {Ssrc} has not had any activity for over {NoActivityTimeoutSeconds} seconds.", Ssrc, NoActivityTimeoutMilliseconds / 1000); - IsTimedOut = true; + logger.LogRtcpSessionNoActivity(Ssrc, NoActivityTimeoutMilliseconds); + IsTimedOut = true; - OnTimeout?.Invoke(MediaType); - } + OnTimeout?.Invoke(MediaType); } + } - //logger.LogDebug("SendRtcpSenderReport ssrc {Ssrc}, last seqnum {LastSeqNum}, pkts {PacketsSentCount}, bytes {OctetsSentCount}", Ssrc, LastSeqNum, PacketsSentCount, OctetsSentCount); + //logger.LogDebug("SendRtcpSenderReport ssrc {Ssrc}, last seqnum {LastSeqNum}, pkts {PacketsSentCount}, bytes {OctetsSentCount}", Ssrc, LastSeqNum, PacketsSentCount, OctetsSentCount); - var report = GetRtcpReport(); + var report = GetRtcpReport(); - OnReportReadyToSend?.Invoke(MediaType, report); + OnReportReadyToSend?.Invoke(MediaType, report); - m_previousPacketsSentCount = PacketsSentCount; + m_previousPacketsSentCount = PacketsSentCount; - var interval = GetNextRtcpInterval(RTCP_MINIMUM_REPORT_PERIOD_MILLISECONDS); - if (m_rtcpReportTimer == null) - { - m_rtcpReportTimer = new Timer(SendReportTimerCallback); - m_rtcpReportTimer.Change(interval, Timeout.Infinite); - } - else - { - m_rtcpReportTimer?.Change(interval, Timeout.Infinite); - } + var interval = GetNextRtcpInterval(RTCP_MINIMUM_REPORT_PERIOD_MILLISECONDS); + if (m_rtcpReportTimer is null) + { + m_rtcpReportTimer = new Timer(SendReportTimerCallback, null, Timeout.Infinite, Timeout.Infinite); + m_rtcpReportTimer.Change(interval, Timeout.Infinite); + } + else + { + m_rtcpReportTimer?.Change(interval, Timeout.Infinite); } } } - catch (ObjectDisposedException) // The RTP socket can disappear between the null check and the report send. - { - m_rtcpReportTimer?.Dispose(); - } - catch (Exception excp) - { - // RTCP reports are not critical enough to bubble the exception up to the application. - logger.LogError(excp, "Exception SendReportTimerCallback. {ErrorMessage}", excp.Message); - m_rtcpReportTimer?.Dispose(); - } } - - /// - /// Gets the RTCP compound packet containing the RTCP reports we send. - /// - /// An RTCP compound packet. - public RTCPCompoundPacket GetRtcpReport() + catch (ObjectDisposedException) // The RTP socket can disappear between the null check and the report send. { - var ntcTime = DateTimeToNtpTimestamp(DateTime.Now); - ReceptionReportSample rr = (m_receptionReport != null) ? m_receptionReport.GetSample(To32Bit(ntcTime)) : null; - var sdesReport = new RTCPSDesReport(Ssrc, Cname); + m_rtcpReportTimer?.Dispose(); + } + catch (Exception excp) + { + // RTCP reports are not critical enough to bubble the exception up to the application. + logger.LogRtcpSessionSendReportError(excp.Message, excp); + m_rtcpReportTimer?.Dispose(); + } + } + + /// + /// Gets the RTCP compound packet containing the RTCP reports we send. + /// + /// An RTCP compound packet. + public RTCPCompoundPacket GetRtcpReport() + { + var ntcTime = DateTimeToNtpTimestamp(DateTime.Now); + var rr = m_receptionReport?.GetSample(To32Bit(ntcTime)); + var sdesReport = new RTCPSDesReport(Ssrc, Cname); - if (PacketsSentCount > m_previousPacketsSentCount) + if (PacketsSentCount > m_previousPacketsSentCount) + { + // If we have sent a packet since the last report then we send an RTCP Sender Report. + // TODO: RTP timestamp should corresponds to the same time as the NTP timestamp + var senderReport = new RTCPSenderReport( + Ssrc, + ntcTime, + LastRtpTimestampSent, + PacketsSentCount, + OctetsSentCount, + (rr is { }) ? [rr] : null); + return new RTCPCompoundPacket(senderReport, sdesReport); + } + else + { + // If we have NOT sent a packet since the last report then we send an RTCP Receiver Report. + if (rr is { }) { - // If we have sent a packet since the last report then we send an RTCP Sender Report. - // TODO: RTP timestamp should corresponds to the same time as the NTP timestamp - var senderReport = new RTCPSenderReport(Ssrc, ntcTime, LastRtpTimestampSent, PacketsSentCount, OctetsSentCount, (rr != null) ? new List { rr } : null); - return new RTCPCompoundPacket(senderReport, sdesReport); + var receiverReport = new RTCPReceiverReport(Ssrc, new List { rr }); + return new RTCPCompoundPacket(receiverReport, sdesReport); } else { - // If we have NOT sent a packet since the last report then we send an RTCP Receiver Report. - if (rr != null) - { - var receiverReport = new RTCPReceiverReport(Ssrc, new List { rr }); - return new RTCPCompoundPacket(receiverReport, sdesReport); - } - else - { - var receiverReport = new RTCPReceiverReport(Ssrc, null); - return new RTCPCompoundPacket(receiverReport, sdesReport); - } + var receiverReport = new RTCPReceiverReport(Ssrc, null); + return new RTCPCompoundPacket(receiverReport, sdesReport); } } + } - /// - /// Gets a pseudo-randomised interval for the next RTCP report period. - /// - /// The base report interval to randomise. - /// A value in milliseconds to use for the next RTCP report interval. - private int GetNextRtcpInterval(int baseInterval) - { - return Crypto.GetRandomInt((int)(RTCP_INTERVAL_LOW_RANDOMISATION_FACTOR * baseInterval), - (int)(RTCP_INTERVAL_HIGH_RANDOMISATION_FACTOR * baseInterval)); - } + /// + /// Gets a pseudo-randomised interval for the next RTCP report period. + /// + /// The base report interval to randomise. + /// A value in milliseconds to use for the next RTCP report interval. + private int GetNextRtcpInterval(int baseInterval) + { + return Crypto.GetRandomInt((int)(RTCP_INTERVAL_LOW_RANDOMISATION_FACTOR * baseInterval), + (int)(RTCP_INTERVAL_HIGH_RANDOMISATION_FACTOR * baseInterval)); + } - public static uint To32Bit(ulong ntpTime) => (uint)((ntpTime >> 16) & 0xFFFFFFFF); - - public static uint DateTimeToNtpTimestamp32(DateTime value) { return (uint)((DateTimeToNtpTimestamp(value) >> 16) & 0xFFFFFFFF); } - - /// - /// Converts specified DateTime value to long NTP time. - /// - /// DateTime value to convert. This value must be in local time. - /// Returns NTP value. - /// - /// Wallclock time (absolute date and time) is represented using the - /// timestamp format of the Network Time Protocol (NPT), which is in - /// seconds relative to 0h UTC on 1 January 1900 [4]. The full - /// resolution NPT timestamp is a 64-bit unsigned fixed-point number with - /// the integer part in the first 32 bits and the fractional part in the - /// last 32 bits. In some fields where a more compact representation is - /// appropriate, only the middle 32 bits are used; that is, the low 16 - /// bits of the integer part and the high 16 bits of the fractional part. - /// The high 16 bits of the integer part must be determined independently. - /// - public static ulong DateTimeToNtpTimestamp(DateTime value) - { - DateTime baseDate = value >= UtcEpoch2036 ? UtcEpoch2036 : UtcEpoch1900; + public static uint To32Bit(ulong ntpTime) => (uint)((ntpTime >> 16) & 0xFFFFFFFF); - TimeSpan elapsedTime = value > baseDate ? value.ToUniversalTime() - baseDate.ToUniversalTime() : baseDate.ToUniversalTime() - value.ToUniversalTime(); + public static uint DateTimeToNtpTimestamp32(DateTime value) { return (uint)((DateTimeToNtpTimestamp(value) >> 16) & 0xFFFFFFFF); } - var seconds = elapsedTime.TotalSeconds; - return ((ulong)seconds << 32) | (ulong)((seconds - (ulong)seconds) * ((ulong)1 << 32)); - } + /// + /// Converts specified DateTime value to long NTP time. + /// + /// DateTime value to convert. This value must be in local time. + /// Returns NTP value. + /// + /// Wallclock time (absolute date and time) is represented using the + /// timestamp format of the Network Time Protocol (NPT), which is in + /// seconds relative to 0h UTC on 1 January 1900 [4]. The full + /// resolution NPT timestamp is a 64-bit unsigned fixed-point number with + /// the integer part in the first 32 bits and the fractional part in the + /// last 32 bits. In some fields where a more compact representation is + /// appropriate, only the middle 32 bits are used; that is, the low 16 + /// bits of the integer part and the high 16 bits of the fractional part. + /// The high 16 bits of the integer part must be determined independently. + /// + public static ulong DateTimeToNtpTimestamp(DateTime value) + { + var baseDate = value >= UtcEpoch2036 ? UtcEpoch2036 : UtcEpoch1900; + + var elapsedTime = value > baseDate ? value.ToUniversalTime() - baseDate.ToUniversalTime() : baseDate.ToUniversalTime() - value.ToUniversalTime(); + + var seconds = elapsedTime.TotalSeconds; + return ((ulong)seconds << 32) | (ulong)((seconds - (ulong)seconds) * ((ulong)1 << 32)); } } diff --git a/src/SIPSorcery/net/RTCP/RTCPTWCCFeedback.cs b/src/SIPSorcery/net/RTCP/RTCPTWCCFeedback.cs index 26be2150bd..0035cb3fcd 100644 --- a/src/SIPSorcery/net/RTCP/RTCPTWCCFeedback.cs +++ b/src/SIPSorcery/net/RTCP/RTCPTWCCFeedback.cs @@ -37,524 +37,573 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public enum TWCCPacketStatusType { - public enum TWCCPacketStatusType - { - NotReceived = 0, - ReceivedSmallDelta = 1, - ReceivedLargeDelta = 2, - Reserved = 3 - } + NotReceived = 0, + ReceivedSmallDelta = 1, + ReceivedLargeDelta = 2, + Reserved = 3 +} +/// +/// Represents the status of a single RTP packet in a TWCC feedback message. +/// +public class TWCCPacketStatus +{ /// - /// Represents the status of a single RTP packet in a TWCC feedback message. + /// The RTP sequence number for this packet. /// - public class TWCCPacketStatus - { - /// - /// The RTP sequence number for this packet. - /// - public ushort SequenceNumber { get; set; } - /// - /// The reception status. - /// - public TWCCPacketStatusType Status { get; set; } - /// - /// The receive time delta in (raw) units (typically 250 µs per unit). Null if not received. - /// - public int? Delta { get; set; } - } + public ushort SequenceNumber { get; set; } + /// + /// The reception status. + /// + public TWCCPacketStatusType Status { get; set; } + /// + /// The receive time delta in (raw) units (typically 250 µs per unit). Null if not received. + /// + public int? Delta { get; set; } +} + +/// +/// Parser and serializer for RTCP TWCC feedback messages as per RFC 8888. +/// +/// Format: +/// +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// |V=2|P| FMT=15 | PT=205 | length | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | SSRC of packet sender | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | SSRC of media source | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Base Sequence Number | Packet Status Count | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reference Time | +/// | (24 bits) | FB Packet Count (8 bits) | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// | Packet Status Chunks (variable length) | +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// | Receive Delta Values (variable length) | +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// +/// Packet Status Chunks are 16-bit fields and come in two flavors: +/// +/// 1. **Run Length Chunk** (when the two MSB are 00): +/// - Bits 15–14: Type (00) +/// - Bits 13–12: Packet Status Symbol (0–3) +/// - Bits 11–0 : Run Length (number of consecutive packets with that symbol) +/// +/// 2. **Status Vector Chunk** (when the two MSB are 10 or 11): +/// - Bits 15–14: Type (10 for two-bit symbols, 11 for one-bit symbols) +/// - Bits 13–0 : For two-bit mode: seven 2-bit symbols; for one-bit mode: fourteen 1-bit symbols. +/// In one-bit mode, a bit value of 0 means packet not received; a value of 1 means received (assumed small delta). +/// +/// For every packet marked as received (i.e. status of ReceivedSmallDelta or ReceivedLargeDelta), +/// a delta field is present in the delta section. For small delta the field is 1 byte (signed), +/// for large delta it is 2 bytes (signed, network order). +/// +public partial class RTCPTWCCFeedback : IByteSerializable +{ + public RTCPHeader Header { get; private set; } + public uint SenderSSRC { get; private set; } + public uint MediaSSRC { get; private set; } + + /// + /// The first (base) sequence number covered by this feedback. + /// + public ushort BaseSequenceNumber { get; private set; } + + /// + /// Total number of packet statuses described. + /// + public ushort PacketStatusCount { get; private set; } + + /// + /// 24-bit reference time (in 1/64 seconds) from the top 24 bits of this 32-bit word. + /// + public uint ReferenceTime { get; private set; } /// - /// Parser and serializer for RTCP TWCC feedback messages as per RFC 8888. - /// - /// Format: - /// - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// |V=2|P| FMT=15 | PT=205 | length | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | SSRC of packet sender | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | SSRC of media source | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Base Sequence Number | Packet Status Count | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Reference Time | - /// | (24 bits) | FB Packet Count (8 bits) | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | | - /// | Packet Status Chunks (variable length) | - /// | | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | | - /// | Receive Delta Values (variable length) | - /// | | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// - /// Packet Status Chunks are 16-bit fields and come in two flavors: - /// - /// 1. **Run Length Chunk** (when the two MSB are 00): - /// - Bits 15–14: Type (00) - /// - Bits 13–12: Packet Status Symbol (0–3) - /// - Bits 11–0 : Run Length (number of consecutive packets with that symbol) - /// - /// 2. **Status Vector Chunk** (when the two MSB are 10 or 11): - /// - Bits 15–14: Type (10 for two-bit symbols, 11 for one-bit symbols) - /// - Bits 13–0 : For two-bit mode: seven 2-bit symbols; for one-bit mode: fourteen 1-bit symbols. - /// In one-bit mode, a bit value of 0 means packet not received; a value of 1 means received (assumed small delta). - /// - /// For every packet marked as received (i.e. status of ReceivedSmallDelta or ReceivedLargeDelta), - /// a delta field is present in the delta section. For small delta the field is 1 byte (signed), - /// for large delta it is 2 bytes (signed, network order). + /// Feedback packet count (the lower 8 bits of the 32-bit word containing ReferenceTime). /// - public class RTCPTWCCFeedback + public byte FeedbackPacketCount { get; private set; } + + /// + /// The list of per-packet statuses (in order from BaseSequenceNumber). + /// + public List PacketStatuses { get; private set; } = new List(); + + /// + /// The resolution multiplier for delta values (e.g. 250 µs per unit). + /// + public int DeltaScale { get; set; } = 250; + + /// + /// Constructs a TWCC feedback message from the raw RTCP packet. + /// + /// The complete RTCP TWCC feedback packet. + /// + /// Parses a TWCC feedback packet from the given byte array. + /// + public RTCPTWCCFeedback(ReadOnlySpan packet) { - public RTCPHeader Header { get; private set; } - public uint SenderSSRC { get; private set; } - public uint MediaSSRC { get; private set; } - - /// - /// The first (base) sequence number covered by this feedback. - /// - public ushort BaseSequenceNumber { get; private set; } - - /// - /// Total number of packet statuses described. - /// - public ushort PacketStatusCount { get; private set; } - - /// - /// 24-bit reference time (in 1/64 seconds) from the top 24 bits of this 32-bit word. - /// - public uint ReferenceTime { get; private set; } - - /// - /// Feedback packet count (the lower 8 bits of the 32-bit word containing ReferenceTime). - /// - public byte FeedbackPacketCount { get; private set; } - - /// - /// The list of per-packet statuses (in order from BaseSequenceNumber). - /// - public List PacketStatuses { get; private set; } = new List(); - - /// - /// The resolution multiplier for delta values (e.g. 250 µs per unit). - /// - public int DeltaScale { get; set; } = 250; - - /// - /// Constructs a TWCC feedback message from the raw RTCP packet. - /// - /// The complete RTCP TWCC feedback packet. - /// - /// Parses a TWCC feedback packet from the given byte array. - /// - public RTCPTWCCFeedback(byte[] packet) - { - ValidatePacket(packet); + ValidatePacket(packet); - // Parse the RTCP header. - Header = new RTCPHeader(packet); - int offset = RTCPHeader.HEADER_BYTES_LENGTH; + // Parse the RTCP header. + Header = new RTCPHeader(packet); + packet = packet.Slice(RTCPHeader.HEADER_BYTES_LENGTH); - // Parse sender and media SSRCs - SenderSSRC = ReadUInt32(packet, ref offset); - MediaSSRC = ReadUInt32(packet, ref offset); + // Parse sender and media SSRCs + SenderSSRC = BinaryExtensions.ReadUInt32BigEndianAndAdvance(ref packet); + MediaSSRC = BinaryExtensions.ReadUInt32BigEndianAndAdvance(ref packet); - // Parse Base Sequence Number and Packet Status Count - BaseSequenceNumber = ReadUInt16(packet, ref offset); - PacketStatusCount = ReadUInt16(packet, ref offset); + // Parse Base Sequence Number and Packet Status Count + BaseSequenceNumber = BinaryExtensions.ReadUInt16BigEndianAndAdvance(ref packet); + PacketStatusCount = BinaryExtensions.ReadUInt16BigEndianAndAdvance(ref packet); - // Parse Reference Time and Feedback Packet Count - ReferenceTime = ParseReferenceTime(packet, ref offset, out byte fbCount); - FeedbackPacketCount = fbCount; + // Parse Reference Time and Feedback Packet Count + ReferenceTime = ParseReferenceTime(ref packet, out var fbCount); + FeedbackPacketCount = fbCount; - // Parse status chunks - var statusSymbols = ParseStatusChunks(packet, ref offset); + // Parse status chunks + var statusSymbols = ParseStatusChunks(ref packet); - // Parse delta values with validation - var (deltaValues, lastOffset) = ParseDeltaValues(packet, offset, statusSymbols); + // Parse delta values with validation + var deltaValues = ParseDeltaValues(ref packet, statusSymbols); - // Build final packet status list - BuildPacketStatusList(statusSymbols, deltaValues); - - } + // Build final packet status list + BuildPacketStatusList(statusSymbols, deltaValues); - private void ParseRunLengthChunk(ushort chunk, List statusSymbols, ref int remainingStatuses) + } + + private void ParseRunLengthChunk(ushort chunk, List statusSymbols, ref int remainingStatuses) + { + // The status bits might be reversed from what we expect + var statusBits = (chunk >> 12) & 0x3; + var symbol = statusBits switch { - // The status bits might be reversed from what we expect - int statusBits = (chunk >> 12) & 0x3; - TWCCPacketStatusType symbol; + 0b00 => TWCCPacketStatusType.NotReceived, + 0b01 => TWCCPacketStatusType.ReceivedSmallDelta, + 0b10 => TWCCPacketStatusType.ReceivedSmallDelta,// Changed from Large to Small + 0b11 => TWCCPacketStatusType.ReceivedLargeDelta, + _ => throw new ArgumentException($"Invalid status bits: {statusBits}"), + }; + var runLength = (ushort)(chunk & 0x0FFF); + + runLength = (ushort)Math.Min(runLength, remainingStatuses); + for (var i = 0; i < runLength; i++) + { + statusSymbols.Add(symbol); + } + remainingStatuses -= runLength; + } - switch (statusBits) - { - case 0: // 00 - symbol = TWCCPacketStatusType.NotReceived; - break; - case 1: // 01 - symbol = TWCCPacketStatusType.ReceivedSmallDelta; - break; - case 2: // 10 - symbol = TWCCPacketStatusType.ReceivedSmallDelta; // Changed from Large to Small - break; - case 3: // 11 - symbol = TWCCPacketStatusType.ReceivedLargeDelta; - break; - default: - throw new ArgumentException($"Invalid status bits: {statusBits}"); - } + private void ValidatePacket(ReadOnlySpan packet) + { + if (packet.Length < (RTCPHeader.HEADER_BYTES_LENGTH + 12)) + { + throw new ArgumentException("Packet too short to be a valid TWCC feedback message."); + } + } - ushort runLength = (ushort)(chunk & 0x0FFF); - - runLength = (ushort)Math.Min(runLength, remainingStatuses); - for (int i = 0; i < runLength; i++) - { - statusSymbols.Add(symbol); - } - remainingStatuses -= runLength; + private uint ParseReferenceTime(ref ReadOnlySpan packet, out byte fbCount) + { + if (packet.Length < 4) + { + throw new ArgumentException("Packet truncated at reference time."); } - private void ValidatePacket(byte[] packet) + var b1 = packet[0]; + var b2 = packet[1]; + var b3 = packet[2]; + fbCount = packet[3]; + packet = packet.Slice(4); + return (uint)((b1 << 16) | (b2 << 8) | b3); + } + + private List ParseStatusChunks(ref ReadOnlySpan packet) + { + var statusSymbols = new List(); + int remainingStatuses = PacketStatusCount; + + while (remainingStatuses > 0) { - if (packet == null) + if (packet.Length < 2) { - throw new ArgumentNullException(nameof(packet)); + throw new ArgumentException($"Packet truncated during status chunk parsing. Expected {remainingStatuses} more statuses."); } - if (packet.Length < (RTCPHeader.HEADER_BYTES_LENGTH + 12)) + var chunk = BinaryExtensions.ReadUInt16BigEndianAndAdvance(ref packet); + var chunkType = chunk >> 14; + + switch (chunkType) { - throw new ArgumentException("Packet too short to be a valid TWCC feedback message."); + case 0: // Run Length Chunk + ParseRunLengthChunk(chunk, statusSymbols, ref remainingStatuses); + break; + case 2: // Two-bit Status Vector + ParseTwoBitStatusVector(chunk, statusSymbols, ref remainingStatuses); + break; + case 3: // One-bit Status Vector + ParseOneBitStatusVector(chunk, statusSymbols, ref remainingStatuses); + break; } } - private uint ParseReferenceTime(byte[] packet, ref int offset, out byte fbCount) - { - if (offset + 4 > packet.Length) - { - throw new ArgumentException("Packet truncated at reference time."); - } + return statusSymbols; + } - byte b1 = packet[offset++]; - byte b2 = packet[offset++]; - byte b3 = packet[offset++]; - fbCount = packet[offset++]; - return (uint)((b1 << 16) | (b2 << 8) | b3); + private void ParseTwoBitStatusVector(ushort chunk, List statusSymbols, ref int remainingStatuses) + { + var symbolsToRead = Math.Min(7, remainingStatuses); + for (var i = 0; i < symbolsToRead; i++) + { + var shift = 12 - (2 * i); + var symVal = (chunk >> shift) & 0x3; + statusSymbols.Add((TWCCPacketStatusType)symVal); } + remainingStatuses -= symbolsToRead; + } - private List ParseStatusChunks(byte[] packet, ref int offset) + private void ParseOneBitStatusVector(ushort chunk, List statusSymbols, ref int remainingStatuses) + { + var symbolsToRead = Math.Min(14, remainingStatuses); + for (var i = 0; i < symbolsToRead; i++) { - var statusSymbols = new List(); - int remainingStatuses = PacketStatusCount; + var shift = 13 - i; + var bit = (chunk >> shift) & 0x1; + statusSymbols.Add(bit == 0 ? TWCCPacketStatusType.NotReceived : TWCCPacketStatusType.ReceivedSmallDelta); + } + remainingStatuses -= symbolsToRead; + } - while (remainingStatuses > 0) - { - if (offset + 2 > packet.Length) - { - throw new ArgumentException($"Packet truncated during status chunk parsing. Expected {remainingStatuses} more statuses."); - } + private List ParseDeltaValues(ref ReadOnlySpan packet, List statusSymbols) + { + var deltaValues = new List(); - ushort chunk = ReadUInt16(packet, ref offset); - int chunkType = chunk >> 14; + foreach (var status in statusSymbols) + { + if (status is TWCCPacketStatusType.NotReceived or TWCCPacketStatusType.Reserved) + { + deltaValues.Add(0); + continue; + } - switch (chunkType) - { - case 0: // Run Length Chunk - ParseRunLengthChunk(chunk, statusSymbols, ref remainingStatuses); - break; - case 2: // Two-bit Status Vector - ParseTwoBitStatusVector(chunk, statusSymbols, ref remainingStatuses); - break; - case 3: // One-bit Status Vector - ParseOneBitStatusVector(chunk, statusSymbols, ref remainingStatuses); - break; - } + // Check if we have enough data for the delta + var deltaSize = status == TWCCPacketStatusType.ReceivedSmallDelta ? 1 : 2; + if (deltaSize > packet.Length) + { + // Instead of throwing, we'll add a special value to indicate truncation + deltaValues.Add(int.MinValue); + break; } - return statusSymbols; + if (status == TWCCPacketStatusType.ReceivedSmallDelta) + { + deltaValues.Add((sbyte)packet[0] * DeltaScale); + packet = packet.Slice(1); + } + else // ReceivedLargeDelta + { + var rawDelta = (short)((packet[0] << 8) | packet[1]); + deltaValues.Add(rawDelta * DeltaScale); + packet = packet.Slice(2); + } } + return deltaValues; + } + + private void BuildPacketStatusList(List statusSymbols, List deltaValues) + { + PacketStatuses = new List(); + var seq = BaseSequenceNumber; - private void ParseTwoBitStatusVector(ushort chunk, List statusSymbols, ref int remainingStatuses) + for (var i = 0; i < statusSymbols.Count; i++) { - int symbolsToRead = Math.Min(7, remainingStatuses); - for (int i = 0; i < symbolsToRead; i++) + int? delta = deltaValues[i] == int.MinValue ? null : + (statusSymbols[i] is TWCCPacketStatusType.NotReceived or + TWCCPacketStatusType.Reserved) ? null : deltaValues[i]; + + PacketStatuses.Add(new TWCCPacketStatus { - int shift = 12 - (2 * i); - int symVal = (chunk >> shift) & 0x3; - statusSymbols.Add((TWCCPacketStatusType)symVal); - } - remainingStatuses -= symbolsToRead; + SequenceNumber = seq++, + Status = statusSymbols[i], + Delta = delta + }); } + } - private void ParseOneBitStatusVector(ushort chunk, List statusSymbols, ref int remainingStatuses) + /// + public int GetByteCount() + { + var length = RTCPHeader.HEADER_BYTES_LENGTH + 4 + 4 + 2 + 2 + 4; + + var packageStatusesCount = PacketStatuses.Count; + for (var i = 0; i < packageStatusesCount;) { - int symbolsToRead = Math.Min(14, remainingStatuses); - for (int i = 0; i < symbolsToRead; i++) + // Try to use run-length chunk: count how many consecutive statuses are identical. + var runLength = 1; + var current = PacketStatuses[i].Status; + while (i + runLength < packageStatusesCount && PacketStatuses[i + runLength].Status == current && runLength < 0x0FFF) { - int shift = 13 - i; - int bit = (chunk >> shift) & 0x1; - statusSymbols.Add(bit == 0 ? TWCCPacketStatusType.NotReceived : TWCCPacketStatusType.ReceivedSmallDelta); + runLength++; } - remainingStatuses -= symbolsToRead; - } - - private (List deltaValues, int lastOffset) ParseDeltaValues(byte[] packet, int offset, List statusSymbols) - { - var deltaValues = new List(); - int expectedDeltaCount = statusSymbols.Count(s => - s == TWCCPacketStatusType.ReceivedSmallDelta || - s == TWCCPacketStatusType.ReceivedLargeDelta); - - foreach (var status in statusSymbols) + if (runLength >= 2) { - if (status == TWCCPacketStatusType.NotReceived || status == TWCCPacketStatusType.Reserved) - { - deltaValues.Add(0); - continue; - } + // Build run-length chunk. + // Currently: + // ushort chunk = (ushort)(((int)current & 0x3) << 12); - // Check if we have enough data for the delta - int deltaSize = status == TWCCPacketStatusType.ReceivedSmallDelta ? 1 : 2; - if (offset + deltaSize > packet.Length) + // Need to modify to use correct status bit mapping: + var statusBits = current switch { - // Instead of throwing, we'll add a special value to indicate truncation - deltaValues.Add(int.MinValue); - break; - } + TWCCPacketStatusType.NotReceived => (ushort)0b00, + TWCCPacketStatusType.ReceivedSmallDelta => (ushort)0b01,// for small delta + TWCCPacketStatusType.ReceivedLargeDelta => (ushort)0b11,// for large delta + _ => (ushort)0b00, + }; + var chunk = (ushort)(statusBits << 12); + chunk |= (ushort)(runLength & 0x0FFF); + length += 2; + i += runLength; + } + else + { + // Otherwise, pack into a two-bit status vector chunk. + var count = Math.Min(7, packageStatusesCount - i); + ushort chunk = 0x8000; // Set top bits to 10 for vector chunk - if (status == TWCCPacketStatusType.ReceivedSmallDelta) + for (var j = 0; j < count; j++) { - deltaValues.Add((sbyte)packet[offset] * DeltaScale); - offset += 1; - } - else // ReceivedLargeDelta - { - short rawDelta = (short)((packet[offset] << 8) | packet[offset + 1]); - deltaValues.Add(rawDelta * DeltaScale); - offset += 2; + // Convert status to correct bit pattern + var statusBits = PacketStatuses[i + j].Status switch + { + TWCCPacketStatusType.NotReceived => (ushort)0b00, + TWCCPacketStatusType.ReceivedSmallDelta => (ushort)0b01, + TWCCPacketStatusType.ReceivedLargeDelta => (ushort)0b11, + _ => (ushort)0b00, + }; + chunk |= (ushort)(statusBits << (12 - 2 * j)); } + length += 2; + i += count; } - - return (deltaValues, offset); } - private void BuildPacketStatusList(List statusSymbols, List deltaValues) + foreach (var ps in PacketStatuses) { - PacketStatuses = new List(); - ushort seq = BaseSequenceNumber; - - for (int i = 0; i < statusSymbols.Count; i++) + length += ps.Status switch { - int? delta = deltaValues[i] == int.MinValue ? null : - (statusSymbols[i] == TWCCPacketStatusType.NotReceived || - statusSymbols[i] == TWCCPacketStatusType.Reserved) ? null : deltaValues[i]; - - PacketStatuses.Add(new TWCCPacketStatus - { - SequenceNumber = seq++, - Status = statusSymbols[i], - Delta = delta - }); - } + TWCCPacketStatusType.ReceivedSmallDelta => 1, + TWCCPacketStatusType.ReceivedLargeDelta => 2, + _ => 0 + }; } - /// - /// Serializes this TWCC feedback message to a byte array. - /// Note: The serialization logic rebuilds the packet status chunks from the PacketStatuses list. - /// This implements the run-length chunk when possible and defaults to two-bit - /// status vector chunks if a run-length encoding isn’t efficient. - /// - /// The serialized RTCP TWCC feedback packet. - public byte[] GetBytes() + var check = GetByteCount(); + + Debug.Assert(check == length); + + return check; + } + + /// + public int WriteBytes(Span buffer) + { + // Serializes this TWCC feedback message to a byte array. + // Note: The serialization logic rebuilds the packet status chunks from the PacketStatuses list. + // This implements the run-length chunk when possible and defaults to two-bit + // status vector chunks if a run-length encoding isn’t efficient. + + var size = GetByteCount(); + + if (buffer.Length < size) { - // Build a list of TWCCPacketStatusType from PacketStatuses. - List symbols = PacketStatuses.Select(ps => ps.Status).ToList(); + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); + } + + WriteBytesCore(buffer.Slice(0, size)); + + return size; + } + + private void WriteBytesCore(Span buffer) + { + // Update the header length (in 32-bit words minus one). + Header.SetLength((ushort)(buffer.Length / 4 - 1)); + _ = Header.WriteBytes(buffer); + buffer = buffer.Slice(RTCPHeader.HEADER_BYTES_LENGTH); - // Reconstruct packet status chunks. - List chunks = new List(); - int i = 0; - while (i < symbols.Count) + // Write Sender and Media SSRC. + BinaryExtensions.WriteUInt32BigEndianAndAdvance(ref buffer, SenderSSRC); + BinaryExtensions.WriteUInt32BigEndianAndAdvance(ref buffer, MediaSSRC); + + // Write Base Sequence Number and Packet Status Count. + BinaryExtensions.WriteUInt16BigEndianAndAdvance(ref buffer, BaseSequenceNumber); + BinaryExtensions.WriteUInt16BigEndianAndAdvance(ref buffer, PacketStatusCount); + + // Build the 32-bit word for ReferenceTime and FeedbackPacketCount. + BinaryExtensions.WriteUInt32BigEndianAndAdvance(ref buffer, (ReferenceTime << 8) | FeedbackPacketCount); + + for (var i = 0; i < PacketStatuses.Count;) + { + // Try to use run-length chunk: count how many consecutive statuses are identical. + var runLength = 1; + var current = PacketStatuses[i].Status; + while (i + runLength < PacketStatuses.Count && PacketStatuses[i + runLength].Status == current && runLength < 0x0FFF) + { + runLength++; + } + if (runLength >= 2) { - // Try to use run-length chunk: count how many consecutive statuses are identical. - int runLength = 1; - TWCCPacketStatusType current = symbols[i]; - while (i + runLength < symbols.Count && symbols[i + runLength] == current && runLength < 0x0FFF) + // Build run-length chunk. + // Currently: + // ushort chunk = (ushort)(((int)current & 0x3) << 12); + + // Need to modify to use correct status bit mapping: + ushort statusBits; + switch (current) { - runLength++; + case TWCCPacketStatusType.NotReceived: + statusBits = 0; // 00 + break; + case TWCCPacketStatusType.ReceivedSmallDelta: + statusBits = 1; // 01 for small delta + // Note: status 10 (2) also means small delta + break; + case TWCCPacketStatusType.ReceivedLargeDelta: + statusBits = 3; // 11 for large delta + break; + default: + statusBits = 0; + break; } - if (runLength >= 2) - { - // Build run-length chunk. - // Currently: - // ushort chunk = (ushort)(((int)current & 0x3) << 12); - // Need to modify to use correct status bit mapping: + var chunk = (ushort)(statusBits << 12); + chunk |= (ushort)(runLength & 0x0FFF); + BinaryExtensions.WriteUInt16BigEndianAndAdvance(ref buffer, chunk); + i += runLength; + } + else + { + // Otherwise, pack into a two-bit status vector chunk. + var count = Math.Min(7, PacketStatuses.Count - i); + ushort chunk = 0x8000; // Set top bits to 10 for vector chunk + + for (var j = 0; j < count; j++) + { + // Convert status to correct bit pattern ushort statusBits; - switch (current) + switch (PacketStatuses[i + j].Status) { case TWCCPacketStatusType.NotReceived: - statusBits = 0; // 00 + statusBits = 0; break; case TWCCPacketStatusType.ReceivedSmallDelta: - statusBits = 1; // 01 for small delta - // Note: status 10 (2) also means small delta + statusBits = 1; break; case TWCCPacketStatusType.ReceivedLargeDelta: - statusBits = 3; // 11 for large delta + statusBits = 3; break; default: statusBits = 0; break; } - ushort chunk = (ushort)(statusBits << 12); - chunk |= (ushort)(runLength & 0x0FFF); - chunks.Add(chunk); - i += runLength; - } - else - { - // Otherwise, pack into a two-bit status vector chunk. - int count = Math.Min(7, symbols.Count - i); - ushort chunk = 0x8000; // Set top bits to 10 for vector chunk - - for (int j = 0; j < count; j++) - { - // Convert status to correct bit pattern - ushort statusBits; - switch (symbols[i + j]) - { - case TWCCPacketStatusType.NotReceived: - statusBits = 0; - break; - case TWCCPacketStatusType.ReceivedSmallDelta: - statusBits = 1; - break; - case TWCCPacketStatusType.ReceivedLargeDelta: - statusBits = 3; - break; - default: - statusBits = 0; - break; - } - - chunk |= (ushort)(statusBits << (12 - 2 * j)); - } - chunks.Add(chunk); - i += count; - } - } - - // Build the delta values array. - List deltaBytes = new List(); - foreach (var ps in PacketStatuses) - { - if (ps.Status == TWCCPacketStatusType.ReceivedSmallDelta) - { - // Delta was stored already scaled; convert back to raw units. - sbyte delta = (sbyte)(ps.Delta.HasValue ? ps.Delta.Value / DeltaScale : 0); - deltaBytes.Add((byte)delta); - } - else if (ps.Status == TWCCPacketStatusType.ReceivedLargeDelta) - { - if (!ps.Delta.HasValue) - { - ps.Delta = 0; - //throw new ApplicationException("Missing delta for a large delta packet."); - } - short delta = (short)(ps.Delta.Value / DeltaScale); - byte high = (byte)(delta >> 8); - byte low = (byte)(delta & 0xFF); - deltaBytes.Add(high); - deltaBytes.Add(low); + chunk |= (ushort)(statusBits << (12 - 2 * j)); } - // For not received or reserved, no delta bytes are added. + BinaryExtensions.WriteUInt16BigEndianAndAdvance(ref buffer, chunk); + i += count; } + } - // Calculate fixed part length. - int fixedPart = RTCPHeader.HEADER_BYTES_LENGTH + 4 + 4 + 2 + 2 + 4; // header, two SSRCs, Base Seq, Status Count, RefTime+FbkCnt - int chunksPart = chunks.Count * 2; - int deltasPart = deltaBytes.Count; - int totalLength = fixedPart + chunksPart + deltasPart; - byte[] buffer = new byte[totalLength]; - - // Write header (we update length later). - Buffer.BlockCopy(Header.GetBytes(), 0, buffer, 0, RTCPHeader.HEADER_BYTES_LENGTH); - int offset = RTCPHeader.HEADER_BYTES_LENGTH; - - // Write Sender and Media SSRC. - WriteUInt32(buffer, ref offset, SenderSSRC); - WriteUInt32(buffer, ref offset, MediaSSRC); - - // Write Base Sequence Number and Packet Status Count. - WriteUInt16(buffer, ref offset, BaseSequenceNumber); - WriteUInt16(buffer, ref offset, PacketStatusCount); - - // Build the 32-bit word for ReferenceTime and FeedbackPacketCount. - uint refTimeAndCount = (ReferenceTime << 8) | FeedbackPacketCount; - WriteUInt32(buffer, ref offset, refTimeAndCount); - - // Write packet status chunks. - foreach (ushort chunk in chunks) + foreach (var ps in PacketStatuses) + { + if (ps.Status == TWCCPacketStatusType.ReceivedSmallDelta) { - WriteUInt16(buffer, ref offset, chunk); + // Delta was stored already scaled; convert back to raw units. + var delta = (sbyte)(ps.Delta.GetValueOrDefault() / DeltaScale); + buffer[0] = (byte)delta; + buffer = buffer.Slice(1); } - - // Write delta values. - foreach (byte b in deltaBytes) + else if (ps.Status == TWCCPacketStatusType.ReceivedLargeDelta) { - buffer[offset++] = b; + var delta = (short)(ps.Delta.GetValueOrDefault() / DeltaScale); + var high = (byte)(delta >> 8); + var low = (byte)(delta & 0xFF); + buffer[0] = high; + buffer[1] = low; + buffer = buffer.Slice(2); } - - // Update the header length (in 32-bit words minus one). - Header.SetLength((ushort)(totalLength / 4 - 1)); - Buffer.BlockCopy(Header.GetBytes(), 0, buffer, 0, RTCPHeader.HEADER_BYTES_LENGTH); - - return buffer; - } - - public override string ToString() - { - var packetStatusInfo = string.Join(", ", PacketStatuses.Select(ps => - $"Seq:{ps.SequenceNumber}({ps.Status}{(ps.Delta.HasValue ? $",Δ:{ps.Delta.Value}" : "")})")); - - return $"TWCC Feedback: SenderSSRC={SenderSSRC}, MediaSSRC={MediaSSRC}, BaseSeq={BaseSequenceNumber}, StatusCount={PacketStatusCount}, RefTime={ReferenceTime} (1/64 sec), FbkPktCount={FeedbackPacketCount}, PacketStatuses=[{packetStatusInfo}]"; + // For not received or reserved, no delta bytes are added. } + } - #region Helper Methods + public override string ToString() + { + var builder = new ValueStringBuilder(stackalloc char[512]); // Use stack allocation for typical size - private uint ReadUInt32(byte[] buffer, ref int offset) + try { - uint value = BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(offset)); - offset += 4; - return value; + ToString(ref builder); + return builder.ToString(); } - - private ushort ReadUInt16(byte[] buffer, ref int offset) + finally { - ushort value = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(offset)); - offset += 2; - return value; + builder.Dispose(); } + } - private void WriteUInt32(byte[] buffer, ref int offset, uint value) + internal void ToString(ref ValueStringBuilder builder) + { + builder.Append("TWCC Feedback: SenderSSRC="); + builder.Append(SenderSSRC); + builder.Append(", MediaSSRC="); + builder.Append(MediaSSRC); + builder.Append(", BaseSeq="); + builder.Append(BaseSequenceNumber); + builder.Append(", StatusCount="); + builder.Append(PacketStatusCount); + builder.Append(", RefTime="); + builder.Append(ReferenceTime); + builder.Append(" (1/64 sec), FbkPktCount="); + builder.Append(FeedbackPacketCount); + builder.Append(", PacketStatuses=["); + + for (var i = 0; i < PacketStatuses.Count; i++) { - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(offset), value); - offset += 4; - } + if (i > 0) + { + builder.Append(", "); + } - private void WriteUInt16(byte[] buffer, ref int offset, ushort value) - { - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(offset), value); - offset += 2; + var ps = PacketStatuses[i]; + builder.Append("Seq:"); + builder.Append(ps.SequenceNumber); + builder.Append('('); + builder.Append(ps.Status.ToStringFast()); + + if (ps.Delta.HasValue) + { + builder.Append(",Δ:"); + builder.Append(ps.Delta.Value); + } + + builder.Append(')'); } - #endregion + builder.Append(']'); } } diff --git a/src/SIPSorcery/net/RTCP/ReceptionReport.cs b/src/SIPSorcery/net/RTCP/ReceptionReport.cs index a93f9afa6e..540d857eae 100644 --- a/src/SIPSorcery/net/RTCP/ReceptionReport.cs +++ b/src/SIPSorcery/net/RTCP/ReceptionReport.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: ReceptionReport.cs // // Description: One or more reception report blocks are included in each @@ -34,427 +34,442 @@ using System; using System.Buffers.Binary; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; -namespace SIPSorcery.Net +/// +/// Represents a point in time sample for a reception report. +/// +public partial class ReceptionReportSample : IByteSerializable { + public const int PAYLOAD_SIZE = 24; + + /// + /// Data source being reported. + /// + public uint SSRC; + + /// + /// Fraction lost since last SR/RR. + /// + public byte FractionLost; + + /// + /// Cumulative number of packets lost (signed!). + /// + public int PacketsLost; + /// - /// Represents a point in time sample for a reception report. + /// Extended last sequence number received. /// - public class ReceptionReportSample + public uint ExtendedHighestSequenceNumber; + + /// + /// Interarrival jitter. + /// + public uint Jitter; + + /// + /// Last SR packet from this source. + /// + public uint LastSenderReportTimestamp; + + /// + /// Delay since last SR packet. + /// + public uint DelaySinceLastSenderReport; + + /// + /// Creates a new Reception Report object. + /// + /// The synchronisation source this reception report is for. + /// The fraction of RTP packets lost since the previous Sender or Receiver + /// Report was sent. + /// The total number of RTP packets that have been lost since the + /// beginning of reception. + /// Extended highest sequence number received from source. + /// Interarrival jitter of the RTP packets received within the last reporting period. + /// The timestamp from the most recent RTCP Sender Report packet + /// received. + /// The delay between receiving the last Sender Report packet and the sending + /// of this Reception Report. + public ReceptionReportSample( + uint ssrc, + byte fractionLost, + int packetsLost, + uint highestSeqNum, + uint jitter, + uint lastSRTimestamp, + uint delaySinceLastSR) { - public const int PAYLOAD_SIZE = 24; - - /// - /// Data source being reported. - /// - public uint SSRC; - - /// - /// Fraction lost since last SR/RR. - /// - public byte FractionLost; - - /// - /// Cumulative number of packets lost (signed!). - /// - public int PacketsLost; - - /// - /// Extended last sequence number received. - /// - public uint ExtendedHighestSequenceNumber; - - /// - /// Interarrival jitter. - /// - public uint Jitter; - - /// - /// Last SR packet from this source. - /// - public uint LastSenderReportTimestamp; - - /// - /// Delay since last SR packet. - /// - public uint DelaySinceLastSenderReport; - - /// - /// Creates a new Reception Report object. - /// - /// The synchronisation source this reception report is for. - /// The fraction of RTP packets lost since the previous Sender or Receiver - /// Report was sent. - /// The total number of RTP packets that have been lost since the - /// beginning of reception. - /// Extended highest sequence number received from source. - /// Interarrival jitter of the RTP packets received within the last reporting period. - /// The timestamp from the most recent RTCP Sender Report packet - /// received. - /// The delay between receiving the last Sender Report packet and the sending - /// of this Reception Report. - public ReceptionReportSample( - uint ssrc, - byte fractionLost, - int packetsLost, - uint highestSeqNum, - uint jitter, - uint lastSRTimestamp, - uint delaySinceLastSR) - { - SSRC = ssrc; - FractionLost = fractionLost; - PacketsLost = packetsLost; - ExtendedHighestSequenceNumber = highestSeqNum; - Jitter = jitter; - LastSenderReportTimestamp = lastSRTimestamp; - DelaySinceLastSenderReport = delaySinceLastSR; - } + SSRC = ssrc; + FractionLost = fractionLost; + PacketsLost = packetsLost; + ExtendedHighestSequenceNumber = highestSeqNum; + Jitter = jitter; + LastSenderReportTimestamp = lastSRTimestamp; + DelaySinceLastSenderReport = delaySinceLastSR; + } - public ReceptionReportSample(byte[] packet) + public ReceptionReportSample(ReadOnlySpan packet) + { + SSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(0, 4)); + FractionLost = packet[4]; + PacketsLost = ReadInt24BigEndian(packet.Slice(5, 3)); + ExtendedHighestSequenceNumber = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(8, 4)); + Jitter = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(12, 4)); + LastSenderReportTimestamp = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(16, 4)); + DelaySinceLastSenderReport = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(20, 4)); + + static int ReadInt24BigEndian(ReadOnlySpan packet) { - SSRC = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(0)); - FractionLost = packet[4]; - PacketsLost = BinaryPrimitives.ReadInt32BigEndian(new ReadOnlySpan(new byte[] { 0x00, packet[5], packet[6], packet[7] })); - ExtendedHighestSequenceNumber = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(8)); - Jitter = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(12)); - LastSenderReportTimestamp = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(16)); - DelaySinceLastSenderReport = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(20)); + Span buffer = stackalloc byte[4]; + packet.CopyTo(buffer.Slice(BitConverter.IsLittleEndian ? 1 : 0, 3)); + return BinaryPrimitives.ReadInt32BigEndian(buffer); } + } + + /// + public int GetByteCount() => 24; + + /// + public int WriteBytes(Span buffer) + { + var size = GetByteCount(); - /// - /// Serialises the reception report block to a byte array. - /// - /// A byte array. - public byte[] GetBytes() + if (buffer.Length < size) { - byte[] payload = new byte[24]; - - BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(0), SSRC); - payload[4] = FractionLost; - var packetsLostBuf = new byte[4]; - BinaryPrimitives.WriteInt32BigEndian(packetsLostBuf, PacketsLost); - Buffer.BlockCopy(packetsLostBuf, 1, payload, 5, 3); - BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(8), ExtendedHighestSequenceNumber); - BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(12), Jitter); - BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(16), LastSenderReportTimestamp); - BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(20), DelaySinceLastSenderReport); - - return payload; + throw new ArgumentOutOfRangeException(nameof(buffer), $"The buffer should have at least {size} bytes and had only {buffer.Length}."); } + + WriteBytesCore(buffer.Slice(0, size)); + + return size; + } + + private void WriteBytesCore(Span buffer) + { + BinaryPrimitives.WriteUInt32BigEndian(buffer, SSRC); + BinaryPrimitives.WriteInt32BigEndian(buffer.Slice(4), PacketsLost); // only 24 bits + buffer[4] = FractionLost; // overwrite first 8 bits of PacketsLost + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(8), ExtendedHighestSequenceNumber); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(12), Jitter); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(16), LastSenderReportTimestamp); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(20), DelaySinceLastSenderReport); } +} + +/// +/// Maintains the reception statistics for a received RTP stream. +/// +public class ReceptionReport +{ + //private const int MAX_DROPOUT = 3000; + //private const int MAX_MISORDER = 100; + //private const int MIN_SEQUENTIAL = 2; + private const int RTP_SEQ_MOD = 1 << 16; + //private const int MAX_POSITIVE_LOSS = 0x7fffff; + //private const int MAX_NEGATIVE_LOSS = 0x800000; + private const int SEQ_NUM_WRAP_LOW = 256; + private const int SEQ_NUM_WRAP_HIGH = 65280; + + /// + /// Data source being reported. + /// + public uint SSRC; /// - /// Maintains the reception statistics for a received RTP stream. + /// highest seq. number seen /// - public class ReceptionReport + private ushort m_max_seq; + + /// + /// Increments by UInt16.MaxValue each time the sequence number wraps around. + /// + private ulong m_cycles; + + /// + /// The first sequence number received. + /// + private uint m_base_seq; + + /// + /// last 'bad' seq number + 1. + /// + private uint m_bad_seq; + + /// + /// sequ. packets till source is valid. + /// + //private uint m_probation; + + /// + /// packets received. + /// + private uint m_received; + + /// + /// packet expected at last interval. + /// + private ulong m_expected_prior; + + /// + /// packet received at last interval. + /// + private uint m_received_prior; + + /// + /// relative trans time for prev pkt. + /// + private uint m_transit; + + /// + /// Estimated jitter. + /// + private uint m_jitter; + + /// + /// Received last SR packet timestamp. + /// + private ReceivedSRTimestamp? m_receivedLSRTimestamp; + + /// + /// Creates a new Reception Report object. + /// + /// The synchronisation source this reception report is for. + public ReceptionReport(uint ssrc) { - //private const int MAX_DROPOUT = 3000; - //private const int MAX_MISORDER = 100; - //private const int MIN_SEQUENTIAL = 2; - private const int RTP_SEQ_MOD = 1 << 16; - //private const int MAX_POSITIVE_LOSS = 0x7fffff; - //private const int MAX_NEGATIVE_LOSS = 0x800000; - private const int SEQ_NUM_WRAP_LOW = 256; - private const int SEQ_NUM_WRAP_HIGH = 65280; - - /// - /// Data source being reported. - /// - public uint SSRC; - - /// - /// highest seq. number seen - /// - private ushort m_max_seq; - - /// - /// Increments by UInt16.MaxValue each time the sequence number wraps around. - /// - private ulong m_cycles; - - /// - /// The first sequence number received. - /// - private uint m_base_seq; - - /// - /// last 'bad' seq number + 1. - /// - private uint m_bad_seq; - - /// - /// sequ. packets till source is valid. - /// - //private uint m_probation; - - /// - /// packets received. - /// - private uint m_received; - - /// - /// packet expected at last interval. - /// - private ulong m_expected_prior; - - /// - /// packet received at last interval. - /// - private uint m_received_prior; - - /// - /// relative trans time for prev pkt. - /// - private uint m_transit; - - /// - /// Estimated jitter. - /// - private uint m_jitter; - - /// - /// Received last SR packet timestamp. - /// - private ReceivedSRTimestamp m_receivedLSRTimestamp = null; - - /// - /// Creates a new Reception Report object. - /// - /// The synchronisation source this reception report is for. - public ReceptionReport(uint ssrc) + SSRC = ssrc; + } + + /// + /// Updates the state when an RTCP sender report is received from the remote party. + /// + /// The sender report timestamp. + internal void RtcpSenderReportReceived(ulong srNtpTimestamp) + { + System.Threading.Interlocked.Exchange(ref m_receivedLSRTimestamp, + new ReceivedSRTimestamp + { + NTP = (uint)((srNtpTimestamp >> 16) & 0xFFFFFFFF), + ReceivedAt = DateTime.Now + }); + } + + /// + /// Carries out the calculations required to measure properties related to the reception of + /// received RTP packets. The algorithms employed are: + /// - RFC3550 A.1 RTP Data Header Validity Checks (for sequence number calculations). + /// - RFC3550 A.3 Determining Number of Packets Expected and Lost. + /// - RFC3550 A.8 Estimating the Interarrival Jitter. + /// + /// The sequence number in the RTP header. + /// The timestamp in the RTP header. + /// The current timestamp in the SAME units as the RTP timestamp. + /// For example for 8Khz audio the arrival timestamp needs 8000 ticks per second. + internal void RtpPacketReceived(ushort seq, uint rtpTimestamp, uint arrivalTimestamp) + { + // Sequence number calculations and cycles as per RFC3550 Appendix A.1. + //if (m_received == 0) + //{ + // init_seq(seq); + // m_max_seq = (ushort)(seq - 1); + // m_probation = MIN_SEQUENTIAL; + //} + //bool ready = update_seq(seq); + + if (m_received == 0) { - SSRC = ssrc; + m_base_seq = seq; } - /// - /// Updates the state when an RTCP sender report is received from the remote party. - /// - /// The sender report timestamp. - internal void RtcpSenderReportReceived(ulong srNtpTimestamp) + m_received++; + + if (seq == m_max_seq + 1) { - System.Threading.Interlocked.Exchange(ref m_receivedLSRTimestamp, - new ReceivedSRTimestamp - { - NTP = (uint)((srNtpTimestamp >> 16) & 0xFFFFFFFF), - ReceivedAt = DateTime.Now - }); + // Packet is in sequence. + m_max_seq = seq; } - - /// - /// Carries out the calculations required to measure properties related to the reception of - /// received RTP packets. The algorithms employed are: - /// - RFC3550 A.1 RTP Data Header Validity Checks (for sequence number calculations). - /// - RFC3550 A.3 Determining Number of Packets Expected and Lost. - /// - RFC3550 A.8 Estimating the Interarrival Jitter. - /// - /// The sequence number in the RTP header. - /// The timestamp in the RTP header. - /// The current timestamp in the SAME units as the RTP timestamp. - /// For example for 8Khz audio the arrival timestamp needs 8000 ticks per second. - internal void RtpPacketReceived(ushort seq, uint rtpTimestamp, uint arrivalTimestamp) + else if (seq == 0 && m_max_seq == ushort.MaxValue) { - // Sequence number calculations and cycles as per RFC3550 Appendix A.1. - //if (m_received == 0) - //{ - // init_seq(seq); - // m_max_seq = (ushort)(seq - 1); - // m_probation = MIN_SEQUENTIAL; - //} - //bool ready = update_seq(seq); - - if (m_received == 0) - { - m_base_seq = seq; - } - - m_received++; - - if (seq == m_max_seq + 1) + // Packet is in sequence and a wrap around has occurred. + m_max_seq = seq; + m_cycles += RTP_SEQ_MOD; + } + else + { + // Out of order, duplicate or skipped sequence number. + if (seq > m_max_seq) { - // Packet is in sequence. + // Seqnum is greater than expected. RTP packet is dropped or out of order. m_max_seq = seq; } - else if (seq == 0 && m_max_seq == ushort.MaxValue) + else if (seq < SEQ_NUM_WRAP_LOW && m_max_seq > SEQ_NUM_WRAP_HIGH) { - // Packet is in sequence and a wrap around has occurred. + // Seqnum is out of order and has wrapped. m_max_seq = seq; m_cycles += RTP_SEQ_MOD; } else { - // Out of order, duplicate or skipped sequence number. - if (seq > m_max_seq) - { - // Seqnum is greater than expected. RTP packet is dropped or out of order. - m_max_seq = seq; - } - else if (seq < SEQ_NUM_WRAP_LOW && m_max_seq > SEQ_NUM_WRAP_HIGH) - { - // Seqnum is out of order and has wrapped. - m_max_seq = seq; - m_cycles += RTP_SEQ_MOD; - } - else - { - // Remaining conditions are: - // - seqnum == m_max_seq indicating a duplicate RTP packet, or - // - is seqnum is more than 1 less than m_max_seqnum. Which most - // likely indicates an RTP packet was delivered out of order. - m_bad_seq++; - } - } - - // Estimating the Interarrival Jitter as defined in RFC3550 Appendix A.8. - uint transit = arrivalTimestamp - rtpTimestamp; - int d = (int)(transit - m_transit); - m_transit = transit; - if (d < 0) - { - d = -d; + // Remaining conditions are: + // - seqnum == m_max_seq indicating a duplicate RTP packet, or + // - is seqnum is more than 1 less than m_max_seqnum. Which most + // likely indicates an RTP packet was delivered out of order. + m_bad_seq++; } - m_jitter += (uint)(d - ((m_jitter + 8) >> 4)); - - //return ready; } - /// - /// Gets a point in time sample for the reception report. - /// - /// A reception report sample. - public ReceptionReportSample GetSample(uint ntpTimestampNow) + // Estimating the Interarrival Jitter as defined in RFC3550 Appendix A.8. + uint transit = arrivalTimestamp - rtpTimestamp; + int d = (int)(transit - m_transit); + m_transit = transit; + if (d < 0) { - // Determining the number of packets expected and lost in RFC3550 Appendix A.3. - ulong extended_max = m_cycles + m_max_seq; - ulong expected = extended_max - m_base_seq + 1; - //int lost = (m_received == 0) ? 0 : (int)(expected - m_received); - - ulong expected_interval = expected - m_expected_prior; - m_expected_prior = expected; - uint received_interval = m_received - m_received_prior; - m_received_prior = m_received; - ulong lost_interval = (m_received == 0) ? 0 : expected_interval - received_interval; - byte fraction = (byte)((expected_interval == 0 || lost_interval <= 0) ? 0 : (lost_interval << 8) / expected_interval); - - // In this case, the estimate is sampled for the reception report as: - uint jitter = m_jitter >> 4; - - var receivedLSRTimestamp = m_receivedLSRTimestamp; - uint delay = receivedLSRTimestamp == null || receivedLSRTimestamp.ReceivedAt == DateTime.MinValue ? - 0 : ntpTimestampNow - RTCPSession.DateTimeToNtpTimestamp32(receivedLSRTimestamp.ReceivedAt); - - return new ReceptionReportSample(SSRC, fraction, (int)lost_interval, m_max_seq, jitter, receivedLSRTimestamp?.NTP ?? 0, delay); + d = -d; } + m_jitter += (uint)(d - ((m_jitter + 8) >> 4)); - /// - /// NOTE 20 Dec 2020: This algorigthm. from RFC3550 Appendix A.1 is intended as part of determining when a new - /// RTP source should be accepted as valid. The intention is not necessarily to be used to determine when - /// a reception report can be generated, which was wat it was being used for here. - /// - /// Initialises the sequence number state for the reception RTP stream. - /// This method is from RFC3550 Appendix A.1 "RTP Data Header Validity Checks". - /// - /// The sequence number from the received RTP packet that triggered this update. - //void init_seq(ushort seq) - //{ - // m_base_seq = seq; - // m_max_seq = seq; - // m_bad_seq = RTP_SEQ_MOD + 1; /* so seq == bad_seq is false */ - // m_cycles = 0; - // m_received = 0; - // m_received_prior = 0; - // m_expected_prior = 0; - //} - - /// - /// NOTE 20 Dec 2020: This algorigthm. from RFC3550 Appendix A.1 is intended to decide when a new RTP - /// source should be accepted as valid. The intention is not necessarily to be used to determine when - /// a reception report can be generated, which was wat it was being used for here. - /// - /// Update the sequence number state for the reception RTP stream. - /// This method is from RFC3550 Appendix A.1 "RTP Data Header Validity Checks". - /// - /// The sequence number from the received RTP packet that triggered this update. - /// True when the required number of packets have been received and a report can be generated. False - /// indicates not yet enough data. - //bool update_seq(ushort seq) - //{ - // ushort udelta = (ushort)(seq - m_max_seq); - - // /* - // * Source is not valid until MIN_SEQUENTIAL packets with - // * sequential sequence numbers have been received. - // */ - // if (m_probation > 0) - // { - // /* packet is in sequence */ - // if (seq == m_max_seq + 1) - // { - // m_probation--; - // m_max_seq = seq; - // if (m_probation == 0) - // { - // init_seq(seq); - // m_received++; - // return false; - // } - // } - // else - // { - // m_probation = MIN_SEQUENTIAL - 1; - // m_max_seq = seq; - // } - // return true; - // } - // else if (udelta < MAX_DROPOUT) - // { - // /* in order, with permissible gap */ - // if (seq < m_max_seq) - // { - // /* - // * Sequence number wrapped - count another 64K cycle. - // */ - // m_cycles += RTP_SEQ_MOD; - // } - // m_max_seq = seq; - // } - // else if (udelta <= RTP_SEQ_MOD - MAX_MISORDER) - // { - // /* the sequence number made a very large jump */ - // if (seq == m_bad_seq) - // { - // /* - // * Two sequential packets -- assume that the other side - // * restarted without telling us so just re-sync - // * (i.e., pretend this was the first packet). - // */ - // init_seq(seq); - // } - // else - // { - // m_bad_seq = (uint)((seq + 1) & (RTP_SEQ_MOD - 1)); - // return true; - // } - // } - // else - // { - // /* duplicate or reordered packet */ - // } - // m_received++; - // return false; - //} + //return ready; } - internal class ReceivedSRTimestamp + /// + /// Gets a point in time sample for the reception report. + /// + /// A reception report sample. + public ReceptionReportSample GetSample(uint ntpTimestampNow) { - /// - /// NTP timestamp in sender report packet, in 32bit. - /// - public uint NTP = 0; - - /// - /// Datetime the sender report was received at. - /// - public DateTime ReceivedAt = DateTime.MinValue; + // Determining the number of packets expected and lost in RFC3550 Appendix A.3. + ulong extended_max = m_cycles + m_max_seq; + ulong expected = extended_max - m_base_seq + 1; + //int lost = (m_received == 0) ? 0 : (int)(expected - m_received); + + ulong expected_interval = expected - m_expected_prior; + m_expected_prior = expected; + uint received_interval = m_received - m_received_prior; + m_received_prior = m_received; + ulong lost_interval = (m_received == 0) ? 0 : expected_interval - received_interval; + byte fraction = (byte)((expected_interval == 0 || lost_interval <= 0) ? 0 : (lost_interval << 8) / expected_interval); + + // In this case, the estimate is sampled for the reception report as: + uint jitter = m_jitter >> 4; + + var receivedLSRTimestamp = m_receivedLSRTimestamp; + uint delay = receivedLSRTimestamp is null || receivedLSRTimestamp.ReceivedAt == DateTime.MinValue ? + 0 : ntpTimestampNow - RTCPSession.DateTimeToNtpTimestamp32(receivedLSRTimestamp.ReceivedAt); + + return new ReceptionReportSample(SSRC, fraction, (int)lost_interval, m_max_seq, jitter, receivedLSRTimestamp?.NTP ?? 0, delay); } + + /// + /// NOTE 20 Dec 2020: This algorigthm. from RFC3550 Appendix A.1 is intended as part of determining when a new + /// RTP source should be accepted as valid. The intention is not necessarily to be used to determine when + /// a reception report can be generated, which was wat it was being used for here. + /// + /// Initialises the sequence number state for the reception RTP stream. + /// This method is from RFC3550 Appendix A.1 "RTP Data Header Validity Checks". + /// + /// The sequence number from the received RTP packet that triggered this update. + //void init_seq(ushort seq) + //{ + // m_base_seq = seq; + // m_max_seq = seq; + // m_bad_seq = RTP_SEQ_MOD + 1; /* so seq == bad_seq is false */ + // m_cycles = 0; + // m_received = 0; + // m_received_prior = 0; + // m_expected_prior = 0; + //} + + /// + /// NOTE 20 Dec 2020: This algorigthm. from RFC3550 Appendix A.1 is intended to decide when a new RTP + /// source should be accepted as valid. The intention is not necessarily to be used to determine when + /// a reception report can be generated, which was wat it was being used for here. + /// + /// Update the sequence number state for the reception RTP stream. + /// This method is from RFC3550 Appendix A.1 "RTP Data Header Validity Checks". + /// + /// The sequence number from the received RTP packet that triggered this update. + /// True when the required number of packets have been received and a report can be generated. False + /// indicates not yet enough data. + //bool update_seq(ushort seq) + //{ + // ushort udelta = (ushort)(seq - m_max_seq); + + // /* + // * Source is not valid until MIN_SEQUENTIAL packets with + // * sequential sequence numbers have been received. + // */ + // if (m_probation > 0) + // { + // /* packet is in sequence */ + // if (seq == m_max_seq + 1) + // { + // m_probation--; + // m_max_seq = seq; + // if (m_probation == 0) + // { + // init_seq(seq); + // m_received++; + // return false; + // } + // } + // else + // { + // m_probation = MIN_SEQUENTIAL - 1; + // m_max_seq = seq; + // } + // return true; + // } + // else if (udelta < MAX_DROPOUT) + // { + // /* in order, with permissible gap */ + // if (seq < m_max_seq) + // { + // /* + // * Sequence number wrapped - count another 64K cycle. + // */ + // m_cycles += RTP_SEQ_MOD; + // } + // m_max_seq = seq; + // } + // else if (udelta <= RTP_SEQ_MOD - MAX_MISORDER) + // { + // /* the sequence number made a very large jump */ + // if (seq == m_bad_seq) + // { + // /* + // * Two sequential packets -- assume that the other side + // * restarted without telling us so just re-sync + // * (i.e., pretend this was the first packet). + // */ + // init_seq(seq); + // } + // else + // { + // m_bad_seq = (uint)((seq + 1) & (RTP_SEQ_MOD - 1)); + // return true; + // } + // } + // else + // { + // /* duplicate or reordered packet */ + // } + // m_received++; + // return false; + //} +} + +internal sealed class ReceivedSRTimestamp +{ + /// + /// NTP timestamp in sender report packet, in 32bit. + /// + public uint NTP; + + /// + /// Datetime the sender report was received at. + /// + public DateTime ReceivedAt = DateTime.MinValue; } diff --git a/src/SIPSorcery/net/RTP/NetRtpLoggingExtensions.cs b/src/SIPSorcery/net/RTP/NetRtpLoggingExtensions.cs new file mode 100644 index 0000000000..cd63aa83ab --- /dev/null +++ b/src/SIPSorcery/net/RTP/NetRtpLoggingExtensions.cs @@ -0,0 +1,664 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using SIPSorcery.Sys; +using SIPSorceryMedia.Abstractions; + +namespace SIPSorcery.Net; + +internal static partial class NetRtpLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionNoLocalMedia", + Level = LogLevel.Warning, + Message = "No local media tracks available for create offer.")] + public static partial void LogRtpSessionNoLocalMedia( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionSetRemoteTrackSsrc", + Level = LogLevel.Debug, + Message = "Set remote track ({MediaType} - index={Index}) SSRC to {SyncSource}.")] + public static partial void LogRtpSessionSetRemoteTrackSsrc( + this ILogger logger, + SDPMediaTypesEnum mediaType, + int index, + uint syncSource); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionEndPointSwitched", + Level = LogLevel.Debug, + Message = "{MediaType} end point switched for RTP ssrc {Ssrc} from {ExpectedEndPoint} to {ReceivedOnEndPoint}.")] + public static partial void LogRtpSessionEndPointSwitched( + this ILogger logger, + SDPMediaTypesEnum mediaType, + uint ssrc, + IPEndPoint expectedEndPoint, + IPEndPoint receivedOnEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionUnrecognisedEndPoint", + Level = LogLevel.Warning, + Message = "RTP packet with SSRC {Ssrc} received from unrecognised end point {ReceivedOnEndPoint}.")] + public static partial void LogRtpSessionUnrecognisedEndPoint( + this ILogger logger, + uint ssrc, + IPEndPoint receivedOnEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionSecureContextNotReady", + Level = LogLevel.Warning, + Message = "RTP or RTCP packet received before secure context ready.")] + public static partial void LogRtpSessionSecureContextNotReady( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionNoMatchingSsrc", + Level = LogLevel.Warning, + Message = "Could not match an RTCP packet against any SSRC's in the session.")] + public static partial void LogRtpSessionNoMatchingSsrc( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionSendRtpRawOnClosedSession", + Level = LogLevel.Warning, + Message = "SendRtpRaw was called for a {MediaType} packet on a closed RTP session.")] + public static partial void LogRtpSessionSendRtpRawOnClosedSession( + this ILogger logger, + SDPMediaTypesEnum mediaType); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionSendRtpRawNoLocalTrack", + Level = LogLevel.Warning, + Message = "SendRtpRaw was called for a {MediaType} packet on an RTP session without a local track.")] + public static partial void LogRtpSessionSendRtpRawNoLocalTrack( + this ILogger logger, + SDPMediaTypesEnum mediaType); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionSendRtpRawInactiveStream", + Level = LogLevel.Warning, + Message = "SendRtpRaw was called for a {MediaType} packet on an RTP session with a Stream Status set to {StreamStatus}")] + public static partial void LogRtpSessionSendRtpRawInactiveStream( + this ILogger logger, + SDPMediaTypesEnum mediaType, + MediaStreamStatusEnum streamStatus); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionSendRtpPacketSecureContextNotReady", + Level = LogLevel.Warning, + Message = "SendRtpPacket cannot be called on a secure session before calling SetSecurityContext.")] + public static partial void LogRtpSessionSendRtpPacketSecureContextNotReady( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionSecureContextAlreadyExists", + Level = LogLevel.Warning, + Message = "Secure context already exists for {MediaType}.")] + public static partial void LogRtpSessionSecureContextAlreadyExists( + this ILogger logger, + SDPMediaTypesEnum mediaType); + + [LoggerMessage( + EventId = 0, + EventName = "RtpChannelClosing", + Level = LogLevel.Debug, + Message = "RTPChannel closing, RTP receiver on port {RTPPort}, Control receiver on port {ControlPort}. Reason: {CloseReason}.")] + public static partial void LogRtpChannelClosing( + this ILogger logger, + int rtpPort, + int controlPort, + string closeReason); + + [LoggerMessage( + EventId = 0, + EventName = "RtpChannelClosingRtpOnly", + Level = LogLevel.Debug, + Message = "RTPChannel closing, RTP receiver on port {RTPPort}. Reason: {CloseReason}.")] + public static partial void LogRtpChannelClosingRtpOnly( + this ILogger logger, + int rtpPort, + string closeReason); + + [LoggerMessage( + EventId = 0, + EventName = "RtpChannelStarted", + Level = LogLevel.Debug, + Message = "RTPChannel for {LocalEndPoint} started.")] + public static partial void LogRtpChannelStarted( + this ILogger logger, + EndPoint? localEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "RtpChannelSendRtpPacketProtectionFailed", + Level = LogLevel.Error, + Message = "SendRTPPacket protection failed, result {RtpError}.")] + public static partial void LogRtpChannelSendRtpPacketProtectionFailed( + this ILogger logger, + int rtpError); + + [LoggerMessage( + EventId = 0, + EventName = "RtpChannelSocketException", + Level = LogLevel.Warning, + Message = "EndSendTo Send error: {SocketErrorCode}")] + public static partial void LogRtpChannelSocketError( + this ILogger logger, + SocketError socketErrorCode); + + [LoggerMessage( + EventId = 0, + EventName = "RtpChannelException", + Level = LogLevel.Error, + Message = "Exception RTPChannel EndSendTo. {Message}")] + public static partial void LogRtpChannelException( + this ILogger logger, + string message, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionRemoteControlEndpointSwitched", + Level = LogLevel.Debug, + Message = "{MediaType} control end point switched from {ControlDestinationEndPoint} to {RemoteEndPoint}.")] + public static partial void LogRtpSessionRemoteControlEndpointSwitched( + this ILogger logger, + SDPMediaTypesEnum mediaType, + IPEndPoint? controlDestinationEndPoint, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionRtcpByeReceived", + Level = LogLevel.Debug, + Message = "RTCP BYE received for SSRC {SSRC}, reason {Reason}.")] + public static partial void LogRtpSessionRtcpByeReceived( + this ILogger logger, + uint ssrc, + string? reason); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionRemoteSdpSsrcAttributes", + Level = LogLevel.Debug, + Message = "LogRemoteSDPSsrcAttributes: {RemoteSDPSsrcAttributes}", + SkipEnabledCheck = true)] + private static partial void LogRtpSessionRemoteSdpSsrcAttributesUnchecked( + this ILogger logger, + string remoteSDPSsrcAttributes); + + public static void LogRtpSessionRemoteSdpSsrcAttributes( + this ILogger logger, + List> audioRemoteSDPSsrcAttributes, + List> videoRemoteSDPSsrcAttributes, + List> textRemoteSDPSsrcAttributes) + { + if (!logger.IsEnabled(LogLevel.Debug)) + { + return; + } + + using var builder = new ValueStringBuilder(); + + builder.Append("Audio:[ "); + foreach (var audioRemoteSDPSsrcAttribute in audioRemoteSDPSsrcAttributes) + { + foreach (var attr in audioRemoteSDPSsrcAttribute) + { + builder.Append(attr.SSRC); + builder.Append(" - "); + } + } + builder.Append("] \r\n Video: [ "); + foreach (var videoRemoteSDPSsrcAttribute in videoRemoteSDPSsrcAttributes) + { + builder.Append(" ["); + foreach (var attr in videoRemoteSDPSsrcAttribute) + { + builder.Append(attr.SSRC); + builder.Append(" - "); + } + builder.Append("] "); + } + builder.Append("] \r\n Text: [ "); + foreach (var textRemoteSDPSsrcAttribute in textRemoteSDPSsrcAttributes) + { + builder.Append(" ["); + foreach (var attr in textRemoteSDPSsrcAttribute) + { + builder.Append(attr.SSRC); + builder.Append(" - "); + } + builder.Append("] "); + } + builder.Append(" ]"); + + logger.LogRtpSessionRemoteSdpSsrcAttributesUnchecked(builder.ToString()); + } + + [LoggerMessage( + EventId = 0, + EventName = "RtpVideoCodecDepacketiserSet", + Level = LogLevel.Debug, + Message = "Video depacketisation codec set to {Codec} for SSRC {SSRC}.", + SkipEnabledCheck = true)] + private static partial void LogRtpVideoCodecDepacketiserSetUnchecked( + this ILogger logger, + VideoCodecsEnum codec, + uint ssrc); + + public static void LogRtpVideoCodecDepacketiserSet( + this ILogger logger, + SDPAudioVideoMediaFormat format, + uint ssrc) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + LogRtpVideoCodecDepacketiserSetUnchecked(logger, format.ToVideoFormat().Codec, ssrc); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "RtpVideoCodecNotImplemented", + Level = LogLevel.Warning, + Message = "Video depacketisation logic for codec {CodecName} has not been implemented, PR's welcome!", + SkipEnabledCheck = true)] + private static partial void LogRtpVideoCodecNotImplementedUnchecked( + this ILogger logger, + string? codecName); + + public static void LogRtpVideoCodecNotImplemented( + this ILogger logger, + SDPAudioVideoMediaFormat format) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + LogRtpVideoCodecNotImplementedUnchecked(logger, format.Name()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "RtpSequenceNumberJumped", + Level = LogLevel.Warning, + Message = "{TrackType} stream sequence number jumped from {LastRemoteSeqNum} to {SequenceNumber}.")] + public static partial void LogRtpSequenceNumberJumped( + this ILogger logger, + string trackType, + ushort lastRemoteSeqNum, + ushort sequenceNumber); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSocketException", + Level = LogLevel.Warning, + Message = "Socket error {SocketErrorCode} in UdpReceiver.BeginReceiveFrom. {Message}")] + public static partial void LogRtpSocketException( + this ILogger logger, + SocketError socketErrorCode, + string message); + + [LoggerMessage( + EventId = 0, + EventName = "RtpDuplicateSeqNum", + Level = LogLevel.Information, + Message = "Duplicate seq number: {sequenceNumber}")] + public static partial void LogRtpDuplicateSeqNum( + this ILogger logger, + ushort sequenceNumber); + + [LoggerMessage( + EventId = 0, + EventName = "SendDtmfEventInProgress", + Level = LogLevel.Warning, + Message = "SendDtmfEvent an RTPEvent is already in progress.")] + public static partial void LogDtmfEventInProgress( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SendDtmfEventCancelled", + Level = LogLevel.Warning, + Message = "SendDtmfEvent was cancelled by caller.")] + public static partial void LogDtmfEventCancelled( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpVideoFramerError", + Level = LogLevel.Warning, + Message = "Discarding RTP packet, VP8 header Start bit not set.")] + public static partial void LogRtpVideoFramerError( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSocketExceptionSendDtmfEvent", + Level = LogLevel.Error, + Message = "SocketException SendDtmfEvent. {errorMessage}")] + public static partial void LogRtpSocketExceptionSendDtmfEvent( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSocketExceptionSendAudioFrame", + Level = LogLevel.Error, + Message = "SocketException SendAudioFrame. {errorMessage}")] + public static partial void LogRtpSocketExceptionSendAudioFrame( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSocketExceptionSendJpegFrame", + Level = LogLevel.Error, + Message = "SocketException SendJpegFrame. {ErrorMessage}")] + public static partial void LogRtpSocketExceptionSendJpegFrame( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSocketExceptionSendVp8Frame", + Level = LogLevel.Error, + Message = "SocketException SendVp8Frame. {errorMessage}")] + public static partial void LogRtpSocketExceptionSendVp8Frame( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSocketExceptionSendAv1Frame", + Level = LogLevel.Error, + Message = "SocketException SendAv1Frame. {errorMessage}")] + public static partial void LogRtpSocketExceptionSendAv1Frame( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSocketExceptionSendVp9Frame", + Level = LogLevel.Error, + Message = "SocketException SendVp9Frame. {errorMessage}")] + public static partial void LogRtpSocketExceptionSendVp9Frame( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpMaximumBandwidthRemoteTrack", + Level = LogLevel.Warning, + Message = "The maximum bandwith cannot be set for remote tracks.")] + public static partial void LogRtpMaximumBandwidthRemoteTrack( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpUnsupportedVideoFormat", + Level = LogLevel.Error, + Message = "Unsupported video format selected {formatName}.")] + public static partial void LogRtpUnsupportedVideoFormat( + this ILogger logger, + string formatName); + + [LoggerMessage( + EventId = 0, + EventName = "RtpCannotCreateRtcpCompoundPacket", + Level = LogLevel.Warning, + Message = "Can't create RTCPCompoundPacket from the provided RTCP bytes.")] + public static partial void LogRtpCannotCreateRtcpCompoundPacket( + this ILogger logger, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSrtpRtcpUnprotectFailed", + Level = LogLevel.Warning, + Message = "SRTP unprotect failed for {mediaType}, result {result}.")] + public static partial void LogRtpSrtpRtcpUnprotectFailed( + this ILogger logger, + SDPMediaTypesEnum mediaType, + int result); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSrtpRtcpProtectFailed", + Level = LogLevel.Warning, + Message = "SRTP RTCP packet protection failed, result {rtpError}.")] + public static partial void LogRtpSrtpRtcpProtectFailed( + this ILogger logger, + int rtpError); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSrtpReportNotReady", + Level = LogLevel.Warning, + Message = "SendRtcpReport cannot be called on a secure session before calling SetSecurityContext.")] + public static partial void LogRtpSrtpReportNotReady( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpInvalidPortNumber", + Level = LogLevel.Warning, + Message = "Remote {sdpMediaType} announcement contained an invalid port number {port}.")] + public static partial void LogRtpInvalidPortNumber( + this ILogger logger, + SDPMediaTypesEnum sdpMediaType, + int port); + + [LoggerMessage( + EventId = 0, + EventName = "RtpDestinationAddressInvalid", + Level = LogLevel.Warning, + Message = "The destination address for Send in RTPChannel cannot be {address}.")] + public static partial void LogRtpDestinationAddressInvalid( + this ILogger logger, + IPAddress address); + + [LoggerMessage( + EventId = 0, + EventName = "RtpFailedToParseReport", + Level = LogLevel.Warning, + Message = "Failed to parse RTCP compound report.")] + public static partial void LogRtpFailedToParseReport( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpPacketWithSsrcNotMatched", + Level = LogLevel.Warning, + Message = "An RTP packet with SSRC {syncSource} and payload ID {payloadType} was received that could not be matched to an audio or video stream.")] + public static partial void LogRtpPacketWithSsrcNotMatched( + this ILogger logger, + uint syncSource, + int payloadType); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionException", + Level = LogLevel.Error, + Message = "Exception in RTPSession SetRemoteDescription. {errorMessage}")] + public static partial void LogRtpSessionException( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionClose", + Level = LogLevel.Error, + Message = "Exception RTPChannel.Close. {errorMessage}")] + public static partial void LogRtpSessionClose( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSecureMediaInvalidTransport", + Level = LogLevel.Error, + Message = "Error negotiating secure media. Invalid Transport {transport}.")] + public static partial void LogRtpSecureMediaInvalidTransport( + this ILogger logger, + string transport); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSecureMediaIncompatibleCrypto", + Level = LogLevel.Error, + Message = "Error negotiating secure media for type {mediaType}. Incompatible crypto parameter.")] + public static partial void LogRtpSecureMediaIncompatibleCrypto( + this ILogger logger, + SDPMediaTypesEnum mediaType); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSecureMediaNoCompatibleCrypto", + Level = LogLevel.Error, + Message = "Error negotiating secure media. No compatible crypto suite.")] + public static partial void LogRtpSecureMediaNoCompatibleCrypto( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpPacketBeforeContextReady", + Level = LogLevel.Warning, + Message = "RTP or RTCP packet received before secure context ready.")] + public static partial void LogRtpPacketBeforeContextReady( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSecureContextError", + Level = LogLevel.Warning, + Message = "SRTCP unprotect failed for {mediaType} track, result {result}.")] + public static partial void LogRtpSecureContextError( + this ILogger logger, + SDPMediaTypesEnum? mediaType, + int result); + + [LoggerMessage( + EventId = 0, + EventName = "RtpChannelGeneralException", + Level = LogLevel.Error, + Message = "Exception RTPChannel.Send.")] + public static partial void LogRtpChannelGeneralException( + this ILogger logger, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpChannelBeginReceiveError", + Level = LogLevel.Error, + Message = "Exception UdpReceiver.BeginReceiveFrom. {errorMessage}")] + public static partial void LogRtpChannelBeginReceiveError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpEndReceiveFromError", + Level = LogLevel.Error, + Message = "Exception UdpReceiver.EndReceiveFrom. {errorMessage}")] + public static partial void LogRtpEndReceiveFromError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtpUnknownVideoCodec", + Level = LogLevel.Warning, + Message = "rtp unknown video, seqnum {sequenceNumber}, ts {timestamp}, marker {markerBit}, payload {payloadLength}.")] + public static partial void LogRtpUnknownVideo( + this ILogger logger, + ushort sequenceNumber, + uint timestamp, + int markerBit, + int payloadLength); + + [LoggerMessage( + EventId = 0, + EventName = "RtpEventInProgress", + Level = LogLevel.Warning, + Message = "An RTPEvent is in progress.")] + public static partial void LogRtpEventInProgress( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SendT140FrameSocketError", + Level = LogLevel.Error, + Message = "SocketException SendT140Frame. {ErrorMessage}")] + public static partial void LogSendT140FrameSocketError( + this ILogger logger, + string errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "SendMJEPGFrameSocketError", + Level = LogLevel.Error, + Message = "SocketException SendMJEPGFrame. {ErrorMessage}")] + public static partial void LogSendMJEPGFrameSocketError( + this ILogger logger, + string errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "TurnRelaySendError", + Level = LogLevel.Warning, + Message = "{Caller} error sending TURN relay request to TURN server at {RelayEndPoint}. {SendResult}.")] + public static partial void LogTurnRelaySendError( + this ILogger logger, + string caller, + IPEndPoint relayEndPoint, + SocketError sendResult); + + [LoggerMessage( + EventId = 0, + EventName = "RtpSessionRtcpPacketReceived", + Level = LogLevel.Trace, + Message = "RTCP packet received from {RemoteEndPoint} {Buffer}", + SkipEnabledCheck = true)] + private static partial void LogRtpSessionRtcpPacketReceivedUnchecked( + this ILogger logger, + IPEndPoint remoteEndPoint, + string buffer); + + public static void LogRtpSessionRtcpPacketReceived( + this ILogger logger, + IPEndPoint remoteEndPoint, + ReadOnlyMemory buffer) + { + if (logger.IsEnabled(LogLevel.Trace)) + { + LogRtpSessionRtcpPacketReceivedUnchecked(logger, remoteEndPoint, buffer.Span.HexStr()); + } + } +} diff --git a/src/SIPSorcery/net/RTP/Ntp64Timestamp.cs b/src/SIPSorcery/net/RTP/Ntp64Timestamp.cs index f041aca795..7610e4fe08 100644 --- a/src/SIPSorcery/net/RTP/Ntp64Timestamp.cs +++ b/src/SIPSorcery/net/RTP/Ntp64Timestamp.cs @@ -1,58 +1,51 @@ using System; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public record struct TimestampPair(uint RtpTimestamp, ulong NtpTimestamp); + +public static class Ntp64Timestamp { - public class TimestampPair + public static ulong AddFraction(ulong timestamp, double fraction) { + var fractionalPart = GetFraction(timestamp); + fractionalPart += ((uint)(fraction * uint.MaxValue)) & 0x00000000FFFFFFFF; + return (timestamp & 0xFFFFFFFF00000000) | fractionalPart; + } + + public static ulong AddSeconds(ulong timestamp, uint seconds) { + var sec = (ulong)GetSeconds(timestamp) + seconds; + return (sec << 32) | (timestamp & 0x00000000FFFFFFFF); + } + + public static uint GetSeconds(ulong timestamp) { + return (uint)((timestamp & 0xFFFFFFFF00000000) >> 32); + } + + public static uint GetFraction(ulong timestamp) { - public uint RtpTimestamp { get; set; } - public ulong NtpTimestamp { get; set; } + return (uint)(timestamp & 0x00000000FFFFFFFF); + } + + public static DateTime GetDatetime(ulong timestamp) { + var epochStart = new DateTime(1900, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var millis = ((double)GetFraction(timestamp) * 1000 / (double)uint.MaxValue); + var time = epochStart + TimeSpan.FromSeconds(GetSeconds(timestamp)) + TimeSpan.FromMilliseconds(millis); + return time; } - public class Ntp64Timestamp + public static ulong InterpolateNtpTime(TimestampPair lastTimestampPair, uint currentRtpTimestamp, int clockrate) { - public static ulong AddFraction(ulong timestamp, double fraction) { - var fractionalPart = GetFraction(timestamp); - fractionalPart += ((uint) (fraction * uint.MaxValue)) & 0x00000000FFFFFFFF; - return (timestamp & 0xFFFFFFFF00000000) | fractionalPart; - } - - public static ulong AddSeconds(ulong timestamp, uint seconds) { - var sec = (ulong) GetSeconds(timestamp) + seconds; - return (sec << 32) | (timestamp & 0x00000000FFFFFFFF); - } - - public static uint GetSeconds(ulong timestamp) { - return (uint)((timestamp & 0xFFFFFFFF00000000) >> 32); - } - - public static uint GetFraction(ulong timestamp) - { - return (uint)(timestamp & 0x00000000FFFFFFFF); - } - - public static DateTime GetDatetime(ulong timestamp) { - var epochStart = new DateTime(1900, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - var millis = ((double)GetFraction(timestamp) * 1000 / (double)uint.MaxValue); - var time = epochStart + TimeSpan.FromSeconds(GetSeconds(timestamp)) + TimeSpan.FromMilliseconds(millis); - return time; - } - - public static ulong InterpolateNtpTime(TimestampPair lastTimestampPair, uint currentRtpTimestamp, int clockrate) - { - var diff = currentRtpTimestamp - lastTimestampPair.RtpTimestamp; - var diffTime = diff / (double)clockrate; - var seconds = Math.Truncate(diffTime); - var fractionalPart = (diffTime - seconds); - - - - var withSeconds = AddSeconds(lastTimestampPair.NtpTimestamp, (uint)seconds); - return AddFraction(withSeconds, fractionalPart); - } - - public static DateTime InterpolateDatetime(TimestampPair lastTimestampPair, uint currentRtpTimestamp, int clockrate) { - var time = InterpolateNtpTime(lastTimestampPair, currentRtpTimestamp, clockrate); - return GetDatetime(time); - } + var diff = currentRtpTimestamp - lastTimestampPair.RtpTimestamp; + var diffTime = diff / (double)clockrate; + var seconds = Math.Truncate(diffTime); + var fractionalPart = (diffTime - seconds); + + var withSeconds = AddSeconds(lastTimestampPair.NtpTimestamp, (uint)seconds); + return AddFraction(withSeconds, fractionalPart); + } + + public static DateTime InterpolateDatetime(TimestampPair lastTimestampPair, uint currentRtpTimestamp, int clockrate) { + var time = InterpolateNtpTime(lastTimestampPair, currentRtpTimestamp, clockrate); + return GetDatetime(time); } } diff --git a/src/SIPSorcery/net/RTP/Packetisation/AV1Depacketiser.cs b/src/SIPSorcery/net/RTP/Packetisation/AV1Depacketiser.cs index 77aacbed9d..b1c2b30754 100644 --- a/src/SIPSorcery/net/RTP/Packetisation/AV1Depacketiser.cs +++ b/src/SIPSorcery/net/RTP/Packetisation/AV1Depacketiser.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: AV1Depacketiser.cs // // Description: Reassembles RTP payloads using the AV1 RTP payload format. @@ -17,86 +17,102 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; using System.IO; namespace SIPSorcery.Net; +/// +/// Reassembles RTP payloads using the AV1 RTP payload format defined by +/// the Alliance for Open Media. +/// public class AV1Depacketiser { private const byte Z_MASK = 0x80; private const byte Y_MASK = 0x40; private const byte N_MASK = 0x08; - private uint _previousTimestamp; - private readonly List> _temporaryRtpPayloads = new List>(); - private readonly MemoryStream _fragmentedObu = new MemoryStream(); + private static readonly Comparison<(int sequenceNumber, Range range)> s_sequenceNumberComparison = + (a, b) => (Math.Abs(b.sequenceNumber - a.sequenceNumber) > (0xFFFF - 2000)) + ? -a.sequenceNumber.CompareTo(b.sequenceNumber) + : a.sequenceNumber.CompareTo(b.sequenceNumber); - public virtual MemoryStream ProcessRTPPayload(byte[] rtpPayload, ushort seqNum, uint timestamp, int markerBit, out bool isKeyFrame) + private uint _previousTimestamp; + private readonly List<(int sequenceNumber, Range range)> _temporaryRtpPayloads = new(); + private readonly MemoryStream _payloadBuffer = new(); + private readonly MemoryStream _fragmentedObu = new(); + + /// + /// Processes an RTP payload and writes the completed AV1 frame to the buffer writer + /// when a full frame has been assembled. + /// + /// The buffer writer to receive the reassembled frame. + /// The RTP payload bytes. + /// The RTP sequence number. + /// The RTP timestamp. + /// The RTP marker bit (1 = last packet of frame). + /// Set to true when the assembled frame is a key frame. + /// true when a complete frame has been written to . + public virtual bool ProcessRTPPayload(IBufferWriter bufferWriter, ReadOnlySpan rtpPayload, ushort seqNum, uint timestamp, int markerBit, out bool isKeyFrame) { if (_previousTimestamp != timestamp && _previousTimestamp > 0) { - _temporaryRtpPayloads.Clear(); + ClearPayloads(); _previousTimestamp = 0; _fragmentedObu.SetLength(0); } - _temporaryRtpPayloads.Add(new KeyValuePair(seqNum, rtpPayload)); + var payloadOffset = (int)_payloadBuffer.Length; + _payloadBuffer.Write(rtpPayload); + _temporaryRtpPayloads.Add((seqNum, payloadOffset..(payloadOffset + rtpPayload.Length))); if (markerBit == 1) { if (_temporaryRtpPayloads.Count > 1) { - _temporaryRtpPayloads.Sort((a, b) => - (Math.Abs(b.Key - a.Key) > (0xFFFF - 2000)) ? -a.Key.CompareTo(b.Key) : a.Key.CompareTo(b.Key)); + _temporaryRtpPayloads.Sort(s_sequenceNumberComparison); } - byte[] frame = ProcessAV1PayloadFrame(_temporaryRtpPayloads, out isKeyFrame); - _temporaryRtpPayloads.Clear(); + var payloadSpan = _payloadBuffer.GetBuffer().AsSpan(0, (int)_payloadBuffer.Length); + var hasFrame = ProcessAV1PayloadFrame(bufferWriter, payloadSpan, _temporaryRtpPayloads, out isKeyFrame); + ClearPayloads(); _previousTimestamp = 0; _fragmentedObu.SetLength(0); - if (frame == null) - { - return null; - } - - var frameStream = new MemoryStream(frame.Length); - frameStream.Write(frame, 0, frame.Length); - frameStream.Position = 0; - return frameStream; + return hasFrame; } isKeyFrame = false; _previousTimestamp = timestamp; - return null; + return false; } - protected virtual byte[] ProcessAV1PayloadFrame(List> rtpPayloads, out bool isKeyFrame) + private bool ProcessAV1PayloadFrame(IBufferWriter bufferWriter, ReadOnlySpan payloadBuffer, List<(int sequenceNumber, Range range)> rtpPayloads, out bool isKeyFrame) { - var obuElements = new List(); + var hasOutput = false; isKeyFrame = false; - foreach (var rtpPayload in rtpPayloads) + foreach (var entry in rtpPayloads) { - var payload = rtpPayload.Value; - if (payload == null || payload.Length == 0) + var payload = payloadBuffer[entry.range]; + + if (payload.IsEmpty) { continue; } - bool z = (payload[0] & Z_MASK) != 0; - bool y = (payload[0] & Y_MASK) != 0; - int w = (payload[0] >> 4) & 0x03; - bool n = (payload[0] & N_MASK) != 0; + var z = (payload[0] & Z_MASK) != 0; + var y = (payload[0] & Y_MASK) != 0; + var w = (payload[0] >> 4) & 0x03; + var n = (payload[0] & N_MASK) != 0; if (n) { isKeyFrame = true; } - var packetElements = ParseObuElements(payload, w); - AddPacketElements(packetElements, z, y, obuElements); + hasOutput |= ParseAndProcessObuElements(bufferWriter, payload, w, z, y); } if (_fragmentedObu.Length > 0) @@ -104,134 +120,166 @@ protected virtual byte[] ProcessAV1PayloadFrame(List> _fragmentedObu.SetLength(0); } - if (obuElements.Count == 0) - { - return null; - } - - int totalLength = 0; - for (int i = 0; i < obuElements.Count; i++) - { - totalLength += obuElements[i].Length; - } - - var frame = new byte[totalLength]; - int offset = 0; - for (int i = 0; i < obuElements.Count; i++) - { - Buffer.BlockCopy(obuElements[i], 0, frame, offset, obuElements[i].Length); - offset += obuElements[i].Length; - } + return hasOutput; + } - return frame; + private void ClearPayloads() + { + _payloadBuffer.SetLength(0); + _temporaryRtpPayloads.Clear(); } - private List ParseObuElements(byte[] payload, int w) + /// + /// Parses OBU element boundaries from the RTP payload and writes completed OBUs + /// directly to using the z/y fragmentation flags. + /// + /// true if any completed OBU was written. + private bool ParseAndProcessObuElements(IBufferWriter bufferWriter, ReadOnlySpan payload, int w, bool z, bool y) { - var obuElements = new List(); - int offset = 1; + // Phase 1: Parse element boundaries on the stack (no heap allocation). + // w is a 2-bit field (0–3). For w==0, count is variable but bounded by payload size. + // REVIEW: If a payload contains more than 16 OBU elements (w==0 path), elements + // beyond the 16th are silently dropped. This is unlikely in practice but could + // cause data loss with unusual encoders. + Span elemOffsets = stackalloc int[16]; + Span elemLengths = stackalloc int[16]; + var elemCount = 0; + var offset = 1; if (w == 0) { - while (offset < payload.Length) + while (offset < payload.Length && elemCount < elemOffsets.Length) { - if (!AV1Packetiser.TryReadLeb128(payload, ref offset, out int obuElementLength, out _)) + if (!TryReadLeb128Span(payload, ref offset, out var len) || offset + len > payload.Length) { break; } - if (offset + obuElementLength > payload.Length) - { - break; - } - - var obuElement = new byte[obuElementLength]; - Buffer.BlockCopy(payload, offset, obuElement, 0, obuElementLength); - offset += obuElementLength; - obuElements.Add(obuElement); + elemOffsets[elemCount] = offset; + elemLengths[elemCount] = len; + elemCount++; + offset += len; } } else { - for (int elementIndex = 0; elementIndex < w && offset < payload.Length; elementIndex++) + for (var i = 0; i < w && offset < payload.Length && elemCount < elemOffsets.Length; i++) { - int obuElementLength; - if (elementIndex == w - 1) + int len; + if (i == w - 1) { - obuElementLength = payload.Length - offset; + len = payload.Length - offset; } - else if (!AV1Packetiser.TryReadLeb128(payload, ref offset, out obuElementLength, out _)) + else if (!TryReadLeb128Span(payload, ref offset, out len)) { break; } - if (offset + obuElementLength > payload.Length) + if (offset + len > payload.Length) { break; } - var obuElement = new byte[obuElementLength]; - Buffer.BlockCopy(payload, offset, obuElement, 0, obuElementLength); - offset += obuElementLength; - obuElements.Add(obuElement); + elemOffsets[elemCount] = offset; + elemLengths[elemCount] = len; + elemCount++; + offset += len; } } - return obuElements; - } - - private void AddPacketElements(List packetElements, bool z, bool y, List completedObus) - { - if (packetElements.Count == 0) + if (elemCount == 0) { - return; + return false; } - int startIndex = 0; - int endExclusive = packetElements.Count; + // Phase 2: Apply z/y fragmentation flags directly from payload slices. + var wrote = false; + var startIndex = 0; + var endExclusive = elemCount; if (z) { - _fragmentedObu.Write(packetElements[0], 0, packetElements[0].Length); + var first = payload.Slice(elemOffsets[0], elemLengths[0]); + _fragmentedObu.Write(first); - if (!(y && packetElements.Count == 1)) + if (!(y && elemCount == 1)) { - AddCompletedObu(_fragmentedObu.ToArray(), completedObus); + wrote |= WriteCompletedObu(bufferWriter, _fragmentedObu.GetBuffer().AsSpan(0, (int)_fragmentedObu.Length)); _fragmentedObu.SetLength(0); } startIndex = 1; } - if (y && packetElements.Count > startIndex) + if (y && elemCount > startIndex) { - endExclusive = packetElements.Count - 1; + endExclusive = elemCount - 1; } - for (int i = startIndex; i < endExclusive; i++) + for (var i = startIndex; i < endExclusive; i++) { - AddCompletedObu(packetElements[i], completedObus); + wrote |= WriteCompletedObu(bufferWriter, payload.Slice(elemOffsets[i], elemLengths[i])); } - if (y && packetElements.Count > startIndex) + if (y && elemCount > startIndex) + { + var last = payload.Slice(elemOffsets[elemCount - 1], elemLengths[elemCount - 1]); + _fragmentedObu.Write(last); + } + + return wrote; + } + + /// + /// Reads a LEB128-encoded unsigned integer from a span. + /// + /// + /// REVIEW: The loop limit of 8 bytes can shift beyond 32 bits (shift reaches 35 on the + /// 6th byte), overflowing the accumulator. Cap at 5 iterations for + /// 32-bit safety, or use if larger values are needed. + /// + private static bool TryReadLeb128Span(ReadOnlySpan buffer, ref int offset, out int value) + { + value = 0; + var shift = 0; + var bytesRead = 0; + + while (offset < buffer.Length && bytesRead < 8) { - byte[] lastElement = packetElements[packetElements.Count - 1]; - _fragmentedObu.Write(lastElement, 0, lastElement.Length); + var current = buffer[offset++]; + bytesRead++; + value |= (current & 0x7f) << shift; + + if ((current & 0x80) == 0) + { + return true; + } + + shift += 7; } + + value = 0; + return false; } - private static void AddCompletedObu(byte[] obu, List completedObus) + /// + /// Writes a completed OBU directly to the buffer writer, filtering out + /// TemporalDelimiter and TileList OBU types. + /// + private static bool WriteCompletedObu(IBufferWriter bufferWriter, ReadOnlySpan obu) { - if (obu == null || obu.Length == 0) + if (obu.IsEmpty) { - return; + return false; } var obuType = AV1Packetiser.GetObuType(obu); - if (obuType != AV1Packetiser.AV1ObuType.TemporalDelimiter && - obuType != AV1Packetiser.AV1ObuType.TileList) + if (obuType is (AV1Packetiser.AV1ObuType.TemporalDelimiter or AV1Packetiser.AV1ObuType.TileList)) { - completedObus.Add(obu); + return false; } + + bufferWriter.Write(obu); + return true; } } diff --git a/src/SIPSorcery/net/RTP/Packetisation/AV1Packetiser.cs b/src/SIPSorcery/net/RTP/Packetisation/AV1Packetiser.cs index 2204fc1036..ba2a0d1cde 100644 --- a/src/SIPSorcery/net/RTP/Packetisation/AV1Packetiser.cs +++ b/src/SIPSorcery/net/RTP/Packetisation/AV1Packetiser.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: AV1Packetiser.cs // // Description: Provides AV1 RTP packetisation helpers. @@ -21,6 +21,19 @@ namespace SIPSorcery.Net; +/// +/// Provides AV1 RTP packetisation helpers based on the Alliance for Open Media +/// RTP Payload Format for AV1. +/// +/// +/// +/// var packets = AV1Packetiser.Packetize(temporalUnit, 1200); +/// foreach (var pkt in packets) +/// { +/// // send pkt.Payload via RTP, set marker bit when pkt.IsLast is true +/// } +/// +/// public class AV1Packetiser { public const int AV1_AGGREGATION_HEADER_LENGTH = 1; @@ -47,284 +60,303 @@ public enum AV1ObuType : byte Padding = 15 } - public struct AV1RtpPacket + public readonly struct AV1RtpPacket { - public byte[] Payload { get; } + public ReadOnlyMemory Payload { get; } public bool IsLast { get; } - public AV1RtpPacket(byte[] payload, bool isLast) + public AV1RtpPacket(ReadOnlyMemory payload, bool isLast) { Payload = payload; IsLast = isLast; } } - public static List Packetize(byte[] temporalUnit, int maxPayloadSize) + /// + /// Packetises an AV1 temporal unit into one or more RTP packets. + /// + /// The AV1 temporal unit (sequence of OBUs) to packetise. + /// The maximum RTP payload size in bytes. + /// A list of RTP packets ready for transmission. + public static List Packetize(ReadOnlySpan temporalUnit, int maxPayloadSize) + { + if (temporalUnit.IsEmpty) { - if (temporalUnit == null || temporalUnit.Length == 0) - { - return new List(); - } + return []; + } - if (maxPayloadSize <= AV1_AGGREGATION_HEADER_LENGTH + 2) - { - throw new ArgumentException("The maximum RTP payload size is too small for AV1 packetisation.", nameof(maxPayloadSize)); - } + if (maxPayloadSize <= AV1_AGGREGATION_HEADER_LENGTH + 2) + { + throw new ArgumentException("The maximum RTP payload size is too small for AV1 packetisation.", nameof(maxPayloadSize)); + } - var packets = new List(); - var transmittedObus = new List(); + var packets = new List(); + var obus = ParseObus(temporalUnit); + obus.RemoveAll(static obu => ShouldSkipObu(GetObuType(obu))); - foreach (var obu in ParseObus(temporalUnit)) - { - if (!ShouldSkipObu(GetObuType(obu))) - { - transmittedObus.Add(obu); - } - } + if (obus.Count == 0) + { + return packets; + } - if (transmittedObus.Count == 0) - { - return packets; - } + var startsCodedVideoSequence = GetObuType(obus[0]) == AV1ObuType.SequenceHeader; + var packetStartIdx = 0; + var currentPacketSize = AV1_AGGREGATION_HEADER_LENGTH; - bool startsCodedVideoSequence = GetObuType(transmittedObus[0]) == AV1ObuType.SequenceHeader; - var currentPacketObus = new List(); - int currentPacketSize = AV1_AGGREGATION_HEADER_LENGTH; + for (var i = 0; i < obus.Count; i++) + { + var obu = obus[i]; + var obuCost = GetLeb128Length(obu.Length) + obu.Length; - for (int i = 0; i < transmittedObus.Count; i++) + if (obuCost + AV1_AGGREGATION_HEADER_LENGTH > maxPayloadSize) { - var obu = transmittedObus[i]; - int obuCost = GetLeb128Size(obu.Length) + obu.Length; - - if (obuCost + AV1_AGGREGATION_HEADER_LENGTH > maxPayloadSize) + if (i > packetStartIdx) { - if (currentPacketObus.Count > 0) - { - packets.Add(CreatePacket(currentPacketObus, false, false, startsCodedVideoSequence && packets.Count == 0, false)); - currentPacketObus.Clear(); - currentPacketSize = AV1_AGGREGATION_HEADER_LENGTH; - } - - foreach (var fragment in FragmentObu(obu, maxPayloadSize, startsCodedVideoSequence && packets.Count == 0)) - { - packets.Add(fragment); - } - - continue; + packets.Add(CreatePacket(obus, packetStartIdx, i - packetStartIdx, false, false, startsCodedVideoSequence && packets.Count == 0, false)); } - if (currentPacketSize + obuCost > maxPayloadSize) - { - packets.Add(CreatePacket(currentPacketObus, false, false, startsCodedVideoSequence && packets.Count == 0, false)); - currentPacketObus = new List(); - currentPacketSize = AV1_AGGREGATION_HEADER_LENGTH; - } + FragmentObu(obu, maxPayloadSize, startsCodedVideoSequence && packets.Count == 0, packets); - currentPacketObus.Add(obu); - currentPacketSize += obuCost; + packetStartIdx = i + 1; + currentPacketSize = AV1_AGGREGATION_HEADER_LENGTH; + continue; } - if (currentPacketObus.Count > 0) + if (currentPacketSize + obuCost > maxPayloadSize) { - packets.Add(CreatePacket(currentPacketObus, false, false, startsCodedVideoSequence && packets.Count == 0, false)); + packets.Add(CreatePacket(obus, packetStartIdx, i - packetStartIdx, false, false, startsCodedVideoSequence && packets.Count == 0, false)); + packetStartIdx = i; + currentPacketSize = AV1_AGGREGATION_HEADER_LENGTH; } - if (packets.Count > 0) - { - packets[packets.Count - 1] = new AV1RtpPacket(packets[packets.Count - 1].Payload, true); - } + currentPacketSize += obuCost; + } - return packets; + if (obus.Count > packetStartIdx) + { + packets.Add(CreatePacket(obus, packetStartIdx, obus.Count - packetStartIdx, false, false, startsCodedVideoSequence && packets.Count == 0, false)); } - public static IEnumerable ParseObus(byte[] temporalUnit) + if (packets.Count > 0) { - if (temporalUnit == null) - { - yield break; - } + packets[packets.Count - 1] = new AV1RtpPacket(packets[packets.Count - 1].Payload, true); + } - int offset = 0; - while (offset < temporalUnit.Length) - { - int obuStart = offset; - byte obuHeader = temporalUnit[offset++]; - bool hasExtension = (obuHeader & OBU_EXTENSION_FLAG_MASK) != 0; - bool hasSizeField = (obuHeader & OBU_HAS_SIZE_FIELD_MASK) != 0; + return packets; + } - if (hasExtension) - { - if (offset >= temporalUnit.Length) - { - throw new ApplicationException("The AV1 OBU extension header was truncated."); - } + /// + /// Parses the OBUs from an AV1 temporal unit byte stream. + /// + public static List ParseObus(ReadOnlySpan temporalUnit) + { + var obus = new List(); - offset++; - } + if (temporalUnit.IsEmpty) + { + return obus; + } - if (!hasSizeField) - { - var finalObu = new byte[temporalUnit.Length - obuStart]; - Buffer.BlockCopy(temporalUnit, obuStart, finalObu, 0, finalObu.Length); - yield return finalObu; - yield break; - } + var offset = 0; + while (offset < temporalUnit.Length) + { + var obuStart = offset; + var obuHeader = temporalUnit[offset++]; + var hasExtension = (obuHeader & OBU_EXTENSION_FLAG_MASK) != 0; + var hasSizeField = (obuHeader & OBU_HAS_SIZE_FIELD_MASK) != 0; - if (!TryReadLeb128(temporalUnit, ref offset, out int obuPayloadSize, out _)) + if (hasExtension) + { + if (offset >= temporalUnit.Length) { - throw new ApplicationException("The AV1 OBU size field could not be parsed."); + throw new SipSorceryException("The AV1 OBU extension header was truncated."); } - if (offset + obuPayloadSize > temporalUnit.Length) - { - throw new ApplicationException("The AV1 OBU payload exceeded the source buffer."); - } + offset++; + } - int totalObuLength = (offset - obuStart) + obuPayloadSize; - var obu = new byte[totalObuLength]; - Buffer.BlockCopy(temporalUnit, obuStart, obu, 0, totalObuLength); + if (!hasSizeField) + { + obus.Add(temporalUnit.Slice(obuStart).ToArray()); + return obus; + } - offset += obuPayloadSize; - yield return obu; + if (!TryReadLeb128(temporalUnit, ref offset, out var obuPayloadSize, out _)) + { + throw new SipSorceryException("The AV1 OBU size field could not be parsed."); } - } - public static AV1ObuType GetObuType(byte[] obu) - { - if (obu == null || obu.Length == 0) + if (offset + obuPayloadSize > temporalUnit.Length) { - return AV1ObuType.Reserved; + throw new SipSorceryException("The AV1 OBU payload exceeded the source buffer."); } - return (AV1ObuType)((obu[0] & OBU_TYPE_MASK) >> OBU_TYPE_SHIFT); + var totalObuLength = (offset - obuStart) + obuPayloadSize; + obus.Add(temporalUnit.Slice(obuStart, totalObuLength).ToArray()); + + offset += obuPayloadSize; } - public static bool TryReadLeb128(byte[] buffer, ref int offset, out int value, out int leb128Length) + return obus; + } + + /// + /// Returns the OBU type from the first byte of an OBU. + /// + public static AV1ObuType GetObuType(ReadOnlySpan obu) + { + if (obu.IsEmpty) { - value = 0; - leb128Length = 0; - int shift = 0; + return AV1ObuType.Reserved; + } - while (offset < buffer.Length && leb128Length < 8) - { - byte current = buffer[offset++]; - leb128Length++; - value |= (current & 0x7f) << shift; + return (AV1ObuType)((obu[0] & OBU_TYPE_MASK) >> OBU_TYPE_SHIFT); + } - if ((current & 0x80) == 0) - { - return true; - } + /// + /// Tries to read a LEB128-encoded unsigned integer from the buffer. + /// + /// + /// REVIEW: The loop limit of 8 bytes can shift beyond 32 bits (shift reaches 35 on the + /// 6th byte), overflowing the accumulator. Cap at 5 iterations for + /// 32-bit safety, or use if larger values are needed. + /// + public static bool TryReadLeb128(ReadOnlySpan buffer, ref int offset, out int value, out int leb128Length) + { + value = 0; + leb128Length = 0; + var shift = 0; - shift += 7; + while (offset < buffer.Length && leb128Length < 8) + { + var current = buffer[offset++]; + leb128Length++; + value |= (current & 0x7f) << shift; + + if ((current & 0x80) == 0) + { + return true; } - value = 0; - return false; + shift += 7; } - public static byte[] WriteLeb128(int value) - { - if (value < 0) - { - throw new ArgumentException("AV1 leb128 values must be non-negative.", nameof(value)); - } + value = 0; + return false; + } - var bytes = new List(); - int remainder = value; + /// + /// Returns the number of bytes needed to encode a value as LEB128. + /// + public static int GetLeb128Length(int value) + { + var size = 1; + var remainder = value >> 7; + while (remainder > 0) + { + size++; + remainder >>= 7; + } - do - { - byte current = (byte)(remainder & 0x7f); - remainder >>= 7; - if (remainder > 0) - { - current |= 0x80; - } + return size; + } - bytes.Add(current); - } while (remainder > 0); + /// + /// Writes a non-negative integer as a LEB128 byte sequence into the destination span. + /// + /// The span to write the encoded bytes into. + /// The non-negative value to encode. + /// The number of bytes written. + public static int WriteLeb128(Span destination, int value) + { + ArgumentOutOfRangeException.ThrowIfNegative(value); - return bytes.ToArray(); - } + var remainder = value; + var written = 0; - private static IEnumerable FragmentObu(byte[] obu, int maxPayloadSize, bool startsCodedVideoSequence) + do { - int payloadCapacity = maxPayloadSize - AV1_AGGREGATION_HEADER_LENGTH; - int offset = 0; - bool isFirstFragment = true; - - while (offset < obu.Length) + var current = (byte)(remainder & 0x7f); + remainder >>= 7; + if (remainder > 0) { - int fragmentLength = GetMaxFragmentLength(obu.Length - offset, payloadCapacity); - var fragment = new byte[fragmentLength]; - Buffer.BlockCopy(obu, offset, fragment, 0, fragmentLength); + current |= 0x80; + } - bool z = !isFirstFragment; - bool y = offset + fragmentLength < obu.Length; - bool n = startsCodedVideoSequence && isFirstFragment; + destination[written++] = current; + } while (remainder > 0); - yield return CreatePacket(new List { fragment }, z, y, n, false); + return written; + } - offset += fragmentLength; - isFirstFragment = false; - } - } + private static void FragmentObu(ReadOnlySpan obu, int maxPayloadSize, bool startsCodedVideoSequence, List packets) + { + var payloadCapacity = maxPayloadSize - AV1_AGGREGATION_HEADER_LENGTH; + var offset = 0; + var isFirstFragment = true; - private static AV1RtpPacket CreatePacket(List obuElements, bool z, bool y, bool n, bool isLast) + while (offset < obu.Length) { - int payloadLength = AV1_AGGREGATION_HEADER_LENGTH; - for (int i = 0; i < obuElements.Count; i++) - { - payloadLength += GetLeb128Size(obuElements[i].Length) + obuElements[i].Length; - } + var fragmentLength = GetMaxFragmentLength(obu.Length - offset, payloadCapacity); - var payload = new byte[payloadLength]; + var z = !isFirstFragment; + var y = offset + fragmentLength < obu.Length; + var n = startsCodedVideoSequence && isFirstFragment; + + var leb128Len = GetLeb128Length(fragmentLength); + var payload = new byte[AV1_AGGREGATION_HEADER_LENGTH + leb128Len + fragmentLength]; payload[0] = (byte)((z ? Z_MASK : 0) | (y ? Y_MASK : 0) | (n ? N_MASK : 0)); + WriteLeb128(payload.AsSpan(1), fragmentLength); + obu.Slice(offset, fragmentLength).CopyTo(payload.AsSpan(1 + leb128Len)); - int dstOffset = 1; - for (int i = 0; i < obuElements.Count; i++) - { - var leb128 = WriteLeb128(obuElements[i].Length); - Buffer.BlockCopy(leb128, 0, payload, dstOffset, leb128.Length); - dstOffset += leb128.Length; + packets.Add(new AV1RtpPacket(payload, false)); - Buffer.BlockCopy(obuElements[i], 0, payload, dstOffset, obuElements[i].Length); - dstOffset += obuElements[i].Length; - } + offset += fragmentLength; + isFirstFragment = false; + } + } - return new AV1RtpPacket(payload, isLast); + private static AV1RtpPacket CreatePacket(List obus, int start, int count, bool z, bool y, bool n, bool isLast) + { + var payloadLength = AV1_AGGREGATION_HEADER_LENGTH; + var end = start + count; + for (var i = start; i < end; i++) + { + payloadLength += GetLeb128Length(obus[i].Length) + obus[i].Length; } - private static bool ShouldSkipObu(AV1ObuType obuType) => - obuType == AV1ObuType.TemporalDelimiter || obuType == AV1ObuType.TileList; + var payload = new byte[payloadLength]; + payload[0] = (byte)((z ? Z_MASK : 0) | (y ? Y_MASK : 0) | (n ? N_MASK : 0)); - private static int GetLeb128Size(int value) + var dstOffset = 1; + for (var i = start; i < end; i++) { - int size = 1; - int remainder = value >> 7; - while (remainder > 0) - { - size++; - remainder >>= 7; - } + var leb128Written = WriteLeb128(payload.AsSpan(dstOffset), obus[i].Length); + dstOffset += leb128Written; - return size; + obus[i].AsSpan().CopyTo(payload.AsSpan(dstOffset)); + dstOffset += obus[i].Length; } - private static int GetMaxFragmentLength(int remainingBytes, int payloadCapacity) - { - int fragmentLength = Math.Min(remainingBytes, payloadCapacity - 1); - while (fragmentLength > 0 && fragmentLength + GetLeb128Size(fragmentLength) > payloadCapacity) - { - fragmentLength--; - } + return new AV1RtpPacket(payload, isLast); + } - if (fragmentLength <= 0) - { - throw new ApplicationException("Unable to fit an AV1 OBU fragment into the RTP payload."); - } + private static bool ShouldSkipObu(AV1ObuType obuType) => + obuType is AV1ObuType.TemporalDelimiter or AV1ObuType.TileList; - return fragmentLength; + private static int GetMaxFragmentLength(int remainingBytes, int payloadCapacity) + { + var fragmentLength = Math.Min(remainingBytes, payloadCapacity - 1); + while (fragmentLength > 0 && fragmentLength + GetLeb128Length(fragmentLength) > payloadCapacity) + { + fragmentLength--; + } + + if (fragmentLength <= 0) + { + throw new SipSorceryException("Unable to fit an AV1 OBU fragment into the RTP payload."); } + + return fragmentLength; + } } diff --git a/src/SIPSorcery/net/RTP/Packetisation/H264Depacketiser.cs b/src/SIPSorcery/net/RTP/Packetisation/H264Depacketiser.cs index d073fe3899..49355b55f2 100644 --- a/src/SIPSorcery/net/RTP/Packetisation/H264Depacketiser.cs +++ b/src/SIPSorcery/net/RTP/Packetisation/H264Depacketiser.cs @@ -1,261 +1,262 @@  using System; +using System.Buffers; using System.Collections.Generic; using System.IO; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// Based in https://github.com/BogdanovKirill/RtspClientSharp/blob/master/RtspClientSharp/MediaParsers/H264VideoPayloadParser.cs +/// Distributed under MIT License +/// +/// @author raf.csoares@kyubinteractive.com +/// +public class H264Depacketiser { - /// - /// Based in https://github.com/BogdanovKirill/RtspClientSharp/blob/master/RtspClientSharp/MediaParsers/H264VideoPayloadParser.cs - /// Distributed under MIT License - /// - /// @author raf.csoares@kyubinteractive.com - /// - public class H264Depacketiser + private const int SPS = 7; + private const int PPS = 8; + private const int IDR_SLICE = 1; + private const int NON_IDR_SLICE = 5; + + //Payload Helper Fields + private uint previous_timestamp; + private int norm, fu_a, fu_b, stap_a, stap_b, mtap16, mtap24; // used for diagnostics stats + private List> temporary_rtp_payloads = new List>(); // used to assemble the RTP packets that form one RTP Frame + private MemoryStream fragmented_nal = new MemoryStream(); // used to concatenate fragmented H264 NALs where NALs are splitted over RTP packets + + public virtual bool ProcessRTPPayload(IBufferWriter bufferWriter, ReadOnlySpan rtpPayload, ushort seqNum, uint timestamp, int markbit, out bool isKeyFrame) { - const int SPS = 7; - const int PPS = 8; - const int IDR_SLICE = 1; - const int NON_IDR_SLICE = 5; - - //Payload Helper Fields - uint previous_timestamp = 0; - int norm, fu_a, fu_b, stap_a, stap_b, mtap16, mtap24 = 0; // used for diagnostics stats - List> temporary_rtp_payloads = new List>(); // used to assemble the RTP packets that form one RTP Frame - MemoryStream fragmented_nal = new MemoryStream(); // used to concatenate fragmented H264 NALs where NALs are splitted over RTP packets - - public virtual MemoryStream ProcessRTPPayload(byte[] rtpPayload, ushort seqNum, uint timestamp, int markbit, out bool isKeyFrame) - { - List nal_units = ProcessRTPPayloadAsNals(rtpPayload, seqNum, timestamp, markbit, out isKeyFrame); + var nal_units = ProcessRTPPayloadAsNals(rtpPayload, seqNum, timestamp, markbit, out isKeyFrame); - if (nal_units != null) - { - //Calculate total buffer size - long totalBufferSize = 0; - for (int i = 0; i < nal_units.Count; i++) - { - var nal = nal_units[i]; - long remaining = nal.Length; + if (nal_units is null) + { + return false; + } - if (remaining > 0) - { - totalBufferSize += remaining + 4; //nal + 0001 - } - else - { - nal_units.RemoveAt(i); - i--; - } - } + //Calculate total buffer size + long totalBufferSize = 0; + for (var i = 0; i < nal_units.Count; i++) + { + var nal = nal_units[i]; + long remaining = nal.Length; - //Merge nals in same buffer using Annex-B separator (0001) - MemoryStream data = new MemoryStream(new byte[totalBufferSize]); - foreach (var nal in nal_units) - { - data.WriteByte(0); - data.WriteByte(0); - data.WriteByte(0); - data.WriteByte(1); - data.Write(nal, 0, nal.Length); - } - return data; + if (remaining > 0) + { + totalBufferSize += remaining + 4; //nal + 0001 + } + else + { + nal_units.RemoveAt(i); + i--; } - return null; } - public virtual List ProcessRTPPayloadAsNals(byte[] rtpPayload, ushort seqNum, uint timestamp, int markbit, out bool isKeyFrame) + //Merge nals in same buffer using Annex-B separator (0001) + var data = new MemoryStream(new byte[totalBufferSize]); + foreach (var nal in nal_units) { - List nal_units = ProcessH264Payload(rtpPayload, seqNum, timestamp, markbit, out isKeyFrame); - - return nal_units; + bufferWriter.Write(new ReadOnlySpan([0, 0, 0, 1])); + bufferWriter.Write(nal.AsSpan()); } - protected virtual List ProcessH264Payload(byte[] rtp_payload, ushort seqNum, uint rtp_timestamp, int rtp_marker, out bool isKeyFrame) + return true; + } + + public virtual List? ProcessRTPPayloadAsNals(ReadOnlySpan rtpPayload, ushort seqNum, uint timestamp, int markbit, out bool isKeyFrame) + { + var nal_units = ProcessH264Payload(rtpPayload, seqNum, timestamp, markbit, out isKeyFrame); + + return nal_units; + } + + protected virtual List? ProcessH264Payload(ReadOnlySpan rtp_payload, ushort seqNum, uint rtp_timestamp, int rtp_marker, out bool isKeyFrame) + { + if (previous_timestamp != rtp_timestamp && previous_timestamp > 0) { - if (previous_timestamp != rtp_timestamp && previous_timestamp > 0) - { - temporary_rtp_payloads.Clear(); - previous_timestamp = 0; - fragmented_nal.SetLength(0); - } + temporary_rtp_payloads.Clear(); + previous_timestamp = 0; + fragmented_nal.SetLength(0); + } - // Add to the list of payloads for the current Frame of video - temporary_rtp_payloads.Add(new KeyValuePair(seqNum, rtp_payload)); // TODO could optimise this and go direct to Process Frame if just 1 packet in frame - if (rtp_marker == 1) + // Add to the list of payloads for the current Frame of video + temporary_rtp_payloads.Add(new KeyValuePair(seqNum, rtp_payload.ToArray())); // TODO could optimise this and go direct to Process Frame if just 1 packet in frame + if (rtp_marker == 1) + { + //Reorder to prevent UDP incorrect package order + if (temporary_rtp_payloads.Count > 1) { - //Reorder to prevent UDP incorrect package order - if (temporary_rtp_payloads.Count > 1) + temporary_rtp_payloads.Sort((a, b) => { - temporary_rtp_payloads.Sort((a, b) => { - // Detect wraparound of sequence to sort packets correctly (Assumption that no more then 2000 packets per frame) - return (Math.Abs(b.Key - a.Key) > (0xFFFF - 2000)) ? -a.Key.CompareTo(b.Key) : a.Key.CompareTo(b.Key); - }); - } + // Detect wraparound of sequence to sort packets correctly (Assumption that no more then 2000 packets per frame) + return (Math.Abs(b.Key - a.Key) > (0xFFFF - 2000)) ? -a.Key.CompareTo(b.Key) : a.Key.CompareTo(b.Key); + }); + } - // End Marker is set. Process the list of RTP Packets (forming 1 RTP frame) and save the NALs to a file - List nal_units = ProcessH264PayloadFrame(temporary_rtp_payloads, out isKeyFrame); - temporary_rtp_payloads.Clear(); - previous_timestamp = 0; - fragmented_nal.SetLength(0); + // End Marker is set. Process the list of RTP Packets (forming 1 RTP frame) and save the NALs to a file + var nal_units = ProcessH264PayloadFrame(temporary_rtp_payloads, out isKeyFrame); + temporary_rtp_payloads.Clear(); + previous_timestamp = 0; + fragmented_nal.SetLength(0); - return nal_units; - } - else - { - isKeyFrame = false; - previous_timestamp = rtp_timestamp; - return null; // we don't have a frame yet. Keep accumulating RTP packets - } + return nal_units; + } + else + { + isKeyFrame = false; + previous_timestamp = rtp_timestamp; + return null; // we don't have a frame yet. Keep accumulating RTP packets } + } + + // Process a RTP Frame. A RTP Frame can consist of several RTP Packets which have the same Timestamp + // Returns a list of NAL Units (with no 00 00 00 01 header and with no Size header) + protected virtual List ProcessH264PayloadFrame(List> rtp_payloads, out bool isKeyFrame) + { + bool? isKeyFrameNullable = null; + var nal_units = new List(); // Stores the NAL units for a Video Frame. May be more than one NAL unit in a video frame. - // Process a RTP Frame. A RTP Frame can consist of several RTP Packets which have the same Timestamp - // Returns a list of NAL Units (with no 00 00 00 01 header and with no Size header) - protected virtual List ProcessH264PayloadFrame(List> rtp_payloads, out bool isKeyFrame) + for (var payload_index = 0; payload_index < rtp_payloads.Count; payload_index++) { - bool? isKeyFrameNullable = null; - List nal_units = new List(); // Stores the NAL units for a Video Frame. May be more than one NAL unit in a video frame. + // Examine the first rtp_payload and the first byte (the NAL header) + var nal_header_f_bit = (rtp_payloads[payload_index].Value[0] >> 7) & 0x01; + var nal_header_nri = (rtp_payloads[payload_index].Value[0] >> 5) & 0x03; + var nal_header_type = (rtp_payloads[payload_index].Value[0] >> 0) & 0x1F; + + // If the Nal Header Type is in the range 1..23 this is a normal NAL (not fragmented) + // So write the NAL to the file + if (nal_header_type is >= 1 and <= 23) + { + norm++; + //Check if is Key Frame + CheckKeyFrame(nal_header_type, ref isKeyFrameNullable); - for (int payload_index = 0; payload_index < rtp_payloads.Count; payload_index++) + nal_units.Add(rtp_payloads[payload_index].Value); + } + // There are 4 types of Aggregation Packet (split over RTP payloads) + else if (nal_header_type == 24) { - // Examine the first rtp_payload and the first byte (the NAL header) - int nal_header_f_bit = (rtp_payloads[payload_index].Value[0] >> 7) & 0x01; - int nal_header_nri = (rtp_payloads[payload_index].Value[0] >> 5) & 0x03; - int nal_header_type = (rtp_payloads[payload_index].Value[0] >> 0) & 0x1F; - - // If the Nal Header Type is in the range 1..23 this is a normal NAL (not fragmented) - // So write the NAL to the file - if (nal_header_type >= 1 && nal_header_type <= 23) - { - norm++; - //Check if is Key Frame - CheckKeyFrame(nal_header_type, ref isKeyFrameNullable); + stap_a++; - nal_units.Add(rtp_payloads[payload_index].Value); - } - // There are 4 types of Aggregation Packet (split over RTP payloads) - else if (nal_header_type == 24) + // RTP packet contains multiple NALs, each with a 16 bit header + // Read 16 byte size + // Read NAL + try { - stap_a++; - - // RTP packet contains multiple NALs, each with a 16 bit header - // Read 16 byte size - // Read NAL - try - { - int ptr = 1; // start after the nal_header_type which was '24' - // if we have at least 2 more bytes (the 16 bit size) then consume more data - while (ptr + 2 < (rtp_payloads[payload_index].Value.Length - 1)) - { - int size = (rtp_payloads[payload_index].Value[ptr] << 8) + (rtp_payloads[payload_index].Value[ptr + 1] << 0); - ptr = ptr + 2; - byte[] nal = new byte[size]; - Buffer.BlockCopy(rtp_payloads[payload_index].Value, ptr, nal, 0, size); // copy the NAL - - byte reconstructed_nal_type = (byte)((nal[0] >> 0) & 0x1F); - //Check if is Key Frame - CheckKeyFrame(reconstructed_nal_type, ref isKeyFrameNullable); - - nal_units.Add(nal); // Add to list of NALs for this RTP frame. Start Codes like 00 00 00 01 get added later - ptr = ptr + size; - } - } - catch + var ptr = 1; // start after the nal_header_type which was '24' + // if we have at least 2 more bytes (the 16 bit size) then consume more data + while (ptr + 2 < (rtp_payloads[payload_index].Value.Length - 1)) { + var size = (rtp_payloads[payload_index].Value[ptr] << 8) + (rtp_payloads[payload_index].Value[ptr + 1] << 0); + ptr = ptr + 2; + var nal = new byte[size]; + Buffer.BlockCopy(rtp_payloads[payload_index].Value, ptr, nal, 0, size); // copy the NAL + + var reconstructed_nal_type = (byte)((nal[0] >> 0) & 0x1F); + //Check if is Key Frame + CheckKeyFrame(reconstructed_nal_type, ref isKeyFrameNullable); + + nal_units.Add(nal); // Add to list of NALs for this RTP frame. Start Codes like 00 00 00 01 get added later + ptr = ptr + size; } } - else if (nal_header_type == 25) + catch { - stap_b++; } - else if (nal_header_type == 26) - { - mtap16++; - } - else if (nal_header_type == 27) - { - mtap24++; - } - else if (nal_header_type == 28) - { - fu_a++; + } + else if (nal_header_type == 25) + { + stap_b++; + } + else if (nal_header_type == 26) + { + mtap16++; + } + else if (nal_header_type == 27) + { + mtap24++; + } + else if (nal_header_type == 28) + { + fu_a++; - // Parse Fragmentation Unit Header - int fu_indicator = rtp_payloads[payload_index].Value[0]; - int fu_header_s = (rtp_payloads[payload_index].Value[1] >> 7) & 0x01; // start marker - int fu_header_e = (rtp_payloads[payload_index].Value[1] >> 6) & 0x01; // end marker - int fu_header_r = (rtp_payloads[payload_index].Value[1] >> 5) & 0x01; // reserved. should be 0 - int fu_header_type = (rtp_payloads[payload_index].Value[1] >> 0) & 0x1F; // Original NAL unit header + // Parse Fragmentation Unit Header + int fu_indicator = rtp_payloads[payload_index].Value[0]; + var fu_header_s = (rtp_payloads[payload_index].Value[1] >> 7) & 0x01; // start marker + var fu_header_e = (rtp_payloads[payload_index].Value[1] >> 6) & 0x01; // end marker + var fu_header_r = (rtp_payloads[payload_index].Value[1] >> 5) & 0x01; // reserved. should be 0 + var fu_header_type = (rtp_payloads[payload_index].Value[1] >> 0) & 0x1F; // Original NAL unit header - // Check Start and End flags - if (fu_header_s == 1 && fu_header_e == 0) - { - // Start of Fragment. - // Initialise the fragmented_nal byte array - // Build the NAL header with the original F and NRI flags but use the the Type field from the fu_header_type - byte reconstructed_nal_type = (byte)((nal_header_f_bit << 7) + (nal_header_nri << 5) + fu_header_type); + // Check Start and End flags + if (fu_header_s == 1 && fu_header_e == 0) + { + // Start of Fragment. + // Initialise the fragmented_nal byte array + // Build the NAL header with the original F and NRI flags but use the the Type field from the fu_header_type + var reconstructed_nal_type = (byte)((nal_header_f_bit << 7) + (nal_header_nri << 5) + fu_header_type); - // Empty the stream - fragmented_nal.SetLength(0); + // Empty the stream + fragmented_nal.SetLength(0); - // Add reconstructed_nal_type byte to the memory stream - fragmented_nal.WriteByte((byte)reconstructed_nal_type); + // Add reconstructed_nal_type byte to the memory stream + fragmented_nal.WriteByte((byte)reconstructed_nal_type); - // copy the rest of the RTP payload to the memory stream - fragmented_nal.Write(rtp_payloads[payload_index].Value, 2, rtp_payloads[payload_index].Value.Length - 2); - } + // copy the rest of the RTP payload to the memory stream + fragmented_nal.Write(rtp_payloads[payload_index].Value, 2, rtp_payloads[payload_index].Value.Length - 2); + } - if (fu_header_s == 0 && fu_header_e == 0) - { - // Middle part of Fragment - // Append this payload to the fragmented_nal - // Data starts after the NAL Unit Type byte and the FU Header byte - fragmented_nal.Write(rtp_payloads[payload_index].Value, 2, rtp_payloads[payload_index].Value.Length - 2); - } + if (fu_header_s == 0 && fu_header_e == 0) + { + // Middle part of Fragment + // Append this payload to the fragmented_nal + // Data starts after the NAL Unit Type byte and the FU Header byte + fragmented_nal.Write(rtp_payloads[payload_index].Value, 2, rtp_payloads[payload_index].Value.Length - 2); + } - if (fu_header_s == 0 && fu_header_e == 1) - { - // End part of Fragment - // Append this payload to the fragmented_nal - // Data starts after the NAL Unit Type byte and the FU Header byte - fragmented_nal.Write(rtp_payloads[payload_index].Value, 2, rtp_payloads[payload_index].Value.Length - 2); + if (fu_header_s == 0 && fu_header_e == 1) + { + // End part of Fragment + // Append this payload to the fragmented_nal + // Data starts after the NAL Unit Type byte and the FU Header byte + fragmented_nal.Write(rtp_payloads[payload_index].Value, 2, rtp_payloads[payload_index].Value.Length - 2); - var fragmeted_nal_array = fragmented_nal.ToArray(); - byte reconstructed_nal_type = (byte)((fragmeted_nal_array[0] >> 0) & 0x1F); + var fragmeted_nal_array = fragmented_nal.ToArray(); + var reconstructed_nal_type = (byte)((fragmeted_nal_array[0] >> 0) & 0x1F); - //Check if is Key Frame - CheckKeyFrame(reconstructed_nal_type, ref isKeyFrameNullable); + //Check if is Key Frame + CheckKeyFrame(reconstructed_nal_type, ref isKeyFrameNullable); - // Add the NAL to the array of NAL units - nal_units.Add(fragmeted_nal_array); - fragmented_nal.SetLength(0); - } + // Add the NAL to the array of NAL units + nal_units.Add(fragmeted_nal_array); + fragmented_nal.SetLength(0); } + } - else if (nal_header_type == 29) - { - fu_b++; - } + else if (nal_header_type == 29) + { + fu_b++; } + } - isKeyFrame = isKeyFrameNullable != null ? isKeyFrameNullable.Value : false; + isKeyFrame = isKeyFrameNullable is { } ? isKeyFrameNullable.Value : false; - // Output all the NALs that form one RTP Frame (one frame of video) - return nal_units; - } + // Output all the NALs that form one RTP Frame (one frame of video) + return nal_units; + } - protected void CheckKeyFrame(int nal_type, ref bool? isKeyFrame) + protected void CheckKeyFrame(int nal_type, ref bool? isKeyFrame) + { + if (isKeyFrame is null) { - if (isKeyFrame == null) - { - isKeyFrame = nal_type == SPS || nal_type == PPS ? new bool?(true) : - (nal_type == NON_IDR_SLICE ? new bool?(false) : null); - } - else - { - isKeyFrame = nal_type == SPS || nal_type == PPS ? - (isKeyFrame.Value ? isKeyFrame : new bool?(false)) : - (nal_type == NON_IDR_SLICE ? new bool?(false) : isKeyFrame); - } + isKeyFrame = nal_type is SPS or PPS ? new bool?(true) : + (nal_type == NON_IDR_SLICE ? new bool?(false) : null); + } + else + { + isKeyFrame = nal_type is SPS or PPS ? + (isKeyFrame.Value ? isKeyFrame : new bool?(false)) : + (nal_type == NON_IDR_SLICE ? new bool?(false) : isKeyFrame); } } } diff --git a/src/SIPSorcery/net/RTP/Packetisation/H264Packetiser.cs b/src/SIPSorcery/net/RTP/Packetisation/H264Packetiser.cs index 00aa61f4da..281620c8bc 100644 --- a/src/SIPSorcery/net/RTP/Packetisation/H264Packetiser.cs +++ b/src/SIPSorcery/net/RTP/Packetisation/H264Packetiser.cs @@ -30,157 +30,159 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Collections.Generic; -using System.Linq; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class H264Packetiser { - public class H264Packetiser + public const int H264_RTP_HEADER_LENGTH = 2; + + public struct H264Nal { - public const int H264_RTP_HEADER_LENGTH = 2; + public byte[] NAL { get; } + public bool IsLast { get; } - public struct H264Nal + public H264Nal(byte[] nal, bool isLast) { - public byte[] NAL { get; } - public bool IsLast { get; } - - public H264Nal(byte[] nal, bool isLast) - { - NAL = nal; - IsLast = isLast; - } + NAL = nal; + IsLast = isLast; } + } - public static IEnumerable ParseNals(byte[] accessUnit) - { - int zeroes = 0; + public static IEnumerable ParseNals(ReadOnlySpan accessUnit) + { + var result = new List(); + int zeroes = 0; - // Parse NALs from H264 access unit, encoded as an Annex B bitstream. - // NALs are delimited by 0x000001 or 0x00000001. - int currPosn = 0; - for (int i = 0; i < accessUnit.Length; i++) + // Parse NALs from H264 access unit, encoded as an Annex B bitstream. + // NALs are delimited by 0x000001 or 0x00000001. + int currPosn = 0; + for (int i = 0; i < accessUnit.Length; i++) + { + if (accessUnit[i] == 0x00) { - if (accessUnit[i] == 0x00) - { - zeroes++; - } - else if (accessUnit[i] == 0x01 && zeroes >= 2) + zeroes++; + } + else if (accessUnit[i] == 0x01 && zeroes >= 2) + { + // This is a NAL start sequence. + int nalStart = i + 1; + if (nalStart - currPosn > 4) { - // This is a NAL start sequence. - int nalStart = i + 1; - if (nalStart - currPosn > 4) - { - int endPosn = nalStart - ((zeroes == 2) ? 3 : 4); - int nalSize = endPosn - currPosn; - bool isLast = currPosn + nalSize == accessUnit.Length; - - yield return new H264Nal(accessUnit.Skip(currPosn).Take(nalSize).ToArray(), isLast); - } + int endPosn = nalStart - ((zeroes == 2) ? 3 : 4); + int nalSize = endPosn - currPosn; + bool isLast = currPosn + nalSize == accessUnit.Length; - currPosn = nalStart; - } - else - { - zeroes = 0; + result.Add(new H264Nal(accessUnit.Slice(currPosn, nalSize).ToArray(), isLast)); } - } - if (currPosn < accessUnit.Length) + currPosn = nalStart; + } + else { - yield return new H264Nal(accessUnit.Skip(currPosn).ToArray(), true); + zeroes = 0; } } - /// - /// Constructs the RTP header for an H264 NAL. This method does NOT support - /// aggregation packets where multiple NALs are sent as a single RTP payload. - /// The supported H264 header type is Single-Time Aggregation Packet type A - /// (STAP-A) and Fragmentation Unit A (FU-A). The headers produced correspond - /// to H264 packetization-mode=1. - /// - /// - /// RTP Payload Format for H.264 Video: - /// https://tools.ietf.org/html/rfc6184 - /// - /// FFmpeg H264 RTP packetisation code: - /// https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/rtpenc_h264_hevc.c - /// - /// When the payload size is less than or equal to max RTP payload, send as - /// Single-Time Aggregation Packet (STAP): - /// https://tools.ietf.org/html/rfc6184#section-5.7.1 - /// - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | RTP Header | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// |STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// |F|NRI| Type | | - /// +-+-+-+-+-+-+-+-+ - /// - /// Type = 24 for STAP-A (NOTE: this is the type of the H264 RTP header - /// and NOT the NAL type). - /// - /// When the payload size is greater than max RTP payload, send as - /// Fragmentation Unit A (FU-A): - /// https://tools.ietf.org/html/rfc6184#section-5.8 - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | FU indicator | FU header | | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Fragmentation Unit (FU) Payload - /// | - /// ... - /// - /// - /// The FU indicator octet has the following format: - /// - /// +---------------+ - /// |0|1|2|3|4|5|6|7| - /// +-+-+-+-+-+-+-+-+ - /// |F|NRI| Type | - /// +---------------+ - /// - /// F and NRI bits come from the NAL being transmitted. - /// Type = 28 for FU-A (NOTE: this is the type of the H264 RTP header - /// and NOT the NAL type). - /// - /// The FU header has the following format: - /// - /// +---------------+ - /// |0|1|2|3|4|5|6|7| - /// +-+-+-+-+-+-+-+-+ - /// |S|E|R| Type | - /// +---------------+ - /// - /// S: Set to 1 for the start of the NAL FU (i.e. first packet in frame). - /// E: Set to 1 for the end of the NAL FU (i.e. the last packet in the frame). - /// R: Reserved bit must be 0. - /// Type: The NAL unit payload type, comes from NAL packet (NOTE: this IS the type of the NAL message). - /// - public static byte[] GetH264RtpHeader(byte nal0, bool isFirstPacket, bool isFinalPacket) + if (currPosn < accessUnit.Length) { - byte nalType = (byte)(nal0 & 0x1F); - //byte nalNri = (byte)((nal0 >> 5) & 0x03); + result.Add(new H264Nal(accessUnit.Slice(currPosn).ToArray(), true)); + } - byte firstHdrByte = (byte)(nal0 & 0xE0); // Has either 24 (STAP-A) or 28 (FU-A) added to it. + return result; + } - byte fuIndicator = (byte)(firstHdrByte + 28); - byte fuHeader = nalType; - if (isFirstPacket) - { - fuHeader += 0x80; - } - else if (isFinalPacket) - { - fuHeader += 0x40; - } + /// + /// Constructs the RTP header for an H264 NAL. This method does NOT support + /// aggregation packets where multiple NALs are sent as a single RTP payload. + /// The supported H264 header type is Single-Time Aggregation Packet type A + /// (STAP-A) and Fragmentation Unit A (FU-A). The headers produced correspond + /// to H264 packetization-mode=1. + /// + /// + /// RTP Payload Format for H.264 Video: + /// https://tools.ietf.org/html/rfc6184 + /// + /// FFmpeg H264 RTP packetisation code: + /// https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/rtpenc_h264_hevc.c + /// + /// When the payload size is less than or equal to max RTP payload, send as + /// Single-Time Aggregation Packet (STAP): + /// https://tools.ietf.org/html/rfc6184#section-5.7.1 + /// + /// 0 1 2 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | RTP Header | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// |STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// |F|NRI| Type | | + /// +-+-+-+-+-+-+-+-+ + /// + /// Type = 24 for STAP-A (NOTE: this is the type of the H264 RTP header + /// and NOT the NAL type). + /// + /// When the payload size is greater than max RTP payload, send as + /// Fragmentation Unit A (FU-A): + /// https://tools.ietf.org/html/rfc6184#section-5.8 + /// 0 1 2 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | FU indicator | FU header | | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Fragmentation Unit (FU) Payload + /// | + /// ... + /// + /// + /// The FU indicator octet has the following format: + /// + /// +---------------+ + /// |0|1|2|3|4|5|6|7| + /// +-+-+-+-+-+-+-+-+ + /// |F|NRI| Type | + /// +---------------+ + /// + /// F and NRI bits come from the NAL being transmitted. + /// Type = 28 for FU-A (NOTE: this is the type of the H264 RTP header + /// and NOT the NAL type). + /// + /// The FU header has the following format: + /// + /// +---------------+ + /// |0|1|2|3|4|5|6|7| + /// +-+-+-+-+-+-+-+-+ + /// |S|E|R| Type | + /// +---------------+ + /// + /// S: Set to 1 for the start of the NAL FU (i.e. first packet in frame). + /// E: Set to 1 for the end of the NAL FU (i.e. the last packet in the frame). + /// R: Reserved bit must be 0. + /// Type: The NAL unit payload type, comes from NAL packet (NOTE: this IS the type of the NAL message). + /// + public static byte[] GetH264RtpHeader(byte nal0, bool isFirstPacket, bool isFinalPacket) + { + byte nalType = (byte)(nal0 & 0x1F); + //byte nalNri = (byte)((nal0 >> 5) & 0x03); - return new byte[] { fuIndicator, fuHeader }; + byte firstHdrByte = (byte)(nal0 & 0xE0); // Has either 24 (STAP-A) or 28 (FU-A) added to it. + + byte fuIndicator = (byte)(firstHdrByte + 28); + byte fuHeader = nalType; + if (isFirstPacket) + { + fuHeader += 0x80; } + else if (isFinalPacket) + { + fuHeader += 0x40; + } + + return new byte[] { fuIndicator, fuHeader }; } } diff --git a/src/SIPSorcery/net/RTP/Packetisation/H265Depacketiser.cs b/src/SIPSorcery/net/RTP/Packetisation/H265Depacketiser.cs index bb4562cb66..a145c57b1b 100644 --- a/src/SIPSorcery/net/RTP/Packetisation/H265Depacketiser.cs +++ b/src/SIPSorcery/net/RTP/Packetisation/H265Depacketiser.cs @@ -15,255 +15,253 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; using System.IO; -using System.Linq; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// Implements depacktizer of H265 units. The implementation follows the RFC7798. +/// +/// The main focus is on handling Aggregated Units (AU) and Fragmentation Units (FU). The implementation does not support PACI packets. +/// +public class H265Depacketiser { - /// - /// Implements depacktizer of H265 units. The implementation follows the RFC7798. - /// - /// The main focus is on handling Aggregated Units (AU) and Fragmentation Units (FU). The implementation does not support PACI packets. - /// - public class H265Depacketiser + private const int VPS = 32; + private const int SPS = 33; + private const int PPS = 34; + + //Payload Helper Fields + private uint previous_timestamp; + private List> temporary_rtp_payloads = new List>(); // used to assemble the RTP packets that form one RTP Frame + + public virtual bool ProcessRTPPayload(IBufferWriter bufferWriter, ReadOnlySpan rtpPayload, ushort seqNum, uint timestamp, int markbit, out bool isKeyFrame) { - const int VPS = 32; - const int SPS = 33; - const int PPS = 34; + var nal_units = ProcessRTPPayloadAsNals(rtpPayload, seqNum, timestamp, markbit, out isKeyFrame); - //Payload Helper Fields - uint previous_timestamp = 0; - List> temporary_rtp_payloads = new List>(); // used to assemble the RTP packets that form one RTP Frame + if (nal_units is null) + { + return false; + } - public virtual MemoryStream ProcessRTPPayload(byte[] rtpPayload, ushort seqNum, uint timestamp, int markbit, out bool isKeyFrame) + //Calculate total buffer size + long totalBufferSize = 0; + for (var i = 0; i < nal_units.Count; i++) { - List nal_units = ProcessRTPPayloadAsNals(rtpPayload, seqNum, timestamp, markbit, out isKeyFrame); + var nal = nal_units[i]; + long remaining = nal.Length; - if (nal_units != null) + if (remaining > 0) { - //Calculate total buffer size - long totalBufferSize = 0; - for (int i = 0; i < nal_units.Count; i++) - { - var nal = nal_units[i]; - long remaining = nal.Length; - - if (remaining > 0) - { - totalBufferSize += remaining + 4; //nal + 0001 - } - else - { - nal_units.RemoveAt(i); - i--; - } - } - - //Merge nals in same buffer using Annex-B separator (0001) - MemoryStream data = new MemoryStream(new byte[totalBufferSize]); - foreach (var nal in nal_units) - { - data.WriteByte(0); - data.WriteByte(0); - data.WriteByte(0); - data.WriteByte(1); - data.Write(nal, 0, nal.Length); - } - return data; + totalBufferSize += remaining + 4; //nal + 0001 + } + else + { + nal_units.RemoveAt(i); + i--; } - return null; } - public virtual List ProcessRTPPayloadAsNals(byte[] rtpPayload, ushort seqNum, uint timestamp, int markbit, out bool isKeyFrame) + //Merge nals in same buffer using Annex-B separator (0001) + var data = new MemoryStream(new byte[totalBufferSize]); + foreach (var nal in nal_units) { - List nal_units = ProcessH265Payload(rtpPayload, seqNum, timestamp, markbit, out isKeyFrame); + bufferWriter.Write(new ReadOnlySpan([0, 0, 0, 1])); + bufferWriter.Write(nal.AsSpan()); + } - return nal_units; + return true; + } + + public virtual List? ProcessRTPPayloadAsNals(ReadOnlySpan rtpPayload, ushort seqNum, uint timestamp, int markbit, out bool isKeyFrame) + { + var nal_units = ProcessH265Payload(rtpPayload, seqNum, timestamp, markbit, out isKeyFrame); + + return nal_units; + } + + protected virtual List? ProcessH265Payload(ReadOnlySpan rtp_payload, ushort seqNum, uint rtp_timestamp, int rtp_marker, out bool isKeyFrame) + { + if (previous_timestamp != rtp_timestamp && previous_timestamp > 0) + { + temporary_rtp_payloads.Clear(); + previous_timestamp = 0; } - protected virtual List ProcessH265Payload(byte[] rtp_payload, ushort seqNum, uint rtp_timestamp, int rtp_marker, out bool isKeyFrame) + // Add to the list of payloads for the current Frame of video + temporary_rtp_payloads.Add(new KeyValuePair(seqNum, rtp_payload.ToArray())); // TODO could optimise this and go direct to Process Frame if just 1 packet in frame + if (rtp_marker == 1) { - if (previous_timestamp != rtp_timestamp && previous_timestamp > 0) + //Reorder to prevent UDP incorrect package order + if (temporary_rtp_payloads.Count > 1) { - temporary_rtp_payloads.Clear(); - previous_timestamp = 0; + temporary_rtp_payloads.Sort((a, b) => + { + // Detect wraparound of sequence to sort packets correctly (Assumption that no more then 2000 packets per frame) + return (Math.Abs(b.Key - a.Key) > (0xFFFF - 2000)) ? -a.Key.CompareTo(b.Key) : a.Key.CompareTo(b.Key); + }); } - // Add to the list of payloads for the current Frame of video - temporary_rtp_payloads.Add(new KeyValuePair(seqNum, rtp_payload)); // TODO could optimise this and go direct to Process Frame if just 1 packet in frame - if (rtp_marker == 1) + // End Marker is set. Process the list of RTP Packets (forming 1 RTP frame) and save the NALs to a file + var nal_units = ProcessH265PayloadFrame(temporary_rtp_payloads, out isKeyFrame); + temporary_rtp_payloads.Clear(); + previous_timestamp = 0; + + return nal_units; + } + else + { + isKeyFrame = false; + previous_timestamp = rtp_timestamp; + return null; // we don't have a frame yet. Keep accumulating RTP packets + } + } + + // Process a RTP Frame. A RTP Frame can consist of several RTP Packets which have the same Timestamp + // Returns a list of NAL Units (with no 00 00 00 01 header and with no Size header) + protected virtual List ProcessH265PayloadFrame(List> rtp_payloads, out bool isKeyFrame) + { + var nal_units = new List(); // Stores the NAL units for a Video Frame. May be more than one NAL unit in a video frame. + + //check payload for Payload headers 48 and 49 + for (var payload_index = 0; payload_index < rtp_payloads.Count; payload_index++) + { + // The first two bytes of the NAL unit contain the NAL header + var nalHeader1 = rtp_payloads[payload_index].Value[0]; + var nalHeader2 = rtp_payloads[payload_index].Value[1]; + + // Extract the fields from the NAL header + var nal_header_f_bit = (nalHeader1 >> 7) & 0x01; + var nal_header_type = (nalHeader1 >> 1) & 0x3F; + var nuhLayerId = ((nalHeader1 & 0x01) << 5) | ((nalHeader2 >> 3) & 0x1F); + var nuhTemporalIdPlus1 = nalHeader2 & 0x07; + + var nalUnits = new List(); + if (nal_header_type == 48) { - //Reorder to prevent UDP incorrect package order - if (temporary_rtp_payloads.Count > 1) + //aggregated RTP Payload + nalUnits = ExtractNalUnitsFromAggregatedRTP(rtp_payloads[payload_index].Value); + foreach (var nalUnit in nalUnits) { - temporary_rtp_payloads.Sort((a, b) => - { - // Detect wraparound of sequence to sort packets correctly (Assumption that no more then 2000 packets per frame) - return (Math.Abs(b.Key - a.Key) > (0xFFFF - 2000)) ? -a.Key.CompareTo(b.Key) : a.Key.CompareTo(b.Key); - }); + nal_units.Add(nalUnit); } + } + else if (nal_header_type == 49) + { - // End Marker is set. Process the list of RTP Packets (forming 1 RTP frame) and save the NALs to a file - List nal_units = ProcessH265PayloadFrame(temporary_rtp_payloads, out isKeyFrame); - temporary_rtp_payloads.Clear(); - previous_timestamp = 0; - - return nal_units; + var nalUnit = MergeFUNalUnitsAccrossMultipleRTPPackages(rtp_payloads, ref payload_index); + if (nalUnit is { }) + { + nal_units.Add(nalUnit); + } } else { - isKeyFrame = false; - previous_timestamp = rtp_timestamp; - return null; // we don't have a frame yet. Keep accumulating RTP packets + nal_units.Add(rtp_payloads[payload_index].Value); } } + isKeyFrame = CheckKeyFrame(nal_units); + + // Output all the NALs that form one RTP Frame (one frame of video) + return nal_units; + } - // Process a RTP Frame. A RTP Frame can consist of several RTP Packets which have the same Timestamp - // Returns a list of NAL Units (with no 00 00 00 01 header and with no Size header) - protected virtual List ProcessH265PayloadFrame(List> rtp_payloads, out bool isKeyFrame) + private byte[]? MergeFUNalUnitsAccrossMultipleRTPPackages(List> rtp_payloads, ref int payload_index) + { + using (var fuNal = new MemoryStream()) { - List nal_units = new List(); // Stores the NAL units for a Video Frame. May be more than one NAL unit in a video frame. + var nalUnitComplete = false; - //check payload for Payload headers 48 and 49 - for (int payload_index = 0; payload_index < rtp_payloads.Count; payload_index++) + while (!nalUnitComplete) { - // The first two bytes of the NAL unit contain the NAL header - byte nalHeader1 = rtp_payloads[payload_index].Value[0]; - byte nalHeader2 = rtp_payloads[payload_index].Value[1]; - - // Extract the fields from the NAL header - int nal_header_f_bit = (nalHeader1 >> 7) & 0x01; - int nal_header_type = (nalHeader1 >> 1) & 0x3F; - int nuhLayerId = ((nalHeader1 & 0x01) << 5) | ((nalHeader2 >> 3) & 0x1F); - int nuhTemporalIdPlus1 = nalHeader2 & 0x07; - - List nalUnits = new List(); - if (nal_header_type == 48) + if (payload_index >= rtp_payloads.Count) { - //aggregated RTP Payload - nalUnits = ExtractNalUnitsFromAggregatedRTP(rtp_payloads[payload_index].Value); - foreach (var nalUnit in nalUnits) - { - nal_units.Add(nalUnit); - } + //Invalid FU, havn't found fu_endOfNal + return null; } - else if (nal_header_type == 49) + + var payload = rtp_payloads[payload_index].Value; + + var fuHeader = payload[2]; + var fu_startOfNal = (fuHeader >> 7) & 0x01; // start marker + var fu_endOfNal = (fuHeader >> 6) & 0x01; // end marker + var fu_type = fuHeader & 0x3F; // fragmented NAL Type + if (fu_startOfNal == 1) { - - byte[] nalUnit = MergeFUNalUnitsAccrossMultipleRTPPackages(rtp_payloads.Select(x => x.Value).ToList(), ref payload_index); - if (nalUnit != null) - { - nal_units.Add(nalUnit); - } + var nalHeader1 = payload[0]; + var nalHeader2 = payload[1]; + + nalHeader1 &= 0x81; // clear the NAL type bits + nalHeader1 |= (byte)((fu_type & 0x3F) << 1); // set the inner NAL type bits + + fuNal.WriteByte(nalHeader1); + fuNal.WriteByte(nalHeader2); } - else + + if (fuNal.Length == 0 && fu_startOfNal != 1) { - nal_units.Add(rtp_payloads[payload_index].Value); + //Invalid FU, first package should be start package + return null; } - } - isKeyFrame = CheckKeyFrame(nal_units); - // Output all the NALs that form one RTP Frame (one frame of video) - return nal_units; - } - - private byte[] MergeFUNalUnitsAccrossMultipleRTPPackages(List rtp_payloads, ref int payload_index) - { - using (MemoryStream fuNal = new MemoryStream()) - { - bool nalUnitComplete = false; + //Copy payload except Payload header and FU Header + fuNal.Write(payload, 3, payload.Length - 3); - while (!nalUnitComplete) + if (fu_endOfNal == 1) { - if (payload_index >= rtp_payloads.Count()) - { - //Invalid FU, havn't found fu_endOfNal - return null; - } - byte[] payload = rtp_payloads[payload_index]; - - - byte fuHeader = payload[2]; - int fu_startOfNal = (fuHeader >> 7) & 0x01; // start marker - int fu_endOfNal = (fuHeader >> 6) & 0x01; // end marker - int fu_type = fuHeader & 0x3F; // fragmented NAL Type - if (fu_startOfNal == 1) - { - byte nalHeader1 = payload[0]; - byte nalHeader2 = payload[1]; - - nalHeader1 &= 0x81; // clear the NAL type bits - nalHeader1 |= (byte)((fu_type & 0x3F) << 1); // set the inner NAL type bits - - fuNal.WriteByte(nalHeader1); - fuNal.WriteByte(nalHeader2); - } - - if (fuNal.Length == 0 && fu_startOfNal != 1) - { - //Invalid FU, first package should be start package - return null; - } - - //Copy payload except Payload header and FU Header - fuNal.Write(payload, 3, rtp_payloads[payload_index].Length - 3); - - if (fu_endOfNal == 1) - { - nalUnitComplete = true; - } - else - { - payload_index++; - } + nalUnitComplete = true; + } + else + { + payload_index++; } - return fuNal.ToArray(); } + return fuNal.ToArray(); } + } - private List ExtractNalUnitsFromAggregatedRTP(byte[] rtpPayload) + private List ExtractNalUnitsFromAggregatedRTP(byte[] rtpPayload) + { + var nalUnits = new List(); + var startIndex = 2; //First two bytes are Payload Header, ignore + while (startIndex < rtpPayload.Length) { - List nalUnits = new List(); - int startIndex = 2; //First two bytes are Payload Header, ignore - while (startIndex < rtpPayload.Length) + if (startIndex + 2 >= rtpPayload.Length) { - if (startIndex + 2 >= rtpPayload.Length) - { - //Not enough data for NAL size - break; - } - - int nalSize = rtpPayload[startIndex] << 8 | rtpPayload[startIndex+1]; - startIndex += 2; //NALUnit size read + //Not enough data for NAL size + break; + } - if(startIndex + nalSize > rtpPayload.Length) - { - //Not enough data for NALUnit - break; - } + var nalSize = rtpPayload[startIndex] << 8 | rtpPayload[startIndex + 1]; + startIndex += 2; //NALUnit size read - byte[] nal = new byte[nalSize]; - Buffer.BlockCopy(rtpPayload, startIndex, nal, 0, nalSize); - nalUnits.Add(nal); - startIndex += nalSize; + if (startIndex + nalSize > rtpPayload.Length) + { + //Not enough data for NALUnit + break; } - return nalUnits; + + var nal = new byte[nalSize]; + Buffer.BlockCopy(rtpPayload, startIndex, nal, 0, nalSize); + nalUnits.Add(nal); + startIndex += nalSize; } + return nalUnits; + } - protected bool CheckKeyFrame(List nalUnits) + protected bool CheckKeyFrame(List nalUnits) + { + foreach (var nalUnit in nalUnits) { - foreach(var nalUnit in nalUnits) + var nal_type = (nalUnit[0] >> 1) & 0x3F; ; + if (nal_type is SPS or + PPS or + VPS) { - int nal_type = (nalUnit[0] >> 1) & 0x3F; ; - if(nal_type == SPS || - nal_type == PPS || - nal_type == VPS) - { - return true; - } + return true; } - return false; } + return false; } } diff --git a/src/SIPSorcery/net/RTP/Packetisation/H265Packetiser.cs b/src/SIPSorcery/net/RTP/Packetisation/H265Packetiser.cs index a124f31f28..923011ab30 100644 --- a/src/SIPSorcery/net/RTP/Packetisation/H265Packetiser.cs +++ b/src/SIPSorcery/net/RTP/Packetisation/H265Packetiser.cs @@ -16,305 +16,307 @@ // License: // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Collections.Generic; -using System.Linq; -namespace SIPSorcery.net.RTP.Packetisation +namespace SIPSorcery.net.RTP.Packetisation; + +public class H265Packetiser { - public class H265Packetiser + public const int H265_RTP_HEADER_LENGTH = 2; + + public struct H265Nal { - public const int H265_RTP_HEADER_LENGTH = 2; + public byte[] NAL { get; } + public bool IsLast { get; } - public struct H265Nal + public H265Nal(byte[] nal, bool isLast) { - public byte[] NAL { get; } - public bool IsLast { get; } - - public H265Nal(byte[] nal, bool isLast) - { - NAL = nal; - IsLast = isLast; - } + NAL = nal; + IsLast = isLast; } + } - public static IEnumerable ParseNals(byte[] accessUnit) - { - int zeroes = 0; + public static IEnumerable ParseNals(ReadOnlySpan accessUnit) + { + var result = new List(); + int zeroes = 0; - // Parse NALs from H265 access unit, encoded as an Annex B bitstream. - // NALs are delimited by 0x000001 or 0x00000001. - int currPosn = 0; - for (int i = 0; i < accessUnit.Length; i++) + // Parse NALs from H265 access unit, encoded as an Annex B bitstream. + // NALs are delimited by 0x000001 or 0x00000001. + int currPosn = 0; + for (int i = 0; i < accessUnit.Length; i++) + { + if (accessUnit[i] == 0x00) { - if (accessUnit[i] == 0x00) - { - zeroes++; - } - else if (accessUnit[i] == 0x01 && zeroes >= 2) + zeroes++; + } + else if (accessUnit[i] == 0x01 && zeroes >= 2) + { + // This is a NAL start sequence. + int nalStart = i + 1; + if (nalStart - currPosn > 4) { - // This is a NAL start sequence. - int nalStart = i + 1; - if (nalStart - currPosn > 4) - { - int endPosn = nalStart - ((zeroes == 2) ? 3 : 4); - int nalSize = endPosn - currPosn; - bool isLast = currPosn + nalSize == accessUnit.Length; + int endPosn = nalStart - ((zeroes == 2) ? 3 : 4); + int nalSize = endPosn - currPosn; + bool isLast = currPosn + nalSize == accessUnit.Length; - yield return new H265Nal(accessUnit.Skip(currPosn).Take(nalSize).ToArray(), isLast); - } - - currPosn = nalStart; - zeroes = 0; - } - else - { - zeroes = 0; + result.Add(new H265Nal(accessUnit.Slice(currPosn, nalSize).ToArray(), isLast)); } - } - if (currPosn < accessUnit.Length) + currPosn = nalStart; + zeroes = 0; + } + else { - yield return new H265Nal(accessUnit.Skip(currPosn).ToArray(), true); + zeroes = 0; } } - /// - /// Constructs the RTP header for an H265 NAL. This method does NOT support - /// aggregation packets where multiple NALs are sent as a single RTP payload. - /// - /// - ///HEVC maintains the NAL unit concept of H.264 with modifications. - ///HEVC uses a two-byte NAL unit header, as shown in Figure 1. The - ///payload of a NAL unit refers to the NAL unit excluding the NAL unit - ///header. - /// - ///+---------------+---------------+ - ///|0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7| - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///|F| Type | LayerId | TID | - ///+-------------+-----------------+ - /// - ///Figure 1: The Structure of the HEVC NAL Unit Header - /// - ///F: 1 bit - ///forbidden_zero_bit. Required to be zero in [HEVC]. - ///In the context of this memo, - ///the value 1 may be used to indicate a syntax violation - /// - ///Type: 6 bits - ///nal_unit_type. This field specifies the NAL unit type as defined - ///in Table 7-1 of [HEVC]. If the most significant bit of this field - ///of a NAL unit is equal to 0 (i.e., the value of this field is less - ///than 32), the NAL unit is a VCL NAL unit. Otherwise, the NAL unit - ///is a non-VCL NAL unit. - /// - ///LayerId: 6 bits - ///nuh_layer_id. Required to be equal to zero in [HEVC]. - /// - ///TID: 3 bits - ///nuh_temporal_id_plus1. This field specifies the temporal - ///identifier of the NAL unit plus 1. The value of TemporalId is - ///equal to TID minus 1. A TID value of 0 is illegal to ensure that - ///there is at least one bit in the NAL unit header equal to 1, so to - ///enable independent considerations of start code emulations in the - ///NAL unit header and in the NAL unit payload data. - /// - /// - /// - ///4.2.Payload Header Usage - /// - ///The first two bytes of the payload of an RTP packet are referred to - ///as the payload header.The payload header consists of the same - ///fields(F, Type, LayerId, and TID) as the NAL unit header as shown in - ///Section 1.1.4, irrespective of the type of the payload structure. - /// - /// - /// - ///4.4.Payload Structures - /// - ///o Single NAL unit packet : Contains a single NAL unit in the payload, - ///and the NAL unit header of the NAL unit also serves as the payload - ///header. - ///o Aggregation Packet(AP) : Contains more than one NAL unit within - ///one access unit. - ///o Fragmentation Unit(FU) : Contains a subset of a single NAL unit. - ///o PACI carrying RTP packet : Contains a payload header(that differs - ///from other payload headers for efficiency), a Payload Header - ///Extension Structure(PHES), and a PACI payload. - /// - /// - /// - ///4.4.1. Single NAL Unit Packets - /// - ///A single NAL unit packet contains exactly one NAL unit, and consists - ///of a payload header (denoted as PayloadHdr), a conditional 16-bit - ///DONL field (in network byte order), and the NAL unit payload data - ///(the NAL unit excluding its NAL unit header) of the contained NAL - ///unit, as shown in Figure 3. - /// - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| PayloadHdr | DONL (conditional) | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| | - ///| NAL unit payload data | - ///| | - ///| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| :...OPTIONAL RTP padding | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// - ///Figure 3: The Structure of a Single NAL Unit Packet - /// - /// - /// - /// 4.4.2. Aggregation Packets (APs) - /// - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| RTP Header | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| PayloadHdr(Type= 48) | NALU 1 Size | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| NALU 1 HDR | | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ NALU 1 Data | - ///| . . . | - ///| | - ///+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| . . . | NALU 2 Size | NALU 2 HDR | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| NALU 2 HDR | | - ///+-+-+-+-+-+-+-+-+ NALU 2 Data | - ///| . . . | - ///| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| :...OPTIONAL RTP padding | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// - /// - /// - ///4.4.3. Fragmentation Units - /// - ///Fragmentation Units (FUs) are introduced to enable fragmenting a - ///single NAL unit into multiple RTP packets. - /// - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| PayloadHdr (Type=49) | FU header | DONL (cond) | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| - ///| DONL (cond) | | - ///|-+-+-+-+-+-+-+-+ | - ///| FU payload | - ///| | - ///| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ///| :...OPTIONAL RTP padding | - ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// - /// Figure 9: The Structure of an FU - /// - ///The fields in the payload header are set as follows. The Type field - ///MUST be equal to 49. The fields F, LayerId, and TID MUST be equal to - ///the fields F, LayerId, and TID, respectively, of the fragmented NAL - ///unit. - /// - ///The FU header consists of an S bit, an E bit, and a 6-bit FuType - ///field, as shown in Figure 10. - /// - ///+---------------+ - ///|0|1|2|3|4|5|6|7| - ///+-+-+-+-+-+-+-+-+ - ///|S|E| FuType | - ///+---------------+ - /// - ///Figure 10: The Structure of FU Header - /// - ///The semantics of the FU header fields are as follows: - /// - ///S: 1 bit - ///When set to 1, the S bit indicates the start of a fragmented NAL - ///unit, i.e., the first byte of the FU payload is also the first - ///byte of the payload of the fragmented NAL unit. When the FU - ///payload is not the start of the fragmented NAL unit payload, the S - ///bit MUST be set to 0. - /// - ///E: 1 bit - ///When set to 1, the E bit indicates the end of a fragmented NAL - ///unit, i.e., the last byte of the payload is also the last byte of - ///the fragmented NAL unit. When the FU payload is not the last - ///fragment of a fragmented NAL unit, the E bit MUST be set to 0. - /// - ///FuType: 6 bits - ///The field FuType MUST be equal to the field Type of the fragmented - ///NAL unit. - /// - public static byte[] GetH265RtpHeader(byte[] nals, bool isFirstPacket, bool isFinalPacket) + if (currPosn < accessUnit.Length) { - // get the type - byte nalType = (byte)((nals[0] >> 1) & 0x3F); - // turn into FU (type=49) - nals[0] = (byte)((nals[0] & 0x81) | (49 << 1)); - // make FU header with previous naltype - byte fuHeader = (byte)(nalType & 0x3f); - if (isFirstPacket) - { - fuHeader |= 0x80; - } - else if (isFinalPacket) - { - fuHeader |= 0x40; - } - return new byte[] { nals[0], nals[1], fuHeader }; + result.Add(new H265Nal(accessUnit.Slice(currPosn).ToArray(), true)); + } + + return result; + } + + /// + /// Constructs the RTP header for an H265 NAL. This method does NOT support + /// aggregation packets where multiple NALs are sent as a single RTP payload. + /// + /// + ///HEVC maintains the NAL unit concept of H.264 with modifications. + ///HEVC uses a two-byte NAL unit header, as shown in Figure 1. The + ///payload of a NAL unit refers to the NAL unit excluding the NAL unit + ///header. + /// + ///+---------------+---------------+ + ///|0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7| + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///|F| Type | LayerId | TID | + ///+-------------+-----------------+ + /// + ///Figure 1: The Structure of the HEVC NAL Unit Header + /// + ///F: 1 bit + ///forbidden_zero_bit. Required to be zero in [HEVC]. + ///In the context of this memo, + ///the value 1 may be used to indicate a syntax violation + /// + ///Type: 6 bits + ///nal_unit_type. This field specifies the NAL unit type as defined + ///in Table 7-1 of [HEVC]. If the most significant bit of this field + ///of a NAL unit is equal to 0 (i.e., the value of this field is less + ///than 32), the NAL unit is a VCL NAL unit. Otherwise, the NAL unit + ///is a non-VCL NAL unit. + /// + ///LayerId: 6 bits + ///nuh_layer_id. Required to be equal to zero in [HEVC]. + /// + ///TID: 3 bits + ///nuh_temporal_id_plus1. This field specifies the temporal + ///identifier of the NAL unit plus 1. The value of TemporalId is + ///equal to TID minus 1. A TID value of 0 is illegal to ensure that + ///there is at least one bit in the NAL unit header equal to 1, so to + ///enable independent considerations of start code emulations in the + ///NAL unit header and in the NAL unit payload data. + /// + /// + /// + ///4.2.Payload Header Usage + /// + ///The first two bytes of the payload of an RTP packet are referred to + ///as the payload header.The payload header consists of the same + ///fields(F, Type, LayerId, and TID) as the NAL unit header as shown in + ///Section 1.1.4, irrespective of the type of the payload structure. + /// + /// + /// + ///4.4.Payload Structures + /// + ///o Single NAL unit packet : Contains a single NAL unit in the payload, + ///and the NAL unit header of the NAL unit also serves as the payload + ///header. + ///o Aggregation Packet(AP) : Contains more than one NAL unit within + ///one access unit. + ///o Fragmentation Unit(FU) : Contains a subset of a single NAL unit. + ///o PACI carrying RTP packet : Contains a payload header(that differs + ///from other payload headers for efficiency), a Payload Header + ///Extension Structure(PHES), and a PACI payload. + /// + /// + /// + ///4.4.1. Single NAL Unit Packets + /// + ///A single NAL unit packet contains exactly one NAL unit, and consists + ///of a payload header (denoted as PayloadHdr), a conditional 16-bit + ///DONL field (in network byte order), and the NAL unit payload data + ///(the NAL unit excluding its NAL unit header) of the contained NAL + ///unit, as shown in Figure 3. + /// + /// 0 1 2 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| PayloadHdr | DONL (conditional) | + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| | + ///| NAL unit payload data | + ///| | + ///| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| :...OPTIONAL RTP padding | + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + ///Figure 3: The Structure of a Single NAL Unit Packet + /// + /// + /// + /// 4.4.2. Aggregation Packets (APs) + /// + /// 0 1 2 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| RTP Header | + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| PayloadHdr(Type= 48) | NALU 1 Size | + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| NALU 1 HDR | | + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ NALU 1 Data | + ///| . . . | + ///| | + ///+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| . . . | NALU 2 Size | NALU 2 HDR | + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| NALU 2 HDR | | + ///+-+-+-+-+-+-+-+-+ NALU 2 Data | + ///| . . . | + ///| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| :...OPTIONAL RTP padding | + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// + /// + ///4.4.3. Fragmentation Units + /// + ///Fragmentation Units (FUs) are introduced to enable fragmenting a + ///single NAL unit into multiple RTP packets. + /// + /// 0 1 2 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| PayloadHdr (Type=49) | FU header | DONL (cond) | + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| + ///| DONL (cond) | | + ///|-+-+-+-+-+-+-+-+ | + ///| FU payload | + ///| | + ///| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ///| :...OPTIONAL RTP padding | + ///+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// Figure 9: The Structure of an FU + /// + ///The fields in the payload header are set as follows. The Type field + ///MUST be equal to 49. The fields F, LayerId, and TID MUST be equal to + ///the fields F, LayerId, and TID, respectively, of the fragmented NAL + ///unit. + /// + ///The FU header consists of an S bit, an E bit, and a 6-bit FuType + ///field, as shown in Figure 10. + /// + ///+---------------+ + ///|0|1|2|3|4|5|6|7| + ///+-+-+-+-+-+-+-+-+ + ///|S|E| FuType | + ///+---------------+ + /// + ///Figure 10: The Structure of FU Header + /// + ///The semantics of the FU header fields are as follows: + /// + ///S: 1 bit + ///When set to 1, the S bit indicates the start of a fragmented NAL + ///unit, i.e., the first byte of the FU payload is also the first + ///byte of the payload of the fragmented NAL unit. When the FU + ///payload is not the start of the fragmented NAL unit payload, the S + ///bit MUST be set to 0. + /// + ///E: 1 bit + ///When set to 1, the E bit indicates the end of a fragmented NAL + ///unit, i.e., the last byte of the payload is also the last byte of + ///the fragmented NAL unit. When the FU payload is not the last + ///fragment of a fragmented NAL unit, the E bit MUST be set to 0. + /// + ///FuType: 6 bits + ///The field FuType MUST be equal to the field Type of the fragmented + ///NAL unit. + /// + public static byte[] GetH265RtpHeader(byte[] nals, bool isFirstPacket, bool isFinalPacket) + { + // get the type + byte nalType = (byte)((nals[0] >> 1) & 0x3F); + // turn into FU (type=49) + nals[0] = (byte)((nals[0] & 0x81) | (49 << 1)); + // make FU header with previous naltype + byte fuHeader = (byte)(nalType & 0x3f); + if (isFirstPacket) + { + fuHeader |= 0x80; + } + else if (isFinalPacket) + { + fuHeader |= 0x40; } + return new byte[] { nals[0], nals[1], fuHeader }; + } - public static IEnumerable CreateAggregated(IEnumerable nals, int RTP_MAX_PAYLOAD) + public static IEnumerable CreateAggregated(IEnumerable nals, int RTP_MAX_PAYLOAD) + { + var newNalList = new List(); + var aggregated = new List(); + var aggregatedLast = false; + var nalCount = 0; + foreach (var nal in nals) { - var newNalList = new List(); - var aggregated = new List(); - var aggregatedLast = false; - var nalCount = 0; - foreach (var nal in nals) + if (nal.NAL.Length + aggregated.Count <= RTP_MAX_PAYLOAD) { - if (nal.NAL.Length + aggregated.Count <= RTP_MAX_PAYLOAD) + if (nalCount == 0) { - if (nalCount == 0) - { - aggregated.Add((byte)((nal.NAL[0] & 0x81) | (48 << 1))); - aggregated.Add(nal.NAL[1]); - } - var length = nal.NAL.Length; - byte[] nalSize = new byte[2]; - nalSize[0] = (byte)(length >> 8); - nalSize[1] = (byte)(length & 0xFF); - aggregated.AddRange(nalSize); - aggregated.AddRange(nal.NAL); - aggregatedLast = nal.IsLast; - nalCount++; + aggregated.Add((byte)((nal.NAL[0] & 0x81) | (48 << 1))); + aggregated.Add(nal.NAL[1]); } - // this NAL is beyond aggregation - else + var length = nal.NAL.Length; + byte[] nalSize = new byte[2]; + nalSize[0] = (byte)(length >> 8); + nalSize[1] = (byte)(length & 0xFF); + aggregated.AddRange(nalSize); + aggregated.AddRange(nal.NAL); + aggregatedLast = nal.IsLast; + nalCount++; + } + // this NAL is beyond aggregation + else + { + // add the previously aggregated NAL + if (nalCount > 0) { - // add the previously aggregated NAL - if (nalCount > 0) - { - newNalList.Add(new H265Nal(aggregated.ToArray(), aggregatedLast)); - aggregated.Clear(); - aggregatedLast = false; - nalCount = 0; - } - - // add this NAL - newNalList.Add(nal); + newNalList.Add(new H265Nal(aggregated.ToArray(), aggregatedLast)); + aggregated.Clear(); + aggregatedLast = false; + nalCount = 0; } - } - // add the previously aggregated NAL even if it is the last NAL - if (nalCount > 0) - { - newNalList.Add(new H265Nal(aggregated.ToArray(), aggregatedLast)); + // add this NAL + newNalList.Add(nal); } + } - return newNalList; + // add the previously aggregated NAL even if it is the last NAL + if (nalCount > 0) + { + newNalList.Add(new H265Nal(aggregated.ToArray(), aggregatedLast)); } + + return newNalList; } } diff --git a/src/SIPSorcery/net/RTP/Packetisation/MJPEGDepacketiser.cs b/src/SIPSorcery/net/RTP/Packetisation/MJPEGDepacketiser.cs index 556bd1b3d2..6d1c91f920 100644 --- a/src/SIPSorcery/net/RTP/Packetisation/MJPEGDepacketiser.cs +++ b/src/SIPSorcery/net/RTP/Packetisation/MJPEGDepacketiser.cs @@ -17,512 +17,450 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- using System; +using System.Buffers; +using System.Buffers.Binary; using System.IO; - -namespace SIPSorcery.net.RTP.Packetisation +using CommunityToolkit.HighPerformance.Buffers; +using Org.BouncyCastle.Asn1.X509; + +namespace SIPSorcery.net.RTP.Packetisation; + +/// +/// Based on https://github.com/BogdanovKirill/RtspClientSharp/blob/master/RtspClientSharp/MediaParsers/MJPEGVideoPayloadParser.cs +/// Distributed under MIT License +/// +/// @author mdr@milestone.dk +/// +public class MJPEGDepacketiser { - /// - /// Based on https://github.com/BogdanovKirill/RtspClientSharp/blob/master/RtspClientSharp/MediaParsers/MJPEGVideoPayloadParser.cs - /// Distributed under MIT License - /// - /// @author mdr@milestone.dk - /// - public class MJPEGDepacketiser + #region Payload helper fields + private const int JpegHeaderSize = 8; + private const int JpegMaxSize = 16 * 1024 * 1024; + + private static byte[] StartMarkerBytes = { 0xFF, 0xD8 }; + private static byte[] EndMarkerBytes = { 0xFF, 0xD9 }; + + private static byte[] DefaultQuantizers = { - #region Payload helper fields - private const int JpegHeaderSize = 8; - private const int JpegMaxSize = 16 * 1024 * 1024; + 16, 11, 12, 14, 12, 10, 16, 14, + 13, 14, 18, 17, 16, 19, 24, 40, + 26, 24, 22, 22, 24, 49, 35, 37, + 29, 40, 58, 51, 61, 60, 57, 51, + 56, 55, 64, 72, 92, 78, 64, 68, + 87, 69, 55, 56, 80, 109, 81, 87, + 95, 98, 103, 104, 103, 62, 77, 113, + 121, 112, 100, 120, 92, 101, 103, 99, + 17, 18, 18, 24, 21, 24, 47, 26, + 26, 47, 99, 66, 56, 66, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + }; + + private static byte[] LumDcCodelens = + { + 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 + }; - private static byte[] StartMarkerBytes = { 0xFF, 0xD8 }; - private static byte[] EndMarkerBytes = { 0xFF, 0xD9 }; + private static byte[] LumDcSymbols = + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 + }; - private static ArraySegment JpegEndMarkerByteSegment = - new ArraySegment(EndMarkerBytes); + private static byte[] LumAcCodelens = + { + 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d + }; - private static byte[] DefaultQuantizers = - { - 16, 11, 12, 14, 12, 10, 16, 14, - 13, 14, 18, 17, 16, 19, 24, 40, - 26, 24, 22, 22, 24, 49, 35, 37, - 29, 40, 58, 51, 61, 60, 57, 51, - 56, 55, 64, 72, 92, 78, 64, 68, - 87, 69, 55, 56, 80, 109, 81, 87, - 95, 98, 103, 104, 103, 62, 77, 113, - 121, 112, 100, 120, 92, 101, 103, 99, - 17, 18, 18, 24, 21, 24, 47, 26, - 26, 47, 99, 66, 56, 66, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99 - }; - - private static byte[] LumDcCodelens = - { - 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 - }; + private static byte[] LumAcSymbols = + { + 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, + 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, + 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, + 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, + 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, + 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, + 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, + 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, + 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, + 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, + 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, + 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, + 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, + 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, + 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0xfa + }; + + private static byte[] ChmDcCodelens = + { + 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 + }; - private static byte[] LumDcSymbols = - { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 - }; + private static byte[] ChmDcSymbols = + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 + }; - private static byte[] LumAcCodelens = - { - 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d - }; + private static byte[] ChmAcCodelens = + { + 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77 + }; - private static byte[] LumAcSymbols = - { - 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, - 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, - 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, - 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, - 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, - 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, - 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, - 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, - 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, - 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, - 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, - 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, - 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, - 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, - 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, - 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, - 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, - 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, - 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, - 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, - 0xf9, 0xfa - }; - - private static byte[] ChmDcCodelens = - { - 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0 - }; + private static byte[] ChmAcSymbols = + { + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, + 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, + 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, + 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, + 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, + 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, + 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, + 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, + 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, + 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, + 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, + 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, + 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, + 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, + 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, + 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0xfa + }; + + private ArrayPoolBufferWriter _frameBuffer = new ArrayPoolBufferWriter(); + + private int _currentDri; + private int _currentQ; + private int _currentType; + private int _currentFrameWidth; + private int _currentFrameHeight; + + private bool _hasExternalQuantizationTable; + + private byte[] _jpegHeaderBytes = Array.Empty(); + + private byte[] _quantizationTables = Array.Empty(); + private int _quantizationTablesLength; + + + #endregion + + public virtual bool ProcessRTPPayload(IBufferWriter bufferWriter, ReadOnlySpan rtpPayload, ushort seqNum, uint timestamp, int markbit, out bool isKeyFrame) + { + //MJPEG only contains full frames + isKeyFrame = true; - private static byte[] ChmDcSymbols = - { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 - }; + var offset = 1; + var fragmentOffset = BinaryPrimitives.ReadUInt32BigEndian(rtpPayload.Slice(offset)) & 0x00FFFFFFU; + offset += 3; - private static byte[] ChmAcCodelens = - { - 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77 - }; + int type = rtpPayload[offset++]; + int q = rtpPayload[offset++]; + var width = rtpPayload[offset++] * 8; + var height = rtpPayload[offset++] * 8; + var dri = 0; - private static byte[] ChmAcSymbols = - { - 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, - 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, - 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, - 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, - 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, - 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, - 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, - 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, - 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, - 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, - 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, - 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, - 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, - 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, - 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, - 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, - 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, - 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, - 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, - 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, - 0xf9, 0xfa - }; - - private MemoryStream _frameStream = new MemoryStream(); - private MemoryStream _returnFrame = new MemoryStream(); - private bool _resetReturnFrame; - - private int _currentDri; - private int _currentQ; - private int _currentType; - private int _currentFrameWidth; - private int _currentFrameHeight; - - private bool _hasExternalQuantizationTable; - - private byte[] _jpegHeaderBytes = new byte[0]; - private ArraySegment _jpegHeaderBytesSegment; - - private byte[] _quantizationTables = new byte[0]; - private int _quantizationTablesLength; - - - #endregion - - public virtual MemoryStream ProcessRTPPayload(byte[] rtpPayload, ushort seqNum, uint timestamp, int markbit, out bool isKeyFrame) + if (type > 63) { - //MJPEG only contains full frames - isKeyFrame = true; + dri = BinaryPrimitives.ReadUInt16BigEndian(rtpPayload.Slice(offset)); + offset += 4; + } - if (_resetReturnFrame) + var frameWritten = false; + + if (fragmentOffset == 0) + { + if (_frameBuffer.WrittenCount != 0) { - _returnFrame = new MemoryStream(); - _resetReturnFrame = false; + GenerateFrame(bufferWriter); + frameWritten = true; } - int offset = 1; - int fragmentOffset = ReadUInt24(rtpPayload, offset); - offset += 3; - int type = rtpPayload[offset++]; - int q = rtpPayload[offset++]; - int width = rtpPayload[offset++] * 8; - int height = rtpPayload[offset++] * 8; - int dri = 0; + var quantizationTablesChanged = false; - if(type > 63) + if (q > 127) { - dri = ReadUInt16(rtpPayload, offset); - offset += 4; - } + int mbz = rtpPayload[offset]; - if(fragmentOffset == 0) - { - if(_frameStream.Position != 0) + if (mbz == 0) { - GenerateFrame(); - } - - bool quantizationTablesChanged = false; + _hasExternalQuantizationTable = true; - if (q > 127) - { - int mbz = rtpPayload[offset]; + var quantizationTablesLength = BinaryPrimitives.ReadUInt16BigEndian(rtpPayload.Slice(offset + 2)); + offset += 4; - if(mbz == 0) + if (!rtpPayload.Slice(offset, quantizationTablesLength).SequenceEqual(_quantizationTables.AsSpan(0, _quantizationTablesLength))) { - _hasExternalQuantizationTable = true; - - int quantizationTablesLength = ReadUInt16(rtpPayload, offset + 2); - offset += 4; - - if(!AreBytesEqual(rtpPayload, offset, quantizationTablesLength, _quantizationTables, 0, _quantizationTablesLength)) + if (_quantizationTablesLength < quantizationTablesLength) { - if(_quantizationTablesLength < quantizationTablesLength) - { - _quantizationTables = new byte[quantizationTablesLength]; - } - - Buffer.BlockCopy(rtpPayload, offset, _quantizationTables, 0, quantizationTablesLength); - _quantizationTablesLength = quantizationTablesLength; - quantizationTablesChanged = true; + _quantizationTables = new byte[quantizationTablesLength]; } - offset += quantizationTablesLength; - + rtpPayload.Slice(offset, quantizationTablesLength).CopyTo(_quantizationTables); + _quantizationTablesLength = quantizationTablesLength; + quantizationTablesChanged = true; } - } - if(quantizationTablesChanged || _currentType != type || _currentQ != q || - _currentFrameWidth != width || _currentFrameHeight != height || _currentDri != dri) - { - _currentType = type; - _currentQ = q; - _currentFrameWidth = width; - _currentFrameHeight = height; - _currentDri = dri; + offset += quantizationTablesLength; - ReInitializeJpegHeader(); } + } + + if (quantizationTablesChanged || _currentType != type || _currentQ != q || + _currentFrameWidth != width || _currentFrameHeight != height || _currentDri != dri) + { + _currentType = type; + _currentQ = q; + _currentFrameWidth = width; + _currentFrameHeight = height; + _currentDri = dri; - _frameStream.Write(_jpegHeaderBytesSegment.Array, _jpegHeaderBytesSegment.Offset, _jpegHeaderBytesSegment.Count); + ReInitializeJpegHeader(); } - //if(fragmentOffset != 0 && _frameStream.Position == 0) - //{ - // return; - //} + _frameBuffer.Write(_jpegHeaderBytes); + } - int dataSize = rtpPayload.Length - offset; + //if(fragmentOffset != 0 && _frameStream.Position == 0) + //{ + // return; + //} - _frameStream.Write(rtpPayload, offset, dataSize); + var dataSize = rtpPayload.Length - offset; - if(_returnFrame.Length > 0) - { - _resetReturnFrame = true; - return _returnFrame; - } - return null; - } + _frameBuffer.Write(rtpPayload.Slice(offset, dataSize)); - private uint ReadUInt32(byte[] buffer, int offset) + return frameWritten; + } + + private void ReInitializeJpegHeader() + { + if (!_hasExternalQuantizationTable) { - return (uint)(buffer[offset] << 24 | - buffer[offset + 1] << 16 | - buffer[offset + 2] << 8 | - buffer[offset + 3]); + GenerateQuantizationTables(_currentQ); } - private int ReadUInt24(byte[] buffer, int offset) + var jpegHeaderSize = GetJpegHeaderSize(_currentDri); + + _jpegHeaderBytes = new byte[jpegHeaderSize]; + FillJpegHeader(_jpegHeaderBytes, _currentType, _currentFrameWidth, _currentFrameHeight, _currentDri); + } + + private void GenerateQuantizationTables(int factor) + { + _quantizationTablesLength = 128; + + if (_quantizationTables.Length < _quantizationTablesLength) { - return buffer[offset] << 16 | - buffer[offset + 1] << 8 | - buffer[offset + 2]; + _quantizationTables = new byte[_quantizationTablesLength]; } - private int ReadUInt16(byte[] buffer, int offset) + int q; + + if (factor < 1) { - return (buffer[offset] << 8) | buffer[offset + 1]; + factor = 1; } - - private bool AreBytesEqual(byte[] bytes1, int offset1, int count1, byte[] bytes2, int offset2, int count2) + else if (factor > 99) { - if (count1 != count2) - { - return false; - } - - for (int i = 0; i < count1; i++) - { - if (bytes1[offset1 + i] != bytes2[offset2 + i]) - { - return false; - } - } - - return true; + factor = 99; } - private static bool EndsWith(byte[] array, int offset, int count, byte[] pattern) + if (factor < 50) { - int patternLength = pattern.Length; - - if (count < patternLength) - { - return false; - } - - offset = offset + count - patternLength; - - for (int i = 0; i < patternLength; i++, offset++) - { - if (array[offset] != pattern[i]) - { - return false; - } - } - - return true; + q = 5000 / factor; } - - private void ReInitializeJpegHeader() + else { - if (!_hasExternalQuantizationTable) - { - GenerateQuantizationTables(_currentQ); - } - - var jpegHeaderSize = GetJpegHeaderSize(_currentDri); - - _jpegHeaderBytes = new byte[jpegHeaderSize]; - _jpegHeaderBytesSegment = new ArraySegment(_jpegHeaderBytes); - - FillJpegHeader(_jpegHeaderBytes, _currentType, _currentFrameWidth, _currentFrameHeight, _currentDri); + q = 200 - factor * 2; } - private void GenerateQuantizationTables(int factor) + for (var i = 0; i < 128; ++i) { - _quantizationTablesLength = 128; - - if (_quantizationTables.Length < _quantizationTablesLength) - { - _quantizationTables = new byte[_quantizationTablesLength]; - } - - int q; + var newVal = (DefaultQuantizers[i] * q + 50) / 100; - if (factor < 1) + if (newVal < 1) { - factor = 1; + newVal = 1; } - else if (factor > 99) + else if (newVal > 255) { - factor = 99; + newVal = 255; } - if (factor < 50) - { - q = 5000 / factor; - } - else - { - q = 200 - factor * 2; - } - - for (var i = 0; i < 128; ++i) - { - int newVal = (DefaultQuantizers[i] * q + 50) / 100; - - if (newVal < 1) - { - newVal = 1; - } - else if (newVal > 255) - { - newVal = 255; - } - - _quantizationTables[i] = (byte)newVal; - } + _quantizationTables[i] = (byte)newVal; } + } - private int GetJpegHeaderSize(int dri) - { - int qtlen = _quantizationTablesLength; + private int GetJpegHeaderSize(int dri) + { + var qtlen = _quantizationTablesLength; - int qtlenHalf = qtlen / 2; - qtlen = qtlenHalf * 2; + var qtlenHalf = qtlen / 2; + qtlen = qtlenHalf * 2; - int qtablesCount = qtlen > 64 ? 2 : 1; - return 485 + qtablesCount * 5 + qtlen + (dri > 0 ? 6 : 0); - } + var qtablesCount = qtlen > 64 ? 2 : 1; + return 485 + qtablesCount * 5 + qtlen + (dri > 0 ? 6 : 0); + } - private void FillJpegHeader(byte[] buffer, int type, int width, int height, int dri) + private void FillJpegHeader(byte[] buffer, int type, int width, int height, int dri) + { + var qtablesCount = _quantizationTablesLength > 64 ? 2 : 1; + var offset = 0; + + buffer[offset++] = 0xFF; + buffer[offset++] = 0xD8; + buffer[offset++] = 0xFF; + buffer[offset++] = 0xe0; + buffer[offset++] = 0x00; + buffer[offset++] = 0x10; + buffer[offset++] = (byte)'J'; + buffer[offset++] = (byte)'F'; + buffer[offset++] = (byte)'I'; + buffer[offset++] = (byte)'F'; + buffer[offset++] = 0x00; + buffer[offset++] = 0x01; + buffer[offset++] = 0x01; + buffer[offset++] = 0x00; + buffer[offset++] = 0x00; + buffer[offset++] = 0x01; + buffer[offset++] = 0x00; + buffer[offset++] = 0x01; + buffer[offset++] = 0x00; + buffer[offset++] = 0x00; + + if (dri > 0) { - int qtablesCount = _quantizationTablesLength > 64 ? 2 : 1; - int offset = 0; - buffer[offset++] = 0xFF; - buffer[offset++] = 0xD8; - buffer[offset++] = 0xFF; - buffer[offset++] = 0xe0; - buffer[offset++] = 0x00; - buffer[offset++] = 0x10; - buffer[offset++] = (byte)'J'; - buffer[offset++] = (byte)'F'; - buffer[offset++] = (byte)'I'; - buffer[offset++] = (byte)'F'; - buffer[offset++] = 0x00; - buffer[offset++] = 0x01; - buffer[offset++] = 0x01; - buffer[offset++] = 0x00; - buffer[offset++] = 0x00; - buffer[offset++] = 0x01; - buffer[offset++] = 0x00; - buffer[offset++] = 0x01; - buffer[offset++] = 0x00; + buffer[offset++] = 0xdd; buffer[offset++] = 0x00; + buffer[offset++] = 0x04; + buffer[offset++] = (byte)(dri >> 8); + buffer[offset++] = (byte)dri; + } - if (dri > 0) - { - buffer[offset++] = 0xFF; - buffer[offset++] = 0xdd; - buffer[offset++] = 0x00; - buffer[offset++] = 0x04; - buffer[offset++] = (byte)(dri >> 8); - buffer[offset++] = (byte)dri; - } + var tableSize = qtablesCount == 1 ? _quantizationTablesLength : _quantizationTablesLength / 2; + buffer[offset++] = 0xFF; + buffer[offset++] = 0xdb; + buffer[offset++] = 0x00; + buffer[offset++] = (byte)(tableSize + 3); + buffer[offset++] = 0x00; + + var qtablesOffset = 0; + Buffer.BlockCopy(_quantizationTables, qtablesOffset, buffer, offset, tableSize); + qtablesOffset += tableSize; + offset += tableSize; + + if (qtablesCount > 1) + { + tableSize = _quantizationTablesLength - _quantizationTablesLength / 2; - int tableSize = qtablesCount == 1 ? _quantizationTablesLength : _quantizationTablesLength / 2; buffer[offset++] = 0xFF; buffer[offset++] = 0xdb; buffer[offset++] = 0x00; buffer[offset++] = (byte)(tableSize + 3); - buffer[offset++] = 0x00; - - int qtablesOffset = 0; + buffer[offset++] = 0x01; Buffer.BlockCopy(_quantizationTables, qtablesOffset, buffer, offset, tableSize); - qtablesOffset += tableSize; offset += tableSize; + } - if (qtablesCount > 1) - { - tableSize = _quantizationTablesLength - _quantizationTablesLength / 2; - - buffer[offset++] = 0xFF; - buffer[offset++] = 0xdb; - buffer[offset++] = 0x00; - buffer[offset++] = (byte)(tableSize + 3); - buffer[offset++] = 0x01; - Buffer.BlockCopy(_quantizationTables, qtablesOffset, buffer, offset, tableSize); - offset += tableSize; - } - - buffer[offset++] = 0xFF; - buffer[offset++] = 0xc0; - buffer[offset++] = 0x00; - buffer[offset++] = 0x11; - buffer[offset++] = 0x08; - buffer[offset++] = (byte)(height >> 8); - buffer[offset++] = (byte)height; - buffer[offset++] = (byte)(width >> 8); - buffer[offset++] = (byte)width; - buffer[offset++] = 0x03; - buffer[offset++] = 0x01; - buffer[offset++] = (type & 1) != 0 ? (byte)0x22 : (byte)0x21; - buffer[offset++] = 0x00; - buffer[offset++] = 0x02; - buffer[offset++] = 0x11; - buffer[offset++] = qtablesCount == 1 ? (byte)0x00 : (byte)0x01; - buffer[offset++] = 0x03; - buffer[offset++] = 0x11; - buffer[offset++] = qtablesCount == 1 ? (byte)0x00 : (byte)0x01; - - CreateHuffmanHeader(buffer, offset, LumDcCodelens, LumDcCodelens.Length, LumDcSymbols, LumDcSymbols.Length, - 0, 0); - offset += 5 + LumDcCodelens.Length + LumDcSymbols.Length; - - CreateHuffmanHeader(buffer, offset, LumAcCodelens, LumAcCodelens.Length, LumAcSymbols, LumAcSymbols.Length, - 0, 1); - offset += 5 + LumAcCodelens.Length + LumAcSymbols.Length; - - CreateHuffmanHeader(buffer, offset, ChmDcCodelens, ChmDcCodelens.Length, ChmDcSymbols, ChmDcSymbols.Length, - 1, 0); - offset += 5 + ChmDcCodelens.Length + ChmDcSymbols.Length; - - CreateHuffmanHeader(buffer, offset, ChmAcCodelens, ChmAcCodelens.Length, ChmAcSymbols, ChmAcSymbols.Length, - 1, 1); - offset += 5 + ChmAcCodelens.Length + ChmAcSymbols.Length; + buffer[offset++] = 0xFF; + buffer[offset++] = 0xc0; + buffer[offset++] = 0x00; + buffer[offset++] = 0x11; + buffer[offset++] = 0x08; + buffer[offset++] = (byte)(height >> 8); + buffer[offset++] = (byte)height; + buffer[offset++] = (byte)(width >> 8); + buffer[offset++] = (byte)width; + buffer[offset++] = 0x03; + buffer[offset++] = 0x01; + buffer[offset++] = (type & 1) != 0 ? (byte)0x22 : (byte)0x21; + buffer[offset++] = 0x00; + buffer[offset++] = 0x02; + buffer[offset++] = 0x11; + buffer[offset++] = qtablesCount == 1 ? (byte)0x00 : (byte)0x01; + buffer[offset++] = 0x03; + buffer[offset++] = 0x11; + buffer[offset++] = qtablesCount == 1 ? (byte)0x00 : (byte)0x01; + + CreateHuffmanHeader(buffer, offset, LumDcCodelens, LumDcCodelens.Length, LumDcSymbols, LumDcSymbols.Length, + 0, 0); + offset += 5 + LumDcCodelens.Length + LumDcSymbols.Length; + + CreateHuffmanHeader(buffer, offset, LumAcCodelens, LumAcCodelens.Length, LumAcSymbols, LumAcSymbols.Length, + 0, 1); + offset += 5 + LumAcCodelens.Length + LumAcSymbols.Length; + + CreateHuffmanHeader(buffer, offset, ChmDcCodelens, ChmDcCodelens.Length, ChmDcSymbols, ChmDcSymbols.Length, + 1, 0); + offset += 5 + ChmDcCodelens.Length + ChmDcSymbols.Length; + + CreateHuffmanHeader(buffer, offset, ChmAcCodelens, ChmAcCodelens.Length, ChmAcSymbols, ChmAcSymbols.Length, + 1, 1); + offset += 5 + ChmAcCodelens.Length + ChmAcSymbols.Length; + + buffer[offset++] = 0xFF; + buffer[offset++] = 0xda; + buffer[offset++] = 0x00; + buffer[offset++] = 0x0C; + buffer[offset++] = 0x03; + buffer[offset++] = 0x01; + buffer[offset++] = 0x00; + buffer[offset++] = 0x02; + buffer[offset++] = 0x11; + buffer[offset++] = 0x03; + buffer[offset++] = 0x11; + buffer[offset++] = 0x00; + buffer[offset++] = 0x3F; + buffer[offset] = 0x00; + } - buffer[offset++] = 0xFF; - buffer[offset++] = 0xda; - buffer[offset++] = 0x00; - buffer[offset++] = 0x0C; - buffer[offset++] = 0x03; - buffer[offset++] = 0x01; - buffer[offset++] = 0x00; - buffer[offset++] = 0x02; - buffer[offset++] = 0x11; - buffer[offset++] = 0x03; - buffer[offset++] = 0x11; - buffer[offset++] = 0x00; - buffer[offset++] = 0x3F; - buffer[offset] = 0x00; - } + private static void CreateHuffmanHeader(byte[] buffer, int offset, byte[] codelens, int ncodes, byte[] symbols, + int nsymbols, int tableNo, int tableClass) + { + buffer[offset++] = 0xff; + buffer[offset++] = 0xc4; + buffer[offset++] = 0; + buffer[offset++] = (byte)(3 + ncodes + nsymbols); + buffer[offset++] = (byte)((tableClass << 4) | tableNo); + Buffer.BlockCopy(codelens, 0, buffer, offset, ncodes); + offset += ncodes; + Buffer.BlockCopy(symbols, 0, buffer, offset, nsymbols); + } - private static void CreateHuffmanHeader(byte[] buffer, int offset, byte[] codelens, int ncodes, byte[] symbols, - int nsymbols, int tableNo, int tableClass) + private void GenerateFrame(IBufferWriter returnFrame) + { + if (!EndsWithEndMarkerBytes(_frameBuffer)) { - buffer[offset++] = 0xff; - buffer[offset++] = 0xc4; - buffer[offset++] = 0; - buffer[offset++] = (byte)(3 + ncodes + nsymbols); - buffer[offset++] = (byte)((tableClass << 4) | tableNo); - Buffer.BlockCopy(codelens, 0, buffer, offset, ncodes); - offset += ncodes; - Buffer.BlockCopy(symbols, 0, buffer, offset, nsymbols); + _frameBuffer.Write(EndMarkerBytes); } - private void GenerateFrame() + returnFrame.Write(_frameBuffer.WrittenSpan); + + _frameBuffer.Clear(); + + static bool EndsWithEndMarkerBytes(ArrayPoolBufferWriter buffer) { - if (!EndsWith(_frameStream.GetBuffer(), 0, - (int)_frameStream.Position, EndMarkerBytes)) + if (buffer.WrittenCount < EndMarkerBytes.Length) { - _frameStream.Write(JpegEndMarkerByteSegment.Array, JpegEndMarkerByteSegment.Offset, JpegEndMarkerByteSegment.Count); + return false; } - _returnFrame.Write(_frameStream.ToArray(), 0, (int)_frameStream.Length); - _frameStream = new MemoryStream(); + return buffer.WrittenSpan[^EndMarkerBytes.Length..].SequenceEqual(EndMarkerBytes); } } } diff --git a/src/SIPSorcery/net/RTP/Packetisation/MJPEGPacketiser.cs b/src/SIPSorcery/net/RTP/Packetisation/MJPEGPacketiser.cs index aae8ea303c..d4cd17ae22 100644 --- a/src/SIPSorcery/net/RTP/Packetisation/MJPEGPacketiser.cs +++ b/src/SIPSorcery/net/RTP/Packetisation/MJPEGPacketiser.cs @@ -16,376 +16,448 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- using System; +using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; using System.Net; -namespace SIPSorcery.net.RTP.Packetisation +namespace SIPSorcery.net.RTP.Packetisation; + +/// +/// This class offers packetisation of MJPEG to be sent over RTP. +/// @author mdr@milestone.dk +/// +public class MJPEGPacketiser { - /// - /// This class offers packetisation of MJPEG to be sent over RTP. - /// @author mdr@milestone.dk - /// - public class MJPEGPacketiser - { - private const int QTableHeaderLength = 2; - private const int QTableLength8Bit = 64; - private const int QTableParamsLength = 1; - private const int QTableBlockLength8Bit = QTableLength8Bit + QTableParamsLength; - private const int JpegNumberOfComponents = 3; //Only support for YUV + private const int QTableHeaderLength = 2; + private const int QTableLength8Bit = 64; + private const int QTableParamsLength = 1; + private const int QTableBlockLength8Bit = QTableLength8Bit + QTableParamsLength; + private const int JpegNumberOfComponents = 3; //Only support for YUV - public class Marker + public class Marker + { + public byte[] MarkerBytes = Array.Empty(); + public byte Type; + public int StartPosition = -1; + } + public struct MJPEG + { + public MJPEG() { - public byte[] MarkerBytes; - public byte Type; - public int StartPosition = -1; - } - public struct MJPEG - { - public MJPEG() - { - MjpegHeader = new MJPEGHeader(); - MjpegHeaderQTable = new MJPEGHeaderQTable(); - MjpegHeaderRestartMarker = new MJPEGHeaderRestartMarker(); - QTables = new List(); - HasRestartMarker = false; - } - public MJPEGHeader MjpegHeader; - public MJPEGHeaderQTable MjpegHeaderQTable; - public MJPEGHeaderRestartMarker MjpegHeaderRestartMarker; - public bool HasRestartMarker; - public List QTables; + MjpegHeader = new MJPEGHeader(); + MjpegHeaderQTable = new MJPEGHeaderQTable(); + MjpegHeaderRestartMarker = new MJPEGHeaderRestartMarker(); + QTables = new List(); + HasRestartMarker = false; } + public MJPEGHeader MjpegHeader; + public MJPEGHeaderQTable MjpegHeaderQTable; + public MJPEGHeaderRestartMarker MjpegHeaderRestartMarker; + public bool HasRestartMarker; + public List QTables; + } + + public class MJPEGData + { + public required byte[] Data; + } - public class MJPEGData + public class MJPEGHeader + { + public byte tspec = (int)JpegHeaderTypesSpec.jpegHeaderTypeSpec_Progressive; + public byte offsetHigh; + public byte offsetMid; + public byte offsetLow; + public byte type = (int)JpegHeaderTypes.jpegHeaderType_422; + public byte q; + public byte width; + public byte height; + + public void SetOffset(int offset) { - public byte[] Data; + offsetHigh = (byte)((offset >> 16) & 0xFF); + offsetMid = (byte)((offset >> 8) & 0xFF); + offsetLow = (byte)((offset >> 0) & 0xFF); } - public class MJPEGHeader + public void SetWidth(int width) { - public byte tspec = (int)JpegHeaderTypesSpec.jpegHeaderTypeSpec_Progressive; - public byte offsetHigh = 0; - public byte offsetMid = 0; - public byte offsetLow = 0; - public byte type = (int)JpegHeaderTypes.jpegHeaderType_422; - public byte q = 0; - public byte width = 0; - public byte height = 0; - - public void SetOffset(int offset) - { - offsetHigh = (byte)((offset >> 16) & 0xFF); - offsetMid = (byte)((offset >> 8) & 0xFF); - offsetLow = (byte)((offset >> 0) & 0xFF); - } - - public void SetWidth(int width) - { - this.width = (byte)(width >> 3); - } - public void SetHeight(int height) - { - this.height = (byte)(height >> 3); - } + this.width = (byte)(width >> 3); } - - public class MJPEGHeaderQTable + public void SetHeight(int height) { - public byte mbz = 0; - public byte precision = 0; - public byte lengthHigh = 0; - public byte lengthLow = 0; + this.height = (byte)(height >> 3); + } + } - public static int GetSize() { return 4; } + public class MJPEGHeaderQTable + { + public byte mbz; + public byte precision; + public byte lengthHigh; + public byte lengthLow; - public void SetLength(int length) - { - lengthHigh = (byte)((length >> 8) & 0xff); - lengthLow = (byte)((length >> 0) & 0xff); - } + public static int GetSize() { return 4; } - public int GetLength() - { - return (lengthHigh << 8) | lengthLow; - } + public void SetLength(int length) + { + lengthHigh = (byte)((length >> 8) & 0xff); + lengthLow = (byte)((length >> 0) & 0xff); } - public class MJPEGHeaderRestartMarker + public int GetLength() { - public ushort RestartInterval = 0; - public byte IsFirst = 1; - public byte IsLast = 1; - public int RestartCount = 0x3FFF; + return (lengthHigh << 8) | lengthLow; } + } + + public class MJPEGHeaderRestartMarker + { + public ushort RestartInterval; + public byte IsFirst = 1; + public byte IsLast = 1; + public int RestartCount = 0x3FFF; + } + + public enum JpegComponents + { + jpegComponent_Y = 0, + jpegComponent_U, + jpegComponent_V + } + + public enum JpegHeaderTypes + { + jpegHeaderType_422 = 0, + jpegHeaderType_420, + jpegHeaderType_422wRestartMarkers = 64, + jpegHeaderType_420wRestartMarkers = 65 + } + + public enum JpegHeaderTypesSpec + { + jpegHeaderTypeSpec_Progressive = 0, + jpegHeaderTypeSpec_OddFiled, + jpegHeaderTypeSpec_EvenFiled, + jpegHeaderTypeSpec_OddAndEvenFileds + } + + public enum JpegMarkerTypes + { + jmt_BeginMarker = 0xFF, + jmt_NotAmarker = 0x00, + + jmt_SOI = 0xD8, + jmt_EOI = 0xD9, + + jmt_SOF0 = 0xC0, + jmt_SOF1 = 0xC1, + + jmt_DQT = 0xDB, + + jmt_SOS = 0xDA, + jmt_DRI = 0xDD + } + + /// + /// Calculates the total number of bytes required to encode the MJPEG RTP header, + /// including the base JPEG header, an optional restart marker header, and optional quantization tables. + /// + /// + /// This method is useful for preallocating the exact buffer size needed to serialize + /// the MJPEG RTP header without relying on dynamic memory growth or pooling. + /// + /// Each packet contains a special JPEG header which immediately follows + /// the rtp header.the first 8 bytes of this header, called the "main + /// jpeg header", are as follows: + /// + /// 0 1 2 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | type-specific | fragment offset | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | type | q | width | height | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// All fields in this header except for the fragment offset field must + /// remain the same in all packets that correspond to the same jpeg + /// frame. + /// A restart marker header and/or quantization table header may follow + /// this header, depending on the values of the type and q fields. + /// + /// The MJPEG metadata containing header fields, restart markers, and quantization tables. + /// + /// The fragment offset of the current RTP packet. Quantization tables are only included if this is 0, + /// as they are typically sent only in the first packet of a fragmented MJPEG frame. + /// + /// The total number of bytes required to encode the MJPEG RTP header. + public static int CalculateMJPEGRTPHeaderLength(MJPEG customData, int offset) + { + var totalLength = 8; // Base JPEG header - public enum JpegComponents + if (customData.HasRestartMarker) { - jpegComponent_Y = 0, - jpegComponent_U, - jpegComponent_V + totalLength += 4; // 2 bytes for RestartInterval + 2 bytes for marker info } - public enum JpegHeaderTypes + var includeQTables = + customData.MjpegHeaderQTable.GetLength() > 0 && + customData.QTables.Count > 0 && + offset == 0; + + if (includeQTables) { - jpegHeaderType_422 = 0, - jpegHeaderType_420, - jpegHeaderType_422wRestartMarkers = 64, - jpegHeaderType_420wRestartMarkers = 65 + totalLength += 4; // QTable header + foreach (var qTable in customData.QTables) + { + totalLength += qTable.Length; + } } - public enum JpegHeaderTypesSpec + return totalLength; + } + + /// + /// Writes an MJPEG RTP header into the provided buffer. + /// This includes the base JPEG header, and optionally a restart marker and quantization tables. + /// + /// + /// Each packet contains a special JPEG header which immediately follows + /// the rtp header.the first 8 bytes of this header, called the "main + /// jpeg header", are as follows: + /// + /// 0 1 2 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | type-specific | fragment offset | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | type | q | width | height | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// All fields in this header except for the fragment offset field must + /// remain the same in all packets that correspond to the same jpeg + /// frame. + /// A restart marker header and/or quantization table header may follow + /// this header, depending on the values of the type and q fields. + /// + /// The MJPEG metadata to serialize into the buffer. + /// The fragment offset of the current RTP packet. + /// + /// The buffer to write the MJPEG RTP header into. Must be at least bytes long. + /// + /// The number of bytes written to the buffer. + public static int WriteMJPEGRTPHeader(MJPEG customData, int offset, Span destination) + { + customData.MjpegHeader.SetOffset(offset); + + var position = 0; + + // Base JPEG header (8 bytes) + destination[position++] = customData.MjpegHeader.tspec; + destination[position++] = customData.MjpegHeader.offsetHigh; + destination[position++] = customData.MjpegHeader.offsetMid; + destination[position++] = customData.MjpegHeader.offsetLow; + destination[position++] = customData.MjpegHeader.type; + destination[position++] = customData.MjpegHeader.q; + destination[position++] = customData.MjpegHeader.width; + destination[position++] = customData.MjpegHeader.height; + + // Restart Marker (optional) + if (customData.HasRestartMarker) { - jpegHeaderTypeSpec_Progressive = 0, - jpegHeaderTypeSpec_OddFiled, - jpegHeaderTypeSpec_EvenFiled, - jpegHeaderTypeSpec_OddAndEvenFileds + BinaryPrimitives.WriteUInt16BigEndian(destination.Slice(position), (ushort)customData.MjpegHeaderRestartMarker.RestartInterval); + position += 2; + + var markerInfo = (ushort)( + ((customData.MjpegHeaderRestartMarker.IsFirst & 0xF) << 8) | + ((customData.MjpegHeaderRestartMarker.IsLast & 0xF) << 7) | + (customData.MjpegHeaderRestartMarker.RestartCount & 0xF) + ); + BinaryPrimitives.WriteUInt16BigEndian(destination.Slice(position), markerInfo); + position += 2; } - public enum JpegMarkerTypes - { - jmt_BeginMarker = 0xFF, - jmt_NotAmarker = 0x00, - - jmt_SOI = 0xD8, - jmt_EOI = 0xD9, - - jmt_SOF0 = 0xC0, - jmt_SOF1 = 0xC1, - - jmt_DQT = 0xDB, - - jmt_SOS = 0xDA, - jmt_DRI = 0xDD - } - - /// - /// Create an RTP header for an MJPEG frame fragment. Typically it will not be a full frame - /// due to the frame size. This method does not support multiple frames in one packet. - /// - /// - /// Each packet contains a special JPEG header which immediately follows - /// the rtp header.the first 8 bytes of this header, called the "main - /// jpeg header", are as follows: - /// - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | type-specific | fragment offset | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | type | q | width | height | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// - /// All fields in this header except for the fragment offset field must - /// remain the same in all packets that correspond to the same jpeg - /// frame. - /// A restart marker header and/or quantization table header may follow - /// this header, depending on the values of the type and q fields. - /// - /// The MJPEG header to be written into bytes - /// The offset of current RTP package - /// - public static byte[] GetMJPEGRTPHeader(MJPEG customData, int offset) + // Quantization Tables (optional) + var includeQTables = + customData.MjpegHeaderQTable.GetLength() > 0 && + customData.QTables.Count > 0 && + offset == 0; + + if (includeQTables) { - customData.MjpegHeader.SetOffset(offset); - var jpegHeader = new byte[8]; - jpegHeader.SetValue(customData.MjpegHeader.tspec, 0); - jpegHeader.SetValue(customData.MjpegHeader.offsetHigh, 1); - jpegHeader.SetValue(customData.MjpegHeader.offsetMid, 2); - jpegHeader.SetValue(customData.MjpegHeader.offsetLow, 3); - jpegHeader.SetValue(customData.MjpegHeader.type, 4); - jpegHeader.SetValue(customData.MjpegHeader.q, 5); - jpegHeader.SetValue(customData.MjpegHeader.width, 6); - jpegHeader.SetValue(customData.MjpegHeader.height, 7); - - var customHeader = jpegHeader; - - if (customData.HasRestartMarker) - { - var restartHeader = new byte[4]; - var restartInterval = BitConverter.GetBytes(customData.MjpegHeaderRestartMarker.RestartInterval); - var isFirst = customData.MjpegHeaderRestartMarker.IsFirst; - var isLast = customData.MjpegHeaderRestartMarker.IsLast; - var restartCount = customData.MjpegHeaderRestartMarker.RestartCount; - var isLastAndFirstAndRestartCount = BitConverter.GetBytes((char)(((isFirst & 0xF) << 8) | ((isLast & 0xF) << 7) | (restartCount & 0xF))); - restartHeader = restartInterval.Concat(isLastAndFirstAndRestartCount).ToArray(); - - customHeader = jpegHeader.Concat(restartHeader).ToArray(); - } - if (customData.MjpegHeaderQTable.GetLength() > 0 && customData.QTables.Count > 0 && offset == 0) + destination[position++] = customData.MjpegHeaderQTable.mbz; + destination[position++] = customData.MjpegHeaderQTable.precision; + destination[position++] = customData.MjpegHeaderQTable.lengthHigh; + destination[position++] = customData.MjpegHeaderQTable.lengthLow; + + foreach (var qTable in customData.QTables) { - var qTableHeader = new byte[4]; - qTableHeader.SetValue(customData.MjpegHeaderQTable.mbz, 0); - qTableHeader.SetValue(customData.MjpegHeaderQTable.precision, 1); - qTableHeader.SetValue(customData.MjpegHeaderQTable.lengthHigh, 2); - qTableHeader.SetValue(customData.MjpegHeaderQTable.lengthLow, 3); - - var qtables = new byte[0]; - foreach (var qTable in customData.QTables) - { - qtables = qtables.Concat(qTable).ToArray(); - } - customHeader = customHeader.Concat(qTableHeader).Concat(qtables).ToArray(); + qTable.CopyTo(destination.Slice(position)); + position += qTable.Length; } - return customHeader; - } - /// - /// Scans the frame for markers and builds the RTPHeader data. - /// Returns the raw frame data. - /// - /// The entire frame data - /// RTPHeader data in a readable structure - /// - public static MJPEGData GetFrameData(byte[] jpegFrame, out MJPEG customData) - { - customData = new MJPEG(); + return position; + } - var index = 0; - var length = jpegFrame.Length; - List markers = new List(); - var currentMarker = new Marker(); - while (index < length) + /// + /// Scans the frame for markers and builds the RTPHeader data. + /// Returns the raw frame data. + /// + /// The entire frame data + /// + public static (MJPEGData? frameData, MJPEG customData) GetFrameData(ReadOnlySpan jpegFrame) + { + var customData = new MJPEG(); + var index = 0; + var length = jpegFrame.Length; + var markers = new List(); + var currentMarker = new Marker(); + + while (index < length) + { + if (index + 1 < length && + ContainsMarker(jpegFrame[index], JpegMarkerTypes.jmt_BeginMarker) && + !ContainsMarker(jpegFrame[index + 1], JpegMarkerTypes.jmt_NotAmarker) && + jpegFrame[index + 1] != 0xFF) { - if (index + 1 < length && ContainsMarker(jpegFrame[index], JpegMarkerTypes.jmt_BeginMarker) && !ContainsMarker(jpegFrame[index + 1], JpegMarkerTypes.jmt_NotAmarker) && jpegFrame[index + 1] != 0xFF) + if (((jpegFrame[index + 1] & 0xF0) == 0xD0) && ((jpegFrame[index + 1] & 0x0F) <= 0x07)) { - if (((jpegFrame[index + 1] & 0xF0) == 0xD0) && ((jpegFrame[index + 1] & 0x0F) <= 0x07)) - { - customData.HasRestartMarker = true; - index += 2; - } - else - { - if (currentMarker.StartPosition > 0) - { - currentMarker.MarkerBytes = jpegFrame.Skip(currentMarker.StartPosition).Take(index + 1).ToArray(); - markers.Add(currentMarker); - currentMarker = new Marker(); - } - currentMarker.Type = jpegFrame[index + 1]; - currentMarker.StartPosition = index + 2; - index += 2; - - } + customData.HasRestartMarker = true; + index += 2; } else { - index++; + if (currentMarker.StartPosition > 0) + { + currentMarker.MarkerBytes = jpegFrame.Slice(currentMarker.StartPosition, index + 1 - currentMarker.StartPosition).ToArray(); + markers.Add(currentMarker); + currentMarker = new Marker(); + } + + currentMarker.Type = jpegFrame[index + 1]; + currentMarker.StartPosition = index + 2; + index += 2; } } - if (currentMarker.StartPosition > 0) + else { - currentMarker.MarkerBytes = jpegFrame.Skip(currentMarker.StartPosition).Take(index).ToArray(); - markers.Add(currentMarker); + index++; } - - MJPEGData mjpeg = null; - foreach (var marker in markers) - { - switch (marker.Type) - { - case (byte)JpegMarkerTypes.jmt_SOF0: - case (byte)JpegMarkerTypes.jmt_SOF1: - ProcessJpegSof(marker, customData); - break; - case (byte)JpegMarkerTypes.jmt_DQT: - ProcessJpegDqt(marker, customData); - break; - case (byte)JpegMarkerTypes.jmt_SOS: - mjpeg = ProcessJpegSos(marker, customData); - break; - case (byte)JpegMarkerTypes.jmt_DRI: - ProcessJpegDri(marker, customData); - break; - } - } - return mjpeg; } - private static bool ContainsMarker(byte testValue, JpegMarkerTypes marker) + if (currentMarker.StartPosition > 0) { - return testValue == (byte)marker; + currentMarker.MarkerBytes = jpegFrame.Slice(currentMarker.StartPosition, length - currentMarker.StartPosition).ToArray(); + markers.Add(currentMarker); } - private static void ProcessJpegDri(Marker marker, MJPEG mjpeg) + var mjpeg = default(MJPEGData); + foreach (var marker in markers) { - var restartInterval = (marker.MarkerBytes[2] << 8) | marker.MarkerBytes[3]; - mjpeg.MjpegHeaderRestartMarker.RestartInterval = (ushort)IPAddress.HostToNetworkOrder(restartInterval); + switch (marker.Type) + { + case (byte)JpegMarkerTypes.jmt_SOF0: + case (byte)JpegMarkerTypes.jmt_SOF1: + ProcessJpegSof(marker, customData); + break; + case (byte)JpegMarkerTypes.jmt_DQT: + ProcessJpegDqt(marker, customData); + break; + case (byte)JpegMarkerTypes.jmt_SOS: + mjpeg = ProcessJpegSos(marker, customData); + break; + case (byte)JpegMarkerTypes.jmt_DRI: + ProcessJpegDri(marker, customData); + break; + } } - private static MJPEGData ProcessJpegSos(Marker marker, MJPEG mjpeg) - { - var hdrLength = (marker.MarkerBytes[0] << 8) | marker.MarkerBytes[1]; - var bytes = marker.MarkerBytes.Skip(hdrLength).ToArray(); - return new MJPEGData() { Data = bytes }; - } + return (mjpeg, customData); + } - private static void ProcessJpegDqt(Marker marker, MJPEG mjpeg) - { - mjpeg.MjpegHeader.q = 255; - var hdrLength = (marker.MarkerBytes[0] << 8) | marker.MarkerBytes[1]; - var precision = (byte)(marker.MarkerBytes[2] >> 4); - mjpeg.MjpegHeaderQTable.precision = precision; + private static bool ContainsMarker(byte testValue, JpegMarkerTypes marker) + { + return testValue == (byte)marker; + } - if ((hdrLength - QTableHeaderLength) % QTableBlockLength8Bit != 0) - { - return; - } - var qCount = (hdrLength - QTableHeaderLength) / QTableBlockLength8Bit; - for (var i = 0; i < qCount; i++) - { - var bytes = marker.MarkerBytes.Skip(QTableHeaderLength + i * QTableBlockLength8Bit + QTableParamsLength).Take(QTableLength8Bit).ToArray(); - mjpeg.QTables.Add(bytes); - mjpeg.MjpegHeaderQTable.SetLength(mjpeg.MjpegHeaderQTable.GetLength() + bytes.Length); - } - } + private static void ProcessJpegDri(Marker marker, MJPEG mjpeg) + { + var restartInterval = (marker.MarkerBytes[2] << 8) | marker.MarkerBytes[3]; + mjpeg.MjpegHeaderRestartMarker.RestartInterval = (ushort)IPAddress.HostToNetworkOrder(restartInterval); + } - private static void ProcessJpegSof(Marker marker, MJPEG mjpeg) + private static MJPEGData ProcessJpegSos(Marker marker, MJPEG mjpeg) + { + var hdrLength = (marker.MarkerBytes[0] << 8) | marker.MarkerBytes[1]; + var bytes = marker.MarkerBytes.AsSpan(hdrLength).ToArray(); + return new MJPEGData() { Data = bytes }; + } + + private static void ProcessJpegDqt(Marker marker, MJPEG mjpeg) + { + mjpeg.MjpegHeader.q = 255; + var hdrLength = (marker.MarkerBytes[0] << 8) | marker.MarkerBytes[1]; + var precision = (byte)(marker.MarkerBytes[2] >> 4); + mjpeg.MjpegHeaderQTable.precision = precision; + + if ((hdrLength - QTableHeaderLength) % QTableBlockLength8Bit != 0) + { + return; + } + var qCount = (hdrLength - QTableHeaderLength) / QTableBlockLength8Bit; + for (var i = 0; i < qCount; i++) { - mjpeg.MjpegHeader.type = mjpeg.HasRestartMarker ? (byte)JpegHeaderTypes.jpegHeaderType_422wRestartMarkers : (byte)JpegHeaderTypes.jpegHeaderType_422; - mjpeg.MjpegHeader.q = 255; + var bytes = marker.MarkerBytes.AsSpan(QTableHeaderLength + i * QTableBlockLength8Bit + QTableParamsLength, QTableLength8Bit).ToArray(); + mjpeg.QTables.Add(bytes); + mjpeg.MjpegHeaderQTable.SetLength(mjpeg.MjpegHeaderQTable.GetLength() + bytes.Length); + } + } - var hdrLength = (marker.MarkerBytes[0] << 8) | marker.MarkerBytes[1]; + private static void ProcessJpegSof(Marker marker, MJPEG mjpeg) + { + mjpeg.MjpegHeader.type = mjpeg.HasRestartMarker ? (byte)JpegHeaderTypes.jpegHeaderType_422wRestartMarkers : (byte)JpegHeaderTypes.jpegHeaderType_422; + mjpeg.MjpegHeader.q = 255; - var precision = marker.MarkerBytes[2]; + var hdrLength = (marker.MarkerBytes[0] << 8) | marker.MarkerBytes[1]; - var height = (marker.MarkerBytes[3] << 8) | marker.MarkerBytes[4]; + var precision = marker.MarkerBytes[2]; - var width = (marker.MarkerBytes[5] << 8) | marker.MarkerBytes[6]; + var height = (marker.MarkerBytes[3] << 8) | marker.MarkerBytes[4]; - var numberF = marker.MarkerBytes[7]; + var width = (marker.MarkerBytes[5] << 8) | marker.MarkerBytes[6]; - if (JpegNumberOfComponents == numberF) - { - byte[] horizontalSf = { 0, 0, 0 }; - byte[] verticalSf = { 0, 0, 0 }; - var startOffset = 8 + 1; - var increment = 3; + var numberF = marker.MarkerBytes[7]; - for (int index = 0; index < 3; index++) - { - var HVByte = marker.MarkerBytes[startOffset + index * increment]; - horizontalSf[index] = (byte)((HVByte >> 4) & 0x0F); - verticalSf[index] = (byte)(HVByte & 0x0F); - } - if (CheckSfValues(horizontalSf, 2, 1, 1) && CheckSfValues(verticalSf, 1, 1, 1)) - { - mjpeg.MjpegHeader.type = mjpeg.HasRestartMarker ? (byte)JpegHeaderTypes.jpegHeaderType_422wRestartMarkers : (byte)JpegHeaderTypes.jpegHeaderType_422; - } - else if (CheckSfValues(horizontalSf, 2, 1, 1) && CheckSfValues(verticalSf, 2, 1, 1)) - { - mjpeg.MjpegHeader.type = mjpeg.HasRestartMarker ? (byte)JpegHeaderTypes.jpegHeaderType_420wRestartMarkers : (byte)JpegHeaderTypes.jpegHeaderType_420; - } + if (JpegNumberOfComponents == numberF) + { + byte[] horizontalSf = { 0, 0, 0 }; + byte[] verticalSf = { 0, 0, 0 }; + var startOffset = 8 + 1; + var increment = 3; - mjpeg.MjpegHeader.SetWidth(width); - mjpeg.MjpegHeader.SetHeight(height); + for (int index = 0; index < 3; index++) + { + var HVByte = marker.MarkerBytes[startOffset + index * increment]; + horizontalSf[index] = (byte)((HVByte >> 4) & 0x0F); + verticalSf[index] = (byte)(HVByte & 0x0F); + } + if (CheckSfValues(horizontalSf, 2, 1, 1) && CheckSfValues(verticalSf, 1, 1, 1)) + { + mjpeg.MjpegHeader.type = mjpeg.HasRestartMarker ? (byte)JpegHeaderTypes.jpegHeaderType_422wRestartMarkers : (byte)JpegHeaderTypes.jpegHeaderType_422; + } + else if (CheckSfValues(horizontalSf, 2, 1, 1) && CheckSfValues(verticalSf, 2, 1, 1)) + { + mjpeg.MjpegHeader.type = mjpeg.HasRestartMarker ? (byte)JpegHeaderTypes.jpegHeaderType_420wRestartMarkers : (byte)JpegHeaderTypes.jpegHeaderType_420; } - } - private static bool CheckSfValues(byte[] sf, int yValue, int uValue, int vValue) - { - return sf[(int)JpegComponents.jpegComponent_Y] == yValue && sf[(int)JpegComponents.jpegComponent_U] == uValue && sf[(int)JpegComponents.jpegComponent_V] == vValue; + mjpeg.MjpegHeader.SetWidth(width); + mjpeg.MjpegHeader.SetHeight(height); } } + + private static bool CheckSfValues(byte[] sf, int yValue, int uValue, int vValue) + { + return sf[(int)JpegComponents.jpegComponent_Y] == yValue && sf[(int)JpegComponents.jpegComponent_U] == uValue && sf[(int)JpegComponents.jpegComponent_V] == vValue; + } } diff --git a/src/SIPSorcery/net/RTP/Packetisation/RtpVP8Header.cs b/src/SIPSorcery/net/RTP/Packetisation/RtpVP8Header.cs index 0fb1ba97dc..b9c0904796 100644 --- a/src/SIPSorcery/net/RTP/Packetisation/RtpVP8Header.cs +++ b/src/SIPSorcery/net/RTP/Packetisation/RtpVP8Header.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: RtpVP8Header.cs // // Description: Represents the RTP header to use for a VP8 encoded payload as per @@ -16,107 +16,104 @@ //----------------------------------------------------------------------------- using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SIPSorcery.Net; -namespace SIPSorcery.Net +/// +/// Representation of the VP8 RTP header as specified in RFC7741 +/// https://tools.ietf.org/html/rfc7741. +/// +public class RtpVP8Header { - /// - /// Representation of the VP8 RTP header as specified in RFC7741 - /// https://tools.ietf.org/html/rfc7741. - /// - public class RtpVP8Header + // Payload Descriptor Fields. + public bool ExtendedControlBitsPresent; // Indicated whether extended control bits are present. + public bool NonReferenceFrame; // When set indicates the frame can be discarded without affecting any other frames. + public bool StartOfVP8Partition; // Should be set when the first payload octet is the start of a new VP8 partition. + public byte PartitionIndex; // Denotes the VP8 partition index that the first payload octet of the packet belongs to. + public bool IsPictureIDPresent; + public bool IsTL0PICIDXPresent; + public bool IsTIDPresent; + public bool IsKEYIDXPresent; + public ushort PictureID; + + // Payload Header Fields. + public int FirstPartitionSize; // The size of the first partition in bytes is calculated from the 19 bits in Size0, SIze1 & Size2 as: size = Size0 + (8 x Size1) + (2048 8 Size2). + public bool ShowFrame; + public int VersionNumber; + public bool IsKeyFrame; + + public int Length { get; private set; } + + private int _payloadDescriptorLength; + public int PayloadDescriptorLength { - // Payload Descriptor Fields. - public bool ExtendedControlBitsPresent; // Indicated whether extended control bits are present. - public bool NonReferenceFrame; // When set indicates the frame can be discarded without affecting any other frames. - public bool StartOfVP8Partition; // Should be set when the first payload octet is the start of a new VP8 partition. - public byte PartitionIndex; // Denotes the VP8 partition index that the first payload octet of the packet belongs to. - public bool IsPictureIDPresent; - public bool IsTL0PICIDXPresent; - public bool IsTIDPresent; - public bool IsKEYIDXPresent; - public ushort PictureID; + get { return _payloadDescriptorLength; } + } - // Payload Header Fields. - public int FirstPartitionSize; // The size of the first partition in bytes is calculated from the 19 bits in Size0, SIze1 & Size2 as: size = Size0 + (8 x Size1) + (2048 8 Size2). - public bool ShowFrame; - public int VersionNumber; - public bool IsKeyFrame; + public RtpVP8Header() + { } - private int _length = 0; - public int Length - { - get { return _length; } - } + public static RtpVP8Header GetVP8Header(ReadOnlySpan rtpPayload) + { + var vp8Header = new RtpVP8Header(); + var payloadHeaderStartIndex = 1; - private int _payloadDescriptorLength; - public int PayloadDescriptorLength + // First byte of payload descriptor. + vp8Header.ExtendedControlBitsPresent = ((rtpPayload[0] >> 7) & 0x01) == 1; + vp8Header.StartOfVP8Partition = ((rtpPayload[0] >> 4) & 0x01) == 1; + vp8Header.Length = 1; + + // Is second byte being used. + if (vp8Header.ExtendedControlBitsPresent) { - get { return _payloadDescriptorLength; } + vp8Header.IsPictureIDPresent = ((rtpPayload[1] >> 7) & 0x01) == 1; + vp8Header.IsTL0PICIDXPresent = ((rtpPayload[1] >> 6) & 0x01) == 1; + vp8Header.IsTIDPresent = ((rtpPayload[1] >> 5) & 0x01) == 1; + vp8Header.IsKEYIDXPresent = ((rtpPayload[1] >> 4) & 0x01) == 1; + vp8Header.Length = 2; + payloadHeaderStartIndex = 2; } - public RtpVP8Header() - { } - - public static RtpVP8Header GetVP8Header(byte[] rtpPayload) + // Is the picture ID being used. + if (vp8Header.IsPictureIDPresent) { - RtpVP8Header vp8Header = new RtpVP8Header(); - int payloadHeaderStartIndex = 1; - - // First byte of payload descriptor. - vp8Header.ExtendedControlBitsPresent = ((rtpPayload[0] >> 7) & 0x01) == 1; - vp8Header.StartOfVP8Partition = ((rtpPayload[0] >> 4) & 0x01) == 1; - vp8Header._length = 1; - - // Is second byte being used. - if (vp8Header.ExtendedControlBitsPresent) + if (((rtpPayload[2] >> 7) & 0x01) == 1) { - vp8Header.IsPictureIDPresent = ((rtpPayload[1] >> 7) & 0x01) == 1; - vp8Header.IsTL0PICIDXPresent = ((rtpPayload[1] >> 6) & 0x01) == 1; - vp8Header.IsTIDPresent = ((rtpPayload[1] >> 5) & 0x01) == 1; - vp8Header.IsKEYIDXPresent = ((rtpPayload[1] >> 4) & 0x01) == 1; - vp8Header._length = 2; - payloadHeaderStartIndex = 2; + // The Picture ID is using two bytes. + vp8Header.Length = 4; + payloadHeaderStartIndex = 4; + vp8Header.PictureID = Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference((rtpPayload.Slice(2)))); // BitConverter.ToUInt16(rtpPayload, 2); } - - // Is the picture ID being used. - if (vp8Header.IsPictureIDPresent) + else { - if (((rtpPayload[2] >> 7) & 0x01) == 1) - { - // The Picture ID is using two bytes. - vp8Header._length = 4; - payloadHeaderStartIndex = 4; - vp8Header.PictureID = BitConverter.ToUInt16(rtpPayload, 2); - } - else - { - // The picture ID is using one byte. - vp8Header.PictureID = rtpPayload[2]; - vp8Header._length = 3; - payloadHeaderStartIndex = 3; - } + // The picture ID is using one byte. + vp8Header.PictureID = rtpPayload[2]; + vp8Header.Length = 3; + payloadHeaderStartIndex = 3; } + } - if (vp8Header.IsTL0PICIDXPresent) - { - vp8Header._length++; - payloadHeaderStartIndex++; - } - if (vp8Header.IsTIDPresent || vp8Header.IsKEYIDXPresent) - { - vp8Header._length++; - payloadHeaderStartIndex++; - } + if (vp8Header.IsTL0PICIDXPresent) + { + vp8Header.Length++; + payloadHeaderStartIndex++; + } + if (vp8Header.IsTIDPresent || vp8Header.IsKEYIDXPresent) + { + vp8Header.Length++; + payloadHeaderStartIndex++; + } - vp8Header._payloadDescriptorLength = payloadHeaderStartIndex; - - bool isPID0 = ((rtpPayload[0] & (1 << 2))==0) && ((rtpPayload[0] & (1 << 1))==0) && ((rtpPayload[0] & (1 << 0))==0); - if (vp8Header.StartOfVP8Partition && isPID0) - { - vp8Header.IsKeyFrame=(rtpPayload[payloadHeaderStartIndex] & (1 << 0)) == 0; - } + vp8Header._payloadDescriptorLength = payloadHeaderStartIndex; - return vp8Header; + var isPID0 = ((rtpPayload[0] & (1 << 2)) == 0) && ((rtpPayload[0] & (1 << 1)) == 0) && ((rtpPayload[0] & (1 << 0)) == 0); + if (vp8Header.StartOfVP8Partition && isPID0) + { + vp8Header.IsKeyFrame = (rtpPayload[payloadHeaderStartIndex] & (1 << 0)) == 0; } + + return vp8Header; } } diff --git a/src/SIPSorcery/net/RTP/Packetisation/RtpVideoFramer.cs b/src/SIPSorcery/net/RTP/Packetisation/RtpVideoFramer.cs index 72a5cf5a01..d0acb5e47c 100644 --- a/src/SIPSorcery/net/RTP/Packetisation/RtpVideoFramer.cs +++ b/src/SIPSorcery/net/RTP/Packetisation/RtpVideoFramer.cs @@ -16,188 +16,199 @@ //----------------------------------------------------------------------------- using System; -using System.Buffers.Binary; -using System.Linq; +using System.Buffers; +using System.Diagnostics; using Microsoft.Extensions.Logging; using SIPSorcery.net.RTP.Packetisation; using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class RtpVideoFramer { - public class RtpVideoFramer + private static readonly ILogger logger = LogFactory.CreateLogger(); + + private VideoCodecsEnum _codec; + private int _maxFrameSize; + private byte[] _currVideoFrame; + private int _currVideoFramePosn; + private AV1Depacketiser? _av1Depacketiser; + private H264Depacketiser? _h264Depacketiser; + private H265Depacketiser? _h265Depacketiser; + private MJPEGDepacketiser? _mJPEGDepacketiser; + + public RtpVideoFramer(VideoCodecsEnum codec, int maxFrameSize) { - private static readonly ILogger logger = LogFactory.CreateLogger(); - - private VideoCodecsEnum _codec; - private int _maxFrameSize; - private byte[] _currVideoFrame; - private int _currVideoFramePosn = 0; - private AV1Depacketiser _av1Depacketiser; - private H264Depacketiser _h264Depacketiser; - private H265Depacketiser _h265Depacketiser; - private MJPEGDepacketiser _mJPEGDepacketiser; - - public RtpVideoFramer(VideoCodecsEnum codec, int maxFrameSize) + if (codec is not (VideoCodecsEnum.VP8 or VideoCodecsEnum.AV1 or VideoCodecsEnum.H264 or VideoCodecsEnum.H265 or VideoCodecsEnum.JPEG)) { - if (!(codec == VideoCodecsEnum.VP8 || codec == VideoCodecsEnum.AV1 || codec == VideoCodecsEnum.H264 || codec == VideoCodecsEnum.H265 || codec == VideoCodecsEnum.JPEG)) - { - throw new NotSupportedException("The RTP video framer currently only understands VP8, AV1, H264, H265 and JPEG encoded frames."); - } - - _codec = codec; - _maxFrameSize = maxFrameSize; - _currVideoFrame = new byte[maxFrameSize]; - - if (_codec == VideoCodecsEnum.AV1) - { - _av1Depacketiser = new AV1Depacketiser(); - } - else if (_codec == VideoCodecsEnum.H264) - { - _h264Depacketiser = new H264Depacketiser(); - } - else if(_codec == VideoCodecsEnum.JPEG) - { - _mJPEGDepacketiser = new MJPEGDepacketiser(); - } - else if(_codec == VideoCodecsEnum.H265) - { - _h265Depacketiser = new H265Depacketiser(); - } + throw new NotSupportedException("The RTP video framer currently only understands VP8, AV1, H264, H265 and JPEG encoded frames."); } - public byte[] GotRtpPacket(RTPPacket rtpPacket) - { - var payload = rtpPacket.GetPayloadBytes(); - - var hdr = rtpPacket.Header; + _codec = codec; + _maxFrameSize = maxFrameSize; + _currVideoFrame = new byte[maxFrameSize]; - if (_codec == VideoCodecsEnum.VP8) - { - //logger.LogDebug("rtp VP8 video, seqnum {SequenceNumber}, ts {Timestamp}, marker {MarkerBit}, payload {PayloadLength}.", hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, payload.Length); + if (_codec == VideoCodecsEnum.AV1) + { + _av1Depacketiser = new AV1Depacketiser(); + } + else if (_codec == VideoCodecsEnum.H264) + { + _h264Depacketiser = new H264Depacketiser(); + } + else if (_codec == VideoCodecsEnum.JPEG) + { + _mJPEGDepacketiser = new MJPEGDepacketiser(); + } + else if (_codec == VideoCodecsEnum.H265) + { + _h265Depacketiser = new H265Depacketiser(); + } + } - if (_currVideoFramePosn + payload.Length >= _maxFrameSize) - { - // Something has gone very wrong. Clear the buffer. - _currVideoFramePosn = 0; - } + public bool GotRtpPacket(IBufferWriter bufferWriter, RTPPacket rtpPacket) + { + var payload = rtpPacket.Payload.Span; + var hdr = rtpPacket.Header; - // New frames must have the VP8 Payload Descriptor Start bit set. - // The tracking of the current video frame position is to deal with a VP8 frame being split across multiple RTP packets - // as per https://tools.ietf.org/html/rfc7741#section-4.4. - if (_currVideoFramePosn > 0 || (payload[0] & 0x10) > 0) + switch (_codec) + { + case VideoCodecsEnum.VP8: { - RtpVP8Header vp8Header = RtpVP8Header.GetVP8Header(payload); + //logger.LogDebug("rtp VP8 video, seqnum {SequenceNumber}, ts {Timestamp}, marker {MarkerBit}, payload {PayloadLength}.", hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, payload.Length); - Buffer.BlockCopy(payload, vp8Header.Length, _currVideoFrame, _currVideoFramePosn, payload.Length - vp8Header.Length); - _currVideoFramePosn += payload.Length - vp8Header.Length; - - if (rtpPacket.Header.MarkerBit > 0) + if (_currVideoFramePosn + payload.Length >= _maxFrameSize) { - var frame = _currVideoFrame.Take(_currVideoFramePosn).ToArray(); - + // Something has gone very wrong. Clear the buffer. _currVideoFramePosn = 0; + } - return frame; + // New frames must have the VP8 Payload Descriptor Start bit set. + // The tracking of the current video frame position is to deal with a VP8 frame being split across multiple RTP packets + // as per https://tools.ietf.org/html/rfc7741#section-4.4. + if (_currVideoFramePosn > 0 || (payload[0] & 0x10) > 0) + { + var vp8Header = RtpVP8Header.GetVP8Header(payload); + + payload.Slice(vp8Header.Length, payload.Length - vp8Header.Length).CopyTo(_currVideoFrame.AsSpan(_currVideoFramePosn)); + _currVideoFramePosn += payload.Length - vp8Header.Length; + + if (rtpPacket.Header.MarkerBit > 0) + { + var frameSpan = _currVideoFrame.AsSpan(0, _currVideoFramePosn); + bufferWriter.Write(frameSpan); + _currVideoFramePosn = 0; + return true; + } + } + else + { + logger.LogRtpVideoFramerError(); } } - else - { - logger.LogWarning("Discarding RTP packet, VP8 header Start bit not set."); - //logger.LogWarning("rtp video, seqnum {SequenceNumber}, ts {Timestamp}, marker {MarkerBit}, payload {PayloadLength}.", hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, payload.Length); - } - } - else if (_codec == VideoCodecsEnum.AV1) - { - var frameStream = _av1Depacketiser.ProcessRTPPayload(payload, hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, out bool isKeyFrame); - if (frameStream != null) + break; + case VideoCodecsEnum.AV1: { - return frameStream.ToArray(); + Debug.Assert(_av1Depacketiser is { }); + if (_av1Depacketiser.ProcessRTPPayload(bufferWriter, payload, hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, out var isKeyFrame)) + { + return true; + } } - } - else if (_codec == VideoCodecsEnum.H264) - { - var frameStream = _h264Depacketiser.ProcessRTPPayload(payload, hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, out bool isKeyFrame); - if (frameStream != null) + break; + case VideoCodecsEnum.H264: { - return frameStream.ToArray(); + Debug.Assert(_h264Depacketiser is { }); + if (_h264Depacketiser.ProcessRTPPayload(bufferWriter, payload, hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, out var isKeyFrame)) + { + return true; + } } - } - else if (_codec == VideoCodecsEnum.H265) - { - var frameStream = _h265Depacketiser.ProcessRTPPayload(payload, hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, out bool isKeyFrame); - if (frameStream != null) + break; + case VideoCodecsEnum.H265: { - return frameStream.ToArray(); + Debug.Assert(_h265Depacketiser is { }); + if (_h265Depacketiser.ProcessRTPPayload(bufferWriter, payload, hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, out var isKeyFrame)) + { + return true; + } } - } - else if(_codec == VideoCodecsEnum.JPEG) - { - var frameStream = _mJPEGDepacketiser.ProcessRTPPayload(payload, hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, out bool isKeyFrame); - if (frameStream != null) + + break; + case VideoCodecsEnum.JPEG: { - return frameStream.ToArray(); + Debug.Assert(_mJPEGDepacketiser is { }); + if (_mJPEGDepacketiser.ProcessRTPPayload(bufferWriter, payload, hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, out var isKeyFrame)) + { + return true; + } } - } - else - { - logger.LogWarning("rtp unknown video, seqnum {SequenceNumber}, ts {Timestamp}, marker {MarkerBit}, payload {PayloadLength}.", hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, payload.Length); - } - return null; + break; + default: + logger.LogRtpUnknownVideo(hdr.SequenceNumber, hdr.Timestamp, hdr.MarkerBit, payload.Length); + break; } - /// - /// Utility function to create RtpJpegHeader either for initial packet or template for further packets - /// - /// - /// 0 1 2 3 - /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type-specific | Fragment Offset | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type | Q | Width | Height | - /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// - /// - /// - /// - /// - /// - /// - public static byte[] CreateLowQualityRtpJpegHeader(uint fragmentOffset, int quality, int width, int height) - { - byte[] rtpJpegHeader = new byte[8] { 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00 }; - - // Byte 0: Type specific - //http://tools.ietf.org/search/rfc2435#section-3.1.1 + return false; + } - // Bytes 1 to 3: Three byte fragment offset - //http://tools.ietf.org/search/rfc2435#section-3.1.2 + /// + /// Utility function to create RtpJpegHeader either for initial packet or template for further packets + /// + /// + /// 0 1 2 3 + /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Type-specific | Fragment Offset | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Type | Q | Width | Height | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// + /// + /// + /// + /// + /// + /// + public static void WriteLowQualityRtpJpegHeader(Span destination, uint fragmentOffset, int quality, int width, int height) + { + if (destination.Length < 8) + { + throw new ArgumentException("Destination span must be at least 8 bytes long.", nameof(destination)); + } - var offsetBytes = new byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(offsetBytes, fragmentOffset); - rtpJpegHeader[1] = offsetBytes[2]; - rtpJpegHeader[2] = offsetBytes[1]; - rtpJpegHeader[3] = offsetBytes[0]; + // Byte 0: Type-specific (always 0) + // https://www.rfc-editor.org/rfc/rfc2435#section-3.1.1 + destination[0] = 0x00; - // Byte 4: JPEG Type. - //http://tools.ietf.org/search/rfc2435#section-3.1.3 + // Bytes 1-3: 24-bit fragment offset in big-endian order + // https://www.rfc-editor.org/rfc/rfc2435#section-3.1.2 + var offset = fragmentOffset; + destination[1] = (byte)((offset >> 16) & 0xFF); + destination[2] = (byte)((offset >> 8) & 0xFF); + destination[3] = (byte)(offset & 0xFF); - //Byte 5: http://tools.ietf.org/search/rfc2435#section-3.1.4 (Q) - rtpJpegHeader[5] = (byte)quality; + // Byte 4: JPEG Type (always 1 for low quality) + // https://www.rfc-editor.org/rfc/rfc2435#section-3.1.3 + destination[4] = 0x01; - // Byte 6: http://tools.ietf.org/search/rfc2435#section-3.1.5 (Width) - rtpJpegHeader[6] = (byte)(width / 8); + // Byte 5: Quality factor (Q) + // https://www.rfc-editor.org/rfc/rfc2435#section-3.1.4 + destination[5] = (byte)quality; - // Byte 7: http://tools.ietf.org/search/rfc2435#section-3.1.6 (Height) - rtpJpegHeader[7] = (byte)(height / 8); + // Byte 6: Width in 8-pixel blocks + // https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5 + destination[6] = (byte)(width / 8); - return rtpJpegHeader; - } + // Byte 7: Height in 8-pixel blocks + // https://www.rfc-editor.org/rfc/rfc2435#section-3.1.6 + destination[7] = (byte)(height / 8); } + } diff --git a/src/SIPSorcery/net/RTP/RTPChannel.cs b/src/SIPSorcery/net/RTP/RTPChannel.cs index da855db946..2f6d9c3682 100644 --- a/src/SIPSorcery/net/RTP/RTPChannel.cs +++ b/src/SIPSorcery/net/RTP/RTPChannel.cs @@ -18,7 +18,8 @@ //----------------------------------------------------------------------------- using System; -using System.Linq; +using System.Buffers; +using System.Diagnostics; using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging; @@ -40,26 +41,24 @@ public enum RTPChannelSocketsEnum public class RTPChannel : IDisposable { private static readonly ILogger logger = LogFactory.CreateLogger(); - protected UdpReceiver m_rtpReceiver; - private Socket m_controlSocket; - protected UdpReceiver m_controlReceiver; - private bool m_rtpReceiverStarted = false; - private bool m_controlReceiverStarted = false; + protected readonly SocketUdpConnection? m_controlConnection; + private bool m_rtpReceiverStarted; + private bool m_controlReceiverStarted; private bool m_isClosed; - public Socket RtpSocket { get; private set; } + public SocketUdpConnection? RtpConnection { get; } /// /// The last remote end point an RTP packet was sent to or received from. Used for /// reporting purposes only. /// - protected IPEndPoint LastRtpDestination { get; set; } + protected IPEndPoint? LastRtpDestination { get; set; } /// /// The last remote end point an RTCP packet was sent to or received from. Used for /// reporting purposes only. /// - internal IPEndPoint LastControlDestination { get; private set; } + internal IPEndPoint? LastControlDestination { get; private set; } /// /// The local port we are listening for RTP (and whatever else is multiplexed) packets on. @@ -71,13 +70,6 @@ public class RTPChannel : IDisposable /// public IPEndPoint RTPLocalEndPoint { get; private set; } - [Obsolete("This property has been renamed to RTPSrflxEndPoint, it will be removed in a future release.", false)] - public IPEndPoint RTPDynamicNATEndPoint - { - get => RTPSrflxEndPoint; - set => RTPSrflxEndPoint = value; - } - /// /// tl;dr Allows the setting of the RTP channel's public endpoint for SDP offers and answers. By itself it's /// typically not enough to deal with NAT @@ -90,7 +82,7 @@ public IPEndPoint RTPDynamicNATEndPoint /// to be enough by itself. The remote party will also need some way to take advantage of knowing the public IP address such /// as by using it with a TURN relay allocation to set the permissions for this endpoint address. /// - public IPEndPoint RTPSrflxEndPoint { get; set; } + public IPEndPoint? RTPSrflxEndPoint { get; set; } /// /// The local port we are listening for RTCP packets on. @@ -100,7 +92,7 @@ public IPEndPoint RTPDynamicNATEndPoint /// /// The local end point the control socket is listening on. /// - public IPEndPoint ControlLocalEndPoint { get; private set; } + public IPEndPoint? ControlLocalEndPoint { get; private set; } /// /// Returns true if the RTP socket supports dual mode IPv4 and IPv6. If the control @@ -110,9 +102,11 @@ public bool IsDualMode { get { - if (RtpSocket != null && RtpSocket.AddressFamily == AddressFamily.InterNetworkV6) + Debug.Assert(RtpConnection is { }); + + if (RtpConnection.Socket is { AddressFamily: AddressFamily.InterNetworkV6 } socket) { - return RtpSocket.DualMode; + return socket.DualMode; } else { @@ -129,9 +123,9 @@ public bool IsClosed /// /// int localPort, IPEndPoint remoteEndPoint, byte[] packet. /// - public event Action OnRTPDataReceived; - public event Action OnControlDataReceived; - public event Action OnClosed; + public event Action>? OnRTPDataReceived; + public event Action>? OnControlDataReceived; + public event Action? OnClosed; /// /// This event gets fired when a STUN message is received by this channel. @@ -141,7 +135,7 @@ public bool IsClosed /// - IPEndPoint: The remote end point the STUN message was received from. /// - bool: True if the message was received via a TURN server relay. /// - public event Action OnStunMessageReceived; + public event Action? OnStunMessageReceived; /// /// Creates a new RTP channel. The RTP and optionally RTCP sockets will be bound in the constructor. @@ -153,24 +147,46 @@ public bool IsClosed /// the RTP and control sockets to. If left empty then the IPv6 any address will be used if IPv6 is supported /// and fallback to the IPv4 any address. /// Optional. The specific port to attempt to bind the RTP port on. - public RTPChannel(bool createControlSocket, IPAddress bindAddress, int bindPort = 0, PortRange rtpPortRange = null) + public RTPChannel(bool createControlSocket, IPAddress? bindAddress, int bindPort = 0, PortRange? rtpPortRange = null) { - NetServices.CreateRtpSocket(createControlSocket, bindAddress, bindPort, rtpPortRange, out var rtpSocket, out m_controlSocket); + NetServices.CreateRtpSocket(createControlSocket, bindAddress, bindPort, rtpPortRange, out var rtpSocket, out var controlSocket); - if (rtpSocket == null) + if (rtpSocket is null) { - throw new ApplicationException("The RTP channel was not able to create an RTP socket."); + throw new SipSorceryException("The RTP channel was not able to create an RTP socket."); } - else if (createControlSocket && m_controlSocket == null) + else if (createControlSocket && controlSocket is null) { - throw new ApplicationException("The RTP channel was not able to create a Control socket."); + throw new SipSorceryException("The RTP channel was not able to create a Control socket."); } - RtpSocket = rtpSocket; - RTPLocalEndPoint = RtpSocket.LocalEndPoint as IPEndPoint; + var localEndPoint = rtpSocket.LocalEndPoint as IPEndPoint; + Debug.Assert(localEndPoint is { }); + RTPLocalEndPoint = localEndPoint; RTPPort = RTPLocalEndPoint.Port; - ControlLocalEndPoint = (m_controlSocket != null) ? m_controlSocket.LocalEndPoint as IPEndPoint : null; - ControlPort = (m_controlSocket != null) ? ControlLocalEndPoint.Port : 0; + + if (controlSocket is { }) + { + ControlLocalEndPoint = controlSocket.LocalEndPoint as IPEndPoint; + Debug.Assert(ControlLocalEndPoint is { }); + ControlPort = ControlLocalEndPoint.Port; + } + else + { + ControlLocalEndPoint = null; + ControlPort = 0; + } + + RtpConnection = new SocketUdpConnection(rtpSocket); + RtpConnection.OnPacketReceived += OnRTPPacketReceived; + RtpConnection.OnClosed += Close; + + if (controlSocket is { }) + { + m_controlConnection = new SocketUdpConnection(controlSocket); + m_controlConnection.OnPacketReceived += OnControlPacketReceived; + m_controlConnection.OnClosed += Close; + } } /// @@ -191,12 +207,11 @@ public void StartRtpReceiver() { m_rtpReceiverStarted = true; - logger.LogDebug("RTPChannel for {LocalEndPoint} started.", RtpSocket.LocalEndPoint); + Debug.Assert(RtpConnection is { }); - m_rtpReceiver = new UdpReceiver(RtpSocket); - m_rtpReceiver.OnPacketReceived += OnRTPPacketReceived; - m_rtpReceiver.OnClosed += Close; - m_rtpReceiver.BeginReceiveFrom(); + logger.LogRtpChannelStarted(RtpConnection.Socket.LocalEndPoint); + + RtpConnection.BeginReceiveFrom(); } } @@ -205,46 +220,43 @@ public void StartRtpReceiver() /// public void StartControlReceiver() { - if (!m_controlReceiverStarted && m_controlSocket != null) + if (!m_controlReceiverStarted && m_controlConnection is { }) { m_controlReceiverStarted = true; - m_controlReceiver = new UdpReceiver(m_controlSocket); - m_controlReceiver.OnPacketReceived += OnControlPacketReceived; - m_controlReceiver.OnClosed += Close; - m_controlReceiver.BeginReceiveFrom(); + m_controlConnection.BeginReceiveFrom(); } } /// /// Closes the session's RTP and control ports. /// - public void Close(string reason) + public void Close(string? reason) { if (!m_isClosed) { try { - string closeReason = reason ?? "normal"; + var closeReason = reason ?? "normal"; - if (m_controlReceiver == null) + if (m_controlConnection is null) { - logger.LogDebug("RTPChannel closing, RTP receiver on port {RTPPort}. Reason: {closeReason}.", RTPPort, closeReason); + logger.LogRtpChannelClosingRtpOnly(RTPPort, closeReason); } else { - logger.LogDebug("RTPChannel closing, RTP receiver on port {RTPPort}, Control receiver on port {ControlPort}. Reason: {closeReason}.", RTPPort, ControlPort, closeReason); + logger.LogRtpChannelClosing(RTPPort, ControlPort, closeReason); } m_isClosed = true; - m_rtpReceiver?.Close(null); - m_controlReceiver?.Close(null); + RtpConnection?.Close(null); + m_controlConnection?.Close(null); OnClosed?.Invoke(closeReason); } catch (Exception excp) { - logger.LogError(excp, "Exception RTPChannel.Close. {ErrorMessage}", excp); + logger.LogRtpSessionClose(excp.Message, excp); } } } @@ -255,42 +267,45 @@ public void Close(string reason) /// The socket to send on. Can be the RTP or Control socket. /// The destination end point to send to. /// The data to send. + /// The onwer of the memory. /// The result of initiating the send. This result does not reflect anything about /// whether the remote party received the packet or not. - public virtual SocketError Send(RTPChannelSocketsEnum sendOn, IPEndPoint dstEndPoint, byte[] buffer) + public virtual SocketError Send(RTPChannelSocketsEnum sendOn, IPEndPoint dstEndPoint, ReadOnlyMemory buffer, IDisposable? memoryOwner = null) { if (m_isClosed) { return SocketError.Disconnecting; } - else if (dstEndPoint == null) + else if (dstEndPoint is null) { - throw new ArgumentException("dstEndPoint", "An empty destination was specified to Send in RTPChannel."); + throw new ArgumentException("An empty destination was specified to Send in RTPChannel.", nameof(dstEndPoint)); } - else if (buffer == null || buffer.Length == 0) + else if (buffer.IsEmpty) { - throw new ArgumentException("buffer", "The buffer must be set and non empty for Send in RTPChannel."); + throw new ArgumentException("The buffer must be set and non empty for Send in RTPChannel.", nameof(buffer)); } else if (IPAddress.Any.Equals(dstEndPoint.Address) || IPAddress.IPv6Any.Equals(dstEndPoint.Address)) { - logger.LogWarning("The destination address for Send in RTPChannel cannot be {Address}.", dstEndPoint.Address); + logger.LogRtpDestinationAddressInvalid(dstEndPoint.Address); return SocketError.DestinationAddressRequired; } else { try { - Socket sendSocket = RtpSocket; + Debug.Assert(RtpConnection is { }); + + var connection = RtpConnection; if (sendOn == RTPChannelSocketsEnum.Control) { LastControlDestination = dstEndPoint; - if (m_controlSocket == null) + if (m_controlConnection is null) { - throw new ApplicationException("RTPChannel was asked to send on the control socket but none exists."); + throw new SipSorceryException("RTPChannel was asked to send on the control socket but none exists."); } else { - sendSocket = m_controlSocket; + connection = m_controlConnection; } } else @@ -299,19 +314,19 @@ public virtual SocketError Send(RTPChannelSocketsEnum sendOn, IPEndPoint dstEndP } //Prevent Send to IPV4 while socket is IPV6 (Mono Error) - if (dstEndPoint.AddressFamily == AddressFamily.InterNetwork && sendSocket.AddressFamily != dstEndPoint.AddressFamily) + if (dstEndPoint.AddressFamily == AddressFamily.InterNetwork && connection.Socket.AddressFamily != dstEndPoint.AddressFamily) { dstEndPoint = new IPEndPoint(dstEndPoint.Address.MapToIPv6(), dstEndPoint.Port); } //Fix ReceiveFrom logic if any previous exception happens - if (!m_rtpReceiver.IsRunningReceive && !m_rtpReceiver.IsClosed) + if (!connection.IsRunningReceive && !connection.IsClosed) { - m_rtpReceiver.BeginReceiveFrom(); + connection.BeginReceiveFrom(); } - sendSocket.BeginSendTo(buffer, 0, buffer.Length, SocketFlags.None, dstEndPoint, EndSendTo, sendSocket); - return SocketError.Success; + return connection.SendTo(dstEndPoint, buffer, memoryOwner); + } catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored. { @@ -323,7 +338,7 @@ public virtual SocketError Send(RTPChannelSocketsEnum sendOn, IPEndPoint dstEndP } catch (Exception excp) { - logger.LogError(excp, "Exception RTPChannel.Send. {ErrorMesssage}", excp.Message); + logger.LogRtpChannelGeneralException(excp); return SocketError.Fault; } } @@ -337,49 +352,23 @@ public virtual SocketError Send(RTPChannelSocketsEnum sendOn, IPEndPoint dstEndP /// The TURN server end point to send the relayed request to. public SocketError SendRelay(RTPChannelSocketsEnum sendOn, IPEndPoint dstEndPoint, byte[] buffer, IPEndPoint relayEndPoint) { - STUNMessage sendReq = new STUNMessage(STUNMessageTypesEnum.SendIndication); + var sendReq = new STUNMessage(STUNMessageTypesEnum.SendIndication); sendReq.AddXORPeerAddressAttribute(dstEndPoint.Address, dstEndPoint.Port); sendReq.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Data, buffer)); - var request = sendReq.ToByteBuffer(null, false); - var sendResult = Send(sendOn, relayEndPoint, request); + var messageSize = sendReq.GetByteBufferSize(null, false); + var rentedMemory = MemoryPool.Shared.Rent(messageSize); + sendReq.WriteToBuffer(rentedMemory.Memory.Span, default, false); + var sendResult = Send(sendOn, relayEndPoint, rentedMemory.Memory.Slice(0, messageSize), rentedMemory); if (sendResult != SocketError.Success) { - logger.LogWarning("{caller} error sending TURN relay request to TURN server at {RelayEndPoint}. {SendResult}.", nameof(RTPChannel), relayEndPoint, sendResult); + logger.LogTurnRelaySendError(nameof(RTPChannel), relayEndPoint, sendResult); } return sendResult; } - /// - /// Ends an async send on one of the channel's sockets. - /// - /// The async result to complete the send with. - private void EndSendTo(IAsyncResult ar) - { - try - { - Socket sendSocket = (Socket)ar.AsyncState; - int bytesSent = sendSocket.EndSendTo(ar); - } - catch (SocketException sockExcp) - { - // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during - // normal RTP operation. For example: - // - the RTP connection may start sending before the remote socket starts listening, - // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old - // or new socket during the transition. - logger.LogWarning(sockExcp, "SocketException RTPChannel EndSendTo ({SocketErrorCode}). {Message}", sockExcp.ErrorCode, sockExcp.Message); - } - catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored. - { } - catch (Exception excp) - { - logger.LogError(excp, "Exception RTPChannel EndSendTo. {Message}", excp.Message); - } - } - /// /// Event handler for packets received on the RTP UDP socket. This channel will detect STUN messages /// and extract STUN messages to deal with ICE connectivity checks and TURN relays. @@ -388,33 +377,44 @@ private void EndSendTo(IAsyncResult ar) /// The local port it was received on. /// The remote end point of the sender. /// The raw packet received (note this may not be RTP if other protocols are being multiplexed). - protected virtual void OnRTPPacketReceived(UdpReceiver receiver, int localPort, IPEndPoint remoteEndPoint, byte[] packet) + protected virtual void OnRTPPacketReceived(SocketConnection receiver, int localPort, IPEndPoint? remoteEndPoint, ReadOnlyMemory packet) { - if (packet?.Length > 0) + if (!packet.IsEmpty) { - //logger.LogDebug("RTPChannel received {Length} bytes from {RemoteEndPoint}.", packet.Length, remoteEndPoint); + Debug.Assert(remoteEndPoint is { }); - bool wasRelayed = false; + var wasRelayed = false; - if (packet[0] == 0x00 && packet[1] == 0x17) + if (packet.Span[0] == 0x00 && packet.Span[1] == 0x17) { wasRelayed = true; // TURN data indication. Extract the data payload and adjust the end point. - var dataIndication = STUNMessage.ParseSTUNMessage(packet, packet.Length); - var dataAttribute = dataIndication.Attributes.Where(x => x.AttributeType == STUNAttributeTypesEnum.Data).FirstOrDefault(); - packet = dataAttribute?.Value; + var dataIndication = STUNMessage.ParseSTUNMessage(packet.Span); + Debug.Assert(dataIndication is not null); - var peerAddrAttribute = dataIndication.Attributes.Where(x => x.AttributeType == STUNAttributeTypesEnum.XORPeerAddress).FirstOrDefault(); - remoteEndPoint = (peerAddrAttribute as STUNXORAddressAttribute)?.GetIPEndPoint(); + foreach (var attribute in dataIndication.Attributes) + { + switch (attribute.AttributeType) + { + case STUNAttributeTypesEnum.Data: + packet = attribute?.Value ?? default; + break; + case STUNAttributeTypesEnum.XORPeerAddress: + remoteEndPoint = (attribute as STUNXORAddressAttribute)?.GetIPEndPoint(); + break; + } + } } + Debug.Assert(remoteEndPoint is not null); LastRtpDestination = remoteEndPoint; - if (packet[0] == 0x00 || packet[0] == 0x01) + if (packet.Span[0] is 0x00 or 0x01) { // STUN packet. - var stunMessage = STUNMessage.ParseSTUNMessage(packet, packet.Length); + var stunMessage = STUNMessage.ParseSTUNMessage(packet.Span); + Debug.Assert(stunMessage is not null); OnStunMessageReceived?.Invoke(stunMessage, remoteEndPoint, wasRelayed); } else @@ -431,17 +431,13 @@ protected virtual void OnRTPPacketReceived(UdpReceiver receiver, int localPort, /// The local port it was received on. /// The remote end point of the sender. /// The raw packet received which should always be an RTCP packet. - private void OnControlPacketReceived(UdpReceiver receiver, int localPort, IPEndPoint remoteEndPoint, byte[] packet) + private void OnControlPacketReceived(SocketConnection receiver, int localPort, IPEndPoint? remoteEndPoint, ReadOnlyMemory packet) { + Debug.Assert(remoteEndPoint is { }); LastControlDestination = remoteEndPoint; OnControlDataReceived?.Invoke(localPort, remoteEndPoint, packet); } - protected void InvokeOnStunMessageReceived(STUNMessage stunMessage, IPEndPoint remoteEndPoint, bool wasRelayed) - { - OnStunMessageReceived?.Invoke(stunMessage, remoteEndPoint, wasRelayed); - } - protected virtual void Dispose(bool disposing) { Close(null); @@ -449,6 +445,7 @@ protected virtual void Dispose(bool disposing) public void Dispose() { - Close(null); + Dispose(true); + GC.SuppressFinalize(this); } } diff --git a/src/SIPSorcery/net/RTP/RTPEvent.cs b/src/SIPSorcery/net/RTP/RTPEvent.cs index 2456512c22..c007f9802f 100644 --- a/src/SIPSorcery/net/RTP/RTPEvent.cs +++ b/src/SIPSorcery/net/RTP/RTPEvent.cs @@ -15,95 +15,93 @@ using System; using System.Buffers.Binary; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class RTPEvent { - public class RTPEvent + public const int DTMF_PACKET_LENGTH = 4; // The length of an RTP DTMF event packet. + public const ushort DEFAULT_VOLUME = 10; + public const int DUPLICATE_COUNT = 3; // The number of packets to duplicate for the start and end of an event. + + /// + /// The ID for the event. For a DTMF tone this is the digit/letter to represent. + /// + public byte EventID { get; private set; } + + /// + /// If true the end of event flag will be set. + /// + public bool EndOfEvent { get; set; } + + /// + /// The volume level to set. + /// + public ushort Volume { get; private set; } + + /// + /// The duration for the full event. + /// + public ushort TotalDuration { get; private set; } + + /// + /// The duration of the current event payload. This value is set in the RTP event data payload. + /// + public ushort Duration { get; set; } + + /// + /// The ID of the event payload type. This gets set in the RTP header. + /// + public int PayloadTypeID { get; private set; } + + /// + /// Create a new RTP event object. + /// + /// The ID for the event. For a DTMF tone this is the digit/letter to represent. + /// If true the end of event flag will be set. + /// The volume level to set. + /// The event duration. + /// The ID of the event payload type. This gets set in the RTP header. + public RTPEvent(byte eventID, bool endOfEvent, ushort volume, ushort totalDuration, int payloadTypeID) { - public const int DTMF_PACKET_LENGTH = 4; // The length of an RTP DTMF event packet. - public const ushort DEFAULT_VOLUME = 10; - public const int DUPLICATE_COUNT = 3; // The number of packets to duplicate for the start and end of an event. - - /// - /// The ID for the event. For a DTMF tone this is the digit/letter to represent. - /// - public byte EventID { get; private set; } - - /// - /// If true the end of event flag will be set. - /// - public bool EndOfEvent { get; set; } - - /// - /// The volume level to set. - /// - public ushort Volume { get; private set; } - - /// - /// The duration for the full event. - /// - public ushort TotalDuration { get; private set; } - - /// - /// The duration of the current event payload. This value is set in the RTP event data payload. - /// - public ushort Duration { get; set; } - - /// - /// The ID of the event payload type. This gets set in the RTP header. - /// - public int PayloadTypeID { get; private set; } - - /// - /// Create a new RTP event object. - /// - /// The ID for the event. For a DTMF tone this is the digit/letter to represent. - /// If true the end of event flag will be set. - /// The volume level to set. - /// The event duration. - /// The ID of the event payload type. This gets set in the RTP header. - public RTPEvent(byte eventID, bool endOfEvent, ushort volume, ushort totalDuration, int payloadTypeID) - { - EventID = eventID; - EndOfEvent = endOfEvent; - Volume = volume; - TotalDuration = totalDuration; - PayloadTypeID = payloadTypeID; - } - - /// - /// Gets the raw buffer for the event. - /// - /// A raw byte buffer for the event. - public byte[] GetEventPayload() - { - byte[] payload = new byte[DTMF_PACKET_LENGTH]; + EventID = eventID; + EndOfEvent = endOfEvent; + Volume = volume; + TotalDuration = totalDuration; + PayloadTypeID = payloadTypeID; + } - payload[0] = EventID; - payload[1] = (byte)(EndOfEvent ? 0x80 : 0x00); - payload[1] += (byte)(Volume & 0xcf); // The Volume field uses 6 bits. + public int GetEventPayloadLength() => DTMF_PACKET_LENGTH; - BinaryPrimitives.WriteUInt16BigEndian(payload.AsSpan(2), Duration); + /// + /// Gets the raw buffer for the event. + /// + /// A raw byte buffer for the event. + public void WriteEventPayload(Span destination) + { + destination[0] = EventID; + destination[1] = (byte)(EndOfEvent ? 0x80 : 0x00); + destination[1] += (byte)(Volume & 0x3F); // Volume uses 6 bits (0-63) - return payload; - } + BinaryPrimitives.WriteUInt16BigEndian(destination.Slice(2, 2), Duration); + } - /// - /// Extract and load an RTP Event from a packet buffer. - /// - /// The packet buffer containing the RTP Event. - public RTPEvent(byte[] packet) + /// + /// Extract and load an RTP Event from a packet buffer. + /// + /// The packet buffer containing the RTP Event. + public RTPEvent(ReadOnlySpan packet) + { + if (packet.Length < DTMF_PACKET_LENGTH) { - if (packet.Length < DTMF_PACKET_LENGTH) - { - throw new ApplicationException("The packet did not contain the minimum number of bytes for an RTP Event packet."); - } + throw new SipSorceryException("The packet did not contain the minimum number of bytes for an RTP Event packet."); + } - EventID = packet[0]; - EndOfEvent = (packet[1] & 0x80) > 1; - Volume = (ushort)(packet[1] & 0xcf); + EventID = packet[0]; + EndOfEvent = (packet[1] & 0x80) > 1; + Volume = (ushort)(packet[1] & 0xcf); - Duration = BinaryPrimitives.ReadUInt16BigEndian(packet.AsSpan(2)); - } + Duration = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(2)); } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/net/RTP/RTPHeader.cs b/src/SIPSorcery/net/RTP/RTPHeader.cs index ffec095c62..37729f94b4 100644 --- a/src/SIPSorcery/net/RTP/RTPHeader.cs +++ b/src/SIPSorcery/net/RTP/RTPHeader.cs @@ -17,12 +17,11 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; using SIPSorcery.Sys; namespace SIPSorcery.Net; -public class RTPHeader +public class RTPHeader : IByteSerializable { public const int MIN_HEADER_LEN = 12; @@ -31,18 +30,18 @@ public class RTPHeader public const int TWO_BYTE_EXTENSION_PROFILE = 0x1000; public int Version = RTP_VERSION; // 2 bits. - public int PaddingFlag = 0; // 1 bit. - public int HeaderExtensionFlag = 0; // 1 bit. - public int CSRCCount = 0; // 4 bits - public int MarkerBit = 0; // 1 bit. - public int PayloadType = 0; // 7 bits. - public UInt16 SequenceNumber; // 16 bits. + public int PaddingFlag; // 1 bit. + public int HeaderExtensionFlag; // 1 bit. + public int CSRCCount; // 4 bits + public int MarkerBit; // 1 bit. + public int PayloadType; // 7 bits. + public ushort SequenceNumber; // 16 bits. public uint Timestamp; // 32 bits. public uint SyncSource; // 32 bits. - public int[] CSRCList; // 32 bits. - public UInt16 ExtensionProfile; // 16 bits. - public UInt16 ExtensionLength; // 16 bits, length of the header extensions in 32 bit words. - public byte[] ExtensionPayload; + public int[]? CSRCList; // 32 bits. + public ushort ExtensionProfile; // 16 bits. + public ushort ExtensionLength; // 16 bits, length of the header extensions in 32 bit words. + public byte[]? ExtensionPayload; public int PayloadSize; public byte PaddingCount; @@ -63,18 +62,17 @@ public RTPHeader() /// Extract and load the RTP header from an RTP packet. /// /// - public RTPHeader(byte[] packet) + public RTPHeader(ReadOnlySpan packet) { if (packet.Length < MIN_HEADER_LEN) { - throw new ApplicationException("The packet did not contain the minimum number of bytes for an RTP header packet."); + throw new SipSorceryException("The packet did not contain the minimum number of bytes for an RTP header packet."); } - UInt16 firstWord = BinaryPrimitives.ReadUInt16BigEndian(packet); - SequenceNumber = BinaryPrimitives.ReadUInt16BigEndian(packet.AsSpan(2)); - Timestamp = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(4)); - SyncSource = BinaryPrimitives.ReadUInt32BigEndian(packet.AsSpan(8)); - + var firstWord = BinaryPrimitives.ReadUInt16BigEndian(packet); + SequenceNumber = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(2)); + Timestamp = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(4)); + SyncSource = BinaryPrimitives.ReadUInt32BigEndian(packet.Slice(8)); Version = firstWord >> 14; PaddingFlag = (firstWord >> 13) & 0x1; @@ -84,20 +82,19 @@ public RTPHeader(byte[] packet) MarkerBit = (firstWord >> 7) & 0x1; PayloadType = firstWord & 0x7f; - int headerExtensionLength = 0; - int headerAndCSRCLength = 12 + 4 * CSRCCount; + var headerExtensionLength = 0; + var headerAndCSRCLength = 12 + 4 * CSRCCount; if (HeaderExtensionFlag == 1 && (packet.Length >= (headerAndCSRCLength + 4))) { - ExtensionProfile = BinaryPrimitives.ReadUInt16BigEndian(packet.AsSpan(12 + 4 * CSRCCount)); + ExtensionProfile = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(12 + 4 * CSRCCount)); headerExtensionLength += 2; - ExtensionLength = BinaryPrimitives.ReadUInt16BigEndian(packet.AsSpan(14 + 4 * CSRCCount)); + ExtensionLength = BinaryPrimitives.ReadUInt16BigEndian(packet.Slice(14 + 4 * CSRCCount)); headerExtensionLength += 2 + ExtensionLength * 4; if (ExtensionLength > 0 && packet.Length >= (headerAndCSRCLength + 4 + ExtensionLength * 4)) { - ExtensionPayload = new byte[ExtensionLength * 4]; - Buffer.BlockCopy(packet, headerAndCSRCLength + 4, ExtensionPayload, 0, ExtensionLength * 4); + ExtensionPayload = packet.Slice(headerAndCSRCLength + 4, ExtensionLength * 4).ToArray(); } } @@ -112,44 +109,56 @@ public RTPHeader(byte[] packet) } } - public byte[] GetHeader(UInt16 sequenceNumber, uint timestamp, uint syncSource) + public void SetHeader(ushort sequenceNumber, uint timestamp, uint syncSource) { SequenceNumber = sequenceNumber; Timestamp = timestamp; SyncSource = syncSource; - - return GetBytes(); } - public byte[] GetBytes() + /// + public int GetByteCount() => Length; + + /// + public int WriteBytes(Span buffer) { - byte[] header = new byte[Length]; + var size = GetByteCount(); - UInt16 firstWord = Convert.ToUInt16(Version * 16384 + PaddingFlag * 8192 + HeaderExtensionFlag * 4096 + CSRCCount * 256 + MarkerBit * 128 + PayloadType); + if (buffer.Length < size) + { + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); + } - BinaryPrimitives.WriteUInt16BigEndian(header.AsSpan(0), firstWord); - BinaryPrimitives.WriteUInt16BigEndian(header.AsSpan(2), SequenceNumber); - BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(4), Timestamp); - BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(8), SyncSource); + WriteBytesCore(buffer.Slice(0, size)); + + return size; + } + + private void WriteBytesCore(Span buffer) + { + var firstWord = Convert.ToUInt16(Version * 16384 + PaddingFlag * 8192 + HeaderExtensionFlag * 4096 + CSRCCount * 256 + MarkerBit * 128 + PayloadType); + + BinaryPrimitives.WriteUInt16BigEndian(buffer, firstWord); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), SequenceNumber); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4), Timestamp); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(8), SyncSource); if (HeaderExtensionFlag == 1) { - BinaryPrimitives.WriteUInt16BigEndian(header.AsSpan(12 + 4 * CSRCCount), ExtensionProfile); - BinaryPrimitives.WriteUInt16BigEndian(header.AsSpan(14 + 4 * CSRCCount), ExtensionLength); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(12 + 4 * CSRCCount), ExtensionProfile); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(14 + 4 * CSRCCount), ExtensionLength); } - if (ExtensionLength > 0 && ExtensionPayload != null) + if (ExtensionLength > 0 && ExtensionPayload is { Length: > 0 }) { - Buffer.BlockCopy(ExtensionPayload, 0, header, 16 + 4 * CSRCCount, ExtensionLength * 4); + ExtensionPayload.CopyTo(buffer.Slice(16 + 4 * CSRCCount)); } - - return header; } - private RTPHeaderExtensionData GetExtensionAtPosition(ref int position, int id, int len, RTPHeaderExtensionType type, out bool invalid) + private RTPHeaderExtensionData? GetExtensionAtPosition(ref int position, int id, int len, RTPHeaderExtensionType type, out bool invalid) { - RTPHeaderExtensionData ext = null; - if (ExtensionPayload != null) + RTPHeaderExtensionData? ext = null; + if (ExtensionPayload is { }) { if (id != 0) { @@ -159,7 +168,7 @@ private RTPHeaderExtensionData GetExtensionAtPosition(ref int position, int id, invalid = true; return null; } - ext = new RTPHeaderExtensionData(id, ExtensionPayload.Skip(position).Take(len).ToArray(), type); + ext = new RTPHeaderExtensionData(id, ExtensionPayload.AsSpan(position, len).ToArray(), type); position += len; } else @@ -195,10 +204,10 @@ public List GetHeaderExtensions() */ var extensions = new List(); - RTPHeaderExtensionData extension = null; + RTPHeaderExtensionData? extension = null; var i = 0; - bool invalid = false; - if (ExtensionPayload != null) + var invalid = false; + if (ExtensionPayload is { }) { while (i + 1 < ExtensionPayload.Length) { @@ -222,7 +231,7 @@ public List GetHeaderExtensions() break; } - if (!invalid && extension != null) + if (!invalid && extension is { }) { extensions.Add(extension); } @@ -239,7 +248,7 @@ public static bool TryParse( { header = new RTPHeader(); consumed = 0; - int offset = 0; + var offset = 0; if (buffer.Length < MIN_HEADER_LEN) { return false; @@ -248,7 +257,7 @@ public static bool TryParse( var firstWord = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(offset)); offset += 2; - header.SequenceNumber =BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(offset)); + header.SequenceNumber = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(offset)); offset += 2; header.Timestamp = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(offset)); offset += 4; @@ -263,11 +272,11 @@ public static bool TryParse( header.MarkerBit = (firstWord >> 7) & 0x1; header.PayloadType = firstWord & 0x7f; - int headerAndCSRCLength = offset + 4 * header.CSRCCount; + var headerAndCSRCLength = offset + 4 * header.CSRCCount; if (header.HeaderExtensionFlag == 1 && (buffer.Length >= (headerAndCSRCLength + 4))) { - header.ExtensionProfile =BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(offset)); + header.ExtensionProfile = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(offset)); offset += 2; header.ExtensionLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(offset)); offset += 2 + header.ExtensionLength * 4; @@ -275,8 +284,7 @@ public static bool TryParse( var extensionPayloadLength = header.ExtensionLength * 4; if (header.ExtensionLength > 0 && buffer.Length >= extensionPayloadLength) { - header.ExtensionPayload = new byte[extensionPayloadLength]; - Buffer.BlockCopy(buffer.ToArray(), headerAndCSRCLength + 4, header.ExtensionPayload, 0, extensionPayloadLength); + header.ExtensionPayload = buffer.Slice(headerAndCSRCLength + 4, extensionPayloadLength).ToArray(); } } @@ -292,7 +300,7 @@ public static bool TryParse( } consumed = offset; - return header.PayloadSize>=0; + return header.PayloadSize >= 0; } /// @@ -308,7 +316,7 @@ public uint GetTimestampDelta(uint previousTs) return 0; } - uint currentTs = this.Timestamp; + var currentTs = this.Timestamp; if (currentTs >= previousTs) { // No wraparound @@ -318,7 +326,7 @@ public uint GetTimestampDelta(uint previousTs) { // Wrapped around 2^32 const ulong FullRange = (ulong)uint.MaxValue + 1UL; - ulong diff = (ulong)currentTs + FullRange - previousTs; + var diff = (ulong)currentTs + FullRange - previousTs; return (uint)(diff & 0xFFFFFFFF); } } diff --git a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/AbsSendTimeExtension.cs b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/AbsSendTimeExtension.cs index 8448f973fe..36523f451a 100644 --- a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/AbsSendTimeExtension.cs +++ b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/AbsSendTimeExtension.cs @@ -1,70 +1,68 @@ using System; using System.Buffers.Binary; -using SIPSorcery.Net; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +// AbsSendTimeExtension is a extension payload format in +// http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +// Code reference: https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/source/rtp_header_extensions.cc#19 +public class AbsSendTimeExtension : RTPHeaderExtension { - // AbsSendTimeExtension is a extension payload format in - // http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time - // Code reference: https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/source/rtp_header_extensions.cc#19 - public class AbsSendTimeExtension: RTPHeaderExtension + public const string RTP_HEADER_EXTENSION_URI = "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"; + internal const int RTP_HEADER_EXTENSION_SIZE = 3; + + public AbsSendTimeExtension(int id) : base(id, RTP_HEADER_EXTENSION_URI, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte) { - public const string RTP_HEADER_EXTENSION_URI = "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"; - internal const int RTP_HEADER_EXTENSION_SIZE = 3; + } - public AbsSendTimeExtension(int id): base(id, RTP_HEADER_EXTENSION_URI, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte) - { - } + public override void Set(object? value) + { + // Nothing to do here + } - public override void Set(Object value) - { - // Nothing to do here - } + internal static byte[] AbsSendTime(int id, int extensionSize, DateTimeOffset now) + { + // inspired by https://github.com/pion/rtp/blob/master/abssendtimeextension.go + ulong unixNanoseconds = (ulong)((now - UnixEpoch).Ticks * 100L); + var seconds = unixNanoseconds / (ulong)1e9; + seconds += 0x83AA7E80UL; // offset in seconds between unix epoch and ntp epoch + var f = unixNanoseconds % (ulong)1e9; + f <<= 32; + f /= (ulong)1e9; + seconds <<= 32; + var ntp = seconds | f; + var abs = ntp >> 14; - internal static byte[] AbsSendTime(int id, int extensionSize, DateTimeOffset now) + return new[] { - // inspired by https://github.com/pion/rtp/blob/master/abssendtimeextension.go - ulong unixNanoseconds = (ulong)((now - UnixEpoch).Ticks * 100L); - var seconds = unixNanoseconds / (ulong)1e9; - seconds += 0x83AA7E80UL; // offset in seconds between unix epoch and ntp epoch - var f = unixNanoseconds % (ulong)1e9; - f <<= 32; - f /= (ulong)1e9; - seconds <<= 32; - var ntp = seconds | f; - var abs = ntp >> 14; + (byte)((id << 4) | extensionSize - 1), + (byte)((abs & 0xff0000UL) >> 16), + (byte)((abs & 0xff00UL) >> 8), + (byte)(abs & 0xffUL) + }; + } - return new[] - { - (byte)((id << 4) | extensionSize - 1), - (byte)((abs & 0xff0000UL) >> 16), - (byte)((abs & 0xff00UL) >> 8), - (byte)(abs & 0xffUL) - }; - } - - public override byte[] Marshal() - { - return AbsSendTime(Id, ExtensionSize, DateTimeOffset.Now); - } + public override byte[] Marshal() + { + return AbsSendTime(Id, ExtensionSize, DateTimeOffset.Now); + } - public override Object Unmarshal(RTPHeader header, byte[] data) - { - var ntpTimestamp = GetUlong(data); - return new TimestampPair() { NtpTimestamp = ntpTimestamp.HasValue ? ntpTimestamp.Value : 0, RtpTimestamp = header.Timestamp }; - } + public override object Unmarshal(RTPHeader header, byte[] data) + { + var ntpTimestamp = GetUlong(data); + return new TimestampPair() { NtpTimestamp = ntpTimestamp ?? 0, RtpTimestamp = header.Timestamp }; + } - // DateTimeOffset.UnixEpoch only available in newer target frameworks - private static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + // DateTimeOffset.UnixEpoch only available in newer target frameworks + private static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); - private ulong? GetUlong(byte[] data) + private ulong? GetUlong(ReadOnlySpan data) + { + if ((data.Length != ExtensionSize) || ((sizeof(ulong) - 1) > data.Length)) { - if ( (data.Length != ExtensionSize) || ((sizeof(ulong) - 1) > data.Length) ) - { - return null; - } - - return BinaryPrimitives.ReadUInt64BigEndian(data); + return null; } + + return BinaryPrimitives.ReadUInt16BigEndian(data); } } diff --git a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/AudioLevelExtension.cs b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/AudioLevelExtension.cs index 07d387e21a..0277df6cb5 100644 --- a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/AudioLevelExtension.cs +++ b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/AudioLevelExtension.cs @@ -1,93 +1,95 @@ using System; +using System.Diagnostics; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +// Code reference: https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/source/rtp_header_extensions.cc#49 +// 0 1 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ID | len=0 |V| level | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +public class AudioLevelExtension : RTPHeaderExtension { - // Code reference: https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/source/rtp_header_extensions.cc#49 - // 0 1 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - // | ID | len=0 |V| level | - // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - public class AudioLevelExtension : RTPHeaderExtension + public class AudioLevel { - public class AudioLevel + public bool Voice; + public ushort Level; + + public AudioLevel() { - public Boolean Voice; - public ushort Level; + Voice = false; + Level = 0; + } - public AudioLevel() + public AudioLevel(byte[] data) + { + if ((data is null) || (data.Length != AudioLevelExtension.RTP_HEADER_EXTENSION_SIZE)) { - Voice = false; - Level = 0; + throw new ArgumentException(nameof(data)); } - public AudioLevel(byte[] data) - { - if ((data == null) || (data.Length != AudioLevelExtension.RTP_HEADER_EXTENSION_SIZE)) - { - throw new ArgumentException(nameof(data)); - } + Voice = (data[0] & 0x80) != 0; + Level = (ushort)(data[0] & 0x7F); + } + }; - Voice = (data[0] & 0x80) != 0; - Level = (ushort)(data[0] & 0x7F); - } - }; + public const string RTP_HEADER_EXTENSION_URI = "urn:ietf:params:rtp-hdrext:ssrc-audio-level"; + internal const int RTP_HEADER_EXTENSION_SIZE = 1; - public const string RTP_HEADER_EXTENSION_URI = "urn:ietf:params:rtp-hdrext:ssrc-audio-level"; - internal const int RTP_HEADER_EXTENSION_SIZE = 1; + private AudioLevel _audioLevel; - private AudioLevel _audioLevel; + public AudioLevelExtension(int id) : base(id, RTP_HEADER_EXTENSION_URI, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte, Net.SDPMediaTypesEnum.audio) + { + _audioLevel = new AudioLevel() + { + Voice = false, + Level = 0 + }; + } - public AudioLevelExtension(int id) : base(id, RTP_HEADER_EXTENSION_URI, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte, Net.SDPMediaTypesEnum.audio) + /// + /// To set Audio Level + /// + /// An object is expected here + public override void Set(object? value) + { + if (value is AudioLevel audioLevel) { - _audioLevel = new AudioLevel() - { - Voice = false, - Level = 0 - }; + _audioLevel = audioLevel; } + } - /// - /// To set Audio Level - /// - /// An object is expected here - public override void Set(Object value) - { - if (value is AudioLevel audioLevel) - { - _audioLevel = audioLevel; - } + public override byte[] Marshal() + { + byte voice = 0; + if (_audioLevel.Voice) + { + voice = 0x80; } - public override byte[] Marshal() + return new[] { - byte voice = 0; - if (_audioLevel.Voice) - { - voice = 0x80; - }; + (byte)((Id << 4) | ExtensionSize - 1), + (byte)(voice | _audioLevel.Level) + }; + } - return new[] - { - (byte)((Id << 4) | ExtensionSize - 1), - (byte)(voice | _audioLevel.Level) - }; - } + public override object Unmarshal(RTPHeader header, byte[] data) + { + Debug.Assert(data is { }); - public override Object Unmarshal(RTPHeader header, byte[] data) + try { - try + if (data.Length != AudioLevelExtension.RTP_HEADER_EXTENSION_SIZE) { - if ((data == null) || (data.Length != AudioLevelExtension.RTP_HEADER_EXTENSION_SIZE)) - { - _audioLevel = new AudioLevel(data); - } + _audioLevel = new AudioLevel(data); } - catch - { - // Nothing to do more - } - return _audioLevel; } + catch + { + // Nothing to do more + } + return _audioLevel; } } diff --git a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/CVOExtension.cs b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/CVOExtension.cs index 39ad768295..b292980868 100644 --- a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/CVOExtension.cs +++ b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/CVOExtension.cs @@ -1,174 +1,173 @@ using SIPSorcery.Net; using System; -namespace SIPSorcery.Net -{ - // CVO (Coordination of Video Orientation) is a extension payload format in https://www.3gpp.org/ftp/Specs/archive/26_series/26.114/26114-i70.zip +namespace SIPSorcery.Net; + +// CVO (Coordination of Video Orientation) is a extension payload format in https://www.3gpp.org/ftp/Specs/archive/26_series/26.114/26114-i70.zip - // Code reference: - // - https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/source/rtp_header_extensions.cc#134 - // - https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/include/rtp_cvo.h +// Code reference: +// - https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/source/rtp_header_extensions.cc#134 +// - https://chromium.googlesource.com/external/webrtc/+/e2a017725570ead5946a4ca8235af27470ca0df9/webrtc/modules/rtp_rtcp/include/rtp_cvo.h - public class CVOExtension: RTPHeaderExtension +public class CVOExtension : RTPHeaderExtension +{ + public class CVO { - public class CVO - { - public Boolean CameraBackFacing; - public Boolean HorizontalFlip; - public VideoRotation VideoRotation; + public bool CameraBackFacing; + public bool HorizontalFlip; + public VideoRotation VideoRotation; - public CVO() - { - CameraBackFacing = false; - HorizontalFlip = false; - VideoRotation = VideoRotation.CW_0; - } + public CVO() + { + CameraBackFacing = false; + HorizontalFlip = false; + VideoRotation = VideoRotation.CW_0; + } - /* CVO byte: |0 0 0 0 C F R1 R0| - With the following definitions: - - C = Camera: indicates the direction of the camera used for this video stream. It can be used by the MTSI client in - receiver to e.g. display the received video differently depending on the source camera. - 0: Front-facing camera, facing the user. If camera direction is unknown by the sending MTSI client in the terminal then this is the default value used. - 1: Back-facing camera, facing away from the user. - - F = Flip: indicates a horizontal (left-right flip) mirror operation on the video as sent on the link. - 0: No flip operation. If the sending MTSI client in terminal does not know if a horizontal mirror operation is necessary, then this is the default value used. - 1: Horizontal flip operation - - R1, R0 = Rotation: indicates the rotation of the video as transmitted on the link. - 0, 0 = 0° rotation => needs 0° CW rotation - 0, 1 = 90° Counter Clockwise (CCW) rotation => needs 90° CW rotation - 1, 0 = 180° CCW rotation => needs 180° CW rotation - 1, 1 = 270° CCW rotation => needs 270° CW rotation - */ - - public CVO(byte cvo_byte) - { - CameraBackFacing = (cvo_byte & 0x8) == 0x8; - HorizontalFlip = (cvo_byte & 0x4) == 0x4; - VideoRotation = ConvertCVOByteToVideoRotation(cvo_byte); - } + /* CVO byte: |0 0 0 0 C F R1 R0| + With the following definitions: + + C = Camera: indicates the direction of the camera used for this video stream. It can be used by the MTSI client in + receiver to e.g. display the received video differently depending on the source camera. + 0: Front-facing camera, facing the user. If camera direction is unknown by the sending MTSI client in the terminal then this is the default value used. + 1: Back-facing camera, facing away from the user. + + F = Flip: indicates a horizontal (left-right flip) mirror operation on the video as sent on the link. + 0: No flip operation. If the sending MTSI client in terminal does not know if a horizontal mirror operation is necessary, then this is the default value used. + 1: Horizontal flip operation + + R1, R0 = Rotation: indicates the rotation of the video as transmitted on the link. + 0, 0 = 0° rotation => needs 0° CW rotation + 0, 1 = 90° Counter Clockwise (CCW) rotation => needs 90° CW rotation + 1, 0 = 180° CCW rotation => needs 180° CW rotation + 1, 1 = 270° CCW rotation => needs 270° CW rotation + */ + + public CVO(byte cvo_byte) + { + CameraBackFacing = (cvo_byte & 0x8) == 0x8; + HorizontalFlip = (cvo_byte & 0x4) == 0x4; + VideoRotation = ConvertCVOByteToVideoRotation(cvo_byte); } + } - public const string RTP_HEADER_EXTENSION_URI = "urn:3gpp:video-orientation"; - internal const int RTP_HEADER_EXTENSION_SIZE = 1; + public const string RTP_HEADER_EXTENSION_URI = "urn:3gpp:video-orientation"; + internal const int RTP_HEADER_EXTENSION_SIZE = 1; - private byte _cvo_byte; - private CVO _cvo; + private byte _cvo_byte; + private CVO _cvo; - public CVOExtension(int id) : base(id, RTP_HEADER_EXTENSION_URI, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte, Net.SDPMediaTypesEnum.video) - { - _cvo_byte = 0; - _cvo = new CVO(); - } + public CVOExtension(int id) : base(id, RTP_HEADER_EXTENSION_URI, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte, Net.SDPMediaTypesEnum.video) + { + _cvo_byte = 0; + _cvo = new CVO(); + } - /// - /// To set video rotation - /// - /// A object is expected here - public override void Set(Object value) + /// + /// To set video rotation + /// + /// A object is expected here + public override void Set(object? value) + { + if (value is CVO cvo) { - if (value is CVO cvo) - { - _cvo_byte = ConvertCVOToCVOByte(cvo); - _cvo = cvo; - } + _cvo_byte = ConvertCVOToCVOByte(cvo); + _cvo = cvo; } + } - public override byte[] Marshal() + public override byte[] Marshal() + { + return new[] { - return new[] - { - (byte)((Id << 4) | ExtensionSize - 1), - _cvo_byte - }; - } + (byte)((Id << 4) | ExtensionSize - 1), + _cvo_byte + }; + } - public override Object Unmarshal(RTPHeader header, byte[] data) + public override object Unmarshal(RTPHeader header, byte[] data) + { + if (data?.Length == ExtensionSize) { - if (data?.Length == ExtensionSize) + var cvoByte = data[0]; + if (_cvo_byte != cvoByte) { - var cvoByte = data[0]; - if (_cvo_byte != cvoByte) - { - _cvo_byte = cvoByte; - _cvo = new CVO(_cvo_byte); - } + _cvo_byte = cvoByte; + _cvo = new CVO(_cvo_byte); } - return _cvo; } + return _cvo; + } - /// - /// Enum for clockwise (CW) rotation in degree. - /// - public enum VideoRotation - { - CW_0 = 0, - CW_90 = 90, - CW_180 = 180, - CW_270 = 270 - }; + /// + /// Enum for clockwise (CW) rotation in degree. + /// + public enum VideoRotation + { + CW_0 = 0, + CW_90 = 90, + CW_180 = 180, + CW_270 = 270 + }; - static byte ConvertCVOToCVOByte(CVO cvo) - { - return (byte) ( (cvo.CameraBackFacing ? 0x8 : 0x0) - + (cvo.HorizontalFlip ? 0x4 : 0x0) - + ConvertVideoRotationToCVOByte(cvo.VideoRotation)); - } + private static byte ConvertCVOToCVOByte(CVO cvo) + { + return (byte)((cvo.CameraBackFacing ? 0x8 : 0x0) + + (cvo.HorizontalFlip ? 0x4 : 0x0) + + ConvertVideoRotationToCVOByte(cvo.VideoRotation)); + } - static byte ConvertVideoRotationToCVOByte(VideoRotation rotation) + private static byte ConvertVideoRotationToCVOByte(VideoRotation rotation) + { + switch (rotation) { - switch (rotation) - { - case VideoRotation.CW_0: - return 0; - case VideoRotation.CW_90: - return 1; - case VideoRotation.CW_180: - return 2; - case VideoRotation.CW_270: - return 3; - default: - return 0; - } + case VideoRotation.CW_0: + return 0; + case VideoRotation.CW_90: + return 1; + case VideoRotation.CW_180: + return 2; + case VideoRotation.CW_270: + return 3; + default: + return 0; } + } - static VideoRotation ConvertCVOByteToVideoRotation(byte cvo_byte) + private static VideoRotation ConvertCVOByteToVideoRotation(byte cvo_byte) + { + /* CVO byte: |0 0 0 0 C F R1 R0| + With the following definitions: + + C = Camera: indicates the direction of the camera used for this video stream. It can be used by the MTSI client in + receiver to e.g. display the received video differently depending on the source camera. + 0: Front-facing camera, facing the user. If camera direction is unknown by the sending MTSI client in the terminal then this is the default value used. + 1: Back-facing camera, facing away from the user. + + F = Flip: indicates a horizontal (left-right flip) mirror operation on the video as sent on the link. + 0: No flip operation. If the sending MTSI client in terminal does not know if a horizontal mirror operation is necessary, then this is the default value used. + 1: Horizontal flip operation + + R1, R0 = Rotation: indicates the rotation of the video as transmitted on the link. + 0, 0 = 0° rotation => needs 0° CW rotation + 0, 1 = 90° Counter Clockwise (CCW) rotation => needs 90° CW rotation + 1, 0 = 180° CCW rotation => needs 180° CW rotation + 1, 1 = 270° CCW rotation => needs 270° CW rotation + */ + + uint rotation_bits = (uint)cvo_byte & 0x3; + switch (rotation_bits) { - /* CVO byte: |0 0 0 0 C F R1 R0| - With the following definitions: - - C = Camera: indicates the direction of the camera used for this video stream. It can be used by the MTSI client in - receiver to e.g. display the received video differently depending on the source camera. - 0: Front-facing camera, facing the user. If camera direction is unknown by the sending MTSI client in the terminal then this is the default value used. - 1: Back-facing camera, facing away from the user. - - F = Flip: indicates a horizontal (left-right flip) mirror operation on the video as sent on the link. - 0: No flip operation. If the sending MTSI client in terminal does not know if a horizontal mirror operation is necessary, then this is the default value used. - 1: Horizontal flip operation - - R1, R0 = Rotation: indicates the rotation of the video as transmitted on the link. - 0, 0 = 0° rotation => needs 0° CW rotation - 0, 1 = 90° Counter Clockwise (CCW) rotation => needs 90° CW rotation - 1, 0 = 180° CCW rotation => needs 180° CW rotation - 1, 1 = 270° CCW rotation => needs 270° CW rotation - */ - - uint rotation_bits = (uint)cvo_byte & 0x3; - switch (rotation_bits) - { - case 0: - return VideoRotation.CW_0; - case 1: - return VideoRotation.CW_90; - case 2: - return VideoRotation.CW_180; - case 3: - return VideoRotation.CW_270; - default: - return VideoRotation.CW_0; - } + case 0: + return VideoRotation.CW_0; + case 1: + return VideoRotation.CW_90; + case 2: + return VideoRotation.CW_180; + case 3: + return VideoRotation.CW_270; + default: + return VideoRotation.CW_0; } } } diff --git a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/RTPHeaderExtension.cs b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/RTPHeaderExtension.cs index 65319d0ad2..a30727180d 100644 --- a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/RTPHeaderExtension.cs +++ b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/RTPHeaderExtension.cs @@ -1,135 +1,133 @@ using System; using System.Collections.Generic; -using System.Linq; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public abstract class RTPHeaderExtension { - public abstract class RTPHeaderExtension + /// + /// Create an RTPHeaderExtension (, , etc ...) based on the URI provided + /// If found, id permits to store the "extmap" value related to this extension + /// It not found returns null + /// + /// extmap value + /// URI of the extension - for example: "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" or "urn:3gpp:video-orientation" + /// A Specific RTPHeaderExtension + public static RTPHeaderExtension? GetRTPHeaderExtension(int id, string uri, SDPMediaTypesEnum media) { - /// - /// Create an RTPHeaderExtension (, , etc ...) based on the URI provided - /// If found, id permits to store the "extmap" value related to this extension - /// It not found returns null - /// - /// extmap value - /// URI of the extension - for example: "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" or "urn:3gpp:video-orientation" - /// A Specific RTPHeaderExtension - public static RTPHeaderExtension GetRTPHeaderExtension(int id, string uri, SDPMediaTypesEnum media) + RTPHeaderExtension? result = null; + switch (uri) { - RTPHeaderExtension result = null; - switch (uri) - { - case AbsSendTimeExtension.RTP_HEADER_EXTENSION_URI: - result = new AbsSendTimeExtension(id); - break; - - case CVOExtension.RTP_HEADER_EXTENSION_URI: - result = new CVOExtension(id); - break; - - case AudioLevelExtension.RTP_HEADER_EXTENSION_URI: - result = new AudioLevelExtension(id); - break; - - case TransportWideCCExtension.RTP_HEADER_EXTENSION_URI: - //case TransportWideCCExtension.RTP_HEADER_EXTENSION_URI_ALT: - result = new TransportWideCCExtension(id); - break; - } + case AbsSendTimeExtension.RTP_HEADER_EXTENSION_URI: + result = new AbsSendTimeExtension(id); + break; - if ( (result != null) && result.IsMediaSupported(media) ) - { - return result; - } + case CVOExtension.RTP_HEADER_EXTENSION_URI: + result = new CVOExtension(id); + break; - return null; + case AudioLevelExtension.RTP_HEADER_EXTENSION_URI: + result = new AudioLevelExtension(id); + break; + + case TransportWideCCExtension.RTP_HEADER_EXTENSION_URI: + //case TransportWideCCExtension.RTP_HEADER_EXTENSION_URI_ALT: + result = new TransportWideCCExtension(id); + break; } - /// - /// To create a RTP Header Extension - /// - /// Id / extmap - /// uri - /// type (one or two bytes) - /// media(s) supported by this extension - set null/empty if all medias are supported - public RTPHeaderExtension(int id, string uri, int extensionSize, RTPHeaderExtensionType type, params SDPMediaTypesEnum[] medias ) + if ((result is { }) && result.IsMediaSupported(media)) { - Id = id; - Uri = uri; - ExtensionSize = extensionSize; - Type = type; - - if (medias != null) - { - Medias = medias.ToList(); - } - else - { - Medias = new List(); - } + return result; } - /// - /// Checks if the URI provided matches the URI of this extension - /// Override this method if the extension can have multiple URIs (for example TransportWideCCExtension) - /// - /// - /// - public virtual bool MatchesExtension(string uri) + return null; + } + + /// + /// To create a RTP Header Extension + /// + /// Id / extmap + /// uri + /// type (one or two bytes) + /// media(s) supported by this extension - set null/empty if all medias are supported + public RTPHeaderExtension(int id, string uri, int extensionSize, RTPHeaderExtensionType type, params SDPMediaTypesEnum[] medias) + { + Id = id; + Uri = uri; + ExtensionSize = extensionSize; + Type = type; + + if (medias is { }) + { + Medias =new List( medias); + } + else { - return Uri.Equals(uri, StringComparison.InvariantCultureIgnoreCase); + Medias = new List(); } + } + + /// + /// Checks if the URI provided matches the URI of this extension + /// Override this method if the extension can have multiple URIs (for example TransportWideCCExtension) + /// + /// + /// + public virtual bool MatchesExtension(string uri) + { + return Uri.Equals(uri, StringComparison.OrdinalIgnoreCase); + } - // Id / "extmap" - public int Id { get; internal set; } + // Id / "extmap" + public int Id { get; internal set; } // Uri public string Uri { get; set; } - public int ExtensionSize { get; } + public int ExtensionSize { get; } - // Medias supported by this extension - if null/empty all medias are supported - public List Medias { get;} + // Medias supported by this extension - if null/empty all medias are supported + public List Medias { get; } - // Type (one or two bytes) - public RTPHeaderExtensionType Type { get; } + // Type (one or two bytes) + public RTPHeaderExtensionType Type { get; } - public Boolean IsMediaSupported(SDPMediaTypesEnum media) + public bool IsMediaSupported(SDPMediaTypesEnum media) + { + if (Medias.Count == 0) { - if (Medias.Count == 0) - { - return true; - } - - return Medias.Contains(media); + return true; } - // Function to call to set a new value to this extension - public abstract void Set(Object obj); + return Medias.Contains(media); + } - // Function to call to get the payload when writting the RTP header - public abstract byte[] Marshal(); + // Function to call to set a new value to this extension + public abstract void Set(object obj); - // Function to call when reading the RTP header - public abstract Object Unmarshal(RTPHeader header, byte[] data); - } + // Function to call to get the payload when writting the RTP header + public abstract byte[] Marshal(); - public enum RTPHeaderExtensionType - { - OneByte, - TwoByte - } + // Function to call when reading the RTP header + public abstract object Unmarshal(RTPHeader header, byte[] data); +} - public class RTPHeaderExtensionData +public enum RTPHeaderExtensionType +{ + OneByte, + TwoByte +} + +public class RTPHeaderExtensionData +{ + public RTPHeaderExtensionData(int id, byte[] data, RTPHeaderExtensionType type) { - public RTPHeaderExtensionData(int id, byte[] data, RTPHeaderExtensionType type) - { - Id = id; - Data = data; - Type = type; - } - public int Id { get; } - public byte[] Data { get; } - public RTPHeaderExtensionType Type { get; } + Id = id; + Data = data; + Type = type; } + public int Id { get; } + public byte[] Data { get; } + public RTPHeaderExtensionType Type { get; } } diff --git a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/TransportWideCCExtension.cs b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/TransportWideCCExtension.cs index 91a333340f..1b1a06abf4 100644 --- a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/TransportWideCCExtension.cs +++ b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/TransportWideCCExtension.cs @@ -16,105 +16,106 @@ * 2025-02-20 Initial creation. */ using System; -using System.Buffers.Binary; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// TransportWideCCExtension implements the Transport Wide Congestion Control (TWCC) +/// RTP header extension as defined in: +/// http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +/// +/// This extension carries a 16-bit sequence number (2 bytes of payload). +/// The one-byte header is constructed as (id << 4) | (extensionSize - 1). +/// +public class TransportWideCCExtension : RTPHeaderExtension { - /// - /// TransportWideCCExtension implements the Transport Wide Congestion Control (TWCC) - /// RTP header extension as defined in: - /// http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 - /// - /// This extension carries a 16-bit sequence number (2 bytes of payload). - /// The one-byte header is constructed as (id << 4) | (extensionSize - 1). - /// - public class TransportWideCCExtension : RTPHeaderExtension - { - // + // - public const string RTP_HEADER_EXTENSION_URI = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; + public const string RTP_HEADER_EXTENSION_URI = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; public override bool MatchesExtension(string uri) { - return string.Equals(uri, RTP_HEADER_EXTENSION_URI, StringComparison.OrdinalIgnoreCase) || - string.Equals(uri, "urn:ietf:params:rtp-hdrext:transport-wide-cc", StringComparison.OrdinalIgnoreCase) || - string.Equals(uri, "http://www.webrtc.org/experiments/rtp-hdrext/transport-wide-cc-02", StringComparison.OrdinalIgnoreCase); + return + string.Equals(uri, RTP_HEADER_EXTENSION_URI, StringComparison.OrdinalIgnoreCase) + || string.Equals(uri, "urn:ietf:params:rtp-hdrext:transport-wide-cc", StringComparison.OrdinalIgnoreCase) //official urn registered with IANA + || string.Equals(uri, "http://www.webrtc.org/experiments/rtp-hdrext/transport-wide-cc-02", StringComparison.OrdinalIgnoreCase); } + internal const int RTP_HEADER_EXTENSION_SIZE = 2; // TWCC payload: 2 bytes for sequence number. - internal const int RTP_HEADER_EXTENSION_SIZE = 2; // TWCC payload: 2 bytes for sequence number. + public TransportWideCCExtension(int id, string uri) + : base(id, uri, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte) + { + } - public TransportWideCCExtension(int id, string uri) - : base(id, uri, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte) - { - } + /// + /// The TWCC sequence number. + /// + public ushort SequenceNumber { get; private set; } - /// - /// The TWCC sequence number. - /// - public ushort SequenceNumber { get; private set; } - - /// - /// Constructs a TWCC header extension with the negotiated extension id. - /// - /// The negotiated header extension id. - public TransportWideCCExtension(int id) - : base(id, RTP_HEADER_EXTENSION_URI, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte) - { - } + /// + /// Constructs a TWCC header extension with the negotiated extension id. + /// + /// The negotiated header extension id. + public TransportWideCCExtension(int id) + : base(id, RTP_HEADER_EXTENSION_URI, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte) + { + } - /// - /// Generic setter override. Expects a ushort representing the sequence number. - /// - /// The TWCC sequence number as an object (ushort). - public override void Set(object value) + /// + /// Generic setter override. Expects a ushort representing the sequence number. + /// + /// The TWCC sequence number as an object (ushort). + public override void Set(object? value) + { + if (value is ushort seq) { - if (value is ushort seq) - { - SequenceNumber = seq; - } - else - { - throw new ArgumentException("Value must be a ushort representing the TWCC sequence number", nameof(value)); - } + SequenceNumber = seq; } - - /// - /// Marshals the TWCC header extension to a byte array. - /// The first byte is the one-byte header (with id and length) and - /// the following two bytes are the sequence number in network (big-endian) order. - /// - /// A byte array containing the marshalled TWCC header extension. - public override byte[] Marshal() + else { - // Construct the one-byte header. (id << 4) | (extensionSize - 1) - byte headerByte = (byte)((Id << 4) | (RTP_HEADER_EXTENSION_SIZE - 1)); + throw new ArgumentException("Value must be a ushort representing the TWCC sequence number", nameof(value)); + } + } - // Convert the sequence number to a 2-byte array in big-endian order. - byte[] seqBytes = new byte[2]; - BinaryPrimitives.WriteUInt16BigEndian(seqBytes, SequenceNumber); + /// + /// Marshals the TWCC header extension to a byte array. + /// The first byte is the one-byte header (with id and length) and + /// the following two bytes are the sequence number in network (big-endian) order. + /// + /// A byte array containing the marshalled TWCC header extension. + public override byte[] Marshal() + { + // Construct the one-byte header. (id << 4) | (extensionSize - 1) + byte headerByte = (byte)((Id << 4) | (RTP_HEADER_EXTENSION_SIZE - 1)); - return new byte[] { headerByte, seqBytes[0], seqBytes[1] }; + // Convert the sequence number to a 2-byte array in big-endian order. + byte[] seqBytes = BitConverter.GetBytes(SequenceNumber); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(seqBytes); } - /// - /// Unmarshals the TWCC header extension from the provided data. - /// - /// The RTP header (if additional context is needed). - /// The extension payload (should be exactly 2 bytes). - /// The extracted TWCC sequence number (as a ushort). - public override object Unmarshal(RTPHeader header, byte[] data) + return new byte[] { headerByte, seqBytes[0], seqBytes[1] }; + } + + /// + /// Unmarshals the TWCC header extension from the provided data. + /// + /// The RTP header (if additional context is needed). + /// The extension payload (should be exactly 2 bytes). + /// The extracted TWCC sequence number (as a ushort). + public override object Unmarshal(RTPHeader header, byte[] data) + { + if (data.Length != RTP_HEADER_EXTENSION_SIZE) { - if (data.Length != RTP_HEADER_EXTENSION_SIZE) - { - throw new ArgumentException($"Invalid TWCC extension payload size, expected {RTP_HEADER_EXTENSION_SIZE} but got {data.Length}."); - } - - // Combine the two bytes into a ushort (big-endian). - ushort seqNum = (ushort)((data[0] << 8) | data[1]); - return seqNum; + throw new ArgumentException($"Invalid TWCC extension payload size, expected {RTP_HEADER_EXTENSION_SIZE} but got {data.Length}."); } + + // Combine the two bytes into a ushort (big-endian). + ushort seqNum = (ushort)((data[0] << 8) | data[1]); + return seqNum; } } diff --git a/src/SIPSorcery/net/RTP/RTPPacket.cs b/src/SIPSorcery/net/RTP/RTPPacket.cs index 016da1060a..1df2829170 100644 --- a/src/SIPSorcery/net/RTP/RTPPacket.cs +++ b/src/SIPSorcery/net/RTP/RTPPacket.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: RTPPacket.cs // // Description: Encapsulation of an RTP packet. @@ -15,149 +15,67 @@ //----------------------------------------------------------------------------- using System; -#if !NETCOREAPP2_1_OR_GREATER || NETFRAMEWORK -using System.Linq; -#endif - -namespace SIPSorcery.Net -{ - public class RTPPacket - { - public RTPHeader Header; - private byte[] _payload; - private ArraySegment _payloadSegment; - private int _srtpProtectionLength = 0; - - public byte[] Payload - { - get { return _payload; } - set { _payload = value; } - } - - public RTPPacket() - { - Header = new RTPHeader(); - } +using System.Diagnostics.CodeAnalysis; +using SIPSorcery.Sys; - public RTPPacket(int payloadSize) - { - Header = new RTPHeader(); - _payload = new byte[payloadSize]; - } +namespace SIPSorcery.Net; - public RTPPacket(byte[] packet) - { - Header = new RTPHeader(packet); - _payload = new byte[Header.PayloadSize]; - Array.Copy(packet, Header.Length, _payload, 0, _payload.Length); - } - - public RTPPacket(ArraySegment packet, int srtpProtectionLength) - { - Header = new RTPHeader(); - _payloadSegment = packet; - _srtpProtectionLength = srtpProtectionLength; - } - - public uint GetPayloadLength() - { - return (uint)(_payload?.Length ?? _payloadSegment.Count); - } - - public byte[] GetPayloadBytes() - { - Payload ??= _payloadSegment.ToArray(); +public class RTPPacket : IByteSerializable +{ + public RTPHeader Header { get; } + public ReadOnlyMemory Payload { get; } - return Payload; - } + public RTPPacket(ReadOnlyMemory packet) + { + Header = new RTPHeader(packet.Span); + Payload = packet.Slice(Header.Length, Header.PayloadSize); + } - public byte GetPayloadByteAt(int index) - { -#if NETCOREAPP2_1_OR_GREATER && !NETFRAMEWORK - return _payload?[index] ?? _payloadSegment[index]; -#else - return _payload?[index] ?? _payloadSegment.ElementAt(index); -#endif - } + public RTPPacket(RTPHeader rtpHeader, ReadOnlyMemory payload) + { + Header = rtpHeader; + Payload = payload; + } - public ArraySegment GetPayloadSegment(int offset, int length) - { - if (_payload != null) - { - return new ArraySegment(_payload, offset, length); - } + /// + public int GetByteCount() => Header.GetByteCount() + Payload.Length; -#if NETCOREAPP2_1_OR_GREATER && !NETFRAMEWORK - return _payloadSegment.Slice(offset, length); -#else - return new ArraySegment(_payloadSegment.Array!, offset + _payloadSegment.Offset, length); -#endif - } + /// + public int WriteBytes(Span buffer) + { + var size = GetByteCount(); - public byte[] GetBytes() + if (buffer.Length < size) { - byte[] header = Header.GetBytes(); - byte[] packet = new byte[header.Length + (_payload?.Length ?? _payloadSegment.Count) + _srtpProtectionLength]; - - Array.Copy(header, packet, header.Length); - - if (_payloadSegment != null) - { -#if NETCOREAPP2_1_OR_GREATER && !NETFRAMEWORK - _payloadSegment.CopyTo(packet, header.Length); -#else - Array.Copy(_payloadSegment.Array!, _payloadSegment.Offset, packet, header.Length, _payloadSegment.Count); -#endif - } - else if (_payload != null) - { - Array.Copy(_payload, 0, packet, header.Length, _payload.Length); - } - else - { - throw new ApplicationException("Either _payloadSegment or _payload should be defined"); - } - - return packet; + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); } - private byte[] GetNullPayload(int numBytes) - { - byte[] payload = new byte[numBytes]; + WriteBytesCore(buffer.Slice(0, size)); - for (int byteCount = 0; byteCount < numBytes; byteCount++) - { - payload[byteCount] = 0xff; - } + return size; + } - return payload; - } + private void WriteBytesCore(Span buffer) + { + var bytesWritten = Header.WriteBytes(buffer); + Payload.Span.CopyTo(buffer.Slice(bytesWritten)); + } - public static bool TryParse( - ReadOnlySpan buffer, - RTPPacket packet, - out int consumed) + public static bool TryParse( + ReadOnlyMemory buffer, + [NotNullWhen(true)] out RTPPacket? packet, + out int consumed) + { + packet = null; + consumed = 0; + if (!RTPHeader.TryParse(buffer.Span, out var header, out var headerConsumed)) { - consumed = 0; - if (RTPHeader.TryParse(buffer, out var header, out var headerConsumed)) - { - packet.Header = header; - consumed += headerConsumed; - packet._payload = buffer.Slice(headerConsumed, header.PayloadSize).ToArray(); - consumed += header.PayloadSize; - return true; - } - return false; } - public static bool TryParse( - ReadOnlySpan buffer, - out RTPPacket packet, - out int consumed) - { - packet = new RTPPacket(); - return TryParse(buffer, packet, out consumed); - } + packet = new RTPPacket(header, buffer.Slice(headerConsumed, header.PayloadSize)); + + consumed = headerConsumed + header.PayloadSize; + return true; } } diff --git a/src/SIPSorcery/net/RTP/RTPReorderBuffer.cs b/src/SIPSorcery/net/RTP/RTPReorderBuffer.cs index 24b25b9d1d..00a5ac49ce 100644 --- a/src/SIPSorcery/net/RTP/RTPReorderBuffer.cs +++ b/src/SIPSorcery/net/RTP/RTPReorderBuffer.cs @@ -1,121 +1,129 @@ using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class RTPReorderBuffer { - public class RTPReorderBuffer - { - private readonly TimeSpan _maxDropOutTime; - private readonly IDateTime _datetimeProvider; - private readonly System.Collections.Generic.LinkedList _data = new System.Collections.Generic.LinkedList(); - private ushort? _currentSeqNumber; + private readonly TimeSpan _maxDropOutTime; + private readonly IDateTime _datetimeProvider; + private readonly System.Collections.Generic.LinkedList _data = new System.Collections.Generic.LinkedList(); + private ushort? _currentSeqNumber; - private static readonly ILogger logger = LogFactory.CreateLogger(); + private static readonly ILogger logger = LogFactory.CreateLogger(); - public RTPReorderBuffer(TimeSpan maxDropOutTime, IDateTime datetimeProvider = null) - { - _maxDropOutTime = maxDropOutTime; - _datetimeProvider = datetimeProvider ?? new DefaultTimeProvider(); - } + public RTPReorderBuffer(TimeSpan maxDropOutTime, IDateTime? datetimeProvider = null) + { + _maxDropOutTime = maxDropOutTime; + _datetimeProvider = datetimeProvider ?? new DefaultTimeProvider(); + } - private RTPPacket First => _data.First?.Value; - private RTPPacket Last => _data.Last?.Value; + private RTPPacket? First => _data.First?.Value; + private RTPPacket? Last => _data.Last?.Value; - private bool IsBeforeWrapAround(RTPPacket packet) { - return IsBeforeWrapAround(packet.Header.SequenceNumber); - } - private bool IsBeforeWrapAround(ushort seq) - { - return seq > ushort.MaxValue / 2 + ushort.MaxValue / 4; - } - private bool IsAfterWrapAround(RTPPacket packet) + private bool IsBeforeWrapAround(RTPPacket packet) + { + return IsBeforeWrapAround(packet.Header.SequenceNumber); + } + private bool IsBeforeWrapAround(ushort seq) + { + return seq > ushort.MaxValue / 2 + ushort.MaxValue / 4; + } + private bool IsAfterWrapAround(RTPPacket packet) + { + return packet.Header.SequenceNumber < ushort.MaxValue / 4; + } + + public bool Get([NotNullWhen(true)] out RTPPacket? packet) + { + packet = null; + if (Last is null) { - return packet.Header.SequenceNumber < ushort.MaxValue / 4; + return false; } - public bool Get(out RTPPacket packet) + if (_currentSeqNumber.HasValue && _currentSeqNumber != Last.Header.SequenceNumber) { - packet = null; - if (Last == null) + + if (_datetimeProvider.Time - Last.Header.ReceivedTime < _maxDropOutTime) { return false; } + } + packet = Last; + _data.RemoveLast(); + _currentSeqNumber = (ushort)checked(packet.Header.SequenceNumber + 1); + return true; + } - if (_currentSeqNumber.HasValue && _currentSeqNumber != Last.Header.SequenceNumber) - { - - if (_datetimeProvider.Time - Last.Header.ReceivedTime < _maxDropOutTime) - { - return false; - } - } - packet = Last; - _data.RemoveLast(); - _currentSeqNumber = (ushort)checked(packet.Header.SequenceNumber + 1); - return true; + public void Add(RTPPacket current) + { + if (_data.Count == 0) + { + _data.AddFirst(current); + return; } - public void Add(RTPPacket current) + // if seq number is greater or equal than we are waiting for then append to last position + if (_currentSeqNumber.HasValue && _currentSeqNumber >= current.Header.SequenceNumber) { - if (_data.Count == 0) + Debug.Assert(Last is { }); + if (Last.Header.SequenceNumber > _currentSeqNumber || IsAfterWrapAround(Last) && IsBeforeWrapAround(_currentSeqNumber.Value)) { - _data.AddFirst(current); + _data.AddLast(current); return; } + } + + Debug.Assert(Last is { }); + Debug.Assert(First is { }); + if (IsBeforeWrapAround(Last) && !IsAfterWrapAround(First) && IsAfterWrapAround(current)) // first incoming packet after wraparound + { + _data.AddFirst(current); + return; + } - // if seq number is greater or equal than we are waiting for then append to last position - if (_currentSeqNumber.HasValue && _currentSeqNumber >= current.Header.SequenceNumber) { - if (Last.Header.SequenceNumber > _currentSeqNumber || IsAfterWrapAround(Last) && IsBeforeWrapAround(_currentSeqNumber.Value)) { - _data.AddLast(current); - return; - } + var node = _data.First; + Debug.Assert(node is { }); + do + { + // if it is packet before wrap around skip all packets after wrap around and then insert the packet + if (IsBeforeWrapAround(current) && IsBeforeWrapAround(Last) && IsAfterWrapAround(node.Value)) + { + node = node.Next; + continue; } - - if (IsBeforeWrapAround(Last) && !IsAfterWrapAround(First) && IsAfterWrapAround(current)) // first incoming packet after wraparound + if (IsBeforeWrapAround(node.Value) && IsAfterWrapAround(current)) { - _data.AddFirst(current); - return; + _data.AddBefore(node, current); + break; } - - var node = _data.First; - do + if (current.Header.SequenceNumber > node.Value.Header.SequenceNumber) { - // if it is packet before wrap around skip all packets after wrap around and then insert the packet - if (IsBeforeWrapAround(current) && IsBeforeWrapAround(Last) && IsAfterWrapAround(node.Value)) - { - node = node.Next; - continue; - } - if (IsBeforeWrapAround(node.Value) && IsAfterWrapAround(current)) - { - _data.AddBefore(node, current); - break; - } - if (current.Header.SequenceNumber > node.Value.Header.SequenceNumber) - { - _data.AddBefore(node, current); - break; - } - if (current.Header.SequenceNumber == node.Value.Header.SequenceNumber) - { - logger.LogInformation("Duplicate seq number: {SequenceNumber}", current.Header.SequenceNumber); - break; - } - - node = node.Next; + _data.AddBefore(node, current); + break; } - while (node != null); + if (current.Header.SequenceNumber == node.Value.Header.SequenceNumber) + { + logger.LogRtpDuplicateSeqNum(current.Header.SequenceNumber); + break; + } + + node = node.Next; } + while (node is { }); } +} - public interface IDateTime - { - DateTime Time { get; } - } +public interface IDateTime +{ + DateTime Time { get; } +} - public class DefaultTimeProvider : IDateTime - { - public DateTime Time => DateTime.Now; - } +public class DefaultTimeProvider : IDateTime +{ + public DateTime Time => DateTime.Now; } diff --git a/src/SIPSorcery/net/RTP/RTPSession.cs b/src/SIPSorcery/net/RTP/RTPSession.cs index 0eeb8797b9..32fb3e63c3 100644 --- a/src/SIPSorcery/net/RTP/RTPSession.cs +++ b/src/SIPSorcery/net/RTP/RTPSession.cs @@ -20,11 +20,13 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Buffers.Binary; +using System.Collections.Frozen; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -32,1490 +34,1519 @@ using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public delegate int ProtectRtpPacket(byte[] payload, int length, out int outputBufferLength); + +/// +/// The RTPSession class is the primary point for interacting with the Real-Time +/// Protocol. It manages all the resources required for setting up and then sending +/// and receiving RTP packets. This class IS designed to be inherited by child +/// classes and for child classes to add audio and video processing logic. +/// +/// +/// The setting up of an RTP stream involved the exchange of Session Descriptions +/// (SDP) with the remote party. This class has adopted the mechanism used by WebRTC. +/// The steps are: +/// 1. If acting as the initiator: +/// a. Create offer, +/// b. Send offer to remote party and get their answer (external to this class, requires signalling), +/// c. Set remote description, +/// d. Optionally perform any additional set up, such as negotiating SRTP keying material, +/// e. Call Start to commence RTCP reporting. +/// 2. If acting as the recipient: +/// a. Receive offer, +/// b. Set remote description. This step MUST be done before an SDP answer can be generated. +/// This step can also result in an error condition if the codecs/formats offered aren't supported, +/// c. Create answer, +/// d. Send answer to remote party (external to this class, requires signalling), +/// e. Optionally perform any additional set up, such as negotiating SRTP keying material, +/// f. Call Start to commence RTCP reporting. +/// +public class RTPSession : IMediaSession, IDisposable { - public delegate int ProtectRtpPacket(byte[] payload, int length, out int outputBufferLength); - /// - /// The RTPSession class is the primary point for interacting with the Real-Time - /// Protocol. It manages all the resources required for setting up and then sending - /// and receiving RTP packets. This class IS designed to be inherited by child - /// classes and for child classes to add audio and video processing logic. + /// Reduced to be smaller than MTU (1400) because header will add extra 2 bytes that will fail to deliver to linux + /// as the real size of the package will be 1402. /// - /// - /// The setting up of an RTP stream involved the exchange of Session Descriptions - /// (SDP) with the remote party. This class has adopted the mechanism used by WebRTC. - /// The steps are: - /// 1. If acting as the initiator: - /// a. Create offer, - /// b. Send offer to remote party and get their answer (external to this class, requires signalling), - /// c. Set remote description, - /// d. Optionally perform any additional set up, such as negotiating SRTP keying material, - /// e. Call Start to commence RTCP reporting. - /// 2. If acting as the recipient: - /// a. Receive offer, - /// b. Set remote description. This step MUST be done before an SDP answer can be generated. - /// This step can also result in an error condition if the codecs/formats offered aren't supported, - /// c. Create answer, - /// d. Send answer to remote party (external to this class, requires signalling), - /// e. Optionally perform any additional set up, such as negotiating SRTP keying material, - /// f. Call Start to commence RTCP reporting. - /// - public class RTPSession : IMediaSession, IDisposable - { - /// - /// Reduced to be smaller than MTU (1400) because header will add extra 2 bytes that will fail to deliver to linux - /// as the real size of the package will be 1402. - /// - protected internal const int RTP_MAX_PAYLOAD = 1200; + protected internal const int RTP_MAX_PAYLOAD = 1200; - /// - /// From libsrtp: SRTP_MAX_TRAILER_LEN is the maximum length of the SRTP trailer - /// (authentication tag and MKI) supported by libSRTP.This value is - /// the maximum number of octets that will be added to an RTP packet by - /// srtp_protect(). - /// - /// srtp_protect(): - /// @warning This function assumes that it can write SRTP_MAX_TRAILER_LEN - /// into the location in memory immediately following the RTP packet. - /// Callers MUST ensure that this much writeable memory is available in - /// the buffer that holds the RTP packet. - /// - /// srtp_protect_rtcp(): - /// @warning This function assumes that it can write SRTP_MAX_TRAILER_LEN+4 - /// to the location in memory immediately following the RTCP packet. - /// Callers MUST ensure that this much writeable memory is available in - /// the buffer that holds the RTCP packet. - /// - public const int SRTP_MAX_PREFIX_LENGTH = 148; - protected internal const int DEFAULT_AUDIO_CLOCK_RATE = 8000; - public const int RTP_EVENT_DEFAULT_SAMPLE_PERIOD_MS = 50; // Default sample period for an RTP event as specified by RFC2833. - public const SDPMediaTypesEnum DEFAULT_MEDIA_TYPE = SDPMediaTypesEnum.audio; // If we can't match an RTP payload ID assume it's audio. - public const int DEFAULT_DTMF_EVENT_PAYLOAD_ID = 101; - public const string RTP_MEDIA_PROFILE = "RTP/AVP"; - public const string RTP_SECUREMEDIA_PROFILE = "RTP/SAVP"; - protected const int SDP_SESSIONID_LENGTH = 10; // The length of the pseudo-random string to use for the session ID. - public const int DTMF_EVENT_DURATION = 1200; // Default duration for a DTMF event. + /// + /// From libsrtp: SRTP_MAX_TRAILER_LEN is the maximum length of the SRTP trailer + /// (authentication tag and MKI) supported by libSRTP.This value is + /// the maximum number of octets that will be added to an RTP packet by + /// srtp_protect(). + /// + /// srtp_protect(): + /// @warning This function assumes that it can write SRTP_MAX_TRAILER_LEN + /// into the location in memory immediately following the RTP packet. + /// Callers MUST ensure that this much writeable memory is available in + /// the buffer that holds the RTP packet. + /// + /// srtp_protect_rtcp(): + /// @warning This function assumes that it can write SRTP_MAX_TRAILER_LEN+4 + /// to the location in memory immediately following the RTCP packet. + /// Callers MUST ensure that this much writeable memory is available in + /// the buffer that holds the RTCP packet. + /// + public const int SRTP_MAX_PREFIX_LENGTH = 148; + protected internal const int DEFAULT_AUDIO_CLOCK_RATE = 8000; + public const int RTP_EVENT_DEFAULT_SAMPLE_PERIOD_MS = 50; // Default sample period for an RTP event as specified by RFC2833. + public const SDPMediaTypesEnum DEFAULT_MEDIA_TYPE = SDPMediaTypesEnum.audio; // If we can't match an RTP payload ID assume it's audio. + public const int DEFAULT_DTMF_EVENT_PAYLOAD_ID = 101; + public const string RTP_MEDIA_PROFILE = "RTP/AVP"; + public const string RTP_SECUREMEDIA_PROFILE = "RTP/SAVP"; + protected const int SDP_SESSIONID_LENGTH = 10; // The length of the pseudo-random string to use for the session ID. + public const int DTMF_EVENT_DURATION = 1200; // Default duration for a DTMF event. - /// - /// When there are no RTP packets being sent for an audio or video stream webrtc.lib - /// still sends RTCP Receiver Reports with this hard coded SSRC. No doubt it's defined - /// in an RFC somewhere but I wasn't able to find it from a quick search. - /// - public const uint RTCP_RR_NOSTREAM_SSRC = 4195875351U; + /// + /// When there are no RTP packets being sent for an audio or video stream webrtc.lib + /// still sends RTCP Receiver Reports with this hard coded SSRC. No doubt it's defined + /// in an RFC somewhere but I wasn't able to find it from a quick search. + /// + public const uint RTCP_RR_NOSTREAM_SSRC = 4195875351U; - protected static readonly ILogger logger = LogFactory.CreateLogger(); + protected static readonly ILogger logger = LogFactory.CreateLogger(); - protected RtpSessionConfig rtpSessionConfig; + protected RtpSessionConfig rtpSessionConfig; - private bool m_acceptRtpFromAny = false; - private string m_sdpSessionID = null; // Need to maintain the same SDP session ID for all offers and answers. - private ulong m_sdpAnnouncementVersion = 0; // The SDP version needs to increase whenever the local SDP is modified (see https://tools.ietf.org/html/rfc6337#section-5.2.5). - internal int m_rtpChannelsCount = 0; // Need to know the number of RTP Channels - private int m_nextMediaInsertionOrder = 0; // Need to keep track of insertion order of different media for SDP renegotiation. + private bool m_acceptRtpFromAny; + private string m_sdpSessionID; // Need to maintain the same SDP session ID for all offers and answers. + private ulong m_sdpAnnouncementVersion; // The SDP version needs to increase whenever the local SDP is modified (see https://tools.ietf.org/html/rfc6337#section-5.2.5). + internal int m_rtpChannelsCount; // Need to know the number of RTP Channels + private int m_nextMediaInsertionOrder; // Need to keep track of insertion order of different media for SDP renegotiation. - // The stream used for the underlying RTP session to create a single RTP channel that will - // be used to multiplex all required media streams. (see addSingleTrack()) - protected MediaStream m_primaryStream; + // The stream used for the underlying RTP session to create a single RTP channel that will + // be used to multiplex all required media streams. (see addSingleTrack()) + protected MediaStream? m_primaryStream; - protected RTPChannel MultiplexRtpChannel = null; + protected RTPChannel? MultiplexRtpChannel; - protected List> audioRemoteSDPSsrcAttributes = new List>(); - protected List> videoRemoteSDPSsrcAttributes = new List>(); - protected List> textRemoteSDPSsrcAttributes = new List>(); + protected List> audioRemoteSDPSsrcAttributes = new List>(); + protected List> videoRemoteSDPSsrcAttributes = new List>(); + protected List> textRemoteSDPSsrcAttributes = new List>(); - /// - /// Track if current remote description is invalid (used in Renegotiation logic) - /// - public virtual bool RequireRenegotiation { get; protected internal set; } + /// + /// Track if current remote description is invalid (used in Renegotiation logic) + /// + public virtual bool RequireRenegotiation { get; protected internal set; } - /// - /// The primary stream for this session - can be an AudioStream or a VideoStream - /// - public MediaStream PrimaryStream + /// + /// The primary stream for this session - can be an AudioStream or a VideoStream + /// + public MediaStream? PrimaryStream + { + get { - get - { - return m_primaryStream; - } + return m_primaryStream; } + } - /// - /// The primary Audio Stream for this session - /// - public AudioStream AudioStream + /// + /// The primary Audio Stream for this session + /// + public AudioStream? AudioStream + { + get { - get + if (AudioStreamList.Count > 0) { - if (AudioStreamList.Count > 0) - { - return AudioStreamList[0]; - } - return null; + return AudioStreamList[0]; } + return null; } + } - /// - /// The primary Video Stream for this session - /// - public VideoStream VideoStream + /// + /// The primary Video Stream for this session + /// + public VideoStream? VideoStream + { + get { - get + if (VideoStreamList.Count > 0) { - if (VideoStreamList.Count > 0) - { - return VideoStreamList[0]; - } - return null; + return VideoStreamList[0]; } + return null; } + } - public TextStream TextStream + public TextStream? TextStream + { + get { - get + if (TextStreamList.Count > 0) { - if (TextStreamList.Count > 0) - { - return TextStreamList[0]; - } - return null; + return TextStreamList[0]; } + return null; } + } - /// - /// The primary local audio stream for this session. Will be null if we are not sending audio. - /// - public MediaStreamTrack AudioLocalTrack => AudioStream?.LocalTrack; + /// + /// The primary local audio stream for this session. Will be null if we are not sending audio. + /// + public MediaStreamTrack? AudioLocalTrack => AudioStream?.LocalTrack; - /// - /// The primary remote audio track for this session. Will be null if the remote party is not sending audio. - /// - public MediaStreamTrack AudioRemoteTrack => AudioStream?.RemoteTrack; + /// + /// The primary remote audio track for this session. Will be null if the remote party is not sending audio. + /// + public MediaStreamTrack? AudioRemoteTrack => AudioStream?.RemoteTrack; - /// - /// The primary reporting session for the audio stream. Will be null if only video is being sent. - /// - public RTCPSession AudioRtcpSession => AudioStream?.RtcpSession; + /// + /// The primary reporting session for the audio stream. Will be null if only video is being sent. + /// + public RTCPSession? AudioRtcpSession => AudioStream?.RtcpSession; - /// - /// The primary Audio remote RTP end point this stream is sending media to. - /// - public IPEndPoint AudioDestinationEndPoint => AudioStream?.DestinationEndPoint; + /// + /// The primary Audio remote RTP end point this stream is sending media to. + /// + public IPEndPoint? AudioDestinationEndPoint => AudioStream?.DestinationEndPoint; - /// - /// The primary Audio remote RTP control end point this stream is sending to RTCP reports for the media stream to. - /// - public IPEndPoint AudioControlDestinationEndPoint => AudioStream?.ControlDestinationEndPoint; + /// + /// The primary Audio remote RTP control end point this stream is sending to RTCP reports for the media stream to. + /// + public IPEndPoint? AudioControlDestinationEndPoint => AudioStream?.ControlDestinationEndPoint; - /// - /// The primary local video track for this session. Will be null if we are not sending video. - /// - public MediaStreamTrack VideoLocalTrack => VideoStream?.LocalTrack; + /// + /// The primary local video track for this session. Will be null if we are not sending video. + /// + public MediaStreamTrack? VideoLocalTrack => VideoStream?.LocalTrack; - /// - /// The primary remote video track for this session. Will be null if the remote party is not sending video. - /// - public MediaStreamTrack VideoRemoteTrack => VideoStream?.RemoteTrack; + /// + /// The primary remote video track for this session. Will be null if the remote party is not sending video. + /// + public MediaStreamTrack? VideoRemoteTrack => VideoStream?.RemoteTrack; - /// - /// The primary reporting session for the video stream. Will be null if only audio is being sent. - /// - public RTCPSession VideoRtcpSession => VideoStream?.RtcpSession; + /// + /// The primary reporting session for the video stream. Will be null if only audio is being sent. + /// + public RTCPSession? VideoRtcpSession => VideoStream?.RtcpSession; - /// - /// The primary Video remote RTP end point this stream is sending media to. - /// - public IPEndPoint VideoDestinationEndPoint => VideoStream?.DestinationEndPoint; + /// + /// The primary Video remote RTP end point this stream is sending media to. + /// + public IPEndPoint? VideoDestinationEndPoint => VideoStream?.DestinationEndPoint; - /// - /// The primary Video remote RTP control end point this stream is sending to RTCP reports for the media stream to. - /// - public IPEndPoint VideoControlDestinationEndPoint => VideoStream?.ControlDestinationEndPoint; + /// + /// The primary Video remote RTP control end point this stream is sending to RTCP reports for the media stream to. + /// + public IPEndPoint? VideoControlDestinationEndPoint => VideoStream?.ControlDestinationEndPoint; - /// - /// The primary local Text track for this session. Will be null if we are not sending Text. - /// - public MediaStreamTrack TextLocalTrack => TextStream?.LocalTrack; + /// + /// The primary local Text track for this session. Will be null if we are not sending Text. + /// + public MediaStreamTrack? TextLocalTrack => TextStream?.LocalTrack; - /// - /// The primary remote Text track for this session. Will be null if the remote party is not sending Text. - /// - public MediaStreamTrack TextRemoteTrack => TextStream?.RemoteTrack; + /// + /// The primary remote Text track for this session. Will be null if the remote party is not sending Text. + /// + public MediaStreamTrack? TextRemoteTrack => TextStream?.RemoteTrack; - /// - /// The primary reporting session for the text stream. Will be null if only audio or video is being sent. - /// - public RTCPSession TextRtcpSession => TextStream?.RtcpSession; + /// + /// The primary reporting session for the text stream. Will be null if only audio or video is being sent. + /// + public RTCPSession? TextRtcpSession => TextStream?.RtcpSession; - /// - /// The primary Text remote RTP end point this stream is sending media to. - /// - public IPEndPoint TextDestinationEndPoint => TextStream?.DestinationEndPoint; + /// + /// The primary Text remote RTP end point this stream is sending media to. + /// + public IPEndPoint? TextDestinationEndPoint => TextStream?.DestinationEndPoint; - /// - /// The primary Text remote RTP control end point this stream is sending to RTCP reports for the media stream to. - /// - public IPEndPoint TextControlDestinationEndPoint => TextStream?.ControlDestinationEndPoint; + /// + /// The primary Text remote RTP control end point this stream is sending to RTCP reports for the media stream to. + /// + public IPEndPoint? TextControlDestinationEndPoint => TextStream?.ControlDestinationEndPoint; - /// - /// List of all Audio Streams for this session - /// - public List AudioStreamList { get; private set; } = new List(); + /// + /// List of all Audio Streams for this session + /// + public List AudioStreamList { get; private set; } = new List(); + + /// + /// List of all Video Streams for this session + /// + public List VideoStreamList { get; private set; } = new List(); - /// - /// List of all Video Streams for this session - /// - public List VideoStreamList { get; private set; } = new List(); + /// + /// List of all Text Streams for this session + /// + public List TextStreamList { get; private set; } = new List(); - /// - /// List of all Text Streams for this session - /// - public List TextStreamList { get; private set; } = new List(); + /// + /// The SDP offered by the remote call party for this session. + /// + public SDP? RemoteDescription { get; protected set; } - /// - /// The SDP offered by the remote call party for this session. - /// - public SDP RemoteDescription { get; protected set; } - - /// - /// If this session is using a secure context this flag MUST be set to indicate - /// the security delegate (SrtpProtect, SrtpUnprotect etc) have been set. - /// - public bool IsSecureContextReady() - { - if (HasAudio && !AudioStream.IsSecurityContextReady()) + /// + /// If this session is using a secure context this flag MUST be set to indicate + /// the security delegate (SrtpProtect, SrtpUnprotect etc) have been set. + /// + public bool IsSecureContextReady() + { + if (HasAudio) + { + Debug.Assert(AudioStream is { }); + if (!AudioStream.IsSecurityContextReady()) { return false; } + } - if (HasVideo && !VideoStream.IsSecurityContextReady()) + if (HasVideo) + { + Debug.Assert(VideoStream is { }); + if (!VideoStream.IsSecurityContextReady()) { return false; } + } - if (HasText && !TextStream.IsSecurityContextReady()) + if (HasText) + { + Debug.Assert(TextStream is { }); + if (!TextStream.IsSecurityContextReady()) { return false; } + } + + return true; + } + + /// + /// If this session is using a secure context this list MAY contain custom + /// Crypto Suites + /// + public FrozenSet SrtpCryptoSuites { get; set; } = FrozenSet.Empty; - return true; + /// + /// Indicates the maximum frame size that can be reconstructed from RTP packets during the depacketisation + /// process. + /// + public int MaxReconstructedVideoFrameSize + { + get + { + Debug.Assert(VideoStream is { }); + return VideoStream.MaxReconstructedVideoFrameSize; } - /// - /// If this session is using a secure context this list MAY contain custom - /// Crypto Suites - /// - public List SrtpCryptoSuites { get; set; } + set + { + Debug.Assert(VideoStream is { }); + VideoStream.MaxReconstructedVideoFrameSize = value; + } + } - /// - /// Indicates the maximum frame size that can be reconstructed from RTP packets during the depacketisation - /// process. - /// - public int MaxReconstructedVideoFrameSize { get => VideoStream.MaxReconstructedVideoFrameSize; set => VideoStream.MaxReconstructedVideoFrameSize = value; } + /// + /// Indicates whether the session has been closed. Once a session is closed it cannot + /// be restarted. + /// + public bool IsClosed { get; private set; } - /// - /// Indicates whether the session has been closed. Once a session is closed it cannot - /// be restarted. - /// - public bool IsClosed { get; private set; } + [Obsolete("Use IsAudioStarted.")] + public bool IsStarted => IsAudioStarted; - [Obsolete("Use IsAudioStarted.")] - public bool IsStarted => IsAudioStarted; + /// + /// Indicates whether the audio session has been started. Starting a audio session tells the RTP + /// socket to start receiving, + /// + public bool IsAudioStarted { get; private set; } - /// - /// Indicates whether the audio session has been started. Starting a audio session tells the RTP - /// socket to start receiving, - /// - public bool IsAudioStarted { get; private set; } + /// + /// Indicates whether the video session has been started. Starting a video session tells the RTP + /// socket to start receiving, + /// + public bool IsVideoStarted { get; private set; } - /// - /// Indicates whether the video session has been started. Starting a video session tells the RTP - /// socket to start receiving, - /// - public bool IsVideoStarted { get; private set; } + /// + /// Indicates whether the text session has been started. Starting a text session tells the RTP + /// socket to start receiving, + /// + public bool IsTextStarted { get; private set; } - /// - /// Indicates whether the text session has been started. Starting a text session tells the RTP - /// socket to start receiving, - /// - public bool IsTextStarted { get; private set; } + /// + /// Indicates whether this session is using audio. + /// + public bool HasAudio + { + get + { + return AudioStream?.HasAudio == true; + } + } - /// - /// Indicates whether this session is using audio. - /// - public bool HasAudio + /// + /// Indicates whether this session is using video. + /// + public bool HasVideo + { + get { - get - { - return AudioStream?.HasAudio == true; - } + return VideoStream?.HasVideo == true; } + } - /// - /// Indicates whether this session is using video. - /// - public bool HasVideo + /// + /// Indicates whether this session is using RTT. + /// + public bool HasText + { + get { - get - { - return VideoStream?.HasVideo == true; - } + return TextStream?.HasText == true; } + } - /// - /// Indicates whether this session is using RTT. - /// - public bool HasText + /// + /// If set to true RTP will be accepted from ANY remote end point. If false + /// certain rules are used to determine whether RTP should be accepted for + /// a particular audio or video stream. It is recommended to leave the + /// value to false unless a specific need exists. + /// + public bool AcceptRtpFromAny + { + get { - get - { - return TextStream?.HasText == true; - } + return m_acceptRtpFromAny; } - /// - /// If set to true RTP will be accepted from ANY remote end point. If false - /// certain rules are used to determine whether RTP should be accepted for - /// a particular audio or video stream. It is recommended to leave the - /// value to false unless a specific need exists. - /// - public bool AcceptRtpFromAny + set { - get + m_acceptRtpFromAny = value; + foreach (var audioStream in AudioStreamList) { - return m_acceptRtpFromAny; + audioStream.AcceptRtpFromAny = value; } - - set + foreach (var videoStream in VideoStreamList) { - m_acceptRtpFromAny = value; - foreach (var audioStream in AudioStreamList) - { - audioStream.AcceptRtpFromAny = value; - } - foreach (var videoStream in VideoStreamList) - { - videoStream.AcceptRtpFromAny = value; - } - foreach (var textStream in TextStreamList) - { - textStream.AcceptRtpFromAny = value; - } - } - } - - /// - /// Set if the session has been bound to a specific IP address. - /// Normally not required but some esoteric call or network set ups may need. - /// - public IPAddress RtpBindAddress => rtpSessionConfig.BindAddress; - - /// - /// Gets fired when the remote SDP is received and the set of common text formats is set. (on the primary one) - /// - public event Action> OnTextFormatsNegotiated; - - /// - /// Gets fired when the remote SDP is received and the set of common text formats is set. (using its index) - /// - public event Action> OnTextFormatsNegotiatedByIndex; - - /// - /// Gets fired when the remote SDP is received and the set of common audio formats is set. (on the primary one) - /// - public event Action> OnAudioFormatsNegotiated; - - /// - /// Gets fired when the remote SDP is received and the set of common audio formats is set. (using its index) - /// - public event Action> OnAudioFormatsNegotiatedByIndex; - - /// - /// Event for receiving an encoded audio frame from the remote party. Encoded in this - /// case refers to media encoding, e.g. PCMU, OPUS. For audio payloads RTP framing is not - /// typically used so each frame will correspond to a single RTP packet. This event - /// aggregates receiving audio frames from all media stream attached to the RTP session. - /// THe media stream index can be used to identify which stream the frame is from in - /// the case there are multiple audio streams. - /// - public event Action OnAudioFrameReceived; - - /// - /// Gets fired when the remote SDP is received and the set of common video formats is set. (on the primary one) - /// - public event Action> OnVideoFormatsNegotiated; - - /// - /// Gets fired when the remote SDP is received and the set of common video formats is set. (using its index) - /// - public event Action> OnVideoFormatsNegotiatedByIndex; - - /// - /// Gets fired when a full video frame is reconstructed from one or more RTP packets - /// received from the remote party. (on the primary one) - /// - /// - /// - Received from end point, - /// - The frame timestamp, - /// - The encoded video frame payload. - /// - The video format of the encoded frame. - /// - public event Action OnVideoFrameReceived; - - /// - /// Gets fired when a full video frame is reconstructed from one or more RTP packets - /// received from the remote party. (using its index) - /// - /// - /// - Index of the VideoStream - /// - Received from end point, - /// - The frame timestamp, - /// - The encoded video frame payload. - /// - The video format of the encoded frame. - /// - public event Action OnVideoFrameReceivedByIndex; - - /// - /// It's recommended to use the and - /// events where applicable in preference to this was lowl level RTP packet event. - /// - /// Gets fired when an RTP packet is received from a remote party. (on the primary one) - /// Parameters are: - /// - Remote endpoint packet was received from, - /// - The media type the packet contains, will be audio or video, - /// - The full RTP packet. - /// - public event Action OnRtpPacketReceived; - - /// - /// It's recommended to use the and - /// events where applicable in preference to this was lowl level RTP packet event. - /// - /// Gets fired when an RTP packet is received from a remote party (using its index). - /// Parameters are: - /// - index of the AudioStream or VideoStream - /// - Remote endpoint packet was received from, - /// - The media type the packet contains, will be audio or video, - /// - The full RTP packet. - /// - public event Action OnRtpPacketReceivedByIndex; - - /// - /// Gets fired when an RTP Header packet is received from a remote party. (on the primary one) - /// Parameters are: - /// - Remote endpoint packet was received from, - /// - The media type the packet contains, will be audio or video, - /// - The RTP Header extension URI, - /// - Object/Value of the header - /// - public event Action OnRtpHeaderReceived; - - /// - /// Gets fired when an RTP Header packet is received from a remote party. - /// Parameters are: - /// - index of the AudioStream or VideoStream - /// - Remote endpoint packet was received from, - /// - The media type the packet contains, will be audio or video, - /// - The RTP Header extension URI, - /// - Object/Value of the header - /// - public event Action OnRtpHeaderReceivedByIndex; - - /// - /// Gets fired when an RTP event is detected on the remote call party's RTP stream (on the primary one). - /// - public event Action OnRtpEvent; - - /// - /// Gets fired when an RTP event is detected on the remote call party's RTP stream (using its index). - /// - public event Action OnRtpEventByIndex; - - /// - /// Gets fired when the RTP session and underlying channel are closed. - /// - public event Action OnRtpClosed; - - /// - /// Gets fired when an RTCP BYE packet is received from the remote party. - /// The string parameter contains the BYE reason. Normally a BYE - /// report means the RTP session is finished. But... cases have been observed where - /// an RTCP BYE is received when a remote party is put on hold and then the session - /// resumes when take off hold. It's up to the application to decide what action to - /// take when n RTCP BYE is received. - /// - public event Action OnRtcpBye; - - /// - /// Fires when the connection for a media type (the primary one) is classified as timed out due to not - /// receiving any RTP or RTCP packets within the given period. - /// - public event Action OnTimeout; - - /// - /// Fires when the connection for a media type (using its index) is classified as timed out due to not - /// receiving any RTP or RTCP packets within the given period. - /// - public event Action OnTimeoutByIndex; - - /// - /// Gets fired when an RTCP report is received (the primary one). This event is for diagnostics only. - /// - public event Action OnReceiveReport; - - /// - /// Gets fired when an RTCP report is received (using its index). This event is for diagnostics only. - /// - public event Action OnReceiveReportByIndex; - - /// - /// Gets fired when an RTCP report is sent (the primary one). This event is for diagnostic purposes only. - /// - public event Action OnSendReport; - - /// - /// Gets fired when an RTCP report is sent (using its index). This event is for diagnostic purposes only. - /// - public event Action OnSendReportByIndex; - - /// - /// Gets fired when the start method is called on the session. This is the point - /// audio and video sources should commence generating samples. - /// - public event Action OnStarted; - - /// - /// Gets fired when the session is closed. This is the point audio and video - /// source should stop generating samples. - /// - public event Action OnClosed; - - public event Action OnRemoteDescriptionChanged; - - /// - /// Creates a new RTP session. The synchronisation source and sequence number are initialised to - /// pseudo random values. - /// - /// If true RTCP reports will be multiplexed with RTP on a single channel. - /// If false (standard mode) then a separate socket is used to send and receive RTCP reports. - /// If true indicated this session is using SRTP to encrypt and authorise - /// RTP and RTCP packets. No communications or reporting will commence until the - /// is explicitly set as complete. - /// If true only a single RTP socket will be used for both audio - /// and video (standard case for WebRTC). If false two separate RTP sockets will be used for - /// audio and video (standard case for VoIP). - /// Optional. If specified this address will be used as the bind address for any RTP - /// and control sockets created. Generally this address does not need to be set. The default behaviour - /// is to bind to [::] or 0.0.0.0,d depending on system support, which minimises network routing - /// causing connection issues. - /// Optional. If specified a single attempt will be made to bind the RTP socket - /// on this port. It's recommended to leave this parameter as the default of 0 to let the Operating - /// System select the port number. - public RTPSession(bool isMediaMultiplexed, bool isRtcpMultiplexed, bool isSecure, IPAddress bindAddress = null, int bindPort = 0, PortRange portRange = null) - : this(new RtpSessionConfig - { - IsMediaMultiplexed = isMediaMultiplexed, - IsRtcpMultiplexed = isRtcpMultiplexed, - RtpSecureMediaOption = isSecure ? RtpSecureMediaOptionEnum.DtlsSrtp : RtpSecureMediaOptionEnum.None, - BindAddress = bindAddress, - BindPort = bindPort, - RtpPortRange = portRange - }) - { - } - - /// - /// Creates a new RTP session. The synchronisation source and sequence number are initialised to - /// pseudo random values. - /// - /// Contains required settings. - public RTPSession(RtpSessionConfig config) - { - rtpSessionConfig = config; - m_sdpSessionID = Crypto.GetRandomInt(SDP_SESSIONID_LENGTH).ToString(); - - if (rtpSessionConfig.UseSdpCryptoNegotiation) + videoStream.AcceptRtpFromAny = value; + } + foreach (var textStream in TextStreamList) { - SrtpCryptoSuites = new List(); - SrtpCryptoSuites.Add(SDPSecurityDescription.CryptoSuites.AES_CM_128_HMAC_SHA1_80); - SrtpCryptoSuites.Add(SDPSecurityDescription.CryptoSuites.AES_CM_128_HMAC_SHA1_32); + textStream.AcceptRtpFromAny = value; } } + } + + /// + /// Set if the session has been bound to a specific IP address. + /// Normally not required but some esoteric call or network set ups may need. + /// + public IPAddress RtpBindAddress + { + get + { + Debug.Assert(rtpSessionConfig is { }); + var address = rtpSessionConfig.BindAddress; + Debug.Assert(address is { }); + return address!; + } + } + + /// + /// Gets fired when the remote SDP is received and the set of common text formats is set. (on the primary one) + /// + public event Action>? OnTextFormatsNegotiated; + + /// + /// Gets fired when the remote SDP is received and the set of common text formats is set. (using its index) + /// + public event Action>? OnTextFormatsNegotiatedByIndex; + + /// + /// Gets fired when the remote SDP is received and the set of common audio formats is set. (on the primary one) + /// + public event Action>? OnAudioFormatsNegotiated; + + /// + /// Gets fired when the remote SDP is received and the set of common audio formats is set. (using its index) + /// + public event Action>? OnAudioFormatsNegotiatedByIndex; + + /// + /// Event for receiving an encoded audio frame from the remote party. Encoded in this + /// case refers to media encoding, e.g. PCMU, OPUS. For audio payloads RTP framing is not + /// typically used so each frame will correspond to a single RTP packet. This event + /// aggregates receiving audio frames from all media stream attached to the RTP session. + /// THe media stream index can be used to identify which stream the frame is from in + /// the case there are multiple audio streams. + /// + public event Action? OnAudioFrameReceived; + + /// + /// Gets fired when the remote SDP is received and the set of common video formats is set. (on the primary one) + /// + public event Action>? OnVideoFormatsNegotiated; + + /// + /// Gets fired when the remote SDP is received and the set of common video formats is set. (using its index) + /// + public event Action>? OnVideoFormatsNegotiatedByIndex; + + /// + /// Gets fired when a full video frame is reconstructed from one or more RTP packets + /// received from the remote party. (on the primary one) + /// + /// + /// - Received from end point, + /// - The frame timestamp, + /// - The encoded video frame payload. + /// - The video format of the encoded frame. + /// + public event Action, VideoFormat>? OnVideoFrameReceived; + + /// + /// Gets fired when a full video frame is reconstructed from one or more RTP packets + /// received from the remote party. (using its index) + /// + /// + /// - Index of the VideoStream + /// - Received from end point, + /// - The frame timestamp, + /// - The encoded video frame payload. + /// - The video format of the encoded frame. + /// + public event Action, VideoFormat>? OnVideoFrameReceivedByIndex; + + /// + /// It's recommended to use the and + /// events where applicable in preference to this was lowl level RTP packet event. + /// + /// Gets fired when an RTP packet is received from a remote party. (on the primary one) + /// Parameters are: + /// - Remote endpoint packet was received from, + /// - The media type the packet contains, will be audio or video, + /// - The full RTP packet. + /// + public event Action? OnRtpPacketReceived; + + /// + /// It's recommended to use the and + /// events where applicable in preference to this was lowl level RTP packet event. + /// + /// Gets fired when an RTP packet is received from a remote party (using its index). + /// Parameters are: + /// - index of the AudioStream or VideoStream + /// - Remote endpoint packet was received from, + /// - The media type the packet contains, will be audio or video, + /// - The full RTP packet. + /// + public event Action? OnRtpPacketReceivedByIndex; + + /// + /// Gets fired when an RTP Header packet is received from a remote party. (on the primary one) + /// Parameters are: + /// - Remote endpoint packet was received from, + /// - The media type the packet contains, will be audio or video, + /// - The RTP Header extension URI, + /// - Object/Value of the header + /// + public event Action? OnRtpHeaderReceived; + + /// + /// Gets fired when an RTP Header packet is received from a remote party. + /// Parameters are: + /// - index of the AudioStream or VideoStream + /// - Remote endpoint packet was received from, + /// - The media type the packet contains, will be audio or video, + /// - The RTP Header extension URI, + /// - Object/Value of the header + /// + public event Action? OnRtpHeaderReceivedByIndex; + + /// + /// Gets fired when an RTP event is detected on the remote call party's RTP stream (on the primary one). + /// + public event Action? OnRtpEvent; + + /// + /// Gets fired when an RTP event is detected on the remote call party's RTP stream (using its index). + /// + public event Action? OnRtpEventByIndex; + + /// + /// Gets fired when the RTP session and underlying channel are closed. + /// + public event Action? OnRtpClosed; + + /// + /// Gets fired when an RTCP BYE packet is received from the remote party. + /// The string parameter contains the BYE reason. Normally a BYE + /// report means the RTP session is finished. But... cases have been observed where + /// an RTCP BYE is received when a remote party is put on hold and then the session + /// resumes when take off hold. It's up to the application to decide what action to + /// take when n RTCP BYE is received. + /// + public event Action? OnRtcpBye; + + /// + /// Fires when the connection for a media type (the primary one) is classified as timed out due to not + /// receiving any RTP or RTCP packets within the given period. + /// + public event Action? OnTimeout; - protected void ResetRemoteSDPSsrcAttributes() + /// + /// Fires when the connection for a media type (using its index) is classified as timed out due to not + /// receiving any RTP or RTCP packets within the given period. + /// + public event Action? OnTimeoutByIndex; + + /// + /// Gets fired when an RTCP report is received (the primary one). This event is for diagnostics only. + /// + public event Action? OnReceiveReport; + + /// + /// Gets fired when an RTCP report is received (using its index). This event is for diagnostics only. + /// + public event Action? OnReceiveReportByIndex; + + /// + /// Gets fired when an RTCP report is sent (the primary one). This event is for diagnostic purposes only. + /// + public event Action? OnSendReport; + + /// + /// Gets fired when an RTCP report is sent (using its index). This event is for diagnostic purposes only. + /// + public event Action? OnSendReportByIndex; + + /// + /// Gets fired when the start method is called on the session. This is the point + /// audio and video sources should commence generating samples. + /// + public event Action? OnStarted; + + /// + /// Gets fired when the session is closed. This is the point audio and video + /// source should stop generating samples. + /// + public event Action? OnClosed; + + public event Action? OnRemoteDescriptionChanged; + + /// + /// Creates a new RTP session. The synchronisation source and sequence number are initialised to + /// pseudo random values. + /// + /// If true RTCP reports will be multiplexed with RTP on a single channel. + /// If false (standard mode) then a separate socket is used to send and receive RTCP reports. + /// If true indicated this session is using SRTP to encrypt and authorise + /// RTP and RTCP packets. No communications or reporting will commence until the + /// is explicitly set as complete. + /// If true only a single RTP socket will be used for both audio + /// and video (standard case for WebRTC). If false two separate RTP sockets will be used for + /// audio and video (standard case for VoIP). + /// Optional. If specified this address will be used as the bind address for any RTP + /// and control sockets created. Generally this address does not need to be set. The default behaviour + /// is to bind to [::] or 0.0.0.0,d depending on system support, which minimises network routing + /// causing connection issues. + /// Optional. If specified a single attempt will be made to bind the RTP socket + /// on this port. It's recommended to leave this parameter as the default of 0 to let the Operating + /// System select the port number. + public RTPSession(bool isMediaMultiplexed, bool isRtcpMultiplexed, bool isSecure, IPAddress? bindAddress = null, int bindPort = 0, PortRange? portRange = null) + : this(new RtpSessionConfig + { + IsMediaMultiplexed = isMediaMultiplexed, + IsRtcpMultiplexed = isRtcpMultiplexed, + RtpSecureMediaOption = isSecure ? RtpSecureMediaOptionEnum.DtlsSrtp : RtpSecureMediaOptionEnum.None, + BindAddress = bindAddress, + BindPort = bindPort, + RtpPortRange = portRange + }) + { + } + + /// + /// Creates a new RTP session. The synchronisation source and sequence number are initialised to + /// pseudo random values. + /// + /// Contains required settings. + public RTPSession(RtpSessionConfig config) + { + rtpSessionConfig = config; + m_sdpSessionID = Crypto.GetRandomInt(SDP_SESSIONID_LENGTH).ToString(); + + if (rtpSessionConfig.UseSdpCryptoNegotiation) { - audioRemoteSDPSsrcAttributes.Clear(); - videoRemoteSDPSsrcAttributes.Clear(); - textRemoteSDPSsrcAttributes.Clear(); + SrtpCryptoSuites = FrozenSet.ToFrozenSet( + [ + SDPSecurityDescription.CryptoSuites.AES_CM_128_HMAC_SHA1_80, + SDPSecurityDescription.CryptoSuites.AES_CM_128_HMAC_SHA1_32 + ]); } + } + + protected void ResetRemoteSDPSsrcAttributes() + { + audioRemoteSDPSsrcAttributes.Clear(); + videoRemoteSDPSsrcAttributes.Clear(); + textRemoteSDPSsrcAttributes.Clear(); + } - protected void AddRemoteSDPSsrcAttributes(SDPMediaTypesEnum mediaType, List sdpSsrcAttributes) + protected void AddRemoteSDPSsrcAttributes(SDPMediaTypesEnum mediaType, List sdpSsrcAttributes) + { + switch (mediaType) { - if (mediaType == SDPMediaTypesEnum.audio) - { + case SDPMediaTypesEnum.audio: audioRemoteSDPSsrcAttributes.Add(sdpSsrcAttributes); - } - else if (mediaType == SDPMediaTypesEnum.video) - { + break; + case SDPMediaTypesEnum.video: videoRemoteSDPSsrcAttributes.Add(sdpSsrcAttributes); - } - else if (mediaType == SDPMediaTypesEnum.text) - { + break; + case SDPMediaTypesEnum.text: textRemoteSDPSsrcAttributes.Add(sdpSsrcAttributes); - } + break; } + } - protected void LogRemoteSDPSsrcAttributes() + private void CreateRtcpSession(MediaStream mediaStream) + { + if (mediaStream.CreateRtcpSession()) { - var stringBuilder = new StringBuilder("Audio:[ "); - foreach (var audioRemoteSDPSsrcAttribute in audioRemoteSDPSsrcAttributes) - { - foreach (var attr in audioRemoteSDPSsrcAttribute) - { - stringBuilder.Append(attr.SSRC).Append(" - "); - } - } - stringBuilder.Append("] \r\n Video: [ "); - foreach (var videoRemoteSDPSsrcAttribute in videoRemoteSDPSsrcAttributes) - { - stringBuilder.Append(" ["); - foreach (var attr in videoRemoteSDPSsrcAttribute) - { - stringBuilder.Append(attr.SSRC).Append(" - "); - } - stringBuilder.Append("] "); - } - stringBuilder.Append("] \r\n Text: [ "); - foreach (var textRemoteSDPSsrcAttribute in textRemoteSDPSsrcAttributes) - { - stringBuilder.Append(" ["); - foreach (var attr in textRemoteSDPSsrcAttribute) - { - stringBuilder.Append(attr.SSRC).Append(" - "); - } - stringBuilder.Append("] "); - } - stringBuilder.Append(" ]"); - var str = stringBuilder.ToString(); - logger.LogDebug("LogRemoteSDPSsrcAttributes: {RemoteSDPSsrcAttributes}", str); - } + mediaStream.OnTimeoutByIndex += RaiseOnTimeOut; + mediaStream.OnSendReportByIndex += RaiseOnSendReport; + mediaStream.OnRtpEventByIndex += RaisedOnRtpEvent; + mediaStream.OnRtpPacketReceivedByIndex += RaisedOnRtpPacketReceived; + mediaStream.OnRtpHeaderReceivedByIndex += RaisedOnRtpHeaderReceived; + mediaStream.OnReceiveReportByIndex += RaisedOnOnReceiveReport; + Debug.Assert(mediaStream.RtcpSession is { }); + mediaStream.RtcpSession.OnReportReadyToSend += SendRtcpReport; - private void CreateRtcpSession(MediaStream mediaStream) - { - if (mediaStream.CreateRtcpSession()) + switch (mediaStream.MediaType) { - mediaStream.OnTimeoutByIndex += RaiseOnTimeOut; - mediaStream.OnSendReportByIndex += RaiseOnSendReport; - mediaStream.OnRtpEventByIndex += RaisedOnRtpEvent; - mediaStream.OnRtpPacketReceivedByIndex += RaisedOnRtpPacketReceived; - mediaStream.OnRtpHeaderReceivedByIndex += RaisedOnRtpHeaderReceived; - mediaStream.OnReceiveReportByIndex += RaisedOnOnReceiveReport; - mediaStream.RtcpSession.OnReportReadyToSend += SendRtcpReport; - - if (mediaStream.MediaType == SDPMediaTypesEnum.audio) - { - var audioStream = mediaStream as AudioStream; - if (audioStream != null) + case SDPMediaTypesEnum.audio: + if (mediaStream is AudioStream audioStream) { audioStream.OnAudioFormatsNegotiatedByIndex += RaisedOnAudioFormatsNegotiated; audioStream.OnAudioFrameReceived += RaiseOnAudioFrameReceived; } - } - else if (mediaStream.MediaType == SDPMediaTypesEnum.text) - { - var textStream = mediaStream as TextStream; - if (textStream != null) + break; + case SDPMediaTypesEnum.text: + if (mediaStream is TextStream textStream) { textStream.OnTextFormatsNegotiatedByIndex += RaisedOnTextFormatsNegotiated; // Text streams currently need to dal with the encoded media by directly // handling the RTP packets with one of the OnRtpPacketReceived events. } - } - else - { - var videoStream = mediaStream as VideoStream; - if (videoStream != null) + break; + default: + if (mediaStream is VideoStream videoStream) { videoStream.OnVideoFormatsNegotiatedByIndex += RaisedOnVideoFormatsNegotiated; videoStream.OnVideoFrameReceivedByIndex += RaisedOnOnVideoFrameReceived; } - } + break; } } + } - private void CloseRtcpSession(MediaStream mediaStream, string reason) + private void CloseRtcpSession(MediaStream mediaStream, string? reason) + { + if (mediaStream.RtcpSession is { }) { - if (mediaStream.RtcpSession != null) - { - mediaStream.OnTimeoutByIndex -= RaiseOnTimeOut; - mediaStream.OnSendReportByIndex -= RaiseOnSendReport; - mediaStream.OnRtpEventByIndex -= RaisedOnRtpEvent; - mediaStream.OnRtpPacketReceivedByIndex -= RaisedOnRtpPacketReceived; - mediaStream.OnReceiveReportByIndex -= RaisedOnOnReceiveReport; + mediaStream.OnTimeoutByIndex -= RaiseOnTimeOut; + mediaStream.OnSendReportByIndex -= RaiseOnSendReport; + mediaStream.OnRtpEventByIndex -= RaisedOnRtpEvent; + mediaStream.OnRtpPacketReceivedByIndex -= RaisedOnRtpPacketReceived; + mediaStream.OnReceiveReportByIndex -= RaisedOnOnReceiveReport; - if (mediaStream.MediaType == SDPMediaTypesEnum.audio) + if (mediaStream.MediaType == SDPMediaTypesEnum.audio) + { + if (mediaStream is AudioStream audioStream) { - var audioStream = mediaStream as AudioStream; - if (audioStream != null) - { - audioStream.OnAudioFormatsNegotiatedByIndex -= RaisedOnAudioFormatsNegotiated; - audioStream.OnAudioFrameReceived -= RaiseOnAudioFrameReceived; - } + audioStream.OnAudioFormatsNegotiatedByIndex -= RaisedOnAudioFormatsNegotiated; + audioStream.OnAudioFrameReceived -= RaiseOnAudioFrameReceived; } - else if (mediaStream.MediaType == SDPMediaTypesEnum.text) + } + else if (mediaStream.MediaType == SDPMediaTypesEnum.text) + { + if (mediaStream is TextStream textStream) { - var textStream = mediaStream as TextStream; - if (textStream != null) - { - textStream.OnTextFormatsNegotiatedByIndex -= RaisedOnTextFormatsNegotiated; - } + textStream.OnTextFormatsNegotiatedByIndex -= RaisedOnTextFormatsNegotiated; } - else + } + else + { + if (mediaStream is VideoStream videoStream) { - var videoStream = mediaStream as VideoStream; - if (videoStream != null) - { - videoStream.OnVideoFormatsNegotiatedByIndex -= RaisedOnVideoFormatsNegotiated; - videoStream.OnVideoFrameReceivedByIndex -= RaisedOnOnVideoFrameReceived; - } + videoStream.OnVideoFormatsNegotiatedByIndex -= RaisedOnVideoFormatsNegotiated; + videoStream.OnVideoFrameReceivedByIndex -= RaisedOnOnVideoFrameReceived; } - - mediaStream.RtcpSession?.Close(reason); - mediaStream.RtcpSession = null; } + + mediaStream.RtcpSession?.Close(reason); + mediaStream.RtcpSession = null; } + } - private void RaiseOnTimeOut(int index, SDPMediaTypesEnum media) + private void RaiseOnTimeOut(int index, SDPMediaTypesEnum media) + { + if (index == 0) { - if (index == 0) - { - OnTimeout?.Invoke(media); - } - OnTimeoutByIndex?.Invoke(index, media); + OnTimeout?.Invoke(media); } + OnTimeoutByIndex?.Invoke(index, media); + } - private void RaiseOnSendReport(int index, SDPMediaTypesEnum media, RTCPCompoundPacket report) + private void RaiseOnSendReport(int index, SDPMediaTypesEnum media, RTCPCompoundPacket report) + { + if (index == 0) { - if (index == 0) - { - OnSendReport?.Invoke(media, report); - } - OnSendReportByIndex?.Invoke(index, media, report); + OnSendReport?.Invoke(media, report); + } + OnSendReportByIndex?.Invoke(index, media, report); + } + + private void RaisedOnRtpEvent(int index, IPEndPoint ipEndPoint, RTPEvent rtpEvent, RTPHeader rtpHeader) + { + if (index == 0) + { + OnRtpEvent?.Invoke(ipEndPoint, rtpEvent, rtpHeader); + } + OnRtpEventByIndex?.Invoke(index, ipEndPoint, rtpEvent, rtpHeader); + } + + private void RaisedOnRtpPacketReceived(int index, IPEndPoint ipEndPoint, SDPMediaTypesEnum media, RTPPacket rtpPacket) + { + if (index == 0) + { + OnRtpPacketReceived?.Invoke(ipEndPoint, media, rtpPacket); + } + OnRtpPacketReceivedByIndex?.Invoke(index, ipEndPoint, media, rtpPacket); + } + + private void RaisedOnRtpHeaderReceived(int index, IPEndPoint ipEndPoint, SDPMediaTypesEnum media, string uri, object value) + { + if (index == 0) + { + OnRtpHeaderReceived?.Invoke(ipEndPoint, media, uri, value); + } + OnRtpHeaderReceivedByIndex?.Invoke(index, ipEndPoint, media, uri, value); + } + + private void RaisedOnOnReceiveReport(int index, IPEndPoint ipEndPoint, SDPMediaTypesEnum media, RTCPCompoundPacket report) + { + if (index == 0) + { + OnReceiveReport?.Invoke(ipEndPoint, media, report); + } + OnReceiveReportByIndex?.Invoke(index, ipEndPoint, media, report); + } + + private void RaisedOnAudioFormatsNegotiated(int index, List audioFormats) + { + if (index == 0) + { + OnAudioFormatsNegotiated?.Invoke(audioFormats); + } + OnAudioFormatsNegotiatedByIndex?.Invoke(index, audioFormats); + } + + private void RaisedOnVideoFormatsNegotiated(int index, List videoFormats) + { + if (index == 0) + { + OnVideoFormatsNegotiated?.Invoke(videoFormats); } + OnVideoFormatsNegotiatedByIndex?.Invoke(index, videoFormats); + } - private void RaisedOnRtpEvent(int index, IPEndPoint ipEndPoint, RTPEvent rtpEvent, RTPHeader rtpHeader) + private void RaisedOnTextFormatsNegotiated(int index, List textFormats) + { + if (index == 0) + { + OnTextFormatsNegotiated?.Invoke(textFormats); + } + OnTextFormatsNegotiatedByIndex?.Invoke(index, textFormats); + } + + private void RaisedOnOnVideoFrameReceived(int index, IPEndPoint ipEndPoint, uint timestamp, ReadOnlyMemory frame, VideoFormat videoFormat) + { + if (index == 0) + { + OnVideoFrameReceived?.Invoke(ipEndPoint, timestamp, frame, videoFormat); + } + OnVideoFrameReceivedByIndex?.Invoke(index, ipEndPoint, timestamp, frame, videoFormat); + } + + private void RaiseOnAudioFrameReceived(EncodedAudioFrame encodedAudioFrame) + { + OnAudioFrameReceived?.Invoke(encodedAudioFrame); + } + + /// + /// Generates the SDP for an offer that can be made to a remote user agent. + /// + /// Optional. If specified this IP address + /// will be used as the address advertised in the SDP offer. If not provided + /// the kernel routing table will be used to determine the local IP address used + /// for Internet access. Any and IPv6Any are special cases. If they are set the respective + /// Internet facing IPv4 or IPv6 address will be used. + /// A task that when complete contains the SDP offer. + public virtual SDP? CreateOffer(IPAddress? connectionAddress) + { + if (AudioStream is { LocalTrack: not null } || + VideoStream is { LocalTrack: not null } || + TextStream is { LocalTrack: not null }) { - if (index == 0) + var mediaStreams = GetMediaStreams(); + + //Revert to DefaultStreamStatus + foreach (var mediaStream in mediaStreams) { - OnRtpEvent?.Invoke(ipEndPoint, rtpEvent, rtpHeader); + if (mediaStream.LocalTrack is { StreamStatus: MediaStreamStatusEnum.Inactive }) + { + mediaStream.LocalTrack.StreamStatus = mediaStream.LocalTrack.DefaultStreamStatus; + } } - OnRtpEventByIndex?.Invoke(index, ipEndPoint, rtpEvent, rtpHeader); + + RequireRenegotiation = true; + + var sdpConnectionAddress = GetSdpConnectionAddress(connectionAddress, null); + + return GetSessionDescription(mediaStreams, sdpConnectionAddress); + } + else + { + logger.LogRtpSessionNoLocalMedia(); + return null; } + } + + /// + /// Generates an SDP answer in response to an offer. The remote description MUST be set + /// prior to calling this method. + /// + /// Optional. If set this address will be used as + /// the SDP Connection address. If not specified the Operating System routing table + /// will be used to lookup the address used to connect to the SDP connection address + /// from the remote offer. Any and IPv6Any are special cases. If they are set the respective + /// Internet facing IPv4 or IPv6 address will be used. + /// A task that when complete contains the SDP answer. + /// As specified in https://tools.ietf.org/html/rfc3264#section-6.1. + /// "If the answerer has no media formats in common for a particular + /// offered stream, the answerer MUST reject that media stream by setting + /// the port to zero." + /// + public virtual SDP? CreateAnswer(IPAddress? connectionAddress) + { + if (RemoteDescription is null) + { + throw new SipSorceryException("The remote description is not set, cannot create SDP answer."); + } + + var offer = RemoteDescription; + + var currentAudioStreamCount = 0; + var currentVideoStreamCount = 0; + var currentTextStreamCount = 0; + + var mediaStreams = new List(); - private void RaisedOnRtpPacketReceived(int index, IPEndPoint ipEndPoint, SDPMediaTypesEnum media, RTPPacket rtpPacket) + // The order of the announcements in the answer must match the order in the offer. + foreach (var announcement in offer.Media) { - if (index == 0) + MediaStream? currentMediaStream = null; + switch (announcement.Media) { - OnRtpPacketReceived?.Invoke(ipEndPoint, media, rtpPacket); + case SDPMediaTypesEnum.audio: + currentMediaStream = GetOrCreateAudioStream(currentAudioStreamCount++); + break; + case SDPMediaTypesEnum.video: + currentMediaStream = GetOrCreateVideoStream(currentVideoStreamCount++); + break; + case SDPMediaTypesEnum.text: + currentMediaStream = GetOrCreateTextStream(currentTextStreamCount++); + break; } - OnRtpPacketReceivedByIndex?.Invoke(index, ipEndPoint, media, rtpPacket); - } - private void RaisedOnRtpHeaderReceived(int index, IPEndPoint ipEndPoint, SDPMediaTypesEnum media, String uri, Object value) - { - if (index == 0) + if (currentMediaStream is { LocalTrack: { } }) { - OnRtpHeaderReceived?.Invoke(ipEndPoint, media, uri, value); + mediaStreams.Add(currentMediaStream); } - OnRtpHeaderReceivedByIndex?.Invoke(index, ipEndPoint, media, uri, value); } - private void RaisedOnOnReceiveReport(int index, IPEndPoint ipEndPoint, SDPMediaTypesEnum media, RTCPCompoundPacket report) + if (connectionAddress is null) { - if (index == 0) + // No specific connection address supplied. Lookup the local address to connect to the offer address. + var offerConnectionAddress = (offer.Connection?.ConnectionAddress is { }) ? IPAddress.Parse(offer.Connection.ConnectionAddress) : null; + + if (offerConnectionAddress is null || offerConnectionAddress == IPAddress.Any || offerConnectionAddress == IPAddress.IPv6Any) { - OnReceiveReport?.Invoke(ipEndPoint, media, report); + connectionAddress = NetServices.InternetDefaultAddress; } - OnReceiveReportByIndex?.Invoke(index, ipEndPoint, media, report); - } - - private void RaisedOnAudioFormatsNegotiated(int index, List audioFormats) - { - if (index == 0) + else { - OnAudioFormatsNegotiated?.Invoke(audioFormats); + connectionAddress = NetServices.GetLocalAddressForRemote(offerConnectionAddress); } - OnAudioFormatsNegotiatedByIndex?.Invoke(index, audioFormats); } - private void RaisedOnVideoFormatsNegotiated(int index, List videoFormats) + var sdpConnectionAddress = GetSdpConnectionAddress( + connectionAddress, + offer.Connection?.ConnectionAddress is null ? null : IPAddress.Parse(offer.Connection.ConnectionAddress)); + + return GetSessionDescription(mediaStreams, sdpConnectionAddress); + } + + /// + /// Attempts to get the IP address to use in the SDP offer or answer. + /// + /// An optional connection address supplied by the calling application to use as a fallback. + /// If the address was triggered by an SDP offer this is the connection address of the remote peer. + /// The IP address to use in the SDP. + private IPAddress? GetSdpConnectionAddress(IPAddress? connectionAddress, IPAddress? offerConnectionAddress) + { + IPAddress? sdpConnectionAddress = null; + + // If a relay endpoint has been set on any of this session's media streams it takes precedence and + // will be used in the SDP offers and answers. + if (GetFirstRelayEndPointFromMediaStreams() is { RemotePeerRelayEndPoint: not null } relayEndPoint) { - if (index == 0) - { - OnVideoFormatsNegotiated?.Invoke(videoFormats); - } - OnVideoFormatsNegotiatedByIndex?.Invoke(index, videoFormats); + sdpConnectionAddress = relayEndPoint.RemotePeerRelayEndPoint.Address; } - private void RaisedOnTextFormatsNegotiated(int index, List textFormats) + // IF a STUN server has been used to get the RTP channel's server reflexive address use that. + if (sdpConnectionAddress is null) { - if (index == 0) + if (GetFirstRTPSrflxEndPointFromMediaStreams() is { } rtpSrflxEndPoint) { - OnTextFormatsNegotiated?.Invoke(textFormats); + sdpConnectionAddress = rtpSrflxEndPoint.Address; } - OnTextFormatsNegotiatedByIndex?.Invoke(index, textFormats); } - private void RaisedOnOnVideoFrameReceived(int index, IPEndPoint ipEndPoint, uint timestamp, byte[] frame, VideoFormat videoFormat) + if (sdpConnectionAddress is null) { - if (index == 0) + // No specific connection address supplied. Lookup the local address to connect to the offer address. + if (offerConnectionAddress is { } && offerConnectionAddress != IPAddress.Any && offerConnectionAddress != IPAddress.IPv6Any) { - OnVideoFrameReceived?.Invoke(ipEndPoint, timestamp, frame, videoFormat); + sdpConnectionAddress = NetServices.GetLocalAddressForRemote(offerConnectionAddress); } - OnVideoFrameReceivedByIndex?.Invoke(index, ipEndPoint, timestamp, frame, videoFormat); } - private void RaiseOnAudioFrameReceived(EncodedAudioFrame encodedAudioFrame) + return sdpConnectionAddress ?? connectionAddress; + } + + protected virtual AudioStream? GetOrCreateAudioStream(int index) + { + if (index < AudioStreamList.Count) { - OnAudioFrameReceived?.Invoke(encodedAudioFrame); + // We ask too fast a new AudioSteram ... + return AudioStreamList[index]; } - - /// - /// Generates the SDP for an offer that can be made to a remote user agent. - /// - /// Optional. If specified this IP address - /// will be used as the address advertised in the SDP offer. If not provided - /// the kernel routing table will be used to determine the local IP address used - /// for Internet access. Any and IPv6Any are special cases. If they are set the respective - /// Internet facing IPv4 or IPv6 address will be used. - /// A task that when complete contains the SDP offer. - public virtual SDP CreateOffer(IPAddress connectionAddress = null) + else if (index == AudioStreamList.Count) { - if ( - (AudioStream == null || AudioStream.LocalTrack == null) && - (VideoStream == null || VideoStream.LocalTrack == null) && - (TextStream == null || TextStream.LocalTrack == null) - ) - { - logger.LogWarning("No local media tracks available for create offer."); - return null; - } - else - { - List mediaStreams = GetMediaStreams(); - - //Revert to DefaultStreamStatus - foreach (var mediaStream in mediaStreams) - { - if (mediaStream.LocalTrack != null && mediaStream.LocalTrack.StreamStatus == MediaStreamStatusEnum.Inactive) - { - mediaStream.LocalTrack.StreamStatus = mediaStream.LocalTrack.DefaultStreamStatus; - } - } - - RequireRenegotiation = true; - - var sdpConnectionAddress = GetSdpConnectionAddress(connectionAddress, null); - - return GetSessionDescription(mediaStreams, sdpConnectionAddress); - } + var audioStream = new AudioStream(rtpSessionConfig, index); + audioStream.MediaInsertionOrder = Interlocked.Increment(ref m_nextMediaInsertionOrder); + AudioStreamList.Add(audioStream); + return audioStream; } + return null; + } - /// - /// Generates an SDP answer in response to an offer. The remote description MUST be set - /// prior to calling this method. - /// - /// Optional. If set this address will be used as - /// the SDP Connection address. If not specified the Operating System routing table - /// will be used to lookup the address used to connect to the SDP connection address - /// from the remote offer. Any and IPv6Any are special cases. If they are set the respective - /// Internet facing IPv4 or IPv6 address will be used. - /// A task that when complete contains the SDP answer. - /// As specified in https://tools.ietf.org/html/rfc3264#section-6.1. - /// "If the answerer has no media formats in common for a particular - /// offered stream, the answerer MUST reject that media stream by setting - /// the port to zero." - /// - public virtual SDP CreateAnswer(IPAddress connectionAddress) + protected virtual VideoStream? GetOrCreateVideoStream(int index) + { + if (index < VideoStreamList.Count) { - if (RemoteDescription == null) - { - throw new ApplicationException("The remote description is not set, cannot create SDP answer."); - } - else - { - var offer = RemoteDescription; - - int currentAudioStreamCount = 0; - int currentVideoStreamCount = 0; - int currentTextStreamCount = 0; - MediaStream currentMediaStream; - - List mediaStreams = new List(); - - // The order of the announcements in the answer must match the order in the offer. - foreach (var announcement in offer.Media) - { - currentMediaStream = null; - // Adjust the local audio tracks to only include compatible capabilities. - if (announcement.Media == SDPMediaTypesEnum.audio) - { - currentMediaStream = GetOrCreateAudioStream(currentAudioStreamCount++); - } - else if (announcement.Media == SDPMediaTypesEnum.video) - { - currentMediaStream = GetOrCreateVideoStream(currentVideoStreamCount++); - } - else if (announcement.Media == SDPMediaTypesEnum.text) - { - currentMediaStream = GetOrCreateTextStream(currentTextStreamCount++); - } - - if (currentMediaStream != null && currentMediaStream.LocalTrack != null) - { - mediaStreams.Add(currentMediaStream); - } - } - - var sdpConnectionAddress = GetSdpConnectionAddress( - connectionAddress, - offer.Connection?.ConnectionAddress != null ? IPAddress.Parse(offer.Connection.ConnectionAddress) : null); - - return GetSessionDescription(mediaStreams, sdpConnectionAddress); - } + // We ask too fast a new AudioStream ... + return VideoStreamList[index]; } + else if (index == VideoStreamList.Count) + { + var videoStream = new VideoStream(rtpSessionConfig, index); + videoStream.MediaInsertionOrder = Interlocked.Increment(ref m_nextMediaInsertionOrder); + VideoStreamList.Add(videoStream); + return videoStream; + } + return null; + } - /// - /// Attempts to get the IP address to use in the SDP offer or answer. - /// - /// An optional connection address supplied by the calling application to use as a fallback. - /// If the address was triggered by an SDP offer this is the connection address of the remote peer. - /// The IP address to use in the SDP. - private IPAddress GetSdpConnectionAddress(IPAddress connectionAddress, IPAddress offerConnectionAddress) + protected virtual TextStream? GetOrCreateTextStream(int index) + { + if (index < TextStreamList.Count) { - IPAddress sdpConnectionAddress = null; + return TextStreamList[index]; + } + else if (index == TextStreamList.Count) + { + var textStream = new TextStream(rtpSessionConfig, index); + textStream.MediaInsertionOrder = Interlocked.Increment(ref m_nextMediaInsertionOrder); + TextStreamList.Add(textStream); + return textStream; + } + return null; + } - // If a relay endpoint has been set on any of this session's media streams it takes precedence and - // will be used in the SDP offers and answers. - var relayEndPoint = GetFirstRelayEndPointFromMediaStreams(); - if (relayEndPoint != null && relayEndPoint.RemotePeerRelayEndPoint != null) + /// + /// Sets the remote SDP description for this session. + /// + /// Whether the remote SDP is an offer or answer. + /// The SDP that will be set as the remote description. + /// If successful an OK enum result. If not an enum result indicating the failure cause. + public virtual SetDescriptionResultEnum SetRemoteDescription(SdpType sdpType, SDP sessionDescription) + { + ArgumentNullException.ThrowIfNull(sessionDescription); + + try + { + if (sessionDescription.Media?.Count == 0) { - sdpConnectionAddress = relayEndPoint.RemotePeerRelayEndPoint.Address; + return SetDescriptionResultEnum.NoRemoteMedia; } - - // IF a STUN server has been used to get the RTP channel's server reflexive address use that. - if (sdpConnectionAddress == null) + else if (sessionDescription.Media?.Count == 1) { - var rtpSrflxEndPoint = GetFirstRTPSrflxEndPointFromMediaStreams(); - if (rtpSrflxEndPoint != null) + var remoteMediaType = sessionDescription.Media[0].Media; + if (remoteMediaType == SDPMediaTypesEnum.audio && ((AudioStream is null) || (AudioStream.LocalTrack is null))) { - sdpConnectionAddress = rtpSrflxEndPoint.Address; + return SetDescriptionResultEnum.NoMatchingMediaType; } - } - - if (sdpConnectionAddress == null) - { - // No specific connection address supplied. Lookup the local address to connect to the offer address. - if (offerConnectionAddress != null && offerConnectionAddress != IPAddress.Any && offerConnectionAddress != IPAddress.IPv6Any) + else if (remoteMediaType == SDPMediaTypesEnum.video && ((VideoStream is null) || (VideoStream.LocalTrack is null))) + { + return SetDescriptionResultEnum.NoMatchingMediaType; + } + else if (remoteMediaType == SDPMediaTypesEnum.text && ((TextStream is null) || (TextStream.LocalTrack is null))) { - sdpConnectionAddress = NetServices.GetLocalAddressForRemote(offerConnectionAddress); + return SetDescriptionResultEnum.NoMatchingMediaType; } } - return sdpConnectionAddress ?? connectionAddress; - } - - protected virtual AudioStream GetOrCreateAudioStream(int index) - { - if (index < AudioStreamList.Count) - { - // We ask too fast a new AudioStram ... - return AudioStreamList[index]; - } - else if (index == AudioStreamList.Count) + // Pre-flight checks have passed. Move onto matching up the local and remote media streams. + IPAddress? connectionAddress = null; + if (sessionDescription.Connection is { } && !string.IsNullOrEmpty(sessionDescription.Connection.ConnectionAddress)) { - AudioStream audioStream = new AudioStream(rtpSessionConfig, index); - audioStream.MediaInsertionOrder = Interlocked.Increment(ref m_nextMediaInsertionOrder); - AudioStreamList.Add(audioStream); - return audioStream; + connectionAddress = IPAddress.Parse(sessionDescription.Connection.ConnectionAddress); } - return null; - } - protected virtual VideoStream GetOrCreateVideoStream(int index) - { - if (index < VideoStreamList.Count) - { - // We ask too fast a new AudioStram ... - return VideoStreamList[index]; - } - else if (index == VideoStreamList.Count) - { - VideoStream videoStream = new VideoStream(rtpSessionConfig, index); - videoStream.MediaInsertionOrder = Interlocked.Increment(ref m_nextMediaInsertionOrder); - VideoStreamList.Add(videoStream); - return videoStream; - } - return null; - } - protected virtual TextStream GetOrCreateTextStream(int index) - { - if (index < TextStreamList.Count) + //Remove Remote Tracks before add new one (this was added to implement renegotiation logic) + foreach (var audioStream in AudioStreamList) { - return TextStreamList[index]; + audioStream.RemoteTrack = null; } - else if (index == TextStreamList.Count) + + foreach (var videoStream in VideoStreamList) { - TextStream textStream = new TextStream(rtpSessionConfig, index); - textStream.MediaInsertionOrder = Interlocked.Increment(ref m_nextMediaInsertionOrder); - TextStreamList.Add(textStream); - return textStream; + videoStream.RemoteTrack = null; } - return null; - } - /// - /// Sets the remote SDP description for this session. - /// - /// Whether the remote SDP is an offer or answer. - /// The SDP that will be set as the remote description. - /// If successful an OK enum result. If not an enum result indicating the failure cause. - public virtual SetDescriptionResultEnum SetRemoteDescription(SdpType sdpType, SDP sessionDescription) - { - if (sessionDescription == null) + foreach (var textStream in TextStreamList) { - throw new ArgumentNullException("sessionDescription", "The session description cannot be null for SetRemoteDescription."); + textStream.RemoteTrack = null; } - try + var currentAudioStreamCount = 0; + var currentVideoStreamCount = 0; + var currentTextStreamCount = 0; + MediaStream? currentMediaStream; + + Debug.Assert(sessionDescription.Media is { }); + + for (var i = 0; i < sessionDescription.Media.Count; i++) { - if (sessionDescription.Media?.Count == 0) + var announcement = sessionDescription.Media[i]; + if (announcement.Media is not SDPMediaTypesEnum.audio and not SDPMediaTypesEnum.video and not SDPMediaTypesEnum.text) { - return SetDescriptionResultEnum.NoRemoteMedia; + continue; } - else if (sessionDescription.Media?.Count == 1) + + if (announcement.Media == SDPMediaTypesEnum.audio) { - var remoteMediaType = sessionDescription.Media.First().Media; - if (remoteMediaType == SDPMediaTypesEnum.audio && ((AudioStream == null) || (AudioStream.LocalTrack == null))) - { - return SetDescriptionResultEnum.NoMatchingMediaType; - } - else if (remoteMediaType == SDPMediaTypesEnum.video && ((VideoStream == null) || (VideoStream.LocalTrack == null))) - { - return SetDescriptionResultEnum.NoMatchingMediaType; - } - else if (remoteMediaType == SDPMediaTypesEnum.text && ((TextStream == null) || (TextStream.LocalTrack == null))) + currentMediaStream = GetOrCreateAudioStream(currentAudioStreamCount++); + if (currentMediaStream is null) { - return SetDescriptionResultEnum.NoMatchingMediaType; + return SetDescriptionResultEnum.Error; } } - - // Pre-flight checks have passed. Move onto matching up the local and remote media streams. - IPAddress connectionAddress = null; - if (sessionDescription.Connection != null && !string.IsNullOrEmpty(sessionDescription.Connection.ConnectionAddress)) - { - connectionAddress = IPAddress.Parse(sessionDescription.Connection.ConnectionAddress); - } - - - //Remove Remote Tracks before add new one (this was added to implement renegotiation logic) - foreach (var audioStream in AudioStreamList) + else if (announcement.Media == SDPMediaTypesEnum.text) { - audioStream.RemoteTrack = null; + currentMediaStream = GetOrCreateTextStream(currentTextStreamCount++); + if (currentMediaStream is null) + { + return SetDescriptionResultEnum.Error; + } } - - foreach (var videoStream in VideoStreamList) + else { - videoStream.RemoteTrack = null; + currentMediaStream = GetOrCreateVideoStream(currentVideoStreamCount++); + if (currentMediaStream is null) + { + return SetDescriptionResultEnum.Error; + } } - foreach (var textStream in TextStreamList) - { - textStream.RemoteTrack = null; - } + var mediaStreamStatus = announcement.MediaStreamStatus ?? MediaStreamStatusEnum.SendRecv; + var remoteTrack = new MediaStreamTrack(announcement.Media, true, announcement.MediaFormats.Values.ToList(), mediaStreamStatus, announcement.SsrcAttributes, announcement.HeaderExtensions); - int currentAudioStreamCount = 0; - int currentVideoStreamCount = 0; - int currentTextStreamCount = 0; - MediaStream currentMediaStream; + currentMediaStream.RemoteTrack = remoteTrack; - foreach (var announcement in sessionDescription.Media.Where(x => x.Media == SDPMediaTypesEnum.audio || x.Media == SDPMediaTypesEnum.video || x.Media == SDPMediaTypesEnum.text)) + if (rtpSessionConfig.UseSdpCryptoNegotiation) { - if (announcement.Media == SDPMediaTypesEnum.audio) - { - currentMediaStream = GetOrCreateAudioStream(currentAudioStreamCount++); - if (currentMediaStream == null) - { - return SetDescriptionResultEnum.Error; - } - } - else if (announcement.Media == SDPMediaTypesEnum.text) - { - currentMediaStream = GetOrCreateTextStream(currentTextStreamCount++); - if (currentMediaStream == null) - { - return SetDescriptionResultEnum.Error; - } - } - else + if (announcement.Transport != RTP_SECUREMEDIA_PROFILE) { - currentMediaStream = GetOrCreateVideoStream(currentVideoStreamCount++); - if (currentMediaStream == null) - { - return SetDescriptionResultEnum.Error; - } + logger.LogRtpSecureMediaInvalidTransport(announcement.Transport); + return SetDescriptionResultEnum.CryptoNegotiationFailed; } - MediaStreamStatusEnum mediaStreamStatus = announcement.MediaStreamStatus.HasValue ? announcement.MediaStreamStatus.Value : MediaStreamStatusEnum.SendRecv; - var remoteTrack = new MediaStreamTrack(announcement.Media, true, announcement.MediaFormats.Values.ToList(), mediaStreamStatus, announcement.SsrcAttributes, announcement.HeaderExtensions); - - currentMediaStream.RemoteTrack = remoteTrack; - - if (rtpSessionConfig.UseSdpCryptoNegotiation) + if (announcement.SecurityDescriptions.Exists(s => SrtpCryptoSuites.Contains(s.CryptoSuite))) { - if (announcement.Transport != RTP_SECUREMEDIA_PROFILE) - { - logger.LogError("Error negotiating secure media. Invalid Transport {Transport}.", announcement.Transport); - return SetDescriptionResultEnum.CryptoNegotiationFailed; - } - - if (announcement.SecurityDescriptions.Count(s => SrtpCryptoSuites.Contains(s.CryptoSuite)) > 0) + // Setup the appropriate srtp handler + var mediaType = announcement.Media; + var srtpHandler = currentMediaStream.GetOrCreateSrtpHandler(); + if (!(srtpHandler.IsNegotiationComplete && srtpHandler.RemoteSecurityDescriptionUnchanged(announcement.SecurityDescriptions))) { - // Setup the appropriate srtp handler - var mediaType = announcement.Media; - var srtpHandler = currentMediaStream.GetOrCreateSrtpHandler(); - if (!(srtpHandler.IsNegotiationComplete && srtpHandler.RemoteSecurityDescriptionUnchanged(announcement.SecurityDescriptions))) + if (!srtpHandler.SetupRemote(announcement.SecurityDescriptions, sdpType)) { - if (!srtpHandler.SetupRemote(announcement.SecurityDescriptions, sdpType)) - { - logger.LogError("Error negotiating secure media for type {MediaType}. Incompatible crypto parameter.", mediaType); - return SetDescriptionResultEnum.CryptoNegotiationFailed; - } + logger.LogRtpSecureMediaIncompatibleCrypto(mediaType); + return SetDescriptionResultEnum.CryptoNegotiationFailed; + } - if (srtpHandler.IsNegotiationComplete) - { - currentMediaStream.SetSecurityContext(srtpHandler.ProtectRTP, srtpHandler.UnprotectRTP, srtpHandler.ProtectRTCP, srtpHandler.UnprotectRTCP); - } + if (srtpHandler.IsNegotiationComplete) + { + currentMediaStream.SetSecurityContext(srtpHandler.ProtectRTP, srtpHandler.UnprotectRTP, srtpHandler.ProtectRTCP, srtpHandler.UnprotectRTCP); } } - // If we had no crypto but we were definetely expecting something since we had a port value - else if (announcement.Port != 0) - { - logger.LogError("Error negotiating secure media. No compatible crypto suite."); - return SetDescriptionResultEnum.CryptoNegotiationFailed; - } } - - List capabilities = null; - if (currentMediaStream.LocalTrack == null) + // If we had no crypto but we were definetely expecting something since we had a port value + else if (announcement.Port != 0) { - capabilities = remoteTrack.Capabilities; - var inactiveLocalTrack = new MediaStreamTrack(currentMediaStream.MediaType, false, remoteTrack.Capabilities, MediaStreamStatusEnum.Inactive); - currentMediaStream.LocalTrack = inactiveLocalTrack; + logger.LogRtpSecureMediaNoCompatibleCrypto(); + return SetDescriptionResultEnum.CryptoNegotiationFailed; } - else - { - capabilities = SDPAudioVideoMediaFormat.GetCompatibleFormats(currentMediaStream.RemoteTrack?.Capabilities, currentMediaStream.LocalTrack?.Capabilities); + } - // The offerer gets to set the codec priority. - // When receiving an offer: the REMOTE party is the offerer, so their Capabilities are the priority key. - // When receiving an answer: LOCAL is the offerer (we sent the offer), so our Capabilities are the priority key. - SDPAudioVideoMediaFormat.SortMediaCapability(capabilities, - sdpType == SdpType.offer ? currentMediaStream.RemoteTrack?.Capabilities : currentMediaStream.LocalTrack?.Capabilities); + List? capabilities = null; + if (currentMediaStream.LocalTrack is null) + { + capabilities = remoteTrack.Capabilities; + var inactiveLocalTrack = new MediaStreamTrack(currentMediaStream.MediaType, false, remoteTrack.Capabilities, MediaStreamStatusEnum.Inactive); + currentMediaStream.LocalTrack = inactiveLocalTrack; + } + else + { + capabilities = SDPAudioVideoMediaFormat.GetCompatibleFormats(currentMediaStream.RemoteTrack?.Capabilities, currentMediaStream.LocalTrack?.Capabilities); - currentMediaStream.LocalTrack.Capabilities = capabilities; - currentMediaStream.RemoteTrack.Capabilities = capabilities; + // The offerer gets to set the codec priority. + // When receiving an offer: the REMOTE party is the offerer, so their Capabilities are the priority key. + // When receiving an answer: LOCAL is the offerer (we sent the offer), so our Capabilities are the priority key. + SDPAudioVideoMediaFormat.SortMediaCapability(capabilities, + sdpType == SdpType.offer ? currentMediaStream.RemoteTrack?.Capabilities : currentMediaStream.LocalTrack?.Capabilities); - if (currentMediaStream.MediaType == SDPMediaTypesEnum.audio) - { - // Adjust the local track's RTP event capability if the remote party has specified a different payload ID. - var currentLocalTrackCapabilities = currentMediaStream.LocalTrack.Capabilities; - SDPAudioVideoMediaFormat? localRTPEventCapabilities = null; - if (currentLocalTrackCapabilities.Any(x => string.Equals(x.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase))) - { - localRTPEventCapabilities = currentLocalTrackCapabilities.First(x => string.Equals(x.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)); - } - else - { - localRTPEventCapabilities = MediaStream.DefaultRTPEventFormat; - } + Debug.Assert(currentMediaStream.LocalTrack is { }); + Debug.Assert(currentMediaStream.RemoteTrack is { }); + currentMediaStream.LocalTrack.Capabilities = capabilities; + currentMediaStream.RemoteTrack.Capabilities = capabilities; - currentMediaStream.LocalTrack.Capabilities = capabilities.Where(x => !string.Equals(x.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)).ToList(); - if (localRTPEventCapabilities != null) + if (currentMediaStream.MediaType == SDPMediaTypesEnum.audio) + { + // Adjust the local track's RTP event capability if the remote party has specified a different payload ID. + // Remove all telephone event capabilities. + var telephoneCapabilityFound = false; + for (var j = 0; j < currentMediaStream.LocalTrack.Capabilities.Count;) + { + var cap = currentMediaStream.LocalTrack.Capabilities[j]; + if (string.Equals(cap.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)) { - currentMediaStream.LocalTrack.Capabilities.Add(localRTPEventCapabilities.Value); - } + if (telephoneCapabilityFound) + { + currentMediaStream.LocalTrack.Capabilities.RemoveAt(j); + continue; + } - // Check whether RTP events can be supported and adjust our parameters to match the remote party if we can. - SDPAudioVideoMediaFormat commonEventFormat = SDPAudioVideoMediaFormat.GetCommonRtpEventFormat(announcement.MediaFormats.Values.ToList(), currentMediaStream.LocalTrack.Capabilities); - if (!commonEventFormat.IsEmpty()) - { - currentMediaStream.NegotiatedRtpEventPayloadID = commonEventFormat.ID; - currentMediaStream.LocalTrack.Capabilities.RemoveAll(x => string.Equals(x.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)); - currentMediaStream.LocalTrack.Capabilities.Add(commonEventFormat); + telephoneCapabilityFound = true; } - } - IPEndPoint remoteRtpEP = GetAnnouncementRTPDestination(announcement, connectionAddress); - SetLocalTrackStreamStatus(currentMediaStream.LocalTrack, remoteTrack.StreamStatus, remoteRtpEP); - - // RFC 3264 Section 8.2: "Removal of a media stream implies that media is - // no longer sent for that stream [...] In the case of RTP, RTCP transmission - // also ceases, as does processing of any received RTCP packets." - // When the remote party rejects a stream (e.g. m=video 0) the local track - // becomes Inactive. Close the RTCP session so its inactivity timer does not - // fire a timeout that tears down the entire call. Closing also sends an RTCP - // BYE per RFC 3550 Section 6.3.7 to allow the remote side to clean up state. - if (currentMediaStream.LocalTrack.StreamStatus == MediaStreamStatusEnum.Inactive - && currentMediaStream.RtcpSession != null - && !currentMediaStream.RtcpSession.IsClosed) - { - logger.LogDebug("Closing RTCP session for {MediaType} after remote party set stream to inactive.", currentMediaStream.MediaType); - currentMediaStream.RtcpSession.Close(null); + j++; } - IPEndPoint remoteRtcpEP = null; - if (remoteTrack.StreamStatus != MediaStreamStatusEnum.Inactive && currentMediaStream.LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) + // Check whether RTP events can be supported and adjust our parameters to match the remote party if we can. + var commonEventFormat = announcement.MediaFormats.Count == 0 + ? SDPAudioVideoMediaFormat.Empty + : SDPAudioVideoMediaFormat.GetCommonRtpEventFormat(announcement.MediaFormats.Values, currentMediaStream.LocalTrack.Capabilities); + if (!commonEventFormat.IsEmpty()) { - remoteRtcpEP = rtpSessionConfig.IsRtcpMultiplexed ? remoteRtpEP : new IPEndPoint(remoteRtpEP.Address, remoteRtpEP.Port + 1); + currentMediaStream.NegotiatedRtpEventPayloadID = commonEventFormat.ID; + currentMediaStream.LocalTrack.Capabilities.RemoveAll(x => string.Equals(x.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)); + currentMediaStream.LocalTrack.Capabilities.Add(commonEventFormat); } - - // When ICE manages the transport (WebRTC/multiplexed), the ICE layer - // sets DestinationEndPoint via SetGlobalDestination when the connection - // is established. Don't overwrite it from SDP during renegotiation. - // For non-ICE (SIP), the SDP address IS the destination. - if (!rtpSessionConfig.IsMediaMultiplexed || currentMediaStream.DestinationEndPoint == null) + else if (!currentMediaStream.LocalTrack.NoDtmfSupport && + !currentMediaStream.LocalTrack.Capabilities.Exists(x => string.Equals(x.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase))) { - currentMediaStream.DestinationEndPoint = (remoteRtpEP != null && remoteRtpEP.Port != SDP.IGNORE_RTP_PORT_NUMBER) ? remoteRtpEP : currentMediaStream.DestinationEndPoint; - currentMediaStream.ControlDestinationEndPoint = (remoteRtcpEP != null && remoteRtcpEP.Port != SDP.IGNORE_RTP_PORT_NUMBER) ? remoteRtcpEP : currentMediaStream.ControlDestinationEndPoint; + currentMediaStream.NegotiatedRtpEventPayloadID = MediaStream.DefaultRTPEventFormat.ID; + currentMediaStream.LocalTrack.Capabilities.Add(MediaStream.DefaultRTPEventFormat); } + } - logger.LogDebug("Setting remote {SdpMediaType} track with sdp destination {DestinationEndPoint} and control destination {ControlDestinationEndPoint}.", currentMediaStream.MediaType, currentMediaStream.DestinationEndPoint, currentMediaStream.ControlDestinationEndPoint); + var remoteRtpEP = GetAnnouncementRTPDestination(announcement, connectionAddress); + SetLocalTrackStreamStatus(currentMediaStream.LocalTrack, remoteTrack.StreamStatus, remoteRtpEP); + IPEndPoint? remoteRtcpEP = null; + if (remoteTrack.StreamStatus != MediaStreamStatusEnum.Inactive && currentMediaStream.LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) + { + Debug.Assert(remoteRtpEP is { }); + remoteRtcpEP = (rtpSessionConfig.IsRtcpMultiplexed) ? remoteRtpEP : new IPEndPoint(remoteRtpEP.Address, remoteRtpEP.Port + 1); } - if (currentMediaStream.MediaType == SDPMediaTypesEnum.audio) + // When ICE manages the transport (WebRTC/multiplexed), the ICE layer + // sets DestinationEndPoint via SetGlobalDestination when the connection + // is established. Don't overwrite it from SDP during renegotiation. + // For non-ICE (SIP), the SDP address IS the destination. + if (!rtpSessionConfig.IsMediaMultiplexed || currentMediaStream.DestinationEndPoint == null) { - if (capabilities?.Where(x => !string.Equals(x.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)).Count() == 0) + if (remoteRtpEP is { Port: not SDP.IGNORE_RTP_PORT_NUMBER } rtpEndPoint) { - return SetDescriptionResultEnum.AudioIncompatible; + currentMediaStream.DestinationEndPoint = rtpEndPoint; } - } - else if (currentMediaStream.MediaType == SDPMediaTypesEnum.text) - { - if (capabilities?.Count == 0 || (currentMediaStream.LocalTrack != null && currentMediaStream.LocalTrack.Capabilities?.Count == 0)) + + if (remoteRtcpEP is { Port: not SDP.IGNORE_RTP_PORT_NUMBER } rtcpEndPoint) { - return SetDescriptionResultEnum.TextIncompatible; + currentMediaStream.ControlDestinationEndPoint = rtcpEndPoint; } } - else if (currentMediaStream.MediaType == SDPMediaTypesEnum.video) + + // When ICE manages the transport (WebRTC/multiplexed), the ICE layer + // sets DestinationEndPoint via SetGlobalDestination when the connection + // is established. Don't overwrite it from SDP during renegotiation. + // For non-ICE (SIP), the SDP address IS the destination. + if (!rtpSessionConfig.IsMediaMultiplexed || currentMediaStream.DestinationEndPoint == null) { - if (capabilities?.Count == 0 || (currentMediaStream.LocalTrack != null && currentMediaStream.LocalTrack.Capabilities?.Count == 0)) - { - return SetDescriptionResultEnum.VideoIncompatible; - } + currentMediaStream.DestinationEndPoint = (remoteRtpEP != null && remoteRtpEP.Port != SDP.IGNORE_RTP_PORT_NUMBER) ? remoteRtpEP : currentMediaStream.DestinationEndPoint; + currentMediaStream.ControlDestinationEndPoint = (remoteRtcpEP != null && remoteRtcpEP.Port != SDP.IGNORE_RTP_PORT_NUMBER) ? remoteRtcpEP : currentMediaStream.ControlDestinationEndPoint; } + + logger.LogRtpSessionSetRemoteTrackSsrc(currentMediaStream.MediaType, 0, currentMediaStream.RemoteTrack?.Ssrc ?? 0); } - //Close old RTCPSessions opened - foreach (var audioStream in AudioStreamList) + if (currentMediaStream.MediaType == SDPMediaTypesEnum.audio) { - if (audioStream.RtcpSession != null && audioStream.RemoteTrack == null && audioStream.LocalTrack == null) + if (capabilities?.Exists(static x => !string.Equals(x.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)) == false) { - audioStream.RtcpSession.Close(null); + return SetDescriptionResultEnum.AudioIncompatible; } } - - //Close old RTCPSessions opened - foreach (var videoStream in VideoStreamList) + else if (currentMediaStream.MediaType == SDPMediaTypesEnum.text) { - if (videoStream.RtcpSession != null && videoStream.RemoteTrack == null && videoStream.LocalTrack == null) + if (capabilities is { Count: 0 } || currentMediaStream.LocalTrack is { Capabilities: { Count: 0 } }) { - videoStream.RtcpSession.Close(null); + return SetDescriptionResultEnum.TextIncompatible; } } - - //Close old RTCPSessions opened - foreach (var textStream in TextStreamList) + else if (currentMediaStream.MediaType == SDPMediaTypesEnum.video) { - if (textStream.RtcpSession != null && textStream.RemoteTrack == null && textStream.LocalTrack == null) + if (capabilities is { Count: 0 } || currentMediaStream.LocalTrack is { Capabilities: { Count: 0 } }) { - textStream.RtcpSession.Close(null); + return SetDescriptionResultEnum.VideoIncompatible; } } + } - foreach (var audioStream in AudioStreamList) + //Close old RTCPSessions opened + foreach (var audioStream in AudioStreamList) + { + if (audioStream.RtcpSession is { } && !audioStream.RtcpSession.IsClosed && + ((audioStream.RemoteTrack is null && audioStream.LocalTrack is null) || + audioStream.LocalTrack?.StreamStatus == MediaStreamStatusEnum.Inactive)) { - audioStream.CheckAudioFormatsNegotiation(); + audioStream.RtcpSession.Close(null); } + } - foreach (var videoStream in VideoStreamList) + //Close old RTCPSessions opened + foreach (var videoStream in VideoStreamList) + { + if (videoStream.RtcpSession is { } && !videoStream.RtcpSession.IsClosed && + ((videoStream.RemoteTrack is null && videoStream.LocalTrack is null) || + videoStream.LocalTrack?.StreamStatus == MediaStreamStatusEnum.Inactive)) { - videoStream.CheckVideoFormatsNegotiation(); + videoStream.RtcpSession.Close(null); } + } - foreach (var textStream in TextStreamList) + //Close old RTCPSessions opened + foreach (var textStream in TextStreamList) + { + if (textStream.RtcpSession is { } && !textStream.RtcpSession.IsClosed && + ((textStream.RemoteTrack is null && textStream.LocalTrack is null) || + textStream.LocalTrack?.StreamStatus == MediaStreamStatusEnum.Inactive)) { - textStream.CheckTextFormatsNegotiation(); + textStream.RtcpSession.Close(null); } + } - // If we get to here then the remote description was compatible with the local media tracks. - // Set the remote description and end points. - RequireRenegotiation = false; - RemoteDescription = sessionDescription; - - OnRemoteDescriptionChanged?.Invoke(RemoteDescription); + foreach (var audioStream in AudioStreamList) + { + audioStream.CheckAudioFormatsNegotiation(); + } - return SetDescriptionResultEnum.OK; + foreach (var videoStream in VideoStreamList) + { + videoStream.CheckVideoFormatsNegotiation(); } - catch (Exception excp) + + foreach (var textStream in TextStreamList) { - logger.LogError(excp, "Exception in RTPSession SetRemoteDescription. {ErrorMessage}.", excp.Message); - return SetDescriptionResultEnum.Error; + textStream.CheckTextFormatsNegotiation(); } + + // If we get to here then the remote description was compatible with the local media tracks. + // Set the remote description and end points. + RequireRenegotiation = false; + RemoteDescription = sessionDescription; + + OnRemoteDescriptionChanged?.Invoke(RemoteDescription); + + return SetDescriptionResultEnum.OK; + } + catch (Exception excp) + { + logger.LogRtpSessionException(excp.Message, excp); + return SetDescriptionResultEnum.Error; } + } - /// - /// Sets the stream status on the primary local audio or primary video media track. - /// - /// The type of the media track. Must be audio or video. - /// The stream status for the media track. - public void SetMediaStreamStatus(SDPMediaTypesEnum kind, MediaStreamStatusEnum status) + /// + /// Sets the stream status on the primary local audio or primary video media track. + /// + /// The type of the media track. Must be audio or video. + /// The stream status for the media track. + public void SetMediaStreamStatus(SDPMediaTypesEnum kind, MediaStreamStatusEnum status) + { + if (kind == SDPMediaTypesEnum.audio && AudioStream?.LocalTrack is { }) { - if (kind == SDPMediaTypesEnum.audio && AudioStream.LocalTrack != null) - { - AudioStream.LocalTrack.StreamStatus = status; - m_sdpAnnouncementVersion++; - } - else if (kind == SDPMediaTypesEnum.video && VideoStream?.LocalTrack != null) + AudioStream.LocalTrack.StreamStatus = status; + m_sdpAnnouncementVersion++; + } + else if (kind == SDPMediaTypesEnum.video && VideoStream?.LocalTrack is { }) + { + VideoStream.LocalTrack.StreamStatus = status; + m_sdpAnnouncementVersion++; + } + else if (kind == SDPMediaTypesEnum.text && TextStream?.LocalTrack is { }) + { + TextStream.LocalTrack.StreamStatus = status; + m_sdpAnnouncementVersion++; + } + } + + /// + /// Gets the RTP end point for an SDP media announcement from the remote peer. + /// + /// The media announcement to get the connection address for. + /// The remote SDP session level connection address. Will be null if not available. + /// An IP end point for an SDP media announcement from the remote peer. + private IPEndPoint? GetAnnouncementRTPDestination(SDPMediaAnnouncement announcement, IPAddress? connectionAddress) + { + var kind = announcement.Media; + IPEndPoint? rtpEndPoint = null; + + var remoteAddr = (announcement.Connection?.ConnectionAddress is { } newConnectionAddress) ? IPAddress.Parse(newConnectionAddress) : connectionAddress; + + if (remoteAddr is { }) + { + if (announcement.Port is < IPEndPoint.MinPort or > IPEndPoint.MaxPort) { - VideoStream.LocalTrack.StreamStatus = status; - m_sdpAnnouncementVersion++; + logger.LogRtpInvalidPortNumber(kind, announcement.Port); + + // Set the remote port number to "9" which means ignore and wait for it be set some other way + // such as when a remote RTP packet or arrives or ICE negotiation completes. + rtpEndPoint = new IPEndPoint(remoteAddr, SDP.IGNORE_RTP_PORT_NUMBER); } - else if (kind == SDPMediaTypesEnum.text && TextStream?.LocalTrack != null) + else { - TextStream.LocalTrack.StreamStatus = status; - m_sdpAnnouncementVersion++; + rtpEndPoint = new IPEndPoint(remoteAddr, announcement.Port); } } - /// - /// Gets the RTP end point for an SDP media announcement from the remote peer. - /// - /// The media announcement to get the connection address for. - /// The remote SDP session level connection address. Will be null if not available. - /// An IP end point for an SDP media announcement from the remote peer. - private IPEndPoint GetAnnouncementRTPDestination(SDPMediaAnnouncement announcement, IPAddress connectionAddress) - { - SDPMediaTypesEnum kind = announcement.Media; - IPEndPoint rtpEndPoint = null; + return rtpEndPoint; + } - var remoteAddr = (announcement.Connection != null) ? IPAddress.Parse(announcement.Connection.ConnectionAddress) : connectionAddress; + /// + /// Used for child classes that require a single RTP channel for all RTP (audio and video) + /// and RTCP communications. + /// + protected void addSingleTrack(bool videoAsPrimary) + { + if (videoAsPrimary) + { + m_primaryStream = GetNextVideoStreamByLocalTrack(); + } + else + { + m_primaryStream = GetNextAudioStreamByLocalTrack(); + } - if (remoteAddr != null) - { - if (announcement.Port < IPEndPoint.MinPort || announcement.Port > IPEndPoint.MaxPort) - { - logger.LogWarning("Remote {SdpMediaType} announcement contained an invalid port number {Port}.", kind, announcement.Port); + InitMediaStream(m_primaryStream); + } - // Set the remote port number to "9" which means ignore and wait for it be set some other way - // such as when a remote RTP packet or arrives or ICE negotiation completes. - rtpEndPoint = new IPEndPoint(remoteAddr, SDP.IGNORE_RTP_PORT_NUMBER); - } - else - { - rtpEndPoint = new IPEndPoint(remoteAddr, announcement.Port); - } - } + private void InitMediaStream(MediaStream currentMediaStream) + { + var rtpChannel = CreateRtpChannel(); + currentMediaStream.AddRtpChannel(rtpChannel); + CreateRtcpSession(currentMediaStream); + } - return rtpEndPoint; + /// + /// Adds a media track to this session. A media track represents an audio or video or text + /// stream and can be a local (which means we're sending) or remote (which means + /// we're receiving). + /// + /// The media track to add to the session. + public virtual void addTrack(MediaStreamTrack track) + { + if (track is null) + { + return; + } + if (track.IsRemote) + { + AddRemoteTrack(track); } - - /// - /// Used for child classes that require a single RTP channel for all RTP (audio and video) - /// and RTCP communications. - /// - protected void addSingleTrack(Boolean videoAsPrimary) + else { - if (videoAsPrimary) - { - m_primaryStream = GetNextVideoStreamByLocalTrack(); - } - else - { - m_primaryStream = GetNextAudioStreamByLocalTrack(); - } - - InitMediaStream(m_primaryStream); + AddLocalTrack(track); } + } - private void InitMediaStream(MediaStream currentMediaStream) + /// + /// Removes a media track from this session. A media track represents an audio or video + /// stream and can be a local (which means we're sending) or remote (which means + /// we're receiving). + /// + /// The media track to add to the session. + public virtual bool removeTrack(MediaStreamTrack track) + { + if (track is null) { - var rtpChannel = CreateRtpChannel(); - currentMediaStream.AddRtpChannel(rtpChannel); - CreateRtcpSession(currentMediaStream); + return false; } - - /// - /// Adds a media track to this session. A media track represents an audio or video or text - /// stream and can be a local (which means we're sending) or remote (which means - /// we're receiving). - /// - /// The media track to add to the session. - public virtual void addTrack(MediaStreamTrack track) + if (track.IsRemote) { - if (track == null) - { - return; - } - if (track.IsRemote) - { - AddRemoteTrack(track); - } - else - { - AddLocalTrack(track); - } + return RemoveRemoteTrack(track); } - - /// - /// Removes a media track from this session. A media track represents an audio or video - /// stream and can be a local (which means we're sending) or remote (which means - /// we're receiving). - /// - /// The media track to add to the session. - public virtual bool removeTrack(MediaStreamTrack track) + else { - if (track == null) - { - return false; - } - if (track.IsRemote) - { - return RemoveRemoteTrack(track); - } - else - { - return RemoveLocalTrack(track); - } + return RemoveLocalTrack(track); } + } - /// - /// Removes a local media stream to this session. - /// - /// The local track to remove. - private bool RemoveLocalTrack(MediaStreamTrack track) - { - // TODO - CI - Do we need to do something else ? How to remove an Audio/Video Stream ? + /// + /// Removes a local media stream to this session. + /// + /// The local track to remove. + private bool RemoveLocalTrack(MediaStreamTrack track) + { + // TODO - CI - Do we need to do something else ? How to remove an Audio/Video Stream ? - if (track == null) - { - return false; - } + if (track is null) + { + return false; + } - if (track.Kind == SDPMediaTypesEnum.audio) - { + switch (track.Kind) + { + case SDPMediaTypesEnum.audio: foreach (var audioStream in AudioStreamList) { if (audioStream.LocalTrack == track) @@ -1525,49 +1556,57 @@ private bool RemoveLocalTrack(MediaStreamTrack track) return true; } } - } - else if (track.Kind == SDPMediaTypesEnum.text) - { + break; + case SDPMediaTypesEnum.text: foreach (var textStream in TextStreamList) { if (textStream.LocalTrack == track) { RequireRenegotiation = true; textStream.LocalTrack = null; + + CloseMediaStream("normal", textStream); + IsVideoStarted = false; + TextStreamList.Remove(textStream); return true; } } - } - else if (track.Kind == SDPMediaTypesEnum.video) - { + break; + case SDPMediaTypesEnum.video: foreach (var videoStream in VideoStreamList) { if (videoStream.LocalTrack == track) { RequireRenegotiation = true; videoStream.LocalTrack = null; + + CloseMediaStream("normal", videoStream); + IsVideoStarted = false; + VideoStreamList.Remove(videoStream); return true; } } - } - return false; + break; } + return false; + } - /// - /// Removes a remote media stream to this session. - /// - /// The remote track to remove. - private bool RemoveRemoteTrack(MediaStreamTrack track) + /// + /// Removes a remote media stream to this session. + /// + /// The remote track to remove. + private bool RemoveRemoteTrack(MediaStreamTrack track) + { + // TODO - CI - Do we need to do something else ? How to remove an Audio/Video Stream ? + if (track is null) { - // TODO - CI - Do we need to do something else ? How to remove an Audio/Video Stream ? - if (track == null) - { - return false; - } + return false; + } - if (track.Kind == SDPMediaTypesEnum.audio) - { - AudioStream audioStream = null; + switch (track.Kind) + { + case SDPMediaTypesEnum.audio: + AudioStream? audioStream = null; foreach (var checkAudioStream in AudioStreamList) { @@ -1580,19 +1619,17 @@ private bool RemoveRemoteTrack(MediaStreamTrack track) } } - if (audioStream != null) + if (audioStream is { }) { - //if ( (audioStream.LocalTrack == null) && (audioStream.RemoteTrack == null) ) + //if ( (audioStream.LocalTrack is null) && (audioStream.RemoteTrack is null) ) //{ // AudioStreamList.Remove(audioStream); //} return true; } - - } - else if (track.Kind == SDPMediaTypesEnum.video) - { - VideoStream videoStream = null; + break; + case SDPMediaTypesEnum.video: + VideoStream? videoStream = null; foreach (var checkVideoStream in VideoStreamList) { if (checkVideoStream.RemoteTrack == track) @@ -1604,18 +1641,17 @@ private bool RemoveRemoteTrack(MediaStreamTrack track) } } - if (videoStream != null) + if (videoStream is { }) { - //if ( (videoStream.LocalTrack == null) && (videoStream.RemoteTrack == null) ) + //if ( (videoStream.LocalTrack is null) && (videoStream.RemoteTrack is null) ) //{ // VideoStreamList.Remove(videoStream); //} return true; } - } - else if (track.Kind == SDPMediaTypesEnum.text) - { - TextStream textStream = null; + break; + case SDPMediaTypesEnum.text: + TextStream? textStream = null; foreach (var checkTextStream in TextStreamList) { if (checkTextStream.RemoteTrack == track) @@ -1627,682 +1663,809 @@ private bool RemoveRemoteTrack(MediaStreamTrack track) } } - if (textStream != null) + if (textStream is { }) { - //if ( (textStream.LocalTrack == null) && (textStream.RemoteTrack == null) ) + //if ( (textStream.LocalTrack is null) && (textStream.RemoteTrack is null) ) //{ // TextStreamList.Remove(textStream); //} return true; } - } - - return false; + break; } - /// - /// Adds a local media stream to this session. Local media tracks should be added by the - /// application to control what session description offers and answers can be made as - /// well as being used to match up with remote tracks. - /// - /// The local track to add. - private void AddLocalTrack(MediaStreamTrack track) + return false; + } + + /// + /// Adds a local media stream to this session. Local media tracks should be added by the + /// application to control what session description offers and answers can be made as + /// well as being used to match up with remote tracks. + /// + /// The local track to add. + private void AddLocalTrack(MediaStreamTrack track) + { + MediaStream currentMediaStream; + switch (track.Kind) { - MediaStream currentMediaStream; - if (track.Kind == SDPMediaTypesEnum.audio) - { + case SDPMediaTypesEnum.audio: currentMediaStream = GetNextAudioStreamByLocalTrack(); - } - else if (track.Kind == SDPMediaTypesEnum.video) - { + break; + case SDPMediaTypesEnum.video: currentMediaStream = GetNextVideoStreamByLocalTrack(); - } - else if (track.Kind == SDPMediaTypesEnum.text) - { + break; + case SDPMediaTypesEnum.text: currentMediaStream = GetNextTextStreamByLocalTrack(); - } - else - { + break; + default: return; - } + } - if (track.StreamStatus == MediaStreamStatusEnum.Inactive) - { - // Inactive tracks don't use/require any local resources. Instead they are place holders - // so that the session description offers/answers can be balanced with the remote party. - // For example if the remote party offers audio and video but we only support audio we - // can reject the call or we can accept the audio and answer with an inactive video - // announcement. - RequireRenegotiation = true; - currentMediaStream.LocalTrack = track; - } - else - { - RequireRenegotiation = true; + if (track.StreamStatus == MediaStreamStatusEnum.Inactive) + { + // Inactive tracks don't use/require any local resources. Instead they are place holders + // so that the session description offers/answers can be balanced with the remote party. + // For example if the remote party offers audio and video but we only support audio we + // can reject the call or we can accept the audio and answer with an inactive video + // announcement. + RequireRenegotiation = true; + currentMediaStream.LocalTrack = track; + } + else + { + RequireRenegotiation = true; - InitMediaStream(currentMediaStream); - currentMediaStream.LocalTrack = track; - } + InitMediaStream(currentMediaStream); + currentMediaStream.LocalTrack = track; } + } - /// - /// Adds a remote media stream to this session. Typically the only way remote tracks - /// should get added is from setting the remote session description. Adding a remote - /// track does not cause the creation of any local resources. - /// - /// The remote track to add. - private void AddRemoteTrack(MediaStreamTrack track) + /// + /// Adds a remote media stream to this session. Typically the only way remote tracks + /// should get added is from setting the remote session description. Adding a remote + /// track does not cause the creation of any local resources. + /// + /// The remote track to add. + private void AddRemoteTrack(MediaStreamTrack track) + { + MediaStream currentMediaStream; + switch (track.Kind) { - MediaStream currentMediaStream; - if (track.Kind == SDPMediaTypesEnum.audio) - { + case SDPMediaTypesEnum.audio: currentMediaStream = GetNextAudioStreamByRemoteTrack(); - } - else if (track.Kind == SDPMediaTypesEnum.video) - { + break; + case SDPMediaTypesEnum.video: currentMediaStream = GetNextVideoStreamByRemoteTrack(); - } - else if (track.Kind == SDPMediaTypesEnum.text) - { + break; + case SDPMediaTypesEnum.text: currentMediaStream = GetNextTextStreamByRemoteTrack(); - } - else - { + break; + default: return; - } + } - RequireRenegotiation = true; - currentMediaStream.RemoteTrack = track; + RequireRenegotiation = true; + currentMediaStream.RemoteTrack = track; - // Even if there's no local audio/video track an RTCP session can still be required - // in case the remote party send reports (presumably in case we decide we do want - // to send or receive audio on this session at some later stage). - CreateRtcpSession(currentMediaStream); + // Even if there's no local audio/video track an RTCP session can still be required + // in case the remote party send reports (presumably in case we decide we do want + // to send or receive audio on this session at some later stage). + CreateRtcpSession(currentMediaStream); + } + protected void SetGlobalDestination(IPEndPoint rtpEndPoint, IPEndPoint rtcpEndPoint) + { + foreach (var audioStream in AudioStreamList) + { + audioStream.SetDestination(rtpEndPoint, rtcpEndPoint); } - protected void SetGlobalDestination(IPEndPoint rtpEndPoint, IPEndPoint rtcpEndPoint) + foreach (var videoStream in VideoStreamList) { - foreach (var audioStream in AudioStreamList) - { - audioStream.SetDestination(rtpEndPoint, rtcpEndPoint); - } + videoStream.SetDestination(rtpEndPoint, rtcpEndPoint); + } - foreach (var videoStream in VideoStreamList) - { - videoStream.SetDestination(rtpEndPoint, rtcpEndPoint); - } + foreach (var textStream in TextStreamList) + { + textStream.SetDestination(rtpEndPoint, rtcpEndPoint); + } + } - foreach (var textStream in TextStreamList) - { - textStream.SetDestination(rtpEndPoint, rtcpEndPoint); - } + protected void SetGlobalSecurityContext(ProtectRtpPacket protectRtp, ProtectRtpPacket unprotectRtp, ProtectRtpPacket protectRtcp, ProtectRtpPacket unprotectRtcp) + { + foreach (var audioStream in AudioStreamList) + { + audioStream.SetSecurityContext(protectRtp, unprotectRtp, protectRtcp, unprotectRtcp); } - protected void SetGlobalSecurityContext(ProtectRtpPacket protectRtp, ProtectRtpPacket unprotectRtp, ProtectRtpPacket protectRtcp, ProtectRtpPacket unprotectRtcp) + foreach (var videoStream in VideoStreamList) { - foreach (var audioStream in AudioStreamList) - { - audioStream.SetSecurityContext(protectRtp, unprotectRtp, protectRtcp, unprotectRtcp); - } + videoStream.SetSecurityContext(protectRtp, unprotectRtp, protectRtcp, unprotectRtcp); + } - foreach (var videoStream in VideoStreamList) - { - videoStream.SetSecurityContext(protectRtp, unprotectRtp, protectRtcp, unprotectRtcp); - } + foreach (var textStream in TextStreamList) + { + textStream.SetSecurityContext(protectRtp, unprotectRtp, protectRtcp, unprotectRtcp); + } + } - foreach (var textStream in TextStreamList) + private void InitIPEndPointAndSecurityContext(MediaStream mediaStream) + { + // Get primary AudioStream + if ((m_primaryStream is { }) && (mediaStream is { })) + { + var secureContext = m_primaryStream.GetSecurityContext(); + if (secureContext is { }) { - textStream.SetSecurityContext(protectRtp, unprotectRtp, protectRtcp, unprotectRtcp); + mediaStream.SetSecurityContext(secureContext.ProtectRtpPacket, secureContext.UnprotectRtpPacket, secureContext.ProtectRtcpPacket, secureContext.UnprotectRtcpPacket); } + + mediaStream.SetDestination(m_primaryStream.DestinationEndPoint, m_primaryStream.ControlDestinationEndPoint); } + } - private void InitIPEndPointAndSecurityContext(MediaStream mediaStream) + protected virtual TextStream GetNextTextStreamByLocalTrack() + { + var index = TextStreamList.Count; + if (index > 0) { - // Get primary AudioStream - if ((m_primaryStream != null) && (mediaStream != null)) + foreach (var textStream in TextStreamList) { - var secureContext = m_primaryStream.GetSecurityContext(); - if (secureContext != null) + if (textStream.LocalTrack is null) { - mediaStream.SetSecurityContext(secureContext.ProtectRtpPacket, secureContext.UnprotectRtpPacket, secureContext.ProtectRtcpPacket, secureContext.UnprotectRtcpPacket); + return textStream; } - mediaStream.SetDestination(m_primaryStream.DestinationEndPoint, m_primaryStream.ControlDestinationEndPoint); } } - protected virtual TextStream GetNextTextStreamByLocalTrack() + var newTextStream = GetOrCreateTextStream(index); + Debug.Assert(newTextStream is { }); + newTextStream.AcceptRtpFromAny = AcceptRtpFromAny; + + // If it's not the first one we need to init it + if (index != 0) + { + InitIPEndPointAndSecurityContext(newTextStream); + } + + return newTextStream; + } + + private TextStream GetNextTextStreamByRemoteTrack() + { + var index = TextStreamList.Count; + if (index > 0) { - int index = TextStreamList.Count; - if (index > 0) + foreach (var textStream in TextStreamList) { - foreach (var textStream in TextStreamList) + if (textStream.RemoteTrack is null) { - if (textStream.LocalTrack == null) - { - return textStream; - } + return textStream; } } + } - var newTextStream = GetOrCreateTextStream(index); - newTextStream.AcceptRtpFromAny = AcceptRtpFromAny; - - if (index != 0) - { - InitIPEndPointAndSecurityContext(newTextStream); - } + // We need to create new TextStream + var newTextStream = GetOrCreateTextStream(index); + Debug.Assert(newTextStream is { }); + newTextStream.AcceptRtpFromAny = AcceptRtpFromAny; - return newTextStream; + // If it's not the first one we need to init it + if (index != 0) + { + InitIPEndPointAndSecurityContext(newTextStream); } - private TextStream GetNextTextStreamByRemoteTrack() + return newTextStream; + } + + protected virtual AudioStream GetNextAudioStreamByLocalTrack() + { + var index = AudioStreamList.Count; + if (index > 0) { - int index = TextStreamList.Count; - if (index > 0) + foreach (var audioStream in AudioStreamList) { - foreach (var textStream in TextStreamList) + if (audioStream.LocalTrack is null) { - if (textStream.RemoteTrack == null) - { - return textStream; - } + return audioStream; } } + } - // We need to create new TextStream - var newTextStream = GetOrCreateTextStream(index); - newTextStream.AcceptRtpFromAny = AcceptRtpFromAny; - - // If it's not the first one we need to init it - if (index != 0) - { - InitIPEndPointAndSecurityContext(newTextStream); - } + // We need to create new AudioStream + var newAudioStream = GetOrCreateAudioStream(index); + Debug.Assert(newAudioStream is { }); + newAudioStream.AcceptRtpFromAny = AcceptRtpFromAny; - return newTextStream; + // If it's not the first one we need to init it + if (index != 0) + { + InitIPEndPointAndSecurityContext(newAudioStream); } - protected virtual AudioStream GetNextAudioStreamByLocalTrack() + return newAudioStream; + } + + private AudioStream GetNextAudioStreamByRemoteTrack() + { + var index = AudioStreamList.Count; + if (index > 0) { - int index = AudioStreamList.Count; - if (index > 0) + foreach (var audioStream in AudioStreamList) { - foreach (var audioStream in AudioStreamList) + if (audioStream.RemoteTrack is null) { - if (audioStream.LocalTrack == null) - { - return audioStream; - } + return audioStream; } } + } - // We need to create new AudioStream - var newAudioStream = GetOrCreateAudioStream(index); - newAudioStream.AcceptRtpFromAny = AcceptRtpFromAny; - - // If it's not the first one we need to init it - if (index != 0) - { - InitIPEndPointAndSecurityContext(newAudioStream); - } + // We need to create new AudioStream + var newAudioStream = GetOrCreateAudioStream(index); + Debug.Assert(newAudioStream is { }); + newAudioStream.AcceptRtpFromAny = AcceptRtpFromAny; - return newAudioStream; + // If it's not the first one we need to init it + if (index != 0) + { + InitIPEndPointAndSecurityContext(newAudioStream); } - private AudioStream GetNextAudioStreamByRemoteTrack() + return newAudioStream; + } + + protected virtual VideoStream GetNextVideoStreamByLocalTrack() + { + var index = VideoStreamList.Count; + if (index > 0) { - int index = AudioStreamList.Count; - if (index > 0) + foreach (var videoStream in VideoStreamList) { - foreach (var audioStream in AudioStreamList) + if (videoStream.LocalTrack is null) { - if (audioStream.RemoteTrack == null) - { - return audioStream; - } + return videoStream; } } + } - // We need to create new AudioStream - var newAudioStream = GetOrCreateAudioStream(index); - newAudioStream.AcceptRtpFromAny = AcceptRtpFromAny; - - // If it's not the first one we need to init it - if (index != 0) - { - InitIPEndPointAndSecurityContext(newAudioStream); - } + // We need to create new VideoStream and Init it + var newVideoStream = GetOrCreateVideoStream(index); + Debug.Assert(newVideoStream is { }); + newVideoStream.AcceptRtpFromAny = AcceptRtpFromAny; - return newAudioStream; - } + InitIPEndPointAndSecurityContext(newVideoStream); + return newVideoStream; + } - protected virtual VideoStream GetNextVideoStreamByLocalTrack() + private VideoStream GetNextVideoStreamByRemoteTrack() + { + var index = VideoStreamList.Count; + if (index > 0) { - int index = VideoStreamList.Count; - if (index > 0) + foreach (var videoStream in VideoStreamList) { - foreach (var videoStream in VideoStreamList) + if (videoStream.RemoteTrack is null) { - if (videoStream.LocalTrack == null) - { - return videoStream; - } + return videoStream; } } + } - // We need to create new VideoStream and Init it - var newVideoStream = GetOrCreateVideoStream(index); - newVideoStream.AcceptRtpFromAny = AcceptRtpFromAny; + // We need to create new VideoStream and Init it + var newVideoStream = GetOrCreateVideoStream(index); + Debug.Assert(newVideoStream is { }); + newVideoStream.AcceptRtpFromAny = AcceptRtpFromAny; - InitIPEndPointAndSecurityContext(newVideoStream); - return newVideoStream; - } + InitIPEndPointAndSecurityContext(newVideoStream); + return newVideoStream; + } - private VideoStream GetNextVideoStreamByRemoteTrack() + /// + /// Adjust the stream status of the local media tracks based on the remote tracks. + /// + private void SetLocalTrackStreamStatus(MediaStreamTrack localTrack, MediaStreamStatusEnum remoteTrackStatus, IPEndPoint? remoteRTPEndPoint) + { + if (localTrack is { }) { - int index = VideoStreamList.Count; - if (index > 0) + if (localTrack.StreamStatus == MediaStreamStatusEnum.Inactive) { - foreach (var videoStream in VideoStreamList) + localTrack.StreamStatus = localTrack.DefaultStreamStatus; + } + + if (remoteTrackStatus == MediaStreamStatusEnum.Inactive) + { + // The remote party does not support this media type. Set the local stream status to inactive. + localTrack.StreamStatus = MediaStreamStatusEnum.Inactive; + } + else if (remoteRTPEndPoint is { }) + { + if (IPAddress.Any.Equals(remoteRTPEndPoint.Address) || IPAddress.IPv6Any.Equals(remoteRTPEndPoint.Address)) { - if (videoStream.RemoteTrack == null) + // A connection address of 0.0.0.0 or [::], which is unreachable, means the media is inactive, except + // if a special port number is used (defined as "9") which indicates that the media announcement is not + // responsible for setting the remote end point for the audio stream. Instead it's most likely being set + // using ICE. + if (remoteRTPEndPoint.Port != SDP.IGNORE_RTP_PORT_NUMBER) { - return videoStream; + localTrack.StreamStatus = MediaStreamStatusEnum.Inactive; } } + else if (remoteRTPEndPoint.Port == 0) + { + localTrack.StreamStatus = MediaStreamStatusEnum.Inactive; + } } + } + } - // We need to create new VideoStream and Init it - var newVideoStream = GetOrCreateVideoStream(index); - newVideoStream.AcceptRtpFromAny = AcceptRtpFromAny; + /// + /// Attempts to get the first relay end point from any of the media streams. A media stream will have a relay end point + /// set if is using TURN. + /// + private TurnRelayEndPoint? GetFirstRelayEndPointFromMediaStreams() + { + foreach (var stream in AudioStreamList) + { + if (stream.IsUsingRelayEndPoint && stream.RtpRelayEndPoint is { } rtpRelayEndPoint) + { + return rtpRelayEndPoint; + } + } - InitIPEndPointAndSecurityContext(newVideoStream); - return newVideoStream; + foreach (var stream in VideoStreamList) + { + if (stream.IsUsingRelayEndPoint && stream.RtpRelayEndPoint is { } rtpRelayEndPoint) + { + return rtpRelayEndPoint; + } } - /// - /// Adjust the stream status of the local media tracks based on the remote tracks. - /// - private void SetLocalTrackStreamStatus(MediaStreamTrack localTrack, MediaStreamStatusEnum remoteTrackStatus, IPEndPoint remoteRTPEndPoint) + foreach (var stream in TextStreamList) { - if (localTrack != null) + if (stream.IsUsingRelayEndPoint && stream.RtpRelayEndPoint is { } rtpRelayEndPoint) { - if (localTrack.StreamStatus == MediaStreamStatusEnum.Inactive) - { - localTrack.StreamStatus = localTrack.DefaultStreamStatus; - } + return rtpRelayEndPoint; + } + } - if (remoteTrackStatus == MediaStreamStatusEnum.Inactive) - { - // The remote party does not support this media type. Set the local stream status to inactive. - localTrack.StreamStatus = MediaStreamStatusEnum.Inactive; - } - else if (remoteRTPEndPoint != null) - { - if (IPAddress.Any.Equals(remoteRTPEndPoint.Address) || IPAddress.IPv6Any.Equals(remoteRTPEndPoint.Address)) - { - // A connection address of 0.0.0.0 or [::], which is unreachable, means the media is inactive, except - // if a special port number is used (defined as "9") which indicates that the media announcement is not - // responsible for setting the remote end point for the audio stream. Instead it's most likely being set - // using ICE. - if (remoteRTPEndPoint.Port != SDP.IGNORE_RTP_PORT_NUMBER) - { - localTrack.StreamStatus = MediaStreamStatusEnum.Inactive; - } - } - else if (remoteRTPEndPoint.Port == 0) - { - localTrack.StreamStatus = MediaStreamStatusEnum.Inactive; - } - } + return null; + } + + /// + /// Attempts to get the first RTP STUN server reflexive end point from any of the media streams. A media stream will only have a STUN reflexive end point + /// set if a STUN client has been used to determine it. + /// + private IPEndPoint? GetFirstRTPSrflxEndPointFromMediaStreams() + { + foreach (var stream in AudioStreamList) + { + if (stream.GetRTPChannel() is { RTPSrflxEndPoint: not null } channel) + { + return channel.RTPSrflxEndPoint; } } - /// - /// Attempts to get the first relay end point from any of the media streams. A media stream will have a relay end point - /// set if is using TURN. - /// - private TurnRelayEndPoint GetFirstRelayEndPointFromMediaStreams() + foreach (var stream in VideoStreamList) { - return - AudioStreamList.FirstOrDefault(x => x.IsUsingRelayEndPoint)?.RtpRelayEndPoint ?? - VideoStreamList.FirstOrDefault(x => x.IsUsingRelayEndPoint)?.RtpRelayEndPoint ?? - TextStreamList.FirstOrDefault(x => x.IsUsingRelayEndPoint)?.RtpRelayEndPoint; + if (stream.GetRTPChannel() is { RTPSrflxEndPoint: not null } channel) + { + return channel.RTPSrflxEndPoint; + } } - /// - /// Attempts to get the first RTP STUN server reflexive end point from any of the media streams. A media stream will only have a STUN reflexive end point - /// set if a STUN client has been used to determine it. - /// - private IPEndPoint GetFirstRTPSrflxEndPointFromMediaStreams() + foreach (var stream in TextStreamList) { - return - AudioStreamList.Where(x => x.GetRTPChannel()?.RTPSrflxEndPoint != null).FirstOrDefault()?.GetRTPChannel()?.RTPSrflxEndPoint ?? - VideoStreamList.Where(x => x.GetRTPChannel()?.RTPSrflxEndPoint != null).FirstOrDefault()?.GetRTPChannel()?.RTPSrflxEndPoint ?? - TextStreamList.Where(x => x.GetRTPChannel()?.RTPSrflxEndPoint != null).FirstOrDefault()?.GetRTPChannel()?.RTPSrflxEndPoint; + if (stream.GetRTPChannel() is { RTPSrflxEndPoint: not null } channel) + { + return channel.RTPSrflxEndPoint; + } } - /// - /// Generates a session description from the provided list of MediaStream. - /// - /// The list of tracks to generate the session description for. - /// Optional. If set this address will be used as - /// the SDP Connection address. If not specified the Internet facing address will - /// be used. IPAddress.Any and IPAddress. Any and IPv6Any are special cases. If they are set the respective - /// Internet facing IPv4 or IPv6 address will be used. - /// A session description payload. - private SDP GetSessionDescription(List mediaStreamList, IPAddress connectionAddress) - { - IPAddress localAddress = connectionAddress; + return null; + } + + /// + /// Generates a session description from the provided list of MediaStream. + /// + /// The list of tracks to generate the session description for. + /// Optional. If set this address will be used as + /// the SDP Connection address. If not specified the Internet facing address will + /// be used. IPAddress.Any and IPAddress. Any and IPv6Any are special cases. If they are set the respective + /// Internet facing IPv4 or IPv6 address will be used. + /// A session description payload. + private SDP GetSessionDescription(List mediaStreamList, IPAddress? connectionAddress) + { + var localAddress = connectionAddress; - if (localAddress == null || localAddress == IPAddress.Any || localAddress == IPAddress.IPv6Any) + if (localAddress is null || localAddress == IPAddress.Any || localAddress == IPAddress.IPv6Any) + { + if (rtpSessionConfig.BindAddress is { }) { - if (rtpSessionConfig.BindAddress != null) + localAddress = rtpSessionConfig.BindAddress; + } + else + { + localAddress = null; + foreach (var audioStream in AudioStreamList) { - localAddress = rtpSessionConfig.BindAddress; + if (audioStream.DestinationEndPoint is { Address: not null }) + { + if (IPAddress.Any.Equals(audioStream.DestinationEndPoint.Address) || IPAddress.IPv6Any.Equals(audioStream.DestinationEndPoint.Address)) + { + // If the remote party has set an inactive media stream via the connection address then we do the same. + localAddress = audioStream.DestinationEndPoint.Address; + break; + } + else + { + localAddress = NetServices.GetLocalAddressForRemote(audioStream.DestinationEndPoint.Address); + break; + } + } } - else + + if (localAddress is null) { - localAddress = null; - foreach (var audioStream in AudioStreamList) + foreach (var videoStream in VideoStreamList) { - if (audioStream.DestinationEndPoint != null && audioStream.DestinationEndPoint.Address != null) + if (videoStream.DestinationEndPoint is { Address: not null }) { - if (IPAddress.Any.Equals(audioStream.DestinationEndPoint.Address) || IPAddress.IPv6Any.Equals(audioStream.DestinationEndPoint.Address)) + if (IPAddress.Any.Equals(videoStream.DestinationEndPoint.Address) || IPAddress.IPv6Any.Equals(videoStream.DestinationEndPoint.Address)) { // If the remote party has set an inactive media stream via the connection address then we do the same. - localAddress = audioStream.DestinationEndPoint.Address; + localAddress = videoStream.DestinationEndPoint.Address; break; } else { - localAddress = NetServices.GetLocalAddressForRemote(audioStream.DestinationEndPoint.Address); + localAddress = NetServices.GetLocalAddressForRemote(videoStream.DestinationEndPoint.Address); break; } } } + } - if (localAddress == null) + if (localAddress is null) + { + foreach (var textStream in TextStreamList) { - foreach (var videoStream in VideoStreamList) + if (textStream.DestinationEndPoint is { Address: not null }) { - if (videoStream.DestinationEndPoint != null && videoStream.DestinationEndPoint.Address != null) + if (IPAddress.Any.Equals(textStream.DestinationEndPoint.Address) || IPAddress.IPv6Any.Equals(textStream.DestinationEndPoint.Address)) { - if (IPAddress.Any.Equals(videoStream.DestinationEndPoint.Address) || IPAddress.IPv6Any.Equals(videoStream.DestinationEndPoint.Address)) - { - // If the remote party has set an inactive media stream via the connection address then we do the same. - localAddress = videoStream.DestinationEndPoint.Address; - break; - } - else - { - localAddress = NetServices.GetLocalAddressForRemote(videoStream.DestinationEndPoint.Address); - break; - } + // If the remote party has set an inactive media stream via the connection address then we do the same. + localAddress = textStream.DestinationEndPoint.Address; + break; } - } - } - - if (localAddress == null) - { - foreach (var textStream in TextStreamList) - { - if (textStream.DestinationEndPoint != null && textStream.DestinationEndPoint.Address != null) + else { - if (IPAddress.Any.Equals(textStream.DestinationEndPoint.Address) || IPAddress.IPv6Any.Equals(textStream.DestinationEndPoint.Address)) - { - // If the remote party has set an inactive media stream via the connection address then we do the same. - localAddress = textStream.DestinationEndPoint.Address; - break; - } - else - { - localAddress = NetServices.GetLocalAddressForRemote(textStream.DestinationEndPoint.Address); - break; - } + localAddress = NetServices.GetLocalAddressForRemote(textStream.DestinationEndPoint.Address); + break; } } } + } - if (localAddress == null) + if (localAddress is null) + { + if (connectionAddress == IPAddress.IPv6Any && NetServices.InternetDefaultIPv6Address is { }) { - if (connectionAddress == IPAddress.IPv6Any && NetServices.InternetDefaultIPv6Address != null) - { - // If an IPv6 address has been requested AND there is a public IPv6 address available use it. - localAddress = NetServices.InternetDefaultIPv6Address; - } - else - { - localAddress = NetServices.InternetDefaultAddress; - } + // If an IPv6 address has been requested AND there is a public IPv6 address available use it. + localAddress = NetServices.InternetDefaultIPv6Address; + } + else + { + localAddress = NetServices.InternetDefaultAddress; } } } + } - SDP sdp = new SDP(IPAddress.Loopback); - sdp.SessionId = m_sdpSessionID; - sdp.AnnouncementVersion = m_sdpAnnouncementVersion; + var sdp = new SDP(IPAddress.Loopback); + sdp.SessionId = m_sdpSessionID; + sdp.AnnouncementVersion = m_sdpAnnouncementVersion; - sdp.Connection = new SDPConnectionInformation(localAddress); + Debug.Assert(localAddress is { }); - int mediaIndex = 0; - int audioMediaIndex = 0; - int videoMediaIndex = 0; - int textMediaIndex = 0; + sdp.Connection = new SDPConnectionInformation(localAddress); - foreach (var mediaStream in mediaStreamList) - { - int mindex = 0; - string midTag = "0"; + var mediaIndex = 0; + var audioMediaIndex = 0; + var videoMediaIndex = 0; + var textMediaIndex = 0; - if (RemoteDescription == null) - { - mindex = mediaIndex; - midTag = mediaIndex.ToString(); - } - else + foreach (var mediaStream in mediaStreamList) + { + var mindex = 0; + var midTag = "0"; + + if (RemoteDescription is null) + { + mindex = mediaIndex; + midTag = mediaIndex.ToString(); + } + else + { + Debug.Assert(mediaStream.LocalTrack is { }); + switch (mediaStream.LocalTrack.Kind) { - if (mediaStream.LocalTrack.Kind == SDPMediaTypesEnum.audio) - { + case SDPMediaTypesEnum.audio: (mindex, midTag) = RemoteDescription.GetIndexForMediaType(mediaStream.LocalTrack.Kind, audioMediaIndex); audioMediaIndex++; - } - else if (mediaStream.LocalTrack.Kind == SDPMediaTypesEnum.video) - { + break; + case SDPMediaTypesEnum.video: (mindex, midTag) = RemoteDescription.GetIndexForMediaType(mediaStream.LocalTrack.Kind, videoMediaIndex); videoMediaIndex++; - } - else if (mediaStream.LocalTrack.Kind == SDPMediaTypesEnum.text) - { + break; + case SDPMediaTypesEnum.text: (mindex, midTag) = RemoteDescription.GetIndexForMediaType(mediaStream.LocalTrack.Kind, textMediaIndex); textMediaIndex++; - } + break; } - mediaIndex++; + } + mediaIndex++; - int rtpPort = 0; // A port of zero means the media type is not supported. - if (mediaStream.LocalTrack.Capabilities != null && mediaStream.LocalTrack.Capabilities.Count() > 0 && mediaStream.LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) + var rtpPort = 0; // A port of zero means the media type is not supported. + Debug.Assert(mediaStream.LocalTrack is { }); + if (mediaStream.LocalTrack.Capabilities is { } && mediaStream.LocalTrack.Capabilities.Count > 0 && mediaStream.LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) + { + if (rtpSessionConfig.IsMediaMultiplexed) { - if (rtpSessionConfig.IsMediaMultiplexed) - { - rtpPort = m_primaryStream.GetRtpPortForSessionDescription(); - } - else if (mediaStream.HasRtpChannel()) - { - // If media stream does not have a Rtp channel it means this media type is not supported and rtpPort will remain zero. - rtpPort = mediaStream.GetRtpPortForSessionDescription(); - } + Debug.Assert(m_primaryStream is { }); + rtpPort = m_primaryStream.GetRtpPortForSessionDescription(); } + else if (mediaStream.HasRtpChannel()) + { + // If media stream does not have a Rtp channel it means this media type is not supported and rtpPort will remain zero. + rtpPort = mediaStream.GetRtpPortForSessionDescription(); + } + } - SDPMediaAnnouncement announcement = new SDPMediaAnnouncement(mediaStream.LocalTrack.Kind, rtpPort, mediaStream.LocalTrack.Capabilities); + Debug.Assert(mediaStream.LocalTrack is { }); + var announcement = new SDPMediaAnnouncement(mediaStream.LocalTrack.Kind, rtpPort, mediaStream.LocalTrack.Capabilities); - announcement.Transport = rtpSessionConfig.UseSdpCryptoNegotiation ? RTP_SECUREMEDIA_PROFILE : RTP_MEDIA_PROFILE; - announcement.MediaStreamStatus = mediaStream.LocalTrack.StreamStatus; - announcement.MediaID = midTag; - announcement.MLineIndex = mindex; + announcement.Transport = rtpSessionConfig.UseSdpCryptoNegotiation ? RTP_SECUREMEDIA_PROFILE : RTP_MEDIA_PROFILE; + announcement.MediaStreamStatus = mediaStream.LocalTrack.StreamStatus; + announcement.MediaID = midTag; + announcement.MLineIndex = mindex; - if (mediaStream.LocalTrack.MaximumBandwidth > 0) - { - announcement.TIASBandwidth = mediaStream.LocalTrack.MaximumBandwidth; - } + if (mediaStream.LocalTrack.MaximumBandwidth > 0) + { + announcement.TIASBandwidth = mediaStream.LocalTrack.MaximumBandwidth; + } + + if (mediaStream.LocalTrack.Ssrc != 0 && mediaStream.RtcpSession?.Cname is { } trackCname) + { + announcement.SsrcAttributes.Add(new SDPSsrcAttribute(mediaStream.LocalTrack.Ssrc, trackCname, null)); + } + + if (rtpSessionConfig.UseSdpCryptoNegotiation) + { + var sdpType = RemoteDescription is null || RequireRenegotiation ? SdpType.offer : SdpType.answer; + var srtpHandler = mediaStream.GetOrCreateSrtpHandler(); - if (mediaStream.LocalTrack.Ssrc != 0) + if (sdpType == SdpType.offer) { - string trackCname = mediaStream.RtcpSession?.Cname; + if (srtpHandler.LocalSecurityDescription is null) + { + // first time security negotiation in SDP offer + uint tag = 1; + foreach (var cryptoSuite in SrtpCryptoSuites) + { + announcement.SecurityDescriptions.Add(SDPSecurityDescription.CreateNew(tag, cryptoSuite)); + tag++; + } - if (trackCname != null) + srtpHandler.SetupLocal(announcement.SecurityDescriptions, sdpType); + } + else { - announcement.SsrcAttributes.Add(new SDPSsrcAttribute(mediaStream.LocalTrack.Ssrc, trackCname, null)); + // reuse negotiated security in SDP offer + announcement.SecurityDescriptions.Add(srtpHandler.LocalSecurityDescription); } } - - if (rtpSessionConfig.UseSdpCryptoNegotiation) + else { - var sdpType = RemoteDescription == null || RequireRenegotiation ? SdpType.offer : SdpType.answer; - var srtpHandler = mediaStream.GetOrCreateSrtpHandler(); - - if (sdpType == SdpType.offer) + if (srtpHandler.LocalSecurityDescription is { }) { - if (srtpHandler.LocalSecurityDescription == null) + // try to reuse security negotiation in SDP answer + if (FindSecurityDescriptionForReuse( + RemoteDescription, + mindex, + srtpHandler.LocalSecurityDescription.Tag, + srtpHandler.LocalSecurityDescription.CryptoSuite) + is { } sec) + { + announcement.SecurityDescriptions.Add(srtpHandler.LocalSecurityDescription); + } + else + { + throw new SipSorceryException("Error reusing crypto attribute for SDP answer. No compatible offer."); + } + + // Local method for finding security description for reuse + static SDPSecurityDescription? FindSecurityDescriptionForReuse(SDP? remoteDescription, int mindex, uint tag, SDPSecurityDescription.CryptoSuites cryptoSuite) { - // first time security negotiation in SDP offer - uint tag = 1; - foreach (SDPSecurityDescription.CryptoSuites cryptoSuite in SrtpCryptoSuites) + if (remoteDescription?.Media is null) + { + return null; + } + + // Find media announcement with matching MLineIndex and search security descriptions in one loop + for (var i = 0; i < remoteDescription.Media.Count; i++) { - announcement.SecurityDescriptions.Add(SDPSecurityDescription.CreateNew(tag, cryptoSuite)); - tag++; + if (remoteDescription.Media[i].MLineIndex == mindex) + { + var mediaAnnouncement = remoteDescription.Media[i]; + if (mediaAnnouncement.SecurityDescriptions is { }) + { + // Search for security description with matching Tag and CryptoSuite + for (var j = 0; j < mediaAnnouncement.SecurityDescriptions.Count; j++) + { + var secDesc = mediaAnnouncement.SecurityDescriptions[j]; + if (secDesc.Tag == tag && secDesc.CryptoSuite == cryptoSuite) + { + return secDesc; + } + } + } + break; // Found the media announcement but no matching security description + } } + return null; + } + } + else + { + // first time security negotiation in SDP answer + // Find compatible security description from the remote offer + if (FindCompatibleSecurityDescription( + RemoteDescription, + mindex) + is { } sec) + { + announcement.SecurityDescriptions.Add(SDPSecurityDescription.CreateNew(sec.Tag, sec.CryptoSuite)); srtpHandler.SetupLocal(announcement.SecurityDescriptions, sdpType); } else { - // reuse negotiated security in SDP offer - announcement.SecurityDescriptions.Add(srtpHandler.LocalSecurityDescription); + throw new SipSorceryException("Error creating crypto attribute for SDP answer. No compatible offer."); } } - else + + // Local method for finding any compatible security description from remote offer + SDPSecurityDescription? FindCompatibleSecurityDescription(SDP? remoteDescription, int mindex) { - if (srtpHandler.LocalSecurityDescription != null) + if (remoteDescription?.Media is null) { - // try to reuse security negotiation in SDP answer - var sec = RemoteDescription?.Media.FirstOrDefault(a => a.MLineIndex == mindex)?.SecurityDescriptions - .FirstOrDefault(s => s.Tag == srtpHandler.LocalSecurityDescription.Tag && s.CryptoSuite == srtpHandler.LocalSecurityDescription.CryptoSuite); - if (sec == null) - { - throw new ApplicationException("Error reusing crypto attribute for SDP answer. No compatible offer."); - } - else - { - announcement.SecurityDescriptions.Add(srtpHandler.LocalSecurityDescription); - } + return null; } - else + + // Find media announcement with matching MLineIndex + for (var i = 0; i < remoteDescription.Media.Count; i++) { - // first time security negotiation in SDP answer - var sec = RemoteDescription?.Media.FirstOrDefault(a => a.MLineIndex == mindex)?.SecurityDescriptions - .FirstOrDefault(s => SrtpCryptoSuites.Contains(s.CryptoSuite)); - if (sec == null) - { - throw new ApplicationException("Error creating crypto attribute for SDP answer. No compatible offer."); - } - else + if (remoteDescription.Media[i].MLineIndex == mindex) { - announcement.SecurityDescriptions.Add(SDPSecurityDescription.CreateNew(sec.Tag, sec.CryptoSuite)); - srtpHandler.SetupLocal(announcement.SecurityDescriptions, sdpType); + var mediaAnnouncement = remoteDescription.Media[i]; + if (mediaAnnouncement.SecurityDescriptions is { }) + { + // Search for first security description with compatible CryptoSuite + for (var j = 0; j < mediaAnnouncement.SecurityDescriptions.Count; j++) + { + var secDesc = mediaAnnouncement.SecurityDescriptions[j]; + if (SrtpCryptoSuites.Contains(secDesc.CryptoSuite)) + { + return secDesc; + } + } + } + break; // Found the media announcement but no compatible security description } } - } - if (srtpHandler.IsNegotiationComplete) - { - mediaStream.SetSecurityContext(srtpHandler.ProtectRTP, srtpHandler.UnprotectRTP, srtpHandler.ProtectRTCP, srtpHandler.UnprotectRTCP); + return null; } } - sdp.Media.Add(announcement); + if (srtpHandler.IsNegotiationComplete) + { + mediaStream.SetSecurityContext(srtpHandler.ProtectRTP, srtpHandler.UnprotectRTP, srtpHandler.ProtectRTCP, srtpHandler.UnprotectRTCP); + } } - return sdp; + sdp.Media.Add(announcement); } - /// - /// Creates a new RTP channel (which manages the UDP socket sending and receiving RTP - /// packets) for use with this session. - /// - /// A new RTPChannel instance. - protected virtual RTPChannel CreateRtpChannel() + return sdp; + } + + /// + /// Creates a new RTP channel (which manages the UDP socket sending and receiving RTP + /// packets) for use with this session. + /// + /// A new RTPChannel instance. + protected virtual RTPChannel CreateRtpChannel() + { + if (rtpSessionConfig.IsMediaMultiplexed) { - if (rtpSessionConfig.IsMediaMultiplexed) + if (MultiplexRtpChannel is { }) { - if (MultiplexRtpChannel != null) - { - return MultiplexRtpChannel; - } + return MultiplexRtpChannel; } + } - // If RTCP is multiplexed we don't need a control socket. - int bindPort = (rtpSessionConfig.BindPort == 0) ? 0 : rtpSessionConfig.BindPort + m_rtpChannelsCount; - var rtpChannel = new RTPChannel(!rtpSessionConfig.IsRtcpMultiplexed, rtpSessionConfig.BindAddress, bindPort, rtpSessionConfig.RtpPortRange); + // If RTCP is multiplexed we don't need a control socket. + var bindPort = (rtpSessionConfig.BindPort == 0) ? 0 : rtpSessionConfig.BindPort + m_rtpChannelsCount; + var rtpChannel = new RTPChannel(!rtpSessionConfig.IsRtcpMultiplexed, rtpSessionConfig.BindAddress, bindPort, rtpSessionConfig.RtpPortRange); - if (rtpSessionConfig.IsMediaMultiplexed) - { - MultiplexRtpChannel = rtpChannel; - } + if (rtpSessionConfig.IsMediaMultiplexed) + { + MultiplexRtpChannel = rtpChannel; + } - rtpChannel.OnRTPDataReceived += OnReceive; - rtpChannel.OnControlDataReceived += OnReceive; // RTCP packets could come on RTP or control socket. - rtpChannel.OnClosed += OnRTPChannelClosed; + rtpChannel.OnRTPDataReceived += OnReceive; + rtpChannel.OnControlDataReceived += OnReceive; // RTCP packets could come on RTP or control socket. + rtpChannel.OnClosed += OnRTPChannelClosed; - // Start the RTP, and if required the Control, socket receivers and the RTCP session. - rtpChannel.Start(); + // Start the RTP, and if required the Control, socket receivers and the RTCP session. + rtpChannel.Start(); - m_rtpChannelsCount++; + m_rtpChannelsCount++; - return rtpChannel; - } + return rtpChannel; + } - /// - /// Gets the media streams available in this session. Will only be audio, video or both. - /// media streams represent an audio or video source that we are sending to the remote party. - /// - /// A list of the local tracks that have been added to this session. - protected List GetMediaStreams() - { - List mediaStreams = new List(); + /// + /// Gets the media streams available in this session. Will only be audio, video or both. + /// media streams represent an audio or video source that we are sending to the remote party. + /// + /// A list of the local tracks that have been added to this session. + protected List GetMediaStreams() + { + List mediaStreams = new List(); - foreach (var stream in AudioStreamList - .Concat(VideoStreamList) - .Concat(TextStreamList)) + AddMediaStreams(mediaStreams, AudioStreamList); + AddMediaStreams(mediaStreams, VideoStreamList); + AddMediaStreams(mediaStreams, TextStreamList); + + mediaStreams.Sort((a, b) => a.MediaInsertionOrder.CompareTo(b.MediaInsertionOrder)); + return mediaStreams; + + static void AddMediaStreams(List targetStreams, List sourceStreams) + where TStream : MediaStream + { + foreach (var stream in sourceStreams) { if (stream.LocalTrack != null) { - mediaStreams.Add(stream); + targetStreams.Add(stream); } else if (stream.RtcpSession != null && !stream.RtcpSession.IsClosed && stream.RemoteTrack != null) { var inactiveTrack = new MediaStreamTrack(stream.MediaType, false, stream.RemoteTrack.Capabilities, MediaStreamStatusEnum.Inactive); stream.LocalTrack = inactiveTrack; - mediaStreams.Add(stream); + targetStreams.Add(stream); } } - - mediaStreams.Sort((a, b) => a.MediaInsertionOrder.CompareTo(b.MediaInsertionOrder)); - return mediaStreams; } + } - /// - /// Starts the RTCP session(s) that monitor this RTP session. - /// - public virtual Task Start() + /// + /// Starts the RTCP session(s) that monitor this RTP session. + /// + public virtual Task Start() + { + if (!IsAudioStarted) { - if (!IsAudioStarted) + foreach (var audioStream in AudioStreamList) { - foreach (var audioStream in AudioStreamList) + if (audioStream.HasAudio && audioStream.RtcpSession is { }) { - if (audioStream.HasAudio && audioStream.RtcpSession != null && audioStream.LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) + Debug.Assert(audioStream.LocalTrack is { }); + if (audioStream.LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) { // The local audio track may have been disabled if there were no matching capabilities with // the remote party. @@ -2311,12 +2474,16 @@ public virtual Task Start() } } } + } - if (!IsVideoStarted) + if (!IsVideoStarted) + { + foreach (var videoStream in VideoStreamList) { - foreach (var videoStream in VideoStreamList) + if (videoStream.HasVideo && videoStream.RtcpSession is { }) { - if (videoStream.HasVideo && videoStream.RtcpSession != null && videoStream.LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) + Debug.Assert(videoStream.LocalTrack is { }); + if (videoStream.LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) { // The local video track may have been disabled if there were no matching capabilities with // the remote party. @@ -2325,12 +2492,16 @@ public virtual Task Start() } } } + } - if (!IsTextStarted) + if (!IsTextStarted) + { + foreach (var textStream in TextStreamList) { - foreach (var textStream in TextStreamList) + if (textStream.HasText && textStream.RtcpSession is { }) { - if (textStream.HasText && textStream.RtcpSession != null && textStream.LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) + Debug.Assert(textStream.LocalTrack is { }); + if (textStream.LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) { // The local video track may have been disabled if there were no matching capabilities with // the remote party. @@ -2339,196 +2510,240 @@ public virtual Task Start() } } } - - OnStarted?.Invoke(); - return Task.CompletedTask; } - /// - /// Sends an audio sample to the remote peer. (on the primary one) - /// - /// The duration in RTP timestamp units of the audio sample. This - /// value is added to the previous RTP timestamp when building the RTP header. - /// The audio sample to set as the RTP packet payload. - public void SendAudio(uint durationRtpUnits, byte[] sample) - { - //logger.LogTrace("SendAudio: durationRtpUnits={DurationRtpUnits}, sample size={Sample}", durationRtpUnits, sample?.Length); + OnStarted?.Invoke(); + return Task.CompletedTask; + } - AudioStream?.SendAudio(durationRtpUnits, sample); - } + /// + /// Sends an audio sample to the remote peer. (on the primary one) + /// + /// The duration in RTP timestamp units of the audio sample. This + /// value is added to the previous RTP timestamp when building the RTP header. + /// The audio sample to set as the RTP packet payload. + public void SendAudio(uint durationRtpUnits, ReadOnlyMemory sample) + { + SendAudio(durationRtpUnits, sample.Span); + } - /// - /// Sends a video sample to the remote peer. (on the primary one) - /// - /// The duration in RTP timestamp units of the video sample. This - /// value is added to the previous RTP timestamp when building the RTP header. - /// The video sample to set as the RTP packet payload. - public void SendVideo(uint durationRtpUnits, byte[] sample) - { - VideoStream?.SendVideo(durationRtpUnits, sample); - } + /// + /// Sends an audio sample to the remote peer. (on the primary one) + /// + /// The duration in RTP timestamp units of the audio sample. This + /// value is added to the previous RTP timestamp when building the RTP header. + /// The audio sample to set as the RTP packet payload. + public void SendAudio(uint durationRtpUnits, ReadOnlySpan sample) + { + AudioStream?.SendAudio(durationRtpUnits, sample); + } - /// - /// Sends a text sample to the remote peer. (on the primary one) - /// - /// The text sample to set as the RTP packet payload. - public void SendText(byte[] sample) - { - TextStream?.SendText(sample); - } + /// + /// Sends an encoded audio frame to the remote peer. (on the primary one) + /// + /// The encoded audio frame containing the audio data, format, and duration information. + public void SendAudio(EncodedAudioFrame encodedAudioFrame) + { + AudioStream?.SendAudio(encodedAudioFrame); + } - /// - /// Sends a DTMF tone as an RTP event to the remote party. (on the primary one) - /// - /// The DTMF tone to send. - /// RTP events can span multiple RTP packets. This token can - /// be used to cancel the send. - public virtual Task SendDtmf(byte key, CancellationToken ct) - { - return AudioStream?.SendDtmf(key, ct); - } + /// + /// Sends a video sample to the remote peer. (on the primary one) + /// + /// The duration in RTP timestamp units of the video sample. This + /// value is added to the previous RTP timestamp when building the RTP header. + /// The video sample to set as the RTP packet payload. + public void SendVideo(uint durationRtpUnits, ReadOnlyMemory sample) + { + VideoStream?.SendVideo(durationRtpUnits, sample.Span); + } - public Task SendDtmfEvent(RTPEvent rtpEvent, CancellationToken cancellationToken, int clockRate = RTPSession.DEFAULT_AUDIO_CLOCK_RATE, int samplePeriod = RTPSession.RTP_EVENT_DEFAULT_SAMPLE_PERIOD_MS) - { - return AudioStream?.SendDtmfEvent(rtpEvent, cancellationToken, clockRate, samplePeriod); - } + /// + /// Sends a text sample to the remote peer. (on the primary one) + /// + /// The text sample to set as the RTP packet payload. + public void SendText(byte[] sample) + { + TextStream?.SendText(sample); + } + + /// + /// Sends a DTMF tone as an RTP event to the remote party. (on the primary one) + /// + /// The DTMF tone to send. + /// RTP events can span multiple RTP packets. This token can + /// be used to cancel the send. + public virtual Task SendDtmf(byte key, CancellationToken ct) + { + return AudioStream?.SendDtmf(key, ct) ?? Task.CompletedTask; + } + + public Task SendDtmfEvent(RTPEvent rtpEvent, CancellationToken cancellationToken, int clockRate = RTPSession.DEFAULT_AUDIO_CLOCK_RATE, int samplePeriod = RTPSession.RTP_EVENT_DEFAULT_SAMPLE_PERIOD_MS) + { + return AudioStream?.SendDtmfEvent(rtpEvent, cancellationToken, clockRate, samplePeriod) ?? Task.CompletedTask; + } - /// - /// Close the session and RTP channel. - /// - public virtual void Close(string reason) + /// + /// Close the session and RTP channel. + /// + public virtual void Close(string? reason) + { + if (!IsClosed) { - if (!IsClosed) - { - IsClosed = true; + IsClosed = true; - foreach (var audioStream in AudioStreamList) + foreach (var audioStream in AudioStreamList) + { + if (audioStream is { }) { - if (audioStream != null) - { - CloseMediaStream(reason, audioStream); - } + CloseMediaStream(reason, audioStream); } + } - foreach (var videoStream in VideoStreamList) + foreach (var videoStream in VideoStreamList) + { + if (videoStream is { }) { - if (videoStream != null) - { - CloseMediaStream(reason, videoStream); - } + CloseMediaStream(reason, videoStream); } + } - foreach (var textStream in TextStreamList) + foreach (var textStream in TextStreamList) + { + if (textStream is { }) { - if (textStream != null) - { - CloseMediaStream(reason, textStream); - } + CloseMediaStream(reason, textStream); } - - OnRtpClosed?.Invoke(reason); - OnClosed?.Invoke(); } + + OnRtpClosed?.Invoke(reason); + OnClosed?.Invoke(); } + } - private void CloseMediaStream(string reason, MediaStream mediaStream) - { - mediaStream.IsClosed = true; - CloseRtcpSession(mediaStream, reason); + private void CloseMediaStream(string? reason, MediaStream mediaStream) + { + mediaStream.IsClosed = true; + CloseRtcpSession(mediaStream, reason); - if (mediaStream.HasRtpChannel()) - { - var rtpChannel = mediaStream.GetRTPChannel(); - rtpChannel.OnRTPDataReceived -= OnReceive; - rtpChannel.OnControlDataReceived -= OnReceive; - rtpChannel.OnClosed -= OnRTPChannelClosed; - rtpChannel.Close(reason); - } + if (mediaStream.HasRtpChannel()) + { + var rtpChannel = mediaStream.GetRTPChannel(); + Debug.Assert(rtpChannel is { }); + rtpChannel.OnRTPDataReceived -= OnReceive; + rtpChannel.OnControlDataReceived -= OnReceive; + rtpChannel.OnClosed -= OnRTPChannelClosed; + rtpChannel.Close(reason); } + } - protected void OnReceive(int localPort, IPEndPoint remoteEndPoint, byte[] buffer) + protected void OnReceive(int localPort, IPEndPoint remoteEndPoint, byte[] buffer) + { + OnReceive(localPort, remoteEndPoint, buffer.AsMemory()); + } + + protected void OnReceive(int localPort, IPEndPoint remoteEndPoint, ReadOnlyMemory buffer) + { + if (remoteEndPoint.Address.IsIPv4MappedToIPv6) { - //logger.LogDebug("RTP Session OnReceive from {RemoteEndPoint} {length} bytes buffer[0]={zeroByte}.", remoteEndPoint, buffer.Length, buffer[0]); + // Required for matching existing RTP end points (typically set from SDP) and + // whether or not the destination end point should be switched. + remoteEndPoint.Address = remoteEndPoint.Address.MapToIPv4(); + } - if (remoteEndPoint.Address.IsIPv4MappedToIPv6) + // Quick sanity check on whether this is not an RTP or RTCP packet. + var bufferSpan = buffer.Span; + if (buffer.Length > RTPHeader.MIN_HEADER_LEN && bufferSpan[0] >= 128 && bufferSpan[0] <= 191) + { + if ((rtpSessionConfig.IsSecure || rtpSessionConfig.UseSdpCryptoNegotiation) && !IsSecureContextReady()) { - // Required for matching existing RTP end points (typically set from SDP) and - // whether or not the destination end point should be switched. - remoteEndPoint.Address = remoteEndPoint.Address.MapToIPv4(); + logger.LogRtpPacketBeforeContextReady(); } - - // Quick sanity check on whether this is not an RTP or RTCP packet. - if (buffer?.Length > RTPHeader.MIN_HEADER_LEN && buffer[0] >= 128 && buffer[0] <= 191) + else { - if ((rtpSessionConfig.IsSecure || rtpSessionConfig.UseSdpCryptoNegotiation) && !IsSecureContextReady()) + var retcReportType = (RTCPReportTypesEnum)bufferSpan[1]; + if (retcReportType is + RTCPReportTypesEnum.SR or + RTCPReportTypesEnum.RR or + RTCPReportTypesEnum.SDES or + RTCPReportTypesEnum.BYE or + RTCPReportTypesEnum.PSFB or + RTCPReportTypesEnum.RTPFB) { - logger.LogWarning("RTP or RTCP packet received before secure context ready."); + // Only call OnReceiveRTCPPacket for supported RTCPCompoundPacket types + OnReceiveRTCPPacket(localPort, remoteEndPoint, buffer); + } + else if (!RTCPReportTypesEnumExtensions.IsDefined(retcReportType)) + { + OnReceiveRTPPacket(localPort, remoteEndPoint, buffer); } else { - if (Enum.IsDefined(typeof(RTCPReportTypesEnum), buffer[1])) - { - // Only call OnReceiveRTCPPacket for supported RTCPCompoundPacket types - if (buffer[1] == (byte)RTCPReportTypesEnum.SR || - buffer[1] == (byte)RTCPReportTypesEnum.RR || - buffer[1] == (byte)RTCPReportTypesEnum.SDES || - buffer[1] == (byte)RTCPReportTypesEnum.BYE || - buffer[1] == (byte)RTCPReportTypesEnum.PSFB || - buffer[1] == (byte)RTCPReportTypesEnum.RTPFB) - { - OnReceiveRTCPPacket(localPort, remoteEndPoint, buffer); - } - } - else - { - OnReceiveRTPPacket(localPort, remoteEndPoint, buffer); - } + logger.LogRtpFailedToParseReport(); } } } + } - private void OnReceiveRTCPPacket(int localPort, IPEndPoint remoteEndPoint, byte[] buffer) - { - logger.LogTrace("RTCP packet received from {RemoteEndPoint} {Buffer}", remoteEndPoint, buffer.HexStr()); - - #region RTCP packet. + private void OnReceiveRTCPPacket(int localPort, IPEndPoint remoteEndPoint, ReadOnlyMemory buffer) + { + logger.LogRtpSessionRtcpPacketReceived(remoteEndPoint, buffer); - // Get the SSRC in order to be able to figure out which media type - // This will let us choose the apropriate unprotect methods + #region RTCP packet. - uint ssrc = BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(4)); + // Get the SSRC in order to be able to figure out which media type + // This will let us choose the apropriate unprotect methods + var ssrc = BinaryPrimitives.ReadUInt32BigEndian(buffer.Span.Slice(4)); - MediaStream mediaStream = GetMediaStream(ssrc); - var secureContext = mediaStream?.GetSecurityContext() ?? PrimaryStream?.GetSecurityContext(); + var mediaStream = GetMediaStream(ssrc); + var secureContext = mediaStream?.GetSecurityContext() ?? PrimaryStream?.GetSecurityContext(); - if (secureContext != null) + if (secureContext is { }) + { + var tempBuffer = ArrayPool.Shared.Rent(buffer.Length); + try { - int res = secureContext.UnprotectRtcpPacket(buffer, buffer.Length, out int outBufLen); + buffer.CopyTo(tempBuffer); + var res = secureContext.UnprotectRtcpPacket(tempBuffer, buffer.Length, out var outBufLen); if (res != 0) { - logger.LogWarning("SRTCP unprotect failed for {MediaType} track, result {Result}.", PrimaryStream.MediaType, res); + logger.LogRtpSecureContextError(mediaStream?.MediaType, res); return; } else { - buffer = buffer.Take(outBufLen).ToArray(); + if (outBufLen < buffer.Length) + { + OnReceiveRTCPPacketCore(tempBuffer.AsSpan(0, outBufLen), remoteEndPoint, mediaStream); + + return; + } } } + finally + { + ArrayPool.Shared.Return(tempBuffer); + } + } + + OnReceiveRTCPPacketCore(buffer.Span, remoteEndPoint, mediaStream); + void OnReceiveRTCPPacketCore(ReadOnlySpan buffer, IPEndPoint remoteEndPoint, MediaStream? mediaStream) + { var rtcpPkt = new RTCPCompoundPacket(buffer); - if (rtcpPkt != null) + if (rtcpPkt is { }) { mediaStream = GetMediaStream(rtcpPkt); - if (rtcpPkt.Bye != null) + if (rtcpPkt.Bye is { } bye) { - logger.LogDebug("RTCP BYE received for SSRC {SSRC}, reason {Reason}.", rtcpPkt.Bye.SSRC, rtcpPkt.Bye.Reason); + logger.LogRtpSessionRtcpByeReceived(bye.SSRC, bye.Reason); // In some cases, such as a SIP re-INVITE, it's possible the RTP session // will keep going with a new remote SSRC. - if (mediaStream?.RemoteTrack != null && rtcpPkt.Bye.SSRC == mediaStream.RemoteTrack.Ssrc) + if (mediaStream?.RemoteTrack is { } && bye.SSRC == mediaStream.RemoteTrack.Ssrc) { - mediaStream.RtcpSession?.RemoveReceptionReport(rtcpPkt.Bye.SSRC); + mediaStream.RtcpSession?.RemoveReceptionReport(bye.SSRC); //AudioDestinationEndPoint = null; //AudioControlDestinationEndPoint = null; mediaStream.RemoteTrack.Ssrc = 0; @@ -2536,26 +2751,26 @@ private void OnReceiveRTCPPacket(int localPort, IPEndPoint remoteEndPoint, byte[ else { // We close peer connection only if there is no more local/remote tracks on the primary stream - if ((m_primaryStream?.RemoteTrack == null) && (m_primaryStream?.LocalTrack == null)) + if ((m_primaryStream?.RemoteTrack is null) && (m_primaryStream?.LocalTrack is null)) { - OnRtcpBye?.Invoke(rtcpPkt.Bye.Reason); + OnRtcpBye?.Invoke(bye.Reason); } } } else if (!IsClosed) { - if (mediaStream?.RtcpSession != null) + if (mediaStream?.RtcpSession is { }) { if (mediaStream.RtcpSession.LastActivityAt == DateTime.MinValue) { // On the first received RTCP report for a session check whether the remote end point matches the // expected remote end point. If not it's "likely" that a private IP address was specified in the SDP. // Take the risk and switch the remote control end point to the one we are receiving from. - if ((mediaStream.ControlDestinationEndPoint == null || + if ((mediaStream.ControlDestinationEndPoint is null || !mediaStream.ControlDestinationEndPoint.Address.Equals(remoteEndPoint.Address) || mediaStream.ControlDestinationEndPoint.Port != remoteEndPoint.Port)) { - logger.LogDebug("{MediaType} control end point switched from {ControlDestinationEndPoint} to {RemoteEndPoint}.", mediaStream.MediaType, mediaStream.ControlDestinationEndPoint, remoteEndPoint); + logger.LogRtpSessionRemoteControlEndpointSwitched(mediaStream.MediaType, mediaStream.ControlDestinationEndPoint, remoteEndPoint); mediaStream.ControlDestinationEndPoint = remoteEndPoint; } } @@ -2577,394 +2792,372 @@ private void OnReceiveRTCPPacket(int localPort, IPEndPoint remoteEndPoint, byte[ } else { - logger.LogWarning("Failed to parse RTCP compound report."); + logger.LogRtpFailedToParseReport(); } - - #endregion } - private void OnReceiveRTPPacket(int localPort, IPEndPoint remoteEndPoint, byte[] buffer) - { - //logger.LogDebug("RTPSession OnReceiveRTPPacket received from {RemoteEndPoint} {length} bytes.", remoteEndPoint, buffer.Length); - - if (!IsClosed) - { - var hdr = new RTPHeader(buffer); - - MediaStream mediaStream = GetMediaStream(hdr.SyncSource); - - if ((mediaStream == null) && (AudioStreamList.Count < 2) && (VideoStreamList.Count < 2) && (TextStreamList.Count < 2)) - { - mediaStream = GetMediaStreamFromPayloadType(hdr.PayloadType); - } - - if (mediaStream == null) - { - mediaStream = GetMediaStreamByRTPPort(localPort); - } + #endregion + } - if (mediaStream == null) - { - logger.LogWarning("An RTP packet with SSRC {SyncSource} and payload ID {PayloadType} was received that could not be matched to an audio or video stream.", hdr.SyncSource, hdr.PayloadType); - return; - } + private void OnReceiveRTPPacket(int localPort, IPEndPoint remoteEndPoint, ReadOnlyMemory buffer) + { + if (!IsClosed) + { + var hdr = new RTPHeader(buffer.Span); - hdr.ReceivedTime = DateTime.Now; - mediaStream.OnReceiveRTPPacket(hdr, localPort, remoteEndPoint, buffer); - } - } + var mediaStream = GetMediaStream(hdr.SyncSource); - private MediaStream GetMediaStreamFromPayloadType(int payloadId) - { - foreach (var audioStream in AudioStreamList) + if ((mediaStream is null) && (AudioStreamList.Count < 2) && (VideoStreamList.Count < 2) && (TextStreamList.Count < 2)) { - if (audioStream.RemoteTrack != null && audioStream.RemoteTrack.IsPayloadIDMatch(payloadId)) - { - return audioStream; - } - else if (audioStream.LocalTrack != null && audioStream.LocalTrack.IsPayloadIDMatch(payloadId)) - { - return audioStream; - } + mediaStream = GetMediaStreamFromPayloadType(hdr.PayloadType); } - foreach (var videoStream in VideoStreamList) - { - if (videoStream.RemoteTrack != null && videoStream.RemoteTrack.IsPayloadIDMatch(payloadId)) - { - return videoStream; - } - else if (videoStream.LocalTrack != null && videoStream.LocalTrack.IsPayloadIDMatch(payloadId)) - { - return videoStream; - } - } + mediaStream ??= GetMediaStreamByRTPPort(localPort); - foreach (var textStream in TextStreamList) + if (mediaStream is null) { - if (textStream.RemoteTrack != null && textStream.RemoteTrack.IsPayloadIDMatch(payloadId)) - { - return textStream; - } - else if (textStream.LocalTrack != null && textStream.LocalTrack.IsPayloadIDMatch(payloadId)) - { - return textStream; - } + logger.LogRtpPacketWithSsrcNotMatched(hdr.SyncSource, hdr.PayloadType); + return; } - return null; + hdr.ReceivedTime = DateTime.Now; + mediaStream.OnReceiveRTPPacket(hdr, localPort, remoteEndPoint, buffer); } + } - private MediaStream GetMediaStreamByRTPPort(int port) - { - foreach (var audioStream in AudioStreamList) - { - if (audioStream?.GetRTPChannel()?.RTPPort == port) - { - return audioStream; - } - } + private MediaStream? GetMediaStreamFromPayloadType(int payloadId) + { + return + GetMediaStreamFromPayloadTypeCore(AudioStreamList, payloadId) + ?? GetMediaStreamFromPayloadTypeCore(VideoStreamList, payloadId) + ?? GetMediaStreamFromPayloadTypeCore(TextStreamList, payloadId); - foreach (var videoStream in VideoStreamList) + static MediaStream? GetMediaStreamFromPayloadTypeCore(List streams, int payloadId) where TStream : MediaStream + { + foreach (var stream in streams) { - if (videoStream?.GetRTPChannel()?.RTPPort == port) + if (stream is { RemoteTrack: { } } && stream.RemoteTrack.IsPayloadIDMatch(payloadId)) { - return videoStream; + return stream; } - } - foreach (var textStream in TextStreamList) - { - if (textStream?.GetRTPChannel()?.RTPPort == port) + if (stream is { LocalTrack: { } } && stream.LocalTrack.IsPayloadIDMatch(payloadId)) { - return textStream; + return stream; } } return null; } + } + + private MediaStream? GetMediaStreamByRTPPort(int port) + { + return + GetMediaStreamByRtpPortCore(AudioStreamList, port) + ?? GetMediaStreamByRtpPortCore(VideoStreamList, port) + ?? GetMediaStreamByRtpPortCore(TextStreamList, port); - private MediaStream GetMediaStream(uint ssrc) + static MediaStream? GetMediaStreamByRtpPortCore(List streams, int port) where TStream : MediaStream { - foreach (var audioStream in AudioStreamList) + foreach (var stream in streams) { - if (audioStream?.RemoteTrack?.IsSsrcMatch(ssrc) == true) + if (stream?.GetRTPChannel()?.RTPPort == port) { - return audioStream; - } - else if (audioStream?.LocalTrack?.IsSsrcMatch(ssrc) == true) - { - return audioStream; + return stream; } } - foreach (var videoStream in VideoStreamList) - { - if (videoStream?.RemoteTrack?.IsSsrcMatch(ssrc) == true) - { - return videoStream; - } - else if (videoStream?.LocalTrack?.IsSsrcMatch(ssrc) == true) - { - return videoStream; - } - } + return null; + } + } - foreach (var textStream in TextStreamList) + private MediaStream? GetMediaStream(uint ssrc) + { + return + GetMediaStreamBySsrcCore(AudioStreamList, ssrc) + ?? GetMediaStreamBySsrcCore(VideoStreamList, ssrc) + ?? GetMediaStreamBySsrcCore(TextStreamList, ssrc); + + static MediaStream? GetMediaStreamBySsrcCore(List streams, uint ssrc) where TStream : MediaStream + { + foreach (var stream in streams) { - if (textStream?.RemoteTrack?.IsSsrcMatch(ssrc) == true) + if (stream?.RemoteTrack?.IsSsrcMatch(ssrc) == true) { - return textStream; + return stream; } - else if (textStream?.LocalTrack?.IsSsrcMatch(ssrc) == true) + if (stream?.LocalTrack?.IsSsrcMatch(ssrc) == true) { - return textStream; + return stream; } } return null; } + } - /// - /// Attempts to get MediaStream that matches a received RTCP report. - /// - /// The RTCP compound packet received from the remote party. - /// If a match could be found an SSRC the MediaStream otherwise null. - private MediaStream GetMediaStream(RTCPCompoundPacket rtcpPkt) + /// + /// Attempts to get MediaStream that matches a received RTCP report. + /// + /// The RTCP compound packet received from the remote party. + /// If a match could be found an SSRC the MediaStream otherwise null. + private MediaStream? GetMediaStream(RTCPCompoundPacket rtcpPkt) + { + if (rtcpPkt.SenderReport is { } senderReport) { - if (rtcpPkt.SenderReport != null) - { - return GetMediaStream(rtcpPkt.SenderReport.SSRC); - } - else if (rtcpPkt.ReceiverReport is { } receiverReport) - { - if (GetMediaStream(receiverReport.SSRC) is { } mediaStream) - { - return mediaStream; - } - } - else if (rtcpPkt.Feedback is { } feedback) + return GetMediaStream(senderReport.SSRC); + } + else if (rtcpPkt.ReceiverReport is { } receiverReport) + { + if (GetMediaStream(receiverReport.SSRC) is { } mediaStream) { - if (GetMediaStream(feedback.SenderSSRC) is { } mediaStream) - { - return mediaStream; - } + return mediaStream; } - else if (rtcpPkt.TWCCFeedback != null) + } + else if (rtcpPkt.Feedback is { } feedback) + { + if (GetMediaStream(feedback.SenderSSRC) is { } mediaStream) { - return GetMediaStream(rtcpPkt.TWCCFeedback.MediaSSRC); + return mediaStream; } + } + else if (rtcpPkt.TWCCFeedback is { }) + { + return GetMediaStream(rtcpPkt.TWCCFeedback.MediaSSRC); + } - // No match on SR/RR SSRC. Check the individual reception reports for a known SSRC. - List receptionReports = null; + // No match on SR/RR SSRC. Check the individual reception reports for a known SSRC. + List? receptionReports = null; - if (rtcpPkt.SenderReport != null) - { - receptionReports = rtcpPkt.SenderReport.ReceptionReports; - } - else if (rtcpPkt.ReceiverReport != null) - { - receptionReports = rtcpPkt.ReceiverReport.ReceptionReports; - } + if (rtcpPkt.SenderReport is { }) + { + receptionReports = rtcpPkt.SenderReport.ReceptionReports; + } + else if (rtcpPkt.ReceiverReport is { }) + { + receptionReports = rtcpPkt.ReceiverReport.ReceptionReports; + } - if (receptionReports != null && receptionReports.Count > 0) + if (receptionReports is { } && receptionReports.Count > 0) + { + foreach (var recRep in receptionReports) { - foreach (var recRep in receptionReports) + var mediaStream = GetMediaStream(recRep.SSRC); + if (mediaStream is { }) { - var mediaStream = GetMediaStream(recRep.SSRC); - if (mediaStream != null) - { - return mediaStream; - } + return mediaStream; } } - - return null; } - /// - /// Allows additional control for sending raw RTP payloads (on the primary one). No framing or other processing is carried out. - /// - /// The media type of the RTP packet being sent. Must be audio or video. - /// The RTP packet payload. - /// The timestamp to set on the RTP header. - /// The value to set on the RTP header marker bit, should be 0 or 1. - /// The payload ID to set in the RTP header. - /// The sequence number of the packet. - public void SendRtpRaw(SDPMediaTypesEnum mediaType, byte[] payload, uint timestamp, int markerBit, int payloadTypeID, ushort seqNum) + return null; + } + + /// + /// Allows additional control for sending raw RTP payloads (on the primary one). No framing or other processing is carried out. + /// + /// The media type of the RTP packet being sent. Must be audio or video. + /// The RTP packet payload. + /// The timestamp to set on the RTP header. + /// The value to set on the RTP header marker bit, should be 0 or 1. + /// The payload ID to set in the RTP header. + /// The sequence number of the packet. + public void SendRtpRaw(SDPMediaTypesEnum mediaType, ReadOnlySpan payload, uint timestamp, int markerBit, int payloadTypeID, ushort seqNum) + { + switch (mediaType) { - if (mediaType == SDPMediaTypesEnum.audio) - { + case SDPMediaTypesEnum.audio: + Debug.Assert(AudioStream is { }); AudioStream.SendRtpRaw(payload, timestamp, markerBit, payloadTypeID, seqNum); - } - else if (mediaType == SDPMediaTypesEnum.video) - { - VideoStream?.SendRtpRaw(payload, timestamp, markerBit, payloadTypeID, seqNum); - } - else if (mediaType == SDPMediaTypesEnum.text) - { - TextStream?.SendRtpRaw(payload, timestamp, markerBit, payloadTypeID, seqNum); - } + break; + case SDPMediaTypesEnum.video: + Debug.Assert(VideoStream is { }); + VideoStream.SendRtpRaw(payload, timestamp, markerBit, payloadTypeID, seqNum); + break; + case SDPMediaTypesEnum.text: + Debug.Assert(TextStream is { }); + TextStream.SendRtpRaw(payload, timestamp, markerBit, payloadTypeID, seqNum); + break; } + } - /// - /// Allows additional control for sending raw RTP payloads (on the primary one). No framing or other processing is carried out. - /// - /// The media type of the RTP packet being sent. Must be audio or video. - /// The RTP packet payload. - /// The timestamp to set on the RTP header. - /// The value to set on the RTP header marker bit, should be 0 or 1. - /// The payload ID to set in the RTP header. - public void SendRtpRaw(SDPMediaTypesEnum mediaType, byte[] payload, uint timestamp, int markerBit, int payloadTypeID) + /// + /// Allows additional control for sending raw RTP payloads (on the primary one). No framing or other processing is carried out. + /// + /// The media type of the RTP packet being sent. Must be audio or video. + /// The RTP packet payload. + /// The timestamp to set on the RTP header. + /// The value to set on the RTP header marker bit, should be 0 or 1. + /// The payload ID to set in the RTP header. + public void SendRtpRaw(SDPMediaTypesEnum mediaType, ReadOnlySpan payload, uint timestamp, int markerBit, int payloadTypeID) + { + switch (mediaType) { - if (mediaType == SDPMediaTypesEnum.audio) - { + case SDPMediaTypesEnum.audio: + Debug.Assert(AudioStream is { }); AudioStream.SendRtpRaw(payload, timestamp, markerBit, payloadTypeID); - } - else if (mediaType == SDPMediaTypesEnum.video) - { - VideoStream?.SendRtpRaw(payload, timestamp, markerBit, payloadTypeID); - } - else if (mediaType == SDPMediaTypesEnum.text) - { - TextStream?.SendRtpRaw(payload, timestamp, markerBit, payloadTypeID); - } + break; + case SDPMediaTypesEnum.video: + Debug.Assert(VideoStream is { }); + VideoStream.SendRtpRaw(payload, timestamp, markerBit, payloadTypeID); + break; + case SDPMediaTypesEnum.text: + Debug.Assert(TextStream is { }); + TextStream.SendRtpRaw(payload, timestamp, markerBit, payloadTypeID); + break; } + } - /// - /// Allows additional control for sending raw RTCP payloads (on the primary one). - /// - /// The media type of the RTCP packet being sent. Must be audio or video. - /// The RTCP packet payload. - public void SendRtcpRaw(SDPMediaTypesEnum mediaType, byte[] payload) + /// + /// Allows additional control for sending raw RTCP payloads (on the primary one). + /// + /// The media type of the RTCP packet being sent. Must be audio or video. + /// The RTCP packet payload. + public void SendRtcpRaw(SDPMediaTypesEnum mediaType, ReadOnlySpan payload) + { + switch (mediaType) { - if (mediaType == SDPMediaTypesEnum.audio) - { + case SDPMediaTypesEnum.audio: + Debug.Assert(AudioStream is { }); AudioStream.SendRtcpRaw(payload); - } - else if (mediaType == SDPMediaTypesEnum.video) - { - VideoStream?.SendRtcpRaw(payload); - } - else if (mediaType == SDPMediaTypesEnum.text) - { - TextStream?.SendRtcpRaw(payload); - } + break; + case SDPMediaTypesEnum.video: + Debug.Assert(VideoStream is { }); + VideoStream.SendRtcpRaw(payload); + break; + case SDPMediaTypesEnum.text: + Debug.Assert(TextStream is { }); + TextStream.SendRtcpRaw(payload); + break; } + } - /// - /// Sets the remote end points for a media type supported by this RTP session. (on the primary one) - /// - /// The media type, must be audio or video, to set the remote end point for. - /// The remote end point for RTP packets corresponding to the media type. - /// The remote end point for RTCP packets corresponding to the media type. - public void SetDestination(SDPMediaTypesEnum mediaType, IPEndPoint rtpEndPoint, IPEndPoint rtcpEndPoint) + /// + /// Sets the remote end points for a media type supported by this RTP session. (on the primary one) + /// + /// The media type, must be audio or video, to set the remote end point for. + /// The remote end point for RTP packets corresponding to the media type. + /// The remote end point for RTCP packets corresponding to the media type. + public void SetDestination(SDPMediaTypesEnum mediaType, IPEndPoint rtpEndPoint, IPEndPoint rtcpEndPoint) + { + if (rtpSessionConfig.IsMediaMultiplexed) { - if (rtpSessionConfig.IsMediaMultiplexed) - { - SetGlobalDestination(rtpEndPoint, rtcpEndPoint); - } - else - { - if (mediaType == SDPMediaTypesEnum.audio) - { - AudioStream.SetDestination(rtpEndPoint, rtcpEndPoint); - } - else if (mediaType == SDPMediaTypesEnum.video) - { - VideoStream?.SetDestination(rtpEndPoint, rtcpEndPoint); - } - else if (mediaType == SDPMediaTypesEnum.text) - { - TextStream?.SetDestination(rtpEndPoint, rtcpEndPoint); - } - } + SetGlobalDestination(rtpEndPoint, rtcpEndPoint); } - - /// - /// Allows sending of RTCP feedback reports (on the primary one) - /// - /// The media type of the RTCP report being sent. Must be audio or video. - /// The feedback report to send. - public void SendRtcpFeedback(SDPMediaTypesEnum mediaType, RTCPFeedback feedback) + else { if (mediaType == SDPMediaTypesEnum.audio) { - AudioStream.SendRtcpFeedback(feedback); + Debug.Assert(AudioStream is { }); + AudioStream.SetDestination(rtpEndPoint, rtcpEndPoint); } else if (mediaType == SDPMediaTypesEnum.video) { - VideoStream?.SendRtcpFeedback(feedback); + Debug.Assert(VideoStream is { }); + VideoStream.SetDestination(rtpEndPoint, rtcpEndPoint); } else if (mediaType == SDPMediaTypesEnum.text) { - TextStream?.SendRtcpFeedback(feedback); + Debug.Assert(TextStream is { }); + TextStream.SetDestination(rtpEndPoint, rtcpEndPoint); } } + } - /// - /// Allows sending of RTCP TWCC feedback reports (on the primary one) - /// - /// The media type of the RTCP report being sent. Must be audio or video. - /// The feedback report to send. - public void SendRtcpTWCCFeedback(SDPMediaTypesEnum mediaType, RTCPTWCCFeedback feedback) + /// + /// Allows sending of RTCP feedback reports (on the primary one) + /// + /// The media type of the RTCP report being sent. Must be audio or video. + /// The feedback report to send. + public void SendRtcpFeedback(SDPMediaTypesEnum mediaType, RTCPFeedback feedback) + { + if (mediaType == SDPMediaTypesEnum.audio) { - if (mediaType == SDPMediaTypesEnum.audio) - { - AudioStream.SendRtcpTWCCFeedback(feedback); - } - else if (mediaType == SDPMediaTypesEnum.video) - { - VideoStream?.SendRtcpTWCCFeedback(feedback); - } - else if (mediaType == SDPMediaTypesEnum.text) - { - TextStream?.SendRtcpTWCCFeedback(feedback); - } + Debug.Assert(AudioStream is { }); + AudioStream.SendRtcpFeedback(feedback); } - - /// - /// Sends the RTCP report to the remote call party. (on the primary one) - /// - /// RTCP report to send. - public void SendRtcpReport(SDPMediaTypesEnum mediaType, RTCPCompoundPacket report) + else if (mediaType == SDPMediaTypesEnum.video) { - if (mediaType == SDPMediaTypesEnum.audio) - { - AudioStream.SendRtcpReport(report); - } - else if (mediaType == SDPMediaTypesEnum.video) - { - VideoStream?.SendRtcpReport(report); - } - else if (mediaType == SDPMediaTypesEnum.text) - { - TextStream?.SendRtcpReport(report); - } + Debug.Assert(VideoStream is { }); + VideoStream.SendRtcpFeedback(feedback); } - - /// - /// Event handler for the RTP channel closure. - /// - private void OnRTPChannelClosed(string reason) + else if (mediaType == SDPMediaTypesEnum.text) { - Close(reason); + Debug.Assert(TextStream is { }); + TextStream.SendRtcpFeedback(feedback); } + } - /// - /// Close the session if the instance is out of scope. - /// - protected virtual void Dispose(bool disposing) + /// + /// Allows sending of RTCP TWCC feedback reports (on the primary one) + /// + /// The media type of the RTCP report being sent. Must be audio or video. + /// The feedback report to send. + public void SendRtcpTWCCFeedback(SDPMediaTypesEnum mediaType, RTCPTWCCFeedback feedback) + { + if (mediaType == SDPMediaTypesEnum.audio) + { + Debug.Assert(AudioStream is { }); + AudioStream.SendRtcpTWCCFeedback(feedback); + } + else if (mediaType == SDPMediaTypesEnum.video) + { + Debug.Assert(VideoStream is { }); + VideoStream.SendRtcpTWCCFeedback(feedback); + } + else if (mediaType == SDPMediaTypesEnum.text) { - Close("disposed"); + Debug.Assert(TextStream is { }); + TextStream.SendRtcpTWCCFeedback(feedback); } + } - /// - /// Close the session if the instance is out of scope. - /// - public virtual void Dispose() + /// + /// Sends the RTCP report to the remote call party. (on the primary one) + /// + /// RTCP report to send. + public void SendRtcpReport(SDPMediaTypesEnum mediaType, RTCPCompoundPacket report) + { + if (mediaType == SDPMediaTypesEnum.audio) + { + Debug.Assert(AudioStream is { }); + AudioStream.SendRtcpReport(report); + } + else if (mediaType == SDPMediaTypesEnum.video) + { + Debug.Assert(VideoStream is { }); + VideoStream.SendRtcpReport(report); + } + else if (mediaType == SDPMediaTypesEnum.text) { - Close("disposed"); + Debug.Assert(TextStream is { }); + TextStream.SendRtcpReport(report); } } + + /// + /// Event handler for the RTP channel closure. + /// + private void OnRTPChannelClosed(string reason) + { + Close(reason); + } + + /// + /// Close the session if the instance is out of scope. + /// + protected virtual void Dispose(bool disposing) + { + Close("disposed"); + } + + /// + /// Close the session if the instance is out of scope. + /// + public virtual void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } } diff --git a/src/SIPSorcery/net/RTP/RTPSessionConfig.cs b/src/SIPSorcery/net/RTP/RTPSessionConfig.cs index a9e2dd17f7..0e7ec02293 100644 --- a/src/SIPSorcery/net/RTP/RTPSessionConfig.cs +++ b/src/SIPSorcery/net/RTP/RTPSessionConfig.cs @@ -17,69 +17,68 @@ using System.Net; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public enum RtpSecureMediaOptionEnum { - public enum RtpSecureMediaOptionEnum - { - /// - /// Secure media not used. - /// - None, + /// + /// Secure media not used. + /// + None, - /// - /// Secure media controled by DtlsSrtp for WebRTC. - /// - DtlsSrtp, + /// + /// Secure media controled by DtlsSrtp for WebRTC. + /// + DtlsSrtp, - /// - /// Secure media negotiated with SDP crypto attributes. - /// - SdpCryptoNegotiation, - } + /// + /// Secure media negotiated with SDP crypto attributes. + /// + SdpCryptoNegotiation, +} - public sealed class RtpSessionConfig - { - /// - /// If true only a single RTP socket will be used for both audio - /// and video (standard case for WebRTC). If false two separate RTP sockets will be used for - /// audio and video (standard case for VoIP). - /// - public bool IsMediaMultiplexed { get; set; } +public sealed class RtpSessionConfig +{ + /// + /// If true only a single RTP socket will be used for both audio + /// and video (standard case for WebRTC). If false two separate RTP sockets will be used for + /// audio and video (standard case for VoIP). + /// + public bool IsMediaMultiplexed { get; set; } - /// - /// If true RTCP reports will be multiplexed with RTP on a single channel. - /// If false (standard mode) then a separate socket is used to send and receive RTCP reports. - /// - public bool IsRtcpMultiplexed { get; set; } + /// + /// If true RTCP reports will be multiplexed with RTP on a single channel. + /// If false (standard mode) then a separate socket is used to send and receive RTCP reports. + /// + public bool IsRtcpMultiplexed { get; set; } - /// - /// Select type of secure media to use. - /// - public RtpSecureMediaOptionEnum RtpSecureMediaOption { get; set; } - - /// - /// Optional. If specified this address will be used as the bind address for any RTP - /// and control sockets created. Generally this address does not need to be set. The default behaviour - /// is to bind to [::] or 0.0.0.0,d depending on system support, which minimises network routing - /// causing connection issues. - /// - public IPAddress BindAddress { get; set; } - - /// - /// Optional. If specified a single attempt will be made to bind the RTP socket - /// on this port. It's recommended to leave this parameter as the default of 0 to let the Operating - /// System select the port number. - /// - public int BindPort { get; set; } + /// + /// Select type of secure media to use. + /// + public RtpSecureMediaOptionEnum RtpSecureMediaOption { get; set; } + + /// + /// Optional. If specified this address will be used as the bind address for any RTP + /// and control sockets created. Generally this address does not need to be set. The default behaviour + /// is to bind to [::] or 0.0.0.0,d depending on system support, which minimises network routing + /// causing connection issues. + /// + public IPAddress? BindAddress { get; set; } + + /// + /// Optional. If specified a single attempt will be made to bind the RTP socket + /// on this port. It's recommended to leave this parameter as the default of 0 to let the Operating + /// System select the port number. + /// + public int BindPort { get; set; } - /// - /// Optional. If specified, overwrites BindPort and calls the PortRange whenever an RTP-Port - /// should be created. - /// - public PortRange RtpPortRange { get; set; } + /// + /// Optional. If specified, overwrites BindPort and calls the PortRange whenever an RTP-Port + /// should be created. + /// + public PortRange? RtpPortRange { get; set; } - public bool IsSecure { get => RtpSecureMediaOption == RtpSecureMediaOptionEnum.DtlsSrtp; } + public bool IsSecure { get => RtpSecureMediaOption == RtpSecureMediaOptionEnum.DtlsSrtp; } - public bool UseSdpCryptoNegotiation { get => RtpSecureMediaOption == RtpSecureMediaOptionEnum.SdpCryptoNegotiation; } - } + public bool UseSdpCryptoNegotiation { get => RtpSecureMediaOption == RtpSecureMediaOptionEnum.SdpCryptoNegotiation; } } diff --git a/src/SIPSorcery/net/RTP/Streams/AudioStream.cs b/src/SIPSorcery/net/RTP/Streams/AudioStream.cs index 7b40b65618..eb7527a4d0 100644 --- a/src/SIPSorcery/net/RTP/Streams/AudioStream.cs +++ b/src/SIPSorcery/net/RTP/Streams/AudioStream.cs @@ -16,7 +16,7 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Threading; @@ -25,301 +25,398 @@ using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class AudioStream : MediaStream { - public class AudioStream : MediaStream - { - private const uint DEFAULT_AUDIO_SAMPLE_DURATION_MILLISECONDS = 20; + private const uint DEFAULT_AUDIO_SAMPLE_DURATION_MILLISECONDS = 20; - protected static readonly ILogger logger = LogFactory.CreateLogger(); - protected bool rtpEventInProgress = false; + protected static readonly ILogger logger = LogFactory.CreateLogger(); + protected bool rtpEventInProgress; - private bool sendingFormatFound = false; + private bool sendingFormatFound; - private bool _rtpPreviousTimestampSet = false; + private bool _rtpPreviousTimestampSet; - /// - /// The RTP timestamp for the previously received RTP packet. Used to calculate the - /// duration of the RTP packet in RTP timestamp units. - /// - private uint _rtpPreviousTimestamp = 0; + /// + /// The RTP timestamp for the previously received RTP packet. Used to calculate the + /// duration of the RTP packet in RTP timestamp units. + /// + private uint _rtpPreviousTimestamp; - /// - /// The audio format negotiated for the audio stream by the SDP offer/answer exchange. - /// - public SDPAudioVideoMediaFormat NegotiatedFormat { get; private set; } + /// + /// The audio format negotiated for the audio stream by the SDP offer/answer exchange. + /// + public SDPAudioVideoMediaFormat NegotiatedFormat { get; private set; } - /// - /// Gets fired when the remote SDP is received and the set of common audio formats is set. - /// - public event Action> OnAudioFormatsNegotiatedByIndex; + /// + /// Gets fired when the remote SDP is received and the set of common audio formats is set. + /// + public event Action>? OnAudioFormatsNegotiatedByIndex; - public event Action OnAudioFrameReceived; + public event Action? OnAudioFrameReceived; - /// - /// Indicates whether this session is using audio. - /// - public bool HasAudio + /// + /// Indicates whether this session is using audio. + /// + public bool HasAudio + { + get { - get - { - return (LocalTrack != null && LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) - || (RemoteTrack != null && RemoteTrack.StreamStatus != MediaStreamStatusEnum.Inactive); - } + return (LocalTrack is { } && LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) + || (RemoteTrack is { } && RemoteTrack.StreamStatus != MediaStreamStatusEnum.Inactive); } + } + + public AudioStream(RtpSessionConfig config, int index) : base(config, index) + { + MediaType = SDPMediaTypesEnum.audio; + } - public AudioStream(RtpSessionConfig config, int index) : base(config, index) + /// + /// Sends an audio sample to the remote peer. + /// + /// The duration in RTP timestamp units of the audio sample. This + /// value is added to the previous RTP timestamp when building the RTP header. + /// The audio sample to set as the RTP packet payload. + public void SendAudio(uint durationRtpUnits, ReadOnlySpan sample) + { + if (!sendingFormatFound) { - MediaType = SDPMediaTypesEnum.audio; + NegotiatedFormat = GetSendingFormat(); + sendingFormatFound = true; } + SendAudioFrame(durationRtpUnits, NegotiatedFormat.ID, sample); + } + + /// + /// Sends an audio sample to the remote peer. + /// + /// The duration in RTP timestamp units of the audio sample. This + /// value is added to the previous RTP timestamp when building the RTP header. + /// The audio sample to set as the RTP packet payload. + public void SendAudio(uint durationRtpUnits, byte[] sample) + { + SendAudio(durationRtpUnits, new ArraySegment(sample)); + } - /// - /// Sends an audio sample to the remote peer. - /// - /// The duration in RTP timestamp units of the audio sample. This - /// value is added to the previous RTP timestamp when building the RTP header. - /// The audio sample to set as the RTP packet payload. - public void SendAudio(uint durationRtpUnits, ArraySegment sample) + /// + /// Sends an encoded audio frame to the remote peer. + /// + /// The encoded audio frame containing the audio data, format, and duration information. + public void SendAudio(EncodedAudioFrame encodedAudioFrame) + { + if (encodedAudioFrame?.AudioFormat is null || encodedAudioFrame.AudioFormat.IsEmpty()) { - if (!sendingFormatFound) - { - NegotiatedFormat = GetSendingFormat(); - sendingFormatFound = true; - } - SendAudioFrame(durationRtpUnits, NegotiatedFormat.ID, sample); + throw new ArgumentException("EncodedAudioFrame must have a valid audio format.", nameof(encodedAudioFrame)); } - /// - /// Sends an audio sample to the remote peer. - /// - /// The duration in RTP timestamp units of the audio sample. This - /// value is added to the previous RTP timestamp when building the RTP header. - /// The audio sample to set as the RTP packet payload. - public void SendAudio(uint durationRtpUnits, byte[] sample) + // Convert duration from milliseconds to RTP timestamp units manually + // RTP timestamp units = milliseconds * (clock_rate / 1000) + var durationRtpUnits = (uint)Math.Round(encodedAudioFrame.DurationMilliSeconds * encodedAudioFrame.AudioFormat.RtpClockRate / 1000.0); + + // Get the format ID for the audio format from our capabilities + var format = GetSendingFormat(encodedAudioFrame.AudioFormat); + if (format.IsEmpty()) { - SendAudio(durationRtpUnits, new ArraySegment(sample)); + throw new InvalidOperationException($"Audio format {encodedAudioFrame.AudioFormat.Codec} is not supported or negotiated for sending."); } - /// - /// Sends an audio packet to the remote party. - /// - /// The duration of the audio payload in timestamp units. This value - /// gets added onto the timestamp being set in the RTP header. - /// The payload ID to set in the RTP header. - /// The audio payload to send. - public void SendAudioFrame(uint duration, int payloadTypeID, ArraySegment bufferSegment) + // Temporarily store the current negotiated format and set it to the frame's format + var previousNegotiatedFormat = NegotiatedFormat; + var previousSendingFormatFound = sendingFormatFound; + + try + { + NegotiatedFormat = format; + sendingFormatFound = true; + + // Use the existing SendAudio method with ReadOnlySpan + SendAudio(durationRtpUnits, encodedAudioFrame.EncodedAudio.Span); + } + finally + { + // Restore the previous state + NegotiatedFormat = previousNegotiatedFormat; + sendingFormatFound = previousSendingFormatFound; + } + } + + /// + /// Attempts to get the sending format that matches the specified audio format. + /// + /// The audio format to find a match for. + /// The compatible SDP media format for the specified audio format. + private SDPAudioVideoMediaFormat GetSendingFormat(AudioFormat audioFormat) + { + if (LocalTrack is null && RemoteTrack is null) { - if (CheckIfCanSendRtpRaw()) + throw new SipSorceryException($"Cannot get the {MediaType} sending format, missing both local and remote {MediaType} track."); + } + + var capabilities = (LocalTrack?.Capabilities ?? RemoteTrack?.Capabilities) ?? throw new SipSorceryException($"Cannot get the {MediaType} sending format, no capabilities available."); + + // Find a format that matches the audio format + foreach (var capability in capabilities) + { + var capabilityAudioFormat = capability.ToAudioFormat(); + if (!capabilityAudioFormat.IsEmpty() && capabilityAudioFormat.Codec == audioFormat.Codec) { - if (rtpEventInProgress) + // Check if clock rates match (if specified) + if (audioFormat.ClockRate == 0 || capabilityAudioFormat.ClockRate == audioFormat.ClockRate) { - //logger.LogWarning(nameof(SendAudioFrame) + " an RTPEvent is in progress."); - return; + return capability; } + } + } + + return SDPAudioVideoMediaFormat.Empty; + } + + /// + /// Sends an audio packet to the remote party. + /// + /// The duration of the audio payload in timestamp units. This value + /// gets added onto the timestamp being set in the RTP header. + /// The payload ID to set in the RTP header. + /// The audio payload to send. + public void SendAudioFrame(uint duration, int payloadTypeID, ReadOnlySpan sample) + { + if (CheckIfCanSendRtpRaw()) + { + if (rtpEventInProgress) + { + //logger.LogWarning(nameof(SendAudioFrame) + " an RTPEvent is in progress."); + return; + } - try + try + { + // Basic RTP audio formats (such as G711, G722) do not have a concept of frames. The payload of the RTP packet is + // considered a single frame. This results in a problem is the audio frame being sent is larger than the MTU. In + // that case the audio frame must be split across mutliple RTP packets. Unlike video frames there's no way to + // indicate that a series of RTP packets are correlated to the same timestamp. For that reason if an audio buffer + // is supplied that's larger than MTU it will be split and the timestamp will be adjusted to best fit each RTP + // payload. + // See https://github.com/sipsorcery/sipsorcery/issues/394. + + var maxPayload = RTPSession.RTP_MAX_PAYLOAD; + var totalPackets = (sample.Length + maxPayload - 1) / maxPayload; + + uint totalIncrement = 0; + Debug.Assert(LocalTrack is { }); + var startTimestamp = LocalTrack.Timestamp; // Keep track of where we started. + + for (var index = 0; index < totalPackets; index++) { - // Basic RTP audio formats (such as G711, G722) do not have a concept of frames. The payload of the RTP packet is - // considered a single frame. This results in a problem is the audio frame being sent is larger than the MTU. In - // that case the audio frame must be split across mutliple RTP packets. Unlike video frames there's no way to - // indicate that a series of RTP packets are correlated to the same timestamp. For that reason if an audio buffer - // is supplied that's larger than MTU it will be split and the timestamp will be adjusted to best fit each RTP - // payload. - // See https://github.com/sipsorcery/sipsorcery/issues/394. + var offset = index * maxPayload; + var payloadLength = Math.Min(maxPayload, sample.Length - offset); - int maxPayload = RTPSession.RTP_MAX_PAYLOAD; - int totalPackets = (bufferSegment.Count + maxPayload - 1) / maxPayload; + var fraction = (double)payloadLength / sample.Length; + var packetDuration = (uint)Math.Round(fraction * duration); - uint totalIncrement = 0; - uint startTimestamp = LocalTrack.Timestamp; // Keep track of where we started. + // RFC3551 specifies that for audio the marker bit should always be 0 except for when returning + // from silence suppression. For video the marker bit DOES get set to 1 for the last packet + // in a frame. + var markerBit = 0; - for (int index = 0; index < totalPackets; index++) - { - int offset = index * maxPayload; - int payloadLength = Math.Min(maxPayload, bufferSegment.Count - offset); - - double fraction = (double)payloadLength / bufferSegment.Count; - uint packetDuration = (uint)Math.Round(fraction * duration); - - // RFC3551 specifies that for audio the marker bit should always be 0 except for when returning - // from silence suppression. For video the marker bit DOES get set to 1 for the last packet - // in a frame. - int markerBit = 0; -#if NETCOREAPP2_1_OR_GREATER && !NETFRAMEWORK - var memorySegment = bufferSegment.Slice(offset, payloadLength); -#else - var memorySegment = new ArraySegment(bufferSegment.Array!, offset, payloadLength); -#endif - // Send this packet at the current LocalTrack.Timestamp - SendRtpRaw(memorySegment, LocalTrack.Timestamp, markerBit, payloadTypeID, true); - - // After sending, increment the timestamp by this packet's portion. - // This ensures the timestamp increments for the next packet, including the first one. - LocalTrack.Timestamp += packetDuration; - totalIncrement += packetDuration; - } + // Send this packet at the current LocalTrack.Timestamp + SendRtpRaw(sample.Slice(offset, payloadLength), LocalTrack.Timestamp, markerBit, payloadTypeID, true); - // After all packets are sent, correct if we haven't incremented exactly by `duration`. - if (totalIncrement != duration) - { - // Add or subtract the difference so total increment equals duration. - LocalTrack.Timestamp += (duration - totalIncrement); - } + // After sending, increment the timestamp by this packet's portion. + // This ensures the timestamp increments for the next packet, including the first one. + LocalTrack.Timestamp += packetDuration; + totalIncrement += packetDuration; } - catch (SocketException sockExcp) + + // After all packets are sent, correct if we haven't incremented exactly by `duration`. + if (totalIncrement != duration) { - logger.LogError(sockExcp, "SocketException SendAudioFrame. {ErrorMessage}", sockExcp.Message); + // Add or subtract the difference so total increment equals duration. + LocalTrack.Timestamp += (duration - totalIncrement); } } + catch (SocketException sockExcp) + { + logger.LogRtpSocketExceptionSendAudioFrame(sockExcp.Message, sockExcp); + } } + } - /// - /// Sends an audio packet to the remote party. - /// - /// The duration of the audio payload in timestamp units. This value - /// gets added onto the timestamp being set in the RTP header. - /// The payload ID to set in the RTP header. - /// The audio payload to send. - public void SendAudioFrame(uint duration, int payloadTypeID, byte[] buffer) - { - SendAudioFrame(duration, payloadTypeID, new ArraySegment(buffer)); - } + /// + /// Sends an audio packet to the remote party. + /// + /// The duration of the audio payload in timestamp units. This value + /// gets added onto the timestamp being set in the RTP header. + /// The payload ID to set in the RTP header. + /// The audio payload to send. + public void SendAudioFrame(uint duration, int payloadTypeID, byte[] buffer) + { + SendAudioFrame(duration, payloadTypeID, new ArraySegment(buffer)); + } - /// - /// Sends an RTP event for a DTMF tone as per RFC2833. Sending the event requires multiple packets to be sent. - /// This method will hold onto the socket until all the packets required for the event have been sent. The send - /// can be cancelled using the cancellation token. - /// - /// The RTP event to send. - /// CancellationToken to allow the operation to be cancelled prematurely. - /// To send an RTP event the clock rate of the underlying stream needs to be known. - /// The sample period in milliseconds being used for the media stream that the event - /// is being inserted into. Should be set to 50ms if main media stream is dynamic or sample period is unknown. - public async Task SendDtmfEvent(RTPEvent rtpEvent, CancellationToken cancellationToken, int clockRate = RTPSession.DEFAULT_AUDIO_CLOCK_RATE, int samplePeriod = RTPSession.RTP_EVENT_DEFAULT_SAMPLE_PERIOD_MS) + /// + /// Sends an RTP event for a DTMF tone as per RFC2833. Sending the event requires multiple packets to be sent. + /// This method will hold onto the socket until all the packets required for the event have been sent. The send + /// can be cancelled using the cancellation token. + /// + /// The RTP event to send. + /// CancellationToken to allow the operation to be cancelled prematurely. + /// To send an RTP event the clock rate of the underlying stream needs to be known. + /// The sample period in milliseconds being used for the media stream that the event + /// is being inserted into. Should be set to 50ms if main media stream is dynamic or sample period is unknown. + public async Task SendDtmfEvent(RTPEvent rtpEvent, CancellationToken cancellationToken, int clockRate = RTPSession.DEFAULT_AUDIO_CLOCK_RATE, int samplePeriod = RTPSession.RTP_EVENT_DEFAULT_SAMPLE_PERIOD_MS) + { + if (CheckIfCanSendRtpRaw()) { - if (CheckIfCanSendRtpRaw()) + if (rtpEventInProgress) { - if (rtpEventInProgress) - { - logger.LogWarning("{Method} an RTPEvent is already in progress.", nameof(SendDtmfEvent)); - return; - } + logger.LogDtmfEventInProgress(); + return; + } - try - { - rtpEventInProgress = true; - // The RTP timestamp step corresponding to the sampling period. This can change depending - // on the codec being used. For example using PCMU with a sampling frequency of 8000Hz and a sample period of 50ms - // the timestamp step is 400 (8000 / (1000 / 50)). For a sample period of 20ms it's 160 (8000 / (1000 / 20)). - ushort rtpTimestampStep = (ushort)(clockRate * samplePeriod / 1000); - - // If only the minimum number of packets are being sent then they are both the start and end of the event. - rtpEvent.EndOfEvent = (rtpEvent.TotalDuration <= rtpTimestampStep); - // The DTMF tone is generally multiple RTP events. Each event has a duration of the RTP timestamp step. - rtpEvent.Duration = rtpTimestampStep; - - // Send the start of event packets. - for (int i = 0; i < RTPEvent.DUPLICATE_COUNT && !cancellationToken.IsCancellationRequested; i++) - { - byte[] buffer = rtpEvent.GetEventPayload(); + try + { + rtpEventInProgress = true; - int markerBit = (i == 0) ? 1 : 0; // Set marker bit for the first packet in the event. + // The RTP timestamp step corresponding to the sampling period. This can change depending + // on the codec being used. For example using PCMU with a sampling frequency of 8000Hz and a sample period of 50ms + // the timestamp step is 400 (8000 / (1000 / 50)). For a sample period of 20ms it's 160 (8000 / (1000 / 20)). + var rtpTimestampStep = (ushort)(clockRate * samplePeriod / 1000); + // If only the minimum number of packets are being sent then they are both the start and end of the event. + rtpEvent.EndOfEvent = (rtpEvent.TotalDuration <= rtpTimestampStep); + // The DTMF tone is generally multiple RTP events. Each event has a duration of the RTP timestamp step. + rtpEvent.Duration = rtpTimestampStep; - SendRtpRaw(buffer, LocalTrack.Timestamp, markerBit, rtpEvent.PayloadTypeID, true); - } + SendStartOfEventPackets(rtpEvent, cancellationToken); - await Task.Delay(samplePeriod, cancellationToken).ConfigureAwait(false); + await Task.Delay(samplePeriod, cancellationToken).ConfigureAwait(false); - if (!rtpEvent.EndOfEvent) + if (!rtpEvent.EndOfEvent) + { + // Send the progressive event packets + while ((rtpEvent.Duration + rtpTimestampStep) < rtpEvent.TotalDuration && !cancellationToken.IsCancellationRequested) { - // Send the progressive event packets - while ((rtpEvent.Duration + rtpTimestampStep) < rtpEvent.TotalDuration && !cancellationToken.IsCancellationRequested) - { - rtpEvent.Duration += rtpTimestampStep; - byte[] buffer = rtpEvent.GetEventPayload(); - - SendRtpRaw(buffer, LocalTrack.Timestamp, 0, rtpEvent.PayloadTypeID, true); - - await Task.Delay(samplePeriod, cancellationToken).ConfigureAwait(false); - } - - // Send the end of event packets. - for (int j = 0; j < RTPEvent.DUPLICATE_COUNT && !cancellationToken.IsCancellationRequested; j++) - { - rtpEvent.EndOfEvent = true; - rtpEvent.Duration = rtpEvent.TotalDuration; - byte[] buffer = rtpEvent.GetEventPayload(); - - SendRtpRaw(buffer, LocalTrack.Timestamp, 0, rtpEvent.PayloadTypeID, true); - } + SendProgressiveEventPacket(rtpEvent, rtpTimestampStep); + + await Task.Delay(samplePeriod, cancellationToken).ConfigureAwait(false); } - LocalTrack.Timestamp += rtpEvent.TotalDuration; - } - catch (SocketException sockExcp) - { - logger.LogError(sockExcp, "SocketException SendDtmfEvent. {ErrorMessage}", sockExcp.Message); - } - catch (TaskCanceledException) - { - logger.LogWarning("SendDtmfEvent was cancelled by caller."); - } - finally - { - rtpEventInProgress = false; + + SendEndOfEventPackets(rtpEvent, cancellationToken); } + + Debug.Assert(LocalTrack is { }); + LocalTrack.Timestamp += rtpEvent.TotalDuration; + } + catch (SocketException sockExcp) + { + logger.LogRtpSocketExceptionSendDtmfEvent(sockExcp.Message, sockExcp); + } + catch (TaskCanceledException) + { + logger.LogDtmfEventCancelled(); + } + finally + { + rtpEventInProgress = false; } } - /// - /// Sends a DTMF tone as an RTP event to the remote party. - /// - /// The DTMF tone to send. - /// RTP events can span multiple RTP packets. This token can - /// be used to cancel the send. - public virtual Task SendDtmf(byte key, CancellationToken ct) + void SendStartOfEventPackets(RTPEvent rtpEvent, CancellationToken cancellationToken) { - var dtmfEvent = new RTPEvent(key, false, RTPEvent.DEFAULT_VOLUME, RTPSession.DTMF_EVENT_DURATION, NegotiatedRtpEventPayloadID); - return SendDtmfEvent(dtmfEvent, ct); + Span buffer = stackalloc byte[RTPEvent.DTMF_PACKET_LENGTH]; + for (var i = 0; i < RTPEvent.DUPLICATE_COUNT && !cancellationToken.IsCancellationRequested; i++) + { + rtpEvent.WriteEventPayload(buffer); + var markerBit = (i == 0) ? 1 : 0; + Debug.Assert(LocalTrack is { }); + SendRtpRaw(buffer, LocalTrack.Timestamp, markerBit, rtpEvent.PayloadTypeID, true); + } } - public void CheckAudioFormatsNegotiation() + void SendProgressiveEventPacket(RTPEvent rtpEvent, ushort rtpTimestampStep) { - if (LocalTrack != null && - LocalTrack.Capabilities.Where(x => !string.Equals(x.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)).Count() > 0) + Span buffer = stackalloc byte[RTPEvent.DTMF_PACKET_LENGTH]; + rtpEvent.Duration += rtpTimestampStep; + rtpEvent.WriteEventPayload(buffer); + Debug.Assert(LocalTrack is { }); + SendRtpRaw(buffer, LocalTrack.Timestamp, 0, rtpEvent.PayloadTypeID, true); + } + + void SendEndOfEventPackets(RTPEvent rtpEvent, CancellationToken cancellationToken) + { + Span buffer = stackalloc byte[RTPEvent.DTMF_PACKET_LENGTH]; + for (var j = 0; j < RTPEvent.DUPLICATE_COUNT && !cancellationToken.IsCancellationRequested; j++) { - OnAudioFormatsNegotiatedByIndex?.Invoke( - Index, - LocalTrack.Capabilities - .Where(x => !string.Equals(x.Name(), SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.ToAudioFormat()).ToList()); + rtpEvent.EndOfEvent = true; + rtpEvent.Duration = rtpEvent.TotalDuration; + rtpEvent.WriteEventPayload(buffer); + Debug.Assert(LocalTrack is { }); + SendRtpRaw(buffer, LocalTrack.Timestamp, 0, rtpEvent.PayloadTypeID, true); } } + } - protected override void ProcessRtpPacket(IPEndPoint remoteEndPoint, RTPPacket rtpPacket, SDPAudioVideoMediaFormat format) - { - //logger.LogDebug("AudioStream ProcessRtpPacket from {ep}, ssrc {ssrc}, seqnum {seqnum}.", remoteEndPoint, rtpPacket.Header.SyncSource, rtpPacket.Header.SequenceNumber); + /// + /// Sends a DTMF tone as an RTP event to the remote party. + /// + /// The DTMF tone to send. + /// RTP events can span multiple RTP packets. This token can + /// be used to cancel the send. + public virtual Task SendDtmf(byte key, CancellationToken ct) + { + var dtmfEvent = new RTPEvent(key, false, RTPEvent.DEFAULT_VOLUME, RTPSession.DTMF_EVENT_DURATION, NegotiatedRtpEventPayloadID); + return SendDtmfEvent(dtmfEvent, ct); + } - var audioFormat = format.ToAudioFormat(); + public void CheckAudioFormatsNegotiation() + { + if (OnAudioFormatsNegotiatedByIndex is not { } onAudioFormatsNegotiatedByIndex + || LocalTrack?.Capabilities is not { Count: > 0 } capabilities) + { + return; + } - if (!audioFormat.IsEmpty()) + var audioFormats = new List(capabilities.Count); + foreach (var capability in capabilities) + { + var name = capability.Name(); + if (!string.Equals(name, SDP.TELEPHONE_EVENT_ATTRIBUTE, StringComparison.CurrentCultureIgnoreCase)) { - uint durationMilliseconds = _rtpPreviousTimestampSet switch - { - true => RtpTimestampExtensions.ToDurationMillisecondsInt(rtpPacket.Header.GetTimestampDelta(_rtpPreviousTimestamp), audioFormat.RtpClockRate), - false => DEFAULT_AUDIO_SAMPLE_DURATION_MILLISECONDS - }; - - OnAudioFrameReceived?.Invoke(new EncodedAudioFrame(Index, format.ToAudioFormat(), durationMilliseconds, rtpPacket.Payload)); + audioFormats.Add(capability.ToAudioFormat()); } + } - RaiseOnRtpPacketReceivedByIndex(remoteEndPoint, rtpPacket); + if (audioFormats.Count == 0) + { + return; + } - _rtpPreviousTimestamp = rtpPacket.Header.Timestamp; + onAudioFormatsNegotiatedByIndex(Index, audioFormats); + } - if (!_rtpPreviousTimestampSet) + protected override void ProcessRtpPacket(IPEndPoint remoteEndPoint, RTPPacket rtpPacket, SDPAudioVideoMediaFormat format) + { + var audioFormat = format.ToAudioFormat(); + + if (!audioFormat.IsEmpty()) + { + var durationMilliseconds = _rtpPreviousTimestampSet switch { - _rtpPreviousTimestampSet = true; - } + true => RtpTimestampExtensions.ToDurationMillisecondsInt(rtpPacket.Header.GetTimestampDelta(_rtpPreviousTimestamp), audioFormat.RtpClockRate), + false => DEFAULT_AUDIO_SAMPLE_DURATION_MILLISECONDS + }; + + OnAudioFrameReceived?.Invoke(new EncodedAudioFrame(Index, format.ToAudioFormat(), durationMilliseconds, rtpPacket.Payload)); + } + + RaiseOnRtpPacketReceivedByIndex(remoteEndPoint, rtpPacket); + + _rtpPreviousTimestamp = rtpPacket.Header.Timestamp; + + if (!_rtpPreviousTimestampSet) + { + _rtpPreviousTimestampSet = true; } } } diff --git a/src/SIPSorcery/net/RTP/Streams/MediaStream.cs b/src/SIPSorcery/net/RTP/Streams/MediaStream.cs index 4b6d9ffeb0..3531400f93 100644 --- a/src/SIPSorcery/net/RTP/Streams/MediaStream.cs +++ b/src/SIPSorcery/net/RTP/Streams/MediaStream.cs @@ -15,804 +15,785 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Net; +using System.Runtime.InteropServices; +using CommunityToolkit.HighPerformance.Buffers; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class MediaStream { - public class MediaStream + protected internal sealed class PendingPackages { - protected internal class PendingPackages - { - public RTPHeader hdr; - public int localPort; - public IPEndPoint remoteEndPoint; - public byte[] buffer; + public RTPHeader hdr { get; } + public int localPort { get; } + public IPEndPoint remoteEndPoint { get; } + public byte[] buffer { get; } - public PendingPackages() { } + public PendingPackages(RTPHeader hdr, int localPort, IPEndPoint remoteEndPoint, byte[] buffer) + { + this.hdr = hdr; + this.localPort = localPort; + this.remoteEndPoint = remoteEndPoint; + this.buffer = buffer; + } - public PendingPackages(RTPHeader hdr, int localPort, IPEndPoint remoteEndPoint, byte[] buffer) - { - this.hdr = hdr; - this.localPort = localPort; - this.remoteEndPoint = remoteEndPoint; - this.buffer = buffer; - } + public PendingPackages(RTPHeader hdr, int localPort, IPEndPoint remoteEndPoint, ReadOnlySpan buffer) + : this(hdr, localPort, remoteEndPoint, buffer.ToArray()) + { } + } - protected object _pendingPackagesLock = new object(); - protected List _pendingPackagesBuffer = new List(); - - private static readonly ILogger logger = LogFactory.CreateLogger(); - - private RtpSessionConfig RtpSessionConfig; - - protected SecureContext SecureContext; - protected SrtpHandler SrtpHandler; - - private RTPReorderBuffer RTPReorderBuffer = null; - - MediaStreamTrack m_localTrack; - MediaStreamTrack m_remoteTrack; - - protected RTPChannel rtpChannel = null; - - protected bool _isClosed = false; - /// - /// Used for keeping track of TWCC packets - /// - private ushort _twccPacketCount = 0; - - public int Index = -1; - - /// - /// Tracks the global order in which this stream was added to the session, - /// across all media types. Used to preserve m-line ordering per RFC 3264 §8. - /// - public int MediaInsertionOrder = -1; - - /// - /// Fires when the connection for a media type is classified as timed out due to not - /// receiving any RTP or RTCP packets within the given period. - /// - public event Action OnTimeoutByIndex; - - /// - /// Gets fired when an RTCP report is sent. This event is for diagnostics only. - /// - public event Action OnSendReportByIndex; - - /// - /// Gets fired when an RTP packet is received from a remote party. - /// Parameters are: - /// - index of the AudioStream or VideoStream - /// - Remote endpoint packet was received from, - /// - The media type the packet contains, will be audio or video, - /// - The full RTP packet. - /// - public event Action OnRtpPacketReceivedByIndex; - - /// - /// Gets fired when an RTP Header packet is received from a remote party. - /// Parameters are: - /// - index of the AudioStream or VideoStream - /// - Remote endpoint packet was received from, - /// - The media type the packet contains, will be audio or video, - /// - The RTP Header exension URI. - /// - public event Action OnRtpHeaderReceivedByIndex; - - /// - /// Gets fired when an RTP event is detected on the remote call party's RTP stream. - /// - public event Action OnRtpEventByIndex; - - /// - /// Gets fired when an RTCP report is received. This event is for diagnostics only. - /// - public event Action OnReceiveReportByIndex; - - public event Action OnIsClosedStateChanged; - - public bool AcceptRtpFromAny { get; set; } = false; - - /// - /// Indicates whether the session has been closed. Once a session is closed it cannot - /// be restarted. - /// - public bool IsClosed + protected object _pendingPackagesLock = new object(); + protected List _pendingPackagesBuffer = new List(); + + private static readonly ILogger logger = LogFactory.CreateLogger(); + + private RtpSessionConfig RtpSessionConfig; + + protected SecureContext? SecureContext; + protected SrtpHandler? SrtpHandler; + + private RTPReorderBuffer? RTPReorderBuffer; + + private MediaStreamTrack? m_localTrack; + private MediaStreamTrack? m_remoteTrack; + + protected RTPChannel? rtpChannel; + + protected bool _isClosed; + /// + /// Used for keeping track of TWCC packets + /// + private ushort _twccPacketCount; + + public int Index = -1; + + /// + /// Tracks the global order in which this stream was added to the session, + /// across all media types. Used to preserve m-line ordering per RFC 3264 §8. + /// + public int MediaInsertionOrder = -1; + + /// + /// Fires when the connection for a media type is classified as timed out due to not + /// receiving any RTP or RTCP packets within the given period. + /// + public event Action? OnTimeoutByIndex; + + /// + /// Gets fired when an RTCP report is sent. This event is for diagnostics only. + /// + public event Action? OnSendReportByIndex; + + /// + /// Gets fired when an RTP packet is received from a remote party. + /// Parameters are: + /// - index of the AudioStream or VideoStream + /// - Remote endpoint packet was received from, + /// - The media type the packet contains, will be audio or video, + /// - The full RTP packet. + /// + public event Action? OnRtpPacketReceivedByIndex; + + /// + /// Gets fired when an RTP Header packet is received from a remote party. + /// Parameters are: + /// - index of the AudioStream or VideoStream + /// - Remote endpoint packet was received from, + /// - The media type the packet contains, will be audio or video, + /// - The RTP Header exension URI. + /// + public event Action? OnRtpHeaderReceivedByIndex; + + /// + /// Gets fired when an RTP event is detected on the remote call party's RTP stream. + /// + public event Action? OnRtpEventByIndex; + + /// + /// Gets fired when an RTCP report is received. This event is for diagnostics only. + /// + public event Action? OnReceiveReportByIndex; + + public event Action? OnIsClosedStateChanged; + + public bool AcceptRtpFromAny { get; set; } + + /// + /// Indicates whether the session has been closed. Once a session is closed it cannot + /// be restarted. + /// + public bool IsClosed + { + get + { + return _isClosed; + } + set { - get + if (_isClosed == value) { - return _isClosed; + return; } - set - { - if (_isClosed == value) - { - return; - } - _isClosed = value; + _isClosed = value; - //Clear previous buffer - ClearPendingPackages(); + //Clear previous buffer + ClearPendingPackages(); - OnIsClosedStateChanged?.Invoke(_isClosed); - } + OnIsClosedStateChanged?.Invoke(_isClosed); } + } - /// - /// In order to detect RTP events from the remote party this property needs to - /// be negotiated to a common payload ID. RTP events are typically DTMF tones. - /// - public int NegotiatedRtpEventPayloadID { get; set; } = RTPSession.DEFAULT_DTMF_EVENT_PAYLOAD_ID; - - /// - /// To type of this media - /// - public SDPMediaTypesEnum MediaType { get; set; } - - /// - /// The local track. Will be null if we are not sending this media. - /// - public MediaStreamTrack LocalTrack + /// + /// In order to detect RTP events from the remote party this property needs to + /// be negotiated to a common payload ID. RTP events are typically DTMF tones. + /// + public int NegotiatedRtpEventPayloadID { get; set; } = RTPSession.DEFAULT_DTMF_EVENT_PAYLOAD_ID; + + /// + /// To type of this media + /// + public SDPMediaTypesEnum MediaType { get; set; } + + /// + /// The local track. Will be null if we are not sending this media. + /// + public MediaStreamTrack? LocalTrack + { + get { - get - { - return m_localTrack; - } - set + return m_localTrack; + } + set + { + m_localTrack = value; + if (m_localTrack is { }) { - m_localTrack = value; - if (m_localTrack != null) + // Need to create a sending SSRC and set it on the RTCP session. + if (RtcpSession is { }) { - // Need to create a sending SSRC and set it on the RTCP session. - if (RtcpSession != null) - { - RtcpSession.Ssrc = m_localTrack.Ssrc; - } + RtcpSession.Ssrc = m_localTrack.Ssrc; + } - if (MediaType == SDPMediaTypesEnum.audio) + if (MediaType == SDPMediaTypesEnum.audio) + { + if (m_localTrack.Capabilities is { Count: > 0 } && !m_localTrack.NoDtmfSupport && + !m_localTrack.Capabilities.Exists(x => x.ID == RTPSession.DEFAULT_DTMF_EVENT_PAYLOAD_ID)) { - if (m_localTrack.Capabilities != null && !m_localTrack.NoDtmfSupport && - !m_localTrack.Capabilities.Any(x => x.ID == RTPSession.DEFAULT_DTMF_EVENT_PAYLOAD_ID)) - { - m_localTrack.Capabilities.Add(DefaultRTPEventFormat); - } + m_localTrack.Capabilities.Add(DefaultRTPEventFormat); } } } } + } - /// - /// The remote track. Will be null if the remote party is not sending this media - /// - public MediaStreamTrack RemoteTrack + /// + /// The remote track. Will be null if the remote party is not sending this media + /// + public MediaStreamTrack? RemoteTrack + { + get { - get - { - return m_remoteTrack; - } - set - { - m_remoteTrack = value; - } + return m_remoteTrack; } - - /// - /// The reporting session for this media stream. - /// - public RTCPSession RtcpSession { get; set; } - - /// - /// The remote RTP end point this stream is sending media to. - /// - public IPEndPoint DestinationEndPoint { get; set; } - - /// - /// The remote RTP control end point this stream is sending to RTCP reports for the media stream to. - /// - public IPEndPoint ControlDestinationEndPoint { get; set; } - - /// - /// This endpoint is used when a relay server (TURN) is being used for the RTP session. All RTP packets - /// will be sent to the relay end point instead of the DestinationEndPoint. - /// - public TurnRelayEndPoint RtpRelayEndPoint { get; set; } - - /// - /// This endpoint is used when a relay server (TURN) is being used for the RTCP session. All RTCP packets - /// will be sent to the relay end point instead of the ControlDestinationEndPoint. - /// - public IPEndPoint RelayControlDestinationEndPoint { get; set; } - - /// - /// If set to true indicates the RTP and RTCP sockets are for a relay server (TURN). - /// All traffic for the session should then be sent to/from the relay and not updated. - /// - public bool IsUsingRelayEndPoint => RtpRelayEndPoint != null; - - /// - /// Default RTP event format that we support. - /// - public static SDPAudioVideoMediaFormat DefaultRTPEventFormat + set { - get - { - return new SDPAudioVideoMediaFormat( - SDPMediaTypesEnum.audio, - RTPSession.DEFAULT_DTMF_EVENT_PAYLOAD_ID, - SDP.TELEPHONE_EVENT_ATTRIBUTE, - RTPSession.DEFAULT_AUDIO_CLOCK_RATE, - SDPAudioVideoMediaFormat.DEFAULT_AUDIO_CHANNEL_COUNT, - "0-16"); - } + m_remoteTrack = value; } + } - public MediaStream(RtpSessionConfig config, int index) + /// + /// The reporting session for this media stream. + /// + public RTCPSession? RtcpSession { get; set; } + + /// + /// The remote RTP end point this stream is sending media to. + /// + public IPEndPoint? DestinationEndPoint { get; set; } + + /// + /// The remote RTP control end point this stream is sending to RTCP reports for the media stream to. + /// + public IPEndPoint? ControlDestinationEndPoint { get; set; } + + /// + /// This endpoint is used when a relay server (TURN) is being used for the RTP session. All RTP packets + /// will be sent to the relay end point instead of the DestinationEndPoint. + /// + public TurnRelayEndPoint? RtpRelayEndPoint { get; set; } + + /// + /// This endpoint is used when a relay server (TURN) is being used for the RTCP session. All RTCP packets + /// will be sent to the relay end point instead of the ControlDestinationEndPoint. + /// + public IPEndPoint? RelayControlDestinationEndPoint { get; set; } + + /// + /// If set to true indicates the RTP and RTCP sockets are for a relay server (TURN). + /// All traffic for the session should then be sent to/from the relay and not updated. + /// + public bool IsUsingRelayEndPoint => RtpRelayEndPoint != null; + + /// + /// Default RTP event format that we support. + /// + public static SDPAudioVideoMediaFormat DefaultRTPEventFormat + { + get { - RtpSessionConfig = config; - this.Index = index; + return new SDPAudioVideoMediaFormat( + SDPMediaTypesEnum.audio, + RTPSession.DEFAULT_DTMF_EVENT_PAYLOAD_ID, + SDP.TELEPHONE_EVENT_ATTRIBUTE, + RTPSession.DEFAULT_AUDIO_CLOCK_RATE, + SDPAudioVideoMediaFormat.DEFAULT_AUDIO_CHANNEL_COUNT, + "0-16"); } + } - public void AddBuffer(TimeSpan dropPacketTimeout) - { - RTPReorderBuffer = new RTPReorderBuffer(dropPacketTimeout); - } + public MediaStream(RtpSessionConfig config, int index) + { + RtpSessionConfig = config; + this.Index = index; + } + + public void AddBuffer(TimeSpan dropPacketTimeout) + { + RTPReorderBuffer = new RTPReorderBuffer(dropPacketTimeout); + } + + public void RemoveBuffer(TimeSpan dropPacketTimeout) + { + RTPReorderBuffer = null; + } + + public bool UseBuffer() + { + return RTPReorderBuffer is { }; + } + + public RTPReorderBuffer? GetBuffer() + { + return RTPReorderBuffer; + } - public void RemoveBuffer(TimeSpan dropPacketTimeout) + public void SetSecurityContext(ProtectRtpPacket protectRtp, ProtectRtpPacket unprotectRtp, ProtectRtpPacket protectRtcp, ProtectRtpPacket unprotectRtcp) + { + if (SecureContext is { }) { - RTPReorderBuffer = null; + logger.LogRtpSessionSecureContextAlreadyExists(MediaType); } - public bool UseBuffer() + SecureContext = new SecureContext(protectRtp, unprotectRtp, protectRtcp, unprotectRtcp); + + DispatchPendingPackages(); + } + + public SecureContext? GetSecurityContext() + { + return SecureContext; + } + + public bool IsSecurityContextReady() + { + return SecureContext is { }; + } + + public SrtpHandler GetOrCreateSrtpHandler() + { + if (SrtpHandler is null) { - return RTPReorderBuffer != null; + SrtpHandler = new SrtpHandler(); } + return SrtpHandler; + } + + public void AddRtpChannel(RTPChannel rtpChannel) + { + this.rtpChannel = rtpChannel; + } + + public bool HasRtpChannel() + { + return rtpChannel is { }; + } + + public RTPChannel? GetRTPChannel() + { + return rtpChannel; + } - public RTPReorderBuffer GetBuffer() + protected bool CheckIfCanSendRtpRaw() + { + if (IsClosed) { - return RTPReorderBuffer; + logger.LogRtpSessionSendRtpRawOnClosedSession(MediaType); + return false; } - public void SetSecurityContext(ProtectRtpPacket protectRtp, ProtectRtpPacket unprotectRtp, ProtectRtpPacket protectRtcp, ProtectRtpPacket unprotectRtcp) + if (LocalTrack is null) { - if (SecureContext != null) - { - logger.LogTrace("Tried adding new SecureContext for media type {MediaType}, but one already existed", MediaType); - } - - SecureContext = new SecureContext(protectRtp, unprotectRtp, protectRtcp, unprotectRtcp); - - DispatchPendingPackages(); + logger.LogRtpSessionSendRtpRawNoLocalTrack(MediaType); + return false; } - public SecureContext GetSecurityContext() + if (LocalTrack.StreamStatus is MediaStreamStatusEnum.RecvOnly or MediaStreamStatusEnum.Inactive) { - return SecureContext; + logger.LogRtpSessionSendRtpRawInactiveStream(MediaType, LocalTrack.StreamStatus); + return false; } - public bool IsSecurityContextReady() + if ((RtpSessionConfig.IsSecure || RtpSessionConfig.UseSdpCryptoNegotiation) && SecureContext?.ProtectRtpPacket is null) { - return SecureContext != null; + logger.LogRtpSessionSendRtpPacketSecureContextNotReady(); + return false; } - private (bool, byte[]) UnprotectBuffer(byte[] buffer) + return true; + } + + protected void SendRtpRaw(ReadOnlySpan data, uint timestamp, int markerBit, int payloadType, bool checkDone, ushort? seqNum = null) + { + if (checkDone || CheckIfCanSendRtpRaw()) { - if (SecureContext != null) + var protectRtpPacket = SecureContext?.ProtectRtpPacket; + var srtpProtectionLength = (protectRtpPacket is { }) ? RTPSession.SRTP_MAX_PREFIX_LENGTH : 0; + + var packetPayloadLength = data.Length + srtpProtectionLength; + var packetPayload = new Memory(new byte[packetPayloadLength]); + var rtpPacket = new RTPPacket(new RTPHeader(), packetPayload); + Debug.Assert(LocalTrack is { }); + rtpPacket.Header.SyncSource = LocalTrack.Ssrc; + rtpPacket.Header.SequenceNumber = seqNum ?? LocalTrack.GetNextSeqNum(); + rtpPacket.Header.Timestamp = timestamp; + rtpPacket.Header.MarkerBit = markerBit; + rtpPacket.Header.PayloadType = payloadType; + + /* https://datatracker.ietf.org/doc/html/rfc5285#section-4.2 + + An example header extension, with three extension elements, some + padding, and including the required RTP fields, follows: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | 0xBE | 0xDE | length=3 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ID | L=0 | data | ID | L=1 | data... | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ...data | 0 (pad) | 0 (pad) | ID | L=3 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | data | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + if (LocalTrack?.HeaderExtensions?.Values.Count > 0) { - var unprotectRtpPacket = SecureContext.UnprotectRtpPacket; - - int res = 0; - int outBufLen = 0; + using var extensionBuffer = new ArrayPoolBufferWriter(); - if (RtpSessionConfig.IsMediaMultiplexed) + foreach (var ext in LocalTrack.HeaderExtensions.Values) { - lock (rtpChannel) + // We support up to 14 extensions .... Not clear at all how to manage more ... + if (ext.Id is < 1 or > 14) { - res = unprotectRtpPacket(buffer, buffer.Length, out outBufLen); + continue; } - } - else - { - res = unprotectRtpPacket(buffer, buffer.Length, out outBufLen); - } - if (res == 0) - { - return (true, buffer.Take(outBufLen).ToArray()); - } - else - { - logger.LogWarning("SRTP unprotect failed for {MediaType}, result {Result}.", MediaType, res); + var extPayLoad = ext.Marshal(); + extPayLoad.CopyTo(extensionBuffer.GetSpan(extPayLoad.Length)); + extensionBuffer.Advance(extPayLoad.Length); } - } - return (false, buffer); - } - public bool EnsureBufferUnprotected(byte[] buf, RTPHeader header, out RTPPacket packet) - { - if (RtpSessionConfig.IsSecure || RtpSessionConfig.UseSdpCryptoNegotiation) - { - var (succeeded, newBuffer) = UnprotectBuffer(buf); - if (!succeeded) + var payloadLength = extensionBuffer.WrittenCount; + if (payloadLength > 0) { - packet = null; - return false; + // Need to round to 4 bytes boundaries + var roundedExtSize = payloadLength % 4; + if (roundedExtSize > 0) + { + var paddingLength = 4 - roundedExtSize; + extensionBuffer.GetSpan(paddingLength)[..paddingLength].Clear(); // Add zero padding + extensionBuffer.Advance(paddingLength); + } + + var payload = extensionBuffer.WrittenMemory.ToArray(); + + rtpPacket.Header.HeaderExtensionFlag = 1; // We have at least one extension + rtpPacket.Header.ExtensionLength = (ushort)(payload.Length / 4); // payload length / 4 + rtpPacket.Header.ExtensionProfile = RTPHeader.ONE_BYTE_EXTENSION_PROFILE; // We support up to 14 extensions .... Not clear at all how to manage more ... + rtpPacket.Header.ExtensionPayload = payload; } - packet = new RTPPacket(newBuffer); } else { - packet = new RTPPacket(buf); + rtpPacket.Header.HeaderExtensionFlag = 0; } - packet.Header.ReceivedTime = header.ReceivedTime; - return true; - } - - public SrtpHandler GetOrCreateSrtpHandler() - { - if (SrtpHandler == null) - { - SrtpHandler = new SrtpHandler(); - } - return SrtpHandler; - } - - public void AddRtpChannel(RTPChannel rtpChannel) - { - this.rtpChannel = rtpChannel; - } - public bool HasRtpChannel() - { - return rtpChannel != null; - } - - public RTPChannel GetRTPChannel() - { - return rtpChannel; - } + data.CopyTo(packetPayload.Span); - protected bool CheckIfCanSendRtpRaw() - { - if (IsClosed) - { - logger.LogWarning("SendRtpRaw was called for a {MediaType} packet on a closed RTP session.", MediaType); - return false; - } + var rtpPacketSize = rtpPacket.GetByteCount(); + var buffer = ArrayPool.Shared.Rent(rtpPacketSize); - if (LocalTrack == null) + try { - logger.LogWarning("SendRtpRaw was called for a {MediaType} packet on an RTP session without a local track.", MediaType); - return false; - } + var rtpPacketBytesWritten = rtpPacket.WriteBytes(buffer); - if ((LocalTrack.StreamStatus == MediaStreamStatusEnum.RecvOnly) || (LocalTrack.StreamStatus == MediaStreamStatusEnum.Inactive)) - { - logger.LogWarning("SendRtpRaw was called for a {MediaType} packet on an RTP session with a Stream Status set to {StreamStatus}", MediaType, LocalTrack.StreamStatus); - return false; + if (protectRtpPacket is null) + { + Debug.Assert(rtpChannel is { }); + Debug.Assert(DestinationEndPoint is { }); + rtpChannel.Send( + RTPChannelSocketsEnum.RTP, + DestinationEndPoint, + new ReadOnlyMemory(buffer, 0, rtpPacketBytesWritten)); + } + else + { + var rtperr = protectRtpPacket(buffer, rtpPacketBytesWritten - srtpProtectionLength, out var outBufLen); + if (rtperr != 0) + { + logger.LogRtpChannelSendRtpPacketProtectionFailed(rtperr); + } + else + { + Debug.Assert(rtpChannel is { }); + Debug.Assert(DestinationEndPoint is { }); + rtpChannel.Send( + RTPChannelSocketsEnum.RTP, + DestinationEndPoint, + new ReadOnlyMemory(buffer, 0, outBufLen)); + } + } } - - if ((RtpSessionConfig.IsSecure || RtpSessionConfig.UseSdpCryptoNegotiation) && SecureContext?.ProtectRtpPacket == null) + finally { - logger.LogWarning("SendRtpPacket cannot be called on a secure session before calling SetSecurityContext."); - return false; + ArrayPool.Shared.Return(buffer); } - return true; + RtcpSession?.RecordRtpPacketSend(rtpPacket); } + } + + protected void SendRtpRaw(byte[] data, uint timestamp, int markerBit, int payloadType, bool checkDone, ushort? seqNum = null) + { + SendRtpRaw(new ArraySegment(data), timestamp, markerBit, payloadType, checkDone, seqNum); + } - private static byte[] Combine(params byte[][] arrays) + /// + /// To set a new value to a RTP Header extension. + /// + /// According the extension the Object expected as value is different - check on each extension + /// + /// The URI of the extension to use + /// Object to set on the extension (check extension to know object type) + public void SetRtpHeaderExtensionValue(string uri, object? value) + { + try { - byte[] rv = new byte[arrays.Sum(a => a.Length)]; - int offset = 0; - foreach (byte[] array in arrays) + if (LocalTrack?.HeaderExtensions?.Values is null) { - System.Buffer.BlockCopy(array, 0, rv, offset, array.Length); - offset += array.Length; + return; } - return rv; - } - protected void SendRtpRaw(ArraySegment data, uint timestamp, int markerBit, int payloadType, Boolean checkDone, ushort? seqNum = null) - { - if (checkDone || CheckIfCanSendRtpRaw()) + foreach (var ext in LocalTrack.HeaderExtensions.Values) { - ProtectRtpPacket protectRtpPacket = SecureContext?.ProtectRtpPacket; - int srtpProtectionLength = (protectRtpPacket != null) ? RTPSession.SRTP_MAX_PREFIX_LENGTH : 0; - - RTPPacket rtpPacket = new RTPPacket(data, srtpProtectionLength); - - rtpPacket.Header.SyncSource = LocalTrack.Ssrc; - rtpPacket.Header.SequenceNumber = seqNum ?? LocalTrack.GetNextSeqNum(); - rtpPacket.Header.Timestamp = timestamp; - rtpPacket.Header.MarkerBit = markerBit; - rtpPacket.Header.PayloadType = payloadType; - - /* https://datatracker.ietf.org/doc/html/rfc5285#section-4.2 - - An example header extension, with three extension elements, some - padding, and including the required RTP fields, follows: - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | 0xBE | 0xDE | length=3 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | ID | L=0 | data | ID | L=1 | data... | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | ...data | 0 (pad) | 0 (pad) | ID | L=3 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | data | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - if (LocalTrack?.HeaderExtensions?.Values.Count > 0) - { - byte[] payload = null; - foreach (var ext in LocalTrack.HeaderExtensions.Values) - { - // We support up to 14 extensions .... Not clear at all how to manage more ... - if ((ext.Id < 1) || (ext.Id > 14)) - { - continue; - } - - // Get extension payload and combine it to global payload - var extPayLoad = ext.Marshal(); - if (payload == null) - { - payload = extPayLoad; - } - else - { - payload = Combine(payload, extPayLoad); - } - } - - if (payload?.Length > 0) - { - // Need to round to 4 bytes boundaries - var roundedExtSize = payload.Length % 4; - if (roundedExtSize > 0) - { - var padding = Enumerable.Repeat((byte)0, 4 - roundedExtSize).ToArray(); - payload = Combine(payload, padding); - } - - rtpPacket.Header.HeaderExtensionFlag = 1; // We have at least one extension - rtpPacket.Header.ExtensionLength = (ushort)(payload.Length / 4); // payload length / 4 - rtpPacket.Header.ExtensionProfile = RTPHeader.ONE_BYTE_EXTENSION_PROFILE; // We support up to 14 extensions .... Not clear at all how to manage more ... - rtpPacket.Header.ExtensionPayload = payload; - } - } - else + if (ext.Uri != uri) { - rtpPacket.Header.HeaderExtensionFlag = 0; + continue; } - var rtpBuffer = rtpPacket.GetBytes(); - - if (protectRtpPacket != null) + switch (ext) { - int rtperr = 0; - int outBufLen = 0; + case CVOExtension cvoExtension when uri == CVOExtension.RTP_HEADER_EXTENSION_URI: + cvoExtension.Set(value); + return; - if (RtpSessionConfig.IsMediaMultiplexed) - { - // Multiplexing means that a single rtpChannel is used by multiple MediaStreams from multiple threads. We have - // to ensure that ProtectRtp is being called only from a single thread, otherwise encryption will fail. - lock (rtpChannel) - { - rtperr = protectRtpPacket(rtpBuffer, rtpBuffer.Length - srtpProtectionLength, out outBufLen); - } - } - else - { - rtperr = protectRtpPacket(rtpBuffer, rtpBuffer.Length - srtpProtectionLength, out outBufLen); - } + case AudioLevelExtension audioLevelExtension when uri == AudioLevelExtension.RTP_HEADER_EXTENSION_URI: + audioLevelExtension.Set(value); + return; - if (rtperr != 0) - { - logger.LogError("SendRTPPacket protection failed, result {RtpError}.", rtperr); + //case TransportWideCCExtension.RTP_HEADER_EXTENSION_URI_ALT: + case TransportWideCCExtension transportWideCCExtension when uri == TransportWideCCExtension.RTP_HEADER_EXTENSION_URI: + transportWideCCExtension.Set(_twccPacketCount++); return; - } - else - { - rtpBuffer = rtpBuffer.Take(outBufLen).ToArray(); - } - } - //logger.LogDebug("Sending key {MediaType} RTP packet {SeqNum} TS {Timestamp} PT {PayloadType} MB {MarkerBit} size {Size} to {EndPoint}.", - // MediaType, rtpPacket.Header.SequenceNumber, rtpPacket.Header.Timestamp, rtpPacket.Header.PayloadType, - // rtpPacket.Header.MarkerBit, rtpBuffer.Length, - // IsUsingRelayEndPoint ? RtpRelayEndPoint.RelayServerEndPoint : DestinationEndPoint); + // Not necessary to set something in AbsSendTimeExtension - just to be coherent here + case AbsSendTimeExtension absSendTimeExtension when uri == AbsSendTimeExtension.RTP_HEADER_EXTENSION_URI: + absSendTimeExtension.Set(value); + return; - if (IsUsingRelayEndPoint) - { - rtpChannel.SendRelay(RTPChannelSocketsEnum.RTP, DestinationEndPoint, rtpBuffer, RtpRelayEndPoint.RelayServerEndPoint); - } - else - { - rtpChannel.Send(RTPChannelSocketsEnum.RTP, DestinationEndPoint, rtpBuffer); + default: + return; } - - RtcpSession?.RecordRtpPacketSend(rtpPacket); } } - - protected void SendRtpRaw(byte[] data, uint timestamp, int markerBit, int payloadType, Boolean checkDone, ushort? seqNum = null) + catch { - SendRtpRaw(new ArraySegment(data), timestamp, markerBit, payloadType, checkDone, seqNum); + // Consider logging the exception or handling it appropriately } + } - /// - /// To set a new value to a RTP Header extension. - /// - /// According the extension the Object expected as value is different - check on each extension - /// - /// The URI of the extension to use - /// Object to set on the extension (check extension to know object type) - public void SetRtpHeaderExtensionValue(String uri, Object value) + /// + /// Allows additional control for sending raw RTP payloads. No framing or other processing is carried out. + /// + /// The RTP packet payload. + /// The timestamp to set on the RTP header. + /// The value to set on the RTP header marker bit, should be 0 or 1. + /// The payload ID to set in the RTP header. + /// The RTP sequence number + public void SendRtpRaw(ReadOnlySpan data, uint timestamp, int markerBit, int payloadType, ushort seqNum) + { + SendRtpRaw(data, timestamp, markerBit, payloadType, false, seqNum); + } + + /// + /// Allows additional control for sending raw RTP payloads. No framing or other processing is carried out. + /// + /// The RTP packet payload. + /// The timestamp to set on the RTP header. + /// The value to set on the RTP header marker bit, should be 0 or 1. + /// The payload ID to set in the RTP header. + public void SendRtpRaw(ReadOnlySpan data, uint timestamp, int markerBit, int payloadType) + { + SendRtpRaw(data, timestamp, markerBit, payloadType, false); + } + + /// + /// Allows additional control for sending raw RTCP payloads. + /// + /// Raw RTCP report data to send. + public void SendRtcpRaw(ReadOnlySpan rtcpBytes) + { + if (SendRtcpReportCore(rtcpBytes)) { + RTCPCompoundPacket? rtcpCompoundPacket = null; try { - var ext = LocalTrack?.HeaderExtensions?.Values?.FirstOrDefault(ext => ext.Uri == uri); - if (ext != null) - { - switch (uri) - { - case CVOExtension.RTP_HEADER_EXTENSION_URI: - if (ext is CVOExtension cvoExtension) - { - cvoExtension.Set(value); - } - break; - - case AudioLevelExtension.RTP_HEADER_EXTENSION_URI: - if (ext is AudioLevelExtension audioLevelExtension) - { - audioLevelExtension.Set(value); - } - break; - - case TransportWideCCExtension.RTP_HEADER_EXTENSION_URI: - //case TransportWideCCExtension.RTP_HEADER_EXTENSION_URI_ALT: - if (ext is TransportWideCCExtension transportWideCCExtension) - { - transportWideCCExtension.Set(_twccPacketCount++); - } - break; - - // Not necessary to set something in AbsSendTimeExtension - just to be coherent here - case AbsSendTimeExtension.RTP_HEADER_EXTENSION_URI: - if (ext is AbsSendTimeExtension absSendTimeExtension) - { - absSendTimeExtension.Set(value); - } - break; - } - } + rtcpCompoundPacket = new RTCPCompoundPacket(rtcpBytes); } - catch + catch (Exception excp) { - + logger.LogRtpCannotCreateRtcpCompoundPacket(excp); } - } - /// - /// Allows additional control for sending raw RTP payloads. No framing or other processing is carried out. - /// - /// The RTP packet payload. - /// The timestamp to set on the RTP header. - /// The value to set on the RTP header marker bit, should be 0 or 1. - /// The payload ID to set in the RTP header. - /// The RTP sequence number - public void SendRtpRaw(byte[] data, uint timestamp, int markerBit, int payloadType, ushort seqNum) - { - SendRtpRaw(data, timestamp, markerBit, payloadType, false, seqNum); + if (rtcpCompoundPacket is { }) + { + OnSendReportByIndex?.Invoke(Index, MediaType, rtcpCompoundPacket); + } } + } - /// - /// Allows additional control for sending raw RTP payloads. No framing or other processing is carried out. - /// - /// The RTP packet payload. - /// The timestamp to set on the RTP header. - /// The value to set on the RTP header marker bit, should be 0 or 1. - /// The payload ID to set in the RTP header. - public void SendRtpRaw(byte[] data, uint timestamp, int markerBit, int payloadType) + /// + /// Sends the RTCP report to the remote call party. + /// + /// The unprotected RTCP payload to transmit. + /// + /// if the report was (or was considered) sent. Returns only when SRTP is required + /// but the security context is not yet ready. If no + /// is set the method returns (nothing to send). + /// + private bool SendRtcpReportCore(ReadOnlySpan rtcpPayload) + { + if ((RtpSessionConfig.IsSecure || RtpSessionConfig.UseSdpCryptoNegotiation) && !IsSecurityContextReady()) { - SendRtpRaw(data, timestamp, markerBit, payloadType, false); + logger.LogRtpSrtpReportNotReady(); + return false; } - - /// - /// Allows additional control for sending raw RTCP payloads - /// - /// Raw RTCP report data to send. - public void SendRtcpRaw(byte[] rtcpBytes) + else if (ControlDestinationEndPoint is { }) { - if (SendRtcpReport(rtcpBytes)) + var sendOnSocket = RtpSessionConfig.IsRtcpMultiplexed ? RTPChannelSocketsEnum.RTP : RTPChannelSocketsEnum.Control; + var protectRtcpPacket = SecureContext?.ProtectRtcpPacket; + if (protectRtcpPacket is null) { - RTCPCompoundPacket rtcpCompoundPacket = null; - try - { - rtcpCompoundPacket = new RTCPCompoundPacket(rtcpBytes); - } - catch (Exception excp) + Debug.Assert(rtpChannel is { }); + var memoryOwner = MemoryPool.Shared.Rent(rtcpPayload.Length); + rtcpPayload.CopyTo(memoryOwner.Memory.Span); + rtpChannel.Send(sendOnSocket, ControlDestinationEndPoint, memoryOwner.Memory.Slice(0, rtcpPayload.Length), memoryOwner); + } + else + { + var unprotectedLength = rtcpPayload.Length; + var memoryOwner = MemoryPool.Shared.Rent(unprotectedLength + RTPSession.SRTP_MAX_PREFIX_LENGTH); + _ = MemoryMarshal.TryGetArray(memoryOwner.Memory, out var segment); + rtcpPayload.CopyTo(memoryOwner.Memory.Span); // copy unprotected payload first + var rtperr = protectRtcpPacket(segment.Array!, unprotectedLength, out var outBufLen); + if (rtperr != 0) { - logger.LogWarning("Can't create RTCPCompoundPacket from the provided RTCP bytes. {Message}", excp.Message); + memoryOwner.Dispose(); + logger.LogRtpSrtpRtcpProtectFailed(rtperr); } - - if (rtcpCompoundPacket != null) + else { - OnSendReportByIndex?.Invoke(Index, MediaType, rtcpCompoundPacket); + Debug.Assert(rtpChannel is { }); + rtpChannel.Send(sendOnSocket, ControlDestinationEndPoint, memoryOwner.Memory.Slice(0, outBufLen), memoryOwner); } } } + return true; + } - /// - /// Sends the RTCP report to the remote call party. - /// - /// The serialised RTCP report to send. - /// True if report was sent - private bool SendRtcpReport(byte[] reportBuffer) + /// + /// Sends an RTCP compound or feedback report to the remote party. + /// + /// The RTCP report implementing to serialise and send (unprotected size). + /// When SRTP is active additional space (SRTP_MAX_PREFIX_LENGTH) is temporarily rented to + /// accommodate authentication/tag data. + /// + /// True if the report was (or was considered) sent. Returns false only when SRTP is required + /// but the security context is not yet ready. If no + /// is set the method returns true (nothing to send). + /// + private bool SendRtcpReportCore(IByteSerializable report) + { + if ((RtpSessionConfig.IsSecure || RtpSessionConfig.UseSdpCryptoNegotiation) && !IsSecurityContextReady()) + { + logger.LogRtpSrtpReportNotReady(); + return false; + } + else if (ControlDestinationEndPoint is { }) { - if ((RtpSessionConfig.IsSecure || RtpSessionConfig.UseSdpCryptoNegotiation) && !IsSecurityContextReady()) + var sendOnSocket = RtpSessionConfig.IsRtcpMultiplexed ? RTPChannelSocketsEnum.RTP : RTPChannelSocketsEnum.Control; + var protectRtcpPacket = SecureContext?.ProtectRtcpPacket; + + var reportLength = report.GetByteCount(); + + Debug.Assert(rtpChannel is { }); + + if (protectRtcpPacket is null) { - logger.LogWarning("SendRtcpReport cannot be called on a secure session before calling SetSecurityContext."); - return false; + var memoryOwner = MemoryPool.Shared.Rent(reportLength); + report.WriteBytes(memoryOwner.Memory.Span); + rtpChannel.Send(sendOnSocket, ControlDestinationEndPoint, memoryOwner.Memory.Slice(0, reportLength), null); } - else if (ControlDestinationEndPoint != null) + else { - //logger.LogDebug("SendRtcpReport: {ReportBytes}", reportBytes.HexStr()); - - var sendOnSocket = RtpSessionConfig.IsRtcpMultiplexed ? RTPChannelSocketsEnum.RTP : RTPChannelSocketsEnum.Control; - - var protectRtcpPacket = SecureContext?.ProtectRtcpPacket; + var memoryOwner = MemoryPool.Shared.Rent(reportLength + RTPSession.SRTP_MAX_PREFIX_LENGTH); + report.WriteBytes(memoryOwner.Memory.Span); // copy unprotected payload first - if (protectRtcpPacket == null) + _ = MemoryMarshal.TryGetArray(memoryOwner.Memory, out var segment); + var rtperr = protectRtcpPacket(segment.Array!, reportLength, out var outBufLen); + if (rtperr != 0) { - logger.LogDebug("Sending key {MediaType} RTCP packet size {Size} to {EndPoint}.", - MediaType, reportBuffer.Length, ControlDestinationEndPoint); - - rtpChannel.Send(sendOnSocket, ControlDestinationEndPoint, reportBuffer); + memoryOwner.Dispose(); + logger.LogRtpSrtpRtcpProtectFailed(rtperr); } else { - byte[] sendBuffer = new byte[reportBuffer.Length + RTPSession.SRTP_MAX_PREFIX_LENGTH]; - Buffer.BlockCopy(reportBuffer, 0, sendBuffer, 0, reportBuffer.Length); - - int rtperr = 0; - int outBufLen = 0; - - if (RtpSessionConfig.IsMediaMultiplexed && RtpSessionConfig.IsRtcpMultiplexed) - { - lock (rtpChannel) - { - rtperr = protectRtcpPacket(sendBuffer, sendBuffer.Length - RTPSession.SRTP_MAX_PREFIX_LENGTH, out outBufLen); - } - } - else - { - rtperr = protectRtcpPacket(sendBuffer, sendBuffer.Length - RTPSession.SRTP_MAX_PREFIX_LENGTH, out outBufLen); - } - - if (rtperr != 0) - { - //logger.LogWarning("SRTP RTCP packet protection failed, result {RtpError}.", rtperr); - } - else - { - - //logger.LogDebug("Sending key {MediaType} RTCP packet size {Size} to {EndPoint}.", - // MediaType, outBufLen, ControlDestinationEndPoint); - - rtpChannel.Send(sendOnSocket, ControlDestinationEndPoint, sendBuffer.Take(outBufLen).ToArray()); - } + Debug.Assert(rtpChannel is { }); + rtpChannel.Send(sendOnSocket, ControlDestinationEndPoint, memoryOwner.Memory.Slice(0, outBufLen), memoryOwner); } } - - return true; } - /// - /// Sends the RTCP report to the remote call party. - /// - /// RTCP report to send. - public void SendRtcpReport(RTCPCompoundPacket report) - { - if ((RtpSessionConfig.IsSecure || RtpSessionConfig.UseSdpCryptoNegotiation) && !IsSecurityContextReady() && report.Bye != null) - { - // Do nothing. The RTCP BYE gets generated when an RTP session is closed. - // If that occurs before the connection was able to set up the secure context - // there's no point trying to send it. - } - else - { - var reportBytes = report.GetBytes(); - SendRtcpReport(reportBytes); - OnSendReportByIndex?.Invoke(Index, MediaType, report); - } - } + return true; + } - /// - /// Allows sending of RTCP feedback reports. - /// - /// The feedback report to send. - public void SendRtcpFeedback(RTCPFeedback feedback) + /// + /// Sends the RTCP report to the remote call party. + /// + /// RTCP report to send. + public void SendRtcpReport(RTCPCompoundPacket report) + { + if ((RtpSessionConfig.IsSecure || RtpSessionConfig.UseSdpCryptoNegotiation) && !IsSecurityContextReady() && report.Bye is { }) { - var reportBytes = feedback.GetBytes(); - SendRtcpReport(reportBytes); + // Do nothing. The RTCP BYE gets generated when an RTP session is closed. + // If that occurs before the connection was able to set up the secure context + // there's no point trying to send it. } - - /// - /// Allows sending of RTCP TWCC feedback reports. - /// - /// The feedback report to send. - public void SendRtcpTWCCFeedback(RTCPTWCCFeedback feedback) + else { - var reportBytes = feedback.GetBytes(); - SendRtcpReport(reportBytes); + SendRtcpReportCore(report); + OnSendReportByIndex?.Invoke(Index, MediaType, report); } + } + + /// + /// Allows sending of RTCP feedback reports. + /// + /// The feedback report to send. + public void SendRtcpFeedback(RTCPFeedback feedback) + { + SendRtcpReportCore(feedback); + } - public void OnReceiveRTPPacket(RTPHeader hdr, int localPort, IPEndPoint remoteEndPoint, byte[] buffer) + /// + /// Allows sending of RTCP TWCC feedback reports. + /// + /// The feedback report to send. + public void SendRtcpTWCCFeedback(RTCPTWCCFeedback feedback) + { + SendRtcpReportCore(feedback); + } + + public void OnReceiveRTPPacket(RTPHeader hdr, int localPort, IPEndPoint remoteEndPoint, ReadOnlyMemory buffer, VideoStream? videoStream = null) + { + if (NegotiatedRtpEventPayloadID != 0 && hdr.PayloadType == NegotiatedRtpEventPayloadID) { - RTPPacket rtpPacket; - if (NegotiatedRtpEventPayloadID != 0 && hdr.PayloadType == NegotiatedRtpEventPayloadID) + if (!EnsureBufferUnprotected(buffer, hdr, out var rtpPacket)) { - if (!EnsureBufferUnprotected(buffer, hdr, out rtpPacket)) - { - // Cache pending packages to use it later to prevent missing frames - // when DTLS was not completed yet as a Server bt already completed as a client - AddPendingPackage(hdr, localPort, remoteEndPoint, buffer); - return; - } + Debug.Assert(videoStream is { }); - RaiseOnRtpEventByIndex(remoteEndPoint, new RTPEvent(rtpPacket.GetPayloadBytes()), rtpPacket.Header); + // Cache pending packages to use it later to prevent missing frames + // when DTLS was not completed yet as a Server bt already completed as a client + AddPendingPackage(hdr, localPort, remoteEndPoint, buffer.Span, videoStream); return; } - // Set the remote track SSRC so that RTCP reports can match the media type. - if (RemoteTrack != null && RemoteTrack.Ssrc == 0 && DestinationEndPoint != null && !IsUsingRelayEndPoint) - { - bool isValidSource = AdjustRemoteEndPoint(hdr.SyncSource, remoteEndPoint); + Debug.Assert(rtpPacket is { }); - if (isValidSource) - { - logger.LogDebug("Set remote track ({MediaType} - index={Index}) SSRC to {SyncSource} remote RTP endpoint {rtpep}.", MediaType, Index, hdr.SyncSource, remoteEndPoint); - RemoteTrack.Ssrc = hdr.SyncSource; - } - } + RaiseOnRtpEventByIndex(remoteEndPoint, new RTPEvent(rtpPacket.Payload.Span), rtpPacket.Header); + return; + } - if (RemoteTrack != null) + // Set the remote track SSRC so that RTCP reports can match the media type. + if (RemoteTrack is { } && RemoteTrack.Ssrc == 0 && DestinationEndPoint is { }) + { + var isValidSource = AdjustRemoteEndPoint(hdr.SyncSource, remoteEndPoint); + + if (isValidSource) { - LogIfWrongSeqNumber($"{MediaType}", hdr, RemoteTrack); - ProcessHeaderExtensions(hdr, remoteEndPoint); + logger.LogRtpSessionSetRemoteTrackSsrc(MediaType, Index, hdr.SyncSource); + RemoteTrack.Ssrc = hdr.SyncSource; } + } - if (!EnsureBufferUnprotected(buffer, hdr, out rtpPacket)) + if (RemoteTrack is { }) + { + LogIfWrongSeqNumber($"{MediaType}", hdr, RemoteTrack); + ProcessHeaderExtensions(hdr, remoteEndPoint); + } + + { + if (!EnsureBufferUnprotected(buffer, hdr, out var rtpPacket)) { return; } + // When receiving an Payload from other peer, it will be related to our LocalDescription, + // not to RemoteDescription (as proved by Azure WebRTC Implementation) var format = LocalTrack?.GetFormatForPayloadID(hdr.PayloadType); - - if (rtpPacket != null && format != null) + if ((rtpPacket is { }) && (format is { })) { if (UseBuffer()) { var reorderBuffer = GetBuffer(); + Debug.Assert(reorderBuffer is { }); reorderBuffer.Add(rtpPacket); while (reorderBuffer.Get(out var bufferedPacket)) { - if (RemoteTrack != null) + if (RemoteTrack is { }) { LogIfWrongSeqNumber($"{MediaType}", bufferedPacket.Header, RemoteTrack); RemoteTrack.LastRemoteSeqNum = bufferedPacket.Header.SequenceNumber; } + ProcessRtpPacket(remoteEndPoint, bufferedPacket, format.Value); } } @@ -825,263 +806,324 @@ public void OnReceiveRTPPacket(RTPHeader hdr, int localPort, IPEndPoint remoteEn } } - /// - /// Do any additional processing for the RTP packet. For vidoe streams this method will be overridden to handle video packetisation. - /// Audio and other media types typially don't use framing but have other processing they'd like to do. - /// - /// The remote peer the RTP pakcet was received from. - /// The RTP apcet received. - /// The SDP format for the payload ID in the RTP header. - protected virtual void ProcessRtpPacket(IPEndPoint remoteEndPoint, RTPPacket rtpPacket, SDPAudioVideoMediaFormat format) + bool EnsureBufferUnprotected(ReadOnlyMemory buf, RTPHeader header, [NotNullWhen(true)] out RTPPacket? packet) { - // If not overridden the default behaviour is to raise an event to inform the owner of the RTP transport - // that a new RTP packet has been received. - RaiseOnRtpPacketReceivedByIndex(remoteEndPoint, rtpPacket); - } + if (RtpSessionConfig.IsSecure || RtpSessionConfig.UseSdpCryptoNegotiation) + { + if (SecureContext is { }) + { + if (MemoryMarshal.TryGetArray(buf, out var segment) && segment.Offset == 0 && segment.Array is { }) + { + packet = CreateRtpPacket(segment.Array, segment.Count); + if (packet is null) + { + return false; + } + } + else + { + var tempBuf = ArrayPool.Shared.Rent(buf.Length); + try + { + buf.CopyTo(tempBuf); + packet = CreateRtpPacket(tempBuf, buf.Length); + if (packet is null) + { + return false; + } + } + finally + { + ArrayPool.Shared.Return(tempBuf); + } + } - public void RaiseOnReceiveReportByIndex(IPEndPoint ipEndPoint, RTCPCompoundPacket rtcpPCompoundPacket) - { - OnReceiveReportByIndex?.Invoke(Index, ipEndPoint, MediaType, rtcpPCompoundPacket); - } + RTPPacket? CreateRtpPacket(byte[] array, int length) + { + var res = SecureContext.UnprotectRtpPacket(array, length, out var outBufLen); + if (res != 0) + { + logger.LogRtpSrtpRtcpUnprotectFailed(MediaType, res); + return null; + } - protected void RaiseOnRtpEventByIndex(IPEndPoint ipEndPoint, RTPEvent rtpEvent, RTPHeader rtpHeader) - { - OnRtpEventByIndex?.Invoke(Index, ipEndPoint, rtpEvent, rtpHeader); - } + return new RTPPacket(array.AsSpan(0, outBufLen).ToArray()); + } + } + else + { + packet = null; + return false; + } + } + else + { + packet = new RTPPacket(buf); + } - protected void RaiseOnRtpPacketReceivedByIndex(IPEndPoint ipEndPoint, RTPPacket rtpPacket) - { - OnRtpPacketReceivedByIndex?.Invoke(Index, ipEndPoint, MediaType, rtpPacket); + packet.Header.ReceivedTime = header.ReceivedTime; + return true; } + } - private void RaiseOnTimeoutByIndex(SDPMediaTypesEnum mediaType) - { - OnTimeoutByIndex?.Invoke(Index, mediaType); - } + /// + /// Do any additional processing for the RTP packet. For vidoe streams this method will be overridden to handle video packetisation. + /// Audio and other media types typially don't use framing but have other processing they'd like to do. + /// + /// The remote peer the RTP pakcet was received from. + /// The RTP apcet received. + /// The SDP format for the payload ID in the RTP header. + protected virtual void ProcessRtpPacket(IPEndPoint remoteEndPoint, RTPPacket rtpPacket, SDPAudioVideoMediaFormat format) + { + // If not overridden the default behaviour is to raise an event to inform the owner of the RTP transport + // that a new RTP packet has been received. + RaiseOnRtpPacketReceivedByIndex(remoteEndPoint, rtpPacket); + } - // Submit all previous cached packages to self - protected virtual void DispatchPendingPackages() - { - PendingPackages[] pendingPackagesArray = null; + public void RaiseOnReceiveReportByIndex(IPEndPoint ipEndPoint, RTCPCompoundPacket rtcpPCompoundPacket) + { + OnReceiveReportByIndex?.Invoke(Index, ipEndPoint, MediaType, rtcpPCompoundPacket); + } - var isContextValid = SecureContext != null && !IsClosed; + protected void RaiseOnRtpEventByIndex(IPEndPoint ipEndPoint, RTPEvent rtpEvent, RTPHeader rtpHeader) + { + OnRtpEventByIndex?.Invoke(Index, ipEndPoint, rtpEvent, rtpHeader); + } - lock (_pendingPackagesLock) + protected void RaiseOnRtpPacketReceivedByIndex(IPEndPoint ipEndPoint, RTPPacket rtpPacket) + { + OnRtpPacketReceivedByIndex?.Invoke(Index, ipEndPoint, MediaType, rtpPacket); + } + + private void RaiseOnTimeoutByIndex(SDPMediaTypesEnum mediaType) + { + OnTimeoutByIndex?.Invoke(Index, mediaType); + } + + // Submit all previous cached packages to self + protected virtual void DispatchPendingPackages() + { + PendingPackages[]? pendingPackagesArray = null; + + var isContextValid = SecureContext is { } && !IsClosed; + + lock (_pendingPackagesLock) + { + if (isContextValid) { - if (isContextValid) - { - pendingPackagesArray = _pendingPackagesBuffer.ToArray(); - } - _pendingPackagesBuffer.Clear(); + pendingPackagesArray = _pendingPackagesBuffer.ToArray(); } - if (isContextValid) + _pendingPackagesBuffer.Clear(); + } + if (isContextValid) + { + Debug.Assert(pendingPackagesArray is { }); + foreach (var pendingPackage in pendingPackagesArray) { - foreach (var pendingPackage in pendingPackagesArray) + if (pendingPackage is { }) { - if (pendingPackage != null) - { - OnReceiveRTPPacket(pendingPackage.hdr, pendingPackage.localPort, pendingPackage.remoteEndPoint, pendingPackage.buffer); - } + OnReceiveRTPPacket(pendingPackage.hdr, pendingPackage.localPort, pendingPackage.remoteEndPoint, pendingPackage.buffer); } } } + } - // Clear previous buffer - protected virtual void ClearPendingPackages() + // Clear previous buffer + protected virtual void ClearPendingPackages() + { + lock (_pendingPackagesLock) { - lock (_pendingPackagesLock) - { - _pendingPackagesBuffer.Clear(); - } + _pendingPackagesBuffer.Clear(); } + } - // Cache pending packages to use it later to prevent missing frames - // when DTLS was not completed yet as a Server but already completed as a client - protected virtual bool AddPendingPackage(RTPHeader hdr, int localPort, IPEndPoint remoteEndPoint, byte[] buffer) - { - const int MAX_PENDING_PACKAGES_BUFFER_SIZE = 32; + // Cache pending packages to use it later to prevent missing frames + // when DTLS was not completed yet as a Server but already completed as a client + protected virtual bool AddPendingPackage(RTPHeader hdr, int localPort, IPEndPoint remoteEndPoint, ReadOnlySpan buffer, VideoStream? videoStream = null) + { + const int MAX_PENDING_PACKAGES_BUFFER_SIZE = 32; - if (SecureContext == null && !IsClosed) + if (SecureContext is null && !IsClosed) + { + lock (_pendingPackagesLock) { - lock (_pendingPackagesLock) + //ensure buffer max size + while (_pendingPackagesBuffer.Count is > 0 and >= MAX_PENDING_PACKAGES_BUFFER_SIZE) { - //ensure buffer max size - while (_pendingPackagesBuffer.Count > 0 && _pendingPackagesBuffer.Count >= MAX_PENDING_PACKAGES_BUFFER_SIZE) - { - _pendingPackagesBuffer.RemoveAt(0); - } - _pendingPackagesBuffer.Add(new PendingPackages(hdr, localPort, remoteEndPoint, buffer)); + _pendingPackagesBuffer.RemoveAt(0); } - return true; + _pendingPackagesBuffer.Add(new PendingPackages(hdr, localPort, remoteEndPoint, buffer)); } - return false; + return true; } + return false; + } - protected void LogIfWrongSeqNumber(string trackType, RTPHeader header, MediaStreamTrack track) + protected void LogIfWrongSeqNumber(string trackType, RTPHeader header, MediaStreamTrack track) + { + if (track.LastRemoteSeqNum != 0 && + header.SequenceNumber != (track.LastRemoteSeqNum + 1) && + !(header.SequenceNumber == 0 && track.LastRemoteSeqNum == ushort.MaxValue)) { - if (track.LastRemoteSeqNum != 0 && - header.SequenceNumber != (track.LastRemoteSeqNum + 1) && - !(header.SequenceNumber == 0 && track.LastRemoteSeqNum == ushort.MaxValue)) - { - logger.LogWarning("{TrackType} stream sequence number jumped from {LastRemoteSeqNum} to {SequenceNumber}.", trackType, track.LastRemoteSeqNum, header.SequenceNumber); - } + logger.LogRtpSequenceNumberJumped(trackType, track.LastRemoteSeqNum, header.SequenceNumber); } + } - /// - /// Adjusts the expected remote end point for a particular media type. - /// - /// The SSRC from the RTP packet header. - /// The actual remote end point that the RTP packet came from. - /// True if remote end point for this media type was the expected one or it was adjusted. False if - /// the remote end point was deemed to be invalid for this media type. - protected bool AdjustRemoteEndPoint(uint ssrc, IPEndPoint receivedOnEndPoint) - { - bool isValidSource = false; - IPEndPoint expectedEndPoint = DestinationEndPoint; + /// + /// Adjusts the expected remote end point for a particular media type. + /// + /// The SSRC from the RTP packet header. + /// The actual remote end point that the RTP packet came from. + /// True if remote end point for this media type was the expected one or it was adjusted. False if + /// the remote end point was deemed to be invalid for this media type. + protected bool AdjustRemoteEndPoint(uint ssrc, IPEndPoint receivedOnEndPoint) + { + var isValidSource = false; + var expectedEndPoint = DestinationEndPoint; - if (expectedEndPoint.Address.Equals(receivedOnEndPoint.Address) && expectedEndPoint.Port == receivedOnEndPoint.Port) - { - // Exact match on actual and expected destination. - isValidSource = true; - } - else if (AcceptRtpFromAny || (expectedEndPoint.Address.IsPrivate() && !receivedOnEndPoint.Address.IsPrivate()) - //|| (IPAddress.Loopback.Equals(receivedOnEndPoint.Address) || IPAddress.IPv6Loopback.Equals(receivedOnEndPoint.Address - ) + Debug.Assert(expectedEndPoint is { }); + if (expectedEndPoint.Address.Equals(receivedOnEndPoint.Address) && expectedEndPoint.Port == receivedOnEndPoint.Port) + { + // Exact match on actual and expected destination. + isValidSource = true; + } + else if (AcceptRtpFromAny || (expectedEndPoint.Address.IsPrivate() && !receivedOnEndPoint.Address.IsPrivate()) + //|| (IPAddress.Loopback.Equals(receivedOnEndPoint.Address) || IPAddress.IPv6Loopback.Equals(receivedOnEndPoint.Address + ) + { + // The end point doesn't match BUT we were supplied a private address in the SDP and the remote source is a public address + // so high probability there's a NAT on the network path. Switch to the remote end point (note this can only happen once + // and only if the SSRV is 0, i.e. this is the first RTP packet. + // If the remote end point is a loopback address then it's likely that this is a test/development + // scenario and the source can be trusted. + // AC 12 Jul 2020: Commented out the expression that allows the end point to be change just because it's a loopback address. + // A breaking case is doing an attended transfer test where two different agents are using loopback addresses. + // The expression allows an older session to override the destination set by a newer remote SDP. + // AC 18 Aug 2020: Despite the carefully crafted rules below and https://github.com/sipsorcery/sipsorcery/issues/197 + // there are still cases that were a problem in one scenario but acceptable in another. To accommodate a new property + // was added to allow the application to decide whether the RTP end point switches should be liberal or not. + logger.LogRtpSessionEndPointSwitched(MediaType, ssrc, expectedEndPoint, receivedOnEndPoint); + + DestinationEndPoint = receivedOnEndPoint; + if (RtpSessionConfig.IsRtcpMultiplexed) { - // The end point doesn't match BUT we were supplied a private address in the SDP and the remote source is a public address - // so high probability there's a NAT on the network path. Switch to the remote end point (note this can only happen once - // and only if the SSRV is 0, i.e. this is the first RTP packet. - // If the remote end point is a loopback address then it's likely that this is a test/development - // scenario and the source can be trusted. - // AC 12 Jul 2020: Commented out the expression that allows the end point to be change just because it's a loopback address. - // A breaking case is doing an attended transfer test where two different agents are using loopback addresses. - // The expression allows an older session to override the destination set by a newer remote SDP. - // AC 18 Aug 2020: Despite the carefully crafted rules below and https://github.com/sipsorcery/sipsorcery/issues/197 - // there are still cases that were a problem in one scenario but acceptable in another. To accommodate a new property - // was added to allow the application to decide whether the RTP end point switches should be liberal or not. - logger.LogDebug("{MediaType} end point switched for RTP ssrc {Ssrc} from {ExpectedEndPoint} to {ReceivedOnEndPoint}.", MediaType, ssrc, expectedEndPoint, receivedOnEndPoint); - - DestinationEndPoint = receivedOnEndPoint; - if (RtpSessionConfig.IsRtcpMultiplexed) - { - ControlDestinationEndPoint = DestinationEndPoint; - } - else - { - ControlDestinationEndPoint = new IPEndPoint(DestinationEndPoint.Address, DestinationEndPoint.Port + 1); - } - - isValidSource = true; + ControlDestinationEndPoint = DestinationEndPoint; } else { - logger.LogWarning("RTP packet with SSRC {Ssrc} received from unrecognised end point {ReceivedOnEndPoint}.", ssrc, receivedOnEndPoint); + ControlDestinationEndPoint = new IPEndPoint(DestinationEndPoint.Address, DestinationEndPoint.Port + 1); } - return isValidSource; + isValidSource = true; } - - /// - /// Creates a new RTCP session for a media track belonging to this RTP session. - /// - /// A new RTCPSession object. The RTCPSession must have its Start method called - /// in order to commence sending RTCP reports. - public bool CreateRtcpSession() + else { - if (RtcpSession == null) - { - RtcpSession = new RTCPSession(MediaType, 0); - RtcpSession.OnTimeout += RaiseOnTimeoutByIndex; - - return true; - } - return false; + logger.LogRtpSessionUnrecognisedEndPoint(ssrc, receivedOnEndPoint); } - /// - /// Sets the remote end points for a media type supported by this RTP session. - /// - /// The remote end point for RTP packets corresponding to the media type. - /// The remote end point for RTCP packets corresponding to the media type. - public void SetDestination(IPEndPoint rtpEndPoint, IPEndPoint rtcpEndPoint) + return isValidSource; + } + + /// + /// Creates a new RTCP session for a media track belonging to this RTP session. + /// + /// A new RTCPSession object. The RTCPSession must have its Start method called + /// in order to commence sending RTCP reports. + public bool CreateRtcpSession() + { + if (RtcpSession is null) { - DestinationEndPoint = rtpEndPoint; - ControlDestinationEndPoint = rtcpEndPoint; + RtcpSession = new RTCPSession(MediaType, 0); + RtcpSession.OnTimeout += RaiseOnTimeoutByIndex; + + return true; } + return false; + } + + /// + /// Sets the remote end points for a media type supported by this RTP session. + /// + /// The remote end point for RTP packets corresponding to the media type. + /// The remote end point for RTCP packets corresponding to the media type. + public void SetDestination(IPEndPoint? rtpEndPoint, IPEndPoint? rtcpEndPoint) + { + DestinationEndPoint = rtpEndPoint; + ControlDestinationEndPoint = rtcpEndPoint; + } - /// - /// Attempts to get the highest priority sending format for the remote call party. - /// - /// The first compatible media format found for the specified media type. - public SDPAudioVideoMediaFormat GetSendingFormat() + /// + /// Attempts to get the highest priority sending format for the remote call party. + /// + /// The first compatible media format found for the specified media type. + public SDPAudioVideoMediaFormat GetSendingFormat() + { + if (LocalTrack is { } || RemoteTrack is { }) { - if (LocalTrack != null || RemoteTrack != null) + if (LocalTrack is null) { - if (LocalTrack == null) - { - return RemoteTrack.Capabilities.First(); - } - else if (RemoteTrack == null) - { - return LocalTrack.Capabilities.First(); - } - - SDPAudioVideoMediaFormat format; - if (MediaType == SDPMediaTypesEnum.audio) - { - format = SDPAudioVideoMediaFormat.GetCompatibleFormats(RemoteTrack.Capabilities, LocalTrack.Capabilities) - .Where(x => x.ID != NegotiatedRtpEventPayloadID).FirstOrDefault(); - } - else - { - format = SDPAudioVideoMediaFormat.GetCompatibleFormats(RemoteTrack.Capabilities, LocalTrack.Capabilities).First(); - } + Debug.Assert(RemoteTrack is { Capabilities: { Count: > 0 } }); + return RemoteTrack.Capabilities[0]; + } + else if (RemoteTrack is null) + { + Debug.Assert(LocalTrack is { Capabilities: { Count: > 0 } }); + return LocalTrack.Capabilities[0]; + } - if (format.IsEmpty()) - { - // It's not expected that this occurs as a compatibility check is done when the remote session description - // is set. By this point a compatible codec should be available. - throw new ApplicationException($"No compatible sending format could be found for media {MediaType}."); - } - else - { - return format; - } + var format = MediaType == SDPMediaTypesEnum.audio + ? SDPAudioVideoMediaFormat.GetFirstCompatibleFormatExcluding(RemoteTrack.Capabilities, LocalTrack.Capabilities, NegotiatedRtpEventPayloadID) + : SDPAudioVideoMediaFormat.GetFirstCompatibleFormat(RemoteTrack.Capabilities, LocalTrack.Capabilities); + if (format.IsEmpty()) + { + // It's not expected that this occurs as a compatibility check is done when the remote session description + // is set. By this point a compatible codec should be available. + throw new SipSorceryException($"No compatible sending format could be found for media {MediaType}."); } else { - throw new ApplicationException($"Cannot get the {MediaType} sending format, missing either local or remote {MediaType} track."); + return format; } } + else + { + throw new SipSorceryException($"Cannot get the {MediaType} sending format, missing either local or remote {MediaType} track."); + } + } - public void ProcessHeaderExtensions(RTPHeader header, IPEndPoint remoteEndPoint) + public void ProcessHeaderExtensions(RTPHeader header, IPEndPoint remoteEndPoint) + { + if (OnRtpHeaderReceivedByIndex is { } onRtpHeaderReceivedByIndex + && RemoteTrack?.HeaderExtensions is { Count: 0 } headerExtensions) { - header.GetHeaderExtensions().ToList().ForEach(rtpHeaderExtensionData => + // Only now do the expensive header extension parsing + foreach (var rtpHeaderExtensionData in header.GetHeaderExtensions()) { - if (RemoteTrack?.HeaderExtensions?.TryGetValue(rtpHeaderExtensionData.Id, out RTPHeaderExtension rtpHeaderExtension) == true) + if (headerExtensions.TryGetValue(rtpHeaderExtensionData.Id, out var rtpHeaderExtension)) { var value = rtpHeaderExtension.Unmarshal(header, rtpHeaderExtensionData.Data); - OnRtpHeaderReceivedByIndex?.Invoke(Index, remoteEndPoint, MediaType, rtpHeaderExtension.Uri, value); + onRtpHeaderReceivedByIndex(Index, remoteEndPoint, MediaType, rtpHeaderExtension.Uri, value); } - }); + } } + } - /// - /// Gets the RTP port to use in the SDP offer or answer. - /// - public int GetRtpPortForSessionDescription() + /// + /// Gets the RTP port to use in the SDP offer or answer. + /// + public int GetRtpPortForSessionDescription() + { + if (IsUsingRelayEndPoint) { - if (IsUsingRelayEndPoint) - { - return RtpRelayEndPoint.RemotePeerRelayEndPoint.Port; - } - - return rtpChannel switch - { - null => 0, - _ when rtpChannel.RTPSrflxEndPoint != null => rtpChannel.RTPSrflxEndPoint.Port, - _ => rtpChannel.RTPPort - }; + Debug.Assert(RtpRelayEndPoint is not null); + return RtpRelayEndPoint.RemotePeerRelayEndPoint.Port; } + + return rtpChannel switch + { + null => 0, + _ when rtpChannel.RTPSrflxEndPoint != null => rtpChannel.RTPSrflxEndPoint.Port, + _ => rtpChannel.RTPPort + }; } } diff --git a/src/SIPSorcery/net/RTP/Streams/MediaStreamTrack.cs b/src/SIPSorcery/net/RTP/Streams/MediaStreamTrack.cs index 29a7d3a515..782f8124b9 100644 --- a/src/SIPSorcery/net/RTP/Streams/MediaStreamTrack.cs +++ b/src/SIPSorcery/net/RTP/Streams/MediaStreamTrack.cs @@ -17,351 +17,396 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class MediaStreamTrack { - public class MediaStreamTrack + private static readonly ILogger logger = LogFactory.CreateLogger(); + + /// + /// The type of media stream represented by this track. Must be audio or video. + /// + public SDPMediaTypesEnum Kind { get; private set; } + + /// + /// The value used in the RTP Synchronisation Source header field for media packets + /// sent using this media stream. + /// Be careful that the RTP Synchronisation Source header field should not be changed + /// unless specific implementations require it. By default this value is chosen randomly, + /// with the intent that no two synchronization sources within the same RTP session + /// will have the same SSRC. + /// + public uint Ssrc { get; set; } + + /// + /// The last seqnum received from the remote peer for this stream. + /// + public ushort LastRemoteSeqNum { get; internal set; } + + // The value used in the RTP Sequence Number header field for media packets. + public ushort SeqNum { get { return (ushort)m_seqNum; } internal set { m_seqNum = value; } } + + /// + /// The last abs-capture-time received from the remote peer for this stream. + /// + public TimestampPair LastAbsoluteCaptureTimestamp { get; internal set; } + + /// + /// The value used in the RTP Timestamp header field for media packets + /// sent using this media stream. + /// + public uint Timestamp { get; internal set; } + + /// + /// Indicates whether this track was sourced by a remote connection. + /// + public bool IsRemote { get; set; } + + /// + /// By default audio channels will support DTMF via telephone events. To opt + /// out of DTMF support set this property to true. + /// + public bool NoDtmfSupport { get; set; } + + /// + /// The media capabilities supported by this track. + /// + public List Capabilities { get; internal set; } + + /// + /// a=extmap - Mapping for RTP header extensions + /// + public Dictionary HeaderExtensions { get; internal set; } + + /// + /// Represents the original and default stream status for the track. This is set + /// when the track is created and does not change. It allows tracks to be set back to + /// their original state after being put on hold etc. For example if a track is + /// added as receive only video source then when after on and off hold it needs to + /// be known that the track reverts receive only rather than sendrecv. + /// + public MediaStreamStatusEnum DefaultStreamStatus { get; private set; } + + /// + /// Holds the stream state of the track. + /// + public MediaStreamStatusEnum StreamStatus { get; internal set; } + + /// + /// If the SDP remote the remote party provides "a=ssrc" attributes, as specified + /// in RFC5576, this property will hold the values. The list can be used when + /// an RTP/RTCP packet is received and needs to be matched against a media type or + /// RTCP report. + /// + public Dictionary SdpSsrc { get; set; } = new Dictionary(); + + private uint _maxBandwith; + + /// + /// If set to a non-zero value for local tracks then a Transport Independent Bandwidth (TIAS) attribute + /// will be included in any SDP for the track's media announcement. For remote tracks thi a non-zero + /// value indicates the a TIAS attribute was set in the remote SDP media announcement. + /// The bandwith is specified in bits per seconds (bps). + /// + /// + /// See https://tools.ietf.org/html/rfc3890. + /// + public uint MaximumBandwidth { - private static readonly ILogger logger = LogFactory.CreateLogger(); - - /// - /// The type of media stream represented by this track. Must be audio or video. - /// - public SDPMediaTypesEnum Kind { get; private set; } - - /// - /// The value used in the RTP Synchronisation Source header field for media packets - /// sent using this media stream. - /// Be careful that the RTP Synchronisation Source header field should not be changed - /// unless specific implementations require it. By default this value is chosen randomly, - /// with the intent that no two synchronization sources within the same RTP session - /// will have the same SSRC. - /// - public uint Ssrc { get; set; } - - /// - /// The last seqnum received from the remote peer for this stream. - /// - public ushort LastRemoteSeqNum { get; internal set; } - - // The value used in the RTP Sequence Number header field for media packets. - public ushort SeqNum { get { return (ushort)m_seqNum; } internal set { m_seqNum = value; } } - - /// - /// The last abs-capture-time received from the remote peer for this stream. - /// - public TimestampPair LastAbsoluteCaptureTimestamp{ get; internal set; } - - /// - /// The value used in the RTP Timestamp header field for media packets - /// sent using this media stream. - /// - public uint Timestamp { get; internal set; } - - /// - /// Indicates whether this track was sourced by a remote connection. - /// - public bool IsRemote { get; set; } - - /// - /// By default audio channels will support DTMF via telephone events. To opt - /// out of DTMF support set this property to true. - /// - public bool NoDtmfSupport { get; set; } - - /// - /// The media capabilities supported by this track. - /// - public List Capabilities { get; internal set; } - - /// - /// a=extmap - Mapping for RTP header extensions - /// - public Dictionary HeaderExtensions { get; internal set; } - - /// - /// Represents the original and default stream status for the track. This is set - /// when the track is created and does not change. It allows tracks to be set back to - /// their original state after being put on hold etc. For example if a track is - /// added as receive only video source then when after on and off hold it needs to - /// be known that the track reverts receive only rather than sendrecv. - /// - public MediaStreamStatusEnum DefaultStreamStatus { get; private set; } - - /// - /// Holds the stream state of the track. - /// - public MediaStreamStatusEnum StreamStatus { get; internal set; } - - /// - /// If the SDP remote the remote party provides "a=ssrc" attributes, as specified - /// in RFC5576, this property will hold the values. The list can be used when - /// an RTP/RTCP packet is received and needs to be matched against a media type or - /// RTCP report. - /// - public Dictionary SdpSsrc { get; set; } = new Dictionary(); - - private uint _maxBandwith = 0; - - /// - /// If set to a non-zero value for local tracks then a Transport Independent Bandwidth (TIAS) attribute - /// will be included in any SDP for the track's media announcement. For remote tracks thi a non-zero - /// value indicates the a TIAS attribute was set in the remote SDP media announcement. - /// The bandwith is specified in bits per seconds (bps). - /// - /// - /// See https://tools.ietf.org/html/rfc3890. - /// - public uint MaximumBandwidth + get => _maxBandwith; + set { - get => _maxBandwith; - set + if (!IsRemote) { - if (!IsRemote) - { - _maxBandwith = value; - } - else - { - logger.LogWarning("The maximum bandwith cannot be set for remote tracks."); - } + _maxBandwith = value; + } + else + { + logger.LogRtpMaximumBandwidthRemoteTrack(); } } + } - /// - /// The value used in the RTP Sequence Number header field for media packets. - /// Although valid values are all in the range of ushort, the underlying field is of type int, - /// because Interlocked.CompareExchange is used to increment in a fast and thread-safe manner - /// and there is no overload for ushort. - /// - private int m_seqNum; - - /// - /// Creates a lightweight class to track a media stream track within an RTP session - /// When supporting RFC3550 (the standard RTP specification) the relationship between - /// an RTP stream and session is 1:1. For WebRTC and RFC8101 there can be multiple - /// streams per session. - /// - /// The type of media for this stream. There can only be one - /// stream per media type. - /// True if this track corresponds to a media announcement from the - /// remote party. - /// The capabilities for the track being added. Where the same media - /// type is supported locally and remotely only the mutual capabilities can be used. This will - /// occur if we receive an SDP offer (add track initiated by the remote party) and we need - /// to remove capabilities we don't support. - /// The initial stream status for the media track. Defaults to - /// send receive. - /// Optional. If the track is being created from an SDP announcement this - /// parameter contains a list of the SSRC attributes that should then match the RTP header SSRC value - /// for this track. - public MediaStreamTrack( - SDPMediaTypesEnum kind, - bool isRemote, - List capabilities, - MediaStreamStatusEnum streamStatus = MediaStreamStatusEnum.SendRecv, - List ssrcAttributes = null, Dictionary headerExtensions = null) + /// + /// The value used in the RTP Sequence Number header field for media packets. + /// Although valid values are all in the range of ushort, the underlying field is of type int, + /// because Interlocked.CompareExchange is used to increment in a fast and thread-safe manner + /// and there is no overload for ushort. + /// + private int m_seqNum; + + /// + /// Creates a lightweight class to track a media stream track within an RTP session + /// When supporting RFC3550 (the standard RTP specification) the relationship between + /// an RTP stream and session is 1:1. For WebRTC and RFC8101 there can be multiple + /// streams per session. + /// + /// The type of media for this stream. There can only be one + /// stream per media type. + /// True if this track corresponds to a media announcement from the + /// remote party. + /// The capabilities for the track being added. Where the same media + /// type is supported locally and remotely only the mutual capabilities can be used. This will + /// occur if we receive an SDP offer (add track initiated by the remote party) and we need + /// to remove capabilities we don't support. + /// The initial stream status for the media track. Defaults to + /// send receive. + /// Optional. If the track is being created from an SDP announcement this + /// parameter contains a list of the SSRC attributes that should then match the RTP header SSRC value + /// for this track. + public MediaStreamTrack( + SDPMediaTypesEnum kind, + bool isRemote, + List capabilities, + MediaStreamStatusEnum streamStatus = MediaStreamStatusEnum.SendRecv, + List? ssrcAttributes = null, + Dictionary? headerExtensions = null) + { + Kind = kind; + IsRemote = isRemote; + Capabilities = capabilities; + StreamStatus = streamStatus; + DefaultStreamStatus = streamStatus; + HeaderExtensions = headerExtensions ?? new Dictionary(); + if (!isRemote) { - Kind = kind; - IsRemote = isRemote; - Capabilities = capabilities; - StreamStatus = streamStatus; - DefaultStreamStatus = streamStatus; - HeaderExtensions = headerExtensions ?? new Dictionary(); - if (!isRemote) - { - Ssrc = Convert.ToUInt32(Crypto.GetRandomInt(0, Int32.MaxValue)); - m_seqNum = Convert.ToUInt16(Crypto.GetRandomInt(0, UInt16.MaxValue)); - } + Ssrc = Convert.ToUInt32(Crypto.GetRandomInt(0, int.MaxValue)); + m_seqNum = Convert.ToUInt16(Crypto.GetRandomInt(0, ushort.MaxValue)); + } - // Add the source attributes from the remote SDP to help match RTP SSRC and RTCP CNAME values against - // RTP and RTCP packets received from the remote party. - if (ssrcAttributes?.Count > 0) + // Add the source attributes from the remote SDP to help match RTP SSRC and RTCP CNAME values against + // RTP and RTCP packets received from the remote party. + if (ssrcAttributes?.Count > 0) + { + foreach (var ssrcAttr in ssrcAttributes) { - foreach (var ssrcAttr in ssrcAttributes) - { - if (!SdpSsrc.ContainsKey(ssrcAttr.SSRC)) - { - SdpSsrc.Add(ssrcAttr.SSRC, ssrcAttr); - } - } + SdpSsrc.TryAdd(ssrcAttr.SSRC, ssrcAttr); } } + } - /// - /// Add a local text track. - /// - /// The text format that the local application supports. - /// Optional. The stream status for the text track, e.g. whether - /// send and receive or only one of. - public MediaStreamTrack( - TextFormat format, - MediaStreamStatusEnum streamStatus = MediaStreamStatusEnum.SendRecv) : - this (SDPMediaTypesEnum.text, false, new List { new SDPAudioVideoMediaFormat(format)}, streamStatus) - { } - - /// - /// Add a local audio track. - /// - /// The audio format that the local application supports. - /// Optional. The stream status for the audio track, e.g. whether - /// send and receive or only one of. - public MediaStreamTrack( - AudioFormat format, - MediaStreamStatusEnum streamStatus = MediaStreamStatusEnum.SendRecv) : - this(SDPMediaTypesEnum.audio, false, new List { new SDPAudioVideoMediaFormat(format) }, streamStatus) - { } - - /// - /// Add a local audio track. - /// - /// The audio formats that the local application supports. - /// Optional. The stream status for the audio track, e.g. whether - /// send and receive or only one of. - public MediaStreamTrack( - List formats, + /// + /// Add a local text track. + /// + /// The text format that the local application supports. + /// Optional. The stream status for the text track, e.g. whether + /// send and receive or only one of. + public MediaStreamTrack( + TextFormat format, MediaStreamStatusEnum streamStatus = MediaStreamStatusEnum.SendRecv) : - this(SDPMediaTypesEnum.audio, false, formats.Select(x => new SDPAudioVideoMediaFormat(x)).ToList(), streamStatus) - { } - - /// - /// Add a local video track. - /// - /// The video format that the local application supports. - /// Optional. The stream status for the video track, e.g. whether - /// send and receive or only one of. - public MediaStreamTrack( - VideoFormat format, - MediaStreamStatusEnum streamStatus = MediaStreamStatusEnum.SendRecv) : - this(SDPMediaTypesEnum.video, false, new List { new SDPAudioVideoMediaFormat(format) }, streamStatus) - { } - - /// - /// Add a local video track. - /// - /// The video formats that the local application supports. - /// Optional. The stream status for the video track, e.g. whether - /// send and receive or only one of. - public MediaStreamTrack( - List formats, + this(SDPMediaTypesEnum.text, false, new List { new SDPAudioVideoMediaFormat(format) }, streamStatus) + { } + + /// + /// Add a local audio track. + /// + /// The audio format that the local application supports. + /// Optional. The stream status for the audio track, e.g. whether + /// send and receive or only one of. + public MediaStreamTrack( + AudioFormat format, MediaStreamStatusEnum streamStatus = MediaStreamStatusEnum.SendRecv) : - this(SDPMediaTypesEnum.video, false, formats.Select(x => new SDPAudioVideoMediaFormat(x)).ToList(), streamStatus) - { } - - /// - /// Adds a local audio track based on one or more well known audio formats. - /// There is no equivalent for a local video track as there is no support in this library for any of - /// the well known video formats. - /// - /// One or more well known audio formats. - public MediaStreamTrack(params SDPWellKnownMediaFormatsEnum[] wellKnownAudioFormats) - : this(wellKnownAudioFormats.Select(x => new AudioFormat(x)).ToList()) - { } - - /// - /// Checks whether the payload ID in an RTP packet received from the remote call party - /// is in this track's list. - /// - /// The payload ID to check against. - /// True if the payload ID matches one of the codecs for this stream. False if not. - public bool IsPayloadIDMatch(int payloadID) + this(SDPMediaTypesEnum.audio, false, new List { new SDPAudioVideoMediaFormat(format) }, streamStatus) + { } + + /// + /// Add a local audio track. + /// + /// The audio formats that the local application supports. + /// Optional. The stream status for the audio track, e.g. whether + /// send and receive or only one of. + public MediaStreamTrack( + List formats, + MediaStreamStatusEnum streamStatus = MediaStreamStatusEnum.SendRecv) : + this(SDPMediaTypesEnum.audio, false, ConvertAudioFormats(formats), streamStatus) + { } + + /// + /// Add a local video track. + /// + /// The video format that the local application supports. + /// Optional. The stream status for the video track, e.g. whether + /// send and receive or only one of. + public MediaStreamTrack( + VideoFormat format, + MediaStreamStatusEnum streamStatus = MediaStreamStatusEnum.SendRecv) : + this(SDPMediaTypesEnum.video, false, new List { new SDPAudioVideoMediaFormat(format) }, streamStatus) + { } + + /// + /// Add a local video track. + /// + /// The video formats that the local application supports. + /// Optional. The stream status for the video track, e.g. whether + /// send and receive or only one of. + public MediaStreamTrack( + List formats, + MediaStreamStatusEnum streamStatus = MediaStreamStatusEnum.SendRecv) : + this(SDPMediaTypesEnum.video, false, ConvertVideoFormats(formats), streamStatus) + { } + + /// + /// Adds a local audio track based on one or more well known audio formats. + /// There is no equivalent for a local video track as there is no support in this library for any of + /// the well known video formats. + /// + /// One or more well known audio formats. + public MediaStreamTrack(params SDPWellKnownMediaFormatsEnum[] wellKnownAudioFormats) + : this(ToAudioFormatList(wellKnownAudioFormats)) + { } + + /// + /// Checks whether the payload ID in an RTP packet received from the remote call party + /// is in this track's list. + /// + /// The payload ID to check against. + /// True if the payload ID matches one of the codecs for this stream. False if not. + public bool IsPayloadIDMatch(int payloadID) + { + return Capabilities?.Exists(x => x.ID == payloadID) == true; + } + + /// + /// Checks whether a SSRC value from an RTP header or RTCP report matches + /// a value expected for this track. + /// + /// The SSRC value to check. + /// True if the SSRC value is expected for this track. False if not. + public bool IsSsrcMatch(uint ssrc) + { + return ssrc == Ssrc || SdpSsrc.ContainsKey(ssrc); + } + + /// + /// Gets the matching audio or video format for a payload ID. + /// + /// The payload ID to get the format for. + /// An audio or video format or null if no payload ID matched. + public SDPAudioVideoMediaFormat? GetFormatForPayloadID(int payloadID) + { + if (Capabilities is { }) { - return Capabilities?.Any(x => x.ID == payloadID) == true; + foreach (var fmt in Capabilities) + { + if (fmt.ID == payloadID) + { + return fmt; + } + } } + return null; + } - /// - /// Checks whether a SSRC value from an RTP header or RTCP report matches - /// a value expected for this track. - /// - /// The SSRC value to check. - /// True if the SSRC value is expected for this track. False if not. - public bool IsSsrcMatch(uint ssrc) + /// + /// To restrict MediaStream Capabilties to one Audio/Video format. This Audio/Video format must already be present in the previous list or if the list is empty/null + /// + /// Usefull once you have successfully created a connection with a Peer to use the same format even even others negocitions are performed + /// + /// The Audio/Video Format to restrict + /// True if the operation has been performed + public bool RestrictCapabilities(SDPAudioVideoMediaFormat sdpAudioVideoMediaFormat) + { + var result = true; + if (Capabilities?.Count > 0) { - return ssrc == Ssrc || SdpSsrc.ContainsKey(ssrc); + result = (Capabilities.Exists(x => x.ID == sdpAudioVideoMediaFormat.ID)); } - /// - /// Gets the matching audio or video format for a payload ID. - /// - /// The payload ID to get the format for. - /// An audio or video format or null if no payload ID matched. - public SDPAudioVideoMediaFormat? GetFormatForPayloadID(int payloadID) + if (result) { - return Capabilities?.FirstOrDefault(x => x.ID == payloadID); + Capabilities = new List { sdpAudioVideoMediaFormat }; } + return true; + } - /// - /// To restrict MediaStream Capabilties to one Audio/Video format. This Audio/Video format must already be present in the previous list or if the list is empty/null - /// - /// Usefull once you have successfully created a connection with a Peer to use the same format even even others negocitions are performed - /// - /// The Audio/Video Format to restrict - /// True if the operation has been performed - public Boolean RestrictCapabilities(SDPAudioVideoMediaFormat sdpAudioVideoMediaFormat) + /// + /// To restrict MediaStream Capabilties to one Video format. This Video format must already be present in the previous list or if the list is empty/null + /// + /// Usefull once you have successfully created a connection with a Peer to use the same format even even others negocitions are performed + /// + /// The Video Format to restrict + /// True if the operation has been performed + public bool RestrictCapabilities(VideoFormat videoFormat) + { + return RestrictCapabilities(new SDPAudioVideoMediaFormat(videoFormat)); + } + + /// + /// To restrict MediaStream Capabilties to one Audio format. This Audio format must already be present in the previous list or if the list is empty/null + /// + /// Usefull once you have successfully created a connection with a Peer to use the same format even even others negocitions are performed + /// + /// The Audio Format to restrict + /// True if the operation has been performed + public bool RestrictCapabilities(AudioFormat audioFormat) + { + return RestrictCapabilities(new SDPAudioVideoMediaFormat(audioFormat)); + } + + /// + /// Returns the next SeqNum to be used in the RTP Sequence Number header field for media packets + /// sent using this media stream. + /// + /// + public ushort GetNextSeqNum() + { + var actualSeqNum = m_seqNum; + int expectedSeqNum; + var attempts = 0; + do { - Boolean result = true; - if (Capabilities?.Count > 0) + if (++attempts > 10) { - result = (Capabilities.Exists(x => x.ID == sdpAudioVideoMediaFormat.ID)); + throw new SipSorceryException("GetNextSeqNum did not return an the next SeqNum due to concurrent updates from other threads within 10 attempts."); } + expectedSeqNum = actualSeqNum; + int nextSeqNum = (actualSeqNum >= ushort.MaxValue) ? (ushort)0 : (ushort)(actualSeqNum + 1); + actualSeqNum = Interlocked.CompareExchange(ref m_seqNum, nextSeqNum, expectedSeqNum); + } while (expectedSeqNum != actualSeqNum); // Try as long as compare-exchange was not successful; in most cases, only one iteration should be needed + return (ushort)expectedSeqNum; + } - if(result) + private static List ConvertAudioFormats(List formats) + { + var list = new List(formats?.Count ?? 0); + if (formats is { }) + { + foreach (var f in formats) { - Capabilities = new List { sdpAudioVideoMediaFormat }; + list.Add(new SDPAudioVideoMediaFormat(f)); } - return true; } + return list; + } - /// - /// To restrict MediaStream Capabilties to one Video format. This Video format must already be present in the previous list or if the list is empty/null - /// - /// Usefull once you have successfully created a connection with a Peer to use the same format even even others negocitions are performed - /// - /// The Video Format to restrict - /// True if the operation has been performed - public Boolean RestrictCapabilities(VideoFormat videoFormat) + private static List ConvertVideoFormats(List formats) + { + var list = new List(formats?.Count ?? 0); + if (formats is { }) { - return RestrictCapabilities(new SDPAudioVideoMediaFormat(videoFormat) ); + foreach (var f in formats) + { + list.Add(new SDPAudioVideoMediaFormat(f)); + } } + return list; + } - /// - /// To restrict MediaStream Capabilties to one Audio format. This Audio format must already be present in the previous list or if the list is empty/null - /// - /// Usefull once you have successfully created a connection with a Peer to use the same format even even others negocitions are performed - /// - /// The Audio Format to restrict - /// True if the operation has been performed - public bool RestrictCapabilities(AudioFormat audioFormat) - { - return RestrictCapabilities(new SDPAudioVideoMediaFormat(audioFormat)); - } - - /// - /// Returns the next SeqNum to be used in the RTP Sequence Number header field for media packets - /// sent using this media stream. - /// - /// - public ushort GetNextSeqNum() + private static List ToAudioFormatList(SDPWellKnownMediaFormatsEnum[] wellKnownAudioFormats) + { + var list = new List(wellKnownAudioFormats?.Length ?? 0); + if (wellKnownAudioFormats is { }) { - var actualSeqNum = m_seqNum; - int expectedSeqNum; - int attempts = 0; - do + foreach (var wf in wellKnownAudioFormats) { - if (++attempts > 10) - { - throw new ApplicationException("GetNextSeqNum did not return an the next SeqNum due to concurrent updates from other threads within 10 attempts."); - } - expectedSeqNum = actualSeqNum; - int nextSeqNum = (actualSeqNum >= UInt16.MaxValue) ? (ushort)0 : (ushort)(actualSeqNum + 1); - actualSeqNum = Interlocked.CompareExchange(ref m_seqNum, nextSeqNum, expectedSeqNum); - } while (expectedSeqNum != actualSeqNum); // Try as long as compare-exchange was not successful; in most cases, only one iteration should be needed - return (ushort)expectedSeqNum; + list.Add(new AudioFormat(wf)); + } } + return list; } } diff --git a/src/SIPSorcery/net/RTP/Streams/TextStream.cs b/src/SIPSorcery/net/RTP/Streams/TextStream.cs index e4396cbd89..2019e7b1ac 100644 --- a/src/SIPSorcery/net/RTP/Streams/TextStream.cs +++ b/src/SIPSorcery/net/RTP/Streams/TextStream.cs @@ -1,117 +1,120 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Net.Sockets; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class TextStream : MediaStream { - public class TextStream : MediaStream - { - protected static readonly ILogger logger = LogFactory.CreateLogger(); + protected static readonly ILogger logger = LogFactory.CreateLogger(); - private DateTime _lastSendTime = DateTime.MinValue; + private DateTime _lastSendTime = DateTime.MinValue; - protected bool rtpEventInProgress = false; + protected bool rtpEventInProgress; - private bool sendingFormatFound = false; + private bool sendingFormatFound; - /// - /// The text format negotiated fir the text stream by the SDP offer/answer exchange. - /// - public SDPAudioVideoMediaFormat NegotiatedFormat { get; private set; } + /// + /// The text format negotiated fir the text stream by the SDP offer/answer exchange. + /// + public SDPAudioVideoMediaFormat NegotiatedFormat { get; private set; } - public Action> OnTextFormatsNegotiatedByIndex { get; internal set; } + public Action>? OnTextFormatsNegotiatedByIndex { get; internal set; } - /// - /// Indicates whether this session is using text. - /// - public bool HasText + /// + /// Indicates whether this session is using text. + /// + public bool HasText + { + get { - get - { - return LocalTrack != null && LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive - || RemoteTrack != null && RemoteTrack.StreamStatus != MediaStreamStatusEnum.Inactive; - } + return LocalTrack is { } && LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive + || RemoteTrack is { } && RemoteTrack.StreamStatus != MediaStreamStatusEnum.Inactive; } + } - public void SendText(byte[] sample) + public void SendText(ReadOnlySpan sample) + { + if (!sendingFormatFound) { - if (!sendingFormatFound) - { - NegotiatedFormat = GetSendingFormat(); - sendingFormatFound = true; - } - SendTextFrame(NegotiatedFormat.ID, sample); + NegotiatedFormat = GetSendingFormat(); + sendingFormatFound = true; } + SendTextFrame(NegotiatedFormat.ID, sample); + } - private void SendTextFrame(int payloadTypeID, byte[] buffer) + private void SendTextFrame(int payloadTypeID, ReadOnlySpan buffer) + { + if (CheckIfCanSendRtpRaw()) { - if (CheckIfCanSendRtpRaw()) + if (rtpEventInProgress) { - if (rtpEventInProgress) - { - logger.LogWarning("An RTPEvent is in progress."); - return; - } - - try - { - // Get the current time - DateTime currentTime = DateTime.UtcNow; - - // Calculate time elapsed since the last frame in milliseconds - uint elapsedMilliseconds = 0; + logger.LogRtpEventInProgress(); + return; + } - if (_lastSendTime != DateTime.MinValue) - { - elapsedMilliseconds = (uint)(currentTime - _lastSendTime).TotalMilliseconds; - } + try + { + // Get the current time + var currentTime = DateTime.UtcNow; - // Update the timestamp with elapsed time - LocalTrack.Timestamp += elapsedMilliseconds; + // Calculate time elapsed since the last frame in milliseconds + uint elapsedMilliseconds = 0; - for (int index = 0; index * RTPSession.RTP_MAX_PAYLOAD < buffer.Length; index++) - { - int offset = (index == 0) ? 0 : (index * RTPSession.RTP_MAX_PAYLOAD); - int payloadLength = (offset + RTPSession.RTP_MAX_PAYLOAD < buffer.Length) ? RTPSession.RTP_MAX_PAYLOAD : buffer.Length - offset; + if (_lastSendTime != DateTime.MinValue) + { + elapsedMilliseconds = (uint)(currentTime - _lastSendTime).TotalMilliseconds; + } - // Set the marker bit for the first packet after idle or session start - int markerBit = _lastSendTime == DateTime.MinValue ? 1 : 0; + // Update the timestamp with elapsed time + Debug.Assert(LocalTrack is { }); + LocalTrack.Timestamp += elapsedMilliseconds; - byte[] payload = new byte[payloadLength]; - Buffer.BlockCopy(buffer, offset, payload, 0, payloadLength); + for (var index = 0; index * RTPSession.RTP_MAX_PAYLOAD < buffer.Length; index++) + { + var offset = (index == 0) ? 0 : (index * RTPSession.RTP_MAX_PAYLOAD); + var payloadLength = (offset + RTPSession.RTP_MAX_PAYLOAD < buffer.Length) ? RTPSession.RTP_MAX_PAYLOAD : buffer.Length - offset; - // Send the RTP packet with the updated timestamp - SendRtpRaw(payload, LocalTrack.Timestamp, markerBit, payloadTypeID, true); - } + // Set the marker bit for the first packet after idle or session start + var markerBit = _lastSendTime == DateTime.MinValue ? 1 : 0; - // Update the last send time - _lastSendTime = currentTime; - } - catch (SocketException sockExcp) - { - logger.LogError(sockExcp, "SocketException SendT140Frame. {ErrorMessage}", sockExcp.Message); + // Send the RTP packet with the updated timestamp + SendRtpRaw(buffer.Slice(offset, payloadLength), LocalTrack.Timestamp, markerBit, payloadTypeID, true); } + + // Update the last send time + _lastSendTime = currentTime; + } + catch (SocketException sockExcp) + { + logger.LogSendT140FrameSocketError(sockExcp.Message, sockExcp); } } + } - public void CheckTextFormatsNegotiation() + public void CheckTextFormatsNegotiation() + { + if (OnTextFormatsNegotiatedByIndex is not { } onTextFormatsNegotiatedByIndex + || LocalTrack?.Capabilities is not { Count: > 0 } capabilities) { - if (LocalTrack != null && LocalTrack.Capabilities?.Count > 0) - { - OnTextFormatsNegotiatedByIndex?.Invoke( - Index, - LocalTrack.Capabilities - .Select(x => x.ToTextFormat()).ToList()); - } + return; } - public TextStream(RtpSessionConfig config, int index) : base(config, index) + var textFormats = new List(capabilities.Count); + foreach (var capability in capabilities) { - MediaType = SDPMediaTypesEnum.text; + textFormats.Add(capability.ToTextFormat()); } + + onTextFormatsNegotiatedByIndex(Index, textFormats); + } + + public TextStream(RtpSessionConfig config, int index) : base(config, index) + { + MediaType = SDPMediaTypesEnum.text; } } diff --git a/src/SIPSorcery/net/RTP/Streams/VideoStream.cs b/src/SIPSorcery/net/RTP/Streams/VideoStream.cs index b0a2791a63..b65869928f 100644 --- a/src/SIPSorcery/net/RTP/Streams/VideoStream.cs +++ b/src/SIPSorcery/net/RTP/Streams/VideoStream.cs @@ -15,432 +15,499 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Sockets; +using CommunityToolkit.HighPerformance.Buffers; using Microsoft.Extensions.Logging; using SIPSorcery.net.RTP.Packetisation; using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class VideoStream : MediaStream { - public class VideoStream : MediaStream + protected static readonly ILogger logger = LogFactory.CreateLogger(); + protected RtpVideoFramer? RtpVideoFramer; + + private VideoFormat sendingFormat; + private bool sendingFormatFound; + + /// + /// Gets fired when the remote SDP is received and the set of common video formats is set. + /// + public event Action>? OnVideoFormatsNegotiatedByIndex; + + /// + /// Gets fired when a full video frame is reconstructed from one or more RTP packets + /// received from the remote party. + /// + /// + /// - Received from end point, + /// - The frame timestamp, + /// - The encoded video frame payload. + /// - The video format of the encoded frame. + /// + public event Action, VideoFormat>? OnVideoFrameReceivedByIndex; + + /// + /// Indicates whether this session is using video. + /// + public bool HasVideo { - protected static readonly ILogger logger = LogFactory.CreateLogger(); - protected RtpVideoFramer RtpVideoFramer; - - private VideoFormat sendingFormat; - private bool sendingFormatFound = false; - - /// - /// Gets fired when the remote SDP is received and the set of common video formats is set. - /// - public event Action> OnVideoFormatsNegotiatedByIndex; - - /// - /// Gets fired when a full video frame is reconstructed from one or more RTP packets - /// received from the remote party. - /// - /// - /// - Received from end point, - /// - The frame timestamp, - /// - The encoded video frame payload. - /// - The video format of the encoded frame. - /// - public event Action OnVideoFrameReceivedByIndex; - - /// - /// Indicates whether this session is using video. - /// - public bool HasVideo + get { - get - { - return (LocalTrack != null && LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) - || (RemoteTrack != null && RemoteTrack.StreamStatus != MediaStreamStatusEnum.Inactive); - } + return (LocalTrack is { } && LocalTrack.StreamStatus != MediaStreamStatusEnum.Inactive) + || (RemoteTrack is { } && RemoteTrack.StreamStatus != MediaStreamStatusEnum.Inactive); } + } - /// - /// Indicates the maximum frame size that can be reconstructed from RTP packets during the depacketisation - /// process. - /// - public int MaxReconstructedVideoFrameSize { get; set; } = 1048576; + /// + /// Indicates the maximum frame size that can be reconstructed from RTP packets during the depacketisation + /// process. + /// + public int MaxReconstructedVideoFrameSize { get; set; } = 1048576; - public VideoStream(RtpSessionConfig config, int index) : base(config, index) - { - MediaType = SDPMediaTypesEnum.video; - NegotiatedRtpEventPayloadID = 0; - } + public VideoStream(RtpSessionConfig config, int index) : base(config, index) + { + MediaType = SDPMediaTypesEnum.video; + NegotiatedRtpEventPayloadID = 0; + } - /// - /// Helper method to send a low quality JPEG image over RTP. This method supports a very abbreviated version of RFC 2435 "RTP Payload Format for JPEG-compressed Video". - /// It's intended as a quick convenient way to send something like a test pattern image over an RTSP connection. More than likely it won't be suitable when a high - /// quality image is required since the header used in this method does not support quantization tables. - /// - /// The duration in timestamp units of the payload (e.g. 3000 for 30fps). - /// The raw encoded bytes of the JPEG image to transmit. - /// The encoder quality of the JPEG image. - /// The width of the JPEG image. - /// The height of the JPEG image. - public void SendJpegFrame(uint duration, int payloadTypeID, byte[] jpegBytes, int jpegQuality, int jpegWidth, int jpegHeight) + /// + /// Helper method to send a low quality JPEG image over RTP. This method supports a very abbreviated version of RFC 2435 "RTP Payload Format for JPEG-compressed Video". + /// It's intended as a quick convenient way to send something like a test pattern image over an RTSP connection. More than likely it won't be suitable when a high + /// quality image is required since the header used in this method does not support quantization tables. + /// + /// The duration in timestamp units of the payload (e.g. 3000 for 30fps). + /// The raw encoded bytes of the JPEG image to transmit. + /// The encoder quality of the JPEG image. + /// The width of the JPEG image. + /// The height of the JPEG image. + public void SendJpegFrame(uint duration, int payloadTypeID, ReadOnlySpan jpegBytes, int jpegQuality, int jpegWidth, int jpegHeight) + { + if (CheckIfCanSendRtpRaw()) { - if (CheckIfCanSendRtpRaw()) + try { - try - { - for (int index = 0; index * RTPSession.RTP_MAX_PAYLOAD < jpegBytes.Length; index++) - { - uint offset = Convert.ToUInt32(index * RTPSession.RTP_MAX_PAYLOAD); - int payloadLength = ((index + 1) * RTPSession.RTP_MAX_PAYLOAD < jpegBytes.Length) ? RTPSession.RTP_MAX_PAYLOAD : jpegBytes.Length - index * RTPSession.RTP_MAX_PAYLOAD; - byte[] jpegHeader = RtpVideoFramer.CreateLowQualityRtpJpegHeader(offset, jpegQuality, jpegWidth, jpegHeight); + var maxPayload = RTPSession.RTP_MAX_PAYLOAD; + var headerLength = 8; - List packetPayload = new List(); - packetPayload.AddRange(jpegHeader); - packetPayload.AddRange(jpegBytes.Skip(index * RTPSession.RTP_MAX_PAYLOAD).Take(payloadLength)); + using var memoryOwner = MemoryPool.Shared.Rent(headerLength + maxPayload); + var payloadSpan = memoryOwner.Memory.Span; - int markerBit = ((index + 1) * RTPSession.RTP_MAX_PAYLOAD < jpegBytes.Length) ? 0 : 1; + var offset = 0u; - SendRtpRaw(packetPayload.ToArray(), LocalTrack.Timestamp, markerBit, payloadTypeID, true); - } + Debug.Assert(LocalTrack is { }); - LocalTrack.Timestamp += duration; - } - catch (SocketException sockExcp) + while (!jpegBytes.IsEmpty) { - logger.LogError(sockExcp, "SocketException SendJpegFrame. {ErrorMessage}", sockExcp.Message); + var payloadLength = Math.Min(maxPayload, jpegBytes.Length); + + // Write JPEG RTP header directly into the buffer + RtpVideoFramer.WriteLowQualityRtpJpegHeader(payloadSpan.Slice(0, headerLength), offset, jpegQuality, jpegWidth, jpegHeight); + + // Copy JPEG payload + jpegBytes.Slice(0, payloadLength).CopyTo(payloadSpan.Slice(headerLength)); + + var isLastPacket = payloadLength >= jpegBytes.Length; + var markerBit = isLastPacket ? 1 : 0; + + SendRtpRaw(payloadSpan.Slice(0, headerLength + payloadLength), LocalTrack.Timestamp, markerBit, payloadTypeID, true); + + jpegBytes = jpegBytes.Slice(payloadLength); + offset += (uint)payloadLength; } + + LocalTrack.Timestamp += duration; + } + catch (SocketException sockExcp) + { + logger.LogRtpSocketExceptionSendJpegFrame(sockExcp.Message, sockExcp); } } + } - /// - /// Sends a H264 frame, represented by an Access Unit, to the remote party. - /// - /// The duration in timestamp units of the payload (e.g. 3000 for 30fps). - /// The payload type ID being used for H264 and that will be set on the RTP header. - /// The encoded H264 access unit to transmit. An access unit can contain one or more - /// NAL's. - /// - /// An Access Unit can contain one or more NAL's. The NAL's have to be parsed in order to be able to package - /// in RTP packets. - /// - /// See Annex B for byte stream specification. - /// - // The same URL without XML escape sequences: https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.264-201602-S!!PDF-E&type=items - public void SendH264Frame(uint duration, int payloadTypeID, byte[] accessUnit) + /// + /// Sends a H264 frame, represented by an Access Unit, to the remote party. + /// + /// The duration in timestamp units of the payload (e.g. 3000 for 30fps). + /// The payload type ID being used for H264 and that will be set on the RTP header. + /// The encoded H264 access unit to transmit. An access unit can contain one or more + /// NAL's. + /// + /// An Access Unit can contain one or more NAL's. The NAL's have to be parsed in order to be able to package + /// in RTP packets. + /// + /// See Annex B for byte stream specification. + /// + // The same URL without XML escape sequences: https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.264-201602-S!!PDF-E&type=items + public void SendH264Frame(uint duration, int payloadTypeID, ReadOnlySpan accessUnit) + { + if (CheckIfCanSendRtpRaw()) { - if (CheckIfCanSendRtpRaw()) + foreach (var nal in H264Packetiser.ParseNals(accessUnit)) { - foreach (var nal in H264Packetiser.ParseNals(accessUnit)) - { - SendH26XNal(duration, payloadTypeID, nal.NAL, nal.IsLast); - } + SendH26XNal(duration, payloadTypeID, nal.NAL, nal.IsLast); } } + } + + /// + /// Sends a single H264 NAL to the remote party. + /// + /// The duration in timestamp units of the payload (e.g. 3000 for 30fps). + /// The payload type ID being used for H264 and that will be set on the RTP header. + /// The buffer containing the NAL to send. + /// Should be set for the last NAL in the H264 access unit. Determines when the markbit gets set + /// and the timestamp incremented. + private void SendH26XNal(uint duration, int payloadTypeID, ReadOnlySpan nal, bool isLastNal, bool is265 = false) + { + var naluHeaderSize = is265 ? 2 : 1; + var naluHeader = nal.Slice(0, naluHeaderSize); + + Debug.Assert(LocalTrack is { }); - /// - /// Sends a single H264 NAL to the remote party. - /// - /// The duration in timestamp units of the payload (e.g. 3000 for 30fps). - /// The payload type ID being used for H264 and that will be set on the RTP header. - /// The buffer containing the NAL to send. - /// Should be set for the last NAL in the H264 access unit. Determines when the markbit gets set - /// and the timestamp incremented. - private void SendH26XNal(uint duration, int payloadTypeID, byte[] nal, bool isLastNal, bool is265 = false) + if (nal.Length <= RTPSession.RTP_MAX_PAYLOAD) { - //logger.LogDebug($"Send NAL {nal.Length}, is last {isLastNal}, timestamp {videoTrack.Timestamp}."); - //logger.LogDebug($"nri {nalNri:X2}, type {nalType:X2}."); - var naluHeaderSize = is265 ? 2 : 1; - byte[] naluHeader = is265 ? nal.Take(2).ToArray() : nal.Take(1).ToArray(); + // Send as Single-Time Aggregation Packet (STAP-A). + var markerBit = isLastNal ? 1 : 0; + var payload = ArrayPool.Shared.Rent(nal.Length); - if (nal.Length <= RTPSession.RTP_MAX_PAYLOAD) + try { - //var naltype = naluHeader[0] >> 1 & 0x3F; - //logger.LogTrace("Sending NALtype {type}", naltype); - - // Send as Single-Time Aggregation Packet (STAP-A). - byte[] payload = new byte[nal.Length]; - int markerBit = isLastNal ? 1 : 0; // There is only ever one packet in a STAP-A. - Buffer.BlockCopy(nal, 0, payload, 0, nal.Length); + nal.CopyTo(payload); - //For TWCC SetRtpHeaderExtensionValue(TransportWideCCExtension.RTP_HEADER_EXTENSION_URI, null); - - SendRtpRaw(payload, LocalTrack.Timestamp, markerBit, payloadTypeID, true); - //logger.LogDebug($"send H264 {videoChannel.RTPLocalEndPoint}->{dstEndPoint} timestamp {videoTrack.Timestamp}, payload length {payload.Length}, seqnum {videoTrack.SeqNum}, marker {markerBit}."); - //logger.LogDebug($"send H264 {videoChannel.RTPLocalEndPoint}->{dstEndPoint} timestamp {videoTrack.Timestamp}, STAP-A {h264RtpHdr.HexStr()}, payload length {payload.Length}, seqnum {videoTrack.SeqNum}, marker {markerBit}."); + SendRtpRaw(payload.AsSpan(0, nal.Length), LocalTrack.Timestamp, markerBit, payloadTypeID, true); } - else + finally { - nal = nal.Skip(naluHeaderSize).ToArray(); - //logger.LogTrace("Fragmenting"); + ArrayPool.Shared.Return(payload); + } + } + else + { + var nalPayload = nal.Slice(naluHeaderSize); - // Send as Fragmentation Unit A (FU-A): - for (int index = 0; index * RTPSession.RTP_MAX_PAYLOAD < nal.Length; index++) - { - int offset = index * RTPSession.RTP_MAX_PAYLOAD; - int payloadLength = ((index + 1) * RTPSession.RTP_MAX_PAYLOAD < nal.Length) ? RTPSession.RTP_MAX_PAYLOAD : nal.Length - index * RTPSession.RTP_MAX_PAYLOAD; + // Send as Fragmentation Unit A (FU-A): + for (var index = 0; index * RTPSession.RTP_MAX_PAYLOAD < nalPayload.Length; index++) + { + var offset = index * RTPSession.RTP_MAX_PAYLOAD; + var payloadLength = Math.Min(RTPSession.RTP_MAX_PAYLOAD, nalPayload.Length - offset); - bool isFirstPacket = index == 0; - bool isFinalPacket = (index + 1) * RTPSession.RTP_MAX_PAYLOAD >= nal.Length; - int markerBit = (isLastNal && isFinalPacket) ? 1 : 0; + var isFirstPacket = index == 0; + var isFinalPacket = offset + payloadLength >= nalPayload.Length; + var markerBit = (isLastNal && isFinalPacket) ? 1 : 0; - byte[] rtpHdr = is265 ? H265Packetiser.GetH265RtpHeader(naluHeader, isFirstPacket, isFinalPacket) : H264Packetiser.GetH264RtpHeader(naluHeader[0], isFirstPacket, isFinalPacket); - - byte[] payload = new byte[payloadLength + rtpHdr.Length]; - Buffer.BlockCopy(rtpHdr, 0, payload, 0, rtpHdr.Length); - Buffer.BlockCopy(nal, offset, payload, rtpHdr.Length, payloadLength); + var rtpHdr = is265 + ? H265Packetiser.GetH265RtpHeader(naluHeader.ToArray(), isFirstPacket, isFinalPacket) + : H264Packetiser.GetH264RtpHeader(naluHeader[0], isFirstPacket, isFinalPacket); - //For TWCC - SetRtpHeaderExtensionValue(TransportWideCCExtension.RTP_HEADER_EXTENSION_URI, null); + var payload = ArrayPool.Shared.Rent(payloadLength + rtpHdr.Length); - SendRtpRaw(payload, LocalTrack.Timestamp, markerBit, payloadTypeID, true); - //logger.LogDebug($"send H264 {videoChannel.RTPLocalEndPoint}->{dstEndPoint} timestamp {videoTrack.Timestamp}, FU-A {h264RtpHdr.HexStr()}, payload length {payloadLength}, seqnum {videoTrack.SeqNum}, marker {markerBit}."); + try + { + rtpHdr.CopyTo(payload.AsSpan(0, rtpHdr.Length)); + nalPayload.Slice(offset, payloadLength).CopyTo(payload.AsSpan(rtpHdr.Length, payloadLength)); + + SetRtpHeaderExtensionValue(TransportWideCCExtension.RTP_HEADER_EXTENSION_URI, null); + SendRtpRaw(payload.AsSpan(0, payloadLength + rtpHdr.Length), LocalTrack.Timestamp, markerBit, payloadTypeID, true); + } + finally + { + ArrayPool.Shared.Return(payload); } } + } - if (isLastNal) - { - LocalTrack.Timestamp += duration; - } + if (isLastNal) + { + LocalTrack.Timestamp += duration; } + } - public void SendH265Frame(uint durationRtpUnits, int payloadID, byte[] sample) + public void SendH265Frame(uint durationRtpUnits, int payloadID, ReadOnlySpan sample) + { + if (CheckIfCanSendRtpRaw()) { - if(CheckIfCanSendRtpRaw()) - { - var nals = H265Packetiser.ParseNals(sample); + var nals = H265Packetiser.ParseNals(sample); - // aggregation is only on 2 or more small nals - if (nals.Where(x => x.NAL.Length < RTPSession.RTP_MAX_PAYLOAD).Count() > 1) + // aggregation is only on 2 or more small nals + foreach (var x in nals) + { + if (x.NAL.Length < RTPSession.RTP_MAX_PAYLOAD) { //logger.LogTrace("(ou) Trying aggregating {nals} nals", nals.Count()); nals = H265Packetiser.CreateAggregated(nals, RTPSession.RTP_MAX_PAYLOAD); + break; } + } - //var i = 1; - foreach (var nal in nals) - { - //logger.LogTrace("(out) SEND {bits}({of}/{all})", nal.NAL.Length, i++, nals.Count()); - SendH26XNal(durationRtpUnits, payloadID, nal.NAL, nal.IsLast, true); - } + //var i = 1; + foreach (var nal in nals) + { + //logger.LogTrace("(out) SEND {bits}({of}/{all})", nal.NAL.Length, i++, nals.Count()); + SendH26XNal(durationRtpUnits, payloadID, nal.NAL, nal.IsLast, true); } } + } - /// - /// Sends a VP8 frame as one or more RTP packets. - /// - /// The duration in timestamp units of the payload. Needs - /// to be based on a 90Khz clock. - /// The payload ID to place in the RTP header. - /// The VP8 encoded payload. - public void SendVp8Frame(uint duration, int payloadTypeID, byte[] buffer) + /// + /// Sends a VP8 frame as one or more RTP packets. + /// + /// The duration in timestamp units of the payload. Needs + /// to be based on a 90Khz clock. + /// The payload ID to place in the RTP header. + /// The VP8 encoded payload. + public void SendVp8Frame(uint duration, int payloadTypeID, ReadOnlySpan buffer) + { + if (CheckIfCanSendRtpRaw()) { - if (CheckIfCanSendRtpRaw()) + try { - try + using var memoryOwner = MemoryPool.Shared.Rent(RTPSession.RTP_MAX_PAYLOAD + 1); + var payloadSpan = memoryOwner.Memory.Span; + payloadSpan[0] = (byte)0x10; + + Debug.Assert(LocalTrack is { }); + + while (!buffer.IsEmpty) { - for (int index = 0; index * RTPSession.RTP_MAX_PAYLOAD < buffer.Length; index++) - { - int offset = index * RTPSession.RTP_MAX_PAYLOAD; - int payloadLength = (offset + RTPSession.RTP_MAX_PAYLOAD < buffer.Length) ? RTPSession.RTP_MAX_PAYLOAD : buffer.Length - offset; + var payloadLength = Math.Min(RTPSession.RTP_MAX_PAYLOAD, buffer.Length); - byte[] vp8HeaderBytes = (index == 0) ? new byte[] { 0x10 } : new byte[] { 0x00 }; - byte[] payload = new byte[payloadLength + vp8HeaderBytes.Length]; - Buffer.BlockCopy(vp8HeaderBytes, 0, payload, 0, vp8HeaderBytes.Length); - Buffer.BlockCopy(buffer, offset, payload, vp8HeaderBytes.Length, payloadLength); + buffer.Slice(0, payloadLength).CopyTo(payloadSpan.Slice(1)); - int markerBit = ((offset + payloadLength) >= buffer.Length) ? 1 : 0; // Set marker bit for the last packet in the frame. + var markerBit = (payloadLength >= buffer.Length) ? 1 : 0; // Set marker bit for the last packet in the frame. - SetRtpHeaderExtensionValue(TransportWideCCExtension.RTP_HEADER_EXTENSION_URI, null); - SendRtpRaw(payload, LocalTrack.Timestamp, markerBit, payloadTypeID, true); - } - LocalTrack.Timestamp += duration; - } - catch (SocketException sockExcp) - { - logger.LogError(sockExcp, "SocketException SendVp8Frame."); + SetRtpHeaderExtensionValue(TransportWideCCExtension.RTP_HEADER_EXTENSION_URI, null); + SendRtpRaw(payloadSpan.Slice(0, payloadLength + 1), LocalTrack.Timestamp, markerBit, payloadTypeID, true); + + payloadSpan[0] = (byte)0x00; + buffer = buffer.Slice(payloadLength); } + + LocalTrack.Timestamp += duration; + } + catch (SocketException sockExcp) + { + logger.LogRtpSocketExceptionSendVp8Frame(sockExcp.Message, sockExcp); } } + } - /// - /// Sends an AV1 temporal unit as one or more RTP packets. - /// - /// The duration in timestamp units of the payload. Needs - /// to be based on a 90Khz clock. - /// The payload ID to place in the RTP header. - /// The AV1 encoded temporal unit, represented as a sequence of OBUs. - public void SendAv1Frame(uint duration, int payloadTypeID, byte[] temporalUnit) + /// + /// Sends an AV1 temporal unit as one or more RTP packets. + /// + /// The duration in timestamp units of the payload. Needs + /// to be based on a 90Khz clock. + /// The payload ID to place in the RTP header. + /// The AV1 encoded temporal unit, represented as a sequence of OBUs. + public void SendAv1Frame(uint duration, int payloadTypeID, ReadOnlySpan temporalUnit) + { + if (CheckIfCanSendRtpRaw()) { - if (CheckIfCanSendRtpRaw()) + try { - try - { - bool sentPacket = false; + bool sentPacket = false; - foreach (var packet in AV1Packetiser.Packetize(temporalUnit, RTPSession.RTP_MAX_PAYLOAD)) - { - int markerBit = packet.IsLast ? 1 : 0; + Debug.Assert(LocalTrack is { }); - SetRtpHeaderExtensionValue(TransportWideCCExtension.RTP_HEADER_EXTENSION_URI, null); - SendRtpRaw(packet.Payload, LocalTrack.Timestamp, markerBit, payloadTypeID, true); - sentPacket = true; - } + foreach (var packet in AV1Packetiser.Packetize(temporalUnit, RTPSession.RTP_MAX_PAYLOAD)) + { + int markerBit = packet.IsLast ? 1 : 0; - if (sentPacket) - { - LocalTrack.Timestamp += duration; - } + SetRtpHeaderExtensionValue(TransportWideCCExtension.RTP_HEADER_EXTENSION_URI, null); + SendRtpRaw(packet.Payload.Span, LocalTrack.Timestamp, markerBit, payloadTypeID, true); + sentPacket = true; } - catch (SocketException sockExcp) + + if (sentPacket) { - logger.LogError(sockExcp, "SocketException SendAv1Frame."); + LocalTrack.Timestamp += duration; } } + catch (SocketException sockExcp) + { + logger.LogRtpSocketExceptionSendAv1Frame(sockExcp.Message, sockExcp); + } } + } - /// - /// Sends a JPEG frame as one or more RTP packets. - /// - /// The duration in timestamp units of the payload. - /// The payload ID to place in the RTP header. - /// The JPEG encoded payload. - public void SendMJPEGFrame(uint durationRtpUnits, int payloadID, byte[] sample) + /// + /// Sends a JPEG frame as one or more RTP packets. + /// + /// + /// + /// + public void SendMJPEGFrame(uint durationRtpUnits, int payloadID, ReadOnlySpan sample) + { + if (CheckIfCanSendRtpRaw()) { - if (CheckIfCanSendRtpRaw()) + try { - try + Debug.Assert(LocalTrack is { }); + + var (frameData, customData) = MJPEGPacketiser.GetFrameData(sample); + Debug.Assert(frameData is { }); + var rtpHeaderLength = MJPEGPacketiser.CalculateMJPEGRTPHeaderLength(customData, 0); + + var totalLength = rtpHeaderLength + frameData.Data.Length; + + if (totalLength <= RTPSession.RTP_MAX_PAYLOAD) { - var frameData = MJPEGPacketiser.GetFrameData(sample, out var customData); + using var memoryOwner = MemoryPool.Shared.Rent(totalLength); + var payloadMemory = memoryOwner.Memory.Slice(0, totalLength); + var payloadSpan = payloadMemory.Span; - var rtpHeader = MJPEGPacketiser.GetMJPEGRTPHeader(customData, 0); - if (rtpHeader.Length + frameData.Data.Length <= RTPSession.RTP_MAX_PAYLOAD) - { - var payload = rtpHeader.Concat(frameData.Data).ToArray(); - SendRtpRaw(payload, LocalTrack.Timestamp, 1, payloadID, true); - } - else - { - var restBytes = frameData.Data; - var offset = 0; - while (restBytes.Length > 0) - { - var dataSize = RTPSession.RTP_MAX_PAYLOAD - rtpHeader.Length; - var isLast = dataSize >= restBytes.Length; - var data = isLast ? restBytes : restBytes.Take(dataSize).ToArray(); - var markerBit = isLast ? 0 : 1; - var payload = rtpHeader.Concat(data).ToArray(); - SendRtpRaw(payload, LocalTrack.Timestamp, markerBit, payloadID, true); - - offset += RTPSession.RTP_MAX_PAYLOAD; - rtpHeader = MJPEGPacketiser.GetMJPEGRTPHeader(customData, offset); - restBytes = restBytes.Skip(data.Length).ToArray(); - } - } + MJPEGPacketiser.WriteMJPEGRTPHeader(customData, 0, payloadSpan); + frameData.Data.CopyTo(payloadSpan.Slice(rtpHeaderLength)); + + SetRtpHeaderExtensionValue(TransportWideCCExtension.RTP_HEADER_EXTENSION_URI, null); + SendRtpRaw(payloadSpan, LocalTrack.Timestamp, 1, payloadID, true); } - catch (SocketException sockExcp) + else { - logger.LogError("SocketException SendMJEPGFrame. " + sockExcp.Message); + var restBytes = frameData.Data.AsSpan(); + var offset = 0; + + while (restBytes.Length > 0) + { + var dataSize = RTPSession.RTP_MAX_PAYLOAD - rtpHeaderLength; + var isLast = dataSize >= restBytes.Length; + var dataLength = isLast ? restBytes.Length : dataSize; + + totalLength = rtpHeaderLength + dataLength; + + using var memoryOwner = MemoryPool.Shared.Rent(totalLength); + var payloadMemory = memoryOwner.Memory.Slice(0, totalLength); + var payloadSpan = payloadMemory.Span; + + MJPEGPacketiser.WriteMJPEGRTPHeader(customData, offset, payloadSpan); + restBytes.Slice(0, dataLength).CopyTo(payloadSpan.Slice(rtpHeaderLength)); + + var markerBit = isLast ? 1 : 0; // Marker bit should be 1 for the last packet + + SetRtpHeaderExtensionValue(TransportWideCCExtension.RTP_HEADER_EXTENSION_URI, null); + SendRtpRaw(payloadSpan, LocalTrack.Timestamp, markerBit, payloadID, true); + + offset += dataLength; + restBytes = restBytes.Slice(dataLength); + } } + + LocalTrack.Timestamp += durationRtpUnits; + } + catch (SocketException sockExcp) + { + logger.LogSendMJEPGFrameSocketError(sockExcp.Message, sockExcp); } } + } - /// - /// Sends a video sample to the remote peer. - /// - /// The duration in RTP timestamp units of the video sample. This - /// value is added to the previous RTP timestamp when building the RTP header. - /// The video sample to set as the RTP packet payload. - public void SendVideo(uint durationRtpUnits, byte[] sample) + /// + /// Sends a video sample to the remote peer. + /// + /// The duration in RTP timestamp units of the video sample. This + /// value is added to the previous RTP timestamp when building the RTP header. + /// The video sample to set as the RTP packet payload. + public void SendVideo(uint durationRtpUnits, ReadOnlySpan sample) + { + if (!sendingFormatFound) { - if (!sendingFormatFound) - { - sendingFormat = GetSendingFormat().ToVideoFormat(); - sendingFormatFound = true; - } + sendingFormat = GetSendingFormat().ToVideoFormat(); + sendingFormatFound = true; + } - int payloadID = sendingFormat.FormatID; + int payloadID = sendingFormat.FormatID; - switch (sendingFormat.Codec) - { - case VideoCodecsEnum.VP8: - SendVp8Frame(durationRtpUnits, payloadID, sample); - break; - case VideoCodecsEnum.AV1: - SendAv1Frame(durationRtpUnits, payloadID, sample); - break; - case VideoCodecsEnum.H264: - SendH264Frame(durationRtpUnits, payloadID, sample); - break; - case VideoCodecsEnum.H265: - SendH265Frame(durationRtpUnits, payloadID, sample); - break; - case VideoCodecsEnum.JPEG: - SendMJPEGFrame(durationRtpUnits, payloadID, sample); - break; - default: - throw new ApplicationException($"Unsupported video format selected {sendingFormat.FormatName}."); - } + switch (sendingFormat.Codec) + { + case VideoCodecsEnum.VP8: + SendVp8Frame(durationRtpUnits, payloadID, sample); + break; + case VideoCodecsEnum.AV1: + SendAv1Frame(durationRtpUnits, payloadID, sample); + break; + case VideoCodecsEnum.H264: + SendH264Frame(durationRtpUnits, payloadID, sample); + break; + case VideoCodecsEnum.H265: + SendH265Frame(durationRtpUnits, payloadID, sample); + break; + case VideoCodecsEnum.JPEG: + SendMJPEGFrame(durationRtpUnits, payloadID, sample); + break; + default: + throw new SipSorceryException($"Unsupported video format selected {sendingFormat.FormatName}."); } + } + + protected override void ProcessRtpPacket(IPEndPoint remoteEndPoint, RTPPacket rtpPacket, SDPAudioVideoMediaFormat format) + { + ProcessVideoRtpFrame(remoteEndPoint, rtpPacket, format); + RaiseOnRtpPacketReceivedByIndex(remoteEndPoint, rtpPacket); + } - protected override void ProcessRtpPacket(IPEndPoint remoteEndPoint, RTPPacket rtpPacket, SDPAudioVideoMediaFormat format) + public void ProcessVideoRtpFrame(IPEndPoint endpoint, RTPPacket packet, SDPAudioVideoMediaFormat format) + { + if (OnVideoFrameReceivedByIndex is not { } onVideoFrameReceivedByIndex) { - ProcessVideoRtpFrame(remoteEndPoint, rtpPacket, format); - RaiseOnRtpPacketReceivedByIndex(remoteEndPoint, rtpPacket); + return; } - public void ProcessVideoRtpFrame(IPEndPoint endpoint, RTPPacket packet, SDPAudioVideoMediaFormat format) + if (RtpVideoFramer is { }) { - if (OnVideoFrameReceivedByIndex == null) + using var bufferWriter = new ArrayPoolBufferWriter(); + if (RtpVideoFramer.GotRtpPacket(bufferWriter, packet)) { - return; + onVideoFrameReceivedByIndex(Index, endpoint, packet.Header.Timestamp, bufferWriter.WrittenMemory, format.ToVideoFormat()); } - - if (RtpVideoFramer != null) + } + else + { + if (format.ToVideoFormat().Codec is + VideoCodecsEnum.VP8 or + VideoCodecsEnum.AV1 or + VideoCodecsEnum.H264 or + VideoCodecsEnum.H265 or + VideoCodecsEnum.JPEG) { - var frame = RtpVideoFramer.GotRtpPacket(packet); - if (frame != null) + logger.LogRtpVideoCodecDepacketiserSet(format, packet.Header.SyncSource); + + RtpVideoFramer = new RtpVideoFramer(format.ToVideoFormat().Codec, MaxReconstructedVideoFrameSize); + + using var bufferWriter = new ArrayPoolBufferWriter(); + if (RtpVideoFramer.GotRtpPacket(bufferWriter, packet)) { - OnVideoFrameReceivedByIndex?.Invoke(Index, endpoint, packet.Header.Timestamp, frame, format.ToVideoFormat()); + onVideoFrameReceivedByIndex(Index, endpoint, packet.Header.Timestamp, bufferWriter.WrittenMemory, format.ToVideoFormat()); } } else { - if (format.ToVideoFormat().Codec == VideoCodecsEnum.VP8 || - format.ToVideoFormat().Codec == VideoCodecsEnum.AV1 || - format.ToVideoFormat().Codec == VideoCodecsEnum.H264 || - format.ToVideoFormat().Codec == VideoCodecsEnum.H265 || - format.ToVideoFormat().Codec == VideoCodecsEnum.JPEG) - { - logger.LogDebug("Video depacketisation codec set to {Codec} for SSRC {SSRC}.", format.ToVideoFormat().Codec, packet.Header.SyncSource); - - RtpVideoFramer = new RtpVideoFramer(format.ToVideoFormat().Codec, MaxReconstructedVideoFrameSize); - - var frame = RtpVideoFramer.GotRtpPacket(packet); - if (frame != null) - { - OnVideoFrameReceivedByIndex?.Invoke(Index, endpoint, packet.Header.Timestamp, frame, format.ToVideoFormat()); - } - } - else - { - logger.LogWarning("Video depacketisation logic for codec {CodecName} has not been implemented, PR's welcome!", format.Name()); - } + logger.LogRtpVideoCodecNotImplemented(format); } } + } - public void CheckVideoFormatsNegotiation() + public void CheckVideoFormatsNegotiation() + { + if (OnVideoFormatsNegotiatedByIndex is not { } onVideoFormatsNegotiatedByIndex + || LocalTrack?.Capabilities is not { Count: > 0 } capabilities) { - if (LocalTrack != null && LocalTrack.Capabilities?.Count() > 0) - { - OnVideoFormatsNegotiatedByIndex?.Invoke( - Index, - LocalTrack.Capabilities - .Select(x => x.ToVideoFormat()).ToList()); - } + return; } + + var videoFormats = new List(capabilities.Count); + foreach (var capability in capabilities) + { + videoFormats.Add(capability.ToVideoFormat()); + } + + onVideoFormatsNegotiatedByIndex(Index, videoFormats); } } diff --git a/src/SIPSorcery/net/RTSP/Mjpeg.cs b/src/SIPSorcery/net/RTSP/Mjpeg.cs index ffcddfe7e3..8cadc980f7 100644 --- a/src/SIPSorcery/net/RTSP/Mjpeg.cs +++ b/src/SIPSorcery/net/RTSP/Mjpeg.cs @@ -44,6 +44,8 @@ The above copyright notice and this permission notice shall be included in all c */ //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Buffers.Binary; using System.Collections.Generic; @@ -172,7 +174,7 @@ static Tags() { } 99, 99, 99, 99, 99, 99, 99, 99 }; - static byte[] CreateJFIFHeader(uint type, uint width, uint height, ArraySegment tables, byte precision, ushort dri) + static byte[] CreateJFIFHeader(uint type, uint width, uint height, ReadOnlySpan tables, byte precision, ushort dri) { List result = new List(); result.Add(Tags.Prefix); @@ -323,11 +325,11 @@ static byte[] CreateQuantizationTables(uint type, uint Q, byte precision) /// /// The tables verbatim, either 1 or 2 (Luminance and Chrominance) /// The table with marker and prefix - static byte[] CreateQuantizationTablesMarker(ArraySegment tables, byte precision) + static byte[] CreateQuantizationTablesMarker(ReadOnlySpan tables, byte precision) { //List result = new List(); - int tableCount = tables.Count / (precision > 0 ? 128 : 64); + int tableCount = tables.Length / (precision > 0 ? 128 : 64); //??Some might have more then 2? if (tableCount > 2) @@ -335,7 +337,7 @@ static byte[] CreateQuantizationTablesMarker(ArraySegment tables, byte pre throw new ArgumentOutOfRangeException("tableCount"); } - int tableSize = tables.Count / tableCount; + int tableSize = tables.Length / tableCount; //Each tag is 4 bytes (prefix and tag) + 2 for len = 4 + 1 for Precision and TableId byte[] result = new byte[(5 * tableCount) + (tableSize * tableCount)]; @@ -347,7 +349,7 @@ static byte[] CreateQuantizationTablesMarker(ArraySegment tables, byte pre result[4] = (byte)(precision << 4 | 0); // Precision and TableId //First table. Type - Luminance usually when two - System.Array.Copy(tables.Array, tables.Offset, result, 5, tableSize); + tables.Slice(0, tableSize).CopyTo(result.AsSpan(5, tableSize)); if (tableCount > 1) { @@ -358,7 +360,7 @@ static byte[] CreateQuantizationTablesMarker(ArraySegment tables, byte pre result[tableSize + 9] = (byte)(precision << 4 | 1);//Precision 0, and table Id //Second Table. Type - Chrominance usually when two - System.Array.Copy(tables.Array, tables.Offset + tableSize, result, 10 + tableSize, tableSize); + tables.Slice(0, tableSize).CopyTo(result.AsSpan(10 + tableSize, tableSize)); } return result; @@ -392,7 +394,7 @@ public static byte[] ProcessMjpegFrame(List framePackets) ushort RestartInterval = 0, RestartCount = 0; //A byte which is bit mapped byte PrecisionTable = 0; - ArraySegment tables = default; + ReadOnlyMemory tables = default; //Using a new MemoryStream for a Buffer using (System.IO.MemoryStream Buffer = new System.IO.MemoryStream()) @@ -415,8 +417,8 @@ public static byte[] ProcessMjpegFrame(List framePackets) //Decode RtpJpeg Header - TypeSpecific = packet.GetPayloadByteAt(offset++); - FragmentOffset = (uint)(packet.GetPayloadByteAt(offset++) << 16 | packet.GetPayloadByteAt(offset++) << 8 | packet.GetPayloadByteAt(offset++)); + TypeSpecific = packet.Payload.Span[offset++]; + FragmentOffset = (uint)(packet.Payload.Span[offset++] << 16 | packet.Payload.Span[offset++] << 8 | packet.Payload.Span[offset++]); #region RFC2435 - The Type Field @@ -499,16 +501,16 @@ doubling the height of the image. #endregion - Type = packet.GetPayloadByteAt(offset++); + Type = packet.Payload.Span[offset++]; type = Type & 1; - if (type > 3 || type > 6) + if (type is > 3 or > 6) { throw new ArgumentException("Type numbers 2-5 are reserved and SHOULD NOT be used. Applications on RFC 2035 should be updated to indicate the presence of restart markers with type 64 or 65 and the Restart Marker header."); } - Quality = packet.GetPayloadByteAt(offset++); - Width = (uint)(packet.GetPayloadByteAt(offset++) * 8); // This should have been 128 or > and the standard would have worked for all resolutions - Height = (uint)(packet.GetPayloadByteAt(offset++) * 8); // Now in certain highres profiles you will need an OnVif extension before the RtpJpeg Header + Quality = packet.Payload.Span[offset++]; + Width = (uint)(packet.Payload.Span[offset++] * 8); // This should have been 128 or > and the standard would have worked for all resolutions + Height = (uint)(packet.Payload.Span[offset++] * 8); // Now in certain highres profiles you will need an OnVif extension before the RtpJpeg Header //It is worth noting Rtp does not care what you send and more tags such as comments and or higher resolution pictures may be sent and these values will simply be ignored. if (Width == 0 || Height == 0) @@ -517,7 +519,7 @@ doubling the height of the image. } //Restart Interval 64 - 127 - if (Type > 63 && Type < 128) + if (Type is > 63 and < 128) { /* This header MUST be present immediately after the main JPEG header @@ -530,8 +532,8 @@ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 | Restart Interval |F|L| Restart Count | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ - RestartInterval = (ushort)(packet.GetPayloadByteAt(offset++) << 8 | packet.GetPayloadByteAt(offset++)); - RestartCount = (ushort)((packet.GetPayloadByteAt(offset++) << 8 | packet.GetPayloadByteAt(offset++)) & 0x3fff); + RestartInterval = (ushort)(packet.Payload.Span[offset++] << 8 | packet.Payload.Span[offset++]); + RestartCount = (ushort)((packet.Payload.Span[offset++] << 8 | packet.Payload.Span[offset++]) & 0x3fff); } //QTables Only occur in the first packet @@ -540,7 +542,7 @@ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 //If the quality > 127 there are usually Quantization Tables if (Quality > 127) { - if ((packet.GetPayloadByteAt(offset++)) != 0) + if ((packet.Payload.Span[offset++]) != 0) { //Must Be Zero is Not Zero if (System.Diagnostics.Debugger.IsAttached) @@ -550,7 +552,7 @@ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 } //Precision - PrecisionTable = (packet.GetPayloadByteAt(offset++)); + PrecisionTable = (packet.Payload.Span[offset++]); #region RFC2435 Length Field @@ -585,34 +587,34 @@ those corresponding to the tables needed by the type in use MUST be #endregion //Length of all tables - ushort Length = (ushort)(packet.GetPayloadByteAt(offset++) << 8 | packet.GetPayloadByteAt(offset++)); + ushort Length = (ushort)(packet.Payload.Span[offset++] << 8 | packet.Payload.Span[offset++]); //If there is Table Data Read it if (Length > 0) { - tables = packet.GetPayloadSegment(offset, Length); + tables = packet.Payload.Slice(offset, Length); offset += (int)Length; } - else if (Length > packet.GetPayloadLength() - offset) + else if (Length > packet.Payload.Length - offset) { continue; // The packet must be discarded } else // Create it from the Quality { - tables = new ArraySegment(CreateQuantizationTables(Quality, type, PrecisionTable)); + tables = new ReadOnlyMemory(CreateQuantizationTables(Quality, type, PrecisionTable)); } } else // Create from the Quality { - tables = new ArraySegment(CreateQuantizationTables(type, Quality, PrecisionTable)); + tables = new ReadOnlyMemory(CreateQuantizationTables(type, Quality, PrecisionTable)); } - byte[] header = CreateJFIFHeader(type, Width, Height, tables, PrecisionTable, RestartInterval); + byte[] header = CreateJFIFHeader(type, Width, Height, tables.Span, PrecisionTable, RestartInterval); Buffer.Write(header, 0, header.Length); } //Write the Payload data from the offset - Buffer.Write(packet.GetPayloadBytes(), offset, (int)packet.GetPayloadLength() - offset); + Buffer.Write(packet.Payload.Span.Slice(offset, packet.Payload.Length - offset)); } //Check for EOI Marker diff --git a/src/SIPSorcery/net/RTSP/RTSPHeader.cs b/src/SIPSorcery/net/RTSP/RTSPHeader.cs index b29c9031d9..2b2e611990 100644 --- a/src/SIPSorcery/net/RTSP/RTSPHeader.cs +++ b/src/SIPSorcery/net/RTSP/RTSPHeader.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Generic; using System.Linq; @@ -107,29 +109,26 @@ public static RTSPTransportHeader Parse(string header) var fieldName = field[fieldKeyValueRange[0]]; var fieldValue = field[fieldKeyValueRange[1]]; - if (fieldName.Equals(CLIENT_RTP_PORT_FIELD_NAME, StringComparison.OrdinalIgnoreCase)) - { - transportHeader.ClientRTPPortRange = fieldValue.Trim().ToString(); - } - else if (fieldName.Equals(DESTINATION_FIELD_NAME, StringComparison.OrdinalIgnoreCase)) - { - transportHeader.Destination = fieldValue.Trim().ToString(); - } - else if (fieldName.Equals(SERVER_RTP_PORT_FIELD_NAME, StringComparison.OrdinalIgnoreCase)) - { - transportHeader.ServerRTPPortRange = fieldValue.Trim().ToString(); - } - else if (fieldName.Equals(SOURCE_FIELD_NAME, StringComparison.OrdinalIgnoreCase)) - { - transportHeader.Source = fieldValue.Trim().ToString(); - } - else if (fieldName.Equals(MODE_FIELD_NAME, StringComparison.OrdinalIgnoreCase)) - { - transportHeader.Mode = fieldValue.Trim().ToString(); - } - else + switch (fieldName) { - logger.LogWarning("An RTSP Transport header parameter was not recognised: {Field}", field.ToString()); + case var fn when (CLIENT_RTP_PORT_FIELD_NAME.Equals(fn, StringComparison.OrdinalIgnoreCase)): + transportHeader.ClientRTPPortRange = fieldValue.Trim().ToString(); + break; + case var fn when (DESTINATION_FIELD_NAME.Equals(fn, StringComparison.OrdinalIgnoreCase)): + transportHeader.Destination = fieldValue.Trim().ToString(); + break; + case var fn when (SERVER_RTP_PORT_FIELD_NAME.Equals(fn, StringComparison.OrdinalIgnoreCase)): + transportHeader.ServerRTPPortRange = fieldValue.Trim().ToString(); + break; + case var fn when (SOURCE_FIELD_NAME.Equals(fn, StringComparison.OrdinalIgnoreCase)): + transportHeader.Source = fieldValue.Trim().ToString(); + break; + case var fn when (MODE_FIELD_NAME.Equals(fn, StringComparison.OrdinalIgnoreCase)): + transportHeader.Mode = fieldValue.Trim().ToString(); + break; + default: + logger.LogWarning("An RTSP Transport header parameter was not recognised: {Field}", field.ToString()); + break; } fieldIndex++; diff --git a/src/SIPSorcery/net/RTSP/RTSPMessage.cs b/src/SIPSorcery/net/RTSP/RTSPMessage.cs index 630261cd77..5022b82db1 100644 --- a/src/SIPSorcery/net/RTSP/RTSPMessage.cs +++ b/src/SIPSorcery/net/RTSP/RTSPMessage.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using System.Text; diff --git a/src/SIPSorcery/net/RTSP/RTSPRequest.cs b/src/SIPSorcery/net/RTSP/RTSPRequest.cs index 24742bae98..c0e7368625 100644 --- a/src/SIPSorcery/net/RTSP/RTSPRequest.cs +++ b/src/SIPSorcery/net/RTSP/RTSPRequest.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using Microsoft.Extensions.Logging; diff --git a/src/SIPSorcery/net/RTSP/RTSPResponse.cs b/src/SIPSorcery/net/RTSP/RTSPResponse.cs index 5912681e70..1ce5dcfb01 100644 --- a/src/SIPSorcery/net/RTSP/RTSPResponse.cs +++ b/src/SIPSorcery/net/RTSP/RTSPResponse.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using Microsoft.Extensions.Logging; diff --git a/src/SIPSorcery/net/RTSP/RTSPURL.cs b/src/SIPSorcery/net/RTSP/RTSPURL.cs index 4ce2cf582a..b2a5f36442 100644 --- a/src/SIPSorcery/net/RTSP/RTSPURL.cs +++ b/src/SIPSorcery/net/RTSP/RTSPURL.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; diff --git a/src/SIPSorcery/net/SCTP/Chunks/SctpAbortChunk.cs b/src/SIPSorcery/net/SCTP/Chunks/SctpAbortChunk.cs index d87e49cf5e..c445d38613 100644 --- a/src/SIPSorcery/net/SCTP/Chunks/SctpAbortChunk.cs +++ b/src/SIPSorcery/net/SCTP/Chunks/SctpAbortChunk.cs @@ -17,58 +17,52 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- -using System; -using System.Linq; -using System.Collections.Generic; -using System.Text; +namespace SIPSorcery.Net; -namespace SIPSorcery.Net +/// +/// The ABORT chunk is sent to the peer of an association to close the +/// association.The ABORT chunk may contain Cause Parameters to inform +/// the receiver about the reason of the abort.DATA chunks MUST NOT be +/// bundled with ABORT.Control chunks (except for INIT, INIT ACK, and +/// SHUTDOWN COMPLETE) MAY be bundled with an ABORT, but they MUST be +/// placed before the ABORT in the SCTP packet or they will be ignored by +/// the receiver. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.7 +/// +public class SctpAbortChunk : SctpErrorChunk { /// - /// The ABORT chunk is sent to the peer of an association to close the - /// association.The ABORT chunk may contain Cause Parameters to inform - /// the receiver about the reason of the abort.DATA chunks MUST NOT be - /// bundled with ABORT.Control chunks (except for INIT, INIT ACK, and - /// SHUTDOWN COMPLETE) MAY be bundled with an ABORT, but they MUST be - /// placed before the ABORT in the SCTP packet or they will be ignored by - /// the receiver. + /// Creates a new ABORT chunk. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.7 - /// - public class SctpAbortChunk : SctpErrorChunk - { - /// - /// Creates a new ABORT chunk. - /// - /// If set to true sets a bit in the chunk header to indicate - /// the sender filled in the Verification Tag expected by the peer. - public SctpAbortChunk(bool verificationTagBit) : - base(SctpChunkType.ABORT, verificationTagBit) - { } + /// If set to true sets a bit in the chunk header to indicate + /// the sender filled in the Verification Tag expected by the peer. + public SctpAbortChunk(bool verificationTagBit) : + base(SctpChunkType.ABORT, verificationTagBit) + { } - /// - /// Gets the user supplied abort reason if available. - /// - /// The abort reason or null if not present. - public string GetAbortReason() + /// + /// Gets the user supplied abort reason if available. + /// + /// The abort reason or null if not present. + public string? GetAbortReason() + { + foreach (var errorCause in ErrorCauses) { - if (ErrorCauses.Any(x => x.CauseCode == SctpErrorCauseCode.UserInitiatedAbort)) + if (errorCause.CauseCode == SctpErrorCauseCode.UserInitiatedAbort) { - var userAbort = (SctpErrorUserInitiatedAbort)(ErrorCauses - .First(x => x.CauseCode == SctpErrorCauseCode.UserInitiatedAbort)); + var userAbort = (SctpErrorUserInitiatedAbort)errorCause; return userAbort.AbortReason; } - else if(ErrorCauses.Any(x => x.CauseCode == SctpErrorCauseCode.ProtocolViolation)) + + if (errorCause.CauseCode == SctpErrorCauseCode.ProtocolViolation) { - var protoViolation = (SctpErrorProtocolViolation)(ErrorCauses - .First(x => x.CauseCode == SctpErrorCauseCode.ProtocolViolation)); + var protoViolation = (SctpErrorProtocolViolation)errorCause; return protoViolation.AdditionalInformation; } - else - { - return null; - } } + + return null; } } diff --git a/src/SIPSorcery/net/SCTP/Chunks/SctpChunk.cs b/src/SIPSorcery/net/SCTP/Chunks/SctpChunk.cs index 0fe9648003..f3b77616f7 100644 --- a/src/SIPSorcery/net/SCTP/Chunks/SctpChunk.cs +++ b/src/SIPSorcery/net/SCTP/Chunks/SctpChunk.cs @@ -18,382 +18,420 @@ //----------------------------------------------------------------------------- using System; -using System.Linq; -using System.Collections; +using System.Buffers.Binary; using System.Collections.Generic; using Microsoft.Extensions.Logging; -using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public static class SctpPadding +{ + public static ushort PadTo4ByteBoundary(int val) + { + return (ushort)(val % 4 == 0 ? val : val + 4 - val % 4); + } +} + +/// +/// The values of the Chunk Types. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.2 +/// +public enum SctpChunkType : byte +{ + DATA = 0, + INIT = 1, + INIT_ACK = 2, + SACK = 3, + HEARTBEAT = 4, + HEARTBEAT_ACK = 5, + ABORT = 6, + SHUTDOWN = 7, + SHUTDOWN_ACK = 8, + ERROR = 9, + COOKIE_ECHO = 10, + COOKIE_ACK = 11, + ECNE = 12, // Not used (specified in the RFC for future use). + CWR = 13, // Not used (specified in the RFC for future use). + SHUTDOWN_COMPLETE = 14, + + // Not defined in RFC4960. + //AUTH = 15, + //PKTDROP = 129, + //RE_CONFIG = 130, + //FORWARDTSN = 192, + //ASCONF = 193, + //ASCONF_ACK = 128, +} + +/// +/// The actions required for unrecognised chunks. The byte value corresponds to the highest +/// order two bits of the chunk type value. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.2 +/// +public enum SctpUnrecognisedChunkActions : byte { - public static class SctpPadding + /// + /// Stop processing this SCTP packet and discard it, do not process any further chunks within it. + /// + Stop = 0x00, + + /// + /// Stop processing this SCTP packet and discard it, do not process any further chunks within it, and report the + /// unrecognized chunk in an 'Unrecognized Chunk Type'. + /// + StopAndReport = 0x01, + + /// + /// Skip this chunk and continue processing. + /// + Skip = 0x02, + + /// + /// Skip this chunk and continue processing, but report in an ERROR chunk using the 'Unrecognized Chunk Type' cause of + /// error. + /// + SkipAndReport = 0x03 +} + +public partial class SctpChunk +{ + public const int SCTP_CHUNK_HEADER_LENGTH = 4; + + protected static ILogger logger = SIPSorcery.LogFactory.CreateLogger(); + + /// + /// This field identifies the type of information contained in the + /// Chunk Value field. + /// + public byte ChunkType; + + /// + /// The usage of these bits depends on the Chunk type as given by the + /// Chunk Type field.Unless otherwise specified, they are set to 0 + /// on transmit and are ignored on receipt. + /// + public byte ChunkFlags; + + /// + /// The Chunk Value field contains the actual information to be + /// transferred in the chunk.The usage and format of this field is + /// dependent on the Chunk Type. + /// + public byte[]? ChunkValue; + + /// + /// If recognised returns the known chunk type. If not recognised returns null. + /// + public SctpChunkType? KnownType { - public static ushort PadTo4ByteBoundary(int val) + get { - return (ushort)(val % 4 == 0 ? val : val + 4 - val % 4); + if (SctpChunkTypeExtensions.IsDefined((SctpChunkType)ChunkType)) + { + return (SctpChunkType)ChunkType; + } + else + { + return null; + } } } /// - /// The values of the Chunk Types. + /// Records any unrecognised parameters received from the remote peer and are classified + /// as needing to be reported. These can be sent back to the remote peer if needed. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.2 - /// - public enum SctpChunkType : byte + public List UnrecognizedPeerParameters = new List(); + + public SctpChunk(SctpChunkType chunkType, byte chunkFlags = 0x00) { - DATA = 0, - INIT = 1, - INIT_ACK = 2, - SACK = 3, - HEARTBEAT = 4, - HEARTBEAT_ACK = 5, - ABORT = 6, - SHUTDOWN = 7, - SHUTDOWN_ACK = 8, - ERROR = 9, - COOKIE_ECHO = 10, - COOKIE_ACK = 11, - ECNE = 12, // Not used (specified in the RFC for future use). - CWR = 13, // Not used (specified in the RFC for future use). - SHUTDOWN_COMPLETE = 14, - - // Not defined in RFC4960. - //AUTH = 15, - //PKTDROP = 129, - //RE_CONFIG = 130, - //FORWARDTSN = 192, - //ASCONF = 193, - //ASCONF_ACK = 128, + ChunkType = (byte)chunkType; + ChunkFlags = chunkFlags; } /// - /// The actions required for unrecognised chunks. The byte value corresponds to the highest - /// order two bits of the chunk type value. + /// This constructor is only intended to be used when parsing the specialised + /// chunk types. Because they are being parsed from a buffer nothing is known + /// about them and this constructor allows starting from a blank slate. + /// + protected SctpChunk() + { } + + /// + /// Calculates the length for the chunk. Chunks are required + /// to be padded out to 4 byte boundaries. This method gets overridden + /// by specialised SCTP chunks that have their own fields that determine the length. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.2 - /// - public enum SctpUnrecognisedChunkActions : byte + /// If true the length field will be padded to a 4 byte boundary. + /// The length of the chunk. + public virtual ushort GetByteCount(bool padded) { - /// - /// Stop processing this SCTP packet and discard it, do not process any further chunks within it. - /// - Stop = 0x00, - - /// - /// Stop processing this SCTP packet and discard it, do not process any further chunks within it, and report the - /// unrecognized chunk in an 'Unrecognized Chunk Type'. - /// - StopAndReport = 0x01, - - /// - /// Skip this chunk and continue processing. - /// - Skip = 0x02, - - /// - /// Skip this chunk and continue processing, but report in an ERROR chunk using the 'Unrecognized Chunk Type' cause of - /// error. - /// - SkipAndReport = 0x03 + var len = (ushort)(SCTP_CHUNK_HEADER_LENGTH + + (ChunkValue is null ? 0 : ChunkValue.Length)); + + return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; } - public class SctpChunk + /// + /// The first 32 bits of all chunks represent the same 3 fields. This method + /// parses those fields and sets them on the current instance. + /// + /// The buffer holding the serialised chunk. + /// The chunk length value. + public ushort ParseFirstWord(ReadOnlySpan buffer) { - public const int SCTP_CHUNK_HEADER_LENGTH = 4; - - protected static ILogger logger = SIPSorcery.LogFactory.CreateLogger(); - - /// - /// This field identifies the type of information contained in the - /// Chunk Value field. - /// - public byte ChunkType; - - /// - /// The usage of these bits depends on the Chunk type as given by the - /// Chunk Type field.Unless otherwise specified, they are set to 0 - /// on transmit and are ignored on receipt. - /// - public byte ChunkFlags; - - /// - /// The Chunk Value field contains the actual information to be - /// transferred in the chunk.The usage and format of this field is - /// dependent on the Chunk Type. - /// - public byte[] ChunkValue; - - /// - /// If recognised returns the known chunk type. If not recognised returns null. - /// - public SctpChunkType? KnownType - { - get - { - if (Enum.IsDefined(typeof(SctpChunkType), ChunkType)) - { - return (SctpChunkType)ChunkType; - } - else - { - return null; - } - } - } + ChunkType = buffer[0]; + ChunkFlags = buffer[1]; + var chunkLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2)); - /// - /// Records any unrecognised parameters received from the remote peer and are classified - /// as needing to be reported. These can be sent back to the remote peer if needed. - /// - public List UnrecognizedPeerParameters = new List(); - - public SctpChunk(SctpChunkType chunkType, byte chunkFlags = 0x00) + if (chunkLength > 0 && buffer.Length < chunkLength) { - ChunkType = (byte)chunkType; - ChunkFlags = chunkFlags; + // The buffer was not big enough to supply the specified chunk length. + throw new SipSorceryException($"The SCTP chunk buffer was too short. Required {chunkLength} bytes but only {buffer.Length} available."); } - /// - /// This constructor is only intended to be used when parsing the specialised - /// chunk types. Because they are being parsed from a buffer nothing is known - /// about them and this constructor allows starting from a blank slate. - /// - protected SctpChunk() - { } - - /// - /// Calculates the length for the chunk. Chunks are required - /// to be padded out to 4 byte boundaries. This method gets overridden - /// by specialised SCTP chunks that have their own fields that determine the length. - /// - /// If true the length field will be padded to a 4 byte boundary. - /// The length of the chunk. - public virtual ushort GetChunkLength(bool padded) - { - var len = (ushort)(SCTP_CHUNK_HEADER_LENGTH - + (ChunkValue == null ? 0 : ChunkValue.Length)); + return chunkLength; + } - return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; - } + /// + /// Writes the chunk header to the buffer. All chunks use the same three + /// header fields. + /// + /// The buffer to write the chunk header to. + /// The position in the buffer to write at. + /// The padded length of this chunk. + protected void WriteChunkHeader(byte[] buffer, int posn) + { + WriteChunkHeader(buffer.AsSpan(posn)); + } - /// - /// The first 32 bits of all chunks represent the same 3 fields. This method - /// parses those fields and sets them on the current instance. - /// - /// The buffer holding the serialised chunk. - /// The position in the buffer that indicates the start of the chunk. - /// The chunk length value. - public ushort ParseFirstWord(byte[] buffer, int posn) - { - ChunkType = buffer[posn]; - ChunkFlags = buffer[posn + 1]; - ushort chunkLength = NetConvert.ParseUInt16(buffer, posn + 2); + /// + /// Writes the chunk header to the buffer. All chunks use the same three + /// header fields. + /// + /// The buffer to write the chunk header to. + /// The number of bytes, including padding, written to the buffer. + protected int WriteChunkHeader(Span buffer) + { + buffer[0] = ChunkType; + buffer[1] = ChunkFlags; + var chunkLength = GetByteCount(false); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), chunkLength); + return chunkLength + 2; + } - if (chunkLength > 0 && buffer.Length < posn + chunkLength) - { - // The buffer was not big enough to supply the specified chunk length. - int bytesRequired = chunkLength; - int bytesAvailable = buffer.Length - posn; - throw new ApplicationException($"The SCTP chunk buffer was too short. Required {bytesRequired} bytes but only {bytesAvailable} available."); - } + /// + /// Serialises the chunk to a pre-allocated buffer. This method gets overridden + /// by specialised SCTP chunks that have their own parameters and need to be serialised + /// differently. + /// + /// The buffer to write the serialised chunk bytes to. It + /// must have the required space already allocated. + /// The number of bytes, including padding, written to the buffer. + public virtual ushort WriteBytes(Span buffer) + { + WriteBytesCore(buffer); - return chunkLength; - } + return GetByteCount(true); + } + + /// + /// Serialises the chunk to a pre-allocated buffer. This method gets overridden + /// by specialised SCTP chunks that have their own parameters and need to be serialised + /// differently. + /// + /// The buffer to write the serialised chunk bytes to. It + /// must have the required space already allocated. + /// The number of bytes, including padding, written to the buffer. + private void WriteBytesCore(Span buffer) + { + var bytesWritten = WriteChunkHeader(buffer); - /// - /// Writes the chunk header to the buffer. All chunks use the same three - /// header fields. - /// - /// The buffer to write the chunk header to. - /// The position in the buffer to write at. - /// The padded length of this chunk. - protected void WriteChunkHeader(byte[] buffer, int posn) + if (ChunkValue is { Length: > 0 } chunkValue) { - buffer[posn] = ChunkType; - buffer[posn + 1] = ChunkFlags; - NetConvert.ToBuffer(GetChunkLength(false), buffer, posn + 2); + chunkValue.CopyTo(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH)); } + } - /// - /// Serialises the chunk to a pre-allocated buffer. This method gets overridden - /// by specialised SCTP chunks that have their own parameters and need to be serialised - /// differently. - /// - /// The buffer to write the serialised chunk bytes to. It - /// must have the required space already allocated. - /// The position in the buffer to write to. - /// The number of bytes, including padding, written to the buffer. - public virtual ushort WriteTo(byte[] buffer, int posn) + /// + /// Handler for processing an unrecognised chunk parameter. + /// + /// The Type-Length-Value (TLV) formatted chunk that was + /// not recognised. + /// True if further parameter parsing for this chunk should be stopped. + /// False to continue. + public bool GotUnrecognisedParameter(SctpTlvChunkParameter chunkParameter) + { + bool stop = false; + + switch (chunkParameter.UnrecognisedAction) { - WriteChunkHeader(buffer, posn); + case SctpUnrecognisedParameterActions.Stop: + stop = true; + break; + case SctpUnrecognisedParameterActions.StopAndReport: + stop = true; + UnrecognizedPeerParameters.Add(chunkParameter); + break; + case SctpUnrecognisedParameterActions.Skip: + break; + case SctpUnrecognisedParameterActions.SkipAndReport: + UnrecognizedPeerParameters.Add(chunkParameter); + break; + } - if (ChunkValue?.Length > 0) - { - Buffer.BlockCopy(ChunkValue, 0, buffer, posn + SCTP_CHUNK_HEADER_LENGTH, ChunkValue.Length); - } + return stop; + } - return GetChunkLength(true); + /// + /// Parses a simple chunk and does not attempt to process any chunk value. + /// This method is suitable when: + /// - the chunk type consists only of the 4 byte header and has + /// no fixed or variable parameters set. + /// + /// The buffer holding the serialised chunk. + /// An SCTP chunk instance. + public static SctpChunk ParseBaseChunk(ReadOnlySpan buffer) + { + var chunk = new SctpChunk(); + var chunkLength = chunk.ParseFirstWord(buffer); + if (chunkLength > SCTP_CHUNK_HEADER_LENGTH) + { + chunk.ChunkValue = buffer.Slice(SCTP_CHUNK_HEADER_LENGTH, chunkLength - SCTP_CHUNK_HEADER_LENGTH).ToArray(); } - /// - /// Handler for processing an unrecognised chunk parameter. - /// - /// The Type-Length-Value (TLV) formatted chunk that was - /// not recognised. - /// True if further parameter parsing for this chunk should be stopped. - /// False to continue. - public bool GotUnrecognisedParameter(SctpTlvChunkParameter chunkParameter) - { - bool stop = false; + return chunk; + } - switch (chunkParameter.UnrecognisedAction) - { - case SctpUnrecognisedParameterActions.Stop: - stop = true; - break; - case SctpUnrecognisedParameterActions.StopAndReport: - stop = true; - UnrecognizedPeerParameters.Add(chunkParameter); - break; - case SctpUnrecognisedParameterActions.Skip: - break; - case SctpUnrecognisedParameterActions.SkipAndReport: - UnrecognizedPeerParameters.Add(chunkParameter); - break; - } + /// + /// Chunks can optionally contain Type-Length-Value (TLV) parameters. This method + /// parses any variable length parameters from a chunk's value. + /// + /// The buffer holding the serialised chunk. + /// A list of chunk parameters. Can be empty. + public static ParametersEnumerator GetParameters(ReadOnlySpan buffer) + => new ParametersEnumerator(buffer); - return stop; + /// + /// Parses an SCTP chunk from a buffer. + /// + /// The buffer holding the serialised chunk. + /// An SCTP chunk instance. + public static SctpChunk Parse(ReadOnlySpan buffer) + { + if (buffer.Length < SCTP_CHUNK_HEADER_LENGTH) + { + throw new SipSorceryException("Buffer did not contain the minimum of bytes for an SCTP chunk."); } - /// - /// Parses a simple chunk and does not attempt to process any chunk value. - /// This method is suitable when: - /// - the chunk type consists only of the 4 byte header and has - /// no fixed or variable parameters set. - /// - /// The buffer holding the serialised chunk. - /// The position to start parsing at. - /// An SCTP chunk instance. - public static SctpChunk ParseBaseChunk(byte[] buffer, int posn) + var chunkType = buffer[0]; + + switch ((SctpChunkType)chunkType) { - var chunk = new SctpChunk(); - ushort chunkLength = chunk.ParseFirstWord(buffer, posn); - if (chunkLength > SCTP_CHUNK_HEADER_LENGTH) - { - chunk.ChunkValue = new byte[chunkLength - SCTP_CHUNK_HEADER_LENGTH]; - Buffer.BlockCopy(buffer, posn + SCTP_CHUNK_HEADER_LENGTH, chunk.ChunkValue, 0, chunk.ChunkValue.Length); - } + case SctpChunkType.ABORT: + return SctpAbortChunk.ParseChunk(buffer, true); + case SctpChunkType.DATA: + return SctpDataChunk.ParseChunk(buffer); + case SctpChunkType.ERROR: + return SctpErrorChunk.ParseChunk(buffer, false); + case SctpChunkType.SACK: + return SctpSackChunk.ParseChunk(buffer); + case SctpChunkType.COOKIE_ACK: + case SctpChunkType.COOKIE_ECHO: + case SctpChunkType.HEARTBEAT: + case SctpChunkType.HEARTBEAT_ACK: + case SctpChunkType.SHUTDOWN_ACK: + case SctpChunkType.SHUTDOWN_COMPLETE: + return ParseBaseChunk(buffer); + case SctpChunkType.INIT: + case SctpChunkType.INIT_ACK: + return SctpInitChunk.ParseChunk(buffer); + case SctpChunkType.SHUTDOWN: + return SctpShutdownChunk.ParseChunk(buffer); + default: + if (!SctpChunkTypeExtensions.IsDefined((SctpChunkType)chunkType)) + { + // Shouldn't reach this point. The SCTP packet parsing logic checks if the chunk is + // recognised before attempting to parse it. + throw new SipSorceryException($"SCTP chunk type of {chunkType} was not recognised."); + } - return chunk; + logger.LogSctpImplementParsingLogic((SctpChunkType)chunkType); + return ParseBaseChunk(buffer); } + } - /// - /// Chunks can optionally contain Type-Length-Value (TLV) parameters. This method - /// parses any variable length parameters from a chunk's value. - /// - /// The buffer holding the serialised chunk. - /// The position in the buffer to start parsing variable length - /// parameters from. - /// The length of the TLV chunk parameters in the buffer. - /// A list of chunk parameters. Can be empty. - public static IEnumerable GetParameters(byte[] buffer, int posn, int length) - { - int paramPosn = posn; + /// + /// Extracts the padded length field from a serialised chunk buffer. + /// + /// The buffer holding the serialised chunk. + /// If true the length field will be padded to a 4 byte boundary. + /// The padded length of the serialised chunk. + public static uint GetChunkLengthFromHeader(ReadOnlySpan buffer, bool padded) + { + var len = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2)); + return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; + } - while (paramPosn < posn + length) - { - var chunkParam = SctpTlvChunkParameter.ParseTlvParameter(buffer, paramPosn); + /// + /// If this chunk is unrecognised then this field dictates how the remainder of the + /// SCTP packet should be handled. + /// + public static SctpUnrecognisedChunkActions GetUnrecognisedChunkAction(ushort chunkType) => + (SctpUnrecognisedChunkActions)(chunkType >> 14 & 0x03); - yield return chunkParam; + /// + /// Copies an unrecognised chunk to a byte buffer and returns it. This method is + /// used to assist in reporting unrecognised chunk types. + /// + /// The buffer containing the chunk. + /// A new buffer containing a copy of the chunk. + public static byte[] CopyUnrecognisedChunk(ReadOnlySpan buffer) + { + var length = (int)SctpChunk.GetChunkLengthFromHeader(buffer, true); + return buffer.Slice(0, length).ToArray(); + } - paramPosn += chunkParam.GetParameterLength(true); - } - } + public ref struct ParametersEnumerator + { + private ReadOnlySpan _buffer; + private SctpTlvChunkParameter? _current; - /// - /// Parses an SCTP chunk from a buffer. - /// - /// The buffer holding the serialised chunk. - /// The position to start parsing at. - /// An SCTP chunk instance. - public static SctpChunk Parse(byte[] buffer, int posn) + public ParametersEnumerator(ReadOnlySpan buffer) { - if (buffer.Length < posn + SCTP_CHUNK_HEADER_LENGTH) - { - throw new ApplicationException("Buffer did not contain the minimum of bytes for an SCTP chunk."); - } + _buffer = buffer; + _current = default!; + } + + public SctpTlvChunkParameter Current => _current!; - byte chunkType = buffer[posn]; + public ParametersEnumerator GetEnumerator() => this; - if (Enum.IsDefined(typeof(SctpChunkType), chunkType)) + public bool MoveNext() + { + if (!_buffer.IsEmpty) { - switch ((SctpChunkType)chunkType) + _current = SctpTlvChunkParameter.ParseTlvParameter(_buffer); + var chunkParameterLength = _current.GetParameterLength(true); + + if (chunkParameterLength >= _buffer.Length) { - case SctpChunkType.ABORT: - return SctpAbortChunk.ParseChunk(buffer, posn, true); - case SctpChunkType.DATA: - return SctpDataChunk.ParseChunk(buffer, posn); - case SctpChunkType.ERROR: - return SctpErrorChunk.ParseChunk(buffer, posn, false); - case SctpChunkType.SACK: - return SctpSackChunk.ParseChunk(buffer, posn); - case SctpChunkType.COOKIE_ACK: - case SctpChunkType.COOKIE_ECHO: - case SctpChunkType.HEARTBEAT: - case SctpChunkType.HEARTBEAT_ACK: - case SctpChunkType.SHUTDOWN_ACK: - case SctpChunkType.SHUTDOWN_COMPLETE: - return ParseBaseChunk(buffer, posn); - case SctpChunkType.INIT: - case SctpChunkType.INIT_ACK: - return SctpInitChunk.ParseChunk(buffer, posn); - case SctpChunkType.SHUTDOWN: - return SctpShutdownChunk.ParseChunk(buffer, posn); - default: - logger.LogDebug("TODO: Implement parsing logic for well known chunk type {ChunkType}.", (SctpChunkType)chunkType); - return ParseBaseChunk(buffer, posn); + _buffer = default; + } + else + { + _buffer = _buffer.Slice(chunkParameterLength); } - } - - // Shouldn't reach this point. The SCTP packet parsing logic checks if the chunk is - // recognised before attempting to parse it. - throw new ApplicationException($"SCTP chunk type of {chunkType} was not recognised."); - } - /// - /// Extracts the padded length field from a serialised chunk buffer. - /// - /// The buffer holding the serialised chunk. - /// The start position of the serialised chunk. - /// If true the length field will be padded to a 4 byte boundary. - /// The padded length of the serialised chunk. - public static uint GetChunkLengthFromHeader(byte[] buffer, int posn, bool padded) - { - ushort len = NetConvert.ParseUInt16(buffer, posn + 2); - return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; - } + return true; + } - /// - /// If this chunk is unrecognised then this field dictates how the remainder of the - /// SCTP packet should be handled. - /// - public static SctpUnrecognisedChunkActions GetUnrecognisedChunkAction(ushort chunkType) => - (SctpUnrecognisedChunkActions)(chunkType >> 14 & 0x03); - - /// - /// Copies an unrecognised chunk to a byte buffer and returns it. This method is - /// used to assist in reporting unrecognised chunk types. - /// - /// The buffer containing the chunk. - /// The position in the buffer that the unrecognised chunk starts. - /// A new buffer containing a copy of the chunk. - public static byte[] CopyUnrecognisedChunk(byte[] buffer, int posn) - { - byte[] unrecognised = new byte[SctpChunk.GetChunkLengthFromHeader(buffer, posn, true)]; - Buffer.BlockCopy(buffer, posn, unrecognised, 0, unrecognised.Length); - return unrecognised; + _current = default; + return false; } } } diff --git a/src/SIPSorcery/net/SCTP/Chunks/SctpDataChunk.cs b/src/SIPSorcery/net/SCTP/Chunks/SctpDataChunk.cs index c066683493..bc7a76f990 100644 --- a/src/SIPSorcery/net/SCTP/Chunks/SctpDataChunk.cs +++ b/src/SIPSorcery/net/SCTP/Chunks/SctpDataChunk.cs @@ -18,205 +18,199 @@ //----------------------------------------------------------------------------- using System; -using SIPSorcery.Sys; +using System.Buffers.Binary; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public partial class SctpDataChunk : SctpChunk { - public class SctpDataChunk : SctpChunk + /// + /// An empty data chunk. The main use is to indicate a DATA chunk has + /// already been delivered to the Upper Layer Protocol (ULP) in + /// . + /// + public static SctpDataChunk EmptyDataChunk = new SctpDataChunk(); + + /// + /// The length in bytes of the fixed parameters used by the DATA chunk. + /// + public const int FIXED_PARAMETERS_LENGTH = 12; + + /// + /// The (U)nordered bit, if set to true, indicates that this is an + /// unordered DATA chunk. + /// + public bool Unordered { get; set; } + + /// + /// The (B)eginning fragment bit, if set, indicates the first fragment + /// of a user message. + /// + public bool Begining { get; set; } = true; + + /// + /// The (E)nding fragment bit, if set, indicates the last fragment of + /// a user message. + /// + public bool Ending { get; set; } = true; + + /// + /// This value represents the Transmission Sequence Number (TSN) for + /// this DATA chunk. + /// + public uint TSN; + + /// + /// Identifies the stream to which the following user data belongs. + /// + public ushort StreamID; + + /// + /// This value represents the Stream Sequence Number of the following + /// user data within the stream using the . + /// + public ushort StreamSeqNum; + + /// + /// Payload Protocol Identifier (PPID). This value represents an application + /// (or upper layer) specified protocol identifier.This value is passed to SCTP + /// by its upper layer and sent to its peer. + /// + public uint PPID; + + /// + /// This is the payload user data. + /// + public byte[]? UserData; + + // These properties are used by the data sender. + internal DateTime LastSentAt; + internal int SendCount; + + private SctpDataChunk() + : base(SctpChunkType.DATA) + { } + + /// + /// Creates a new DATA chunk. + /// + /// Must be set to true if the application wants to send this data chunk + /// without requiring it to be delivered to the remote part in order. + /// Must be set to true for the first chunk in a user data payload. + /// Must be set to true for the last chunk in a user data payload. Note that + /// and must both be set to true when the full payload + /// is being sent in a single data chunk. + /// The Transmission Sequence Number for this chunk. + /// Optional. The stream ID for this data chunk. + /// Optional. The stream sequence number for this send. Set to 0 for unordered streams. + /// Optional. The payload protocol ID for this data chunk. + /// The data to send. + public SctpDataChunk( + bool isUnordered, + bool isBegining, + bool isEnd, + uint tsn, + ushort streamID, + ushort seqnum, + uint ppid, + byte[] data) : base(SctpChunkType.DATA) { - /// - /// An empty data chunk. The main use is to indicate a DATA chunk has - /// already been delivered to the Upper Layer Protocol (ULP) in - /// . - /// - public static SctpDataChunk EmptyDataChunk = new SctpDataChunk(); - - /// - /// The length in bytes of the fixed parameters used by the DATA chunk. - /// - public const int FIXED_PARAMETERS_LENGTH = 12; - - /// - /// The (U)nordered bit, if set to true, indicates that this is an - /// unordered DATA chunk. - /// - public bool Unordered { get; set; } = false; - - /// - /// The (B)eginning fragment bit, if set, indicates the first fragment - /// of a user message. - /// - public bool Begining { get; set; } = true; - - /// - /// The (E)nding fragment bit, if set, indicates the last fragment of - /// a user message. - /// - public bool Ending { get; set; } = true; - - /// - /// This value represents the Transmission Sequence Number (TSN) for - /// this DATA chunk. - /// - public uint TSN; - - /// - /// Identifies the stream to which the following user data belongs. - /// - public ushort StreamID; - - /// - /// This value represents the Stream Sequence Number of the following - /// user data within the stream using the . - /// - public ushort StreamSeqNum; - - /// - /// Payload Protocol Identifier (PPID). This value represents an application - /// (or upper layer) specified protocol identifier.This value is passed to SCTP - /// by its upper layer and sent to its peer. - /// - public uint PPID; - - /// - /// This is the payload user data. - /// - public byte[] UserData; - - // These properties are used by the data sender. - internal DateTime LastSentAt; - internal int SendCount; - - private SctpDataChunk() - : base(SctpChunkType.DATA) - { } - - /// - /// Creates a new DATA chunk. - /// - /// Must be set to true if the application wants to send this data chunk - /// without requiring it to be delivered to the remote part in order. - /// Must be set to true for the first chunk in a user data payload. - /// Must be set to true for the last chunk in a user data payload. Note that - /// and must both be set to true when the full payload - /// is being sent in a single data chunk. - /// The Transmission Sequence Number for this chunk. - /// Optional. The stream ID for this data chunk. - /// Optional. The stream sequence number for this send. Set to 0 for unordered streams. - /// Optional. The payload protocol ID for this data chunk. - /// The data to send. - public SctpDataChunk( - bool isUnordered, - bool isBegining, - bool isEnd, - uint tsn, - ushort streamID, - ushort seqnum, - uint ppid, - byte[] data) : base(SctpChunkType.DATA) - { - if (data == null || data.Length == 0) - { - throw new ArgumentNullException("data", "The SctpDataChunk data parameter cannot be empty."); - } - - Unordered = isUnordered; - Begining = isBegining; - Ending = isEnd; - TSN = tsn; - StreamID = streamID; - StreamSeqNum = seqnum; - PPID = ppid; - UserData = data; - - ChunkFlags = (byte)( - (Unordered ? 0x04 : 0x0) + - (Begining ? 0x02 : 0x0) + - (Ending ? 0x01 : 0x0)); - } - - /// - /// Calculates the length for DATA chunk. - /// - /// If true the length field will be padded to a 4 byte boundary. - /// The length of the chunk. - public override ushort GetChunkLength(bool padded) + if (data is null || data.Length == 0) { - ushort len = SCTP_CHUNK_HEADER_LENGTH + FIXED_PARAMETERS_LENGTH; - len += (ushort)(UserData != null ? UserData.Length : 0); - return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; + throw new ArgumentException("The SctpDataChunk data parameter cannot be empty.", nameof(data)); } - /// - /// Serialises a DATA chunk to a pre-allocated buffer. - /// - /// The buffer to write the serialised chunk bytes to. It - /// must have the required space already allocated. - /// The position in the buffer to write to. - /// The number of bytes, including padding, written to the buffer. - public override ushort WriteTo(byte[] buffer, int posn) - { - WriteChunkHeader(buffer, posn); + Unordered = isUnordered; + Begining = isBegining; + Ending = isEnd; + TSN = tsn; + StreamID = streamID; + StreamSeqNum = seqnum; + PPID = ppid; + UserData = data; + + ChunkFlags = (byte)( + (Unordered ? 0x04 : 0x0) + + (Begining ? 0x02 : 0x0) + + (Ending ? 0x01 : 0x0)); + } - // Write fixed parameters. - int startPosn = posn + SCTP_CHUNK_HEADER_LENGTH; + /// + /// Calculates the length for DATA chunk. + /// + /// If true the length field will be padded to a 4 byte boundary. + /// The length of the chunk. + public override ushort GetByteCount(bool padded) + { + ushort len = SCTP_CHUNK_HEADER_LENGTH + FIXED_PARAMETERS_LENGTH; + len += (ushort)(UserData is { } ? UserData.Length : 0); + return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; + } - NetConvert.ToBuffer(TSN, buffer, startPosn); - NetConvert.ToBuffer(StreamID, buffer, startPosn + 4); - NetConvert.ToBuffer(StreamSeqNum, buffer, startPosn + 6); - NetConvert.ToBuffer(PPID, buffer, startPosn + 8); + /// + /// Serialises a DATA chunk to a pre-allocated buffer. + /// + /// The buffer to write the serialised chunk bytes to. It + /// must have the required space already allocated. + /// The number of bytes, including padding, written to the buffer. + public override ushort WriteBytes(Span buffer) + { + WriteBytesCore(buffer); - int userDataPosn = startPosn + FIXED_PARAMETERS_LENGTH; + return GetByteCount(true); + } - if (UserData != null) - { - Buffer.BlockCopy(UserData, 0, buffer, userDataPosn, UserData.Length); - } + private void WriteBytesCore(Span buffer) + { + WriteChunkHeader(buffer); - return GetChunkLength(true); - } + // Write fixed parameters. + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH), TSN); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 4), StreamID); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 6), StreamSeqNum); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 8), PPID); - public bool IsEmpty() + if (UserData is { Length: > 0 } userData) { - return UserData == null; + userData.CopyTo(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + FIXED_PARAMETERS_LENGTH)); } + } - /// - /// Parses the DATA chunk fields - /// - /// The buffer holding the serialised chunk. - /// The position to start parsing at. - public static SctpDataChunk ParseChunk(byte[] buffer, int posn) - { - var dataChunk = new SctpDataChunk(); - ushort chunkLen = dataChunk.ParseFirstWord(buffer, posn); - - if (chunkLen < FIXED_PARAMETERS_LENGTH) - { - throw new ApplicationException($"SCTP data chunk cannot be parsed as buffer too short for fixed parameter fields."); - } + public bool IsEmpty() + { + return UserData is null; + } - dataChunk.Unordered = (dataChunk.ChunkFlags & 0x04) > 0; - dataChunk.Begining = (dataChunk.ChunkFlags & 0x02) > 0; - dataChunk.Ending = (dataChunk.ChunkFlags & 0x01) > 0; + /// + /// Parses the DATA chunk fields + /// + /// The buffer holding the serialised chunk. + public static SctpDataChunk ParseChunk(ReadOnlySpan buffer) + { + var dataChunk = new SctpDataChunk(); + var chunkLen = dataChunk.ParseFirstWord(buffer); - int startPosn = posn + SCTP_CHUNK_HEADER_LENGTH; + if (chunkLen < FIXED_PARAMETERS_LENGTH) + { + throw new SipSorceryException($"SCTP data chunk cannot be parsed as buffer too short for fixed parameter fields."); + } - dataChunk.TSN = NetConvert.ParseUInt32(buffer, startPosn); - dataChunk.StreamID = NetConvert.ParseUInt16(buffer, startPosn + 4); - dataChunk.StreamSeqNum = NetConvert.ParseUInt16(buffer, startPosn + 6); - dataChunk.PPID = NetConvert.ParseUInt32(buffer, startPosn + 8); + dataChunk.Unordered = (dataChunk.ChunkFlags & 0x04) > 0; + dataChunk.Begining = (dataChunk.ChunkFlags & 0x02) > 0; + dataChunk.Ending = (dataChunk.ChunkFlags & 0x01) > 0; - int userDataPosn = startPosn + FIXED_PARAMETERS_LENGTH; - int userDataLen = chunkLen - SCTP_CHUNK_HEADER_LENGTH - FIXED_PARAMETERS_LENGTH; + dataChunk.TSN = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH)); + dataChunk.StreamID = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 4)); + dataChunk.StreamSeqNum = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 6)); + dataChunk.PPID = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 8)); - if (userDataLen > 0) - { - dataChunk.UserData = new byte[userDataLen]; - Buffer.BlockCopy(buffer, userDataPosn, dataChunk.UserData, 0, dataChunk.UserData.Length); - } + var userDataLen = chunkLen - SCTP_CHUNK_HEADER_LENGTH - FIXED_PARAMETERS_LENGTH; - return dataChunk; + if (userDataLen > 0) + { + dataChunk.UserData = buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + FIXED_PARAMETERS_LENGTH, userDataLen).ToArray(); } + + return dataChunk; } } diff --git a/src/SIPSorcery/net/SCTP/Chunks/SctpErrorCauses.cs b/src/SIPSorcery/net/SCTP/Chunks/SctpErrorCauses.cs index 8007d70466..1bc39cdfa7 100644 --- a/src/SIPSorcery/net/SCTP/Chunks/SctpErrorCauses.cs +++ b/src/SIPSorcery/net/SCTP/Chunks/SctpErrorCauses.cs @@ -19,430 +19,449 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Text; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// Defined in https://tools.ietf.org/html/rfc4960#section-3.3.10 +/// +public enum SctpErrorCauseCode : ushort { - /// - /// Defined in https://tools.ietf.org/html/rfc4960#section-3.3.10 - /// - public enum SctpErrorCauseCode : ushort - { - InvalidStreamIdentifier = 1, - MissingMandatoryParameter = 2, - StaleCookieError = 3, - OutOfResource = 4, - UnresolvableAddress = 5, - UnrecognizedChunkType = 6, - InvalidMandatoryParameter = 7, - UnrecognizedParameters = 8, - NoUserData = 9, - CookieReceivedWhileShuttingDown = 10, - RestartAssociationWithNewAddress = 11, - UserInitiatedAbort = 12, - ProtocolViolation = 13 - } + InvalidStreamIdentifier = 1, + MissingMandatoryParameter = 2, + StaleCookieError = 3, + OutOfResource = 4, + UnresolvableAddress = 5, + UnrecognizedChunkType = 6, + InvalidMandatoryParameter = 7, + UnrecognizedParameters = 8, + NoUserData = 9, + CookieReceivedWhileShuttingDown = 10, + RestartAssociationWithNewAddress = 11, + UserInitiatedAbort = 12, + ProtocolViolation = 13 +} - public interface ISctpErrorCause - { - SctpErrorCauseCode CauseCode { get; } - ushort GetErrorCauseLength(bool padded); - int WriteTo(byte[] buffer, int posn); - } +public interface ISctpErrorCause +{ + SctpErrorCauseCode CauseCode { get; } + ushort GetErrorCauseLength(bool padded); + ushort WriteBytes(Span buffer); +} - /// - /// This structure captures all SCTP errors that don't have an additional - /// parameter. - /// - /// - /// Out of Resource: https://tools.ietf.org/html/rfc4960#section-3.3.10.4 - /// Invalid Mandatory Parameter: https://tools.ietf.org/html/rfc4960#section-3.3.10.7 - /// Cookie Received While Shutting Down: https://tools.ietf.org/html/rfc4960#section-3.3.10.10 - /// - public struct SctpCauseOnlyError : ISctpErrorCause - { - private const ushort ERROR_CAUSE_LENGTH = 4; +/// +/// This structure captures all SCTP errors that don't have an additional +/// parameter. +/// +/// +/// Out of Resource: https://tools.ietf.org/html/rfc4960#section-3.3.10.4 +/// Invalid Mandatory Parameter: https://tools.ietf.org/html/rfc4960#section-3.3.10.7 +/// Cookie Received While Shutting Down: https://tools.ietf.org/html/rfc4960#section-3.3.10.10 +/// +public struct SctpCauseOnlyError : ISctpErrorCause +{ + private const ushort ERROR_CAUSE_LENGTH = 4; - public static readonly List SupportedErrorCauses = - new List - { - SctpErrorCauseCode.OutOfResource, - SctpErrorCauseCode.InvalidMandatoryParameter, - SctpErrorCauseCode.CookieReceivedWhileShuttingDown - }; + public static readonly List SupportedErrorCauses = + new List + { + SctpErrorCauseCode.OutOfResource, + SctpErrorCauseCode.InvalidMandatoryParameter, + SctpErrorCauseCode.CookieReceivedWhileShuttingDown + }; - public SctpErrorCauseCode CauseCode { get; private set; } + public SctpErrorCauseCode CauseCode { get; private set; } - public SctpCauseOnlyError(SctpErrorCauseCode causeCode) + public SctpCauseOnlyError(SctpErrorCauseCode causeCode) + { + if (!SupportedErrorCauses.Contains(causeCode)) { - if (!SupportedErrorCauses.Contains(causeCode)) - { - throw new ApplicationException($"SCTP error struct should not be used for {causeCode}, use the specific error type."); - } - - CauseCode = causeCode; + throw new SipSorceryException($"SCTP error struct should not be used for {causeCode}, use the specific error type."); } - public ushort GetErrorCauseLength(bool padded) => ERROR_CAUSE_LENGTH; + CauseCode = causeCode; + } - public int WriteTo(byte[] buffer, int posn) - { - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(ERROR_CAUSE_LENGTH, buffer, posn + 2); - return ERROR_CAUSE_LENGTH; - } + public ushort GetErrorCauseLength(bool padded) => ERROR_CAUSE_LENGTH; + + public ushort WriteBytes(Span buffer) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), ERROR_CAUSE_LENGTH); + return ERROR_CAUSE_LENGTH; } +} + +/// +/// Invalid Stream Identifier: Indicates endpoint received a DATA chunk +/// sent to a nonexistent stream. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.10.1 +/// +public struct SctpErrorInvalidStreamIdentifier : ISctpErrorCause +{ + private const ushort ERROR_CAUSE_LENGTH = 8; + + public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.InvalidStreamIdentifier; /// - /// Invalid Stream Identifier: Indicates endpoint received a DATA chunk - /// sent to a nonexistent stream. + /// The invalid stream identifier. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.10.1 - /// - public struct SctpErrorInvalidStreamIdentifier : ISctpErrorCause - { - private const ushort ERROR_CAUSE_LENGTH = 8; + public ushort StreamID; - public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.InvalidStreamIdentifier; + public ushort GetErrorCauseLength(bool padded) => ERROR_CAUSE_LENGTH; - /// - /// The invalid stream identifier. - /// - public ushort StreamID; + public ushort WriteBytes(Span buffer) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), ERROR_CAUSE_LENGTH); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(4), StreamID); + return ERROR_CAUSE_LENGTH; + } +} - public ushort GetErrorCauseLength(bool padded) => ERROR_CAUSE_LENGTH; +/// +/// Indicates that one or more mandatory Type-Length-Value (TLV) format +/// parameters are missing in a received INIT or INIT ACK. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.10.2 +/// +public struct SctpErrorMissingMandatoryParameter : ISctpErrorCause +{ + public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.MissingMandatoryParameter; - public int WriteTo(byte[] buffer, int posn) - { - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(ERROR_CAUSE_LENGTH, buffer, posn + 2); - NetConvert.ToBuffer(StreamID, buffer, posn + 4); - return ERROR_CAUSE_LENGTH; - } - } + public List MissingParameters; - /// - /// Indicates that one or more mandatory Type-Length-Value (TLV) format - /// parameters are missing in a received INIT or INIT ACK. - /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.10.2 - /// - public struct SctpErrorMissingMandatoryParameter : ISctpErrorCause + public ushort GetErrorCauseLength(bool padded) { - public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.MissingMandatoryParameter; + var len = (ushort)(4 + ((MissingParameters is { }) ? MissingParameters.Count * 2 : 0)); + return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; + } - public List MissingParameters; + public ushort WriteBytes(Span buffer) + { + var len = GetErrorCauseLength(true); - public ushort GetErrorCauseLength(bool padded) - { - ushort len = (ushort)(4 + ((MissingParameters != null) ? MissingParameters.Count * 2 : 0)); - return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; - } + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), len); - public int WriteTo(byte[] buffer, int posn) + if (MissingParameters is { Count: > 0 } missingParameters) { - var len = GetErrorCauseLength(true); - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(len, buffer, posn + 2); - if (MissingParameters != null) + buffer = buffer.Slice(4); + foreach (var missing in missingParameters) { - int valPosn = posn + 4; - foreach (var missing in MissingParameters) - { - NetConvert.ToBuffer(missing, buffer, valPosn); - valPosn += 2; - } + BinaryPrimitives.WriteUInt16BigEndian(buffer, missing); + buffer = buffer.Slice(2); } - return len; } + + return len; } +} - /// - /// Indicates the receipt of a valid State Cookie that has expired. - /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.10.3 - /// - public struct SctpErrorStaleCookieError : ISctpErrorCause - { - private const ushort ERROR_CAUSE_LENGTH = 8; +/// +/// Indicates the receipt of a valid State Cookie that has expired. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.10.3 +/// +public struct SctpErrorStaleCookieError : ISctpErrorCause +{ + private const ushort ERROR_CAUSE_LENGTH = 8; - public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.StaleCookieError; + public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.StaleCookieError; - /// - /// The difference, in microseconds, between the current time and the time the State Cookie expired. - /// - public uint MeasureOfStaleness; + /// + /// The difference, in microseconds, between the current time and the time the State Cookie expired. + /// + public uint MeasureOfStaleness; - public ushort GetErrorCauseLength(bool padded) => ERROR_CAUSE_LENGTH; + public ushort GetErrorCauseLength(bool padded) => ERROR_CAUSE_LENGTH; - public int WriteTo(byte[] buffer, int posn) - { - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(ERROR_CAUSE_LENGTH, buffer, posn + 2); - NetConvert.ToBuffer(MeasureOfStaleness, buffer, posn + 4); - return ERROR_CAUSE_LENGTH; - } + public ushort WriteBytes(Span buffer) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), ERROR_CAUSE_LENGTH); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4), MeasureOfStaleness); + return ERROR_CAUSE_LENGTH; } +} + +/// +/// Indicates that the sender is not able to resolve the specified address parameter +/// (e.g., type of address is not supported by the sender). This is usually sent in +/// combination with or within an ABORT. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.10.5 +/// +public struct SctpErrorUnresolvableAddress : ISctpErrorCause +{ + public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.UnresolvableAddress; /// - /// Indicates that the sender is not able to resolve the specified address parameter - /// (e.g., type of address is not supported by the sender). This is usually sent in - /// combination with or within an ABORT. + /// The Unresolvable Address field contains the complete Type, Length, + /// and Value of the address parameter(or Host Name parameter) that + /// contains the unresolvable address or host name. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.10.5 - /// - public struct SctpErrorUnresolvableAddress : ISctpErrorCause + public byte[]? UnresolvableAddress; + + public ushort GetErrorCauseLength(bool padded) + { + var len = (ushort)(4 + ((UnresolvableAddress is { }) ? UnresolvableAddress.Length : 0)); + return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; + } + + public ushort WriteBytes(Span buffer) { - public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.UnresolvableAddress; + var len = GetErrorCauseLength(true); - /// - /// The Unresolvable Address field contains the complete Type, Length, - /// and Value of the address parameter(or Host Name parameter) that - /// contains the unresolvable address or host name. - /// - public byte[] UnresolvableAddress; + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), len); - public ushort GetErrorCauseLength(bool padded) + if (UnresolvableAddress is { Length: > 0 } uresolvableAddress) { - ushort len = (ushort)(4 + ((UnresolvableAddress != null) ? UnresolvableAddress.Length : 0)); - return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; + uresolvableAddress.CopyTo(buffer.Slice(4)); } - public int WriteTo(byte[] buffer, int posn) - { - var len = GetErrorCauseLength(true); - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(len, buffer, posn + 2); - if (UnresolvableAddress != null) - { - Buffer.BlockCopy(UnresolvableAddress, 0, buffer, posn + 4, UnresolvableAddress.Length); - } - return len; - } + return len; } +} + +/// +/// Indicates that the sender is out of resource. This +/// is usually sent in combination with or within an ABORT. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.10.6 +/// +public struct SctpErrorUnrecognizedChunkType : ISctpErrorCause +{ + public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.UnrecognizedChunkType; /// - /// Indicates that the sender is out of resource. This - /// is usually sent in combination with or within an ABORT. + /// The Unrecognized Chunk field contains the unrecognized chunk from + /// the SCTP packet complete with Chunk Type, Chunk Flags, and Chunk + /// Length. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.10.6 - /// - public struct SctpErrorUnrecognizedChunkType : ISctpErrorCause + public byte[]? UnrecognizedChunk; + + public ushort GetErrorCauseLength(bool padded) { - public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.UnrecognizedChunkType; + var len = (ushort)(4 + ((UnrecognizedChunk is { }) ? UnrecognizedChunk.Length : 0)); + return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; + } - /// - /// The Unrecognized Chunk field contains the unrecognized chunk from - /// the SCTP packet complete with Chunk Type, Chunk Flags, and Chunk - /// Length. - /// - public byte[] UnrecognizedChunk; + public ushort WriteBytes(Span buffer) + { + var len = GetErrorCauseLength(true); - public ushort GetErrorCauseLength(bool padded) - { - ushort len = (ushort)(4 + ((UnrecognizedChunk != null) ? UnrecognizedChunk.Length : 0)); - return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; - } + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), len); - public int WriteTo(byte[] buffer, int posn) + if (UnrecognizedChunk is { Length: > 0 } unrecognizedChunk) { - var len = GetErrorCauseLength(true); - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(len, buffer, posn + 2); - if (UnrecognizedChunk != null) - { - Buffer.BlockCopy(UnrecognizedChunk, 0, buffer, posn + 4, UnrecognizedChunk.Length); - } - return len; + unrecognizedChunk.CopyTo(buffer.Slice(4)); } + + return len; } +} + +/// +/// This error cause is returned to the originator of the INIT ACK chunk +/// if the receiver does not recognize one or more optional variable parameters in +/// the INIT ACK chunk. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.10.8 +/// +public struct SctpErrorUnrecognizedParameters : ISctpErrorCause +{ + public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.UnrecognizedParameters; /// - /// This error cause is returned to the originator of the INIT ACK chunk - /// if the receiver does not recognize one or more optional variable parameters in - /// the INIT ACK chunk. + /// The Unrecognized Parameters field contains the unrecognized + /// parameters copied from the INIT ACK chunk complete with TLV. This + /// error cause is normally contained in an ERROR chunk bundled with + /// the COOKIE ECHO chunk when responding to the INIT ACK, when the + /// sender of the COOKIE ECHO chunk wishes to report unrecognized + /// parameters. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.10.8 - /// - public struct SctpErrorUnrecognizedParameters : ISctpErrorCause + public byte[]? UnrecognizedParameters; + + public ushort GetErrorCauseLength(bool padded) { - public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.UnrecognizedParameters; - - /// - /// The Unrecognized Parameters field contains the unrecognized - /// parameters copied from the INIT ACK chunk complete with TLV. This - /// error cause is normally contained in an ERROR chunk bundled with - /// the COOKIE ECHO chunk when responding to the INIT ACK, when the - /// sender of the COOKIE ECHO chunk wishes to report unrecognized - /// parameters. - /// - public byte[] UnrecognizedParameters; - - public ushort GetErrorCauseLength(bool padded) - { - ushort len = (ushort)(4 + ((UnrecognizedParameters != null) ? UnrecognizedParameters.Length : 0)); - return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; - } + var len = (ushort)(4 + ((UnrecognizedParameters is { }) ? UnrecognizedParameters.Length : 0)); + return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; + } - public int WriteTo(byte[] buffer, int posn) + public ushort WriteBytes(Span buffer_) + { + var len = GetErrorCauseLength(true); + + BinaryPrimitives.WriteUInt16BigEndian(buffer_, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer_.Slice(2), len); + + if (UnrecognizedParameters is { Length: > 0 } unrecognizedParameters) { - var len = GetErrorCauseLength(true); - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(len, buffer, posn + 2); - if (UnrecognizedParameters != null) - { - Buffer.BlockCopy(UnrecognizedParameters, 0, buffer, posn + 4, UnrecognizedParameters.Length); - } - return len; + unrecognizedParameters.CopyTo(buffer_.Slice(4)); } + + return len; } +} - /// - /// This error cause is returned to the originator of a - /// DATA chunk if a received DATA chunk has no user data. - /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.10.9 - /// - public struct SctpErrorNoUserData : ISctpErrorCause - { - private const ushort ERROR_CAUSE_LENGTH = 8; +/// +/// This error cause is returned to the originator of a +/// DATA chunk if a received DATA chunk has no user data. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.10.9 +/// +public struct SctpErrorNoUserData : ISctpErrorCause +{ + private const ushort ERROR_CAUSE_LENGTH = 8; - public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.NoUserData; + public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.NoUserData; - /// - /// The TSN value field contains the TSN of the DATA chunk received - /// with no user data field. - /// - public uint TSN; + /// + /// The TSN value field contains the TSN of the DATA chunk received + /// with no user data field. + /// + public uint TSN; - public ushort GetErrorCauseLength(bool padded) => ERROR_CAUSE_LENGTH; + public ushort GetErrorCauseLength(bool padded) => ERROR_CAUSE_LENGTH; - public int WriteTo(byte[] buffer, int posn) - { - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(ERROR_CAUSE_LENGTH, buffer, posn + 2); - NetConvert.ToBuffer(TSN, buffer, posn + 4); - return ERROR_CAUSE_LENGTH; - } + public ushort WriteBytes(Span buffer) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), ERROR_CAUSE_LENGTH); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4), TSN); + return ERROR_CAUSE_LENGTH; } +} + +/// +/// An INIT was received on an existing association.But the INIT added addresses to the +/// association that were previously NOT part of the association. The new addresses are +/// listed in the error code.This ERROR is normally sent as part of an ABORT refusing the INIT. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.10.11 +/// +public struct SctpErrorRestartAssociationWithNewAddress : ISctpErrorCause +{ + public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.RestartAssociationWithNewAddress; /// - /// An INIT was received on an existing association.But the INIT added addresses to the - /// association that were previously NOT part of the association. The new addresses are - /// listed in the error code.This ERROR is normally sent as part of an ABORT refusing the INIT. + /// Each New Address TLV is an exact copy of the TLV that was found + /// in the INIT chunk that was new, including the Parameter Type and the + /// Parameter Length. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.10.11 - /// - public struct SctpErrorRestartAssociationWithNewAddress : ISctpErrorCause + public byte[]? NewAddressTLVs; + + public ushort GetErrorCauseLength(bool padded) { - public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.RestartAssociationWithNewAddress; + var len = (ushort)(4 + ((NewAddressTLVs is { }) ? NewAddressTLVs.Length : 0)); + return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; + } - /// - /// Each New Address TLV is an exact copy of the TLV that was found - /// in the INIT chunk that was new, including the Parameter Type and the - /// Parameter Length. - /// - public byte[] NewAddressTLVs; + public ushort WriteBytes(Span buffer) + { + var len = GetErrorCauseLength(true); - public ushort GetErrorCauseLength(bool padded) - { - ushort len = (ushort)(4 + ((NewAddressTLVs != null) ? NewAddressTLVs.Length : 0)); - return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; - } + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), len); - public int WriteTo(byte[] buffer, int posn) + if (NewAddressTLVs is { Length: > 0 } newAddressTLVs) { - var len = GetErrorCauseLength(true); - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(len, buffer, posn + 2); - if (NewAddressTLVs != null) - { - Buffer.BlockCopy(NewAddressTLVs, 0, buffer, posn + 4, NewAddressTLVs.Length); - } - return len; + newAddressTLVs.CopyTo(buffer.Slice(4)); } + + return len; } +} + +/// +/// This error cause MAY be included in ABORT chunks that are sent +/// because of an upper-layer request. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.10.12 +/// +public struct SctpErrorUserInitiatedAbort : ISctpErrorCause +{ + public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.UserInitiatedAbort; /// - /// This error cause MAY be included in ABORT chunks that are sent - /// because of an upper-layer request. + /// Optional descriptive abort reason from Upper Layer Protocol (ULP). /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.10.12 - /// - public struct SctpErrorUserInitiatedAbort : ISctpErrorCause + public string? AbortReason; + + public ushort GetErrorCauseLength(bool padded) + { + var len = (ushort)(4 + ((!string.IsNullOrEmpty(AbortReason)) ? Encoding.UTF8.GetByteCount(AbortReason) : 0)); + return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; + } + + public ushort WriteBytes(Span buffer_) { - public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.UserInitiatedAbort; + var len = GetErrorCauseLength(true); - /// - /// Optional descriptive abort reason from Upper Layer Protocol (ULP). - /// - public string AbortReason; + BinaryPrimitives.WriteUInt16BigEndian(buffer_, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer_.Slice(2), len); - public ushort GetErrorCauseLength(bool padded) + if (!string.IsNullOrEmpty(AbortReason)) { - ushort len = (ushort)(4 + ((!string.IsNullOrEmpty(AbortReason)) ? Encoding.UTF8.GetByteCount(AbortReason) : 0)); - return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; + Encoding.UTF8.GetBytes(AbortReason.AsSpan(), buffer_.Slice(4)); } - public int WriteTo(byte[] buffer, int posn) - { - var len = GetErrorCauseLength(true); - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(len, buffer, posn + 2); - if (!string.IsNullOrEmpty(AbortReason)) - { - var reasonBuffer = Encoding.UTF8.GetBytes(AbortReason); - Buffer.BlockCopy(reasonBuffer, 0, buffer, posn + 4, reasonBuffer.Length); - } - return len; - } + return len; } +} + +/// +/// This error cause MAY be included in ABORT chunks that are sent +/// because an SCTP endpoint detects a protocol violation of the peer +/// that is not covered by any of the more specific error causes +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.10.13 +/// +public struct SctpErrorProtocolViolation : ISctpErrorCause +{ + public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.ProtocolViolation; /// - /// This error cause MAY be included in ABORT chunks that are sent - /// because an SCTP endpoint detects a protocol violation of the peer - /// that is not covered by any of the more specific error causes + /// Optional description of the violation. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.10.13 - /// - public struct SctpErrorProtocolViolation : ISctpErrorCause + public string? AdditionalInformation; + + public ushort GetErrorCauseLength(bool padded) { - public SctpErrorCauseCode CauseCode => SctpErrorCauseCode.ProtocolViolation; + var len = (ushort)(4 + ((!string.IsNullOrEmpty(AdditionalInformation)) ? Encoding.UTF8.GetByteCount(AdditionalInformation) : 0)); + return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; + } - /// - /// Optional description of the violation. - /// - public string AdditionalInformation; + public ushort WriteBytes(Span buffer) + { + var len = GetErrorCauseLength(true); - public ushort GetErrorCauseLength(bool padded) - { - ushort len = (ushort)(4 + ((!string.IsNullOrEmpty(AdditionalInformation)) ? Encoding.UTF8.GetByteCount(AdditionalInformation) : 0)); - return padded ? SctpPadding.PadTo4ByteBoundary(len) : len; - } + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)CauseCode); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), len); - public int WriteTo(byte[] buffer, int posn) + if (!string.IsNullOrEmpty(AdditionalInformation)) { - var len = GetErrorCauseLength(true); - NetConvert.ToBuffer((ushort)CauseCode, buffer, posn); - NetConvert.ToBuffer(len, buffer, posn + 2); - if (!string.IsNullOrEmpty(AdditionalInformation)) - { - var reasonBuffer = Encoding.UTF8.GetBytes(AdditionalInformation); - Buffer.BlockCopy(reasonBuffer, 0, buffer, posn + 4, reasonBuffer.Length); - } - return len; + Encoding.UTF8.GetBytes(AdditionalInformation.AsSpan(), buffer.Slice(4)); } + + return len; } } diff --git a/src/SIPSorcery/net/SCTP/Chunks/SctpErrorChunk.cs b/src/SIPSorcery/net/SCTP/Chunks/SctpErrorChunk.cs index 9c4358a41a..10a0440451 100644 --- a/src/SIPSorcery/net/SCTP/Chunks/SctpErrorChunk.cs +++ b/src/SIPSorcery/net/SCTP/Chunks/SctpErrorChunk.cs @@ -18,213 +18,219 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Text; -using Microsoft.Extensions.Logging; -using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// An endpoint sends this chunk to its peer endpoint to notify it of +/// certain error conditions. It contains one or more error causes. An +/// Operation Error is not considered fatal in and of itself, but may be +/// used with an ABORT chunk to report a fatal condition. +/// +public partial class SctpErrorChunk : SctpChunk { + private const byte ABORT_CHUNK_TBIT_FLAG = 0x01; + + public List ErrorCauses { get; private set; } = new List(); + /// - /// An endpoint sends this chunk to its peer endpoint to notify it of - /// certain error conditions. It contains one or more error causes. An - /// Operation Error is not considered fatal in and of itself, but may be - /// used with an ABORT chunk to report a fatal condition. + /// This constructor is for the ABORT chunk type which is identical to the + /// ERROR chunk except for the optional verification tag bit. /// - public class SctpErrorChunk : SctpChunk + /// The chunk type, typically ABORT. + /// + protected SctpErrorChunk(SctpChunkType chunkType, bool verificationTagBit) + : base(chunkType) { - private const byte ABORT_CHUNK_TBIT_FLAG = 0x01; - - public List ErrorCauses { get; private set; } = new List(); - - /// - /// This constructor is for the ABORT chunk type which is identical to the - /// ERROR chunk except for the optional verification tag bit. - /// - /// The chunk type, typically ABORT. - /// - protected SctpErrorChunk(SctpChunkType chunkType, bool verificationTagBit) - : base(chunkType) + if (verificationTagBit) { - if(verificationTagBit) - { - ChunkFlags = ABORT_CHUNK_TBIT_FLAG; - } + ChunkFlags = ABORT_CHUNK_TBIT_FLAG; } + } - public SctpErrorChunk() : base(SctpChunkType.ERROR) - { } - - /// - /// Creates a new ERROR chunk. - /// - /// The initial error cause code to set on this chunk. - public SctpErrorChunk(SctpErrorCauseCode errorCauseCode) : - this(new SctpCauseOnlyError(errorCauseCode)) - { } - - /// - /// Creates a new ERROR chunk. - /// - /// The initial error cause to set on this chunk. - public SctpErrorChunk(ISctpErrorCause errorCause) : base(SctpChunkType.ERROR) - { - ErrorCauses.Add(errorCause); - } + public SctpErrorChunk() : base(SctpChunkType.ERROR) + { } - /// - /// Adds an additional error cause parameter to the chunk. - /// - /// The additional error cause to add to the chunk. - public void AddErrorCause(ISctpErrorCause errorCause) - { - ErrorCauses.Add(errorCause); - } + /// + /// Creates a new ERROR chunk. + /// + /// The initial error cause code to set on this chunk. + public SctpErrorChunk(SctpErrorCauseCode errorCauseCode) : + this(new SctpCauseOnlyError(errorCauseCode)) + { } + + /// + /// Creates a new ERROR chunk. + /// + /// The initial error cause to set on this chunk. + public SctpErrorChunk(ISctpErrorCause errorCause) : base(SctpChunkType.ERROR) + { + ErrorCauses.Add(errorCause); + } + + /// + /// Adds an additional error cause parameter to the chunk. + /// + /// The additional error cause to add to the chunk. + public void AddErrorCause(ISctpErrorCause errorCause) + { + ErrorCauses.Add(errorCause); + } - /// - /// Calculates the length for the chunk. - /// - /// If true the length field will be padded to a 4 byte boundary. - /// The padded length of the chunk. - public override ushort GetChunkLength(bool padded) + /// + /// Calculates the length for the chunk. + /// + /// If true the length field will be padded to a 4 byte boundary. + /// The padded length of the chunk. + public override ushort GetByteCount(bool padded) + { + ushort len = SCTP_CHUNK_HEADER_LENGTH; + if (ErrorCauses is { } && ErrorCauses.Count > 0) { - ushort len = SCTP_CHUNK_HEADER_LENGTH; - if(ErrorCauses != null && ErrorCauses.Count > 0) + foreach (var cause in ErrorCauses) { - foreach(var cause in ErrorCauses) - { - len += cause.GetErrorCauseLength(padded); - } + len += cause.GetErrorCauseLength(padded); } - return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; } + return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; + } + + /// + /// Serialises the ERROR chunk to a pre-allocated buffer. + /// + /// The buffer to write the serialised chunk bytes to. It + /// must have the required space already allocated. + /// The number of bytes, including padding, written to the buffer. + public override ushort WriteBytes(Span buffer) + { + WriteBytesCore(buffer); - /// - /// Serialises the ERROR chunk to a pre-allocated buffer. - /// - /// The buffer to write the serialised chunk bytes to. It - /// must have the required space already allocated. - /// The position in the buffer to write to. - /// The number of bytes, including padding, written to the buffer. - public override ushort WriteTo(byte[] buffer, int posn) + return GetByteCount(true); + } + + private void WriteBytesCore(Span buffer) + { + WriteChunkHeader(buffer); + + if (ErrorCauses is { Count: > 0 } errorCauses) { - WriteChunkHeader(buffer, posn); - if (ErrorCauses != null && ErrorCauses.Count > 0) + buffer = buffer.Slice(4); + foreach (var cause in errorCauses) { - int causePosn = posn + 4; - foreach (var cause in ErrorCauses) - { - causePosn += cause.WriteTo(buffer, causePosn); - } + var bytesWritten = cause.WriteBytes(buffer); + buffer = buffer.Slice(bytesWritten); } - return GetChunkLength(true); } + } - /// - /// Parses the ERROR chunk fields. - /// - /// The buffer holding the serialised chunk. - /// The position to start parsing at. - public static SctpErrorChunk ParseChunk(byte[] buffer, int posn, bool isAbort) - { - var errorChunk = (isAbort) ? new SctpAbortChunk(false) : new SctpErrorChunk(); - ushort chunkLen = errorChunk.ParseFirstWord(buffer, posn); + /// + /// Parses the ERROR chunk fields. + /// + /// The buffer holding the serialised chunk. + public static SctpErrorChunk ParseChunk(ReadOnlySpan buffer, bool isAbort) + { + var errorChunk = (isAbort) ? new SctpAbortChunk(false) : new SctpErrorChunk(); + var chunkLen = errorChunk.ParseFirstWord(buffer); + + var paramPosn = SCTP_CHUNK_HEADER_LENGTH; + var paramsBufferLength = chunkLen - SCTP_CHUNK_HEADER_LENGTH; - int paramPosn = posn + SCTP_CHUNK_HEADER_LENGTH; - int paramsBufferLength = chunkLen - SCTP_CHUNK_HEADER_LENGTH; + if (paramPosn < paramsBufferLength) + { + var stopProcessing = false; - if (paramPosn < paramsBufferLength) + foreach (var varParam in GetParameters(buffer.Slice(paramPosn, paramsBufferLength))) { - bool stopProcessing = false; + Debug.Assert(varParam is { }); - foreach (var varParam in GetParameters(buffer, paramPosn, paramsBufferLength)) + switch (varParam.ParameterType) { - switch (varParam.ParameterType) - { - case (ushort)SctpErrorCauseCode.InvalidStreamIdentifier: - ushort streamID = (ushort)((varParam.ParameterValue != null) ? - NetConvert.ParseUInt16(varParam.ParameterValue, 0) : 0); - var invalidStreamID = new SctpErrorInvalidStreamIdentifier { StreamID = streamID }; - errorChunk.AddErrorCause(invalidStreamID); - break; - case (ushort)SctpErrorCauseCode.MissingMandatoryParameter: - List missingIDs = new List(); - if (varParam.ParameterValue != null) + case (ushort)SctpErrorCauseCode.InvalidStreamIdentifier: + var streamID = (ushort)((varParam.ParameterValue is { }) ? + BinaryPrimitives.ReadUInt16BigEndian(varParam.ParameterValue) : 0); + var invalidStreamID = new SctpErrorInvalidStreamIdentifier { StreamID = streamID }; + errorChunk.AddErrorCause(invalidStreamID); + break; + case (ushort)SctpErrorCauseCode.MissingMandatoryParameter: + var missingIDs = new List(); + if (varParam.ParameterValue is { }) + { + for (var i = 0; i < varParam.ParameterValue.Length; i += 2) { - for (int i = 0; i < varParam.ParameterValue.Length; i += 2) - { - missingIDs.Add(NetConvert.ParseUInt16(varParam.ParameterValue, i)); - } + missingIDs.Add(BinaryPrimitives.ReadUInt16BigEndian(varParam.ParameterValue.AsSpan(i))); } - var missingMandatory = new SctpErrorMissingMandatoryParameter { MissingParameters = missingIDs }; - errorChunk.AddErrorCause(missingMandatory); - break; - case (ushort)SctpErrorCauseCode.StaleCookieError: - uint staleness = (uint)((varParam.ParameterValue != null) ? - NetConvert.ParseUInt32(varParam.ParameterValue, 0) : 0); - var staleCookie = new SctpErrorStaleCookieError { MeasureOfStaleness = staleness }; - errorChunk.AddErrorCause(staleCookie); - break; - case (ushort)SctpErrorCauseCode.OutOfResource: - errorChunk.AddErrorCause(new SctpCauseOnlyError(SctpErrorCauseCode.OutOfResource)); - break; - case (ushort)SctpErrorCauseCode.UnresolvableAddress: - var unresolvable = new SctpErrorUnresolvableAddress { UnresolvableAddress = varParam.ParameterValue }; - errorChunk.AddErrorCause(unresolvable); - break; - case (ushort)SctpErrorCauseCode.UnrecognizedChunkType: - var unrecognised = new SctpErrorUnrecognizedChunkType { UnrecognizedChunk = varParam.ParameterValue }; - errorChunk.AddErrorCause(unrecognised); - break; - case (ushort)SctpErrorCauseCode.InvalidMandatoryParameter: - errorChunk.AddErrorCause(new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter)); - break; - case (ushort)SctpErrorCauseCode.UnrecognizedParameters: - var unrecognisedParams = new SctpErrorUnrecognizedParameters { UnrecognizedParameters = varParam.ParameterValue }; - errorChunk.AddErrorCause(unrecognisedParams); - break; - case (ushort)SctpErrorCauseCode.NoUserData: - uint tsn = (uint)((varParam.ParameterValue != null) ? - NetConvert.ParseUInt32(varParam.ParameterValue, 0) : 0); - var noData = new SctpErrorNoUserData { TSN = tsn }; - errorChunk.AddErrorCause(noData); - break; - case (ushort)SctpErrorCauseCode.CookieReceivedWhileShuttingDown: - errorChunk.AddErrorCause(new SctpCauseOnlyError(SctpErrorCauseCode.CookieReceivedWhileShuttingDown)); - break; - case (ushort)SctpErrorCauseCode.RestartAssociationWithNewAddress: - var restartAddress = new SctpErrorRestartAssociationWithNewAddress - { NewAddressTLVs = varParam.ParameterValue }; - errorChunk.AddErrorCause(restartAddress); - break; - case (ushort)SctpErrorCauseCode.UserInitiatedAbort: - string reason = (varParam.ParameterValue != null) ? - Encoding.UTF8.GetString(varParam.ParameterValue) : null; - var userAbort = new SctpErrorUserInitiatedAbort { AbortReason = reason }; - errorChunk.AddErrorCause(userAbort); - break; - case (ushort)SctpErrorCauseCode.ProtocolViolation: - string info = (varParam.ParameterValue != null) ? - Encoding.UTF8.GetString(varParam.ParameterValue) : null; - var protocolViolation = new SctpErrorProtocolViolation { AdditionalInformation = info }; - errorChunk.AddErrorCause(protocolViolation); - break; - default: - // Parameter was not recognised. - errorChunk.GotUnrecognisedParameter(varParam); - break; - } - - if (stopProcessing) - { - logger.LogWarning("SCTP unrecognised parameter {ParameterType} for chunk type {ChunkType} indicated no further chunks should be processed.", varParam.ParameterType, SctpChunkType.ERROR); + } + var missingMandatory = new SctpErrorMissingMandatoryParameter { MissingParameters = missingIDs }; + errorChunk.AddErrorCause(missingMandatory); + break; + case (ushort)SctpErrorCauseCode.StaleCookieError: + var staleness = (uint)((varParam.ParameterValue is { }) ? + BinaryPrimitives.ReadUInt32BigEndian(varParam.ParameterValue) : 0); + var staleCookie = new SctpErrorStaleCookieError { MeasureOfStaleness = staleness }; + errorChunk.AddErrorCause(staleCookie); + break; + case (ushort)SctpErrorCauseCode.OutOfResource: + errorChunk.AddErrorCause(new SctpCauseOnlyError(SctpErrorCauseCode.OutOfResource)); + break; + case (ushort)SctpErrorCauseCode.UnresolvableAddress: + var unresolvable = new SctpErrorUnresolvableAddress { UnresolvableAddress = varParam.ParameterValue }; + errorChunk.AddErrorCause(unresolvable); + break; + case (ushort)SctpErrorCauseCode.UnrecognizedChunkType: + var unrecognised = new SctpErrorUnrecognizedChunkType { UnrecognizedChunk = varParam.ParameterValue }; + errorChunk.AddErrorCause(unrecognised); + break; + case (ushort)SctpErrorCauseCode.InvalidMandatoryParameter: + errorChunk.AddErrorCause(new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter)); + break; + case (ushort)SctpErrorCauseCode.UnrecognizedParameters: + var unrecognisedParams = new SctpErrorUnrecognizedParameters { UnrecognizedParameters = varParam.ParameterValue }; + errorChunk.AddErrorCause(unrecognisedParams); + break; + case (ushort)SctpErrorCauseCode.NoUserData: + uint tsn = (uint)((varParam.ParameterValue is { }) ? + BinaryPrimitives.ReadUInt32BigEndian(varParam.ParameterValue) : 0); + var noData = new SctpErrorNoUserData { TSN = tsn }; + errorChunk.AddErrorCause(noData); + break; + case (ushort)SctpErrorCauseCode.CookieReceivedWhileShuttingDown: + errorChunk.AddErrorCause(new SctpCauseOnlyError(SctpErrorCauseCode.CookieReceivedWhileShuttingDown)); + break; + case (ushort)SctpErrorCauseCode.RestartAssociationWithNewAddress: + var restartAddress = new SctpErrorRestartAssociationWithNewAddress + { NewAddressTLVs = varParam.ParameterValue }; + errorChunk.AddErrorCause(restartAddress); + break; + case (ushort)SctpErrorCauseCode.UserInitiatedAbort: + var reason = (varParam.ParameterValue is { }) ? + Encoding.UTF8.GetString(varParam.ParameterValue) : null; + var userAbort = new SctpErrorUserInitiatedAbort { AbortReason = reason }; + errorChunk.AddErrorCause(userAbort); + break; + case (ushort)SctpErrorCauseCode.ProtocolViolation: + var info = (varParam.ParameterValue is { }) ? + Encoding.UTF8.GetString(varParam.ParameterValue) : null; + var protocolViolation = new SctpErrorProtocolViolation { AdditionalInformation = info }; + errorChunk.AddErrorCause(protocolViolation); + break; + default: + // Parameter was not recognised. + errorChunk.GotUnrecognisedParameter(varParam); break; - } } - } - return errorChunk; + if (stopProcessing) + { + logger.LogSctpUnrecognisedParameter(varParam.ParameterType, SctpChunkType.ERROR); + break; + } + } } + + return errorChunk; } } diff --git a/src/SIPSorcery/net/SCTP/Chunks/SctpInitChunk.cs b/src/SIPSorcery/net/SCTP/Chunks/SctpInitChunk.cs index 166aa56bb8..46a67ef6bc 100644 --- a/src/SIPSorcery/net/SCTP/Chunks/SctpInitChunk.cs +++ b/src/SIPSorcery/net/SCTP/Chunks/SctpInitChunk.cs @@ -18,392 +18,413 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Text; -using Microsoft.Extensions.Logging; -using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// The optional or variable length Type-Length-Value (TLV) parameters +/// that can be used with INIT and INIT ACK chunks. +/// +public enum SctpInitChunkParameterType : ushort { + IPv4Address = 5, + IPv6Address = 6, + StateCookie = 7, // INIT ACK only. + UnrecognizedParameter = 8, // INIT ACK only. + CookiePreservative = 9, + HostNameAddress = 11, + SupportedAddressTypes = 12, + EcnCapable = 32768 +} + +/// +/// This class is used to represent both an INIT and INIT ACK chunk. +/// The only structural difference between them is the INIT ACK requires +/// the Cookie variable parameter to be set. +/// The INIT chunk is used to initiate an SCTP association between two +/// endpoints. The INIT ACK chunk is used to respond to an incoming +/// INIT chunk from a remote peer. +/// +public partial class SctpInitChunk : SctpChunk +{ + public const int FIXED_PARAMETERS_LENGTH = 16; + + // Lengths for the optional parameter values. + private const ushort PARAMVAL_LENGTH_IPV4 = 4; + private const ushort PARAMVAL_LENGTH_IPV6 = 16; + private const ushort PARAMVAL_LENGTH_COOKIE_PRESERVATIVE = 4; + + /// + /// The receiver of the INIT (the responding end) records the value of + /// the Initiate Tag parameter.This value MUST be placed into the + /// Verification Tag field of every SCTP packet that the receiver of + /// the INIT transmits within this association. + /// + public uint InitiateTag; + + /// + /// Advertised Receiver Window Credit. This value represents the dedicated + /// buffer space, in number of bytes, the sender of the INIT has reserved in + /// association with this window. + /// + public uint ARwnd; + + /// + /// Defines the number of outbound streams the sender of this INIT + /// chunk wishes to create in this association. + /// + public ushort NumberOutboundStreams; + + /// + /// Defines the maximum number of streams the sender of this INIT + /// chunk allows the peer end to create in this association. + /// + public ushort NumberInboundStreams; + /// - /// The optional or variable length Type-Length-Value (TLV) parameters - /// that can be used with INIT and INIT ACK chunks. + /// The initial Transmission Sequence Number (TSN) that the sender will use. /// - public enum SctpInitChunkParameterType : ushort + public uint InitialTSN; + + /// + /// Optional list of IP address parameters that can be included in INIT chunks. + /// + public List Addresses = new List(); + + /// + /// The sender of the INIT shall use this parameter to suggest to the + /// receiver of the INIT for a longer life-span of the State Cookie. + /// + public uint CookiePreservative; + + /// + /// The sender of INIT uses this parameter to pass its Host Name (in + /// place of its IP addresses) to its peer.The peer is responsible for + /// resolving the name.Using this parameter might make it more likely + /// for the association to work across a NAT box. + /// + public string? HostnameAddress; + + /// + /// The sender of INIT uses this parameter to list all the address types + /// it can support. Options are IPv4 (5), IPv6 (6) and Hostname (11). + /// + public List SupportedAddressTypes = new List(); + + /// + /// INIT ACK only. Mandatory. This parameter value MUST contain all the necessary state and + /// parameter information required for the sender of this INIT ACK to create the association, + /// along with a Message Authentication Code (MAC). + /// + public byte[]? StateCookie; + + /// + /// INIT ACK only. Optional. This parameter is returned to the originator of the INIT chunk + /// if the INIT contains an unrecognized parameter that has a value that indicates it should + /// be reported to the sender. This parameter value field will contain unrecognized parameters + /// copied from the INIT chunk complete with Parameter Type, Length, and Value fields. + /// + public List UnrecognizedParameters = new List(); + + private SctpInitChunk() + { } + + /// + /// Initialises the chunk as either INIT or INIT ACK. + /// + /// Either INIT or INIT ACK. + public SctpInitChunk(SctpChunkType initChunkType, + uint initiateTag, + uint initialTSN, + uint arwnd, + ushort numberOutboundStreams, + ushort numberInboundStreams) : base(initChunkType) { - IPv4Address = 5, - IPv6Address = 6, - StateCookie = 7, // INIT ACK only. - UnrecognizedParameter = 8, // INIT ACK only. - CookiePreservative = 9, - HostNameAddress = 11, - SupportedAddressTypes = 12, - EcnCapable = 32768 + InitiateTag = initiateTag; + NumberOutboundStreams = numberOutboundStreams; + NumberInboundStreams = numberInboundStreams; + InitialTSN = initialTSN; + ARwnd = arwnd; } /// - /// This class is used to represent both an INIT and INIT ACK chunk. - /// The only structural difference between them is the INIT ACK requires - /// the Cookie variable parameter to be set. - /// The INIT chunk is used to initiate an SCTP association between two - /// endpoints. The INIT ACK chunk is used to respond to an incoming - /// INIT chunk from a remote peer. + /// Gets the length of the optional and variable length parameters for this + /// INIT or INIT ACK chunk. /// - public class SctpInitChunk : SctpChunk + /// If true the length field will be padded to a 4 byte boundary. + /// The length of the optional and variable length parameters. + private ushort GetVariableParametersLength(bool padded) { - public const int FIXED_PARAMETERS_LENGTH = 16; - - // Lengths for the optional parameter values. - private const ushort PARAMVAL_LENGTH_IPV4 = 4; - private const ushort PARAMVAL_LENGTH_IPV6 = 16; - private const ushort PARAMVAL_LENGTH_COOKIE_PRESERVATIVE = 4; - - /// - /// The receiver of the INIT (the responding end) records the value of - /// the Initiate Tag parameter.This value MUST be placed into the - /// Verification Tag field of every SCTP packet that the receiver of - /// the INIT transmits within this association. - /// - public uint InitiateTag; - - /// - /// Advertised Receiver Window Credit. This value represents the dedicated - /// buffer space, in number of bytes, the sender of the INIT has reserved in - /// association with this window. - /// - public uint ARwnd; - - /// - /// Defines the number of outbound streams the sender of this INIT - /// chunk wishes to create in this association. - /// - public ushort NumberOutboundStreams; - - /// - /// Defines the maximum number of streams the sender of this INIT - /// chunk allows the peer end to create in this association. - /// - public ushort NumberInboundStreams; - - /// - /// The initial Transmission Sequence Number (TSN) that the sender will use. - /// - public uint InitialTSN; - - /// - /// Optional list of IP address parameters that can be included in INIT chunks. - /// - public List Addresses = new List(); - - /// - /// The sender of the INIT shall use this parameter to suggest to the - /// receiver of the INIT for a longer life-span of the State Cookie. - /// - public uint CookiePreservative; - - /// - /// The sender of INIT uses this parameter to pass its Host Name (in - /// place of its IP addresses) to its peer.The peer is responsible for - /// resolving the name.Using this parameter might make it more likely - /// for the association to work across a NAT box. - /// - public string HostnameAddress; - - /// - /// The sender of INIT uses this parameter to list all the address types - /// it can support. Options are IPv4 (5), IPv6 (6) and Hostname (11). - /// - public List SupportedAddressTypes = new List(); - - /// - /// INIT ACK only. Mandatory. This parameter value MUST contain all the necessary state and - /// parameter information required for the sender of this INIT ACK to create the association, - /// along with a Message Authentication Code (MAC). - /// - public byte[] StateCookie; - - /// - /// INIT ACK only. Optional. This parameter is returned to the originator of the INIT chunk - /// if the INIT contains an unrecognized parameter that has a value that indicates it should - /// be reported to the sender. This parameter value field will contain unrecognized parameters - /// copied from the INIT chunk complete with Parameter Type, Length, and Value fields. - /// - public List UnrecognizedParameters = new List(); - - private SctpInitChunk() - { } - - /// - /// Initialises the chunk as either INIT or INIT ACK. - /// - /// Either INIT or INIT ACK. - public SctpInitChunk(SctpChunkType initChunkType, - uint initiateTag, - uint initialTSN, - uint arwnd, - ushort numberOutboundStreams, - ushort numberInboundStreams) : base(initChunkType) - { - InitiateTag = initiateTag; - NumberOutboundStreams = numberOutboundStreams; - NumberInboundStreams = numberInboundStreams; - InitialTSN = initialTSN; - ARwnd = arwnd; - } + int len = 0; - /// - /// Gets the length of the optional and variable length parameters for this - /// INIT or INIT ACK chunk. - /// - /// If true the length field will be padded to a 4 byte boundary. - /// The length of the optional and variable length parameters. - private ushort GetVariableParametersLength(bool padded) + var ipv4Count = 0; + var ipv6Count = 0; + foreach (var x in Addresses) { - int len = 0; - - len += Addresses.Count(x => x.AddressFamily == AddressFamily.InterNetwork) * - (SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + PARAMVAL_LENGTH_IPV4); - - len += Addresses.Count(x => x.AddressFamily == AddressFamily.InterNetworkV6) * - (SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + PARAMVAL_LENGTH_IPV6); - - if (CookiePreservative > 0) + if (x.AddressFamily == AddressFamily.InterNetwork) { - len += SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + - PARAMVAL_LENGTH_COOKIE_PRESERVATIVE; + ipv4Count++; } - - if (!string.IsNullOrEmpty(HostnameAddress)) + else if (x.AddressFamily == AddressFamily.InterNetworkV6) { - len += SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + - SctpPadding.PadTo4ByteBoundary(Encoding.UTF8.GetByteCount(HostnameAddress)); + ipv6Count++; } + } - if (SupportedAddressTypes.Count > 0) - { - len += SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + - SctpPadding.PadTo4ByteBoundary(SupportedAddressTypes.Count * 2); - } + len += ipv4Count * (SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + PARAMVAL_LENGTH_IPV4); - if (StateCookie != null) - { - len += SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + - SctpPadding.PadTo4ByteBoundary(StateCookie.Length); - } + len += ipv6Count * (SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + PARAMVAL_LENGTH_IPV6); - foreach (var unrecognised in UnrecognizedPeerParameters) - { - len += SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + - unrecognised.GetParameterLength(true); - } + if (CookiePreservative > 0) + { + len += SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + + PARAMVAL_LENGTH_COOKIE_PRESERVATIVE; + } - return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : (ushort)len; + if (!string.IsNullOrEmpty(HostnameAddress)) + { + len += SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + + SctpPadding.PadTo4ByteBoundary(Encoding.UTF8.GetByteCount(HostnameAddress)); } - /// - /// Writes the optional and variable length parameters to a Type-Length-Value (TLV) - /// parameter list. - /// - /// A TLV parameter list holding the optional and variable length parameters. - private List GetVariableParameters() + if (SupportedAddressTypes.Count > 0) { - List varParams = new List(); + len += SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + + SctpPadding.PadTo4ByteBoundary(SupportedAddressTypes.Count * 2); + } - // Add the optional and variable length parameters as Type-Length-Value (TLV) formatted. - foreach (var address in Addresses) - { - ushort addrParamType = (ushort)(address.AddressFamily == AddressFamily.InterNetwork ? - SctpInitChunkParameterType.IPv4Address : SctpInitChunkParameterType.IPv6Address); - var addrParam = new SctpTlvChunkParameter(addrParamType, address.GetAddressBytes()); - varParams.Add(addrParam); - } + if (StateCookie is { }) + { + len += SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + + SctpPadding.PadTo4ByteBoundary(StateCookie.Length); + } - if (CookiePreservative > 0) - { - varParams.Add( - new SctpTlvChunkParameter((ushort)SctpInitChunkParameterType.CookiePreservative, - NetConvert.GetBytes(CookiePreservative) - )); - } + foreach (var unrecognised in UnrecognizedPeerParameters) + { + len += SctpTlvChunkParameter.SCTP_PARAMETER_HEADER_LENGTH + + unrecognised.GetParameterLength(true); + } - if (!string.IsNullOrEmpty(HostnameAddress)) - { - varParams.Add( - new SctpTlvChunkParameter((ushort)SctpInitChunkParameterType.HostNameAddress, - Encoding.UTF8.GetBytes(HostnameAddress) - )); - } + return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : (ushort)len; + } - if (SupportedAddressTypes.Count > 0) - { - byte[] paramVal = new byte[SupportedAddressTypes.Count * 2]; - int paramValPosn = 0; - foreach (var supAddr in SupportedAddressTypes) - { - NetConvert.ToBuffer((ushort)supAddr, paramVal, paramValPosn); - paramValPosn += 2; - } - varParams.Add( - new SctpTlvChunkParameter((ushort)SctpInitChunkParameterType.SupportedAddressTypes, paramVal)); - } + /// + /// Writes the optional and variable length parameters to a Type-Length-Value (TLV) + /// parameter list. + /// + /// A TLV parameter list holding the optional and variable length parameters. + private List GetVariableParameters() + { + List varParams = new List(); - if (StateCookie != null) - { - varParams.Add( - new SctpTlvChunkParameter((ushort)SctpInitChunkParameterType.StateCookie, StateCookie)); - } + // Add the optional and variable length parameters as Type-Length-Value (TLV) formatted. + foreach (var address in Addresses) + { + ushort addrParamType = (ushort)(address.AddressFamily == AddressFamily.InterNetwork ? + SctpInitChunkParameterType.IPv4Address : SctpInitChunkParameterType.IPv6Address); + var addrParam = new SctpTlvChunkParameter(addrParamType, address.GetAddressBytes()); + varParams.Add(addrParam); + } - foreach (var unrecognised in UnrecognizedPeerParameters) - { - varParams.Add( - new SctpTlvChunkParameter((ushort)SctpInitChunkParameterType.UnrecognizedParameter, unrecognised.GetBytes())); - } + if (CookiePreservative > 0) + { + var buffer = new byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32BigEndian(buffer, CookiePreservative); + + varParams.Add( + new SctpTlvChunkParameter((ushort)SctpInitChunkParameterType.CookiePreservative, + buffer + )); + } - return varParams; + if (!string.IsNullOrEmpty(HostnameAddress)) + { + varParams.Add( + new SctpTlvChunkParameter((ushort)SctpInitChunkParameterType.HostNameAddress, + Encoding.UTF8.GetBytes(HostnameAddress) + )); } - /// - /// Calculates the length for INIT and INIT ACK chunks. - /// - /// If true the length field will be padded to a 4 byte boundary. - /// The length of the chunk. - public override ushort GetChunkLength(bool padded) + if (SupportedAddressTypes.Count > 0) { - var len = (ushort)(SCTP_CHUNK_HEADER_LENGTH + - FIXED_PARAMETERS_LENGTH + - GetVariableParametersLength(false)); + var paramVal = new byte[SupportedAddressTypes.Count * 2]; + var paramValPosn = 0; + foreach (var supAddr in SupportedAddressTypes) + { + BinaryPrimitives.WriteUInt16BigEndian(paramVal.AsSpan(paramValPosn), (ushort)supAddr); + paramValPosn += 2; + } + varParams.Add( + new SctpTlvChunkParameter((ushort)SctpInitChunkParameterType.SupportedAddressTypes, paramVal)); + } - return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; + if (StateCookie is { }) + { + varParams.Add( + new SctpTlvChunkParameter((ushort)SctpInitChunkParameterType.StateCookie, StateCookie)); } - /// - /// Serialises an INIT or INIT ACK chunk to a pre-allocated buffer. - /// - /// The buffer to write the serialised chunk bytes to. It - /// must have the required space already allocated. - /// The position in the buffer to write to. - /// The number of bytes, including padding, written to the buffer. - public override ushort WriteTo(byte[] buffer, int posn) + foreach (var unrecognised in UnrecognizedPeerParameters) { - WriteChunkHeader(buffer, posn); + var paramBuffer = new byte[unrecognised.GetParameterLength(true)]; + _ = unrecognised.WriteBytes(paramBuffer); + varParams.Add( + new SctpTlvChunkParameter((ushort)SctpInitChunkParameterType.UnrecognizedParameter, paramBuffer)); + } - // Write fixed parameters. - int startPosn = posn + SCTP_CHUNK_HEADER_LENGTH; + return varParams; + } - NetConvert.ToBuffer(InitiateTag, buffer, startPosn); - NetConvert.ToBuffer(ARwnd, buffer, startPosn + 4); - NetConvert.ToBuffer(NumberOutboundStreams, buffer, startPosn + 8); - NetConvert.ToBuffer(NumberInboundStreams, buffer, startPosn + 10); - NetConvert.ToBuffer(InitialTSN, buffer, startPosn + 12); + /// + /// Calculates the length for INIT and INIT ACK chunks. + /// + /// If true the length field will be padded to a 4 byte boundary. + /// The length of the chunk. + public override ushort GetByteCount(bool padded) + { + var len = (ushort)(SCTP_CHUNK_HEADER_LENGTH + + FIXED_PARAMETERS_LENGTH + + GetVariableParametersLength(false)); + + return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; + } + + /// + /// Serialises an INIT or INIT ACK chunk to a pre-allocated buffer. + /// + /// The buffer to write the serialised chunk bytes to. It + /// must have the required space already allocated. + /// The number of bytes, including padding, written to the buffer. + public override ushort WriteBytes(Span buffer) + { + WriteBytesCore(buffer); - var varParameters = GetVariableParameters(); + return GetByteCount(true); + } - // Write optional parameters. - if (varParameters.Count > 0) + private void WriteBytesCore(Span buffer) + { + var bytesWritten = WriteChunkHeader(buffer); + + // Write fixed parameters. + + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH), InitiateTag); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 4), ARwnd); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 8), NumberOutboundStreams); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 10), NumberInboundStreams); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 12), InitialTSN); + + // Write optional parameters. + if (GetVariableParameters() is { Count: > 0 } varParameters) + { + buffer = buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + FIXED_PARAMETERS_LENGTH); + foreach (var optParam in varParameters) { - int paramPosn = startPosn + FIXED_PARAMETERS_LENGTH; - foreach (var optParam in varParameters) - { - paramPosn += optParam.WriteTo(buffer, paramPosn); - } + var bytesWriten = optParam.WriteBytes(buffer); + buffer = buffer.Slice(bytesWriten); } - - return GetChunkLength(true); } + } - /// - /// Parses the INIT or INIT ACK chunk fields - /// - /// The buffer holding the serialised chunk. - /// The position to start parsing at. - public static SctpInitChunk ParseChunk(byte[] buffer, int posn) - { - var initChunk = new SctpInitChunk(); - ushort chunkLen = initChunk.ParseFirstWord(buffer, posn); + /// + /// Parses the INIT or INIT ACK chunk fields + /// + /// The buffer holding the serialised chunk. + public static SctpInitChunk ParseChunk(ReadOnlySpan buffer) + { + var initChunk = new SctpInitChunk(); + var chunkLen = initChunk.ParseFirstWord(buffer); - int startPosn = posn + SCTP_CHUNK_HEADER_LENGTH; + initChunk.InitiateTag = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH)); + initChunk.ARwnd = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 4)); + initChunk.NumberOutboundStreams = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 8)); + initChunk.NumberInboundStreams = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 10)); + initChunk.InitialTSN = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 12)); - initChunk.InitiateTag = NetConvert.ParseUInt32(buffer, startPosn); - initChunk.ARwnd = NetConvert.ParseUInt32(buffer, startPosn + 4); - initChunk.NumberOutboundStreams = NetConvert.ParseUInt16(buffer, startPosn + 8); - initChunk.NumberInboundStreams = NetConvert.ParseUInt16(buffer, startPosn + 10); - initChunk.InitialTSN = NetConvert.ParseUInt32(buffer, startPosn + 12); + var paramPosn = SCTP_CHUNK_HEADER_LENGTH + FIXED_PARAMETERS_LENGTH; + var paramsBufferLength = chunkLen - SCTP_CHUNK_HEADER_LENGTH - FIXED_PARAMETERS_LENGTH; - int paramPosn = startPosn + FIXED_PARAMETERS_LENGTH; - int paramsBufferLength = chunkLen - SCTP_CHUNK_HEADER_LENGTH - FIXED_PARAMETERS_LENGTH; + if (paramPosn < paramsBufferLength) + { + var stopProcessing = false; - if (paramPosn < paramsBufferLength) + foreach (var varParam in GetParameters(buffer.Slice(paramPosn, paramsBufferLength))) { - bool stopProcessing = false; + Debug.Assert(varParam is { }); - foreach (var varParam in GetParameters(buffer, paramPosn, paramsBufferLength)) + switch (varParam.ParameterType) { - switch (varParam.ParameterType) - { - case (ushort)SctpInitChunkParameterType.IPv4Address: - case (ushort)SctpInitChunkParameterType.IPv6Address: - var address = new IPAddress(varParam.ParameterValue); - initChunk.Addresses.Add(address); - break; - - case (ushort)SctpInitChunkParameterType.CookiePreservative: - initChunk.CookiePreservative = NetConvert.ParseUInt32(varParam.ParameterValue, 0); - break; - - case (ushort)SctpInitChunkParameterType.HostNameAddress: - initChunk.HostnameAddress = Encoding.UTF8.GetString(varParam.ParameterValue); - break; - - case (ushort)SctpInitChunkParameterType.SupportedAddressTypes: - for (int valPosn = 0; valPosn < varParam.ParameterValue.Length; valPosn += 2) + case (ushort)SctpInitChunkParameterType.IPv4Address: + case (ushort)SctpInitChunkParameterType.IPv6Address: + Debug.Assert(varParam.ParameterValue is { }); + var address = new IPAddress(varParam.ParameterValue); + initChunk.Addresses.Add(address); + break; + + case (ushort)SctpInitChunkParameterType.CookiePreservative: + Debug.Assert(varParam.ParameterValue is { Length: >= sizeof(uint) }); + initChunk.CookiePreservative = BinaryPrimitives.ReadUInt32BigEndian(varParam.ParameterValue.AsSpan()); + break; + + case (ushort)SctpInitChunkParameterType.HostNameAddress: + Debug.Assert(varParam.ParameterValue is { }); + initChunk.HostnameAddress = Encoding.UTF8.GetString(varParam.ParameterValue); + break; + + case (ushort)SctpInitChunkParameterType.SupportedAddressTypes: + Debug.Assert(varParam.ParameterValue is { }); + for (int valPosn = 0; valPosn < varParam.ParameterValue.Length; valPosn += 2) + { + switch (BinaryPrimitives.ReadUInt16BigEndian(varParam.ParameterValue.AsSpan(valPosn))) { - switch (NetConvert.ParseUInt16(varParam.ParameterValue, valPosn)) - { - case (ushort)SctpInitChunkParameterType.IPv4Address: - initChunk.SupportedAddressTypes.Add(SctpInitChunkParameterType.IPv4Address); - break; - case (ushort)SctpInitChunkParameterType.IPv6Address: - initChunk.SupportedAddressTypes.Add(SctpInitChunkParameterType.IPv6Address); - break; - case (ushort)SctpInitChunkParameterType.HostNameAddress: - initChunk.SupportedAddressTypes.Add(SctpInitChunkParameterType.HostNameAddress); - break; - } + case (ushort)SctpInitChunkParameterType.IPv4Address: + initChunk.SupportedAddressTypes.Add(SctpInitChunkParameterType.IPv4Address); + break; + case (ushort)SctpInitChunkParameterType.IPv6Address: + initChunk.SupportedAddressTypes.Add(SctpInitChunkParameterType.IPv6Address); + break; + case (ushort)SctpInitChunkParameterType.HostNameAddress: + initChunk.SupportedAddressTypes.Add(SctpInitChunkParameterType.HostNameAddress); + break; } - break; - - case (ushort)SctpInitChunkParameterType.EcnCapable: - break; - - case (ushort)SctpInitChunkParameterType.StateCookie: - // Used with INIT ACK chunks only. - initChunk.StateCookie = varParam.ParameterValue; - break; - - case (ushort)SctpInitChunkParameterType.UnrecognizedParameter: - // Used with INIT ACK chunks only. This parameter is the remote peer returning - // a list of parameters it did not understand in the INIT chunk. - initChunk.UnrecognizedParameters.Add(varParam.ParameterValue); - break; - - default: - // Parameters are not recognised in an INIT or INIT ACK. - initChunk.GotUnrecognisedParameter(varParam); - break; - } - - if (stopProcessing) - { - logger.LogWarning("SCTP unrecognised parameter {ParameterType} for chunk type {ChunkType} indicated no further chunks should be processed.", varParam.ParameterType, initChunk.KnownType); + } + break; + + case (ushort)SctpInitChunkParameterType.EcnCapable: + break; + + case (ushort)SctpInitChunkParameterType.StateCookie: + // Used with INIT ACK chunks only. + initChunk.StateCookie = varParam.ParameterValue; + break; + + case (ushort)SctpInitChunkParameterType.UnrecognizedParameter: + // Used with INIT ACK chunks only. This parameter is the remote peer returning + // a list of parameters it did not understand in the INIT chunk. + Debug.Assert(varParam.ParameterValue is { }); + initChunk.UnrecognizedParameters.Add(varParam.ParameterValue); + break; + + default: + // Parameters are not recognised in an INIT or INIT ACK. + initChunk.GotUnrecognisedParameter(varParam); break; - } } - } - return initChunk; + if (stopProcessing) + { + logger.LogSctpUnrecognisedParameter(varParam.ParameterType, initChunk.KnownType); + break; + } + } } + + return initChunk; } } diff --git a/src/SIPSorcery/net/SCTP/Chunks/SctpSackChunk.cs b/src/SIPSorcery/net/SCTP/Chunks/SctpSackChunk.cs index 339740e2ae..a3bcad3af2 100644 --- a/src/SIPSorcery/net/SCTP/Chunks/SctpSackChunk.cs +++ b/src/SIPSorcery/net/SCTP/Chunks/SctpSackChunk.cs @@ -17,147 +17,146 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; +using System.Buffers.Binary; using System.Collections.Generic; -using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// This chunk is sent to the peer endpoint to acknowledge received DATA +/// chunks and to inform the peer endpoint of gaps in the received +/// sub-sequences of DATA chunks as represented by their Transmission +/// Sequence Numbers (TSN). +/// +public partial class SctpSackChunk : SctpChunk { + public const int FIXED_PARAMETERS_LENGTH = 12; + private const int GAP_REPORT_LENGTH = 4; + private const int DUPLICATE_TSN_LENGTH = 4; + /// - /// This chunk is sent to the peer endpoint to acknowledge received DATA - /// chunks and to inform the peer endpoint of gaps in the received - /// sub-sequences of DATA chunks as represented by their Transmission - /// Sequence Numbers (TSN). + /// This parameter contains the TSN of the last chunk received in + /// sequence before any gaps. /// - public class SctpSackChunk : SctpChunk - { - public const int FIXED_PARAMETERS_LENGTH = 12; - private const int GAP_REPORT_LENGTH = 4; - private const int DUPLICATE_TSN_LENGTH = 4; - - /// - /// This parameter contains the TSN of the last chunk received in - /// sequence before any gaps. - /// - public uint CumulativeTsnAck; - - /// - /// Advertised Receiver Window Credit. This field indicates the updated - /// receive buffer space in bytes of the sender of this SACK - /// - public uint ARwnd; - - /// - /// The gap ACK blocks. Each entry represents a gap in the forward out of order - /// TSNs received. - /// - public List GapAckBlocks = new List(); - - /// - /// Indicates the number of times a TSN was received in duplicate - /// since the last SACK was sent. - /// - public List DuplicateTSN = new List(); - - private SctpSackChunk() : base(SctpChunkType.SACK) - { } - - /// - /// Creates a new SACK chunk. - /// - /// The last TSN that was received from the remote party. - /// The current Advertised Receiver Window Credit. - public SctpSackChunk(uint cumulativeTsnAck, uint arwnd) : base(SctpChunkType.SACK) - { - CumulativeTsnAck = cumulativeTsnAck; - ARwnd = arwnd; - } + public uint CumulativeTsnAck; - /// - /// Calculates the padded length for the chunk. - /// - /// If true the length field will be padded to a 4 byte boundary. - /// The length of the chunk. - public override ushort GetChunkLength(bool padded) - { - var len = (ushort)(SCTP_CHUNK_HEADER_LENGTH + - FIXED_PARAMETERS_LENGTH + - GapAckBlocks.Count * GAP_REPORT_LENGTH + - DuplicateTSN.Count * DUPLICATE_TSN_LENGTH); + /// + /// Advertised Receiver Window Credit. This field indicates the updated + /// receive buffer space in bytes of the sender of this SACK + /// + public uint ARwnd; - // Guaranteed to be in a 4 byte boundary so no need to pad. - return len; - } + /// + /// The gap ACK blocks. Each entry represents a gap in the forward out of order + /// TSNs received. + /// + public List GapAckBlocks = new List(); - /// - /// Serialises the SACK chunk to a pre-allocated buffer. - /// - /// The buffer to write the serialised chunk bytes to. It - /// must have the required space already allocated. - /// The position in the buffer to write to. - /// The number of bytes, including padding, written to the buffer. - public override ushort WriteTo(byte[] buffer, int posn) - { - WriteChunkHeader(buffer, posn); + /// + /// Indicates the number of times a TSN was received in duplicate + /// since the last SACK was sent. + /// + public List DuplicateTSN = new List(); + + private SctpSackChunk() : base(SctpChunkType.SACK) + { } + + /// + /// Creates a new SACK chunk. + /// + /// The last TSN that was received from the remote party. + /// The current Advertised Receiver Window Credit. + public SctpSackChunk(uint cumulativeTsnAck, uint arwnd) : base(SctpChunkType.SACK) + { + CumulativeTsnAck = cumulativeTsnAck; + ARwnd = arwnd; + } + + /// + /// Calculates the padded length for the chunk. + /// + /// If true the length field will be padded to a 4 byte boundary. + /// The length of the chunk. + public override ushort GetByteCount(bool padded) + { + var len = (ushort)(SCTP_CHUNK_HEADER_LENGTH + + FIXED_PARAMETERS_LENGTH + + GapAckBlocks.Count * GAP_REPORT_LENGTH + + DuplicateTSN.Count * DUPLICATE_TSN_LENGTH); - ushort startPosn = (ushort)(posn + SCTP_CHUNK_HEADER_LENGTH); + // Guaranteed to be in a 4 byte boundary so no need to pad. + return len; + } - NetConvert.ToBuffer(CumulativeTsnAck, buffer, startPosn); - NetConvert.ToBuffer(ARwnd, buffer, startPosn + 4); - NetConvert.ToBuffer((ushort)GapAckBlocks.Count, buffer, startPosn + 8); - NetConvert.ToBuffer((ushort)DuplicateTSN.Count, buffer, startPosn + 10); + /// + /// Serialises the SACK chunk to a pre-allocated buffer. + /// + /// The buffer to write the serialised chunk bytes to. It + /// must have the required space already allocated. + /// The number of bytes, including padding, written to the buffer. + public override ushort WriteBytes(Span buffer) + { + WriteBytesCore(buffer); - int reportPosn = startPosn + FIXED_PARAMETERS_LENGTH; + return GetByteCount(true); + } - foreach (var gapBlock in GapAckBlocks) - { - NetConvert.ToBuffer(gapBlock.Start, buffer, reportPosn); - NetConvert.ToBuffer(gapBlock.End, buffer, reportPosn + 2); - reportPosn += GAP_REPORT_LENGTH; - } + private void WriteBytesCore(Span buffer) + { + var bytesWritten = WriteChunkHeader(buffer); - foreach(var dupTSN in DuplicateTSN) - { - NetConvert.ToBuffer(dupTSN, buffer, reportPosn); - reportPosn += DUPLICATE_TSN_LENGTH; - } + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH), CumulativeTsnAck); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 4), ARwnd); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 8), (ushort)GapAckBlocks.Count); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH + 10), (ushort)DuplicateTSN.Count); - return GetChunkLength(true); - } + var reportPosn = SCTP_CHUNK_HEADER_LENGTH + FIXED_PARAMETERS_LENGTH; - /// - /// Parses the SACK chunk fields. - /// - /// The buffer holding the serialised chunk. - /// The position to start parsing at. - public static SctpSackChunk ParseChunk(byte[] buffer, int posn) + foreach (var gapBlock in GapAckBlocks) { - var sackChunk = new SctpSackChunk(); - ushort chunkLen = sackChunk.ParseFirstWord(buffer, posn); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(reportPosn), gapBlock.Start); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(reportPosn + 2), gapBlock.End); + reportPosn += GAP_REPORT_LENGTH; + } - ushort startPosn = (ushort)(posn + SCTP_CHUNK_HEADER_LENGTH); + foreach (var dupTSN in DuplicateTSN) + { + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(reportPosn), dupTSN); + reportPosn += DUPLICATE_TSN_LENGTH; + } + } - sackChunk.CumulativeTsnAck = NetConvert.ParseUInt32(buffer, startPosn); - sackChunk.ARwnd = NetConvert.ParseUInt32(buffer, startPosn + 4); - ushort numGapAckBlocks = NetConvert.ParseUInt16(buffer, startPosn + 8); - ushort numDuplicateTSNs = NetConvert.ParseUInt16(buffer, startPosn + 10); + /// + /// Parses the SACK chunk fields. + /// + /// The buffer holding the serialised chunk. + public static SctpSackChunk ParseChunk(ReadOnlySpan buffer) + { + var sackChunk = new SctpSackChunk(); + var chunkLen = sackChunk.ParseFirstWord(buffer); - int reportPosn = startPosn + FIXED_PARAMETERS_LENGTH; + sackChunk.CumulativeTsnAck = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice((ushort)(SCTP_CHUNK_HEADER_LENGTH))); + sackChunk.ARwnd = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(((ushort)(SCTP_CHUNK_HEADER_LENGTH)) + 4)); + var numGapAckBlocks = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(((ushort)(SCTP_CHUNK_HEADER_LENGTH)) + 8)); + var numDuplicateTSNs = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(((ushort)(SCTP_CHUNK_HEADER_LENGTH)) + 10)); - for (int i=0; i < numGapAckBlocks; i++) - { - ushort start = NetConvert.ParseUInt16(buffer, reportPosn); - ushort end = NetConvert.ParseUInt16(buffer, reportPosn + 2); - sackChunk.GapAckBlocks.Add(new SctpTsnGapBlock { Start = start, End = end }); - reportPosn += GAP_REPORT_LENGTH; - } + var reportPosn = ((ushort)(SCTP_CHUNK_HEADER_LENGTH)) + FIXED_PARAMETERS_LENGTH; - for(int j=0; j < numDuplicateTSNs; j++) - { - sackChunk.DuplicateTSN.Add(NetConvert.ParseUInt32(buffer, reportPosn)); - reportPosn += DUPLICATE_TSN_LENGTH; - } + for (var i = 0; i < numGapAckBlocks; i++) + { + var start = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(reportPosn)); + var end = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(reportPosn + 2)); + sackChunk.GapAckBlocks.Add(new SctpTsnGapBlock { Start = start, End = end }); + reportPosn += GAP_REPORT_LENGTH; + } - return sackChunk; + for (var j = 0; j < numDuplicateTSNs; j++) + { + sackChunk.DuplicateTSN.Add(BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(reportPosn))); + reportPosn += DUPLICATE_TSN_LENGTH; } + + return sackChunk; } } diff --git a/src/SIPSorcery/net/SCTP/Chunks/SctpShutdownChunk.cs b/src/SIPSorcery/net/SCTP/Chunks/SctpShutdownChunk.cs index b4b3dee9cb..8f036cd44c 100644 --- a/src/SIPSorcery/net/SCTP/Chunks/SctpShutdownChunk.cs +++ b/src/SIPSorcery/net/SCTP/Chunks/SctpShutdownChunk.cs @@ -17,73 +17,78 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- -using SIPSorcery.Sys; +using System; +using System.Buffers.Binary; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// An endpoint in an association MUST use this chunk to initiate a +/// graceful close of the association with its peer. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.3.8 +/// +public partial class SctpShutdownChunk : SctpChunk { + public const int FIXED_PARAMETERS_LENGTH = 4; + + /// + /// This parameter contains the TSN of the last chunk received in + /// sequence before any gaps. + /// + public uint? CumulativeTsnAck; + + private SctpShutdownChunk() : base(SctpChunkType.SHUTDOWN) + { } + /// - /// An endpoint in an association MUST use this chunk to initiate a - /// graceful close of the association with its peer. + /// Creates a new SHUTDOWN chunk. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.3.8 - /// - public class SctpShutdownChunk : SctpChunk + /// The last TSN that was received from the remote party. + public SctpShutdownChunk(uint? cumulativeTsnAck) : base(SctpChunkType.SHUTDOWN) { - public const int FIXED_PARAMETERS_LENGTH = 4; + CumulativeTsnAck = cumulativeTsnAck; + } - /// - /// This parameter contains the TSN of the last chunk received in - /// sequence before any gaps. - /// - public uint? CumulativeTsnAck; + /// + /// Calculates the padded length for the chunk. + /// + /// If true the length field will be padded to a 4 byte boundary. + /// The padded length of the chunk. + public override ushort GetByteCount(bool padded) + { + return SCTP_CHUNK_HEADER_LENGTH + FIXED_PARAMETERS_LENGTH; + } - private SctpShutdownChunk() : base(SctpChunkType.SHUTDOWN) - { } + /// + /// Serialises the SHUTDOWN chunk to a pre-allocated buffer. + /// + /// The buffer to write the serialised chunk bytes to. It + /// must have the required space already allocated. + /// The number of bytes, including padding, written to the buffer. + public override ushort WriteBytes(Span buffer) + { + WriteBytesCore(buffer); - /// - /// Creates a new SHUTDOWN chunk. - /// - /// The last TSN that was received from the remote party. - public SctpShutdownChunk(uint? cumulativeTsnAck) : base(SctpChunkType.SHUTDOWN) - { - CumulativeTsnAck = cumulativeTsnAck; - } + return GetByteCount(true); + } - /// - /// Calculates the padded length for the chunk. - /// - /// If true the length field will be padded to a 4 byte boundary. - /// The padded length of the chunk. - public override ushort GetChunkLength(bool padded) - { - return SCTP_CHUNK_HEADER_LENGTH + FIXED_PARAMETERS_LENGTH; - } + private void WriteBytesCore(Span buffer) + { + var bytesWritten = WriteChunkHeader(buffer); - /// - /// Serialises the SHUTDOWN chunk to a pre-allocated buffer. - /// - /// The buffer to write the serialised chunk bytes to. It - /// must have the required space already allocated. - /// The position in the buffer to write to. - /// The number of bytes, including padding, written to the buffer. - public override ushort WriteTo(byte[] buffer, int posn) - { - WriteChunkHeader(buffer, posn); - NetConvert.ToBuffer(CumulativeTsnAck.GetValueOrDefault(), buffer, posn + SCTP_CHUNK_HEADER_LENGTH); - return GetChunkLength(true); - } + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH), CumulativeTsnAck.GetValueOrDefault()); + } - /// - /// Parses the SHUTDOWN chunk fields. - /// - /// The buffer holding the serialised chunk. - /// The position to start parsing at. - public static SctpShutdownChunk ParseChunk(byte[] buffer, int posn) - { - var shutdownChunk = new SctpShutdownChunk(); - shutdownChunk.CumulativeTsnAck = NetConvert.ParseUInt32(buffer, posn + SCTP_CHUNK_HEADER_LENGTH); - return shutdownChunk; - } + /// + /// Parses the SHUTDOWN chunk fields. + /// + /// The buffer holding the serialised chunk. + public static SctpShutdownChunk ParseChunk(ReadOnlySpan buffer) + { + var shutdownChunk = new SctpShutdownChunk(); + shutdownChunk.CumulativeTsnAck = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(SCTP_CHUNK_HEADER_LENGTH)); + return shutdownChunk; } } diff --git a/src/SIPSorcery/net/SCTP/Chunks/SctpTlvChunkParameter.cs b/src/SIPSorcery/net/SCTP/Chunks/SctpTlvChunkParameter.cs index 094bf53e3a..d967c183a7 100644 --- a/src/SIPSorcery/net/SCTP/Chunks/SctpTlvChunkParameter.cs +++ b/src/SIPSorcery/net/SCTP/Chunks/SctpTlvChunkParameter.cs @@ -19,226 +19,210 @@ //----------------------------------------------------------------------------- using System; -using System.Net; -using System.Net.Sockets; +using System.Buffers.Binary; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +//public enum SctpChunkParameterType : ushort +//{ +// Unknown = 0, +// HeartbeatInfo = 1, +// IPv4Address = 5, +// IPv6Address = 6, +// StateCookie = 7, +// UnrecognizedParameters = 8, +// CookiePreservative = 9, +// HostNameAddress = 11, +// SupportedAddressTypes = 12, +// OutgoingSSNResetRequestParameter = 13, +// IncomingSSNResetRequestParameter = 14, +// SSNTSNResetRequestParameter = 15, +// ReconfigurationResponseParameter = 16, +// AddOutgoingStreamsRequestParameter = 17, +// AddIncomingStreamsRequestParameter = 18, +// ReservedforECNCapable = 32768, +// Random = 32770, +// ChunkList = 32771, +// RequestedHMACAlgorithmParameter = 32772, +// Padding = 32773, +// SupportedExtensions = 32776, +// ForwardTSNsupported = 49152, +// AddIPAddress = 49153, +// DeleteIPAddress = 49154, +// ErrorCauseIndication = 49155, +// SetPrimaryAddress = 49156, +// SuccessIndication = 49157, +// AdaptationLayerIndication = 49158 +//} + +/// +/// The actions required for unrecognised parameters. The byte value corresponds to the highest +/// order two bits of the parameter type value. +/// +/// +/// https://tools.ietf.org/html/rfc4960#section-3.2.1 +/// +public enum SctpUnrecognisedParameterActions : byte { - //public enum SctpChunkParameterType : ushort - //{ - // Unknown = 0, - // HeartbeatInfo = 1, - // IPv4Address = 5, - // IPv6Address = 6, - // StateCookie = 7, - // UnrecognizedParameters = 8, - // CookiePreservative = 9, - // HostNameAddress = 11, - // SupportedAddressTypes = 12, - // OutgoingSSNResetRequestParameter = 13, - // IncomingSSNResetRequestParameter = 14, - // SSNTSNResetRequestParameter = 15, - // ReconfigurationResponseParameter = 16, - // AddOutgoingStreamsRequestParameter = 17, - // AddIncomingStreamsRequestParameter = 18, - // ReservedforECNCapable = 32768, - // Random = 32770, - // ChunkList = 32771, - // RequestedHMACAlgorithmParameter = 32772, - // Padding = 32773, - // SupportedExtensions = 32776, - // ForwardTSNsupported = 49152, - // AddIPAddress = 49153, - // DeleteIPAddress = 49154, - // ErrorCauseIndication = 49155, - // SetPrimaryAddress = 49156, - // SuccessIndication = 49157, - // AdaptationLayerIndication = 49158 - //} + /// + /// Stop processing this parameter; do not process any further parameters within this chunk. + /// + Stop = 0x00, + + /// + /// Stop processing this parameter, do not process any further parameters within this chunk, and report the unrecognized + /// parameter in an 'Unrecognized Parameter'. + /// + StopAndReport = 0x01, + + /// + /// Skip this parameter and continue processing. + /// + Skip = 0x02, /// - /// The actions required for unrecognised parameters. The byte value corresponds to the highest - /// order two bits of the parameter type value. + /// Skip this parameter and continue processing but report the unrecognized parameter in an 'Unrecognized Parameter'. /// - /// - /// https://tools.ietf.org/html/rfc4960#section-3.2.1 - /// - public enum SctpUnrecognisedParameterActions : byte + SkipAndReport = 0x03 +} + +/// +/// Represents the a variable length parameter field for use within +/// a Chunk. All chunk parameters use the same underlying Type-Length-Value (TLV) +/// format but then specialise how the fields are used. +/// +/// +/// From https://tools.ietf.org/html/rfc4960#section-3.2.1 (final section): +/// Note that a parameter type MUST be unique +/// across all chunks.For example, the parameter type '5' is used to +/// represent an IPv4 address. The value '5' then +/// is reserved across all chunks to represent an IPv4 address and MUST +/// NOT be reused with a different meaning in any other chunk. +/// +public partial class SctpTlvChunkParameter +{ + public const int SCTP_PARAMETER_HEADER_LENGTH = 4; + + private static ILogger logger = SIPSorcery.LogFactory.CreateLogger(); + + /// + /// The type of the chunk parameter. + /// + public ushort ParameterType { get; protected set; } + + /// + /// The information contained in the parameter. + /// + public byte[]? ParameterValue; + + /// + /// If this parameter is unrecognised by the parent chunk then this field dictates + /// how it should handle it. + /// + public SctpUnrecognisedParameterActions UnrecognisedAction => + (SctpUnrecognisedParameterActions)(ParameterType >> 14 & 0x03); + + protected SctpTlvChunkParameter() + { } + + /// + /// Creates a new chunk parameter instance. + /// + public SctpTlvChunkParameter(ushort parameterType, byte[] parameterValue) { - /// - /// Stop processing this parameter; do not process any further parameters within this chunk. - /// - Stop = 0x00, - - /// - /// Stop processing this parameter, do not process any further parameters within this chunk, and report the unrecognized - /// parameter in an 'Unrecognized Parameter'. - /// - StopAndReport = 0x01, - - /// - /// Skip this parameter and continue processing. - /// - Skip = 0x02, - - /// - /// Skip this parameter and continue processing but report the unrecognized parameter in an 'Unrecognized Parameter'. - /// - SkipAndReport = 0x03 + ParameterType = parameterType; + ParameterValue = parameterValue; } /// - /// Represents the a variable length parameter field for use within - /// a Chunk. All chunk parameters use the same underlying Type-Length-Value (TLV) - /// format but then specialise how the fields are used. + /// Calculates the length for the chunk parameter. /// - /// - /// From https://tools.ietf.org/html/rfc4960#section-3.2.1 (final section): - /// Note that a parameter type MUST be unique - /// across all chunks.For example, the parameter type '5' is used to - /// represent an IPv4 address. The value '5' then - /// is reserved across all chunks to represent an IPv4 address and MUST - /// NOT be reused with a different meaning in any other chunk. - /// - public class SctpTlvChunkParameter + /// If true the length field will be padded to a 4 byte boundary. + /// The length of the chunk. This method gets overridden by specialised SCTP parameters + /// that each have their own fields that determine the length. + public virtual ushort GetParameterLength(bool padded) { - public const int SCTP_PARAMETER_HEADER_LENGTH = 4; - - private static ILogger logger = SIPSorcery.LogFactory.CreateLogger(); - - /// - /// The type of the chunk parameter. - /// - public ushort ParameterType { get; protected set; } - - /// - /// The information contained in the parameter. - /// - public byte[] ParameterValue; - - /// - /// If this parameter is unrecognised by the parent chunk then this field dictates - /// how it should handle it. - /// - public SctpUnrecognisedParameterActions UnrecognisedAction => - (SctpUnrecognisedParameterActions) (ParameterType >> 14 & 0x03); - - protected SctpTlvChunkParameter() - { } - - /// - /// Creates a new chunk parameter instance. - /// - public SctpTlvChunkParameter(ushort parameterType, byte[] parameterValue) - { - ParameterType = parameterType; - ParameterValue = parameterValue; - } + var len = (ushort)(SCTP_PARAMETER_HEADER_LENGTH + + (ParameterValue is null ? 0 : ParameterValue.Length)); - /// - /// Calculates the length for the chunk parameter. - /// - /// If true the length field will be padded to a 4 byte boundary. - /// The length of the chunk. This method gets overridden by specialised SCTP parameters - /// that each have their own fields that determine the length. - public virtual ushort GetParameterLength(bool padded) - { - ushort len = (ushort)(SCTP_PARAMETER_HEADER_LENGTH - + (ParameterValue == null ? 0 : ParameterValue.Length)); + return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; + } - return (padded) ? SctpPadding.PadTo4ByteBoundary(len) : len; - } + /// + /// Writes the parameter header to the buffer. All chunk parameters use the same two + /// header fields. + /// + /// The buffer to write the chunk parameter header to. + protected void WriteParameterHeader(Span buffer) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer, ParameterType); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), GetParameterLength(false)); + } - /// - /// Writes the parameter header to the buffer. All chunk parameters use the same two - /// header fields. - /// - /// The buffer to write the chunk parameter header to. - /// The position in the buffer to write at. - protected void WriteParameterHeader(byte[] buffer, int posn) - { - NetConvert.ToBuffer(ParameterType, buffer, posn); - NetConvert.ToBuffer(GetParameterLength(false), buffer, posn + 2); - } + /// + /// Serialises the chunk parameter to a pre-allocated buffer. This method gets overridden + /// by specialised SCTP chunk parameters that have their own data and need to be serialised + /// differently. + /// + /// The buffer to write the serialised chunk parameter bytes to. It + /// must have the required space already allocated. + /// The number of bytes, including padding, written to the buffer. + public virtual int WriteBytes(Span buffer) + { + WriteBytesCore(buffer); - /// - /// Serialises the chunk parameter to a pre-allocated buffer. This method gets overridden - /// by specialised SCTP chunk parameters that have their own data and need to be serialised - /// differently. - /// - /// The buffer to write the serialised chunk parameter bytes to. It - /// must have the required space already allocated. - /// The position in the buffer to write to. - /// The number of bytes, including padding, written to the buffer. - public virtual int WriteTo(byte[] buffer, int posn) - { - WriteParameterHeader(buffer, posn); + return GetParameterLength(true); + } - if (ParameterValue?.Length > 0) - { - Buffer.BlockCopy(ParameterValue, 0, buffer, posn + SCTP_PARAMETER_HEADER_LENGTH, ParameterValue.Length); - } + private void WriteBytesCore(Span buffer) + { + WriteParameterHeader(buffer); - return GetParameterLength(true); + if (ParameterValue is { Length: > 0 } parameterValue) + { + parameterValue.CopyTo(buffer.Slice(SCTP_PARAMETER_HEADER_LENGTH)); } + } - /// - /// Serialises an SCTP chunk parameter to a byte array. - /// - /// The byte array containing the serialised chunk parameter. - public byte[] GetBytes() + /// + /// The first 32 bits of all chunk parameters represent the type and length. This method + /// parses those fields and sets them on the current instance. + /// + /// The buffer holding the serialised chunk parameter. + public ushort ParseFirstWord(ReadOnlySpan buffer) + { + ParameterType = BinaryPrimitives.ReadUInt16BigEndian(buffer); + var paramLen = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2)); + + if (paramLen > 0 && buffer.Length < paramLen) { - byte[] buffer = new byte[GetParameterLength(true)]; - WriteTo(buffer, 0); - return buffer; + // The buffer was not big enough to supply the specified chunk parameter. + throw new SipSorceryException($"The SCTP chunk parameter buffer was too short. Required {paramLen} bytes but only {buffer.Length} available."); } - /// - /// The first 32 bits of all chunk parameters represent the type and length. This method - /// parses those fields and sets them on the current instance. - /// - /// The buffer holding the serialised chunk parameter. - /// The position in the buffer that indicates the start of the chunk parameter. - public ushort ParseFirstWord(byte[] buffer, int posn) + return paramLen; + } + + /// + /// Parses an SCTP Type-Length-Value (TLV) chunk parameter from a buffer. + /// + /// The buffer holding the serialised TLV chunk parameter. + /// An SCTP TLV chunk parameter instance. + public static SctpTlvChunkParameter ParseTlvParameter(ReadOnlySpan buffer) + { + if (buffer.Length < SCTP_PARAMETER_HEADER_LENGTH) { - ParameterType = NetConvert.ParseUInt16(buffer, posn); - ushort paramLen = NetConvert.ParseUInt16(buffer, posn + 2); - - if (paramLen > 0 && buffer.Length < posn + paramLen) - { - // The buffer was not big enough to supply the specified chunk parameter. - int bytesRequired = paramLen; - int bytesAvailable = buffer.Length - posn; - throw new ApplicationException($"The SCTP chunk parameter buffer was too short. Required {bytesRequired} bytes but only {bytesAvailable} available."); - } - - return paramLen; + throw new SipSorceryException("Buffer did not contain the minimum of bytes for an SCTP TLV chunk parameter."); } - /// - /// Parses an SCTP Type-Length-Value (TLV) chunk parameter from a buffer. - /// - /// The buffer holding the serialised TLV chunk parameter. - /// The position to start parsing at. - /// An SCTP TLV chunk parameter instance. - public static SctpTlvChunkParameter ParseTlvParameter(byte[] buffer, int posn) + var tlvParam = new SctpTlvChunkParameter(); + var paramLen = tlvParam.ParseFirstWord(buffer); + if (paramLen > SCTP_PARAMETER_HEADER_LENGTH) { - if (buffer.Length < posn + SCTP_PARAMETER_HEADER_LENGTH) - { - throw new ApplicationException("Buffer did not contain the minimum of bytes for an SCTP TLV chunk parameter."); - } - - var tlvParam = new SctpTlvChunkParameter(); - ushort paramLen = tlvParam.ParseFirstWord(buffer, posn); - if (paramLen > SCTP_PARAMETER_HEADER_LENGTH) - { - tlvParam.ParameterValue = new byte[paramLen - SCTP_PARAMETER_HEADER_LENGTH]; - Buffer.BlockCopy(buffer, posn + SCTP_PARAMETER_HEADER_LENGTH, tlvParam.ParameterValue, - 0, tlvParam.ParameterValue.Length); - } - return tlvParam; + tlvParam.ParameterValue = buffer.Slice(SCTP_PARAMETER_HEADER_LENGTH, paramLen - SCTP_PARAMETER_HEADER_LENGTH).ToArray(); } + return tlvParam; } } diff --git a/src/SIPSorcery/net/SCTP/NetSctpLoggingExtensions.cs b/src/SIPSorcery/net/SCTP/NetSctpLoggingExtensions.cs new file mode 100644 index 0000000000..50ba221765 --- /dev/null +++ b/src/SIPSorcery/net/SCTP/NetSctpLoggingExtensions.cs @@ -0,0 +1,752 @@ +using System; +using System.Net; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; + +internal static partial class NetSdpLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "SctpPacketReceivedAborted", + Level = LogLevel.Warning, + Message = "SCTP packet received but association has been aborted, ignoring.")] + public static partial void LogSctpPacketReceivedAborted( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpPacketDroppedWrongVerificationTag", + Level = LogLevel.Warning, + Message = "SCTP packet dropped due to wrong verification tag, expected {ExpectedVerificationTag} got {ReceivedVerificationTag}.")] + public static partial void LogSctpPacketDroppedWrongVerificationTag( + this ILogger logger, + uint expectedVerificationTag, + uint receivedVerificationTag); + + [LoggerMessage( + EventId = 0, + EventName = "SctpPacketDroppedWrongDestinationPort", + Level = LogLevel.Warning, + Message = "SCTP packet dropped due to wrong SCTP destination port, expected {ExpectedDestinationPort} got {ReceivedDestinationPort}.")] + public static partial void LogSctpPacketDroppedWrongDestinationPort( + this ILogger logger, + ushort expectedDestinationPort, + ushort receivedDestinationPort); + + [LoggerMessage( + EventId = 0, + EventName = "SctpPacketDroppedWrongSourcePort", + Level = LogLevel.Warning, + Message = "SCTP packet dropped due to wrong SCTP source port, expected {ExpectedSourcePort} got {ReceivedSourcePort}.")] + public static partial void LogSctpPacketDroppedWrongSourcePort( + this ILogger logger, + ushort expectedSourcePort, + ushort receivedSourcePort); + + [LoggerMessage( + EventId = 0, + EventName = "SctpDataChunkReceived", + Level = LogLevel.Trace, + Message = "SCTP data chunk received on ID {ID} with TSN {TSN}, payload length {PayloadLength}, flags {Flags}.")] + public static partial void LogSctpDataChunkReceived( + this ILogger logger, + string id, + uint tsn, + int payloadLength, + byte flags); + + [LoggerMessage( + EventId = 0, + EventName = "SctpPacketAbortChunkReceived", + Level = LogLevel.Warning, + Message = "SCTP packet ABORT chunk received from remote party, reason {Reason}.")] + public static partial void LogSctpPacketAbortChunkReceived( + this ILogger logger, + string? reason); + + [LoggerMessage( + EventId = 0, + EventName = "SctpErrorReceived", + Level = LogLevel.Warning, + Message = "SCTP error {CauseCode}.")] + public static partial void LogSctpErrorReceived( + this ILogger logger, + SctpErrorCauseCode causeCode); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSendingShutdown", + Level = LogLevel.Trace, + Message = "SCTP sending shutdown for association {ID}, ACK TSN {ackTSN}.")] + public static partial void LogSctpSendingShutdown( + this ILogger logger, + string id, + uint? ackTSN); + + [LoggerMessage( + EventId = 0, + EventName = "SctpStateChanged", + Level = LogLevel.Trace, + Message = "SCTP state for association {ID} changed to {State}.")] + public static partial void LogSctpStateChanged( + this ILogger logger, + string id, + SctpAssociationState state); + + [LoggerMessage( + EventId = 0, + EventName = "ReceivedChunk", + Level = LogLevel.Trace, + Message = "SCTP receiver got data chunk with TSN {TSN}, last in order TSN {LastInOrderTSN}, in order receive count {InOrderReceiveCount}.")] + public static partial void LogReceivedChunk( + this ILogger logger, + uint tsn, + uint lastInOrderTSN, + uint inOrderReceiveCount); + + [LoggerMessage( + EventId = 0, + EventName = "SctpDuplicateTsnReceived", + Level = LogLevel.Trace, + Message = "SCTP duplicate TSN received for {TSN}.")] + public static partial void LogSctpDuplicateTsnReceived( + this ILogger logger, + uint tsn); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSenderExitingRetransmitMode", + Level = LogLevel.Trace, + Message = "SCTP sender exiting retransmit mode.")] + public static partial void LogSctpSenderExitingRetransmitMode( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpFirstSackReceived", + Level = LogLevel.Trace, + Message = "SCTP first SACK remote peer TSN ACK {CumulativeTsnAck} next sender TSN {TSN}, arwnd {ARwnd} (gap reports {GapAckBlocksCount}).")] + public static partial void LogSctpFirstSackReceived( + this ILogger logger, + uint cumulativeTsnAck, + uint tsn, + uint arwnd, + int gapAckBlocksCount); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSackTsnTooDistant", + Level = LogLevel.Warning, + Message = "SCTP SACK TSN from remote peer of {CumulativeTsnAck} was too distant from the expected {CumulativeAckTSN}, ignoring.")] + public static partial void LogSctpSackTsnTooDistant( + this ILogger logger, + uint cumulativeTsnAck, + uint cumulativeAckTSN); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSackTsnBehindExpected", + Level = LogLevel.Warning, + Message = "SCTP SACK TSN from remote peer of {CumulativeTsnAck} was behind expected {CumulativeAckTSN}, ignoring.")] + public static partial void LogSctpSackTsnBehindExpected( + this ILogger logger, + uint cumulativeTsnAck, + uint cumulativeAckTSN); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSackReceived", + Level = LogLevel.Trace, + Message = "SCTP SACK remote peer TSN ACK {CumulativeTsnAck}, next sender TSN {TSN}, arwnd {ARwnd} (gap reports {GapAckBlocksCount}).")] + public static partial void LogSctpSackReceived( + this ILogger logger, + uint cumulativeTsnAck, + uint tsn, + uint arwnd, + int gapAckBlocksCount); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSackReceivedNoChange", + Level = LogLevel.Trace, + Message = "SCTP SACK remote peer TSN ACK no change {CumulativeAckTSN}, next sender TSN {TSN}, arwnd {ARwnd} (gap reports {GapAckBlocksCount}).")] + public static partial void LogSctpSackReceivedNoChange( + this ILogger logger, + uint cumulativeAckTSN, + uint tsn, + uint arwnd, + int gapAckBlocksCount); + + [LoggerMessage( + EventId = 0, + EventName = "ExitingFastRecovery", + Level = LogLevel.Trace, + Message = "SCTP sender exiting fast recovery at TSN {FastRecoveryExitPoint}")] + public static partial void LogExitingFastRecovery( + this ILogger logger, + uint fastRecoveryExitPoint); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAcknowledgedDataChunkReceipt", + Level = LogLevel.Trace, + Message = "SCTP acknowledged data chunk receipt in gap report for TSN {TSN}")] + public static partial void LogSctpAcknowledgedDataChunkReceipt( + this ILogger logger, + uint tsn); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSackGapReport", + Level = LogLevel.Trace, + Message = "SCTP SACK gap report start TSN {goodTSNStart} gap report end TSN {gapBlockEnd} first missing TSN {missingTSN}.")] + public static partial void LogSctpSackGapReport( + this ILogger logger, + uint goodTSNStart, + uint gapBlockEnd, + uint missingTSN); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSackGapAddingRetransmitEntry", + Level = LogLevel.Trace, + Message = "SCTP SACK gap adding retransmit entry for TSN {TSN}.")] + public static partial void LogSctpSackGapAddingRetransmitEntry( + this ILogger logger, + uint tsn); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSenderEnteringFastRecoveryMode", + Level = LogLevel.Trace, + Message = "SCTP sender entering fast recovery mode due to missing TSN {MissingTsn}. Fast recovery exit point {FastRecoveryExitPoint}.")] + public static partial void LogSctpSenderEnteringFastRecoveryMode( + this ILogger logger, + uint missingTsn, + uint fastRecoveryExitPoint); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSenderRemovingUnconfirmedChunks", + Level = LogLevel.Trace, + Message = "SCTP data sender removing unconfirmed chunks cumulative ACK TSN {CumulativeAckTsn}, SACK TSN {SackTsn}.")] + public static partial void LogSctpSenderRemovingUnconfirmedChunks( + this ILogger logger, + uint cumulativeAckTSN, + uint sackTSN); + + [LoggerMessage( + EventId = 0, + EventName = "SctpResendingMissingDataChunk", + Level = LogLevel.Trace, + Message = "SCTP resending missing data chunk for TSN {TSN}, data length {UserDataLength}, flags {ChunkFlags:X2}, send count {SendCount}.")] + public static partial void LogSctpResendingMissingDataChunk( + this ILogger logger, + uint tSN, + int userDataLength, + byte chunkFlags, + int sendCount); + + [LoggerMessage( + EventId = 0, + EventName = "SctpRetransmittingDataChunk", + Level = LogLevel.Trace, + Message = "SCTP retransmitting data chunk for TSN {Tsn}, data length {DataLength}, flags {ChunkFlags}, send count {SendCount}.")] + public static partial void LogSctpRetransmittingDataChunk( + this ILogger logger, + uint tSN, + int dataLength, + byte chunkFlags, + int sendCount); + + [LoggerMessage( + EventId = 0, + EventName = "EnteringRetransmitMode", + Level = LogLevel.Trace, + Message = "SCTP sender entering retransmit mode.")] + public static partial void LogEnteringRetransmitMode( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpResendingMissingDataChunk2", + Level = LogLevel.Trace, + Message = "SCTP resending missing data chunk for TSN {TSN}, data length {UserDataLength}, flags {ChunkFlags:X2}, send count {SendCount}.")] + public static partial void LogSctpResendingMissingDataChunk2( + this ILogger logger, + uint tSN, + int userDataLength, + byte chunkFlags, + int sendCount); + + [LoggerMessage( + EventId = 0, + EventName = "SlowStartIncreased", + Level = LogLevel.Trace, + Message = "SCTP sender congestion window in slow-start increased from {OldCongestionWindow} to {NewCongestionWindow}.")] + public static partial void LogSlowStartIncreased( + this ILogger logger, + uint oldCongestionWindow, + uint newCongestionWindow); + + [LoggerMessage( + EventId = 0, + EventName = "CongestionAvoidanceIncreased", + Level = LogLevel.Trace, + Message = "SCTP sender congestion window in congestion avoidance increased from {OldCongestionWindow} to {NewCongestionWindow}.")] + public static partial void LogCongestionAvoidanceIncreased( + this ILogger logger, + uint oldCongestionWindow, + uint newCongestionWindow); + + [LoggerMessage( + EventId = 0, + EventName = "SctpCreatingNewAssociation", + Level = LogLevel.Debug, + Message = "SCTP creating new association for {RemoteEndPoint}.")] + public static partial void LogSctpCreatingNewAssociation( + this ILogger logger, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "SctpFailedToAddNewAssociation", + Level = LogLevel.Error, + Message = "SCTP failed to add new association to dictionary.")] + public static partial void LogSctpFailedToAddNewAssociation( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpTransportFailedToAddAssociation", + Level = LogLevel.Warning, + Message = "SCTP transport failed to add association.")] + public static partial void LogSctpTransportFailedToAddAssociation( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpCookieEchoInvalidHMAC", + Level = LogLevel.Warning, + Message = "SCTP COOKIE ECHO chunk had an invalid HMAC, calculated {CalculatedHMAC}, cookie {CookieHMAC}.")] + public static partial void LogSctpCookieEchoInvalidHMAC( + this ILogger logger, + string calculatedHMAC, + string cookieHMAC); + + [LoggerMessage( + EventId = 0, + EventName = "SctpCookieEchoStale", + Level = LogLevel.Warning, + Message = "SCTP COOKIE ECHO chunk was stale, created at {CreatedAt}, now {Now}, lifetime {Lifetime}s.")] + public static partial void LogSctpCookieEchoStale( + this ILogger logger, + string createdAt, + string now, + int lifetime); + + [LoggerMessage( + EventId = 0, + EventName = "SctpDataReceiverOldChunk", + Level = LogLevel.Warning, + Message = "SCTP data receiver received an old data chunk with TSN {TSN} when the expected TSN was {ExpectedTSN}, ignoring.")] + public static partial void LogSctpDataReceiverOldChunk( + this ILogger logger, + uint tsn, + uint expectedTSN); + + [LoggerMessage( + EventId = 0, + EventName = "SctpDataReceiverChunkTooDistant", + Level = LogLevel.Warning, + Message = "SCTP data receiver received a data chunk with a TSN {TSN} when the expected TSN was {ExpectedTSN} and a window size of {WindowSize}, ignoring.")] + public static partial void LogSctpDataReceiverChunkTooDistant( + this ILogger logger, + uint tsn, + uint expectedTSN, + ushort windowSize); + + [LoggerMessage( + EventId = 0, + EventName = "SctpDataReceiverInitialChunkTooDistant", + Level = LogLevel.Warning, + Message = "SCTP data receiver received a data chunk with a TSN {TSN} when the initial TSN was {InitialTSN} and a window size of {WindowSize}, ignoring.")] + public static partial void LogSctpDataReceiverInitialChunkTooDistant( + this ILogger logger, + uint tsn, + uint initialTSN, + ushort windowSize); + + [LoggerMessage( + EventId = 0, + EventName = "SctpStreamBufferCapacity", + Level = LogLevel.Warning, + Message = "Stream {StreamID} is at buffer capacity. Rejected out-of-order data chunk TSN {TSN}.")] + public static partial void LogSctpStreamBufferCapacity( + this ILogger logger, + ushort streamID, + uint tsn); + + [LoggerMessage( + EventId = 0, + EventName = "SctpChunkBufferTooShort", + Level = LogLevel.Warning, + Message = "The SCTP chunk buffer was too short. Required {BytesRequired} bytes but only {BytesAvailable} available.")] + public static partial void LogSctpChunkBufferTooShort( + this ILogger logger, + int bytesRequired, + int bytesAvailable); + + [LoggerMessage( + EventId = 0, + EventName = "SctpImplementParsingLogic", + Level = LogLevel.Debug, + Message = "TODO: Implement parsing logic for well known chunk type {ChunkType}.")] + public static partial void LogSctpImplementParsingLogic( + this ILogger logger, + SctpChunkType chunkType); + + [LoggerMessage( + EventId = 0, + EventName = "SctpPacketDroppedInvalidChecksum", + Level = LogLevel.Warning, + Message = "SCTP packet from UDP {RemoteEndPoint} dropped due to invalid checksum.")] + public static partial void LogSctpPacketDroppedInvalidChecksum( + this ILogger logger, + IPEndPoint? remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "SctpErrorAcquiringHandshakeCookie", + Level = LogLevel.Warning, + Message = "SCTP error acquiring handshake cookie from COOKIE ECHO chunk.")] + public static partial void LogSctpErrorAcquiringHandshakeCookie( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpTransportEncapsulationReceiverClosed", + Level = LogLevel.Information, + Message = "SCTP transport encapsulation receiver closed with reason: {Reason}.")] + public static partial void LogSctpTransportEncapsulationReceiverClosed( + this ILogger logger, + string? reason); + + [LoggerMessage( + EventId = 0, + EventName = "SctpTransportOnEncapsulationSocketPacketReceivedException", + Level = LogLevel.Error, + Message = "Exception SctpTransport.OnEncapsulationSocketPacketReceived. {ErrorMessage}")] + public static partial void LogSctpTransportOnEncapsulationSocketPacketReceivedException( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "SctpCookie", + Level = LogLevel.Debug, + Message = "Cookie: {Cookie}", + SkipEnabledCheck = true)] + private static partial void LogSctpCookieUnchecked( + this ILogger logger, + string cookie); + + public static void LogSctpCookie( + this ILogger logger, + SctpTransportCookie cookie) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + LogSctpCookieUnchecked(logger, JsonSerializer.Serialize(cookie, SipSorceryJsonSerializerContext.Default.SctpTransportCookie)); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "SctpSenderWaitPeriod", + Level = LogLevel.Trace, + Message = "SCTP sender wait period {Wait}ms, arwnd {ReceiverWindow}, cwnd {CongestionWindow} " + + "outstanding bytes {OutstandingBytes}, send queue {SendQueueCount}, missing {MissingChunksCount} " + + "unconfirmed {UnconfirmedChunksCount}.")] + public static partial void LogSctpSenderWaitPeriod( + this ILogger logger, + int wait, + uint receiverWindow, + uint congestionWindow, + uint outstandingBytes, + int sendQueueCount, + int missingChunksCount, + int unconfirmedChunksCount); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSenderBurstSize", + Level = LogLevel.Trace, + Message = "SCTP sender burst size {BurstSize}, in retransmit mode {InRetransmitMode}, cwnd {CongestionWindow}, arwnd {ReceiverWindow}.")] + public static partial void LogSctpSenderBurstSize( + this ILogger logger, + int burstSize, + bool inRetransmitMode, + uint congestionWindow, + uint receiverWindow); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSenderRetransmitMode", + Level = LogLevel.Debug, + Message = "SCTP sender wait period {Wait}ms, arwnd {ReceiverWindow}, cwnd {CongestionWindow} " + + "outstanding bytes {OutstandingBytes}, send queue {SendQueueCount}, missing {MissingChunksCount} " + + "unconfirmed {UnconfirmedChunksCount}.")] + public static partial void LogSctpSenderRetransmitMode( + this ILogger logger, + int wait, + uint receiverWindow, + uint congestionWindow, + uint outstandingBytes, + int sendQueueCount, + int missingChunksCount, + int unconfirmedChunksCount); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAssociationDataSendThreadStarted", + Level = LogLevel.Debug, + Message = "SCTP association data send thread started for association {AssociationID}.")] + public static partial void LogSctpDataSendThreadStarted( + this ILogger logger, + string associationID); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAssociationDataSendThreadStopped", + Level = LogLevel.Debug, + Message = "SCTP association data send thread stopped for association {AssociationID}.")] + public static partial void LogSctpDataSendThreadStopped( + this ILogger logger, + string associationID); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSackGapReportStartTooDistant", + Level = LogLevel.Warning, + Message = "SCTP SACK gap report had a start TSN of {GoodTsnStart} too distant from last good TSN {LastAckTsn}, ignoring rest of SACK.")] + public static partial void LogSctpSackGapReportStartTooDistant( + this ILogger logger, + uint goodTsnStart, + uint lastAckTsn); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSackGapReportStartBehind", + Level = LogLevel.Warning, + Message = "SCTP SACK gap report had a start TSN of {GoodTsnStart} behind last good TSN {LastAckTsn}, ignoring rest of SACK.")] + public static partial void LogSctpSackGapReportStartBehind( + this ILogger logger, + uint goodTsnStart, + uint lastAckTsn); + + [LoggerMessage( + EventId = 0, + EventName = "SctpNoMatchingUnconfirmedChunk", + Level = LogLevel.Warning, + Message = "SCTP SACK gap report reported missing TSN of {MissingTSN} but no matching unconfirmed chunk available.")] + public static partial void LogSctpNoMatchingUnconfirmedChunk( + this ILogger logger, + uint missingTSN); + + [LoggerMessage( + EventId = 0, + EventName = "SctpWindowSizeSet", + Level = LogLevel.Debug, + Message = "SCTP windows size for data receiver set at {WindowSize}.")] + public static partial void LogSctpWindowSizeSet( + this ILogger logger, + int windowSize); + + [LoggerMessage( + EventId = 0, + EventName = "SctpTransportClosed", + Level = LogLevel.Information, + Message = "SCTP transport encapsulation receiver closed with reason: {reason}.")] + public static partial void LogSctpTransportClosed( + this ILogger logger, + string reason); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAssociationCannotInitialise", + Level = LogLevel.Warning, + Message = "SCTP association cannot be initialised in state {state}.")] + public static partial void LogSctpAssociationCannotInitialise( + this ILogger logger, + SctpAssociationState state); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAssociationCannotInitialiseAfterAbortOrShutdown", + Level = LogLevel.Warning, + Message = "SCTP association cannot be initialised after an abort or shutdown.")] + public static partial void LogSctpAssociationCannotInitialiseAfterAbortOrShutdown( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAssociationCannotSendDataInState", + Level = LogLevel.Warning, + Message = "SCTP send data is not allowed for an association in state {state}.")] + public static partial void LogSctpAssociationCannotSendDataInState( + this ILogger logger, + SctpAssociationState state); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSendDataNotAllowedAfterAbort", + Level = LogLevel.Warning, + Message = "SCTP send data is not allowed on an aborted association.")] + public static partial void LogSctpSendDataNotAllowedAfterAbort( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpSourcePortCannotBeUpdated", + Level = LogLevel.Warning, + Message = "SCTP source port cannot be updated when the association is in state {state}.")] + public static partial void LogSctpSourcePortCannotBeUpdated( + this ILogger logger, + SctpAssociationState state); + + [LoggerMessage( + EventId = 0, + EventName = "SctpDestinationPortCannotBeUpdated", + Level = LogLevel.Warning, + Message = "SCTP destination port cannot be updated when the association is in state {state}.")] + public static partial void LogSctpDestinationPortCannotBeUpdated( + this ILogger logger, + SctpAssociationState state); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAssociationCookieInitialisationNotAllowed", + Level = LogLevel.Warning, + Message = "SCTP association cannot initialise with a cookie after an abort or shutdown.")] + public static partial void LogSctpAssociationCookieInitialisationNotAllowed( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAssociationTimedOutInitAck", + Level = LogLevel.Warning, + Message = "SCTP timed out waiting for INIT ACK chunk from remote peer.")] + public static partial void LogSctpAssociationTimedOutInitAck( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAssociationTimedOutCookieAck", + Level = LogLevel.Warning, + Message = "SCTP timed out waiting for COOKIE ACK chunk from remote peer.")] + public static partial void LogSctpAssociationTimedOutCookieAck( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAssociationNoRuleForChunk", + Level = LogLevel.Warning, + Message = "SCTP association no rule for {chunkType} in state of {state}.")] + public static partial void LogSctpAssociationNoRuleForChunk( + this ILogger logger, + SctpChunkType chunkType, + SctpAssociationState state); + + [LoggerMessage( + EventId = 0, + EventName = "SctpInitAckInWrongState", + Level = LogLevel.Warning, + Message = "SCTP association received INIT_ACK chunk in wrong state of {state}, ignoring.")] + public static partial void LogSctpInitAckInWrongState( + this ILogger logger, + SctpAssociationState state); + + [LoggerMessage( + EventId = 0, + EventName = "SctpUnrecognisedChunkType", + Level = LogLevel.Warning, + Message = "SCTP unrecognised chunk type {chunkType} indicated no further chunks should be processed.")] + public static partial void LogSctpUnrecognisedChunkType( + this ILogger logger, + byte chunkType); + + [LoggerMessage( + EventId = 0, + EventName = "SctpUnrecognisedParameter", + Level = LogLevel.Warning, + Message = "SCTP unrecognised parameter {ParameterType} for chunk type {ChunkType} indicated no further chunks should be processed.")] + public static partial void LogSctpUnrecognisedParameter( + this ILogger logger, + ushort parameterType, + SctpChunkType? chunkType); + + [LoggerMessage( + EventId = 0, + EventName = "SctpPacketReceivedException", + Level = LogLevel.Error, + Message = "Exception SctpTransport.OnEncapsulationSocketPacketReceived. {ErrorMessage}")] + public static partial void LogSctpPacketReceivedException( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "SctpDataReceiverDistantInitialTsn", + Level = LogLevel.Warning, + Message = "SCTP data receiver received a data chunk with a TSN {TSN} when the initial TSN was {InitialTSN} and a window size of {WindowSize}, ignoring.")] + public static partial void LogSctpDataReceiverDistantInitialTsn( + this ILogger logger, + uint tsn, + uint initialTsn, + ushort windowSize); + + [LoggerMessage( + EventId = 0, + EventName = "SctpDataReceiverDistantLastInOrderTsn", + Level = LogLevel.Warning, + Message = "SCTP data receiver received a data chunk with a TSN {TSN} when the expected TSN was {ExpectedTSN} and a window size of {WindowSize}, ignoring.")] + public static partial void LogSctpDataReceiverDistantLastInOrderTsn( + this ILogger logger, + uint tsn, + uint expectedTsn, + ushort windowSize); + + [LoggerMessage( + EventId = 0, + EventName = "SctpDataReceiverOldChunkTsn", + Level = LogLevel.Warning, + Message = "SCTP data receiver received an old data chunk with TSN {TSN} when the expected TSN was {ExpectedTSN}, ignoring.")] + public static partial void LogSctpDataReceiverOldChunkTsn( + this ILogger logger, + uint tsn, + uint expectedTsn); + + [LoggerMessage( + EventId = 0, + EventName = "SctpTransportAssociationFailed", + Level = LogLevel.Warning, + Message = "SCTP transport failed to add association.")] + public static partial void LogSctpTransportAssociationFailed( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SctpStreamBufferAtCapacity", + Level = LogLevel.Warning, + Message = "Stream {streamId} is at buffer capacity. Rejected out-of-order data chunk TSN {tsn}.")] + public static partial void LogSctpStreamBufferAtCapacity( + this ILogger logger, + ushort streamId, + uint tsn); +} diff --git a/src/SIPSorcery/net/SCTP/SctpAssociation.cs b/src/SIPSorcery/net/SCTP/SctpAssociation.cs index ad6069ca46..750ac5039f 100644 --- a/src/SIPSorcery/net/SCTP/SctpAssociation.cs +++ b/src/SIPSorcery/net/SCTP/SctpAssociation.cs @@ -14,796 +14,829 @@ //----------------------------------------------------------------------------- using System; -using System.Linq; +using System.Buffers; +using System.Diagnostics; using System.Net; using System.Text; using System.Threading; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public enum SctpAssociationState { - public enum SctpAssociationState - { - Closed, - CookieWait, - CookieEchoed, - Established, - ShutdownPending, - ShutdownSent, - ShutdownReceived, - ShutdownAckSent - } + Closed, + CookieWait, + CookieEchoed, + Established, + ShutdownPending, + ShutdownSent, + ShutdownReceived, + ShutdownAckSent +} + +/// +/// Represents the current status of an SCTP association. +/// +/// +/// The address list items have not been included due to the assumption +/// they are not relevant for SCTP encapsulated in UDP. +/// The status data is defined on page 115 of the SCTP RFC +/// https://tools.ietf.org/html/rfc4960#page-115. +/// +public struct SctpStatus +{ + public SctpAssociationState AssociationConnectionState; + public int ReceiverWindowSize; + public int CongestionWindowSizes; + public int UnacknowledgedChunksCount; + public int PendingReceiptChunksCount; +} + +/// +/// An SCTP association represents an established connection between two SCTP endpoints. +/// This class also represents the Transmission Control Block (TCB) referred to in RFC4960. +/// +public class SctpAssociation +{ + public const uint DEFAULT_ADVERTISED_RECEIVE_WINDOW = 262144U; + public const int DEFAULT_NUMBER_OUTBOUND_STREAMS = 65535; + public const int DEFAULT_NUMBER_INBOUND_STREAMS = 65535; + private const byte SHUTDOWN_CHUNK_TBIT_FLAG = 0x01; + + /// + /// Length of time to wait for the INIT ACK response after sending an INIT. + /// + private const int T1_INIT_TIMER_MILLISECONDS = 1000; + + private const int MAX_INIT_RETRANSMITS = 3; + + /// + /// Length of time to wait for the COOKIE ACK response after sending a COOKIE ECHO. + /// + private const int T1_COOKIE_TIMER_MILLISECONDS = 1000; + + private const int MAX_COOKIE_ECHO_RETRANSMITS = 3; + + private static ILogger logger = LogFactory.CreateLogger(); + + private SctpTransport _sctpTransport; + private ushort _sctpSourcePort; + private ushort _sctpDestinationPort; + private ushort _defaultMTU; + private ushort _numberOutboundStreams; + private ushort _numberInboundStreams; + private bool _wasAborted; + private bool _wasShutdown; + private bool _initialisationFailed; + private int _initRetransmits; + private int _cookieEchoRetransmits; + + /// + /// Handles logic for DATA chunk receives (fragmentation, in order delivery etc). + /// + private SctpDataReceiver? _dataReceiver; /// - /// Represents the current status of an SCTP association. + /// Handles logic for sending DATA chunks (retransmits, windows management etc). + /// + private SctpDataSender? _dataSender; + + /// + /// T1 init timer to monitor an INIT request sent to a remote peer. /// /// - /// The address list items have not been included due to the assumption - /// they are not relevant for SCTP encapsulated in UDP. - /// The status data is defined on page 115 of the SCTP RFC - /// https://tools.ietf.org/html/rfc4960#page-115. + /// https://tools.ietf.org/html/rfc4960#section-5.1 (section A) /// - public struct SctpStatus + private Timer? _t1Init; + + /// + /// T1 init timer to monitor an COOKIE ECHO request sent to a remote peer. + /// + /// + /// https://tools.ietf.org/html/rfc4960#section-5.1 (section C) + /// + private Timer? _t1Cookie; + + /// + /// The total size (in bytes) of outgoing user data queued in the . + /// + public ulong SendBufferedAmount => _dataSender?.BufferedAmount ?? 0; + + public uint VerificationTag { get; private set; } + + /// + /// Transaction Sequence Number (TSN). A monotonically increasing number that must be + /// included in every DATA chunk. + /// + public uint TSN { - public SctpAssociationState AssociationConnectionState; - public int ReceiverWindowSize; - public int CongestionWindowSizes; - public int UnacknowledgedChunksCount; - public int PendingReceiptChunksCount; + get + { + Debug.Assert(_dataSender is { }); + return _dataSender.TSN; + } } /// - /// An SCTP association represents an established connection between two SCTP endpoints. - /// This class also represents the Transmission Control Block (TCB) referred to in RFC4960. + /// A unique ID for this association. The ID is not part of the SCTP protocol. It + /// is provided as a convenience measure in case a transport of application needs + /// to keep track of multiple associations. /// - public class SctpAssociation + public readonly string ID; + + /// + /// Advertised Receiver Window Credit. This value represents the dedicated + /// buffer space, in number of bytes, that will be used for the receive buffer + /// for this association. + /// + public uint ARwnd { get; private set; } + + private uint _remoteVerificationTag; + private uint _remoteInitialTSN; + + /// + /// The remote destination end point for this association. The underlying transport + /// will supply this field if it is needed (the UDP encapsulation transport needs it, + /// the DTSL transport does not). + /// + public IPEndPoint? Destination { get; private set; } + + /// + /// Indicates the current connection state of the association. + /// + public SctpAssociationState State { get; private set; } + + /// + /// Event to notify application that the association state has changed. + /// + public event Action? OnAssociationStateChanged; + + /// + /// Event to notify application that user data is available. + /// + public event Action? OnData; + + /// + /// Event to notify the application that the remote party aborted this + /// association. + /// + public event Action? OnAbortReceived; + + /// + /// Event to notify the application that an error occurred that caused + /// the association to be aborted locally. + /// + public event Action? OnAborted; + + /// + /// Create a new SCTP association instance where the INIT will be generated + /// from this end of the connection. + /// + /// The transport layer doing the actual sending and receiving of + /// packets, e.g. UDP, DTLS, raw sockets etc. + /// Optional. The remote destination end point for this association. + /// Some transports, such as DTLS, are already established and do not use this parameter. + /// The source port for the SCTP packet header. + /// The destination port for the SCTP packet header. + /// The default Maximum Transmission Unit (MTU) for the underlying + /// transport. This determines the maximum size of an SCTP packet that will be used with + /// the transport. + /// Optional. The local transport (e.g. UDP or DTLS) port being + /// used for the underlying SCTP transport. This be set on the SCTP association's ID to aid in + /// diagnostics. + public SctpAssociation( + SctpTransport sctpTransport, + IPEndPoint? destination, + ushort sctpSourcePort, + ushort sctpDestinationPort, + ushort defaultMTU, + int localTransportPort, + ushort numberOutboundStreams = DEFAULT_NUMBER_OUTBOUND_STREAMS, + ushort numberInboundStreams = DEFAULT_NUMBER_INBOUND_STREAMS) { - public const uint DEFAULT_ADVERTISED_RECEIVE_WINDOW = 262144U; - public const int DEFAULT_NUMBER_OUTBOUND_STREAMS = 65535; - public const int DEFAULT_NUMBER_INBOUND_STREAMS = 65535; - private const byte SHUTDOWN_CHUNK_TBIT_FLAG = 0x01; - - /// - /// Length of time to wait for the INIT ACK response after sending an INIT. - /// - private const int T1_INIT_TIMER_MILLISECONDS = 1000; - - private const int MAX_INIT_RETRANSMITS = 3; - - /// - /// Length of time to wait for the COOKIE ACK response after sending a COOKIE ECHO. - /// - private const int T1_COOKIE_TIMER_MILLISECONDS = 1000; - - private const int MAX_COOKIE_ECHO_RETRANSMITS = 3; - - private static ILogger logger = LogFactory.CreateLogger(); - - SctpTransport _sctpTransport; - private ushort _sctpSourcePort; - private ushort _sctpDestinationPort; - private ushort _defaultMTU; - private ushort _numberOutboundStreams; - private ushort _numberInboundStreams; - private bool _wasAborted; - private bool _wasShutdown; - private bool _initialisationFailed; - private int _initRetransmits; - private int _cookieEchoRetransmits; - - /// - /// Handles logic for DATA chunk receives (fragmentation, in order delivery etc). - /// - private SctpDataReceiver _dataReceiver; - - /// - /// Handles logic for sending DATA chunks (retransmits, windows management etc). - /// - private SctpDataSender _dataSender; - - /// - /// T1 init timer to monitor an INIT request sent to a remote peer. - /// - /// - /// https://tools.ietf.org/html/rfc4960#section-5.1 (section A) - /// - private Timer _t1Init; - - /// - /// T1 init timer to monitor an COOKIE ECHO request sent to a remote peer. - /// - /// - /// https://tools.ietf.org/html/rfc4960#section-5.1 (section C) - /// - private Timer _t1Cookie; - - /// - /// The total size (in bytes) of outgoing user data queued in the . - /// - public ulong SendBufferedAmount => _dataSender?.BufferedAmount ?? 0; - - public uint VerificationTag { get; private set; } - - /// - /// Transaction Sequence Number (TSN). A monotonically increasing number that must be - /// included in every DATA chunk. - /// - public uint TSN => _dataSender.TSN; - - /// - /// A unique ID for this association. The ID is not part of the SCTP protocol. It - /// is provided as a convenience measure in case a transport of application needs - /// to keep track of multiple associations. - /// - public readonly string ID; - - /// - /// Advertised Receiver Window Credit. This value represents the dedicated - /// buffer space, in number of bytes, that will be used for the receive buffer - /// for this association. - /// - public uint ARwnd { get; private set; } - - private uint _remoteVerificationTag; - private uint _remoteInitialTSN; - - /// - /// The remote destination end point for this association. The underlying transport - /// will supply this field if it is needed (the UDP encapsulation transport needs it, - /// the DTSL transport does not). - /// - public IPEndPoint Destination { get; private set; } - - /// - /// Indicates the current connection state of the association. - /// - public SctpAssociationState State { get; private set; } - - /// - /// Event to notify application that the association state has changed. - /// - public event Action OnAssociationStateChanged; - - /// - /// Event to notify application that user data is available. - /// - public event Action OnData; - - /// - /// Event to notify the application that the remote party aborted this - /// association. - /// - public event Action OnAbortReceived; - - /// - /// Event to notify the application that an error occurred that caused - /// the association to be aborted locally. - /// - public event Action OnAborted; - - /// - /// Create a new SCTP association instance where the INIT will be generated - /// from this end of the connection. - /// - /// The transport layer doing the actual sending and receiving of - /// packets, e.g. UDP, DTLS, raw sockets etc. - /// Optional. The remote destination end point for this association. - /// Some transports, such as DTLS, are already established and do not use this parameter. - /// The source port for the SCTP packet header. - /// The destination port for the SCTP packet header. - /// The default Maximum Transmission Unit (MTU) for the underlying - /// transport. This determines the maximum size of an SCTP packet that will be used with - /// the transport. - /// Optional. The local transport (e.g. UDP or DTLS) port being - /// used for the underlying SCTP transport. This be set on the SCTP association's ID to aid in - /// diagnostics. - public SctpAssociation( - SctpTransport sctpTransport, - IPEndPoint destination, - ushort sctpSourcePort, - ushort sctpDestinationPort, - ushort defaultMTU, - int localTransportPort, - ushort numberOutboundStreams = DEFAULT_NUMBER_OUTBOUND_STREAMS, - ushort numberInboundStreams = DEFAULT_NUMBER_INBOUND_STREAMS) + _sctpTransport = sctpTransport; + Destination = destination; + _sctpSourcePort = sctpSourcePort; + _sctpDestinationPort = sctpDestinationPort; + _defaultMTU = defaultMTU; + _numberOutboundStreams = numberOutboundStreams; + _numberInboundStreams = numberInboundStreams; + VerificationTag = Crypto.GetRandomUInt(true); + + ID = $"{sctpSourcePort}:{sctpDestinationPort}:{localTransportPort}"; + ARwnd = DEFAULT_ADVERTISED_RECEIVE_WINDOW; + + _dataReceiver = new SctpDataReceiver(ARwnd, _defaultMTU, 0); + _dataSender = new SctpDataSender(ID, this.SendChunk, defaultMTU, Crypto.GetRandomUInt(true), DEFAULT_ADVERTISED_RECEIVE_WINDOW); + + State = SctpAssociationState.Closed; + } + + /// + /// Create a new SCTP association instance from the cookie that was previously + /// sent to the remote party in an INIT ACK chunk. + /// + public SctpAssociation( + SctpTransport sctpTransport, + SctpTransportCookie cookie, + int localTransportPort) + { + _sctpTransport = sctpTransport; + ID = $"{cookie.SourcePort}:{cookie.DestinationPort}:{localTransportPort}"; + State = SctpAssociationState.Closed; + + GotCookie(cookie); + } + + /// + /// Attempts to update the association's SCTP source port. + /// + /// The updated source port. + public void UpdateSourcePort(ushort port) + { + if (State != SctpAssociationState.Closed) { - _sctpTransport = sctpTransport; - Destination = destination; - _sctpSourcePort = sctpSourcePort; - _sctpDestinationPort = sctpDestinationPort; - _defaultMTU = defaultMTU; - _numberOutboundStreams = numberOutboundStreams; - _numberInboundStreams = numberInboundStreams; - VerificationTag = Crypto.GetRandomUInt(true); - - ID = $"{sctpSourcePort}:{sctpDestinationPort}:{localTransportPort}"; - ARwnd = DEFAULT_ADVERTISED_RECEIVE_WINDOW; - - _dataReceiver = new SctpDataReceiver(ARwnd, _defaultMTU, 0); - _dataSender = new SctpDataSender(ID, this.SendChunk, defaultMTU, Crypto.GetRandomUInt(true), DEFAULT_ADVERTISED_RECEIVE_WINDOW); - - State = SctpAssociationState.Closed; + logger.LogSctpSourcePortCannotBeUpdated(State); } - - /// - /// Create a new SCTP association instance from the cookie that was previously - /// sent to the remote party in an INIT ACK chunk. - /// - public SctpAssociation( - SctpTransport sctpTransport, - SctpTransportCookie cookie, - int localTransportPort) + else { - _sctpTransport = sctpTransport; - ID = $"{cookie.SourcePort}:{cookie.DestinationPort}:{localTransportPort}"; - State = SctpAssociationState.Closed; - - GotCookie(cookie); + _sctpSourcePort = port; } + } - /// - /// Attempts to update the association's SCTP source port. - /// - /// The updated source port. - public void UpdateSourcePort(ushort port) + /// + /// Attempts to update the association's SCTP destination port. + /// + /// The updated destination port. + public void UpdateDestinationPort(ushort port) + { + if (State != SctpAssociationState.Closed) { - if (State != SctpAssociationState.Closed) - { - logger.LogWarning("SCTP source port cannot be updated when the association is in state {State}.", State); - } - else - { - _sctpSourcePort = port; - } + logger.LogSctpDestinationPortCannotBeUpdated(State); } - - /// - /// Attempts to update the association's SCTP destination port. - /// - /// The updated destination port. - public void UpdateDestinationPort(ushort port) + else { - if (State != SctpAssociationState.Closed) - { - logger.LogWarning("SCTP destination port cannot be updated when the association is in state {State}.", State); - } - else - { - _sctpDestinationPort = port; - } + _sctpDestinationPort = port; } + } - /// - /// Attempts to initialise the association by sending an INIT chunk to the remote peer. - /// - public void Init() + /// + /// Attempts to initialise the association by sending an INIT chunk to the remote peer. + /// + public void Init() + { + if (_wasAborted || _wasShutdown || _initialisationFailed) { - if (_wasAborted || _wasShutdown || _initialisationFailed) - { - logger.LogWarning("SCTP association cannot be initialised after an abort or shutdown."); - } - else if (State == SctpAssociationState.Closed) - { - SendInit(); - } - else - { - logger.LogWarning("SCTP association cannot be initialised in state {State}.", State); - } + logger.LogSctpAssociationCannotInitialiseAfterAbortOrShutdown(); } - - /// - /// Initialises the association state based on the echoed cookie (the cookie that we sent - /// to the remote party and was then echoed back to us). An association can only be initialised - /// from a cookie prior to it being used and prior to it ever having entered the established state. - /// - /// The echoed cookie that was returned from the remote party. - public void GotCookie(SctpTransportCookie cookie) + else if (State == SctpAssociationState.Closed) { - // The CookieEchoed state is allowed, even though a cookie should be creating a brand - // new association rather than one that has already sent an INIT, in order to deal with - // a race condition where both SCTP end points attempt to establish the association at - // the same time using the same ports. - if (_wasAborted || _wasShutdown) - { - logger.LogWarning("SCTP association cannot initialise with a cookie after an abort or shutdown."); - } - else if (!(State == SctpAssociationState.Closed || State == SctpAssociationState.CookieEchoed)) - { - throw new ApplicationException($"SCTP association cannot initialise with a cookie in state {State}."); - } - else - { - _sctpSourcePort = cookie.SourcePort; - _sctpDestinationPort = cookie.DestinationPort; - VerificationTag = cookie.Tag; - ARwnd = cookie.ARwnd; - Destination = !string.IsNullOrEmpty(cookie.RemoteEndPoint) ? - IPSocket.Parse(cookie.RemoteEndPoint) : null; - - if (_dataReceiver == null) - { - _dataReceiver = new SctpDataReceiver(ARwnd, _defaultMTU, cookie.RemoteTSN); - } - - if (_dataSender == null) - { - _dataSender = new SctpDataSender(ID, this.SendChunk, _defaultMTU, cookie.TSN, cookie.RemoteARwnd); - } - - InitRemoteProperties(cookie.RemoteTag, cookie.RemoteTSN, cookie.RemoteARwnd); - - var cookieAckChunk = new SctpChunk(SctpChunkType.COOKIE_ACK); - SendChunk(cookieAckChunk); - - SetState(SctpAssociationState.Established); - _dataSender.StartSending(); - CancelTimers(); - } + SendInit(); } - - /// - /// Initialises the association's properties that record the state of the remote party. - /// - internal void InitRemoteProperties( - uint remoteVerificationTag, - uint remoteInitialTSN, - uint remoteARwnd) + else { - _remoteVerificationTag = remoteVerificationTag; - _remoteInitialTSN = remoteInitialTSN; - - _dataReceiver.SetInitialTSN(remoteInitialTSN); - _dataSender.SetReceiverWindow(remoteARwnd); + logger.LogSctpAssociationCannotInitialise(State); } + } - /// - /// Implements the SCTP association state machine. - /// - /// An SCTP packet received from the remote party. - /// - /// SCTP Association State Diagram: - /// https://tools.ietf.org/html/rfc4960#section-4 - /// - internal void OnPacketReceived(SctpPacket packet) + /// + /// Initialises the association state based on the echoed cookie (the cookie that we sent + /// to the remote party and was then echoed back to us). An association can only be initialised + /// from a cookie prior to it being used and prior to it ever having entered the established state. + /// + /// The echoed cookie that was returned from the remote party. + public void GotCookie(SctpTransportCookie cookie) + { + // The CookieEchoed state is allowed, even though a cookie should be creating a brand + // new association rather than one that has already sent an INIT, in order to deal with + // a race condition where both SCTP end points attempt to establish the association at + // the same time using the same ports. + if (_wasAborted || _wasShutdown) { - if (_wasAborted) - { - logger.LogWarning("SCTP packet received but association has been aborted, ignoring."); - } - else if (packet.Header.VerificationTag != VerificationTag) - { - logger.LogWarning("SCTP packet dropped due to wrong verification tag, expected {VerificationTag} got {VerificationTagHeader}.", VerificationTag, packet.Header.VerificationTag); - } - else if (!_sctpTransport.IsPortAgnostic && packet.Header.DestinationPort != _sctpSourcePort) + logger.LogSctpAssociationCannotInitialiseAfterAbortOrShutdown(); + } + else if (State is not (SctpAssociationState.Closed or SctpAssociationState.CookieEchoed)) + { + throw new SipSorceryException($"SCTP association cannot initialise with a cookie in state {State}."); + } + else + { + _sctpSourcePort = cookie.SourcePort; + _sctpDestinationPort = cookie.DestinationPort; + VerificationTag = cookie.Tag; + ARwnd = cookie.ARwnd; + Destination = !string.IsNullOrEmpty(cookie.RemoteEndPoint) ? + IPSocket.Parse(cookie.RemoteEndPoint) : null; + + if (_dataReceiver is null) { - logger.LogWarning("SCTP packet dropped due to wrong SCTP destination port, expected {SourcePort} got {DestinationPort}.", _sctpSourcePort, packet.Header.DestinationPort); + _dataReceiver = new SctpDataReceiver(ARwnd, _defaultMTU, cookie.RemoteTSN); } - else if (!_sctpTransport.IsPortAgnostic && packet.Header.SourcePort != _sctpDestinationPort) + + if (_dataSender is null) { - logger.LogWarning("SCTP packet dropped due to wrong SCTP source port, expected {DestinationPort} got {SourcePortHeader}.", _sctpDestinationPort, packet.Header.SourcePort); + _dataSender = new SctpDataSender(ID, this.SendChunk, _defaultMTU, cookie.TSN, cookie.RemoteARwnd); } - else - { - foreach (var chunk in packet.Chunks) - { - var chunkType = (SctpChunkType)chunk.ChunkType; - - switch (chunkType) - { - case SctpChunkType.ABORT: - string abortReason = (chunk as SctpAbortChunk).GetAbortReason(); - logger.LogWarning("SCTP packet ABORT chunk received from remote party, reason {AbortReason}.", abortReason); - _wasAborted = true; - _dataSender?.Close(); - OnAbortReceived?.Invoke(abortReason); - break; - - case var _ when chunkType == SctpChunkType.COOKIE_ACK && State != SctpAssociationState.CookieEchoed: - // https://tools.ietf.org/html/rfc4960#section-5.2.5 - // At any state other than COOKIE-ECHOED, an endpoint should silently - // discard a received COOKIE ACK chunk. - break; - - case var _ when chunkType == SctpChunkType.COOKIE_ACK && State == SctpAssociationState.CookieEchoed: - SetState(SctpAssociationState.Established); - CancelTimers(); - _dataSender.StartSending(); - break; - - case SctpChunkType.COOKIE_ECHO: - // In standard operation an SCTP association gets created when the parent transport - // receives a COOKIE ECHO chunk. The association gets initialised from the chunk and - // does not need to process it. - // The scenarios in https://tools.ietf.org/html/rfc4960#section-5.2 describe where - // an association could receive a COOKIE ECHO. - break; - - case SctpChunkType.DATA: - var dataChunk = chunk as SctpDataChunk; - - if (dataChunk.UserData == null || dataChunk.UserData.Length == 0) - { - // Fatal condition: - // - If an endpoint receives a DATA chunk with no user data (i.e., the - // Length field is set to 16), it MUST send an ABORT with error cause - // set to "No User Data". (RFC4960 pg. 80) - Abort(new SctpErrorNoUserData { TSN = (chunk as SctpDataChunk).TSN }); - } - else - { - logger.LogTrace("SCTP data chunk received on ID {ID} with TSN {TSN}, payload length {PayloadLength}, flags {Flags}.", ID, dataChunk.TSN, dataChunk.UserData.Length, dataChunk.ChunkFlags); - - // A received data chunk can result in multiple data frames becoming available. - // For example if a stream has out of order frames already received and the next - // in order frame arrives then all the in order ones will be supplied. - var sortedFrames = _dataReceiver.OnDataChunk(dataChunk); - var sack = _dataReceiver.GetSackChunk(); - if (sack != null) - { - SendChunk(sack); - } - - foreach (var frame in sortedFrames) - { - OnData?.Invoke(frame); - } - } + InitRemoteProperties(cookie.RemoteTag, cookie.RemoteTSN, cookie.RemoteARwnd); - break; + var cookieAckChunk = new SctpChunk(SctpChunkType.COOKIE_ACK); + SendChunk(cookieAckChunk); - case SctpChunkType.ERROR: - var errorChunk = chunk as SctpErrorChunk; - foreach (var err in errorChunk.ErrorCauses) - { - logger.LogWarning("SCTP error {CauseCode}.", err.CauseCode); - } - break; + SetState(SctpAssociationState.Established); + _dataSender.StartSending(); + CancelTimers(); + } + } - case SctpChunkType.HEARTBEAT: - // The HEARTBEAT ACK sends back the same chunk but with the type changed. - chunk.ChunkType = (byte)SctpChunkType.HEARTBEAT_ACK; - SendChunk(chunk); - break; + /// + /// Initialises the association's properties that record the state of the remote party. + /// + internal void InitRemoteProperties( + uint remoteVerificationTag, + uint remoteInitialTSN, + uint remoteARwnd) + { + _remoteVerificationTag = remoteVerificationTag; + _remoteInitialTSN = remoteInitialTSN; - case var _ when chunkType == SctpChunkType.INIT_ACK && State != SctpAssociationState.CookieWait: - // https://tools.ietf.org/html/rfc4960#section-5.2.3 - // If an INIT ACK is received by an endpoint in any state other than the - // COOKIE - WAIT state, the endpoint should discard the INIT ACK chunk. - break; + Debug.Assert(_dataReceiver is { }); + Debug.Assert(_dataSender is { }); + _dataReceiver.SetInitialTSN(remoteInitialTSN); + _dataSender.SetReceiverWindow(remoteARwnd); + } - case var _ when chunkType == SctpChunkType.INIT_ACK && State == SctpAssociationState.CookieWait: + /// + /// Implements the SCTP association state machine. + /// + /// An SCTP packet received from the remote party. + /// + /// SCTP Association State Diagram: + /// https://tools.ietf.org/html/rfc4960#section-4 + /// + internal void OnPacketReceived(SctpPacket packet) + { + if (_wasAborted) + { + logger.LogSctpPacketReceivedAborted(); + } + else if (packet.Header.VerificationTag != VerificationTag) + { + logger.LogSctpPacketDroppedWrongVerificationTag(VerificationTag, packet.Header.VerificationTag); + } + else if (!_sctpTransport.IsPortAgnostic && packet.Header.DestinationPort != _sctpSourcePort) + { + logger.LogSctpPacketDroppedWrongDestinationPort(_sctpSourcePort, packet.Header.DestinationPort); + } + else if (!_sctpTransport.IsPortAgnostic && packet.Header.SourcePort != _sctpDestinationPort) + { + logger.LogSctpPacketDroppedWrongSourcePort(_sctpDestinationPort, packet.Header.SourcePort); + } + else + { + foreach (var chunk in packet.Chunks) + { + var chunkType = (SctpChunkType)chunk.ChunkType; - if (_t1Init != null) + switch (chunkType) + { + case SctpChunkType.ABORT: + var sctpAbortChunk = chunk as SctpAbortChunk; + Debug.Assert(sctpAbortChunk is not null); + var abortReason = sctpAbortChunk.GetAbortReason(); + logger.LogSctpPacketAbortChunkReceived(abortReason); + _wasAborted = true; + _dataSender?.Close(); + OnAbortReceived?.Invoke(abortReason); + break; + + case SctpChunkType.COOKIE_ACK when State != SctpAssociationState.CookieEchoed: + // https://tools.ietf.org/html/rfc4960#section-5.2.5 + // At any state other than COOKIE-ECHOED, an endpoint should silently + // discard a received COOKIE ACK chunk. + break; + + case SctpChunkType.COOKIE_ACK when State == SctpAssociationState.CookieEchoed: + SetState(SctpAssociationState.Established); + CancelTimers(); + Debug.Assert(_dataSender is { }); + _dataSender.StartSending(); + break; + + case SctpChunkType.COOKIE_ECHO: + // In standard operation an SCTP association gets created when the parent transport + // receives a COOKIE ECHO chunk. The association gets initialised from the chunk and + // does not need to process it. + // The scenarios in https://tools.ietf.org/html/rfc4960#section-5.2 describe where + // an association could receive a COOKIE ECHO. + break; + + case SctpChunkType.DATA: + var dataChunk = chunk as SctpDataChunk; + + Debug.Assert(dataChunk is { }); + + if (dataChunk.UserData is null || dataChunk.UserData.Length == 0) + { + // Fatal condition: + // - If an endpoint receives a DATA chunk with no user data (i.e., the + // Length field is set to 16), it MUST send an ABORT with error cause + // set to "No User Data". (RFC4960 pg. 80) + Abort(new SctpErrorNoUserData { TSN = dataChunk.TSN }); + } + else + { + logger.LogSctpDataChunkReceived(ID, dataChunk.TSN, dataChunk.UserData.Length, dataChunk.ChunkFlags); + + // A received data chunk can result in multiple data frames becoming available. + // For example if a stream has out of order frames already received and the next + // in order frame arrives then all the in order ones will be supplied. + Debug.Assert(_dataReceiver is { }); + var sortedFrames = _dataReceiver.OnDataChunk(dataChunk); + + var sack = _dataReceiver.GetSackChunk(); + if (sack is { }) { - _t1Init.Dispose(); - _t1Init = null; + SendChunk(sack); } - var initAckChunk = chunk as SctpInitChunk; - - if (initAckChunk.InitiateTag == 0 || - initAckChunk.NumberInboundStreams == 0 || - initAckChunk.NumberOutboundStreams == 0) + foreach (var frame in sortedFrames) { - // Fatal conditions: - // - The Initiate Tag MUST NOT take the value 0. (RFC4960 pg 30). - // - Note: A receiver of an INIT ACK with the OS value set to 0 SHOULD - // destroy the association discarding its TCB. (RFC4960 pg 31). - // - Note: A receiver of an INIT ACK with the MIS value set to 0 SHOULD - // destroy the association discarding its TCB. (RFC4960 pg 31). - Abort(new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter)); + OnData?.Invoke(frame); } - else + } + + break; + + case SctpChunkType.ERROR: + var errorChunk = chunk as SctpErrorChunk; + Debug.Assert(errorChunk is { }); + foreach (var err in errorChunk.ErrorCauses) + { + logger.LogSctpErrorReceived(err.CauseCode); + } + break; + + case SctpChunkType.HEARTBEAT: + // The HEARTBEAT ACK sends back the same chunk but with the type changed. + chunk.ChunkType = (byte)SctpChunkType.HEARTBEAT_ACK; + SendChunk(chunk); + break; + + case SctpChunkType.INIT_ACK when State != SctpAssociationState.CookieWait: + // https://tools.ietf.org/html/rfc4960#section-5.2.3 + // If an INIT ACK is received by an endpoint in any state other than the + // COOKIE - WAIT state, the endpoint should discard the INIT ACK chunk. + break; + + case SctpChunkType.INIT_ACK when State == SctpAssociationState.CookieWait: + + if (_t1Init is { }) + { + _t1Init.Dispose(); + _t1Init = null; + } + + var initAckChunk = chunk as SctpInitChunk; + Debug.Assert(initAckChunk is { }); + + if (initAckChunk.InitiateTag == 0 || + initAckChunk.NumberInboundStreams == 0 || + initAckChunk.NumberOutboundStreams == 0) + { + // Fatal conditions: + // - The Initiate Tag MUST NOT take the value 0. (RFC4960 pg 30). + // - Note: A receiver of an INIT ACK with the OS value set to 0 SHOULD + // destroy the association discarding its TCB. (RFC4960 pg 31). + // - Note: A receiver of an INIT ACK with the MIS value set to 0 SHOULD + // destroy the association discarding its TCB. (RFC4960 pg 31). + Abort(new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter)); + } + else + { + InitRemoteProperties(initAckChunk.InitiateTag, initAckChunk.InitialTSN, initAckChunk.ARwnd); + + var cookie = initAckChunk.StateCookie; + + // The cookie chunk parameter can be changed to a COOKE ECHO CHUNK by changing the first two bytes. + // But it's more convenient to create a new chunk. + var cookieEchoChunk = new SctpChunk(SctpChunkType.COOKIE_ECHO) { ChunkValue = cookie }; + var cookieEchoPkt = GetControlPacket(cookieEchoChunk); + + if (initAckChunk.UnrecognizedPeerParameters.Count > 0) { - InitRemoteProperties(initAckChunk.InitiateTag, initAckChunk.InitialTSN, initAckChunk.ARwnd); - - var cookie = initAckChunk.StateCookie; - - // The cookie chunk parameter can be changed to a COOKE ECHO CHUNK by changing the first two bytes. - // But it's more convenient to create a new chunk. - var cookieEchoChunk = new SctpChunk(SctpChunkType.COOKIE_ECHO) { ChunkValue = cookie }; - var cookieEchoPkt = GetControlPacket(cookieEchoChunk); + var errChunk = new SctpErrorChunk(); - if (initAckChunk.UnrecognizedPeerParameters.Count > 0) + foreach (var unrecognised in initAckChunk.UnrecognizedPeerParameters) { - var errChunk = new SctpErrorChunk(); - - foreach (var unrecognised in initAckChunk.UnrecognizedPeerParameters) - { - var unrecognisedParams = new SctpErrorUnrecognizedParameters { UnrecognizedParameters = unrecognised.GetBytes() }; - errChunk.AddErrorCause(unrecognisedParams); - } - - cookieEchoPkt.AddChunk(errChunk); + var buffer = new byte[unrecognised.GetParameterLength(true)]; + _ = unrecognised.WriteBytes(buffer); + var unrecognisedParams = new SctpErrorUnrecognizedParameters { UnrecognizedParameters = buffer }; + errChunk.AddErrorCause(unrecognisedParams); } - SendPacket(cookieEchoPkt); - SetState(SctpAssociationState.CookieEchoed); - - _t1Cookie = new Timer(T1CookieTimerExpired, cookieEchoPkt, Timeout.Infinite, Timeout.Infinite); - _t1Cookie.Change(T1_COOKIE_TIMER_MILLISECONDS, T1_COOKIE_TIMER_MILLISECONDS); + cookieEchoPkt.AddChunk(errChunk); } - break; - - case var _ when chunkType == SctpChunkType.INIT_ACK && State != SctpAssociationState.CookieWait: - logger.LogWarning("SCTP association received INIT_ACK chunk in wrong state of {State}, ignoring.", State); - break; - - case SctpChunkType.SACK: - _dataSender.GotSack(chunk as SctpSackChunk); - break; - - case var _ when chunkType == SctpChunkType.SHUTDOWN && State == SctpAssociationState.Established: - // TODO: Check outstanding data chunks. - _dataSender?.Close(); - var shutdownAck = new SctpChunk(SctpChunkType.SHUTDOWN_ACK); - SendChunk(shutdownAck); - SetState(SctpAssociationState.ShutdownAckSent); - break; - - case var _ when chunkType == SctpChunkType.SHUTDOWN_ACK && State == SctpAssociationState.ShutdownSent: - SetState(SctpAssociationState.Closed); - var shutCompleteChunk = new SctpChunk(SctpChunkType.SHUTDOWN_COMPLETE, - (byte)(_remoteVerificationTag != 0 ? SHUTDOWN_CHUNK_TBIT_FLAG : 0x00)); - var shutCompletePkt = GetControlPacket(shutCompleteChunk); - shutCompletePkt.Header.VerificationTag = packet.Header.VerificationTag; - SendPacket(shutCompletePkt); - break; - - case var ct when ct == SctpChunkType.SHUTDOWN_COMPLETE && - (State == SctpAssociationState.ShutdownAckSent || State == SctpAssociationState.ShutdownSent): - _wasShutdown = true; - SetState(SctpAssociationState.Closed); - break; - - default: - logger.LogWarning("SCTP association no rule for {ChunkType} in state of {State}.", chunkType, State); - break; - } + + SendPacket(cookieEchoPkt); + SetState(SctpAssociationState.CookieEchoed); + + _t1Cookie = new Timer(T1CookieTimerExpired, cookieEchoPkt, Timeout.Infinite, Timeout.Infinite); + _t1Cookie.Change(T1_COOKIE_TIMER_MILLISECONDS, T1_COOKIE_TIMER_MILLISECONDS); + } + break; + + case SctpChunkType.INIT_ACK when State != SctpAssociationState.CookieWait: + logger.LogSctpInitAckInWrongState(State); + break; + + case SctpChunkType.SACK: + Debug.Assert(_dataSender is { }); + Debug.Assert(chunk is SctpSackChunk); + _dataSender.GotSack((chunk as SctpSackChunk)!); + break; + + case SctpChunkType.SHUTDOWN when State == SctpAssociationState.Established: + // TODO: Check outstanding data chunks. + _dataSender?.Close(); + var shutdownAck = new SctpChunk(SctpChunkType.SHUTDOWN_ACK); + SendChunk(shutdownAck); + SetState(SctpAssociationState.ShutdownAckSent); + break; + + case SctpChunkType.SHUTDOWN_ACK when State == SctpAssociationState.ShutdownSent: + SetState(SctpAssociationState.Closed); + var shutCompleteChunk = new SctpChunk(SctpChunkType.SHUTDOWN_COMPLETE, + (byte)(_remoteVerificationTag != 0 ? SHUTDOWN_CHUNK_TBIT_FLAG : 0x00)); + var shutCompletePkt = GetControlPacket(shutCompleteChunk); + shutCompletePkt.SetHeaderVerificationTag(packet.Header.VerificationTag); + SendPacket(shutCompletePkt); + break; + + case SctpChunkType.SHUTDOWN_COMPLETE when (State is SctpAssociationState.ShutdownAckSent or SctpAssociationState.ShutdownSent): + _wasShutdown = true; + SetState(SctpAssociationState.Closed); + break; + + default: + logger.LogSctpAssociationNoRuleForChunk(chunkType, State); + break; } } } + } - /// - /// Sends a DATA chunk to the remote peer. - /// - /// The stream ID to sent the data on. - /// The payload protocol ID for the data. - /// The string data to send. - public void SendData(ushort streamID, uint ppid, string message) - { - if (string.IsNullOrEmpty(message)) - { - throw new ArgumentNullException("The message cannot be empty when sending a data chunk on an SCTP association."); - } + /// + /// Sends a DATA chunk to the remote peer. + /// + /// The stream ID to sent the data on. + /// The payload protocol ID for the data. + /// The string data to send. + public void SendData(ushort streamID, uint ppid, string message) + { + ArgumentNullException.ThrowIfNullOrEmpty(message); - SendData(streamID, ppid, Encoding.UTF8.GetBytes(message)); - } + SendData(streamID, ppid, Encoding.UTF8.GetBytes(message)); + } - /// - /// Sends a DATA chunk to the remote peer. - /// - /// The stream ID to sent the data on. - /// The payload protocol ID for the data. - /// The byte data to send. - /// The offset in at which to begin sending. Defaults to 0. - /// The number of bytes to send. Defaults to -1, meaning all bytes from to the end of the array. - public void SendData(ushort streamID, uint ppid, byte[] data, int offset = 0, int count = -1) + /// + /// Sends a DATA chunk to the remote peer. + /// + /// The stream ID to sent the data on. + /// The payload protocol ID for the data. + /// The byte data to send. + /// The offset in at which to begin sending. Defaults to 0. + /// The number of bytes to send. Defaults to -1, meaning all bytes from to the end of the array. + public void SendData(ushort streamID, uint ppid, byte[] data, int offset = 0, int count = -1) + { + if (_wasAborted) { - if (_wasAborted) - { - logger.LogWarning("SCTP send data is not allowed on an aborted association."); - } - else if (!(State == SctpAssociationState.Established || - State == SctpAssociationState.ShutdownPending || - State == SctpAssociationState.ShutdownReceived)) - { - logger.LogWarning("SCTP send data is not allowed for an association in state {State}.", State); - } - else - { - _dataSender.SendData(streamID, ppid, data, offset, count); - } + logger.LogSctpSendDataNotAllowedAfterAbort(); } - - /// - /// Gets an SCTP packet for a control (non-data) chunk. - /// - /// The control chunk to get a packet for. - /// A single control chunk SCTP packet. - public SctpPacket GetControlPacket(SctpChunk chunk) + else if (State is not (SctpAssociationState.Established or + SctpAssociationState.ShutdownPending or + SctpAssociationState.ShutdownReceived)) + { + logger.LogSctpAssociationCannotSendDataInState(State); + } + else { - SctpPacket pkt = new SctpPacket( - _sctpSourcePort, - _sctpDestinationPort, - _remoteVerificationTag); + Debug.Assert(_dataSender is { }); + _dataSender.SendData(streamID, ppid, data, offset, count); + } + } - pkt.AddChunk(chunk); + /// + /// Gets an SCTP packet for a control (non-data) chunk. + /// + /// The control chunk to get a packet for. + /// A single control chunk SCTP packet. + public SctpPacket GetControlPacket(SctpChunk chunk) + { + var pkt = new SctpPacket( + _sctpSourcePort, + _sctpDestinationPort, + _remoteVerificationTag); - return pkt; - } + pkt.AddChunk(chunk); - /// - /// Initiates the shutdown of the association by sending a shutdown - /// control chunk to the remote party. - /// - public void Shutdown() + return pkt; + } + + /// + /// Initiates the shutdown of the association by sending a shutdown + /// control chunk to the remote party. + /// + public void Shutdown() + { + if (!_wasAborted) { - if (!_wasAborted) - { - SetState(SctpAssociationState.ShutdownPending); + SetState(SctpAssociationState.ShutdownPending); - // TODO: Check outstanding data chunks. + // TODO: Check outstanding data chunks. - // If no DATA chunks have been received use the initial TSN - 1 from - // the remote party. Seems weird to use the - 1, and couldn't find anything - // in the RFC that says to do it, but that's what usrsctp accepts. - uint? ackTSN = _dataReceiver.CumulativeAckTSN ?? _remoteInitialTSN - 1; + // If no DATA chunks have been received use the initial TSN - 1 from + // the remote party. Seems weird to use the - 1, and couldn't find anything + // in the RFC that says to do it, but that's what usrsctp accepts. + Debug.Assert(_dataReceiver is { }); + uint? ackTSN = _dataReceiver.CumulativeAckTSN ?? _remoteInitialTSN - 1; - logger.LogTrace("SCTP sending shutdown for association {ID}, ACK TSN {ackTSN}.", ID, ackTSN); + logger.LogSctpSendingShutdown(ID, ackTSN); - SetState(SctpAssociationState.ShutdownSent); + SetState(SctpAssociationState.ShutdownSent); - SctpShutdownChunk shutdownChunk = new SctpShutdownChunk(ackTSN); - SendChunk(shutdownChunk); + var shutdownChunk = new SctpShutdownChunk(ackTSN); + SendChunk(shutdownChunk); - _dataSender.Close(); - } + Debug.Assert(_dataSender is { }); + _dataSender.Close(); } + } - /// - /// Sends an SCTP control packet with an abort chunk to terminate - /// the association. - /// - /// The cause of the abort. - public void Abort(ISctpErrorCause errorCause) + /// + /// Sends an SCTP control packet with an abort chunk to terminate + /// the association. + /// + /// The cause of the abort. + public void Abort(ISctpErrorCause errorCause) + { + if (!_wasAborted) { - if (!_wasAborted) - { - _wasAborted = true; - bool tBit = _remoteVerificationTag != 0; - var abortChunk = new SctpAbortChunk(tBit); - abortChunk.AddErrorCause(errorCause); + _wasAborted = true; + var tBit = _remoteVerificationTag != 0; + var abortChunk = new SctpAbortChunk(tBit); + abortChunk.AddErrorCause(errorCause); - SendChunk(abortChunk); + SendChunk(abortChunk); - OnAborted?.Invoke(errorCause.CauseCode.ToString()); + OnAborted?.Invoke(errorCause.CauseCode.ToStringFast()); - _dataSender.Close(); - } + Debug.Assert(_dataSender is { }); + _dataSender.Close(); } + } - /// - /// Updates the state of the association. - /// - /// The new association state. - internal void SetState(SctpAssociationState state) - { - logger.LogTrace("SCTP state for association {ID} changed to {State}.", ID, state); - State = state; - OnAssociationStateChanged?.Invoke(state); - } + /// + /// Updates the state of the association. + /// + /// The new association state. + internal void SetState(SctpAssociationState state) + { + logger.LogSctpStateChanged(ID, state); + State = state; + OnAssociationStateChanged?.Invoke(state); + } - /// - /// Attempts to create an association with a remote party by sending an initialisation - /// control chunk. - /// - private void SendInit() + /// + /// Attempts to create an association with a remote party by sending an initialisation + /// control chunk. + /// + private void SendInit() + { + if (!_wasAborted) { - if (!_wasAborted) - { - // A packet containing an INIT chunk MUST have a zero Verification Tag (RFC4960 Pg 15). - SctpPacket init = new SctpPacket(_sctpSourcePort, _sctpDestinationPort, 0); + // A packet containing an INIT chunk MUST have a zero Verification Tag (RFC4960 Pg 15). + var init = new SctpPacket(_sctpSourcePort, _sctpDestinationPort, 0); - SctpInitChunk initChunk = new SctpInitChunk( - SctpChunkType.INIT, - VerificationTag, - TSN, - ARwnd, - _numberOutboundStreams, - _numberInboundStreams); - init.AddChunk(initChunk); + var initChunk = new SctpInitChunk( + SctpChunkType.INIT, + VerificationTag, + TSN, + ARwnd, + _numberOutboundStreams, + _numberInboundStreams); + init.AddChunk(initChunk); - SetState(SctpAssociationState.CookieWait); + SetState(SctpAssociationState.CookieWait); - byte[] buffer = init.GetBytes(); - _sctpTransport.Send(ID, buffer, 0, buffer.Length); + var size = init.GetByteCount(); + var memoryOwner = MemoryPool.Shared.Rent(size); + _ = init.WriteBytes(memoryOwner.Memory.Span); - _t1Init = new Timer(T1InitTimerExpired, init, Timeout.Infinite, Timeout.Infinite); - _t1Init.Change(T1_INIT_TIMER_MILLISECONDS, T1_INIT_TIMER_MILLISECONDS); - } + _sctpTransport.Send(ID, memoryOwner.Memory.Slice(0, size), memoryOwner); + + _t1Init = new Timer(T1InitTimerExpired, init, Timeout.Infinite, Timeout.Infinite); + _t1Init.Change(T1_INIT_TIMER_MILLISECONDS, T1_INIT_TIMER_MILLISECONDS); } + } - /// - /// Sends a SCTP chunk to the remote party. - /// - /// The chunk to send. - internal void SendChunk(SctpChunk chunk) + /// + /// Sends a SCTP chunk to the remote party. + /// + /// The chunk to send. + internal void SendChunk(SctpChunk chunk) + { + if (!_wasAborted) { - if (!_wasAborted) - { - SctpPacket pkt = new SctpPacket( - _sctpSourcePort, - _sctpDestinationPort, - _remoteVerificationTag); + var pkt = new SctpPacket( + _sctpSourcePort, + _sctpDestinationPort, + _remoteVerificationTag); - pkt.AddChunk(chunk); + pkt.AddChunk(chunk); - byte[] buffer = pkt.GetBytes(); + var size = pkt.GetByteCount(); + var memoryOwner = MemoryPool.Shared.Rent(size); + _ = pkt.WriteBytes(memoryOwner.Memory.Span); - _sctpTransport.Send(ID, buffer, 0, buffer.Length); - } + _sctpTransport.Send(ID, memoryOwner.Memory.Slice(0, size), memoryOwner); } + } - /// - /// Sends an SCTP packet to the remote peer. - /// - /// The packet to send. - private void SendPacket(SctpPacket pkt) + /// + /// Sends an SCTP packet to the remote peer. + /// + /// The packet to send. + private void SendPacket(SctpPacket pkt) + { + if (!_wasAborted) { - if (!_wasAborted) - { - byte[] buffer = pkt.GetBytes(); - _sctpTransport.Send(ID, buffer, 0, buffer.Length); - } + var size = pkt.GetByteCount(); + var memoryOwner = MemoryPool.Shared.Rent(size); + _ = pkt.WriteBytes(memoryOwner.Memory.Span); + + _sctpTransport.Send(ID, memoryOwner.Memory.Slice(0, size), memoryOwner); } + } - private void CancelTimers() + private void CancelTimers() + { + if (_t1Init is { }) { - if (_t1Init != null) - { - _t1Init.Dispose(); - _t1Init = null; - } + _t1Init.Dispose(); + _t1Init = null; + } - if (_t1Cookie != null) - { - _t1Cookie.Dispose(); - _t1Cookie = null; - } + if (_t1Cookie is { }) + { + _t1Cookie.Dispose(); + _t1Cookie = null; } + } - private void T1InitTimerExpired(object state) + private void T1InitTimerExpired(object? state) + { + if (_initRetransmits >= MAX_INIT_RETRANSMITS) { - if (_initRetransmits >= MAX_INIT_RETRANSMITS) - { - _t1Init.Dispose(); - _t1Init = null; - _initialisationFailed = true; + Debug.Assert(_t1Init is { }); + _t1Init.Dispose(); + _t1Init = null; + _initialisationFailed = true; - logger.LogWarning("SCTP timed out waiting for INIT ACK chunk from remote peer."); + logger.LogSctpAssociationTimedOutInitAck(); - SetState(SctpAssociationState.Closed); - } - else - { - var init = state as SctpPacket; - SendPacket(init); - _initRetransmits++; - } + SetState(SctpAssociationState.Closed); + } + else + { + var init = state as SctpPacket; + Debug.Assert(init is { }); + SendPacket(init); + _initRetransmits++; } + } - private void T1CookieTimerExpired(object state) + private void T1CookieTimerExpired(object? state) + { + if (_cookieEchoRetransmits >= MAX_COOKIE_ECHO_RETRANSMITS) { - if (_cookieEchoRetransmits >= MAX_COOKIE_ECHO_RETRANSMITS) - { - _t1Cookie.Dispose(); - _t1Cookie = null; - _initialisationFailed = true; + Debug.Assert(_t1Cookie is { }); + _t1Cookie.Dispose(); + _t1Cookie = null; + _initialisationFailed = true; - logger.LogWarning("SCTP timed out waiting for COOKIE ACK chunk from remote peer."); + logger.LogSctpAssociationTimedOutCookieAck(); - SetState(SctpAssociationState.Closed); - } - else - { - var cookieEchoPkt = state as SctpPacket; - SendPacket(cookieEchoPkt); - _cookieEchoRetransmits++; - } + SetState(SctpAssociationState.Closed); + } + else + { + var cookieEchoPkt = state as SctpPacket; + Debug.Assert(cookieEchoPkt is { }); + SendPacket(cookieEchoPkt); + _cookieEchoRetransmits++; } } } diff --git a/src/SIPSorcery/net/SCTP/SctpDataReceiver.cs b/src/SIPSorcery/net/SCTP/SctpDataReceiver.cs index dd2c805a94..7c16aade73 100644 --- a/src/SIPSorcery/net/SCTP/SctpDataReceiver.cs +++ b/src/SIPSorcery/net/SCTP/SctpDataReceiver.cs @@ -15,563 +15,562 @@ //----------------------------------------------------------------------------- using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Diagnostics; using Microsoft.Extensions.Logging; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public struct SctpDataFrame { - public struct SctpDataFrame + public static SctpDataFrame Empty; + + public bool Unordered; + public ushort StreamID; + public ushort StreamSeqNum; + public uint PPID; + public byte[]? UserData; + + /// The stream ID of the chunk. + /// The stream sequence number of the chunk. Will be 0 for unordered streams. + /// The payload protocol ID for the chunk. + /// The chunk data. + public SctpDataFrame(bool unordered, ushort streamID, ushort streamSeqNum, uint ppid, byte[]? userData) { - public static SctpDataFrame Empty = new SctpDataFrame(); - - public bool Unordered; - public ushort StreamID; - public ushort StreamSeqNum; - public uint PPID; - public byte[] UserData; - - /// The stream ID of the chunk. - /// The stream sequence number of the chunk. Will be 0 for unordered streams. - /// The payload protocol ID for the chunk. - /// The chunk data. - public SctpDataFrame(bool unordered, ushort streamID, ushort streamSeqNum, uint ppid, byte[] userData) - { - Unordered = unordered; - StreamID = streamID; - StreamSeqNum = streamSeqNum; - PPID = ppid; - UserData = userData; - } + Unordered = unordered; + StreamID = streamID; + StreamSeqNum = streamSeqNum; + PPID = ppid; + UserData = userData; + } - public bool IsEmpty() - { - return UserData == null; - } + public bool IsEmpty() + { + return UserData is null; } +} + +public struct SctpTsnGapBlock +{ + /// + /// Indicates the Start offset TSN for this Gap Ack Block. To + /// calculate the actual TSN number the Cumulative TSN Ack is added to + /// this offset number.This calculated TSN identifies the first TSN + /// in this Gap Ack Block that has been received. + /// + public ushort Start; + + /// + /// Indicates the End offset TSN for this Gap Ack Block. To calculate + /// the actual TSN number, the Cumulative TSN Ack is added to this + /// offset number.This calculated TSN identifies the TSN of the last + /// DATA chunk received in this Gap Ack Block. + /// + public ushort End; +} + +/// +/// Processes incoming data chunks and handles fragmentation and congestion control. This +/// class does NOT handle in order delivery. Different streams on the same association +/// can have different ordering requirements so it's left up to each stream handler to +/// deal with full frames as they see fit. +/// +public class SctpDataReceiver +{ + /// + /// The window size is the maximum number of entries that can be recorded in the + /// receive dictionary. + /// + private const ushort WINDOW_SIZE_MINIMUM = 100; + + /// + /// The maximum number of out of order frames that will be queued per stream ID. + /// + private const int MAXIMUM_OUTOFORDER_FRAMES = 25; + + /// + /// The maximum size of an SCTP fragmented message. + /// + private const int MAX_FRAME_SIZE = 262144; + + private static ILogger logger = LogFactory.CreateLogger(); + + /// + /// This dictionary holds data chunk Transaction Sequence Numbers (TSN) that have + /// been received out of order and are in advance of the expected TSN. + /// + private SortedDictionary _forwardTSN = new SortedDictionary(); + + /// + /// Storage for fragmented chunks. + /// + private Dictionary _fragmentedChunks = new Dictionary(); + + /// + /// Keeps track of the latest sequence number for each stream. Used to ensure + /// stream chunks are delivered in order. + /// + private Dictionary _streamLatestSeqNums = new Dictionary(); + + /// + /// A dictionary of dictionaries used to hold out of order stream chunks. + /// + private Dictionary> _streamOutOfOrderFrames = + new Dictionary>(); + + /// + /// The maximum amount of received data that will be stored at any one time. + /// This is part of the SCTP congestion window mechanism. It limits the number + /// of bytes, a sender can send to a particular destination transport address + /// before receiving an acknowledgement. + /// + private uint _receiveWindow; + + /// + /// The most recent in order TSN received. This is the value that gets used + /// in the "Cumulative TSN Ack" field to SACK chunks. + /// + private uint _lastInOrderTSN; + + /// + /// The window size is the maximum number of chunks we're prepared to hold in the + /// receive dictionary. + /// + private ushort _windowSize; + + /// + /// Record of the duplicate Transaction Sequence Number (TSN) chunks received since + /// the last SACK chunk was generated. + /// + private Dictionary _duplicateTSN = new Dictionary(); + + /// + /// Gets the Transaction Sequence Number (TSN) that can be acknowledged to the remote peer. + /// It represents the most recent in order TSN that has been received. If no in order + /// TSN's have been received then null will be returned. + /// + public uint? CumulativeAckTSN => (_inOrderReceiveCount > 0) ? _lastInOrderTSN : (uint?)null; + + /// + /// A count of the total entries in the receive dictionary. Note that if chunks + /// have been received out of order this count could include chunks that have + /// already been processed. They are kept in the dictionary as empty chunks to + /// track which TSN's have been received. + /// + public int ForwardTSNCount => _forwardTSN.Count; + + private uint _initialTSN; + private uint _inOrderReceiveCount; - public struct SctpTsnGapBlock + /// + /// Creates a new SCTP data receiver instance. + /// + /// The size of the receive window. This is the window around the + /// expected Transaction Sequence Number (TSN). If a data chunk is received with a TSN outside + /// the window it is ignored. + /// The Maximum Transmission Unit for the network layer that the SCTP + /// association is being used with. + /// The initial TSN for the association from the INIT handshake. + public SctpDataReceiver(uint receiveWindow, uint mtu, uint initialTSN) { - /// - /// Indicates the Start offset TSN for this Gap Ack Block. To - /// calculate the actual TSN number the Cumulative TSN Ack is added to - /// this offset number.This calculated TSN identifies the first TSN - /// in this Gap Ack Block that has been received. - /// - public ushort Start; - - /// - /// Indicates the End offset TSN for this Gap Ack Block. To calculate - /// the actual TSN number, the Cumulative TSN Ack is added to this - /// offset number.This calculated TSN identifies the TSN of the last - /// DATA chunk received in this Gap Ack Block. - /// - public ushort End; + _receiveWindow = receiveWindow != 0 ? receiveWindow : SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW; + _initialTSN = initialTSN; + + mtu = mtu != 0 ? mtu : SctpUdpTransport.DEFAULT_UDP_MTU; + _windowSize = (ushort)(_receiveWindow / mtu); + _windowSize = (_windowSize < WINDOW_SIZE_MINIMUM) ? WINDOW_SIZE_MINIMUM : _windowSize; + + logger.LogSctpWindowSizeSet(_windowSize); } /// - /// Processes incoming data chunks and handles fragmentation and congestion control. This - /// class does NOT handle in order delivery. Different streams on the same association - /// can have different ordering requirements so it's left up to each stream handler to - /// deal with full frames as they see fit. + /// Used to set the initial TSN for the remote party when it's not known at creation time. /// - public class SctpDataReceiver + /// The initial Transaction Sequence Number (TSN) for the + /// remote party. + public void SetInitialTSN(uint tsn) { - /// - /// The window size is the maximum number of entries that can be recorded in the - /// receive dictionary. - /// - private const ushort WINDOW_SIZE_MINIMUM = 100; - - /// - /// The maximum number of out of order frames that will be queued per stream ID. - /// - private const int MAXIMUM_OUTOFORDER_FRAMES = 25; - - /// - /// The maximum size of an SCTP fragmented message. - /// - private const int MAX_FRAME_SIZE = 262144; - - private static ILogger logger = LogFactory.CreateLogger(); - - /// - /// This dictionary holds data chunk Transaction Sequence Numbers (TSN) that have - /// been received out of order and are in advance of the expected TSN. - /// - private SortedDictionary _forwardTSN = new SortedDictionary(); - - /// - /// Storage for fragmented chunks. - /// - private Dictionary _fragmentedChunks = new Dictionary(); - - /// - /// Keeps track of the latest sequence number for each stream. Used to ensure - /// stream chunks are delivered in order. - /// - private Dictionary _streamLatestSeqNums = new Dictionary(); - - /// - /// A dictionary of dictionaries used to hold out of order stream chunks. - /// - private Dictionary> _streamOutOfOrderFrames = - new Dictionary>(); - - /// - /// The maximum amount of received data that will be stored at any one time. - /// This is part of the SCTP congestion window mechanism. It limits the number - /// of bytes, a sender can send to a particular destination transport address - /// before receiving an acknowledgement. - /// - private uint _receiveWindow; - - /// - /// The most recent in order TSN received. This is the value that gets used - /// in the "Cumulative TSN Ack" field to SACK chunks. - /// - private uint _lastInOrderTSN; - - /// - /// The window size is the maximum number of chunks we're prepared to hold in the - /// receive dictionary. - /// - private ushort _windowSize; - - /// - /// Record of the duplicate Transaction Sequence Number (TSN) chunks received since - /// the last SACK chunk was generated. - /// - private Dictionary _duplicateTSN = new Dictionary(); - - /// - /// Gets the Transaction Sequence Number (TSN) that can be acknowledged to the remote peer. - /// It represents the most recent in order TSN that has been received. If no in order - /// TSN's have been received then null will be returned. - /// - public uint? CumulativeAckTSN => (_inOrderReceiveCount > 0) ? _lastInOrderTSN : (uint?)null; - - /// - /// A count of the total entries in the receive dictionary. Note that if chunks - /// have been received out of order this count could include chunks that have - /// already been processed. They are kept in the dictionary as empty chunks to - /// track which TSN's have been received. - /// - public int ForwardTSNCount => _forwardTSN.Count; - - private uint _initialTSN; - private uint _inOrderReceiveCount; - - /// - /// Creates a new SCTP data receiver instance. - /// - /// The size of the receive window. This is the window around the - /// expected Transaction Sequence Number (TSN). If a data chunk is received with a TSN outside - /// the window it is ignored. - /// The Maximum Transmission Unit for the network layer that the SCTP - /// association is being used with. - /// The initial TSN for the association from the INIT handshake. - public SctpDataReceiver(uint receiveWindow, uint mtu, uint initialTSN) - { - _receiveWindow = receiveWindow != 0 ? receiveWindow : SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW; - _initialTSN = initialTSN; + _initialTSN = tsn; + } - mtu = mtu != 0 ? mtu : SctpUdpTransport.DEFAULT_UDP_MTU; - _windowSize = (ushort)(_receiveWindow / mtu); - _windowSize = (_windowSize < WINDOW_SIZE_MINIMUM) ? WINDOW_SIZE_MINIMUM : _windowSize; + /// + /// Handler for processing new data chunks. + /// + /// The newly received data chunk. + /// If the received chunk resulted in a full chunk becoming available one + /// or more new frames will be returned otherwise an empty frame is returned. Multiple + /// frames may be returned if this chunk is part of a stream and was received out + /// or order. For unordered chunks the list will always have a single entry. + public List OnDataChunk(SctpDataChunk dataChunk) + { + var sortedFrames = new List(); + var frame = SctpDataFrame.Empty; - logger.LogDebug("SCTP windows size for data receiver set at {WindowSize}.", _windowSize); + if (_inOrderReceiveCount == 0 && + GetDistance(_initialTSN, dataChunk.TSN) > _windowSize) + { + logger.LogSctpDataReceiverDistantInitialTsn(dataChunk.TSN, _initialTSN, _windowSize); } - - /// - /// Used to set the initial TSN for the remote party when it's not known at creation time. - /// - /// The initial Transaction Sequence Number (TSN) for the - /// remote party. - public void SetInitialTSN(uint tsn) + else if (_inOrderReceiveCount > 0 && + GetDistance(_lastInOrderTSN, dataChunk.TSN) > _windowSize) { - _initialTSN = tsn; + logger.LogSctpDataReceiverDistantLastInOrderTsn(dataChunk.TSN, _lastInOrderTSN + 1, _windowSize); } - - /// - /// Handler for processing new data chunks. - /// - /// The newly received data chunk. - /// If the received chunk resulted in a full chunk becoming available one - /// or more new frames will be returned otherwise an empty frame is returned. Multiple - /// frames may be returned if this chunk is part of a stream and was received out - /// or order. For unordered chunks the list will always have a single entry. - public List OnDataChunk(SctpDataChunk dataChunk) + else if (_inOrderReceiveCount > 0 && + !IsNewer(_lastInOrderTSN, dataChunk.TSN)) { - var sortedFrames = new List(); - var frame = SctpDataFrame.Empty; - - if (_inOrderReceiveCount == 0 && - GetDistance(_initialTSN, dataChunk.TSN) > _windowSize) - { - logger.LogWarning("SCTP data receiver received a data chunk with a TSN {TSN} when the initial TSN was {InitialTSN} and a window size of {WindowSize}, ignoring.", dataChunk.TSN, _initialTSN, _windowSize); - } - else if (_inOrderReceiveCount > 0 && - GetDistance(_lastInOrderTSN, dataChunk.TSN) > _windowSize) - { - logger.LogWarning("SCTP data receiver received a data chunk with a TSN {TSN} when the expected TSN was {ExpectedTSN} and a window size of {WindowSize}, ignoring.", dataChunk.TSN, _lastInOrderTSN + 1, _windowSize); - } - else if (_inOrderReceiveCount > 0 && - !IsNewer(_lastInOrderTSN, dataChunk.TSN)) - { - logger.LogWarning("SCTP data receiver received an old data chunk with TSN {TSN} when the expected TSN was {ExpectedTSN}, ignoring.", dataChunk.TSN, _lastInOrderTSN + 1); - } - else if (!_forwardTSN.ContainsKey(dataChunk.TSN)) - { - logger.LogTrace("SCTP receiver got data chunk with TSN {TSN}, last in order TSN {LastInOrderTSN}, in order receive count {InOrderReceiveCount}.", dataChunk.TSN, _lastInOrderTSN, _inOrderReceiveCount); + logger.LogSctpDataReceiverOldChunkTsn(dataChunk.TSN, _lastInOrderTSN + 1); + } + else if (!_forwardTSN.ContainsKey(dataChunk.TSN)) + { + logger.LogReceivedChunk(dataChunk.TSN, _lastInOrderTSN, _inOrderReceiveCount); - bool processFrame = true; + var processFrame = true; - // Relying on unsigned integer wrapping. - unchecked + // Relying on unsigned integer wrapping. + unchecked + { + if ((_inOrderReceiveCount > 0 && _lastInOrderTSN + 1 == dataChunk.TSN) || + (_inOrderReceiveCount == 0 && dataChunk.TSN == _initialTSN)) { - if ((_inOrderReceiveCount > 0 && _lastInOrderTSN + 1 == dataChunk.TSN) || - (_inOrderReceiveCount == 0 && dataChunk.TSN == _initialTSN)) - { - _inOrderReceiveCount++; - _lastInOrderTSN = dataChunk.TSN; + _inOrderReceiveCount++; + _lastInOrderTSN = dataChunk.TSN; - // See if the in order TSN can be bumped using any out of order chunks - // already received. - if (_inOrderReceiveCount > 0 && _forwardTSN.Count > 0) - { - while (_forwardTSN.ContainsKey(_lastInOrderTSN + 1)) - { - _lastInOrderTSN++; - _inOrderReceiveCount++; - _forwardTSN.Remove(_lastInOrderTSN); - } - } - } - else + // See if the in order TSN can be bumped using any out of order chunks + // already received. + if (_inOrderReceiveCount > 0 && _forwardTSN.Count > 0) { - if (!dataChunk.Unordered && - _streamOutOfOrderFrames.TryGetValue(dataChunk.StreamID, out var outOfOrder) && - outOfOrder.Count >= MAXIMUM_OUTOFORDER_FRAMES) + while (_forwardTSN.ContainsKey(_lastInOrderTSN + 1)) { - // Stream is nearing capacity, only chunks that advance _lastInOrderTSN can be accepted. - logger.LogWarning("Stream {StreamID} is at buffer capacity. Rejected out-of-order data chunk TSN {TSN}.", dataChunk.StreamID, dataChunk.TSN); - processFrame = false; - } - else - { - _forwardTSN.Add(dataChunk.TSN, 1); + _lastInOrderTSN++; + _inOrderReceiveCount++; + _forwardTSN.Remove(_lastInOrderTSN); } } } - - if (processFrame) + else { - // Now go about processing the data chunk. - if (dataChunk.Begining && dataChunk.Ending) + if (!dataChunk.Unordered && + _streamOutOfOrderFrames.TryGetValue(dataChunk.StreamID, out var outOfOrder) && + outOfOrder.Count >= MAXIMUM_OUTOFORDER_FRAMES) { - // Single packet chunk. - frame = new SctpDataFrame( - dataChunk.Unordered, - dataChunk.StreamID, - dataChunk.StreamSeqNum, - dataChunk.PPID, - dataChunk.UserData); + // Stream is nearing capacity, only chunks that advance _lastInOrderTSN can be accepted. + logger.LogSctpStreamBufferAtCapacity(dataChunk.StreamID, dataChunk.TSN); + processFrame = false; } else { - // This is a data chunk fragment. - _fragmentedChunks.Add(dataChunk.TSN, dataChunk); - (var begin, var end) = GetChunkBeginAndEnd(_fragmentedChunks, dataChunk.TSN); - - if (begin != null && end != null) - { - frame = GetFragmentedChunk(_fragmentedChunks, begin.Value, end.Value); - } + _forwardTSN.Add(dataChunk.TSN, 1); } } } - else + + if (processFrame) { - logger.LogTrace("SCTP duplicate TSN received for {TSN}.", dataChunk.TSN); - if (!_duplicateTSN.ContainsKey(dataChunk.TSN)) + // Now go about processing the data chunk. + if (dataChunk.Begining && dataChunk.Ending) { - _duplicateTSN.Add(dataChunk.TSN, 1); + // Single packet chunk. + Debug.Assert(dataChunk is { }); + frame = new SctpDataFrame( + dataChunk.Unordered, + dataChunk.StreamID, + dataChunk.StreamSeqNum, + dataChunk.PPID, + dataChunk.UserData); } else { - _duplicateTSN[dataChunk.TSN] = _duplicateTSN[dataChunk.TSN] + 1; + // This is a data chunk fragment. + _fragmentedChunks.Add(dataChunk.TSN, dataChunk); + (var begin, var end) = GetChunkBeginAndEnd(_fragmentedChunks, dataChunk.TSN); + + if (begin is { } && end is { }) + { + frame = GetFragmentedChunk(_fragmentedChunks, begin.Value, end.Value); + } } } - - if (!frame.IsEmpty() && !dataChunk.Unordered) + } + else + { + logger.LogSctpDuplicateTsnReceived(dataChunk.TSN); + if (!_duplicateTSN.TryGetValue(dataChunk.TSN, out var duplicateTsnValue)) { - return ProcessStreamFrame(frame); + _duplicateTSN.Add(dataChunk.TSN, 1); } else { - if (!frame.IsEmpty()) - { - sortedFrames.Add(frame); - } - - return sortedFrames; + _duplicateTSN[dataChunk.TSN] = duplicateTsnValue + 1; } } - /// - /// Gets a SACK chunk that represents the current state of the receiver. - /// - /// A SACK chunk that can be sent to the remote peer to update the ACK TSN and - /// request a retransmit of any missing DATA chunks. - public SctpSackChunk GetSackChunk() + if (!frame.IsEmpty() && !dataChunk.Unordered) { - // Can't create a SACK until the initial DATA chunk has been received. - if (_inOrderReceiveCount > 0) - { - SctpSackChunk sack = new SctpSackChunk(_lastInOrderTSN, _receiveWindow); - sack.GapAckBlocks = GetForwardTSNGaps(); - sack.DuplicateTSN = _duplicateTSN.Keys.ToList(); - return sack; - } - else + return ProcessStreamFrame(frame); + } + else + { + if (!frame.IsEmpty()) { - return null; + sortedFrames.Add(frame); } + + return sortedFrames; } + } + + /// + /// Gets a SACK chunk that represents the current state of the receiver. + /// + /// A SACK chunk that can be sent to the remote peer to update the ACK TSN and + /// request a retransmit of any missing DATA chunks. + public SctpSackChunk? GetSackChunk() + { + // Can't create a SACK until the initial DATA chunk has been received. + if (_inOrderReceiveCount > 0) + { + var sack = new SctpSackChunk(_lastInOrderTSN, _receiveWindow); + sack.GapAckBlocks = GetForwardTSNGaps(); + sack.DuplicateTSN = new List(_duplicateTSN.Keys); + return sack; + } + else + { + return null; + } + } + + /// + /// Gets a list of the gaps in the forward TSN records. Typically the TSN gap + /// reports are used in SACK chunks to inform the remote peer which DATA chunk + /// TSNs have not yet been received. + /// + /// A list of TSN gap blocks. An empty list means there are no gaps. + internal List GetForwardTSNGaps() + { + var gaps = new List(); - /// - /// Gets a list of the gaps in the forward TSN records. Typically the TSN gap - /// reports are used in SACK chunks to inform the remote peer which DATA chunk - /// TSNs have not yet been received. - /// - /// A list of TSN gap blocks. An empty list means there are no gaps. - internal List GetForwardTSNGaps() + // Can't create gap reports until the initial DATA chunk has been received. + if (_inOrderReceiveCount > 0) { - List gaps = new List(); + var tsnAck = _lastInOrderTSN; - // Can't create gap reports until the initial DATA chunk has been received. - if (_inOrderReceiveCount > 0) + if (_forwardTSN.Count > 0) { - uint tsnAck = _lastInOrderTSN; + ushort? start = null; + uint prev = 0; - if (_forwardTSN.Count > 0) + foreach (var tsn in _forwardTSN.Keys) { - ushort? start = null; - uint prev = 0; - - foreach (var tsn in _forwardTSN.Keys) + if (start is null) { - if (start == null) - { - start = (ushort)(tsn - tsnAck); - prev = tsn; - } - else if (tsn != prev + 1) - { - ushort end = (ushort)(prev - tsnAck); - gaps.Add(new SctpTsnGapBlock { Start = start.Value, End = end }); - start = (ushort)(tsn - tsnAck); - prev = tsn; - } - else - { - prev++; - } + start = (ushort)(tsn - tsnAck); + prev = tsn; + } + else if (tsn != prev + 1) + { + var end = (ushort)(prev - tsnAck); + Debug.Assert(start.HasValue); + gaps.Add(new SctpTsnGapBlock { Start = start.GetValueOrDefault(), End = end }); + start = (ushort)(tsn - tsnAck); + prev = tsn; + } + else + { + prev++; } - - gaps.Add(new SctpTsnGapBlock { Start = start.Value, End = (ushort)(prev - tsnAck) }); } - } - return gaps; + Debug.Assert(start.HasValue); + gaps.Add(new SctpTsnGapBlock { Start = start.GetValueOrDefault(), End = (ushort)(prev - tsnAck) }); + } } - /// - /// Processes a data frame that is now ready and that is part of an SCTP stream. - /// Stream frames must be delivered in order. - /// - /// The data frame that became ready from the latest DATA chunk receive. - /// A sorted list of frames for the matching stream ID. Will be empty - /// if the supplied frame is out of order for its stream. - private List ProcessStreamFrame(SctpDataFrame frame) + return gaps; + } + + /// + /// Processes a data frame that is now ready and that is part of an SCTP stream. + /// Stream frames must be delivered in order. + /// + /// The data frame that became ready from the latest DATA chunk receive. + /// A sorted list of frames for the matching stream ID. Will be empty + /// if the supplied frame is out of order for its stream. + private List ProcessStreamFrame(SctpDataFrame frame) + { + // Relying on unsigned short wrapping. + unchecked { - // Relying on unsigned short wrapping. - unchecked + // This is a stream chunk. Need to ensure in order delivery. + var sortedFrames = new List(); + + if (!_streamLatestSeqNums.TryGetValue(frame.StreamID, out var value)) + { + // First frame for this stream. + _streamLatestSeqNums.Add(frame.StreamID, frame.StreamSeqNum); + sortedFrames.Add(frame); + } + else if ((ushort)(value + 1) == frame.StreamSeqNum) { - // This is a stream chunk. Need to ensure in order delivery. - var sortedFrames = new List(); + // Expected seqnum for stream. + _streamLatestSeqNums[frame.StreamID] = frame.StreamSeqNum; + sortedFrames.Add(frame); - if (!_streamLatestSeqNums.ContainsKey(frame.StreamID)) + // There could also be out of order frames that can now be delivered. + if (_streamOutOfOrderFrames.TryGetValue(frame.StreamID, out var outOfOrder) && outOfOrder.Count > 0) { - // First frame for this stream. - _streamLatestSeqNums.Add(frame.StreamID, frame.StreamSeqNum); - sortedFrames.Add(frame); - } - else if ((ushort)(_streamLatestSeqNums[frame.StreamID] + 1) == frame.StreamSeqNum) - { - // Expected seqnum for stream. - _streamLatestSeqNums[frame.StreamID] = frame.StreamSeqNum; - sortedFrames.Add(frame); - - // There could also be out of order frames that can now be delivered. - if (_streamOutOfOrderFrames.ContainsKey(frame.StreamID) && - _streamOutOfOrderFrames[frame.StreamID].Count > 0) + var nextSeqnum = (ushort)(_streamLatestSeqNums[frame.StreamID] + 1); + while (outOfOrder.ContainsKey(nextSeqnum) && + outOfOrder.TryGetValue(nextSeqnum, out var nextFrame)) { - var outOfOrder = _streamOutOfOrderFrames[frame.StreamID]; - - ushort nextSeqnum = (ushort)(_streamLatestSeqNums[frame.StreamID] + 1); - while (outOfOrder.ContainsKey(nextSeqnum) && - outOfOrder.TryGetValue(nextSeqnum, out var nextFrame)) - { - sortedFrames.Add(nextFrame); - _streamLatestSeqNums[frame.StreamID] = nextSeqnum; - outOfOrder.Remove(nextSeqnum); - nextSeqnum++; - } + sortedFrames.Add(nextFrame); + _streamLatestSeqNums[frame.StreamID] = nextSeqnum; + outOfOrder.Remove(nextSeqnum); + nextSeqnum++; } } - else + } + else + { + // Stream seqnum is out of order. + if (!_streamOutOfOrderFrames.TryGetValue(frame.StreamID, out var outOfOrder)) { - // Stream seqnum is out of order. - if (!_streamOutOfOrderFrames.ContainsKey(frame.StreamID)) - { - _streamOutOfOrderFrames[frame.StreamID] = new Dictionary(); - } - - _streamOutOfOrderFrames[frame.StreamID].Add(frame.StreamSeqNum, frame); + outOfOrder = new Dictionary(); + _streamOutOfOrderFrames[frame.StreamID] = outOfOrder; } - return sortedFrames; + outOfOrder.Add(frame.StreamSeqNum, frame); } + + return sortedFrames; } + } - /// - /// Checks whether the fragmented chunk for the supplied TSN is complete and if so - /// returns its begin and end TSNs. - /// - /// The TSN of the fragmented chunk to check for completeness. - /// The dictionary containing the chunk fragments. - /// If the chunk is complete the begin and end TSNs will be returned. If - /// the fragmented chunk is incomplete one or both of the begin and/or end TSNs will be null. - private (uint?, uint?) GetChunkBeginAndEnd(Dictionary fragments, uint tsn) + /// + /// Checks whether the fragmented chunk for the supplied TSN is complete and if so + /// returns its begin and end TSNs. + /// + /// The TSN of the fragmented chunk to check for completeness. + /// The dictionary containing the chunk fragments. + /// If the chunk is complete the begin and end TSNs will be returned. If + /// the fragmented chunk is incomplete one or both of the begin and/or end TSNs will be null. + private (uint?, uint?) GetChunkBeginAndEnd(Dictionary fragments, uint tsn) + { + unchecked { - unchecked + var beginTSN = fragments[tsn].Begining ? (uint?)tsn : null; + var endTSN = fragments[tsn].Ending ? (uint?)tsn : null; + + var revTSN = tsn - 1; + while (beginTSN is null && fragments.ContainsKey(revTSN)) { - uint? beginTSN = fragments[tsn].Begining ? (uint?)tsn : null; - uint? endTSN = fragments[tsn].Ending ? (uint?)tsn : null; + if (fragments[revTSN].Begining) + { + beginTSN = revTSN; + } + else + { + revTSN--; + } + } - uint revTSN = tsn - 1; - while (beginTSN == null && fragments.ContainsKey(revTSN)) + if (beginTSN is { }) + { + var fwdTSN = tsn + 1; + while (endTSN is null && fragments.ContainsKey(fwdTSN)) { - if (fragments[revTSN].Begining) + if (fragments[fwdTSN].Ending) { - beginTSN = revTSN; + endTSN = fwdTSN; } else { - revTSN--; - } - } - - if (beginTSN != null) - { - uint fwdTSN = tsn + 1; - while (endTSN == null && fragments.ContainsKey(fwdTSN)) - { - if (fragments[fwdTSN].Ending) - { - endTSN = fwdTSN; - } - else - { - fwdTSN++; - } + fwdTSN++; } } - - return (beginTSN, endTSN); } + + return (beginTSN, endTSN); } + } - /// - /// Extracts a fragmented chunk from the receive dictionary and passes it to the ULP. - /// - /// The dictionary containing the chunk fragments. - /// The beginning TSN for the fragment. - /// The end TSN for the fragment. - private SctpDataFrame GetFragmentedChunk(Dictionary fragments, uint beginTSN, uint endTSN) + /// + /// Extracts a fragmented chunk from the receive dictionary and passes it to the ULP. + /// + /// The dictionary containing the chunk fragments. + /// The beginning TSN for the fragment. + /// The end TSN for the fragment. + private SctpDataFrame GetFragmentedChunk(Dictionary fragments, uint beginTSN, uint endTSN) + { + unchecked { - unchecked - { - byte[] full = new byte[MAX_FRAME_SIZE]; - int posn = 0; - var beginChunk = fragments[beginTSN]; - var frame = new SctpDataFrame(beginChunk.Unordered, beginChunk.StreamID, beginChunk.StreamSeqNum, beginChunk.PPID, full); + var full = new byte[MAX_FRAME_SIZE]; + var posn = 0; + var beginChunk = fragments[beginTSN]; + var frame = new SctpDataFrame(beginChunk.Unordered, beginChunk.StreamID, beginChunk.StreamSeqNum, beginChunk.PPID, full); - uint afterEndTSN = endTSN + 1; - uint tsn = beginTSN; + var afterEndTSN = endTSN + 1; + var tsn = beginTSN; - while (tsn != afterEndTSN) - { - var fragment = fragments[tsn].UserData; - Buffer.BlockCopy(fragment, 0, full, posn, fragment.Length); - posn += fragment.Length; - fragments.Remove(tsn); - tsn++; - } + while (tsn != afterEndTSN) + { + var fragment = fragments[tsn].UserData; + Debug.Assert(fragment is { }); + Buffer.BlockCopy(fragment, 0, full, posn, fragment.Length); + posn += fragment.Length; + fragments.Remove(tsn); + tsn++; + } - frame.UserData = frame.UserData.Take(posn).ToArray(); + frame.UserData = frame.UserData.AsSpan(0, posn).ToArray(); - return frame; - } + return frame; } + } - /// - /// Determines if a received TSN is newer than the expected TSN taking - /// into account if TSN wrap around has occurred. - /// - /// The TSN to compare against. - /// The received TSN. - /// True if the received TSN is newer than the reference TSN - /// or false if not. - public static bool IsNewer(uint tsn, uint receivedTSN) + /// + /// Determines if a received TSN is newer than the expected TSN taking + /// into account if TSN wrap around has occurred. + /// + /// The TSN to compare against. + /// The received TSN. + /// True if the received TSN is newer than the reference TSN + /// or false if not. + public static bool IsNewer(uint tsn, uint receivedTSN) + { + if (tsn < uint.MaxValue / 2 && receivedTSN > uint.MaxValue / 2) { - if (tsn < uint.MaxValue / 2 && receivedTSN > uint.MaxValue / 2) - { - // TSN wrap has occurred and the received TSN is old. - return false; - } - else if (tsn > uint.MaxValue / 2 && receivedTSN < uint.MaxValue / 2) - { - // TSN wrap has occurred and the received TSN is new. - return true; - } - else - { - return receivedTSN > tsn; - } + // TSN wrap has occurred and the received TSN is old. + return false; } - - public static bool IsNewerOrEqual(uint tsn, uint receivedTSN) + else if (tsn > uint.MaxValue / 2 && receivedTSN < uint.MaxValue / 2) { - return tsn == receivedTSN || IsNewer(tsn, receivedTSN); + // TSN wrap has occurred and the received TSN is new. + return true; } - - /// - /// Gets the distance between two unsigned integers. The "distance" means how many - /// points are there between the two unsigned integers and allows wrapping from - /// the unsigned integer maximum to zero. - /// - /// The shortest distance between the two unsigned integers. - public static uint GetDistance(uint start, uint end) + else { - uint fwdDistance = end - start; - uint backDistance = start - end; - - return (fwdDistance < backDistance) ? fwdDistance : backDistance; + return receivedTSN > tsn; } } + + public static bool IsNewerOrEqual(uint tsn, uint receivedTSN) + { + return tsn == receivedTSN || IsNewer(tsn, receivedTSN); + } + + /// + /// Gets the distance between two unsigned integers. The "distance" means how many + /// points are there between the two unsigned integers and allows wrapping from + /// the unsigned integer maximum to zero. + /// + /// The shortest distance between the two unsigned integers. + public static uint GetDistance(uint start, uint end) + { + var fwdDistance = end - start; + var backDistance = start - end; + + return (fwdDistance < backDistance) ? fwdDistance : backDistance; + } } diff --git a/src/SIPSorcery/net/SCTP/SctpDataSender.cs b/src/SIPSorcery/net/SCTP/SctpDataSender.cs index a37cef77d9..5ae15c2ad0 100644 --- a/src/SIPSorcery/net/SCTP/SctpDataSender.cs +++ b/src/SIPSorcery/net/SCTP/SctpDataSender.cs @@ -20,583 +20,617 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class SctpDataSender { - public class SctpDataSender + public const ushort DEFAULT_SCTP_MTU = 1300; + + public const uint CONGESTION_WINDOW_FACTOR = 4380; + + /// + /// Used to limit the number of packets that are sent at any one time, i.e. when + /// the transmit timer fires do not send more than this many packets. + /// + public const int MAX_BURST = 4; + + /// + /// Milliseconds to wait between bursts if no SACK chunks are received in the interim. + /// Eventually if no SACK chunks are received the congestion or receiver windows + /// will reach zero and enforce a longer period. + /// + public const int BURST_PERIOD_MILLISECONDS = 50; + + /// + /// Retransmission timeout initial value. + /// + public const int RTO_INITIAL_SECONDS = 3; + + /// + /// The minimum value for the Retransmission timeout. + /// + public const int RTO_MIN_SECONDS = 1; + + /// + /// The maximum value for the Retransmission timeout. + /// + public const int RTO_MAX_SECONDS = 60; + + private static ILogger logger = LogFactory.CreateLogger(); + + /// + /// Callback method that sends data chunks. + /// + internal Action _sendDataChunk; + + private string _associationID; + private ushort _defaultMTU; + private uint _initialTSN; + private bool _gotFirstSACK; + private bool _isStarted; + private bool _isClosed; + private int _lastAckedDataChunkSize; + private bool _inRetransmitMode; + private bool _inFastRecoveryMode; + private uint _fastRecoveryExitPoint; + private ManualResetEventSlim _senderMre = new ManualResetEventSlim(); + + /// + /// Congestion control window (cwnd, in bytes), which is adjusted by + /// the sender based on observed network conditions. + /// + internal uint _congestionWindow; + + /// + /// The current Advertised Receiver Window Credit for the remote peer. + /// This value represents the dedicated buffer space on the remote peer, + /// in number of bytes, that will be used for the receive buffer for DATA + /// chunks sent to it. + /// + internal uint _receiverWindow; + + /// + /// Slow-start threshold (ssthresh, in bytes), which is used by the + /// sender to distinguish slow-start and congestion avoidance phases. + /// + private uint _slowStartThreshold; + + /// + /// The initial Advertised Receiver Window Credit for the remote peer. + /// This value represents the dedicated buffer space on the remote peer, + /// in number of bytes, that will be used for the receive buffer for DATA + /// chunks sent to it. + /// + private uint _initialRemoteARwnd; + + internal int _burstPeriodMilliseconds = BURST_PERIOD_MILLISECONDS; + /// + /// Retransmission timeout. + /// See https://datatracker.ietf.org/doc/html/rfc4960#section-6.3.1 + /// + internal double _rto = RTO_INITIAL_SECONDS * 1000; + internal int _rtoInitialMilliseconds = RTO_INITIAL_SECONDS * 1000; + internal int _rtoMinimumMilliseconds = RTO_MIN_SECONDS * 1000; + internal int _rtoMaximumMilliseconds = RTO_MAX_SECONDS * 1000; + private bool _hasRoundTripTime; + private double _smoothedRoundTripTime; // "SRTT" + private double _roundTripTimeVariation; // "RTTVAR" + private double _rtoAlpha = 0.125; // Suggested value in rfc4960#section-15 + private double _rtoBeta = 0.25; // Suggested value in rfc4960#section-15 + + /// + /// A count of the bytes currently in-flight to the remote peer. + /// + internal uint _outstandingBytes { - public const ushort DEFAULT_SCTP_MTU = 1300; - - public const uint CONGESTION_WINDOW_FACTOR = 4380; - - /// - /// Used to limit the number of packets that are sent at any one time, i.e. when - /// the transmit timer fires do not send more than this many packets. - /// - public const int MAX_BURST = 4; - - /// - /// Milliseconds to wait between bursts if no SACK chunks are received in the interim. - /// Eventually if no SACK chunks are received the congestion or receiver windows - /// will reach zero and enforce a longer period. - /// - public const int BURST_PERIOD_MILLISECONDS = 50; - - /// - /// Retransmission timeout initial value. - /// - public const int RTO_INITIAL_SECONDS = 3; - - /// - /// The minimum value for the Retransmission timeout. - /// - public const int RTO_MIN_SECONDS = 1; - - /// - /// The maximum value for the Retransmission timeout. - /// - public const int RTO_MAX_SECONDS = 60; - - private static ILogger logger = LogFactory.CreateLogger(); - - /// - /// Callback method that sends data chunks. - /// - internal Action _sendDataChunk; - - private string _associationID; - private ushort _defaultMTU; - private uint _initialTSN; - private bool _gotFirstSACK; - private bool _isStarted; - private bool _isClosed; - private int _lastAckedDataChunkSize; - private bool _inRetransmitMode; - private bool _inFastRecoveryMode; - private uint _fastRecoveryExitPoint; - private ManualResetEventSlim _senderMre = new ManualResetEventSlim(); - - /// - /// Congestion control window (cwnd, in bytes), which is adjusted by - /// the sender based on observed network conditions. - /// - internal uint _congestionWindow; - - /// - /// The current Advertised Receiver Window Credit for the remote peer. - /// This value represents the dedicated buffer space on the remote peer, - /// in number of bytes, that will be used for the receive buffer for DATA - /// chunks sent to it. - /// - internal uint _receiverWindow; - - /// - /// Slow-start threshold (ssthresh, in bytes), which is used by the - /// sender to distinguish slow-start and congestion avoidance phases. - /// - private uint _slowStartThreshold; - - /// - /// The initial Advertised Receiver Window Credit for the remote peer. - /// This value represents the dedicated buffer space on the remote peer, - /// in number of bytes, that will be used for the receive buffer for DATA - /// chunks sent to it. - /// - private uint _initialRemoteARwnd; - - internal int _burstPeriodMilliseconds = BURST_PERIOD_MILLISECONDS; - /// - /// Retransmission timeout. - /// See https://datatracker.ietf.org/doc/html/rfc4960#section-6.3.1 - /// - internal double _rto = RTO_INITIAL_SECONDS * 1000; - internal int _rtoInitialMilliseconds = RTO_INITIAL_SECONDS * 1000; - internal int _rtoMinimumMilliseconds = RTO_MIN_SECONDS * 1000; - internal int _rtoMaximumMilliseconds = RTO_MAX_SECONDS * 1000; - private bool _hasRoundTripTime; - private double _smoothedRoundTripTime; // "SRTT" - private double _roundTripTimeVariation; // "RTTVAR" - private double _rtoAlpha = 0.125; // Suggested value in rfc4960#section-15 - private double _rtoBeta = 0.25; // Suggested value in rfc4960#section-15 - - /// - /// A count of the bytes currently in-flight to the remote peer. - /// - internal uint _outstandingBytes => - (uint)(_unconfirmedChunks.Sum(x => x.Value.UserData.Length)); - - /// - /// The TSN that the remote peer has acknowledged. - /// - private uint _cumulativeAckTSN; - - /// - /// Keeps track of the sequence numbers for each of the streams being - /// used by the association. - /// - private Dictionary _streamSeqnums = new Dictionary(); - - /// - /// Queue to hold SCTP frames that are waiting to be sent to the remote peer. - /// - private ConcurrentQueue _sendQueue = new ConcurrentQueue(); - - /// - /// Chunks that have been sent to the remote peer but have yet to be acknowledged. - /// - private ConcurrentDictionary _unconfirmedChunks = new ConcurrentDictionary(); - - /// - /// Chunks that have been flagged by a gap report from the remote peer as missing - /// and that need to be re-sent. - /// - internal ConcurrentDictionary _missingChunks = new ConcurrentDictionary(); - - /// - /// The total size (in bytes) of queued user data that will be sent to the peer. - /// - public ulong BufferedAmount => (ulong)_sendQueue.Sum(x => x.UserData?.Length ?? 0); - - /// - /// The Transaction Sequence Number (TSN) that will be used in the next DATA chunk sent. - /// - public uint TSN { get; internal set; } - - public SctpDataSender( - string associationID, - Action sendDataChunk, - ushort defaultMTU, - uint initialTSN, - uint remoteARwnd) + get { - _associationID = associationID; - _sendDataChunk = sendDataChunk; - _defaultMTU = defaultMTU > 0 ? defaultMTU : DEFAULT_SCTP_MTU; - _initialTSN = initialTSN; - TSN = initialTSN; - _initialRemoteARwnd = remoteARwnd; - _receiverWindow = remoteARwnd; - - // RFC4960 7.2.1 (point 1) - _congestionWindow = (uint)(Math.Min(4 * _defaultMTU, Math.Max(2 * _defaultMTU, CONGESTION_WINDOW_FACTOR))); - - // RFC4960 7.2.1 (point 3) - _slowStartThreshold = _initialRemoteARwnd; + uint total = 0; + foreach (var kvp in _unconfirmedChunks) + { + Debug.Assert(kvp.Value.UserData is { }); + total += (uint)kvp.Value.UserData.Length; + } + return total; } + } - public void SetReceiverWindow(uint remoteARwnd) + /// + /// The TSN that the remote peer has acknowledged. + /// + private uint _cumulativeAckTSN; + + /// + /// Keeps track of the sequence numbers for each of the streams being + /// used by the association. + /// + private Dictionary _streamSeqnums = new Dictionary(); + + /// + /// Queue to hold SCTP frames that are waiting to be sent to the remote peer. + /// + private ConcurrentQueue _sendQueue = new ConcurrentQueue(); + + /// + /// Chunks that have been sent to the remote peer but have yet to be acknowledged. + /// + private ConcurrentDictionary _unconfirmedChunks = new ConcurrentDictionary(); + + /// + /// Chunks that have been flagged by a gap report from the remote peer as missing + /// and that need to be re-sent. + /// + internal ConcurrentDictionary _missingChunks = new ConcurrentDictionary(); + + /// + /// The total size (in bytes) of queued user data that will be sent to the peer. + /// + public ulong BufferedAmount + { + get { - _initialRemoteARwnd = remoteARwnd; + ulong total = 0; + foreach (var x in _sendQueue) + { + total += (ulong)(x.UserData?.Length ?? 0); + } + return total; } + } + + /// + /// The Transaction Sequence Number (TSN) that will be used in the next DATA chunk sent. + /// + public uint TSN { get; internal set; } + + public SctpDataSender( + string associationID, + Action sendDataChunk, + ushort defaultMTU, + uint initialTSN, + uint remoteARwnd) + { + _associationID = associationID; + _sendDataChunk = sendDataChunk; + _defaultMTU = defaultMTU > 0 ? defaultMTU : DEFAULT_SCTP_MTU; + _initialTSN = initialTSN; + TSN = initialTSN; + _initialRemoteARwnd = remoteARwnd; + _receiverWindow = remoteARwnd; + + // RFC4960 7.2.1 (point 1) + _congestionWindow = (uint)(Math.Min(4 * _defaultMTU, Math.Max(2 * _defaultMTU, CONGESTION_WINDOW_FACTOR))); + + // RFC4960 7.2.1 (point 3) + _slowStartThreshold = _initialRemoteARwnd; + } + + public void SetReceiverWindow(uint remoteARwnd) + { + _initialRemoteARwnd = remoteARwnd; + } - /// - /// Handler for SACK chunks received from the remote peer. - /// - /// The SACK chunk from the remote peer. - public void GotSack(SctpSackChunk sack) + /// + /// Handler for SACK chunks received from the remote peer. + /// + /// The SACK chunk from the remote peer. + public void GotSack(SctpSackChunk sack) + { + if (sack is { }) { - if (sack != null) + if (_inRetransmitMode) + { + logger.LogSctpSenderExitingRetransmitMode(); + _inRetransmitMode = false; + } + + unchecked { - if (_inRetransmitMode) + uint maxTSNDistance = SctpDataReceiver.GetDistance(_cumulativeAckTSN, TSN); + bool processGapReports = true; + uint cumAckTSNBeforeSackProcessing = _cumulativeAckTSN; + + if (_unconfirmedChunks.TryGetValue(sack.CumulativeTsnAck, out var result)) { - logger.LogTrace("SCTP sender exiting retransmit mode."); - _inRetransmitMode = false; + // Don't include retransmits in round trip calculation + if (result.SendCount == 1) + { + UpdateRoundTripTime(result); + } + + Debug.Assert(result.UserData is { }); + _lastAckedDataChunkSize = result.UserData.Length; } - unchecked + if (!_gotFirstSACK) { - uint maxTSNDistance = SctpDataReceiver.GetDistance(_cumulativeAckTSN, TSN); - bool processGapReports = true; - uint cumAckTSNBeforeSackProcessing = _cumulativeAckTSN; - - if (_unconfirmedChunks.TryGetValue(sack.CumulativeTsnAck, out var result)) + if (SctpDataReceiver.GetDistance(_initialTSN, sack.CumulativeTsnAck) < maxTSNDistance + && SctpDataReceiver.IsNewerOrEqual(_initialTSN, sack.CumulativeTsnAck)) { - // Don't include retransmits in round trip calculation - if (result.SendCount == 1) - { - UpdateRoundTripTime(result); - } - - _lastAckedDataChunkSize = result.UserData.Length; + logger.LogSctpFirstSackReceived(sack.CumulativeTsnAck, TSN, sack.ARwnd, sack.GapAckBlocks.Count); + _gotFirstSACK = true; + _cumulativeAckTSN = _initialTSN; + RemoveAckedUnconfirmedChunks(sack.CumulativeTsnAck); } - - if (!_gotFirstSACK) + } + else + { + if (_cumulativeAckTSN != sack.CumulativeTsnAck) { - if (SctpDataReceiver.GetDistance(_initialTSN, sack.CumulativeTsnAck) < maxTSNDistance - && SctpDataReceiver.IsNewerOrEqual(_initialTSN, sack.CumulativeTsnAck)) + if (SctpDataReceiver.GetDistance(_cumulativeAckTSN, sack.CumulativeTsnAck) > maxTSNDistance) { - logger.LogTrace("SCTP first SACK remote peer TSN ACK {CumulativeTsnAck} next sender TSN {TSN}, arwnd {ARwnd} (gap reports {GapAckBlocksCount}).", sack.CumulativeTsnAck, TSN, sack.ARwnd, sack.GapAckBlocks.Count); - _gotFirstSACK = true; - _cumulativeAckTSN = _initialTSN; - RemoveAckedUnconfirmedChunks(sack.CumulativeTsnAck); + logger.LogSctpSackTsnTooDistant(sack.CumulativeTsnAck, _cumulativeAckTSN); + processGapReports = false; } - } - else - { - if (_cumulativeAckTSN != sack.CumulativeTsnAck) + else if (!SctpDataReceiver.IsNewer(_cumulativeAckTSN, sack.CumulativeTsnAck)) { - if (SctpDataReceiver.GetDistance(_cumulativeAckTSN, sack.CumulativeTsnAck) > maxTSNDistance) - { - logger.LogWarning("SCTP SACK TSN from remote peer of {CumulativeTsnAck} was too distant from the expected {CumulativeAckTSN}, ignoring.", sack.CumulativeTsnAck, _cumulativeAckTSN); - processGapReports = false; - } - else if (!SctpDataReceiver.IsNewer(_cumulativeAckTSN, sack.CumulativeTsnAck)) - { - logger.LogWarning("SCTP SACK TSN from remote peer of {CumulativeTsnAck} was behind expected {CumulativeAckTSN}, ignoring.", sack.CumulativeTsnAck, _cumulativeAckTSN); - processGapReports = false; - } - else - { - logger.LogTrace("SCTP SACK remote peer TSN ACK {CumulativeTsnAck}, next sender TSN {TSN}, arwnd {ARwnd} (gap reports {GapAckBlocksCount}).", sack.CumulativeTsnAck, TSN, sack.ARwnd, sack.GapAckBlocks.Count); - RemoveAckedUnconfirmedChunks(sack.CumulativeTsnAck); - } + logger.LogSctpSackTsnBehindExpected(sack.CumulativeTsnAck, _cumulativeAckTSN); + processGapReports = false; } else { - logger.LogTrace("SCTP SACK remote peer TSN ACK no change {CumulativeAckTSN}, next sender TSN {TSN}, arwnd {ARwnd} (gap reports {GapAckBlocksCount}).", _cumulativeAckTSN, TSN, sack.ARwnd, sack.GapAckBlocks.Count); + logger.LogSctpSackReceived(sack.CumulativeTsnAck, TSN, sack.ARwnd, sack.GapAckBlocks.Count); RemoveAckedUnconfirmedChunks(sack.CumulativeTsnAck); } } - - if (sack.DuplicateTSN.Count > 0) - { - // The remote is reporting that we have sent a duplicate TSN. - // This is probably because a SACK chunk was dropped. - // Ensure that we stop sending the duplicate. - foreach (uint duplicateTSN in sack.DuplicateTSN) - { - _unconfirmedChunks.TryRemove(duplicateTSN, out _); - _missingChunks.TryRemove(duplicateTSN, out _); - } - } - - - // Check gap reports. Only process them if the cumulative ACK TSN was acceptable. - if (processGapReports && sack.GapAckBlocks.Count > 0) + else { - bool didIncrementCumAckTSN = SctpDataReceiver.IsNewer(cumAckTSNBeforeSackProcessing, _cumulativeAckTSN); - ProcessGapReports(sack.GapAckBlocks, maxTSNDistance, didIncrementCumAckTSN); + logger.LogSctpSackReceivedNoChange(_cumulativeAckTSN, TSN, sack.ARwnd, sack.GapAckBlocks.Count); + RemoveAckedUnconfirmedChunks(sack.CumulativeTsnAck); } + } - // rfc4960 6.2.1 D iv - // If the Cumulative TSN Ack matches or exceeds the Fast Recovery exitpoint(Section 7.2.4), Fast Recovery is exited. - if (_inFastRecoveryMode && SctpDataReceiver.IsNewerOrEqual(_fastRecoveryExitPoint, _cumulativeAckTSN)) + if (sack.DuplicateTSN.Count > 0) + { + // The remote is reporting that we have sent a duplicate TSN. + // This is probably because a SACK chunk was dropped. + // Ensure that we stop sending the duplicate. + foreach (uint duplicateTSN in sack.DuplicateTSN) { - logger.LogTrace("SCTP sender exiting fast recovery at TSN {FastRecoveryExitPoint}", _fastRecoveryExitPoint); - _inFastRecoveryMode = false; + _unconfirmedChunks.TryRemove(duplicateTSN, out _); + _missingChunks.TryRemove(duplicateTSN, out _); } } - _receiverWindow = CalculateReceiverWindow(sack.ARwnd); - _congestionWindow = CalculateCongestionWindow(_lastAckedDataChunkSize); - // SACK's will normally allow more data to be sent. - _senderMre.Set(); + // Check gap reports. Only process them if the cumulative ACK TSN was acceptable. + if (processGapReports && sack.GapAckBlocks.Count > 0) + { + bool didIncrementCumAckTSN = SctpDataReceiver.IsNewer(cumAckTSNBeforeSackProcessing, _cumulativeAckTSN); + ProcessGapReports(sack.GapAckBlocks, maxTSNDistance, didIncrementCumAckTSN); + } + + // rfc4960 6.2.1 D iv + // If the Cumulative TSN Ack matches or exceeds the Fast Recovery exitpoint(Section 7.2.4), Fast Recovery is exited. + if (_inFastRecoveryMode && SctpDataReceiver.IsNewerOrEqual(_fastRecoveryExitPoint, _cumulativeAckTSN)) + { + logger.LogExitingFastRecovery(_fastRecoveryExitPoint); + _inFastRecoveryMode = false; + } } + + _receiverWindow = CalculateReceiverWindow(sack.ARwnd); + _congestionWindow = CalculateCongestionWindow(_lastAckedDataChunkSize); + + // SACK's will normally allow more data to be sent. + _senderMre.Set(); } + } + + /// + /// Sends a DATA chunk to the remote peer. + /// + /// The stream ID to sent the data on. + /// The payload protocol ID for the data. + /// The byte data to send. + /// The offset in at which to begin sending. Defaults to 0. + /// The number of bytes to send. Defaults to -1, meaning all bytes from to the end of the array. + public void SendData(ushort streamID, uint ppid, byte[] data, int offset = 0, int count = -1) + { + int dataCount = count < 0 ? data.Length - offset : count; - /// - /// Sends a DATA chunk to the remote peer. - /// - /// The stream ID to sent the data on. - /// The payload protocol ID for the data. - /// The byte data to send. - /// The offset in at which to begin sending. Defaults to 0. - /// The number of bytes to send. Defaults to -1, meaning all bytes from to the end of the array. - public void SendData(ushort streamID, uint ppid, byte[] data, int offset = 0, int count = -1) + lock (_sendQueue) { - int dataCount = count < 0 ? data.Length - offset : count; + ushort seqnum = 0; - lock (_sendQueue) + if (_streamSeqnums.TryGetValue(streamID, out var streamSeqnum)) { - ushort seqnum = 0; - - if (_streamSeqnums.ContainsKey(streamID)) - { - unchecked - { - _streamSeqnums[streamID] = (ushort)(_streamSeqnums[streamID] + 1); - seqnum = _streamSeqnums[streamID]; - } - } - else + unchecked { - _streamSeqnums.Add(streamID, 0); + _streamSeqnums[streamID] = (ushort)(streamSeqnum + 1); + seqnum = _streamSeqnums[streamID]; } + } + else + { + _streamSeqnums.Add(streamID, 0); + } - for (int index = 0; index * _defaultMTU < dataCount; index++) - { - int chunkOffset = index * _defaultMTU; - int payloadLength = (chunkOffset + _defaultMTU < dataCount) ? _defaultMTU : dataCount - chunkOffset; + for (int index = 0; index * _defaultMTU < dataCount; index++) + { + int chunkOffset = index * _defaultMTU; + int payloadLength = (chunkOffset + _defaultMTU < dataCount) ? _defaultMTU : dataCount - chunkOffset; - // Future TODO: Replace with slice when System.Memory is introduced as a dependency. - byte[] payload = new byte[payloadLength]; - Buffer.BlockCopy(data, offset + chunkOffset, payload, 0, payloadLength); + // Future TODO: Replace with slice when System.Memory is introduced as a dependency. + byte[] payload = new byte[payloadLength]; + Buffer.BlockCopy(data, offset + chunkOffset, payload, 0, payloadLength); - bool isBegining = index == 0; - bool isEnd = ((chunkOffset + payloadLength) >= dataCount) ? true : false; + bool isBegining = index == 0; + bool isEnd = (chunkOffset + payloadLength) >= dataCount; - SctpDataChunk dataChunk = new SctpDataChunk( - false, - isBegining, - isEnd, - TSN, - streamID, - seqnum, - ppid, - payload); + SctpDataChunk dataChunk = new SctpDataChunk( + false, + isBegining, + isEnd, + TSN, + streamID, + seqnum, + ppid, + payload); - _sendQueue.Enqueue(dataChunk); + _sendQueue.Enqueue(dataChunk); - TSN = (TSN == UInt32.MaxValue) ? 0 : TSN + 1; - } - - _senderMre.Set(); + TSN = (TSN == uint.MaxValue) ? 0 : TSN + 1; } - } - /// - /// Start the sending thread to process the new DATA chunks from the application and - /// any retransmits or timed out chunks. - /// - public void StartSending() - { - if (!_isStarted) - { - _isStarted = true; - var sendThread = new Thread(DoSend); - sendThread.IsBackground = true; - sendThread.Start(); - } + _senderMre.Set(); } + } - /// - /// Stops the sending thread. - /// - public void Close() + /// + /// Start the sending thread to process the new DATA chunks from the application and + /// any retransmits or timed out chunks. + /// + public void StartSending() + { + if (!_isStarted) { - _isClosed = true; + _isStarted = true; + var sendThread = new Thread(DoSend); + sendThread.IsBackground = true; + sendThread.Start(); } + } - /// - /// Updates the sender state for the gap reports received in a SACH chunk from the - /// remote peer. - /// - /// The gap reports from the remote peer. - /// The maximum distance any valid TSN should be from the current - /// ACK'ed TSN. If this distance gets exceeded by a gap report then it's likely something has been - /// miscalculated. - /// If true, processing of the SACK incremented the - private void ProcessGapReports(List sackGapBlocks, uint maxTSNDistance, bool didSackIncrementTSN) - { - uint lastAckTSN = _cumulativeAckTSN; + /// + /// Stops the sending thread. + /// + public void Close() + { + _isClosed = true; + } + + /// + /// Updates the sender state for the gap reports received in a SACH chunk from the + /// remote peer. + /// + /// The gap reports from the remote peer. + /// The maximum distance any valid TSN should be from the current + /// ACK'ed TSN. If this distance gets exceeded by a gap report then it's likely something has been + /// miscalculated. + /// If true, processing of the SACK incremented the + private void ProcessGapReports(List sackGapBlocks, uint maxTSNDistance, bool didSackIncrementTSN) + { + uint lastAckTSN = _cumulativeAckTSN; - // https://www.rfc-editor.org/rfc/rfc4960#section-7.2.4 - // For each incoming SACK, miss indications are incremented only for missing TSNs prior to the highest TSN newly acknowledged in the SACK. - uint highestTsnNewlyAcknowledged = lastAckTSN; + // https://www.rfc-editor.org/rfc/rfc4960#section-7.2.4 + // For each incoming SACK, miss indications are incremented only for missing TSNs prior to the highest TSN newly acknowledged in the SACK. + uint highestTsnNewlyAcknowledged = lastAckTSN; - unchecked { + unchecked + { - // Parse the gap report to identify missing chunks that have now been acknowledged in the gap report - foreach (var block in sackGapBlocks) + // Parse the gap report to identify missing chunks that have now been acknowledged in the gap report + foreach (var block in sackGapBlocks) + { + for (ushort offset = block.Start; offset <= block.End; offset++) { - for (ushort offset = block.Start; offset <= block.End; offset++) - { - uint goodTSN = _cumulativeAckTSN + offset; + uint goodTSN = _cumulativeAckTSN + offset; - _missingChunks.TryRemove(goodTSN, out _); - if (_unconfirmedChunks.TryRemove(goodTSN, out _)) - { - logger.LogTrace("SCTP acknowledged data chunk receipt in gap report for TSN {goodTSN}", goodTSN); - highestTsnNewlyAcknowledged = goodTSN; - } + _missingChunks.TryRemove(goodTSN, out _); + if (_unconfirmedChunks.TryRemove(goodTSN, out _)) + { + logger.LogSctpAcknowledgedDataChunkReceipt(goodTSN); + highestTsnNewlyAcknowledged = goodTSN; } } + } - foreach (var gapBlock in sackGapBlocks) - { - uint goodTSNStart = _cumulativeAckTSN + gapBlock.Start; + foreach (var gapBlock in sackGapBlocks) + { + uint goodTSNStart = _cumulativeAckTSN + gapBlock.Start; - if (SctpDataReceiver.GetDistance(lastAckTSN, goodTSNStart) > maxTSNDistance) - { - logger.LogWarning("SCTP SACK gap report had a start TSN of {goodTSNStart} too distant from last good TSN {lastAckTSN}, ignoring rest of SACK.", goodTSNStart, lastAckTSN); - break; - } - else if (!SctpDataReceiver.IsNewer(lastAckTSN, goodTSNStart)) - { - logger.LogWarning("SCTP SACK gap report had a start TSN of {goodTSNStart} behind last good TSN {lastAckTSN}, ignoring rest of SACK.", goodTSNStart, lastAckTSN); - break; - } - else - { - uint missingTSN = lastAckTSN + 1; + if (SctpDataReceiver.GetDistance(lastAckTSN, goodTSNStart) > maxTSNDistance) + { + logger.LogSctpSackGapReportStartTooDistant(goodTSNStart, lastAckTSN); + break; + } + else if (!SctpDataReceiver.IsNewer(lastAckTSN, goodTSNStart)) + { + logger.LogSctpSackGapReportStartBehind(goodTSNStart, lastAckTSN); + break; + } + else + { + uint missingTSN = lastAckTSN + 1; - logger.LogTrace("SCTP SACK gap report start TSN {goodTSNStart} gap report end TSN {gapBlockEnd} first missing TSN {missingTSN}.", goodTSNStart, _cumulativeAckTSN + gapBlock.End, missingTSN); + logger.LogSctpSackGapReport(goodTSNStart, _cumulativeAckTSN + gapBlock.End, missingTSN); - while (missingTSN != goodTSNStart) + while (missingTSN != goodTSNStart) + { + if (!_missingChunks.TryGetValue(missingTSN, out int missCount)) { - if (!_missingChunks.TryGetValue(missingTSN, out int missCount)) + if (!_unconfirmedChunks.ContainsKey(missingTSN)) { - if (!_unconfirmedChunks.ContainsKey(missingTSN)) - { - // What to do? Can't retransmit a chunk that's no longer available. - // Hope it's a transient error from a duplicate or out of order SACK. - // TODO: Maybe keep count of how many time this occurs and send an ABORT if it - // gets to a certain threshold. - logger.LogWarning("SCTP SACK gap report reported missing TSN of {MissingTSN} but no matching unconfirmed chunk available.", missingTSN); - break; - } - else - { - logger.LogTrace("SCTP SACK gap adding retransmit entry for TSN {MissingTSN}.", missingTSN); - _missingChunks.TryAdd(missingTSN, 1); - } + // What to do? Can't retransmit a chunk that's no longer available. + // Hope it's a transient error from a duplicate or out of order SACK. + // TODO: Maybe keep count of how many time this occurs and send an ABORT if it + // gets to a certain threshold. + logger.LogSctpNoMatchingUnconfirmedChunk(missingTSN); + break; } - else if ( - // If an endpoint is in Fast Recovery and a SACK arrives that advances the Cumulative TSN Ack - // Point, the miss indications are incremented for all TSNs reported missing in the SACK. - (_inFastRecoveryMode && didSackIncrementTSN) || - // For each incoming SACK, miss indications are incremented only - // for missing TSNs prior to the highest TSN newly acknowledged in the SACK. - SctpDataReceiver.IsNewer(missingTSN, highestTsnNewlyAcknowledged)) + else { - _missingChunks.TryUpdate(missingTSN, missCount + 1, missCount); + logger.LogSctpSackGapAddingRetransmitEntry(missingTSN); + _missingChunks.TryAdd(missingTSN, 1); + } + } + else if ( + // If an endpoint is in Fast Recovery and a SACK arrives that advances the Cumulative TSN Ack + // Point, the miss indications are incremented for all TSNs reported missing in the SACK. + (_inFastRecoveryMode && didSackIncrementTSN) || + // For each incoming SACK, miss indications are incremented only + // for missing TSNs prior to the highest TSN newly acknowledged in the SACK. + SctpDataReceiver.IsNewer(missingTSN, highestTsnNewlyAcknowledged)) + { + _missingChunks.TryUpdate(missingTSN, missCount + 1, missCount); - // rfc 7.2.4: When the third consecutive miss indication is received for a TSN(s), the data sender shall do the following... - if (missCount + 1 == 3) + // rfc 7.2.4: When the third consecutive miss indication is received for a TSN(s), the data sender shall do the following... + if (missCount + 1 == 3) + { + if (!_inFastRecoveryMode) // RFC4960 7.2.4 (2) { - if (!_inFastRecoveryMode) // RFC4960 7.2.4 (2) - { - _inFastRecoveryMode = true; - // mark the highest outstanding TSN as the Fast Recovery exit point - _fastRecoveryExitPoint = _cumulativeAckTSN + sackGapBlocks.Last().End; - - logger.LogTrace("SCTP sender entering fast recovery mode due to missing TSN {MissingTSN}. Fast recovery exit point {FastRecoveryExitPoint}.", missingTSN, _fastRecoveryExitPoint); - // RFC4960 7.2.3 - _slowStartThreshold = (uint)Math.Max(_congestionWindow / 2, 4 * _defaultMTU); - _congestionWindow = _defaultMTU; - } + _inFastRecoveryMode = true; + // mark the highest outstanding TSN as the Fast Recovery exit point + Debug.Assert(sackGapBlocks.Count > 0); + _fastRecoveryExitPoint = _cumulativeAckTSN + sackGapBlocks[^1].End; + + logger.LogSctpSenderEnteringFastRecoveryMode(missingTSN, _fastRecoveryExitPoint); + // RFC4960 7.2.3 + _slowStartThreshold = (uint)Math.Max(_congestionWindow / 2, 4 * _defaultMTU); + _congestionWindow = _defaultMTU; } } - - missingTSN++; } - } - lastAckTSN = _cumulativeAckTSN + gapBlock.End; + missingTSN++; + } } + + lastAckTSN = _cumulativeAckTSN + gapBlock.End; } } + } - /// - /// Removes the chunks waiting for a SACK confirmation from the unconfirmed queue. - /// - /// The acknowledged TSN received from in a SACK from the remote peer. - private void RemoveAckedUnconfirmedChunks(uint sackTSN) - { - logger.LogTrace("SCTP data sender removing unconfirmed chunks cumulative ACK TSN {CumulativeAckTSN}, SACK TSN {SackTSN}.", _cumulativeAckTSN, sackTSN); + /// + /// Removes the chunks waiting for a SACK confirmation from the unconfirmed queue. + /// + /// The acknowledged TSN received from in a SACK from the remote peer. + private void RemoveAckedUnconfirmedChunks(uint sackTSN) + { + logger.LogSctpSenderRemovingUnconfirmedChunks(_cumulativeAckTSN, sackTSN); - if (_cumulativeAckTSN == sackTSN) - { - // This is normal for the first SACK received. - _unconfirmedChunks.TryRemove(_cumulativeAckTSN, out _); - _missingChunks.TryRemove(_cumulativeAckTSN, out _); - } - else + if (_cumulativeAckTSN == sackTSN) + { + // This is normal for the first SACK received. + _unconfirmedChunks.TryRemove(_cumulativeAckTSN, out _); + _missingChunks.TryRemove(_cumulativeAckTSN, out _); + } + else + { + unchecked { - unchecked + for (uint offset = 0; offset <= SctpDataReceiver.GetDistance(_cumulativeAckTSN, sackTSN); offset++) { - for (uint offset = 0; offset <= SctpDataReceiver.GetDistance(_cumulativeAckTSN, sackTSN); offset++) - { - uint ackd = _cumulativeAckTSN + offset; - _unconfirmedChunks.TryRemove(ackd, out _); - _missingChunks.TryRemove(ackd, out _); - } - _cumulativeAckTSN = sackTSN; + uint ackd = _cumulativeAckTSN + offset; + _unconfirmedChunks.TryRemove(ackd, out _); + _missingChunks.TryRemove(ackd, out _); } + _cumulativeAckTSN = sackTSN; } } + } - /// - /// Worker thread to process the send and retransmit queues. - /// - private void DoSend(object state) - { - logger.LogDebug("SCTP association data send thread started for association {AssociationID}.", _associationID); + /// + /// Worker thread to process the send and retransmit queues. + /// + private void DoSend(object? state) + { + logger.LogSctpDataSendThreadStarted(_associationID); - while (!_isClosed) + while (!_isClosed) + { + // Reset the sender wake event at the START of each iteration. Resetting + // AFTER the send work introduces a lost-wakeup race with SACK arrival: + // a SACK that fires _senderMre.Set() between the last chunk sent and + // the Reset() gets its signal wiped by Reset(), and the thread then + // blocks in _senderMre.Wait() for the full BURST_PERIOD_MILLISECONDS + // even though more work is ready to go. On loopback where SACKs + // round-trip in microseconds this window is hit almost every burst, + // capping throughput at the RFC4960 §7.2.2 steady-state of roughly + // MAX_BURST * MTU / BURST_PERIOD (~104 KB/s for MTU=1300, period=50ms). + // Resetting first preserves any Set() that happens during send work so + // Wait() returns immediately on the next iteration. + _senderMre.Reset(); + + var outstandingBytes = _outstandingBytes; + // DateTime.Now calls have been a tiny bit expensive in the past so get a small saving by only + // calling once per loop. + DateTime now = DateTime.Now; + + var burstSize = (_inRetransmitMode || _inFastRecoveryMode || _congestionWindow < outstandingBytes || _receiverWindow == 0) ? 1 : MAX_BURST; + var chunksSent = 0; + + //logger.LogTrace($"SCTP sender burst size {burstSize}, in retransmit mode {_inRetransmitMode}, cwnd {_congestionWindow}, arwnd {_receiverWindow}."); + + // Missing chunks from a SACK gap report take priority. + if (_missingChunks.Count > 0) { - // Reset the sender wake event at the START of each iteration. Resetting - // AFTER the send work introduces a lost-wakeup race with SACK arrival: - // a SACK that fires _senderMre.Set() between the last chunk sent and - // the Reset() gets its signal wiped by Reset(), and the thread then - // blocks in _senderMre.Wait() for the full BURST_PERIOD_MILLISECONDS - // even though more work is ready to go. On loopback where SACKs - // round-trip in microseconds this window is hit almost every burst, - // capping throughput at the RFC4960 §7.2.2 steady-state of roughly - // MAX_BURST * MTU / BURST_PERIOD (~104 KB/s for MTU=1300, period=50ms). - // Resetting first preserves any Set() that happens during send work so - // Wait() returns immediately on the next iteration. - _senderMre.Reset(); - - var outstandingBytes = _outstandingBytes; - // DateTime.Now calls have been a tiny bit expensive in the past so get a small saving by only - // calling once per loop. - DateTime now = DateTime.Now; - - int burstSize = (_inRetransmitMode || _inFastRecoveryMode || _congestionWindow < outstandingBytes || _receiverWindow == 0) ? 1 : MAX_BURST; - int chunksSent = 0; - - //logger.LogTrace($"SCTP sender burst size {burstSize}, in retransmit mode {_inRetransmitMode}, cwnd {_congestionWindow}, arwnd {_receiverWindow}."); - - // Missing chunks from a SACK gap report take priority. - if (_missingChunks.Count > 0) + foreach (var missing in _missingChunks) { - foreach (var missing in _missingChunks) + if (missing.Value >= 3) // RFC4960 7.2.4 Fast retransmission { - if (missing.Value >= 3) // RFC4960 7.2.4 Fast retransmission + if (_unconfirmedChunks.TryGetValue(missing.Key, out var missingChunk)) { - if (_unconfirmedChunks.TryGetValue(missing.Key, out var missingChunk)) - { - missingChunk.LastSentAt = now; - missingChunk.SendCount += 1; + missingChunk.LastSentAt = now; + missingChunk.SendCount += 1; - logger.LogTrace("SCTP resending missing data chunk for TSN {TSN}, data length {UserDataLength}, flags {ChunkFlags:X2}, send count {SendCount}.", missingChunk.TSN, missingChunk.UserData.Length, missingChunk.ChunkFlags, missingChunk.SendCount); + Debug.Assert(missingChunk.UserData is { }); + logger.LogSctpResendingMissingDataChunk(missingChunk.TSN, missingChunk.UserData.Length, missingChunk.ChunkFlags, missingChunk.SendCount); - _sendDataChunk(missingChunk); - chunksSent++; - _missingChunks.TryUpdate(missing.Key, 0, missing.Value); - } - } - if (chunksSent >= burstSize) - { - break; + _sendDataChunk(missingChunk); + chunksSent++; + _missingChunks.TryUpdate(missing.Key, 0, missing.Value); } } + if (chunksSent >= burstSize) + { + break; + } } + } - // Check if there are any unconfirmed transactions that are due for a retransmit. - if (chunksSent < burstSize && _unconfirmedChunks.Count > 0) + // Check if there are any unconfirmed transactions that are due for a retransmit. + if (chunksSent < burstSize && _unconfirmedChunks.Count > 0) + { + var retransmitNeeded = burstSize - chunksSent; + var retransmitSent = 0; + foreach (var chunk in _unconfirmedChunks.Values) { - foreach (var chunk in _unconfirmedChunks.Values - .Where(x => now.Subtract(x.LastSentAt).TotalMilliseconds > (_hasRoundTripTime ? _rto : _rtoInitialMilliseconds)) - .Take(burstSize - chunksSent)) + var elapsed = now.Subtract(chunk.LastSentAt).TotalMilliseconds; + var rto = _hasRoundTripTime ? _rto : _rtoInitialMilliseconds; + + if (elapsed > rto) { chunk.LastSentAt = DateTime.Now; chunk.SendCount += 1; - logger.LogTrace("SCTP retransmitting data chunk for TSN {TSN}, data length {DataLength}, flags {ChunkFlags}, send count {SendCount}.", chunk.TSN, chunk.UserData.Length, chunk.ChunkFlags, chunk.SendCount); + Debug.Assert(chunk.UserData is { }); + logger.LogSctpRetransmittingDataChunk(chunk.TSN, chunk.UserData.Length, chunk.ChunkFlags, chunk.SendCount); _sendDataChunk(chunk); chunksSent++; - + retransmitSent++; + if (!_inRetransmitMode) { - logger.LogTrace("SCTP sender entering retransmit mode."); + logger.LogEnteringRetransmitMode(); _inRetransmitMode = true; // When the T3-rtx timer expires on an address, SCTP should perform slow start. @@ -611,157 +645,163 @@ private void DoSend(object state) _rto = Math.Min(_rtoMaximumMilliseconds, _rto * 2); } } + + if (retransmitSent >= retransmitNeeded) + { + break; + } } } - // rfc4960 6.1: At any given time, the sender MUST NOT transmit new data to a given transport address - // if it has cwnd or more bytes of data outstanding to that transport address. + } + // rfc4960 6.1: At any given time, the sender MUST NOT transmit new data to a given transport address + // if it has cwnd or more bytes of data outstanding to that transport address. - // Send any new data chunks that have not yet been sent. - if (chunksSent < burstSize && _sendQueue.Count > 0 && _congestionWindow > outstandingBytes) + // Send any new data chunks that have not yet been sent. + if (chunksSent < burstSize && _sendQueue.Count > 0 && _congestionWindow > outstandingBytes) + { + while (chunksSent < burstSize && _sendQueue.TryDequeue(out var dataChunk)) { - while (chunksSent < burstSize && _sendQueue.TryDequeue(out var dataChunk)) - { - dataChunk.LastSentAt = DateTime.Now; - dataChunk.SendCount = 1; + dataChunk.LastSentAt = DateTime.Now; + dataChunk.SendCount = 1; - logger.LogTrace("SCTP resending missing data chunk for TSN {TSN}, data length {UserDataLength}, flags {ChunkFlags:X2}, send count {SendCount}.", dataChunk.TSN, dataChunk.UserData.Length, dataChunk.ChunkFlags, dataChunk.SendCount); + Debug.Assert(dataChunk.UserData is { }); + logger.LogSctpResendingMissingDataChunk2(dataChunk.TSN, dataChunk.UserData.Length, dataChunk.ChunkFlags, dataChunk.SendCount); - _unconfirmedChunks.TryAdd(dataChunk.TSN, dataChunk); - _sendDataChunk(dataChunk); - chunksSent++; - } + _unconfirmedChunks.TryAdd(dataChunk.TSN, dataChunk); + _sendDataChunk(dataChunk); + chunksSent++; } - - int wait = GetSendWaitMilliseconds(); - //logger.LogTrace($"SCTP sender wait period {wait}ms, arwnd {_receiverWindow}, cwnd {_congestionWindow} " + - // $"outstanding bytes {_outstandingBytes}, send queue {_sendQueue.Count}, missing {_missingChunks.Count} " - // + $"unconfirmed {_unconfirmedChunks.Count}."); - - _senderMre.Wait(wait); } - logger.LogDebug("SCTP association data send thread stopped for association {AssociationID}.", _associationID); + var wait = GetSendWaitMilliseconds(); + //logger.LogTrace($"SCTP sender wait period {wait}ms, arwnd {_receiverWindow}, cwnd {_congestionWindow} " + + // $"outstanding bytes {_outstandingBytes}, send queue {_sendQueue.Count}, missing {_missingChunks.Count} " + // + $"unconfirmed {_unconfirmedChunks.Count}."); + + _senderMre.Wait(wait); } - /// - /// Determines how many milliseconds the send thread should wait before the next send attempt. - /// - private int GetSendWaitMilliseconds() + logger.LogSctpDataSendThreadStopped(_associationID); + } + + /// + /// Determines how many milliseconds the send thread should wait before the next send attempt. + /// + private int GetSendWaitMilliseconds() + { + if (_sendQueue.Count > 0 || _missingChunks.Count > 0) { - if (_sendQueue.Count > 0 || _missingChunks.Count > 0) - { - if (_receiverWindow > 0 && _congestionWindow > _outstandingBytes) - { - return _burstPeriodMilliseconds; - } - else - { - return _rtoMinimumMilliseconds; - } - } - else if (_unconfirmedChunks.Count > 0) + if (_receiverWindow > 0 && _congestionWindow > _outstandingBytes) { - return (int)(_hasRoundTripTime ? _rto : _rtoInitialMilliseconds); + return _burstPeriodMilliseconds; } else { - return _rtoInitialMilliseconds; + return _rtoMinimumMilliseconds; } } + else if (_unconfirmedChunks.Count > 0) + { + return (int)(_hasRoundTripTime ? _rto : _rtoInitialMilliseconds); + } + else + { + return _rtoInitialMilliseconds; + } + } - /// - /// Updates the round trip time. - /// See https://datatracker.ietf.org/doc/html/rfc4960#section-6.3.1 - /// - /// The last sent and acknowledged chunk used to calculate the round trip time - private void UpdateRoundTripTime(SctpDataChunk acknowledgedChunk) + /// + /// Updates the round trip time. + /// See https://datatracker.ietf.org/doc/html/rfc4960#section-6.3.1 + /// + /// The last sent and acknowledged chunk used to calculate the round trip time + private void UpdateRoundTripTime(SctpDataChunk acknowledgedChunk) + { + // rfc 4960 6.3.1 C5: RTT measurements MUST NOT be made using packets that were retransmitted + if (acknowledgedChunk.SendCount > 1) { - // rfc 4960 6.3.1 C5: RTT measurements MUST NOT be made using packets that were retransmitted - if (acknowledgedChunk.SendCount > 1) - { - return; - } - - var rttMilliseconds = (DateTime.Now - acknowledgedChunk.LastSentAt).TotalMilliseconds; + return; + } - if (!_hasRoundTripTime) - { - // rfc 4960 6.3.1 C2 - _smoothedRoundTripTime = rttMilliseconds; - _roundTripTimeVariation = rttMilliseconds / 2; - _rto = _smoothedRoundTripTime + 4 * _roundTripTimeVariation; - _hasRoundTripTime = true; - } - else - { - // rfc 4960 6.3.1 C3 - _roundTripTimeVariation = (1 - _rtoBeta) * _roundTripTimeVariation + _rtoBeta * Math.Abs(_smoothedRoundTripTime - rttMilliseconds); - _smoothedRoundTripTime = (1 - _rtoAlpha) * _smoothedRoundTripTime + _rtoAlpha * rttMilliseconds; - _rto = _smoothedRoundTripTime + 4 * _roundTripTimeVariation; - } + var rttMilliseconds = (DateTime.Now - acknowledgedChunk.LastSentAt).TotalMilliseconds; - // rfc 4960 6.3.1 C6-7 - _rto = Math.Min(Math.Max(_rto, _rtoMinimumMilliseconds), _rtoMaximumMilliseconds); + if (!_hasRoundTripTime) + { + // rfc 4960 6.3.1 C2 + _smoothedRoundTripTime = rttMilliseconds; + _roundTripTimeVariation = rttMilliseconds / 2; + _rto = _smoothedRoundTripTime + 4 * _roundTripTimeVariation; + _hasRoundTripTime = true; } - - /// - /// Calculates the receiver window based on the value supplied from a SACK chunk. - /// Note the receive window in the SACK chunk does not take account for in flight - /// DATA chunks hence the need for this calculation. - /// - /// The last receive window value supplied by the remote peer - /// either in the INIT handshake or in a SACK chunk. - /// - /// See https://tools.ietf.org/html/rfc4960#section-6.2.1. - /// - /// The new value to use for the receiver window. - private uint CalculateReceiverWindow(uint advertisedReceiveWindow) + else { - return (advertisedReceiveWindow > _outstandingBytes) ? advertisedReceiveWindow - _outstandingBytes : 0; + // rfc 4960 6.3.1 C3 + _roundTripTimeVariation = (1 - _rtoBeta) * _roundTripTimeVariation + _rtoBeta * Math.Abs(_smoothedRoundTripTime - rttMilliseconds); + _smoothedRoundTripTime = (1 - _rtoAlpha) * _smoothedRoundTripTime + _rtoAlpha * rttMilliseconds; + _rto = _smoothedRoundTripTime + 4 * _roundTripTimeVariation; } - /// - /// Calculates an updated value for the congestion window. - /// - /// The size of last ACK'ed DATA chunk. - /// A congestion window value. - private uint CalculateCongestionWindow(int lastAckDataChunkSize) + // rfc 4960 6.3.1 C6-7 + _rto = Math.Min(Math.Max(_rto, _rtoMinimumMilliseconds), _rtoMaximumMilliseconds); + } + + /// + /// Calculates the receiver window based on the value supplied from a SACK chunk. + /// Note the receive window in the SACK chunk does not take account for in flight + /// DATA chunks hence the need for this calculation. + /// + /// The last receive window value supplied by the remote peer + /// either in the INIT handshake or in a SACK chunk. + /// + /// See https://tools.ietf.org/html/rfc4960#section-6.2.1. + /// + /// The new value to use for the receiver window. + private uint CalculateReceiverWindow(uint advertisedReceiveWindow) + { + return (advertisedReceiveWindow > _outstandingBytes) ? advertisedReceiveWindow - _outstandingBytes : 0; + } + + /// + /// Calculates an updated value for the congestion window. + /// + /// The size of last ACK'ed DATA chunk. + /// A congestion window value. + private uint CalculateCongestionWindow(int lastAckDataChunkSize) + { + if (_congestionWindow < _slowStartThreshold) { - if (_congestionWindow < _slowStartThreshold) - { - // In Slow-Start mode, see RFC4960 7.2.1. + // In Slow-Start mode, see RFC4960 7.2.1. - if (_congestionWindow < _outstandingBytes) - { - // When cwnd is less than or equal to ssthresh, an SCTP endpoint MUST - // use the slow - start algorithm to increase cwnd only if the current - // congestion window is being fully utilized. - uint increasedCwnd = (uint)(_congestionWindow + Math.Min(lastAckDataChunkSize, _defaultMTU)); + if (_congestionWindow < _outstandingBytes) + { + // When cwnd is less than or equal to ssthresh, an SCTP endpoint MUST + // use the slow - start algorithm to increase cwnd only if the current + // congestion window is being fully utilized. + uint increasedCwnd = (uint)(_congestionWindow + Math.Min(lastAckDataChunkSize, _defaultMTU)); - logger.LogTrace("SCTP sender congestion window in slow-start increased from {OldCwnd} to {NewCwnd}.", _congestionWindow, increasedCwnd); + logger.LogSlowStartIncreased(_congestionWindow, increasedCwnd); - return increasedCwnd; - } - else - { - return _congestionWindow; - } + return increasedCwnd; } else { - // In Congestion Avoidance mode, see RFC4960 7.2.2. + return _congestionWindow; + } + } + else + { + // In Congestion Avoidance mode, see RFC4960 7.2.2. - if (_congestionWindow < _outstandingBytes) - { - logger.LogTrace("SCTP sender congestion window in congestion avoidance increased from {OldCwnd} to {NewCwnd}.", _congestionWindow, _congestionWindow + _defaultMTU); + if (_congestionWindow < _outstandingBytes) + { + logger.LogCongestionAvoidanceIncreased(_congestionWindow, _congestionWindow + _defaultMTU); - return _congestionWindow + _defaultMTU; - } - else - { - return _congestionWindow; - } + return _congestionWindow + _defaultMTU; + } + else + { + return _congestionWindow; } } } diff --git a/src/SIPSorcery/net/SCTP/SctpHeader.cs b/src/SIPSorcery/net/SCTP/SctpHeader.cs index 0e08a0e98e..eeb8a6879f 100644 --- a/src/SIPSorcery/net/SCTP/SctpHeader.cs +++ b/src/SIPSorcery/net/SCTP/SctpHeader.cs @@ -18,70 +18,80 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers.Binary; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public partial struct SctpHeader : IByteSerializable { - public struct SctpHeader - { - public const int SCTP_HEADER_LENGTH = 12; + public const int SCTP_HEADER_LENGTH = 12; - /// - /// The SCTP sender's port number. - /// - public ushort SourcePort; + /// + /// The SCTP sender's port number. + /// + public ushort SourcePort; - /// - /// The SCTP port number to which this packet is destined. - /// - public ushort DestinationPort; + /// + /// The SCTP port number to which this packet is destined. + /// + public ushort DestinationPort; - /// - /// The receiver of this packet uses the Verification Tag to validate - /// the sender of this SCTP packet. - /// - public uint VerificationTag; + /// + /// The receiver of this packet uses the Verification Tag to validate + /// the sender of this SCTP packet. + /// + public uint VerificationTag; - /// - /// The CRC32c checksum of this SCTP packet. - /// - public uint Checksum { get; private set; } + /// + /// The CRC32c checksum of this SCTP packet. + /// + public uint Checksum { get; private set; } - /// - /// Serialises the header to a pre-allocated buffer. - /// - /// The buffer to write the SCTP header bytes to. It - /// must have the required space already allocated. - /// The position in the buffer to write the header - /// bytes to. - public void WriteToBuffer(byte[] buffer, int posn) - { - NetConvert.ToBuffer(SourcePort, buffer, posn); - NetConvert.ToBuffer(DestinationPort, buffer, posn + 2); - NetConvert.ToBuffer(VerificationTag, buffer, posn + 4); - } + /// + /// Serialises the header to a pre-allocated buffer. + /// + /// The buffer to write the SCTP header bytes to. It + /// must have the required space already allocated. + /// The position in the buffer to write the header + /// bytes to. + public void WriteToBuffer(byte[] buffer, int posn) + { + _ = WriteBytes(buffer.AsSpan(posn)); + } - /// - /// Parses the an SCTP header from a buffer. - /// - /// The buffer to parse the SCTP header from. - /// The position in the buffer to start parsing the header from. - /// A new SCTPHeaer instance. - public static SctpHeader Parse(byte[] buffer, int posn) - { - if (buffer.Length < SCTP_HEADER_LENGTH) - { - throw new ApplicationException("The buffer did not contain the minimum number of bytes for an SCTP header."); - } + /// + public int GetByteCount() => 8; - SctpHeader header = new SctpHeader(); + /// + public int WriteBytes(Span buffer) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer, SourcePort); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), DestinationPort); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4), VerificationTag); - header.SourcePort = NetConvert.ParseUInt16(buffer, posn); - header.DestinationPort = NetConvert.ParseUInt16(buffer, posn + 2); - header.VerificationTag = NetConvert.ParseUInt32(buffer, posn + 4); - header.Checksum = NetConvert.ParseUInt32(buffer, posn + 8); + return 8; + } - return header; + /// + /// Parses the an SCTP header from a buffer. + /// + /// The buffer to parse the SCTP header from. + /// A new SCTPHeaer instance. + public static SctpHeader Parse(ReadOnlySpan buffer) + { + if (buffer.Length < SCTP_HEADER_LENGTH) + { + throw new SipSorceryException("The buffer did not contain the minimum number of bytes for an SCTP header."); } + + var header = new SctpHeader(); + + header.SourcePort = BinaryPrimitives.ReadUInt16BigEndian(buffer); + header.DestinationPort = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2)); + header.VerificationTag = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(4)); + header.Checksum = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(8)); + + return header; } } diff --git a/src/SIPSorcery/net/SCTP/SctpPacket.cs b/src/SIPSorcery/net/SCTP/SctpPacket.cs index 453e474cbe..bac003a489 100644 --- a/src/SIPSorcery/net/SCTP/SctpPacket.cs +++ b/src/SIPSorcery/net/SCTP/SctpPacket.cs @@ -18,255 +18,290 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; +using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public static class CRC32C { - public class CRC32C - { - private const uint INITIAL_POLYNOMIAL = 0x82f63b78; + private const uint INITIAL_POLYNOMIAL = 0x82f63b78; - private static readonly uint[] _table = new uint[256]; + private static readonly uint[] _table = new uint[256]; - static CRC32C() + static CRC32C() + { + var poly = INITIAL_POLYNOMIAL; + for (uint i = 0; i < 256; i++) { - uint poly = INITIAL_POLYNOMIAL; - for (uint i = 0; i < 256; i++) + var res = i; + for (var k = 0; k < 8; k++) { - uint res = i; - for (int k = 0; k < 8; k++) - { - res = (res & 1) == 1 ? poly ^ (res >> 1) : (res >> 1); - } - _table[i] = res; + res = (res & 1) == 1 ? poly ^ (res >> 1) : (res >> 1); } + _table[i] = res; } + } - public static uint Calculate(byte[] buffer, int offset, int length) + public static uint Calculate(byte[] buffer, int offset, int length) + { + return Calculate(buffer.AsSpan(offset, length)); + } + + public static uint Calculate(ReadOnlySpan buffer) + { + var crc = ~0u; + var length = buffer.Length; + var offset = 0; + while (--length >= 0) { - uint crc = ~0u; - while (--length >= 0) - { - crc = _table[(crc ^ buffer[offset++]) & 0xff] ^ crc >> 8; - } - return crc ^ 0xffffffff; + crc = _table[(crc ^ buffer[offset++]) & 0xff] ^ crc >> 8; } + return crc ^ 0xffffffff; } +} +/// +/// An SCTP packet is composed of a common header and chunks. A chunk +/// contains either control information or user data. +/// +public partial class SctpPacket : IByteSerializable +{ /// - /// An SCTP packet is composed of a common header and chunks. A chunk - /// contains either control information or user data. + /// The position in a serialised SCTP packet buffer that the verification + /// tag field starts. /// - public class SctpPacket - { - /// - /// The position in a serialised SCTP packet buffer that the verification - /// tag field starts. - /// - public const int VERIFICATIONTAG_BUFFER_POSITION = 4; - - /// - /// The position in a serialised SCTP packet buffer that the checksum - /// field starts. - /// - public const int CHECKSUM_BUFFER_POSITION = 8; - - private static ILogger logger = LogFactory.CreateLogger(); - - /// - /// The common header for the SCTP packet. - /// - public SctpHeader Header; - - /// - /// The list of one or recognised chunks after parsing with - /// or chunks that have been manually added for an outgoing SCTP packet. - /// - public List Chunks; - - /// - /// A list of the blobs for chunks that weren't recognised when parsing - /// a received packet. - /// - public List UnrecognisedChunks; - - private SctpPacket() - { } - - /// - /// Creates a new SCTP packet instance. - /// - /// The source port value to place in the packet header. - /// The destination port value to place in the packet header. - /// The verification tag value to place in the packet header. - public SctpPacket( - ushort sourcePort, - ushort destinationPort, - uint verificationTag) - { - Header = new SctpHeader + public const int VERIFICATIONTAG_BUFFER_POSITION = 4; + + /// + /// The position in a serialised SCTP packet buffer that the checksum + /// field starts. + /// + public const int CHECKSUM_BUFFER_POSITION = 8; + + private static ILogger logger = LogFactory.CreateLogger(); + + private SctpHeader header; + + /// + /// The common header for the SCTP packet. + /// + public SctpHeader Header => header; + + /// + /// The list of one or recognised chunks after parsing with + /// or chunks that have been manually added for an outgoing SCTP packet. + /// + public List Chunks { get; } + + /// + /// A list of the blobs for chunks that weren't recognised when parsing + /// a received packet. + /// + public List UnrecognisedChunks { get; } + + /// + /// Creates a new SCTP packet instance. + /// + /// The source port value to place in the packet header. + /// The destination port value to place in the packet header. + /// The verification tag value to place in the packet header. + public SctpPacket( + ushort sourcePort, + ushort destinationPort, + uint verificationTag) + : this( + new SctpHeader { SourcePort = sourcePort, DestinationPort = destinationPort, VerificationTag = verificationTag - }; + }, + new List(), + new List()) + { + } - Chunks = new List(); - UnrecognisedChunks = new List(); - } + private SctpPacket(SctpHeader header, List chunks, List unrecognisedChunks) + { + this.header = header; + Chunks = chunks; + UnrecognisedChunks = unrecognisedChunks; + } - /// - /// Serialises an SCTP packet to a byte array. - /// - /// The byte array containing the serialised SCTP packet. - public byte[] GetBytes() + /// + public int GetByteCount() + { + var total = SctpHeader.SCTP_HEADER_LENGTH; + foreach (var chunk in Chunks) { - int chunksLength = Chunks.Sum(x => x.GetChunkLength(true)); - byte[] buffer = new byte[SctpHeader.SCTP_HEADER_LENGTH + chunksLength]; + total += chunk.GetByteCount(true); + } + return total; + } - Header.WriteToBuffer(buffer, 0); + /// + public int WriteBytes(Span buffer) + { + var size = GetByteCount(); - int writePosn = SctpHeader.SCTP_HEADER_LENGTH; - foreach (var chunk in Chunks) - { - writePosn += chunk.WriteTo(buffer, writePosn); - } + if (buffer.Length < size) + { + throw new ArgumentOutOfRangeException($"The buffer should have at least {size} bytes and had only {buffer.Length}."); + } - NetConvert.ToBuffer(0U, buffer, CHECKSUM_BUFFER_POSITION); - uint checksum = CRC32C.Calculate(buffer, 0, buffer.Length); - NetConvert.ToBuffer(NetConvert.EndianFlip(checksum), buffer, CHECKSUM_BUFFER_POSITION); + WriteBytesCore(buffer.Slice(0, size)); - return buffer; - } + return size; + } + + private void WriteBytesCore(Span buffer) + { + var bytesWritten = Header.WriteBytes(buffer); - /// - /// Adds a new chunk to send with an outgoing packet. - /// - /// The chunk to add. - public void AddChunk(SctpChunk chunk) + var contentBuffer = buffer.Slice(SctpHeader.SCTP_HEADER_LENGTH); + foreach (var chunk in Chunks) { - Chunks.Add(chunk); + bytesWritten = chunk.WriteBytes(contentBuffer); + contentBuffer = contentBuffer.Slice(bytesWritten); } - /// - /// Parses an SCTP packet from a serialised buffer. - /// - /// The buffer holding the serialised packet. - /// The position in the buffer of the packet. - /// The length of the serialised packet in the buffer. - public static SctpPacket Parse(byte[] buffer, int offset, int length) - { - var pkt = new SctpPacket(); - pkt.Header = SctpHeader.Parse(buffer, offset); - (pkt.Chunks, pkt.UnrecognisedChunks) = ParseChunks(buffer, offset, length); + var checksumBuffer = buffer.Slice(CHECKSUM_BUFFER_POSITION, sizeof(uint)); + checksumBuffer.Clear(); + var checksum = CRC32C.Calculate(buffer); + BinaryPrimitives.WriteUInt32LittleEndian(checksumBuffer, checksum); + } - return pkt; - } + /// + /// Adds a new chunk to send with an outgoing packet. + /// + /// The chunk to add. + public void AddChunk(SctpChunk chunk) + { + Chunks.Add(chunk); + } - /// - /// Parses the chunks from a serialised SCTP packet. - /// - /// The buffer holding the serialised packet. - /// The position in the buffer of the packet. - /// The length of the serialised packet in the buffer. - /// The lsit of parsed chunks and a list of unrecognised chunks that were not de-serialised. - private static (List chunks, List unrecognisedChunks) ParseChunks(byte[] buffer, int offset, int length) - { - List chunks = new List(); - List unrecognisedChunks = new List(); + /// + /// Parses an SCTP packet from a serialised buffer. + /// + /// The buffer holding the serialised packet. + public static SctpPacket Parse(ReadOnlySpan buffer) + { + var (chunks, unrecognisedChunks) = ParseChunks(buffer); + var pkt = new SctpPacket(SctpHeader.Parse(buffer), chunks, unrecognisedChunks); - int posn = offset + SctpHeader.SCTP_HEADER_LENGTH; + return pkt; + } - bool stop = false; + /// + /// Parses the chunks from a serialised SCTP packet. + /// + /// The buffer holding the serialised packet. + /// The lsit of parsed chunks and a list of unrecognised chunks that were not de-serialised. + private static (List chunks, List unrecognisedChunks) ParseChunks(ReadOnlySpan buffer) + { + var chunks = new List(); + var unrecognisedChunks = new List(); - while (posn < length) - { - byte chunkType = buffer[posn]; + var posn = SctpHeader.SCTP_HEADER_LENGTH; - if (Enum.IsDefined(typeof(SctpChunkType), chunkType)) - { - var chunk = SctpChunk.Parse(buffer, posn); - chunks.Add(chunk); - } - else - { - switch (SctpChunk.GetUnrecognisedChunkAction(chunkType)) - { - case SctpUnrecognisedChunkActions.Stop: - stop = true; - break; - case SctpUnrecognisedChunkActions.StopAndReport: - stop = true; - unrecognisedChunks.Add(SctpChunk.CopyUnrecognisedChunk(buffer, posn)); - break; - case SctpUnrecognisedChunkActions.Skip: - break; - case SctpUnrecognisedChunkActions.SkipAndReport: - unrecognisedChunks.Add(SctpChunk.CopyUnrecognisedChunk(buffer, posn)); - break; - } - } + var stop = false; + + while (posn < buffer.Length) + { + var chunkSpan = buffer.Slice(posn); + var chunkType = chunkSpan[0]; - if (stop) + if (SctpChunkTypeExtensions.IsDefined((SctpChunkType)chunkType)) + { + var chunk = SctpChunk.Parse(chunkSpan); + chunks.Add(chunk); + } + else + { + switch (SctpChunk.GetUnrecognisedChunkAction(chunkType)) { - logger.LogWarning("SCTP unrecognised chunk type {chunkType} indicated no further chunks should be processed.", chunkType); - break; + case SctpUnrecognisedChunkActions.Stop: + stop = true; + break; + case SctpUnrecognisedChunkActions.StopAndReport: + stop = true; + unrecognisedChunks.Add(SctpChunk.CopyUnrecognisedChunk(chunkSpan)); + break; + case SctpUnrecognisedChunkActions.Skip: + break; + case SctpUnrecognisedChunkActions.SkipAndReport: + unrecognisedChunks.Add(SctpChunk.CopyUnrecognisedChunk(chunkSpan)); + break; } + } - posn += (int)SctpChunk.GetChunkLengthFromHeader(buffer, posn, true); + if (stop) + { + logger.LogSctpUnrecognisedChunkType(chunkType); + break; } - return (chunks, unrecognisedChunks); + posn += (int)SctpChunk.GetChunkLengthFromHeader(chunkSpan, true); } - /// - /// Verifies whether the checksum for a serialised SCTP packet is valid. - /// - /// The buffer holding the serialised packet. - /// The start position in the buffer. - /// The length of the packet in the buffer. - /// True if the checksum was valid, false if not. - public static bool VerifyChecksum(byte[] buffer, int posn, int length) - { - uint origChecksum = NetConvert.ParseUInt32(buffer, posn + CHECKSUM_BUFFER_POSITION); - NetConvert.ToBuffer(0U, buffer, posn + CHECKSUM_BUFFER_POSITION); - uint calcChecksum = CRC32C.Calculate(buffer, posn, length); - - // Put the original checksum back. - NetConvert.ToBuffer(origChecksum, buffer, posn + CHECKSUM_BUFFER_POSITION); - - return origChecksum == NetConvert.EndianFlip(calcChecksum); - } + return (chunks, unrecognisedChunks); + } - /// - /// Gets the verification tag from a serialised SCTP packet. This allows - /// a pre-flight check to be carried out before de-serialising the whole buffer. - /// - /// The buffer holding the serialised packet. - /// The start position in the buffer. - /// The length of the packet in the buffer. - /// The verification tag for the serialised SCTP packet. - public static uint GetVerificationTag(byte[] buffer, int posn, int length) + /// + /// Verifies whether the checksum for a serialised SCTP packet is valid. + /// + /// The buffer holding the serialised packet. + /// True if the checksum was valid, false if not. + public static bool VerifyChecksum(ReadOnlySpan buffer) + { + var tempBuffer = ArrayPool.Shared.Rent(buffer.Length); + try { - return NetConvert.ParseUInt32(buffer, posn + VERIFICATIONTAG_BUFFER_POSITION); + var tempSpan = tempBuffer.AsSpan(0, buffer.Length); + buffer.CopyTo(tempBuffer); + var checksumSpan = tempSpan.Slice(CHECKSUM_BUFFER_POSITION, sizeof(uint)); + var origChecksum = BinaryPrimitives.ReadUInt32LittleEndian(checksumSpan); + checksumSpan.Clear(); + var calcChecksum = CRC32C.Calculate(tempSpan); + + return origChecksum == calcChecksum; } - - /// - /// Performs verification checks on a serialised SCTP packet. - /// - /// The buffer holding the serialised packet. - /// The start position in the buffer. - /// The length of the packet in the buffer. - /// The required verification tag for the serialised - /// packet. This should match the verification tag supplied by the remote party. - /// True if the packet is valid, false if not. - public static bool IsValid(byte[] buffer, int posn, int length, uint requiredTag) + finally { - return GetVerificationTag(buffer, posn, length) == requiredTag && - VerifyChecksum(buffer, posn, length); + ArrayPool.Shared.Return(tempBuffer); } } + + /// + /// Gets the verification tag from a serialised SCTP packet. This allows + /// a pre-flight check to be carried out before de-serialising the whole buffer. + /// + /// The buffer holding the serialised packet. + /// The verification tag for the serialised SCTP packet. + public static uint GetVerificationTag(ReadOnlySpan buffer) + { + return BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(VERIFICATIONTAG_BUFFER_POSITION)); + } + + /// + /// Performs verification checks on a serialised SCTP packet. + /// + /// The buffer holding the serialised packet. + /// The required verification tag for the serialised + /// packet. This should match the verification tag supplied by the remote party. + /// True if the packet is valid, false if not. + public static bool IsValid(ReadOnlySpan buffer, uint requiredTag) + { + return GetVerificationTag(buffer) == requiredTag && + VerifyChecksum(buffer); + } + + public void SetHeaderVerificationTag(uint verificationTag) + { + header.VerificationTag = verificationTag; + } } diff --git a/src/SIPSorcery/net/SCTP/SctpTransport.cs b/src/SIPSorcery/net/SCTP/SctpTransport.cs index 826815de1f..921aa6057e 100644 --- a/src/SIPSorcery/net/SCTP/SctpTransport.cs +++ b/src/SIPSorcery/net/SCTP/SctpTransport.cs @@ -18,499 +18,540 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; +using System.Diagnostics; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Security.Cryptography; -using System.Text; +using System.Text.Json; +using CommunityToolkit.HighPerformance.Buffers; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -using TinyJson; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// The opaque cookie structure that will be sent in response to an SCTP INIT +/// packet. +/// +public struct SctpTransportCookie +{ + public static SctpTransportCookie Empty = new SctpTransportCookie() { _isEmpty = true }; + + public ushort SourcePort { get; set; } + public ushort DestinationPort { get; set; } + public uint RemoteTag { get; set; } + public uint RemoteTSN { get; set; } + public uint RemoteARwnd { get; set; } + public string RemoteEndPoint { get; set; } + public uint Tag { get; set; } + public uint TSN { get; set; } + public uint ARwnd { get; set; } + public string CreatedAt { get; set; } + public int Lifetime { get; set; } + public string HMAC { get; set; } + + private bool _isEmpty; + + public bool IsEmpty() + { + return _isEmpty; + } +} + +/// +/// Contains the common methods that an SCTP transport layer needs to implement. +/// As well as being able to be carried directly in IP packets, SCTP packets can +/// also be wrapped in higher level protocols. +/// +public abstract partial class SctpTransport { + private const int HMAC_KEY_SIZE = 64; + + /// + /// As per https://tools.ietf.org/html/rfc4960#section-15. + /// + public const int DEFAULT_COOKIE_LIFETIME_SECONDS = 60; + + private static ILogger logger = SIPSorcery.LogFactory.CreateLogger(); + /// - /// The opaque cookie structure that will be sent in response to an SCTP INIT - /// packet. + /// Ephemeral secret key to use for generating cookie HMAC's. The purpose of the HMAC is + /// to prevent resource depletion attacks. This does not justify using an external key store. /// - public struct SctpTransportCookie + private static byte[] _hmacKey = new byte[HMAC_KEY_SIZE]; + + /// + /// This property can be used to indicate whether an SCTP transport layer is port agnostic. + /// For example a DTLS transport is likely to only ever create a single SCTP association + /// and the SCTP ports are redundant for matching end points. This allows the checks done + /// on received SCTP packets to be more accepting about the ports used in the SCTP packet + /// header. + /// + /// + /// True if the transport implementation does not rely on the SCTP source and + /// destination port for end point matching. False if it does. + /// + public virtual bool IsPortAgnostic => false; + + public abstract void Send(string? associationID, ReadOnlyMemory buffer, IDisposable? memoryOwner = null); + + static SctpTransport() { - public static SctpTransportCookie Empty = new SctpTransportCookie() { _isEmpty = true }; - - public ushort SourcePort { get; set; } - public ushort DestinationPort { get; set; } - public uint RemoteTag { get; set; } - public uint RemoteTSN { get; set; } - public uint RemoteARwnd { get; set; } - public string RemoteEndPoint { get; set; } - public uint Tag { get; set; } - public uint TSN { get; set; } - public uint ARwnd { get; set; } - public string CreatedAt { get; set; } - public int Lifetime { get; set; } - public string HMAC { get; set; } - - private bool _isEmpty; - - public bool IsEmpty() + Crypto.GetRandomBytes(_hmacKey); + } + + protected void GotInit(SctpPacket initPacket, IPEndPoint? remoteEndPoint) + { + // INIT packets have specific processing rules in order to prevent resource exhaustion. + // See Section 5 of RFC 4960 https://tools.ietf.org/html/rfc4960#section-5 "Association Initialization". + + var initChunk = (SctpInitChunk)FindSingleChunkByType(initPacket.Chunks, SctpChunkType.INIT); + + if (initChunk.InitiateTag == 0 || + initChunk.NumberInboundStreams == 0 || + initChunk.NumberOutboundStreams == 0) { - return _isEmpty; + // If the value of the Initiate Tag in a received INIT chunk is found + // to be 0, the receiver MUST treat it as an error and close the + // association by transmitting an ABORT. (RFC4960 pg. 25) + + // Note: A receiver of an INIT with the OS value set to 0 SHOULD + // abort the association. (RFC4960 pg. 25) + + // Note: A receiver of an INIT with the MIS value of 0 SHOULD abort + // the association. (RFC4960 pg. 26) + + SendError( + true, + initPacket.Header.DestinationPort, + initPacket.Header.SourcePort, + initChunk.InitiateTag, + new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter)); + } + else + { + var initAckPacket = GetInitAck(initPacket, remoteEndPoint); + var size = initAckPacket.GetByteCount(); + var memoryOwner = MemoryPool.Shared.Rent(size); + _ = initAckPacket.WriteBytes(memoryOwner.Memory.Span); + Send(null, memoryOwner.Memory.Slice(0, size), memoryOwner); } } /// - /// Contains the common methods that an SCTP transport layer needs to implement. - /// As well as being able to be carried directly in IP packets, SCTP packets can - /// also be wrapped in higher level protocols. + /// Gets a cookie to send in an INIT ACK chunk. This method + /// is overloadable so that different transports can tailor how the cookie + /// is created. For example the WebRTC SCTP transport only ever uses a + /// single association so the local Tag and TSN properties must be + /// the same rather than random. /// - public abstract class SctpTransport + protected virtual SctpTransportCookie GetInitAckCookie( + ushort sourcePort, + ushort destinationPort, + uint remoteTag, + uint remoteTSN, + uint remoteARwnd, + string remoteEndPoint, + int lifeTimeExtension = 0) { - private const int HMAC_KEY_SIZE = 64; - - /// - /// As per https://tools.ietf.org/html/rfc4960#section-15. - /// - public const int DEFAULT_COOKIE_LIFETIME_SECONDS = 60; - - private static ILogger logger = SIPSorcery.LogFactory.CreateLogger(); - - /// - /// Ephemeral secret key to use for generating cookie HMAC's. The purpose of the HMAC is - /// to prevent resource depletion attacks. This does not justify using an external key store. - /// - private static byte[] _hmacKey = new byte[HMAC_KEY_SIZE]; - - /// - /// This property can be used to indicate whether an SCTP transport layer is port agnostic. - /// For example a DTLS transport is likely to only ever create a single SCTP association - /// and the SCTP ports are redundant for matching end points. This allows the checks done - /// on received SCTP packets to be more accepting about the ports used in the SCTP packet - /// header. - /// - /// - /// True if the transport implementation does not rely on the SCTP source and - /// destination port for end point matching. False if it does. - /// - public virtual bool IsPortAgnostic => false; - - public abstract void Send(string associationID, byte[] buffer, int offset, int length); - - static SctpTransport() + var cookie = new SctpTransportCookie { - Crypto.GetRandomBytes(_hmacKey); - } + SourcePort = sourcePort, + DestinationPort = destinationPort, + RemoteTag = remoteTag, + RemoteTSN = remoteTSN, + RemoteARwnd = remoteARwnd, + RemoteEndPoint = remoteEndPoint, + Tag = Crypto.GetRandomUInt(), + TSN = Crypto.GetRandomUInt(), + ARwnd = SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW, + CreatedAt = DateTime.Now.ToString("o"), + Lifetime = DEFAULT_COOKIE_LIFETIME_SECONDS + lifeTimeExtension, + HMAC = string.Empty + }; + + return cookie; + } - protected void GotInit(SctpPacket initPacket, IPEndPoint remoteEndPoint) + /// + /// Creates the INIT ACK chunk and packet to send as a response to an SCTP + /// packet containing an INIT chunk. + /// + /// The received packet containing the INIT chunk. + /// Optional. The remote IP end point the INIT packet was + /// received on. For transports that don't use an IP transport directly this parameter + /// can be set to null and it will not form part of the COOKIE ECHO checks. + /// An SCTP packet with a single INIT ACK chunk. + protected SctpPacket GetInitAck(SctpPacket initPacket, IPEndPoint? remoteEP) + { + var initChunk = (SctpInitChunk)FindSingleChunkByType(initPacket.Chunks, SctpChunkType.INIT); + + var initAckPacket = new SctpPacket( + initPacket.Header.DestinationPort, + initPacket.Header.SourcePort, + initChunk.InitiateTag); + + var cookie = GetInitAckCookie( + initPacket.Header.DestinationPort, + initPacket.Header.SourcePort, + initChunk.InitiateTag, + initChunk.InitialTSN, + initChunk.ARwnd, + remoteEP is { } ? remoteEP.ToString() : string.Empty, + (int)(initChunk.CookiePreservative / 1000)); + + using var buffer = new ArrayPoolBufferWriter(); + using (var writer = new Utf8JsonWriter(buffer)) { - // INIT packets have specific processing rules in order to prevent resource exhaustion. - // See Section 5 of RFC 4960 https://tools.ietf.org/html/rfc4960#section-5 "Association Initialization". - - SctpInitChunk initChunk = initPacket.Chunks.Single(x => x.KnownType == SctpChunkType.INIT) as SctpInitChunk; - - if (initChunk.InitiateTag == 0 || - initChunk.NumberInboundStreams == 0 || - initChunk.NumberOutboundStreams == 0) - { - // If the value of the Initiate Tag in a received INIT chunk is found - // to be 0, the receiver MUST treat it as an error and close the - // association by transmitting an ABORT. (RFC4960 pg. 25) - - // Note: A receiver of an INIT with the OS value set to 0 SHOULD - // abort the association. (RFC4960 pg. 25) - - // Note: A receiver of an INIT with the MIS value of 0 SHOULD abort - // the association. (RFC4960 pg. 26) - - SendError( - true, - initPacket.Header.DestinationPort, - initPacket.Header.SourcePort, - initChunk.InitiateTag, - new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter)); - } - else - { - var initAckPacket = GetInitAck(initPacket, remoteEndPoint); - var buffer = initAckPacket.GetBytes(); - Send(null, buffer, 0, buffer.Length); - } + JsonSerializer.Serialize(writer, cookie, SipSorceryJsonSerializerContext.Default.SctpTransportCookie); } - /// - /// Gets a cookie to send in an INIT ACK chunk. This method - /// is overloadable so that different transports can tailor how the cookie - /// is created. For example the WebRTC SCTP transport only ever uses a - /// single association so the local Tag and TSN properties must be - /// the same rather than random. - /// - protected virtual SctpTransportCookie GetInitAckCookie( - ushort sourcePort, - ushort destinationPort, - uint remoteTag, - uint remoteTSN, - uint remoteARwnd, - string remoteEndPoint, - int lifeTimeExtension = 0) + using (var hmac = new HMACSHA256(_hmacKey)) { - var cookie = new SctpTransportCookie - { - SourcePort = sourcePort, - DestinationPort = destinationPort, - RemoteTag = remoteTag, - RemoteTSN = remoteTSN, - RemoteARwnd = remoteARwnd, - RemoteEndPoint = remoteEndPoint, - Tag = Crypto.GetRandomUInt(), - TSN = Crypto.GetRandomUInt(), - ARwnd = SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW, - CreatedAt = DateTime.Now.ToString("o"), - Lifetime = DEFAULT_COOKIE_LIFETIME_SECONDS + lifeTimeExtension, - HMAC = string.Empty - }; - - return cookie; + var result = hmac.ComputeHash(buffer.WrittenMemory); + cookie.HMAC = result.HexStr(); } - /// - /// Creates the INIT ACK chunk and packet to send as a response to an SCTP - /// packet containing an INIT chunk. - /// - /// The received packet containing the INIT chunk. - /// Optional. The remote IP end point the INIT packet was - /// received on. For transports that don't use an IP transport directly this parameter - /// can be set to null and it will not form part of the COOKIE ECHO checks. - /// An SCTP packet with a single INIT ACK chunk. - protected SctpPacket GetInitAck(SctpPacket initPacket, IPEndPoint remoteEP) + buffer.Clear(); + using (var writer = new Utf8JsonWriter(buffer)) { - SctpInitChunk initChunk = initPacket.Chunks.Single(x => x.KnownType == SctpChunkType.INIT) as SctpInitChunk; - - SctpPacket initAckPacket = new SctpPacket( - initPacket.Header.DestinationPort, - initPacket.Header.SourcePort, - initChunk.InitiateTag); - - var cookie = GetInitAckCookie( - initPacket.Header.DestinationPort, - initPacket.Header.SourcePort, - initChunk.InitiateTag, - initChunk.InitialTSN, - initChunk.ARwnd, - remoteEP != null ? remoteEP.ToString() : string.Empty, - (int)(initChunk.CookiePreservative / 1000)); - - var json = cookie.ToJson(); - var jsonBuffer = Encoding.UTF8.GetBytes(json); - - using (HMACSHA256 hmac = new HMACSHA256(_hmacKey)) - { - var result = hmac.ComputeHash(jsonBuffer); - cookie.HMAC = result.HexStr(); - } + JsonSerializer.Serialize(writer, cookie, SipSorceryJsonSerializerContext.Default.SctpTransportCookie); + } - var jsonWithHMAC = cookie.ToJson(); - var jsonBufferWithHMAC = Encoding.UTF8.GetBytes(jsonWithHMAC); + var initAckChunk = new SctpInitChunk( + SctpChunkType.INIT_ACK, + cookie.Tag, + cookie.TSN, + cookie.ARwnd, + SctpAssociation.DEFAULT_NUMBER_OUTBOUND_STREAMS, + SctpAssociation.DEFAULT_NUMBER_INBOUND_STREAMS); + initAckChunk.StateCookie = buffer.WrittenMemory.ToArray(); + initAckChunk.UnrecognizedPeerParameters = initChunk.UnrecognizedPeerParameters; - SctpInitChunk initAckChunk = new SctpInitChunk( - SctpChunkType.INIT_ACK, - cookie.Tag, - cookie.TSN, - cookie.ARwnd, - SctpAssociation.DEFAULT_NUMBER_OUTBOUND_STREAMS, - SctpAssociation.DEFAULT_NUMBER_INBOUND_STREAMS); - initAckChunk.StateCookie = jsonBufferWithHMAC; - initAckChunk.UnrecognizedPeerParameters = initChunk.UnrecognizedPeerParameters; + initAckPacket.AddChunk(initAckChunk); - initAckPacket.AddChunk(initAckChunk); + return initAckPacket; + } - return initAckPacket; - } + /// + /// Attempts to retrieve the cookie that should have been set by this peer from a COOKIE ECHO + /// chunk. This is the step in the handshake that a new SCTP association will be created + /// for a remote party. Providing the state cookie is valid create a new association. + /// + /// The packet containing the COOKIE ECHO chunk received from the remote party. + /// If the state cookie in the chunk is valid a new SCTP association will be returned. IF + /// it's not valid an empty cookie will be returned and an error response gets sent to the peer. + protected SctpTransportCookie GetCookie(SctpPacket sctpPacket) + { + var cookieEcho = FindSingleChunkByType(sctpPacket.Chunks, SctpChunkType.COOKIE_ECHO); + var cookieBuffer = cookieEcho.ChunkValue; + Debug.Assert(cookieBuffer is { }); + var cookie = JsonSerializer.Deserialize(cookieBuffer, SipSorceryJsonSerializerContext.Default.SctpTransportCookie); - /// - /// Attempts to retrieve the cookie that should have been set by this peer from a COOKIE ECHO - /// chunk. This is the step in the handshake that a new SCTP association will be created - /// for a remote party. Providing the state cookie is valid create a new association. - /// - /// The packet containing the COOKIE ECHO chunk received from the remote party. - /// If the state cookie in the chunk is valid a new SCTP association will be returned. IF - /// it's not valid an empty cookie will be returned and an error response gets sent to the peer. - protected SctpTransportCookie GetCookie(SctpPacket sctpPacket) + logger.LogSctpCookie(cookie); + + var calculatedHMAC = GetCookieHMAC(cookieBuffer); + if (calculatedHMAC != cookie.HMAC) + { + logger.LogSctpCookieEchoInvalidHMAC(calculatedHMAC, cookie.HMAC); + SendError( + true, + sctpPacket.Header.DestinationPort, + sctpPacket.Header.SourcePort, + 0, + new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter)); + return SctpTransportCookie.Empty; + } + else if (DateTime.Now.Subtract(DateTime.Parse(cookie.CreatedAt)).TotalSeconds > cookie.Lifetime) { - var cookieEcho = sctpPacket.Chunks.Single(x => x.KnownType == SctpChunkType.COOKIE_ECHO); - var cookieBuffer = cookieEcho.ChunkValue; - var cookie = JSONParser.FromJson(Encoding.UTF8.GetString(cookieBuffer)); + logger.LogSctpCookieEchoStale(cookie.CreatedAt, DateTime.Now.ToString("o"), cookie.Lifetime); + var diff = DateTime.Now.Subtract(DateTime.Parse(cookie.CreatedAt).AddSeconds(cookie.Lifetime)); + SendError( + true, + sctpPacket.Header.DestinationPort, + sctpPacket.Header.SourcePort, + 0, + new SctpErrorStaleCookieError { MeasureOfStaleness = (uint)(diff.TotalMilliseconds * 1000) }); + return SctpTransportCookie.Empty; + } + else + { + return cookie; + } + } - logger.LogDebug("Cookie: {Cookie}", cookie.ToJson()); + /// + /// Checks whether the state cookie that is supplied in a COOKIE ECHO chunk is valid for + /// this SCTP transport. + /// + /// The buffer holding the state cookie. + /// True if the cookie is determined as valid, false if not. + protected string GetCookieHMAC(byte[] buffer) + { + var cookie = JsonSerializer.Deserialize(buffer, SipSorceryJsonSerializerContext.Default.SctpTransportCookie); + cookie.HMAC = string.Empty; - string calculatedHMAC = GetCookieHMAC(cookieBuffer); - if (calculatedHMAC != cookie.HMAC) - { - logger.LogWarning("SCTP COOKIE ECHO chunk had an invalid HMAC, calculated {calculatedHMAC}, cookie {cookieHMAC}.", calculatedHMAC, cookie.HMAC); - SendError( - true, - sctpPacket.Header.DestinationPort, - sctpPacket.Header.SourcePort, - 0, - new SctpCauseOnlyError(SctpErrorCauseCode.InvalidMandatoryParameter)); - return SctpTransportCookie.Empty; - } - else if (DateTime.Now.Subtract(DateTime.Parse(cookie.CreatedAt)).TotalSeconds > cookie.Lifetime) - { - logger.LogWarning("SCTP COOKIE ECHO chunk was stale, created at {CreatedAt}, now {Now}, lifetime {Lifetime}s.", cookie.CreatedAt, DateTime.Now.ToString("o"), cookie.Lifetime); - var diff = DateTime.Now.Subtract(DateTime.Parse(cookie.CreatedAt).AddSeconds(cookie.Lifetime)); - SendError( - true, - sctpPacket.Header.DestinationPort, - sctpPacket.Header.SourcePort, - 0, - new SctpErrorStaleCookieError { MeasureOfStaleness = (uint)(diff.TotalMilliseconds * 1000) }); - return SctpTransportCookie.Empty; - } - else - { - return cookie; - } + using var tempBuffer = new ArrayPoolBufferWriter(); + using (var writer = new Utf8JsonWriter(tempBuffer)) + { + JsonSerializer.Serialize(writer, cookie, SipSorceryJsonSerializerContext.Default.SctpTransportCookie); } - /// - /// Checks whether the state cookie that is supplied in a COOKIE ECHO chunk is valid for - /// this SCTP transport. - /// - /// The buffer holding the state cookie. - /// True if the cookie is determined as valid, false if not. - protected string GetCookieHMAC(byte[] buffer) + using (var hmac = new HMACSHA256(_hmacKey)) { - var cookie = JSONParser.FromJson(Encoding.UTF8.GetString(buffer)); - string hmacCalculated = null; - cookie.HMAC = string.Empty; - - byte[] cookiePreImage = Encoding.UTF8.GetBytes(cookie.ToJson()); - - using (HMACSHA256 hmac = new HMACSHA256(_hmacKey)) - { - var result = hmac.ComputeHash(cookiePreImage); - hmacCalculated = result.HexStr(); - } + var result = hmac.ComputeHash(tempBuffer.WrittenMemory); + var hmacCalculated = result.HexStr(); return hmacCalculated; } + } - /// - /// Send an SCTP packet with one of the error type chunks (ABORT or ERROR) to the remote peer. - /// - /// Set to true to use an ABORT chunk otherwise an ERROR chunk will be used. - /// The SCTP destination port. - /// The SCTP source port. - /// If available the initial tag for the remote peer. - /// The error to send. - private void SendError( - bool isAbort, - ushort destinationPort, - ushort sourcePort, - uint initiateTag, - ISctpErrorCause error) - { - SctpPacket errorPacket = new SctpPacket( - destinationPort, - sourcePort, - initiateTag); + /// + /// Send an SCTP packet with one of the error type chunks (ABORT or ERROR) to the remote peer. + /// + /// Set to true to use an ABORT chunk otherwise an ERROR chunk will be used. + /// The SCTP destination port. + /// The SCTP source port. + /// If available the initial tag for the remote peer. + /// The error to send. + private void SendError( + bool isAbort, + ushort destinationPort, + ushort sourcePort, + uint initiateTag, + ISctpErrorCause error) + { + var errorPacket = new SctpPacket( + destinationPort, + sourcePort, + initiateTag); + + var errorChunk = isAbort ? new SctpAbortChunk(true) : new SctpErrorChunk(); + errorChunk.AddErrorCause(error); + errorPacket.AddChunk(errorChunk); + + var size = errorPacket.GetByteCount(); + var memoryOwner = MemoryPool.Shared.Rent(size); + _ = errorPacket.WriteBytes(memoryOwner.Memory.Span); + Send(null, memoryOwner.Memory.Slice(0, size), memoryOwner); + } - SctpErrorChunk errorChunk = isAbort ? new SctpAbortChunk(true) : new SctpErrorChunk(); - errorChunk.AddErrorCause(error); - errorPacket.AddChunk(errorChunk); + /// + /// This method allows SCTP to initialise its internal data structures + /// and allocate necessary resources for setting up its operation + /// environment. + /// + /// SCTP port number, if the application wants it to be specified. + /// The local SCTP instance name. + public string Initialize(ushort localPort) + { + return "local SCTP instance name"; + } - var buffer = errorPacket.GetBytes(); - Send(null, buffer, 0, buffer.Length); - } + /// + /// Initiates an association to a specific peer end point + /// + /// + /// + /// An association ID, which is a local handle to the SCTP association. + public string Associate(IPAddress destination, int streamCount) + { + return "association ID"; + } - /// - /// This method allows SCTP to initialise its internal data structures - /// and allocate necessary resources for setting up its operation - /// environment. - /// - /// SCTP port number, if the application wants it to be specified. - /// The local SCTP instance name. - public string Initialize(ushort localPort) - { - return "local SCTP instance name"; - } + /// + /// Gracefully closes an association. Any locally queued user data will + /// be delivered to the peer.The association will be terminated only + /// after the peer acknowledges all the SCTP packets sent. + /// + /// Local handle to the SCTP association. + public void Shutdown(string associationID) + { - /// - /// Initiates an association to a specific peer end point - /// - /// - /// - /// An association ID, which is a local handle to the SCTP association. - public string Associate(IPAddress destination, int streamCount) - { - return "association ID"; - } + } - /// - /// Gracefully closes an association. Any locally queued user data will - /// be delivered to the peer.The association will be terminated only - /// after the peer acknowledges all the SCTP packets sent. - /// - /// Local handle to the SCTP association. - public void Shutdown(string associationID) - { + /// + /// Ungracefully closes an association. Any locally queued user data + /// will be discarded, and an ABORT chunk is sent to the peer. + /// + /// Local handle to the SCTP association. + public void Abort(string associationID) + { - } + } - /// - /// Ungracefully closes an association. Any locally queued user data - /// will be discarded, and an ABORT chunk is sent to the peer. - /// - /// Local handle to the SCTP association. - public void Abort(string associationID) - { + /// + /// This is the main method to send user data via SCTP. + /// + /// Local handle to the SCTP association. + /// The buffer holding the data to send. + /// Optional. A 32-bit integer that will be carried in the + /// sending failure notification to the application if the transportation of + /// this user message fails. + /// Optional. To indicate which stream to send the data on. If not + /// specified, stream 0 will be used. + /// Optional. specifies the life time of the user data. The user + /// data will not be sent by SCTP after the life time expires.This + /// parameter can be used to avoid efforts to transmit stale user + /// messages. + /// + public string Send(string associationID, ReadOnlyMemory buffer, IDisposable? memoryOwner, int contextID, int streamID, int lifeTime) + { + memoryOwner?.Dispose(); + return "ok"; + } - } + /// + /// Instructs the local SCTP to use the specified destination transport + /// address as the primary path for sending packets. + /// + /// + /// + public string SetPrimary(string associationID) + { + // Note: Seems like this will be a noop for SCTP encapsulated in UDP. + return "ok"; + } - /// - /// This is the main method to send user data via SCTP. - /// - /// Local handle to the SCTP association. - /// The buffer holding the data to send. - /// The number of bytes from the buffer to send. - /// Optional. A 32-bit integer that will be carried in the - /// sending failure notification to the application if the transportation of - /// this user message fails. - /// Optional. To indicate which stream to send the data on. If not - /// specified, stream 0 will be used. - /// Optional. specifies the life time of the user data. The user - /// data will not be sent by SCTP after the life time expires.This - /// parameter can be used to avoid efforts to transmit stale user - /// messages. - /// - public string Send(string associationID, byte[] buffer, int length, int contextID, int streamID, int lifeTime) - { - return "ok"; - } + /// + /// This method shall read the first user message in the SCTP in-queue + /// into the buffer specified by the application, if there is one available.The + /// size of the message read, in bytes, will be returned. + /// + /// Local handle to the SCTP association. + /// The buffer to place the received data into. + /// The maximum size of the data to receive. + /// Optional. If specified indicates which stream to + /// receive the data on. + /// + public int Receive(string associationID, byte[] buffer, int length, int streamID) + { + return 0; + } - /// - /// Instructs the local SCTP to use the specified destination transport - /// address as the primary path for sending packets. - /// - /// - /// - public string SetPrimary(string associationID) - { - // Note: Seems like this will be a noop for SCTP encapsulated in UDP. - return "ok"; - } + /// + /// Returns the current status of the association. + /// + /// Local handle to the SCTP association. + /// + public SctpStatus Status(string associationID) + { + return new SctpStatus(); + } - /// - /// This method shall read the first user message in the SCTP in-queue - /// into the buffer specified by the application, if there is one available.The - /// size of the message read, in bytes, will be returned. - /// - /// Local handle to the SCTP association. - /// The buffer to place the received data into. - /// The maximum size of the data to receive. - /// Optional. If specified indicates which stream to - /// receive the data on. - /// - public int Receive(string associationID, byte[] buffer, int length, int streamID) - { - return 0; - } + /// + /// Instructs the local endpoint to enable or disable heartbeat on the + /// specified destination transport address. + /// + /// Local handle to the SCTP association. + /// Indicates the frequency of the heartbeat if + /// this is to enable heartbeat on a destination transport address. + /// This value is added to the RTO of the destination transport + /// address.This value, if present, affects all destinations. + /// + public string ChangeHeartbeat(string associationID, int interval) + { + return "ok"; + } - /// - /// Returns the current status of the association. - /// - /// Local handle to the SCTP association. - /// - public SctpStatus Status(string associationID) - { - return new SctpStatus(); - } + /// + /// Instructs the local endpoint to perform a HeartBeat on the specified + /// destination transport address of the given association. + /// + /// Local handle to the SCTP association. + /// Indicates whether the transmission of the HEARTBEAT + /// chunk to the destination address is successful. + public string RequestHeartbeat(string associationID) + { + return "ok"; + } - /// - /// Instructs the local endpoint to enable or disable heartbeat on the - /// specified destination transport address. - /// - /// Local handle to the SCTP association. - /// Indicates the frequency of the heartbeat if - /// this is to enable heartbeat on a destination transport address. - /// This value is added to the RTO of the destination transport - /// address.This value, if present, affects all destinations. - /// - public string ChangeHeartbeat(string associationID, int interval) - { - return "ok"; - } + /// + /// Instructs the local SCTP to report the current Smoothed Round Trip Time (SRTT) + /// measurement on the specified destination transport address of the given + /// association. + /// + /// Local handle to the SCTP association. + /// An integer containing the most recent SRTT in milliseconds. + public int GetSrttReport(string associationID) + { + return 0; + } - /// - /// Instructs the local endpoint to perform a HeartBeat on the specified - /// destination transport address of the given association. - /// - /// Local handle to the SCTP association. - /// Indicates whether the transmission of the HEARTBEAT - /// chunk to the destination address is successful. - public string RequestHeartbeat(string associationID) - { - return "ok"; - } + /// + /// This method allows the local SCTP to customise the protocol + /// parameters. + /// + /// Local handle to the SCTP association. + /// The specific names and values of the + /// protocol parameters that the SCTP user wishes to customise. + public void SetProtocolParameters(string associationID, object protocolParameters) + { - /// - /// Instructs the local SCTP to report the current Smoothed Round Trip Time (SRTT) - /// measurement on the specified destination transport address of the given - /// association. - /// - /// Local handle to the SCTP association. - /// An integer containing the most recent SRTT in milliseconds. - public int GetSrttReport(string associationID) - { - return 0; - } + } - /// - /// This method allows the local SCTP to customise the protocol - /// parameters. - /// - /// Local handle to the SCTP association. - /// The specific names and values of the - /// protocol parameters that the SCTP user wishes to customise. - public void SetProtocolParameters(string associationID, object protocolParameters) - { + /// + /// ?? + /// + /// The identification passed to the application in the + /// failure notification. + /// The buffer to store the received message. + /// The maximum size of the data to receive. + /// This is a return value that is set to indicate which + /// stream the data was sent to. + public void ReceiveUnsent(string dataRetrievalID, byte[] buffer, int length, int streamID) + { - } + } - /// - /// ?? - /// - /// The identification passed to the application in the - /// failure notification. - /// The buffer to store the received message. - /// The maximum size of the data to receive. - /// This is a return value that is set to indicate which - /// stream the data was sent to. - public void ReceiveUnsent(string dataRetrievalID, byte[] buffer, int length, int streamID) - { + /// + /// ?? + /// + /// The identification passed to the application in the + /// failure notification. + /// The buffer to store the received message. + /// The maximum size of the data to receive. + /// This is a return value that is set to indicate which + /// stream the data was sent to. + public void ReceiveUnacknowledged(string dataRetrievalID, byte[] buffer, int length, int streamID) + { - } + } - /// - /// ?? - /// - /// The identification passed to the application in the - /// failure notification. - /// The buffer to store the received message. - /// The maximum size of the data to receive. - /// This is a return value that is set to indicate which - /// stream the data was sent to. - public void ReceiveUnacknowledged(string dataRetrievalID, byte[] buffer, int length, int streamID) - { + /// + /// Release the resources for the specified SCTP instance. + /// + /// + public void Destroy(string instanceName) + { - } + } - /// - /// Release the resources for the specified SCTP instance. - /// - /// - public void Destroy(string instanceName) + private static SctpChunk FindSingleChunkByType(List chunks, SctpChunkType type) + { + SctpChunk? chunk = null; + var found = false; + foreach (var ch in chunks) { + if (ch.KnownType == type) + { + if (found) + { + // More than one match: throw immediately. + throw new InvalidOperationException("Sequence contains more than one matching SctpChunk"); + } + + chunk = ch; + found = true; + } + } + if (found) + { + return chunk!; } + + throw new InvalidOperationException("Sequence contains no matching SctpChunk"); } } diff --git a/src/SIPSorcery/net/SCTP/SctpUdpTransport.cs b/src/SIPSorcery/net/SCTP/SctpUdpTransport.cs index 23d0f930bd..ef5f539006 100644 --- a/src/SIPSorcery/net/SCTP/SctpUdpTransport.cs +++ b/src/SIPSorcery/net/SCTP/SctpUdpTransport.cs @@ -19,163 +19,186 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Linq; using System.Net; -using System.Net.Sockets; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// Represents an SCTP transport that encapsulates SCTP packet in UDP. +/// +public class SctpUdpTransport : SctpTransport { - /// - /// Represents an SCTP transport that encapsulates SCTP packet in UDP. - /// - public class SctpUdpTransport : SctpTransport - { - public const ushort DEFAULT_UDP_MTU = 1300; + public const ushort DEFAULT_UDP_MTU = 1300; - private static ILogger logger = SIPSorcery.LogFactory.CreateLogger(); + private static ILogger logger = SIPSorcery.LogFactory.CreateLogger(); - /// - /// The UDP encapsulation socket if the instance is managing its own transport layer. - /// For WebRTC data channels the socket will not be managed externally. - /// - private Socket _udpEncapSocket; + /// + /// The UDP encapsulation socket if the instance is managing its own transport layer. + /// For WebRTC data channels the socket will not be managed externally. + /// + private SocketUdpConnection _udpReceiver; - private ConcurrentDictionary _associations = new ConcurrentDictionary(); + private ConcurrentDictionary _associations = new ConcurrentDictionary(); - /// - /// Creates a new UDP transport capable of encapsulating SCTP packets. - /// - /// The port to bind to for the UDP encapsulation socket. - /// Optional. The portRange which should be used to get a listening port. - public SctpUdpTransport(int udpEncapPort = 0, PortRange portRange = null) - { - NetServices.CreateRtpSocket(false, IPAddress.IPv6Any, udpEncapPort, portRange, out _udpEncapSocket, out _); - UdpReceiver udpReceiver = new UdpReceiver(_udpEncapSocket); - udpReceiver.OnPacketReceived += OnEncapsulationSocketPacketReceived; - udpReceiver.OnClosed += OnEncapsulationSocketClosed; - udpReceiver.BeginReceiveFrom(); - } + /// + /// Creates a new UDP transport capable of encapsulating SCTP packets. + /// + /// The port to bind to for the UDP encapsulation socket. + /// Optional. The portRange which should be used to get a listening port. + public SctpUdpTransport(int udpEncapPort = 0, PortRange? portRange = null) + { + NetServices.CreateRtpSocket(false, IPAddress.IPv6Any, udpEncapPort, portRange, out var udpEncapSocket, out _); + Debug.Assert(udpEncapSocket is { }); + _udpReceiver = new SocketUdpConnection(udpEncapSocket); + _udpReceiver.OnPacketReceived += OnEncapsulationSocketPacketReceived; + _udpReceiver.OnClosed += OnEncapsulationSocketClosed; + _udpReceiver.BeginReceiveFrom(); + } - /// - /// Event handler for a packet receive on the UDP encapsulation socket. - /// - /// The UDP receiver that received the packet. - /// The local port the packet was received on. - /// The remote end point the packet was received from. - /// A buffer containing the packet. - private void OnEncapsulationSocketPacketReceived(UdpReceiver receiver, int localPort, IPEndPoint remoteEndPoint, byte[] packet) + /// + /// Event handler for a packet receive on the UDP encapsulation socket. + /// + /// The UDP receiver that received the packet. + /// The local port the packet was received on. + /// The remote end point the packet was received from. + /// A buffer containing the packet. + private void OnEncapsulationSocketPacketReceived(SocketConnection receiver, int localPort, IPEndPoint? remoteEndPoint, ReadOnlyMemory packet) + { + try { - try + if (!SctpPacket.VerifyChecksum(packet.Span)) { - if (!SctpPacket.VerifyChecksum(packet, 0, packet.Length)) + logger.LogSctpPacketDroppedInvalidChecksum(remoteEndPoint); + } + else + { + var sctpPacket = SctpPacket.Parse(packet.Span); + + // Process packet. + if (sctpPacket.Header.VerificationTag == 0) { - logger.LogWarning("SCTP packet from UDP {RemoteEndPoint} dropped due to invalid checksum.", remoteEndPoint); + GotInit(sctpPacket, remoteEndPoint); } - else + else if (HasCookieEcho(sctpPacket)) { - var sctpPacket = SctpPacket.Parse(packet, 0, packet.Length); + // The COOKIE ECHO chunk is the 3rd step in the SCTP handshake when the remote party has + // requested a new association be created. + var cookie = base.GetCookie(sctpPacket); - // Process packet. - if (sctpPacket.Header.VerificationTag == 0) + if (cookie.IsEmpty()) { - GotInit(sctpPacket, remoteEndPoint); + logger.LogSctpErrorAcquiringHandshakeCookie(); } - else if (sctpPacket.Chunks.Any(x => x.KnownType == SctpChunkType.COOKIE_ECHO)) + else { - // The COOKIE ECHO chunk is the 3rd step in the SCTP handshake when the remote party has - // requested a new association be created. - var cookie = base.GetCookie(sctpPacket); + Debug.Assert(remoteEndPoint is { }); + logger.LogSctpCreatingNewAssociation(remoteEndPoint); + + var association = new SctpAssociation(this, cookie, localPort); - if (cookie.IsEmpty()) + if (_associations.TryAdd(association.ID, association)) { - logger.LogWarning("SCTP error acquiring handshake cookie from COOKIE ECHO chunk."); + if (sctpPacket.Chunks.Count > 1) + { + // There could be DATA chunks after the COOKIE ECHO chunk. + association.OnPacketReceived(sctpPacket); + } } else { - logger.LogDebug("SCTP creating new association for {RemoteEndPoint}.", remoteEndPoint); - - var association = new SctpAssociation(this, cookie, localPort); - - if (_associations.TryAdd(association.ID, association)) - { - if (sctpPacket.Chunks.Count > 1) - { - // There could be DATA chunks after the COOKIE ECHO chunk. - association.OnPacketReceived(sctpPacket); - } - } - else - { - logger.LogError("SCTP failed to add new association to dictionary."); - } + logger.LogSctpFailedToAddNewAssociation(); } } - else + } + else + { + // TODO: Lookup the existing association for the packet. + // TODO: ConcurrentDictionary is not the best performing here. Consider a different structure. + Debug.Assert(_associations.Count > 0); + _associations.First().Value.OnPacketReceived(sctpPacket); + } + + static bool HasCookieEcho(SctpPacket sctpPacket) + { + var hasCookieEcho = false; + foreach (var ch in sctpPacket.Chunks) { - // TODO: Lookup the existing association for the packet. - _associations.Values.First().OnPacketReceived(sctpPacket); + if (ch.KnownType == SctpChunkType.COOKIE_ECHO) + { + hasCookieEcho = true; + break; + } } + + return hasCookieEcho; } } - catch (Exception excp) - { - logger.LogError(excp, "Exception SctpTransport.OnEncapsulationSocketPacketReceived. {ErrorMessage}", excp.Message); - } } - - /// - /// Event handler for the UDP encapsulation socket closing. - /// - /// - private void OnEncapsulationSocketClosed(string reason) + catch (Exception excp) { - logger.LogInformation("SCTP transport encapsulation receiver closed with reason: {Reason}.", reason); + logger.LogSctpPacketReceivedException($"Exception SctpTransport.OnEncapsulationSocketPacketReceived. {excp.Message}", excp); } + } - public override void Send(string associationID, byte[] buffer, int offset, int length) + /// + /// Event handler for the UDP encapsulation socket closing. + /// + /// + private void OnEncapsulationSocketClosed(string? reason) + { + logger.LogSctpTransportEncapsulationReceiverClosed(reason); + } + + public override void Send(string? associationID, ReadOnlyMemory buffer, IDisposable? memoryOwner = null) + { + using (memoryOwner) { + Debug.Assert(!string.IsNullOrEmpty(associationID)); if (_associations.TryGetValue(associationID, out var assoc)) { - _udpEncapSocket.SendTo(buffer, offset, length, SocketFlags.None, assoc.Destination); + Debug.Assert(assoc.Destination is { }); + _udpReceiver.SendTo(assoc.Destination, buffer, null); } } + } + + /// + /// Requests a new association be created. + /// + /// The UDP endpoint to attempt to create the association with. + /// The SCTP source port. + /// The SCTP destination port. + /// An SCTP association. + public SctpAssociation? Associate( + IPEndPoint destination, + ushort sourcePort, + ushort destinationPort, + ushort numberOutboundStreams = SctpAssociation.DEFAULT_NUMBER_OUTBOUND_STREAMS, + ushort numberInboundStreams = SctpAssociation.DEFAULT_NUMBER_INBOUND_STREAMS) + { + var association = new SctpAssociation( + this, + destination, + sourcePort, + destinationPort, + DEFAULT_UDP_MTU, + numberOutboundStreams, + numberInboundStreams); - /// - /// Requests a new association be created. - /// - /// The UDP endpoint to attempt to create the association with. - /// The SCTP source port. - /// The SCTP destination port. - /// An SCTP association. - public SctpAssociation Associate( - IPEndPoint destination, - ushort sourcePort, - ushort destinationPort, - ushort numberOutboundStreams = SctpAssociation.DEFAULT_NUMBER_OUTBOUND_STREAMS, - ushort numberInboundStreams = SctpAssociation.DEFAULT_NUMBER_INBOUND_STREAMS) + if (_associations.TryAdd(association.ID, association)) { - var association = new SctpAssociation( - this, - destination, - sourcePort, - destinationPort, - DEFAULT_UDP_MTU, - numberOutboundStreams, - numberInboundStreams); - - if (_associations.TryAdd(association.ID, association)) - { - association.Init(); - return association; - } - else - { - logger.LogWarning("SCTP transport failed to add association."); - association.Shutdown(); - return null; - } + association.Init(); + return association; + } + else + { + logger.LogSctpTransportFailedToAddAssociation(); + association.Shutdown(); + return null; } } } diff --git a/src/SIPSorcery/net/SDP/NetSdpLoggingExtensions.cs b/src/SIPSorcery/net/SDP/NetSdpLoggingExtensions.cs new file mode 100644 index 0000000000..d29c5fcb2a --- /dev/null +++ b/src/SIPSorcery/net/SDP/NetSdpLoggingExtensions.cs @@ -0,0 +1,261 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace SIPSorcery.Net; + +internal static partial class NetSdpLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "SdpInvalidSdpLineFormat", + Level = LogLevel.Warning, + Message = "The SDP message had an invalid SDP line format for 'o=': {sdpLineTrimmed}")] + public static partial void LogSdpInvalidSdpLineFormat( + this ILogger logger, + string sdpLineTrimmed); + + [LoggerMessage( + EventId = 0, + EventName = "SdpUnrecognisedMediaFormat", + Level = LogLevel.Warning, + Message = "Excluding unrecognised well known media format ID {formatId}.")] + public static partial void LogSdpUnrecognisedMediaFormat( + this ILogger logger, + int formatId); + + [LoggerMessage( + EventId = 0, + EventName = "SdpMediaFormatAttribute", + Level = LogLevel.Warning, + Message = "Non-numeric audio/video media format attribute in SDP: {sdpLine}")] + public static partial void LogSdpMediaFormatAttribute( + this ILogger logger, + string sdpLine); + + [LoggerMessage( + EventId = 0, + EventName = "SdpInvalidMediaFormatParamAttribute", + Level = LogLevel.Warning, + Message = "Invalid media format parameter attribute in SDP: {sdpLine}")] + public static partial void LogSdpInvalidMediaFormatParamAttribute( + this ILogger logger, + string sdpLine); + + [LoggerMessage( + EventId = 0, + EventName = "SdpNoActiveMediaAnnouncement", + Level = LogLevel.Warning, + Message = "There was no active media announcement for a media format attribute, ignoring.")] + public static partial void LogSdpNoActiveMediaAnnouncement( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpNoActiveMediaAnnouncementForParam", + Level = LogLevel.Warning, + Message = "There was no active media announcement for a media format parameter attribute, ignoring.")] + public static partial void LogSdpNoActiveMediaAnnouncementForParam( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpMediaIdOnlyOnAnnouncement", + Level = LogLevel.Warning, + Message = "A media ID can only be set on a media announcement.")] + public static partial void LogSdpMediaIdOnlyOnAnnouncement( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpSsrcGroupIdOnlyOnAnnouncement", + Level = LogLevel.Warning, + Message = "A ssrc-group ID can only be set on a media announcement.")] + public static partial void LogSdpSsrcGroupIdOnlyOnAnnouncement( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpSsrcAttributeOnlyOnAnnouncement", + Level = LogLevel.Warning, + Message = "An ssrc attribute can only be set on a media announcement.")] + public static partial void LogSdpSsrcAttributeOnlyOnAnnouncement( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpInvalidSctpPort", + Level = LogLevel.Warning, + Message = "An sctp-port value of {sctpPortStr} was not recognised as a valid port.")] + public static partial void LogSdpInvalidSctpPort( + this ILogger logger, + string sctpPortStr); + + [LoggerMessage( + EventId = 0, + EventName = "SdpInvalidMaxMessageSize", + Level = LogLevel.Warning, + Message = "A max-message-size value of {maxMessageSizeStr} was not recognised as a valid long.")] + public static partial void LogSdpInvalidMaxMessageSize( + this ILogger logger, + string maxMessageSizeStr); + + [LoggerMessage( + EventId = 0, + EventName = "SdpSctpMapOnlyOnAnnouncement", + Level = LogLevel.Warning, + Message = "An sctpmap attribute can only be set on a media announcement.")] + public static partial void LogSdpSctpMapOnlyOnAnnouncement( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpSctpPortOnlyOnAnnouncement", + Level = LogLevel.Warning, + Message = "An sctp-port attribute can only be set on a media announcement.")] + public static partial void LogSdpSctpPortOnlyOnAnnouncement( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpMaxMessageSizeOnlyOnAnnouncement", + Level = LogLevel.Warning, + Message = "A max-message-size attribute can only be set on a media announcement.")] + public static partial void LogSdpMaxMessageSizeOnlyOnAnnouncement( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpAcceptTypesOnlyOnAnnouncement", + Level = LogLevel.Warning, + Message = "A accept-types attribute can only be set on a media announcement.")] + public static partial void LogSdpAcceptTypesOnlyOnAnnouncement( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpPathOnlyOnAnnouncement", + Level = LogLevel.Warning, + Message = "A path attribute can only be set on a media announcement.")] + public static partial void LogSdpPathOnlyOnAnnouncement( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpCryptoParsingError", + Level = LogLevel.Warning, + Message = "Error Parsing SDP-Line(a=crypto)")] + public static partial void LogSdpCryptoParsingError( + this ILogger logger, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "SdpParseException", + Level = LogLevel.Error, + Message = "Exception ParseSDPDescription. {errorMessage}")] + public static partial void LogSdpParseException( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "SdpDuplicateConnectionAttribute", + Level = LogLevel.Warning, + Message = "The SDP message had a duplicate connection attribute which was ignored.")] + public static partial void LogSdpDuplicateConnectionAttribute( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpInvalidMediaLine", + Level = LogLevel.Warning, + Message = "A media line in SDP was invalid: {sdpLine}.", + SkipEnabledCheck = true)] + private static partial void LogSdpInvalidMediaLineUnchecked( + this ILogger logger, + string sdpLine); + + public static void LogSdpInvalidMediaLine( + this ILogger logger, + string sdpLine) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + LogSdpInvalidMediaLineUnchecked(logger, sdpLine.Substring(2)); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "SdpInvalidIceRole", + Level = LogLevel.Warning, + Message = "ICE role was not recognised from SDP attribute: {sdpLineTrimmed}.")] + public static partial void LogSdpInvalidIceRole( + this ILogger logger, + string sdpLineTrimmed); + + [LoggerMessage( + EventId = 0, + EventName = "SdpMissingColon", + Level = LogLevel.Warning, + Message = "ICE role SDP attribute was missing the mandatory colon: {sdpLineTrimmed}.")] + public static partial void LogSdpMissingColon( + this ILogger logger, + string sdpLineTrimmed); + + [LoggerMessage( + EventId = 0, + EventName = "SdpInvalidHeaderExtension", + Level = LogLevel.Warning, + Message = "Invalid id of header extension in " + SDPMediaAnnouncement.MEDIA_EXTENSION_MAP_ATTRIBUE_PREFIX)] + public static partial void LogSdpInvalidHeaderExtension( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "SdpInvalidVersion", + Level = LogLevel.Warning, + Message = "The Version value in an SDP description could not be parsed as a decimal: {sdpLine}.")] + public static partial void LogSdpInvalidVersion( + this ILogger logger, + string sdpLine); + + [LoggerMessage( + EventId = 0, + EventName = "SdpParseError", + Level = LogLevel.Error, + Message = "Failed to parse SDP announcement. {ErrorMessage}")] + public static partial void LogSdpParseError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "SdpMediaFormatError", + Level = LogLevel.Warning, + Message = "Media format error in SDP announcement. {ErrorMessage}")] + public static partial void LogSdpMediaFormatError( + this ILogger logger, + string errorMessage); + + [LoggerMessage( + EventId = 0, + EventName = "SdpInvalidIceCandidate", + Level = LogLevel.Error, + Message = "Invalid ICE candidate: {IceCandidate}")] + private static partial void LogSdpInvalidIceCandidateUnchecked( + this ILogger logger, + string iceCandidate); + + public static void LogSdpInvalidIceCandidate( + this ILogger logger, + ReadOnlySpan iceCandidate) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + LogSdpInvalidIceCandidateUnchecked(logger, iceCandidate.ToString()); + } + } +} diff --git a/src/SIPSorcery/net/SDP/SDP.cs b/src/SIPSorcery/net/SDP/SDP.cs index 9c32ea8a83..b882d2c259 100644 --- a/src/SIPSorcery/net/SDP/SDP.cs +++ b/src/SIPSorcery/net/SDP/SDP.cs @@ -102,1151 +102,1216 @@ //----------------------------------------------------------------------------- using System; -using System.Buffers; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; -using System.Text; +using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; -using Polyfills; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public partial class SDP { - public class SDP + public const string CRLF = "\r\n"; + public const string SDP_MIME_CONTENTTYPE = "application/sdp"; + public const decimal SDP_PROTOCOL_VERSION = 0M; + public const string GROUP_ATRIBUTE_PREFIX = "group"; + public const string DTLS_FINGERPRINT_ATTRIBUTE_PREFIX = "fingerprint"; + public const string ICE_CANDIDATE_ATTRIBUTE_PREFIX = "candidate"; + public const string ICE_SETUP_ATTRIBUTE_PREFIX = "setup"; + public const string ADDRESS_TYPE_IPV4 = "IP4"; + public const string ADDRESS_TYPE_IPV6 = "IP6"; + public const string DEFAULT_TIMING = "0 0"; + public const string MEDIA_ID_ATTRIBUTE_PREFIX = "mid"; + public const int IGNORE_RTP_PORT_NUMBER = 9; + public const string TELEPHONE_EVENT_ATTRIBUTE = "telephone-event"; + public const int MEDIA_INDEX_NOT_PRESENT = -1; + public const string MEDIA_INDEX_TAG_NOT_PRESENT = ""; + public const MediaStreamStatusEnum DEFAULT_STREAM_STATUS = MediaStreamStatusEnum.SendRecv; + + // ICE attributes. + public const string ICE_LITE_IMPLEMENTATION_ATTRIBUTE_PREFIX = "ice-lite"; + public const string ICE_UFRAG_ATTRIBUTE_PREFIX = "ice-ufrag"; + public const string ICE_PWD_ATTRIBUTE_PREFIX = "ice-pwd"; + public const string END_ICE_CANDIDATES_ATTRIBUTE = "end-of-candidates"; + public const string ICE_OPTIONS = "ice-options"; + + private static readonly ILogger logger = LogFactory.CreateLogger(); + + public decimal Version = SDP_PROTOCOL_VERSION; + + // Owner fields. + public string Username = "-"; // Username of the session originator. + public string SessionId = "-"; // Unique Id for the session. + public ulong AnnouncementVersion; // Version number for each announcement, number must be increased for each subsequent SDP modification. + public string NetworkType = "IN"; // Type of network, IN = Internet. + public string AddressType = ADDRESS_TYPE_IPV4; // Address type, typically IP4 or IP6. + public string? AddressOrHost; // IP Address or Host of the machine that created the session, either FQDN or dotted quad or textual for IPv6. + public string Owner => $"{Username} {SessionId} {AnnouncementVersion} {NetworkType} {AddressType} {AddressOrHost}"; + + public string SessionName = "sipsorcery"; // Common name of the session. + public string Timing = DEFAULT_TIMING; + public List BandwidthAttributes = new List(); + + // Optional fields. + public string? SessionDescription; + public string? URI; // URI for additional information about the session. + public string[]? OriginatorEmailAddresses; // Email addresses for the person responsible for the session. + public string[]? OriginatorPhoneNumbers; // Phone numbers for the person responsible for the session. + public IceImplementationEnum IceImplementation = IceImplementationEnum.full; + public string? IceUfrag; // If ICE is being used the username for the STUN requests. + public string? IcePwd; // If ICE is being used the password for the STUN requests. + public IceRolesEnum? IceRole; + public string? DtlsFingerprint; // If DTLS handshake is being used this is the fingerprint or our DTLS certificate. + public List? IceCandidates; + + /// + /// Indicates multiple media offers will be bundled on a single RTP connection. + /// Example: a=group:BUNDLE audio video + /// + public string? Group; + + public SDPConnectionInformation? Connection; + + // Media. + public List Media = new List(); + + /// + /// The stream status of this session. The default is sendrecv. + /// If child media announcements have an explicit status set then + /// they take precedence. + /// + public MediaStreamStatusEnum? SessionMediaStreamStatus { get; set; } + + public List ExtraSessionAttributes = new List(); // Attributes that were not recognised. + + public SDP() + { } + + public SDP(IPAddress address) { - public const string CRLF = "\r\n"; - public const string SDP_MIME_CONTENTTYPE = "application/sdp"; - public const decimal SDP_PROTOCOL_VERSION = 0M; - public const string GROUP_ATRIBUTE_PREFIX = "group"; - public const string DTLS_FINGERPRINT_ATTRIBUTE_PREFIX = "fingerprint"; - public const string ICE_CANDIDATE_ATTRIBUTE_PREFIX = "candidate"; - public const string ICE_SETUP_ATTRIBUTE_PREFIX = "setup"; - public const string ADDRESS_TYPE_IPV4 = "IP4"; - public const string ADDRESS_TYPE_IPV6 = "IP6"; - public const string DEFAULT_TIMING = "0 0"; - public const string MEDIA_ID_ATTRIBUTE_PREFIX = "mid"; - public const int IGNORE_RTP_PORT_NUMBER = 9; - public const string TELEPHONE_EVENT_ATTRIBUTE = "telephone-event"; - public const int MEDIA_INDEX_NOT_PRESENT = -1; - public const string MEDIA_INDEX_TAG_NOT_PRESENT = ""; - public const MediaStreamStatusEnum DEFAULT_STREAM_STATUS = MediaStreamStatusEnum.SendRecv; - - // ICE attributes. - public const string ICE_LITE_IMPLEMENTATION_ATTRIBUTE_PREFIX = "ice-lite"; - public const string ICE_UFRAG_ATTRIBUTE_PREFIX = "ice-ufrag"; - public const string ICE_PWD_ATTRIBUTE_PREFIX = "ice-pwd"; - public const string END_ICE_CANDIDATES_ATTRIBUTE = "end-of-candidates"; - public const string ICE_OPTIONS = "ice-options"; - - private static readonly ILogger logger = LogFactory.CreateLogger(); - - public decimal Version = SDP_PROTOCOL_VERSION; - - private string m_rawSdp = null; - - // Owner fields. - public string Username = "-"; // Username of the session originator. - public string SessionId = "-"; // Unique Id for the session. - public ulong AnnouncementVersion = 0; // Version number for each announcement, number must be increased for each subsequent SDP modification. - public string NetworkType = "IN"; // Type of network, IN = Internet. - public string AddressType = ADDRESS_TYPE_IPV4; // Address type, typically IP4 or IP6. - public string AddressOrHost; // IP Address or Host of the machine that created the session, either FQDN or dotted quad or textual for IPv6. - public string Owner - { - get { return $"{Username} {SessionId} {AnnouncementVersion} {NetworkType} {AddressType} {AddressOrHost}"; } - } + AddressOrHost = address.ToString(); + AddressType = (address.AddressFamily == AddressFamily.InterNetworkV6) ? ADDRESS_TYPE_IPV6 : ADDRESS_TYPE_IPV4; + } - public string SessionName = "sipsorcery"; // Common name of the session. - public string Timing = DEFAULT_TIMING; - public List BandwidthAttributes = new List(); - - // Optional fields. - public string SessionDescription; - public string URI; // URI for additional information about the session. - public string[] OriginatorEmailAddresses; // Email addresses for the person responsible for the session. - public string[] OriginatorPhoneNumbers; // Phone numbers for the person responsible for the session. - public IceImplementationEnum IceImplementation = IceImplementationEnum.full; - public string IceUfrag; // If ICE is being used the username for the STUN requests. - public string IcePwd; // If ICE is being used the password for the STUN requests. - public IceRolesEnum? IceRole = null; - public string DtlsFingerprint; // If DTLS handshake is being used this is the fingerprint or our DTLS certificate. - public List IceCandidates; - - /// - /// Indicates multiple media offers will be bundled on a single RTP connection. - /// Example: a=group:BUNDLE audio video - /// - public string Group; - - public SDPConnectionInformation Connection; - - // Media. - public List Media = new List(); - - /// - /// The stream status of this session. The default is sendrecv. - /// If child media announcements have an explicit status set then - /// they take precedence. - /// - public MediaStreamStatusEnum? SessionMediaStreamStatus { get; set; } = null; - - public List ExtraSessionAttributes = new List(); // Attributes that were not recognised. - - public SDP() - { } - - public SDP(IPAddress address) + public static SDP? ParseSDPDescription(ReadOnlySpan sdpDescription) + { + if (sdpDescription.IsEmpty || sdpDescription.IndexOfAnyExcept(SearchValues.WhiteSpaceChars) < 0) { - AddressOrHost = address.ToString(); - AddressType = (address.AddressFamily == AddressFamily.InterNetworkV6) ? ADDRESS_TYPE_IPV6 : ADDRESS_TYPE_IPV4; + return null; } - public static SDP ParseSDPDescription(string sdpDescription) + try { - try + var sdp = new SDP(); + + var mLineIndex = 0; + SDPMediaAnnouncement? activeAnnouncement = null; + + // If a media announcement fmtp atribute is found before the rtpmap it will be stored + // in this dictionary. A dynamic media format type cannot be created without an rtpmap. + var pendingFmtp = new Dictionary(); + + var sdpDescriptionSpan = sdpDescription; + foreach (var lineRange in sdpDescriptionSpan.SplitAny(SearchValues.NewLineChars)) { - if (!string.IsNullOrWhiteSpace(sdpDescription)) - { - SDP sdp = new SDP(); - sdp.m_rawSdp = sdpDescription; - int mLineIndex = 0; - SDPMediaAnnouncement activeAnnouncement = null; + var line = sdpDescriptionSpan[lineRange].Trim(); - // If a media announcement fmtp atribute is found before the rtpmap it will be stored - // in this dictionary. A dynamic media format type cannot be created without an rtpmap. - Dictionary _pendingFmtp = new Dictionary(); + if (line.Length < 2 || line[1] != '=') + { + continue; + } - var sdpDescriptionSpan = sdpDescription.AsSpan(); - Span ownerFieldRanges = stackalloc Range[6]; - const StringSplitOptions TrimEntries = (StringSplitOptions)2; - const StringSplitOptions RemoveEmptyAndTrimSplitOptions = StringSplitOptions.RemoveEmptyEntries | TrimEntries; + var type = line[0]; + var value = line.Slice(2); - static bool StartsWithAttribute(ReadOnlySpan line, string attributePrefix) => - line.StartsWith("a=", StringComparison.Ordinal) && - line.Slice(2).StartsWith(attributePrefix, StringComparison.Ordinal); + switch (type) + { + case 'v': + if (!decimal.TryParse(value, out sdp.Version)) + { + logger.LogSdpInvalidVersion(value.ToString()); + } + break; - static bool EqualsAttribute(ReadOnlySpan line, string attribute) => - line.Length == attribute.Length + 2 && - line.StartsWith("a=", StringComparison.Ordinal) && - line.Slice(2).Equals(attribute.AsSpan(), StringComparison.Ordinal); + case 'o': + ParseOrigin(value, sdp); + break; - static ReadOnlySpan SliceAfterColon(ReadOnlySpan line) => - line.Slice(line.IndexOf(':') + 1); + case 's': + sdp.SessionName = value.ToString(); + break; - static bool TryReadToken(ReadOnlySpan value, ref int offset, out int tokenStart, out int tokenLength) - { - while (offset < value.Length && char.IsWhiteSpace(value[offset])) + case 'i': + if (activeAnnouncement is { }) + { + activeAnnouncement.MediaDescription = value.ToString(); + } + else { - offset++; + sdp.SessionDescription = value.ToString(); } - if (offset == value.Length) + break; + + case 'c': + if (activeAnnouncement is { }) { - tokenStart = 0; - tokenLength = 0; - return false; + activeAnnouncement.Connection = SDPConnectionInformation.ParseConnectionInformation(line); + } + else if (sdp.Connection is null) + { + sdp.Connection = SDPConnectionInformation.ParseConnectionInformation(line); + } + else + { + logger.LogSdpDuplicateConnectionAttribute(); } - tokenStart = offset; - var endIndex = offset; - while (endIndex < value.Length && !char.IsWhiteSpace(value[endIndex])) + break; + + case 'b': + ParseBandwidth(value, sdp, activeAnnouncement); + break; + + case 't': + sdp.Timing = value.ToString(); + break; + + case 'm': + ParseMedia(line, sdp, ref activeAnnouncement, ref mLineIndex); + break; + + case 'a': + ParseAttribute(line, sdp, activeAnnouncement, pendingFmtp); + break; + + default: + if (activeAnnouncement is { }) { - endIndex++; + activeAnnouncement.AddExtra(line.ToString()); } + else + { + sdp.AddExtra(line.ToString()); + } + break; + } - tokenLength = endIndex - tokenStart; - offset = endIndex; - return true; + static void ParseOrigin(ReadOnlySpan value, SDP sdp) + { + Span fields = stackalloc Range[6]; + var count = value.Split(fields, ' ', StringSplitOptions.RemoveEmptyEntries); + + if (count >= 5) + { + sdp.Username = value[fields[0]].ToString(); + sdp.SessionId = value[fields[1]].ToString(); + sdp.AnnouncementVersion = ulong.TryParse(value[fields[2]], out var version) ? version : 0; + sdp.NetworkType = value[fields[3]].ToString(); + sdp.AddressType = value[fields[4]].ToString(); + sdp.AddressOrHost = count > 5 ? value[fields[5]].ToString() : null; + } + else + { + logger.LogSdpInvalidSdpLineFormat(value.ToString()); } + } - static bool TrySplitAttributeValue( - ReadOnlySpan line, - int prefixLength, - out int idStart, - out int idLength, - out int attributeStart) + static void ParseBandwidth(ReadOnlySpan value, SDP sdp, SDPMediaAnnouncement? activeAnnouncement) + { + if (activeAnnouncement is { }) { - var offset = prefixLength; - if (!TryReadToken(line, ref offset, out idStart, out idLength)) + var colonIndex = value.IndexOf(':'); + var key = colonIndex != -1 ? value.Slice(0, colonIndex) : value; + var attrValue = colonIndex != -1 && colonIndex + 1 < value.Length + ? value.Slice(colonIndex + 1) + : ReadOnlySpan.Empty; + if (key.SequenceEqual(SDPMediaAnnouncement.TIAS_BANDWIDTH_ATTRIBUE_NAME.AsSpan())) { - attributeStart = 0; - return false; + if (uint.TryParse(attrValue, out var tias)) + { + activeAnnouncement.TIASBandwidth = tias; + } + } + else + { + activeAnnouncement.BandwidthAttributes.Add(value.ToString()); + } + } + else + { + sdp.BandwidthAttributes.Add(value.ToString()); + } + } + + static void ParseMedia(ReadOnlySpan line, SDP sdp, ref SDPMediaAnnouncement? activeAnnouncement, ref int mLineIndex) + { + if (TryParseMediaDescription(line.Slice(2), out var type, out var port, out var portCount, out var transport, out var formats)) + { + var announcement = new SDPMediaAnnouncement(); + announcement.MLineIndex = mLineIndex; + announcement.Media = SDPMediaTypes.GetSDPMediaType(type); + announcement.Port = port; + + if (portCount is { } portCountValue) + { + announcement.PortCount = portCountValue; } - while (offset < line.Length && char.IsWhiteSpace(line[offset])) + announcement.Transport = transport; + announcement.ParseMediaFormats(formats); + if (announcement.Media is SDPMediaTypesEnum.audio or SDPMediaTypesEnum.video or SDPMediaTypesEnum.text) { - offset++; + announcement.MediaStreamStatus = sdp.SessionMediaStreamStatus is { } ? sdp.SessionMediaStreamStatus.Value : + MediaStreamStatusEnum.SendRecv; } + sdp.Media.Add(announcement); - attributeStart = offset; - return attributeStart < line.Length; + activeAnnouncement = announcement; + } + else + { + logger.LogSdpInvalidMediaLine(line.ToString()); } - static bool TryParseMediaLine( - ReadOnlySpan mediaLine, - out int mediaTypeStart, - out int mediaTypeLength, + mLineIndex++; + + /// (?<type>\w+)\s+(?<port>\d+)(?:\/(?<portCount>\d+))?\s+(?<transport>\S+)\s*(?<formats>.*) + static bool TryParseMediaDescription( + ReadOnlySpan input, + [NotNullWhen(true)] out string? type, out int port, out int? portCount, - out int transportStart, - out int transportLength, - out int formatsStart) + [NotNullWhen(true)] out string? transport, + [NotNullWhen(true)] out string? formats) { - mediaTypeStart = 0; - mediaTypeLength = 0; - port = 0; - portCount = null; - transportStart = 0; - transportLength = 0; - formatsStart = 0; - - var offset = 0; - if (!TryReadToken(mediaLine, ref offset, out mediaTypeStart, out mediaTypeLength) || - !TryReadToken(mediaLine, ref offset, out var portStart, out var portLength) || - !TryReadToken(mediaLine, ref offset, out transportStart, out transportLength)) + type = default; + port = default; + portCount = default; + transport = default; + formats = default; + + // Parse type + var typeEnd = input.IndexOfAny(SearchValues.WhiteSpaceChars); + if (typeEnd <= 0) + { + return false; + } + + type = input[..typeEnd].ToString(); + + // Skip whitespace after type + var i = typeEnd + input[typeEnd..].IndexOfAnyExcept(SearchValues.WhiteSpaceChars); + if (i >= input.Length) + { + return false; + } + + // Parse port + var portStart = i; + var portEnd = input[portStart..].IndexOfAnyExcept(SearchValues.DigitChars); + if (portEnd <= 0) { return false; } - var portToken = mediaLine.Slice(portStart, portLength); - var slashIndex = portToken.IndexOf('/'); - var portSpan = slashIndex == -1 ? portToken : portToken.Slice(0, slashIndex); - if (!int.TryParse(portSpan, out port)) + portEnd += portStart; + if (!int.TryParse(input[portStart..portEnd], out port)) { return false; } - if (slashIndex != -1) + i = portEnd; + + // Optional: / + if (i < input.Length && input[i] == '/') { - var portCountSpan = portToken.Slice(slashIndex + 1); - if (portCountSpan.IsEmpty || !int.TryParse(portCountSpan, out var parsedPortCount)) + i++; + var portCountStart = i; + var portCountEnd = input[portCountStart..].IndexOfAnyExcept(SearchValues.DigitChars); + if (portCountEnd <= 0) + { + return false; + } + + portCountEnd += portCountStart; + if (!int.TryParse(input[portCountStart..portCountEnd], out var parsedPortCount)) { return false; } portCount = parsedPortCount; + i = portCountEnd; } - while (offset < mediaLine.Length && char.IsWhiteSpace(mediaLine[offset])) + // Skip whitespace before transport + var transportStartOffset = input[i..].IndexOfAnyExcept(SearchValues.WhiteSpaceChars); + if (transportStartOffset == -1) { - offset++; + return false; } - formatsStart = offset; + i += transportStartOffset; + + // Parse transport + var transportEndOffset = input[i..].IndexOfAny(SearchValues.WhiteSpaceChars); + var transportEnd = transportEndOffset == -1 ? input.Length : i + transportEndOffset; + transport = input[i..transportEnd].ToString(); + + i = transportEnd; + + // Skip whitespace before formats + var formatsStartOffset = input[i..].IndexOfAnyExcept(SearchValues.WhiteSpaceChars); + i = formatsStartOffset == -1 ? input.Length : i + formatsStartOffset; + + formats = input[i..].ToString(); return true; } + } - static bool TryParseExtensionMap(ReadOnlySpan line, out int id, out int uriStart, out int uriLength) + static void ParseAttribute( + ReadOnlySpan line, + SDP sdp, + SDPMediaAnnouncement? activeAnnouncement, + Dictionary pendingFmtp) + { + var value = line.Slice(2); + var colonIndex = value.IndexOf(':'); + var key = colonIndex != -1 ? value.Slice(0, colonIndex) : value; + var attrValue = colonIndex != -1 && colonIndex + 1 < value.Length + ? value.Slice(colonIndex + 1) + : ReadOnlySpan.Empty; + + if (key.SequenceEqual(GROUP_ATRIBUTE_PREFIX.AsSpan())) + { + sdp.Group = attrValue.ToString(); + } + else if (key.SequenceEqual(ICE_LITE_IMPLEMENTATION_ATTRIBUTE_PREFIX.AsSpan())) { - id = 0; - uriStart = 0; - uriLength = 0; - var offset = SDPMediaAnnouncement.MEDIA_EXTENSION_MAP_ATTRIBUE_PREFIX.Length; - - if (!TryReadToken(line, ref offset, out var idStart, out var idLength) || - !int.TryParse(line.Slice(idStart, idLength), out id) || - !TryReadToken(line, ref offset, out uriStart, out uriLength)) + sdp.IceImplementation = IceImplementationEnum.lite; + } + else if (key.SequenceEqual(ICE_UFRAG_ATTRIBUTE_PREFIX.AsSpan())) + { + if (activeAnnouncement is { }) { - return false; + activeAnnouncement.IceUfrag = attrValue.ToString(); } - - while (offset < line.Length && char.IsWhiteSpace(line[offset])) + else { - offset++; + sdp.IceUfrag = attrValue.ToString(); } - - return offset == line.Length; } - - static bool TryParseMediaStreamStatus(ReadOnlySpan attribute, out MediaStreamStatusEnum mediaStreamStatus) + else if (key.SequenceEqual(ICE_PWD_ATTRIBUTE_PREFIX.AsSpan())) { - mediaStreamStatus = MediaStreamStatusEnum.SendRecv; - - if (attribute.Equals(MediaStreamStatusType.SEND_RECV_ATTRIBUTE.AsSpan(), StringComparison.OrdinalIgnoreCase)) + if (activeAnnouncement is { }) { - mediaStreamStatus = MediaStreamStatusEnum.SendRecv; - return true; + activeAnnouncement.IcePwd = attrValue.ToString(); } - - if (attribute.Equals(MediaStreamStatusType.SEND_ONLY_ATTRIBUTE.AsSpan(), StringComparison.OrdinalIgnoreCase)) + else { - mediaStreamStatus = MediaStreamStatusEnum.SendOnly; - return true; + sdp.IcePwd = attrValue.ToString(); } - - if (attribute.Equals(MediaStreamStatusType.RECV_ONLY_ATTRIBUTE.AsSpan(), StringComparison.OrdinalIgnoreCase)) + } + else if (key.SequenceEqual(ICE_SETUP_ATTRIBUTE_PREFIX.AsSpan())) + { + if (!attrValue.IsEmpty) { - mediaStreamStatus = MediaStreamStatusEnum.RecvOnly; - return true; + if (IceRolesEnumExtensions.TryParse(attrValue, out var iceRole, true)) + { + if (activeAnnouncement is { }) + { + activeAnnouncement.IceRole = iceRole; + } + else + { + sdp.IceRole = iceRole; + } + } + else + { + logger.LogSdpInvalidIceRole(line.ToString()); + } } - - if (attribute.Equals(MediaStreamStatusType.INACTIVE_ATTRIBUTE.AsSpan(), StringComparison.OrdinalIgnoreCase)) + else { - mediaStreamStatus = MediaStreamStatusEnum.Inactive; - return true; + logger.LogSdpMissingColon(line.ToString()); } - - return false; } - - var sdpLineRangeBuffer = ArrayPool.Shared.Rent(sdpDescriptionSpan.Length + 1); - try + else if (key.SequenceEqual(DTLS_FINGERPRINT_ATTRIBUTE_PREFIX.AsSpan())) { - var sdpLineRanges = sdpLineRangeBuffer.AsSpan(0, sdpDescriptionSpan.Length + 1); - var sdpLineCount = sdpDescriptionSpan.SplitAny( - sdpLineRanges, - "\r\n".AsSpan(), - RemoveEmptyAndTrimSplitOptions); - - for (var sdpLineIndex = 0; sdpLineIndex < sdpLineCount; sdpLineIndex++) + if (activeAnnouncement is { }) { - var sdpLineTrimmedSpan = sdpDescriptionSpan[sdpLineRanges[sdpLineIndex]]; - - switch (sdpLineTrimmedSpan) + activeAnnouncement.DtlsFingerprint = attrValue.ToString(); + } + else + { + sdp.DtlsFingerprint = attrValue.ToString(); + } + } + else if (key.SequenceEqual(ICE_CANDIDATE_ATTRIBUTE_PREFIX.AsSpan())) + { + if (RTCIceCandidate.TryParse(attrValue, out var candidate)) + { + if (activeAnnouncement is { }) { - case var _ when sdpLineTrimmedSpan.StartsWith("v=", StringComparison.Ordinal): - if (!Decimal.TryParse(sdpLineTrimmedSpan.Slice(2), out sdp.Version)) - { - logger.LogWarning("The Version value in an SDP description could not be parsed as a decimal: {sdpLine}.", sdpLineTrimmedSpan.ToString()); - } - break; - - case var _ when sdpLineTrimmedSpan.StartsWith("o=", StringComparison.Ordinal): - var ownerFieldsSpan = sdpLineTrimmedSpan.Slice(2); - var ownerFieldCount = ownerFieldsSpan.Split(ownerFieldRanges, ' ', StringSplitOptions.RemoveEmptyEntries); - - if (ownerFieldCount >= 5) - { - sdp.Username = ownerFieldsSpan[ownerFieldRanges[0]].ToString(); - sdp.SessionId = ownerFieldsSpan[ownerFieldRanges[1]].ToString(); - sdp.AnnouncementVersion = UInt64.TryParse(ownerFieldsSpan[ownerFieldRanges[2]].ToString(), out var version) ? version : 0; - sdp.NetworkType = ownerFieldsSpan[ownerFieldRanges[3]].ToString(); - sdp.AddressType = ownerFieldsSpan[ownerFieldRanges[4]].ToString(); - sdp.AddressOrHost = ownerFieldCount > 5 ? ownerFieldsSpan[ownerFieldRanges[5]].ToString() : null; - } - else + activeAnnouncement.IceCandidates ??= new List(); + activeAnnouncement.IceCandidates.Add(candidate); + } + else + { + sdp.IceCandidates ??= new List(); + sdp.IceCandidates.Add(candidate); + } + } + else if (logger.IsEnabled(LogLevel.Warning)) + { + logger.LogSdpInvalidIceCandidate(attrValue); + } + } + else if (key.SequenceEqual(END_ICE_CANDIDATES_ATTRIBUTE.AsSpan())) + { + // TODO: Set a flag. + } + else if (key.SequenceEqual(SDPMediaAnnouncement.MEDIA_EXTENSION_MAP_ATTRIBUE_NAME.AsSpan())) + { + if (activeAnnouncement is { }) + { + if (activeAnnouncement.Media is SDPMediaTypesEnum.audio or SDPMediaTypesEnum.video) + { + if (TryParseNumericIdAndUrl(attrValue, out var extensionId, out var uri)) + { + var rtpExtension = RTPHeaderExtension.GetRTPHeaderExtension(extensionId, uri, activeAnnouncement.Media); + if ((rtpExtension is { }) && !activeAnnouncement.HeaderExtensions.ContainsKey(extensionId)) { - logger.LogWarning("The SDP message had an invalid SDP line format for 'o=': {sdpLineTrimmed}", sdpLineTrimmedSpan.ToString()); + activeAnnouncement.HeaderExtensions.Add(extensionId, rtpExtension); } - break; - - case var _ when sdpLineTrimmedSpan.StartsWith("s=", StringComparison.Ordinal): - sdp.SessionName = sdpLineTrimmedSpan.Slice(2).ToString(); - break; - - case var _ when sdpLineTrimmedSpan.StartsWith("i=", StringComparison.Ordinal): - if (activeAnnouncement != null) + } + else + { + logger.LogSdpInvalidHeaderExtension(); + } + } + } + } + else if (key.SequenceEqual(SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUTE_NAME.AsSpan())) + { + if (activeAnnouncement is { }) + { + if (activeAnnouncement.Media is SDPMediaTypesEnum.audio or SDPMediaTypesEnum.video or SDPMediaTypesEnum.text) + { + // Parse the rtpmap attribute for audio/video announcements. + if (TryParseNumericIdAndStringAttribute(attrValue, out var formatId, out var rtpmap)) + { + if (activeAnnouncement.MediaFormats.TryGetValue(formatId, out var mediaFormat)) { - activeAnnouncement.MediaDescription = sdpLineTrimmedSpan.Slice(2).ToString(); + activeAnnouncement.MediaFormats[formatId] = mediaFormat.WithUpdatedRtpmap(rtpmap); } else { - sdp.SessionDescription = sdpLineTrimmedSpan.Slice(2).ToString(); - } - - break; - - case var _ when sdpLineTrimmedSpan.StartsWith("c=", StringComparison.Ordinal): - - if (activeAnnouncement != null) - { - activeAnnouncement.Connection = SDPConnectionInformation.ParseConnectionInformation(sdpLineTrimmedSpan.ToString()); + _ = pendingFmtp.TryGetValue(formatId, out var fmtp); + activeAnnouncement.MediaFormats.Add(formatId, new SDPAudioVideoMediaFormat(activeAnnouncement.Media, formatId, rtpmap, fmtp)); } - else if (sdp.Connection == null) + } + else + { + // This is a recognised rtpmap attribute with an invalid numeric payload ID. + // Drop it instead of preserving it as an unknown extra attribute. + } + } + else + { + // Parse the rtpmap attribute for NON audio/video announcements. + if (TryParseStringIdAndStringAttribute(attrValue, out var formatID, out var rtpmap)) + { + if (activeAnnouncement.ApplicationMediaFormats.TryGetValue(formatID, out var mediaFormat)) { - sdp.Connection = SDPConnectionInformation.ParseConnectionInformation(sdpLineTrimmedSpan.ToString()); + activeAnnouncement.ApplicationMediaFormats[formatID] = mediaFormat.WithUpdatedRtpmap(rtpmap); } else { - logger.LogWarning("The SDP message had a duplicate connection attribute which was ignored."); + activeAnnouncement.ApplicationMediaFormats.Add(formatID, new SDPApplicationMediaFormat(formatID, rtpmap, null)); } - - break; - - case var l when l.StartsWith("b=", StringComparison.Ordinal): - if (activeAnnouncement != null) + } + else + { + activeAnnouncement.AddExtra(line.ToString()); + } + } + } + else + { + logger.LogSdpNoActiveMediaAnnouncement(); + } + } + else if (key.SequenceEqual(SDPMediaAnnouncement.MEDIA_FORMAT_PARAMETERS_ATTRIBUE_NAME.AsSpan())) + { + if (activeAnnouncement is { }) + { + if (activeAnnouncement.Media is SDPMediaTypesEnum.audio or SDPMediaTypesEnum.video or SDPMediaTypesEnum.text) + { + // Parse the fmtp attribute for audio/video announcements. + if (TryParseNumericIdAndStringAttribute(attrValue, out var avFormatID, out var fmtp)) + { + if (activeAnnouncement.MediaFormats.TryGetValue(avFormatID, out var mediaFormat)) { - if (l.StartsWith(SDPMediaAnnouncement.TIAS_BANDWIDTH_ATTRIBUE_PREFIX, StringComparison.Ordinal)) - { - if (uint.TryParse(SliceAfterColon(l), out var tias)) - { - activeAnnouncement.TIASBandwidth = tias; - } - } - else - { - activeAnnouncement.BandwidthAttributes.Add(sdpLineTrimmedSpan.Slice(2).ToString()); - } + activeAnnouncement.MediaFormats[avFormatID] = mediaFormat.WithUpdatedFmtp(fmtp.ToString()); } else { - sdp.BandwidthAttributes.Add(sdpLineTrimmedSpan.Slice(2).ToString()); + // Store the fmtp attribute for use when the rtpmap attribute turns up. + pendingFmtp.Remove(avFormatID); + pendingFmtp.Add(avFormatID, fmtp.ToString()); } - break; - - case var _ when sdpLineTrimmedSpan.StartsWith("t=", StringComparison.Ordinal): - sdp.Timing = sdpLineTrimmedSpan.Slice(2).ToString(); - break; - - case var _ when sdpLineTrimmedSpan.StartsWith("m=", StringComparison.Ordinal): - var mediaLine = sdpLineTrimmedSpan.Slice(2); - if (TryParseMediaLine( - mediaLine, - out var mediaTypeStart, - out var mediaTypeLength, - out var port, - out var portCount, - out var transportStart, - out var transportLength, - out var formatsStart)) + } + else + { + activeAnnouncement.AddExtra(line.ToString()); + } + } + else + { + // TODO: optimize this + // Parse the fmtp attribute for NON audio/video announcements. + if (TryParseStringIdAndStringAttribute(attrValue, out var formatID, out var fmtp)) + { + if (activeAnnouncement.ApplicationMediaFormats.TryGetValue(formatID, out var mediaFormat)) { - var announcement = new SDPMediaAnnouncement(); - announcement.MLineIndex = mLineIndex; - announcement.Media = SDPMediaTypes.GetSDPMediaType(mediaLine.Slice(mediaTypeStart, mediaTypeLength).ToString()); - - // Parse the primary port. - announcement.Port = port; - if (portCount.HasValue) - { - announcement.PortCount = portCount.Value; - } - - announcement.Transport = mediaLine.Slice(transportStart, transportLength).ToString(); - announcement.ParseMediaFormats(mediaLine.Slice(formatsStart).ToString()); - if (announcement.Media == SDPMediaTypesEnum.audio || announcement.Media == SDPMediaTypesEnum.video || announcement.Media == SDPMediaTypesEnum.text) - { - announcement.MediaStreamStatus = sdp.SessionMediaStreamStatus != null ? sdp.SessionMediaStreamStatus.Value : - MediaStreamStatusEnum.SendRecv; - } - sdp.Media.Add(announcement); - - activeAnnouncement = announcement; + activeAnnouncement.ApplicationMediaFormats[formatID] = mediaFormat.WithUpdatedFmtp(fmtp); } else { - logger.LogWarning("A media line in SDP was invalid: {sdpLine}.", sdpLineTrimmedSpan.Slice(2).ToString()); + activeAnnouncement.ApplicationMediaFormats.Add(formatID, new SDPApplicationMediaFormat(formatID, null, fmtp)); } + } + else + { + activeAnnouncement.AddExtra(line.ToString()); + } + } + } + else + { + logger.LogSdpNoActiveMediaAnnouncementForParam(); + } + } + else if (key.SequenceEqual(SDPSecurityDescription.CRYPTO_ATTRIBUTE_NAME.AsSpan())) + { + //2018-12-21 rj2: add a=crypto + if (activeAnnouncement is { }) + { + try + { + activeAnnouncement.AddCryptoLine(line.ToString()); + } + catch (FormatException fex) + { + logger.LogSdpCryptoParsingError(fex); + } + } + } + else if (key.SequenceEqual(MEDIA_ID_ATTRIBUTE_PREFIX.AsSpan())) + { + if (activeAnnouncement is { }) + { + activeAnnouncement.MediaID = attrValue.ToString(); + } + else + { + logger.LogSdpMediaIdOnlyOnAnnouncement(); + } + } + else if (key.SequenceEqual(SDPMediaAnnouncement.MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_NAME.AsSpan())) + { + if (activeAnnouncement is { }) + { + var span = attrValue; + var spaceIndex = span.IndexOf(' '); - mLineIndex++; - break; - - case var _ when StartsWithAttribute(sdpLineTrimmedSpan, GROUP_ATRIBUTE_PREFIX): - sdp.Group = SliceAfterColon(sdpLineTrimmedSpan).ToString(); - break; - case var _ when StartsWithAttribute(sdpLineTrimmedSpan, ICE_LITE_IMPLEMENTATION_ATTRIBUTE_PREFIX): - sdp.IceImplementation = IceImplementationEnum.lite; - break; - case var _ when StartsWithAttribute(sdpLineTrimmedSpan, ICE_UFRAG_ATTRIBUTE_PREFIX): - if (activeAnnouncement != null) - { - activeAnnouncement.IceUfrag = SliceAfterColon(sdpLineTrimmedSpan).ToString(); - } - else - { - sdp.IceUfrag = SliceAfterColon(sdpLineTrimmedSpan).ToString(); - } - break; + // Set the ID. + if (spaceIndex != -1) + { + var idSpan = span.Slice(0, spaceIndex); + activeAnnouncement.SsrcGroupID = idSpan.ToString(); + span = span.Slice(spaceIndex + 1); + } + else + { + activeAnnouncement.SsrcGroupID = attrValue.ToString(); + span = ReadOnlySpan.Empty; + } - case var _ when StartsWithAttribute(sdpLineTrimmedSpan, ICE_PWD_ATTRIBUTE_PREFIX): - if (activeAnnouncement != null) - { - activeAnnouncement.IcePwd = SliceAfterColon(sdpLineTrimmedSpan).ToString(); - } - else - { - sdp.IcePwd = SliceAfterColon(sdpLineTrimmedSpan).ToString(); - } - break; + // Add attributes for each of the SSRC values. + foreach (var token in span.Split(' ')) + { + var ssrcSpan = span[token].Trim(); + if (uint.TryParse(ssrcSpan, out var ssrc)) + { + activeAnnouncement.SsrcAttributes.Add(new SDPSsrcAttribute(ssrc, null, activeAnnouncement.SsrcGroupID)); + } + } + } + else + { + logger.LogSdpSsrcGroupIdOnlyOnAnnouncement(); + } + } + else if (key.SequenceEqual(SDPMediaAnnouncement.MEDIA_FORMAT_SSRC_ATTRIBUE_NAME.AsSpan())) + { + if (activeAnnouncement is { }) + { + var firstSpace = attrValue.IndexOf(' '); + if (firstSpace == -1) + { + return; + } - case var _ when StartsWithAttribute(sdpLineTrimmedSpan, ICE_SETUP_ATTRIBUTE_PREFIX): - var colonIndex = sdpLineTrimmedSpan.IndexOf(':'); - if (colonIndex != -1 && sdpLineTrimmedSpan.Length > colonIndex) + var firstField = attrValue[..firstSpace]; + if (uint.TryParse(firstField, out var ssrc)) + { + if (GetFirstMatchingAssrcAttribute(activeAnnouncement, ssrc) is not { } ssrcAttribute) + { + ssrcAttribute = new SDPSsrcAttribute(ssrc, null, null); + activeAnnouncement.SsrcAttributes.Add(ssrcAttribute); + } + + var remaining = attrValue[(firstSpace + 1)..]; + var secondSpace = remaining.IndexOf(' '); + var secondField = secondSpace == -1 + ? remaining + : remaining[..secondSpace]; + + if (secondField.StartsWith("cname:".AsSpan())) + { + ssrcAttribute.Cname = secondField[6..].ToString(); + } + + static SDPSsrcAttribute? GetFirstMatchingAssrcAttribute(SDPMediaAnnouncement activeAnnouncement, uint ssrc) + { + SDPSsrcAttribute? ssrcAttribute = null; + foreach (var attr in activeAnnouncement.SsrcAttributes) { - var iceRoleStr = sdpLineTrimmedSpan.Slice(colonIndex + 1).Trim().ToString(); - if (Enum.TryParse(iceRoleStr, true, out var iceRole)) - { - if (activeAnnouncement != null) - { - activeAnnouncement.IceRole = iceRole; - } - else - { - sdp.IceRole = iceRole; - } - } - else + if (attr.SSRC == ssrc) { - logger.LogWarning("ICE role was not recognised from SDP attribute: {sdpLineTrimmed}.", sdpLineTrimmedSpan.ToString()); + ssrcAttribute = attr; + break; } } - else - { - logger.LogWarning("ICE role SDP attribute was missing the mandatory colon: {sdpLineTrimmed}.", sdpLineTrimmedSpan.ToString()); - } - break; - case var _ when StartsWithAttribute(sdpLineTrimmedSpan, DTLS_FINGERPRINT_ATTRIBUTE_PREFIX): - if (activeAnnouncement != null) - { - activeAnnouncement.DtlsFingerprint = SliceAfterColon(sdpLineTrimmedSpan).ToString(); - } - else - { - sdp.DtlsFingerprint = SliceAfterColon(sdpLineTrimmedSpan).ToString(); - } - break; + return ssrcAttribute; + } + } + } + else + { + logger.LogSdpSsrcAttributeOnlyOnAnnouncement(); + } + } + else if (key.SequenceEqual(SDPMediaAnnouncement.MEDIA_FORMAT_SCTP_MAP_ATTRIBUE_NAME.AsSpan())) + { + if (activeAnnouncement is { }) + { + activeAnnouncement.SctpMap = attrValue.ToString(); - case var _ when StartsWithAttribute(sdpLineTrimmedSpan, ICE_CANDIDATE_ATTRIBUTE_PREFIX): - if (activeAnnouncement != null) - { - if (activeAnnouncement.IceCandidates == null) - { - activeAnnouncement.IceCandidates = new List(); - } - activeAnnouncement.IceCandidates.Add(SliceAfterColon(sdpLineTrimmedSpan).ToString()); - } - else - { - if (sdp.IceCandidates == null) - { - sdp.IceCandidates = new List(); - } - sdp.IceCandidates.Add(SliceAfterColon(sdpLineTrimmedSpan).ToString()); - } - break; - - case var _ when EqualsAttribute(sdpLineTrimmedSpan, END_ICE_CANDIDATES_ATTRIBUTE): - // TODO: Set a flag. - break; - case var l when l.StartsWith(SDPMediaAnnouncement.MEDIA_EXTENSION_MAP_ATTRIBUE_PREFIX, StringComparison.Ordinal): - if (activeAnnouncement != null && - (activeAnnouncement.Media == SDPMediaTypesEnum.audio || activeAnnouncement.Media == SDPMediaTypesEnum.video) && - TryParseExtensionMap(l, out var extensionId, out var uriStart, out var uriLength)) - { - var rtpExtension = RTPHeaderExtension.GetRTPHeaderExtension(extensionId, l.Slice(uriStart, uriLength).ToString(), activeAnnouncement.Media); - if ((rtpExtension != null) && !activeAnnouncement.HeaderExtensions.ContainsKey(extensionId)) - { - activeAnnouncement.HeaderExtensions.Add(extensionId, rtpExtension); - } - } + // Parse sctp-port and max-message-size from space-separated values + // Format: "sctpPort protocol maxMessageSize [additional-params...]" + Span fields = stackalloc Range[4]; + var count = attrValue.Split(fields, ' ', StringSplitOptions.RemoveEmptyEntries); - break; - case var l when l.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUTE_PREFIX, StringComparison.Ordinal): - if (activeAnnouncement != null) - { - if (activeAnnouncement.Media == SDPMediaTypesEnum.audio || activeAnnouncement.Media == SDPMediaTypesEnum.video || activeAnnouncement.Media == SDPMediaTypesEnum.text) - { - // Parse the rtpmap attribute for audio/video announcements. - if (TrySplitAttributeValue( - l, - SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUTE_PREFIX.Length, - out var formatIDStart, - out var formatIDLength, - out var rtpmapStart)) - { - var formatID = l.Slice(formatIDStart, formatIDLength); - if (int.TryParse(formatID, out var mediaFormatId)) - { - var rtpmap = l.Slice(rtpmapStart).ToString(); - if (activeAnnouncement.MediaFormats.ContainsKey(mediaFormatId)) - { - activeAnnouncement.MediaFormats[mediaFormatId] = activeAnnouncement.MediaFormats[mediaFormatId].WithUpdatedRtpmap(rtpmap); - } - else - { - var fmtp = _pendingFmtp.ContainsKey(mediaFormatId) ? _pendingFmtp[mediaFormatId] : null; - activeAnnouncement.MediaFormats.Add(mediaFormatId, new SDPAudioVideoMediaFormat(activeAnnouncement.Media, mediaFormatId, rtpmap, fmtp)); - } - } - else - { - logger.LogWarning("Non-numeric audio/video media format attribute in SDP: {sdpLine}", sdpLineTrimmedSpan.ToString()); - } - } - else - { - activeAnnouncement.AddExtra(sdpLineTrimmedSpan.ToString()); - } - } - else - { - // Parse the rtpmap attribute for NON audio/video announcements. - if (TrySplitAttributeValue( - l, - SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUTE_PREFIX.Length, - out var formatIDStart, - out var formatIDLength, - out var rtpmapStart)) - { - var formatID = l.Slice(formatIDStart, formatIDLength).ToString(); - var rtpmap = l.Slice(rtpmapStart).ToString(); - - if (activeAnnouncement.ApplicationMediaFormats.ContainsKey(formatID)) - { - activeAnnouncement.ApplicationMediaFormats[formatID] = activeAnnouncement.ApplicationMediaFormats[formatID].WithUpdatedRtpmap(rtpmap); - } - else - { - activeAnnouncement.ApplicationMediaFormats.Add(formatID, new SDPApplicationMediaFormat(formatID, rtpmap, null)); - } - } - else - { - activeAnnouncement.AddExtra(sdpLineTrimmedSpan.ToString()); - } - } - } - else - { - logger.LogWarning("There was no active media announcement for a media format attribute, ignoring."); - } - break; + if (count >= 1) + { + var sctpPortSpan = attrValue[fields[0]]; + if (ushort.TryParse(sctpPortSpan, out var sctpPort)) + { + activeAnnouncement.SctpPort = sctpPort; + } + else + { + logger.LogSdpInvalidSctpPort(sctpPortSpan.ToString()); + } + } - case var l when l.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_PARAMETERS_ATTRIBUE_PREFIX, StringComparison.Ordinal): - if (activeAnnouncement != null) - { - if (activeAnnouncement.Media == SDPMediaTypesEnum.audio || activeAnnouncement.Media == SDPMediaTypesEnum.video || activeAnnouncement.Media == SDPMediaTypesEnum.text) - { - // Parse the fmtp attribute for audio/video announcements. - if (TrySplitAttributeValue( - l, - SDPMediaAnnouncement.MEDIA_FORMAT_PARAMETERS_ATTRIBUE_PREFIX.Length, - out var avFormatIDStart, - out var avFormatIDLength, - out var fmtpStart)) - { - var avFormatID = l.Slice(avFormatIDStart, avFormatIDLength); - if (int.TryParse(avFormatID, out var fmtpFormatId)) - { - var fmtp = l.Slice(fmtpStart).ToString(); - if (activeAnnouncement.MediaFormats.ContainsKey(fmtpFormatId)) - { - activeAnnouncement.MediaFormats[fmtpFormatId] = activeAnnouncement.MediaFormats[fmtpFormatId].WithUpdatedFmtp(fmtp); - } - else - { - // Store the fmtp attribute for use when the rtpmap attribute turns up. - if (_pendingFmtp.ContainsKey(fmtpFormatId)) - { - _pendingFmtp.Remove(fmtpFormatId); - } - _pendingFmtp.Add(fmtpFormatId, fmtp); - } - } - else - { - logger.LogWarning("Invalid media format parameter attribute in SDP: {sdpLine}", sdpLineTrimmedSpan.ToString()); - } - } - else - { - activeAnnouncement.AddExtra(sdpLineTrimmedSpan.ToString()); - } - } - else - { - // Parse the fmtp attribute for NON audio/video announcements. - if (TrySplitAttributeValue( - l, - SDPMediaAnnouncement.MEDIA_FORMAT_PARAMETERS_ATTRIBUE_PREFIX.Length, - out var formatIDStart, - out var formatIDLength, - out var fmtpStart)) - { - var formatID = l.Slice(formatIDStart, formatIDLength).ToString(); - var fmtp = l.Slice(fmtpStart).ToString(); - - if (activeAnnouncement.ApplicationMediaFormats.ContainsKey(formatID)) - { - activeAnnouncement.ApplicationMediaFormats[formatID] = activeAnnouncement.ApplicationMediaFormats[formatID].WithUpdatedFmtp(fmtp); - } - else - { - activeAnnouncement.ApplicationMediaFormats.Add(formatID, new SDPApplicationMediaFormat(formatID, null, fmtp)); - } - } - else - { - activeAnnouncement.AddExtra(sdpLineTrimmedSpan.ToString()); - } - } - } - else - { - logger.LogWarning("There was no active media announcement for a media format parameter attribute, ignoring."); - } - break; - - case var _ when sdpLineTrimmedSpan.StartsWith(SDPSecurityDescription.CRYPTO_ATTRIBUE_PREFIX, StringComparison.Ordinal): - //2018-12-21 rj2: add a=crypto - if (activeAnnouncement != null) - { - try - { - activeAnnouncement.AddCryptoLine(sdpLineTrimmedSpan.ToString()); - } - catch (FormatException fex) - { - logger.LogWarning("Error Parsing SDP-Line(a=crypto) {Exception}", fex); - } - } - break; + if (count >= 3) + { + var maxMessageSizeSpan = attrValue[fields[2]]; + if (!long.TryParse(maxMessageSizeSpan, out activeAnnouncement.MaxMessageSize)) + { + logger.LogSdpInvalidMaxMessageSize(maxMessageSizeSpan.ToString()); + } + } + } + else + { + logger.LogSdpSctpMapOnlyOnAnnouncement(); + } + } + else if (key.SequenceEqual(SDPMediaAnnouncement.MEDIA_FORMAT_SCTP_PORT_ATTRIBUE_NAME.AsSpan())) + { + if (activeAnnouncement is { }) + { + if (ushort.TryParse(attrValue, out var sctpPort)) + { + activeAnnouncement.SctpPort = sctpPort; + } + else + { + logger.LogSdpInvalidSctpPort(attrValue.ToString()); + } + } + else + { + logger.LogSdpSctpPortOnlyOnAnnouncement(); + } + } + else if (key.SequenceEqual(SDPMediaAnnouncement.MEDIA_FORMAT_MAX_MESSAGE_SIZE_ATTRIBUE_NAME.AsSpan())) + { + if (activeAnnouncement is { }) + { + if (!long.TryParse(attrValue, out activeAnnouncement.MaxMessageSize)) + { + logger.LogSdpInvalidMaxMessageSize(attrValue.ToString()); + } + } + else + { + logger.LogSdpMaxMessageSizeOnlyOnAnnouncement(); + } + } + else if (key.SequenceEqual(SDPMediaAnnouncement.MEDIA_FORMAT_PATH_ACCEPT_TYPES_NAME.AsSpan())) + { + if (activeAnnouncement is { }) + { + var acceptTypesList = attrValue.Trim().SplitToList(' '); + activeAnnouncement.MessageMediaFormat.AcceptTypes = acceptTypesList; + } + else + { + logger.LogSdpAcceptTypesOnlyOnAnnouncement(); + } + } + else if (key.SequenceEqual(SDPMediaAnnouncement.MEDIA_FORMAT_PATH_MSRP_NAME.AsSpan())) + { + const string mediaFormatPathMsrpSchemeAndDelimiter = SDPMediaAnnouncement.MEDIA_FORMAT_PATH_MSRP_SCHEME + "://"; + if (activeAnnouncement is { } && attrValue.StartsWith(mediaFormatPathMsrpSchemeAndDelimiter.AsSpan())) + { + const int mediaFormatPathMsrpSchemeAndDelimiterLength = 7; + Debug.Assert(mediaFormatPathMsrpSchemeAndDelimiterLength == mediaFormatPathMsrpSchemeAndDelimiter.Length); - case var _ when StartsWithAttribute(sdpLineTrimmedSpan, MEDIA_ID_ATTRIBUTE_PREFIX): - if (activeAnnouncement != null) - { - activeAnnouncement.MediaID = SliceAfterColon(sdpLineTrimmedSpan).ToString(); - } - else - { - logger.LogWarning("A media ID can only be set on a media announcement."); - } - break; + attrValue = attrValue.Slice(mediaFormatPathMsrpSchemeAndDelimiterLength); + var messageMediaFormatIP = attrValue.Slice(0, attrValue.IndexOf(':')); + activeAnnouncement.MessageMediaFormat.IP = messageMediaFormatIP.ToString(); - case var _ when sdpLineTrimmedSpan.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_PREFIX, StringComparison.Ordinal): - if (activeAnnouncement != null) - { - var fields = SliceAfterColon(sdpLineTrimmedSpan); - var fieldIndex = 0; + attrValue = attrValue.Slice(messageMediaFormatIP.Length + 1); + var messageMediaFormatPort = attrValue.Slice(0, attrValue.IndexOf('/')); + activeAnnouncement.MessageMediaFormat.Port = messageMediaFormatPort.ToString(); - // Set the ID. - foreach (var fieldRange in fields.Split(' ')) - { - var ssrcField = fields[fieldRange]; - if (fieldIndex == 0) - { - activeAnnouncement.SsrcGroupID = ssrcField.ToString(); - } - else if (uint.TryParse(ssrcField, out var ssrc)) - { - // Add attributes for each of the SSRC values. - activeAnnouncement.SsrcAttributes.Add(new SDPSsrcAttribute(ssrc, null, activeAnnouncement.SsrcGroupID)); - } - - fieldIndex++; - } - } - else - { - logger.LogWarning("A ssrc-group ID can only be set on a media announcement."); - } - break; + attrValue = attrValue.Slice(messageMediaFormatPort.Length + 1); + var messageMediaFormatEndpoint = attrValue; + activeAnnouncement.MessageMediaFormat.Endpoint = messageMediaFormatEndpoint.ToString(); + } + else + { + logger.LogSdpPathOnlyOnAnnouncement(); + } + } + else if (MediaStreamStatusType.IsMediaStreamStatusAttribute(line.ToString(), out var mediaStreamStatus)) + { + if (activeAnnouncement is { }) + { + activeAnnouncement.MediaStreamStatus = mediaStreamStatus; + } + else + { + sdp.SessionMediaStreamStatus = mediaStreamStatus; + } + } + } - case var _ when sdpLineTrimmedSpan.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_SSRC_ATTRIBUE_PREFIX, StringComparison.Ordinal): - if (activeAnnouncement != null) - { - var ssrcFields = SliceAfterColon(sdpLineTrimmedSpan); - var ssrcField = default(ReadOnlySpan); - var cnameField = default(ReadOnlySpan); - var fieldIndex = 0; + /// ^(?<id>\d+)\s+(?<attribute>.*)$ + static bool TryParseNumericIdAndStringAttribute(ReadOnlySpan input, out int id, [NotNullWhen(true)] out string? attribute) + { + id = default; + attribute = default; - foreach (var fieldRange in ssrcFields.Split(' ')) - { - if (fieldIndex == 0) - { - ssrcField = ssrcFields[fieldRange]; - } - else if (fieldIndex == 1) - { - cnameField = ssrcFields[fieldRange]; - break; - } - - fieldIndex++; - } + var digitEnd = input.IndexOfAnyExcept(SearchValues.DigitChars); - if (uint.TryParse(ssrcField, out var ssrc)) - { - var ssrcAttribute = activeAnnouncement.SsrcAttributes.FirstOrDefault(x => x.SSRC == ssrc); - if (ssrcAttribute == null) - { - ssrcAttribute = new SDPSsrcAttribute(ssrc, null, null); - activeAnnouncement.SsrcAttributes.Add(ssrcAttribute); - } - - if (!cnameField.IsEmpty && - cnameField.StartsWith(SDPSsrcAttribute.MEDIA_CNAME_ATTRIBUE_PREFIX, StringComparison.Ordinal)) - { - ssrcAttribute.Cname = cnameField.Slice(cnameField.IndexOf(':') + 1).ToString(); - } - } - } - else - { - logger.LogWarning("An ssrc attribute can only be set on a media announcement."); - } - break; + if (digitEnd <= 0) + { + // No digits at start or input is all digits (no attribute) + return false; + } - case var _ when TryParseMediaStreamStatus(sdpLineTrimmedSpan, out var mediaStreamStatus): - if (activeAnnouncement != null) - { - activeAnnouncement.MediaStreamStatus = mediaStreamStatus; - } - else - { - sdp.SessionMediaStreamStatus = mediaStreamStatus; - } - break; + _ = int.TryParse(input[..digitEnd], out id); // not expected to fail - case var _ when sdpLineTrimmedSpan.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_SCTP_MAP_ATTRIBUE_PREFIX, StringComparison.Ordinal): - if (activeAnnouncement != null) - { - var sctpMapFields = SliceAfterColon(sdpLineTrimmedSpan); - activeAnnouncement.SctpMap = sctpMapFields.ToString(); + input = input[digitEnd..]; + var nonWhitespaceIndex = input.IndexOfAnyExcept(SearchValues.WhiteSpaceChars); - var sctpPortField = default(ReadOnlySpan); - var maxMessageSizeField = default(ReadOnlySpan); - var fieldIndex = 0; + if (nonWhitespaceIndex < 0) + { + // No non white spaces after id + return false; + } - foreach (var fieldRange in sctpMapFields.Split(' ')) - { - if (fieldIndex == 0) - { - sctpPortField = sctpMapFields[fieldRange]; - } - else if (fieldIndex == 2) - { - maxMessageSizeField = sctpMapFields[fieldRange]; - break; - } - - fieldIndex++; - } + attribute = input[nonWhitespaceIndex..].ToString(); + return true; + } - if (ushort.TryParse(sctpPortField, out var sctpPort)) - { - activeAnnouncement.SctpPort = sctpPort; - } - else - { - logger.LogWarning("An sctp-port value of {sctpPortStr} was not recognised as a valid port.", sctpPortField.ToString()); - } + /// ^(?<id>\S+)\s+(?<attribute>.*)$ + static bool TryParseStringIdAndStringAttribute( + ReadOnlySpan input, + [NotNullWhen(true)] out string? id, + [NotNullWhen(true)] out string? attribute) + { + id = default; + attribute = default; - if (!long.TryParse(maxMessageSizeField, out activeAnnouncement.MaxMessageSize)) - { - logger.LogWarning("A max-message-size value of {maxMessageSizeStr} was not recognised as a valid long.", maxMessageSizeField.ToString()); - } - } - else - { - logger.LogWarning("An sctpmap attribute can only be set on a media announcement."); - } - break; + // Find the first whitespace (end of ID) + var idEnd = input.IndexOfAny(SearchValues.WhiteSpaceChars); + if (idEnd <= 0) + { + // Either starts with whitespace or no whitespace at all + return false; + } - case var _ when sdpLineTrimmedSpan.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_SCTP_PORT_ATTRIBUE_PREFIX, StringComparison.Ordinal): - if (activeAnnouncement != null) - { - var sctpPortStr = SliceAfterColon(sdpLineTrimmedSpan); + id = input[..idEnd].ToString(); - if (ushort.TryParse(sctpPortStr, out var sctpPort)) - { - activeAnnouncement.SctpPort = sctpPort; - } - else - { - logger.LogWarning("An sctp-port value of {sctpPortStr} was not recognised as a valid port.", sctpPortStr.ToString()); - } - } - else - { - logger.LogWarning("An sctp-port attribute can only be set on a media announcement."); - } - break; + // Skip all whitespace after the ID + var attrStart = input[idEnd..].IndexOfAnyExcept(SearchValues.WhiteSpaceChars); + attribute = attrStart == -1 + ? string.Empty + : input[(idEnd + attrStart)..].ToString(); - case var _ when sdpLineTrimmedSpan.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_MAX_MESSAGE_SIZE_ATTRIBUE_PREFIX, StringComparison.Ordinal): - if (activeAnnouncement != null) - { - var maxMessageSizeStr = SliceAfterColon(sdpLineTrimmedSpan); - if (!long.TryParse(maxMessageSizeStr, out activeAnnouncement.MaxMessageSize)) - { - logger.LogWarning("A max-message-size value of {maxMessageSizeStr} was not recognised as a valid long.", maxMessageSizeStr.ToString()); - } - } - else - { - logger.LogWarning("A max-message-size attribute can only be set on a media announcement."); - } - break; + return true; + } - case var _ when sdpLineTrimmedSpan.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_PATH_ACCEPT_TYPES_PREFIX, StringComparison.Ordinal): - if (activeAnnouncement != null) - { - var acceptTypes = SliceAfterColon(sdpLineTrimmedSpan).Trim(); - var acceptTypesList = new List(); - foreach (var acceptTypeRange in acceptTypes.Split(' ')) - { - acceptTypesList.Add(acceptTypes[acceptTypeRange].ToString()); - } - activeAnnouncement.MessageMediaFormat.AcceptTypes = acceptTypesList; - } - else - { - logger.LogWarning("A accept-types attribute can only be set on a media announcement."); - } - break; - case var _ when sdpLineTrimmedSpan.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_PATH_MSRP_PREFIX, StringComparison.Ordinal): - if (activeAnnouncement != null) - { - var pathStr = SliceAfterColon(sdpLineTrimmedSpan); - var pathTrimmedStr = pathStr.Slice(pathStr.IndexOf(':') + 3); - activeAnnouncement.MessageMediaFormat.IP = pathTrimmedStr.Slice(0, pathTrimmedStr.IndexOf(':')).ToString(); + /// ^(?<id>\d+)\s+(?<url>.*)$ + static bool TryParseNumericIdAndUrl( + ReadOnlySpan input, + out int id, + [NotNullWhen(true)] out string? url) + { + id = default; + url = default; - pathTrimmedStr = pathTrimmedStr.Slice(pathTrimmedStr.IndexOf(':') + 1); - activeAnnouncement.MessageMediaFormat.Port = pathTrimmedStr.Slice(0, pathTrimmedStr.IndexOf('/')).ToString(); + // Find where the digits end + var digitEnd = input.IndexOfAnyExcept(SearchValues.DigitChars); + if (digitEnd <= 0 || digitEnd >= input.Length) + { + return false; + } - pathTrimmedStr = pathTrimmedStr.Slice(pathTrimmedStr.IndexOf('/') + 1); - activeAnnouncement.MessageMediaFormat.Endpoint = pathTrimmedStr.ToString(); + // Expect exactly one space after the digits + if (input[digitEnd] != ' ') + { + return false; + } - } - else - { - logger.LogWarning("A path attribute can only be set on a media announcement."); - } - break; + _ = int.TryParse(input[..digitEnd], out id); // not expected to fail - default: - if (activeAnnouncement != null) - { - activeAnnouncement.AddExtra(sdpLineTrimmedSpan.ToString()); - } - else - { - sdp.AddExtra(sdpLineTrimmedSpan.ToString()); - } - break; - } - } - } - finally + // The URL must be non-empty and contain no whitespace + var urlSpan = input[(digitEnd + 1)..]; + if (urlSpan.IsEmpty || urlSpan.IndexOfAny(SearchValues.WhiteSpaceChars) != -1) { - ArrayPool.Shared.Return(sdpLineRangeBuffer); + return false; } - return sdp; - } - else - { - return null; + url = urlSpan.ToString(); + return true; } + } - catch (Exception excp) - { - logger.LogError(excp, "Exception ParseSDPDescription. {ErrorMessage}", excp.Message); - throw; - } + + return sdp; + } + catch (Exception excp) + { + logger.LogSdpParseException(excp.Message, excp); + throw; } + } - public void AddExtra(string attribute) + public void AddExtra(string attribute) + { + if (!string.IsNullOrWhiteSpace(attribute)) { - if (!string.IsNullOrWhiteSpace(attribute)) - { - ExtraSessionAttributes.Add(attribute); - } + ExtraSessionAttributes.Add(attribute); } + } + + public override string ToString() + { + var builder = new ValueStringBuilder(); - public string RawString() + try { - if (string.IsNullOrWhiteSpace(this.m_rawSdp)) - { - return this.ToString(); - } - return this.m_rawSdp; + ToString(ref builder); + + return builder.ToString(); + } + finally + { + builder.Dispose(); } + } + + internal void ToString(ref ValueStringBuilder builder) + { + builder.Append("v="); + builder.Append(SDP_PROTOCOL_VERSION); + builder.Append(CRLF); + + builder.Append("o="); + builder.Append(Owner); + builder.Append(CRLF); - public override string ToString() + builder.Append("s="); + builder.Append(SessionName); + builder.Append(CRLF); + + if (Connection is { }) { - var sdp = new StringBuilder(); - sdp.Append("v=").Append(SDP_PROTOCOL_VERSION).Append(CRLF) - .Append("o=").Append(Owner).Append(CRLF) - .Append("s=").Append(SessionName).Append(CRLF); + Connection.ToString(ref builder); + } - if (Connection != null) - { - sdp.Append(Connection); - } + foreach (var bandwidth in BandwidthAttributes) + { + builder.Append("b="); + builder.Append(bandwidth); + builder.Append(CRLF); + } - foreach (string bandwidth in BandwidthAttributes) - { - sdp.Append("b=").Append(bandwidth).Append(CRLF); - } + builder.Append("t="); + builder.Append(Timing); + builder.Append(CRLF); - sdp.Append("t=").Append(Timing).Append(CRLF); + if (!string.IsNullOrWhiteSpace(IceUfrag)) + { + builder.Append("a="); + builder.Append(ICE_UFRAG_ATTRIBUTE_PREFIX); + builder.Append(":"); + builder.Append(IceUfrag); + builder.Append(CRLF); + } - if (!string.IsNullOrWhiteSpace(IceUfrag)) - { - sdp.Append("a=").Append(ICE_UFRAG_ATTRIBUTE_PREFIX).Append(':').Append(IceUfrag).Append(CRLF); - } + if (!string.IsNullOrWhiteSpace(IcePwd)) + { + builder.Append("a="); + builder.Append(ICE_PWD_ATTRIBUTE_PREFIX); + builder.Append(":"); + builder.Append(IcePwd); + builder.Append(CRLF); + } - if (!string.IsNullOrWhiteSpace(IcePwd)) - { - sdp.Append("a=").Append(ICE_PWD_ATTRIBUTE_PREFIX).Append(':').Append(IcePwd).Append(CRLF); - } + if (IceRole is { }) + { + builder.Append("a="); + builder.Append(SDP.ICE_SETUP_ATTRIBUTE_PREFIX); + builder.Append(":"); - if (IceRole != null) + if (IceRole is { } iceRole) { - sdp.Append("a=").Append(SDP.ICE_SETUP_ATTRIBUTE_PREFIX).Append(':').Append(IceRole).Append(CRLF); + builder.Append(iceRole.ToStringFast()); } - if (!string.IsNullOrWhiteSpace(DtlsFingerprint)) - { - sdp.Append("a=").Append(DTLS_FINGERPRINT_ATTRIBUTE_PREFIX).Append(':').Append(DtlsFingerprint).Append(CRLF); - } + builder.Append(CRLF); + } - if (IceCandidates?.Count > 0) - { - foreach (var candidate in IceCandidates) - { - sdp.Append("a=").Append(SDP.ICE_CANDIDATE_ATTRIBUTE_PREFIX).Append(':').Append(candidate).Append(CRLF); - } - } + if (!string.IsNullOrWhiteSpace(DtlsFingerprint)) + { + builder.Append("a="); + builder.Append(DTLS_FINGERPRINT_ATTRIBUTE_PREFIX); + builder.Append(":"); + builder.Append(DtlsFingerprint); + builder.Append(CRLF); + } - if (!string.IsNullOrWhiteSpace(SessionDescription)) + if (IceCandidates?.Count > 0) + { + foreach (var candidate in IceCandidates) { - sdp.Append("i=").Append(SessionDescription).Append(CRLF); + builder.Append("a="); + builder.Append(SDP.ICE_CANDIDATE_ATTRIBUTE_PREFIX); + builder.Append(":"); + candidate.ToString(ref builder); + builder.Append(CRLF); } + } - if (!string.IsNullOrWhiteSpace(URI)) - { - sdp.Append("u=").Append(URI).Append(CRLF); - } + if (!string.IsNullOrWhiteSpace(SessionDescription)) + { + builder.Append("i="); + builder.Append(SessionDescription); + builder.Append(CRLF); + } - if (OriginatorEmailAddresses != null && OriginatorEmailAddresses.Length > 0) + if (!string.IsNullOrWhiteSpace(URI)) + { + builder.Append("u="); + builder.Append(URI); + builder.Append(CRLF); + } + + if (OriginatorEmailAddresses is { }) + { + foreach (var originatorAddress in OriginatorEmailAddresses) { - foreach (string originatorAddress in OriginatorEmailAddresses) + if (!string.IsNullOrWhiteSpace(originatorAddress)) { - if (!string.IsNullOrWhiteSpace(originatorAddress)) - { - sdp.Append("e=").Append(originatorAddress).Append(CRLF); - } + builder.Append("e="); + builder.Append(originatorAddress); + builder.Append(CRLF); } } + } - if (OriginatorPhoneNumbers != null && OriginatorPhoneNumbers.Length > 0) + if (OriginatorPhoneNumbers is { }) + { + foreach (var originatorNumber in OriginatorPhoneNumbers) { - foreach (string originatorNumber in OriginatorPhoneNumbers) + if (!string.IsNullOrWhiteSpace(originatorNumber)) { - if (!string.IsNullOrWhiteSpace(originatorNumber)) - { - sdp.Append("p=").Append(originatorNumber).Append(CRLF); - } + builder.Append("p="); + builder.Append(originatorNumber); + builder.Append(CRLF); } } + } - if (Group != null) - { - sdp.Append("a=").Append(GROUP_ATRIBUTE_PREFIX).Append(':').Append(Group).Append(CRLF); - } + if (Group is { }) + { + builder.Append("a="); + builder.Append(GROUP_ATRIBUTE_PREFIX); + builder.Append(":"); + builder.Append(Group); + builder.Append(CRLF); + } - foreach (string extra in ExtraSessionAttributes) + foreach (var extra in ExtraSessionAttributes) + { + if (!string.IsNullOrWhiteSpace(extra)) { - if (!string.IsNullOrWhiteSpace(extra)) - { - sdp.Append(extra).Append(CRLF); - } + builder.Append(extra); + builder.Append(CRLF); } + } + + if (SessionMediaStreamStatus is { }) + { + builder.Append(MediaStreamStatusType.GetAttributeForMediaStreamStatus(SessionMediaStreamStatus.Value)); + builder.Append(CRLF); + } - if (SessionMediaStreamStatus != null) + if (Media.Count > 0) + { + Media.Sort((a, b) => { - sdp.Append(MediaStreamStatusType.GetAttributeForMediaStreamStatus(SessionMediaStreamStatus.Value)).Append(CRLF); - } + var cmp = a.MLineIndex.CompareTo(b.MLineIndex); + return cmp != 0 ? cmp : string.Compare(a.MediaID, b.MediaID, StringComparison.Ordinal); + }); - //foreach (SDPMediaAnnouncement media in Media.OrderBy(x => x.MLineIndex).ThenBy(x => x.MediaID)) - foreach (SDPMediaAnnouncement media in Media.OrderBy(x => x.MLineIndex).ThenBy(x => x.MediaID)) + foreach (var media in Media) { - if (media != null) + if (media is { }) { - sdp.Append(media); + media.ToString(ref builder); } } - - return sdp.ToString(); } + } - /// - /// A convenience method to get the RTP end point for single audio offer SDP payloads. - /// - /// The RTP end point for the first media end point. - public IPEndPoint GetSDPRTPEndPoint() - { - // Find first media offer. - var sessionConnection = Connection; - var firstMediaOffer = Media.FirstOrDefault(); + /// + /// A convenience method to get the RTP end point for single audio offer SDP payloads. + /// + /// The RTP end point for the first media end point. + public IPEndPoint? GetSDPRTPEndPoint() + { + // Find first media offer. + var sessionConnection = Connection; + SDPMediaAnnouncement? firstMediaOffer = Media.Count != 0 ? Media[0] : null; - if (sessionConnection != null && firstMediaOffer != null) - { - return new IPEndPoint(IPAddress.Parse(sessionConnection.ConnectionAddress), firstMediaOffer.Port); - } - else if (firstMediaOffer != null && firstMediaOffer.Connection != null) - { - return new IPEndPoint(IPAddress.Parse(firstMediaOffer.Connection.ConnectionAddress), firstMediaOffer.Port); - } - else - { - return null; - } + if (sessionConnection is { } && firstMediaOffer is { }) + { + Debug.Assert(sessionConnection.ConnectionAddress is { }); + return new IPEndPoint(IPAddress.Parse(sessionConnection.ConnectionAddress), firstMediaOffer.Port); } - - /// - /// A convenience method to get the RTP end point for single audio offer SDP payloads. - /// - /// A string representing the SDP payload. - /// The RTP end point for the first media end point. - public static IPEndPoint GetSDPRTPEndPoint(string sdpMessage) + else if (firstMediaOffer is { } && firstMediaOffer.Connection is { }) { - return ParseSDPDescription(sdpMessage) - .GetSDPRTPEndPoint(); + Debug.Assert(firstMediaOffer.Connection?.ConnectionAddress is { }); + return new IPEndPoint(IPAddress.Parse(firstMediaOffer.Connection.ConnectionAddress), firstMediaOffer.Port); } - - /// - /// Gets the media stream status for the specified media announcement. - /// - /// The type of media (audio, video etc) to get the status for. - /// THe index of the announcement to get the status for. - /// The media stream status set on the announcement or if there is none the session. If - /// there is also no status set on the session then the default value of sendrecv is returned. - public MediaStreamStatusEnum GetMediaStreamStatus(SDPMediaTypesEnum mediaType, int announcementIndex) + else { - var announcements = Media.Where(x => x.Media == mediaType).ToList(); + return null; + } + } - if (announcements == null || announcements.Count() < announcementIndex + 1) - { - return DEFAULT_STREAM_STATUS; - } - else + /// + /// A convenience method to get the RTP end point for single audio offer SDP payloads. + /// + /// A string representing the SDP payload. + /// The RTP end point for the first media end point. + public static IPEndPoint? GetSDPRTPEndPoint(string sdpMessage) + { + var sdp = ParseSDPDescription(sdpMessage.AsSpan()); + Debug.Assert(sdp is { }); + return sdp.GetSDPRTPEndPoint(); + } + + /// + /// Gets the media stream status for the specified media announcement. + /// + /// The type of media (audio, video etc) to get the status for. + /// THe index of the announcement to get the status for. + /// The media stream status set on the announcement or if there is none the session. If + /// there is also no status set on the session then the default value of sendrecv is returned. + public MediaStreamStatusEnum GetMediaStreamStatus(SDPMediaTypesEnum mediaType, int announcementIndex) + { + var foundIndex = 0; + + foreach (var media in Media) + { + if (media.Media == mediaType) { - var announcement = announcements[announcementIndex]; - return announcement.MediaStreamStatus.HasValue ? announcement.MediaStreamStatus.Value : DEFAULT_STREAM_STATUS; + if (foundIndex == announcementIndex) + { + return media.MediaStreamStatus.GetValueOrDefault(DEFAULT_STREAM_STATUS); + } + + foundIndex++; } } - /// - /// Media announcements can be placed in SDP in any order BUT the orders must match - /// up in offer/answer pairs. This method can be used to get the index for a specific - /// media type. It is useful for obtaining the index of a particular media type when - /// constructing an SDP answer. - /// - /// - public (int, string) GetIndexForMediaType(SDPMediaTypesEnum mediaType, int mediaIndex) + return DEFAULT_STREAM_STATUS; + } + + /// + /// Media announcements can be placed in SDP in any order BUT the orders must match + /// up in offer/answer pairs. This method can be used to get the index for a specific + /// media type. It is useful for obtaining the index of a particular media type when + /// constructing an SDP answer. + /// + /// + public (int, string?) GetIndexForMediaType(SDPMediaTypesEnum mediaType, int mediaIndex) + { + var fullIndex = 0; + var mIndex = 0; + foreach (var ann in Media) { - int fullIndex = 0; - int mIndex = 0; - foreach (var ann in Media) + if (ann.Media == mediaType) { - if (ann.Media == mediaType) + if (mIndex == mediaIndex) { - if (mIndex == mediaIndex) - { - return (fullIndex, ann.MediaID); - } - mIndex++; + return (fullIndex, ann.MediaID); } - fullIndex++; + mIndex++; } - - return (MEDIA_INDEX_NOT_PRESENT, MEDIA_INDEX_TAG_NOT_PRESENT); + fullIndex++; } + + return (MEDIA_INDEX_NOT_PRESENT, MEDIA_INDEX_TAG_NOT_PRESENT); } } diff --git a/src/SIPSorcery/net/SDP/SDPApplicationMediaFormat.cs b/src/SIPSorcery/net/SDP/SDPApplicationMediaFormat.cs index ef1e5aa884..1269e4a978 100644 --- a/src/SIPSorcery/net/SDP/SDPApplicationMediaFormat.cs +++ b/src/SIPSorcery/net/SDP/SDPApplicationMediaFormat.cs @@ -15,43 +15,42 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public struct SDPApplicationMediaFormat { - public struct SDPApplicationMediaFormat + public string ID; + + public string? Rtpmap; + + public string? Fmtp; + + public SDPApplicationMediaFormat(string id) + { + ID = id; + Rtpmap = null; + Fmtp = null; + } + + public SDPApplicationMediaFormat(string id, string? rtpmap, string? fmtp) { - public string ID; - - public string Rtpmap; - - public string Fmtp; - - public SDPApplicationMediaFormat(string id) - { - ID = id; - Rtpmap = null; - Fmtp = null; - } - - public SDPApplicationMediaFormat(string id, string rtpmap, string fmtp) - { - ID = id; - Rtpmap = rtpmap; - Fmtp = fmtp; - } - - /// - /// Creates a new media format based on an existing format but with a different ID. - /// The typical case for this is during the SDP offer/answer exchange the dynamic format ID's for the - /// equivalent type need to be adjusted by one party. - /// - /// The ID to set on the new format. - public SDPApplicationMediaFormat WithUpdatedID(string id) => - new SDPApplicationMediaFormat(id, Rtpmap, Fmtp); - - public SDPApplicationMediaFormat WithUpdatedRtpmap(string rtpmap) => - new SDPApplicationMediaFormat(ID, rtpmap, Fmtp); - - public SDPApplicationMediaFormat WithUpdatedFmtp(string fmtp) => - new SDPApplicationMediaFormat(ID, Rtpmap, fmtp); + ID = id; + Rtpmap = rtpmap; + Fmtp = fmtp; } + + /// + /// Creates a new media format based on an existing format but with a different ID. + /// The typical case for this is during the SDP offer/answer exchange the dynamic format ID's for the + /// equivalent type need to be adjusted by one party. + /// + /// The ID to set on the new format. + public SDPApplicationMediaFormat WithUpdatedID(string id) => + new SDPApplicationMediaFormat(id, Rtpmap, Fmtp); + + public SDPApplicationMediaFormat WithUpdatedRtpmap(string rtpmap) => + new SDPApplicationMediaFormat(ID, rtpmap, Fmtp); + + public SDPApplicationMediaFormat WithUpdatedFmtp(string fmtp) => + new SDPApplicationMediaFormat(ID, Rtpmap, fmtp); } diff --git a/src/SIPSorcery/net/SDP/SDPAudioVideoMediaFormat.cs b/src/SIPSorcery/net/SDP/SDPAudioVideoMediaFormat.cs index 1e0da6761a..ba5ddd44d6 100644 --- a/src/SIPSorcery/net/SDP/SDPAudioVideoMediaFormat.cs +++ b/src/SIPSorcery/net/SDP/SDPAudioVideoMediaFormat.cs @@ -16,650 +16,647 @@ using System; using System.Collections.Generic; -using System.Linq; -using Polyfills; +using System.Diagnostics; using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// Represents a single media format within a media announcement. Often the whole media format can be represented and +/// described by a single character, e.g. "0" without additional info represents standard "PCMU", "8" represents "PCMA" +/// etc. For other media types that have variable parameters additional attributes can be provided. +/// +/// +/// This struct is designed to be immutable. If new information becomes available for a media format, such as when +/// parsing further into an SDP payload, a new media format should be created. TODO: With C#9 the struct could become a +/// "record" type. +/// +public struct SDPAudioVideoMediaFormat { + public const int DYNAMIC_ID_MIN = 96; + public const int DYNAMIC_ID_MAX = 127; + public const int DEFAULT_AUDIO_CHANNEL_COUNT = 1; + + public static SDPAudioVideoMediaFormat Empty; + /// - /// Represents a single media format within a media announcement. Often the whole media format can - /// be represented and described by a single character, e.g. "0" without additional info represents - /// standard "PCMU", "8" represents "PCMA" etc. For other media types that have variable parameters - /// additional attributes can be provided. + /// Indicates whether the format is for audio or video. /// - /// This struct is designed to be immutable. If new information becomes available for a - /// media format, such as when parsing further into an SDP payload, a new media format should be - /// created. - /// TODO: With C#9 the struct could become a "record" type. - /// - public struct SDPAudioVideoMediaFormat - { - public const int DYNAMIC_ID_MIN = 96; - public const int DYNAMIC_ID_MAX = 127; - public const int DEFAULT_AUDIO_CHANNEL_COUNT = 1; - - public static SDPAudioVideoMediaFormat Empty = new SDPAudioVideoMediaFormat() { _isEmpty = true }; - - /// - /// Indicates whether the format is for audio or video. - /// - public SDPMediaTypesEnum Kind { get; } - - /// - /// The mandatory ID for the media format. Warning, even though some ID's are normally used to represent - /// a standard media type, e.g "0" for "PCMU" etc, there is no guarantee that's the case. "0" can be used - /// for any media format if there is a format attribute describing it. In the absence of a format attribute - /// then it is required that it represents a standard media type. - /// - /// Note (rj2): FormatID MUST be string (not int), in case ID is 't38' and type is 'image' - /// Note to above: The FormatID is always numeric for profile "RTP/AVP" and "RTP/SAVP", see - /// https://tools.ietf.org/html/rfc4566#section-5.14 and section on "fmt": - /// "If the [proto] sub-field is "RTP/AVP" or "RTP/SAVP" the [fmt] - /// sub-fields contain RTP payload type numbers" - /// In the case of T38 the format name is "t38" but the formatID must be set as a dynamic ID. - /// - /// // Example - /// // Note in this example "0" is representing a standard format so the format attribute is optional. - /// m=audio 12228 RTP/AVP 0 101 // "0" and "101" are media format ID's. - /// a=rtpmap:0 PCMU/8000 // "0" is the media format ID. - /// a=rtpmap:101 telephone-event/8000 // "101" is the media format ID. - /// a=fmtp:101 0-16 - /// - /// - /// // t38 example from https://tools.ietf.org/html/rfc4612. - /// m=audio 6800 RTP/AVP 0 98 - /// a=rtpmap:98 t38/8000 - /// a=fmtp:98 T38FaxVersion=2;T38FaxRateManagement=transferredTCF - /// - /// - public int ID { get; } - - /// - /// The optional rtpmap attribute properties for the media format. For standard media types this is not necessary. - /// - /// // Example - /// a=rtpmap:0 PCMU/8000 - /// a=rtpmap:101 telephone-event/8000 ← "101 telephone-event/8000" is the rtpmap properties. - /// a=fmtp:101 0-16 - /// - /// - public string Rtpmap { get; } - - /// - /// The optional format parameter attribute for the media format. For standard media types this is not necessary. - /// - /// // Example - /// a=rtpmap:0 PCMU/8000 - /// a=rtpmap:101 telephone-event/8000 - /// a=fmtp:101 0-16 ← "101 0-16" is the fmtp attribute. - /// - /// - public string Fmtp { get; } - - public IEnumerable SupportedRtcpFeedbackMessages - { - get - { - yield return "transport-cc"; - //yield return "goog-remb"; - } - } + public SDPMediaTypesEnum Kind { get; } - /// - /// The standard name of the media format. - /// - /// // Example - /// a=rtpmap:0 PCMU/8000 ← "PCMU" is the media format name. - /// a=rtpmap:101 telephone-event/8000 - /// a=fmtp:101 0-16 - /// - /// - //public string Name { get; set; } + /// + /// The mandatory ID for the media format. Warning, even though some ID's are normally used to represent a standard + /// media type, e.g "0" for "PCMU" etc, there is no guarantee that's the case. "0" can be used for any media format + /// if there is a format attribute describing it. In the absence of a format attribute then it is required that it + /// represents a standard media type. + /// Note (rj2): FormatID MUST be string (not int), in case ID is 't38' and type is 'image' Note to above: The + /// FormatID is always numeric for profile "RTP/AVP" and "RTP/SAVP", see + /// https://tools.ietf.org/html/rfc4566#section-5.14 and section on "fmt": "If the [proto] sub-field is "RTP/AVP" or + /// "RTP/SAVP" the [fmt] sub-fields contain RTP payload type numbers" In the case of T38 the format name is "t38" + /// but the formatID must be set as a dynamic ID. // Example // Note in this example "0" is representing a + /// standard format so the format attribute is optional. m=audio 12228 RTP/AVP 0 101 // "0" and "101" are media + /// format ID's. a=rtpmap:0 PCMU/8000 // "0" is the media format ID. a=rtpmap:101 telephone-event/8000 // "101" is + /// the media format ID. a=fmtp:101 0-16 // t38 example from https://tools.ietf.org/html/rfc4612. + /// m=audio 6800 RTP/AVP 0 98 a=rtpmap:98 t38/8000 a=fmtp:98 T38FaxVersion=2;T38FaxRateManagement=transferredTCF + /// + /// + public int ID { get; } - private bool _isEmpty; + /// + /// The optional rtpmap attribute properties for the media format. For standard media types this is not necessary. + /// // Example a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 ← "101 telephone-event/8000" is the + /// rtpmap properties. a=fmtp:101 0-16 + /// + public string? Rtpmap { get; } - /// - /// Creates a new SDP media format for a well known media type. Well known type are those that use - /// ID's less than 96 and don't require rtpmap or fmtp attributes. - /// - public SDPAudioVideoMediaFormat(SDPWellKnownMediaFormatsEnum knownFormat) + /// + /// The optional format parameter attribute for the media format. For standard media types this is not necessary. + /// // Example a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-16 ← "101 0-16" is the + /// fmtp attribute. + /// + public string? Fmtp { get; } + + public IEnumerable SupportedRtcpFeedbackMessages + { + get { - Kind = AudioVideoWellKnown.WellKnownAudioFormats.ContainsKey(knownFormat) ? SDPMediaTypesEnum.audio : - SDPMediaTypesEnum.video; + yield return "transport-cc"; + //yield return "goog-remb"; + } + } - ID = (int)knownFormat; - Rtpmap = null; - Fmtp = null; - _isEmpty = false; + /// + /// The standard name of the media format. // Example a=rtpmap:0 PCMU/8000 ← "PCMU" is the media format name. + /// a=rtpmap:101 telephone-event/8000 a=fmtp:101 0-16 + /// + //public string Name { get; set; } - if (Kind == SDPMediaTypesEnum.audio) - { - var audioFormat = AudioVideoWellKnown.WellKnownAudioFormats[knownFormat]; - Rtpmap = SetRtpmap(audioFormat.FormatName, audioFormat.RtpClockRate, audioFormat.ChannelCount); - } - else - { - var videoFormat = AudioVideoWellKnown.WellKnownVideoFormats[knownFormat]; - Rtpmap = SetRtpmap(videoFormat.FormatName, videoFormat.ClockRate, 0); - } - } + private bool _isNotEmpty; - public bool IsH264 - { - get - { - return Rtpmap.AsSpan().Trim().StartsWith("H264", StringComparison.OrdinalIgnoreCase); - } - } + /// + /// Creates a new SDP media format for a well known media type. Well known type are those that use ID's less than 96 + /// and don't require rtpmap or fmtp attributes. + /// + public SDPAudioVideoMediaFormat(SDPWellKnownMediaFormatsEnum knownFormat) + { + Kind = AudioVideoWellKnown.WellKnownAudioFormats.ContainsKey(knownFormat) ? SDPMediaTypesEnum.audio : + SDPMediaTypesEnum.video; - public bool IsAV1 - { - get - { - return MatchesCodecName("AV1"); - } - } + ID = (int)knownFormat; + Rtpmap = null; + Fmtp = null; + _isNotEmpty = true; - public bool IsMJPEG + if (Kind == SDPMediaTypesEnum.audio) { - get - { - return Rtpmap.AsSpan().Trim().StartsWith("JPEG", StringComparison.OrdinalIgnoreCase); - } + var audioFormat = AudioVideoWellKnown.WellKnownAudioFormats[knownFormat]; + Rtpmap = SetRtpmap(audioFormat.FormatName, audioFormat.RtpClockRate, audioFormat.ChannelCount); } - - public bool isH265 + else { - get - { - return Rtpmap.AsSpan().Trim().StartsWith("H265", StringComparison.OrdinalIgnoreCase); - } + var videoFormat = AudioVideoWellKnown.WellKnownVideoFormats[knownFormat]; + Rtpmap = SetRtpmap(videoFormat.FormatName, videoFormat.ClockRate, 0); } + } + + public bool IsH264 => RtmapIs("H264"); + + public bool IsAV1 => RtmapIs("AV1"); + + public bool IsMJPEG => RtmapIs("JPEG"); + + public bool isH265 => RtmapIs("H265"); + + private bool RtmapIs(ReadOnlySpan codec) => Rtpmap is not null && Rtpmap.AsSpan().TrimStart().StartsWith(codec, StringComparison.OrdinalIgnoreCase); - public bool CheckCompatible() + public bool CheckCompatible() + { + if (IsH264 || IsMJPEG || isH265) { - if (IsH264 || IsMJPEG || isH265) + Debug.Assert(Fmtp is { }); + var parameters = ParseWebRtcParameters(Fmtp); + if (parameters.TryGetValue("packetization-mode", out var packetizationMode)) { - var parameters = ParseWebRtcParameters(Fmtp); - if (parameters.TryGetValue("packetization-mode", out string packetizationMode)) + if (packetizationMode != "1") { - if (packetizationMode != "1") - { - return false; - } + return false; } } - return true; } + return true; + } - private static Dictionary ParseWebRtcParameters(string input) + private static Dictionary ParseWebRtcParameters(ReadOnlySpan input) + { + var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (input.IsEmpty) { - var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (string.IsNullOrEmpty(input)) - { - return parameters; - } - - Span keyValueRange = stackalloc Range[3]; - var inputSpan = input.AsSpan(); - foreach (var pairRange in inputSpan.Split(';')) - { - var pair = inputSpan[pairRange]; - if (pair.Split(keyValueRange, '=') == 2) - { - parameters[pair[keyValueRange[0]].Trim().ToString()] = pair[keyValueRange[1]].Trim().ToString(); - } - } - return parameters; } - private bool MatchesCodecName(string codecName) + Span keyValueRange = stackalloc Range[3]; + foreach (var pairRange in input.Split(';')) { - if (!TryParseRtpmap(Rtpmap, out var name, out _, out _)) + var pairSpan = input[pairRange]; + var pair = input[pairRange]; + if (pair.Split(keyValueRange, '=') == 2) { - return false; + parameters[pair[keyValueRange[0]].Trim().ToString()] = pair[keyValueRange[1]].Trim().ToString(); } + } - if (string.Equals(name, codecName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } + return parameters; + } - return name.Length > codecName.Length && - name.StartsWith(codecName, StringComparison.OrdinalIgnoreCase) && - !char.IsDigit(name[codecName.Length]); - } + /// + /// Creates a new SDP media format for a dynamic media type. Dynamic media types are those that use ID's between 96 + /// and 127 inclusive and require an rtpmap attribute and optionally an fmtp attribute. + /// + public SDPAudioVideoMediaFormat(SDPMediaTypesEnum kind, int id, string rtpmap, string? fmtp = null) + { + ArgumentOutOfRangeException.ThrowIfNegative(id); + ArgumentOutOfRangeException.ThrowIfGreaterThan(id, DYNAMIC_ID_MAX); + ArgumentException.ThrowIfNullOrWhiteSpace(rtpmap); + + Kind = kind; + ID = id; + Rtpmap = rtpmap; + Fmtp = fmtp; + _isNotEmpty = true; + } + /// + /// Creates a new SDP media format for a dynamic media type. Dynamic media types are those that use ID's between 96 + /// and 127 inclusive and require an rtpmap attribute and optionally an fmtp attribute. + /// + public SDPAudioVideoMediaFormat(SDPMediaTypesEnum kind, int id, string name, int clockRate, int channels = DEFAULT_AUDIO_CHANNEL_COUNT, string? fmtp = null) + { + ArgumentOutOfRangeException.ThrowIfNegative(id); + ArgumentOutOfRangeException.ThrowIfGreaterThan(id, DYNAMIC_ID_MAX); + ArgumentException.ThrowIfNullOrWhiteSpace(name); - /// - /// Creates a new SDP media format for a dynamic media type. Dynamic media types are those that use - /// ID's between 96 and 127 inclusive and require an rtpmap attribute and optionally an fmtp attribute. - /// - public SDPAudioVideoMediaFormat(SDPMediaTypesEnum kind, int id, string rtpmap, string fmtp = null) - { - if (id < 0 || id > DYNAMIC_ID_MAX) - { - throw new ApplicationException($"SDP media format IDs must be between 0 and {DYNAMIC_ID_MAX}."); - } - else if (string.IsNullOrWhiteSpace(rtpmap)) - { - throw new ArgumentNullException("rtpmap", "The rtpmap parameter cannot be empty for a dynamic SDPMediaFormat."); - } + Kind = kind; + ID = id; + Rtpmap = null; + Fmtp = fmtp; + _isNotEmpty = true; - Kind = kind; - ID = id; - Rtpmap = rtpmap; - Fmtp = fmtp; - _isEmpty = false; - } + Rtpmap = SetRtpmap(name, clockRate, channels); + } - /// - /// Creates a new SDP media format for a dynamic media type. Dynamic media types are those that use - /// ID's between 96 and 127 inclusive and require an rtpmap attribute and optionally an fmtp attribute. - /// - public SDPAudioVideoMediaFormat(SDPMediaTypesEnum kind, int id, string name, int clockRate, int channels = DEFAULT_AUDIO_CHANNEL_COUNT, string fmtp = null) - { - if (id < 0 || id > DYNAMIC_ID_MAX) - { - throw new ApplicationException($"SDP media format ID must be between 0 and {DYNAMIC_ID_MAX}."); - } - else if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException("name", "The name parameter cannot be empty for a dynamic SDPMediaFormat."); - } + /// + /// Creates a new SDP media format from a Audio Format instance. The Audio Format contains the equivalent + /// information to the SDP format object but has well defined audio properties separate from the SDP serialisation. + /// + /// The Audio Format to map to an SDP format. + public SDPAudioVideoMediaFormat(AudioFormat audioFormat) + { + Kind = SDPMediaTypesEnum.audio; + ID = audioFormat.FormatID; + Rtpmap = null; + Fmtp = audioFormat.Parameters; + _isNotEmpty = true; - Kind = kind; - ID = id; - Rtpmap = null; - Fmtp = fmtp; - _isEmpty = false; + Rtpmap = SetRtpmap(audioFormat.FormatName, audioFormat.RtpClockRate, audioFormat.ChannelCount); + } - Rtpmap = SetRtpmap(name, clockRate, channels); - } + /// + /// Creates a new SDP media format from a Video Format instance. The Video Format contains the equivalent + /// information to the SDP format object but has well defined video properties separate from the SDP serialisation. + /// + /// The Video Format to map to an SDP format. + public SDPAudioVideoMediaFormat(VideoFormat videoFormat) + { + Kind = SDPMediaTypesEnum.video; + ID = videoFormat.FormatID; + Rtpmap = null; + Fmtp = videoFormat.Parameters; + _isNotEmpty = true; - /// - /// Creates a new SDP media format from a Audio Format instance. The Audio Format contains the - /// equivalent information to the SDP format object but has well defined audio properties separate - /// from the SDP serialisation. - /// - /// The Audio Format to map to an SDP format. - public SDPAudioVideoMediaFormat(AudioFormat audioFormat) - { - Kind = SDPMediaTypesEnum.audio; - ID = audioFormat.FormatID; - Rtpmap = null; - Fmtp = audioFormat.Parameters; - _isEmpty = false; + Rtpmap = SetRtpmap(videoFormat.FormatName, videoFormat.ClockRate); + } - Rtpmap = SetRtpmap(audioFormat.FormatName, audioFormat.RtpClockRate, audioFormat.ChannelCount); - } + public SDPAudioVideoMediaFormat(TextFormat textFormat) + { + Kind = SDPMediaTypesEnum.text; + ID = textFormat.FormatID; + Rtpmap = null; + Fmtp = textFormat.Parameters; + _isNotEmpty = true; - /// - /// Creates a new SDP media format from a Video Format instance. The Video Format contains the - /// equivalent information to the SDP format object but has well defined video properties separate - /// from the SDP serialisation. - /// - /// The Video Format to map to an SDP format. - public SDPAudioVideoMediaFormat(VideoFormat videoFormat) - { - Kind = SDPMediaTypesEnum.video; - ID = videoFormat.FormatID; - Rtpmap = null; - Fmtp = videoFormat.Parameters; - _isEmpty = false; + Rtpmap = SetRtpmap(textFormat.FormatName, textFormat.ClockRate); + } - Rtpmap = SetRtpmap(videoFormat.FormatName, videoFormat.ClockRate); - } + private string SetRtpmap(string name, int clockRate, int channels = DEFAULT_AUDIO_CHANNEL_COUNT) => Kind is SDPMediaTypesEnum.video or SDPMediaTypesEnum.text + ? $"{name}/{clockRate}" + : (channels == DEFAULT_AUDIO_CHANNEL_COUNT) ? $"{name}/{clockRate}" : $"{name}/{clockRate}/{channels}"; - public SDPAudioVideoMediaFormat(TextFormat textFormat) + public bool IsEmpty() => !_isNotEmpty; + public int ClockRate() + { + if (Kind == SDPMediaTypesEnum.video) { - Kind = SDPMediaTypesEnum.text; - ID = textFormat.FormatID; - Rtpmap = null; - Fmtp = textFormat.Parameters; - _isEmpty = false; - - Rtpmap = SetRtpmap(textFormat.FormatName, textFormat.ClockRate); + return ToVideoFormat().ClockRate; } - - private string SetRtpmap(string name, int clockRate, int channels = DEFAULT_AUDIO_CHANNEL_COUNT) => Kind == SDPMediaTypesEnum.video || Kind == SDPMediaTypesEnum.text - ? $"{name}/{clockRate}" - : (channels == DEFAULT_AUDIO_CHANNEL_COUNT) ? $"{name}/{clockRate}" : $"{name}/{clockRate}/{channels}"; - - public bool IsEmpty() => _isEmpty; - public int ClockRate() + else if (Kind == SDPMediaTypesEnum.text) { - if (Kind == SDPMediaTypesEnum.video) - { - return ToVideoFormat().ClockRate; - } - else if (Kind == SDPMediaTypesEnum.text) - { - return ToTextFormat().ClockRate; - } - else - { - return ToAudioFormat().ClockRate; - } + return ToTextFormat().ClockRate; + } + else + { + return ToAudioFormat().ClockRate; } + } - public int Channels() => Kind == SDPMediaTypesEnum.video || Kind == SDPMediaTypesEnum.text - ? 0 - : TryParseRtpmap(Rtpmap, out _, out _, out var channels) ? channels : DEFAULT_AUDIO_CHANNEL_COUNT; + public int Channels() + => Kind is SDPMediaTypesEnum.video or SDPMediaTypesEnum.text + ? 0 + : TryParseRtpmap(Rtpmap.AsSpan(), out _, out _, out var channels) ? channels : DEFAULT_AUDIO_CHANNEL_COUNT; - public string Name() + public string? Name() + { + // Rtpmap taks priority over well known media type as ID's can be changed. + if (Rtpmap is { } && TryParseRtpmap(Rtpmap.AsSpan(), out var name, out _, out _)) { - // Rtpmap taks priority over well known media type as ID's can be changed. - if (Rtpmap != null && TryParseRtpmap(Rtpmap, out var name, out _, out _)) - { - return name; - } - else if (Enum.IsDefined(typeof(SDPWellKnownMediaFormatsEnum), ID)) - { - // If no rtpmap available then it must be a well known format. - return Enum.ToObject(typeof(SDPWellKnownMediaFormatsEnum), ID).ToString(); - } - else - { - return null; - } + return name; } + else if (SDPWellKnownMediaFormatsEnum.IsDefined((SDPWellKnownMediaFormatsEnum)ID)) + { + // If no rtpmap available then it must be a well known format. + return ((SDPWellKnownMediaFormatsEnum)ID).ToStringFast(); + } + else + { + return null; + } + } - /// - /// Creates a new media format based on an existing format but with a different ID. - /// The typical case for this is during the SDP offer/answer exchange the dynamic format ID's for the - /// equivalent type need to be adjusted by one party. - /// - /// The ID to set on the new format. - /// A new format. - public SDPAudioVideoMediaFormat WithUpdatedID(int id) => - new SDPAudioVideoMediaFormat(Kind, id, Rtpmap, Fmtp); + /// + /// Creates a new media format based on an existing format but with a different ID. The typical case for this is + /// during the SDP offer/answer exchange the dynamic format ID's for the equivalent type need to be adjusted by one + /// party. + /// + /// The ID to set on the new format. + /// A new format. + public SDPAudioVideoMediaFormat WithUpdatedID(int id) + { + Debug.Assert(!string.IsNullOrEmpty(Rtpmap)); + return new SDPAudioVideoMediaFormat(Kind, id, Rtpmap, Fmtp); + } - public SDPAudioVideoMediaFormat WithUpdatedRtpmap(string rtpmap) => - new SDPAudioVideoMediaFormat(Kind, ID, rtpmap, Fmtp); + public SDPAudioVideoMediaFormat WithUpdatedRtpmap(string rtpmap) + { + Debug.Assert(!string.IsNullOrEmpty(Rtpmap)); + return new SDPAudioVideoMediaFormat(Kind, ID, rtpmap, Fmtp); + } - public SDPAudioVideoMediaFormat WithUpdatedFmtp(string fmtp) => - new SDPAudioVideoMediaFormat(Kind, ID, Rtpmap, fmtp); + public SDPAudioVideoMediaFormat WithUpdatedFmtp(string fmtp) + { + Debug.Assert(!string.IsNullOrEmpty(Rtpmap)); + return new SDPAudioVideoMediaFormat(Kind, ID, Rtpmap, fmtp); + } - /// - /// Maps an audio SDP media type to a media abstraction layer audio format. - /// - /// An audio format value. - public AudioFormat ToAudioFormat() + /// + /// Maps an audio SDP media type to a media abstraction layer audio format. + /// + /// An audio format value. + public AudioFormat ToAudioFormat() + { + // Rtpmap takes priority over well known media type as ID's can be changed. + if (Rtpmap is { } && TryParseRtpmap(Rtpmap.AsSpan(), out var name, out var rtpClockRate, out var channels)) { - // Rtpmap takes priority over well known media type as ID's can be changed. - if (Rtpmap != null && TryParseRtpmap(Rtpmap, out var name, out int rtpClockRate, out int channels)) - { - int clockRate = rtpClockRate; + var clockRate = rtpClockRate; - // G722 is a special case. It's the only audio format that uses the wrong RTP clock rate. - // It sets 8000 in the SDP but then expects samples to be sent as 16KHz. - // See https://tools.ietf.org/html/rfc3551#section-4.5.2. - if (string.Equals(name, "G722", StringComparison.OrdinalIgnoreCase) && rtpClockRate == 8000) - { - clockRate = 16000; - } - - return new AudioFormat(ID, name?.ToUpper(), clockRate, rtpClockRate, channels, Fmtp); - } - else if (ID < DYNAMIC_ID_MIN - && Enum.TryParse(Name(), out var wellKnownFormat) - && AudioVideoWellKnown.WellKnownAudioFormats.ContainsKey(wellKnownFormat)) - { - return AudioVideoWellKnown.WellKnownAudioFormats[wellKnownFormat]; - } - else + // G722 is a special case. It's the only audio format that uses the wrong RTP clock rate. + // It sets 8000 in the SDP but then expects samples to be sent as 16KHz. + // See https://tools.ietf.org/html/rfc3551#section-4.5.2. + if (string.Equals(name, "G722", StringComparison.OrdinalIgnoreCase) && rtpClockRate == 8000) { - return AudioFormat.Empty; + clockRate = 16000; } + + name = name?.ToUpperInvariant(); + Debug.Assert(!string.IsNullOrWhiteSpace(name)); + return new AudioFormat(ID, name, clockRate, rtpClockRate, channels, Fmtp); + } + else if (ID < DYNAMIC_ID_MIN + && SDPWellKnownMediaFormatsEnumExtensions.TryParse(Name(), out var wellKnownFormat) + && AudioVideoWellKnown.WellKnownAudioFormats.TryGetValue(wellKnownFormat, out var value)) + { + return value; + } + else + { + return AudioFormat.Empty; } + } - /// - /// Maps a video SDP media type to a media abstraction layer video format. - /// - /// A video format value. - public VideoFormat ToVideoFormat() + /// + /// Maps a video SDP media type to a media abstraction layer video format. + /// + /// A video format value. + public VideoFormat ToVideoFormat() + { + // Rtpmap taks priority over well known media type as ID's can be changed. + // But we don't currently support any of the well known video types any way. + if (TryParseRtpmap(Rtpmap.AsSpan(), out var name, out var clockRate, out _)) { - // Rtpmap taks priority over well known media type as ID's can be changed. - // But we don't currently support any of the well known video types any way. - if (TryParseRtpmap(Rtpmap, out var name, out int clockRate, out _)) - { - return new VideoFormat(ID, name?.ToUpper(), clockRate, Fmtp); - } - else - { - return VideoFormat.Empty; - } + name = name?.ToUpperInvariant(); + Debug.Assert(!string.IsNullOrWhiteSpace(name)); + return new VideoFormat(ID, name, clockRate, Fmtp); + } + else + { + return VideoFormat.Empty; } + } - /// - /// Maps a video SDP media type to a media abstraction layer text format. - /// - /// A text format value. - public TextFormat ToTextFormat() + /// + /// Maps a video SDP media type to a media abstraction layer text format. + /// + /// A text format value. + public TextFormat ToTextFormat() + { + // Rtpmap taks priority over well known media type as ID's can be changed. + // But we don't currently support any of the well known text types any way. + if (TryParseRtpmap(Rtpmap.AsSpan(), out var name, out var clockRate, out _)) { - // Rtpmap taks priority over well known media type as ID's can be changed. - // But we don't currently support any of the well known text types any way. - if (TryParseRtpmap(Rtpmap, out var name, out int clockRate, out _)) - { - return new TextFormat(ID, name, clockRate, Fmtp); - } - else - { - return TextFormat.Empty; - } + name = name?.ToUpperInvariant(); + Debug.Assert(!string.IsNullOrWhiteSpace(name)); + return new TextFormat(ID, name, clockRate, Fmtp); } + else + { + return TextFormat.Empty; + } + } - /// - /// For two formats to be a match only the codec and rtpmap parameters need to match. The - /// fmtp parameter does not matter. - /// - public static bool AreMatch(SDPAudioVideoMediaFormat format1, SDPAudioVideoMediaFormat format2) + /// + /// For two formats to be a match only the codec and rtpmap parameters need to match. The fmtp parameter does not + /// matter. + /// + public static bool AreMatch(SDPAudioVideoMediaFormat format1, SDPAudioVideoMediaFormat format2) + { + // rtpmap takes priority as well known format ID's can be overruled. + if (format1.Rtpmap is { } && format2.Rtpmap is { }) { - // rtpmap takes priority as well known format ID's can be overruled. - if (format1.Rtpmap != null && format2.Rtpmap != null) - { - if (format1.Rtpmap.AsSpan().Trim().Equals(format2.Rtpmap.AsSpan().Trim(), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - if (format1.ID < DYNAMIC_ID_MIN - && format1.ID == format2.ID - && string.Equals(format1.Name(), format2.Name(), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(format1.Rtpmap.Trim(), format2.Rtpmap.Trim(), StringComparison.OrdinalIgnoreCase)) { - // Well known format type. return true; } - return false; - } - - /// - /// Attempts to get the compatible formats between two lists. Formats for - /// "RTP Events" are not included. - /// - /// The first list to match the media formats for. - /// The second list to match the media formats for. - /// A list of media formats that are compatible for BOTH lists. - public static List GetCompatibleFormats(List a, List b) + if (format1.ID < DYNAMIC_ID_MIN + && format1.ID == format2.ID + && string.Equals(format1.Name(), format2.Name(), StringComparison.OrdinalIgnoreCase)) { - List compatible = new List(); + // Well known format type. + return true; + } + return false; - if (a == null || a.Count == 0) - { - // Preferable to return an empty list. - //throw new ArgumentNullException("a", "The first media format list supplied was empty."); - } - else if (b == null || b.Count == 0) - { - // Preferable to return an empty list. - //throw new ArgumentNullException("b", "The second media format list supplied was empty."); - } - else + } + + /// + /// Attempts to get the compatible formats between two lists. Formats for "RTP Events" are not included. + /// + /// The first list to match the media formats for. + /// The second list to match the media formats for. + /// A list of media formats that are compatible for BOTH lists. + public static List GetCompatibleFormats(List? a, List? b) + { + var compatible = new List(); + + if (a is null || a.Count == 0) + { + // Preferable to return an empty list. + //throw new ArgumentNullException("a", "The first media format list supplied was empty."); + } + else if (b is null || b.Count == 0) + { + // Preferable to return an empty list. + //throw new ArgumentNullException("b", "The second media format list supplied was empty."); + } + else + { + foreach (var format in a) { - foreach (var format in a) + var hasMatch = false; + foreach (var otherFormat in b) { - if (b.Any(x => AreMatch(format, x))) + if (AreMatch(format, otherFormat)) { - if (format.CheckCompatible()) - { - compatible.Add(format); - } + hasMatch = true; + break; } } + + if (hasMatch && format.CheckCompatible()) + { + compatible.Add(format); + } } + } + + return compatible; + } - return compatible; + /// + /// Attempts to get the first compatible format between two lists without using LINQ or full iteration. This method + /// is optimized for performance by returning immediately when the first compatible format is found. + /// + /// The first list to match the media formats for. + /// The second list to match the media formats for. + /// The first compatible media format found, or Empty if no compatible formats exist. + public static SDPAudioVideoMediaFormat GetFirstCompatibleFormat(List? a, List? b) + { + // Early exit for null or empty lists + if (a is null or { Count: 0 } || b is null or { Count: 0 }) + { + return Empty; } - /// - /// Sort capabilities array based on another capability array - /// - /// - /// - public static void SortMediaCapability(List capabilities, List priorityOrder) + // Iterate through the first list to find the first compatible format + foreach (var format in a) { - //Fix Capabilities Order - if (priorityOrder != null && capabilities != null) + // Check if this format has a match in the second list + foreach (var otherFormat in b) { - capabilities.Sort((a, b) => + if (AreMatch(format, otherFormat)) { - //Sort By Indexes - var aSort = priorityOrder.FindIndex(c => c.ID == a.ID); - var bSort = priorityOrder.FindIndex(c => c.ID == b.ID); - - //Sort Values - if (aSort < 0) - { - aSort = int.MaxValue; - } - if (bSort < 0) + // Check compatibility before returning + if (format.CheckCompatible()) { - bSort = int.MaxValue; + return format; } - - return aSort.CompareTo(bSort); - }); + // If not compatible, break out of inner loop to try next format in 'a' + break; + } } } - /// - /// Parses an rtpmap attribute in the form "name/clock" or "name/clock/channels". - /// - public static bool TryParseRtpmap(string rtpmap, out string name, out int clockRate, out int channels) + return Empty; + } + + /// + /// Attempts to get the first compatible format between two lists while excluding a specific RTP event payload ID. + /// This method is optimized for performance by returning immediately when the first compatible format is found. + /// + /// The first list to match the media formats for. + /// The second list to match the media formats for. + /// + /// The payload ID to exclude from the search (typically RTP event payload ID). + /// + /// + /// The first compatible media format found that doesn't match the excluded payload ID, or Empty if no compatible + /// formats exist. + /// + public static SDPAudioVideoMediaFormat GetFirstCompatibleFormatExcluding(List? a, List? b, int excludePayloadID) + { + // Early exit for null or empty lists + if (a is null or { Count: 0 } || b is null or { Count: 0 }) { - name = null; - clockRate = 0; - channels = DEFAULT_AUDIO_CHANNEL_COUNT; + return Empty; + } - if (string.IsNullOrWhiteSpace(rtpmap)) + // Iterate through the first list to find the first compatible format + foreach (var format in a) + { + // Skip formats that match the excluded payload ID + if (format.ID == excludePayloadID) { - return false; + continue; } - else - { - var rtpmapSpan = rtpmap.AsSpan().Trim(); - var nameSpan = default(ReadOnlySpan); - var clockRateSpan = default(ReadOnlySpan); - var channelsSpan = default(ReadOnlySpan); - var fieldIndex = 0; - - foreach (var fieldRange in rtpmapSpan.Split('/')) - { - var field = rtpmapSpan[fieldRange].Trim(); - - if (fieldIndex == 0) - { - nameSpan = field; - } - else if (fieldIndex == 1) - { - clockRateSpan = field; - } - else if (fieldIndex == 2) - { - channelsSpan = field; - break; - } - fieldIndex++; - } - - if (fieldIndex >= 1) + // Check if this format has a match in the second list + foreach (var otherFormat in b) + { + if (AreMatch(format, otherFormat)) { - if (!int.TryParse(clockRateSpan, out clockRate)) - { - return false; - } - - if (!channelsSpan.IsEmpty && !int.TryParse(channelsSpan, out channels)) + // Check compatibility before returning + if (format.CheckCompatible()) { - return false; + return format; } - - name = nameSpan.ToString(); - return true; - } - else - { - return false; + // If not compatible, break out of inner loop to try next format in 'a' + break; } } } - /// - /// Attempts to get a common SDP media format that supports telephone events. - /// If compatible an RTP event format will be returned that matches the local format with the remote format. - /// - /// The first of supported media formats. - /// The second of supported media formats. - /// An SDP media format with a compatible RTP event format. - public static SDPAudioVideoMediaFormat GetCommonRtpEventFormat(List a, List b) + return Empty; + } + + /// + /// Sort capabilities array based on another capability array + /// + /// + /// + public static void SortMediaCapability(List? capabilities, List? priorityOrder) + { + //Fix Capabilities Order + if (priorityOrder is { } && capabilities is { }) { - if (a == null || b == null || a.Count == 0 || b.Count() == 0) + capabilities.Sort((a, b) => { - return Empty; - } - else - { - // Check if RTP events are supported and if required adjust the local format ID. - var aEventFormat = GetFormatForName(a, SDP.TELEPHONE_EVENT_ATTRIBUTE); - var bEventFormat = GetFormatForName(b, SDP.TELEPHONE_EVENT_ATTRIBUTE); + //Sort By Indexes + var aSort = priorityOrder.FindIndex(c => c.ID == a.ID); + var bSort = priorityOrder.FindIndex(c => c.ID == b.ID); - if (!aEventFormat.IsEmpty() && !bEventFormat.IsEmpty()) + //Sort Values + if (aSort < 0) { - // Both support RTP events. If using different format ID's choose the first one. - return aEventFormat; + aSort = int.MaxValue; } - else + if (bSort < 0) { - return Empty; + bSort = int.MaxValue; } - } + + return aSort.CompareTo(bSort); + }); + } + } + + /// + /// Parses an rtpmap attribute in the form "name/clock" or "name/clock/channels". + /// + public static bool TryParseRtpmap(ReadOnlySpan rtpmap, out string? name, out int clockRate, out int channels) + { + name = null; + clockRate = 0; + channels = DEFAULT_AUDIO_CHANNEL_COUNT; + + rtpmap = rtpmap.Trim(); + if (rtpmap.IsEmpty) + { + return false; } - /// - /// Attempts to get a matching entry in a list of media formats for a specific format name. - /// - /// The list of formats to search. - /// The format name to search for. - /// If found the matching format or the empty format if not. - public static SDPAudioVideoMediaFormat GetFormatForName(List formats, string formatName) + Span fields = stackalloc Range[3]; + rtpmap.Split(fields, '/'); + var nameSpan = rtpmap[fields[0]].Trim(); + var clockRateSpan = rtpmap[fields[1]].Trim(); + var channelsSpan = rtpmap[fields[2]].Trim(); + + if (!clockRateSpan.IsEmpty && !int.TryParse(clockRateSpan, out clockRate)) { - if (formats == null || formats.Count == 0) - { - return Empty; - } - else + return false; + } + + if (!channelsSpan.IsEmpty && !int.TryParse(channelsSpan, out channels)) + { + return false; + } + + name = nameSpan.ToString(); + + return true; + } + + /// + /// Attempts to get a common SDP media format that supports telephone events. If compatible an RTP event format will + /// be returned that matches the local format with the remote format. + /// + /// The first of supported media formats. + /// The second of supported media formats. + /// An SDP media format with a compatible RTP event format. + public static SDPAudioVideoMediaFormat GetCommonRtpEventFormat(IEnumerable a, IEnumerable b) + { + // Check if RTP events are supported and if required adjust the local format ID. + var aEventFormat = GetFormatForName(a, SDP.TELEPHONE_EVENT_ATTRIBUTE); + var bEventFormat = GetFormatForName(b, SDP.TELEPHONE_EVENT_ATTRIBUTE); + + if (!aEventFormat.IsEmpty() && !bEventFormat.IsEmpty()) + { + // Both support RTP events. If using different format ID's choose the first one. + return aEventFormat; + } + else + { + return Empty; + } + } + + /// + /// Attempts to get a matching entry in a list of media formats for a specific format name. + /// + /// The list of formats to search. + /// The format name to search for. + /// If found the matching format or the empty format if not. + public static SDPAudioVideoMediaFormat GetFormatForName(IEnumerable formats, string formatName) + { + if (formats is { } && formatName is { }) + { + foreach (var format in formats) { - return formats.Any(x => string.Equals(x.Name(), formatName, StringComparison.OrdinalIgnoreCase)) ? - formats.First(x => string.Equals(x.Name(), formatName, StringComparison.OrdinalIgnoreCase)) : - Empty; + if (string.Equals(format.Name(), formatName, StringComparison.OrdinalIgnoreCase)) + { + return format; + } } } + + return Empty; } } diff --git a/src/SIPSorcery/net/SDP/SDPConnectionInformation.cs b/src/SIPSorcery/net/SDP/SDPConnectionInformation.cs index 4f64aafe20..869e710a0e 100644 --- a/src/SIPSorcery/net/SDP/SDPConnectionInformation.cs +++ b/src/SIPSorcery/net/SDP/SDPConnectionInformation.cs @@ -16,73 +16,93 @@ using System; using System.Net; using System.Net.Sockets; -using Polyfills; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class SDPConnectionInformation { - public class SDPConnectionInformation - { - public const string CONNECTION_ADDRESS_TYPE_IPV4 = "IP4"; - public const string CONNECTION_ADDRESS_TYPE_IPV6 = "IP6"; + public const string CONNECTION_ADDRESS_TYPE_IPV4 = "IP4"; + public const string CONNECTION_ADDRESS_TYPE_IPV6 = "IP6"; + + public const string m_CRLF = "\r\n"; + + /// + /// Type of network, IN = Internet. + /// + public string ConnectionNetworkType = "IN"; + + /// + /// Session level address family. + /// + public string ConnectionAddressType = CONNECTION_ADDRESS_TYPE_IPV4; + + /// + /// IP or multicast address for the media connection. + /// + public string? ConnectionAddress; - public const string m_CRLF = "\r\n"; + private SDPConnectionInformation() + { } - /// - /// Type of network, IN = Internet. - /// - public string ConnectionNetworkType = "IN"; + public SDPConnectionInformation(IPAddress connectionAddress) + { + ConnectionAddress = connectionAddress.ToString(); + ConnectionAddressType = (connectionAddress.AddressFamily == AddressFamily.InterNetworkV6) ? CONNECTION_ADDRESS_TYPE_IPV6 : CONNECTION_ADDRESS_TYPE_IPV4; + } - /// - /// Session level address family. - /// - public string ConnectionAddressType = CONNECTION_ADDRESS_TYPE_IPV4; + public static SDPConnectionInformation ParseConnectionInformation(ReadOnlySpan connectionLine) + { + var connectionInfo = new SDPConnectionInformation(); - /// - /// IP or multicast address for the media connection. - /// - public string ConnectionAddress; + connectionLine = connectionLine.Slice(2).Trim(); - private SDPConnectionInformation() - { } + Span fields = stackalloc Range[3]; + var fieldCount = connectionLine.Split(fields, ' ', StringSplitOptions.RemoveEmptyEntries); - public SDPConnectionInformation(IPAddress connectionAddress) + if (fieldCount > 0) { - ConnectionAddress = connectionAddress.ToString(); - ConnectionAddressType = (connectionAddress.AddressFamily == AddressFamily.InterNetworkV6) ? CONNECTION_ADDRESS_TYPE_IPV6 : CONNECTION_ADDRESS_TYPE_IPV4; + connectionInfo.ConnectionNetworkType = connectionLine[fields[0]].Trim().ToString(); } - public static SDPConnectionInformation ParseConnectionInformation(string connectionLine) + if (fieldCount > 1) { - SDPConnectionInformation connectionInfo = new SDPConnectionInformation(); - var connectionFields = connectionLine.AsSpan(2).Trim(); - var fieldIndex = 0; - foreach (var fieldRange in connectionFields.Split(' ')) - { - var field = connectionFields[fieldRange].Trim().ToString(); - if (fieldIndex == 0) - { - connectionInfo.ConnectionNetworkType = field; - } - else if (fieldIndex == 1) - { - connectionInfo.ConnectionAddressType = field; - } - else if (fieldIndex == 2) - { - connectionInfo.ConnectionAddress = field; - break; - } - - fieldIndex++; - } - - return connectionInfo; + connectionInfo.ConnectionAddressType = connectionLine[fields[1]].Trim().ToString(); } - public override string ToString() + if (fieldCount >= 2) { - return $"c={ConnectionNetworkType} {ConnectionAddressType} {ConnectionAddress}{m_CRLF}"; + connectionInfo.ConnectionAddress = connectionLine[fields[2]].Trim().ToString(); } + + return connectionInfo; + } + + public override string ToString() + { + var builder = new ValueStringBuilder(); + + try + { + ToString(ref builder); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) + { + builder.Append("c="); + builder.Append(ConnectionNetworkType); + builder.Append(' '); + builder.Append(ConnectionAddressType); + builder.Append(' '); + builder.Append(ConnectionAddress); + builder.Append(m_CRLF); + } } diff --git a/src/SIPSorcery/net/SDP/SDPMediaAnnouncement.cs b/src/SIPSorcery/net/SDP/SDPMediaAnnouncement.cs index 1f536cd9d0..1979f5e77b 100644 --- a/src/SIPSorcery/net/SDP/SDPMediaAnnouncement.cs +++ b/src/SIPSorcery/net/SDP/SDPMediaAnnouncement.cs @@ -28,514 +28,643 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Diagnostics; using Microsoft.Extensions.Logging; -using Polyfills; using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// An attribute used to defined additional properties about +/// a media source and the relationship between them. +/// As specified in RFC5576, https://tools.ietf.org/html/rfc5576. +/// +public class SDPSsrcAttribute { + public const string MEDIA_CNAME_ATTRIBUE_PREFIX = "cname"; + + public uint SSRC { get; set; } + + public string? Cname { get; set; } + + public string? GroupID { get; set; } + /// - /// An attribute used to defined additional properties about - /// a media source and the relationship between them. - /// As specified in RFC5576, https://tools.ietf.org/html/rfc5576. + /// Default constructor. /// - public class SDPSsrcAttribute + /// The SSRC that should match an RTP stream. + /// Optional. The CNAME value to use in RTCP SDES sections. + /// Optional. If this "ssrc" attribute is part of a + /// group this is the group ID. + public SDPSsrcAttribute(uint ssrc, string? cname, string? groupID) { - public const string MEDIA_CNAME_ATTRIBUE_PREFIX = "cname"; + SSRC = ssrc; + Cname = cname; + GroupID = groupID; + } +} - public uint SSRC { get; set; } +public class SDPMediaAnnouncement +{ + public const string MEDIA_EXTENSION_MAP_ATTRIBUE_NAME = "extmap"; + public const string MEDIA_EXTENSION_MAP_ATTRIBUE_PREFIX = "a=" + MEDIA_EXTENSION_MAP_ATTRIBUE_NAME + ":"; + public const string MEDIA_FORMAT_ATTRIBUTE_NAME = "rtpmap"; + public const string MEDIA_FORMAT_ATTRIBUTE_PREFIX = "a=" + MEDIA_FORMAT_ATTRIBUTE_NAME + ":"; + public const string MEDIA_FORMAT_FEEDBACK_PREFIX = "a=rtcp-fb:"; + public const string MEDIA_FORMAT_PARAMETERS_ATTRIBUE_NAME = "fmtp"; + public const string MEDIA_FORMAT_PARAMETERS_ATTRIBUE_PREFIX = "a=" + MEDIA_FORMAT_PARAMETERS_ATTRIBUE_NAME + ":"; + public const string MEDIA_FORMAT_SSRC_ATTRIBUE_NAME = "ssrc"; + public const string MEDIA_FORMAT_SSRC_ATTRIBUE_PREFIX = "a=" + MEDIA_FORMAT_SSRC_ATTRIBUE_NAME + ":"; + public const string MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_NAME = "ssrc-group"; + public const string MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_PREFIX = "a=" + MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_NAME + ":"; + public const string MEDIA_FORMAT_SCTP_MAP_ATTRIBUE_NAME = "sctpmap"; + public const string MEDIA_FORMAT_SCTP_MAP_ATTRIBUE_PREFIX = "a=" + MEDIA_FORMAT_SCTP_MAP_ATTRIBUE_NAME + ":"; + public const string MEDIA_FORMAT_SCTP_PORT_ATTRIBUE_NAME = "sctp-port"; + public const string MEDIA_FORMAT_SCTP_PORT_ATTRIBUE_PREFIX = "a=" + MEDIA_FORMAT_SCTP_PORT_ATTRIBUE_NAME + ":"; + public const string MEDIA_FORMAT_MAX_MESSAGE_SIZE_ATTRIBUE_NAME = "max-message-size"; + public const string MEDIA_FORMAT_MAX_MESSAGE_SIZE_ATTRIBUE_PREFIX = "a=" + MEDIA_FORMAT_MAX_MESSAGE_SIZE_ATTRIBUE_NAME + ":"; + public const string MEDIA_FORMAT_PATH_MSRP_NAME = "path"; + public const string MEDIA_FORMAT_PATH_MSRP_SCHEME = "msrp"; + public const string MEDIA_FORMAT_PATH_MSRP_PREFIX = "a=" + MEDIA_FORMAT_PATH_MSRP_NAME + ":" + "msrp:" + ":"; + public const string MEDIA_FORMAT_PATH_ACCEPT_TYPES_NAME = "accept-types"; + public const string MEDIA_FORMAT_PATH_ACCEPT_TYPES_PREFIX = "a=" + MEDIA_FORMAT_PATH_ACCEPT_TYPES_NAME + ":"; + public const string TIAS_BANDWIDTH_ATTRIBUE_NAME = "TIAS"; + public const string TIAS_BANDWIDTH_ATTRIBUE_PREFIX = "b=" + TIAS_BANDWIDTH_ATTRIBUE_NAME + ":"; + + public const MediaStreamStatusEnum DEFAULT_STREAM_STATUS = MediaStreamStatusEnum.SendRecv; + + public const string m_CRLF = "\r\n"; + + private static readonly ILogger logger = LogFactory.CreateLogger(); + + public SDPConnectionInformation? Connection; + + // Media Announcement fields. + public SDPMediaTypesEnum Media = SDPMediaTypesEnum.audio; // Media type for the stream. + public int Port; // For UDP transports should be in the range 1024 to 65535 and for RTP compliance should be even (only even ports used for data). + /// + /// Gets or sets the number of consecutive ports specified for the media stream in the SDP. + /// When the SDP media line includes a port range (e.g., "30000/2"), this property holds the count of ports. + /// Typically, a value of 2 indicates that two ports are allocated: one for RTP and the following port for RTCP. + /// + public int PortCount { get; set; } + public string Transport = "RTP/AVP"; // Defined types RTP/AVP (RTP Audio Visual Profile) and udp. + public string? IceUfrag; // If ICE is being used the username for the STUN requests. + public string? IcePwd; // If ICE is being used the password for the STUN requests. + public string? IceOptions; // Optional attribute to specify support ICE options, e.g. "trickle". + public bool IceEndOfCandidates; // If ICE candidate trickling is being used this needs to be set if all candidates have been gathered. + public IceRolesEnum? IceRole; + public string? DtlsFingerprint; // If DTLS handshake is being used this is the fingerprint or our DTLS certificate. + public int MLineIndex; - public string Cname { get; set; } + /// + /// If being used in a bundle this the ID for the announcement. + /// Example: a=mid:audio or a=mid:video. + /// + public string? MediaID; - public string GroupID { get; set; } + /// + /// The "ssrc" attributes group ID as specified in RFC5576. + /// + public string? SsrcGroupID; - /// - /// Default constructor. - /// - /// The SSRC that should match an RTP stream. - /// Optional. The CNAME value to use in RTCP SDES sections. - /// Optional. If this "ssrc" attribute is part of a - /// group this is the group ID. - public SDPSsrcAttribute(uint ssrc, string cname, string groupID) - { - SSRC = ssrc; - Cname = cname; - GroupID = groupID; - } + /// + /// The "sctpmap" attribute defined in https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26 for + /// use in WebRTC data channels. + /// + public string? SctpMap; + + /// + /// The "sctp-port" attribute defined in https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26 for + /// use in WebRTC data channels. + /// + public ushort? SctpPort; + + /// + /// The "max-message-size" attribute defined in https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26 for + /// use in WebRTC data channels. + /// + public long MaxMessageSize; + + /// + /// If the RFC5576 is being used this is the list of "ssrc" attributes + /// supplied. + /// + public List SsrcAttributes = new List(); + + /// + /// Optional Transport Independent Application Specific Maximum (TIAS) bandwidth. + /// + public uint TIASBandwidth; + + public List BandwidthAttributes = new List(); + + /// + /// In media definitions, "i=" fields are primarily intended for labelling media streams https://tools.ietf.org/html/rfc4566#page-12 + /// + public string? MediaDescription; + + /// + /// For AVP these will normally be a media payload type as defined in the RTP Audio/Video Profile. + /// + public Dictionary MediaFormats = new Dictionary(); + + /// + /// a=extmap - Mapping for RTP header extensions + /// + public Dictionary HeaderExtensions = new Dictionary(); + + /// + /// For AVP these will normally be a media payload type as defined in the RTP Audio/Video Profile. + /// + public SDPMessageMediaFormat MessageMediaFormat = new SDPMessageMediaFormat(); + + /// + /// List of media formats for "application media announcements. Application media announcements have different + /// semantics to audio/video announcements. They can also use aribtrary strings as the format ID. + /// + public Dictionary ApplicationMediaFormats = new Dictionary(); + + public List ExtraMediaAttributes = new List(); // Attributes that were not recognised. + public List SecurityDescriptions = new List(); //2018-12-21 rj2: add a=crypto parsing etc. + public List? IceCandidates; + + /// + /// The stream status of this media announcement. + /// + public MediaStreamStatusEnum? MediaStreamStatus { get; set; } + + public SDPMediaAnnouncement() + { } + + public SDPMediaAnnouncement(int port) + { + Port = port; } - public class SDPMediaAnnouncement + public SDPMediaAnnouncement(SDPConnectionInformation connection) { - public const string MEDIA_EXTENSION_MAP_ATTRIBUE_PREFIX = "a=extmap:"; - public const string MEDIA_FORMAT_ATTRIBUTE_PREFIX = "a=rtpmap:"; - public const string MEDIA_FORMAT_FEEDBACK_PREFIX = "a=rtcp-fb:"; - public const string MEDIA_FORMAT_PARAMETERS_ATTRIBUE_PREFIX = "a=fmtp:"; - public const string MEDIA_FORMAT_SSRC_ATTRIBUE_PREFIX = "a=ssrc:"; - public const string MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_PREFIX = "a=ssrc-group:"; - public const string MEDIA_FORMAT_SCTP_MAP_ATTRIBUE_PREFIX = "a=sctpmap:"; - public const string MEDIA_FORMAT_SCTP_PORT_ATTRIBUE_PREFIX = "a=sctp-port:"; - public const string MEDIA_FORMAT_MAX_MESSAGE_SIZE_ATTRIBUE_PREFIX = "a=max-message-size:"; - public const string MEDIA_FORMAT_PATH_MSRP_PREFIX = "a=path:msrp:"; - public const string MEDIA_FORMAT_PATH_ACCEPT_TYPES_PREFIX = "a=accept-types:"; - public const string TIAS_BANDWIDTH_ATTRIBUE_PREFIX = "b=TIAS:"; - - public const MediaStreamStatusEnum DEFAULT_STREAM_STATUS = MediaStreamStatusEnum.SendRecv; - - public const string m_CRLF = "\r\n"; - - private static readonly ILogger logger = LogFactory.CreateLogger(); - - public SDPConnectionInformation Connection; - - // Media Announcement fields. - public SDPMediaTypesEnum Media = SDPMediaTypesEnum.audio; // Media type for the stream. - public int Port; // For UDP transports should be in the range 1024 to 65535 and for RTP compliance should be even (only even ports used for data). - /// - /// Gets or sets the number of consecutive ports specified for the media stream in the SDP. - /// When the SDP media line includes a port range (e.g., "30000/2"), this property holds the count of ports. - /// Typically, a value of 2 indicates that two ports are allocated: one for RTP and the following port for RTCP. - /// - public int PortCount { get; set; } - public string Transport = "RTP/AVP"; // Defined types RTP/AVP (RTP Audio Visual Profile) and udp. - public string IceUfrag; // If ICE is being used the username for the STUN requests. - public string IcePwd; // If ICE is being used the password for the STUN requests. - public string IceOptions; // Optional attribute to specify support ICE options, e.g. "trickle". - public bool IceEndOfCandidates; // If ICE candidate trickling is being used this needs to be set if all candidates have been gathered. - public IceRolesEnum? IceRole = null; - public string DtlsFingerprint; // If DTLS handshake is being used this is the fingerprint or our DTLS certificate. - public int MLineIndex = 0; - - /// - /// If being used in a bundle this the ID for the announcement. - /// Example: a=mid:audio or a=mid:video. - /// - public string MediaID; - - /// - /// The "ssrc" attributes group ID as specified in RFC5576. - /// - public string SsrcGroupID; - - /// - /// The "sctpmap" attribute defined in https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26 for - /// use in WebRTC data channels. - /// - public string SctpMap; - - /// - /// The "sctp-port" attribute defined in https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26 for - /// use in WebRTC data channels. - /// - public ushort? SctpPort = null; - - /// - /// The "max-message-size" attribute defined in https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26 for - /// use in WebRTC data channels. - /// - public long MaxMessageSize = 0; - - /// - /// If the RFC5576 is being used this is the list of "ssrc" attributes - /// supplied. - /// - public List SsrcAttributes = new List(); - - /// - /// Optional Transport Independent Application Specific Maximum (TIAS) bandwidth. - /// - public uint TIASBandwidth = 0; - - public List BandwidthAttributes = new List(); - - /// - /// In media definitions, "i=" fields are primarily intended for labelling media streams https://tools.ietf.org/html/rfc4566#page-12 - /// - public string MediaDescription; - - /// - /// For AVP these will normally be a media payload type as defined in the RTP Audio/Video Profile. - /// - public Dictionary MediaFormats = new Dictionary(); - - /// - /// a=extmap - Mapping for RTP header extensions - /// - public Dictionary HeaderExtensions = new Dictionary(); - - /// - /// For AVP these will normally be a media payload type as defined in the RTP Audio/Video Profile. - /// - public SDPMessageMediaFormat MessageMediaFormat = new SDPMessageMediaFormat(); - - /// - /// List of media formats for "application media announcements. Application media announcements have different - /// semantics to audio/video announcements. They can also use aribtrary strings as the format ID. - /// - public Dictionary ApplicationMediaFormats = new Dictionary(); - - public List ExtraMediaAttributes = new List(); // Attributes that were not recognised. - public List SecurityDescriptions = new List(); //2018-12-21 rj2: add a=crypto parsing etc. - public List IceCandidates; - - /// - /// The stream status of this media announcement. - /// - public MediaStreamStatusEnum? MediaStreamStatus { get; set; } - - public SDPMediaAnnouncement() - { } - - public SDPMediaAnnouncement(int port) - { - Port = port; - } - - public SDPMediaAnnouncement(SDPConnectionInformation connection) - { - Connection = connection; - } - - public SDPMediaAnnouncement(SDPMediaTypesEnum mediaType, int port, List mediaFormats) - { - Media = mediaType; - Port = port; - MediaStreamStatus = DEFAULT_STREAM_STATUS; + Connection = connection; + } - if (mediaFormats != null) + public SDPMediaAnnouncement(SDPMediaTypesEnum mediaType, int port, List? mediaFormats) + { + Media = mediaType; + Port = port; + MediaStreamStatus = DEFAULT_STREAM_STATUS; + + if (mediaFormats is { }) + { + foreach (var fmt in mediaFormats) { - foreach (var fmt in mediaFormats) - { - if (!MediaFormats.ContainsKey(fmt.ID)) - { - MediaFormats.Add(fmt.ID, fmt); - } - } + MediaFormats.TryAdd(fmt.ID, fmt); } } + } - public SDPMediaAnnouncement(SDPMediaTypesEnum mediaType, int port, List appMediaFormats) - { - Media = mediaType; - Port = port; + public SDPMediaAnnouncement(SDPMediaTypesEnum mediaType, int port, List? appMediaFormats) + { + Media = mediaType; + Port = port; - if (appMediaFormats != null) + if (appMediaFormats is { }) + { + foreach (var fmt in appMediaFormats) { - foreach (var fmt in appMediaFormats) - { - if (!ApplicationMediaFormats.ContainsKey(fmt.ID)) - { - ApplicationMediaFormats.Add(fmt.ID, fmt); - } - } + ApplicationMediaFormats.TryAdd(fmt.ID, fmt); } } + } - public SDPMediaAnnouncement(SDPMediaTypesEnum mediaType, SDPConnectionInformation connection, int port, SDPMessageMediaFormat messageMediaFormat) - { - Media = mediaType; - Port = port; - Connection = connection; + public SDPMediaAnnouncement(SDPMediaTypesEnum mediaType, SDPConnectionInformation connection, int port, SDPMessageMediaFormat messageMediaFormat) + { + Media = mediaType; + Port = port; + Connection = connection; + + MessageMediaFormat = messageMediaFormat; + } - MessageMediaFormat = messageMediaFormat; + public void ParseMediaFormats(string formatList) + { + if (string.IsNullOrWhiteSpace(formatList)) + { + return; } - public void ParseMediaFormats(string formatList) + var span = formatList.AsSpan().Trim(); + + foreach (var range in span.SplitAny(SearchValues.WhiteSpaceChars)) { - if (!String.IsNullOrWhiteSpace(formatList)) + var token = span[range]; + + if (token.Length == 0) { - var formatListSpan = formatList.AsSpan(); - foreach (var formatIDRange in formatListSpan.SplitAny()) - { - var formatIDSpan = formatListSpan[formatIDRange]; + continue; + } - if (Media == SDPMediaTypesEnum.application) - { - var formatID = formatIDSpan.ToString(); - ApplicationMediaFormats.Add(formatID, new SDPApplicationMediaFormat(formatID)); - } - else if (Media == SDPMediaTypesEnum.message) + if (Media == SDPMediaTypesEnum.application) + { + var formatID = token.ToString(); + + ApplicationMediaFormats.Add(formatID, new SDPApplicationMediaFormat(formatID)); + } + else if (Media == SDPMediaTypesEnum.message) + { + // TODO: Handle message media type + } + else + { + if (int.TryParse(token, out var id) + && !MediaFormats.ContainsKey(id) + && id < SDPAudioVideoMediaFormat.DYNAMIC_ID_MIN) + { + if (SDPWellKnownMediaFormatsEnumExtensions.IsDefined((SDPWellKnownMediaFormatsEnum)id) && + SDPWellKnownMediaFormatsEnumExtensions.TryParse(token, out var wellKnown)) { - //TODO + MediaFormats.Add(id, new SDPAudioVideoMediaFormat(wellKnown)); } else { - if (int.TryParse(formatIDSpan, out var id) - && !MediaFormats.ContainsKey(id) - && id < SDPAudioVideoMediaFormat.DYNAMIC_ID_MIN) - { - var formatID = formatIDSpan.ToString(); - if (Enum.IsDefined(typeof(SDPWellKnownMediaFormatsEnum), id) && - Enum.TryParse(formatID, out var wellKnown)) - { - MediaFormats.Add(id, new SDPAudioVideoMediaFormat(wellKnown)); - } - else - { - logger.LogWarning("Excluding unrecognised well known media format ID {FormatID}.", id); - } - } + logger.LogSdpUnrecognisedMediaFormat(id); } } } } + } + + public override string ToString() + { + var builder = new ValueStringBuilder(); - public override string ToString() + try { - var announcement = new StringBuilder(); - announcement.Append("m=").Append(Media).Append(' ').Append(Port).Append(' ').Append(Transport).Append(' ') - .Append(GetFormatListToString()).Append(m_CRLF); + ToString(ref builder); - if (!string.IsNullOrWhiteSpace(MediaDescription)) - { - announcement.Append("i=").Append(MediaDescription).Append(m_CRLF); - } + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } - if (Connection != null) - { - announcement.Append(Connection); - } + internal void ToString(ref ValueStringBuilder builder) + { + builder.Append("m="); + builder.Append(Media.ToStringFast()); + builder.Append(' '); + builder.Append(Port); + builder.Append(' '); + builder.Append(Transport); + builder.Append(' '); + WriteFormatListString(ref builder); + builder.Append(m_CRLF); + + if (!string.IsNullOrWhiteSpace(MediaDescription)) + { + builder.Append("i="); + builder.Append(MediaDescription); + builder.Append(m_CRLF); + } - if (TIASBandwidth > 0) - { - announcement.Append(TIAS_BANDWIDTH_ATTRIBUE_PREFIX).Append(TIASBandwidth).Append(m_CRLF); - } + if (Connection is { }) + { + Connection.ToString(ref builder); + } - foreach (string bandwidthAttribute in BandwidthAttributes) - { - announcement.Append("b=").Append(bandwidthAttribute).Append(m_CRLF); - } + if (TIASBandwidth > 0) + { + builder.Append(TIAS_BANDWIDTH_ATTRIBUE_PREFIX); + builder.Append(TIASBandwidth); + builder.Append(m_CRLF); + } - if (!string.IsNullOrWhiteSpace(IceUfrag)) - { - announcement.Append("a=").Append(SDP.ICE_UFRAG_ATTRIBUTE_PREFIX).Append(':').Append(IceUfrag).Append(m_CRLF); - } + foreach (var bandwidthAttribute in BandwidthAttributes) + { + builder.Append("b="); + builder.Append(bandwidthAttribute); + builder.Append(m_CRLF); + } - if (!string.IsNullOrWhiteSpace(IcePwd)) - { - announcement.Append("a=").Append(SDP.ICE_PWD_ATTRIBUTE_PREFIX).Append(':').Append(IcePwd).Append(m_CRLF); - } + if (!string.IsNullOrWhiteSpace(IceUfrag)) + { + builder.Append("a="); + builder.Append(SDP.ICE_UFRAG_ATTRIBUTE_PREFIX); + builder.Append(':'); + builder.Append(IceUfrag); + builder.Append(m_CRLF); + } - if (!string.IsNullOrWhiteSpace(DtlsFingerprint)) - { - announcement.Append("a=").Append(SDP.DTLS_FINGERPRINT_ATTRIBUTE_PREFIX).Append(':').Append(DtlsFingerprint).Append(m_CRLF); - } + if (!string.IsNullOrWhiteSpace(IcePwd)) + { + builder.Append("a="); + builder.Append(SDP.ICE_PWD_ATTRIBUTE_PREFIX); + builder.Append(':'); + builder.Append(IcePwd); + builder.Append(m_CRLF); + } - if (IceRole != null) - { - announcement.Append("a=").Append(SDP.ICE_SETUP_ATTRIBUTE_PREFIX).Append(':').Append(IceRole).Append(m_CRLF); - } + if (!string.IsNullOrWhiteSpace(DtlsFingerprint)) + { + builder.Append("a="); + builder.Append(SDP.DTLS_FINGERPRINT_ATTRIBUTE_PREFIX); + builder.Append(':'); + builder.Append(DtlsFingerprint); + builder.Append(m_CRLF); + } - if (IceCandidates?.Count() > 0) - { - foreach (var candidate in IceCandidates) - { - announcement.Append("a=").Append(SDP.ICE_CANDIDATE_ATTRIBUTE_PREFIX).Append(':').Append(candidate).Append(m_CRLF); - } - } + if (IceRole is { }) + { + builder.Append("a="); + builder.Append(SDP.ICE_SETUP_ATTRIBUTE_PREFIX); + builder.Append(':'); - if (IceOptions != null) + if (IceRole is { } iceRole) { - announcement.Append("a=").Append(SDP.ICE_OPTIONS).Append(':').Append(IceOptions).Append(m_CRLF); + builder.Append(iceRole.ToStringFast()); } - if (IceEndOfCandidates) + builder.Append(m_CRLF); + } + + if (IceCandidates is { }) + { + foreach (var candidate in IceCandidates) { - announcement.Append("a=").Append(SDP.END_ICE_CANDIDATES_ATTRIBUTE).Append(m_CRLF); + builder.Append("a="); + builder.Append(SDP.ICE_CANDIDATE_ATTRIBUTE_PREFIX); + builder.Append(':'); + candidate.ToString(ref builder); + builder.Append(m_CRLF); } + } + + if (IceOptions is { }) + { + builder.Append("a="); + builder.Append(SDP.ICE_OPTIONS); + builder.Append(':'); + builder.Append(IceOptions); + builder.Append(m_CRLF); + } + + if (IceEndOfCandidates) + { + builder.Append("a="); + builder.Append(SDP.END_ICE_CANDIDATES_ATTRIBUTE); + builder.Append(m_CRLF); + } - if (!string.IsNullOrWhiteSpace(MediaID)) + if (!string.IsNullOrWhiteSpace(MediaID)) + { + builder.Append("a="); + builder.Append(SDP.MEDIA_ID_ATTRIBUTE_PREFIX); + builder.Append(':'); + builder.Append(MediaID); + builder.Append(m_CRLF); + } + + GetFormatListAttributesToString(ref builder); + + foreach (var ext in HeaderExtensions) + { + builder.Append(MEDIA_EXTENSION_MAP_ATTRIBUE_PREFIX); + builder.Append(ext.Value.Id); + builder.Append(' '); + builder.Append(ext.Value.Uri); + builder.Append(m_CRLF); + } + + foreach (var extra in ExtraMediaAttributes) + { + if (!string.IsNullOrWhiteSpace(extra)) { - announcement.Append("a=").Append(SDP.MEDIA_ID_ATTRIBUTE_PREFIX).Append(':').Append(MediaID).Append(m_CRLF); + builder.Append(extra); + builder.Append(m_CRLF); } + } - announcement.Append(GetFormatListAttributesToString()); + foreach (var desc in this.SecurityDescriptions) + { + desc.ToString(ref builder); + builder.Append(m_CRLF); + } + + if (MediaStreamStatus is { }) + { + builder.Append(MediaStreamStatusType.GetAttributeForMediaStreamStatus(MediaStreamStatus.Value)); + builder.Append(m_CRLF); + } - foreach (var headerExtension in HeaderExtensions) + if (SsrcGroupID is { } && SsrcAttributes.Count > 0) + { + builder.Append(MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_PREFIX); + builder.Append(SsrcGroupID); + foreach (var ssrcAttr in SsrcAttributes) { - announcement.Append(MEDIA_EXTENSION_MAP_ATTRIBUE_PREFIX).Append(headerExtension.Value.Id).Append(' ') - .Append(headerExtension.Value.Uri).Append(m_CRLF); + builder.Append(' '); + builder.Append(ssrcAttr.SSRC); } + builder.Append(m_CRLF); + } - foreach (string extra in ExtraMediaAttributes) + if (SsrcAttributes.Count > 0) + { + foreach (var ssrcAttr in SsrcAttributes) { - if (!string.IsNullOrWhiteSpace(extra)) + builder.Append(MEDIA_FORMAT_SSRC_ATTRIBUE_PREFIX); + builder.Append(ssrcAttr.SSRC); + if (!string.IsNullOrWhiteSpace(ssrcAttr.Cname)) { - announcement.Append(extra).Append(m_CRLF); + builder.Append(' '); + builder.Append(SDPSsrcAttribute.MEDIA_CNAME_ATTRIBUE_PREFIX); + builder.Append(':'); + builder.Append(ssrcAttr.Cname); } + builder.Append(m_CRLF); } + } - foreach (SDPSecurityDescription desc in this.SecurityDescriptions) + // If the "sctpmap" attribute is set, use it instead of the separate "sctpport" and "max-message-size" + // attributes. They both contain the same information. The "sctpmap" is the legacy attribute and if + // an application sets it then it's likely to be for a specific reason. + if (SctpMap is { }) + { + builder.Append(MEDIA_FORMAT_SCTP_MAP_ATTRIBUE_PREFIX); + builder.Append(SctpMap); + builder.Append(m_CRLF); + } + else + { + if (SctpPort is { }) { - announcement.Append(desc.ToString()).Append(m_CRLF); + builder.Append(MEDIA_FORMAT_SCTP_PORT_ATTRIBUE_PREFIX); + builder.Append(SctpPort); + builder.Append(m_CRLF); } - if (MediaStreamStatus != null) + if (MaxMessageSize != 0) { - announcement.Append(MediaStreamStatusType.GetAttributeForMediaStreamStatus(MediaStreamStatus.Value)).Append(m_CRLF); + builder.Append(MEDIA_FORMAT_MAX_MESSAGE_SIZE_ATTRIBUE_PREFIX); + builder.Append(MaxMessageSize); + builder.Append(m_CRLF); } + } + } - if (SsrcGroupID != null && SsrcAttributes.Count > 0) - { - announcement.Append(MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_PREFIX).Append(SsrcGroupID); - foreach (var ssrcAttr in SsrcAttributes) - { - announcement.Append(' ').Append(ssrcAttr.SSRC); - } - announcement.Append(m_CRLF); - } + public string? GetFormatListToString() + { + if (Media == SDPMediaTypesEnum.message) + { + return "*"; + } - if (SsrcAttributes.Count > 0) - { - foreach (var ssrcAttr in SsrcAttributes) - { - if (!string.IsNullOrWhiteSpace(ssrcAttr.Cname)) - { - announcement.Append(MEDIA_FORMAT_SSRC_ATTRIBUE_PREFIX).Append(ssrcAttr.SSRC).Append(' ') - .Append(SDPSsrcAttribute.MEDIA_CNAME_ATTRIBUE_PREFIX).Append(':').Append(ssrcAttr.Cname).Append(m_CRLF); - } - else - { - announcement.Append(MEDIA_FORMAT_SSRC_ATTRIBUE_PREFIX).Append(ssrcAttr.SSRC).Append(m_CRLF); - } - } - } + var builder = new ValueStringBuilder(); + + try + { + WriteFormatListString(ref builder); - // If the "sctpmap" attribute is set, use it instead of the separate "sctpport" and "max-message-size" - // attributes. They both contain the same information. The "sctpmap" is the legacy attribute and if - // an application sets it then it's likely to be for a specific reason. - if (SctpMap != null) + if (Media == SDPMediaTypesEnum.application) { - announcement.Append(MEDIA_FORMAT_SCTP_MAP_ATTRIBUE_PREFIX).Append(SctpMap).Append(m_CRLF); + return builder.ToString(); } else { - if (SctpPort != null) - { - announcement.Append(MEDIA_FORMAT_SCTP_PORT_ATTRIBUE_PREFIX).Append(SctpPort).Append(m_CRLF); - } - - if (MaxMessageSize != 0) - { - announcement.Append(MEDIA_FORMAT_MAX_MESSAGE_SIZE_ATTRIBUE_PREFIX).Append(MaxMessageSize).Append(m_CRLF); - } + return builder.Length > 0 ? builder.ToString() : null; } - - return announcement.ToString(); } + finally + { + builder.Dispose(); + } + } - public string GetFormatListToString() + internal void WriteFormatListString(ref ValueStringBuilder builder) + { + if (Media == SDPMediaTypesEnum.message) { - if (Media == SDPMediaTypesEnum.application) + builder.Append('*'); + } + else if (Media == SDPMediaTypesEnum.application) + { + var first = true; + foreach (var appFormat in ApplicationMediaFormats) { - StringBuilder sb = new StringBuilder(); - foreach (var appFormat in ApplicationMediaFormats) + if (!first) { - sb.Append(appFormat.Key); - sb.Append(" "); + builder.Append(' '); } - - return sb.ToString().Trim(); - } - else if (Media == SDPMediaTypesEnum.message) - { - return "*"; + builder.Append(appFormat.Key); + first = false; } - else + } + else + { + var first = true; + foreach (var mediaFormat in MediaFormats) { - var mediaFormatList = default(StringBuilder); - foreach (var mediaFormat in MediaFormats) + if (!first) { - mediaFormatList ??= new StringBuilder(); - mediaFormatList.Append(mediaFormat.Key).Append(' '); + builder.Append(' '); } - - if (mediaFormatList == null) - { - return null; - } - - mediaFormatList.Length--; - return mediaFormatList.ToString(); + builder.Append(mediaFormat.Key); + first = false; } } + } - public string GetFormatListAttributesToString() + private void GetFormatListAttributesToString(ref ValueStringBuilder builder) + { + switch (Media) { - if (Media == SDPMediaTypesEnum.application) - { - if (ApplicationMediaFormats.Count > 0) + case SDPMediaTypesEnum.application: { - StringBuilder sb = new StringBuilder(); - foreach (var appFormat in ApplicationMediaFormats) + if (ApplicationMediaFormats.Count > 0) { - if (appFormat.Value.Rtpmap != null) + foreach (var appFormat in ApplicationMediaFormats) { - sb.Append(MEDIA_FORMAT_ATTRIBUTE_PREFIX).Append(appFormat.Key).Append(' ') - .Append(appFormat.Value.Rtpmap).Append(m_CRLF); - } + if (appFormat.Value.Rtpmap is { }) + { + builder.Append(MEDIA_FORMAT_ATTRIBUTE_PREFIX); + builder.Append(appFormat.Key); + builder.Append(' '); + builder.Append(appFormat.Value.Rtpmap); + builder.Append(m_CRLF); + } - if (appFormat.Value.Fmtp != null) - { - sb.Append(MEDIA_FORMAT_PARAMETERS_ATTRIBUE_PREFIX).Append(appFormat.Key).Append(' ') - .Append(appFormat.Value.Fmtp).Append(m_CRLF); + if (appFormat.Value.Fmtp is { }) + { + builder.Append(MEDIA_FORMAT_PARAMETERS_ATTRIBUE_PREFIX); + builder.Append(appFormat.Key); + builder.Append(' '); + builder.Append(appFormat.Value.Fmtp); + builder.Append(m_CRLF); + } } } - - return sb.ToString(); - } - else - { - return null; } - } - else if (Media == SDPMediaTypesEnum.message) - { - StringBuilder sb = new StringBuilder(); - var mediaFormat = MessageMediaFormat; - var acceptTypes = mediaFormat.AcceptTypes; - if (acceptTypes != null && acceptTypes.Count > 0) + break; + + case SDPMediaTypesEnum.message: { - sb.Append(MEDIA_FORMAT_PATH_ACCEPT_TYPES_PREFIX); - foreach (var type in acceptTypes) + var mediaFormat = MessageMediaFormat; + var acceptTypes = mediaFormat.AcceptTypes; + if (acceptTypes is { } && acceptTypes.Count > 0) { - sb.Append(type).Append(' '); + builder.Append(MEDIA_FORMAT_PATH_ACCEPT_TYPES_PREFIX); + foreach (var type in acceptTypes) + { + builder.Append(type); + builder.Append(' '); + } + builder.Append(m_CRLF); } - sb.Append(m_CRLF); - } - - if (mediaFormat.Endpoint != null) - { - sb.Append(MEDIA_FORMAT_PATH_MSRP_PREFIX).Append("//").Append(Connection.ConnectionAddress).Append(':') - .Append(Port).Append('/').Append(mediaFormat.Endpoint).Append(m_CRLF); + if (mediaFormat.Endpoint is { }) + { + builder.Append(MEDIA_FORMAT_PATH_MSRP_PREFIX); + builder.Append("//"); + Debug.Assert(Connection is { }); + builder.Append(Connection.ConnectionAddress); + builder.Append(':'); + builder.Append(Port); + builder.Append('/'); + builder.Append(mediaFormat.Endpoint); + builder.Append(m_CRLF); + } } - return sb.ToString(); - } - else - { - var formatAttributes = default(StringBuilder); + break; - if (MediaFormats != null) + default: + if (MediaFormats is { }) { - foreach (var mediaFormat in MediaFormats.Select(y => y.Value)) + foreach (var kv in MediaFormats) { - formatAttributes ??= new StringBuilder(); - if (mediaFormat.Rtpmap == null) + var mediaFormat = kv.Value; + if (mediaFormat.Rtpmap is null) { // Well known media formats are not required to add an rtpmap but we do so any way as some SIP // stacks don't work without it. - formatAttributes.Append(MEDIA_FORMAT_ATTRIBUTE_PREFIX).Append(mediaFormat.ID).Append(' ') - .Append(mediaFormat.Name()).Append('/').Append(mediaFormat.ClockRate()).Append(m_CRLF); + builder.Append(MEDIA_FORMAT_ATTRIBUTE_PREFIX); + builder.Append(mediaFormat.ID); + builder.Append(' '); + builder.Append(mediaFormat.Name()); + builder.Append('/'); + builder.Append(mediaFormat.ClockRate()); + builder.Append(m_CRLF); } else { - formatAttributes.Append(MEDIA_FORMAT_ATTRIBUTE_PREFIX).Append(mediaFormat.ID).Append(' ') - .Append(mediaFormat.Rtpmap).Append(m_CRLF); + builder.Append(MEDIA_FORMAT_ATTRIBUTE_PREFIX); + builder.Append(mediaFormat.ID); + builder.Append(' '); + builder.Append(mediaFormat.Rtpmap); + builder.Append(m_CRLF); } // Leaving out the feedback attribute for now. It should only be added where it's present in a parsed SDP packet or @@ -545,86 +674,94 @@ public string GetFormatListAttributesToString() { foreach (var rtcpFeedbackMessage in mediaFormat.SupportedRtcpFeedbackMessages) { - formatAttributes.Append(MEDIA_FORMAT_FEEDBACK_PREFIX).Append(mediaFormat.ID).Append(' ') - .Append(rtcpFeedbackMessage).Append(m_CRLF); + builder.Append(MEDIA_FORMAT_FEEDBACK_PREFIX); + builder.Append(mediaFormat.ID); + builder.Append(' '); + builder.Append(rtcpFeedbackMessage); + builder.Append(m_CRLF); } } - if (mediaFormat.Fmtp != null) + if (mediaFormat.Fmtp is { }) { - formatAttributes.Append(MEDIA_FORMAT_PARAMETERS_ATTRIBUE_PREFIX).Append(mediaFormat.ID).Append(' ') - .Append(mediaFormat.Fmtp).Append(m_CRLF); + builder.Append(MEDIA_FORMAT_PARAMETERS_ATTRIBUE_PREFIX); + builder.Append(mediaFormat.ID); + builder.Append(' '); + builder.Append(mediaFormat.Fmtp); + builder.Append(m_CRLF); } } } - return formatAttributes?.ToString(); - } + break; } + } - public void AddExtra(string attribute) + public void AddExtra(string attribute) + { + if (!string.IsNullOrWhiteSpace(attribute)) { - if (!string.IsNullOrWhiteSpace(attribute)) - { - ExtraMediaAttributes.Add(attribute); - } + ExtraMediaAttributes.Add(attribute); } + } - public bool HasCryptoLine(SDPSecurityDescription.CryptoSuites cryptoSuite) + public bool HasCryptoLine(SDPSecurityDescription.CryptoSuites cryptoSuite) + { + if (this.SecurityDescriptions is null) { - if (this.SecurityDescriptions == null) - { - return false; - } - foreach (SDPSecurityDescription secdesc in this.SecurityDescriptions) - { - if (secdesc.CryptoSuite == cryptoSuite) - { - return true; - } - } - return false; } - - public SDPSecurityDescription GetCryptoLine(SDPSecurityDescription.CryptoSuites cryptoSuite) + foreach (var secdesc in this.SecurityDescriptions) { - if (this.SecurityDescriptions == null) + if (secdesc.CryptoSuite == cryptoSuite) { - return null; - } - foreach (SDPSecurityDescription secdesc in this.SecurityDescriptions) - { - if (secdesc.CryptoSuite == cryptoSuite) - { - return secdesc; - } + return true; } + } + + return false; + } + public SDPSecurityDescription? GetCryptoLine(SDPSecurityDescription.CryptoSuites cryptoSuite) + { + if (this.SecurityDescriptions is null) + { return null; } - - public void AddCryptoLine(string crypto) + foreach (var secdesc in this.SecurityDescriptions) { - this.SecurityDescriptions.Add(SDPSecurityDescription.Parse(crypto)); + if (secdesc.CryptoSuite == cryptoSuite) + { + return secdesc; + } } - /// - /// Attempts to locate a media format corresponding to telephone events. If available its - /// format ID is returned. - /// - /// If found the format ID for telephone events or -1 if not. - public int GetTelephoneEventFormatID() + return null; + } + + public void AddCryptoLine(string crypto) + { + var sdpSecurityDescription = SDPSecurityDescription.Parse(crypto); + Debug.Assert(sdpSecurityDescription is { }); + this.SecurityDescriptions.Add(sdpSecurityDescription); + } + + /// + /// Attempts to locate a media format corresponding to telephone events. If available its + /// format ID is returned. + /// + /// If found the format ID for telephone events or -1 if not. + public int GetTelephoneEventFormatID() + { + foreach (var kv in MediaFormats) { - foreach (var mediaFormat in MediaFormats.Values) + var mediaFormat = kv.Value; + if (mediaFormat.Name()?.StartsWith(SDP.TELEPHONE_EVENT_ATTRIBUTE) == true) { - if (mediaFormat.Name()?.StartsWith(SDP.TELEPHONE_EVENT_ATTRIBUTE) == true) - { - return mediaFormat.ID; - } + return mediaFormat.ID; } - - return -1; } + + return -1; } } diff --git a/src/SIPSorcery/net/SDP/SDPMessageMediaFormat.cs b/src/SIPSorcery/net/SDP/SDPMessageMediaFormat.cs index b219f80033..e4709e2070 100644 --- a/src/SIPSorcery/net/SDP/SDPMessageMediaFormat.cs +++ b/src/SIPSorcery/net/SDP/SDPMessageMediaFormat.cs @@ -17,15 +17,14 @@ using System.Collections.Generic; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public class SDPMessageMediaFormat { - public class SDPMessageMediaFormat - { - public List AcceptTypes; + public List? AcceptTypes; - public string Endpoint; - public string IP; + public string? Endpoint; + public string? IP; - public string Port; - } -} \ No newline at end of file + public string? Port; +} diff --git a/src/SIPSorcery/net/SDP/SDPSecurityDescription.cs b/src/SIPSorcery/net/SDP/SDPSecurityDescription.cs index 03a38552dc..4abe3569d3 100644 --- a/src/SIPSorcery/net/SDP/SDPSecurityDescription.cs +++ b/src/SIPSorcery/net/SDP/SDPSecurityDescription.cs @@ -8,950 +8,1230 @@ // rj2 using System; +using System.Collections.Frozen; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text; -using Polyfills; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// (SDP) Security Descriptions for Media Streams implementation as basically defined in RFC 4568. +/// +/// Example 1: Parse crypto attribute +/// +/// string crypto = "a=crypto:1 AES_256_CM_HMAC_SHA1_80 inline:GTuZoqOsesiK4wfyL7Rsq6uHHwhqVGA+aVuAUnsmWktYacZyJu6/6tUQeUti0Q=="; +/// SDPSecurityDescription localcrypto = SDPSecurityDescription.Parse(crypto); +/// +/// +/// +/// Example 2: Parse crypto attribute +/// +/// SDPMediaAnnouncement mediaAudio = new SDPMediaAnnouncement(); +/// //[...]set some SDPMediaAnnouncement properties +/// SDPSecurityDescription localcrypto = SDPSecurityDescription.CreateNew(); +/// localcrypto.KeyParams.Clear(); +/// localcrypto.KeyParams.Add(SDPSecurityDescription.KeyParameter.CreateNew(SDPSecurityDescription.CryptoSuites.AES_CM_128_HMAC_SHA1_32)); +/// mediaAudio.SecurityDescriptions.Add(localcrypto); +/// mediaAudio.ToString(); +/// +/// string crypto = "a=crypto:1 AES_256_CM_HMAC_SHA1_80 inline:GTuZoqOsesiK4wfyL7Rsq6uHHwhqVGA+aVuAUnsmWktYacZyJu6/6tUQeUti0Q=="; +/// SDPSecurityDescription desc = SDPSecurityDescription.Parse(crypto); +/// +/// +/// +public class SDPSecurityDescription : IEquatable { - /// - /// (SDP) Security Descriptions for Media Streams implementation as basically defined in RFC 4568. - /// - /// Example 1: Parse crypto attribute - /// - /// string crypto = "a=crypto:1 AES_256_CM_HMAC_SHA1_80 inline:GTuZoqOsesiK4wfyL7Rsq6uHHwhqVGA+aVuAUnsmWktYacZyJu6/6tUQeUti0Q=="; - /// SDPSecurityDescription localcrypto = SDPSecurityDescription.Parse(crypto); - /// - /// - /// - /// Example 2: Parse crypto attribute - /// - /// SDPMediaAnnouncement mediaAudio = new SDPMediaAnnouncement(); - /// //[...]set some SDPMediaAnnouncement properties - /// SDPSecurityDescription localcrypto = SDPSecurityDescription.CreateNew(); - /// localcrypto.KeyParams.Clear(); - /// localcrypto.KeyParams.Add(SDPSecurityDescription.KeyParameter.CreateNew(SDPSecurityDescription.CryptoSuites.AES_CM_128_HMAC_SHA1_32)); - /// mediaAudio.SecurityDescriptions.Add(localcrypto); - /// mediaAudio.ToString(); - /// - /// string crypto = "a=crypto:1 AES_256_CM_HMAC_SHA1_80 inline:GTuZoqOsesiK4wfyL7Rsq6uHHwhqVGA+aVuAUnsmWktYacZyJu6/6tUQeUti0Q=="; - /// SDPSecurityDescription desc = SDPSecurityDescription.Parse(crypto); - /// - /// - /// - public class SDPSecurityDescription + public const string CRYPTO_ATTRIBUTE_NAME = "crypto"; + public const string CRYPTO_ATTRIBUE_PREFIX = $"a={CRYPTO_ATTRIBUTE_NAME}:"; + private static readonly char[] WHITE_SPACES = new char[] { ' ', '\t' }; + private const char SEMI_COLON = ';'; + private const string COLON = ":"; + private const string WHITE_SPACE = " "; + public enum CryptoSuites + { + unknown, + AES_CM_128_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc4568 + AES_CM_128_HMAC_SHA1_32, //https://tools.ietf.org/html/rfc4568 + F8_128_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc4568 + AEAD_AES_128_GCM, //https://tools.ietf.org/html/rfc7714 + AEAD_AES_256_GCM, //https://tools.ietf.org/html/rfc7714 + AES_192_CM_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc6188 + AES_192_CM_HMAC_SHA1_32, //https://tools.ietf.org/html/rfc6188 + AES_256_CM_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc6188 + AES_256_CM_HMAC_SHA1_32, //https://tools.ietf.org/html/rfc6188 + //duplicates, for wrong spelling in Ozeki-voip-sdk and who knows where else + AES_CM_192_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc6188 + AES_CM_192_HMAC_SHA1_32, //https://tools.ietf.org/html/rfc6188 + AES_CM_256_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc6188 + AES_CM_256_HMAC_SHA1_32 //https://tools.ietf.org/html/rfc6188 + } + private static readonly FrozenDictionary s_cryptoSuiteLookup = CreateCryptoSuiteLookup(); + private static FrozenDictionary CreateCryptoSuiteLookup() { - public const string CRYPTO_ATTRIBUE_PREFIX = "a=crypto:"; - private static readonly char[] WHITE_SPACES = new char[] { ' ', '\t' }; - private const char SEMI_COLON = ';'; - private const string COLON = ":"; - private const string WHITE_SPACE = " "; - public enum CryptoSuites + var values = (CryptoSuites[])Enum.GetValues(typeof(CryptoSuites)); + var lookup = new Dictionary(values.Length - 1, StringComparer.Ordinal); + foreach (CryptoSuites cs in values) { - unknown, - AES_CM_128_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc4568 - AES_CM_128_HMAC_SHA1_32, //https://tools.ietf.org/html/rfc4568 - F8_128_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc4568 - AEAD_AES_128_GCM, //https://tools.ietf.org/html/rfc7714 - AEAD_AES_256_GCM, //https://tools.ietf.org/html/rfc7714 - AES_192_CM_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc6188 - AES_192_CM_HMAC_SHA1_32, //https://tools.ietf.org/html/rfc6188 - AES_256_CM_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc6188 - AES_256_CM_HMAC_SHA1_32, //https://tools.ietf.org/html/rfc6188 - //duplicates, for wrong spelling in Ozeki-voip-sdk and who knows where else - AES_CM_192_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc6188 - AES_CM_192_HMAC_SHA1_32, //https://tools.ietf.org/html/rfc6188 - AES_CM_256_HMAC_SHA1_80, //https://tools.ietf.org/html/rfc6188 - AES_CM_256_HMAC_SHA1_32 //https://tools.ietf.org/html/rfc6188 - } - private static readonly Dictionary s_cryptoSuiteLookup = CreateCryptoSuiteLookup(); - private static Dictionary CreateCryptoSuiteLookup() - { - var values = Enum.GetValues(typeof(CryptoSuites)); - var lookup = new Dictionary(values.Length - 1, StringComparer.Ordinal); - foreach (CryptoSuites cs in values) - { - if (cs != CryptoSuites.unknown) - { - lookup[cs.ToString()] = cs; - } + if (cs != CryptoSuites.unknown) + { + lookup[cs.ToString()] = cs; } - return lookup; } - public class KeyParameter + return lookup.ToFrozenDictionary(); + } + public class KeyParameter + { + private const char COLON = ':'; + private const char PIPE = '|'; + public const string KEY_METHOD = "inline"; + + //128 bit for AES_CM_128_HMAC_SHA1_80, AES_CM_128_HMAC_SHA1_32, F8_128_HMAC_SHA1_80, AEAD_AES_128_GCM + //192 bit for AES_192_CM_HMAC_SHA1_80, AES_192_CM_HMAC_SHA1_32 + //256 bit for AEAD_AES_256_GCM, AES_256_CM_HMAC_SHA1_80, AES_256_CM_HMAC_SHA1_32 + // + public byte[] Key { - private const string COLON = ":"; - private const string PIPE = "|"; - public const string KEY_METHOD = "inline"; - private byte[] m_key = null; - //128 bit for AES_CM_128_HMAC_SHA1_80, AES_CM_128_HMAC_SHA1_32, F8_128_HMAC_SHA1_80, AEAD_AES_128_GCM - //192 bit for AES_192_CM_HMAC_SHA1_80, AES_192_CM_HMAC_SHA1_32 - //256 bit for AEAD_AES_256_GCM, AES_256_CM_HMAC_SHA1_80, AES_256_CM_HMAC_SHA1_32 - // - public byte[] Key + get + { + Debug.Assert(field is { Length: >= 16 }); + return field; + } + set { - get + ArgumentNullException.ThrowIfNull(value); + ArgumentOutOfRangeException.ThrowIfLessThan(value.Length, 16); + if (!IsValidKey(value)) { - return this.m_key; + throw value == null + ? new ArgumentNullException("Key", "Key must have a value") + : new ArgumentOutOfRangeException("Key", "Key must be at least 16 characters long"); } - set - { - if (!IsValidKey(value)) - { - throw value == null - ? new ArgumentNullException("Key", "Key must have a value") - : new ArgumentOutOfRangeException("Key", "Key must be at least 16 characters long"); - } - this.m_key = value; - } + field = value; + } + } + + //112 bit for AES_CM_128_HMAC_SHA1_80, AES_CM_128_HMAC_SHA1_32, F8_128_HMAC_SHA1_80 + //112 bit for AES_192_CM_HMAC_SHA1_80,AES_192_CM_HMAC_SHA1_32 , AES_256_CM_HMAC_SHA1_80, AES_256_CM_HMAC_SHA1_32 + //96 bit for AEAD_AES_128_GCM + // + public byte[] Salt + { + get + { + Debug.Assert(field is { Length: >= 12 }); + return field; } - private byte[] m_salt = null; - //112 bit for AES_CM_128_HMAC_SHA1_80, AES_CM_128_HMAC_SHA1_32, F8_128_HMAC_SHA1_80 - //112 bit for AES_192_CM_HMAC_SHA1_80,AES_192_CM_HMAC_SHA1_32 , AES_256_CM_HMAC_SHA1_80, AES_256_CM_HMAC_SHA1_32 - //96 bit for AEAD_AES_128_GCM - // - public byte[] Salt + set { - get + if (!IsValidSalt(value)) { - return this.m_salt; + throw value == null + ? new ArgumentNullException("Salt", "Salt must have a value") + : new ArgumentOutOfRangeException("Salt", "Salt must be at least 12 characters long"); } - set - { - if (!IsValidSalt(value)) - { - throw value == null - ? new ArgumentNullException("Salt", "Salt must have a value") - : new ArgumentOutOfRangeException("Salt", "Salt must be at least 12 characters long"); - } - this.m_salt = value; - } + field = value; + } + } + + public string KeySaltBase64 + { + get + { + var b = new byte[this.Key.Length + this.Salt.Length]; + Array.Copy(this.Key, 0, b, 0, this.Key.Length); + Array.Copy(this.Salt, 0, b, this.Key.Length, this.Salt.Length); + var s64 = Convert.ToBase64String(b); + //removal of Padding-Characters "=" happens when decoding of Base64-String + //https://tools.ietf.org/html/rfc4568 page 13 + //s64 = s64.TrimEnd('='); + return s64; } - public string KeySaltBase64 + } + + public ulong LifeTime + { + get { - get - { - byte[] b = new byte[this.Key.Length + this.Salt.Length]; - Array.Copy(this.Key, 0, b, 0, this.Key.Length); - Array.Copy(this.Salt, 0, b, this.Key.Length, this.Salt.Length); - string s64 = Convert.ToBase64String(b); - //removal of Padding-Characters "=" happens when decoding of Base64-String - //https://tools.ietf.org/html/rfc4568 page 13 - //s64 = s64.TrimEnd('='); - return s64; - } + return field; } - private ulong m_lifeTime = 0; - public ulong LifeTime + set { - get + if (!IsValidLifeTime(value)) { - return this.m_lifeTime; + throw new ArgumentOutOfRangeException("LifeTime", "LifeTime value must be power of 2"); } - set + + var i = 0; + var temp = value; + while (temp > 1) { - if (!IsValidLifeTime(value)) - { - throw new ArgumentOutOfRangeException("LifeTime", "LifeTime value must be power of 2 (excluding 2^0)"); - } + temp >>= 1; + i++; + } - int i = 0; - ulong temp = value; - while (temp > 1) - { - temp >>= 1; - i++; - } + field = value; + string lifeTimeString = $"2^{i}"; - this.m_lifeTime = value; - this.m_sLifeTime = $"2^{i}"; + if (lifeTimeString != LifeTimeString) + { + LifeTimeString = lifeTimeString; } } - private string m_sLifeTime = null; - public string LifeTimeString + } + + public string? LifeTimeString + { + get + { + return field; + } + set { - get + if (!TryParseLifeTimeString(value, out ulong lifeTime)) { - return this.m_sLifeTime; + throw new ArgumentException("LifeTimeString must be in format '2^n' where n is a positive integer", "LifeTimeString"); } - set - { - if (!TryParseLifeTimeString(value, out var lifeTime)) - { - throw new ArgumentException("LifeTimeString must be in format '2^n' where n is a positive integer", "LifeTimeString"); - } - this.m_lifeTime = lifeTime; - this.m_sLifeTime = value; + field = value; + + if (lifeTime != LifeTime) + { + LifeTime = lifeTime; } + } - public uint MkiValue + } + + public uint MkiValue { get; set; } + + public uint MkiLength + { + get { - get; - set; + return field; } - private uint m_mkiLength = 0; - public uint MkiLength + set { - get + if (value is > 0 and <= 128) { - return this.m_mkiLength; + field = value; } - set + else { - if (value > 0 && value <= 128) - { - this.m_mkiLength = value; - } - else - { - throw new ArgumentOutOfRangeException("MkiLength", "MkiLength value must between 1 and 128"); - } + throw new ArgumentOutOfRangeException("MkiLength", "MkiLength value must between 1 and 128"); } } - public KeyParameter() : this(Sys.Crypto.GetRandomString(128 / 8), Sys.Crypto.GetRandomString(112 / 8)) - { + } + public KeyParameter() : this(Sys.Crypto.GetRandomString(128 / 8), Sys.Crypto.GetRandomString(112 / 8)) + { + + } + + public KeyParameter(string key, string salt) + { + this.Key = Encoding.ASCII.GetBytes(key); + this.Salt = Encoding.ASCII.GetBytes(salt); + } + + public KeyParameter(byte[] key, byte[] salt) + { + this.Key = key; + this.Salt = salt; + } + public override string ToString() + { + var builder = new ValueStringBuilder(stackalloc char[128]); + + try + { + ToString(ref builder); + return builder.ToString(); } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) + { + builder.Append(KEY_METHOD); + builder.Append(COLON); + builder.Append(this.KeySaltBase64); - public KeyParameter(string key, string salt) + if (!string.IsNullOrWhiteSpace(this.LifeTimeString)) + { + builder.Append(PIPE); + builder.Append(this.LifeTimeString); + } + else if (this.LifeTime > 0) { - this.Key = Encoding.ASCII.GetBytes(key); - this.Salt = Encoding.ASCII.GetBytes(salt); + builder.Append(PIPE); + builder.Append(this.LifeTime); } - public KeyParameter(byte[] key, byte[] salt) + if (this.MkiLength > 0 && this.MkiValue > 0) { - this.Key = key; - this.Salt = salt; + builder.Append(PIPE); + builder.Append(this.MkiValue); + builder.Append(COLON); + builder.Append(this.MkiLength); } + } - public override string ToString() + public static KeyParameter? Parse(ReadOnlySpan keyParamString, CryptoSuites cryptoSuite = CryptoSuites.AES_CM_128_HMAC_SHA1_80) + { + if (!TryParse(keyParamString, out var result, cryptoSuite)) { - var s = new StringBuilder(); - s.Append(KEY_METHOD).Append(COLON).Append(this.KeySaltBase64); - if (!string.IsNullOrWhiteSpace(this.LifeTimeString)) - { - s.Append(PIPE).Append(this.LifeTimeString); - } - else if (this.LifeTime > 0) - { - s.Append(PIPE).Append(this.LifeTime); - } + throw new FormatException($"keyParam '{keyParamString.ToString()}' is not recognized as a valid KEY_PARAM "); + } - if (this.MkiLength > 0 && this.MkiValue > 0) - { - s.Append(PIPE).Append(this.MkiValue).Append(COLON).Append(this.MkiLength); - } + return result!; + } - return s.ToString(); - } + public static bool TryParse(ReadOnlySpan keyParamString, out KeyParameter? keyParam, CryptoSuites cryptoSuite = CryptoSuites.AES_CM_128_HMAC_SHA1_80) + { + keyParam = null; - public static KeyParameter Parse(string keyParamString, CryptoSuites cryptoSuite = CryptoSuites.AES_CM_128_HMAC_SHA1_80) + keyParamString = keyParamString.Trim(); + if (keyParamString.IsEmpty) { - if (!TryParse(keyParamString, out var keyParam, cryptoSuite)) - { - throw new FormatException($"keyParam '{keyParamString}' is not recognized as a valid KEY_PARAM "); - } - return keyParam; + return false; } - public static bool TryParse(string keyParamString, out KeyParameter keyParam, CryptoSuites cryptoSuite = CryptoSuites.AES_CM_128_HMAC_SHA1_80) + if (keyParamString.StartsWith(KEY_METHOD, StringComparison.Ordinal)) { - keyParam = null; - - static bool CheckValidKeyInfoCharacters(ReadOnlySpan keyInfo) + var poscln = keyParamString.IndexOf(COLON); + if (poscln == KEY_METHOD.Length) { - foreach (var c in keyInfo) + var sKeyInfo = keyParamString.Slice(poscln + 1); + if (!sKeyInfo.Contains(SEMI_COLON)) { - if (c < 0x21 || c > 0x7e) + if (!checkValidKeyInfoCharacters(sKeyInfo) + || !parseKeyInfo(sKeyInfo, out var sMkiVal, out var sMkiLen, out var sLifeTime, out var sBase64KeySalt)) { return false; } - } - return true; - } - static bool ParseKeyInfo(ReadOnlySpan keyInfo, out string mkiValue, out string mkiLen, out string lifeTimeString, out string base64KeySalt) - { - mkiValue = null; - mkiLen = null; - lifeTimeString = null; - base64KeySalt = null; - //KeyInfo must only contain visible printing characters - //and 40 char long, as its is the base64representation of concatenated Key and Salt - var pospipe1 = keyInfo.IndexOf(PIPE); - if (pospipe1 > 0) - { - base64KeySalt = keyInfo.Slice(0, pospipe1).ToString(); - //find lifetime and mki - //both may be omitted, but mki is recognized by a colon - //usually lifetime comes before mki, if specified - var afterFirstPipe = pospipe1 + 1; - var keyInfoTail = keyInfo.Slice(afterFirstPipe); - var relativeColon = keyInfoTail.IndexOf(COLON); - var relativePipe = keyInfoTail.IndexOf(PIPE); - var posclnmki = relativeColon == -1 ? -1 : afterFirstPipe + relativeColon; - var pospipe2 = relativePipe == -1 ? -1 : afterFirstPipe + relativePipe; - - if (posclnmki > 0 && pospipe2 < 0) - { - mkiValue = keyInfo.Slice(pospipe1 + 1, posclnmki - pospipe1 - 1).ToString(); - mkiLen = keyInfo.Slice(posclnmki + 1).ToString(); - } - else if (posclnmki > 0 && pospipe2 < posclnmki) - { - lifeTimeString = keyInfo.Slice(pospipe1 + 1, pospipe2 - pospipe1 - 1).ToString(); - mkiValue = keyInfo.Slice(pospipe2 + 1, posclnmki - pospipe2 - 1).ToString(); - mkiLen = keyInfo.Slice(posclnmki + 1).ToString(); - } - else if (posclnmki > 0 && pospipe2 > posclnmki) + if (!string.IsNullOrWhiteSpace(sBase64KeySalt)) { - mkiValue = keyInfo.Slice(pospipe1 + 1, posclnmki - pospipe1 - 1).ToString(); - mkiLen = keyInfo.Slice(posclnmki + 1, pospipe2 - posclnmki - 1).ToString(); - lifeTimeString = keyInfo.Slice(pospipe2 + 1).ToString(); + if (!parseKeySaltBase64(cryptoSuite, sBase64KeySalt, out var bKey, out var bSalt) + || !IsValidKey(bKey) + || !IsValidSalt(bSalt)) + { + return false; + } + + Debug.Assert(bKey is { }); + Debug.Assert(bSalt is { }); + + keyParam = new KeyParameter(bKey, bSalt!); } - else if (posclnmki < 0 && pospipe2 < 0) + else { - lifeTimeString = keyInfo.Slice(pospipe1 + 1).ToString(); + keyParam = new KeyParameter(); } - else if (posclnmki < 0 && pospipe2 > 0) + + if (!string.IsNullOrWhiteSpace(sMkiVal) && !string.IsNullOrWhiteSpace(sMkiLen)) { - return false; - } - } - else - { - base64KeySalt = keyInfo.ToString(); - } + if (!uint.TryParse(sMkiVal, out uint mkiValue) + || !uint.TryParse(sMkiLen, out uint mkiLen) + || !(mkiLen > 0 && mkiLen <= 128)) + { + keyParam = null; + return false; + } - return true; - } + keyParam.MkiValue = mkiValue; + keyParam.MkiLength = mkiLen; + } - if (!string.IsNullOrWhiteSpace(keyParamString)) - { - var p = keyParamString.AsSpan().Trim(); - if (p.StartsWith(KEY_METHOD, StringComparison.Ordinal)) - { - int poscln = p.IndexOf(COLON); - if (poscln == KEY_METHOD.Length) + if (!string.IsNullOrWhiteSpace(sLifeTime)) { - var sKeyInfo = p.Slice(poscln + 1); - if (!sKeyInfo.Contains(SEMI_COLON)) + if (sLifeTime.Contains('^')) { - if ((!CheckValidKeyInfoCharacters(sKeyInfo)) - || (!ParseKeyInfo(sKeyInfo, out var sMkiVal, out var sMkiLen, out var sLifeTime, out var sBase64KeySalt))) + if (!TryParseLifeTimeString(sLifeTime, out ulong lifeTime)) { + keyParam = null; return false; } - if (!string.IsNullOrWhiteSpace(sBase64KeySalt)) - { - if (!parseKeySaltBase64(cryptoSuite, sBase64KeySalt, out var bKey, out var bSalt) - || !IsValidKey(bKey) - || !IsValidSalt(bSalt)) - { - return false; - } - - keyParam = new KeyParameter(bKey, bSalt); - } - else - { - keyParam = new KeyParameter(); - } - - if (!string.IsNullOrWhiteSpace(sMkiVal) && !string.IsNullOrWhiteSpace(sMkiLen)) - { - if (!uint.TryParse(sMkiVal, out var mkiValue) - || !uint.TryParse(sMkiLen, out var mkiLen) - || !(mkiLen > 0 && mkiLen <= 128)) - { - keyParam = null; - return false; - } - - keyParam.MkiValue = mkiValue; - keyParam.MkiLength = mkiLen; - } - - if (!string.IsNullOrWhiteSpace(sLifeTime)) + keyParam.LifeTime = lifeTime; + } + else + { + if (!uint.TryParse(sLifeTime, out uint lifeTime) + || !IsValidLifeTime(lifeTime)) { - if (sLifeTime.Contains("^")) - { - if (!TryParseLifeTimeString(sLifeTime, out var lifeTime)) - { - keyParam = null; - return false; - } - - keyParam.LifeTime = lifeTime; - } - else - { - if (!uint.TryParse(sLifeTime, out var lifeTime) - || !IsValidLifeTime(lifeTime)) - { - keyParam = null; - return false; - } - - keyParam.LifeTime = lifeTime; - } + keyParam = null; + return false; } - return true; + keyParam.LifeTime = lifeTime; } } + + return true; } } - - keyParam = null; - return false; } - private static bool parseKeySaltBase64(CryptoSuites cryptoSuite, string base64KeySalt, out byte[] key, out byte[] salt) - { - key = null; - salt = null; - - byte[] keysalt; - try - { - keysalt = Convert.FromBase64String(base64KeySalt); - } - catch - { - return false; - } - - int keyLength = 0; - int saltLength = 0; - int saltOffset = 0; - - switch (cryptoSuite) - { - case CryptoSuites.AES_CM_128_HMAC_SHA1_32: - case CryptoSuites.AES_CM_128_HMAC_SHA1_80: - case CryptoSuites.F8_128_HMAC_SHA1_80: - case CryptoSuites.AEAD_AES_128_GCM: - keyLength = 128 / 8; - saltOffset = 128 / 8; - saltLength = (cryptoSuite == CryptoSuites.AEAD_AES_128_GCM) ? 96 / 8 : 112 / 8; - break; - case CryptoSuites.AES_192_CM_HMAC_SHA1_80: - case CryptoSuites.AES_192_CM_HMAC_SHA1_32: - case CryptoSuites.AES_CM_192_HMAC_SHA1_80: - case CryptoSuites.AES_CM_192_HMAC_SHA1_32: - keyLength = 192 / 8; - saltOffset = 192 / 8; - saltLength = 112 / 8; - break; - case CryptoSuites.AEAD_AES_256_GCM: - keyLength = 256 / 8; - saltOffset = 256 / 8; - saltLength = 96 / 8; - break; - case CryptoSuites.AES_256_CM_HMAC_SHA1_80: - case CryptoSuites.AES_256_CM_HMAC_SHA1_32: - case CryptoSuites.AES_CM_256_HMAC_SHA1_80: - case CryptoSuites.AES_CM_256_HMAC_SHA1_32: - keyLength = 256 / 8; - saltOffset = 256 / 8; - saltLength = 112 / 8; - break; - default: - return false; - } - - if (keysalt.Length < keyLength + saltLength) - { - return false; - } - - key = new byte[keyLength]; - Array.Copy(keysalt, 0, key, 0, keyLength); + keyParam = null; + return false; + } - salt = new byte[saltLength]; - Array.Copy(keysalt, saltOffset, salt, 0, saltLength); + private static bool parseKeySaltBase64( + CryptoSuites cryptoSuite, + string base64KeySalt, + [NotNullWhen(true)]out byte[]? key, + [NotNullWhen(true)]out byte[]? salt) + { + key = null; + salt = null; - return true; + byte[] keysalt; + try + { + keysalt = Convert.FromBase64String(base64KeySalt); } - - private static bool IsValidLifeTime(ulong value) + catch { - return value >= 2 && (value & (value - 1)) == 0; + return false; } - private static bool IsValidKey(byte[] key) + int keyLength = 0; + int saltLength = 0; + int saltOffset = 0; + + switch (cryptoSuite) { - return key != null && key.Length >= 16; + case CryptoSuites.AES_CM_128_HMAC_SHA1_32: + case CryptoSuites.AES_CM_128_HMAC_SHA1_80: + case CryptoSuites.F8_128_HMAC_SHA1_80: + case CryptoSuites.AEAD_AES_128_GCM: + keyLength = 128 / 8; + saltOffset = 128 / 8; + saltLength = (cryptoSuite == CryptoSuites.AEAD_AES_128_GCM) ? 96 / 8 : 112 / 8; + break; + case CryptoSuites.AES_192_CM_HMAC_SHA1_80: + case CryptoSuites.AES_192_CM_HMAC_SHA1_32: + case CryptoSuites.AES_CM_192_HMAC_SHA1_80: + case CryptoSuites.AES_CM_192_HMAC_SHA1_32: + keyLength = 192 / 8; + saltOffset = 192 / 8; + saltLength = 112 / 8; + break; + case CryptoSuites.AEAD_AES_256_GCM: + keyLength = 256 / 8; + saltOffset = 256 / 8; + saltLength = 96 / 8; + break; + case CryptoSuites.AES_256_CM_HMAC_SHA1_80: + case CryptoSuites.AES_256_CM_HMAC_SHA1_32: + case CryptoSuites.AES_CM_256_HMAC_SHA1_80: + case CryptoSuites.AES_CM_256_HMAC_SHA1_32: + keyLength = 256 / 8; + saltOffset = 256 / 8; + saltLength = 112 / 8; + break; + default: + return false; } - private static bool IsValidSalt(byte[] salt) + if (keysalt.Length < keyLength + saltLength) { - return salt != null && salt.Length >= 12; + return false; } - private static bool TryParseLifeTimeString(string lifeTimeString, out ulong lifeTime) - { - lifeTime = 0; + key = new byte[keyLength]; + Array.Copy(keysalt, 0, key, 0, keyLength); - var lifeTimeSpan = lifeTimeString.AsSpan().Trim(); - if (lifeTimeSpan.IsEmpty || !lifeTimeSpan.StartsWith("2^", StringComparison.Ordinal)) - { - return false; - } + salt = new byte[saltLength]; + Array.Copy(keysalt, saltOffset, salt, 0, saltLength); - if (!ulong.TryParse(lifeTimeSpan.Slice(2), out var exponent) || exponent < 1 || exponent >= 64) + return true; + } + + private static bool checkValidKeyInfoCharacters(ReadOnlySpan keyInfo) + { + foreach (var c in keyInfo) + { + if (c is < (char)0x21 or > (char)0x7e) { return false; } - - lifeTime = 1UL << (int)exponent; - return true; } - public static KeyParameter CreateNew(CryptoSuites cryptoSuite, string key = null, string salt = null) - { - switch (cryptoSuite) - { - case CryptoSuites.AES_CM_128_HMAC_SHA1_32: - case CryptoSuites.AES_CM_128_HMAC_SHA1_80: - case CryptoSuites.F8_128_HMAC_SHA1_80: - if (string.IsNullOrWhiteSpace(key)) - { - key = Sys.Crypto.GetRandomString(128 / 8); - } - if (string.IsNullOrWhiteSpace(salt)) - { - salt = Sys.Crypto.GetRandomString(112 / 8); - } - return new KeyParameter(key, salt); - case CryptoSuites.AES_192_CM_HMAC_SHA1_80: - case CryptoSuites.AES_192_CM_HMAC_SHA1_32: - case CryptoSuites.AES_CM_192_HMAC_SHA1_80: - case CryptoSuites.AES_CM_192_HMAC_SHA1_32: - if (string.IsNullOrWhiteSpace(key)) - { - key = Sys.Crypto.GetRandomString(192 / 8); - } - if (string.IsNullOrWhiteSpace(salt)) - { - salt = Sys.Crypto.GetRandomString(112 / 8); - } - return new KeyParameter(key, salt); - case CryptoSuites.AEAD_AES_128_GCM: - if (string.IsNullOrWhiteSpace(key)) - { - key = Sys.Crypto.GetRandomString(128 / 8); - } - if (string.IsNullOrWhiteSpace(salt)) - { - salt = Sys.Crypto.GetRandomString(96 / 8); - } - return new KeyParameter(key, salt); - case CryptoSuites.AEAD_AES_256_GCM: - if (string.IsNullOrWhiteSpace(key)) - { - key = Sys.Crypto.GetRandomString(256 / 8); - } - if (string.IsNullOrWhiteSpace(salt)) - { - salt = Sys.Crypto.GetRandomString(96 / 8); - } - return new KeyParameter(key, salt); - case CryptoSuites.AES_256_CM_HMAC_SHA1_80: - case CryptoSuites.AES_256_CM_HMAC_SHA1_32: - case CryptoSuites.AES_CM_256_HMAC_SHA1_80: - case CryptoSuites.AES_CM_256_HMAC_SHA1_32: - if (string.IsNullOrWhiteSpace(key)) - { - key = Sys.Crypto.GetRandomString(256 / 8); - } - if (string.IsNullOrWhiteSpace(salt)) - { - salt = Sys.Crypto.GetRandomString(112 / 8); - } - return new KeyParameter(key, salt); + return true; + } - } - return null; - } + private static bool IsValidLifeTime(ulong value) + { + return value >= 2 && (value & (value - 1)) == 0; } - public class SessionParameter + private static bool IsValidKey(byte[] key) { - public enum SrtpSessionParams - { - unknown, - kdr, - UNENCRYPTED_SRTP, - UNENCRYPTED_SRTCP, - UNAUTHENTICATED_SRTP, - fec_order, - fec_key, - wsh - } - public SrtpSessionParams SrtpSessionParam - { - get; - set; - } - public enum FecTypes + return key != null && key.Length >= 16; + } + + private static bool IsValidSalt(byte[] salt) + { + return salt != null && salt.Length >= 12; + } + + private static bool TryParseLifeTimeString(ReadOnlySpan lifeTimeString, out ulong lifeTime) + { + lifeTime = 0; + + if (lifeTimeString.IsEmptyOrWhiteSpace() || !lifeTimeString.StartsWith("2^")) { - unknown, - FEC_SRTP, - SRTP_FEC + return false; } - public FecTypes FecOrder + + var exponentPart = lifeTimeString.Slice(2); + if (!ulong.TryParse(exponentPart, out ulong exponent) || exponent < 1) { - get; - set; + return false; } - public const string FEC_KEY_PREFIX = "FEC_KEY="; - public const string FEC_ORDER_PREFIX = "FEC_ORDER="; - public const string WSH_PREFIX = "WSH="; - public const string KDR_PREFIX = "KDR="; - private static readonly Dictionary s_fecTypesLookup = CreateFecTypesLookup(); - private static Dictionary CreateFecTypesLookup() + lifeTime = (ulong)Math.Pow(2, (double)exponent); + return true; + } + + private static bool parseKeyInfo(ReadOnlySpan keyInfo, out string? mkiValue, out string? mkiLen, out string? lifeTimeString, out string? base64KeySalt) + { + // Examples of keyInfo formats: + // - "WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz" (base64 only) + // - "WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz|2^20" (base64|lifetime) + // - "WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz|2^20|1:4" (base64|lifetime|mki:mkilen) + // - "WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz|1:4" (base64|mki:mkilen) + + mkiValue = null; + mkiLen = null; + lifeTimeString = null; + base64KeySalt = null; + //KeyInfo must only contain visible printing characters + //and 40 char long, as its is the base64representation of concatenated Key and Salt + var pospipe1 = keyInfo.IndexOf(PIPE); + if (pospipe1 > 0) { - var values = Enum.GetValues(typeof(FecTypes)); - var lookup = new Dictionary(values.Length - 1, StringComparer.Ordinal); - foreach (FecTypes ft in values) + base64KeySalt = keyInfo.Slice(0, pospipe1).ToString(); + //find lifetime and mki + //both may be omitted, but mki is recognized by a colon + //usually lifetime comes before mki, if specified + var remaining = keyInfo.Slice(pospipe1 + 1); + var posclnmki = remaining.IndexOf(COLON); + var pospipe2 = remaining.IndexOf(PIPE); + if (posclnmki > 0 && pospipe2 < 0) { - if (ft != FecTypes.unknown) - { - lookup[ft.ToString()] = ft; - } + mkiValue = remaining.Slice(0, posclnmki).ToString(); + mkiLen = remaining.Slice(posclnmki + 1).ToString(); } - return lookup; - } - - private ulong m_kdr = 0; - public ulong Kdr - { - get + else if (posclnmki > 0 && pospipe2 < posclnmki) { - return this.m_kdr; + lifeTimeString = remaining.Slice(0, pospipe2).ToString(); + mkiValue = remaining.Slice(pospipe2 + 1, posclnmki - pospipe2 - 1).ToString(); + mkiLen = remaining.Slice(posclnmki + 1).ToString(); } - set + else if (posclnmki > 0 && pospipe2 > posclnmki) { - if (value < 0 || value > 24) - { - throw new ArgumentOutOfRangeException("Kdr", "Kdr must be between 0 and 24"); - } - - this.m_kdr = value; + mkiValue = remaining.Slice(0, posclnmki).ToString(); + mkiLen = remaining.Slice(posclnmki + 1, pospipe2 - posclnmki - 1).ToString(); + lifeTimeString = remaining.Slice(pospipe2 + 1).ToString(); } - } - private ulong m_wsh = 64; - public ulong Wsh - { - get + else if (posclnmki < 0 && pospipe2 < 0) { - return this.m_wsh; + lifeTimeString = remaining.ToString(); } - set + else if (posclnmki < 0 && pospipe2 > 0) { - if (value < 64) - { - throw new ArgumentOutOfRangeException("WSH", "WSH must be greater than 64"); - } - - this.m_wsh = value; + return false; } } - - public KeyParameter FecKey + else { - get; - set; + base64KeySalt = keyInfo.ToString(); } - public SessionParameter() : this(SrtpSessionParams.unknown) - { + return true; + } - } - public SessionParameter(SrtpSessionParams paramType) + public static KeyParameter? CreateNew(CryptoSuites cryptoSuite, string? key = null, string? salt = null) + { + switch (cryptoSuite) { - this.SrtpSessionParam = paramType; - } - public override string ToString() - { - if (this.SrtpSessionParam == SrtpSessionParams.unknown) - { - return ""; - } - - switch (this.SrtpSessionParam) - { - case SrtpSessionParams.UNAUTHENTICATED_SRTP: - case SrtpSessionParams.UNENCRYPTED_SRTP: - case SrtpSessionParams.UNENCRYPTED_SRTCP: - return this.SrtpSessionParam.ToString(); - case SrtpSessionParams.wsh: - return $"{WSH_PREFIX}{this.Wsh}"; - case SrtpSessionParams.kdr: - return $"{KDR_PREFIX}{this.Kdr}"; - case SrtpSessionParams.fec_order: - return $"{FEC_ORDER_PREFIX}{this.FecOrder.ToString()}"; - case SrtpSessionParams.fec_key: - return $"{FEC_KEY_PREFIX}{this.FecKey?.ToString()}"; - } - return ""; - } - - public static SessionParameter Parse(string sessionParam, CryptoSuites cryptoSuite = CryptoSuites.AES_CM_128_HMAC_SHA1_80) - { - if (!TryParse(sessionParam, out var result, cryptoSuite)) - { - throw new FormatException($"sessionParam '{sessionParam}' is not recognized as a valid SRTP_SESSION_PARAM "); - } - - return result; - } - - public static bool TryParse(string sessionParam, out SessionParameter result, CryptoSuites cryptoSuite = CryptoSuites.AES_CM_128_HMAC_SHA1_80) - { - result = null; - - if (string.IsNullOrWhiteSpace(sessionParam)) - { - return true; - } - - var p = sessionParam.AsSpan().Trim(); - SessionParameter.SrtpSessionParams paramType = SrtpSessionParams.unknown; - if (p.StartsWith(KDR_PREFIX, StringComparison.Ordinal)) - { - if (uint.TryParse(p.Slice(KDR_PREFIX.Length), out var kdr)) + case CryptoSuites.AES_CM_128_HMAC_SHA1_32: + case CryptoSuites.AES_CM_128_HMAC_SHA1_80: + case CryptoSuites.F8_128_HMAC_SHA1_80: + if (string.IsNullOrWhiteSpace(key)) { - result = new SessionParameter(SrtpSessionParams.kdr) { Kdr = kdr }; - return true; + key = Sys.Crypto.GetRandomString(128 / 8); } - } - else if (p.StartsWith(WSH_PREFIX, StringComparison.Ordinal)) - { - if (uint.TryParse(p.Slice(WSH_PREFIX.Length), out var wsh)) + if (string.IsNullOrWhiteSpace(salt)) { - result = new SessionParameter(SrtpSessionParams.wsh) { Wsh = wsh }; - return true; + salt = Sys.Crypto.GetRandomString(112 / 8); } - } - else if (p.StartsWith(FEC_KEY_PREFIX, StringComparison.Ordinal)) - { - var sFecKey = p.Slice(FEC_KEY_PREFIX.Length).ToString(); - if (!KeyParameter.TryParse(sFecKey, out var fecKey, cryptoSuite)) + return new KeyParameter(key, salt); + case CryptoSuites.AES_192_CM_HMAC_SHA1_80: + case CryptoSuites.AES_192_CM_HMAC_SHA1_32: + case CryptoSuites.AES_CM_192_HMAC_SHA1_80: + case CryptoSuites.AES_CM_192_HMAC_SHA1_32: + if (string.IsNullOrWhiteSpace(key)) { - return false; + key = Sys.Crypto.GetRandomString(192 / 8); } - result = new SessionParameter(SrtpSessionParams.fec_key) { FecKey = fecKey }; - return true; - } - else if (p.StartsWith(FEC_ORDER_PREFIX, StringComparison.Ordinal)) - { - var sFecOrder = p.Slice(FEC_ORDER_PREFIX.Length).ToString(); - if (!s_fecTypesLookup.TryGetValue(sFecOrder, out var fecOrder)) + if (string.IsNullOrWhiteSpace(salt)) { - return false; + salt = Sys.Crypto.GetRandomString(112 / 8); } - - result = new SessionParameter(SrtpSessionParams.fec_order) { FecOrder = fecOrder }; - return true; - } - else - { - var paramString = p.ToString(); - if (!Enum.TryParse(paramString, out paramType) || paramType.ToString() != paramString) + return new KeyParameter(key, salt); + case CryptoSuites.AEAD_AES_128_GCM: + if (string.IsNullOrWhiteSpace(key)) { - return false; + key = Sys.Crypto.GetRandomString(128 / 8); } - - switch (paramType) + if (string.IsNullOrWhiteSpace(salt)) { - case SrtpSessionParams.UNAUTHENTICATED_SRTP: - case SrtpSessionParams.UNENCRYPTED_SRTCP: - case SrtpSessionParams.UNENCRYPTED_SRTP: - result = new SessionParameter(paramType); - return true; + salt = Sys.Crypto.GetRandomString(96 / 8); } - } + return new KeyParameter(key, salt); + case CryptoSuites.AEAD_AES_256_GCM: + if (string.IsNullOrWhiteSpace(key)) + { + key = Sys.Crypto.GetRandomString(256 / 8); + } + if (string.IsNullOrWhiteSpace(salt)) + { + salt = Sys.Crypto.GetRandomString(96 / 8); + } + return new KeyParameter(key, salt); + case CryptoSuites.AES_256_CM_HMAC_SHA1_80: + case CryptoSuites.AES_256_CM_HMAC_SHA1_32: + case CryptoSuites.AES_CM_256_HMAC_SHA1_80: + case CryptoSuites.AES_CM_256_HMAC_SHA1_32: + if (string.IsNullOrWhiteSpace(key)) + { + key = Sys.Crypto.GetRandomString(256 / 8); + } + if (string.IsNullOrWhiteSpace(salt)) + { + salt = Sys.Crypto.GetRandomString(112 / 8); + } + return new KeyParameter(key, salt); - return false; } + return null; } + } + public class SessionParameter + { + public enum SrtpSessionParams + { + unknown, + kdr, + UNENCRYPTED_SRTP, + UNENCRYPTED_SRTCP, + UNAUTHENTICATED_SRTP, + fec_order, + fec_key, + wsh + } + public SrtpSessionParams SrtpSessionParam + { + get; + set; + } + public enum FecTypes + { + unknown, + FEC_SRTP, + SRTP_FEC + } + public FecTypes FecOrder + { + get; + set; + } + public const string FEC_KEY_PREFIX = "FEC_KEY="; + public const string FEC_ORDER_PREFIX = "FEC_ORDER="; + public const string WSH_PREFIX = "WSH="; + public const string KDR_PREFIX = "KDR="; - private uint m_iTag = 1; - public uint Tag + private ulong m_kdr = 0; + public ulong Kdr { get { - return this.m_iTag; + return this.m_kdr; } set { - if (value > 0 && value < 1000000000) + if (value is < 0 or > 24) { - this.m_iTag = value; + throw new ArgumentOutOfRangeException("Kdr", "Kdr must be between 0 and 24"); } - else + + this.m_kdr = value; + } + } + private ulong m_wsh = 64; + public ulong Wsh + { + get + { + return this.m_wsh; + } + set + { + if (value < 64) { - throw new ArgumentOutOfRangeException("Tag", "Tag value must be greater than 0 and not exceed 9 digits"); + throw new ArgumentOutOfRangeException("WSH", "WSH must be greater than 64"); } + + this.m_wsh = value; } } - - public CryptoSuites CryptoSuite + public KeyParameter? FecKey { get; set; } - public List KeyParams + public SessionParameter() : this(SrtpSessionParams.unknown) { - get; - set; + } - public SessionParameter SessionParam + public SessionParameter(SrtpSessionParams paramType) { - get; - set; + this.SrtpSessionParam = paramType; } - public SDPSecurityDescription() : this(1, CryptoSuites.AES_CM_128_HMAC_SHA1_80) + public override string ToString() { + if (this.SrtpSessionParam == SrtpSessionParams.unknown) + { + return ""; + } + + var builder = new ValueStringBuilder(stackalloc char[64]); + try + { + ToString(ref builder); + return builder.ToString(); + } + finally + { + builder.Dispose(); + } } - public SDPSecurityDescription(uint tag, CryptoSuites cryptoSuite) + + internal void ToString(ref ValueStringBuilder builder) { - this.Tag = tag; - this.CryptoSuite = cryptoSuite; - this.KeyParams = new List(); + if (this.SrtpSessionParam == SrtpSessionParams.unknown) + { + return; + } + + switch (this.SrtpSessionParam) + { + case SrtpSessionParams.UNAUTHENTICATED_SRTP: + case SrtpSessionParams.UNENCRYPTED_SRTP: + case SrtpSessionParams.UNENCRYPTED_SRTCP: + builder.Append(this.SrtpSessionParam.ToStringFast()); + break; + case SrtpSessionParams.wsh: + builder.Append(WSH_PREFIX); + builder.Append(this.Wsh); + break; + case SrtpSessionParams.kdr: + builder.Append(KDR_PREFIX); + builder.Append(this.Kdr); + break; + case SrtpSessionParams.fec_order: + builder.Append(FEC_ORDER_PREFIX); + builder.Append(this.FecOrder.ToStringFast()); + break; + case SrtpSessionParams.fec_key: + builder.Append(FEC_KEY_PREFIX); + if (this.FecKey is { }) + { + this.FecKey.ToString(ref builder); + } + break; + } } - public static SDPSecurityDescription CreateNew(uint tag = 1, CryptoSuites cryptoSuite = CryptoSuites.AES_CM_128_HMAC_SHA1_80) + public static SessionParameter? Parse(ReadOnlySpan sessionParam, CryptoSuites cryptoSuite = CryptoSuites.AES_CM_128_HMAC_SHA1_80) { - SDPSecurityDescription secdesc = new SDPSecurityDescription(tag, cryptoSuite); - secdesc.KeyParams.Add(KeyParameter.CreateNew(cryptoSuite)); - return secdesc; + if (!TryParse(sessionParam, out var result, cryptoSuite)) + { + throw new FormatException($"sessionParam is not recognized as a valid SRTP_SESSION_PARAM "); + } + + return result; } - public override string ToString() + public static bool TryParse(ReadOnlySpan sessionParam, out SessionParameter? result, CryptoSuites cryptoSuite = CryptoSuites.AES_CM_128_HMAC_SHA1_80) { - if (this.Tag < 1 || this.CryptoSuite == CryptoSuites.unknown || this.KeyParams.Count < 1) + result = null; + + sessionParam = sessionParam.Trim(); + if (sessionParam.IsEmpty) { - return null; + return true; } - var s = new StringBuilder(); - s.Append(CRYPTO_ATTRIBUE_PREFIX).Append(this.Tag).Append(WHITE_SPACE).Append(this.CryptoSuite).Append(WHITE_SPACE); - for (int i = 0; i < this.KeyParams.Count; i++) + var paramType = SrtpSessionParams.unknown; + if (sessionParam.StartsWith(KDR_PREFIX, StringComparison.Ordinal)) { - if (i > 0) + if (uint.TryParse(sessionParam.Slice(KDR_PREFIX.Length), System.Globalization.NumberStyles.None, null, out var kdr)) { - s.Append(SEMI_COLON); + result = new SessionParameter(SrtpSessionParams.kdr) { Kdr = kdr }; + return true; + } + } + else if (sessionParam.StartsWith(WSH_PREFIX, StringComparison.Ordinal)) + { + if (uint.TryParse(sessionParam.Slice(WSH_PREFIX.Length), System.Globalization.NumberStyles.None, null, out var wsh)) + { + result = new SessionParameter(SrtpSessionParams.wsh) { Wsh = wsh }; + return true; + } + } + else if (sessionParam.StartsWith(FEC_KEY_PREFIX, StringComparison.Ordinal)) + { + var sFecKey = sessionParam.Slice(FEC_KEY_PREFIX.Length); + if (!KeyParameter.TryParse(sFecKey, out var fecKey, cryptoSuite)) + { + return false; + } + result = new SessionParameter(SrtpSessionParams.fec_key) { FecKey = fecKey }; + return true; + } + else if (sessionParam.StartsWith(FEC_ORDER_PREFIX, StringComparison.Ordinal)) + { + var sFecOrder = sessionParam.Slice(FEC_ORDER_PREFIX.Length); + if (!FecTypesExtensions.TryParse(sFecOrder, out var fecOrder, ignoreCase: false) + || fecOrder == FecTypes.unknown + || !FecTypesExtensions.IsDefined(sFecOrder)) + { + return false; } - s.Append(this.KeyParams[i].ToString()); + result = new SessionParameter(SrtpSessionParams.fec_order) { FecOrder = fecOrder }; + return true; } - if (this.SessionParam != null) + else { - s.Append(WHITE_SPACE).Append(this.SessionParam.ToString()); + if (!SrtpSessionParamsExtensions.TryParse(sessionParam.ToString(), out paramType, ignoreCase: false) || paramType == SrtpSessionParams.unknown) + { + return false; + } + + switch (paramType) + { + case SrtpSessionParams.UNAUTHENTICATED_SRTP: + case SrtpSessionParams.UNENCRYPTED_SRTCP: + case SrtpSessionParams.UNENCRYPTED_SRTP: + result = new SessionParameter(paramType); + return true; + } } - return s.ToString(); + + return false; } + } + - public static SDPSecurityDescription Parse(string cryptoLine) + private uint m_iTag = 1; + public uint Tag + { + get + { + return this.m_iTag; + } + set { - if (!TryParse(cryptoLine, out var securityDescription)) + if (value is > 0 and < 1000000000) + { + this.m_iTag = value; + } + else { - throw new FormatException($"cryptoLine '{cryptoLine}' is not recognized as a valid SDP Security Description "); + throw new ArgumentOutOfRangeException("Tag", "Tag value must be greater than 0 and not exceed 9 digits"); } + } + } - return securityDescription; + + public CryptoSuites CryptoSuite + { + get; + set; + } + + public List KeyParams + { + get; + set; + } + public SessionParameter? SessionParam + { + get; + set; + } + public SDPSecurityDescription() : this(1, CryptoSuites.AES_CM_128_HMAC_SHA1_80) + { + + } + public SDPSecurityDescription(uint tag, CryptoSuites cryptoSuite) + { + this.Tag = tag; + this.CryptoSuite = cryptoSuite; + this.KeyParams = new List(); + } + + public static SDPSecurityDescription CreateNew(uint tag = 1, CryptoSuites cryptoSuite = CryptoSuites.AES_CM_128_HMAC_SHA1_80) + { + var secdesc = new SDPSecurityDescription(tag, cryptoSuite); + var keyParameter = KeyParameter.CreateNew(cryptoSuite); + Debug.Assert(keyParameter is { }); + secdesc.KeyParams.Add(keyParameter); + return secdesc; + } + + public override string? ToString() + { + if (this.Tag < 1 || this.CryptoSuite == CryptoSuites.unknown || !(this.KeyParams is { Count: > 0 })) + { + return null; + } + + var builder = new ValueStringBuilder(stackalloc char[256]); + + try + { + ToString(ref builder); + + return builder.ToString(); + } + finally + { + builder.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder builder) + { + if (this.Tag < 1 || this.CryptoSuite == CryptoSuites.unknown || !(this.KeyParams is { Count: > 0 })) + { + return; } - public static bool TryParse(string cryptoLine, out SDPSecurityDescription securityDescription) + builder.Append(CRYPTO_ATTRIBUE_PREFIX); + builder.Append(this.Tag); + builder.Append(WHITE_SPACE); + builder.Append(this.CryptoSuite.ToStringFast()); + builder.Append(WHITE_SPACE); + + for (var i = 0; i < this.KeyParams.Count; i++) { - securityDescription = null; - if (string.IsNullOrWhiteSpace(cryptoLine)) + if (i > 0) { - return true; + builder.Append(SEMI_COLON); } - if (!cryptoLine.StartsWith(CRYPTO_ATTRIBUE_PREFIX)) + this.KeyParams[i].ToString(ref builder); + } + + if (this.SessionParam is { }) + { + builder.Append(WHITE_SPACE); + this.SessionParam.ToString(ref builder); + } + } + + public static SDPSecurityDescription? Parse(ReadOnlySpan cryptoLine) + { + if (!TryParse(cryptoLine, out var securityDescription)) + { + throw new FormatException($"cryptoLine '{cryptoLine.ToString()}' is not recognized as a valid SDP Security Description "); + } + + return securityDescription; + } + + public static bool TryParse(ReadOnlySpan cryptoLine, out SDPSecurityDescription? securityDescription) + { + securityDescription = null; + if (cryptoLine.IsEmptyOrWhiteSpace()) + { + return true; + } + + if (!cryptoLine.StartsWith(CRYPTO_ATTRIBUE_PREFIX, StringComparison.Ordinal)) + { + return false; + } + + var sCryptoValue = cryptoLine.Slice(cryptoLine.IndexOf(COLON) + 1); + + Span sCryptoParts = stackalloc Range[5]; + var sCryptoPartsCount = sCryptoValue.SplitAny(sCryptoParts, WHITE_SPACES, StringSplitOptions.RemoveEmptyEntries); + + if (sCryptoPartsCount < 2) + { + return false; + } + + if (!uint.TryParse(sCryptoValue[sCryptoParts[0]], out var tag)) + { + return false; + } + + var cryptoSuiteSpan = sCryptoValue[sCryptoParts[1]]; + if (!CryptoSuitesExtensions.TryParse(cryptoSuiteSpan, out var cryptoSuite) + || cryptoSuite == CryptoSuites.unknown + || !CryptoSuitesExtensions.IsDefined(cryptoSuiteSpan)) + { + return false; + } + + if (sCryptoPartsCount < 3) + { + return false; + } + + List? keyParams = null; + var keyParamsSpan = sCryptoValue[sCryptoParts[2]]; + foreach (var keyRange in keyParamsSpan.SplitAny([SEMI_COLON])) + { + var keyParam = keyParamsSpan[keyRange]; + if (!KeyParameter.TryParse(keyParam, out var parsedKeyParam, cryptoSuite) || parsedKeyParam is null) { + securityDescription = null; return false; } - var sCryptoValue = cryptoLine.AsSpan(cryptoLine.IndexOf(COLON) + 1); + keyParams ??= new(); + keyParams.Add(parsedKeyParam); + } + + if (keyParams is null) + { + return false; + } + + securityDescription = new SDPSecurityDescription(); + securityDescription.Tag = tag; + securityDescription.CryptoSuite = cryptoSuite; + securityDescription.KeyParams = keyParams; - securityDescription = new SDPSecurityDescription(); - Span sCryptoPartRanges = stackalloc Range[5]; - var sCryptoPartCount = sCryptoValue.SplitAny(sCryptoPartRanges, WHITE_SPACES.AsSpan(), StringSplitOptions.RemoveEmptyEntries); - if (sCryptoValue.Length < 2) + if (sCryptoPartsCount > 3) + { + var sessionParamSpan = sCryptoValue[sCryptoParts[3]]; + + if (!sessionParamSpan.IsEmpty) { - return false; + if (!SessionParameter.TryParse(sessionParamSpan, out var sessionParam, cryptoSuite)) + { + securityDescription = null; + return false; + } + securityDescription.SessionParam = sessionParam; } + } + + return true; + } + /// + /// Determines whether the specified SDPSecurityDescription is equal to the current SDPSecurityDescription. + /// Equality is based on comparing the individual fields that make up the security description. + /// + /// The SDPSecurityDescription to compare with the current instance. + /// true if the specified SDPSecurityDescription is equal to the current instance; otherwise, false. + public bool Equals(SDPSecurityDescription? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + // Compare core properties + if (Tag != other.Tag || CryptoSuite != other.CryptoSuite) + { + return false; + } + + // Compare KeyParams collections + if (KeyParams.Count != other.KeyParams.Count) + { + return false; + } - if (sCryptoPartCount < 2) + for (var i = 0; i < KeyParams.Count; i++) + { + if (!AreKeyParametersEqual(KeyParams[i], other.KeyParams[i])) { return false; } + } + + // Compare SessionParam (using null-safe comparison) + return AreSessionParametersEqual(SessionParam, other.SessionParam); - if (!uint.TryParse(sCryptoValue[sCryptoPartRanges[0]], out var tag)) + static bool AreKeyParametersEqual(KeyParameter left, KeyParameter right) + { + if (ReferenceEquals(left, right)) { - return false; + return true; } - securityDescription.Tag = tag; - if (!s_cryptoSuiteLookup.TryGetValue(sCryptoValue[sCryptoPartRanges[1]].ToString(), out var cryptoSuite)) + return left.Key.SequenceEqual(right.Key) && + left.Salt.SequenceEqual(right.Salt) && + left.LifeTime == right.LifeTime && + string.Equals(left.LifeTimeString, right.LifeTimeString, StringComparison.Ordinal) && + left.MkiValue == right.MkiValue && + left.MkiLength == right.MkiLength; + } + + static bool AreSessionParametersEqual(SessionParameter? left, SessionParameter? right) + { + if (ReferenceEquals(left, right)) + { + return true; + } + + if (left is null || right is null) { return false; } - securityDescription.CryptoSuite = cryptoSuite; - if (sCryptoPartCount < 3) + if (left.SrtpSessionParam != right.SrtpSessionParam) { return false; } - var sKeyParams = sCryptoValue[sCryptoPartRanges[2]]; - var hasKeyParam = false; - foreach (var keyParamRange in sKeyParams.Split(SEMI_COLON)) + Debug.Assert(left.FecKey is not null); + return left.SrtpSessionParam switch { - hasKeyParam = true; - if (!KeyParameter.TryParse(sKeyParams[keyParamRange].ToString(), out var keyParam, securityDescription.CryptoSuite)) - { - securityDescription = null; - return false; - } - securityDescription.KeyParams.Add(keyParam); + SessionParameter.SrtpSessionParams.kdr => left.Kdr == right.Kdr, + SessionParameter.SrtpSessionParams.wsh => left.Wsh == right.Wsh, + SessionParameter.SrtpSessionParams.fec_order => left.FecOrder == right.FecOrder, + SessionParameter.SrtpSessionParams.fec_key => AreKeyParametersEqual(left.FecKey, right.FecKey!), + _ => true // For simple enum-only parameters + }; + } + } + + /// + /// Determines whether the specified object is equal to the current SDPSecurityDescription. + /// + /// The object to compare with the current instance. + /// true if the specified object is equal to the current instance; otherwise, false. + public override bool Equals(object? obj) + { + return Equals(obj as SDPSecurityDescription); + } + + /// + /// Returns a hash code for the current SDPSecurityDescription. + /// The hash code is based on the individual fields that make up the security description. + /// + /// A hash code for the current instance. + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(Tag); + hash.Add(CryptoSuite); + + // Add each KeyParameter's hash contribution + foreach (var keyParam in KeyParams) + { + var keyParamHash = new HashCode(); + + // Hash the key bytes + foreach (var b in keyParam.Key) + { + keyParamHash.Add(b); } - if (!hasKeyParam) + // Hash the salt bytes + foreach (var b in keyParam.Salt) { - securityDescription = null; - return false; + keyParamHash.Add(b); } - if (sCryptoPartCount > 3) + keyParamHash.Add(keyParam.LifeTime); + keyParamHash.Add(keyParam.LifeTimeString); + keyParamHash.Add(keyParam.MkiValue); + keyParamHash.Add(keyParam.MkiLength); + + hash.Add(keyParamHash.ToHashCode()); + } + + // Add SessionParam hash if present + if (SessionParam is not null) + { + var sessionParamHash = new HashCode(); + sessionParamHash.Add(SessionParam.SrtpSessionParam); + + switch (SessionParam.SrtpSessionParam) { - if (!SessionParameter.TryParse(sCryptoValue[sCryptoPartRanges[3]].ToString(), out var sessionParam, securityDescription.CryptoSuite)) - { - securityDescription = null; - return false; - } - securityDescription.SessionParam = sessionParam; + case SessionParameter.SrtpSessionParams.kdr: + sessionParamHash.Add(SessionParam.Kdr); + break; + case SessionParameter.SrtpSessionParams.wsh: + sessionParamHash.Add(SessionParam.Wsh); + break; + case SessionParameter.SrtpSessionParams.fec_order: + sessionParamHash.Add(SessionParam.FecOrder); + break; + case SessionParameter.SrtpSessionParams.fec_key: + if (SessionParam.FecKey is not null) + { + var fecKeyHash = new HashCode(); + + // Hash the FecKey bytes + foreach (var b in SessionParam.FecKey.Key) + { + fecKeyHash.Add(b); + } + + // Hash the FecKey salt bytes + foreach (var b in SessionParam.FecKey.Salt) + { + fecKeyHash.Add(b); + } + + fecKeyHash.Add(SessionParam.FecKey.LifeTime); + fecKeyHash.Add(SessionParam.FecKey.LifeTimeString); + fecKeyHash.Add(SessionParam.FecKey.MkiValue); + fecKeyHash.Add(SessionParam.FecKey.MkiLength); + + sessionParamHash.Add(fecKeyHash.ToHashCode()); + } + break; } - return true; + hash.Add(sessionParamHash.ToHashCode()); } + + return hash.ToHashCode(); } } diff --git a/src/SIPSorcery/net/SDP/SDPTypes.cs b/src/SIPSorcery/net/SDP/SDPTypes.cs index f577e1c1b7..3fd832c9ce 100644 --- a/src/SIPSorcery/net/SDP/SDPTypes.cs +++ b/src/SIPSorcery/net/SDP/SDPTypes.cs @@ -21,7 +21,7 @@ namespace SIPSorcery.Net; -public class SDPMediaTypes +public static class SDPMediaTypes { public static SDPMediaTypesEnum GetSDPMediaType(string mediaType) { @@ -49,7 +49,7 @@ public enum MediaStreamStatusEnum Inactive = 3 // The offerer is not ready to send or receive packets. } -public class MediaStreamStatusType +public static class MediaStreamStatusType { public const string SEND_RECV_ATTRIBUTE = "a=sendrecv"; public const string SEND_ONLY_ATTRIBUTE = "a=sendonly"; diff --git a/src/SIPSorcery/net/STUN/NetStunLoggingExtensions.cs b/src/SIPSorcery/net/STUN/NetStunLoggingExtensions.cs new file mode 100644 index 0000000000..8bcf7671c0 --- /dev/null +++ b/src/SIPSorcery/net/STUN/NetStunLoggingExtensions.cs @@ -0,0 +1,356 @@ +using System; +using System.Net; +using DnsClient; +using Microsoft.Extensions.Logging; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; + +internal static partial class NetStunLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "StunListenerCreated", + Level = LogLevel.Information, + Message = "STUNListener created {Address}:{Port}.")] + public static partial void LogStunListenerCreated( + this ILogger logger, + IPAddress address, + int port); + + [LoggerMessage( + EventId = 0, + EventName = "StunDeterminingPublicIP", + Level = LogLevel.Debug, + Message = "STUNClient attempting to determine public IP from {StunServer}.")] + public static partial void LogStunDeterminingPublicIP( + this ILogger logger, + string stunServer); + + [LoggerMessage( + EventId = 0, + EventName = "StunInitialResponse", + Level = LogLevel.Debug, + Message = "STUNClient Response to initial STUN message received from {ResponseEndPoint}.")] + public static partial void LogStunInitialResponse( + this ILogger logger, + IPEndPoint responseEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "StunPublicIPResult", + Level = LogLevel.Debug, + Message = "STUNClient Public IP={PublicAddress} Port={PublicPort}.")] + public static partial void LogStunPublicIPResult( + this ILogger logger, + IPAddress publicAddress, + int publicPort); + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerClosing", + Level = LogLevel.Debug, + Message = "Closing STUNListener.")] + public static partial void LogStunListenerClosing( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "StunServerAdditionalSockets", + Level = LogLevel.Debug, + Message = "STUN Server additional sockets, primary={PrimaryEndPoint}, secondary={SecondaryEndPoint}.")] + public static partial void LogStunServerAdditionalSockets( + this ILogger logger, + string primaryEndPoint, + string secondaryEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "StunServerPrimaryException", + Level = LogLevel.Debug, + Message = "Exception STUNPrimaryReceived. {ErrorMessage}")] + public static partial void LogStunServerPrimaryException( + this ILogger logger, + string errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "StunServerSecondaryException", + Level = LogLevel.Debug, + Message = "Exception STUNSecondaryReceived. {ErrorMessage}")] + public static partial void LogStunServerSecondaryException( + this ILogger logger, + string errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "StunDnsSrvLookupFailure", + Level = LogLevel.Debug, + Message = "STUNDns SRV lookup failure for {Uri}. {ErrorMessage}")] + public static partial void LogStunDnsSrvLookupFailure( + this ILogger logger, + STUNUri uri, + string? errorMessage, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "StunDnsOsLookupFailed", + Level = LogLevel.Warning, + Message = "Operating System DNS lookup failed for {host}.")] + public static partial void LogStunDnsOsLookupFailed( + this ILogger logger, + string host); + + [LoggerMessage( + EventId = 0, + EventName = "StunDnsLookupFailure", + Level = LogLevel.Warning, + Message = "STUNDns lookup failure for {host} and query {queryType}. {errorMessage}")] + public static partial void LogStunDnsLookupFailure( + this ILogger logger, + string host, + QueryType queryType, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunClientReceiveError", + Level = LogLevel.Warning, + Message = "Exception STUNClient Receive. {errorMessage}")] + public static partial void LogStunClientReceiveError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunClientTimeout", + Level = LogLevel.Warning, + Message = "STUNClient server response timed out after {timeout}s.")] + public static partial void LogStunClientTimeout( + this ILogger logger, + int timeout); + + [LoggerMessage( + EventId = 0, + EventName = "StunClientGetPublicIPError", + Level = LogLevel.Error, + Message = "Exception STUNClient GetPublicIPAddress. {errorMessage}")] + public static partial void LogStunClientGetPublicIPError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerSendNotAccessible", + Level = LogLevel.Warning, + Message = "The STUNListener was not accessible when attempting to send a message to {destinationEndPoint}.", + SkipEnabledCheck = true)] + private static partial void LogStunListenerSendNotAccessibleUnchecked( + this ILogger logger, + string destinationEndPoint); + + public static void LogStunListenerSendNotAccessible( + this ILogger logger, + IPEndPoint destinationEndPoint) + { + if (logger.IsEnabled(LogLevel.Warning)) + { + logger.LogStunListenerSendNotAccessibleUnchecked(IPSocket.GetSocketString(destinationEndPoint)); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerCloseError", + Level = LogLevel.Warning, + Message = "Exception STUNListener Close. {errorMessage}")] + public static partial void LogStunListenerCloseError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerReadError", + Level = LogLevel.Error, + Message = "Unable to read from STUNListener local end point {address}:{port}")] + public static partial void LogStunListenerReadError( + this ILogger logger, + IPAddress address, + int port); + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerProcessError", + Level = LogLevel.Error, + Message = "Exception processing STUNListener MessageReceived. {errorMessage}")] + public static partial void LogStunListenerProcessError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerEmptyDestination", + Level = LogLevel.Error, + Message = "An empty destination was specified to Send in STUNListener.")] + public static partial void LogStunListenerEmptyDestination( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerSendError", + Level = LogLevel.Error, + Message = "Exception ({exceptionType}) STUNListener Send (sendto=>{destinationEndPoint}). {errorMessage}", + SkipEnabledCheck = true)] + private static partial void LogStunListenerSendErrorUnchecked( + this ILogger logger, + Type exceptionType, + string destinationEndPoint, + string errorMessage, + Exception exception); + + public static void LogStunListenerSendError( + this ILogger logger, + IPEndPoint destinationEndPoint, + Exception exception) + { + if (logger.IsEnabled(LogLevel.Error)) + { + logger.LogStunListenerSendErrorUnchecked(exception.GetType(), IPSocket.GetSocketString(destinationEndPoint), exception.Message, exception); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerConstructor", + Level = LogLevel.Error, + Message = "Exception STUNListener (ctor). {errorMessage}")] + public static partial void LogStunListenerConstructor( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerDispose", + Level = LogLevel.Error, + Message = "Exception Disposing STUNListener. {errorMessage}")] + public static partial void LogStunListenerDispose( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerInitSockets", + Level = LogLevel.Error, + Message = "Exception STUNListener InitialiseSockets. {errorMessage}")] + public static partial void LogStunListenerInitSockets( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerListen", + Level = LogLevel.Error, + Message = "Exception STUNListener Listen. {errorMessage}")] + public static partial void LogStunListenerListen( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunListenerListening", + Level = LogLevel.Error, + Message = "Exception listening in STUNListener. {errorMessage}")] + public static partial void LogStunListenerListening( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunServerStop", + Level = LogLevel.Error, + Message = "Exception StunServer Stop. {errorMessage}")] + public static partial void LogStunServerStop( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunServerFirePrimaryRequest", + Level = LogLevel.Error, + Message = "Exception FireSTUNPrimaryRequestInTraceEvent. {errorMessage}")] + public static partial void LogStunServerFirePrimaryRequest( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunServerFireSecondaryRequest", + Level = LogLevel.Error, + Message = "Exception FireSTUNSecondaryRequestInTraceEvent. {errorMessage}")] + public static partial void LogStunServerFireSecondaryRequest( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunServerFirePrimaryResponse", + Level = LogLevel.Error, + Message = "Exception FireSTUNPrimaryResponseOutTraceEvent. {errorMessage}")] + public static partial void LogStunServerFirePrimaryResponse( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunServerFireSecondaryResponse", + Level = LogLevel.Error, + Message = "Exception FireSTUNSecondaryResponseOutTraceEvent. {errorMessage}")] + public static partial void LogStunServerFireSecondaryResponse( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "StunAttributeLengthOverflow", + Level = LogLevel.Warning, + Message = "The attribute length on a STUN parameter was greater than the available number of bytes. Type: {attributeType}")] + public static partial void LogStunAttributeLengthOverflow( + this ILogger logger, + STUNAttributeTypesEnum attributeType); + + [LoggerMessage( + EventId = 0, + EventName = "StunMessageReceived", + Level = LogLevel.Debug, + Message = "STUN message received from {RemoteEndPoint}.")] + public static partial void LogStunMessageReceived( + this ILogger logger, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "StunMessageInvalid", + Level = LogLevel.Warning, + Message = "Invalid STUN message received from {RemoteEndPoint}.")] + public static partial void LogStunMessageInvalid( + this ILogger logger, + IPEndPoint remoteEndPoint); +} diff --git a/src/SIPSorcery/net/STUN/STUNAppState.cs b/src/SIPSorcery/net/STUN/STUNAppState.cs index 7b20c1714e..9d280bce63 100644 --- a/src/SIPSorcery/net/STUN/STUNAppState.cs +++ b/src/SIPSorcery/net/STUN/STUNAppState.cs @@ -1,4 +1,4 @@ -// ============================================================================ +// ============================================================================ // FileName: STUNLog.cs // // Description: @@ -15,49 +15,45 @@ // ============================================================================ using System; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; -namespace SIPSorcery.Net +public static class Utility { - public class Utility + public static ushort ReverseEndian(ushort val) { - public static UInt16 ReverseEndian(UInt16 val) - { - return Convert.ToUInt16(val << 8 & 0xff00 | (val >> 8)); - } + return Convert.ToUInt16(val << 8 & 0xff00 | (val >> 8)); + } + + public static uint ReverseEndian(uint val) + { + return Convert.ToUInt32((val << 24 & 0xff000000) | (val << 8 & 0x00ff0000) | (val >> 8 & 0xff00) | (val >> 24)); + } - public static UInt32 ReverseEndian(UInt32 val) + public static string? PrintBuffer(byte[] buffer) + { + if (buffer.Length == 0) { - return Convert.ToUInt32((val << 24 & 0xff000000) | (val << 8 & 0x00ff0000) | (val >> 8 & 0xff00) | (val >> 24)); + return null; } - public static string PrintBuffer(byte[] buffer) + using var builder = new ValueStringBuilder(stackalloc char[256]); + + for (var index = 0; index < buffer.Length; index++) { - string bufferStr = null; + builder.Append(buffer[index], "X2"); - for (int index = 0; index < buffer.Length; index++) + if ((index + 1) % 4 == 0) { - string byteStr = buffer[index].ToString("X"); - - if (byteStr.Length == 1) - { - bufferStr += $"0{byteStr}"; - } - else - { - bufferStr += byteStr; - } - - if ((index + 1) % 4 == 0) - { - bufferStr += "\n"; - } - else - { - bufferStr += " | "; - } + builder.Append('\n'); + } + else + { + builder.Append(" | "); } - - return bufferStr; } + + return builder.ToString(); } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/net/STUN/STUNAttributes/STUNAddressAttribute.cs b/src/SIPSorcery/net/STUN/STUNAttributes/STUNAddressAttribute.cs index 0fb559f686..8f03f6f4d1 100644 --- a/src/SIPSorcery/net/STUN/STUNAttributes/STUNAddressAttribute.cs +++ b/src/SIPSorcery/net/STUN/STUNAttributes/STUNAddressAttribute.cs @@ -15,89 +15,96 @@ using System; using System.Buffers.Binary; +using System.Diagnostics; using System.Net; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// There's no proper explanation of why this STUN attribute was obsoleted. My guess is to favour using the XOR Maoped Address attribute +/// BUT that does not help when a STUN server provides this atype of address attribute. It will still need to be parsed and understood, +/// Reverted this obsoletion on 13 Nov 2024 AC. +/// +//[Obsolete("Provided for backward compatibility with RFC3489 clients.")] +public partial class STUNAddressAttribute : STUNAddressAttributeBase { + /// + /// Parses an IPv4 Address attribute. + /// /// /// There's no proper explanation of why this STUN attribute was obsoleted. My guess is to favour using the XOR Maoped Address attribute /// BUT that does not help when a STUN server provides this atype of address attribute. It will still need to be parsed and understood, /// Reverted this obsoletion on 13 Nov 2024 AC. /// //[Obsolete("Provided for backward compatibility with RFC3489 clients.")] - public class STUNAddressAttribute : STUNAddressAttributeBase + public STUNAddressAttribute(ReadOnlyMemory attributeValue) + : this(STUNAttributeTypesEnum.MappedAddress, attributeValue) { - /// - /// Parses an IPv4 Address attribute. - /// - /// - /// There's no proper explanation of why this STUN attribute was obsoleted. My guess is to favour using the XOR Maoped Address attribute - /// BUT that does not help when a STUN server provides this atype of address attribute. It will still need to be parsed and understood, - /// Reverted this obsoletion on 13 Nov 2024 AC. - /// - //[Obsolete("Provided for backward compatibility with RFC3489 clients.")] - public STUNAddressAttribute(byte[] attributeValue) - : base(STUNAttributeTypesEnum.MappedAddress, attributeValue) - { - Port = BinaryPrimitives.ReadUInt16BigEndian(attributeValue.AsSpan(2)); + } - Address = new IPAddress(new byte[] { attributeValue[4], attributeValue[5], attributeValue[6], attributeValue[7] }); - } + /// + /// Parses an IPv4 Address attribute. + /// + /// + /// There's no proper explanation of why this STUN attribute was obsoleted. My guess is to favour using the XOR Maoped Address attribute + /// BUT that does not help when a STUN server provides this atype of address attribute. It will still need to be parsed and understood, + /// Reverted this obsoletion on 13 Nov 2024 AC. + /// + //[Obsolete("Provided for backward compatibility with RFC3489 clients.")] + public STUNAddressAttribute(STUNAttributeTypesEnum attributeType, ReadOnlyMemory attributeValue) + : base(attributeType, attributeValue) + { + Port = BinaryPrimitives.ReadUInt16BigEndian(attributeValue.Span.Slice(2, 2)); + + Address = IPAddress.Create(attributeValue.Span.Slice(4, 4)); + } - /// - /// Parses an IPv4 Address attribute. - /// - /// - /// There's no proper explanation of why this STUN attribute was obsoleted. My guess is to favour using the XOR Maoped Address attribute - /// BUT that does not help when a STUN server provides this atype of address attribute. It will still need to be parsed and understood, - /// Reverted this obsoletion on 13 Nov 2024 AC. - /// - //[Obsolete("Provided for backward compatibility with RFC3489 clients.")] - public STUNAddressAttribute(STUNAttributeTypesEnum attributeType, byte[] attributeValue) - : base(attributeType, attributeValue) - { - Port = BinaryPrimitives.ReadUInt16BigEndian(attributeValue.AsSpan(2)); + /// + /// Parses an IPv4 Address attribute. + /// + /// + /// There's no proper explanation of why this STUN attribute was obsoleted. My guess is to favour using the XOR Maoped Address attribute + /// BUT that does not help when a STUN server provides this atype of address attribute. It will still need to be parsed and understood, + /// Reverted this obsoletion on 13 Nov 2024 AC. + /// + //[Obsolete("Provided for backward compatibility with RFC3489 clients.")] + public STUNAddressAttribute(STUNAttributeTypesEnum attributeType, int port, IPAddress address) + : base(attributeType, null) + { + Port = port; + Address = address; - Address = new IPAddress(new byte[] { attributeValue[4], attributeValue[5], attributeValue[6], attributeValue[7] }); - } + //base.AttributeType = attributeType; + //base.Length = ADDRESS_ATTRIBUTE_LENGTH; + } - /// - /// Parses an IPv4 Address attribute. - /// - /// - /// There's no proper explanation of why this STUN attribute was obsoleted. My guess is to favour using the XOR Maoped Address attribute - /// BUT that does not help when a STUN server provides this atype of address attribute. It will still need to be parsed and understood, - /// Reverted this obsoletion on 13 Nov 2024 AC. - /// - //[Obsolete("Provided for backward compatibility with RFC3489 clients.")] - public STUNAddressAttribute(STUNAttributeTypesEnum attributeType, int port, IPAddress address) - : base(attributeType, null) - { - Port = port; - Address = address; + /// + public override int GetByteCount() => STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + ADDRESS_ATTRIBUTE_IPV4_LENGTH; - base.AttributeType = attributeType; - //base.Length = ADDRESS_ATTRIBUTE_LENGTH; - } + /// + public override int WriteBytes(Span buffer) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(0, 2), (ushort)base.AttributeType); - public override int ToByteBuffer(byte[] buffer, int startIndex) - { - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(startIndex), (UInt16)base.AttributeType); - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(startIndex + 2), ADDRESS_ATTRIBUTE_IPV4_LENGTH); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2, 2), ADDRESS_ATTRIBUTE_IPV4_LENGTH); - buffer[startIndex + 5] = (byte)Family; + buffer[5] = (byte)Family; - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(startIndex + 6), Convert.ToUInt16(Port)); - Buffer.BlockCopy(Address.GetAddressBytes(), 0, buffer, startIndex + 8, 4); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(6, 2), (ushort)Port); - return STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + ADDRESS_ATTRIBUTE_IPV4_LENGTH; - } + Debug.Assert(Address is { }); + Address.GetAddressBytes().CopyTo(buffer.Slice(8, 4)); - public override string ToString() - { - string attrDescrStr = $"STUN Attribute: {base.AttributeType}, address={Address.ToString()}, port={Port}."; + return STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + ADDRESS_ATTRIBUTE_IPV4_LENGTH; + } - return attrDescrStr; - } + private protected override void ValueToString(ref ValueStringBuilder sb) + { + sb.Append("Address="); + Debug.Assert(Address is { }); + sb.Append(Address); + sb.Append(", Port="); + sb.Append(Port); } } diff --git a/src/SIPSorcery/net/STUN/STUNAttributes/STUNAddressAttributeBase.cs b/src/SIPSorcery/net/STUN/STUNAttributes/STUNAddressAttributeBase.cs index 6fea343082..20b4094295 100644 --- a/src/SIPSorcery/net/STUN/STUNAttributes/STUNAddressAttributeBase.cs +++ b/src/SIPSorcery/net/STUN/STUNAttributes/STUNAddressAttributeBase.cs @@ -1,41 +1,51 @@ using System; -using System.Collections.Generic; +using System.Diagnostics; using System.Net; -using System.Text; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public abstract partial class STUNAddressAttributeBase : STUNAttribute { - public abstract class STUNAddressAttributeBase : STUNAttribute + /// + /// Obsolete. + ///
Please use or instead. + ///

+ ///
+ [Obsolete("Default attribute length for IPv4 only.")] + public const ushort ADDRESS_ATTRIBUTE_LENGTH = 8; + + public const ushort ADDRESS_ATTRIBUTE_IPV4_LENGTH = 8; + public const ushort ADDRESS_ATTRIBUTE_IPV6_LENGTH = 20; + + protected ushort AddressAttributeLength = ADDRESS_ATTRIBUTE_IPV4_LENGTH; + protected byte[]? TransactionId; + + /// + /// Defaults to IPv4 (0x01 // 1) + /// + public int Family = 1; // Ipv4 = 1, IPv6 = 2. + public int Port; + public IPAddress? Address; + + public override ushort PaddedLength + { + get => AddressAttributeLength; + } + + public STUNAddressAttributeBase(STUNAttributeTypesEnum attributeType, ReadOnlyMemory value) + : base(attributeType, value) + { + } + + private protected override void ValueToString(ref ValueStringBuilder sb) { - /// - /// Obsolete. - ///
Please use or instead. - ///

- ///
- [Obsolete("Default attribute length for IPv4 only.")] - public const UInt16 ADDRESS_ATTRIBUTE_LENGTH = 8; - - public const UInt16 ADDRESS_ATTRIBUTE_IPV4_LENGTH = 8; - public const UInt16 ADDRESS_ATTRIBUTE_IPV6_LENGTH = 20; - - protected UInt16 AddressAttributeLength = ADDRESS_ATTRIBUTE_IPV4_LENGTH; - protected byte[] TransactionId; - - /// - /// Defaults to IPv4 (0x01 // 1) - /// - public int Family = 1; // Ipv4 = 1, IPv6 = 2. - public int Port; - public IPAddress Address; - - public override UInt16 PaddedLength - { - get => AddressAttributeLength; - } - - public STUNAddressAttributeBase(STUNAttributeTypesEnum attributeType, byte[] value) - : base(attributeType, value) - { - } + sb.Append("Address="); + Debug.Assert(Address is { }); + sb.Append(Address); + sb.Append(", Port="); + sb.Append(Port); + sb.Append(", Family="); + sb.Append(Family switch { 1 => "IPV4", 2 => "IPV6", _ => "Invalid", }); } } diff --git a/src/SIPSorcery/net/STUN/STUNAttributes/STUNAttribute.cs b/src/SIPSorcery/net/STUN/STUNAttributes/STUNAttribute.cs index 74dd29ccaa..93544a7902 100644 --- a/src/SIPSorcery/net/STUN/STUNAttributes/STUNAttribute.cs +++ b/src/SIPSorcery/net/STUN/STUNAttributes/STUNAttribute.cs @@ -39,228 +39,267 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; +using System.Diagnostics; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public enum STUNAttributeTypesEnum : ushort +{ + Unknown = 0, + MappedAddress = 0x0001, + ResponseAddress = 0x0002, // Not used in RFC5389. + ChangeRequest = 0x0003, // Not used in RFC5389. + SourceAddress = 0x0004, // Not used in RFC5389. + ChangedAddress = 0x0005, // Not used in RFC5389. + Username = 0x0006, + Password = 0x0007, // Not used in RFC5389. + MessageIntegrity = 0x0008, + ErrorCode = 0x0009, + UnknownAttributes = 0x000A, + ReflectedFrom = 0x000B, // Not used in RFC5389. + Realm = 0x0014, + Nonce = 0x0015, + RequestedAddressFamily = 0x0017,// Added in RFC6156. + XORMappedAddress = 0x0020, + + Software = 0x8022, // Added in RFC5389. + AlternateServer = 0x8023, // Added in RFC5389. + FingerPrint = 0x8028, // Added in RFC5389. + + IceControlled = 0x8029, // Added in RFC8445. + IceControlling = 0x802a, // Added in RFC8445. + Priority = 0x0024, // Added in RFC8445. + + UseCandidate = 0x0025, // Added in RFC5245. + + // New attributes defined in TURN (RFC5766). + ChannelNumber = 0x000C, + Lifetime = 0x000D, + XORPeerAddress = 0x0012, + Data = 0x0013, + XORRelayedAddress = 0x0016, + EvenPort = 0x0018, + RequestedTransport = 0x0019, + DontFragment = 0x001A, + ReservationToken = 0x0022, + + ConnectionId = 0x002a, // Added in RFC6062. +} + +public static class STUNAttributeTypes { - public enum STUNAttributeTypesEnum : ushort + public static STUNAttributeTypesEnum GetSTUNAttributeTypeForId(int stunAttributeTypeId) { - Unknown = 0, - MappedAddress = 0x0001, - ResponseAddress = 0x0002, // Not used in RFC5389. - ChangeRequest = 0x0003, // Not used in RFC5389. - SourceAddress = 0x0004, // Not used in RFC5389. - ChangedAddress = 0x0005, // Not used in RFC5389. - Username = 0x0006, - Password = 0x0007, // Not used in RFC5389. - MessageIntegrity = 0x0008, - ErrorCode = 0x0009, - UnknownAttributes = 0x000A, - ReflectedFrom = 0x000B, // Not used in RFC5389. - Realm = 0x0014, - Nonce = 0x0015, - RequestedAddressFamily = 0x0017,// Added in RFC6156. - XORMappedAddress = 0x0020, - - Software = 0x8022, // Added in RFC5389. - AlternateServer = 0x8023, // Added in RFC5389. - FingerPrint = 0x8028, // Added in RFC5389. - - IceControlled = 0x8029, // Added in RFC8445. - IceControlling = 0x802a, // Added in RFC8445. - Priority = 0x0024, // Added in RFC8445. - - UseCandidate = 0x0025, // Added in RFC5245. - - // New attributes defined in TURN (RFC5766). - ChannelNumber = 0x000C, - Lifetime = 0x000D, - XORPeerAddress = 0x0012, - Data = 0x0013, - XORRelayedAddress = 0x0016, - EvenPort = 0x0018, - RequestedTransport = 0x0019, - DontFragment = 0x001A, - ReservationToken = 0x0022, - - ConnectionId = 0x002a, // Added in RFC6062. + return (STUNAttributeTypesEnum)Enum.Parse(typeof(STUNAttributeTypesEnum), stunAttributeTypeId.ToString(), true); } +} + +public static class STUNAttributeConstants +{ + public static readonly byte[] UdpTransportType = new byte[] { 0x11, 0x00, 0x00, 0x00 }; // The payload type for UDP in a RequestedTransport type attribute. + public static readonly byte[] TcpTransportType = new byte[] { 0x06, 0x00, 0x00, 0x00 }; // The payload type for TCP in a RequestedTransport type attribute. + + /// + /// The requested TURN relay ip address is IPv4 (RFC5389, Section 15.1) + /// + public static readonly byte[] IPv4AddressFamily = new byte[] { 0x01, 0x00, 0x00, 0x00 }; + /// + /// The requested TURN relay ip address is IPv6 (RFC5389, Section 15.1) + /// + public static readonly byte[] IPv6AddressFamily = new byte[] { 0x02, 0x00, 0x00, 0x00 }; +} + +public partial class STUNAttribute : IByteSerializable +{ + public const short STUNATTRIBUTE_HEADER_LENGTH = 4; + + private static readonly ILogger logger = LogFactory.CreateLogger(); - public class STUNAttributeTypes + public STUNAttributeTypesEnum AttributeType { get; private set; } = STUNAttributeTypesEnum.Unknown; + public ReadOnlyMemory Value { get; private set; } + + public virtual ushort PaddedLength { - public static STUNAttributeTypesEnum GetSTUNAttributeTypeForId(int stunAttributeTypeId) + get { - return (STUNAttributeTypesEnum)Enum.Parse(typeof(STUNAttributeTypesEnum), stunAttributeTypeId.ToString(), true); + if (!Value.IsEmpty) + { + return Convert.ToUInt16((Value.Length % 4 == 0) ? Value.Length : Value.Length + (4 - (Value.Length % 4))); + } + else + { + return 0; + } } } - public class STUNAttributeConstants + public STUNAttribute(STUNAttributeTypesEnum attributeType, ReadOnlyMemory value) { - public static readonly byte[] UdpTransportType = new byte[] { 0x11, 0x00, 0x00, 0x00 }; // The payload type for UDP in a RequestedTransport type attribute. - public static readonly byte[] TcpTransportType = new byte[] { 0x06, 0x00, 0x00, 0x00 }; // The payload type for TCP in a RequestedTransport type attribute. - - /// - /// The requested TURN relay ip address is IPv4 (RFC5389, Section 15.1) - /// - public static readonly byte[] IPv4AddressFamily = new byte[] { 0x01, 0x00, 0x00, 0x00 }; - /// - /// The requested TURN relay ip address is IPv6 (RFC5389, Section 15.1) - /// - public static readonly byte[] IPv6AddressFamily = new byte[] { 0x02, 0x00, 0x00, 0x00 }; + AttributeType = attributeType; + Value = value; } - public class STUNAttribute + public STUNAttribute(STUNAttributeTypesEnum attributeType, ushort value) { - public const short STUNATTRIBUTE_HEADER_LENGTH = 4; + AttributeType = attributeType; + var bytes = new byte[sizeof(ushort)]; + BinaryPrimitives.WriteUInt16BigEndian(bytes, value); + Value = bytes; + } + + public STUNAttribute(STUNAttributeTypesEnum attributeType, uint value) + { + AttributeType = attributeType; + var bytes = new byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32BigEndian(bytes, value); + Value = bytes; + } - private static readonly ILogger logger = LogFactory.CreateLogger(); + public STUNAttribute(STUNAttributeTypesEnum attributeType, ulong value) + { + AttributeType = attributeType; + var bytes = new byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64BigEndian(bytes, value); + Value = bytes; + } - public STUNAttributeTypesEnum AttributeType = STUNAttributeTypesEnum.Unknown; - public byte[] Value; + public static List? ParseMessageAttributes(ReadOnlySpan buffer) + => ParseMessageAttributes(buffer, null); - public virtual UInt16 PaddedLength + public static List? ParseMessageAttributes(ReadOnlySpan buffer, STUNHeader? header) + { + if (buffer.IsEmpty) { - get - { - if (Value != null) - { - return Convert.ToUInt16((Value.Length % 4 == 0) ? Value.Length : Value.Length + (4 - (Value.Length % 4))); - } - else - { - return 0; - } - } + return null; } - public STUNAttribute(STUNAttributeTypesEnum attributeType, byte[] value) - { - AttributeType = attributeType; - Value = value; - } + var attributes = new List(); - public STUNAttribute(STUNAttributeTypesEnum attributeType, ushort value) - { - AttributeType = attributeType; - Value = NetConvert.GetBytes(value); - } + buffer = ParseMessageAttributes(buffer, header, attributes); - public STUNAttribute(STUNAttributeTypesEnum attributeType, uint value) - { - AttributeType = attributeType; - Value = NetConvert.GetBytes(value); - } + return attributes; + } - public STUNAttribute(STUNAttributeTypesEnum attributeType, ulong value) + public static ReadOnlySpan ParseMessageAttributes(ReadOnlySpan buffer, STUNHeader? header, List attributes) + { + while (buffer.Length >= 4) { - AttributeType = attributeType; - Value = NetConvert.GetBytes(value); - } + var stunAttributeType = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(0, 2)); + var stunAttributeLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2, 2)); + byte[]? stunAttributeValue = null; - public static List ParseMessageAttributes(byte[] buffer, int startIndex, int endIndex) => ParseMessageAttributes(buffer, startIndex, endIndex, null); + var attributeType = STUNAttributeTypes.GetSTUNAttributeTypeForId(stunAttributeType); - public static List ParseMessageAttributes(byte[] buffer, int startIndex, int endIndex, STUNHeader header) - { - if (buffer != null && buffer.Length > startIndex && buffer.Length >= endIndex) + if (stunAttributeLength > 0) { - List attributes = new List(); - int startAttIndex = startIndex; - - while (startAttIndex < endIndex - 4) + if (stunAttributeLength > buffer.Length - 4) { - UInt16 stunAttributeType = NetConvert.ParseUInt16(buffer, startAttIndex); - UInt16 stunAttributeLength = NetConvert.ParseUInt16(buffer, startAttIndex + 2); - byte[] stunAttributeValue = null; - - STUNAttributeTypesEnum attributeType = STUNAttributeTypes.GetSTUNAttributeTypeForId(stunAttributeType); - - if (stunAttributeLength > 0) - { - if (stunAttributeLength + startAttIndex + 4 > endIndex) - { - logger.LogWarning("The attribute length on a STUN parameter was greater than the available number of bytes. Type: {AttributeType}", attributeType); - } - else - { - stunAttributeValue = new byte[stunAttributeLength]; - Buffer.BlockCopy(buffer, startAttIndex + 4, stunAttributeValue, 0, stunAttributeLength); - } - } - - if(stunAttributeValue == null && stunAttributeLength > 0) - { - break; - } - STUNAttribute attribute = null; - if (attributeType == STUNAttributeTypesEnum.ChangeRequest) - { - attribute = new STUNChangeRequestAttribute(stunAttributeValue); - } - else if (attributeType == STUNAttributeTypesEnum.MappedAddress || attributeType == STUNAttributeTypesEnum.AlternateServer) - { - attribute = new STUNAddressAttribute(attributeType, stunAttributeValue); - } - else if (attributeType == STUNAttributeTypesEnum.ErrorCode) - { - attribute = new STUNErrorCodeAttribute(stunAttributeValue); - } - else if (attributeType == STUNAttributeTypesEnum.XORMappedAddress || attributeType == STUNAttributeTypesEnum.XORPeerAddress || attributeType == STUNAttributeTypesEnum.XORRelayedAddress) - { - attribute = new STUNXORAddressAttribute(attributeType, stunAttributeValue, header.TransactionId); - } - else if(attributeType == STUNAttributeTypesEnum.ConnectionId) - { - attribute = new STUNConnectionIdAttribute(stunAttributeValue); - } - else - { - attribute = new STUNAttribute(attributeType, stunAttributeValue); - } - - attributes.Add(attribute); - - // Attributes start on 32 bit word boundaries so where an attribute length is not a multiple of 4 it gets padded. - int padding = (stunAttributeLength % 4 != 0) ? 4 - (stunAttributeLength % 4) : 0; - - startAttIndex = startAttIndex + 4 + stunAttributeLength + padding; + logger.LogStunAttributeLengthOverflow(attributeType); + break; } + else + { + stunAttributeValue = buffer.Slice(4, stunAttributeLength).ToArray(); + } + } - return attributes; + STUNAttribute attribute; + if (attributeType == STUNAttributeTypesEnum.ChangeRequest) + { + Debug.Assert(stunAttributeValue is { }); + attribute = new STUNChangeRequestAttribute(stunAttributeValue); } - else + else if (attributeType is STUNAttributeTypesEnum.MappedAddress or STUNAttributeTypesEnum.AlternateServer) { - return null; + attribute = new STUNAddressAttribute(attributeType, stunAttributeValue); } - } - - public virtual int ToByteBuffer(byte[] buffer, int startIndex) - { - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(startIndex), (ushort)AttributeType); - - if (Value != null && Value.Length > 0) + else if (attributeType == STUNAttributeTypesEnum.ErrorCode) { - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(startIndex + 2), Convert.ToUInt16(Value.Length)); + Debug.Assert(stunAttributeValue is { }); + attribute = new STUNErrorCodeAttribute(stunAttributeValue); } - else + else if (attributeType is STUNAttributeTypesEnum.XORMappedAddress or STUNAttributeTypesEnum.XORPeerAddress or STUNAttributeTypesEnum.XORRelayedAddress) { - buffer[startIndex + 2] = 0x00; - buffer[startIndex + 3] = 0x00; + Debug.Assert(header is { }); + attribute = new STUNXORAddressAttribute(attributeType, stunAttributeValue, header.TransactionId); } - - if (Value != null && Value.Length > 0) + else if (attributeType == STUNAttributeTypesEnum.ConnectionId) { - Buffer.BlockCopy(Value, 0, buffer, startIndex + 4, Value.Length); + attribute = new STUNConnectionIdAttribute(stunAttributeValue); } + else + { + attribute = new STUNAttribute(attributeType, stunAttributeValue); + } + + attributes.Add(attribute); + + // Attributes start on 32 bit word boundaries so where an attribute length is not a multiple of 4 it gets padded. + var padding = (4 - (stunAttributeLength & 0b11)) & 0b11; + + buffer = buffer.Slice(4 + stunAttributeLength + padding); + } + + return buffer; + } - return STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + PaddedLength; + /// + public virtual int GetByteCount() => STUNATTRIBUTE_HEADER_LENGTH + PaddedLength; + + /// + public virtual int WriteBytes(Span buffer) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(0, 2), (ushort)AttributeType); + + if (!Value.IsEmpty) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2, 2), (ushort)Value.Length); + } + else + { + buffer[2] = 0x00; + buffer[3] = 0x00; } - public new virtual string ToString() + if (!Value.IsEmpty) { - string attrDescrString = $"STUN Attribute: {AttributeType.ToString()}, length={PaddedLength}."; + Value.Span.CopyTo(buffer.Slice(4, Value.Length)); + } + + return STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + PaddedLength; + } + + public override string ToString() + { + var sb = new ValueStringBuilder(stackalloc char[256]); - return attrDescrString; + try + { + ToString(ref sb); + + return sb.ToString(); } + finally + { + sb.Dispose(); + } + } + + internal void ToString(ref ValueStringBuilder sb) + { + sb.Append("STUN Attribute: "); + sb.Append(AttributeType.ToStringFast()); + sb.Append(", "); + ValueToString(ref sb); + sb.Append('.'); + } + + private protected virtual void ValueToString(ref ValueStringBuilder sb) + { + sb.Append(Value.Span); + sb.Append(", length="); + sb.Append(PaddedLength); } } diff --git a/src/SIPSorcery/net/STUN/STUNAttributes/STUNChangeRequestAttribute.cs b/src/SIPSorcery/net/STUN/STUNAttributes/STUNChangeRequestAttribute.cs index b41738deb0..96e8da6a01 100644 --- a/src/SIPSorcery/net/STUN/STUNAttributes/STUNChangeRequestAttribute.cs +++ b/src/SIPSorcery/net/STUN/STUNAttributes/STUNChangeRequestAttribute.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: STUNChangeRequestAttribute.cs // // Description: Implements STUN change request attribute as defined in RFC5389. @@ -14,48 +14,51 @@ //----------------------------------------------------------------------------- using System; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net; -namespace SIPSorcery.Net +public partial class STUNChangeRequestAttribute : STUNAttribute { - public class STUNChangeRequestAttribute : STUNAttribute + public const ushort CHANGEREQUEST_ATTRIBUTE_LENGTH = 4; + + public bool ChangeAddress; + public bool ChangePort; + + public override ushort PaddedLength { - public const UInt16 CHANGEREQUEST_ATTRIBUTE_LENGTH = 4; + get { return CHANGEREQUEST_ATTRIBUTE_LENGTH; } + } + + private byte m_changeRequestByte; - public bool ChangeAddress = false; - public bool ChangePort = false; + public STUNChangeRequestAttribute(byte[] attributeValue) + : base(STUNAttributeTypesEnum.ChangeRequest, attributeValue) + { + m_changeRequestByte = attributeValue[3]; - public override UInt16 PaddedLength + if (m_changeRequestByte == 0x02) { - get { return CHANGEREQUEST_ATTRIBUTE_LENGTH; } + ChangePort = true; } - - private byte m_changeRequestByte; - - public STUNChangeRequestAttribute(byte[] attributeValue) - : base(STUNAttributeTypesEnum.ChangeRequest, attributeValue) + else if (m_changeRequestByte == 0x04) { - m_changeRequestByte = attributeValue[3]; - - if (m_changeRequestByte == 0x02) - { - ChangePort = true; - } - else if (m_changeRequestByte == 0x04) - { - ChangeAddress = true; - } - else if (m_changeRequestByte == 0x06) - { - ChangePort = true; - ChangeAddress = true; - } + ChangeAddress = true; } - - public override string ToString() + else if (m_changeRequestByte == 0x06) { - string attrDescrStr = $"STUN Attribute: {STUNAttributeTypesEnum.ChangeRequest.ToString()}, key byte={m_changeRequestByte.ToString("X")}, change address={ChangeAddress}, change port={ChangePort}."; - - return attrDescrStr; + ChangePort = true; + ChangeAddress = true; } } + + private protected override void ValueToString(ref ValueStringBuilder sb) + { + sb.Append("key byte="); + sb.Append(m_changeRequestByte, "X"); + sb.Append(", change address="); + sb.Append(ChangeAddress); + sb.Append(", change port="); + sb.Append(ChangePort); + } } diff --git a/src/SIPSorcery/net/STUN/STUNAttributes/STUNConnectionIdAttribute.cs b/src/SIPSorcery/net/STUN/STUNAttributes/STUNConnectionIdAttribute.cs index bce3d091cc..741c5cfc87 100644 --- a/src/SIPSorcery/net/STUN/STUNAttributes/STUNConnectionIdAttribute.cs +++ b/src/SIPSorcery/net/STUN/STUNAttributes/STUNConnectionIdAttribute.cs @@ -15,38 +15,29 @@ using System; using System.Buffers.Binary; -using System.Text; +using SIPSorcery.Sys; -namespace SIPSorcery.Net -{ - public class STUNConnectionIdAttribute : STUNAttribute - { - public readonly uint ConnectionId; - - public STUNConnectionIdAttribute(byte[] attributeValue) - : base(STUNAttributeTypesEnum.ConnectionId, attributeValue) - { - ConnectionId = BinaryPrimitives.ReadUInt32BigEndian(attributeValue); - } +namespace SIPSorcery.Net; - private static byte[] GetBytes(uint value) - { - var buf = new byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(buf, value); - return buf; - } +public partial class STUNConnectionIdAttribute : STUNAttribute +{ + public readonly uint ConnectionId; - public STUNConnectionIdAttribute(uint connectionId) - : base(STUNAttributeTypesEnum.ConnectionId, GetBytes(connectionId)) - { - ConnectionId = connectionId; - } + public STUNConnectionIdAttribute(ReadOnlyMemory attributeValue) + : base(STUNAttributeTypesEnum.ConnectionId, attributeValue) + { + ConnectionId = BinaryPrimitives.ReadUInt32BigEndian(attributeValue.Span); + } - public override string ToString() - { - string attrDescrStr = $"STUN CONNECTION_ID Attribute: value={ConnectionId}."; + public STUNConnectionIdAttribute(uint connectionId) + : base(STUNAttributeTypesEnum.ConnectionId, connectionId) + { + ConnectionId = connectionId; + } - return attrDescrStr; - } + private protected override void ValueToString(ref ValueStringBuilder sb) + { + sb.Append("connection ID="); + sb.Append(ConnectionId); } } diff --git a/src/SIPSorcery/net/STUN/STUNAttributes/STUNErrorCodeAttribute.cs b/src/SIPSorcery/net/STUN/STUNAttributes/STUNErrorCodeAttribute.cs index 709af72a10..13db5a2255 100644 --- a/src/SIPSorcery/net/STUN/STUNAttributes/STUNErrorCodeAttribute.cs +++ b/src/SIPSorcery/net/STUN/STUNAttributes/STUNErrorCodeAttribute.cs @@ -15,54 +15,64 @@ using System; using System.Text; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public partial class STUNErrorCodeAttribute : STUNAttribute { - public class STUNErrorCodeAttribute : STUNAttribute - { - public byte ErrorClass; // The hundreds value of the error code must be between 3 and 6. - public byte ErrorNumber; // The units value of the error code must be between 0 and 99. - public string ReasonPhrase; + public byte ErrorClass; // The hundreds value of the error code must be between 3 and 6. + public byte ErrorNumber; // The units value of the error code must be between 0 and 99. + public string ReasonPhrase; - public int ErrorCode + public int ErrorCode + { + get { - get - { - return ErrorClass * 100 + ErrorNumber; - } + return ErrorClass * 100 + ErrorNumber; } + } - public STUNErrorCodeAttribute(byte[] attributeValue) - : base(STUNAttributeTypesEnum.ErrorCode, attributeValue) - { - ErrorClass = attributeValue[2]; - ErrorNumber = attributeValue[3]; - ReasonPhrase = Encoding.UTF8.GetString(attributeValue, 4, attributeValue.Length - 4); - } + public STUNErrorCodeAttribute(byte[] attributeValue) + : base(STUNAttributeTypesEnum.ErrorCode, attributeValue) + { + ErrorClass = attributeValue[2]; + ErrorNumber = attributeValue[3]; + ReasonPhrase = Encoding.UTF8.GetString(attributeValue, 4, attributeValue.Length - 4); + } - public STUNErrorCodeAttribute(int errorCode, string reasonPhrase) + public STUNErrorCodeAttribute(int errorCode, string reasonPhrase) : base(STUNAttributeTypesEnum.ErrorCode, BuildValue(errorCode, reasonPhrase)) - { - ErrorClass = (byte)(errorCode / 100); - ErrorNumber = (byte)(errorCode % 100); - ReasonPhrase = reasonPhrase; - } + { + ErrorClass = (byte)(errorCode / 100); + ErrorNumber = (byte)(errorCode % 100); + ReasonPhrase = reasonPhrase; + } - private static byte[] BuildValue(int errorCode, string reasonPhrase) - { - byte[] reasonBytes = Encoding.UTF8.GetBytes(reasonPhrase ?? string.Empty); - byte[] value = new byte[4 + reasonBytes.Length]; - value[2] = (byte)(errorCode / 100); - value[3] = (byte)(errorCode % 100); - Buffer.BlockCopy(reasonBytes, 0, value, 4, reasonBytes.Length); - return value; - } + private static byte[] BuildValue(int errorCode, string reasonPhrase) + { + var reasonBytes = Encoding.UTF8.GetBytes(reasonPhrase ?? string.Empty); + var value = new byte[4 + reasonBytes.Length]; + value[2] = (byte)(errorCode / 100); + value[3] = (byte)(errorCode % 100); + Buffer.BlockCopy(reasonBytes, 0, value, 4, reasonBytes.Length); + return value; + } - public override string ToString() - { - string attrDescrStr = $"STUN ERROR_CODE_ADDRESS Attribute: error code={ErrorCode}, reason phrase={ReasonPhrase}."; + /// + public override int GetByteCount() + { + var reasonBytesLen = string.IsNullOrEmpty(ReasonPhrase) ? 0 : Encoding.UTF8.GetByteCount(ReasonPhrase); + var valueLen = 4 + reasonBytesLen; // 2 reserved + class + number + reason + var paddedValueLen = (valueLen % 4 == 0) ? valueLen : valueLen + (4 - (valueLen % 4)); + return STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + paddedValueLen; + } - return attrDescrStr; - } + private protected override void ValueToString(ref ValueStringBuilder sb) + { + sb.Append("error code="); + sb.Append(ErrorCode); + sb.Append(", reason phrase="); + sb.Append(ReasonPhrase); } } diff --git a/src/SIPSorcery/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs b/src/SIPSorcery/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs index 544256c421..341f375b35 100644 --- a/src/SIPSorcery/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs +++ b/src/SIPSorcery/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs @@ -15,139 +15,151 @@ using System; using System.Buffers.Binary; -using System.Linq; +using System.Diagnostics; using System.Net; +using System.Runtime.InteropServices; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// This attribute is the same as the mapped address attribute except the address details are XOR'ed with the STUN magic cookie. +/// THe reason for this is to stop NAT application layer gateways from doing string replacements of private IP addresses and ports. +/// +public partial class STUNXORAddressAttribute : STUNAddressAttributeBase { /// - /// This attribute is the same as the mapped address attribute except the address details are XOR'ed with the STUN magic cookie. - /// THe reason for this is to stop NAT application layer gateways from doing string replacements of private IP addresses and ports. + /// Parses an XOR-d (encoded) Address attribute with IPv4/IPv6 support. /// - public class STUNXORAddressAttribute : STUNAddressAttributeBase + /// of + /// or + /// or + /// the raw bytes + /// the + public STUNXORAddressAttribute(STUNAttributeTypesEnum attributeType, ReadOnlyMemory attributeValue, ReadOnlySpan transactionId) + : base(attributeType, attributeValue) { - /// - /// Obsolete. - ///
For IPv6 support, please parse using - ///
- ///

- /// Parses an XOR-d (encoded) IPv4 Address attribute. - ///
- [Obsolete("Provided for backward compatibility with RFC3489 clients.")] - public STUNXORAddressAttribute(STUNAttributeTypesEnum attributeType, byte[] attributeValue) - : this(attributeType, attributeValue, null) - { - } + var attributeValueSpan = attributeValue.Span; + Family = attributeValueSpan[1]; + AddressAttributeLength = Family == 1 ? ADDRESS_ATTRIBUTE_IPV4_LENGTH : ADDRESS_ATTRIBUTE_IPV6_LENGTH; + TransactionId = transactionId.ToArray(); - /// - /// Parses an XOR-d (encoded) Address attribute with IPv4/IPv6 support. - /// - /// of - /// or - /// or - /// the raw bytes - /// the - public STUNXORAddressAttribute(STUNAttributeTypesEnum attributeType, byte[] attributeValue, byte[] transactionId) - : base(attributeType, attributeValue) - { - Family = attributeValue[1]; - AddressAttributeLength = Family == 1 ? ADDRESS_ATTRIBUTE_IPV4_LENGTH : ADDRESS_ATTRIBUTE_IPV6_LENGTH; - TransactionId = transactionId; - - byte[] address; - - Port = BinaryPrimitives.ReadUInt16BigEndian(attributeValue.AsSpan(2)) ^ (UInt16)(STUNHeader.MAGIC_COOKIE >> 16); - uint xorAddrBE = BinaryPrimitives.ReadUInt32BigEndian(attributeValue.AsSpan(4)) ^ STUNHeader.MAGIC_COOKIE; - address = new byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(address, xorAddrBE); - - if (Family == STUNAttributeConstants.IPv6AddressFamily[0] && TransactionId != null) - { - address = address.Concat(BitConverter.GetBytes(BitConverter.ToUInt32(attributeValue, 08) ^ BitConverter.ToUInt32(TransactionId, 0))) - .Concat(BitConverter.GetBytes(BitConverter.ToUInt32(attributeValue, 12) ^ BitConverter.ToUInt32(TransactionId, 4))) - .Concat(BitConverter.GetBytes(BitConverter.ToUInt32(attributeValue, 16) ^ BitConverter.ToUInt32(TransactionId, 8))) - .ToArray(); - } - - Address = new IPAddress(address); - } + var port = BinaryPrimitives.ReadUInt16BigEndian(attributeValueSpan.Slice(2)); + Port = (ushort)(port ^ (STUNHeader.MAGIC_COOKIE >> 16)); - /// - /// Obsolete. - ///
For IPv6 support, please create using - ///

- /// Creates an XOR-d (encoded) IPv4 Address attribute. - ///
- [Obsolete("Provided for backward compatibility with RFC3489 clients.")] - public STUNXORAddressAttribute(STUNAttributeTypesEnum attributeType, int port, IPAddress address) - : this(attributeType, port, address, null) + if (Family == STUNAttributeConstants.IPv4AddressFamily[0]) { + var ipv4 = BinaryPrimitives.ReadUInt32BigEndian(attributeValueSpan.Slice(4)) ^ STUNHeader.MAGIC_COOKIE; + Span addressBytes = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(addressBytes, ipv4); + Address = new IPAddress(MemoryMarshal.Read(addressBytes)); } - - /// - /// Creates an XOR-d (encoded) Address attribute with IPv4/IPv6 support. - /// - /// of - /// or - /// or - /// Allocated Port - /// Allocated IPAddress - /// the - public STUNXORAddressAttribute(STUNAttributeTypesEnum attributeType, int port, IPAddress address, byte[] transactionId) - : base(attributeType, null) + else if (Family == STUNAttributeConstants.IPv6AddressFamily[0] && TransactionId is { }) { - Port = port; - Address = address; - Family = address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork ? 1 : 2; - AddressAttributeLength = Family == 1 ? ADDRESS_ATTRIBUTE_IPV4_LENGTH : ADDRESS_ATTRIBUTE_IPV6_LENGTH; - TransactionId = transactionId; + Span addressBytes = stackalloc byte[16]; + + var part0 = BinaryPrimitives.ReadUInt32BigEndian(attributeValueSpan.Slice(4)); + var tid0 = BinaryPrimitives.ReadUInt32BigEndian(TransactionId.AsSpan(0)); + BinaryPrimitives.WriteUInt32BigEndian(addressBytes.Slice(0), part0 ^ tid0); + + var part1 = BinaryPrimitives.ReadUInt32BigEndian(attributeValueSpan.Slice(8)); + var tid1 = BinaryPrimitives.ReadUInt32BigEndian(TransactionId.AsSpan(4)); + BinaryPrimitives.WriteUInt32BigEndian(addressBytes.Slice(4), part1 ^ tid1); + + var part2 = BinaryPrimitives.ReadUInt32BigEndian(attributeValueSpan.Slice(12)); + var tid2 = BinaryPrimitives.ReadUInt32BigEndian(TransactionId.AsSpan(8)); + BinaryPrimitives.WriteUInt32BigEndian(addressBytes.Slice(8), part2 ^ tid2); + + var lastPart = BinaryPrimitives.ReadUInt32BigEndian(attributeValueSpan.Slice(4 + 12)); + BinaryPrimitives.WriteUInt32LittleEndian(addressBytes.Slice(12), lastPart ^ STUNHeader.MAGIC_COOKIE); + + Address = IPAddress.Create(addressBytes); } + } + + /// + /// Obsolete. + ///
For IPv6 support, please create using + ///

+ /// Creates an XOR-d (encoded) IPv4 Address attribute. + ///
+ [Obsolete("Provided for backward compatibility with RFC3489 clients.")] + public STUNXORAddressAttribute(STUNAttributeTypesEnum attributeType, int port, IPAddress address) + : this(attributeType, port, address, null) + { + } - public override int ToByteBuffer(byte[] buffer, int startIndex) + /// + /// Creates an XOR-d (encoded) Address attribute with IPv4/IPv6 support. + /// + /// of + /// or + /// or + /// Allocated Port + /// Allocated IPAddress + /// the + public STUNXORAddressAttribute(STUNAttributeTypesEnum attributeType, int port, IPAddress address, byte[]? transactionId) + : base(attributeType, null) + { + Port = port; + Address = address; + Family = address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork ? 1 : 2; + AddressAttributeLength = Family == 1 ? ADDRESS_ATTRIBUTE_IPV4_LENGTH : ADDRESS_ATTRIBUTE_IPV6_LENGTH; + TransactionId = transactionId; + } + + /// + public override int GetByteCount() => STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + AddressAttributeLength; + + /// + public override int WriteBytes(Span buffer) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(0, 2), (ushort)base.AttributeType); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2, 2), AddressAttributeLength); + + buffer[4] = 0x00; + buffer[5] = (byte)Family; + + Debug.Assert(Address is { }); + var address = Address.GetAddressBytes(); + + var xorPort = (ushort)(Port ^ (STUNHeader.MAGIC_COOKIE >> 16)); + var xorAddress = BinaryPrimitives.ReadUInt32BigEndian(address) ^ STUNHeader.MAGIC_COOKIE; + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(6, 2), xorPort); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(8, 4), xorAddress); + + if (Family == STUNAttributeConstants.IPv6AddressFamily[0] && TransactionId is { }) { - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(startIndex), (UInt16)base.AttributeType); - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(startIndex + 2), AddressAttributeLength); - - buffer[startIndex + 4] = 0x00; - buffer[startIndex + 5] = (byte)Family; - - var address = Address.GetAddressBytes(); - - UInt16 xorPort = Convert.ToUInt16(Convert.ToUInt16(Port) ^ (UInt16)(STUNHeader.MAGIC_COOKIE >> 16)); - UInt32 xorAddress = BinaryPrimitives.ReadUInt32BigEndian(address) ^ STUNHeader.MAGIC_COOKIE; - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(startIndex + 6), xorPort); - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(startIndex + 8), xorAddress); - - if (Family == STUNAttributeConstants.IPv6AddressFamily[0] && TransactionId != null) - { - Buffer.BlockCopy( - BitConverter.GetBytes(BitConverter.ToUInt32(address, 04) ^ BitConverter.ToUInt32(TransactionId, 0)) - .Concat(BitConverter.GetBytes(BitConverter.ToUInt32(address, 08) ^ BitConverter.ToUInt32(TransactionId, 4))) - .Concat(BitConverter.GetBytes(BitConverter.ToUInt32(address, 12) ^ BitConverter.ToUInt32(TransactionId, 8))) - .ToArray(), - 0, buffer, startIndex + 12, 12); - } - - return STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + PaddedLength; + BinaryPrimitives.WriteUInt32BigEndian( + buffer.Slice(12, 4), + BinaryPrimitives.ReadUInt32BigEndian(address.AsSpan(4)) ^ BinaryPrimitives.ReadUInt32BigEndian(TransactionId.AsSpan(0)) + ); + + BinaryPrimitives.WriteUInt32BigEndian( + buffer.Slice(16, 4), + BinaryPrimitives.ReadUInt32BigEndian(address.AsSpan(8)) ^ BinaryPrimitives.ReadUInt32BigEndian(TransactionId.AsSpan(4)) + ); + + BinaryPrimitives.WriteUInt32BigEndian( + buffer.Slice(20, 4), + BinaryPrimitives.ReadUInt32BigEndian(address.AsSpan(12)) ^ BinaryPrimitives.ReadUInt32BigEndian(TransactionId.AsSpan(8)) + ); } - public override string ToString() - { - string attrDescrStr = $"STUN XOR_MAPPED_ADDRESS Attribute: {base.AttributeType}, address={Address.ToString()}, port={Port}."; + return STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + PaddedLength; + } - return attrDescrStr; + public IPEndPoint? GetIPEndPoint() + { + if (Address is { }) + { + return new IPEndPoint(Address, Port); } - - public IPEndPoint GetIPEndPoint() + else { - if (Address != null) - { - return new IPEndPoint(Address, Port); - } - else - { - return null; - } + return null; } } + + private protected override void ValueToString(ref ValueStringBuilder sb) => base.ValueToString(ref sb); } diff --git a/src/SIPSorcery/net/STUN/STUNClient.cs b/src/SIPSorcery/net/STUN/STUNClient.cs index ece53fe3af..c391fa153d 100644 --- a/src/SIPSorcery/net/STUN/STUNClient.cs +++ b/src/SIPSorcery/net/STUN/STUNClient.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Linq; using System.Net; @@ -86,7 +88,8 @@ public static IPEndPoint GetPublicIPEndPoint(string stunServer, int port = DEFAU using (UdpClient udpClient = new UdpClient(stunServer, port)) { STUNMessage initMessage = new STUNMessage(STUNMessageTypesEnum.BindingRequest); - byte[] stunMessageBytes = initMessage.ToByteBuffer(null, false); + byte[] stunMessageBytes = new byte[initMessage.GetByteBufferSize(null, false)]; + initMessage.WriteToBuffer(stunMessageBytes, null, false); udpClient.Send(stunMessageBytes, stunMessageBytes.Length); IPEndPoint publicEndPoint = null; @@ -102,7 +105,7 @@ public static IPEndPoint GetPublicIPEndPoint(string stunServer, int port = DEFAU if (stunResponseBuffer != null && stunResponseBuffer.Length > 0) { logger.LogDebug("STUNClient Response to initial STUN message received from {stunResponseEndPoint}.", stunResponseEndPoint); - STUNMessage stunResponse = STUNMessage.ParseSTUNMessage(stunResponseBuffer, stunResponseBuffer.Length); + STUNMessage stunResponse = STUNMessage.ParseSTUNMessage(stunResponseBuffer.AsSpan()); if (stunResponse.Attributes.Count > 0) { @@ -214,7 +217,8 @@ void OnStunMessageReceived(STUNMessage stunResponse, IPEndPoint remoteEndPoint, logger.LogDebug("STUNClient sending BindingRequest for RTP channel {LocalEndPoint} to {StunServer}.", rtpChannel.RTPLocalEndPoint, stunServer); var initMessage = new STUNMessage(STUNMessageTypesEnum.BindingRequest); - var bytes = initMessage.ToByteBuffer(null, false); + var bytes = new byte[initMessage.GetByteBufferSize(null, false)]; + initMessage.WriteToBuffer(bytes, null, false); rtpChannel.Send(RTPChannelSocketsEnum.RTP, stunServer, bytes); // Race the response against a timeout. diff --git a/src/SIPSorcery/net/STUN/STUNClientExtensions.cs b/src/SIPSorcery/net/STUN/STUNClientExtensions.cs index 68bd07f5fe..610dfe0dfe 100644 --- a/src/SIPSorcery/net/STUN/STUNClientExtensions.cs +++ b/src/SIPSorcery/net/STUN/STUNClientExtensions.cs @@ -14,6 +14,8 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; diff --git a/src/SIPSorcery/net/STUN/STUNDns.cs b/src/SIPSorcery/net/STUN/STUNDns.cs index 3345aa524b..f684fdd08e 100644 --- a/src/SIPSorcery/net/STUN/STUNDns.cs +++ b/src/SIPSorcery/net/STUN/STUNDns.cs @@ -37,6 +37,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Sockets; @@ -46,239 +47,247 @@ using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public static class STUNDns { - public class STUNDns - { - public const string MDNS_TLD = "local"; // Top Level Domain name for multicast lookups as per RFC6762. - public const int DNS_TIMEOUT_SECONDS = 1; - public const int DNS_RETRIES_PER_SERVER = 1; + public const string MDNS_TLD = "local"; // Top Level Domain name for multicast lookups as per RFC6762. + public const int DNS_TIMEOUT_SECONDS = 1; + public const int DNS_RETRIES_PER_SERVER = 1; - private static readonly ILogger logger = LogFactory.CreateLogger(); + private static readonly ILogger logger = LogFactory.CreateLogger(typeof(STUNDns).FullName!); - private static LookupClient _lookupClient; + private static LookupClient _lookupClient; - /// - /// Set to true to attempt a DNS lookup over TCP if the UDP lookup fails. - /// - private static bool _dnsUseTcpFallback; + /// + /// Set to true to attempt a DNS lookup over TCP if the UDP lookup fails. + /// + private static bool _dnsUseTcpFallback; - /// - /// Set to true to attempt a DNS lookup over TCP if the UDP lookup fails. - /// - public static bool DnsUseTcpFallback + /// + /// Set to true to attempt a DNS lookup over TCP if the UDP lookup fails. + /// + public static bool DnsUseTcpFallback + { + get => _dnsUseTcpFallback; + set { - get => _dnsUseTcpFallback; - set + if (_dnsUseTcpFallback != value) { - if (_dnsUseTcpFallback != value) - { - _dnsUseTcpFallback = value; - _lookupClient = CreateLookupClient(); - } + _dnsUseTcpFallback = value; + _lookupClient = CreateLookupClient(); } } + } - static STUNDns() - { - _lookupClient = CreateLookupClient(); - } + static STUNDns() + { + _lookupClient = CreateLookupClient(); + } - /// - /// Resolve method that can be used to request an AAAA result and fallback to a A - /// lookup if none found. - /// - /// The URI to lookup. - /// True if IPv6 (AAAA record lookup) is preferred. - /// An IPEndPoint or null. - public static Task Resolve(STUNUri uri, bool preferIPv6 = false) + /// + /// Resolve method that can be used to request an AAAA result and fallback to a A + /// lookup if none found. + /// + /// The URI to lookup. + /// True if IPv6 (AAAA record lookup) is preferred. + /// An IPEndPoint or null. + public static Task Resolve(STUNUri uri, bool preferIPv6 = false) + { + return Resolve(uri, preferIPv6 ? QueryType.AAAA : QueryType.A); + } + + /// + /// Resolve method that performs either an A or AAAA record lookup. If required + /// a SRV record lookup will be performed prior to the A or AAAA lookup. + /// + /// The STUN uri to lookup. + /// Whether the address lookup should be A or AAAA. + /// An IPEndPoint or null. + private static async Task Resolve(STUNUri uri, QueryType queryType) + { + ArgumentNullException.ThrowIfNull(uri); + ArgumentException.ThrowIfNullOrWhiteSpace(uri.Host); + + if (IPAddress.TryParse(uri.Host, out var ipAddress)) { - return Resolve(uri, preferIPv6 ? QueryType.AAAA : QueryType.A); + // Target is already an IP address, no DNS lookup required. + return new IPEndPoint(ipAddress, uri.Port); } - - /// - /// Resolve method that performs either an A or AAAA record lookup. If required - /// a SRV record lookup will be performed prior to the A or AAAA lookup. - /// - /// The STUN uri to lookup. - /// Whether the address lookup should be A or AAAA. - /// An IPEndPoint or null. - private static async Task Resolve(STUNUri uri, QueryType queryType) + else { - if (uri == null || String.IsNullOrWhiteSpace(uri.Host)) - { - throw new ArgumentNullException("uri", "DNS resolve was supplied an empty input."); - } + var useDnsClient = true; - if (IPAddress.TryParse(uri.Host, out var ipAddress)) + if (!uri.Host.Contains('.') || uri.Host.EndsWith(MDNS_TLD) || queryType == QueryType.A || queryType == QueryType.AAAA) { - // Target is already an IP address, no DNS lookup required. - return new IPEndPoint(ipAddress, uri.Port); - } - else - { - bool useDnsClient = true; + useDnsClient = false; + var family = (queryType == QueryType.AAAA) + ? AddressFamily.InterNetworkV6 + : AddressFamily.InterNetwork; - if (!uri.Host.Contains(".") || uri.Host.EndsWith(MDNS_TLD) || queryType == QueryType.A || queryType == QueryType.AAAA) - { - useDnsClient = false; - AddressFamily family = (queryType == QueryType.AAAA) ? AddressFamily.InterNetworkV6 : - AddressFamily.InterNetwork; + // The lookup is for a local network host. Use the OS DNS logic as the + // main DNS client can be configured to use external DNS servers that won't + // be able to lookup this hostname. - // The lookup is for a local network host. Use the OS DNS logic as the - // main DNS client can be configured to use external DNS servers that won't - // be able to lookup this hostname. + IPHostEntry? hostEntry = null; - IPHostEntry hostEntry = null; + try + { + hostEntry = await Dns.GetHostEntryAsync(uri.Host).ConfigureAwait(ConfigureAwaitOptions.None); + } + catch (SocketException) + { + // Socket exception gets thrown for failed lookups, + } - try - { - hostEntry = Dns.GetHostEntry(uri.Host); - } - catch (SocketException) + if (hostEntry is { }) + { + var addressList = hostEntry.AddressList ?? Array.Empty(); + + if (addressList.Length == 0) { - // Socket exception gets thrown for failed lookups, + logger.LogStunDnsOsLookupFailed(uri.Host); + useDnsClient = true; } - - if (hostEntry != null) + else { - var addressList = hostEntry.AddressList; - - if (addressList?.Length == 0) + for (var i = 0; i < addressList.Length; i++) { - logger.LogWarning("Operating System DNS lookup failed for {Host}.", uri.Host); - useDnsClient = true; - //return null; - } - else - { - if (addressList.Any(x => x.AddressFamily == family)) + if (addressList[i].AddressFamily == family) { - var addressResult = addressList.First(x => x.AddressFamily == family); - return new IPEndPoint(addressResult, uri.Port); - } - else - { - // Didn't get a result for the preferred address family so just use the - // first available result. - var addressResult = addressList.First(); - return new IPEndPoint(addressResult, uri.Port); + return new IPEndPoint(addressList[i], uri.Port); } } + + // Didn't get a result for the preferred address family so just use the + // first available result. + return new IPEndPoint(addressList[0], uri.Port); } - else - { - useDnsClient = true; - //return null; - } } + else + { + useDnsClient = true; + } + } - if (useDnsClient) + if (useDnsClient) + { + if (uri.ExplicitPort) { - if (uri.ExplicitPort) - { - return HostQuery(uri.Host, uri.Port, queryType); - } - else + return HostQuery(uri.Host, uri.Port, queryType); + } + else + { + try { - try - { - ServiceHostEntry srvResult = null; - // No explicit port so use a SRV -> (A | AAAA -> A) record lookup. - var result = await _lookupClient.ResolveServiceAsync(uri.Host, uri.Scheme.ToString(), uri.Protocol.ToString().ToLower()).ConfigureAwait(false); - if (result == null || result.Count() == 0) - { - //logger.LogDebug("STUNDns SRV lookup returned no results for {uri}.", uri); - } - else - { - srvResult = result.OrderBy(y => y.Priority).ThenByDescending(w => w.Weight).FirstOrDefault(); - } + // No explicit port so use a SRV -> (A | AAAA -> A) record lookup. + var result = await _lookupClient.ResolveServiceAsync(uri.Host, uri.Scheme.ToStringFast(), uri.Protocol.ToLowerString()).ConfigureAwait(false); + Debug.Assert(result is { }); - string host = uri.Host; // If no SRV results then fallback is to lookup the hostname directly. - int port = uri.Port; // If no SRV results then fallback is to use the default port. + string? host; + int port; - if (srvResult != null) + if (result.Length <= 0) + { + host = uri.Host; // If no SRV results then fallback is to lookup the hostname directly. + port = uri.Port; // If no SRV results then fallback is to use the default port. + } + else + { + // result.OrderBy(y => y.Priority).ThenByDescending(w => w.Weight).FirstOrDefault(); + var srvResult = result[0]; + for (var i = 1; i < result.Length; i++) { - host = srvResult.HostName; - port = srvResult.Port; + var entry = result[i]; + if (entry.Priority < srvResult.Priority || + (entry.Priority == srvResult.Priority && entry.Weight > srvResult.Weight)) + { + srvResult = entry; + } } - return HostQuery(host, port, queryType); - } - catch (Exception e) - { - logger.LogDebug(e, "STUNDns SRV lookup failure for {uri}. {ErrorMessage}", uri, e.InnerException?.Message); - return null; + host = srvResult.HostName; + port = srvResult.Port; } + + return HostQuery(host, port, queryType); + } + catch (Exception e) + { + logger.LogStunDnsSrvLookupFailure(uri, e.InnerException?.Message, e); + return null; } } } - return null; } + return null; + } - /// - /// Attempts to resolve a hostname. - /// - /// The hostname to resolve. - /// The service port to use in the end pint result (not used for the lookup). - /// The lookup query type, either A or AAAA. - /// If successful an IPEndPoint or null if not. - private static IPEndPoint HostQuery(string host, int port, QueryType queryType) + /// + /// Attempts to resolve a hostname. + /// + /// The hostname to resolve. + /// The service port to use in the end pint result (not used for the lookup). + /// The lookup query type, either A or AAAA. + /// If successful an IPEndPoint or null if not. + private static IPEndPoint? HostQuery(string host, int port, QueryType queryType) + { + try { - try - { - var answers = _lookupClient.Query(host, queryType).Answers; - if (answers.Count > 0) - { - return GetFromLookupResult(answers, port); - } - } - catch (Exception excp) + var answers = _lookupClient.Query(host, queryType).Answers; + if (answers.Count > 0) { - logger.LogWarning(excp, "STUNDns lookup failure for {Host} and query {QueryType}. {ErrorMessage}", host, queryType, excp.Message); + return GetFromLookupResult(answers, port); } - - if (queryType == QueryType.AAAA) - { - return HostQuery(host, port, QueryType.A); - } - - return null; + } + catch (Exception excp) + { + logger.LogStunDnsLookupFailure(host, queryType, excp.Message, excp); } - /// - /// Helper method to extract the appropriate IP address from a DNS lookup result. - /// The query may have returned an AAAA or A record. This method checks which - /// and extracts the IP address accordingly. - /// - /// The DNS lookup result. - /// The port for the IP end point. - /// An IP end point or null. - private static IPEndPoint GetFromLookupResult(IEnumerable answers, int port) + if (queryType == QueryType.AAAA) { - var addrRecord = answers.OfType().FirstOrDefault(); - return addrRecord != null - ? new IPEndPoint(addrRecord.Address, port) - : null; + return HostQuery(host, port, QueryType.A); } - /// - /// Creates a LookupClient - /// - /// A LookupClient - private static LookupClient CreateLookupClient() + return null; + } + + /// + /// Helper method to extract the appropriate IP address from a DNS lookup result. + /// The query may have returned an AAAA or A record. This method checks which + /// and extracts the IP address accordingly. + /// + /// The DNS lookup result. + /// The port for the IP end point. + /// An IP end point or null. + private static IPEndPoint? GetFromLookupResult(IEnumerable answers, int port) + { + foreach (var rr in answers) { - var nameServers = NameServer.ResolveNameServers(skipIPv6SiteLocal: true, fallbackToGooglePublicDns: true); - LookupClientOptions clientOptions = new LookupClientOptions(nameServers.ToArray()) + if (rr is AddressRecord ar) { - Retries = DNS_RETRIES_PER_SERVER, - Timeout = TimeSpan.FromSeconds(DNS_TIMEOUT_SECONDS), - UseCache = true, - UseTcpFallback = DnsUseTcpFallback - }; - - return new LookupClient(clientOptions); + return new IPEndPoint(ar.Address, port); + } } + return null; + } + + /// + /// Creates a LookupClient + /// + /// A LookupClient + private static LookupClient CreateLookupClient() + { + var nameServers = NameServer.ResolveNameServers(skipIPv6SiteLocal: true, fallbackToGooglePublicDns: true); + var clientOptions = new LookupClientOptions(nameServers.ToArray()) + { + Retries = DNS_RETRIES_PER_SERVER, + Timeout = TimeSpan.FromSeconds(DNS_TIMEOUT_SECONDS), + UseCache = true, + UseTcpFallback = DnsUseTcpFallback + }; + + return new LookupClient(clientOptions); } } diff --git a/src/SIPSorcery/net/STUN/STUNHeader.cs b/src/SIPSorcery/net/STUN/STUNHeader.cs index 5a4af0d2c3..d2d089098e 100644 --- a/src/SIPSorcery/net/STUN/STUNHeader.cs +++ b/src/SIPSorcery/net/STUN/STUNHeader.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: STUNHeader.cs // // Description: Implements STUN header as defined in RFC5389 @@ -75,118 +75,111 @@ using System.Buffers.Binary; using System.Text; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public enum STUNMessageTypesEnum : ushort { - public enum STUNMessageTypesEnum : ushort - { - BindingRequest = 0x0001, - BindingSuccessResponse = 0x0101, - BindingErrorResponse = 0x0111, - - // New methods defined in TURN (RFC5766). - Allocate = 0x0003, - Refresh = 0x0004, - Send = 0x0006, - Data = 0x0007, - CreatePermission = 0x0008, - ChannelBind = 0x0009, - - SendIndication = 0x0016, - DataIndication = 0x0017, - - AllocateSuccessResponse = 0x0103, - RefreshSuccessResponse = 0x0104, - CreatePermissionSuccessResponse = 0x0108, - ChannelBindSuccessResponse = 0x0109, - AllocateErrorResponse = 0x0113, - RefreshErrorResponse = 0x0114, - CreatePermissionErrorResponse = 0x0118, - ChannelBindErrorResponse = 0x0119, - - // New methods defined in TURN (RFC6062). - Connect = 0x000a, - ConnectionBind = 0x000b, - ConnectionAttempt = 0x000c, - } + BindingRequest = 0x0001, + BindingSuccessResponse = 0x0101, + BindingErrorResponse = 0x0111, + + // New methods defined in TURN (RFC5766). + Allocate = 0x0003, + Refresh = 0x0004, + Send = 0x0006, + Data = 0x0007, + CreatePermission = 0x0008, + ChannelBind = 0x0009, + + SendIndication = 0x0016, + DataIndication = 0x0017, + + AllocateSuccessResponse = 0x0103, + RefreshSuccessResponse = 0x0104, + CreatePermissionSuccessResponse = 0x0108, + ChannelBindSuccessResponse = 0x0109, + AllocateErrorResponse = 0x0113, + RefreshErrorResponse = 0x0114, + CreatePermissionErrorResponse = 0x0118, + ChannelBindErrorResponse = 0x0119, + + // New methods defined in TURN (RFC6062). + Connect = 0x000a, + ConnectionBind = 0x000b, + ConnectionAttempt = 0x000c, +} - /// - /// The class is interpreted from the message type. It does not get explicitly - /// set in the STUN header. - /// - public enum STUNClassTypesEnum - { - Request = 0, - Indication = 1, - SuccessResponse = 2, - ErrorResponse = 3, - } +/// +/// The class is interpreted from the message type. It does not get explicitly +/// set in the STUN header. +/// +public enum STUNClassTypesEnum +{ + Request = 0, + Indication = 1, + SuccessResponse = 2, + ErrorResponse = 3, +} - public class STUNMessageTypes +public static class STUNMessageTypes +{ + public static STUNMessageTypesEnum GetSTUNMessageTypeForId(int stunMessageTypeId) { - public static STUNMessageTypesEnum GetSTUNMessageTypeForId(int stunMessageTypeId) - { - return (STUNMessageTypesEnum)Enum.Parse(typeof(STUNMessageTypesEnum), stunMessageTypeId.ToString(), true); - } + return (STUNMessageTypesEnum)Enum.Parse(typeof(STUNMessageTypesEnum), stunMessageTypeId.ToString(), true); } +} - public class STUNHeader +public partial class STUNHeader +{ + public const byte STUN_INITIAL_BYTE_MASK = 0xc0; // Mask to check that the first two bits of the packet are 00. + public const ushort STUN_MESSAGE_CLASS_MASK = 0x0110; + public const int STUN_HEADER_LENGTH = 20; + public const uint MAGIC_COOKIE = 0x2112A442; + public const int TRANSACTION_ID_LENGTH = 12; + + public STUNMessageTypesEnum MessageType = STUNMessageTypesEnum.BindingRequest; + public STUNClassTypesEnum MessageClass { - public const byte STUN_INITIAL_BYTE_MASK = 0xc0; // Mask to check that the first two bits of the packet are 00. - public const ushort STUN_MESSAGE_CLASS_MASK = 0x0110; - public const int STUN_HEADER_LENGTH = 20; - public const UInt32 MAGIC_COOKIE = 0x2112A442; - public const int TRANSACTION_ID_LENGTH = 12; - - public STUNMessageTypesEnum MessageType = STUNMessageTypesEnum.BindingRequest; - public STUNClassTypesEnum MessageClass + get { - get - { - int @class = ((ushort)MessageType >> 8 & 0x01) * 2 | ((ushort)MessageType >> 4 & 0x01); - return (STUNClassTypesEnum)@class; - } + int @class = ((ushort)MessageType >> 8 & 0x01) * 2 | ((ushort)MessageType >> 4 & 0x01); + return (STUNClassTypesEnum)@class; } + } - public UInt16 MessageLength; - public byte[] TransactionId = new byte[TRANSACTION_ID_LENGTH]; + public ushort MessageLength; + public byte[] TransactionId = new byte[TRANSACTION_ID_LENGTH]; - public STUNHeader() - { } + public STUNHeader() + { } - public STUNHeader(STUNMessageTypesEnum messageType) - { - MessageType = messageType; - TransactionId = Encoding.ASCII.GetBytes(Guid.NewGuid().ToString().Substring(0, TRANSACTION_ID_LENGTH)); - } + public STUNHeader(STUNMessageTypesEnum messageType) + { + MessageType = messageType; + TransactionId = Encoding.ASCII.GetBytes(Guid.NewGuid().ToString().Substring(0, TRANSACTION_ID_LENGTH)); + } - public static STUNHeader ParseSTUNHeader(byte[] buffer) + public static STUNHeader? ParseSTUNHeader(ReadOnlySpan bufferSegment) + { + if ((bufferSegment[0] & STUN_INITIAL_BYTE_MASK) != 0) { - return ParseSTUNHeader(new ArraySegment(buffer, 0, buffer.Length)); + throw new SipSorceryException("The STUN header did not begin with 0x00."); } - public static STUNHeader ParseSTUNHeader(ArraySegment bufferSegment) + if (bufferSegment.Length >= STUN_HEADER_LENGTH) { - var startIndex = bufferSegment.Offset; - if ((bufferSegment.Array[startIndex] & STUN_INITIAL_BYTE_MASK) != 0) - { - throw new ApplicationException("The STUN header did not begin with 0x00."); - } - - if (bufferSegment != null && bufferSegment.Count > 0 && bufferSegment.Count >= STUN_HEADER_LENGTH) - { - STUNHeader stunHeader = new STUNHeader(); + var stunHeader = new STUNHeader(); - UInt16 stunTypeValue = BinaryPrimitives.ReadUInt16BigEndian(bufferSegment.Array.AsSpan(startIndex)); - UInt16 stunMessageLength = BinaryPrimitives.ReadUInt16BigEndian(bufferSegment.Array.AsSpan(startIndex + 2)); + var stunTypeValue = BinaryPrimitives.ReadUInt16BigEndian(bufferSegment); + var stunMessageLength = BinaryPrimitives.ReadUInt16BigEndian(bufferSegment.Slice(2)); - stunHeader.MessageType = STUNMessageTypes.GetSTUNMessageTypeForId(stunTypeValue); - stunHeader.MessageLength = stunMessageLength; - Buffer.BlockCopy(bufferSegment.Array, startIndex + 8, stunHeader.TransactionId, 0, TRANSACTION_ID_LENGTH); + stunHeader.MessageType = STUNMessageTypes.GetSTUNMessageTypeForId(stunTypeValue); + stunHeader.MessageLength = stunMessageLength; + stunHeader.TransactionId = bufferSegment.Slice(8, TRANSACTION_ID_LENGTH).ToArray(); - return stunHeader; - } - - return null; + return stunHeader; } + + return null; } } diff --git a/src/SIPSorcery/net/STUN/STUNListener.cs b/src/SIPSorcery/net/STUN/STUNListener.cs index 0d2d31d904..823f646b59 100644 --- a/src/SIPSorcery/net/STUN/STUNListener.cs +++ b/src/SIPSorcery/net/STUN/STUNListener.cs @@ -13,6 +13,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using System.Net.Sockets; diff --git a/src/SIPSorcery/net/STUN/STUNMessage.cs b/src/SIPSorcery/net/STUN/STUNMessage.cs index ebde015494..cde7760c6f 100644 --- a/src/SIPSorcery/net/STUN/STUNMessage.cs +++ b/src/SIPSorcery/net/STUN/STUNMessage.cs @@ -14,279 +14,360 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Net; using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public partial class STUNMessage { - public class STUNMessage - { - private const int FINGERPRINT_XOR = 0x5354554e; - private const int MESSAGE_INTEGRITY_ATTRIBUTE_HMAC_LENGTH = 20; - private const int FINGERPRINT_ATTRIBUTE_CRC32_LENGTH = 4; + private const int FINGERPRINT_XOR = 0x5354554e; + private const int MESSAGE_INTEGRITY_ATTRIBUTE_HMAC_LENGTH = 20; + private const int FINGERPRINT_ATTRIBUTE_CRC32_LENGTH = 4; - private static readonly ILogger logger = LogFactory.CreateLogger(); + private static readonly ILogger logger = LogFactory.CreateLogger(); - /// - /// For parsed STUN messages this indicates whether a valid fingerprint - /// as attached to the message. - /// - public bool isFingerprintValid { get; private set; } = false; + /// + /// For parsed STUN messages this indicates whether a valid fingerprint + /// as attached to the message. + /// + public bool isFingerprintValid { get; private set; } - /// - /// For received STUN messages this is the raw buffer. - /// - private byte[] _receivedBuffer; + /// + /// For received STUN messages this is the raw buffer. + /// + private Memory _receivedBuffer; - public STUNHeader Header = new STUNHeader(); - public List Attributes = new List(); + public STUNHeader Header { get; } + public List Attributes { get; private set; } = new List(); - public ushort PaddedSize + public ushort PaddedSize + { + get { - get - { - return (ushort)(STUNHeader.STUN_HEADER_LENGTH + Header.MessageLength); - } + Debug.Assert(Header is { }); + return (ushort)(STUNHeader.STUN_HEADER_LENGTH + Header.MessageLength); } + } - public STUNMessage() - { } + public STUNMessage(STUNHeader header) + { + Header = header; + } - public STUNMessage(STUNMessageTypesEnum stunMessageType) - { - Header = new STUNHeader(stunMessageType); - } + public STUNMessage(STUNMessageTypesEnum stunMessageType) + : this(new STUNHeader(stunMessageType)) + { + } - public void AddUsernameAttribute(string username) - { - byte[] usernameBytes = Encoding.UTF8.GetBytes(username); - Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Username, usernameBytes)); - } + public void AddUsernameAttribute(ReadOnlySpan username) + { + var usernameBytes = Encoding.UTF8.GetBytes(username); + AddUsernameAttribute(usernameBytes.AsMemory()); + } - public void AddNonceAttribute(string nonce) - { - byte[] nonceBytes = Encoding.UTF8.GetBytes(nonce); - Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Nonce, nonceBytes)); - } + public void AddUsernameAttribute(ReadOnlyMemory usernameBytes) + { + Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Username, usernameBytes)); + } - public void AddXORMappedAddressAttribute(IPAddress remoteAddress, int remotePort) - { - AddXORAddressAttribute(STUNAttributeTypesEnum.XORMappedAddress, remoteAddress, remotePort); - } + public void AddNonceAttribute(string nonce) + { + var nonceBytes = Encoding.UTF8.GetBytes(nonce); + Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Nonce, nonceBytes.AsMemory())); + } - public void AddXORPeerAddressAttribute(IPAddress remoteAddress, int remotePort) - { - AddXORAddressAttribute(STUNAttributeTypesEnum.XORPeerAddress, remoteAddress, remotePort); - } + public void AddXORMappedAddressAttribute(IPAddress remoteAddress, int remotePort) + { + AddXORAddressAttribute(STUNAttributeTypesEnum.XORMappedAddress, remoteAddress, remotePort); + } - public void AddXORAddressAttribute(STUNAttributeTypesEnum addressType, IPAddress remoteAddress, int remotePort) - { - STUNXORAddressAttribute xorAddressAttribute = new STUNXORAddressAttribute(addressType, remotePort, remoteAddress, Header.TransactionId); - Attributes.Add(xorAddressAttribute); - } + public void AddXORPeerAddressAttribute(IPAddress remoteAddress, int remotePort) + { + AddXORAddressAttribute(STUNAttributeTypesEnum.XORPeerAddress, remoteAddress, remotePort); + } + + public void AddXORAddressAttribute(STUNAttributeTypesEnum addressType, IPAddress remoteAddress, int remotePort) + { + var xorAddressAttribute = new STUNXORAddressAttribute(addressType, remotePort, remoteAddress, Header.TransactionId); + Attributes.Add(xorAddressAttribute); + } - public static STUNMessage ParseSTUNMessage(byte[] buffer, int bufferLength) + public static STUNMessage? ParseSTUNMessage(ReadOnlySpan buffer) + { + if (!buffer.IsEmpty) { - if (buffer != null && buffer.Length > 0 && buffer.Length >= bufferLength) - { - STUNMessage stunMessage = new STUNMessage(); - stunMessage._receivedBuffer = buffer.Take(bufferLength).ToArray(); - stunMessage.Header = STUNHeader.ParseSTUNHeader(buffer); + var header = STUNHeader.ParseSTUNHeader(buffer); + Debug.Assert(header is { }); + var stunMessage = new STUNMessage(header); + stunMessage._receivedBuffer = buffer.ToArray(); - if (stunMessage.Header.MessageLength > 0) - { - stunMessage.Attributes = STUNAttribute.ParseMessageAttributes(buffer, STUNHeader.STUN_HEADER_LENGTH, bufferLength, stunMessage.Header); - } + if (stunMessage.Header is { MessageLength: > 0 }) + { + STUNAttribute.ParseMessageAttributes(buffer.Slice(STUNHeader.STUN_HEADER_LENGTH), stunMessage.Header, stunMessage.Attributes); - if (stunMessage.Attributes.Count > 0 && stunMessage.Attributes.Last().AttributeType == STUNAttributeTypesEnum.FingerPrint) + if (stunMessage.Attributes is { Count: > 0 } && + stunMessage.Attributes[stunMessage.Attributes.Count - 1] is { AttributeType: STUNAttributeTypesEnum.FingerPrint } fingerprintAttribute) { // Check fingerprint. - var fingerprintAttribute = stunMessage.Attributes.Last(); - var input = buffer.Take(bufferLength - STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH - FINGERPRINT_ATTRIBUTE_CRC32_LENGTH).ToArray(); + var input = buffer.Slice(0, buffer.Length - STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH - FINGERPRINT_ATTRIBUTE_CRC32_LENGTH); - uint crc = Crc32.Compute(input) ^ FINGERPRINT_XOR; - var fingerPrint = new byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(fingerPrint, crc); + var crc = Crc32.Compute(input) ^ FINGERPRINT_XOR; + var fingerprint = BinaryPrimitives.ReadUInt32BigEndian(fingerprintAttribute.Value.Span); - //logger.LogDebug($"STUNMessage supplied fingerprint attribute: {fingerprintAttribute.Value.HexStr()}."); - //logger.LogDebug($"STUNMessage calculated fingerprint attribute: {fingerPrint.HexStr()}."); - - if (fingerprintAttribute.Value.HexStr() == fingerPrint.HexStr()) + if (crc == fingerprint) { stunMessage.isFingerprintValid = true; } } - - return stunMessage; } - return null; + return stunMessage; } - public byte[] ToByteBufferStringKey(string messageIntegrityKey, bool addFingerprint) - { - return ToByteBuffer(messageIntegrityKey.NotNullOrBlank() ? System.Text.Encoding.UTF8.GetBytes(messageIntegrityKey) : null, addFingerprint); - } + return null; + } - public byte[] ToByteBuffer(byte[] messageIntegrityKey, bool addFingerprint) + public int GetByteBufferSizeStringKey(string? messageIntegrityKey, bool addFingerprint) + { + if (string.IsNullOrWhiteSpace(messageIntegrityKey)) { - UInt16 attributesLength = 0; - foreach (STUNAttribute attribute in Attributes) - { - attributesLength += Convert.ToUInt16(STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + attribute.PaddedLength); - } + return GetByteBufferSize(ReadOnlySpan.Empty, addFingerprint); + } - if (messageIntegrityKey != null) - { - attributesLength += STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + MESSAGE_INTEGRITY_ATTRIBUTE_HMAC_LENGTH; - } + var maxByteCount = Encoding.UTF8.GetMaxByteCount(messageIntegrityKey.Length); + var rentedBuffer = ArrayPool.Shared.Rent(maxByteCount); - int messageLength = STUNHeader.STUN_HEADER_LENGTH + attributesLength; + try + { + var actualByteCount = Encoding.UTF8.GetBytes(messageIntegrityKey.AsSpan(), rentedBuffer); + var keySpan = new ReadOnlySpan(rentedBuffer, 0, actualByteCount); + return GetByteBufferSize(keySpan, addFingerprint); + } + finally + { + ArrayPool.Shared.Return(rentedBuffer); + } + } - byte[] buffer = new byte[messageLength]; + public void WriteToBufferStringKey(Span destination, string? messageIntegrityKey, bool addFingerprint) + { + ReadOnlySpan keySpan; - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0), (UInt16)Header.MessageType); - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2), attributesLength); - BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(4), STUNHeader.MAGIC_COOKIE); + if (messageIntegrityKey.NotNullOrBlank()) + { + var maxByteCount = Encoding.UTF8.GetMaxByteCount(messageIntegrityKey.Length); + var rentedBuffer = ArrayPool.Shared.Rent(maxByteCount); - Buffer.BlockCopy(Header.TransactionId, 0, buffer, 8, STUNHeader.TRANSACTION_ID_LENGTH); + try + { + var actualByteCount = Encoding.UTF8.GetBytes(messageIntegrityKey.AsSpan(), rentedBuffer); + keySpan = new ReadOnlySpan(rentedBuffer, 0, actualByteCount); - int attributeIndex = 20; - foreach (STUNAttribute attr in Attributes) + WriteToBuffer(destination, keySpan, addFingerprint); + } + finally { - attributeIndex += attr.ToByteBuffer(buffer, attributeIndex); + ArrayPool.Shared.Return(rentedBuffer); } + } + else + { + WriteToBuffer(destination, ReadOnlySpan.Empty, addFingerprint); + } + } - //logger.LogDebug($"Pre HMAC STUN message: {ByteBufferInfo.HexStr(buffer, attributeIndex)}"); + public int GetByteBufferSize(ReadOnlySpan messageIntegrityKey, bool addFingerprint) + { + var attributesLength = 0; - if (messageIntegrityKey != null) - { - var integrityAttibtue = new STUNAttribute(STUNAttributeTypesEnum.MessageIntegrity, new byte[MESSAGE_INTEGRITY_ATTRIBUTE_HMAC_LENGTH]); + foreach (var attribute in Attributes) + { + attributesLength += (ushort)(STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + attribute.PaddedLength); + } - HMACSHA1 hmacSHA = new HMACSHA1(messageIntegrityKey); - byte[] hmac = hmacSHA.ComputeHash(buffer, 0, attributeIndex); + if (!messageIntegrityKey.IsEmpty) + { + attributesLength += (ushort)(STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + MESSAGE_INTEGRITY_ATTRIBUTE_HMAC_LENGTH); + } - integrityAttibtue.Value = hmac; - attributeIndex += integrityAttibtue.ToByteBuffer(buffer, attributeIndex); - } + if (addFingerprint) + { + attributesLength += (ushort)(STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + FINGERPRINT_ATTRIBUTE_CRC32_LENGTH); + } - if (addFingerprint) - { - // The fingerprint attribute length has not been included in the length in the STUN header so adjust it now. - attributesLength += STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + FINGERPRINT_ATTRIBUTE_CRC32_LENGTH; - messageLength += STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + FINGERPRINT_ATTRIBUTE_CRC32_LENGTH; + return STUNHeader.STUN_HEADER_LENGTH + attributesLength; + } - BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2), attributesLength); + public void WriteToBuffer(Span buffer, ReadOnlySpan messageIntegrityKey, bool addFingerprint) + { + var attributesLength = (ushort)( + GetByteBufferSize(messageIntegrityKey, addFingerprint) + - STUNHeader.STUN_HEADER_LENGTH + - (addFingerprint ? STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + FINGERPRINT_ATTRIBUTE_CRC32_LENGTH : 0) + ); + + // Write STUN header + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(0, 2), (ushort)Header.MessageType); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2, 2), attributesLength); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4, 4), STUNHeader.MAGIC_COOKIE); + Header.TransactionId.CopyTo(buffer.Slice(8, STUNHeader.TRANSACTION_ID_LENGTH)); + + var attributeIndex = 20; + foreach (var attr in Attributes) + { + attributeIndex += attr.WriteBytes(buffer.Slice(attributeIndex)); + } + + if (!messageIntegrityKey.IsEmpty) + { + using var hmacSHA = new HMACSHA1(messageIntegrityKey.ToArray()); + var message = buffer.Slice(0, attributeIndex); + var hmac = hmacSHA.ComputeHash(message); + var integrityAttribute = new STUNAttribute(STUNAttributeTypesEnum.MessageIntegrity, hmac.AsMemory()); + attributeIndex += integrityAttribute.WriteBytes(buffer.Slice(attributeIndex)); + } - var fingerprintAttribute = new STUNAttribute(STUNAttributeTypesEnum.FingerPrint, new byte[FINGERPRINT_ATTRIBUTE_CRC32_LENGTH]); - uint crc = Crc32.Compute(buffer) ^ FINGERPRINT_XOR; - var fingerPrint = new byte[4]; - BinaryPrimitives.WriteUInt32BigEndian(fingerPrint, crc); - fingerprintAttribute.Value = fingerPrint; + if (addFingerprint) + { + // The fingerprint attribute length has not been included in the length in the STUN header so adjust it now. + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2, 2), attributesLength += STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + FINGERPRINT_ATTRIBUTE_CRC32_LENGTH); - Array.Resize(ref buffer, messageLength); - fingerprintAttribute.ToByteBuffer(buffer, attributeIndex); - } + var input = buffer.Slice(0, buffer.Length - STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH - FINGERPRINT_ATTRIBUTE_CRC32_LENGTH); + var crc = Crc32.Compute(input) ^ FINGERPRINT_XOR; + var fingerprint = new byte[FINGERPRINT_ATTRIBUTE_CRC32_LENGTH]; + BinaryPrimitives.WriteUInt32BigEndian(fingerprint, crc); - return buffer; + var fingerprintAttribute = new STUNAttribute(STUNAttributeTypesEnum.FingerPrint, fingerprint.AsMemory()); + fingerprintAttribute.WriteBytes(buffer.Slice(attributeIndex)); } + } + + public override string ToString() + { + var sb = new ValueStringBuilder(stackalloc char[256]); - public override string ToString() + try { - string messageDescr = $"STUN Message: {Header.MessageType.ToString()}, length={Header.MessageLength}"; + ToString(ref sb); - foreach (STUNAttribute attribute in Attributes) - { - messageDescr += $"\n {attribute.ToString()}"; - } + return sb.ToString(); + } + finally + { + sb.Dispose(); + } + } - return messageDescr; + internal void ToString(ref ValueStringBuilder sb) + { + Debug.Assert(Header is { }); + Debug.Assert(Attributes is { }); + + sb.Append("STUN Message: "); + sb.Append(Header.MessageType.ToStringFast()); + sb.Append('['); + sb.Append((int)Header.MessageType); + sb.Append("], length="); + sb.Append(Header.MessageLength); + sb.Append(", transactionID="); + sb.Append(Header.TransactionId); + + foreach (var attribute in Attributes) + { + sb.Append("\n "); + attribute.ToString(ref sb); } + } - /// - /// Returns the first attribute of the requested type, or null if none is - /// present. Plain foreach rather than LINQ so this stays cheap on the hot - /// path — STUN messages routinely carry only a handful of attributes. - /// - public STUNAttribute GetFirstAttribute(STUNAttributeTypesEnum attributeType) + /// + /// Returns the first attribute of the requested type, or null if none is + /// present. Plain foreach rather than LINQ so this stays cheap on the hot + /// path — STUN messages routinely carry only a handful of attributes. + /// + public STUNAttribute? GetFirstAttribute(STUNAttributeTypesEnum attributeType) + { + foreach (var attribute in Attributes) { - foreach (var attribute in Attributes) + if (attribute.AttributeType == attributeType) { - if (attribute.AttributeType == attributeType) - { - return attribute; - } + return attribute; } - return null; } + return null; + } - /// - /// Check that the message integrity attribute is correct. - /// - /// The message integrity key that was used to generate - /// the HMAC for the original message. - /// True if the HMAC of the STUN message is valid. False if not. - public bool CheckIntegrity(byte[] messageIntegrityKey) - { - bool isHmacValid = false; + /// + /// Check that the message integrity attribute is correct. + /// + /// The message integrity key that was used to generate + /// the HMAC for the original message. + /// True if the fingerprint and HMAC of the STUN message are valid. False if not. + public bool CheckIntegrity(byte[] messageIntegrityKey) + { + // Find the MESSAGE-INTEGRITY attribute. It can be either: + // - The last attribute (no FINGERPRINT present) + // - The second-to-last attribute (FINGERPRINT is last) + STUNAttribute messageIntegrityAttribute; + var hasFingerprint = false; - // Find the MESSAGE-INTEGRITY attribute. It can be either: - // - The last attribute (no FINGERPRINT present) - // - The second-to-last attribute (FINGERPRINT is last) - STUNAttribute messageIntegrityAttribute = null; - bool hasFingerprint = false; + if (Attributes.Count > 1 && Attributes[Attributes.Count - 1].AttributeType == STUNAttributeTypesEnum.MessageIntegrity) + { + messageIntegrityAttribute = Attributes[Attributes.Count - 1]; + } + else if (Attributes.Count >= 2 && Attributes[Attributes.Count - 1].AttributeType == STUNAttributeTypesEnum.FingerPrint + && Attributes[Attributes.Count - 2].AttributeType == STUNAttributeTypesEnum.MessageIntegrity) + { + messageIntegrityAttribute = Attributes[Attributes.Count - 2]; + hasFingerprint = true; + } + else + { + return false; + } - if (Attributes.Count >= 1 && Attributes[Attributes.Count - 1].AttributeType == STUNAttributeTypesEnum.MessageIntegrity) - { - messageIntegrityAttribute = Attributes[Attributes.Count - 1]; - } - else if (Attributes.Count >= 2 && Attributes[Attributes.Count - 1].AttributeType == STUNAttributeTypesEnum.FingerPrint - && Attributes[Attributes.Count - 2].AttributeType == STUNAttributeTypesEnum.MessageIntegrity) - { - messageIntegrityAttribute = Attributes[Attributes.Count - 2]; - hasFingerprint = true; - } + Debug.Assert(messageIntegrityAttribute is not null); - if (messageIntegrityAttribute != null && _receivedBuffer != null) - { - // When FINGERPRINT is present and valid, verify it first. - if (hasFingerprint && !isFingerprintValid) - { - return false; - } + if (_receivedBuffer.IsEmpty) + { + return false; + } - // Calculate the pre-image length: everything before the MESSAGE-INTEGRITY attribute. - int fingerprintSize = hasFingerprint - ? STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + FINGERPRINT_ATTRIBUTE_CRC32_LENGTH - : 0; + // When FINGERPRINT is present and valid, verify it first. + if (hasFingerprint && !isFingerprintValid) + { + return false; + } - int preImageLength = _receivedBuffer.Length - - fingerprintSize - - STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH - - MESSAGE_INTEGRITY_ATTRIBUTE_HMAC_LENGTH; + // Calculate the pre-image length: everything before the MESSAGE-INTEGRITY attribute. + int fingerprintSize = hasFingerprint + ? STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + FINGERPRINT_ATTRIBUTE_CRC32_LENGTH + : 0; - // Per RFC 5389 Section 15.4: the message length field must be adjusted to - // include MESSAGE-INTEGRITY but exclude FINGERPRINT (if present). - ushort length = hasFingerprint - ? (ushort)(Header.MessageLength - STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH - FINGERPRINT_ATTRIBUTE_CRC32_LENGTH) - : Header.MessageLength; + int preImageLength = _receivedBuffer.Length + - fingerprintSize + - STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH + - MESSAGE_INTEGRITY_ATTRIBUTE_HMAC_LENGTH; - BinaryPrimitives.WriteUInt16BigEndian(_receivedBuffer.AsSpan(2), length); + // Per RFC 5389 Section 15.4: the message length field must be adjusted to + // include MESSAGE-INTEGRITY but exclude FINGERPRINT (if present). + var length = hasFingerprint + ? (ushort)(Header.MessageLength - STUNAttribute.STUNATTRIBUTE_HEADER_LENGTH - FINGERPRINT_ATTRIBUTE_CRC32_LENGTH) + : Header.MessageLength; - HMACSHA1 hmacSHA = new HMACSHA1(messageIntegrityKey); - byte[] calculatedHmac = hmacSHA.ComputeHash(_receivedBuffer, 0, preImageLength); + BinaryPrimitives.WriteUInt16BigEndian(_receivedBuffer.Span.Slice(2, 2), length); - isHmacValid = messageIntegrityAttribute.Value.HexStr() == calculatedHmac.HexStr(); - } + HMACSHA1 hmacSHA = new HMACSHA1(messageIntegrityKey); + byte[] calculatedHmac = hmacSHA.ComputeHash(_receivedBuffer.Slice(0, preImageLength)); - return isHmacValid; - } + return messageIntegrityAttribute.Value.Span.SequenceEqual(calculatedHmac); } } diff --git a/src/SIPSorcery/net/STUN/STUNServer.cs b/src/SIPSorcery/net/STUN/STUNServer.cs index 0156f145d5..bdef5f842f 100644 --- a/src/SIPSorcery/net/STUN/STUNServer.cs +++ b/src/SIPSorcery/net/STUN/STUNServer.cs @@ -13,9 +13,12 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using System.Net.Sockets; +using CommunityToolkit.HighPerformance.Buffers; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; @@ -73,13 +76,14 @@ public void STUNPrimaryReceived(IPEndPoint localEndPoint, IPEndPoint receivedEnd //Console.WriteLine("\n=> received from " + IPSocketAddress.GetSocketString(receivedEndPoint) + " on " + IPSocketAddress.GetSocketString(receivedOnEndPoint)); //Console.WriteLine(Utility.PrintBuffer(buffer)); - STUNMessage stunRequest = STUNMessage.ParseSTUNMessage(buffer, bufferLength); + STUNMessage stunRequest = STUNMessage.ParseSTUNMessage(buffer.AsSpan(0, bufferLength)); //Console.WriteLine(stunRequest.ToString()); FireSTUNPrimaryRequestInTraceEvent(localEndPoint, receivedEndPoint, stunRequest); STUNMessage stunResponse = GetResponse(receivedEndPoint, stunRequest, true); - byte[] stunResponseBuffer = stunResponse.ToByteBuffer(null, false); + byte[] stunResponseBuffer = new byte[stunResponse.GetByteBufferSize(null, false)]; + stunResponse.WriteToBuffer(stunResponseBuffer, null, false); bool changeAddress = false; bool changePort = false; @@ -142,13 +146,14 @@ public void STUNSecondaryReceived(IPEndPoint localEndPoint, IPEndPoint receivedE //Console.WriteLine("\n=> received from " + IPSocketAddress.GetSocketString(receivedEndPoint) + " on " + IPSocketAddress.GetSocketString(receivedOnEndPoint)); //Console.WriteLine(Utility.PrintBuffer(buffer)); - STUNMessage stunRequest = STUNMessage.ParseSTUNMessage(buffer, bufferLength); + STUNMessage stunRequest = STUNMessage.ParseSTUNMessage(buffer.AsSpan(0, bufferLength)); //Console.WriteLine(stunRequest.ToString()); FireSTUNSecondaryRequestInTraceEvent(localEndPoint, receivedEndPoint, stunRequest); STUNMessage stunResponse = GetResponse(receivedEndPoint, stunRequest, true); - byte[] stunResponseBuffer = stunResponse.ToByteBuffer(null, false); + byte[] stunResponseBuffer = new byte[stunResponse.GetByteBufferSize(null, false)]; + stunResponse.WriteToBuffer(stunResponseBuffer, null, false); bool changeAddress = false; bool changePort = false; @@ -208,9 +213,11 @@ private STUNMessage GetResponse(IPEndPoint receivedEndPoint, STUNMessage stunReq { if (stunRequest.Header.MessageType == STUNMessageTypesEnum.BindingRequest) { - STUNMessage stunResponse = new STUNMessage(); - stunResponse.Header.MessageType = STUNMessageTypesEnum.BindingSuccessResponse; - stunResponse.Header.TransactionId = stunRequest.Header.TransactionId; + var header = new STUNHeader(); + header.MessageType = STUNMessageTypesEnum.BindingSuccessResponse; + header.TransactionId = stunRequest.Header.TransactionId; + + STUNMessage stunResponse = new STUNMessage(header); // Add MappedAddress attribute to indicate the socket the request was received from. STUNAddressAttribute mappedAddressAtt = new STUNAddressAttribute(STUNAttributeTypesEnum.MappedAddress, receivedEndPoint.Port, receivedEndPoint.Address); diff --git a/src/SIPSorcery/net/STUN/STUNUri.cs b/src/SIPSorcery/net/STUN/STUNUri.cs index 10f986f9b9..d9ffbc3788 100644 --- a/src/SIPSorcery/net/STUN/STUNUri.cs +++ b/src/SIPSorcery/net/STUN/STUNUri.cs @@ -18,332 +18,358 @@ using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Net.Sockets; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public static class STUNConstants { - public class STUNConstants - { - public const int DEFAULT_STUN_PORT = 3478; - public const int DEFAULT_STUN_TLS_PORT = 5349; - public const int DEFAULT_TURN_PORT = 3478; - public const int DEFAULT_TURN_TLS_PORT = 5349; + public const int DEFAULT_STUN_PORT = 3478; + public const int DEFAULT_STUN_TLS_PORT = 5349; + public const int DEFAULT_TURN_PORT = 3478; + public const int DEFAULT_TURN_TLS_PORT = 5349; + + public static int GetPortForScheme(STUNSchemesEnum scheme) + => scheme switch + { + STUNSchemesEnum.stun or STUNSchemesEnum.turn => DEFAULT_TURN_PORT, + STUNSchemesEnum.stuns or STUNSchemesEnum.turns => DEFAULT_TURN_TLS_PORT, + _ => throw new SipSorceryException("STUN or TURN scheme not recognised in STUNConstants.GetPortForScheme."), + }; + + public static STUNProtocolsEnum GetTransportForScheme(STUNSchemesEnum scheme) + => scheme switch + { + STUNSchemesEnum.stun or STUNSchemesEnum.turn => STUNProtocolsEnum.udp, + STUNSchemesEnum.stuns or STUNSchemesEnum.turns => STUNProtocolsEnum.tls, + _ => throw new SipSorceryException("STUN or TURN scheme not recognised in STUNConstants.GetTransportForScheme."), + }; +} + +public enum STUNSchemesEnum +{ + stun = 0, + stuns = 1, + turn = 2, + turns = 3 +} + +/// +/// A list of the transport layer protocols that are supported by STUNand TURN (the network layers +/// supported are IPv4 mad IPv6). +/// +public enum STUNProtocolsEnum +{ + /// + /// User Datagram Protocol. + /// + udp = 1, + /// . + /// Transmission Control Protocol + /// + tcp = 2, + /// + /// Transport Layer Security. + /// + tls = 3, + /// + /// Transport Layer Security over UDP. + /// + dtls = 4, +} - public static int GetPortForScheme(STUNSchemesEnum scheme) +public sealed class STUNUri : IEquatable +{ + public const string SCHEME_TRANSPORT_TCP = "transport=tcp"; + public const string SCHEME_TRANSPORT_TLS = "transport=tls"; + + public static readonly string SCHEME_TRANSPORT_SEPARATOR = "transport="; + public const char SCHEME_ADDR_SEPARATOR = ':'; + public const int SCHEME_MAX_LENGTH = 5; + + public const STUNSchemesEnum DefaultSTUNScheme = STUNSchemesEnum.stun; + + public STUNProtocolsEnum Transport { get; } = STUNProtocolsEnum.udp; + public STUNSchemesEnum Scheme { get; } = DefaultSTUNScheme; + + public string Host { get; } + public int Port { get; } + + /// + /// If the port is specified in a URI it affects the way a DNS lookup occurs. + /// An explicit port means to lookup the A or AAAA record directly without + /// checking for SRV records. + /// + public bool ExplicitPort { get; } + + /// + /// The network protocol for this URI type. + /// + public ProtocolType Protocol + { + get { - switch (scheme) + if (Transport is STUNProtocolsEnum.tcp or STUNProtocolsEnum.tls) + { + return ProtocolType.Tcp; + } + else { - case STUNSchemesEnum.stun: - return DEFAULT_STUN_PORT; - case STUNSchemesEnum.stuns: - return DEFAULT_STUN_TLS_PORT; - case STUNSchemesEnum.turn: - return DEFAULT_TURN_PORT; - case STUNSchemesEnum.turns: - return DEFAULT_TURN_TLS_PORT; - default: - throw new ApplicationException("STUN or TURN scheme not recognised in STUNConstants.GetPortForScheme."); + return ProtocolType.Udp; } } } - public enum STUNSchemesEnum + [EditorBrowsable(EditorBrowsableState.Advanced)] + public STUNUri(STUNSchemesEnum scheme, string host, int port) { - stun = 0, - stuns = 1, - turn = 2, - turns = 3 + Scheme = scheme; + Host = host; + Port = port; } - /// - /// A list of the transport layer protocols that are supported by STUNand TURN (the network layers - /// supported are IPv4 mad IPv6). - /// - public enum STUNProtocolsEnum + public STUNUri(STUNSchemesEnum scheme, string host, int port = STUNConstants.DEFAULT_STUN_PORT, STUNProtocolsEnum transport = STUNProtocolsEnum.udp, bool explicitPort = false) { - /// - /// User Datagram Protocol. - /// - udp = 1, - /// . - /// Transmission Control Protocol - /// - tcp = 2, - /// - /// Transport Layer Security. - /// - tls = 3, - /// - /// Transport Layer Security over UDP. - /// - dtls = 4, + Scheme = scheme; + Host = host; + Port = port; + Transport = transport; + ExplicitPort = explicitPort; } - public sealed class STUNUri : IEquatable + public static STUNUri ParseSTUNUri(string uriStr) { - public const string SCHEME_TRANSPORT_TCP = "transport=tcp"; - public const string SCHEME_TRANSPORT_TLS = "transport=tls"; - - public static readonly string SCHEME_TRANSPORT_SEPARATOR = "?transport="; - public const char SCHEME_ADDR_SEPARATOR = ':'; - public const int SCHEME_MAX_LENGTH = 5; + if (!TryParse(uriStr, out var uri)) + { + throw new FormatException($"A STUN URI cannot be parsed from an empty 'uriStr'."); + } - public const STUNSchemesEnum DefaultSTUNScheme = STUNSchemesEnum.stun; + return uri; + } - public STUNProtocolsEnum Transport { get; } = STUNProtocolsEnum.udp; - public STUNSchemesEnum Scheme { get; } = DefaultSTUNScheme; + public static bool TryParse(string uriStr, [NotNullWhen(true)] out STUNUri? uri) + { + if (string.IsNullOrEmpty(uriStr)) + { + uri = null; + return false; + } - public string Host { get; } - public int Port { get; } + return TryParse(uriStr.AsSpan(), out uri); + } - /// - /// If the port is specified in a URI it affects the way a DNS lookup occurs. - /// An explicit port means to lookup the A or AAAA record directly without - /// checking for SRV records. - /// - public bool ExplicitPort { get; } + public static bool TryParse(ReadOnlySpan uriSpan, [NotNullWhen(true)] out STUNUri? uri) + { + uri = null; - /// - /// The network protocol for this URI type. - /// - public ProtocolType Protocol + uriSpan = uriSpan.Trim(); + ReadOnlySpan querySpan; + if ((uriSpan.IndexOf('?') is { } queryStart) && queryStart >= 0) { - get - { - if (Transport == STUNProtocolsEnum.tcp || Transport == STUNProtocolsEnum.tls) - { - return ProtocolType.Tcp; - } - else - { - return ProtocolType.Udp; - } - } + querySpan = uriSpan.Slice(queryStart + 1); + uriSpan = uriSpan.Slice(0, queryStart); + } + else + { + querySpan = ReadOnlySpan.Empty; } - private STUNUri() - { } + var scheme = DefaultSTUNScheme; + + // Handle scheme parsing - [EditorBrowsable(EditorBrowsableState.Advanced)] - public STUNUri(STUNSchemesEnum scheme, string host, int port) + if (uriSpan.StartsWith("stun:".AsSpan(), StringComparison.OrdinalIgnoreCase)) { - Scheme = scheme; - Host = host; - Port = port; + scheme = STUNSchemesEnum.stun; + uriSpan = uriSpan.Slice(5); } - - public STUNUri(STUNSchemesEnum scheme, string host, int port = STUNConstants.DEFAULT_STUN_PORT, STUNProtocolsEnum transport = STUNProtocolsEnum.udp, bool explicitPort = false) + else if (uriSpan.StartsWith("stuns:".AsSpan(), StringComparison.OrdinalIgnoreCase)) { - Scheme = scheme; - Host = host; - Port = port; - Transport = transport; - ExplicitPort = explicitPort; + scheme = STUNSchemesEnum.stuns; + uriSpan = uriSpan.Slice(6); } - - public static STUNUri ParseSTUNUri(string uriStr) + else if (uriSpan.StartsWith("turn:".AsSpan(), StringComparison.OrdinalIgnoreCase)) { - if (!TryParse(uriStr, out var uri)) - { - throw new ApplicationException("A STUN URI cannot be parsed from an empty string."); - } - - return uri; + scheme = STUNSchemesEnum.turn; + uriSpan = uriSpan.Slice(5); + } + else if (uriSpan.StartsWith("turns:".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + scheme = STUNSchemesEnum.turns; + uriSpan = uriSpan.Slice(6); } - public static bool TryParse(string uriStr, out STUNUri uri) + if (uriSpan.IsEmpty) { - uri = null; + return false; + } - if (string.IsNullOrEmpty(uriStr)) - { - return false; - } + var explicitPort = false; + int port; + string host; - ReadOnlySpan uriSpan = uriStr.AsSpan(); - STUNProtocolsEnum transport = STUNProtocolsEnum.udp; - bool explicitTransport = false; + var lastColonPos = uriSpan.LastIndexOf(':'); + if (lastColonPos != -1) + { + explicitPort = true; - // Handle transport protocol - int transportIndex = uriSpan.IndexOf('?'); - if (transportIndex >= 0 && uriSpan.Slice(transportIndex, SCHEME_TRANSPORT_SEPARATOR.Length).SequenceEqual(SCHEME_TRANSPORT_SEPARATOR.AsSpan())) + if (IPSocket.TryParseIPEndPoint(uriSpan, out var ipEndPoint)) { - explicitTransport = true; - var protocolSpan = uriSpan.Slice(transportIndex + SCHEME_TRANSPORT_SEPARATOR.Length).Trim(); -#if NET6_0_OR_GREATER - if (!protocolSpan.IsEmpty && !Enum.TryParse(protocolSpan, true, out transport)) -#else - if (!protocolSpan.IsEmpty && !Enum.TryParse(protocolSpan.ToString(), true, out transport)) -#endif + if (ipEndPoint.AddressFamily == AddressFamily.InterNetworkV6) { - transport = STUNProtocolsEnum.udp; + host = $"[{ipEndPoint.Address}]"; } - uriSpan = uriSpan.Slice(0, transportIndex); - } - - uriSpan = uriSpan.Trim(); - var scheme = DefaultSTUNScheme; - - // Handle scheme parsing - if (uriSpan.Length > SCHEME_MAX_LENGTH + 2) - { - ReadOnlySpan schemeSpan = uriSpan.Slice(0, SCHEME_MAX_LENGTH + 1); - int colonPosn = schemeSpan.IndexOf(SCHEME_ADDR_SEPARATOR); - - if (colonPosn >= 0) + else { -#if NET6_0_OR_GREATER - if (!Enum.TryParse(schemeSpan.Slice(0, colonPosn), true, out scheme)) -#else - if (!Enum.TryParse(schemeSpan.Slice(0, colonPosn).ToString(), true, out scheme)) -#endif - { - scheme = DefaultSTUNScheme; - } - uriSpan = uriSpan.Slice(colonPosn + 1); + host = ipEndPoint.Address.ToString(); } - } - // RFC 7064 (STUN) / RFC 7065 (TURN): when no transport is specified the default depends on - // the scheme. stun/turn default to UDP; the secure schemes stuns/turns default to TCP - // (TLS runs over TCP). Without this a "turns:host:443" URI would default to UDP and the - // TURN allocation would be attempted over UDP against a TLS/TCP listener and fail. - if (!explicitTransport && (scheme == STUNSchemesEnum.turns || scheme == STUNSchemesEnum.stuns)) - { - transport = STUNProtocolsEnum.tcp; + port = ipEndPoint.Port; } - - var explicitPort = false; - int port; - string host; - - int lastColonPos = uriSpan.LastIndexOf(':'); - if (lastColonPos != -1) + else { - explicitPort = true; - - if (IPSocket.TryParseIPEndPoint(uriSpan, out var ipEndPoint)) + if ( + !int.TryParse(uriSpan.Slice(lastColonPos + 1), out port) + || port <= 0 || port > 65535) { - if (ipEndPoint.AddressFamily == AddressFamily.InterNetworkV6) - { - host = $"[{ipEndPoint.Address}]"; - } - else - { - host = ipEndPoint.Address.ToString(); - } - port = ipEndPoint.Port; + return false; } - else + + var hostSpan = uriSpan.Slice(0, lastColonPos); + + if (hostSpan.IsEmpty || hostSpan.IndexOfAny(SearchValues.InvalidHostNameChars) >= 0) { - host = uriSpan.Slice(0, lastColonPos).ToString(); -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - if (!int.TryParse(uriSpan.Slice(lastColonPos + 1), out port)) -#else - if (!int.TryParse(uriSpan.Slice(lastColonPos + 1).ToString(), out port)) -#endif - { - port = STUNConstants.GetPortForScheme(scheme); - } + return false; } + + host = hostSpan.ToLowerString(); } - else + } + else + { + if (uriSpan.IsEmpty || uriSpan.IndexOfAny(SearchValues.InvalidHostNameChars) >= 0) { - host = uriSpan.ToString(); - port = STUNConstants.GetPortForScheme(scheme); + return false; } - uri = new STUNUri(scheme, host, port: port, transport: transport, explicitPort: explicitPort); - return true; + host = uriSpan.ToLowerString(); + + port = STUNConstants.GetPortForScheme(scheme); + } + + var transport = STUNConstants.GetTransportForScheme(scheme); + if (scheme is STUNSchemesEnum.stuns or STUNSchemesEnum.turns) + { + transport = STUNProtocolsEnum.tcp; } - public override string ToString() + // Handle transport protocol + if (!querySpan.IsEmpty) { - if ((Scheme == STUNSchemesEnum.stun && Port == STUNConstants.DEFAULT_STUN_PORT) || - (Scheme == STUNSchemesEnum.turn && Port == STUNConstants.DEFAULT_TURN_PORT) || - (Scheme == STUNSchemesEnum.stuns && Port == STUNConstants.DEFAULT_STUN_TLS_PORT) || - (Scheme == STUNSchemesEnum.turns && Port == STUNConstants.DEFAULT_TURN_TLS_PORT)) + if (querySpan.StartsWith(SCHEME_TRANSPORT_SEPARATOR.AsSpan(), StringComparison.OrdinalIgnoreCase)) { - if (Protocol != ProtocolType.Udp) + var protocolSpan = querySpan.Slice(SCHEME_TRANSPORT_SEPARATOR.Length).Trim(); + if (protocolSpan.IsEmpty || !STUNProtocolsEnumExtensions.TryParse(protocolSpan, out transport, true)) { - return $"{Scheme}{SCHEME_ADDR_SEPARATOR}{Host}?transport={Protocol.ToString().ToLower()}"; - } - else - { - return $"{Scheme}{SCHEME_ADDR_SEPARATOR}{Host}"; + return false; } } else { - if (Protocol != ProtocolType.Udp) - { - return $"{Scheme}{SCHEME_ADDR_SEPARATOR}{Host}:{Port}?transport={Protocol.ToString().ToLower()}"; - } - else - { - return $"{Scheme}{SCHEME_ADDR_SEPARATOR}{Host}:{Port}"; - } + return false; } } - public static bool AreEqual(STUNUri uri1, STUNUri uri2) - { - return uri1 == uri2; - } + uri = new STUNUri(scheme, host, port: port, transport: transport, explicitPort: explicitPort); + return true; + } + + public override string ToString() + { + using var sb = new ValueStringBuilder(stackalloc char[256]); + + sb.Append(Scheme.ToStringFast()); + sb.Append(SCHEME_ADDR_SEPARATOR); + sb.Append(Host); - public bool Equals(STUNUri other) + if ((Scheme == STUNSchemesEnum.stun && Port != STUNConstants.DEFAULT_STUN_PORT) || + (Scheme == STUNSchemesEnum.turn && Port != STUNConstants.DEFAULT_TURN_PORT) || + (Scheme == STUNSchemesEnum.stuns && Port != STUNConstants.DEFAULT_STUN_TLS_PORT) || + (Scheme == STUNSchemesEnum.turns && Port != STUNConstants.DEFAULT_TURN_TLS_PORT)) { - return (this == other); + sb.Append(SCHEME_ADDR_SEPARATOR); + sb.Append(Port); } - public override bool Equals(object obj) + if (((Scheme is STUNSchemesEnum.stun or STUNSchemesEnum.turn) && Transport != STUNProtocolsEnum.udp) || + ((Scheme is STUNSchemesEnum.stuns or STUNSchemesEnum.turns) && Transport != STUNProtocolsEnum.tls)) { - return Equals(this, (STUNUri)obj); + sb.Append('?'); + sb.Append(SCHEME_TRANSPORT_SEPARATOR); + sb.Append(Transport.ToStringFast()); } - public static bool operator ==(STUNUri uri1, STUNUri uri2) - { - if (object.ReferenceEquals(uri1, uri2)) - { - return true; - } - else if (uri1 is null || uri2 is null) - { - return false; - } - else if (uri1.Host == null || uri2.Host == null) - { - return false; - } - else if (uri1.Scheme != uri2.Scheme) - { - return false; - } - else if (uri1.Transport != uri2.Transport) - { - return false; - } - else if (uri1.Port != uri2.Port) - { - return false; - } - else if (uri1.ExplicitPort != uri2.ExplicitPort) - { - return false; - } + return sb.ToString(); + } + + public static bool AreEqual(STUNUri uri1, STUNUri uri2) + { + return uri1 == uri2; + } + + public bool Equals(STUNUri? other) + { + return (this == other); + } + + public override bool Equals(object? obj) + { + return Equals(this, (STUNUri?)obj); + } + public static bool operator ==(STUNUri? uri1, STUNUri? uri2) + { + if (object.ReferenceEquals(uri1, uri2)) + { return true; } - - public static bool operator !=(STUNUri x, STUNUri y) + else if (uri1 is null || uri2 is null) { - return !(x == y); + return false; } - - public override int GetHashCode() + else if (uri1.Host is null || uri2.Host is null) + { + return false; + } + else if (uri1.Scheme != uri2.Scheme) + { + return false; + } + else if (uri1.Transport != uri2.Transport) + { + return false; + } + else if (uri1.Port != uri2.Port) + { + return false; + } + else if (uri1.ExplicitPort != uri2.ExplicitPort) { - return HashCode.Combine(Scheme, Transport, Host, Port, ExplicitPort); + return false; } + + return true; + } + + public static bool operator !=(STUNUri? x, STUNUri? y) + { + return !(x == y); + } + + public override int GetHashCode() + { + return HashCode.Combine(Scheme, Transport, Host, Port, ExplicitPort); } } diff --git a/src/SIPSorcery/net/STUN/StunMessageExtensions.cs b/src/SIPSorcery/net/STUN/StunMessageExtensions.cs new file mode 100644 index 0000000000..af60c5d17d --- /dev/null +++ b/src/SIPSorcery/net/STUN/StunMessageExtensions.cs @@ -0,0 +1,25 @@ +using SIPSorcery.Net; + +namespace SIPSorcery.Net; + +internal static class StunMessageExtensions +{ + public static STUNAttribute? FirstOrDefaultAttribute(this STUNMessage stunMessage, STUNAttributeTypesEnum attributeType) + { + foreach (var attribute in stunMessage.Attributes) + { + if (attribute.AttributeType == attributeType) + { + return attribute; + } + } + + return null; + } + + public static TAttribute? FirstOrDefaultAttribute(this STUNMessage stunMessage, STUNAttributeTypesEnum attributeType) + where TAttribute : STUNAttribute + { + return stunMessage.FirstOrDefaultAttribute(attributeType) as TAttribute; + } +} diff --git a/src/SIPSorcery/net/TURN/IceTcpReceiver.cs b/src/SIPSorcery/net/TURN/IceTcpReceiver.cs index 3359ab180a..35dd3962ee 100644 --- a/src/SIPSorcery/net/TURN/IceTcpReceiver.cs +++ b/src/SIPSorcery/net/TURN/IceTcpReceiver.cs @@ -14,6 +14,8 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using System.Net.Sockets; diff --git a/src/SIPSorcery/net/TURN/TurnClient.cs b/src/SIPSorcery/net/TURN/TurnClient.cs index a5edb8990e..b09cf9aa0d 100644 --- a/src/SIPSorcery/net/TURN/TurnClient.cs +++ b/src/SIPSorcery/net/TURN/TurnClient.cs @@ -1,4 +1,4 @@ - //----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: TurnClient.cs // // Description: TURN client implementation. Initial use case is to allocate a relay @@ -15,13 +15,16 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; +using System.Buffers.Binary; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Buffers.Binary; using Microsoft.Extensions.Logging; using Org.BouncyCastle.Crypto.Digests; using SIPSorcery.Sys; @@ -232,12 +235,11 @@ private void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoint _iceServer.ErrorResponseCount++; - var errCodeAttribute = stunResponse.GetFirstAttribute(STUNAttributeTypesEnum.ErrorCode) as STUNErrorCodeAttribute; - if (errCodeAttribute != null) + if (stunResponse.GetFirstAttribute(STUNAttributeTypesEnum.ErrorCode) is STUNErrorCodeAttribute errCodeAttribute) { var alternateServerAttribute = stunResponse.GetFirstAttribute(STUNAttributeTypesEnum.AlternateServer) as STUNAddressAttribute; - if (errCodeAttribute.ErrorCode == IceServer.STUN_UNAUTHORISED_ERROR_CODE || errCodeAttribute.ErrorCode == IceServer.STUN_STALE_NONCE_ERROR_CODE) + if (errCodeAttribute.ErrorCode is IceServer.STUN_UNAUTHORISED_ERROR_CODE or IceServer.STUN_STALE_NONCE_ERROR_CODE) { logger.LogWarning("TURN client error response code {errorCode} for an Allocate request to {Uri} from {remoteEP}.", errCodeAttribute.ErrorCode, _iceServer.Uri, remoteEndPoint); @@ -281,7 +283,7 @@ private void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoint if (permissionLifetime != null) { - permissionDuration = TimeSpan.FromSeconds(BinaryPrimitives.ReadUInt32BigEndian(permissionLifetime.Value)); + permissionDuration = TimeSpan.FromSeconds(BinaryPrimitives.ReadUInt32BigEndian(permissionLifetime.Value.Span)); logger.LogDebug("TURN permission lifetime attribute value {lifetimeSeconds}s.", permissionDuration.TotalSeconds); } @@ -308,10 +310,9 @@ private void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoint _iceServer.ErrorResponseCount++; - var errCodeAttribute = stunResponse.GetFirstAttribute(STUNAttributeTypesEnum.ErrorCode) as STUNErrorCodeAttribute; - if (errCodeAttribute != null) + if (stunResponse.GetFirstAttribute(STUNAttributeTypesEnum.ErrorCode) is STUNErrorCodeAttribute errCodeAttribute) { - if (errCodeAttribute.ErrorCode == IceServer.STUN_UNAUTHORISED_ERROR_CODE || errCodeAttribute.ErrorCode == IceServer.STUN_STALE_NONCE_ERROR_CODE) + if (errCodeAttribute.ErrorCode is IceServer.STUN_UNAUTHORISED_ERROR_CODE or IceServer.STUN_STALE_NONCE_ERROR_CODE) { logger.LogWarning("TURN client error response code {errorCode} for a Create Permission request to {Uri} from {remoteEP}.", errCodeAttribute.ErrorCode, _iceServer.Uri, remoteEndPoint); @@ -339,7 +340,7 @@ private void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoint { logger.LogInformation("TURN client received a success response for a Refresh request to {Uri} from {remoteEP}.", _iceServer.Uri, remoteEndPoint); - ScheduleAllocateRefresh(stunResponse.GetFirstAttribute(STUNAttributeTypesEnum.Lifetime)); + ScheduleAllocateRefresh(stunResponse.Attributes.FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.Lifetime)); } else { @@ -359,7 +360,7 @@ private void ScheduleAllocateRefresh(STUNAttribute lifetimeAttribute) if (lifetimeAttribute != null) { - var lifetimeSpan = TimeSpan.FromSeconds(BinaryPrimitives.ReadUInt32BigEndian(lifetimeAttribute.Value)); + var lifetimeSpan = TimeSpan.FromSeconds(BinaryPrimitives.ReadUInt32BigEndian(lifetimeAttribute.Value.Span)); logger.LogDebug("TURN allocate lifetime attribute value {lifetimeSeconds}s.", lifetimeSpan.TotalSeconds); @@ -395,10 +396,10 @@ private void SetAuthenticationFields(STUNMessage stunResponse) { // Set the authentication properties authenticate. var nonceAttribute = stunResponse.GetFirstAttribute(STUNAttributeTypesEnum.Nonce); - _iceServer.Nonce = nonceAttribute?.Value; + _iceServer.Nonce = nonceAttribute?.Value ?? default; var realmAttribute = stunResponse.GetFirstAttribute(STUNAttributeTypesEnum.Realm); - _iceServer.Realm = realmAttribute?.Value; + _iceServer.Realm = realmAttribute?.Value ?? default; } /// @@ -427,13 +428,14 @@ private SocketError SendTurnAllocateRequest(IceServer iceServer) byte[] allocateReqBytes = null; - if (iceServer.Nonce != null && iceServer.Realm != null && iceServer._username != null && iceServer._password != null) + if (iceServer.Nonce.IsEmpty && iceServer.Realm.IsEmpty && iceServer.Username.IsEmpty && iceServer.Password.IsEmpty) { - allocateReqBytes = GetAuthenticatedStunRequest(allocateRequest, iceServer._username, iceServer.Realm, iceServer._password, iceServer.Nonce); + allocateReqBytes = GetAuthenticatedStunRequest(allocateRequest, iceServer.Username, iceServer.Realm, iceServer.Password, iceServer.Nonce); } else { - allocateReqBytes = allocateRequest.ToByteBuffer(null, false); + allocateReqBytes = new byte[allocateRequest.GetByteBufferSize(null, false)]; + allocateRequest.WriteToBuffer(allocateReqBytes, null, false); } var sendResult = _rtpChannel.Send(RTPChannelSocketsEnum.RTP, iceServer.ServerEndPoint, allocateReqBytes); @@ -441,7 +443,7 @@ private SocketError SendTurnAllocateRequest(IceServer iceServer) if (sendResult != SocketError.Success) { logger.LogWarning("Error sending TURN Allocate request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.", - iceServer.OutstandingRequestsSent, iceServer._uri, iceServer.ServerEndPoint, sendResult); + iceServer.OutstandingRequestsSent, iceServer.Uri, iceServer.ServerEndPoint, sendResult); } else { @@ -477,13 +479,14 @@ private SocketError SendTurnCreatePermissionsRequest(IceServer iceServer, IPEndP byte[] createPermissionReqBytes = null; - if (iceServer.Nonce != null && iceServer.Realm != null && iceServer._username != null && iceServer._password != null) + if (iceServer.Nonce.IsEmpty && iceServer.Realm.IsEmpty && iceServer.Username.IsEmpty && iceServer.Password.IsEmpty) { - createPermissionReqBytes = GetAuthenticatedStunRequest(permissionsRequest, iceServer._username, iceServer.Realm, iceServer._password, iceServer.Nonce); + createPermissionReqBytes = GetAuthenticatedStunRequest(permissionsRequest, iceServer.Username, iceServer.Realm, iceServer.Password, iceServer.Nonce); } else { - createPermissionReqBytes = permissionsRequest.ToByteBuffer(null, false); + createPermissionReqBytes = new byte[permissionsRequest.GetByteBufferSize(null, false)]; + permissionsRequest.WriteToBuffer(createPermissionReqBytes, null, false); } var sendResult = _rtpChannel.Send(RTPChannelSocketsEnum.RTP, iceServer.ServerEndPoint, createPermissionReqBytes); @@ -491,7 +494,7 @@ private SocketError SendTurnCreatePermissionsRequest(IceServer iceServer, IPEndP if (sendResult != SocketError.Success) { logger.LogWarning("Error sending TURN Create Permissions request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.", - iceServer.OutstandingRequestsSent, iceServer._uri, iceServer.ServerEndPoint, sendResult); + iceServer.OutstandingRequestsSent, iceServer.Uri, iceServer.ServerEndPoint, sendResult); } else { @@ -526,13 +529,14 @@ private SocketError SendTurnRefreshRequest(IceServer iceServer) byte[] allocateReqBytes = null; - if (iceServer.Nonce != null && iceServer.Realm != null && iceServer._username != null && iceServer._password != null) + if (iceServer.Nonce.IsEmpty && iceServer.Realm.IsEmpty && iceServer.Username.IsEmpty && iceServer.Password.IsEmpty) { - allocateReqBytes = GetAuthenticatedStunRequest(allocateRequest, iceServer._username, iceServer.Realm, iceServer._password, iceServer.Nonce); + allocateReqBytes = GetAuthenticatedStunRequest(allocateRequest, iceServer.Username, iceServer.Realm, iceServer.Password, iceServer.Nonce); } else { - allocateReqBytes = allocateRequest.ToByteBuffer(null, false); + allocateReqBytes = new byte[allocateRequest.GetByteBufferSize(null, false)]; + allocateRequest.WriteToBuffer(allocateReqBytes, null, false); } var sendResult = _rtpChannel.Send(RTPChannelSocketsEnum.RTP, iceServer.ServerEndPoint, allocateReqBytes); @@ -540,7 +544,7 @@ private SocketError SendTurnRefreshRequest(IceServer iceServer) if (sendResult != SocketError.Success) { logger.LogWarning("Error sending TURN Refresh request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.", - iceServer.OutstandingRequestsSent, iceServer._uri, iceServer.ServerEndPoint, sendResult); + iceServer.OutstandingRequestsSent, iceServer.Uri, iceServer.ServerEndPoint, sendResult); } else { @@ -554,14 +558,14 @@ private SocketError SendTurnRefreshRequest(IceServer iceServer) /// Adds the authentication fields to a STUN request. /// /// The serialised STUN request. - private byte[] GetAuthenticatedStunRequest(STUNMessage stunRequest, string username, byte[] realm, string password, byte[] nonce) + private byte[] GetAuthenticatedStunRequest(STUNMessage stunRequest, ReadOnlyMemory username, ReadOnlyMemory realm, ReadOnlyMemory password, ReadOnlyMemory nonce) { stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Nonce, nonce)); stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Realm, realm)); stunRequest.AddUsernameAttribute(username); // See https://tools.ietf.org/html/rfc5389#section-15.4 - string key = $"{username}:{Encoding.UTF8.GetString(realm)}:{password}"; + string key = $"{username.ToString()}:{Encoding.UTF8.GetString(realm.Span)}:{password.ToString()}"; var buffer = Encoding.UTF8.GetBytes(key); var md5Digest = new MD5Digest(); var hash = new byte[md5Digest.GetDigestSize()]; @@ -569,7 +573,9 @@ private byte[] GetAuthenticatedStunRequest(STUNMessage stunRequest, string usern md5Digest.BlockUpdate(buffer, 0, buffer.Length); md5Digest.DoFinal(hash, 0); - return stunRequest.ToByteBuffer(hash, true); + var requestBytes = new byte[stunRequest.GetByteBufferSize(hash, true)]; + stunRequest.WriteToBuffer(requestBytes, hash, true); + return requestBytes; } private void OnClosed(string closeReason) diff --git a/src/SIPSorcery/net/TURN/TurnClientExtensions.cs b/src/SIPSorcery/net/TURN/TurnClientExtensions.cs index ef91cda040..d0746aaa53 100644 --- a/src/SIPSorcery/net/TURN/TurnClientExtensions.cs +++ b/src/SIPSorcery/net/TURN/TurnClientExtensions.cs @@ -14,6 +14,8 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System.Net; using System.Threading; using System.Threading.Tasks; diff --git a/src/SIPSorcery/net/TURN/TurnRelayEndPoint.cs b/src/SIPSorcery/net/TURN/TurnRelayEndPoint.cs index 6e8900d114..a04171ebb2 100644 --- a/src/SIPSorcery/net/TURN/TurnRelayEndPoint.cs +++ b/src/SIPSorcery/net/TURN/TurnRelayEndPoint.cs @@ -25,12 +25,12 @@ public class TurnRelayEndPoint /// for sending and receiving data to/from the TURN server. The RTP channel needs to use this end /// point when it wants to send TURN relay packets that will be forwarded to the remote peer. ///
- public IPEndPoint RelayServerEndPoint { get; set; } + public required IPEndPoint RelayServerEndPoint { get; init; } /// /// Gets or sets the remote peer relay endpoint. This is the end point allocated on the TURN server /// that the remote peer will send its packets to. This end point needs to be using in the SDP offer/answer /// that is sent to the remote peer so it knows where to send its RTP packets. /// - public IPEndPoint RemotePeerRelayEndPoint { get; set; } + public required IPEndPoint RemotePeerRelayEndPoint { get; init; } } diff --git a/src/SIPSorcery/net/TURN/TurnServer.cs b/src/SIPSorcery/net/TURN/TurnServer.cs index 325853680f..906e13e7d9 100644 --- a/src/SIPSorcery/net/TURN/TurnServer.cs +++ b/src/SIPSorcery/net/TURN/TurnServer.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -57,8 +59,8 @@ public class TurnServerConfig public bool EnableUdp { get; set; } = true; /// - /// The address advertised in XOR-RELAYED-ADDRESS responses. Set to a public IP - /// when the server is behind NAT. Defaults to . + /// The address advertised in XOR-RELAYED-ADDRESS responses. Set to a public IP when the server is behind NAT. + /// Defaults to . /// public IPAddress RelayAddress { get; set; } @@ -83,19 +85,18 @@ public class TurnServerConfig public int DefaultLifetimeSeconds { get; set; } = 600; /// - /// Optional shared secret enabling REST-style ephemeral credentials - /// (draft-uberti-behave-turn-rest, also referenced by RFC 8489 Section 9.2). - /// When set, / are ignored. Clients must - /// present USERNAME = "{unix-expiry}:{userId}" and - /// PASSWORD = base64(HMAC-SHA1(StaticAuthSecret, "USERNAME:REALM")). The expiry - /// is enforced and expired credentials are rejected with 401 Unauthorized. + /// Optional shared secret enabling REST-style ephemeral credentials (draft-uberti-behave-turn-rest, also + /// referenced by RFC 8489 Section 9.2). When set, / are ignored. + /// Clients must present USERNAME = "{unix-expiry}:{userId}" and PASSWORD = + /// base64(HMAC-SHA1(StaticAuthSecret, "USERNAME:REALM")). The expiry is enforced and expired credentials + /// are rejected with 401 Unauthorized. /// public string StaticAuthSecret { get; set; } /// - /// Inclusive lower bound for the relay UDP port. If both - /// and are zero (the default) the relay socket is bound to - /// an ephemeral port chosen by the OS. + /// Inclusive lower bound for the relay UDP port. If both and + /// are zero (the default) the relay socket is bound to an ephemeral port chosen by + /// the OS. /// public int RelayPortMin { get; set; } = 0; @@ -123,8 +124,8 @@ public class TurnAllocation : IDisposable public DateTime Expiry { get; set; } /// - /// Installed permissions: peer IP address → expiry time. - /// Per RFC 5766 Section 8, permissions expire after 300 seconds. + /// Installed permissions: peer IP address → expiry time. Per RFC 5766 Section 8, permissions expire after 300 + /// seconds. /// public ConcurrentDictionary Permissions { get; } = new ConcurrentDictionary(); @@ -160,56 +161,33 @@ public void Dispose() } /// - /// A lightweight TURN relay server (RFC 5766) supporting TCP and UDP control channels. - /// Provides NAT traversal by relaying UDP traffic between clients and peers. - /// Intended for development, testing, and small-scale/embedded scenarios — not for - /// production use at scale (use coturn or similar for that). + /// A lightweight TURN relay server (RFC 5766) supporting TCP and UDP control channels. Provides NAT traversal by + /// relaying UDP traffic between clients and peers. Intended for development, testing, and small-scale/embedded + /// scenarios — not for production use at scale (use coturn or similar for that). /// /// - /// Known limitations (contributions welcome): - /// - /// Two credential modes: a single long-term username/password, or REST-style - /// ephemeral credentials (draft-uberti-behave-turn-rest / - /// RFC 8489 Section 9.2) when is - /// set. There is no per-user credential database for non-REST deployments. - /// No nonce validation/expiry — nonces are generated but never verified on subsequent - /// requests, so replay attacks are possible within the allocation lifetime. - /// No rate limiting or per-IP allocation caps — a misbehaving client can exhaust - /// server resources. - /// No TLS/DTLS for the control channel — credentials are sent in the clear unless the - /// transport is already secured. - /// UDP-only relay — the relay leg is always UDP; no TCP relay (RFC 6062) or - /// TURN-over-TLS (RFC 5766 Section 6). - /// No REQUESTED-TRANSPORT validation — the attribute is ignored entirely. - /// No EVEN-PORT / RESERVATION-TOKEN support. - /// IPv4 only (no IPv6 relay addresses). - /// Allocation lifetime is not capped — clients can request arbitrarily long lifetimes. - /// No ALTERNATE-SERVER support. - /// - /// Security considerations: - /// - /// Default credentials (turn-user / turn-pass) — callers MUST configure - /// real credentials; defaults are intentionally weak to encourage replacement. - /// Default listen address is loopback — safe by default, but if bound to a public - /// interface without TLS, credentials travel in cleartext. - /// No input validation on allocation count or relay port range — in production you - /// would want to bound these. - /// + /// Known limitations (contributions welcome): Two + /// credential modes: a single long-term username/password, or REST-style ephemeral credentials + /// (draft-uberti-behave-turn-rest / RFC 8489 Section 9.2) when is + /// set. There is no per-user credential database for non-REST deployments. No nonce validation/expiry + /// — nonces are generated but never verified on subsequent requests, so replay attacks are possible within the + /// allocation lifetime. No rate limiting or per-IP allocation caps — a misbehaving client can exhaust + /// server resources. No TLS/DTLS for the control channel — credentials are sent in the clear unless + /// the transport is already secured. UDP-only relay — the relay leg is always UDP; no TCP relay (RFC + /// 6062) or TURN-over-TLS (RFC 5766 Section 6). No REQUESTED-TRANSPORT validation — the attribute is + /// ignored entirely. No EVEN-PORT / RESERVATION-TOKEN support. IPv4 only (no IPv6 relay + /// addresses). Allocation lifetime is not capped — clients can request arbitrarily long + /// lifetimes. No ALTERNATE-SERVER support. Security + /// considerations: Default credentials (turn-user / + /// turn-pass) — callers MUST configure real credentials; defaults are intentionally weak to encourage + /// replacement. Default listen address is loopback — safe by default, but if bound to a public + /// interface without TLS, credentials travel in cleartext. No input validation on allocation count or + /// relay port range — in production you would want to bound these. /// /// - /// - /// var server = new TurnServer(new TurnServerConfig - /// { - /// ListenAddress = IPAddress.Loopback, - /// Port = 3478, - /// Username = "user", - /// Password = "pass", - /// Realm = "example.com" - /// }); - /// server.Start(); - /// // ... server is running ... - /// server.Dispose(); // or server.Stop(); - /// + /// var server = new TurnServer(new TurnServerConfig { ListenAddress = IPAddress.Loopback, Port = 3478, + /// Username = "user", Password = "pass", Realm = "example.com" }); server.Start(); // ... server is running ... + /// server.Dispose(); // or server.Stop(); /// public class TurnServer : IDisposable { @@ -246,16 +224,13 @@ public class TurnServer : IDisposable public IReadOnlyDictionary Allocations => _allocations; /// - /// Translates a packet's observed source endpoint into the advertised relay endpoint - /// when the source is one of this server's own relay sockets. Returns null if - /// the endpoint isn't recognized as a local relay. - /// - /// This is the hook that makes hairpinning work when a peer on the same machine as - /// the TURN server uses one of its allocations: the OS picks a local interface - /// address as the source IP, which differs from the public IP advertised in - /// XOR-RELAYED-ADDRESS. Wiring this method into - /// RTCPeerConnection.RemoteEndpointTranslator lets the ICE source filter and - /// candidate matcher reconcile the two views. + /// Translates a packet's observed source endpoint into the advertised relay endpoint when the source is one of + /// this server's own relay sockets. Returns null if the endpoint isn't recognized as a local relay. + /// This is the hook that makes hairpinning work when a peer on the same machine as the TURN server uses one of + /// its allocations: the OS picks a local interface address as the source IP, which differs from the public IP + /// advertised in XOR-RELAYED-ADDRESS. Wiring this method into + /// RTCPeerConnection.RemoteEndpointTranslator lets the ICE source filter and candidate matcher reconcile + /// the two views. /// public IPEndPoint TranslateLocalSource(IPEndPoint observedSource) { @@ -498,7 +473,7 @@ private async Task HandleTcpClientAsync(TcpClient tcpClient) if (remaining > 0 && !await ReadExactAsync(stream, fullMsg, 4, remaining).ConfigureAwait(false)) break; - var stunMsg = STUNMessage.ParseSTUNMessage(fullMsg, fullMsg.Length); + var stunMsg = STUNMessage.ParseSTUNMessage(fullMsg.AsSpan(0, fullMsg.Length)); if (stunMsg == null) { logger.LogWarning("Failed to parse STUN message from TCP client {Client}.", clientId); @@ -597,7 +572,7 @@ private void HandleUdpDatagram(byte[] data, IPEndPoint remoteEndPoint) return; } - var stunMsg = STUNMessage.ParseSTUNMessage(data, data.Length); + var stunMsg = STUNMessage.ParseSTUNMessage(data.AsSpan(0, data.Length)); if (stunMsg == null) { logger.LogWarning("Failed to parse STUN message from UDP client {Client}.", clientId); @@ -646,7 +621,8 @@ private void ProcessMessage( case STUNMessageTypesEnum.BindingRequest: { var response = HandleBindingRequest(msg, clientEndPoint); - var bytes = response.ToByteBuffer(null, false); + var bytes = new byte[response.GetByteBufferSize(null, false)]; + response.WriteToBuffer(bytes, null, false); _ = sendResponse(bytes); } break; @@ -656,9 +632,9 @@ private void ProcessMessage( var (response, signingKey) = HandleAllocate(msg, clientId, clientEndPoint, tcpStream, udpClientEndPoint, udpControlSocket, ref allocation); - var bytes = signingKey != null - ? response.ToByteBuffer(signingKey, true) - : response.ToByteBuffer(null, false); + var includeIntegrity = signingKey != null; + var bytes = new byte[response.GetByteBufferSize(signingKey, includeIntegrity)]; + response.WriteToBuffer(bytes, signingKey, includeIntegrity); _ = sendResponse(bytes); } break; @@ -711,23 +687,22 @@ private STUNMessage HandleBindingRequest(STUNMessage request, IPEndPoint clientE } /// - /// Serialize a response, signing it with the allocation's cached HMAC key when - /// available, falling back to the server's static key (long-term cred mode). In REST - /// mode without a known allocation the response goes out unsigned — the client will - /// retry with fresh credentials anyway. + /// Serialize a response, signing it with the allocation's cached HMAC key when available, falling back to the + /// server's static key (long-term cred mode). In REST mode without a known allocation the response goes out + /// unsigned — the client will retry with fresh credentials anyway. /// private byte[] SignResponse(STUNMessage response, TurnAllocation allocation) { var key = allocation?.HmacKey ?? _hmacKey; - return key != null - ? response.ToByteBuffer(key, true) - : response.ToByteBuffer(null, false); + var includeIntegrity = key != null; + var bytes = new byte[response.GetByteBufferSize(key, includeIntegrity)]; + response.WriteToBuffer(bytes, key, includeIntegrity); + return bytes; } /// - /// In REST mode, derive the per-user long-term HMAC key from the USERNAME in the - /// request and validate the embedded expiry. Returns false (with rejectReason - /// populated) when the credential is malformed or expired. + /// In REST mode, derive the per-user long-term HMAC key from the USERNAME in the request and validate the + /// embedded expiry. Returns false (with rejectReason populated) when the credential is malformed or expired. /// private bool TryDeriveRestKey(STUNMessage request, out byte[] key, out string rejectReason) { @@ -741,7 +716,7 @@ private bool TryDeriveRestKey(STUNMessage request, out byte[] key, out string re return false; } - var username = Encoding.UTF8.GetString(usernameAttr.Value); + var username = Encoding.UTF8.GetString(usernameAttr.Value.Span); var colonIdx = username.IndexOf(':'); if (colonIdx <= 0) { @@ -907,9 +882,9 @@ private STUNMessage BuildAuthChallenge(STUNMessage request) } /// - /// Bind the relay UDP socket. If a relay port range is configured walk it in order - /// and bind to the first free port; if no range is set let the OS pick an ephemeral - /// port. Returns false when a range was set but every port in it is occupied. + /// Bind the relay UDP socket. If a relay port range is configured walk it in order and bind to the first free + /// port; if no range is set let the OS pick an ephemeral port. Returns false when a range was set but every + /// port in it is occupied. /// private bool TryBindRelaySocket(out UdpClient socket) { @@ -954,8 +929,8 @@ private STUNMessage HandleRefresh(STUNMessage request, string clientId, ref Turn uint lifetime = (uint)_config.DefaultLifetimeSeconds; if (lifetimeAttr?.Value != null && lifetimeAttr.Value.Length >= 4) { - lifetime = (uint)((lifetimeAttr.Value[0] << 24) | (lifetimeAttr.Value[1] << 16) | - (lifetimeAttr.Value[2] << 8) | lifetimeAttr.Value[3]); + lifetime = (uint)((lifetimeAttr.Value.Span[0] << 24) | (lifetimeAttr.Value.Span[1] << 16) | + (lifetimeAttr.Value.Span[2] << 8) | lifetimeAttr.Value.Span[3]); } if (lifetime == 0) @@ -1027,7 +1002,7 @@ private STUNMessage HandleChannelBind(STUNMessage request, TurnAllocation alloca return errResponse; } - var channelNumber = (ushort)((channelAttr.Value[0] << 8) | channelAttr.Value[1]); + var channelNumber = (ushort)((channelAttr.Value.Span[0] << 8) | channelAttr.Value.Span[1]); var peerAttr = request.GetFirstAttribute(STUNAttributeTypesEnum.XORPeerAddress); if (peerAttr?.Value == null) @@ -1076,7 +1051,7 @@ private void HandleSendIndication(STUNMessage msg, TurnAllocation allocation) try { - allocation.RelaySocket.Send(dataAttr.Value, dataAttr.Value.Length, peerEndpoint); + allocation.RelaySocket.Send(dataAttr.Value.Span, peerEndpoint); } catch (Exception ex) { @@ -1157,7 +1132,8 @@ private async Task RelayUdpToClientAsync(TurnAllocation allocation) result.RemoteEndPoint.Address, result.RemoteEndPoint.Port); indication.Attributes.Add(new STUNAttribute( STUNAttributeTypesEnum.Data, result.Buffer)); - var bytes = indication.ToByteBuffer(null, false); + var bytes = new byte[indication.GetByteBufferSize(null, false)]; + indication.WriteToBuffer(bytes, null, false); await SendToClientAsync(allocation, bytes).ConfigureAwait(false); } } diff --git a/src/SIPSorcery/net/WebRTC/DCEP.cs b/src/SIPSorcery/net/WebRTC/DCEP.cs index 8f1f97531b..99a6e45984 100644 --- a/src/SIPSorcery/net/WebRTC/DCEP.cs +++ b/src/SIPSorcery/net/WebRTC/DCEP.cs @@ -25,206 +25,184 @@ //----------------------------------------------------------------------------- using System; -using System.Collections.Generic; +using System.Buffers.Binary; using System.Text; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public enum DataChannelMessageTypes : byte { - public enum DataChannelMessageTypes : byte - { - ACK = 0x02, - OPEN = 0x03, - } + ACK = 0x02, + OPEN = 0x03, +} - public enum DataChannelTypes : byte - { - /// - /// The data channel provides a reliable in-order bidirectional communication. - /// - DATA_CHANNEL_RELIABLE = 0x00, - - /// - /// The data channel provides a partially reliable in-order bidirectional - /// communication. User messages will not be retransmitted more - /// times than specified in the Reliability Parameter - /// - DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT = 0x01, - - /// - /// The data channel provides a partially reliable in-order bidirectional - /// communication. User messages might not be transmitted or - /// retransmitted after a specified lifetime given in milliseconds - /// in the Reliability Parameter. This lifetime starts when - /// providing the user message to the protocol stack. - /// - DATA_CHANNEL_PARTIAL_RELIABLE_TIMED = 0x02, - - /// - /// The data channel provides a reliable unordered bidirectional communication. - /// - DATA_CHANNEL_RELIABLE_UNORDERED = 0x80, - - /// - /// The data channel provides a partially reliable unordered bidirectional - /// communication. User messages will not be retransmitted more - /// times than specified in the Reliability Parameter. - /// - DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT_UNORDERED = 0x81, - - /// - /// The data channel provides a partially reliable unordered bidirectional - /// communication. User messages might not be transmitted or - /// retransmitted after a specified lifetime given in milliseconds - /// in the Reliability Parameter. This lifetime starts when - /// providing the user message to the protocol stack. - /// - DATA_CHANNEL_PARTIAL_RELIABLE_TIMED_UNORDERED = 0x82 - } +public enum DataChannelTypes : byte +{ + /// + /// The data channel provides a reliable in-order bidirectional communication. + /// + DATA_CHANNEL_RELIABLE = 0x00, + + /// + /// The data channel provides a partially reliable in-order bidirectional + /// communication. User messages will not be retransmitted more + /// times than specified in the Reliability Parameter + /// + DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT = 0x01, + + /// + /// The data channel provides a partially reliable in-order bidirectional + /// communication. User messages might not be transmitted or + /// retransmitted after a specified lifetime given in milliseconds + /// in the Reliability Parameter. This lifetime starts when + /// providing the user message to the protocol stack. + /// + DATA_CHANNEL_PARTIAL_RELIABLE_TIMED = 0x02, + + /// + /// The data channel provides a reliable unordered bidirectional communication. + /// + DATA_CHANNEL_RELIABLE_UNORDERED = 0x80, + + /// + /// The data channel provides a partially reliable unordered bidirectional + /// communication. User messages will not be retransmitted more + /// times than specified in the Reliability Parameter. + /// + DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT_UNORDERED = 0x81, + + /// + /// The data channel provides a partially reliable unordered bidirectional + /// communication. User messages might not be transmitted or + /// retransmitted after a specified lifetime given in milliseconds + /// in the Reliability Parameter. This lifetime starts when + /// providing the user message to the protocol stack. + /// + DATA_CHANNEL_PARTIAL_RELIABLE_TIMED_UNORDERED = 0x82 +} + +/// +/// Represents a Data Channel Establishment Protocol (DECP) OPEN message. +/// This message is initially sent using the data channel on the stream +/// used for user messages. +/// +/// +/// See https://tools.ietf.org/html/rfc8832#section-5.1 +/// +public partial struct DataChannelOpenMessage : IByteSerializable +{ + public const int DCEP_OPEN_FIXED_PARAMETERS_LENGTH = 12; + + /// + /// This field holds the IANA-defined message type for the + /// DATA_CHANNEL_OPEN message.The value of this field is 0x03. + /// + public byte MessageType; + + /// + /// This field specifies the type of data channel to be opened. + /// For a list of the formal options . + /// + public byte ChannelType; /// - /// Represents a Data Channel Establishment Protocol (DECP) OPEN message. - /// This message is initially sent using the data channel on the stream - /// used for user messages. + /// The priority of the data channel. + /// + public ushort Priority; + + /// + /// Used to set tolerance for partially reliable data channels. + /// + public uint Reliability; + + /// + /// The name of the data channel. May be an empty string. + /// + public string Label; + + /// + /// If it is a non-empty string, it specifies a protocol registered in the + /// "WebSocket Subprotocol Name Registry" created in RFC6455. /// /// - /// See https://tools.ietf.org/html/rfc8832#section-5.1 + /// The websocket subprotocol names and specification are available at + /// https://tools.ietf.org/html/rfc7118 /// - public struct DataChannelOpenMessage + public string? Protocol; + + /// + /// Parses the an DCEP open message from a buffer. + /// + /// The buffer to parse the message from. + /// A new DCEP open message instance. + public static DataChannelOpenMessage Parse(ReadOnlySpan buffer) { - public const int DCEP_OPEN_FIXED_PARAMETERS_LENGTH = 12; - - /// - /// This field holds the IANA-defined message type for the - /// DATA_CHANNEL_OPEN message.The value of this field is 0x03. - /// - public byte MessageType; - - /// - /// This field specifies the type of data channel to be opened. - /// For a list of the formal options . - /// - public byte ChannelType; - - /// - /// The priority of the data channel. - /// - public ushort Priority; - - /// - /// Used to set tolerance for partially reliable data channels. - /// - public uint Reliability; - - /// - /// The name of the data channel. May be an empty string. - /// - public string Label; - - /// - /// If it is a non-empty string, it specifies a protocol registered in the - /// "WebSocket Subprotocol Name Registry" created in RFC6455. - /// - /// - /// The websocket subprotocol names and specification are available at - /// https://tools.ietf.org/html/rfc7118 - /// - public string Protocol; - - /// - /// Parses the an DCEP open message from a buffer. - /// - /// The buffer to parse the message from. - /// The position in the buffer to start parsing from. - /// A new DCEP open message instance. - public static DataChannelOpenMessage Parse(byte[] buffer, int posn) + if (buffer.Length < DCEP_OPEN_FIXED_PARAMETERS_LENGTH) { - if (buffer.Length < DCEP_OPEN_FIXED_PARAMETERS_LENGTH) - { - throw new ApplicationException("The buffer did not contain the minimum number of bytes for a DCEP open message."); - } - - var dcepOpen = new DataChannelOpenMessage(); - - dcepOpen.MessageType = buffer[posn]; - dcepOpen.ChannelType = buffer[posn + 1]; - dcepOpen.Priority = NetConvert.ParseUInt16(buffer, posn + 2); - dcepOpen.Reliability = NetConvert.ParseUInt32(buffer, posn + 4); + throw new SipSorceryException("The buffer did not contain the minimum number of bytes for a DCEP open message."); + } - ushort labelLength = NetConvert.ParseUInt16(buffer, posn + 8); - ushort protocolLength = NetConvert.ParseUInt16(buffer, posn + 10); + var dcepOpen = new DataChannelOpenMessage(); - if (labelLength > 0) - { - dcepOpen.Label = Encoding.UTF8.GetString(buffer, 12, labelLength); - } + dcepOpen.MessageType = buffer[0]; + dcepOpen.ChannelType = buffer[1]; + dcepOpen.Priority = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2)); + dcepOpen.Reliability = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(4)); - if (protocolLength > 0) - { - dcepOpen.Protocol = Encoding.UTF8.GetString(buffer, 12 + labelLength, protocolLength); - } + var labelLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(8)); + var protocolLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(10)); - return dcepOpen; + if (labelLength > 0) + { + dcepOpen.Label = Encoding.UTF8.GetString(buffer.Slice(12, labelLength)); } - /// - /// Gets the length of the serialised DCEP OPEN message. - /// - /// The serialised length of this DECEP OPEN message. - public int GetLength() + if (protocolLength > 0) { - ushort labelLength = (ushort)(Label != null ? Encoding.UTF8.GetByteCount(Label) : 0); - ushort protocolLength = (ushort)(Protocol != null ? Encoding.UTF8.GetByteCount(Protocol) : 0); - - return DCEP_OPEN_FIXED_PARAMETERS_LENGTH + labelLength + protocolLength; + dcepOpen.Protocol = Encoding.UTF8.GetString(buffer.Slice(12 + labelLength, protocolLength)); } - /// - /// Serialises a Data Channel Establishment Protocol (DECP) OPEN message to a - /// pre-allocated buffer. - /// - /// The buffer to write the serialised chunk bytes to. It - /// must have the required space already allocated. - /// The position in the buffer to write to. - /// The number of bytes, including padding, written to the buffer. - public ushort WriteTo(byte[] buffer, int posn) - { - buffer[posn] = MessageType; - buffer[posn + 1] = ChannelType; - NetConvert.ToBuffer(Priority, buffer, posn + 2); - NetConvert.ToBuffer(Reliability, buffer, posn + 4); + return dcepOpen; + } - ushort labelLength = (ushort)(Label != null ? Encoding.UTF8.GetByteCount(Label) : 0); - ushort protocolLength = (ushort)(Protocol != null ? Encoding.UTF8.GetByteCount(Protocol) : 0); + /// + public int GetByteCount() + { + var labelLength = (ushort)(Label is { } ? Encoding.UTF8.GetByteCount(Label) : 0); + var protocolLength = (ushort)(Protocol is { } ? Encoding.UTF8.GetByteCount(Protocol) : 0); - NetConvert.ToBuffer(labelLength, buffer, posn + 8); - NetConvert.ToBuffer(protocolLength, buffer, posn + 10); + return DCEP_OPEN_FIXED_PARAMETERS_LENGTH + labelLength + protocolLength; + } - posn += DCEP_OPEN_FIXED_PARAMETERS_LENGTH; + /// + public int WriteBytes(Span buffer) + { + buffer[0] = MessageType; + buffer[1] = ChannelType; + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), Priority); + BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(4), Reliability); - if (labelLength > 0) - { - Buffer.BlockCopy(Encoding.UTF8.GetBytes(Label), 0, buffer, posn, labelLength); - posn += labelLength; - } + var labelLength = (ushort)(Label is { } ? Encoding.UTF8.GetByteCount(Label) : 0); + var protocolLength = (ushort)(Protocol is { } ? Encoding.UTF8.GetByteCount(Protocol) : 0); - if (protocolLength > 0) - { - Buffer.BlockCopy(Encoding.UTF8.GetBytes(Protocol), 0, buffer, posn, protocolLength); - posn += protocolLength; - } + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(8), labelLength); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(10), protocolLength); - return (ushort)posn; + var len = (ushort)DCEP_OPEN_FIXED_PARAMETERS_LENGTH; + + if (labelLength > 0) + { + Encoding.UTF8.GetBytes(Label.AsSpan(), buffer.Slice(len)); + len += labelLength; } - /// - /// Serialises the DCEP OPEN message to a buffer. - /// - public byte[] GetBytes() + if (protocolLength > 0) { - var buffer = new byte[GetLength()]; - WriteTo(buffer, 0); - return buffer; + Encoding.UTF8.GetBytes(Protocol.AsSpan(), buffer.Slice(len)); + len += protocolLength; } + + return len; } } diff --git a/src/SIPSorcery/net/WebRTC/IRTCDataChannel.cs b/src/SIPSorcery/net/WebRTC/IRTCDataChannel.cs index 1bdbe2a269..8b6883543d 100644 --- a/src/SIPSorcery/net/WebRTC/IRTCDataChannel.cs +++ b/src/SIPSorcery/net/WebRTC/IRTCDataChannel.cs @@ -34,143 +34,142 @@ using System; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public delegate void OnDataChannelMessageDelegate(RTCDataChannel dc, DataChannelPayloadProtocols protocol, byte[] data); + +public enum RTCDataChannelState +{ + /// + /// The user agent is attempting to establish the underlying data transport. + /// This is the initial state of an RTCDataChannel object, whether created + /// with createDataChannel, or dispatched as a part of an RTCDataChannelEvent. + /// + connecting, + + /// + /// The underlying data transport is established and communication is possible. + /// + open, + + /// + /// The procedure to close down the underlying data transport has started. + /// + closing, + + /// + /// The underlying data transport has been closed or could not be established. + /// + closed +}; + +/// +/// The RTCDataChannel interface represents a bi-directional data channel between two peers. +/// +/// +/// Specification https://www.w3.org/TR/webrtc/#webidl-1143016005 +/// +internal interface IRTCDataChannel +{ + /// + /// The label attribute represents a label that can be used to distinguish this RTCDataChannel + /// object from other RTCDataChannel objects. Scripts are allowed to create multiple RTCDataChannel + /// objects with the same label. On getting, the attribute MUST return the value of the [[DataChannelLabel]] slot. + /// + string? label { get; } + + /// + /// The ordered attribute returns true if the RTCDataChannel is ordered, and false if out of order delivery + /// is allowed. On getting, the attribute MUST return the value of the [[Ordered]] slot + /// + bool ordered { get; } + + /// + /// he maxPacketLifeTime attribute returns the length of the time window (in milliseconds) during which + /// transmissions and retransmissions may occur in unreliable mode. On getting, the attribute MUST return the + /// value of the [[MaxPacketLifeTime]] slot. + /// + ushort? maxPacketLifeTime { get; } + + /// + /// The maxRetransmits attribute returns the maximum number of retransmissions that are attempted in unreliable mode. + /// On getting, the attribute MUST return the value of the [[MaxRetransmits]] slot. + /// + ushort? maxRetransmits { get; } + + /// + /// The protocol attribute returns the name of the sub-protocol used with this RTCDataChannel. On getting, the + /// attribute MUST return the value of the [[DataChannelProtocol]] slot. + /// + string? protocol { get; } + + /// + /// he negotiated attribute returns true if this RTCDataChannel was negotiated by the application, or false otherwise. + /// On getting, the attribute MUST return the value of the [[Negotiated]] slot. + /// + bool negotiated { get; } + + /// + /// The id attribute returns the ID for this RTCDataChannel. The value is initially null, which is what will be returned if + /// the ID was not provided at channel creation time, and the DTLS role of the SCTP transport has not yet been negotiated. + /// Otherwise, it will return the ID that was either selected by the script or generated by the user agent according to + /// [RTCWEB-DATA-PROTOCOL]. After the ID is set to a non-null value, it will not change. On getting, the attribute MUST return + /// the value of the [[DataChannelId]] slot. + /// + ushort? id { get; } + + /// + /// The readyState attribute represents the state of the RTCDataChannel object. On getting, the attribute MUST return the + /// value of the [[ReadyState]] slot. + /// + RTCDataChannelState readyState { get; } + + /// + /// The bufferedAmount attribute MUST, on getting, return the value of the [[BufferedAmount]] slot. The attribute exposes the + /// number of bytes of application data (UTF-8 text and binary data) that have been queued using send(). Even though the data + /// transmission can occur in parallel, the returned value MUST NOT be decreased before the current task yielded back to the + /// event loop to prevent race conditions. The value does not include framing overhead incurred by the protocol, or buffering + /// done by the operating system or network hardware. The value of the [[BufferedAmount]] slot will only increase with each + /// call to the send() method as long as the [[ReadyState]] slot is open; however, the slot does not reset to zero once the + /// channel closes. When the underlying data transport sends data from its queue, the user agent MUST queue a task that reduces + /// [[BufferedAmount]] with the number of bytes that was sent. + /// + ulong bufferedAmount { get; } + + /// + /// The bufferedAmountLowThreshold attribute sets the threshold at which the bufferedAmount is considered to be low. When the + /// bufferedAmount decreases from above this threshold to equal or below it, the bufferedamountlow event fires. The + /// bufferedAmountLowThreshold is initially zero on each new RTCDataChannel, but the application may change its value at any time. + /// + ulong bufferedAmountLowThreshold { get; set; } + + /// + /// The RTCDataChannel object's underlying data transport has been established (or re-established). + /// + event Action onopen; + + //event Action onbufferedamountlow; + event Action onerror; + //event Action onclosing; + event Action onclose; + void close(); + + /// + /// A message was successfully received. + /// + event OnDataChannelMessageDelegate onmessage; + + string? binaryType { get; set; } + void send(string data); + void send(byte[] data, int offset = 0, int count = -1); +}; + +public class RTCDataChannelInit { - public delegate void OnDataChannelMessageDelegate(RTCDataChannel dc, DataChannelPayloadProtocols protocol, byte[] data); - - public enum RTCDataChannelState - { - /// - /// The user agent is attempting to establish the underlying data transport. - /// This is the initial state of an RTCDataChannel object, whether created - /// with createDataChannel, or dispatched as a part of an RTCDataChannelEvent. - /// - connecting, - - /// - /// The underlying data transport is established and communication is possible. - /// - open, - - /// - /// The procedure to close down the underlying data transport has started. - /// - closing, - - /// - /// The underlying data transport has been closed or could not be established. - /// - closed - }; - - /// - /// The RTCDataChannel interface represents a bi-directional data channel between two peers. - /// - /// - /// Specification https://www.w3.org/TR/webrtc/#webidl-1143016005 - /// - interface IRTCDataChannel - { - /// - /// The label attribute represents a label that can be used to distinguish this RTCDataChannel - /// object from other RTCDataChannel objects. Scripts are allowed to create multiple RTCDataChannel - /// objects with the same label. On getting, the attribute MUST return the value of the [[DataChannelLabel]] slot. - /// - string label { get; } - - /// - /// The ordered attribute returns true if the RTCDataChannel is ordered, and false if out of order delivery - /// is allowed. On getting, the attribute MUST return the value of the [[Ordered]] slot - /// - bool ordered { get; } - - /// - /// he maxPacketLifeTime attribute returns the length of the time window (in milliseconds) during which - /// transmissions and retransmissions may occur in unreliable mode. On getting, the attribute MUST return the - /// value of the [[MaxPacketLifeTime]] slot. - /// - ushort? maxPacketLifeTime { get; } - - /// - /// The maxRetransmits attribute returns the maximum number of retransmissions that are attempted in unreliable mode. - /// On getting, the attribute MUST return the value of the [[MaxRetransmits]] slot. - /// - ushort? maxRetransmits { get; } - - /// - /// The protocol attribute returns the name of the sub-protocol used with this RTCDataChannel. On getting, the - /// attribute MUST return the value of the [[DataChannelProtocol]] slot. - /// - string protocol { get; } - - /// - /// he negotiated attribute returns true if this RTCDataChannel was negotiated by the application, or false otherwise. - /// On getting, the attribute MUST return the value of the [[Negotiated]] slot. - /// - bool negotiated { get; } - - /// - /// The id attribute returns the ID for this RTCDataChannel. The value is initially null, which is what will be returned if - /// the ID was not provided at channel creation time, and the DTLS role of the SCTP transport has not yet been negotiated. - /// Otherwise, it will return the ID that was either selected by the script or generated by the user agent according to - /// [RTCWEB-DATA-PROTOCOL]. After the ID is set to a non-null value, it will not change. On getting, the attribute MUST return - /// the value of the [[DataChannelId]] slot. - /// - ushort? id { get; } - - /// - /// The readyState attribute represents the state of the RTCDataChannel object. On getting, the attribute MUST return the - /// value of the [[ReadyState]] slot. - /// - RTCDataChannelState readyState { get; } - - /// - /// The bufferedAmount attribute MUST, on getting, return the value of the [[BufferedAmount]] slot. The attribute exposes the - /// number of bytes of application data (UTF-8 text and binary data) that have been queued using send(). Even though the data - /// transmission can occur in parallel, the returned value MUST NOT be decreased before the current task yielded back to the - /// event loop to prevent race conditions. The value does not include framing overhead incurred by the protocol, or buffering - /// done by the operating system or network hardware. The value of the [[BufferedAmount]] slot will only increase with each - /// call to the send() method as long as the [[ReadyState]] slot is open; however, the slot does not reset to zero once the - /// channel closes. When the underlying data transport sends data from its queue, the user agent MUST queue a task that reduces - /// [[BufferedAmount]] with the number of bytes that was sent. - /// - ulong bufferedAmount { get; } - - /// - /// The bufferedAmountLowThreshold attribute sets the threshold at which the bufferedAmount is considered to be low. When the - /// bufferedAmount decreases from above this threshold to equal or below it, the bufferedamountlow event fires. The - /// bufferedAmountLowThreshold is initially zero on each new RTCDataChannel, but the application may change its value at any time. - /// - ulong bufferedAmountLowThreshold { get; set; } - - /// - /// The RTCDataChannel object's underlying data transport has been established (or re-established). - /// - event Action onopen; - - //event Action onbufferedamountlow; - event Action onerror; - //event Action onclosing; - event Action onclose; - void close(); - - /// - /// A message was successfully received. - /// - event OnDataChannelMessageDelegate onmessage; - - string binaryType { get; set; } - void send(string data); - void send(byte[] data, int offset = 0, int count = -1); - }; - - public class RTCDataChannelInit - { - public bool? ordered { get; set; } - public ushort? maxPacketLifeTime { get; set; } - public ushort? maxRetransmits { get; set; } - public string protocol { get; set; } - public bool? negotiated { get; set; } - public ushort? id { get; set; } - }; -} + public bool? ordered { get; set; } + public ushort? maxPacketLifeTime { get; set; } + public ushort? maxRetransmits { get; set; } + public string? protocol { get; set; } + public bool? negotiated { get; set; } + public ushort? id { get; set; } +}; diff --git a/src/SIPSorcery/net/WebRTC/IRTCPeerConnection.cs b/src/SIPSorcery/net/WebRTC/IRTCPeerConnection.cs index a7d05b80db..1b931d5b28 100644 --- a/src/SIPSorcery/net/WebRTC/IRTCPeerConnection.cs +++ b/src/SIPSorcery/net/WebRTC/IRTCPeerConnection.cs @@ -21,475 +21,496 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net; +using System.Net.Security; +using System.Runtime.Serialization; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using SIPSorcery.Sys; -namespace SIPSorcery.Net -{ - public enum RTCSdpType - { - answer = 0, - offer = 1, - pranswer = 2, - rollback = 3 - } +namespace SIPSorcery.Net; - public class RTCOfferOptions - { - /// - /// If set it indicates that any available ICE candidates should NOT be added - /// to the offer SDP. By default "host" candidates should always be available - /// and will be added to the offer SDP. - /// - public bool X_ExcludeIceCandidates; - - /// - /// If set to true it indicates the generation of the SDP offer should wait until the ICE gathering - /// is compelte so the ICE cnadidates can be included in the SDP offer. - /// - public bool X_WaitForIceGatheringToComplete; - } +public enum RTCSdpType +{ + [EnumMember(Value = "answer")] + answer = 0, + [EnumMember(Value = "offer")] + offer = 1, + [EnumMember(Value = "pranswer")] + pranswer = 2, + [EnumMember(Value = "rollback")] + rollback = 3 +} +public class RTCOfferOptions +{ /// - /// Options for creating an SDP answer. + /// If set it indicates that any available ICE candidates should NOT be added + /// to the offer SDP. By default "host" candidates should always be available + /// and will be added to the offer SDP. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dictionary-rtcofferansweroptions-members. - /// - public class RTCAnswerOptions - { - /// - /// If set it indicates that any available ICE candidates should NOT be added - /// to the offer SDP. By default "host" candidates should always be available - /// and will be added to the offer SDP. - /// - public bool X_ExcludeIceCandidates; - - /// - /// If set to true it indicates the generation of the SDP answer should wait until ICE gathering - /// is complete so the server reflexive (srflx) and relay ICE candidates can be included in the - /// SDP answer. This is required for non-trickle signalling (e.g. a single HTTP offer/answer - /// exchange) where there is no channel to deliver candidates after the answer has been sent. - /// - public bool X_WaitForIceGatheringToComplete; - } - - public class RTCSessionDescription - { - public RTCSdpType type; - public SDP sdp; - } + public bool X_ExcludeIceCandidates; /// - /// The types of credentials for an ICE server. + /// If set to true it indicates the generation of the SDP offer should wait until the ICE gathering + /// is compelte so the ICE cnadidates can be included in the SDP offer. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#rtcicecredentialtype-enum. - /// - public enum RTCIceCredentialType - { - password - } + public bool X_WaitForIceGatheringToComplete; +} +/// +/// Options for creating an SDP answer. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#dictionary-rtcofferansweroptions-members. +/// +public class RTCAnswerOptions +{ /// - /// Used to specify properties for a STUN or TURN server that can be used by an ICE agent. + /// If set it indicates that any available ICE candidates should NOT be added + /// to the offer SDP. By default "host" candidates should always be available + /// and will be added to the offer SDP. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#rtciceserver-dictionary. - /// - public class RTCIceServer + public bool X_ExcludeIceCandidates; +} + +public class RTCSessionDescription +{ + public RTCSdpType type; + public SDP? sdp; +} + +/// +/// The types of credentials for an ICE server. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#rtcicecredentialtype-enum. +/// +public enum RTCIceCredentialType +{ + password +} + +/// +/// Used to specify properties for a STUN or TURN server that can be used by an ICE agent. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#rtciceserver-dictionary. +/// +public class RTCIceServer +{ + public string? urls; + public string? username; + public RTCIceCredentialType credentialType; + public string? credential; + public SslClientAuthenticationOptions? SslClientAuthenticationOptions; + + public static RTCIceServer Parse(ReadOnlySpan iceServer) { - public string urls; - public string username; - public RTCIceCredentialType credentialType; - public string credential; + int firstSep = iceServer.IndexOf(';'); + int secondSep = firstSep != -1 ? iceServer.Slice(firstSep + 1).IndexOf(';') : -1; - public static RTCIceServer Parse(string iceServer) - { - var fields = iceServer.Split(';'); + string urls; + string? username = null; + string? credential = null; - return new RTCIceServer + if (firstSep == -1) + { + urls = iceServer.ToString(); + } + else + { + urls = iceServer.Slice(0, firstSep).ToString(); + var rest = iceServer.Slice(firstSep + 1); + if (secondSep == -1) { - urls = fields[0], - username = fields.Length > 1 ? fields[1] : null, - credential = fields.Length > 2 ? fields[2] : null, - credentialType = RTCIceCredentialType.password - }; + username = rest.ToString(); + } + else + { + username = rest.Slice(0, secondSep).ToString(); + credential = rest.Slice(secondSep + 1).ToString(); + } } - } - /// - /// Determines which ICE candidates can be used for a peer connection. - /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcicetransportpolicy. - /// - public enum RTCIceTransportPolicy - { - all, - relay + return new RTCIceServer + { + urls = urls, + username = username, + credential = credential, + credentialType = RTCIceCredentialType.password + }; } +} + +/// +/// Determines which ICE candidates can be used for a peer connection. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#dom-rtcicetransportpolicy. +/// +public enum RTCIceTransportPolicy +{ + all, + relay +} +/// +/// Affects which media tracks are negotiated if the remote end point is not bundle aware. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#dom-rtcbundlepolicy. +/// +public enum RTCBundlePolicy +{ + balanced, + max_compat, + max_bundle +} + +/// +/// The RTCP multiplex options for ICE candidates. This option is currently redundant +/// since the single option means RTCP multiplexing MUST be available or the SDP negotiation +/// will fail. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#dom-rtcrtcpmuxpolicy. +/// +public enum RTCRtcpMuxPolicy +{ + require +} + +/// +/// Represents a fingerprint of a certificate used to authenticate WebRTC communications. +/// +public class RTCDtlsFingerprint +{ /// - /// Affects which media tracks are negotiated if the remote end point is not bundle aware. + /// One of the hash function algorithms defined in the 'Hash function Textual Names' registry. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcbundlepolicy. - /// - public enum RTCBundlePolicy - { - balanced, - max_compat, - max_bundle - } + public string? algorithm; /// - /// The RTCP multiplex options for ICE candidates. This option is currently redundant - /// since the single option means RTCP multiplexing MUST be available or the SDP negotiation - /// will fail. + /// The value of the certificate fingerprint in lower-case hex string as expressed utilising + /// the syntax of 'fingerprint' in [RFC4572] Section 5. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcrtcpmuxpolicy. - /// - public enum RTCRtcpMuxPolicy + public string? value; + + public override string ToString() { - require + Debug.Assert(value is { }); + // FireFox wasn't happy unless the fingerprint hash was in upper case. + return $"{algorithm} {value.ToUpper()}"; } /// - /// Represents a fingerprint of a certificate used to authenticate WebRTC communications. + /// Attempts to parse the fingerprint fields from a string. /// - public class RTCDtlsFingerprint + /// The string to parse from. + /// If successful a fingerprint object. + /// True if a fingerprint was successfully parsed. False if not. + public static bool TryParse(string str, out RTCDtlsFingerprint? fingerprint) { - /// - /// One of the hash function algorithms defined in the 'Hash function Textual Names' registry. - /// - public string algorithm; - - /// - /// The value of the certificate fingerprint in lower-case hex string as expressed utilising - /// the syntax of 'fingerprint' in [RFC4572] Section 5. - /// - public string value; - - public override string ToString() + fingerprint = null; + + if (string.IsNullOrWhiteSpace(str)) { - // FireFox wasn't happy unless the fingerprint hash was in upper case. - return $"{algorithm} {value.ToUpper()}"; + return false; } - /// - /// Attempts to parse the fingerprint fields from a string. - /// - /// The string to parse from. - /// If successful a fingerprint object. - /// True if a fingerprint was successfully parsed. False if not. - public static bool TryParse(string str, out RTCDtlsFingerprint fingerprint) + var strSpan = str.AsSpan().Trim(); + var spaceIndex = strSpan.IndexOf(' '); + if (spaceIndex == -1) { - fingerprint = null; + return false; + } - if (string.IsNullOrEmpty(str)) - { - return false; - } - else - { - int spaceIndex = str.IndexOf(' '); - if (spaceIndex == -1) - { - return false; - } - else - { - string algStr = str.Substring(0, spaceIndex); - string val = str.Substring(spaceIndex + 1); - - if (!DtlsUtils.IsHashSupported(algStr)) - { - return false; - } - else - { - fingerprint = new RTCDtlsFingerprint - { - algorithm = algStr, - value = val - }; - return true; - } - } - } + var algorithm = strSpan.Slice(0, spaceIndex); + var value = strSpan.Slice(spaceIndex + 1); + + if (!DtlsUtils.IsHashSupported(algorithm.ToString())) + { + return false; } + + fingerprint = new RTCDtlsFingerprint + { + algorithm = algorithm.ToLowerString(), + value = value.ToLowerString() + }; + + return true; } +} +/// +/// Represents a certificate used to authenticate WebRTC communications. +/// +/// +/// TODO: +/// From https://www.w3.org/TR/webrtc/#methods-4: +/// "Implementations SHOULD store the sensitive keying material in a secure module safe from +/// same-process memory attacks." +/// +[Obsolete("Use RTCCertificate2 instead")] +public class RTCCertificate +{ /// - /// Represents a certificate used to authenticate WebRTC communications. + /// The expires attribute indicates the date and time in milliseconds relative to 1970-01-01T00:00:00Z + /// after which the certificate will be considered invalid by the browser. /// - /// - /// TODO: - /// From https://www.w3.org/TR/webrtc/#methods-4: - /// "Implementations SHOULD store the sensitive keying material in a secure module safe from - /// same-process memory attacks." - /// - [Obsolete("Use RTCCertificate2 instead")] - public class RTCCertificate + public long expires { - /// - /// The expires attribute indicates the date and time in milliseconds relative to 1970-01-01T00:00:00Z - /// after which the certificate will be considered invalid by the browser. - /// - public long expires + get { - get + if (Certificate is null) + { + return 0; + } + else { - if (Certificate == null) - { - return 0; - } - else - { - return Certificate.NotAfter.ToUnixTime(); - } + return Certificate.NotAfter.ToUnixTime(); } } + } - public X509Certificate2 Certificate; + public X509Certificate2? Certificate; - public List getFingerprints() - { - return new List { DtlsUtils.Fingerprint(Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(Certificate)) }; - } + public List getFingerprints() + { + return new List { DtlsUtils.Fingerprint(Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(Certificate)) }; } +} +/// +/// Represents a certificate used to authenticate WebRTC communications. +/// +/// +/// TODO: +/// From https://www.w3.org/TR/webrtc/#methods-4: +/// "Implementations SHOULD store the sensitive keying material in a secure module safe from +/// same-process memory attacks." +/// +public class RTCCertificate2 +{ /// - /// Represents a certificate used to authenticate WebRTC communications. + /// The expires attribute indicates the date and time in milliseconds relative to 1970-01-01T00:00:00Z + /// after which the certificate will be considered invalid by the browser. /// - /// - /// TODO: - /// From https://www.w3.org/TR/webrtc/#methods-4: - /// "Implementations SHOULD store the sensitive keying material in a secure module safe from - /// same-process memory attacks." - /// - public class RTCCertificate2 + public long expires { - /// - /// The expires attribute indicates the date and time in milliseconds relative to 1970-01-01T00:00:00Z - /// after which the certificate will be considered invalid by the browser. - /// - public long expires + get { - get + if (Certificate is null) { - if (Certificate == null) - { - return 0; - } - else - { - return Certificate.NotAfter.ToUnixTime(); - } + return 0; + } + else + { + return Certificate.NotAfter.ToUnixTime(); } } + } - public Org.BouncyCastle.X509.X509Certificate Certificate; + public Org.BouncyCastle.X509.X509Certificate? Certificate; - public Org.BouncyCastle.Crypto.AsymmetricKeyParameter PrivateKey; + public Org.BouncyCastle.Crypto.AsymmetricKeyParameter? PrivateKey; - public List getFingerprints() - { - return new List { DtlsUtils.Fingerprint(Certificate) }; - } + public List getFingerprints() + { + Debug.Assert(Certificate is { }); + return new List { DtlsUtils.Fingerprint(Certificate) }; } +} + +/// +/// Defines the parameters to configure how a new RTCPeerConnection is created. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#rtcconfiguration-dictionary. +/// +public class RTCConfiguration +{ + public List? iceServers; + public RTCIceTransportPolicy iceTransportPolicy; + public RTCBundlePolicy bundlePolicy; + public RTCRtcpMuxPolicy rtcpMuxPolicy; + public List? certificates2; /// - /// Defines the parameters to configure how a new RTCPeerConnection is created. + /// The Bouncy Castle DTLS logic enforces the use of Extended Master + /// Secret Keys as per RFC7627. Some WebRTC implementations do not support + /// Extended Master Secret Keys (for example Kurento in Mar 2021) and this + /// configuration option is made available for cases where an application + /// explicitly decides it's acceptable to disable them. /// /// - /// As specified in https://www.w3.org/TR/webrtc/#rtcconfiguration-dictionary. + /// From https://tools.ietf.org/html/rfc7627#section-4: + /// "Clients and servers SHOULD NOT accept handshakes that do not use the + /// extended master secret, especially if they rely on features like + /// compound authentication that fall into the vulnerable cases described + /// in Section 6.1." /// - public class RTCConfiguration - { - public List iceServers; - public RTCIceTransportPolicy iceTransportPolicy; - public RTCBundlePolicy bundlePolicy; - public RTCRtcpMuxPolicy rtcpMuxPolicy; - public List certificates2; - - /// - /// The Bouncy Castle DTLS logic enforces the use of Extended Master - /// Secret Keys as per RFC7627. Some WebRTC implementations do not support - /// Extended Master Secret Keys (for example Kurento in Mar 2021) and this - /// configuration option is made available for cases where an application - /// explicitly decides it's acceptable to disable them. - /// - /// - /// From https://tools.ietf.org/html/rfc7627#section-4: - /// "Clients and servers SHOULD NOT accept handshakes that do not use the - /// extended master secret, especially if they rely on features like - /// compound authentication that fall into the vulnerable cases described - /// in Section 6.1." - /// - public bool X_DisableExtendedMasterSecretKey; - - /// - /// Size of the pre-fetched ICE pool. Defaults to 0. - /// - public int iceCandidatePoolSize = 0; - - /// - /// Optional. If specified this address will be used as the bind address for any RTP - /// and control sockets created. Generally this address does not need to be set. The default behaviour - /// is to bind to [::] or 0.0.0.0, depending on system support, which minimises network routing - /// causing connection issues. - /// - public IPAddress X_BindAddress; - - /// - /// Optional. If set to true the feedback profile set in the SDP offers and answers will be - /// UDP/TLS/RTP/SAVPF instead of UDP/TLS/RTP/SAVP. - /// - public bool X_UseRtpFeedbackProfile; - - /// - /// When gathering host ICE candidates for the local machine the default behaviour is - /// to only use IP addresses on the interface that the OS routing table selects to connect - /// to the destination, or the Internet facing interface if the destination is unknown. - /// This default behaviour is to shield the leaking of all local IP addresses into ICE - /// candidates. In some circumstances, and after weighing up the security concerns, - /// it's very useful to include all interfaces in when generating the address list. - /// Setting this parameter to true will cause all interfaces to be used irrespective of - /// the destination address - /// - public bool X_ICEIncludeAllInterfaceAddresses; - - /// - /// Set to true to use the RSA key in the certificate for the DTLS handshake. The default - /// is to use ECDSA. Chrome has defaulted to ECDSA since 2016 (see https://developer.chrome.com/blog/webrtc-ecdsa). - /// - public bool X_UseRsaForDtlsCertificate; - - /// - /// Timeout for gathering local IP addresses - /// - public int X_GatherTimeoutMs = 30000; - } + public bool X_DisableExtendedMasterSecretKey; /// - /// Signalling states for a WebRTC peer connection. + /// Size of the pre-fetched ICE pool. Defaults to 0. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcsignalingstate. - /// - public enum RTCSignalingState - { - stable, - have_local_offer, - have_remote_offer, - have_local_pranswer, - have_remote_pranswer, - closed - } + public int iceCandidatePoolSize; /// - /// The states a peer connection transitions through. - /// The difference between the IceConnectionState and the PeerConnectionState is somewhat subtle: - /// - IceConnectionState: applies to the connection checks amongst ICE candidates and is - /// set as completed as soon as a local and remote candidate have set their nominated candidate, - /// - PeerConnectionState: takes into account the IceConnectionState but also includes the DTLS - /// handshake and actions at the application layer such as a request to close the peer connection. + /// Optional. If specified this address will be used as the bind address for any RTP + /// and control sockets created. Generally this address does not need to be set. The default behaviour + /// is to bind to [::] or 0.0.0.0, depending on system support, which minimises network routing + /// causing connection issues. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#rtcpeerconnectionstate-enum. - /// - public enum RTCPeerConnectionState - { - closed, - failed, - disconnected, - @new, - connecting, - connected - } + public IPAddress? X_BindAddress; - public interface IRTCPeerConnection - { - //IRTCPeerConnection(RTCConfiguration configuration = null); - RTCSessionDescriptionInit createOffer(RTCOfferOptions options = null); - RTCSessionDescriptionInit createAnswer(RTCAnswerOptions options = null); - Task setLocalDescription(RTCSessionDescriptionInit description); - RTCSessionDescription localDescription { get; } - RTCSessionDescription currentLocalDescription { get; } - RTCSessionDescription pendingLocalDescription { get; } - SetDescriptionResultEnum setRemoteDescription(RTCSessionDescriptionInit description); - RTCSessionDescription remoteDescription { get; } - RTCSessionDescription currentRemoteDescription { get; } - RTCSessionDescription pendingRemoteDescription { get; } - void addIceCandidate(RTCIceCandidateInit candidate = null); - RTCSignalingState signalingState { get; } - RTCIceGatheringState iceGatheringState { get; } - RTCIceConnectionState iceConnectionState { get; } - RTCPeerConnectionState connectionState { get; } - bool canTrickleIceCandidates { get; } - void restartIce(); - RTCConfiguration getConfiguration(); - void setConfiguration(RTCConfiguration configuration = null); - void close(); - event Action onnegotiationneeded; - event Action onicecandidate; - event Action onicecandidateerror; - event Action onsignalingstatechange; - event Action oniceconnectionstatechange; - event Action onicegatheringstatechange; - event Action onconnectionstatechange; - - // TODO: Extensions for the RTCMediaAPI - // https://www.w3.org/TR/webrtc/#rtcpeerconnection-interface-extensions. - //List getSenders(); - //List getReceivers(); - //List getTransceivers(); - //RTCRtpSender addTrack(MediaStreamTrack track, param MediaStream[] streams); - //void removeTrack(RTCRtpSender sender); - //RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind, - ////optional RTCRtpTransceiverInit init = {}); - //event ontrack; - }; + /// + /// Optional. If set to true the feedback profile set in the SDP offers and answers will be + /// UDP/TLS/RTP/SAVPF instead of UDP/TLS/RTP/SAVP. + /// + public bool X_UseRtpFeedbackProfile; /// - /// The RTCRtpSender interface allows an application to control how a given MediaStreamTrack - /// is encoded and transmitted to a remote peer. When setParameters is called on an - /// RTCRtpSender object, the encoding is changed appropriately. + /// When gathering host ICE candidates for the local machine the default behaviour is + /// to only use IP addresses on the interface that the OS routing table selects to connect + /// to the destination, or the Internet facing interface if the destination is unknown. + /// This default behaviour is to shield the leaking of all local IP addresses into ICE + /// candidates. In some circumstances, and after weighing up the security concerns, + /// it's very useful to include all interfaces in when generating the address list. + /// Setting this parameter to true will cause all interfaces to be used irrespective of + /// the destination address /// - /// - /// As specified at https://www.w3.org/TR/webrtc/#rtcrtpsender-interface. - /// - public interface IRTCRtpSender - { - MediaStreamTrack track { get; } - //readonly attribute RTCDtlsTransport? transport; - //static RTCRtpCapabilities? getCapabilities(DOMString kind); - //Task setParameters(RTCRtpSendParameters parameters); - //RTCRtpSendParameters getParameters(); - //Task replaceTrack(MediaStreamTrack withTrack); - //void setStreams(MediaStream... streams); - //Task getStats(); - }; + public bool X_ICEIncludeAllInterfaceAddresses; /// - /// The RTCRtpReceiver interface allows an application to inspect the receipt of a MediaStreamTrack. + /// Set to true to use the RSA key in the certificate for the DTLS handshake. The default + /// is to use ECDSA. Chrome has defaulted to ECDSA since 2016 (see https://developer.chrome.com/blog/webrtc-ecdsa). /// - /// - /// As specified at https://www.w3.org/TR/webrtc/#rtcrtpreceiver-interface. - /// - public interface IRTCRtpReceiver - { - MediaStreamTrack track { get; } - //readonly attribute RTCDtlsTransport? transport; - //static RTCRtpCapabilities? getCapabilities(DOMString kind); - //RTCRtpReceiveParameters getParameters(); - //sequence getContributingSources(); - //sequence getSynchronizationSources(); - //Task getStats(); - }; + public bool X_UseRsaForDtlsCertificate; + + /// + /// Timeout for gathering local IP addresses + /// + public int X_GatherTimeoutMs = 30000; +} + +/// +/// Signalling states for a WebRTC peer connection. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#dom-rtcsignalingstate. +/// +public enum RTCSignalingState +{ + stable, + have_local_offer, + have_remote_offer, + have_local_pranswer, + have_remote_pranswer, + closed +} + +/// +/// The states a peer connection transitions through. +/// The difference between the IceConnectionState and the PeerConnectionState is somewhat subtle: +/// - IceConnectionState: applies to the connection checks amongst ICE candidates and is +/// set as completed as soon as a local and remote candidate have set their nominated candidate, +/// - PeerConnectionState: takes into account the IceConnectionState but also includes the DTLS +/// handshake and actions at the application layer such as a request to close the peer connection. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#rtcpeerconnectionstate-enum. +/// +public enum RTCPeerConnectionState +{ + closed, + failed, + disconnected, + @new, + connecting, + connected } + +public interface IRTCPeerConnection +{ + //IRTCPeerConnection(RTCConfiguration configuration = null); + RTCSessionDescriptionInit createOffer(RTCOfferOptions? options = null); + RTCSessionDescriptionInit? createAnswer(RTCAnswerOptions? options = null); + Task setLocalDescription(RTCSessionDescriptionInit description); + RTCSessionDescription? localDescription { get; } + RTCSessionDescription? currentLocalDescription { get; } + RTCSessionDescription? pendingLocalDescription { get; } + SetDescriptionResultEnum setRemoteDescription(RTCSessionDescriptionInit description); + RTCSessionDescription? remoteDescription { get; } + RTCSessionDescription? currentRemoteDescription { get; } + RTCSessionDescription? pendingRemoteDescription { get; } + void addIceCandidate(RTCIceCandidateInit candidate); + RTCSignalingState signalingState { get; } + RTCIceGatheringState iceGatheringState { get; } + RTCIceConnectionState iceConnectionState { get; } + RTCPeerConnectionState connectionState { get; } + bool canTrickleIceCandidates { get; } + void restartIce(); + RTCConfiguration getConfiguration(); + void setConfiguration(RTCConfiguration? configuration = null); + void close(); + event Action? onnegotiationneeded; + event Action onicecandidate; + event Action? onicecandidateerror; + event Action? onsignalingstatechange; + event Action? oniceconnectionstatechange; + event Action? onicegatheringstatechange; + event Action? onconnectionstatechange; + + // TODO: Extensions for the RTCMediaAPI + // https://www.w3.org/TR/webrtc/#rtcpeerconnection-interface-extensions. + //List getSenders(); + //List getReceivers(); + //List getTransceivers(); + //RTCRtpSender addTrack(MediaStreamTrack track, param MediaStream[] streams); + //void removeTrack(RTCRtpSender sender); + //RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind, + ////optional RTCRtpTransceiverInit init = {}); + //event ontrack; +}; + +/// +/// The RTCRtpSender interface allows an application to control how a given MediaStreamTrack +/// is encoded and transmitted to a remote peer. When setParameters is called on an +/// RTCRtpSender object, the encoding is changed appropriately. +/// +/// +/// As specified at https://www.w3.org/TR/webrtc/#rtcrtpsender-interface. +/// +public interface IRTCRtpSender +{ + MediaStreamTrack track { get; } + //readonly attribute RTCDtlsTransport? transport; + //static RTCRtpCapabilities? getCapabilities(DOMString kind); + //Task setParameters(RTCRtpSendParameters parameters); + //RTCRtpSendParameters getParameters(); + //Task replaceTrack(MediaStreamTrack withTrack); + //void setStreams(MediaStream... streams); + //Task getStats(); +}; + +/// +/// The RTCRtpReceiver interface allows an application to inspect the receipt of a MediaStreamTrack. +/// +/// +/// As specified at https://www.w3.org/TR/webrtc/#rtcrtpreceiver-interface. +/// +public interface IRTCRtpReceiver +{ + MediaStreamTrack track { get; } + //readonly attribute RTCDtlsTransport? transport; + //static RTCRtpCapabilities? getCapabilities(DOMString kind); + //RTCRtpReceiveParameters getParameters(); + //sequence getContributingSources(); + //sequence getSynchronizationSources(); + //Task getStats(); +}; diff --git a/src/SIPSorcery/net/WebRTC/RTCDataChannel.cs b/src/SIPSorcery/net/WebRTC/RTCDataChannel.cs index 261373d1d0..9715311d59 100644 --- a/src/SIPSorcery/net/WebRTC/RTCDataChannel.cs +++ b/src/SIPSorcery/net/WebRTC/RTCDataChannel.cs @@ -15,255 +15,266 @@ //----------------------------------------------------------------------------- using System; +using System.Diagnostics; using System.Text; using Microsoft.Extensions.Logging; -using SIPSorcery.Sys; -namespace SIPSorcery.Net -{ - /// - /// The assignments for SCTP payload protocol IDs used with - /// WebRTC data channels. - /// - /// - /// See https://tools.ietf.org/html/rfc8831#section-8 - /// - public enum DataChannelPayloadProtocols : uint - { - WebRTC_DCEP = 50, // Data Channel Establishment Protocol (DCEP). - WebRTC_String = 51, - WebRTC_Binary_Partial = 52, // Deprecated. - WebRTC_Binary = 53, - WebRTC_String_Partial = 54, // Deprecated. - WebRTC_String_Empty = 56, - WebRTC_Binary_Empty = 57 - } +namespace SIPSorcery.Net; - /// - /// A WebRTC data channel is generic transport service - /// that allows peers to exchange generic data in a peer - /// to peer manner. - /// - public class RTCDataChannel : IRTCDataChannel - { - private static readonly ILogger logger = LogFactory.CreateLogger(); +/// +/// The assignments for SCTP payload protocol IDs used with +/// WebRTC data channels. +/// +/// +/// See https://tools.ietf.org/html/rfc8831#section-8 +/// +public enum DataChannelPayloadProtocols : uint +{ + WebRTC_DCEP = 50, // Data Channel Establishment Protocol (DCEP). + WebRTC_String = 51, + WebRTC_Binary_Partial = 52, // Deprecated. + WebRTC_Binary = 53, + WebRTC_String_Partial = 54, // Deprecated. + WebRTC_String_Empty = 56, + WebRTC_Binary_Empty = 57 +} - public string label { get; set; } +/// +/// A WebRTC data channel is generic transport service +/// that allows peers to exchange generic data in a peer +/// to peer manner. +/// +public class RTCDataChannel : IRTCDataChannel +{ + private static readonly ILogger logger = LogFactory.CreateLogger(); - public bool ordered { get; set; } + public string? label { get; set; } - public ushort? maxPacketLifeTime { get; set; } + public bool ordered { get; set; } - public ushort? maxRetransmits { get; set; } + public ushort? maxPacketLifeTime { get; set; } - public string protocol { get; set; } + public ushort? maxRetransmits { get; set; } - public bool negotiated { get; set; } + public string? protocol { get; set; } - public ushort? id { get; set; } + public bool negotiated { get; set; } - public RTCDataChannelState readyState { get; internal set; } = RTCDataChannelState.connecting; + public ushort? id { get; set; } - public ulong bufferedAmount => _transport?.RTCSctpAssociation?.SendBufferedAmount ?? 0; + public RTCDataChannelState readyState { get; internal set; } = RTCDataChannelState.connecting; - public ulong bufferedAmountLowThreshold { get; set; } - public string binaryType { get; set; } + public ulong bufferedAmount => _transport?.RTCSctpAssociation?.SendBufferedAmount ?? 0; - //public long MaxMessageSize { get; set; } + public ulong bufferedAmountLowThreshold { get; set; } + public string? binaryType { get; set; } - public string Error { get; private set; } + //public long MaxMessageSize { get; set; } - public bool IsOpened { get; internal set; } = false; + public string? Error { get; private set; } - private RTCSctpTransport _transport; + public bool IsOpened { get; internal set; } - public event Action onopen; - //public event Action onbufferedamountlow; - public event Action onerror; - //public event Action onclosing; - public event Action onclose; - public event OnDataChannelMessageDelegate onmessage; + private RTCSctpTransport _transport; - public RTCDataChannel(RTCSctpTransport transport, RTCDataChannelInit init = null) - { - _transport = transport; + public event Action? onopen; + //public event Action onbufferedamountlow; + public event Action? onerror; + //public event Action onclosing; + public event Action? onclose; + public event OnDataChannelMessageDelegate? onmessage; - if (init == null) { - ordered = true; - return; - } - // TODO: Utilize ordered, maxPacketLifeTime, maxRetransmits, and protocol; - ordered = init.ordered ?? true; - maxPacketLifeTime = init.maxPacketLifeTime; - maxRetransmits = init.maxRetransmits; - protocol = init.protocol ?? ""; - negotiated = init.negotiated ?? false; - id = init.id; - } + public RTCDataChannel(RTCSctpTransport transport, RTCDataChannelInit? init = null) + { + _transport = transport; - internal void GotAck() + if (init is null) { - logger.LogDebug("Data channel for label {label} now open.", label); - IsOpened = true; - readyState = RTCDataChannelState.open; - onopen?.Invoke(); + ordered = true; + return; } + // TODO: Utilize ordered, maxPacketLifeTime, maxRetransmits, and protocol; + ordered = init.ordered ?? true; + maxPacketLifeTime = init.maxPacketLifeTime; + maxRetransmits = init.maxRetransmits; + protocol = init.protocol ?? ""; + negotiated = init.negotiated ?? false; + id = init.id; + } + + internal void GotAck() + { + logger.LogWebRtcDataChannelOpen(label); + IsOpened = true; + readyState = RTCDataChannelState.open; + onopen?.Invoke(); + } + + /// + /// Sets the error message is there was a problem creating the data channel. + /// + internal void SetError(string error) + { + Error = error; + onerror?.Invoke(error); + } - /// - /// Sets the error message is there was a problem creating the data channel. - /// - internal void SetError(string error) + public void close() + { + IsOpened = false; + readyState = RTCDataChannelState.closed; + logger.LogWebRtcDataChannelClose(id); + onclose?.Invoke(); + } + + /// + /// Sends a string data payload on the data channel. + /// + /// The string message to send. + public void send(string message) + { + if (message is { } && Encoding.UTF8.GetByteCount(message) > _transport.maxMessageSize) { - Error = error; - onerror?.Invoke(error); + throw new SipSorceryException( + $"Data channel {label} was requested to send data of length {Encoding.UTF8.GetByteCount(message)} that exceeded the maximum allowed message size of {_transport.maxMessageSize}."); } - - public void close() + else if (_transport.state != RTCSctpTransportState.Connected) { - IsOpened = false; - readyState = RTCDataChannelState.closed; - logger.LogDebug("Data channel with id {id} has been closed", id); - onclose?.Invoke(); + logger.LogWebRtcDataChannelSendFailed(_transport.state); } - - /// - /// Sends a string data payload on the data channel. - /// - /// The string message to send. - public void send(string message) + else { - if (message != null && Encoding.UTF8.GetByteCount(message) > _transport.maxMessageSize) - { - throw new ApplicationException($"Data channel {label} was requested to send data of length {Encoding.UTF8.GetByteCount(message)} that exceeded the maximum allowed message size of {_transport.maxMessageSize}."); - } - else if (_transport.state != RTCSctpTransportState.Connected) - { - logger.LogWarning("WebRTC data channel send failed due to SCTP transport in state {TransportState}.", _transport.state); - } - else + lock (this) { - lock (this) + if (string.IsNullOrEmpty(message)) + { + _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), + (uint)DataChannelPayloadProtocols.WebRTC_String_Empty, + new byte[] { 0x00 }); + } + else { - if (string.IsNullOrEmpty(message)) - { - _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), - (uint)DataChannelPayloadProtocols.WebRTC_String_Empty, - new byte[] { 0x00 }); - } - else - { - _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), - (uint)DataChannelPayloadProtocols.WebRTC_String, - Encoding.UTF8.GetBytes(message)); - } + _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), + (uint)DataChannelPayloadProtocols.WebRTC_String, + Encoding.UTF8.GetBytes(message)); } } } + } - /// - /// Sends a binary data payload on the data channel. - /// - /// The data to send. - /// The offset in at which to begin sending. Defaults to 0. - /// The number of bytes to send. Defaults to -1, meaning all bytes from to the end of the array. - public void send(byte[] data, int offset = 0, int count = -1) - { - int effectiveCount = count < 0 ? data.Length - offset : count; + /// + /// Sends a binary data payload on the data channel. + /// + /// The data to send. + /// The offset in at which to begin sending. Defaults to 0. + /// The number of bytes to send. Defaults to -1, meaning all bytes from to the end of the array. + public void send(byte[] data, int offset = 0, int count = -1) + { + int effectiveCount = count < 0 ? data.Length - offset : count; - if (effectiveCount > _transport.maxMessageSize) - { - throw new ApplicationException($"Data channel {label} was requested to send data of length {effectiveCount} that exceeded the maximum allowed message size of {_transport.maxMessageSize}."); - } - else if (_transport.state != RTCSctpTransportState.Connected) - { - logger.LogWarning("WebRTC data channel send failed due to SCTP transport in state {TransportState}.", _transport.state); - } - else + if (effectiveCount > _transport.maxMessageSize) + { + throw new ApplicationException( + $"Data channel {label} was requested to send data of length {effectiveCount} that exceeded the maximum allowed message size of {_transport.maxMessageSize}."); + } + else if (_transport.state != RTCSctpTransportState.Connected) + { + logger.LogWebRtcDataChannelSendFailed(_transport.state); + } + else + { + lock (this) { - lock (this) + if (effectiveCount == 0) { - if (effectiveCount == 0) - { - _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), - (uint)DataChannelPayloadProtocols.WebRTC_Binary_Empty, - new byte[] { 0x00 }); - } - else - { - _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), - (uint)DataChannelPayloadProtocols.WebRTC_Binary, - data, offset, effectiveCount); - } + _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), + (uint)DataChannelPayloadProtocols.WebRTC_Binary_Empty, + new byte[] { 0x00 }); + } + else + { + Debug.Assert(data is { }); + _transport.RTCSctpAssociation.SendData( + id.GetValueOrDefault(), + (uint)DataChannelPayloadProtocols.WebRTC_Binary, + data, + offset, + effectiveCount); } } } + } - /// - /// Sends an OPEN Data Channel Establishment Protocol (DCEP) message - /// to open a data channel on the remote peer for send/receive. - /// - internal void SendDcepOpen() + /// + /// Sends an OPEN Data Channel Establishment Protocol (DCEP) message + /// to open a data channel on the remote peer for send/receive. + /// + internal void SendDcepOpen() + { + var type = (byte)DataChannelTypes.DATA_CHANNEL_RELIABLE; + if (!ordered) { - byte type = (byte)DataChannelTypes.DATA_CHANNEL_RELIABLE; - if (!ordered) - { - type += (byte)DataChannelTypes.DATA_CHANNEL_RELIABLE_UNORDERED; - } - if (maxPacketLifeTime > 0) - { - type += (byte)DataChannelTypes.DATA_CHANNEL_PARTIAL_RELIABLE_TIMED; - } - else if(maxRetransmits > 0) - { - type += (byte)DataChannelTypes.DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT; - } - - var dcepOpen = new DataChannelOpenMessage() - { - MessageType = (byte)DataChannelMessageTypes.OPEN, - ChannelType = (byte)type, - Label = label, - Protocol = protocol, - }; - - lock (this) - { - _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), - (uint)DataChannelPayloadProtocols.WebRTC_DCEP, - dcepOpen.GetBytes()); - } + type += (byte)DataChannelTypes.DATA_CHANNEL_RELIABLE_UNORDERED; + } + if (maxPacketLifeTime > 0) + { + type += (byte)DataChannelTypes.DATA_CHANNEL_PARTIAL_RELIABLE_TIMED; + } + else if (maxRetransmits > 0) + { + type += (byte)DataChannelTypes.DATA_CHANNEL_PARTIAL_RELIABLE_REXMIT; } - /// - /// Sends an ACK response for a Data Channel Establishment Protocol (DCEP) - /// control message. - /// - internal void SendDcepAck() + Debug.Assert(label is { }); + var dcepOpen = new DataChannelOpenMessage() { - lock (this) - { - _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), - (uint)DataChannelPayloadProtocols.WebRTC_DCEP, - new byte[] { (byte)DataChannelMessageTypes.ACK }); - } + MessageType = (byte)DataChannelMessageTypes.OPEN, + ChannelType = (byte)type, + Label = label, + Protocol = protocol, + }; + + lock (this) + { + var payload = new byte[dcepOpen.GetByteCount()]; + _ = dcepOpen.WriteBytes(payload.AsSpan()); + + _transport.RTCSctpAssociation.SendData( + id.GetValueOrDefault(), + (uint)DataChannelPayloadProtocols.WebRTC_DCEP, + payload); } + } - /// - /// Event handler for an SCTP data chunk being received for this data channel. - /// - internal void GotData(ushort streamID, ushort streamSeqNum, uint ppID, byte[] data) + /// + /// Sends an ACK response for a Data Channel Establishment Protocol (DCEP) + /// control message. + /// + internal void SendDcepAck() + { + lock (this) { - //logger.LogTrace($"WebRTC data channel GotData stream ID {streamID}, stream seqnum {streamSeqNum}, ppid {ppID}, label {label}."); + _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), + (uint)DataChannelPayloadProtocols.WebRTC_DCEP, + new byte[] { (byte)DataChannelMessageTypes.ACK }); + } + } - // If the ppID is not recognised default to binary. - DataChannelPayloadProtocols payloadType = DataChannelPayloadProtocols.WebRTC_Binary; + /// + /// Event handler for an SCTP data chunk being received for this data channel. + /// + internal void GotData(ushort streamID, ushort streamSeqNum, uint ppID, byte[] data) + { + //logger.LogWebRtcDcepDataChunk(streamID, streamSeqNum, ppID, label); - if (Enum.IsDefined(typeof(DataChannelPayloadProtocols), ppID)) - { - payloadType = (DataChannelPayloadProtocols)ppID; - } + // If the ppID is not recognised default to binary. + DataChannelPayloadProtocols payloadType = DataChannelPayloadProtocols.WebRTC_Binary; - onmessage?.Invoke(this, (DataChannelPayloadProtocols)ppID, data); + if (DataChannelPayloadProtocolsExtensions.IsDefined((DataChannelPayloadProtocols)ppID)) + { + payloadType = (DataChannelPayloadProtocols)ppID; } + + onmessage?.Invoke(this, (DataChannelPayloadProtocols)ppID, data); } } diff --git a/src/SIPSorcery/net/WebRTC/RTCDataChannelCollection.cs b/src/SIPSorcery/net/WebRTC/RTCDataChannelCollection.cs index eab23829be..c089958b3c 100644 --- a/src/SIPSorcery/net/WebRTC/RTCDataChannelCollection.cs +++ b/src/SIPSorcery/net/WebRTC/RTCDataChannelCollection.cs @@ -1,94 +1,100 @@ -using System; +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +internal sealed class RTCDataChannelCollection : IReadOnlyCollection { - class RTCDataChannelCollection : IReadOnlyCollection - { - readonly ConcurrentBag pendingChannels = new ConcurrentBag(); - readonly ConcurrentDictionary activeChannels = new ConcurrentDictionary(); - readonly Func useEvenIds; - - readonly object idSyncObj = new object(); - ushort lastChannelId = ushort.MaxValue - 1; + private readonly ConcurrentBag pendingChannels = new ConcurrentBag(); + private readonly ConcurrentDictionary activeChannels = new ConcurrentDictionary(); + private readonly Func useEvenIds; + + private readonly object idSyncObj = new object(); + private ushort lastChannelId = ushort.MaxValue - 1; - public int Count => pendingChannels.Count + activeChannels.Count; + public int Count => pendingChannels.Count + activeChannels.Count; - public RTCDataChannelCollection(Func useEvenIds) - => this.useEvenIds = useEvenIds; + public RTCDataChannelCollection(Func useEvenIds) + => this.useEvenIds = useEvenIds; - public void AddPendingChannel(RTCDataChannel channel) - => pendingChannels.Add(channel); + public void AddPendingChannel(RTCDataChannel channel) + => pendingChannels.Add(channel); - public IEnumerable ActivatePendingChannels() + public IEnumerable ActivatePendingChannels() + { + while (pendingChannels.TryTake(out var channel)) { - while (pendingChannels.TryTake(out var channel)) - { - AddActiveChannel(channel); - yield return channel; - } + AddActiveChannel(channel); + yield return channel; } - - public bool TryGetChannel(ushort dataChannelID, out RTCDataChannel result) - => activeChannels.TryGetValue(dataChannelID, out result); - - public bool AddActiveChannel(RTCDataChannel channel) + } + + public bool TryGetChannel(ushort dataChannelID, [MaybeNullWhen(false)] out RTCDataChannel? result) + => activeChannels.TryGetValue(dataChannelID, out result); + + public bool AddActiveChannel(RTCDataChannel channel) + { + if (channel.id.HasValue) { - if (channel.id.HasValue) + if (!activeChannels.TryAdd(channel.id.Value, channel)) { - if (!activeChannels.TryAdd(channel.id.Value, channel)) - return false; + return false; } - else + } + else + { + while (true) { - while (true) + var id = GetNextChannelID(); + if (activeChannels.TryAdd(id, channel)) { - var id = GetNextChannelID(); - if (activeChannels.TryAdd(id, channel)) - { - channel.id = id; - break; - } + channel.id = id; + break; } } + } - channel.onclose += OnClose; - channel.onerror += OnError; - return true; - - void OnClose() - { - channel.onclose -= OnClose; - channel.onerror -= OnError; - activeChannels.TryRemove(channel.id.Value, out _); - } - void OnError(string error) => OnClose(); + channel.onclose += OnClose; + channel.onerror += OnError; + return true; + + void OnClose() + { + channel.onclose -= OnClose; + channel.onerror -= OnError; + activeChannels.TryRemove(channel.id.Value, out _); } - - ushort GetNextChannelID() + void OnError(string error) => OnClose(); + } + + private ushort GetNextChannelID() + { + lock (idSyncObj) { - lock (idSyncObj) + unchecked { - unchecked + // The SCTP stream identifier 65535 is reserved due to SCTP INIT and + // INIT - ACK chunks only allowing a maximum of 65535 streams to be + // negotiated(0 - 65534) - https://tools.ietf.org/html/rfc8832 + if (lastChannelId == ushort.MaxValue - 3) + { + lastChannelId += 4; + } + else { - // The SCTP stream identifier 65535 is reserved due to SCTP INIT and - // INIT - ACK chunks only allowing a maximum of 65535 streams to be - // negotiated(0 - 65534) - https://tools.ietf.org/html/rfc8832 - if (lastChannelId == ushort.MaxValue - 3) - lastChannelId += 4; - else - lastChannelId += 2; + lastChannelId += 2; } - return useEvenIds() ? lastChannelId : (ushort) (lastChannelId + 1); } + return useEvenIds() ? lastChannelId : (ushort)(lastChannelId + 1); } + } - public IEnumerator GetEnumerator() - => pendingChannels.Concat(activeChannels.Select(e => e.Value)).GetEnumerator(); + public IEnumerator GetEnumerator() + => pendingChannels.Concat(activeChannels.Select(static e => e.Value)).GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs b/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs index a874d13f51..ba507f6702 100644 --- a/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs +++ b/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs @@ -35,1927 +35,2113 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using SIPSorcery.SIP.App; -using SIPSorcery.Sys; using Org.BouncyCastle.Tls; using Org.BouncyCastle.Tls.Crypto.Impl.BC; using SIPSorcery.Net.SharpSRTP.DTLS; using SIPSorcery.Net.SharpSRTP.DTLSSRTP; +using SIPSorcery.SIP.App; +using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// Initialiser for the RTCSessionDescription instance. +/// +/// +/// As specified in https://www.w3.org/TR/webrtc/#rtcsessiondescription-class. +/// +public class RTCSessionDescriptionInit { /// - /// Initialiser for the RTCSessionDescription instance. + /// The type of the Session Description. /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#rtcsessiondescription-class. - /// - public class RTCSessionDescriptionInit + [JsonPropertyName("type")] + public required RTCSdpType type { get; init; } + + /// + /// A string representation of the Session Description. + /// + [JsonPropertyName("sdp")] + public required string sdp { get; init; } + + public string toJSON() { - /// - /// The type of the Session Description. - /// - public RTCSdpType type { get; set; } + return JsonSerializer.Serialize(this, SipSorceryJsonSerializerContext.Default.RTCSessionDescriptionInit); + } - /// - /// A string representation of the Session Description. - /// - public string sdp { get; set; } + public static bool TryParse(ReadOnlySpan json, [NotNullWhen(true)] out RTCSessionDescriptionInit? init) + { + init = null; - public string toJSON() + if (json.IsEmptyOrWhiteSpace()) { - return TinyJson.JSONWriter.ToJson(this); + return false; } - public static bool TryParse(string json, out RTCSessionDescriptionInit init) - { - init = null; + // System.Text.Json rejects unescaped CR/LF inside JSON string values. + // Single pass: only rent a buffer and backfill when the first offending + // character is encountered inside a JSON string. + char[]? rented = null; + int pos = 0; + bool inString = false; + bool escaped = false; - if (string.IsNullOrWhiteSpace(json)) + try + { + for (int i = 0; i < json.Length; i++) { - return false; + char c = json[i]; + + if (escaped) + { + if (rented is not null) + { + rented[pos++] = c; + } + + escaped = false; + continue; + } + + if (inString) + { + if (c is '\r' or '\n' && rented is null) + { + // First offending char: rent and backfill everything before this point. + rented = ArrayPool.Shared.Rent(json.Length * 2); + json[..i].CopyTo(rented); + pos = i; + } + + if (rented is not null) + { + switch (c) + { + case '\\': + rented[pos++] = c; + escaped = true; + break; + case '"': + rented[pos++] = c; + inString = false; + break; + case '\r': + rented[pos++] = '\\'; + rented[pos++] = 'r'; + break; + case '\n': + rented[pos++] = '\\'; + rented[pos++] = 'n'; + break; + default: + rented[pos++] = c; + break; + } + } + else + { + switch (c) + { + case '\\': escaped = true; break; + case '"': inString = false; break; + } + } + } + else + { + if (c == '"') + { + inString = true; + } + + if (rented is not null) + { + rented[pos++] = c; + } + } } - else - { - init = TinyJson.JSONParser.FromJson(json); - // To qualify as parsed all required fields must be set. - return init != null && - init.sdp != null; + ReadOnlySpan toParse = rented is not null + ? rented.AsSpan(0, pos) + : json; + + init = JsonSerializer.Deserialize(toParse, SipSorceryJsonSerializerContext.Default.RTCSessionDescriptionInit); + } + finally + { + if (rented is not null) + { + ArrayPool.Shared.Return(rented); } } + + // To qualify as parsed all required fields must be set. + return init is { } && + init.sdp is { }; } +} + +/// +/// Represents a WebRTC RTCPeerConnection. +/// +/// +/// Interface is defined in https://www.w3.org/TR/webrtc/#interface-definition. +/// The Session Description offer/answer mechanisms are detailed in +/// https://tools.ietf.org/html/rfc8829 "JavaScript Session Establishment Protocol (JSEP)". +/// +public class RTCPeerConnection : RTPSession, IRTCPeerConnection +{ + // SDP constants. + //private new const string RTP_MEDIA_PROFILE = "RTP/SAVP"; + private const string RTP_MEDIA_NON_FEEDBACK_PROFILE = "UDP/TLS/RTP/SAVP"; + private const string RTP_MEDIA_FEEDBACK_PROFILE = "UDP/TLS/RTP/SAVPF"; + private const string RTP_MEDIA_DATACHANNEL_DTLS_PROFILE = "DTLS/SCTP"; // Legacy. + private const string RTP_MEDIA_DATACHANNEL_UDPDTLS_PROFILE = "UDP/DTLS/SCTP"; + private const string SDP_DATACHANNEL_FORMAT_ID = "webrtc-datachannel"; + private const string RTCP_MUX_ATTRIBUTE = "a=rtcp-mux"; // Indicates the media announcement is using multiplexed RTCP. + private const string BUNDLE_ATTRIBUTE = "BUNDLE"; + private const string ICE_OPTIONS = "ice2,trickle"; // Supported ICE options. + private const string NORMAL_CLOSE_REASON = "normal"; + private const ushort SCTP_DEFAULT_PORT = 5000; /// - /// Represents a WebRTC RTCPeerConnection. + /// The period to wait for the SCTP association to complete before giving up. + /// In theory this should be very quick as the DTLS connection should already have been established + /// and the SCTP logic only needs to send the small handshake messages to establish + /// the association. /// - /// - /// Interface is defined in https://www.w3.org/TR/webrtc/#interface-definition. - /// The Session Description offer/answer mechanisms are detailed in - /// https://tools.ietf.org/html/rfc8829 "JavaScript Session Establishment Protocol (JSEP)". - /// - public class RTCPeerConnection : RTPSession, IRTCPeerConnection - { - // SDP constants. - //private new const string RTP_MEDIA_PROFILE = "RTP/SAVP"; - private const string RTP_MEDIA_NON_FEEDBACK_PROFILE = "UDP/TLS/RTP/SAVP"; - private const string RTP_MEDIA_FEEDBACK_PROFILE = "UDP/TLS/RTP/SAVPF"; - private const string RTP_MEDIA_DATACHANNEL_DTLS_PROFILE = "DTLS/SCTP"; // Legacy. - private const string RTP_MEDIA_DATACHANNEL_UDPDTLS_PROFILE = "UDP/DTLS/SCTP"; - private const string SDP_DATACHANNEL_FORMAT_ID = "webrtc-datachannel"; - private const string RTCP_MUX_ATTRIBUTE = "a=rtcp-mux"; // Indicates the media announcement is using multiplexed RTCP. - private const string BUNDLE_ATTRIBUTE = "BUNDLE"; - private const string ICE_OPTIONS = "ice2,trickle"; // Supported ICE options. - private const string NORMAL_CLOSE_REASON = "normal"; - private const ushort SCTP_DEFAULT_PORT = 5000; + private const int SCTP_ASSOCIATE_TIMEOUT_SECONDS = 2; - /// - /// The period to wait for the SCTP association to complete before giving up. - /// In theory this should be very quick as the DTLS connection should already have been established - /// and the SCTP logic only needs to send the small handshake messages to establish - /// the association. - /// - private const int SCTP_ASSOCIATE_TIMEOUT_SECONDS = 2; + private new readonly string RTP_MEDIA_PROFILE = RTP_MEDIA_NON_FEEDBACK_PROFILE; + private readonly string RTCP_ATTRIBUTE = $"a=rtcp:{SDP.IGNORE_RTP_PORT_NUMBER} IN IP4 0.0.0.0"; - private new readonly string RTP_MEDIA_PROFILE = RTP_MEDIA_NON_FEEDBACK_PROFILE; - private readonly string RTCP_ATTRIBUTE = $"a=rtcp:{SDP.IGNORE_RTP_PORT_NUMBER} IN IP4 0.0.0.0"; + public string SessionID { get; private set; } + public string? SdpSessionID { get; private set; } + public string LocalSdpSessionID { get; private set; } - public string SessionID { get; private set; } - public string SdpSessionID { get; private set; } - public string LocalSdpSessionID { get; private set; } + private RtpIceChannel _rtpIceChannel; - private RtpIceChannel _rtpIceChannel; + private readonly RTCDataChannelCollection _dataChannels; + public IReadOnlyCollection DataChannels => _dataChannels; - private readonly RTCDataChannelCollection _dataChannels; - public IReadOnlyCollection DataChannels => _dataChannels; + private Org.BouncyCastle.Tls.Certificate _dtlsCertificate; + private Org.BouncyCastle.Crypto.AsymmetricKeyParameter? _dtlsPrivateKey; + private BcTlsCrypto _crypto; + private DtlsSrtpTransport? _dtlsHandle; + private Task _iceInitiateGatheringTask; + private readonly TaskCompletionSource _iceCompletedGatheringTask = new(); - private Org.BouncyCastle.Tls.Certificate _dtlsCertificate; - private Org.BouncyCastle.Crypto.AsymmetricKeyParameter _dtlsPrivateKey; - private BcTlsCrypto _crypto; - private DtlsSrtpTransport _dtlsHandle; - private Task _iceInitiateGatheringTask; - private readonly TaskCompletionSource _iceCompletedGatheringTask = new(); + private Dictionary? _rtpExtensionsUsed; // < Uri, Id> - private Dictionary _rtpExtensionsUsed; // < Uri, Id> - - /// - /// Local ICE candidates that have been supplied directly by the application. - /// Useful for cases where the application may has extra information about the - /// network set up such as 1:1 NATs as used by Azure and AWS. - /// - private List _applicationIceCandidates = new List(); + /// + /// Local ICE candidates that have been supplied directly by the application. + /// Useful for cases where the application may has extra information about the + /// network set up such as 1:1 NATs as used by Azure and AWS. + /// + private List _applicationIceCandidates = new List(); - /// - /// The ICE role the peer is acting in. - /// - public IceRolesEnum IceRole { get; set; } = IceRolesEnum.actpass; + /// + /// The ICE role the peer is acting in. + /// + public IceRolesEnum IceRole { get; set; } = IceRolesEnum.actpass; - /// - /// The DTLS fingerprint supplied by the remote peer in their SDP. Needs to be checked - /// that the certificate supplied during the DTLS handshake matches. - /// - public RTCDtlsFingerprint RemotePeerDtlsFingerprint { get; private set; } + /// + /// The DTLS fingerprint supplied by the remote peer in their SDP. Needs to be checked + /// that the certificate supplied during the DTLS handshake matches. + /// + public RTCDtlsFingerprint? RemotePeerDtlsFingerprint { get; private set; } - public string DtlsCertificateSignatureAlgorithm { get; private set; } = string.Empty; + public string DtlsCertificateSignatureAlgorithm { get; private set; } = string.Empty; - public bool IsDtlsNegotiationComplete { get; private set; } = false; + public bool IsDtlsNegotiationComplete { get; private set; } - public RTCSessionDescription localDescription { get; private set; } + public RTCSessionDescription? localDescription { get; private set; } - public RTCSessionDescription remoteDescription { get; private set; } + public RTCSessionDescription? remoteDescription { get; private set; } - public RTCSessionDescription currentLocalDescription => localDescription; + public RTCSessionDescription? currentLocalDescription => localDescription; - public RTCSessionDescription pendingLocalDescription => null; + public RTCSessionDescription? pendingLocalDescription => null; - public RTCSessionDescription currentRemoteDescription => remoteDescription; + public RTCSessionDescription? currentRemoteDescription => remoteDescription; - public RTCSessionDescription pendingRemoteDescription => null; + public RTCSessionDescription? pendingRemoteDescription => null; - public RTCSignalingState signalingState { get; private set; } = RTCSignalingState.closed; + public RTCSignalingState signalingState { get; private set; } = RTCSignalingState.closed; - public RTCIceGatheringState iceGatheringState + public RTCIceGatheringState iceGatheringState + { + get { - get - { - return _rtpIceChannel != null ? _rtpIceChannel.IceGatheringState : RTCIceGatheringState.@new; - } + return _rtpIceChannel is { } ? _rtpIceChannel.IceGatheringState : RTCIceGatheringState.@new; } + } - public RTCIceConnectionState iceConnectionState + public RTCIceConnectionState iceConnectionState + { + get { - get - { - return _rtpIceChannel != null ? _rtpIceChannel.IceConnectionState : RTCIceConnectionState.@new; - } + return _rtpIceChannel is { } ? _rtpIceChannel.IceConnectionState : RTCIceConnectionState.@new; } + } - public RTCPeerConnectionState connectionState { get; private set; } = RTCPeerConnectionState.@new; + public RTCPeerConnectionState connectionState { get; private set; } = RTCPeerConnectionState.@new; - public bool canTrickleIceCandidates { get => true; } + public bool canTrickleIceCandidates { get => true; } - private RTCConfiguration _configuration; + private RTCConfiguration _configuration; - /// - /// The fingerprint of the certificate being used to negotiate the DTLS handshake with the - /// remote peer. - /// - public RTCDtlsFingerprint DtlsCertificateFingerprint { get; private set; } + /// + /// The fingerprint of the certificate being used to negotiate the DTLS handshake with the + /// remote peer. + /// + public RTCDtlsFingerprint DtlsCertificateFingerprint { get; private set; } - /// - /// The SCTP transport over which SCTP data is sent and received. - /// - /// - /// WebRTC API definition: - /// https://www.w3.org/TR/webrtc/#attributes-15 - /// - public RTCSctpTransport sctp { get; private set; } + /// + /// The SCTP transport over which SCTP data is sent and received. + /// + /// + /// WebRTC API definition: + /// https://www.w3.org/TR/webrtc/#attributes-15 + /// + public RTCSctpTransport sctp { get; private set; } - /// - /// Informs the application that session negotiation needs to be done (i.e. a createOffer call - /// followed by setLocalDescription). - /// - public event Action onnegotiationneeded; + /// + /// Informs the application that session negotiation needs to be done (i.e. a createOffer call + /// followed by setLocalDescription). + /// + public event Action? onnegotiationneeded; - private event Action _onIceCandidate; - /// - /// A new ICE candidate is available for the Peer Connection. - /// - public event Action onicecandidate + private event Action? _onIceCandidate; + /// + /// A new ICE candidate is available for the Peer Connection. + /// + public event Action onicecandidate + { + add { - add + var notifyIce = _onIceCandidate is null && value is { }; + _onIceCandidate += value; + if (notifyIce) { - var notifyIce = _onIceCandidate == null && value != null; - _onIceCandidate += value; - if (notifyIce) + foreach (var ice in _rtpIceChannel.Candidates) { - foreach (var ice in _rtpIceChannel.Candidates) - { - _onIceCandidate?.Invoke(ice); - } + _onIceCandidate?.Invoke(ice); } } - remove - { - _onIceCandidate -= value; - } } + remove + { + _onIceCandidate -= value; + } + } - protected CancellationTokenSource _cancellationSource = new CancellationTokenSource(); - protected object _renegotiationLock = new object(); - protected volatile bool _requireRenegotiation = true; + protected CancellationTokenSource? _cancellationSource = new CancellationTokenSource(); + protected object _renegotiationLock = new object(); + protected volatile bool _requireRenegotiation = true; - public override bool RequireRenegotiation + public override bool RequireRenegotiation + { + get { - get - { - return _requireRenegotiation; - } + return _requireRenegotiation; + } - protected internal set + protected internal set + { + lock (_renegotiationLock) { - lock (_renegotiationLock) - { - _requireRenegotiation = value; - - // RemoteDescription is intentionally preserved during renegotiation. - // createBaseSdp() needs it to maintain the m-line order from the - // previous offer/answer exchange (RFC 3264 §8). - // if (_requireRenegotiation) - // { - // RemoteDescription = null; - // } - } + _requireRenegotiation = value; - //Remove NegotiationTask when state not stable - if (!_requireRenegotiation || signalingState != RTCSignalingState.stable) - { - CancelOnNegotiationNeededTask(); - } - //Call Renegotiation Delayed (We need to wait as user can try add multiple tracks in sequence) - else - { - StartOnNegotiationNeededTask(); - } + // RemoteDescription is intentionally preserved during renegotiation. + // createBaseSdp() needs it to maintain the m-line order from the + // previous offer/answer exchange (RFC 3264 §8). + // if (_requireRenegotiation) + // { + // RemoteDescription = null; + // } } - } - /// - /// A failure occurred when gathering ICE candidates. - /// - public event Action onicecandidateerror; - - /// - /// The signaling state has changed. This state change is the result of either setLocalDescription or - /// setRemoteDescription being invoked. - /// - public event Action onsignalingstatechange; - - /// - /// This Peer Connection's ICE connection state has changed. - /// - public event Action oniceconnectionstatechange; - - /// - /// This Peer Connection's ICE gathering state has changed. - /// - public event Action onicegatheringstatechange; - - /// - /// The state of the peer connection. A state of connected means the ICE checks have - /// succeeded and the DTLS handshake has completed. Once in the connected state it's - /// suitable for media packets can be exchanged. - /// - public event Action onconnectionstatechange; - - /// - /// Fires when a new data channel is created by the remote peer. - /// - public event Action ondatachannel; - - /// - /// Constructor to create a new RTC peer connection instance. - /// - public RTCPeerConnection() : - this(null) - { } - - /// - /// Constructor to create a new RTC peer connection instance. - /// - /// Optional. - public RTCPeerConnection(RTCConfiguration configuration, int bindPort = 0, PortRange portRange = null, Boolean videoAsPrimary = false) : - base(true, true, true, configuration?.X_BindAddress, bindPort, portRange) - { - _crypto = new BcTlsCrypto(); - _dataChannels = new RTCDataChannelCollection(useEvenIds: () => _dtlsHandle.IsClient); - - if (_configuration != null && - _configuration.iceTransportPolicy == RTCIceTransportPolicy.relay && - _configuration.iceServers?.Count == 0) + //Remove NegotiationTask when state not stable + if (!_requireRenegotiation || signalingState != RTCSignalingState.stable) { - throw new ApplicationException("RTCPeerConnection must have at least one ICE server specified for a relay only transport policy."); + CancelOnNegotiationNeededTask(); } - - if (configuration != null) + //Call Renegotiation Delayed (We need to wait as user can try add multiple tracks in sequence) + else { - _configuration = configuration; + StartOnNegotiationNeededTask(); + } + } + } - //test turns: - //_configuration.iceTransportPolicy = RTCIceTransportPolicy.relay; - //_configuration.iceServers = _configuration.iceServers.Where(p => p.urls.StartsWith("turns")).ToList(); + /// + /// A failure occurred when gathering ICE candidates. + /// + public event Action? onicecandidateerror; - if (!InitializeCertificates(configuration)) - { - logger.LogDebug("No DTLS certificate is provided in the configuration"); - } + /// + /// The signaling state has changed. This state change is the result of either setLocalDescription or + /// setRemoteDescription being invoked. + /// + public event Action? onsignalingstatechange; - if (_configuration.X_UseRtpFeedbackProfile) - { - RTP_MEDIA_PROFILE = RTP_MEDIA_FEEDBACK_PROFILE; - } - } - else - { - _configuration = new RTCConfiguration(); - } + /// + /// This Peer Connection's ICE connection state has changed. + /// + public event Action? oniceconnectionstatechange; - if (_dtlsCertificate == null) - { - // No certificate was provided so create a new self signed one. - (_dtlsCertificate, _dtlsPrivateKey) = DtlsUtils.CreateSelfSignedTlsCert(_crypto, useRsa: _configuration.X_UseRsaForDtlsCertificate); - } + /// + /// This Peer Connection's ICE gathering state has changed. + /// + public event Action? onicegatheringstatechange; - DtlsCertificateFingerprint = DtlsUtils.Fingerprint(_dtlsCertificate); + /// + /// The state of the peer connection. A state of connected means the ICE checks have + /// succeeded and the DTLS handshake has completed. Once in the connected state it's + /// suitable for media packets can be exchanged. + /// + public event Action? onconnectionstatechange; + + /// + /// Fires when a new data channel is created by the remote peer. + /// + public event Action? ondatachannel; + + /// + /// Constructor to create a new RTC peer connection instance. + /// + public RTCPeerConnection() : + this(null) + { } - SessionID = Guid.NewGuid().ToString(); - LocalSdpSessionID = Crypto.GetRandomInt(5).ToString(); + /// + /// Constructor to create a new RTC peer connection instance. + /// + /// Optional. + public RTCPeerConnection(RTCConfiguration? configuration, int bindPort = 0, PortRange? portRange = null, bool videoAsPrimary = false) : + base(true, true, true, configuration?.X_BindAddress, bindPort, portRange) + { + _crypto = new BcTlsCrypto(); + _dataChannels = new RTCDataChannelCollection(useEvenIds: () => + { + Debug.Assert(_dtlsHandle is { }); + return _dtlsHandle.IsClient; + }); - // Request the underlying RTP session to create a single RTP channel that will - // be used to multiplex all required media streams. - addSingleTrack(videoAsPrimary); + if (_configuration is { } && + _configuration.iceTransportPolicy == RTCIceTransportPolicy.relay && + _configuration.iceServers?.Count == 0) + { + throw new SipSorceryException("RTCPeerConnection must have at least one ICE server specified for a relay only transport policy."); + } + + if (configuration is { }) + { + _configuration = configuration; - _rtpIceChannel = GetRtpChannel(); + //test turns: + //_configuration.iceTransportPolicy = RTCIceTransportPolicy.relay; + //_configuration.iceServers = _configuration.iceServers.Where(p => p.urls.StartsWith("turns")).ToList(); - // Propagate any translator that was set before the channel existed. - if (_remoteEndpointTranslator != null) + if (!InitializeCertificates(configuration)) { - _rtpIceChannel.RemoteEndpointTranslator = _remoteEndpointTranslator; + logger.LogWebRtcNoCertificate(); } - _rtpIceChannel.OnIceCandidate += (candidate) => _onIceCandidate?.Invoke(candidate); - _rtpIceChannel.OnIceConnectionStateChange += IceConnectionStateChange; - _rtpIceChannel.OnIceGatheringStateChange += (state) => onicegatheringstatechange?.Invoke(state); - _rtpIceChannel.OnIceGatheringStateChange += (state) => + if (_configuration.X_UseRtpFeedbackProfile) { - if (state == RTCIceGatheringState.complete) { _iceCompletedGatheringTask.TrySetResult(true); } - }; - _rtpIceChannel.OnIceCandidateError += (candidate, error) => onicecandidateerror?.Invoke(candidate, error); + RTP_MEDIA_PROFILE = RTP_MEDIA_FEEDBACK_PROFILE; + } + } + else + { + _configuration = new RTCConfiguration(); + } + + if (_dtlsCertificate is null) + { + // No certificate was provided so create a new self signed one. + (_dtlsCertificate, _dtlsPrivateKey) = DtlsUtils.CreateSelfSignedTlsCert(_crypto, useRsa: configuration?.X_UseRsaForDtlsCertificate ?? false); + } - OnRtpClosed += Close; - OnRtcpBye += Close; + DtlsCertificateFingerprint = DtlsUtils.Fingerprint(_dtlsCertificate); - //Cancel Negotiation Task Event to Prevent Duplicated Calls - onnegotiationneeded += CancelOnNegotiationNeededTask; + SessionID = Guid.NewGuid().ToString(); + LocalSdpSessionID = Crypto.GetRandomInt(5).ToString(); - sctp = new RTCSctpTransport(SCTP_DEFAULT_PORT, SCTP_DEFAULT_PORT, _rtpIceChannel.RTPPort); + // Request the underlying RTP session to create a single RTP channel that will + // be used to multiplex all required media streams. + addSingleTrack(videoAsPrimary); - onnegotiationneeded?.Invoke(); + _rtpIceChannel = GetRtpChannel(); - // This is the point the ICE session potentially starts contacting STUN and TURN servers. - // This job was moved to a background thread as it was observed that interacting with the OS network - // calls and/or initialising DNS was taking up to 600ms, see - // https://github.com/sipsorcery-org/sipsorcery/issues/456. - _iceInitiateGatheringTask = Task.Run(_rtpIceChannel.StartGathering); + // Propagate any translator that was set before the channel existed. + if (_remoteEndpointTranslator != null) + { + _rtpIceChannel.RemoteEndpointTranslator = _remoteEndpointTranslator; } - private bool InitializeCertificates(RTCConfiguration configuration) + _rtpIceChannel.OnIceCandidate += (candidate) => _onIceCandidate?.Invoke(candidate); + _rtpIceChannel.OnIceConnectionStateChange += IceConnectionStateChange; + _rtpIceChannel.OnIceGatheringStateChange += (state) => onicegatheringstatechange?.Invoke(state); + _rtpIceChannel.OnIceGatheringStateChange += (state) => { - if (configuration.certificates2 == null || configuration.certificates2.Count == 0) - { - return false; - } + if (state == RTCIceGatheringState.complete) { _iceCompletedGatheringTask.TrySetResult(true); } + }; + _rtpIceChannel.OnIceCandidateError += (candidate, error) => onicecandidateerror?.Invoke(candidate, error); - _dtlsCertificate = new Certificate(new[] { new BcTlsCertificate(_crypto, configuration.certificates2[0].Certificate.CertificateStructure) }); - _dtlsPrivateKey = configuration.certificates2[0].PrivateKey; + OnRtpClosed += Close; + OnRtcpBye += Close; - return true; - } + //Cancel Negotiation Task Event to Prevent Duplicated Calls + onnegotiationneeded += CancelOnNegotiationNeededTask; + + sctp = new RTCSctpTransport(SCTP_DEFAULT_PORT, SCTP_DEFAULT_PORT, _rtpIceChannel.RTPPort); + + onnegotiationneeded?.Invoke(); + + // This is the point the ICE session potentially starts contacting STUN and TURN servers. + // This job was moved to a background thread as it was observed that interacting with the OS network + // calls and/or initialising DNS was taking up to 600ms, see + // https://github.com/sipsorcery-org/sipsorcery/issues/456. + _iceInitiateGatheringTask = Task.Run(_rtpIceChannel.StartGathering); + } - /// - /// Event handler for ICE connection state changes. - /// - /// The new ICE connection state. - private async void IceConnectionStateChange(RTCIceConnectionState iceState) + private bool InitializeCertificates(RTCConfiguration configuration) + { + if (configuration.certificates2 is null || configuration.certificates2.Count == 0) { - oniceconnectionstatechange?.Invoke(iceConnectionState); + return false; + } + + Debug.Assert(configuration is { }); + Debug.Assert(configuration.certificates2 is { Count: > 0 }); + Debug.Assert(configuration.certificates2[0]?.Certificate is { }); + Debug.Assert(configuration.certificates2[0]?.Certificate?.CertificateStructure is { }); + _dtlsCertificate = new Certificate(new[] { new BcTlsCertificate(_crypto, configuration.certificates2[0]?.Certificate?.CertificateStructure) }); + _dtlsPrivateKey = configuration.certificates2[0].PrivateKey; + + return true; + } + + /// + /// Event handler for ICE connection state changes. + /// + /// The new ICE connection state. + private async void IceConnectionStateChange(RTCIceConnectionState iceState) + { + oniceconnectionstatechange?.Invoke(iceConnectionState); - if (iceState == RTCIceConnectionState.connected && _rtpIceChannel.NominatedEntry != null) + if (iceState == RTCIceConnectionState.connected && _rtpIceChannel.NominatedEntry is { }) + { + if (_dtlsHandle is { }) { - if (_dtlsHandle != null) + var destinationEndPoint = _rtpIceChannel.NominatedEntry.RemoteCandidate.DestinationEndPoint; + Debug.Assert(base.PrimaryStream is { }); + Debug.Assert(destinationEndPoint is { }); + if (base.PrimaryStream.DestinationEndPoint?.Address.Equals(destinationEndPoint.Address) == false || + base.PrimaryStream.DestinationEndPoint?.Port != destinationEndPoint.Port) { - if (base.PrimaryStream.DestinationEndPoint?.Address.Equals(_rtpIceChannel.NominatedEntry.RemoteCandidate.DestinationEndPoint.Address) == false || - base.PrimaryStream.DestinationEndPoint?.Port != _rtpIceChannel.NominatedEntry.RemoteCandidate.DestinationEndPoint.Port) - { - // Already connected and this event is due to change in the nominated remote candidate. - var connectedEP = _rtpIceChannel.NominatedEntry.RemoteCandidate.DestinationEndPoint; + // Already connected and this event is due to change in the nominated remote candidate. + var connectedEP = destinationEndPoint; - SetGlobalDestination(connectedEP, connectedEP); - logger.LogDebug("ICE changing connected remote end point to {connectedEP}.", connectedEP); - } - - if (connectionState == RTCPeerConnectionState.disconnected || - connectionState == RTCPeerConnectionState.failed) - { - // The ICE connection state change is due to a re-connection. - connectionState = RTCPeerConnectionState.connected; - onconnectionstatechange?.Invoke(connectionState); - } + SetGlobalDestination(connectedEP, connectedEP); + logger.LogWebRtcIceRemoteEndpointChange(connectedEP); } - else + + if (connectionState is RTCPeerConnectionState.disconnected or + RTCPeerConnectionState.failed) { - connectionState = RTCPeerConnectionState.connecting; + // The ICE connection state change is due to a re-connection. + connectionState = RTCPeerConnectionState.connected; onconnectionstatechange?.Invoke(connectionState); + } + } + else + { + connectionState = RTCPeerConnectionState.connecting; + onconnectionstatechange?.Invoke(connectionState); - var connectedEP = _rtpIceChannel.NominatedEntry.RemoteCandidate.DestinationEndPoint; + var connectedEP = _rtpIceChannel.NominatedEntry.RemoteCandidate.DestinationEndPoint; + Debug.Assert(connectedEP is { }); - SetGlobalDestination(connectedEP, connectedEP); - logger.LogDebug("ICE connected to remote end point {connectedEP}.", connectedEP); + SetGlobalDestination(connectedEP, connectedEP); + logger.LogWebRtcIceConnected(connectedEP); + + var disableDtlsExtendedMasterSecret = _configuration is { X_DisableExtendedMasterSecretKey: true }; - bool disableDtlsExtendedMasterSecret = _configuration != null && _configuration.X_DisableExtendedMasterSecretKey; + Debug.Assert(_dtlsPrivateKey is { }); - _dtlsHandle = new DtlsSrtpTransport( - IceRole == IceRolesEnum.active ? - new DtlsSrtpClient(_crypto, _dtlsCertificate, _dtlsPrivateKey, _configuration.X_UseRsaForDtlsCertificate ? SignatureAlgorithm.rsa : SignatureAlgorithm.ecdsa) - { ForceUseExtendedMasterSecret = !disableDtlsExtendedMasterSecret } : - new DtlsSrtpServer(_crypto, _dtlsCertificate, _dtlsPrivateKey, _configuration.X_UseRsaForDtlsCertificate ? SignatureAlgorithm.rsa : SignatureAlgorithm.ecdsa) - { ForceUseExtendedMasterSecret = !disableDtlsExtendedMasterSecret, ForceDisableMKI = true } - ); + _dtlsHandle = new DtlsSrtpTransport( + IceRole == IceRolesEnum.active + ? new DtlsSrtpClient(_crypto, _dtlsCertificate, _dtlsPrivateKey, _configuration.X_UseRsaForDtlsCertificate ? SignatureAlgorithm.rsa : SignatureAlgorithm.ecdsa) + { + ForceUseExtendedMasterSecret = !disableDtlsExtendedMasterSecret + } + : new DtlsSrtpServer(_crypto, _dtlsCertificate, _dtlsPrivateKey, _configuration.X_UseRsaForDtlsCertificate ? SignatureAlgorithm.rsa : SignatureAlgorithm.ecdsa) + { + ForceUseExtendedMasterSecret = !disableDtlsExtendedMasterSecret, + ForceDisableMKI = true + } + ); - _dtlsHandle.OnAlert += OnDtlsAlert; + _dtlsHandle.OnAlert += OnDtlsAlert; - logger.LogDebug("Starting DLS handshake with role {IceRole}.", IceRole); + logger.LogWebRtcDtlsHandshakeStarted(IceRole); - try - { - bool handshakeResult = await Task.Run(() => DoDtlsHandshake(_dtlsHandle)).ConfigureAwait(false); + try + { + var handshakeResult = await Task.Run(() => DoDtlsHandshake(_dtlsHandle)).ConfigureAwait(false); - connectionState = handshakeResult ? RTCPeerConnectionState.connected : connectionState = RTCPeerConnectionState.failed; - onconnectionstatechange?.Invoke(connectionState); + connectionState = handshakeResult ? RTCPeerConnectionState.connected : connectionState = RTCPeerConnectionState.failed; + onconnectionstatechange?.Invoke(connectionState); - if (connectionState == RTCPeerConnectionState.connected) - { - await base.Start().ConfigureAwait(false); - await InitialiseSctpTransport().ConfigureAwait(false); - } - } - catch (Exception excp) + if (connectionState == RTCPeerConnectionState.connected) { - logger.LogWarning(excp, "RTCPeerConnection DTLS handshake failed. {ErrorMessage}", excp.Message); + await base.Start().ConfigureAwait(false); + await InitialiseSctpTransport().ConfigureAwait(false); + } + } + catch (Exception excp) + { + logger.LogWebRtcDtlsHandshakeError(excp.Message, excp); - //connectionState = RTCPeerConnectionState.failed; - //onconnectionstatechange?.Invoke(connectionState); + //connectionState = RTCPeerConnectionState.failed; + //onconnectionstatechange?.Invoke(connectionState); - Close("dtls handshake failed"); - } + Close("dtls handshake failed"); } } + } - if (iceConnectionState == RTCIceConnectionState.checking) - { - // Not sure about this correspondence between the ICE and peer connection states. - // TODO: Double check spec. - //connectionState = RTCPeerConnectionState.connecting; - //onconnectionstatechange?.Invoke(connectionState); - } - else if (iceConnectionState == RTCIceConnectionState.disconnected) + if (iceConnectionState == RTCIceConnectionState.checking) + { + // Not sure about this correspondence between the ICE and peer connection states. + // TODO: Double check spec. + //connectionState = RTCPeerConnectionState.connecting; + //onconnectionstatechange?.Invoke(connectionState); + } + else if (iceConnectionState == RTCIceConnectionState.disconnected) + { + if (connectionState == RTCPeerConnectionState.connected) { - if (connectionState == RTCPeerConnectionState.connected) - { - connectionState = RTCPeerConnectionState.disconnected; - onconnectionstatechange?.Invoke(connectionState); - } - else - { - connectionState = RTCPeerConnectionState.failed; - onconnectionstatechange?.Invoke(connectionState); - } + connectionState = RTCPeerConnectionState.disconnected; + onconnectionstatechange?.Invoke(connectionState); } - else if (iceConnectionState == RTCIceConnectionState.failed) + else { connectionState = RTCPeerConnectionState.failed; onconnectionstatechange?.Invoke(connectionState); } } + else if (iceConnectionState == RTCIceConnectionState.failed) + { + connectionState = RTCPeerConnectionState.failed; + onconnectionstatechange?.Invoke(connectionState); + } + } - /// - /// Creates a new RTP ICE channel (which manages the UDP socket sending and receiving RTP - /// packets) for use with this session. - /// - /// A new RTPChannel instance. - protected override RTPChannel CreateRtpChannel() + /// + /// Creates a new RTP ICE channel (which manages the UDP socket sending and receiving RTP + /// packets) for use with this session. + /// + /// A new RTPChannel instance. + protected override RTPChannel CreateRtpChannel() + { + if (rtpSessionConfig.IsMediaMultiplexed) { - if (rtpSessionConfig.IsMediaMultiplexed) + if (MultiplexRtpChannel is { }) { - if (MultiplexRtpChannel != null) - { - return MultiplexRtpChannel; - } + return MultiplexRtpChannel; } + } - var rtpIceChannel = new RtpIceChannel( + var rtpIceChannel = new RtpIceChannel( _configuration?.X_BindAddress, RTCIceComponent.rtp, _configuration?.iceServers, - _configuration != null ? _configuration.iceTransportPolicy : RTCIceTransportPolicy.all, - _configuration != null ? _configuration.X_ICEIncludeAllInterfaceAddresses : false, + _configuration is { } ? _configuration.iceTransportPolicy : RTCIceTransportPolicy.all, + _configuration is { } ? _configuration.X_ICEIncludeAllInterfaceAddresses : false, rtpSessionConfig.BindPort == 0 ? 0 : rtpSessionConfig.BindPort + m_rtpChannelsCount * 2, rtpSessionConfig.RtpPortRange); - if (rtpSessionConfig.IsMediaMultiplexed) - { - MultiplexRtpChannel = rtpIceChannel; - } - - rtpIceChannel.OnRTPDataReceived += OnRTPDataReceived; - - // Start the RTP, and if required the Control, socket receivers and the RTCP session. - rtpIceChannel.Start(); + if (rtpSessionConfig.IsMediaMultiplexed) + { + MultiplexRtpChannel = rtpIceChannel; + } - m_rtpChannelsCount++; + rtpIceChannel.OnRTPDataReceived += OnRTPDataReceived; - return rtpIceChannel; - } + // Start the RTP, and if required the Control, socket receivers and the RTCP session. + rtpIceChannel.Start(); - /// - /// Sets the local SDP. - /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dom-peerconnection-setlocaldescription. - /// - /// Optional. The session description to set as - /// local description. If not supplied then an offer or answer will be created as required. - /// - public Task setLocalDescription(RTCSessionDescriptionInit init) - { - localDescription = new RTCSessionDescription { type = init.type, sdp = SDP.ParseSDPDescription(init.sdp) }; + m_rtpChannelsCount++; - if (init.type == RTCSdpType.offer) - { - _rtpIceChannel.IsController = true; - } + return rtpIceChannel; + } - if (signalingState == RTCSignalingState.have_remote_offer) - { - signalingState = RTCSignalingState.stable; - onsignalingstatechange?.Invoke(); - } - else - { - signalingState = RTCSignalingState.have_local_offer; - onsignalingstatechange?.Invoke(); - } + /// + /// Sets the local SDP. + /// + /// + /// As specified in https://www.w3.org/TR/webrtc/#dom-peerconnection-setlocaldescription. + /// + /// Optional. The session description to set as + /// local description. If not supplied then an offer or answer will be created as required. + /// + public Task setLocalDescription(RTCSessionDescriptionInit init) + { + localDescription = new RTCSessionDescription { type = init.type, sdp = SDP.ParseSDPDescription(init.sdp.AsSpan()) }; - return Task.CompletedTask; + if (init.type == RTCSdpType.offer) + { + _rtpIceChannel.IsController = true; } - /// - /// This set remote description overload is a convenience method for SIP/VoIP callers - /// instead of WebRTC callers. The method signature better matches what the SIP - /// user agent is expecting. - /// TODO: Using two very similar overloads could cause confusion. Possibly - /// consolidate. - /// - /// Whether the remote SDP is an offer or answer. - /// The SDP from the remote party. - /// The result of attempting to set the remote description. - public override SetDescriptionResultEnum SetRemoteDescription(SdpType sdpType, SDP sessionDescription) + if (signalingState == RTCSignalingState.have_remote_offer) { - RTCSessionDescriptionInit init = new RTCSessionDescriptionInit - { - sdp = sessionDescription.ToString(), - type = (sdpType == SdpType.answer) ? RTCSdpType.answer : RTCSdpType.offer - }; - - return setRemoteDescription(init); + signalingState = RTCSignalingState.stable; + onsignalingstatechange?.Invoke(); + } + else + { + signalingState = RTCSignalingState.have_local_offer; + onsignalingstatechange?.Invoke(); } - /// - /// Updates the session after receiving the remote SDP. - /// - /// The answer/offer SDP from the remote party. - public SetDescriptionResultEnum setRemoteDescription(RTCSessionDescriptionInit init) + return Task.CompletedTask; + } + + /// + /// This set remote description overload is a convenience method for SIP/VoIP callers + /// instead of WebRTC callers. The method signature better matches what the SIP + /// user agent is expecting. + /// TODO: Using two very similar overloads could cause confusion. Possibly + /// consolidate. + /// + /// Whether the remote SDP is an offer or answer. + /// The SDP from the remote party. + /// The result of attempting to set the remote description. + public override SetDescriptionResultEnum SetRemoteDescription(SdpType sdpType, SDP sessionDescription) + { + var init = new RTCSessionDescriptionInit { - remoteDescription = new RTCSessionDescription { type = init.type, sdp = SDP.ParseSDPDescription(init.sdp) }; + sdp = sessionDescription.ToString(), + type = (sdpType == SdpType.answer) ? RTCSdpType.answer : RTCSdpType.offer + }; - SDP remoteSdp = remoteDescription.sdp; // SDP.ParseSDPDescription(init.sdp); + return setRemoteDescription(init); + } - // Need to store uri/id of know extensions - _rtpExtensionsUsed ??= new Dictionary(); - foreach (var ann in remoteSdp.Media) + /// + /// Updates the session after receiving the remote SDP. + /// + /// The answer/offer SDP from the remote party. + public SetDescriptionResultEnum setRemoteDescription(RTCSessionDescriptionInit init) + { + remoteDescription = new RTCSessionDescription { type = init.type, sdp = SDP.ParseSDPDescription(init.sdp.AsSpan()) }; + + var remoteSdp = remoteDescription.sdp; // SDP.ParseSDPDescription(init.sdp); + Debug.Assert(remoteSdp is { }); + + // Need to store uri/id of know extensions + _rtpExtensionsUsed ??= new Dictionary(); + foreach (var ann in remoteSdp.Media) + { + if (ann.Media is SDPMediaTypesEnum.audio or SDPMediaTypesEnum.video) { - if ((ann.Media == SDPMediaTypesEnum.audio) || (ann.Media == SDPMediaTypesEnum.video)) + var extensions = ann.HeaderExtensions?.Values; + if (extensions is { }) { - var extensions = ann.HeaderExtensions?.Values; - if (extensions != null) + foreach (var extension in extensions) { - foreach (var extension in extensions) - { - logger.LogDebug("[setRemoteDescription] - Extension:[{Id} - {Uri}]", extension.Id, extension.Uri); - _rtpExtensionsUsed[extension.Uri] = extension.Id; - } + logger.LogWebRtcRemoteDescription(extension.Id, extension.Uri); + _rtpExtensionsUsed[extension.Uri] = extension.Id; } } } + } - SdpType sdpType = (init.type == RTCSdpType.offer) ? SdpType.offer : SdpType.answer; + var sdpType = (init.type == RTCSdpType.offer) ? SdpType.offer : SdpType.answer; - switch (signalingState) - { - case var sigState when sigState == RTCSignalingState.have_local_offer && sdpType == SdpType.offer: - logger.LogWarning("RTCPeerConnection received an SDP offer but was already in {SignalingState} state. Remote offer rejected.", sigState); - return SetDescriptionResultEnum.WrongSdpTypeOfferAfterOffer; - } + switch (signalingState) + { + case RTCSignalingState.have_local_offer when sdpType == SdpType.offer: + logger.LogWebRtcSignalingStateRejectOffer(signalingState); + return SetDescriptionResultEnum.WrongSdpTypeOfferAfterOffer; + } - var setResult = base.SetRemoteDescription(sdpType, remoteSdp); + var setResult = base.SetRemoteDescription(sdpType, remoteSdp); - if (setResult == SetDescriptionResultEnum.OK) + if (setResult == SetDescriptionResultEnum.OK) + { + var remoteIceUser = remoteSdp.IceUfrag; + var remoteIcePassword = remoteSdp.IcePwd; + var dtlsFingerprint = remoteSdp.DtlsFingerprint; + var remoteIceRole = remoteSdp.IceRole; + + foreach (var ann in remoteSdp.Media) { - string remoteIceUser = remoteSdp.IceUfrag; - string remoteIcePassword = remoteSdp.IcePwd; - string dtlsFingerprint = remoteSdp.DtlsFingerprint; - IceRolesEnum? remoteIceRole = remoteSdp.IceRole; + if (remoteIceUser is null || remoteIcePassword is null || dtlsFingerprint is null || remoteIceRole is null) + { + remoteIceUser = remoteIceUser ?? ann.IceUfrag; + remoteIcePassword = remoteIcePassword ?? ann.IcePwd; + dtlsFingerprint = dtlsFingerprint ?? ann.DtlsFingerprint; + remoteIceRole = remoteIceRole ?? ann.IceRole; + } - foreach (var ann in remoteSdp.Media) + // Check for data channel announcements. + if (HasSdpDataChannelFormat(ann)) { - if (remoteIceUser == null || remoteIcePassword == null || dtlsFingerprint == null || remoteIceRole == null) + if (ann.Transport is RTP_MEDIA_DATACHANNEL_DTLS_PROFILE or + RTP_MEDIA_DATACHANNEL_UDPDTLS_PROFILE) { - remoteIceUser = remoteIceUser ?? ann.IceUfrag; - remoteIcePassword = remoteIcePassword ?? ann.IcePwd; dtlsFingerprint = dtlsFingerprint ?? ann.DtlsFingerprint; - remoteIceRole = remoteIceRole ?? ann.IceRole; + remoteIceRole = remoteIceRole ?? remoteSdp.IceRole; } - - // Check for data channel announcements. - if (ann.Media == SDPMediaTypesEnum.application && - ann.MediaFormats.Count() == 1 && - ann.ApplicationMediaFormats.Single().Key == SDP_DATACHANNEL_FORMAT_ID) + else { - if (ann.Transport == RTP_MEDIA_DATACHANNEL_DTLS_PROFILE || - ann.Transport == RTP_MEDIA_DATACHANNEL_UDPDTLS_PROFILE) - { - dtlsFingerprint = dtlsFingerprint ?? ann.DtlsFingerprint; - remoteIceRole = remoteIceRole ?? remoteSdp.IceRole; - } - else - { - logger.LogWarning("The remote SDP requested an unsupported data channel transport of {Transport}.", ann.Transport); - return SetDescriptionResultEnum.DataChannelTransportNotSupported; - } + logger.LogWebRtcDataTransportUnsupported(ann.Transport); + return SetDescriptionResultEnum.DataChannelTransportNotSupported; } } - SdpSessionID = remoteSdp.SessionId; - - if (remoteSdp.IceImplementation == IceImplementationEnum.lite) - { - _rtpIceChannel.IsController = true; - } - if (init.type == RTCSdpType.answer) - { - _rtpIceChannel.IsController = true; - IceRole = remoteIceRole == IceRolesEnum.passive ? IceRolesEnum.active : IceRolesEnum.passive; - } - //As Chrome does not support changing IceRole while renegotiating we need to keep same previous IceRole if we already negotiated before - else - { - // Set DTLS role as client. - IceRole = IceRolesEnum.active; - } - - if (remoteIceUser != null && remoteIcePassword != null) - { - _rtpIceChannel.SetRemoteCredentials(remoteIceUser, remoteIcePassword); - } - - if (!string.IsNullOrWhiteSpace(dtlsFingerprint)) + static bool HasSdpDataChannelFormat(SDPMediaAnnouncement ann) { - dtlsFingerprint = dtlsFingerprint.Trim().ToLower(); - if (RTCDtlsFingerprint.TryParse(dtlsFingerprint, out var remoteFingerprint)) + if (ann.Media == SDPMediaTypesEnum.application) { - RemotePeerDtlsFingerprint = remoteFingerprint; + return false; } - else + foreach (var kv in ann.ApplicationMediaFormats) { - logger.LogWarning("The DTLS fingerprint was invalid or not supported."); - return SetDescriptionResultEnum.DtlsFingerprintDigestNotSupported; + return kv.Key == SDP_DATACHANNEL_FORMAT_ID; } + + return false; + } + } + + SdpSessionID = remoteSdp.SessionId; + + if (remoteSdp.IceImplementation == IceImplementationEnum.lite) + { + _rtpIceChannel.IsController = true; + } + if (init.type == RTCSdpType.answer) + { + _rtpIceChannel.IsController = true; + IceRole = remoteIceRole == IceRolesEnum.passive ? IceRolesEnum.active : IceRolesEnum.passive; + } + //As Chrome does not support changing IceRole while renegotiating we need to keep same previous IceRole if we already negotiated before + else + { + // Set DTLS role as client. + IceRole = IceRolesEnum.active; + } + + if (remoteIceUser is { } && remoteIcePassword is { }) + { + _rtpIceChannel.SetRemoteCredentials(remoteIceUser, remoteIcePassword); + } + + if (!string.IsNullOrWhiteSpace(dtlsFingerprint)) + { + dtlsFingerprint = dtlsFingerprint.Trim().ToLower(); + if (RTCDtlsFingerprint.TryParse(dtlsFingerprint, out var remoteFingerprint)) + { + RemotePeerDtlsFingerprint = remoteFingerprint; } else { - logger.LogWarning("The DTLS fingerprint was missing from the remote party's session description."); - return SetDescriptionResultEnum.DtlsFingerprintMissing; + logger.LogWebRtcDtlsFingerprintInvalid(); + return SetDescriptionResultEnum.DtlsFingerprintDigestNotSupported; } + } + else + { + logger.LogWebRtcDtlsFingerprintMissing(); + return SetDescriptionResultEnum.DtlsFingerprintMissing; + } - // All browsers seem to have gone to trickling ICE candidates now but just - // in case one or more are given we can start the STUN dance immediately. - if (remoteSdp.IceCandidates != null) + // All browsers seem to have gone to trickling ICE candidates now but just + // in case one or more are given we can start the STUN dance immediately. + if (remoteSdp.IceCandidates is { }) + { + foreach (var iceCandidate in remoteSdp.IceCandidates) { - foreach (var iceCandidate in remoteSdp.IceCandidates) - { - addIceCandidate(new RTCIceCandidateInit { candidate = iceCandidate }); - } + addIceCandidate(new RTCIceCandidateInit { candidate = iceCandidate.ToString() }); } + } - ResetRemoteSDPSsrcAttributes(); - foreach (var media in remoteSdp.Media) + ResetRemoteSDPSsrcAttributes(); + foreach (var media in remoteSdp.Media) + { + if (media.IceCandidates is { }) { - if (media.IceCandidates != null) + foreach (var iceCandidate in media.IceCandidates) { - foreach (var iceCandidate in media.IceCandidates) - { - addIceCandidate(new RTCIceCandidateInit { candidate = iceCandidate }); - } + addIceCandidate(new RTCIceCandidateInit { candidate = iceCandidate.ToString() }); } - - AddRemoteSDPSsrcAttributes(media.Media, media.SsrcAttributes); } - LogRemoteSDPSsrcAttributes(); + AddRemoteSDPSsrcAttributes(media.Media, media.SsrcAttributes); + } - UpdatedSctpDestinationPort(); + logger.LogRtpSessionRemoteSdpSsrcAttributes(audioRemoteSDPSsrcAttributes, videoRemoteSDPSsrcAttributes, textRemoteSDPSsrcAttributes); - if (init.type == RTCSdpType.offer) - { - signalingState = RTCSignalingState.have_remote_offer; - onsignalingstatechange?.Invoke(); - } - else + UpdatedSctpDestinationPort(); + + if (init.type == RTCSdpType.offer) + { + signalingState = RTCSignalingState.have_remote_offer; + onsignalingstatechange?.Invoke(); + } + else + { + signalingState = RTCSignalingState.stable; + onsignalingstatechange?.Invoke(); + } + + // Trigger the ICE candidate events for any non-host candidates, host candidates are always included in the + // SDP offer/answer. The reason for the trigger is that ICE candidates cannot be sent to the remote peer + // until it is ready to receive them which is indicated by the remote offer being received. + foreach (var nonHostCand in _rtpIceChannel.Candidates) + { + if (nonHostCand.type != RTCIceCandidateType.host) { - signalingState = RTCSignalingState.stable; - onsignalingstatechange?.Invoke(); + _onIceCandidate?.Invoke(nonHostCand); } + } + } + + return setResult; + } + + /// + /// Close the session including the underlying RTP session and channels. + /// + /// An optional descriptive reason for the closure. + public override void Close(string? reason) + { + if (!IsClosed) + { + logger.LogWebRtcPeerConnectionClose(reason ?? ""); - // Trigger the ICE candidate events for any non-host candidates, host candidates are always included in the - // SDP offer/answer. The reason for the trigger is that ICE candidates cannot be sent to the remote peer - // until it is ready to receive them which is indicated by the remote offer being received. - foreach (var nonHostCand in _rtpIceChannel.Candidates.Where(x => x.type != RTCIceCandidateType.host)) + // Close all DataChannels + if (DataChannels?.Count > 0) + { + foreach (var dc in DataChannels) { - _onIceCandidate?.Invoke(nonHostCand); + dc?.close(); } } - return setResult; + _rtpIceChannel?.Close(); + _dtlsHandle?.Close(); + + sctp?.Close(); + + base.Close(reason); // Here Audio and/or Video Streams are closed + + connectionState = RTCPeerConnectionState.closed; + onconnectionstatechange?.Invoke(RTCPeerConnectionState.closed); + } + } + + /// + /// Closes the connection with the default reason. + /// + public void close() + { + Close(NORMAL_CLOSE_REASON); + } + + /// + /// Generates the SDP for an offer that can be made to a remote peer. + /// + /// + /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-createoffer. + /// + /// Optional. If supplied the options will be sued to apply additional + /// controls over the generated offer SDP. + public RTCSessionDescriptionInit createOffer(RTCOfferOptions? options = null) + { + var mediaStreamList = GetMediaStreams(); + //Revert to DefaultStreamStatus + foreach (var mediaStream in mediaStreamList) + { + if (mediaStream.LocalTrack is { } && mediaStream.LocalTrack.StreamStatus == MediaStreamStatusEnum.Inactive) + { + mediaStream.LocalTrack.StreamStatus = mediaStream.LocalTrack.DefaultStreamStatus; + } } - /// - /// Close the session including the underlying RTP session and channels. - /// - /// An optional descriptive reason for the closure. - public override void Close(string reason) + var excludeIceCandidates = options is { X_ExcludeIceCandidates: true }; + var waitForIceGatheringToComplete = options is { X_WaitForIceGatheringToComplete: true }; + + var offerSdp = createBaseSdp(mediaStreamList, excludeIceCandidates, waitForIceGatheringToComplete); + + var indexAudioStream = 0; + var indexVideoStream = 0; + _rtpExtensionsUsed ??= new Dictionary(); + foreach (var ann in offerSdp.Media) { - if (!IsClosed) + // Audio - Add RTP Extension we want or can support + if (ann.Media == SDPMediaTypesEnum.audio) + { + ann.HeaderExtensions.Clear(); + + var localHeaderExtensions = AudioStreamList[indexAudioStream].LocalTrack?.HeaderExtensions?.Values; + if (localHeaderExtensions is { }) + { + foreach (var localExtension in localHeaderExtensions) + { + // We must ensure to use same Id by extension + if (_rtpExtensionsUsed.TryGetValue(localExtension.Uri, out var rtpExtensionsUsedValue)) + { + localExtension.Id = rtpExtensionsUsedValue; + } + else + { + _rtpExtensionsUsed[localExtension.Uri] = localExtension.Id; + } + + logger.LogWebRtcCreateOfferHeaderExtension(ann.Media, ann.MediaID, localExtension.Id, localExtension.Uri); + ann.HeaderExtensions[localExtension.Id] = localExtension; + } + } + indexAudioStream++; + } + // Video - Add RTP Extension we want or can support + else if (ann.Media == SDPMediaTypesEnum.video) { - logger.LogDebug("Peer connection closed with reason {Reason}.", reason != null ? reason : ""); + ann.HeaderExtensions.Clear(); - // Close all DataChannels - if (DataChannels?.Count > 0) + var localHeaderExtensions = VideoStreamList[indexVideoStream].LocalTrack?.HeaderExtensions?.Values; + if (localHeaderExtensions is { }) { - foreach (var dc in DataChannels) + foreach (var localExtension in localHeaderExtensions) { - dc?.close(); + // We must ensure to use same Id by extension + if (_rtpExtensionsUsed.TryGetValue(localExtension.Uri, out var value)) + { + localExtension.Id = value; + } + else + { + _rtpExtensionsUsed[localExtension.Uri] = localExtension.Id; + } + + logger.LogWebRtcCreateOfferHeaderExtension(ann.Media, ann.MediaID, localExtension.Id, localExtension.Uri); + ann.HeaderExtensions[localExtension.Id] = localExtension; } } + indexVideoStream++; + } + ann.IceRole = IceRole; + } - _rtpIceChannel?.Close(); - _dtlsHandle?.Close(); + var initDescription = new RTCSessionDescriptionInit + { + type = RTCSdpType.offer, + sdp = offerSdp.ToString() + }; - sctp?.Close(); + return initDescription; + } - base.Close(reason); // Here Audio and/or Video Streams are closed + /// + /// Convenience overload to suit SIP/VoIP callers. + /// TODO: Consolidate with createAnswer. + /// + /// Not used. + /// An SDP payload to answer an offer from the remote party. + public override SDP? CreateOffer(IPAddress? connectionAddress) + { + var result = createOffer(null); - connectionState = RTCPeerConnectionState.closed; - onconnectionstatechange?.Invoke(RTCPeerConnectionState.closed); - } + if (result?.sdp is { }) + { + return SDP.ParseSDPDescription(result.sdp.AsSpan()); } - /// - /// Closes the connection with the default reason. - /// - public void close() + return null; + } + + /// + /// Convenience overload to suit SIP/VoIP callers. + /// TODO: Consolidate with createAnswer. + /// + /// Not used. + /// An SDP payload to answer an offer from the remote party. + public override SDP? CreateAnswer(IPAddress? connectionAddress) + { + var result = createAnswer(null); + + if (result?.sdp is { }) { - Close(NORMAL_CLOSE_REASON); + return SDP.ParseSDPDescription(result.sdp.AsSpan()); } - /// - /// Generates the SDP for an offer that can be made to a remote peer. - /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-createoffer. - /// - /// Optional. If supplied the options will be sued to apply additional - /// controls over the generated offer SDP. - public RTCSessionDescriptionInit createOffer(RTCOfferOptions options = null) + return null; + } + + /// + /// Creates an answer to an SDP offer from a remote peer. + /// + /// + /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-createanswer and + /// https://tools.ietf.org/html/rfc3264#section-6.1. + /// + /// Optional. If supplied the options will be used to apply additional + /// controls over the generated answer SDP. + public RTCSessionDescriptionInit createAnswer(RTCAnswerOptions? options = null) + { + if (remoteDescription is null) { - List mediaStreamList = GetMediaStreams(); + throw new SipSorceryException("The remote SDP must be set before an SDP answer can be created."); + } + else + { + var mediaStreamList = GetMediaStreams(); //Revert to DefaultStreamStatus foreach (var mediaStream in mediaStreamList) { - if (mediaStream.LocalTrack != null && mediaStream.LocalTrack.StreamStatus == MediaStreamStatusEnum.Inactive) + if (mediaStream.LocalTrack is { } && mediaStream.LocalTrack.StreamStatus == MediaStreamStatusEnum.Inactive) { mediaStream.LocalTrack.StreamStatus = mediaStream.LocalTrack.DefaultStreamStatus; } } - bool excludeIceCandidates = options != null && options.X_ExcludeIceCandidates; - bool waitForIceGatheringToComplete = options != null && options.X_WaitForIceGatheringToComplete; + var excludeIceCandidates = options is { } && options.X_ExcludeIceCandidates; + var answerSdp = createBaseSdp(mediaStreamList, excludeIceCandidates); - var offerSdp = createBaseSdp(mediaStreamList, excludeIceCandidates, waitForIceGatheringToComplete); - - int indexAudioStream = 0; - int indexVideoStream = 0; + var indexAudioStream = 0; + var indexVideoStream = 0; _rtpExtensionsUsed ??= new Dictionary(); - foreach (var ann in offerSdp.Media) + foreach (var ann in answerSdp.Media) { - // Audio - Add RTP Extension we want or can support + // Audio - RTP Extension must be same on Local and Remote Track if (ann.Media == SDPMediaTypesEnum.audio) { ann.HeaderExtensions.Clear(); - var localHeaderExtensions = AudioStreamList[indexAudioStream].LocalTrack?.HeaderExtensions?.Values; - if (localHeaderExtensions != null) + var localHeaderExtensions = AudioStreamList[indexAudioStream].LocalTrack?.HeaderExtensions; + var remoteHeaderExtensions = AudioStreamList[indexAudioStream].RemoteTrack?.HeaderExtensions?.Values; + if ((remoteHeaderExtensions is { Count: > 0 }) && (localHeaderExtensions is { Count: > 0 })) { - foreach (var localExtension in localHeaderExtensions) + foreach (var remoteExtension in remoteHeaderExtensions) { - // We must ensure to use same Id by extension - if (_rtpExtensionsUsed.ContainsKey(localExtension.Uri)) + var localExtension = FindLocalExtensionByUri(localHeaderExtensions, remoteExtension.Uri); + if ((localExtension is { }) && _rtpExtensionsUsed.TryGetValue(remoteExtension.Uri, out var value)) { - localExtension.Id = _rtpExtensionsUsed[localExtension.Uri]; - } - else - { - _rtpExtensionsUsed[localExtension.Uri] = localExtension.Id; - } + // We must ensure to use same Id by extension + localExtension.Id = value; + localExtension.Uri = remoteExtension.Uri;// Keep same Uri as remote - logger.LogDebug("[createOffer] - {Media}:[{MediaID}] - Add HeaderExtensions:[{Id} - {Uri}]", ann.Media, ann.MediaID, localExtension.Id, localExtension.Uri); - ann.HeaderExtensions[localExtension.Id] = localExtension; + logger.LogWebRtcCreateAnswerHeaderExtension(ann.Media, ann.MediaID, localExtension.Id, localExtension.Uri); + ann.HeaderExtensions.Add(localExtension.Id, localExtension); + } } } indexAudioStream++; } - // Video - Add RTP Extension we want or can support + // Video - RTP Extension must be same on Local and Remote Track else if (ann.Media == SDPMediaTypesEnum.video) { ann.HeaderExtensions.Clear(); - var localHeaderExtensions = VideoStreamList[indexVideoStream].LocalTrack?.HeaderExtensions?.Values; - if (localHeaderExtensions != null) + var localHeaderExtensions = VideoStreamList[indexVideoStream].LocalTrack?.HeaderExtensions; + var remoteHeaderExtensions = VideoStreamList[indexVideoStream].RemoteTrack?.HeaderExtensions?.Values; + if ((remoteHeaderExtensions is { Count: > 0 }) && (localHeaderExtensions is { Count: > 0 })) { - foreach (var localExtension in localHeaderExtensions) + foreach (var remoteExtension in remoteHeaderExtensions) { - // We must ensure to use same Id by extension - if (_rtpExtensionsUsed.ContainsKey(localExtension.Uri)) + var localExtension = FindLocalExtensionByUri(localHeaderExtensions, remoteExtension.Uri); + if ((localExtension is { }) && _rtpExtensionsUsed.TryGetValue(remoteExtension.Uri, out var value)) { - localExtension.Id = _rtpExtensionsUsed[localExtension.Uri]; - } - else - { - _rtpExtensionsUsed[localExtension.Uri] = localExtension.Id; - } + // We must ensure to use same Id by extension + localExtension.Id = value; + localExtension.Uri = remoteExtension.Uri;// Keep same Uri as remote - logger.LogDebug("[createOffer] - {Media}:[{MediaID}] - Add HeaderExtensions:[{Id} - {Uri}]", ann.Media, ann.MediaID, localExtension.Id, localExtension.Uri); - ann.HeaderExtensions[localExtension.Id] = localExtension; + logger.LogWebRtcCreateAnswerHeaderExtension(ann.Media, ann.MediaID, localExtension.Id, localExtension.Uri); + ann.HeaderExtensions.Add(localExtension.Id, localExtension); + } } } indexVideoStream++; } - ann.IceRole = IceRole; + + static RTPHeaderExtension? FindLocalExtensionByUri(Dictionary? localHeaderExtensions, string uri) + { + if (localHeaderExtensions is null) + { + return null; + } + + foreach (var (_, le) in localHeaderExtensions) + { + if (le.MatchesExtension(uri)) + { + return le; + } + } + + return null; + } } - RTCSessionDescriptionInit initDescription = new RTCSessionDescriptionInit + //if (answerSdp.Media.Any(x => x.Media == SDPMediaTypesEnum.audio)) + //{ + // var audioAnnouncement = answerSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.audio).Single(); + // audioAnnouncement.IceRole = IceRole; + //} + + //if (answerSdp.Media.Any(x => x.Media == SDPMediaTypesEnum.video)) + //{ + // var videoAnnouncement = answerSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.video).Single(); + // videoAnnouncement.IceRole = IceRole; + //} + + var initDescription = new RTCSessionDescriptionInit { - type = RTCSdpType.offer, - sdp = offerSdp.ToString() + type = RTCSdpType.answer, + sdp = answerSdp.ToString() }; return initDescription; } + } - /// - /// Convenience overload to suit SIP/VoIP callers. - /// TODO: Consolidate with createAnswer. - /// - /// Not used. - /// An SDP payload to answer an offer from the remote party. - public override SDP CreateOffer(IPAddress connectionAddress) - { - var result = createOffer(null); + /// + /// For standard use this method should not need to be called. The remote peer's ICE + /// user and password will be set when from the SDP. This method is provided for + /// diagnostics purposes. + /// + /// The remote peer's ICE user value. + /// The remote peer's ICE password value. + public void SetRemoteCredentials(string remoteIceUser, string remoteIcePassword) + { + _rtpIceChannel.SetRemoteCredentials(remoteIceUser, remoteIcePassword); + } + + /// + /// Gets the RTP channel being used to send and receive data on this peer connection. + /// Unlike the base RTP session peer connections only ever use a single RTP channel. + /// Audio and video (and RTCP) are all multiplexed on the same channel. + /// + public RtpIceChannel GetRtpChannel() + { + Debug.Assert(PrimaryStream is { }); + var rtpIceChannel = PrimaryStream.GetRTPChannel() as RtpIceChannel; + Debug.Assert(rtpIceChannel is { }); + return rtpIceChannel!; + } - if (result?.sdp != null) + /// + /// Generates the base SDP for an offer or answer. The SDP will then be tailored depending + /// on whether it's being used in an offer or an answer. + /// + /// THe media streamss to add to the SDP description. + /// If true it indicates the caller does not want ICE candidates added + /// to the SDP. + /// If set to true the SDP generation will wait until the ICE gathering is complete + /// before generating the SDP. This is a convenient way to get ICE candidates to be included in the SDP. + /// + /// From https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-4.2.5: + /// "The transport address from the peer for the default destination + /// is set to IPv4/IPv6 address values "0.0.0.0"/"::" and port value + /// of "9". This MUST NOT be considered as a ICE failure by the peer + /// agent and the ICE processing MUST continue as usual." + /// + private SDP createBaseSdp(List mediaStreamList, bool excludeIceCandidates = false, bool waitForIceGatheringToComplete = false) + { + // Make sure the ICE gathering of local IP addresses is complete. + // This task should complete very quickly (<1s) but it is deemed very useful to wait + // for it to complete as it allows local ICE candidates to be included in the SDP. + // In theory it would be better to an async/await but that would result in a breaking + // change to the API and for a one off (once per class instance not once per method call) + // delay of a few hundred milliseconds it was decided not to break the API. + using (var ct = new CancellationTokenSource(TimeSpan.FromMilliseconds(_configuration.X_GatherTimeoutMs))) + { + try { - return SDP.ParseSDPDescription(result.sdp); + _iceInitiateGatheringTask.Wait(ct.Token); + } + catch (OperationCanceledException) + { + logger.LogWebRtcGatheringTimeout(_configuration.X_GatherTimeoutMs); } - - return null; } - /// - /// Convenience overload to suit SIP/VoIP callers. - /// TODO: Consolidate with createAnswer. - /// - /// Not used. - /// An SDP payload to answer an offer from the remote party. - public override SDP CreateAnswer(IPAddress connectionAddress) + if (waitForIceGatheringToComplete) { - var result = createAnswer(null); - - if (result?.sdp != null) + using (var ct = new CancellationTokenSource(TimeSpan.FromMilliseconds(_configuration.X_GatherTimeoutMs))) { - return SDP.ParseSDPDescription(result.sdp); + try + { + _iceCompletedGatheringTask.Task.Wait(); + } + catch (OperationCanceledException) + { + logger.LogWebRtcGatheringCompleteTimeout(_configuration.X_GatherTimeoutMs); + } } - - return null; } - /// - /// Creates an answer to an SDP offer from a remote peer. - /// - /// - /// As specified in https://www.w3.org/TR/webrtc/#dom-rtcpeerconnection-createanswer and - /// https://tools.ietf.org/html/rfc3264#section-6.1. - /// - /// Optional. If supplied the options will be used to apply additional - /// controls over the generated answer SDP. - public RTCSessionDescriptionInit createAnswer(RTCAnswerOptions options = null) + var offerSdp = new SDP(IPAddress.Loopback); + offerSdp.SessionId = LocalSdpSessionID; + + var dtlsFingerprint = this.DtlsCertificateFingerprint.ToString(); + var iceCandidatesAdded = false; + + // Media announcements must be in the same order in the offer and answer. + // Existing media types reuse their index from the previous answer; new types + // (not present in RemoteDescription) are appended after all existing m-lines + // per RFC 3264 §8. + var mediaIndex = 0; + var audioMediaIndex = 0; + var videoMediaIndex = 0; + var nextNewMLineIndex = RemoteDescription?.Media.Count ?? 0; + foreach (var mediaStream in mediaStreamList) { - if (remoteDescription == null) + Debug.Assert(mediaStream.LocalTrack is { }); + + var mindex = 0; + var midTag = "0"; + + if (RemoteDescription is null) { - throw new ApplicationException("The remote SDP must be set before an SDP answer can be created."); + mindex = mediaIndex; + midTag = mediaIndex.ToString(); } else { - List mediaStreamList = GetMediaStreams(); - //Revert to DefaultStreamStatus - foreach (var mediaStream in mediaStreamList) + if (mediaStream.LocalTrack.Kind == SDPMediaTypesEnum.audio) { - if (mediaStream.LocalTrack != null && mediaStream.LocalTrack.StreamStatus == MediaStreamStatusEnum.Inactive) - { - mediaStream.LocalTrack.StreamStatus = mediaStream.LocalTrack.DefaultStreamStatus; - } + (mindex, midTag) = RemoteDescription.GetIndexForMediaType(mediaStream.LocalTrack.Kind, audioMediaIndex); + audioMediaIndex++; + } + else if (mediaStream.LocalTrack.Kind == SDPMediaTypesEnum.video) + { + (mindex, midTag) = RemoteDescription.GetIndexForMediaType(mediaStream.LocalTrack.Kind, videoMediaIndex); + videoMediaIndex++; } + } + mediaIndex++; - bool excludeIceCandidates = options != null && options.X_ExcludeIceCandidates; - bool waitForIceGatheringToComplete = options != null && options.X_WaitForIceGatheringToComplete; - var answerSdp = createBaseSdp(mediaStreamList, excludeIceCandidates, waitForIceGatheringToComplete); + if (mindex == SDP.MEDIA_INDEX_NOT_PRESENT) + { + logger.LogWebRtcCheckpointExcluded(mediaStream.LocalTrack.Kind); - int indexAudioStream = 0; - int indexVideoStream = 0; - _rtpExtensionsUsed ??= new Dictionary(); - foreach (var ann in answerSdp.Media) - { - // Audio - RTP Extension must be same on Local and Remote Track - if (ann.Media == SDPMediaTypesEnum.audio) - { - ann.HeaderExtensions.Clear(); + // New media type added after the initial offer/answer — append it + // after all existing m-lines so the ordering of previously negotiated + // m-lines is preserved (RFC 3264 §8). + mindex = nextNewMLineIndex; + midTag = nextNewMLineIndex.ToString(); + nextNewMLineIndex++; + } - var localHeaderExtensions = AudioStreamList[indexAudioStream].LocalTrack?.HeaderExtensions?.Values; - var remoteHeaderExtensions = AudioStreamList[indexAudioStream].RemoteTrack?.HeaderExtensions?.Values; - if ((remoteHeaderExtensions?.Count > 0) && (localHeaderExtensions?.Count > 0)) - { - foreach (var remoteExtension in remoteHeaderExtensions) - { - var localExtension = localHeaderExtensions.FirstOrDefault(ext => ext.MatchesExtension(remoteExtension.Uri)); - if ((localExtension != null) && _rtpExtensionsUsed.ContainsKey(remoteExtension.Uri)) - { - // We must ensure to use same Id by extension - localExtension.Id = _rtpExtensionsUsed[remoteExtension.Uri]; - localExtension.Uri = remoteExtension.Uri;// Keep same Uri as remote - - logger.LogDebug("[createAnswer] - {Media}:[{MediaID}] - Add HeaderExtensions:[{Id} - {Uri}]", ann.Media, ann.MediaID, localExtension.Id, localExtension.Uri); - ann.HeaderExtensions.Add(localExtension.Id, localExtension); - } - } - } - indexAudioStream++; - } - // Video - RTP Extension must be same on Local and Remote Track - else if (ann.Media == SDPMediaTypesEnum.video) - { - ann.HeaderExtensions.Clear(); + { + var announcement = new SDPMediaAnnouncement( + mediaStream.LocalTrack.Kind, + SDP.IGNORE_RTP_PORT_NUMBER, + mediaStream.LocalTrack.Capabilities); - var localHeaderExtensions = VideoStreamList[indexVideoStream].LocalTrack?.HeaderExtensions?.Values; - var remoteHeaderExtensions = VideoStreamList[indexVideoStream].RemoteTrack?.HeaderExtensions?.Values; - if ((remoteHeaderExtensions?.Count > 0) && (localHeaderExtensions?.Count > 0)) - { - foreach (var remoteExtension in remoteHeaderExtensions) - { - var localExtension = localHeaderExtensions.FirstOrDefault(ext => ext.MatchesExtension(remoteExtension.Uri)); - if ((localExtension != null) && _rtpExtensionsUsed.ContainsKey(remoteExtension.Uri)) - { - // We must ensure to use same Id by extension - localExtension.Id = _rtpExtensionsUsed[remoteExtension.Uri]; - localExtension.Uri = remoteExtension.Uri; // Keep same Uri as remote - - logger.LogDebug("[createAnswer] - {Media}:[{MediaID}] - Add HeaderExtensions:[{Id} - {Uri}]", ann.Media, ann.MediaID, localExtension.Id, localExtension.Uri); - ann.HeaderExtensions.Add(localExtension.Id, localExtension); - } - } - } - indexVideoStream++; - } - } + announcement.Transport = RTP_MEDIA_PROFILE; + announcement.Connection = new SDPConnectionInformation(IPAddress.Any); + announcement.AddExtra(RTCP_MUX_ATTRIBUTE); + announcement.AddExtra(RTCP_ATTRIBUTE); + announcement.MediaStreamStatus = mediaStream.LocalTrack.StreamStatus; + announcement.MediaID = midTag; + announcement.MLineIndex = mindex; - // RFC 4145 Section 4.1: An SDP answer MUST use setup:active or - // setup:passive, never setup:actpass. Ensure all media - // announcements carry the resolved role. - var answerRole = (IceRole == IceRolesEnum.active) - ? IceRolesEnum.active - : IceRolesEnum.passive; + announcement.IceUfrag = _rtpIceChannel.LocalIceUser; + announcement.IcePwd = _rtpIceChannel.LocalIcePassword; + announcement.IceOptions = ICE_OPTIONS; + announcement.IceRole = IceRole; + announcement.DtlsFingerprint = dtlsFingerprint; - foreach (var ann in answerSdp.Media) + if (iceCandidatesAdded == false && !excludeIceCandidates) { - ann.IceRole = answerRole; + AddIceCandidates(announcement); + iceCandidatesAdded = true; } - RTCSessionDescriptionInit initDescription = new RTCSessionDescriptionInit + if (mediaStream.LocalTrack.Ssrc != 0) { - type = RTCSdpType.answer, - sdp = answerSdp.ToString() - }; + var trackCname = mediaStream.RtcpSession?.Cname; + + if (trackCname is { }) + { + announcement.SsrcAttributes.Add(new SDPSsrcAttribute(mediaStream.LocalTrack.Ssrc, trackCname, null)); + } + } - return initDescription; + offerSdp.Media.Add(announcement); } } - /// - /// For standard use this method should not need to be called. The remote peer's ICE - /// user and password will be set when from the SDP. This method is provided for - /// diagnostics purposes. - /// - /// The remote peer's ICE user value. - /// The remote peer's ICE password value. - public void SetRemoteCredentials(string remoteIceUser, string remoteIcePassword) - { - _rtpIceChannel.SetRemoteCredentials(remoteIceUser, remoteIcePassword); - } - - /// - /// Gets the RTP channel being used to send and receive data on this peer connection. - /// Unlike the base RTP session peer connections only ever use a single RTP channel. - /// Audio and video (and RTCP) are all multiplexed on the same channel. - /// - public RtpIceChannel GetRtpChannel() - { - return PrimaryStream.GetRTPChannel() as RtpIceChannel; - } - - /// - /// Generates the base SDP for an offer or answer. The SDP will then be tailored depending - /// on whether it's being used in an offer or an answer. - /// - /// THe media streamss to add to the SDP description. - /// If true it indicates the caller does not want ICE candidates added - /// to the SDP. - /// If set to true the SDP generation will wait until the ICE gathering is complete - /// before generating the SDP. This is a convenient way to get ICE candidates to be included in the SDP. - /// - /// From https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-4.2.5: - /// "The transport address from the peer for the default destination - /// is set to IPv4/IPv6 address values "0.0.0.0"/"::" and port value - /// of "9". This MUST NOT be considered as a ICE failure by the peer - /// agent and the ICE processing MUST continue as usual." - /// - private SDP createBaseSdp(List mediaStreamList, bool excludeIceCandidates = false, bool waitForIceGatheringToComplete = false) - { - // Make sure the ICE gathering of local IP addresses is complete. - // This task should complete very quickly (<1s) but it is deemed very useful to wait - // for it to complete as it allows local ICE candidates to be included in the SDP. - // In theory it would be better to an async/await but that would result in a breaking - // change to the API and for a one off (once per class instance not once per method call) - // delay of a few hundred milliseconds it was decided not to break the API. - using (var ct = new CancellationTokenSource(TimeSpan.FromMilliseconds(_configuration.X_GatherTimeoutMs))) + if (DataChannels.Count > 0 || (RemoteDescription?.Media.Exists(static x => x.Media == SDPMediaTypesEnum.application) ?? false)) + { + (var mindex, var midTag) = RemoteDescription is null ? (mediaIndex, mediaIndex.ToString()) : RemoteDescription.GetIndexForMediaType(SDPMediaTypesEnum.application, 0); + mediaIndex++; + + if (mindex == SDP.MEDIA_INDEX_NOT_PRESENT) { - try - { - _iceInitiateGatheringTask.Wait(ct.Token); - } - catch (OperationCanceledException) - { - logger.LogWarning("ICE gathering timed out after {GatherTimeoutMs}ms", _configuration.X_GatherTimeoutMs); - } + logger.LogWebRtcMediaAnnouncementWarn(); } - - if (waitForIceGatheringToComplete) + else { - using (var ct = new CancellationTokenSource(TimeSpan.FromMilliseconds(_configuration.X_GatherTimeoutMs))) + var dataChannelAnnouncement = new SDPMediaAnnouncement( + SDPMediaTypesEnum.application, + SDP.IGNORE_RTP_PORT_NUMBER, + new List { new SDPApplicationMediaFormat(SDP_DATACHANNEL_FORMAT_ID) }); + dataChannelAnnouncement.Transport = RTP_MEDIA_DATACHANNEL_UDPDTLS_PROFILE; + dataChannelAnnouncement.Connection = new SDPConnectionInformation(IPAddress.Any); + + dataChannelAnnouncement.SctpPort = SCTP_DEFAULT_PORT; + dataChannelAnnouncement.MaxMessageSize = sctp.maxMessageSize; + dataChannelAnnouncement.MLineIndex = mindex; + dataChannelAnnouncement.MediaID = midTag; + dataChannelAnnouncement.IceUfrag = _rtpIceChannel.LocalIceUser; + dataChannelAnnouncement.IcePwd = _rtpIceChannel.LocalIcePassword; + dataChannelAnnouncement.IceOptions = ICE_OPTIONS; + dataChannelAnnouncement.IceRole = IceRole; + dataChannelAnnouncement.DtlsFingerprint = dtlsFingerprint; + + if (iceCandidatesAdded == false && !excludeIceCandidates) { - try - { - _iceCompletedGatheringTask.Task.Wait(); - } - catch (OperationCanceledException) - { - logger.LogWarning("Waiting for ICE gathering to complete timed out after {GatherTimeoutMs}ms", _configuration.X_GatherTimeoutMs); - } + AddIceCandidates(dataChannelAnnouncement); + iceCandidatesAdded = true; } - } - - SDP offerSdp = new SDP(IPAddress.Loopback); - offerSdp.SessionId = LocalSdpSessionID; - string dtlsFingerprint = this.DtlsCertificateFingerprint.ToString(); - bool iceCandidatesAdded = false; + offerSdp.Media.Add(dataChannelAnnouncement); + } + } - // Local function to add ICE candidates to one of the media announcements. - void AddIceCandidates(SDPMediaAnnouncement announcement) + // Set the Bundle attribute to indicate all media announcements are being multiplexed. + if (offerSdp.Media?.Count > 0) + { + offerSdp.Group = BUNDLE_ATTRIBUTE; + // order by MLineIndex then MediaID without LINQ + offerSdp.Media.Sort((a, b) => { - if (_rtpIceChannel.Candidates?.Count > 0) - { - announcement.IceCandidates = new List(); - - // Add ICE candidates. - foreach (var iceCandidate in _rtpIceChannel.Candidates) - { - announcement.IceCandidates.Add(iceCandidate.ToString()); - } + var cmp = a.MLineIndex.CompareTo(b.MLineIndex); + return cmp != 0 ? cmp : string.Compare(a.MediaID, b.MediaID, StringComparison.Ordinal); + }); + foreach (var ann in offerSdp.Media) + { + offerSdp.Group += $" {ann.MediaID}"; + } + } - foreach (var iceCandidate in _applicationIceCandidates) - { - announcement.IceCandidates.Add(iceCandidate.ToString()); - } + return offerSdp; - if (_rtpIceChannel.IceGatheringState == RTCIceGatheringState.complete) - { - announcement.AddExtra($"a={SDP.END_ICE_CANDIDATES_ATTRIBUTE}"); - } - } - }; - - // Media announcements must be in the same order in the offer and answer. - // Existing media types reuse their index from the previous answer; new types - // (not present in RemoteDescription) are appended after all existing m-lines - // per RFC 3264 §8. - int mediaIndex = 0; - int audioMediaIndex = 0; - int videoMediaIndex = 0; - int nextNewMLineIndex = RemoteDescription?.Media.Count ?? 0; - foreach (var mediaStream in mediaStreamList) + // Local function to add ICE candidates to one of the media announcements. + void AddIceCandidates(SDPMediaAnnouncement announcement) + { + if (_rtpIceChannel.Candidates?.Count > 0) { - int mindex = 0; - string midTag = "0"; + announcement.IceCandidates = new List(); - if (RemoteDescription == null) - { - mindex = mediaIndex; - midTag = mediaIndex.ToString(); - } - else + // Add ICE candidates. + foreach (var iceCandidate in _rtpIceChannel.Candidates) { - if (mediaStream.LocalTrack.Kind == SDPMediaTypesEnum.audio) - { - (mindex, midTag) = RemoteDescription.GetIndexForMediaType(mediaStream.LocalTrack.Kind, audioMediaIndex); - audioMediaIndex++; - } - else if (mediaStream.LocalTrack.Kind == SDPMediaTypesEnum.video) - { - (mindex, midTag) = RemoteDescription.GetIndexForMediaType(mediaStream.LocalTrack.Kind, videoMediaIndex); - videoMediaIndex++; - } + announcement.IceCandidates.Add(iceCandidate); } - mediaIndex++; - if (mindex == SDP.MEDIA_INDEX_NOT_PRESENT) + foreach (var iceCandidate in _applicationIceCandidates) { - // New media type added after the initial offer/answer — append it - // after all existing m-lines so the ordering of previously negotiated - // m-lines is preserved (RFC 3264 §8). - mindex = nextNewMLineIndex; - midTag = nextNewMLineIndex.ToString(); - nextNewMLineIndex++; + announcement.IceCandidates.Add(iceCandidate); } + if (_rtpIceChannel.IceGatheringState == RTCIceGatheringState.complete) { - SDPMediaAnnouncement announcement = new SDPMediaAnnouncement( - mediaStream.LocalTrack.Kind, - SDP.IGNORE_RTP_PORT_NUMBER, - mediaStream.LocalTrack.Capabilities); - - announcement.Transport = RTP_MEDIA_PROFILE; - announcement.Connection = new SDPConnectionInformation(IPAddress.Any); - announcement.AddExtra(RTCP_MUX_ATTRIBUTE); - announcement.AddExtra(RTCP_ATTRIBUTE); - announcement.MediaStreamStatus = mediaStream.LocalTrack.StreamStatus; - announcement.MediaID = midTag; - announcement.MLineIndex = mindex; - - announcement.IceUfrag = _rtpIceChannel.LocalIceUser; - announcement.IcePwd = _rtpIceChannel.LocalIcePassword; - announcement.IceOptions = ICE_OPTIONS; - announcement.IceRole = IceRole; - announcement.DtlsFingerprint = dtlsFingerprint; - - if (iceCandidatesAdded == false && !excludeIceCandidates) - { - AddIceCandidates(announcement); - iceCandidatesAdded = true; - } - - if (mediaStream.LocalTrack.Ssrc != 0) - { - string trackCname = mediaStream.RtcpSession?.Cname; - - if (trackCname != null) - { - announcement.SsrcAttributes.Add(new SDPSsrcAttribute(mediaStream.LocalTrack.Ssrc, trackCname, null)); - } - } - - offerSdp.Media.Add(announcement); + announcement.AddExtra($"a={SDP.END_ICE_CANDIDATES_ATTRIBUTE}"); } } + } + } - if (DataChannels.Count > 0 || (RemoteDescription?.Media.Any(x => x.Media == SDPMediaTypesEnum.application) ?? false)) - { - (int mindex, string midTag) = RemoteDescription == null ? (mediaIndex, mediaIndex.ToString()) : RemoteDescription.GetIndexForMediaType(SDPMediaTypesEnum.application, 0); - mediaIndex++; + /// + /// From RFC5764: forward to RTP + /// | | + /// packet --> | 19 < B< 64 -+--> forward to DTLS + /// | | + /// | B< 2 -+--> forward to STUN + /// +----------------+ + /// ]]> + /// + /// The local port on the RTP socket that received the packet. + /// The remote end point the packet was received from. + /// The data received. + private void OnRTPDataReceived(int localPort, IPEndPoint remoteEP, ReadOnlyMemory buffer) + { + //logger.LogDebug($"RTP channel received a packet from {remoteEP}, {buffer?.Length} bytes."); - if (mindex == SDP.MEDIA_INDEX_NOT_PRESENT) - { - logger.LogWarning("Media announcement for data channel establishment omitted due to no reciprocal remote announcement."); - } - else - { - SDPMediaAnnouncement dataChannelAnnouncement = new SDPMediaAnnouncement( - SDPMediaTypesEnum.application, - SDP.IGNORE_RTP_PORT_NUMBER, - new List { new SDPApplicationMediaFormat(SDP_DATACHANNEL_FORMAT_ID) }); - dataChannelAnnouncement.Transport = RTP_MEDIA_DATACHANNEL_UDPDTLS_PROFILE; - dataChannelAnnouncement.Connection = new SDPConnectionInformation(IPAddress.Any); - - dataChannelAnnouncement.SctpPort = SCTP_DEFAULT_PORT; - dataChannelAnnouncement.MaxMessageSize = sctp.maxMessageSize; - dataChannelAnnouncement.MLineIndex = mindex; - dataChannelAnnouncement.MediaID = midTag; - dataChannelAnnouncement.IceUfrag = _rtpIceChannel.LocalIceUser; - dataChannelAnnouncement.IcePwd = _rtpIceChannel.LocalIcePassword; - dataChannelAnnouncement.IceOptions = ICE_OPTIONS; - dataChannelAnnouncement.IceRole = IceRole; - dataChannelAnnouncement.DtlsFingerprint = dtlsFingerprint; - - if (iceCandidatesAdded == false && !excludeIceCandidates) - { - AddIceCandidates(dataChannelAnnouncement); - iceCandidatesAdded = true; - } + // By this point the RTP ICE channel has already processed any STUN packets which means + // it's only necessary to separate RTP/RTCP from DTLS. + // Because DTLS packets can be fragmented and RTP/RTCP should never be, use the RTP/RTCP + // prefix to distinguish. - offerSdp.Media.Add(dataChannelAnnouncement); - } - } - - // Set the Bundle attribute to indicate all media announcements are being multiplexed. - if (offerSdp.Media?.Count > 0) - { - offerSdp.Group = BUNDLE_ATTRIBUTE; - foreach (var ann in offerSdp.Media.OrderBy(x => x.MLineIndex).ThenBy(x => x.MediaID)) - { - offerSdp.Group += $" {ann.MediaID}"; - } + if (!buffer.IsEmpty) + { + // ICE source-address filter (issue #1559). Non-STUN packets are + // only forwarded to DTLS / RTP if their source matches the + // currently nominated ICE candidate pair's remote endpoint. + // Mirrors what libwebrtc does in + // webrtc/p2p/base/connection.cc (Connection::OnReadPacket only + // signals when the packet is from remote_candidate_.address()) + // and what pion does in pion/ice/agent.go (handleInbound + // silently drops non-STUN packets that don't originate from + // the selected pair). + // + // Without this filter an attacker who can guess the local + // port can flood DTLS ClientHello packets to interfere with + // a genuine handshake. STUN packets are filtered out earlier + // in the RTP channel and aren't subject to this check + // (consent freshness / ICE restart / new pair nomination + // still happen via the STUN path). + if (!IsFromSelectedIceCandidate(remoteEP)) + { + var nominatedEP = _rtpIceChannel?.NominatedEntry?.RemoteCandidate?.DestinationEndPoint; + logger.LogWebRtcIceSourceFilterDrop(buffer.Length, remoteEP, nominatedEP); + + return; } - return offerSdp; - } - - /// - /// From RFC5764: forward to RTP - /// | | - /// packet --> | 19 < B< 64 -+--> forward to DTLS - /// | | - /// | B< 2 -+--> forward to STUN - /// +----------------+ - /// ]]> - /// - /// The local port on the RTP socket that received the packet. - /// The remote end point the packet was received from. - /// The data received. - private void OnRTPDataReceived(int localPort, IPEndPoint remoteEP, byte[] buffer) - { - //logger.LogDebug($"RTP channel received a packet from {remoteEP}, {buffer?.Length} bytes."); - - // By this point the RTP ICE channel has already processed any STUN packets which means - // it's only necessary to separate RTP/RTCP from DTLS. - // Because DTLS packets can be fragmented and RTP/RTCP should never be, use the RTP/RTCP - // prefix to distinguish. - - if (buffer?.Length > 0) + try { - // ICE source-address filter (issue #1559). Non-STUN packets are - // only forwarded to DTLS / RTP if their source matches the - // currently nominated ICE candidate pair's remote endpoint. - // Mirrors what libwebrtc does in - // webrtc/p2p/base/connection.cc (Connection::OnReadPacket only - // signals when the packet is from remote_candidate_.address()) - // and what pion does in pion/ice/agent.go (handleInbound - // silently drops non-STUN packets that don't originate from - // the selected pair). - // - // Without this filter an attacker who can guess the local - // port can flood DTLS ClientHello packets to interfere with - // a genuine handshake. STUN packets are filtered out earlier - // in the RTP channel and aren't subject to this check - // (consent freshness / ICE restart / new pair nomination - // still happen via the STUN path). - if (!IsFromSelectedIceCandidate(remoteEP)) + if (buffer.Length > RTPHeader.MIN_HEADER_LEN && buffer.Span[0] >= 128 && buffer.Span[0] <= 191) { - if (logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) - { - var nominatedEP = _rtpIceChannel?.NominatedEntry?.RemoteCandidate?.DestinationEndPoint; - logger.LogDebug( - "Dropped {ByteCount} byte non-STUN packet from {RemoteEndPoint}; nominated ICE remote is {NominatedEndPoint} (issue #1559).", - buffer.Length, remoteEP, nominatedEP); - } - return; + // RTP/RTCP packet. + base.OnReceive(localPort, remoteEP, buffer); } - - try + else { - if (buffer?.Length > RTPHeader.MIN_HEADER_LEN && buffer[0] >= 128 && buffer[0] <= 191) + if (_dtlsHandle is { }) { - // RTP/RTCP packet. - base.OnReceive(localPort, remoteEP, buffer); + //logger.LogDebug($"DTLS transport received {buffer.Length} bytes from {AudioDestinationEndPoint}."); + //TODO: Optimize to avoid array Allocation + _dtlsHandle.WriteToRecvStream(buffer.ToArray()); } else { - if (_dtlsHandle != null) - { - //logger.LogDebug($"DTLS transport received {buffer.Length} bytes from {AudioDestinationEndPoint}."); - _dtlsHandle.WriteToRecvStream(buffer); - } - else - { - logger.LogWarning("DTLS packet received {BufferLength} bytes from {RemoteEndPoint} but no DTLS transport available.", buffer.Length, remoteEP); - } + logger.LogWebRtcDtlsRecvNoTransport(buffer.Length, remoteEP); } } - catch (Exception excp) - { - logger.LogError(excp, "Exception RTCPeerConnection.OnRTPDataReceived {ErrorMessage}", excp.Message); - } } - } - - /// - /// Returns true if matches the address + - /// port of the currently nominated ICE candidate pair's remote - /// candidate. Returns false when no pair has been nominated yet, or - /// when the source endpoint does not match. - /// - /// Used by to filter incoming - /// non-STUN traffic, mirroring the source-check libwebrtc and pion - /// apply at the ICE layer (issue #1559). - /// - /// For TURN-relayed candidates the receive path in - /// already rewrites the remote endpoint - /// from the TURN server's address to the peer's apparent address - /// (XOR-PEER-ADDRESS in the Data indication), so the comparison - /// here works the same way for host and relay pairs. - /// - internal bool IsFromSelectedIceCandidate(IPEndPoint remoteEP) - { - if (remoteEP == null) { return false; } - var nominatedEP = _rtpIceChannel?.NominatedEntry?.RemoteCandidate?.DestinationEndPoint; - if (nominatedEP == null) { return false; } - - // Apply the optional source translator first. This lets callers reconcile - // observed source endpoints with advertised ones for hairpin scenarios (a - // peer on the same machine as the TURN server hitting one of its own - // allocations — the OS picks a local interface IP as source which won't - // match the public IP advertised in XOR-RELAYED-ADDRESS). - var effectiveRemoteEP = RemoteEndpointTranslator?.Invoke(remoteEP) ?? remoteEP; - - // Map IPv4-mapped IPv6 addresses (::ffff:x.x.x.x) to pure IPv4 before comparison. - // This handles the case where the nominated endpoint was stored as IPv4 but - // the received packet shows up as an IPv4-mapped IPv6 address (or vice-versa) - // when using dual-stack sockets. - var nominatedAddr = nominatedEP.Address.IsIPv4MappedToIPv6 - ? nominatedEP.Address.MapToIPv4() - : nominatedEP.Address; - var remoteAddr = effectiveRemoteEP.Address.IsIPv4MappedToIPv6 - ? effectiveRemoteEP.Address.MapToIPv4() - : effectiveRemoteEP.Address; - - return nominatedEP.Port == effectiveRemoteEP.Port - && nominatedAddr.Equals(remoteAddr); - } - - private Func _remoteEndpointTranslator; - - /// - /// Optional hook to normalize the source endpoint of received traffic before it's - /// compared against ICE candidates and the nominated pair. Used to reconcile the - /// address an in-process TURN relay socket uses when sending to a local destination - /// (a local interface IP) with the advertised relay address (typically a public IP - /// in XOR-RELAYED-ADDRESS). - /// - /// The delegate receives the observed source endpoint and returns either a - /// translated endpoint (when it recognizes the source as a known relay socket) - /// or null / the input unchanged when no translation applies. - /// - /// Setting this property also propagates the value to the underlying - /// so peer-reflexive candidate creation honours the - /// same mapping. When unset, behaviour is identical to prior versions. - /// - public Func RemoteEndpointTranslator - { - get => _remoteEndpointTranslator; - set + catch (Exception excp) { - _remoteEndpointTranslator = value; - if (_rtpIceChannel != null) - { - _rtpIceChannel.RemoteEndpointTranslator = value; - } + logger.LogWebRtcRtpDataReceiveError(excp.Message, excp); } } + } - /// - /// Used to add a local ICE candidate. These are for candidates that the application may - /// want to provide in addition to the ones that will be automatically determined. An - /// example is when a machine is behind a 1:1 NAT and the application wants a host - /// candidate with the public IP address to be included. - /// - /// The ICE candidate to add. - /// - /// var natCandidate = new RTCIceCandidate(RTCIceProtocol.udp, natAddress, natPort, RTCIceCandidateType.host); - /// pc.addLocalIceCandidate(natCandidate); - /// - public void addLocalIceCandidate(RTCIceCandidate candidate) + /// + /// Returns true if matches the address + + /// port of the currently nominated ICE candidate pair's remote + /// candidate. Returns false when no pair has been nominated yet, or + /// when the source endpoint does not match. + /// + /// Used by to filter incoming + /// non-STUN traffic, mirroring the source-check libwebrtc and pion + /// apply at the ICE layer (issue #1559). + /// + /// For TURN-relayed candidates the receive path in + /// already rewrites the remote endpoint + /// from the TURN server's address to the peer's apparent address + /// (XOR-PEER-ADDRESS in the Data indication), so the comparison + /// here works the same way for host and relay pairs. + /// + internal bool IsFromSelectedIceCandidate(IPEndPoint? remoteEP) + { + if (remoteEP is null) { - candidate.usernameFragment = _rtpIceChannel.LocalIceUser; - _applicationIceCandidates.Add(candidate); + return false; } - /// - /// Used to add remote ICE candidates to the peer connection's checklist. - /// - /// The remote ICE candidate to add. - public void addIceCandidate(RTCIceCandidateInit candidateInit) + var nominatedEP = _rtpIceChannel?.NominatedEntry?.RemoteCandidate?.DestinationEndPoint; + if (nominatedEP is null) { - RTCIceCandidate candidate = new RTCIceCandidate(candidateInit); + return false; + } - if (_rtpIceChannel.Component == candidate.component) - { - _rtpIceChannel.AddRemoteCandidate(candidate); - } - else + // Apply the optional source translator first. This lets callers reconcile + // observed source endpoints with advertised ones for hairpin scenarios (a + // peer on the same machine as the TURN server hitting one of its own + // allocations — the OS picks a local interface IP as source which won't + // match the public IP advertised in XOR-RELAYED-ADDRESS). + var effectiveRemoteEP = RemoteEndpointTranslator?.Invoke(remoteEP) ?? remoteEP; + + // Map IPv4-mapped IPv6 addresses (::ffff:x.x.x.x) to pure IPv4 before comparison. + // This handles the case where the nominated endpoint was stored as IPv4 but + // the received packet shows up as an IPv4-mapped IPv6 address (or vice-versa) + // when using dual-stack sockets. + var nominatedAddr = nominatedEP.Address.IsIPv4MappedToIPv6 + ? nominatedEP.Address.MapToIPv4() + : nominatedEP.Address; + var remoteAddr = effectiveRemoteEP.Address.IsIPv4MappedToIPv6 + ? effectiveRemoteEP.Address.MapToIPv4() + : effectiveRemoteEP.Address; + + return nominatedEP.Port == effectiveRemoteEP.Port + && nominatedAddr.Equals(remoteAddr); + } + + private Func? _remoteEndpointTranslator; + + /// + /// Optional hook to normalize the source endpoint of received traffic before it's + /// compared against ICE candidates and the nominated pair. Used to reconcile the + /// address an in-process TURN relay socket uses when sending to a local destination + /// (a local interface IP) with the advertised relay address (typically a public IP + /// in XOR-RELAYED-ADDRESS). + /// + /// The delegate receives the observed source endpoint and returns either a + /// translated endpoint (when it recognizes the source as a known relay socket) + /// or null / the input unchanged when no translation applies. + /// + /// Setting this property also propagates the value to the underlying + /// so peer-reflexive candidate creation honours the + /// same mapping. When unset, behaviour is identical to prior versions. + /// + public Func? RemoteEndpointTranslator + { + get => _remoteEndpointTranslator; + set + { + _remoteEndpointTranslator = value; + if (_rtpIceChannel != null) { - logger.LogWarning("Remote ICE candidate not added as no available ICE session for component {Component}.", candidate.component); + _rtpIceChannel.RemoteEndpointTranslator = value; } } + } - /// - /// Restarts the ICE session gathering and connection checks. - /// - public void restartIce() - { - _rtpIceChannel.Restart(); - } + /// + /// Used to add a local ICE candidate. These are for candidates that the application may + /// want to provide in addition to the ones that will be automatically determined. An + /// example is when a machine is behind a 1:1 NAT and the application wants a host + /// candidate with the public IP address to be included. + /// + /// The ICE candidate to add. + /// + /// var natCandidate = new RTCIceCandidate(RTCIceProtocol.udp, natAddress, natPort, RTCIceCandidateType.host); + /// pc.addLocalIceCandidate(natCandidate); + /// + public void addLocalIceCandidate(RTCIceCandidate candidate) + { + candidate.usernameFragment = _rtpIceChannel.LocalIceUser; + _applicationIceCandidates.Add(candidate); + } - /// - /// Gets the initial optional configuration settings this peer connection was created - /// with. - /// - /// If available the initial configuration options. - public RTCConfiguration getConfiguration() + /// + /// Used to add remote ICE candidates to the peer connection's checklist. + /// + /// The remote ICE candidate to add. + public void addIceCandidate(RTCIceCandidateInit candidateInit) + { + var candidate = new RTCIceCandidate(candidateInit); + + if (_rtpIceChannel.Component == candidate.component) { - return _configuration; + _rtpIceChannel.AddRemoteCandidate(candidate); } - - /// - /// Not implemented. Configuration options cannot currently be changed once the peer - /// connection has been initialised. - /// - public void setConfiguration(RTCConfiguration configuration = null) + else { - throw new NotImplementedException(); + logger.LogWebRtcIceSessionError(candidate.component); } + } - /// - /// Once the SDP exchange has been made the SCTP transport ports are known. If the destination - /// port is not using the default value attempt to update it on teh SCTP transprot. - /// - private void UpdatedSctpDestinationPort() - { - // If a data channel was requested by the application then create the SCTP association. - var sctpAnn = RemoteDescription.Media.Where(x => x.Media == SDPMediaTypesEnum.application).FirstOrDefault(); - ushort destinationPort = sctpAnn?.SctpPort != null ? sctpAnn.SctpPort.Value : SCTP_DEFAULT_PORT; + /// + /// Restarts the ICE session gathering and connection checks. + /// + public void restartIce() + { + _rtpIceChannel.Restart(); + } + + /// + /// Gets the initial optional configuration settings this peer connection was created + /// with. + /// + /// If available the initial configuration options. + public RTCConfiguration getConfiguration() + { + return _configuration; + } + + /// + /// Not implemented. Configuration options cannot currently be changed once the peer + /// connection has been initialised. + /// + public void setConfiguration(RTCConfiguration? configuration = null) + { + throw new NotImplementedException(); + } - if (destinationPort != SCTP_DEFAULT_PORT) + /// + /// Once the SDP exchange has been made the SCTP transport ports are known. If the destination + /// port is not using the default value attempt to update it on teh SCTP transprot. + /// + private void UpdatedSctpDestinationPort() + { + Debug.Assert(RemoteDescription is { }); + + // If a data channel was requested by the application then create the SCTP association. + foreach (var ann in RemoteDescription.Media) + { + if (ann.Media == SDPMediaTypesEnum.application) { - sctp.UpdateDestinationPort(destinationPort); + if (ann.SctpPort is { } sctpPort && sctpPort != SCTP_DEFAULT_PORT) + { + sctp.UpdateDestinationPort(sctpPort); + } + + return; } } + } - /// - /// These internal function is used to call Renegotiation Event with delay as the user should call addTrack/removeTrack in sequence so we need a small delay to prevent multiple renegotiation calls - /// - /// Current Executing Task - protected virtual Task StartOnNegotiationNeededTask() - { - const int RENEGOTIATION_CALL_DELAY = 100; + /// + /// These internal function is used to call Renegotiation Event with delay as the user should call addTrack/removeTrack in sequence so we need a small delay to prevent multiple renegotiation calls + /// + /// Current Executing Task + protected virtual Task StartOnNegotiationNeededTask() + { + const int RENEGOTIATION_CALL_DELAY = 100; - //We need to reset the timer every time that we call this function - CancelOnNegotiationNeededTask(); + //We need to reset the timer every time that we call this function + CancelOnNegotiationNeededTask(); - CancellationToken token; - lock (_renegotiationLock) + CancellationToken token; + lock (_renegotiationLock) + { + _cancellationSource = new CancellationTokenSource(); + token = _cancellationSource.Token; + } + return Task.Run(async () => + { + //Call Renegotiation Delayed + await Task.Delay(RENEGOTIATION_CALL_DELAY, token).ConfigureAwait(false); + + //Prevent continue with cancellation requested + if (token.IsCancellationRequested) { - _cancellationSource = new CancellationTokenSource(); - token = _cancellationSource.Token; + return; } - return Task.Run(async () => + else { - //Call Renegotiation Delayed - await Task.Delay(RENEGOTIATION_CALL_DELAY, token); - - //Prevent continue with cancellation requested - if (token.IsCancellationRequested) + if (_requireRenegotiation) { - return; + //We Already Subscribe CancelRenegotiationEventTask in Constructor so we dont need to handle with this function again here + onnegotiationneeded?.Invoke(); } - else - { - if (_requireRenegotiation) - { - //We Already Subscribe CancelRenegotiationEventTask in Constructor so we dont need to handle with this function again here - onnegotiationneeded?.Invoke(); - } - } - }, token); - } + } + }, token); + } - /// - /// Cancel current Negotiation Event Call to prevent running thread to call OnNegotiationNeeded - /// - protected virtual void CancelOnNegotiationNeededTask() + /// + /// Cancel current Negotiation Event Call to prevent running thread to call OnNegotiationNeeded + /// + protected virtual void CancelOnNegotiationNeededTask() + { + lock (_renegotiationLock) { - lock (_renegotiationLock) + if (_cancellationSource is { }) { - if (_cancellationSource != null) + if (!_cancellationSource.IsCancellationRequested) { - if (!_cancellationSource.IsCancellationRequested) - { - _cancellationSource.Cancel(); - } - - _cancellationSource = null; + _cancellationSource.Cancel(); } + + _cancellationSource = null; } } + } - /// - /// Initialises the SCTP transport. This will result in the DTLS SCTP transport listening - /// for incoming INIT packets if the remote peer attempts to create the association. The local - /// peer will NOT attempt to establish the association at this point. It's up to the - /// application to specify it wants a data channel to initiate the SCTP association attempt. - /// - private async Task InitialiseSctpTransport() + /// + /// Initialises the SCTP transport. This will result in the DTLS SCTP transport listening + /// for incoming INIT packets if the remote peer attempts to create the association. The local + /// peer will NOT attempt to establish the association at this point. It's up to the + /// application to specify it wants a data channel to initiate the SCTP association attempt. + /// + private async Task InitialiseSctpTransport() + { + try { - try - { - sctp.OnStateChanged += OnSctpTransportStateChanged; - sctp.Start(_dtlsHandle.Transport, _dtlsHandle.IsClient); + sctp.OnStateChanged += OnSctpTransportStateChanged; + Debug.Assert(_dtlsHandle is { }); + Debug.Assert(_dtlsHandle.Transport is { }); + sctp.Start(_dtlsHandle.Transport, _dtlsHandle.IsClient); - if (DataChannels.Count > 0) - { - await InitialiseSctpAssociation().ConfigureAwait(false); - } - } - catch (Exception excp) + if (DataChannels.Count > 0) { - logger.LogError(excp, "SCTP exception establishing association, data channels will not be available. {ErrorMessage}", excp.Message); - sctp?.Close(); + await InitialiseSctpAssociation().ConfigureAwait(false); } } + catch (Exception excp) + { + logger.LogWebRtcSctpEstablishError(excp.Message, excp); + sctp?.Close(); + } + } - /// - /// Event handler for changes to the SCTP transport state. - /// - /// The new transport state. - private void OnSctpTransportStateChanged(RTCSctpTransportState state) + /// + /// Event handler for changes to the SCTP transport state. + /// + /// The new transport state. + private void OnSctpTransportStateChanged(RTCSctpTransportState state) + { + if (state == RTCSctpTransportState.Connected) { - if (state == RTCSctpTransportState.Connected) - { - logger.LogDebug("SCTP transport successfully connected."); + logger.LogWebRtcSctpTransportConnected(); - sctp.RTCSctpAssociation.OnDataChannelData += OnSctpAssociationDataChunk; - sctp.RTCSctpAssociation.OnDataChannelOpened += OnSctpAssociationDataChannelOpened; - sctp.RTCSctpAssociation.OnNewDataChannel += OnSctpAssociationNewDataChannel; + sctp.RTCSctpAssociation.OnDataChannelData += OnSctpAssociationDataChunk; + sctp.RTCSctpAssociation.OnDataChannelOpened += OnSctpAssociationDataChannelOpened; + sctp.RTCSctpAssociation.OnNewDataChannel += OnSctpAssociationNewDataChannel; - // Create new SCTP streams for any outstanding data channel requests. - foreach (var dataChannel in _dataChannels.ActivatePendingChannels()) - { - OpenDataChannel(dataChannel); - } + // Create new SCTP streams for any outstanding data channel requests. + foreach (var dataChannel in _dataChannels.ActivatePendingChannels()) + { + OpenDataChannel(dataChannel); } } + } - /// - /// Event handler for a new data channel being opened by the remote peer. - /// - private void OnSctpAssociationNewDataChannel(ushort streamID, DataChannelTypes type, ushort priority, uint reliability, string label, string protocol) - { - logger.LogInformation("WebRTC new data channel opened by remote peer for stream ID {StreamID}, type {Type}, priority {Priority}, reliability {Reliability}, label {Label}, protocol {Protocol}.", - streamID, type, priority, reliability, label, protocol); + /// + /// Event handler for a new data channel being opened by the remote peer. + /// + private void OnSctpAssociationNewDataChannel(ushort streamID, DataChannelTypes type, ushort priority, uint reliability, string label, string? protocol) + { + logger.LogWebRtcNewDataChannel(streamID, type, priority, reliability, label, protocol); - // TODO: Set reliability, priority etc. properties on the data channel. - var dc = new RTCDataChannel(sctp) - { - id = streamID, - label = label, - IsOpened = true, - readyState = RTCDataChannelState.open, - protocol = protocol - }; + // TODO: Set reliability, priority etc. properties on the data channel. + var dc = new RTCDataChannel(sctp) + { + id = streamID, + label = label, + IsOpened = true, + readyState = RTCDataChannelState.open, + protocol = protocol + }; - dc.SendDcepAck(); + dc.SendDcepAck(); - if (_dataChannels.AddActiveChannel(dc)) - { - ondatachannel?.Invoke(dc); - } - else - { - // TODO: What's the correct behaviour here?? I guess use the newest one and remove the old one? - logger.LogWarning("WebRTC duplicate data channel requested for stream ID {StreamID}.", streamID); - } + if (_dataChannels.AddActiveChannel(dc)) + { + ondatachannel?.Invoke(dc); } - - /// - /// Event handler for the confirmation that a data channel opened by this peer has been acknowledged. - /// - /// The ID of the stream corresponding to the acknowledged data channel. - private void OnSctpAssociationDataChannelOpened(ushort streamID) + else { - _dataChannels.TryGetChannel(streamID, out var dc); + // TODO: What's the correct behaviour here?? I guess use the newest one and remove the old one? + logger.LogWebRtcDuplicateDataChannel(streamID); + } + } - string label = dc != null ? dc.label : ""; - logger.LogDebug("WebRTC data channel opened label {Label} and stream ID {StreamID}.", label, streamID); + /// + /// Event handler for the confirmation that a data channel opened by this peer has been acknowledged. + /// + /// The ID of the stream corresponding to the acknowledged data channel. + private void OnSctpAssociationDataChannelOpened(ushort streamID) + { + _dataChannels.TryGetChannel(streamID, out var dc); - if (dc != null) - { - dc.GotAck(); - } - else - { - logger.LogWarning("WebRTC data channel got ACK but data channel not found for stream ID {StreamID}.", streamID); - } + var label = dc is { } ? dc.label : ""; + Debug.Assert(label is { }); + logger.LogWebRtcDataChannelOpened(label, streamID); + + if (dc is { }) + { + dc.GotAck(); + } + else + { + logger.LogWebRtcDataChannelIdError(streamID); } + } - /// - /// Event handler for an SCTP DATA chunk being received on the SCTP association. - /// - private void OnSctpAssociationDataChunk(SctpDataFrame frame) + /// + /// Event handler for an SCTP DATA chunk being received on the SCTP association. + /// + private void OnSctpAssociationDataChunk(SctpDataFrame frame) + { + if (_dataChannels.TryGetChannel(frame.StreamID, out var dc)) { - if (_dataChannels.TryGetChannel(frame.StreamID, out var dc)) - { - dc.GotData(frame.StreamID, frame.StreamSeqNum, frame.PPID, frame.UserData); - } - else - { - logger.LogWarning("WebRTC data channel got data but no channel found for stream ID {StreamID}.", frame.StreamID); - } + Debug.Assert(dc is { }); + Debug.Assert(frame.UserData is { }); + dc.GotData(frame.StreamID, frame.StreamSeqNum, frame.PPID, frame.UserData); + } + else + { + logger.LogWebRtcDataChannelForStreamId(frame.StreamID); } + } - /// - /// When a data channel is requested an SCTP association is needed. This method attempts to - /// initialise the association if it is not already available. - /// - private async Task InitialiseSctpAssociation() + /// + /// When a data channel is requested an SCTP association is needed. This method attempts to + /// initialise the association if it is not already available. + /// + private async Task InitialiseSctpAssociation() + { + if (sctp.RTCSctpAssociation.State != SctpAssociationState.Established) { - if (sctp.RTCSctpAssociation.State != SctpAssociationState.Established) - { - sctp.Associate(); - } + sctp.Associate(); + } - if (sctp.state != RTCSctpTransportState.Connected) + if (sctp.state != RTCSctpTransportState.Connected) + { + var onSctpConnectedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + sctp.OnStateChanged += (state) => { - TaskCompletionSource onSctpConnectedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - sctp.OnStateChanged += (state) => + logger.LogWebRtcSctpConnecting(state); + + if (state == RTCSctpTransportState.Connected) { - logger.LogDebug("SCTP transport for create data channel request changed to state {State}.", state); + onSctpConnectedTcs.TrySetResult(true); + } + }; - if (state == RTCSctpTransportState.Connected) - { - onSctpConnectedTcs.TrySetResult(true); - } - }; + var startTime = DateTime.Now; - DateTime startTime = DateTime.Now; + var completedTask = await Task.WhenAny(onSctpConnectedTcs.Task, Task.Delay(SCTP_ASSOCIATE_TIMEOUT_SECONDS * 1000)).ConfigureAwait(false); - var completedTask = await Task.WhenAny(onSctpConnectedTcs.Task, Task.Delay(SCTP_ASSOCIATE_TIMEOUT_SECONDS * 1000)).ConfigureAwait(false); + if (sctp.state != RTCSctpTransportState.Connected) + { + var duration = DateTime.Now.Subtract(startTime).TotalMilliseconds; - if (sctp.state != RTCSctpTransportState.Connected) + if (completedTask != onSctpConnectedTcs.Task) { - var duration = DateTime.Now.Subtract(startTime).TotalMilliseconds; - - if (completedTask != onSctpConnectedTcs.Task) - { - throw new ApplicationException($"SCTP association timed out after {duration:0.##}ms with association in state {sctp.RTCSctpAssociation.State} when attempting to create a data channel."); - } - else - { - throw new ApplicationException($"SCTP association failed after {duration:0.##}ms with association in state {sctp.RTCSctpAssociation.State} when attempting to create a data channel."); - } + throw new SipSorceryException($"SCTP association timed out after {duration:0.##}ms with association in state {sctp.RTCSctpAssociation.State} when attempting to create a data channel."); + } + else + { + throw new SipSorceryException($"SCTP association failed after {duration:0.##}ms with association in state {sctp.RTCSctpAssociation.State} when attempting to create a data channel."); } } } + } - /// - /// Adds a new data channel to the peer connection. - /// - /// - /// WebRTC API definition: - /// https://www.w3.org/TR/webrtc/#methods-11 - /// - /// The label used to identify the data channel. - /// The data channel created. - public async Task createDataChannel(string label, RTCDataChannelInit init = null) + /// + /// Adds a new data channel to the peer connection. + /// + /// + /// WebRTC API definition: + /// https://www.w3.org/TR/webrtc/#methods-11 + /// + /// The label used to identify the data channel. + /// The data channel created. + public async Task createDataChannel(string label, RTCDataChannelInit? init = null) + { + logger.LogWebRtcDataChannelCreate(label); + + var channel = new RTCDataChannel(sctp, init) { - logger.LogDebug("Data channel create request for label {Label}.", label); + label = label, + }; - RTCDataChannel channel = new RTCDataChannel(sctp, init) - { - label = label, - }; + if (connectionState == RTCPeerConnectionState.connected) + { + // If the peer connection is not in a connected state there's no point doing anything + // with the SCTP transport. If the peer connection does connect then a check will + // be made for any pending data channels and the SCTP operations will be done then. - if (connectionState == RTCPeerConnectionState.connected) + if (sctp is null || sctp.state != RTCSctpTransportState.Connected) { - // If the peer connection is not in a connected state there's no point doing anything - // with the SCTP transport. If the peer connection does connect then a check will - // be made for any pending data channels and the SCTP operations will be done then. - - if (sctp == null || sctp.state != RTCSctpTransportState.Connected) + throw new SipSorceryException("No SCTP transport is available."); + } + else + { + if (sctp.RTCSctpAssociation is null || + sctp.RTCSctpAssociation.State != SctpAssociationState.Established) { - throw new ApplicationException("No SCTP transport is available."); + await InitialiseSctpAssociation().ConfigureAwait(false); } - else - { - if (sctp.RTCSctpAssociation == null || - sctp.RTCSctpAssociation.State != SctpAssociationState.Established) - { - await InitialiseSctpAssociation().ConfigureAwait(false); - } - _dataChannels.AddActiveChannel(channel); - OpenDataChannel(channel); + _dataChannels.AddActiveChannel(channel); + OpenDataChannel(channel); - // Wait for the DCEP ACK from the remote peer. - TaskCompletionSource isopen = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - channel.onopen += () => isopen.TrySetResult(string.Empty); - channel.onerror += (err) => isopen.TrySetResult(err); - var error = await isopen.Task.ConfigureAwait(false); + // Wait for the DCEP ACK from the remote peer. + var isopen = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + channel.onopen += () => isopen.TrySetResult(string.Empty); + channel.onerror += (err) => isopen.TrySetResult(err); + var error = await isopen.Task.ConfigureAwait(false); - if (error != string.Empty) - { - throw new ApplicationException($"Data channel creation failed with: {error}"); - } - else - { - return channel; - } + if (!string.IsNullOrEmpty(error)) + { + throw new SipSorceryException($"Data channel creation failed with: {error}"); + } + else + { + return channel; } - } - else - { - // Data channels can be created prior to the SCTP transport being available. - // They will act as placeholders and then be opened once the SCTP transport - // becomes available. - _dataChannels.AddPendingChannel(channel); - return channel; } } - - /// - /// Sends the Data Channel Establishment Protocol (DCEP) OPEN message to configure the data - /// channel on the remote peer. - /// - /// The data channel to open. - private void OpenDataChannel(RTCDataChannel dataChannel) + else { - if (dataChannel.negotiated) - { - logger.LogDebug("WebRTC data channel negotiated out of band with label {Label} and stream ID {StreamID}; invoking open event", dataChannel.label, dataChannel.id); - dataChannel.GotAck(); - } - else if (dataChannel.id.HasValue) - { - logger.LogDebug("WebRTC attempting to open data channel with label {Label} and stream ID {StreamID}.", dataChannel.label, dataChannel.id); - dataChannel.SendDcepOpen(); - } - else - { - logger.LogError("Attempt to open a data channel without an assigned ID has failed."); - } + // Data channels can be created prior to the SCTP transport being available. + // They will act as placeholders and then be opened once the SCTP transport + // becomes available. + _dataChannels.AddPendingChannel(channel); + return channel; } + } - /// - /// DtlsHandshake requires DtlsSrtpTransport to work. - /// DtlsSrtpTransport is similar to C++ DTLS class combined with Srtp class and can perform - /// Handshake as Server or Client in same call. The constructor of transport require a DtlsStrpClient - /// or DtlsSrtpServer to work. - /// - /// The DTLS transport handle to perform the handshake with. - /// True if the DTLS handshake is successful or false if not. - private bool DoDtlsHandshake(DtlsSrtpTransport dtlsHandle) + /// + /// Sends the Data Channel Establishment Protocol (DCEP) OPEN message to configure the data + /// channel on the remote peer. + /// + /// The data channel to open. + private void OpenDataChannel(RTCDataChannel dataChannel) + { + if (dataChannel.negotiated) + { + logger.LogWebRtcDataChannelNegotiated(dataChannel.label, dataChannel.id); + dataChannel.GotAck(); + } + else if (dataChannel.id.HasValue) { - logger.LogDebug("RTCPeerConnection DoDtlsHandshake started."); + logger.LogWebRtcDataChannelOpenAttempt(dataChannel.label, dataChannel.id); + dataChannel.SendDcepOpen(); + } + else + { + logger.LogWebRtcDataChannelIdOpenAttemptFailed(); + } + } - var rtpChannel = PrimaryStream.GetRTPChannel(); + /// + /// DtlsHandshake requires DtlsSrtpTransport to work. + /// DtlsSrtpTransport is similar to C++ DTLS class combined with Srtp class and can perform + /// Handshake as Server or Client in same call. The constructor of transport require a DtlsStrpClient + /// or DtlsSrtpServer to work. + /// + /// The DTLS transport handle to perform the handshake with. + /// True if the DTLS handshake is successful or false if not. + private bool DoDtlsHandshake(DtlsSrtpTransport dtlsHandle) + { + logger.LogWebRtcDtlsHandshakeStarting(); + + Debug.Assert(PrimaryStream is not null); + var rtpChannel = PrimaryStream.GetRTPChannel(); - dtlsHandle.OnDataReady += (buf) => + dtlsHandle.OnDataReady += (buf) + => { - //logger.LogDebug($"DTLS transport sending {buf.Length} bytes to {AudioDestinationEndPoint}."); + Debug.Assert(rtpChannel is { }); + Debug.Assert(PrimaryStream.DestinationEndPoint is { }); rtpChannel.Send(RTPChannelSocketsEnum.RTP, PrimaryStream.DestinationEndPoint, buf); }; - var handshakeResult = dtlsHandle.DoHandshake(out var handshakeError); + var handshakeResult = dtlsHandle.DoHandshake(out var handshakeError); - if (!handshakeResult) - { - handshakeError = handshakeError ?? "unknown"; - logger.LogWarning("RTCPeerConnection DTLS handshake failed with error {HandshakeError}.", handshakeError); - Close("dtls handshake failed"); + if (!handshakeResult) + { + handshakeError = handshakeError ?? "unknown"; + logger.LogWebRtcDtlsHandshakeWarn(handshakeError); + Close("dtls handshake failed"); + return false; + } + else + { + logger.LogWebRtcDtlsHandshakeResult(handshakeResult, dtlsHandle.IsHandshakeComplete()); + + var expectedFp = RemotePeerDtlsFingerprint; + Debug.Assert(expectedFp is { }); + Debug.Assert(expectedFp.algorithm is { }); + Debug.Assert(dtlsHandle is { }); + var remoteCertificate = dtlsHandle.GetRemoteCertificate(); + Debug.Assert(remoteCertificate is { }); + var tlsCertificate = remoteCertificate.GetCertificateAt(0); + Debug.Assert(tlsCertificate is { }); + var remoteFingerprint = DtlsUtils.Fingerprint(expectedFp.algorithm, tlsCertificate); + + if (!string.Equals(remoteFingerprint.value, expectedFp.value, StringComparison.OrdinalIgnoreCase)) + { + logger.LogWebRtcDtlsFingerprintMismatch(expectedFp, remoteFingerprint); + Close("dtls fingerprint mismatch"); return false; } else { - logger.LogDebug("RTCPeerConnection DTLS handshake result {HandshakeResult}, is handshake complete {IsHandshakeComplete}.", - handshakeResult, dtlsHandle.IsHandshakeComplete()); - - var expectedFp = RemotePeerDtlsFingerprint; - var remoteFingerprint = DtlsUtils.Fingerprint(expectedFp.algorithm, dtlsHandle.GetRemoteCertificate().GetCertificateAt(0)); - - if (!string.Equals(remoteFingerprint.value, expectedFp.value, StringComparison.OrdinalIgnoreCase)) - { - logger.LogWarning("RTCPeerConnection remote certificate fingerprint mismatch, expected {ExpectedFingerprint}, actual {RemoteFingerprint}.", expectedFp, remoteFingerprint); - Close("dtls fingerprint mismatch"); - return false; - } - else - { - logger.LogDebug("RTCPeerConnection remote certificate fingerprint matched expected value of {RemoteFingerprintValue} for {RemoteFingerprintAlgorithm}.", remoteFingerprint.value, remoteFingerprint.algorithm); + logger.LogWebRtcRemoteCertificateFingerprint(remoteFingerprint.value, remoteFingerprint.algorithm); - SetGlobalSecurityContext(dtlsHandle.ProtectRTP, - dtlsHandle.UnprotectRTP, - dtlsHandle.ProtectRTCP, - dtlsHandle.UnprotectRTCP); + SetGlobalSecurityContext(dtlsHandle.ProtectRTP, + dtlsHandle.UnprotectRTP, + dtlsHandle.ProtectRTCP, + dtlsHandle.UnprotectRTCP); + IsDtlsNegotiationComplete = true; - IsDtlsNegotiationComplete = true; - - return true; - } + return true; } } + } - /// - /// Event handler for TLS alerts from the DTLS transport. - /// - /// The level of the alert: warning or critical. - /// The type of the alert. - /// An optional description for the alert. - private void OnDtlsAlert(TlsAlertLevelsEnum alertLevel, TlsAlertTypesEnum alertType, string alertDescription) + /// + /// Event handler for TLS alerts from the DTLS transport. + /// + /// The level of the alert: warning or critical. + /// The type of the alert. + /// An optional description for the alert. + private void OnDtlsAlert(TlsAlertLevelsEnum alertLevel, TlsAlertTypesEnum alertType, string alertDescription) + { + if (alertType == TlsAlertTypesEnum.CloseNotify) { - if (alertType == TlsAlertTypesEnum.CloseNotify) - { - logger.LogDebug("Closing peer connection as a result of DTLS close notification."); - - // A DTLS close_notify from the remote peer means the secure - // channel is gone -- the entire peer connection is no longer - // usable. Per the WebRTC spec the RTCDtlsTransport state moves - // to "closed" which propagates to the RTCPeerConnection. - // libwebrtc, Firefox and pion all close the whole peer - // connection at this point. - // - // Without this Close() call the SCTP association alone is - // closed but the underlying RTP/UDP socket keeps running and - // continues to send periodic STUN consent freshness checks + - // RTP/RTCP packets at the now-gone remote port. The remote - // OS responds with ICMP "port unreachable" for each one, - // which surfaces as a tight loop of - // SocketException UdpReceiver.EndReceiveFrom (ConnectionReset) - // warnings until the application shuts itself down. - Close("Remote DTLS close notification received"); - } - else - { - string alertMsg = !string.IsNullOrEmpty(alertDescription) ? $": {alertDescription}" : "."; - logger.LogWarning("DTLS unexpected {AlertLevel} alert {AlertType}{AlertMsg}", alertLevel, alertType, alertMsg); - } + logger.LogWebRtcClosePeerConnection(); + + // A DTLS close_notify from the remote peer means the secure + // channel is gone -- the entire peer connection is no longer + // usable. Per the WebRTC spec the RTCDtlsTransport state moves + // to "closed" which propagates to the RTCPeerConnection. + // libwebrtc, Firefox and pion all close the whole peer + // connection at this point. + // + // Without this Close() call the SCTP association alone is + // closed but the underlying RTP/UDP socket keeps running and + // continues to send periodic STUN consent freshness checks + + // RTP/RTCP packets at the now-gone remote port. The remote + // OS responds with ICMP "port unreachable" for each one, + // which surfaces as a tight loop of + // SocketException UdpReceiver.EndReceiveFrom (ConnectionReset) + // warnings until the application shuts itself down. + Close("Remote DTLS close notification received"); } - - /// - /// Close the session if the instance is out of scope. - /// - protected override void Dispose(bool disposing) + else { - Close("disposed"); + var alertMsg = !string.IsNullOrEmpty(alertDescription) ? $": {alertDescription}" : "."; + logger.LogWebRtcDtlsAlert(alertLevel, alertType, alertMsg); } + } - /// - /// Close the session if the instance is out of scope. - /// - public override void Dispose() - { - Close("disposed"); - } + /// + /// Close the session if the instance is out of scope. + /// + protected override void Dispose(bool disposing) + { + Close("disposed"); + } + + /// + /// Close the session if the instance is out of scope. + /// + public override void Dispose() + { + Close("disposed"); } } diff --git a/src/SIPSorcery/net/WebRTC/RTCPeerSctpAssociation.cs b/src/SIPSorcery/net/WebRTC/RTCPeerSctpAssociation.cs index d9769b8d57..5abfa3844e 100644 --- a/src/SIPSorcery/net/WebRTC/RTCPeerSctpAssociation.cs +++ b/src/SIPSorcery/net/WebRTC/RTCPeerSctpAssociation.cs @@ -24,115 +24,115 @@ //----------------------------------------------------------------------------- using System; +using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Text; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -namespace SIPSorcery.Net -{ - public delegate void OnRTCDataChannelOpened(ushort streamID); +namespace SIPSorcery.Net; + +public delegate void OnRTCDataChannelOpened(ushort streamID); - public delegate void OnNewRTCDataChannel(ushort streamID, DataChannelTypes type, ushort priority, uint reliability, string label, string protocol); +public delegate void OnNewRTCDataChannel(ushort streamID, DataChannelTypes type, ushort priority, uint reliability, string label, string? protocol); - public class RTCPeerSctpAssociation : SctpAssociation +public class RTCPeerSctpAssociation : SctpAssociation +{ + // TODO: Add MTU path discovery. + public const ushort DEFAULT_DTLS_MTU = 1200; + + private static readonly ILogger logger = LogFactory.CreateLogger(); + + /// + /// The DTLS transport to send and receive SCTP packets on. + /// + private RTCSctpTransport _rtcSctpTransport; + + /// + /// Event notifications for user data on an SCTP stream representing a data channel. + /// + public event Action? OnDataChannelData; + + /// + /// Event notifications for the request to open a data channel being confirmed. This + /// event corresponds to the DCEP ACK message for a DCEP OPEN message by this peer. + /// + public event OnRTCDataChannelOpened? OnDataChannelOpened; + + /// + /// Event notification for a new data channel open request from the remote peer. + /// + public event OnNewRTCDataChannel? OnNewDataChannel; + + /// + /// Creates a new SCTP association with the remote peer. + /// + /// The DTLS transport that will be used to encapsulate the + /// SCTP packets. + /// The source port to use when forming the association. + /// The destination port to use when forming the association. + /// Optional. The local UDP port being used for the DTLS connection. This + /// will be set on the SCTP association to aid in diagnostics. + public RTCPeerSctpAssociation(RTCSctpTransport rtcSctpTransport, ushort srcPort, ushort dstPort, int dtlsPort) + : base(rtcSctpTransport, null, srcPort, dstPort, DEFAULT_DTLS_MTU, dtlsPort) { - // TODO: Add MTU path discovery. - public const ushort DEFAULT_DTLS_MTU = 1200; - - private static readonly ILogger logger = LogFactory.CreateLogger(); - - /// - /// The DTLS transport to send and receive SCTP packets on. - /// - private RTCSctpTransport _rtcSctpTransport; - - /// - /// Event notifications for user data on an SCTP stream representing a data channel. - /// - public event Action OnDataChannelData; - - /// - /// Event notifications for the request to open a data channel being confirmed. This - /// event corresponds to the DCEP ACK message for a DCEP OPEN message by this peer. - /// - public event OnRTCDataChannelOpened OnDataChannelOpened; - - /// - /// Event notification for a new data channel open request from the remote peer. - /// - public event OnNewRTCDataChannel OnNewDataChannel; - - /// - /// Creates a new SCTP association with the remote peer. - /// - /// The DTLS transport that will be used to encapsulate the - /// SCTP packets. - /// The source port to use when forming the association. - /// The destination port to use when forming the association. - /// Optional. The local UDP port being used for the DTLS connection. This - /// will be set on the SCTP association to aid in diagnostics. - public RTCPeerSctpAssociation(RTCSctpTransport rtcSctpTransport, ushort srcPort, ushort dstPort, int dtlsPort) - : base(rtcSctpTransport, null, srcPort, dstPort, DEFAULT_DTLS_MTU, dtlsPort) - { - _rtcSctpTransport = rtcSctpTransport; - logger.LogDebug("SCTP creating DTLS based association, is DTLS client {IsDtlsClient}, ID {ID}.", _rtcSctpTransport.IsDtlsClient, ID); + _rtcSctpTransport = rtcSctpTransport; + logger.LogSctpAssociationCreating(_rtcSctpTransport.IsDtlsClient, ID); - OnData += OnDataFrameReceived; - } + OnData += OnDataFrameReceived; + } - /// - /// Event handler for a DATA chunk being received. The chunk can be either a DCEP message or data channel data - /// payload. - /// - /// The received data frame which could represent one or more chunks depending - /// on fragmentation.. - private void OnDataFrameReceived(SctpDataFrame dataFrame) + /// + /// Event handler for a DATA chunk being received. The chunk can be either a DCEP message or data channel data + /// payload. + /// + /// The received data frame which could represent one or more chunks depending + /// on fragmentation.. + private void OnDataFrameReceived(SctpDataFrame dataFrame) + { + switch (dataFrame) { - switch (dataFrame) - { - case var frame when frame.PPID == (uint)DataChannelPayloadProtocols.WebRTC_DCEP: - switch (frame.UserData[0]) - { - case (byte)DataChannelMessageTypes.ACK: - OnDataChannelOpened?.Invoke(frame.StreamID); - break; - case (byte)DataChannelMessageTypes.OPEN: - var dcepOpen = DataChannelOpenMessage.Parse(frame.UserData, 0); - - logger.LogDebug("DCEP OPEN channel type {ChannelType}, priority {Priority}, reliability {Reliability}, label {Label}, protocol {Protocol}.", - dcepOpen.ChannelType, dcepOpen.Priority, dcepOpen.Reliability, dcepOpen.Label, dcepOpen.Protocol); - - DataChannelTypes channelType = DataChannelTypes.DATA_CHANNEL_RELIABLE; - if(Enum.IsDefined(typeof(DataChannelTypes), dcepOpen.ChannelType)) - { - channelType = (DataChannelTypes)dcepOpen.ChannelType; - } - else - { - logger.LogWarning("DECP OPEN channel type of {ChannelType} not recognised, defaulting to {DefaultChannelType}.", dcepOpen.ChannelType, channelType); - } - - OnNewDataChannel?.Invoke( - frame.StreamID, - channelType, - dcepOpen.Priority, - dcepOpen.Reliability, - dcepOpen.Label, - dcepOpen.Protocol); - - break; - default: - logger.LogWarning("DCEP message type {MessageType} not recognised, ignoring.", frame.UserData[0]); - break; - } - break; - - default: - OnDataChannelData?.Invoke(dataFrame); - break; - } + case { PPID: (uint)DataChannelPayloadProtocols.WebRTC_DCEP } frame: + Debug.Assert(frame.UserData is { Length: > 0 }); + switch (frame.UserData[0]) + { + case (byte)DataChannelMessageTypes.ACK: + OnDataChannelOpened?.Invoke(frame.StreamID); + break; + case (byte)DataChannelMessageTypes.OPEN: + var dcepOpen = DataChannelOpenMessage.Parse(frame.UserData); + + logger.LogWebRtcDcepOpen(dcepOpen.ChannelType, dcepOpen.Priority, dcepOpen.Reliability, dcepOpen.Label, dcepOpen.Protocol); + + var channelType = DataChannelTypes.DATA_CHANNEL_RELIABLE; + if (DataChannelTypesExtensions.IsDefined((DataChannelTypes)dcepOpen.ChannelType)) + { + channelType = (DataChannelTypes)dcepOpen.ChannelType; + } + else + { + logger.LogWebRtcDcepUnknownChannelType(dcepOpen.ChannelType, channelType); + } + + OnNewDataChannel?.Invoke( + frame.StreamID, + channelType, + dcepOpen.Priority, + dcepOpen.Reliability, + dcepOpen.Label, + dcepOpen.Protocol); + + break; + default: + logger.LogWebRtcDcepUnrecognized(frame.UserData[0]); + break; + } + break; + + default: + OnDataChannelData?.Invoke(dataFrame); + break; } } } diff --git a/src/SIPSorcery/net/WebRTC/RTCSctpTransport.cs b/src/SIPSorcery/net/WebRTC/RTCSctpTransport.cs index 91739fcbe2..d59059fece 100644 --- a/src/SIPSorcery/net/WebRTC/RTCSctpTransport.cs +++ b/src/SIPSorcery/net/WebRTC/RTCSctpTransport.cs @@ -16,6 +16,8 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; +using System.Diagnostics; using System.Linq; using System.Net.Sockets; using System.Threading; @@ -23,267 +25,271 @@ using Org.BouncyCastle.Tls; using SIPSorcery.Sys; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +public enum RTCSctpTransportState { - public enum RTCSctpTransportState - { - Connecting, - Connected, - Closed - }; + Connecting, + Connected, + Closed +}; + +/// +/// Represents an SCTP transport that uses a DTLS transport. +/// +/// +/// DTLS encapsulation of SCTP: +/// https://tools.ietf.org/html/rfc8261 +/// +/// WebRTC API RTCSctpTransport Interface definition: +/// https://www.w3.org/TR/webrtc/#webidl-1410933428 +/// +public partial class RTCSctpTransport : SctpTransport +{ + private const string THREAD_NAME_PREFIX = "rtcsctprecv-"; + + /// + /// The DTLS transport has no mechanism to cancel a pending receive. The workaround is + /// to set a timeout on each receive call. + /// + private const int RECEIVE_TIMEOUT_MILLISECONDS = 1000; + + /// + /// The default maximum size of payload that can be sent on a data channel. + /// + /// + /// https://www.w3.org/TR/webrtc/#sctp-transport-update-mms + /// + internal const uint SCTP_DEFAULT_MAX_MESSAGE_SIZE = 262144; + + private static readonly ILogger logger = LogFactory.CreateLogger(); + + /// + /// The SCTP ports are redundant for a DTLS transport. There will only ever be one + /// SCTP association so the SCTP ports do not need to be used for end point matching. + /// + public override bool IsPortAgnostic => true; + + /// + /// The transport over which all SCTP packets for data channels + /// will be sent and received. + /// + public DatagramTransport? transport { get; private set; } /// - /// Represents an SCTP transport that uses a DTLS transport. + /// Indicates the role of this peer in the DTLS connection. This influences + /// the selection of stream ID's for SCTP messages. + /// + public bool IsDtlsClient { get; private set; } + + /// + /// The current state of the SCTP transport. + /// + public RTCSctpTransportState state { get; private set; } + + /// + /// The maximum size of data that can be passed to RTCDataChannel's send() method. /// /// - /// DTLS encapsulation of SCTP: - /// https://tools.ietf.org/html/rfc8261 - /// - /// WebRTC API RTCSctpTransport Interface definition: - /// https://www.w3.org/TR/webrtc/#webidl-1410933428 + /// See https://www.w3.org/TR/webrtc/#sctp-transport-update-mms. /// - public class RTCSctpTransport : SctpTransport + public uint maxMessageSize => SCTP_DEFAULT_MAX_MESSAGE_SIZE; + + /// + /// The maximum number of data channel's that can be used simultaneously (where each + /// data channel is a stream on the same SCTP association). + /// + public readonly ushort maxChannels; + + public RTCPeerSctpAssociation RTCSctpAssociation { get; private set; } + + /// + /// Event for notifications about changes to the SCTP transport state. + /// + public event Action? OnStateChanged; + + private bool _isStarted; + private volatile bool _isClosed; + private Thread? _receiveThread; + private readonly object _lock = new object(); + + /// + /// Creates a new SCTP transport that runs on top of an established DTLS connection. + /// + /// The SCTP source port. + /// The SCTP destination port. + /// Optional. The local UDP port being used for the DTLS connection. This + /// will be set on the SCTP association to aid in diagnostics. + public RTCSctpTransport(ushort sourcePort, ushort destinationPort, int dtlsPort) { - private const string THREAD_NAME_PREFIX = "rtcsctprecv-"; - - /// - /// The DTLS transport has no mechanism to cancel a pending receive. The workaround is - /// to set a timeout on each receive call. - /// - private const int RECEIVE_TIMEOUT_MILLISECONDS = 1000; - - /// - /// The default maximum size of payload that can be sent on a data channel. - /// - /// - /// https://www.w3.org/TR/webrtc/#sctp-transport-update-mms - /// - internal const uint SCTP_DEFAULT_MAX_MESSAGE_SIZE = 262144; - - private static readonly ILogger logger = LogFactory.CreateLogger(); - - /// - /// The SCTP ports are redundant for a DTLS transport. There will only ever be one - /// SCTP association so the SCTP ports do not need to be used for end point matching. - /// - public override bool IsPortAgnostic => true; - - /// - /// The transport over which all SCTP packets for data channels - /// will be sent and received. - /// - public DatagramTransport transport { get; private set; } - - /// - /// Indicates the role of this peer in the DTLS connection. This influences - /// the selection of stream ID's for SCTP messages. - /// - public bool IsDtlsClient { get; private set; } - - /// - /// The current state of the SCTP transport. - /// - public RTCSctpTransportState state { get; private set; } - - /// - /// The maximum size of data that can be passed to RTCDataChannel's send() method. - /// - /// - /// See https://www.w3.org/TR/webrtc/#sctp-transport-update-mms. - /// - public uint maxMessageSize => SCTP_DEFAULT_MAX_MESSAGE_SIZE; - - /// - /// The maximum number of data channel's that can be used simultaneously (where each - /// data channel is a stream on the same SCTP association). - /// - public readonly ushort maxChannels; - - public RTCPeerSctpAssociation RTCSctpAssociation { get; private set; } - - /// - /// Event for notifications about changes to the SCTP transport state. - /// - public event Action OnStateChanged; - - private bool _isStarted; - private volatile bool _isClosed; - private Thread _receiveThread; - private readonly object _lock = new object(); - - /// - /// Creates a new SCTP transport that runs on top of an established DTLS connection. - /// - /// The SCTP source port. - /// The SCTP destination port. - /// Optional. The local UDP port being used for the DTLS connection. This - /// will be set on the SCTP association to aid in diagnostics. - public RTCSctpTransport(ushort sourcePort, ushort destinationPort, int dtlsPort) - { - SetState(RTCSctpTransportState.Closed); + SetState(RTCSctpTransportState.Closed); - RTCSctpAssociation = new RTCPeerSctpAssociation(this, sourcePort, destinationPort, dtlsPort); - RTCSctpAssociation.OnAssociationStateChanged += OnAssociationStateChanged; - } + RTCSctpAssociation = new RTCPeerSctpAssociation(this, sourcePort, destinationPort, dtlsPort); + RTCSctpAssociation.OnAssociationStateChanged += OnAssociationStateChanged; + } - /// - /// Attempts to update the SCTP source port the association managed by this transport will use. - /// - /// The updated source port. - public void UpdateSourcePort(ushort port) + /// + /// Attempts to update the SCTP source port the association managed by this transport will use. + /// + /// The updated source port. + public void UpdateSourcePort(ushort port) + { + if (state != RTCSctpTransportState.Closed) { - if (state != RTCSctpTransportState.Closed) - { - logger.LogWarning("SCTP source port cannot be updated when the transport is in state {State}.", state); - } - else - { - RTCSctpAssociation.UpdateSourcePort(port); - } + logger.LogWebRtcIcePortStateError(state); + } + else + { + RTCSctpAssociation.UpdateSourcePort(port); } + } - /// - /// Attempts to update the SCTP destination port the association managed by this transport will use. - /// - /// The updated destination port. - public void UpdateDestinationPort(ushort port) + /// + /// Attempts to update the SCTP destination port the association managed by this transport will use. + /// + /// The updated destination port. + public void UpdateDestinationPort(ushort port) + { + if (state != RTCSctpTransportState.Closed) { - if (state != RTCSctpTransportState.Closed) - { - logger.LogWarning("SCTP destination port cannot be updated when the transport is in state {State}.", state); - } - else - { - RTCSctpAssociation.UpdateDestinationPort(port); - } + logger.LogWebRtcIcePortStateError(state); } + else + { + RTCSctpAssociation.UpdateDestinationPort(port); + } + } - /// - /// Starts the SCTP transport receive thread. - /// - public void Start(DatagramTransport dtlsTransport, bool isDtlsClient) + /// + /// Starts the SCTP transport receive thread. + /// + public void Start(DatagramTransport dtlsTransport, bool isDtlsClient) + { + if (!_isStarted) { - if (!_isStarted) - { - _isStarted = true; + _isStarted = true; - transport = dtlsTransport; - IsDtlsClient = isDtlsClient; + transport = dtlsTransport; + IsDtlsClient = isDtlsClient; - _receiveThread = new Thread(DoReceive); - _receiveThread.Name = $"{THREAD_NAME_PREFIX}{RTCSctpAssociation.ID}"; - _receiveThread.IsBackground = true; - _receiveThread.Start(); - } + _receiveThread = new Thread(DoReceive); + _receiveThread.Name = $"{THREAD_NAME_PREFIX}{RTCSctpAssociation.ID}"; + _receiveThread.IsBackground = true; + _receiveThread.Start(); } + } - /// - /// Attempts to create and initialise a new SCTP association with the remote party. - /// - public void Associate() - { - SetState(RTCSctpTransportState.Connecting); - RTCSctpAssociation.Init(); - } + /// + /// Attempts to create and initialise a new SCTP association with the remote party. + /// + public void Associate() + { + SetState(RTCSctpTransportState.Connecting); + RTCSctpAssociation.Init(); + } - /// - /// Closes the SCTP association and stops the receive thread. - /// - public void Close() + /// + /// Closes the SCTP association and stops the receive thread. + /// + public void Close() + { + lock (_lock) { - lock (_lock) + if (!_isClosed) { - if (!_isClosed) + if (state == RTCSctpTransportState.Connected) { - if (state == RTCSctpTransportState.Connected) - { - RTCSctpAssociation?.Shutdown(); - } - else - { - // If not connected (e.g. still in Connecting state), Shutdown() won't be called - // which means the data sender thread won't be stopped. Abort ensures cleanup. - RTCSctpAssociation?.Abort(new SctpErrorUserInitiatedAbort { AbortReason = "SCTP transport closing." }); - } - _isClosed = true; + RTCSctpAssociation?.Shutdown(); } + else + { + // If not connected (e.g. still in Connecting state), Shutdown() won't be called + // which means the data sender thread won't be stopped. Abort ensures cleanup. + RTCSctpAssociation?.Abort(new SctpErrorUserInitiatedAbort { AbortReason = "SCTP transport closing." }); + } + _isClosed = true; } } + } - /// - /// Event handler to coordinate changes to the SCTP association state with the overall - /// SCTP transport state. - /// - /// The state of the SCTP association. - private void OnAssociationStateChanged(SctpAssociationState associationState) + /// + /// Event handler to coordinate changes to the SCTP association state with the overall + /// SCTP transport state. + /// + /// The state of the SCTP association. + private void OnAssociationStateChanged(SctpAssociationState associationState) + { + if (associationState == SctpAssociationState.Established) { - if (associationState == SctpAssociationState.Established) - { - SetState(RTCSctpTransportState.Connected); - } - else if (associationState == SctpAssociationState.Closed) - { - SetState(RTCSctpTransportState.Closed); - } + SetState(RTCSctpTransportState.Connected); } - - /// - /// Sets the state for the SCTP transport. - /// - /// The new state to set. - private void SetState(RTCSctpTransportState newState) + else if (associationState == SctpAssociationState.Closed) { - state = newState; - OnStateChanged?.Invoke(state); + SetState(RTCSctpTransportState.Closed); } + } - /// - /// Gets a cookie to send in an INIT ACK chunk. This SCTP - /// transport for a WebRTC peer connection needs to use the same - /// local tag and TSN in every chunk as only a single association - /// is ever maintained. - /// - protected override SctpTransportCookie GetInitAckCookie( - ushort sourcePort, - ushort destinationPort, - uint remoteTag, - uint remoteTSN, - uint remoteARwnd, - string remoteEndPoint, - int lifeTimeExtension = 0) - { - var cookie = new SctpTransportCookie - { - SourcePort = sourcePort, - DestinationPort = destinationPort, - RemoteTag = remoteTag, - RemoteTSN = remoteTSN, - RemoteARwnd = remoteARwnd, - RemoteEndPoint = remoteEndPoint, - Tag = RTCSctpAssociation.VerificationTag, - TSN = RTCSctpAssociation.TSN, - ARwnd = SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW, - CreatedAt = DateTime.Now.ToString("o"), - Lifetime = DEFAULT_COOKIE_LIFETIME_SECONDS + lifeTimeExtension, - HMAC = string.Empty - }; - - return cookie; - } + /// + /// Sets the state for the SCTP transport. + /// + /// The new state to set. + private void SetState(RTCSctpTransportState newState) + { + state = newState; + OnStateChanged?.Invoke(state); + } - /// - /// This method runs on a dedicated thread to listen for incoming SCTP - /// packets on the DTLS transport. - /// - private void DoReceive(object state) + /// + /// Gets a cookie to send in an INIT ACK chunk. This SCTP + /// transport for a WebRTC peer connection needs to use the same + /// local tag and TSN in every chunk as only a single association + /// is ever maintained. + /// + protected override SctpTransportCookie GetInitAckCookie( + ushort sourcePort, + ushort destinationPort, + uint remoteTag, + uint remoteTSN, + uint remoteARwnd, + string remoteEndPoint, + int lifeTimeExtension = 0) + { + var cookie = new SctpTransportCookie { - byte[] recvBuffer = new byte[SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW]; + SourcePort = sourcePort, + DestinationPort = destinationPort, + RemoteTag = remoteTag, + RemoteTSN = remoteTSN, + RemoteARwnd = remoteARwnd, + RemoteEndPoint = remoteEndPoint, + Tag = RTCSctpAssociation.VerificationTag, + TSN = RTCSctpAssociation.TSN, + ARwnd = SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW, + CreatedAt = DateTime.Now.ToString("o"), + Lifetime = DEFAULT_COOKIE_LIFETIME_SECONDS + lifeTimeExtension, + HMAC = string.Empty + }; + + return cookie; + } + /// + /// This method runs on a dedicated thread to listen for incoming SCTP + /// packets on the DTLS transport. + /// + private void DoReceive() + { + var recvBuffer = ArrayPool.Shared.Rent((int)SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW); + + try + { while (!_isClosed) { try { - int bytesRead = transport.Receive(recvBuffer, 0, recvBuffer.Length, RECEIVE_TIMEOUT_MILLISECONDS); + Debug.Assert(transport is { }); + + var bytesRead = transport.Receive(recvBuffer, 0, (int)SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW, RECEIVE_TIMEOUT_MILLISECONDS); if (bytesRead == DtlsSrtpTransport.DTLS_RETRANSMISSION_CODE) { @@ -293,22 +299,20 @@ private void DoReceive(object state) } else if (bytesRead > 0) { - if (!SctpPacket.VerifyChecksum(recvBuffer, 0, bytesRead)) + if (!SctpPacket.VerifyChecksum(recvBuffer.AsSpan(0, bytesRead))) { - logger.LogWarning("SCTP packet received on DTLS transport dropped due to invalid checksum."); + logger.LogRtcSctpDiscardedPacket(); } else { - var pkt = SctpPacket.Parse(recvBuffer, 0, bytesRead); + var pkt = SctpPacket.Parse(recvBuffer.AsSpan(0, bytesRead)); - if (pkt.Chunks.Any(x => x.KnownType == SctpChunkType.INIT)) + if (pkt.Chunks.Find(static x => x.KnownType == SctpChunkType.INIT) is SctpInitChunk initChunk) { - var initChunk = pkt.Chunks.First(x => x.KnownType == SctpChunkType.INIT) as SctpInitChunk; - logger.LogDebug("SCTP INIT packet received, initial tag {InitiateTag}, initial TSN {InitialTSN}.", initChunk.InitiateTag, initChunk.InitialTSN); - + logger.LogRtcSctpInit(initChunk.InitiateTag, initChunk.InitialTSN); GotInit(pkt, null); } - else if (pkt.Chunks.Any(x => x.KnownType == SctpChunkType.COOKIE_ECHO)) + else if (pkt.Chunks.Exists(static x => x.KnownType == SctpChunkType.COOKIE_ECHO)) { // The COOKIE ECHO chunk is the 3rd step in the SCTP handshake when the remote party has // requested a new association be created. @@ -316,13 +320,13 @@ private void DoReceive(object state) if (cookie.IsEmpty()) { - logger.LogWarning("SCTP error acquiring handshake cookie from COOKIE ECHO chunk."); + logger.LogRtcSctpWarning(); } else { RTCSctpAssociation.GotCookie(cookie); - if (pkt.Chunks.Count() > 1) + if (pkt.Chunks.Count > 1) { // There could be DATA chunks after the COOKIE ECHO chunk. RTCSctpAssociation.OnPacketReceived(pkt); @@ -338,59 +342,63 @@ private void DoReceive(object state) else if (_isClosed) { // The DTLS transport has been closed or is no longer available. - logger.LogWarning("SCTP the RTCSctpTransport DTLS transport returned an error."); + logger.LogRtcSctpReceive(); break; } } - catch (ApplicationException appExcp) + catch (SipSorceryException appExcp) { // Treat application exceptions as recoverable, things like SCTP packet parse failures. - logger.LogWarning("SCTP error processing RTCSctpTransport receive. {Message}", appExcp.Message); + logger.LogWebRtcSctpProcessError(appExcp.Message); } - catch (TlsFatalAlert alert) when (alert.InnerException is SocketException) + catch (TlsFatalAlert alert) when (alert.InnerException is SocketException sockExcp) { - var sockExcp = alert.InnerException as SocketException; - logger.LogWarning(sockExcp, "SCTP RTCSctpTransport receive socket failure {SocketErrorCode}.", sockExcp.SocketErrorCode); + logger.LogWebRtcIceSocketError(sockExcp.SocketErrorCode, sockExcp); break; } catch (Exception excp) { - logger.LogError(excp, "SCTP fatal error processing RTCSctpTransport receive. {ErrorMessage}", excp.Message); + logger.LogWebRtcScpError(excp.Message, excp); break; } } + } + finally + { + ArrayPool.Shared.Return(recvBuffer); + } - if (!_isClosed) - { - logger.LogWarning("SCTP association {ID} receive thread stopped.", RTCSctpAssociation.ID); - } - - SetState(RTCSctpTransportState.Closed); + if (!_isClosed) + { + logger.LogRtcSctpAssociation(RTCSctpAssociation.ID); } - /// - /// This method is called by the SCTP association when it wants to send an SCTP packet - /// to the remote party. - /// - /// Not used for the DTLS transport. - /// The buffer containing the data to send. - /// The position in the buffer to send from. - /// The number of bytes to send. - public override void Send(string associationID, byte[] buffer, int offset, int length) + SetState(RTCSctpTransportState.Closed); + } + + /// + /// This method is called by the SCTP association when it wants to send an SCTP packet + /// to the remote party. + /// + /// Not used for the DTLS transport. + /// The buffer containing the data to send. + public override void Send(string? associationID, ReadOnlyMemory buffer, IDisposable? memoryOwner = null) + { + if (buffer.Length > maxMessageSize) { - if (length > maxMessageSize) - { - throw new ApplicationException($"RTCSctpTransport was requested to send data of length {length} that exceeded the maximum allowed message size of {maxMessageSize}."); - } + throw new SipSorceryException($"RTCSctpTransport was requested to send data of length {buffer.Length} " + + $" that exceeded the maximum allowed message size of {maxMessageSize}."); + } - if (!_isClosed) + if (!_isClosed) + { + lock (_lock) { - lock (_lock) + if (!_isClosed) { - if (!_isClosed) - { - transport.Send(buffer, offset, length); - } + Debug.Assert(transport is { }); + + transport.Send(buffer); } } } diff --git a/src/SIPSorcery/net/WebRTC/WebRTCRestSignalingPeer.cs b/src/SIPSorcery/net/WebRTC/WebRTCRestSignalingPeer.cs index 8d7e474f41..7edaa160fd 100644 --- a/src/SIPSorcery/net/WebRTC/WebRTCRestSignalingPeer.cs +++ b/src/SIPSorcery/net/WebRTC/WebRTCRestSignalingPeer.cs @@ -17,6 +17,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net; using System.Net.Http; diff --git a/src/SIPSorcery/net/WebRTC/WebRTCWebSocketClient.cs b/src/SIPSorcery/net/WebRTC/WebRTCWebSocketClient.cs index 65d6bb0573..78c74907bc 100644 --- a/src/SIPSorcery/net/WebRTC/WebRTCWebSocketClient.cs +++ b/src/SIPSorcery/net/WebRTC/WebRTCWebSocketClient.cs @@ -16,6 +16,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net.WebSockets; using System.Text; @@ -23,149 +25,148 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// This class is NOT a required component for using WebRTC. It is a +/// convenience class provided to assist when using a corresponding WebRTC peer +/// running a web socket server (which is the case for most of the demo applications +/// that go with this library). +/// +public class WebRTCWebSocketClient { + private const int MAX_RECEIVE_BUFFER = 8192; + private const int MAX_SEND_BUFFER = 8192; + private const int WEB_SOCKET_CONNECTION_TIMEOUT_MS = 10000; + + private readonly ILogger logger = LogFactory.CreateLogger(); + + private Uri _webSocketServerUri; + private Func> _createPeerConnection; + + private RTCPeerConnection _pc; + public RTCPeerConnection RTCPeerConnection => _pc; + /// - /// This class is NOT a required component for using WebRTC. It is a - /// convenience class provided to assist when using a corresponding WebRTC peer - /// running a web socket server (which is the case for most of the demo applications - /// that go with this library). + /// Default constructor. /// - public class WebRTCWebSocketClient + /// The web socket server URL to connect to for the SDP and + /// ICE candidate exchange. + public WebRTCWebSocketClient( + string webSocketServer, + Func> createPeerConnection) { - private const int MAX_RECEIVE_BUFFER = 8192; - private const int MAX_SEND_BUFFER = 8192; - private const int WEB_SOCKET_CONNECTION_TIMEOUT_MS = 10000; - - private readonly ILogger logger = LogFactory.CreateLogger(); - - private Uri _webSocketServerUri; - private Func> _createPeerConnection; - - private RTCPeerConnection _pc; - public RTCPeerConnection RTCPeerConnection => _pc; - - /// - /// Default constructor. - /// - /// The web socket server URL to connect to for the SDP and - /// ICE candidate exchange. - public WebRTCWebSocketClient( - string webSocketServer, - Func> createPeerConnection) + if (string.IsNullOrWhiteSpace(webSocketServer)) { - if (string.IsNullOrWhiteSpace(webSocketServer)) - { - throw new ArgumentNullException("The web socket server URI must be supplied."); - } - - _webSocketServerUri = new Uri(webSocketServer); - _createPeerConnection = createPeerConnection; + throw new ArgumentNullException("The web socket server URI must be supplied."); } - /// - /// Creates a new WebRTC peer connection and then starts polling the web socket server. - /// An SDP offer is expected from the server. Once it has been received an SDP answer - /// will be returned. - /// - public async Task Start(CancellationToken cancellation) - { - _pc = await _createPeerConnection().ConfigureAwait(false); + _webSocketServerUri = new Uri(webSocketServer); + _createPeerConnection = createPeerConnection; + } - logger.LogDebug("websocket-client attempting to connect to {WebSocketServerUri}.", _webSocketServerUri); + /// + /// Creates a new WebRTC peer connection and then starts polling the web socket server. + /// An SDP offer is expected from the server. Once it has been received an SDP answer + /// will be returned. + /// + public async Task Start(CancellationToken cancellation) + { + _pc = await _createPeerConnection().ConfigureAwait(false); - var webSocketClient = new ClientWebSocket(); - // As best I can tell the point of the CreateClientBuffer call is to set the size of the internal - // web socket buffers. The return buffer seems to be for cases where direct access to the raw - // web socket data is desired. - _ = WebSocket.CreateClientBuffer(MAX_RECEIVE_BUFFER, MAX_SEND_BUFFER); - CancellationTokenSource connectCts = new CancellationTokenSource(); - connectCts.CancelAfter(WEB_SOCKET_CONNECTION_TIMEOUT_MS); - await webSocketClient.ConnectAsync(_webSocketServerUri, connectCts.Token).ConfigureAwait(false); + logger.LogDebug("websocket-client attempting to connect to {WebSocketServerUri}.", _webSocketServerUri); - if (webSocketClient.State == WebSocketState.Open) - { - logger.LogDebug("websocket-client starting receive task for server {WebSocketServerUri}.", _webSocketServerUri); + var webSocketClient = new ClientWebSocket(); + // As best I can tell the point of the CreateClientBuffer call is to set the size of the internal + // web socket buffers. The return buffer seems to be for cases where direct access to the raw + // web socket data is desired. + _ = WebSocket.CreateClientBuffer(MAX_RECEIVE_BUFFER, MAX_SEND_BUFFER); + CancellationTokenSource connectCts = new CancellationTokenSource(); + connectCts.CancelAfter(WEB_SOCKET_CONNECTION_TIMEOUT_MS); + await webSocketClient.ConnectAsync(_webSocketServerUri, connectCts.Token).ConfigureAwait(false); - _pc.onicecandidate += async (candidate) => - { - logger.LogDebug("WebRTCWebSocketClient sending ICE candidate to server."); - await webSocketClient.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(candidate.toJSON())), WebSocketMessageType.Text, true, cancellation); - }; + if (webSocketClient.State == WebSocketState.Open) + { + logger.LogDebug("websocket-client starting receive task for server {WebSocketServerUri}.", _webSocketServerUri); - _ = Task.Run(() => ReceiveFromWebSocket(_pc, webSocketClient, cancellation)).ConfigureAwait(false); - } - else + _pc.onicecandidate += async (candidate) => { - _pc.Close("web socket connection failure"); - } + logger.LogDebug("WebRTCWebSocketClient sending ICE candidate to server."); + await webSocketClient.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(candidate.toJSON())), WebSocketMessageType.Text, true, cancellation); + }; + + _ = Task.Run(() => ReceiveFromWebSocket(_pc, webSocketClient, cancellation)).ConfigureAwait(false); + } + else + { + _pc.Close("web socket connection failure"); } + } + + private async Task ReceiveFromWebSocket(RTCPeerConnection pc, ClientWebSocket ws, CancellationToken ct) + { + var buffer = new byte[MAX_RECEIVE_BUFFER]; + int posn = 0; - private async Task ReceiveFromWebSocket(RTCPeerConnection pc, ClientWebSocket ws, CancellationToken ct) + while (ws.State == WebSocketState.Open && + (pc.connectionState == RTCPeerConnectionState.@new || pc.connectionState == RTCPeerConnectionState.connecting)) { - var buffer = new byte[MAX_RECEIVE_BUFFER]; - int posn = 0; + WebSocketReceiveResult receiveResult; + do + { + receiveResult = await ws.ReceiveAsync(new ArraySegment(buffer, posn, MAX_RECEIVE_BUFFER - posn), ct).ConfigureAwait(false); + posn += receiveResult.Count; + } + while (!receiveResult.EndOfMessage); - while (ws.State == WebSocketState.Open && - (pc.connectionState == RTCPeerConnectionState.@new || pc.connectionState == RTCPeerConnectionState.connecting)) + if (posn > 0) { - WebSocketReceiveResult receiveResult; - do - { - receiveResult = await ws.ReceiveAsync(new ArraySegment(buffer, posn, MAX_RECEIVE_BUFFER - posn), ct).ConfigureAwait(false); - posn += receiveResult.Count; - } - while (!receiveResult.EndOfMessage); + var jsonMsg = Encoding.UTF8.GetString(buffer, 0, posn); + string jsonResp = await OnMessage(jsonMsg, pc); - if (posn > 0) + if (jsonResp != null) { - var jsonMsg = Encoding.UTF8.GetString(buffer, 0, posn); - string jsonResp = await OnMessage(jsonMsg, pc); - - if (jsonResp != null) - { - await ws.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(jsonResp)), WebSocketMessageType.Text, true, ct).ConfigureAwait(false); - } + await ws.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(jsonResp)), WebSocketMessageType.Text, true, ct).ConfigureAwait(false); } - - posn = 0; } - logger.LogDebug("websocket-client receive loop exiting."); + posn = 0; } - private async Task OnMessage(string jsonStr, RTCPeerConnection pc) + logger.LogDebug("websocket-client receive loop exiting."); + } + + private async Task OnMessage(string jsonStr, RTCPeerConnection pc) + { + if (RTCIceCandidateInit.TryParse(jsonStr, out var iceCandidateInit)) + { + logger.LogDebug("Got remote ICE candidate."); + pc.addIceCandidate(iceCandidateInit); + } + else if (RTCSessionDescriptionInit.TryParse(jsonStr, out var descriptionInit)) { - if (RTCIceCandidateInit.TryParse(jsonStr, out var iceCandidateInit)) + logger.LogDebug("Got remote SDP, type {DescriptionType}.", descriptionInit.type); + + var result = pc.setRemoteDescription(descriptionInit); + if (result != SetDescriptionResultEnum.OK) { - logger.LogDebug("Got remote ICE candidate."); - pc.addIceCandidate(iceCandidateInit); + logger.LogWarning("Failed to set remote description, {Result}.", result); + pc.Close("failed to set remote description"); } - else if (RTCSessionDescriptionInit.TryParse(jsonStr, out var descriptionInit)) - { - logger.LogDebug("Got remote SDP, type {DescriptionType}.", descriptionInit.type); - var result = pc.setRemoteDescription(descriptionInit); - if (result != SetDescriptionResultEnum.OK) - { - logger.LogWarning("Failed to set remote description, {Result}.", result); - pc.Close("failed to set remote description"); - } - - if (descriptionInit.type == RTCSdpType.offer) - { - var answerSdp = pc.createAnswer(null); - await pc.setLocalDescription(answerSdp).ConfigureAwait(false); - - return answerSdp.toJSON(); - } - } - else + if (descriptionInit.type == RTCSdpType.offer) { - logger.LogWarning("websocket-client could not parse JSON message. {JsonStr}", jsonStr); - } + var answerSdp = pc.createAnswer(null); + await pc.setLocalDescription(answerSdp).ConfigureAwait(false); - return null; + return answerSdp.toJSON(); + } } + else + { + logger.LogWarning("websocket-client could not parse JSON message. {JsonStr}", jsonStr); + } + + return null; } } diff --git a/src/SIPSorcery/net/WebRTC/WebRTCWebSocketPeer.cs b/src/SIPSorcery/net/WebRTC/WebRTCWebSocketPeer.cs index 3413b5e6ed..fe54e95f58 100644 --- a/src/SIPSorcery/net/WebRTC/WebRTCWebSocketPeer.cs +++ b/src/SIPSorcery/net/WebRTC/WebRTCWebSocketPeer.cs @@ -15,143 +15,144 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using WebSocketSharp; using WebSocketSharp.Server; -namespace SIPSorcery.Net +namespace SIPSorcery.Net; + +/// +/// This class is NOT a required component for using WebRTC. It is a convenience +/// class provided to assist when using a web socket server for the WebRTC +/// signalling. +/// +public class WebRTCWebSocketPeer : WebSocketBehavior { + private readonly ILogger logger = LogFactory.CreateLogger(); + + private RTCPeerConnection _pc; + public RTCPeerConnection RTCPeerConnection => _pc; + /// - /// This class is NOT a required component for using WebRTC. It is a convenience - /// class provided to assist when using a web socket server for the WebRTC - /// signalling. + /// Optional property to allow the peer connection SDP offer options to be set. /// - public class WebRTCWebSocketPeer : WebSocketBehavior - { - private readonly ILogger logger = LogFactory.CreateLogger(); - - private RTCPeerConnection _pc; - public RTCPeerConnection RTCPeerConnection => _pc; + public RTCOfferOptions OfferOptions { get; set; } - /// - /// Optional property to allow the peer connection SDP offer options to be set. - /// - public RTCOfferOptions OfferOptions { get; set; } + /// + /// Optional property to allow the peer connection SDP answer options to be set. + /// + public RTCAnswerOptions AnswerOptions { get; set; } - /// - /// Optional property to allow the peer connection SDP answer options to be set. - /// - public RTCAnswerOptions AnswerOptions { get; set; } + /// + /// Optional filter that can be applied to remote ICE candidates. The filter is + /// primarily intended for use in testing. In real application scenarios it's + /// normally desirable to accept all remote ICE candidates. + /// + public Func FilterRemoteICECandidates { get; set; } - /// - /// Optional filter that can be applied to remote ICE candidates. The filter is - /// primarily intended for use in testing. In real application scenarios it's - /// normally desirable to accept all remote ICE candidates. - /// - public Func FilterRemoteICECandidates { get; set; } + public Func> CreatePeerConnection; - public Func> CreatePeerConnection; + public WebRTCWebSocketPeer() + { } - public WebRTCWebSocketPeer() - { } + protected override async void OnMessage(MessageEventArgs e) + { + //logger.LogDebug($"OnMessage: {e.Data}"); - protected override async void OnMessage(MessageEventArgs e) + if (RTCIceCandidateInit.TryParse(e.Data, out var iceCandidateInit)) { - //logger.LogDebug($"OnMessage: {e.Data}"); + logger.LogDebug("Got remote ICE candidate."); - if (RTCIceCandidateInit.TryParse(e.Data, out var iceCandidateInit)) + bool useCandidate = true; + if (FilterRemoteICECandidates != null && !string.IsNullOrWhiteSpace(iceCandidateInit.candidate)) { - logger.LogDebug("Got remote ICE candidate."); - - bool useCandidate = true; - if (FilterRemoteICECandidates != null && !string.IsNullOrWhiteSpace(iceCandidateInit.candidate)) - { - useCandidate = FilterRemoteICECandidates(iceCandidateInit); - } + useCandidate = FilterRemoteICECandidates(iceCandidateInit); + } - if (!useCandidate) - { - logger.LogDebug("WebRTCWebSocketPeer excluding ICE candidate due to filter: {Candidate}", iceCandidateInit.candidate); - } - else - { - _pc.addIceCandidate(iceCandidateInit); - } + if (!useCandidate) + { + logger.LogDebug("WebRTCWebSocketPeer excluding ICE candidate due to filter: {Candidate}", iceCandidateInit.candidate); + } + else + { + _pc.addIceCandidate(iceCandidateInit); } - else if (RTCSessionDescriptionInit.TryParse(e.Data, out var descriptionInit)) + } + else if (RTCSessionDescriptionInit.TryParse(e.Data, out var descriptionInit)) + { + logger.LogDebug("Got remote SDP, type {DescriptionType}.", descriptionInit.type); + var result = _pc.setRemoteDescription(descriptionInit); + if (result != SetDescriptionResultEnum.OK) { - logger.LogDebug("Got remote SDP, type {DescriptionType}.", descriptionInit.type); - var result = _pc.setRemoteDescription(descriptionInit); - if (result != SetDescriptionResultEnum.OK) - { - logger.LogWarning("Failed to set remote description, {Result}.", result); - _pc.Close("failed to set remote description"); - this.Close(); - } - else + logger.LogWarning("Failed to set remote description, {Result}.", result); + _pc.Close("failed to set remote description"); + this.Close(); + } + else + { + if (_pc.signalingState == RTCSignalingState.have_remote_offer) { - if (_pc.signalingState == RTCSignalingState.have_remote_offer) - { - var answerSdp = _pc.createAnswer(AnswerOptions); - await _pc.setLocalDescription(answerSdp).ConfigureAwait(false); + var answerSdp = _pc.createAnswer(AnswerOptions); + await _pc.setLocalDescription(answerSdp).ConfigureAwait(false); - logger.LogDebug("Sending SDP answer to client {UserEndPoint}.", Context.UserEndPoint); - // Don't log SDP can contain sensitive info, albeit very short lived. - //logger.LogDebug(answerSdp.sdp); + logger.LogDebug("Sending SDP answer to client {UserEndPoint}.", Context.UserEndPoint); + // Don't log SDP can contain sensitive info, albeit very short lived. + //logger.LogDebug(answerSdp.sdp); - Context.WebSocket.Send(answerSdp.toJSON()); - } + Context.WebSocket.Send(answerSdp.toJSON()); } } - else - { - logger.LogWarning("websocket-server could not parse JSON message. {MessageData}", e.Data); - } } - - protected override async void OnOpen() + else { - base.OnOpen(); + logger.LogWarning("websocket-server could not parse JSON message. {MessageData}", e.Data); + } + } - logger.LogDebug("Web socket client connection from {UserEndPoint}.", Context.UserEndPoint); + protected override async void OnOpen() + { + base.OnOpen(); - _pc = await CreatePeerConnection().ConfigureAwait(false); + logger.LogDebug("Web socket client connection from {UserEndPoint}.", Context.UserEndPoint); - _pc.onicecandidate += (iceCandidate) => - { - if (_pc.signalingState == RTCSignalingState.have_remote_offer || - _pc.signalingState == RTCSignalingState.stable) - { - Context.WebSocket.Send(iceCandidate.toJSON()); - } - }; + _pc = await CreatePeerConnection().ConfigureAwait(false); - if (base.Context.QueryString["role"] != "offer") + _pc.onicecandidate += (iceCandidate) => + { + if (_pc.signalingState is RTCSignalingState.have_remote_offer or + RTCSignalingState.stable) { - var offerSdp = _pc.createOffer(OfferOptions); - await _pc.setLocalDescription(offerSdp).ConfigureAwait(false); + Context.WebSocket.Send(iceCandidate.toJSON()); + } + }; - logger.LogDebug("Sending SDP offer to client {UserEndPoint}.", Context.UserEndPoint); - // Don't log SDP can contain sensitive info, albeit very short lived. - //logger.LogDebug(offerSdp.sdp); + if (base.Context.QueryString["role"] != "offer") + { + var offerSdp = _pc.createOffer(OfferOptions); + await _pc.setLocalDescription(offerSdp).ConfigureAwait(false); - try - { - Context.WebSocket.Send(offerSdp.toJSON()); - } - catch (Exception ex) - { - logger.LogError("An error has occurred during the OnOpen event.\n{Exception}.", ex.ToString()); - } + logger.LogDebug("Sending SDP offer to client {UserEndPoint}.", Context.UserEndPoint); + // Don't log SDP can contain sensitive info, albeit very short lived. + //logger.LogDebug(offerSdp.sdp); + + try + { + Context.WebSocket.Send(offerSdp.toJSON()); + } + catch (Exception ex) + { + logger.LogError("An error has occurred during the OnOpen event.\n{Exception}.", ex.ToString()); } } + } - protected override void OnClose(CloseEventArgs e) - { - _pc?.Close("Signalling web socket closed."); - base.OnClose(e); - } + protected override void OnClose(CloseEventArgs e) + { + _pc?.Close("Signalling web socket closed."); + base.OnClose(e); } } diff --git a/src/SIPSorcery/net/WebRTC/WebRTCWebSocketPeerAspNet.cs b/src/SIPSorcery/net/WebRTC/WebRTCWebSocketPeerAspNet.cs index 29a61e1b09..8ee093bcd2 100644 --- a/src/SIPSorcery/net/WebRTC/WebRTCWebSocketPeerAspNet.cs +++ b/src/SIPSorcery/net/WebRTC/WebRTCWebSocketPeerAspNet.cs @@ -15,6 +15,8 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Net.WebSockets; using System.Text; @@ -97,8 +99,8 @@ public async Task Run() { _logger.LogDebug("Got local ICE candidate, {Candidate}.", iceCandidate.candidate); - if (_pc.signalingState == RTCSignalingState.have_remote_offer || - _pc.signalingState == RTCSignalingState.stable) + if (_pc.signalingState is RTCSignalingState.have_remote_offer or + RTCSignalingState.stable) { await SendMessageAsync(iceCandidate.toJSON()); } diff --git a/src/SIPSorcery/net/WebRTC/WebRtcLoggingExtensions.cs b/src/SIPSorcery/net/WebRTC/WebRtcLoggingExtensions.cs new file mode 100644 index 0000000000..5242368ef3 --- /dev/null +++ b/src/SIPSorcery/net/WebRTC/WebRtcLoggingExtensions.cs @@ -0,0 +1,821 @@ +using System; +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto.Tls; +using SIPSorcery.Net.SharpSRTP.DTLS; + +namespace SIPSorcery.Net; + +internal static partial class WebRtcLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelOpen", + Level = LogLevel.Debug, + Message = "Data channel for label {Label} now open.")] + public static partial void LogWebRtcDataChannelOpen( + this ILogger logger, + string? label); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelClose", + Level = LogLevel.Debug, + Message = "Data channel with id {Id} has been closed.")] + public static partial void LogWebRtcDataChannelClose( + this ILogger logger, + ushort? id); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcPeerConnectionClose", + Level = LogLevel.Debug, + Message = "Peer connection closed with reason {Reason}.")] + public static partial void LogWebRtcPeerConnectionClose( + this ILogger logger, + string reason); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelCreate", + Level = LogLevel.Debug, + Message = "Data channel create request for label {Label}.")] + public static partial void LogWebRtcDataChannelCreate( + this ILogger logger, + string label); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSctpConnecting", + Level = LogLevel.Debug, + Message = "SCTP transport for create data channel request changed to state {State}.")] + public static partial void LogWebRtcSctpConnecting( + this ILogger logger, + RTCSctpTransportState state); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcCertificateCreated", + Level = LogLevel.Debug, + Message = "RTCPeerConnection created with DTLS certificate with fingerprint {DtlsCertificateFingerprint} and signature algorithm {DtlsCertificateSignatureAlgorithm}.")] + public static partial void LogWebRtcCertificateCreated( + this ILogger logger, + RTCDtlsFingerprint dtlsCertificateFingerprint, + string dtlsCertificateSignatureAlgorithm); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSctpTransportConnected", + Level = LogLevel.Debug, + Message = "SCTP transport successfully connected.")] + public static partial void LogWebRtcSctpTransportConnected( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcNewDataChannel", + Level = LogLevel.Information, + Message = "WebRTC new data channel opened by remote peer for stream ID {StreamID}, type {Type}, priority {Priority}, reliability {Reliability}, label {Label}, protocol {Protocol}.")] + public static partial void LogWebRtcNewDataChannel( + this ILogger logger, + ushort streamID, + DataChannelTypes type, + ushort priority, + uint reliability, + string label, + string? protocol); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDtlsHandshakeStarted", + Level = LogLevel.Debug, + Message = "Starting DLS handshake with role {IceRole}.")] + public static partial void LogWebRtcDtlsHandshakeStarted( + this ILogger logger, + IceRolesEnum iceRole); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDtlsHandshakeStarting", + Level = LogLevel.Debug, + Message = "RTCPeerConnection DoDtlsHandshake started.")] + public static partial void LogWebRtcDtlsHandshakeStarting( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebSocketClientConnecting", + Level = LogLevel.Debug, + Message = "websocket-client attempting to connect to {WebSocketServerUri}.")] + public static partial void LogWebSocketClientConnecting( + this ILogger logger, + Uri webSocketServerUri); + + [LoggerMessage( + EventId = 0, + EventName = "WebSocketClientStartReceive", + Level = LogLevel.Debug, + Message = "websocket-client starting receive task for server {WebSocketServerUri}.")] + public static partial void LogWebSocketClientStartReceive( + this ILogger logger, + Uri webSocketServerUri); + + [LoggerMessage( + EventId = 0, + EventName = "WebSocketClientSendingIceCandidate", + Level = LogLevel.Debug, + Message = "WebRTCWebSocketClient sending ICE candidate to server.")] + public static partial void LogWebSocketClientSendingIceCandidate( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebSocketClientReceiveLoopExit", + Level = LogLevel.Debug, + Message = "websocket-client receive loop exiting.")] + public static partial void LogWebSocketClientReceiveLoopExit( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebSocketClientGotRemoteIceCandidate", + Level = LogLevel.Debug, + Message = "Got remote ICE candidate.")] + public static partial void LogWebSocketClientGotRemoteIceCandidate( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebSocketClientGotRemoteSdp", + Level = LogLevel.Debug, + Message = "Got remote SDP, type {SdpType}.")] + public static partial void LogWebSocketClientGotRemoteSdp( + this ILogger logger, + RTCSdpType sdpType); + + [LoggerMessage( + EventId = 0, + EventName = "SctpAssociationCreating", + Level = LogLevel.Debug, + Message = "SCTP creating DTLS based association, is DTLS client {IsDtlsClient}, ID {AssociationId}.")] + public static partial void LogSctpAssociationCreating( + this ILogger logger, + bool isDtlsClient, + string associationId); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDcepOpen", + Level = LogLevel.Debug, + Message = "DCEP OPEN channel type {ChannelType}, priority {Priority}, reliability {Reliability}, label {Label}, protocol {Protocol}.")] + public static partial void LogWebRtcDcepOpen( + this ILogger logger, + byte channelType, + ushort priority, + uint reliability, + string label, + string? protocol); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcNoCertificate", + Level = LogLevel.Debug, + Message = "No DTLS certificate is provided in the configuration")] + public static partial void LogWebRtcNoCertificate( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelSendFailed", + Level = LogLevel.Warning, + Message = "WebRTC data channel send failed due to SCTP transport in state {TransportState}.")] + public static partial void LogWebRtcDataChannelSendFailed( + this ILogger logger, + RTCSctpTransportState transportState); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelIdOpenAttemptFailed", + Level = LogLevel.Error, + Message = "Attempt to open a data channel without an assigned ID has failed.")] + public static partial void LogWebRtcDataChannelIdOpenAttemptFailed( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcIceRemoteEndpointChange", + Level = LogLevel.Debug, + Message = "ICE changing connected remote end point to {ConnectedEndpoint}.")] + public static partial void LogWebRtcIceRemoteEndpointChange( + this ILogger logger, + IPEndPoint connectedEndpoint); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcIceConnected", + Level = LogLevel.Debug, + Message = "ICE connected to remote end point {ConnectedEndpoint}.")] + public static partial void LogWebRtcIceConnected( + this ILogger logger, + IPEndPoint connectedEndpoint); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelOpenAttempt", + Level = LogLevel.Debug, + Message = "WebRTC attempting to open data channel with label {Label} and stream ID {StreamID}.")] + public static partial void LogWebRtcDataChannelOpenAttempt( + this ILogger logger, + string? label, + ushort? streamID); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcRemoteDescription", + Level = LogLevel.Debug, + Message = "[setRemoteDescription] - Extension:[{Id} - {Uri}]")] + public static partial void LogWebRtcRemoteDescription( + this ILogger logger, + int id, + string uri); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDtlsHandshakeResult", + Level = LogLevel.Debug, + Message = "RTCPeerConnection DTLS handshake result {HandshakeResult}, is handshake complete {IsHandshakeComplete}.")] + public static partial void LogWebRtcDtlsHandshakeResult( + this ILogger logger, + bool handshakeResult, + bool isHandshakeComplete); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcRemoteCertificateFingerprint", + Level = LogLevel.Debug, + Message = "RTCPeerConnection remote certificate fingerprint matched expected value of {RemoteFingerprintValue} for {RemoteFingerprintAlgorithm}.")] + public static partial void LogWebRtcRemoteCertificateFingerprint( + this ILogger logger, + string? remoteFingerprintValue, + string? remoteFingerprintAlgorithm); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcClosePeerConnection", + Level = LogLevel.Debug, + Message = "Closing peer connection as a result of DTLS close notification.")] + public static partial void LogWebRtcClosePeerConnection( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcCreateOfferHeaderExtension", + Level = LogLevel.Debug, + Message = "[createOffer] - {Media}:[{MediaID}] - Add HeaderExtensions:[{Id} - {Uri}]")] + public static partial void LogWebRtcCreateOfferHeaderExtension( + this ILogger logger, + SDPMediaTypesEnum media, + string? mediaID, + int id, + string uri); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcCreateAnswerHeaderExtension", + Level = LogLevel.Debug, + Message = "[createAnswer] - {Media}:[{MediaID}] - Add HeaderExtensions:[{Id} - {Uri}]")] + public static partial void LogWebRtcCreateAnswerHeaderExtension( + this ILogger logger, + SDPMediaTypesEnum media, + string? mediaID, + int id, + string uri); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelOpened", + Level = LogLevel.Debug, + Message = "WebRTC data channel opened label {Label} and stream ID {StreamID}.")] + public static partial void LogWebRtcDataChannelOpened( + this ILogger logger, + string label, + ushort streamID); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDcepDataChunk", + Level = LogLevel.Trace, + Message = "WebRTC data channel GotData stream ID {StreamID}, stream seqnum {StreamSeqNum}, ppid {PpID}, label {Label}.")] + public static partial void LogWebRtcDcepDataChunk( + this ILogger logger, + ushort streamID, + ushort streamSeqNum, + uint ppID, + string label); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelForStreamId", + Level = LogLevel.Warning, + Message = "WebRTC data channel got data but no channel found for stream ID {StreamID}.")] + public static partial void LogWebRtcDataChannelForStreamId( + this ILogger logger, + ushort streamID); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelNegotiated", + Level = LogLevel.Debug, + Message = "WebRTC data channel negotiated out of band with label {Label} and stream ID {StreamID}; invoking open event")] + public static partial void LogWebRtcDataChannelNegotiated( + this ILogger logger, + string? label, + ushort? streamID); + + [LoggerMessage( + EventId = 0, + EventName = "WebSocketClientConnection", + Level = LogLevel.Debug, + Message = "Web socket client connection from {UserEndPoint}.")] + public static partial void LogWebSocketClientConnection( + this ILogger logger, + object userEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "WebSocketClientSdpOffer", + Level = LogLevel.Debug, + Message = "Sending SDP offer to client {UserEndPoint}.")] + public static partial void LogWebSocketClientSdpOffer( + this ILogger logger, + object userEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "WebSocketClientSdpAnswer", + Level = LogLevel.Debug, + Message = "Sending SDP answer to client {UserEndPoint}.")] + public static partial void LogWebSocketClientSdpAnswer( + this ILogger logger, + object userEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "RtcSctpInit", + Level = LogLevel.Debug, + Message = "SCTP INIT packet received, initial tag {InitiateTag}, initial TSN {InitialTSN}.")] + public static partial void LogRtcSctpInit( + this ILogger logger, + uint initiateTag, + uint initialTSN); + + [LoggerMessage( + EventId = 0, + EventName = "RtcSctpWarning", + Level = LogLevel.Warning, + Message = "SCTP error acquiring handshake cookie from COOKIE ECHO chunk.")] + public static partial void LogRtcSctpWarning( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "RtcSctpAssociation", + Level = LogLevel.Warning, + Message = "SCTP association {ID} receive thread stopped.")] + public static partial void LogRtcSctpAssociation( + this ILogger logger, + string id); + + [LoggerMessage( + EventId = 0, + EventName = "RtcSctpReceive", + Level = LogLevel.Warning, + Message = "SCTP the RTCSctpTransport DTLS transport returned an error.")] + public static partial void LogRtcSctpReceive( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSignalingPeerExcludeIceCandidate", + Level = LogLevel.Debug, + Message = "WebRTCWebSocketPeer excluding ICE candidate due to filter: {Candidate}")] + public static partial void LogWebRtcSignalingPeerExcludeIceCandidate( + this ILogger logger, + string candidate); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSignalingJsonParseFailed", + Level = LogLevel.Warning, + Message = "websocket-server could not parse JSON message. {MessageData}")] + public static partial void LogWebRtcSignalingJsonParseFailed( + this ILogger logger, + string messageData); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSignalingRestStart", + Level = LogLevel.Debug, + Message = "webrtc-rest starting receive task for server {RestServerUri}, our ID {OurID} and their ID {TheirID}.")] + public static partial void LogWebRtcSignalingRestStart( + this ILogger logger, + Uri restServerUri, + string ourID, + string theirID); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSignalingRestSendOffer", + Level = LogLevel.Debug, + Message = "webrtc-rest sending initial SDP offer to server.")] + public static partial void LogWebRtcSignalingRestSendOffer( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSignalingRestRetry", + Level = LogLevel.Debug, + Message = "webrtc-rest server initial connection attempt failed, will retry in {RetryPeriod}ms.")] + public static partial void LogWebRtcSignalingRestRetry( + this ILogger logger, + int retryPeriod); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSignalingRestReceiveExit", + Level = LogLevel.Debug, + Message = "webrtc-rest receive task exiting.")] + public static partial void LogWebRtcSignalingRestReceiveExit( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSignalingRestRemoteCandidate", + Level = LogLevel.Debug, + Message = "Got remote ICE candidate, {Candidate}")] + public static partial void LogWebRtcSignalingRestRemoteCandidate( + this ILogger logger, + string candidate); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcReceiveLoopCancel", + Level = LogLevel.Debug, + Message = "cancelling HTTP receive task.")] + public static partial void LogWebRtcReceiveLoopCancel( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcIceCandidate", + Level = LogLevel.Debug, + Message = "webrtc-rest onicecandidate: {CandidateStr}", + SkipEnabledCheck = true)] + private static partial void LogWebRtcIceCandidateUnchecked( + this ILogger logger, + string candidateStr); + + public static void LogWebRtcIceCandidate( + this ILogger logger, + RTCIceCandidate candidate) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogWebRtcIceCandidateUnchecked(candidate.ToShortString()); + } + } + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSignalingFilterIceCandidate", + Level = LogLevel.Debug, + Message = "WebRTCRestPeer excluding ICE candidate due to filter: {Candidate}")] + public static partial void LogWebRtcSignalingFilterIceCandidate( + this ILogger logger, + string candidate); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSignalingStateRejectOffer", + Level = LogLevel.Warning, + Message = "RTCPeerConnection received an SDP offer but was already in {signalingState} state. Remote offer rejected.")] + public static partial void LogWebRtcSignalingStateRejectOffer( + this ILogger logger, + RTCSignalingState signalingState); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataTransportUnsupported", + Level = LogLevel.Warning, + Message = "The remote SDP requested an unsupported data channel transport of {transport}.")] + public static partial void LogWebRtcDataTransportUnsupported( + this ILogger logger, + string transport); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDtlsFingerprintInvalid", + Level = LogLevel.Warning, + Message = "The DTLS fingerprint was invalid or not supported.")] + public static partial void LogWebRtcDtlsFingerprintInvalid( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDtlsFingerprintMissing", + Level = LogLevel.Warning, + Message = "The DTLS fingerprint was missing from the remote party's session description.")] + public static partial void LogWebRtcDtlsFingerprintMissing( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcIceComponentError", + Level = LogLevel.Warning, + Message = "Remote ICE candidate not added as no available ICE session for component {component}.")] + public static partial void LogWebRtcIceComponentError( + this ILogger logger, + int component); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDtlsAlert", + Level = LogLevel.Warning, + Message = "DTLS unexpected {alertLevel} alert {alertType}{alertMsg}")] + public static partial void LogWebRtcDtlsAlert( + this ILogger logger, + TlsAlertLevelsEnum alertLevel, + TlsAlertTypesEnum alertType, + string alertMsg); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcIcePortStateError", + Level = LogLevel.Warning, + Message = "SCTP source port cannot be updated when the transport is in state {state}.")] + public static partial void LogWebRtcIcePortStateError( + this ILogger logger, + RTCSctpTransportState state); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDuplicateDataChannel", + Level = LogLevel.Warning, + Message = "WebRTC duplicate data channel requested for stream ID {streamId}.")] + public static partial void LogWebRtcDuplicateDataChannel( + this ILogger logger, + ushort streamId); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcScpError", + Level = LogLevel.Error, + Message = "SCTP fatal error processing RTCSctpTransport receive. {errorMessage}")] + public static partial void LogWebRtcScpError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcIceSocketError", + Level = LogLevel.Warning, + Message = "SCTP RTCSctpTransport receive socket failure {socketErrorCode}.")] + public static partial void LogWebRtcIceSocketError( + this ILogger logger, + SocketError socketErrorCode, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDtlsHandshakeError", + Level = LogLevel.Warning, + Message = "RTCPeerConnection DTLS handshake failed. {errorMessage}")] + public static partial void LogWebRtcDtlsHandshakeError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSctpEstablishError", + Level = LogLevel.Error, + Message = "SCTP exception establishing association, data channels will not be available. {errorMessage}")] + public static partial void LogWebRtcSctpEstablishError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDtlsFingerprintMismatch", + Level = LogLevel.Warning, + Message = "RTCPeerConnection remote certificate fingerprint mismatch, expected {expectedFingerprint}, actual {remoteFingerprint}.")] + public static partial void LogWebRtcDtlsFingerprintMismatch( + this ILogger logger, + RTCDtlsFingerprint expectedFingerprint, + RTCDtlsFingerprint remoteFingerprint); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSetDescriptionError", + Level = LogLevel.Warning, + Message = "Failed to set remote description, {result}.")] + public static partial void LogWebRtcSetDescriptionError( + this ILogger logger, + SetDescriptionResultEnum result); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDtlsRecvNoTransport", + Level = LogLevel.Warning, + Message = "DTLS packet received {bufferLength} bytes from {remoteEndPoint} but no DTLS transport available.")] + public static partial void LogWebRtcDtlsRecvNoTransport( + this ILogger logger, + int bufferLength, + IPEndPoint remoteEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcIceSourceFilterDrop", + Level = LogLevel.Debug, + Message = "Dropped {byteCount} byte non-STUN packet from {remoteEndPoint}; nominated ICE remote is {nominatedEndPoint} (issue #1559).")] + public static partial void LogWebRtcIceSourceFilterDrop( + this ILogger logger, + int byteCount, + IPEndPoint remoteEndPoint, + IPEndPoint? nominatedEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcCheckpointExcluded", + Level = LogLevel.Warning, + Message = "Media announcement for {kind} omitted due to no reciprocal remote announcement.")] + public static partial void LogWebRtcCheckpointExcluded( + this ILogger logger, + SDPMediaTypesEnum kind); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelNotFound", + Level = LogLevel.Warning, + Message = "WebRTC data channel got ACK but data channel not found for stream ID {streamId}.")] + public static partial void LogWebRtcDataChannelNotFound( + this ILogger logger, + ushort streamId); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDcepUnrecognized", + Level = LogLevel.Warning, + Message = "DCEP message type {messageType} not recognised, ignoring.")] + public static partial void LogWebRtcDcepUnrecognized( + this ILogger logger, + byte messageType); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcGatheringTimeout", + Level = LogLevel.Warning, + Message = "ICE gathering timed out after {gatherTimeoutMs}Ms")] + public static partial void LogWebRtcGatheringTimeout( + this ILogger logger, + int gatherTimeoutMs); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDcepUnknownChannelType", + Level = LogLevel.Warning, + Message = "DECP OPEN channel type of {channelType} not recognised, defaulting to {defaultChannelType}.")] + public static partial void LogWebRtcDcepUnknownChannelType( + this ILogger logger, + byte channelType, + DataChannelTypes defaultChannelType); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSendOfferError", + Level = LogLevel.Error, + Message = "An error has occurred during the OnOpen event.")] + public static partial void LogWebRtcSendOfferError( + this ILogger logger, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "RtcSctpDiscardedPacket", + Level = LogLevel.Warning, + Message = "SCTP packet received on DTLS transport dropped due to invalid checksum.")] + public static partial void LogRtcSctpDiscardedPacket( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcCertificateFingerprint", + Level = LogLevel.Warning, + Message = "RTCPeerConnection was passed a certificate for {friendlyName} with a non-exportable RSA private key.")] + public static partial void LogWebRtcCertificateFingerprint( + this ILogger logger, + string friendlyName); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSctpProcessError", + Level = LogLevel.Warning, + Message = "SCTP error processing RTCSctpTransport receive. {message}")] + public static partial void LogWebRtcSctpProcessError( + this ILogger logger, + string message); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcIceSessionError", + Level = LogLevel.Warning, + Message = "Remote ICE candidate not added as no available ICE session for component {component}.")] + public static partial void LogWebRtcIceSessionError( + this ILogger logger, + RTCIceComponent component); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDataChannelIdError", + Level = LogLevel.Warning, + Message = "WebRTC data channel got ACK but data channel not found for stream ID {streamId}.")] + public static partial void LogWebRtcDataChannelIdError( + this ILogger logger, + ushort streamId); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcRestServerError", + Level = LogLevel.Warning, + Message = "webrtc-rest server connection attempt failed.")] + public static partial void LogWebRtcRestServerError( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcRestServerDecodeError", + Level = LogLevel.Warning, + Message = "webrtc-rest could not parse JSON message. {signal}")] + public static partial void LogWebRtcRestServerDecodeError( + this ILogger logger, + string signal); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcRestTaskError", + Level = LogLevel.Error, + Message = "Exception receiving webrtc signal. {errorMessage}")] + public static partial void LogWebRtcRestTaskError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcDtlsHandshakeWarn", + Level = LogLevel.Warning, + Message = "RTCPeerConnection DTLS handshake failed with error {handshakeError}.")] + public static partial void LogWebRtcDtlsHandshakeWarn( + this ILogger logger, + string handshakeError); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcRtpDataReceiveError", + Level = LogLevel.Error, + Message = "Exception RTCPeerConnection.OnRTPDataReceived {errorMessage}")] + public static partial void LogWebRtcRtpDataReceiveError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcMediaAnnouncementWarn", + Level = LogLevel.Warning, + Message = "Media announcement for data channel establishment omitted due to no reciprocal remote announcement.")] + public static partial void LogWebRtcMediaAnnouncementWarn( + this ILogger logger); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcSessionDescription", + Level = LogLevel.Debug, + Message = "WebRTC local session description set to {Type}.")] + public static partial void LogWebRtcSessionDescription( + this ILogger logger, + string type); + + [LoggerMessage( + EventId = 0, + EventName = "WebRtcGatheringCompleteTimeout", + Level = LogLevel.Warning, + Message = "Waiting for ICE gathering to complete timed out after {GatherTimeoutMs}ms")] + public static partial void LogWebRtcGatheringCompleteTimeout( + this ILogger logger, + int gatherTimeoutMs); +} diff --git a/src/SIPSorcery/sys/ArgumentExceptionExtensions.cs b/src/SIPSorcery/sys/ArgumentExceptionExtensions.cs new file mode 100644 index 0000000000..0b24dcd159 --- /dev/null +++ b/src/SIPSorcery/sys/ArgumentExceptionExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SIPSorcery.Sys; + +internal static class ArgumentExceptionExtensions +{ + extension(global::System.ArgumentException ex) + { +#if NET60_OR_GREATER + [StackTraceHidden] +#endif + public static void ThrowIfEmpty(ReadOnlySpan argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (argument.Length == 0) + { + ThrowArgumentException("Argument cannot be empty.", paramName); + } + } + +#if NET60_OR_GREATER + [StackTraceHidden] +#endif + public static void ThrowIfEmptyWhiteSpace(ReadOnlySpan argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (argument.IsEmptyOrWhiteSpace()) + { + ThrowArgumentException("The value cannot be empty or composed entirely of whitespace.", paramName); + } + } + + [DoesNotReturn] + private static void ThrowArgumentException(string message, string? paramName) => throw new ArgumentException(message, paramName); + } +} diff --git a/src/SIPSorcery/net/DtlsSrtp/Lib/PolyfillExtensions.cs b/src/SIPSorcery/sys/ArraySegmentExtensions.cs similarity index 81% rename from src/SIPSorcery/net/DtlsSrtp/Lib/PolyfillExtensions.cs rename to src/SIPSorcery/sys/ArraySegmentExtensions.cs index ce2f737d80..63787cf59c 100644 --- a/src/SIPSorcery/net/DtlsSrtp/Lib/PolyfillExtensions.cs +++ b/src/SIPSorcery/sys/ArraySegmentExtensions.cs @@ -1,23 +1,9 @@ using System; using System.Runtime.CompilerServices; -internal static partial class PolyfillExtensions +internal static partial class ArraySegmentExtensions { #if !NET8_0_OR_GREATER - extension(GC) - { - /// - /// Allocate an array while skipping zero-initialization if possible. - /// - /// Specifies the type of the array element. - /// Specifies the length of the array. - [MethodImpl(MethodImplOptions.AggressiveInlining)] // forced to ensure no perf drop for small memory buffers (hot path) - public static T[] AllocateUninitializedArray(int length) // T[] rather than T?[] to match `new T[length]` behavior - { - return new T[length]; - } - } - extension(ArraySegment source) { public int Length => source.Count; @@ -70,7 +56,7 @@ public void CopyTo(T[] destination) } } - extension (byte[] bytes) + extension(byte[] bytes) { public void CopyTo(Span destination) { diff --git a/src/SIPSorcery/sys/BinaryExtensions.cs b/src/SIPSorcery/sys/BinaryExtensions.cs new file mode 100644 index 0000000000..89f6878e89 --- /dev/null +++ b/src/SIPSorcery/sys/BinaryExtensions.cs @@ -0,0 +1,165 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +#if NET8_0_OR_GREATER +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +#endif + +namespace SIPSorcery.Sys; + +internal static class BinaryExtensions +{ + public static ushort ReadUInt16BigEndianAndAdvance(ref ReadOnlySpan buffer, int offset = 0) + { + buffer = buffer.Slice(offset); + var value = BinaryPrimitives.ReadUInt16BigEndian(buffer); + buffer = buffer.Slice(sizeof(ushort)); + return value; + } + + public static uint ReadUInt32BigEndianAndAdvance(ref ReadOnlySpan buffer, int offset = 0) + { + buffer = buffer.Slice(offset); + var value = BinaryPrimitives.ReadUInt32BigEndian(buffer); + buffer = buffer.Slice(sizeof(uint)); + return value; + } + + public static void WriteUInt16BigEndianAndAdvance(ref Span buffer, ushort value, int offset = 0) + { + buffer = buffer.Slice(offset); + BinaryPrimitives.WriteUInt16BigEndian(buffer, value); + buffer = buffer.Slice(sizeof(ushort)); + } + + public static void WriteUInt32BigEndianAndAdvance(ref Span buffer, uint value, int offset = 0) + { + buffer = buffer.Slice(offset); + BinaryPrimitives.WriteUInt32BigEndian(buffer, value); + buffer = buffer.Slice(sizeof(uint)); + } + + public static void Xor(Span data, ReadOnlySpan other) + { + Xor(data, other, data); + } + + public static void Xor(ReadOnlySpan a, ReadOnlySpan b, Span output) + { + var i = 0; + +#if NET8_0_OR_GREATER + ref var aRef = ref MemoryMarshal.GetReference(a); + ref var bRef = ref MemoryMarshal.GetReference(b); + ref var oRef = ref MemoryMarshal.GetReference(output); + + if (Vector512.IsHardwareAccelerated) + { + for (; i <= output.Length - 64; i += 64) + { + (Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)) ^ + Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i))) + .StoreUnsafe(ref Unsafe.Add(ref oRef, i)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + for (; i <= output.Length - 32; i += 32) + { + (Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)) ^ + Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i))) + .StoreUnsafe(ref Unsafe.Add(ref oRef, i)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; i <= output.Length - 16; i += 16) + { + (Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)) ^ + Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i))) + .StoreUnsafe(ref Unsafe.Add(ref oRef, i)); + } + } +#endif + + for (; i <= output.Length - 8; i += 8) + { + BinaryPrimitives.WriteUInt64BigEndian(output.Slice(i, 8), + BinaryPrimitives.ReadUInt64BigEndian(a.Slice(i, 8)) ^ + BinaryPrimitives.ReadUInt64BigEndian(b.Slice(i, 8))); + } + + if (i <= output.Length - 4) + { + BinaryPrimitives.WriteUInt32BigEndian(output.Slice(i, 4), + BinaryPrimitives.ReadUInt32BigEndian(a.Slice(i, 4)) ^ + BinaryPrimitives.ReadUInt32BigEndian(b.Slice(i, 4))); + i += 4; + } + + if (i <= output.Length - 2) + { + BinaryPrimitives.WriteUInt16BigEndian(output.Slice(i, 2), + (ushort)(BinaryPrimitives.ReadUInt16BigEndian(a.Slice(i, 2)) ^ + BinaryPrimitives.ReadUInt16BigEndian(b.Slice(i, 2)))); + i += 2; + } + + if (i < output.Length) + { + output[i] = (byte)(a[i] ^ b[i]); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Xor128(Span data, ReadOnlySpan other) + { +#if NET8_0_OR_GREATER + var result = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(data)) ^ + Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(other)); + result.StoreUnsafe(ref MemoryMarshal.GetReference(data)); +#else + Xor64(data, other); + Xor64(data.Slice(8), other.Slice(8)); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Xor64(Span data, ReadOnlySpan other) + { + Xor64(data, BinaryPrimitives.ReadUInt64BigEndian(other)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Xor64(Span data, ulong other) + { + BinaryPrimitives.WriteUInt64BigEndian(data, BinaryPrimitives.ReadUInt64BigEndian(data) ^ other); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Xor32(Span data, ReadOnlySpan other) + { + Xor32(data, BinaryPrimitives.ReadUInt32BigEndian(other)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Xor32(Span data, uint other) + { + BinaryPrimitives.WriteUInt32BigEndian(data, BinaryPrimitives.ReadUInt32BigEndian(data) ^ other); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Xor16(Span data, ReadOnlySpan other) + { + Xor16(data, BinaryPrimitives.ReadUInt16BigEndian(other)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Xor16(Span data, ushort other) + { + BinaryPrimitives.WriteUInt16BigEndian(data, (ushort)(BinaryPrimitives.ReadUInt16BigEndian(data) ^ other)); + } +} diff --git a/src/SIPSorcery/sys/BouncyCastleExtensions.cs b/src/SIPSorcery/sys/BouncyCastleExtensions.cs new file mode 100644 index 0000000000..a25cfed220 --- /dev/null +++ b/src/SIPSorcery/sys/BouncyCastleExtensions.cs @@ -0,0 +1,155 @@ +using System; +using System.Buffers; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Tls; + +namespace SIPSorcery.Sys; + +internal static class BouncyCastleExtensions +{ + extension(DatagramSender datagramSender) + { + public void Send(ReadOnlyMemory buffer) + { +#if NET6_0_OR_GREATER + datagramSender.Send(buffer.Span); +#else + if (MemoryMarshal.TryGetArray(buffer, out ArraySegment segment)) + { + datagramSender.Send(segment.Array, segment.Offset, segment.Count); + } + else + { + throw new NotSupportedException("Only array-backed memory is supported."); + } +#endif + } + } + +#if !NETSTANDARD2_0_OR_GREATER || !NETCOREAPP2_1_OR_GREATER + extension(GeneralDigest digest) + { + public void BlockUpdate(ReadOnlySpan input) + { + digest.BlockUpdate(input.ToArray(), 0, input.Length); + } + + public int DoFinal(Span output) + { + var tempBuffer = new byte[output.Length]; + var result = digest.DoFinal(tempBuffer, 0); + tempBuffer.CopyTo(output); + return result; + } + } +#endif + + extension(KeyParameter) + { +#if NET8_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static KeyParameter Create(ReadOnlyMemory key) + { + return new KeyParameter(key.Span); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static KeyParameter Create(ReadOnlySpan key) + { + return new KeyParameter(key); + } +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static KeyParameter Create(ReadOnlyMemory memory) + { + if (System.Runtime.InteropServices.MemoryMarshal.TryGetArray(memory, out ArraySegment segment)) + { + return KeyParameter.Create(segment); + } + // Fallback for non-array-backed memory + return new KeyParameter(memory.ToArray()); + } +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static KeyParameter Create(ArraySegment key) + { + return new KeyParameter(key.Array, key.Offset, key.Count); + } + } + +#if !NETCOREAPP2_1_OR_GREATER && !NETSTANDARD2_1_OR_GREATER + extension(IBlockCipher blockCipher) + { + /// Process a block. + /// The input block as a span. + /// The output span. + /// If input block is wrong size, or output span too small. + /// The number of bytes processed and produced. + public int ProcessBlock(ReadOnlySpan input, Span output) + { + var buffer = ArrayPool.Shared.Rent(output.Length); + + try + { + var result = blockCipher.ProcessBlock(input.ToArray(), 0, buffer, 0); + buffer.AsSpan(0, result).CopyTo(output); + return result; + } + finally + { + ArrayPool.Shared.Return(buffer, true); + } + } + } +#endif + +#if !NET8_0_OR_GREATER + extension(IBlockCipher engine) + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ProcessBlock(ArraySegment input, ArraySegment output) + { + return engine.ProcessBlock(input.Array, input.Offset, output.Array, output.Offset); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ProcessBlock(byte[] input, byte[] output) + { + return engine.ProcessBlock(input, 0, output, 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ProcessBlock(byte[] input, ArraySegment output) + { + return engine.ProcessBlock(input, 0, output.Array, output.Offset); + } + } + + extension(IAeadBlockCipher engine) + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ProcessBytes(ArraySegment input, ArraySegment output) + { + return engine.ProcessBytes(input.Array, input.Offset, input.Count, output.Array, output.Offset); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ProcessAadBytes(ArraySegment input) + { + engine.ProcessAadBytes(input.Array, input.Offset, input.Count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int DoFinal(ArraySegment output) + { + return engine.DoFinal(output.Array, output.Offset); + } + } +#endif +} diff --git a/src/SIPSorcery/sys/BufferUtils.cs b/src/SIPSorcery/sys/BufferUtils.cs index c1fe3daa81..5d04052996 100644 --- a/src/SIPSorcery/sys/BufferUtils.cs +++ b/src/SIPSorcery/sys/BufferUtils.cs @@ -13,6 +13,9 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +#nullable disable + +using System; using System.Text; namespace SIPSorcery.Sys diff --git a/src/SIPSorcery/sys/CRC32.cs b/src/SIPSorcery/sys/CRC32.cs index 39dd79fc5c..61f7893e9a 100644 --- a/src/SIPSorcery/sys/CRC32.cs +++ b/src/SIPSorcery/sys/CRC32.cs @@ -1,125 +1,155 @@ // from http://damieng.com/blog/2006/08/08/Calculating_CRC32_in_C_and_NET using System; +using System.Buffers.Binary; using System.Security.Cryptography; -namespace SIPSorcery.Sys +namespace SIPSorcery.Sys; + +public class Crc32 : HashAlgorithm { - public class Crc32 : HashAlgorithm + public const uint DefaultPolynomial = 0xedb88320; + public const uint DefaultSeed = 0xffffffff; + + private uint hash; + private uint seed; + private uint[] table; + private static uint[]? defaultTable; + + public Crc32() { - public const UInt32 DefaultPolynomial = 0xedb88320; - public const UInt32 DefaultSeed = 0xffffffff; + table = InitializeTable(DefaultPolynomial); + seed = DefaultSeed; + Initialize(); + } - private UInt32 hash; - private UInt32 seed; - private UInt32[] table; - private static UInt32[] defaultTable; + public Crc32(uint polynomial, uint seed) + { + table = InitializeTable(polynomial); + this.seed = seed; + Initialize(); + } - public Crc32() - { - table = InitializeTable(DefaultPolynomial); - seed = DefaultSeed; - Initialize(); - } + public override void Initialize() + { + hash = seed; + } - public Crc32(UInt32 polynomial, UInt32 seed) - { - table = InitializeTable(polynomial); - this.seed = seed; - Initialize(); - } + protected override void HashCore(byte[] buffer, int start, int length) + { + hash = CalculateHash(table, hash, buffer.AsSpan(start, length)); + } - public override void Initialize() - { - hash = seed; - } + protected +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER || NET5_0_OR_GREATER + override +#else + virtual +#endif + void HashCore(ReadOnlySpan buffer) + { + hash = CalculateHash(table, hash, buffer); + } - protected override void HashCore(byte[] buffer, int start, int length) - { - hash = CalculateHash(table, hash, buffer, start, length); - } + protected override byte[] HashFinal() + { + var hashBuffer = UInt32ToBigEndianBytes(~hash); + this.HashValue = hashBuffer; + return hashBuffer; + } - protected override byte[] HashFinal() - { - byte[] hashBuffer = UInt32ToBigEndianBytes(~hash); - this.HashValue = hashBuffer; - return hashBuffer; - } + public override int HashSize + { + get { return 32; } + } - public override int HashSize - { - get { return 32; } - } + public static uint Compute(byte[] buffer) + { + return Compute(buffer.AsSpan(0, buffer.Length)); + } - public static UInt32 Compute(byte[] buffer) - { - return ~CalculateHash(InitializeTable(DefaultPolynomial), DefaultSeed, buffer, 0, buffer.Length); - } + public static uint Compute(uint seed, byte[] buffer) + { + return Compute(seed, buffer.AsSpan()); + } - public static UInt32 Compute(UInt32 seed, byte[] buffer) - { - return ~CalculateHash(InitializeTable(DefaultPolynomial), seed, buffer, 0, buffer.Length); - } + public static uint Compute(uint polynomial, uint seed, byte[] buffer) + { + return Compute(polynomial, seed, buffer.AsSpan()); + } - public static UInt32 Compute(UInt32 polynomial, UInt32 seed, byte[] buffer) + public static uint Compute(ReadOnlySpan buffer) + { + return ~CalculateHash(InitializeTable(DefaultPolynomial), DefaultSeed, buffer); + } + + public static uint Compute(uint seed, ReadOnlySpan buffer) + { + return ~CalculateHash(InitializeTable(DefaultPolynomial), seed, buffer); + } + + public static uint Compute(uint polynomial, uint seed, ReadOnlySpan buffer) + { + return ~CalculateHash(InitializeTable(polynomial), seed, buffer); + } + + private static uint[] InitializeTable(uint polynomial) + { + if (polynomial == DefaultPolynomial && defaultTable is { }) { - return ~CalculateHash(InitializeTable(polynomial), seed, buffer, 0, buffer.Length); + return defaultTable; } - private static UInt32[] InitializeTable(UInt32 polynomial) + var createTable = new uint[256]; + for (var i = 0; i < 256; i++) { - if (polynomial == DefaultPolynomial && defaultTable != null) - { - return defaultTable; - } - - UInt32[] createTable = new UInt32[256]; - for (int i = 0; i < 256; i++) + var entry = (uint)i; + for (var j = 0; j < 8; j++) { - UInt32 entry = (UInt32)i; - for (int j = 0; j < 8; j++) + if ((entry & 1) == 1) { - if ((entry & 1) == 1) - { - entry = (entry >> 1) ^ polynomial; - } - else - { - entry = entry >> 1; - } + entry = (entry >> 1) ^ polynomial; + } + else + { + entry = entry >> 1; } - createTable[i] = entry; } + createTable[i] = entry; + } - if (polynomial == DefaultPolynomial) - { - defaultTable = createTable; - } + if (polynomial == DefaultPolynomial) + { + defaultTable = createTable; + } + + return createTable; + } - return createTable; + private static uint CalculateHash(ReadOnlySpan table, uint seed, ReadOnlySpan buffer) + { + /* + if (Sse42.IsSupported) + { +    uint crc = Sse42.Crc32(seed, value); } + */ - private static UInt32 CalculateHash(UInt32[] table, UInt32 seed, byte[] buffer, int start, int size) + var crc = seed; + for (var i = 0; i < buffer.Length; i++) { - UInt32 crc = seed; - for (int i = start; i < size; i++) + unchecked { - unchecked - { - crc = (crc >> 8) ^ table[buffer[i] ^ crc & 0xff]; - } + crc = (crc >> 8) ^ table[buffer[i] ^ (byte)(crc & 0xff)]; } - return crc; } + return crc; + } - private byte[] UInt32ToBigEndianBytes(UInt32 x) - { - return new byte[] { - (byte)((x >> 24) & 0xff), - (byte)((x >> 16) & 0xff), - (byte)((x >> 8) & 0xff), - (byte)(x & 0xff) - }; - } + private byte[] UInt32ToBigEndianBytes(uint x) + { + var result = new byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32BigEndian(result, x); + return result; } } diff --git a/src/SIPSorcery/sys/Crypto/Crypto.cs b/src/SIPSorcery/sys/Crypto/Crypto.cs index d8cbe673de..2f12eb103a 100644 --- a/src/SIPSorcery/sys/Crypto/Crypto.cs +++ b/src/SIPSorcery/sys/Crypto/Crypto.cs @@ -23,388 +23,387 @@ using System.Threading; using Microsoft.Extensions.Logging; -namespace SIPSorcery.Sys +namespace SIPSorcery.Sys; + +public static class Crypto { - public class Crypto - { - // TODO: When .NET Standard and Framework support are deprecated these pragmas can be removed. + // TODO: When .NET Standard and Framework support are deprecated these pragmas can be removed. #pragma warning disable SYSLIB0001 #pragma warning disable SYSLIB0021 #pragma warning disable SYSLIB0023 - public const int DEFAULT_RANDOM_LENGTH = 10; // Number of digits to return for default random numbers. - public const int AES_KEY_SIZE = 32; - public const int AES_IV_SIZE = 16; - private const string CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + public const int DEFAULT_RANDOM_LENGTH = 10; // Number of digits to return for default random numbers. + public const int AES_KEY_SIZE = 32; + public const int AES_IV_SIZE = 16; + private const string CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - private static readonly ILogger logger = LogFactory.CreateLogger(); + private static readonly ILogger logger = LogFactory.CreateLogger(typeof(Crypto).FullName!); - static int seed = Environment.TickCount; + private static int seed = Environment.TickCount; - static readonly ThreadLocal random = - new ThreadLocal(() => new Random(Interlocked.Increment(ref seed))); + private static readonly ThreadLocal random = + new ThreadLocal(() => new Random(Interlocked.Increment(ref seed))); - public static int Rand() - { - return random.Value.Next(); - } + public static int Rand() + { + return random.Value!.Next(); + } + + public static int Rand(int maxValue) + { + return random.Value!.Next(maxValue); + } + + private static RNGCryptoServiceProvider m_randomProvider = new RNGCryptoServiceProvider(); - public static int Rand(int maxValue) + public static string SymmetricEncrypt(string key, string iv, string plainText) + { + if (plainText.IsNullOrBlank()) { - return random.Value.Next(maxValue); + throw new ApplicationException("The plain text string cannot be empty in SymmetricEncrypt."); } - private static RNGCryptoServiceProvider m_randomProvider = new RNGCryptoServiceProvider(); + return SymmetricEncrypt(key, iv, Encoding.UTF8.GetBytes(plainText)); + } - public static string SymmetricEncrypt(string key, string iv, string plainText) + public static string SymmetricEncrypt(string key, string iv, byte[] plainTextBytes) + { + if (key.IsNullOrBlank()) { - if (plainText.IsNullOrBlank()) - { - throw new ApplicationException("The plain text string cannot be empty in SymmetricEncrypt."); - } - - return SymmetricEncrypt(key, iv, Encoding.UTF8.GetBytes(plainText)); + throw new ApplicationException("The key string cannot be empty in SymmetricEncrypt."); } - - public static string SymmetricEncrypt(string key, string iv, byte[] plainTextBytes) + else if (iv.IsNullOrBlank()) { - if (key.IsNullOrBlank()) - { - throw new ApplicationException("The key string cannot be empty in SymmetricEncrypt."); - } - else if (iv.IsNullOrBlank()) - { - throw new ApplicationException("The initialisation vector cannot be empty in SymmetricEncrypt."); - } - else if (plainTextBytes == null) - { - throw new ApplicationException("The plain text string cannot be empty in SymmetricEncrypt."); - } + throw new ApplicationException("The initialisation vector cannot be empty in SymmetricEncrypt."); + } + else if (plainTextBytes == null) + { + throw new ApplicationException("The plain text string cannot be empty in SymmetricEncrypt."); + } - AesManaged aes = new AesManaged(); - ICryptoTransform encryptor = aes.CreateEncryptor(GetFixedLengthByteArray(key, AES_KEY_SIZE), GetFixedLengthByteArray(iv, AES_IV_SIZE)); + AesManaged aes = new AesManaged(); + ICryptoTransform encryptor = aes.CreateEncryptor(GetFixedLengthByteArray(key, AES_KEY_SIZE), GetFixedLengthByteArray(iv, AES_IV_SIZE)); - MemoryStream resultStream = new MemoryStream(); - CryptoStream cryptoStream = new CryptoStream(resultStream, encryptor, CryptoStreamMode.Write); - cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); - cryptoStream.Close(); + MemoryStream resultStream = new MemoryStream(); + CryptoStream cryptoStream = new CryptoStream(resultStream, encryptor, CryptoStreamMode.Write); + cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); + cryptoStream.Close(); - return Convert.ToBase64String(resultStream.ToArray()); - } + return Convert.ToBase64String(resultStream.ToArray()); + } - public static string SymmetricDecrypt(string key, string iv, string cipherText) + public static string SymmetricDecrypt(string key, string iv, string cipherText) + { + if (cipherText.IsNullOrBlank()) { - if (cipherText.IsNullOrBlank()) - { - throw new ApplicationException("The cipher text string cannot be empty in SymmetricDecrypt."); - } - - return SymmetricDecrypt(key, iv, Convert.FromBase64String(cipherText)); + throw new ApplicationException("The cipher text string cannot be empty in SymmetricDecrypt."); } - public static string SymmetricDecrypt(string key, string iv, byte[] cipherBytes) - { - if (key.IsNullOrBlank()) - { - throw new ApplicationException("The key string cannot be empty in SymmetricDecrypt."); - } - else if (iv.IsNullOrBlank()) - { - throw new ApplicationException("The initialisation vector cannot be empty in SymmetricDecrypt."); - } - else if (cipherBytes == null) - { - throw new ApplicationException("The cipher byte array cannot be empty in SymmetricDecrypt."); - } - - AesManaged aes = new AesManaged(); - ICryptoTransform encryptor = aes.CreateDecryptor(GetFixedLengthByteArray(key, AES_KEY_SIZE), GetFixedLengthByteArray(iv, AES_IV_SIZE)); + return SymmetricDecrypt(key, iv, Convert.FromBase64String(cipherText)); + } - MemoryStream cipherStream = new MemoryStream(cipherBytes); - CryptoStream cryptoStream = new CryptoStream(cipherStream, encryptor, CryptoStreamMode.Read); - StreamReader cryptoStreamReader = new StreamReader(cryptoStream); - return cryptoStreamReader.ReadToEnd(); + public static string SymmetricDecrypt(string key, string iv, byte[] cipherBytes) + { + if (key.IsNullOrBlank()) + { + throw new ApplicationException("The key string cannot be empty in SymmetricDecrypt."); } - - private static byte[] GetFixedLengthByteArray(string value, int length) + else if (iv.IsNullOrBlank()) { - if (value.Length < length) - { - while (value.Length < length) - { - value += 0x00; - } - } - else if (value.Length > length) - { - value = value.Substring(0, length); - } - - return Encoding.UTF8.GetBytes(value); + throw new ApplicationException("The initialisation vector cannot be empty in SymmetricDecrypt."); } - - public static string GetRandomString(int length) + else if (cipherBytes == null) { - char[] buffer = new char[length]; + throw new ApplicationException("The cipher byte array cannot be empty in SymmetricDecrypt."); + } - for (int i = 0; i < length; i++) + AesManaged aes = new AesManaged(); + ICryptoTransform encryptor = aes.CreateDecryptor(GetFixedLengthByteArray(key, AES_KEY_SIZE), GetFixedLengthByteArray(iv, AES_IV_SIZE)); + + MemoryStream cipherStream = new MemoryStream(cipherBytes); + CryptoStream cryptoStream = new CryptoStream(cipherStream, encryptor, CryptoStreamMode.Read); + StreamReader cryptoStreamReader = new StreamReader(cryptoStream); + return cryptoStreamReader.ReadToEnd(); + } + + private static byte[] GetFixedLengthByteArray(string value, int length) + { + if (value.Length < length) + { + while (value.Length < length) { - buffer[i] = CHARS[Rand(CHARS.Length)]; + value += 0x00; } - return new string(buffer); } - - public static string GetRandomString() + else if (value.Length > length) { - return GetRandomString(DEFAULT_RANDOM_LENGTH); + value = value.Substring(0, length); } - /// - /// Returns a 10 digit random number. - /// - /// - public static int GetRandomInt() + return Encoding.UTF8.GetBytes(value); + } + + public static string GetRandomString(int length) + { + var buffer = new char[length]; + + for (var i = 0; i < length; i++) { - return GetRandomInt(DEFAULT_RANDOM_LENGTH); + buffer[i] = CHARS[Rand(CHARS.Length)]; } + return new string(buffer); + } - /// - /// Returns a random number of a specified length. - /// - public static int GetRandomInt(int length) - { - int randomStart = 1000000000; - int randomEnd = Int32.MaxValue; + public static string GetRandomString() + { + return GetRandomString(DEFAULT_RANDOM_LENGTH); + } - if (length > 0 && length < DEFAULT_RANDOM_LENGTH) - { - randomStart = Convert.ToInt32(Math.Pow(10, length - 1)); - randomEnd = Convert.ToInt32(Math.Pow(10, length) - 1); - } + /// + /// Returns a 10 digit random number. + /// + /// + public static int GetRandomInt() + { + return GetRandomInt(DEFAULT_RANDOM_LENGTH); + } - return GetRandomInt(randomStart, randomEnd); - } + /// + /// Returns a random number of a specified length. + /// + public static int GetRandomInt(int length) + { + var randomStart = 1000000000; + var randomEnd = int.MaxValue; - public static Int32 GetRandomInt(Int32 minValue, Int32 maxValue) + if (length is > 0 and < DEFAULT_RANDOM_LENGTH) { + randomStart = Convert.ToInt32(Math.Pow(10, length - 1)); + randomEnd = Convert.ToInt32(Math.Pow(10, length) - 1); + } - if (minValue > maxValue) - { - throw new ArgumentOutOfRangeException("minValue"); - } - else if (minValue == maxValue) - { - return minValue; - } + return GetRandomInt(randomStart, randomEnd); + } - Int64 diff = maxValue - minValue + 1; - int attempts = 0; - while (attempts < 10) - { - byte[] uint32Buffer = new byte[4]; - m_randomProvider.GetBytes(uint32Buffer); - UInt32 rand = BitConverter.ToUInt32(uint32Buffer, 0); - - Int64 max = (1 + (Int64)UInt32.MaxValue); - Int64 remainder = max % diff; - if (rand <= max - remainder) - { - return (Int32)(minValue + (rand % diff)); - } - attempts++; - } - throw new ApplicationException("GetRandomInt did not return an appropriate random number within 10 attempts."); - } + public static int GetRandomInt(int minValue, int maxValue) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(minValue, maxValue); - public static UInt16 GetRandomUInt16() + if (minValue == maxValue) { - byte[] uint16Buffer = new byte[2]; - m_randomProvider.GetBytes(uint16Buffer); - return BitConverter.ToUInt16(uint16Buffer, 0); + return minValue; } - public static UInt32 GetRandomUInt(bool noZero = false) + long diff = maxValue - minValue + 1; + var attempts = 0; + while (attempts < 10) { - byte[] uint32Buffer = new byte[4]; + var uint32Buffer = new byte[4]; m_randomProvider.GetBytes(uint32Buffer); - var randomUint = BitConverter.ToUInt32(uint32Buffer, 0); + var rand = BitConverter.ToUInt32(uint32Buffer, 0); - if(noZero && randomUint == 0) + var max = (1 + (long)uint.MaxValue); + var remainder = max % diff; + if (rand <= max - remainder) { - m_randomProvider.GetBytes(uint32Buffer); - randomUint = BitConverter.ToUInt32(uint32Buffer, 0); + return (int)(minValue + (rand % diff)); } - - return randomUint; + attempts++; } - public static UInt64 GetRandomULong() - { - byte[] uint64Buffer = new byte[8]; - m_randomProvider.GetBytes(uint64Buffer); - return BitConverter.ToUInt64(uint64Buffer, 0); - } + throw new SipSorceryException("GetRandomInt did not return an appropriate random number within 10 attempts."); + } - public static byte[] createRandomSalt(int length) + public static ushort GetRandomUInt16() + { + var uint16Buffer = new byte[2]; + m_randomProvider.GetBytes(uint16Buffer); + return BitConverter.ToUInt16(uint16Buffer, 0); + } + + public static uint GetRandomUInt(bool noZero = false) + { + var uint32Buffer = new byte[4]; + m_randomProvider.GetBytes(uint32Buffer); + var randomUint = BitConverter.ToUInt32(uint32Buffer, 0); + + if (noZero && randomUint == 0) { - byte[] randBytes = new byte[length]; - m_randomProvider.GetBytes(randBytes); - return randBytes; + m_randomProvider.GetBytes(uint32Buffer); + randomUint = BitConverter.ToUInt32(uint32Buffer, 0); } - /// - /// This version reads the whole file in at once. This is not great since it can consume - /// a lot of memory if the file is large. However a buffered approach generates - /// different hashes across different platforms. - /// - /// - /// - public static string GetHash(string filepath) - { - // Check and then attempt to open the plain-text stream. - FileStream fileStream = GetFileStream(filepath); + return randomUint; + } + + public static ulong GetRandomULong() + { + var uint64Buffer = new byte[8]; + m_randomProvider.GetBytes(uint64Buffer); + return BitConverter.ToUInt64(uint64Buffer, 0); + } + + public static byte[] createRandomSalt(int length) + { + var randBytes = new byte[length]; + m_randomProvider.GetBytes(randBytes); + return randBytes; + } + + /// + /// This version reads the whole file in at once. This is not great since it can consume a lot of memory if the file + /// is large. However a buffered approach generates different hashes across different platforms. + /// + /// + /// + public static string GetHash(string filepath) + { + // Check and then attempt to open the plain-text stream. + FileStream fileStream = GetFileStream(filepath); - // Encrypt the file using its hash as the key. - SHA1 shaM = new SHA1Managed(); + // Encrypt the file using its hash as the key. + SHA1 shaM = new SHA1Managed(); - // Buffer to read in plain text blocks. - byte[] fileBuffer = new byte[fileStream.Length]; + // Buffer to read in plain text blocks. + var fileBuffer = new byte[fileStream.Length]; #if NET9_0_OR_GREATER - fileStream.ReadExactly(fileBuffer, 0, (int)fileStream.Length); + fileStream.ReadExactly(fileBuffer, 0, (int)fileStream.Length); #else - fileStream.Read(fileBuffer, 0, (int)fileStream.Length); + fileStream.Read(fileBuffer, 0, (int)fileStream.Length); #endif - fileStream.Close(); + fileStream.Close(); - byte[] overallHash = shaM.ComputeHash(fileBuffer); + var overallHash = shaM.ComputeHash(fileBuffer); - return Convert.ToBase64String(overallHash); - } + return Convert.ToBase64String(overallHash); + } - /// - /// Used by methods wishing to perform a hash operation on a file. This method - /// will perform a number of checks and if happy return a read only file stream. - /// - /// The path to the input file for the hash operation. - /// A read only file stream for the file or throws an exception if there is a problem. - private static FileStream GetFileStream(string filepath) + /// + /// Used by methods wishing to perform a hash operation on a file. This method will perform a number of checks and + /// if happy return a read only file stream. + /// + /// The path to the input file for the hash operation. + /// A read only file stream for the file or throws an exception if there is a problem. + private static FileStream GetFileStream(string filepath) + { + // Check that the file exists. + if (!File.Exists(filepath)) { - // Check that the file exists. - if (!File.Exists(filepath)) - { - logger.LogError("Cannot open a non-existent file for a hash operation, {FilePath}.", filepath); - throw new IOException($"Cannot open a non-existent file for a hash operation, {filepath}."); - } - - // Open the file. - FileStream inputStream = File.OpenRead(filepath); - - if (inputStream.Length == 0) - { - inputStream.Close(); - logger.LogError("Cannot perform a hash operation on an empty file, {FilePath}.", filepath); - throw new IOException($"Cannot perform a hash operation on an empty file, {filepath}."); - } - - return inputStream; + logger.LogError("Cannot open a non-existent file for a hash operation, {FilePath}.", filepath); + throw new IOException($"Cannot open a non-existent file for a hash operation, {filepath}."); } - /// - /// Gets an "X2" string representation of a random number. - /// - /// The byte length of the random number string to obtain. - /// A string representation of the random number. It will be twice the length of byteLength. - public static string GetRandomByteString(int byteLength) - { - byte[] myKey = new byte[byteLength]; - m_randomProvider.GetBytes(myKey); - string sessionID = null; - myKey.ToList().ForEach(b => sessionID += b.ToString("x2")); - return sessionID; - } + // Open the file. + FileStream inputStream = File.OpenRead(filepath); - /// - /// Fills a buffer with random bytes. - /// - /// The buffer to fill. - public static void GetRandomBytes(byte[] buffer) + if (inputStream.Length == 0) { - m_randomProvider.GetBytes(buffer); + inputStream.Close(); + logger.LogError("Cannot perform a hash operation on an empty file, {FilePath}.", filepath); + throw new IOException($"Cannot perform a hash operation on an empty file, {filepath}."); } - public static byte[] GetSHAHash(params string[] values) - { - SHA1 sha = new SHA1Managed(); - string plainText = null; - foreach (string value in values) - { - plainText += value; - } - return sha.ComputeHash(Encoding.UTF8.GetBytes(plainText)); - } + return inputStream; + } - public static string GetSHAHashAsString(params string[] values) + /// + /// Gets an "X2" string representation of a random number. + /// + /// The byte length of the random number string to obtain. + /// A string representation of the random number. It will be twice the length of byteLength. + public static string? GetRandomByteString(int byteLength) + { + var myKey = new byte[byteLength]; + m_randomProvider.GetBytes(myKey); + string? sessionID = null; + myKey.ToList().ForEach(b => sessionID += b.ToString("x2")); + return sessionID; + } + + /// + /// Fills a buffer with random bytes. + /// + /// The buffer to fill. + public static void GetRandomBytes(byte[] buffer) + { + m_randomProvider.GetBytes(buffer); + } + + public static byte[] GetSHAHash(params string[] values) + { + SHA1 sha = new SHA1Managed(); + string plainText = ""; + foreach (var value in values) { - return Convert.ToBase64String(GetSHAHash(values)); + plainText += value; } + return sha.ComputeHash(Encoding.UTF8.GetBytes(plainText)); + } + + public static string GetSHAHashAsString(params string[] values) + { + return Convert.ToBase64String(GetSHAHash(values)); + } + + /// + /// Returns the hash with each byte as an X2 string. This is useful for situations where the hash needs to only + /// contain safe ASCII characters. + /// + /// The list of string to concatenate and hash. + /// A string with "safe" (0-9 and A-F) characters representing the hash. + public static string? GetSHAHashAsHex(params string[] values) + { + var hash = GetSHAHash(values); + string? hashStr = null; + hash.ToList().ForEach(b => hashStr += b.ToString("x2")); + return hashStr; + } - /// - /// Returns the hash with each byte as an X2 string. This is useful for situations where - /// the hash needs to only contain safe ASCII characters. - /// - /// The list of string to concatenate and hash. - /// A string with "safe" (0-9 and A-F) characters representing the hash. - public static string GetSHAHashAsHex(params string[] values) + /// + /// Gets the HSA256 hash of an arbitrary buffer. + /// + /// The buffer to hash. + /// A hex string representing the hashed buffer. + public static string GetSHA256Hash(byte[] buffer) + { + using (SHA256Managed sha256 = new SHA256Managed()) { - byte[] hash = GetSHAHash(values); - string hashStr = null; - hash.ToList().ForEach(b => hashStr += b.ToString("x2")); - return hashStr; + return sha256.ComputeHash(buffer).HexStr(); } + } - /// - /// Gets the HSA256 hash of an arbitrary buffer. - /// - /// The buffer to hash. - /// A hex string representing the hashed buffer. - public static string GetSHA256Hash(byte[] buffer) + /// + /// Attempts to load an X509 certificate from a Windows OS certificate store. + /// + /// The certificate store to load from, can be CurrentUser or LocalMachine. + /// The subject name of the certificate to attempt to load. + /// + /// Checks if the certificate is current and has a verifiable certificate issuer list. Should be set to false for + /// self issued certificates. + /// + /// A certificate object if the load is successful otherwise null. + public static X509Certificate2? LoadCertificate(StoreLocation storeLocation, string certificateSubject, bool checkValidity) + { + X509Store store = new X509Store(storeLocation); + logger.LogDebug("Certificate store {StoreLocation} opened", store.Location); + store.Open(OpenFlags.OpenExistingOnly); + X509Certificate2Collection collection = store.Certificates.Find(X509FindType.FindBySubjectName, certificateSubject, checkValidity); + if (collection != null && collection.Count > 0) { - using(SHA256Managed sha256 = new SHA256Managed()) - { - return sha256.ComputeHash(buffer).HexStr(); - } + X509Certificate2 serverCertificate = collection[0]; + var verifyCert = serverCertificate.Verify(); + logger.LogDebug("X509 certificate loaded from current user store, subject={Subject}, valid={Valid}.", serverCertificate.Subject, verifyCert); + return serverCertificate; } - - /// - /// Attempts to load an X509 certificate from a Windows OS certificate store. - /// - /// The certificate store to load from, can be CurrentUser or LocalMachine. - /// The subject name of the certificate to attempt to load. - /// Checks if the certificate is current and has a verifiable certificate issuer list. Should be - /// set to false for self issued certificates. - /// A certificate object if the load is successful otherwise null. - public static X509Certificate2 LoadCertificate(StoreLocation storeLocation, string certificateSubject, bool checkValidity) + else { - X509Store store = new X509Store(storeLocation); - logger.LogDebug("Certificate store {StoreLocation} opened", store.Location); - store.Open(OpenFlags.OpenExistingOnly); - X509Certificate2Collection collection = store.Certificates.Find(X509FindType.FindBySubjectName, certificateSubject, checkValidity); - if (collection != null && collection.Count > 0) - { - X509Certificate2 serverCertificate = collection[0]; - bool verifyCert = serverCertificate.Verify(); - logger.LogDebug("X509 certificate loaded from current user store, subject={Subject}, valid={Valid}.", serverCertificate.Subject, verifyCert); - return serverCertificate; - } - else - { - logger.LogWarning("X509 certificate with subject name={CertificateSubject}, not found in {StoreLocation} store.", certificateSubject, store.Location); - return null; - } + logger.LogWarning("X509 certificate with subject name={CertificateSubject}, not found in {StoreLocation} store.", certificateSubject, store.Location); + return null; } } +} #pragma warning restore SYSLIB0001 #pragma warning restore SYSLIB0021 #pragma warning restore SYSLIB0023 -} + diff --git a/src/SIPSorcery/sys/Crypto/PasswordHash.cs b/src/SIPSorcery/sys/Crypto/PasswordHash.cs index 9e04dc6421..b5b8ff8132 100644 --- a/src/SIPSorcery/sys/Crypto/PasswordHash.cs +++ b/src/SIPSorcery/sys/Crypto/PasswordHash.cs @@ -32,11 +32,15 @@ public class PasswordHash private static RNGCryptoServiceProvider _randomProvider = new RNGCryptoServiceProvider(); /// - /// Generates a salt that can be used to generate a password hash. The salt is a combination of a block of bytes to represent the - /// salt entropy and an integer that represents the iteration count to feed into the RFC289 algorithm used to derive the password hash. - /// The iterations count is used to slow down the hash generating algorithm to mitigate brute force and rainbow table attacks. + /// Generates a salt that can be used to generate a password hash. The salt is a combination of a block of bytes + /// to represent the salt entropy and an integer that represents the iteration count to feed into the RFC289 + /// algorithm used to derive the password hash. The iterations count is used to slow down the hash generating + /// algorithm to mitigate brute force and rainbow table attacks. /// - /// The number of iterations used to derive the password bytes. Must be greater than the constant specifying the minimum iterations. + /// + /// The number of iterations used to derive the password bytes. Must be greater than the constant specifying the + /// minimum iterations. + /// /// A string it the format iterations.salt. public static string GenerateSalt(int? explicitIterations = null) { @@ -64,7 +68,7 @@ public static string Hash(string value, string salt) var i = salt.IndexOf('.'); var iters = int.Parse(salt.Substring(0, i), System.Globalization.NumberStyles.HexNumber); salt = salt.Substring(i + 1); - byte[] key = null; + byte[] key; byte[] saltBytes = Convert.FromBase64String(salt); diff --git a/src/SIPSorcery/sys/EncodingExtensions.cs b/src/SIPSorcery/sys/EncodingExtensions.cs new file mode 100644 index 0000000000..a4f7ba3eb3 --- /dev/null +++ b/src/SIPSorcery/sys/EncodingExtensions.cs @@ -0,0 +1,67 @@ +using System; +using System.Text; + +namespace SIPSorcery.Sys; + +internal static class EncodingExtensions +{ + extension(global::System.Text.Encoding encoding) + { + /// + /// Checks if the encoding of the given string matches the provided bytes. + /// + /// The string to encode and compare. + /// The byte sequence to compare against the encoded string. + /// + /// true if the encoded bytes of exactly match ; + /// otherwise, false. + /// + public bool Equals(string? str, ReadOnlySpan bytes) + { + if (str is null) + { + return false; + } + + var byteCount = encoding.GetByteCount(str); + if (byteCount != bytes.Length) + { + return false; + } + + var charSpan = str.AsSpan(); + + if (byteCount <= 1024) + { + return EqualsCore(encoding, charSpan, stackalloc byte[byteCount], bytes); + } + else + { + var pool = System.Buffers.ArrayPool.Shared; + var rented = pool.Rent(byteCount); + try + { + return EqualsCore(encoding, charSpan, rented.AsSpan(0, byteCount), bytes); + } + finally + { + pool.Return(rented); + } + } + + static bool EqualsCore(Encoding encoding, ReadOnlySpan charSpan, Span strBytes, ReadOnlySpan bytes) + { + encoding.GetBytes(charSpan, strBytes); + return strBytes.SequenceEqual(bytes); + } + } + + public byte[] GetBytes(ReadOnlySpan chars) + { + var byteCount = encoding.GetByteCount(chars); + var bytes = new byte[byteCount]; + encoding.GetBytes(chars, bytes); + return bytes; + } + } +} diff --git a/src/SIPSorcery/sys/EnumExtensions.cs b/src/SIPSorcery/sys/EnumExtensions.cs new file mode 100644 index 0000000000..cb06d5eb7a --- /dev/null +++ b/src/SIPSorcery/sys/EnumExtensions.cs @@ -0,0 +1,29 @@ +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] diff --git a/src/SIPSorcery/sys/Formatting/NumberFormatting.cs b/src/SIPSorcery/sys/Formatting/NumberFormatting.cs index b62544c494..5e8806027f 100644 --- a/src/SIPSorcery/sys/Formatting/NumberFormatting.cs +++ b/src/SIPSorcery/sys/Formatting/NumberFormatting.cs @@ -1,4 +1,4 @@ -// ============================================================================ +// ============================================================================ // FileName: NumberFormatting.cs // // Description: diff --git a/src/SIPSorcery/sys/HashExtensions.cs b/src/SIPSorcery/sys/HashExtensions.cs new file mode 100644 index 0000000000..2560ff098a --- /dev/null +++ b/src/SIPSorcery/sys/HashExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace SIPSorcery.Sys; + +internal static class HashExtensions +{ + public static byte[] ComputeHash(this HashAlgorithm hashAlgorithm, ReadOnlySpan buffer) + { + var tempBuffer = ArrayPool.Shared.Rent(buffer.Length); + try + { + buffer.CopyTo(tempBuffer); + return hashAlgorithm.ComputeHash(tempBuffer, 0 , buffer.Length); + } + finally + { + ArrayPool.Shared.Return(tempBuffer); + } + } + + public static byte[] ComputeHash(this HashAlgorithm hashAlgorithm, ReadOnlyMemory buffer) + { + if (MemoryMarshal.TryGetArray(buffer, out var segment)) + { + return hashAlgorithm.ComputeHash(segment.Array!, segment.Offset, segment.Count); + } + + return hashAlgorithm.ComputeHash(buffer.Span); + } +} diff --git a/src/SIPSorcery/sys/IByteSerializable.cs b/src/SIPSorcery/sys/IByteSerializable.cs new file mode 100644 index 0000000000..ca13171491 --- /dev/null +++ b/src/SIPSorcery/sys/IByteSerializable.cs @@ -0,0 +1,28 @@ +using System; + +namespace SIPSorcery.Sys; + +/// +/// Provides a lightweight contract for types that can report the exact number of bytes +/// they require for serialisation and write their serialised representation into a caller +/// supplied . Implementations MUST write exactly +/// bytes starting at index 0 of the supplied buffer. +/// +public interface IByteSerializable +{ + /// + /// Gets the exact number of bytes required to serialise this instance. + /// + /// Total byte count of the serialised form. + int GetByteCount(); + + /// + /// Writes the serialised representation into . + /// Implementations MUST: (1) ensure has a length of at least + /// , (2) write exactly that many bytes starting at index 0, + /// and (3) return the number of bytes written (typically the same value as ). + /// + /// Destination span to receive the serialised bytes. + /// The number of bytes written. + int WriteBytes(Span buffer); +} diff --git a/src/SIPSorcery/sys/JSONWriter.cs b/src/SIPSorcery/sys/JSONWriter.cs index ab0ff7574d..9c50f89d78 100644 --- a/src/SIPSorcery/sys/JSONWriter.cs +++ b/src/SIPSorcery/sys/JSONWriter.cs @@ -14,6 +14,8 @@ // MIT, see https://github.com/zanders3/json/blob/master/LICENSE. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections; using System.Collections.Generic; @@ -51,7 +53,7 @@ static void AppendValue(StringBuilder stringBuilder, object item) string str = item.ToString(); for (int i = 0; i < str.Length; ++i) { - if (str[i] < ' ' || str[i] == '"' || str[i] == '\\') + if (str[i] is < ' ' or '"' or '\\') { stringBuilder.Append('\\'); int j = "\"\\\n\r\t\b\f".IndexOf(str[i]); diff --git a/src/SIPSorcery/sys/JsonParser.cs b/src/SIPSorcery/sys/JsonParser.cs index 7a810606e6..3fe4032f25 100644 --- a/src/SIPSorcery/sys/JsonParser.cs +++ b/src/SIPSorcery/sys/JsonParser.cs @@ -14,6 +14,8 @@ // MIT, see https://github.com/zanders3/json/blob/master/LICENSE. //----------------------------------------------------------------------------- +#nullable disable + using System; using System.Collections; using System.Collections.Generic; @@ -386,4 +388,4 @@ static object ParseObject(Type type, string json) return instance; } } -} \ No newline at end of file +} diff --git a/src/SIPSorcery/sys/Memory.cs b/src/SIPSorcery/sys/Memory.cs new file mode 100644 index 0000000000..a22410bc26 --- /dev/null +++ b/src/SIPSorcery/sys/Memory.cs @@ -0,0 +1,11 @@ +using System; + +namespace SIPSorcery.Sys; + +public delegate void ReadOnlySpanAction(ReadOnlySpan span); + +public delegate void ReadOnlyMemoryAction(ReadOnlyMemory span); + +public delegate void SpanAction(Span span); + +public delegate void MemoryAction(Memory span); diff --git a/src/SIPSorcery/sys/MemoryOperations.cs b/src/SIPSorcery/sys/MemoryOperations.cs new file mode 100644 index 0000000000..dc93265a92 --- /dev/null +++ b/src/SIPSorcery/sys/MemoryOperations.cs @@ -0,0 +1,71 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SIPSorcery.Sys; + +internal static class MemoryOperations +{ + public static string ToLowerString(this ReadOnlySpan span) + { + var buffer = ArrayPool.Shared.Rent(span.Length); + + try + { + for (var i = 0; i < span.Length; i++) + { + buffer[i] = char.ToLower(span[i]); + } + + return new string(buffer, 0, span.Length); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public static void ToLittleEndianBytes(this ReadOnlySpan shorts, Span bytes) + { +#if NETFRAMEWORK || NETSTANDARD2_0 + if (bytes.Length < shorts.Length * 2) + { + throw new ArgumentException("Destination span is too small.", nameof(bytes)); + } + + int byteOffset = 0; + for (int i = 0; i < shorts.Length; i++) + { + BinaryPrimitives.WriteInt16LittleEndian(bytes.Slice(byteOffset, 2), shorts[i]); + byteOffset += 2; + } +#else + ref var source = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(shorts)); + ref var destination = ref MemoryMarshal.GetReference(bytes); + + for (var i = shorts.Length; i > 0; i--) + { + var destSpan = MemoryMarshal.CreateSpan(ref destination, 2); + BinaryPrimitives.WriteInt16LittleEndian(destSpan, source); + + source = ref Unsafe.Add(ref source, 1); + destination = ref Unsafe.Add(ref destination, 2); + } +#endif + } + + public static List SplitToList(this ReadOnlySpan value, char separator) + { + var result = new List(); + + foreach (var token in value.Split(separator)) + { + result.Add(value[token].Trim().ToString()); + } + + return result; + } +} diff --git a/src/SIPSorcery/sys/Net/IPAddressExtension.cs b/src/SIPSorcery/sys/Net/IPAddressExtension.cs new file mode 100644 index 0000000000..075656c1f8 --- /dev/null +++ b/src/SIPSorcery/sys/Net/IPAddressExtension.cs @@ -0,0 +1,18 @@ +using System; + +namespace System.Net; + +internal static class IPAddressExtension +{ + extension(IPAddress) + { + public static IPAddress Create(ReadOnlySpan address) + => +#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER + new IPAddress(address) +#else + new IPAddress(address.ToArray()) +#endif + ; + } +} diff --git a/src/SIPSorcery/sys/Net/IPSocket.cs b/src/SIPSorcery/sys/Net/IPSocket.cs index f5eb689fbc..c34842c1ae 100644 --- a/src/SIPSorcery/sys/Net/IPSocket.cs +++ b/src/SIPSorcery/sys/Net/IPSocket.cs @@ -18,416 +18,404 @@ //----------------------------------------------------------------------------- using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Net; using System.Net.Sockets; -namespace SIPSorcery.Sys +namespace SIPSorcery.Sys; + +public static class IPSocket { - public class IPSocket + /// + /// Specifies the minimum acceptable value for the Port property. + /// + public const int MinPort = 0x00000000; + + /// + /// Specifies the maximum acceptable value for the Port property. + /// + public const int MaxPort = 0x0000FFFF; + + /// + /// This code is based on the IPEndPoint.ToString method in the dotnet source code at + /// https://github.com/dotnet/corefx/blob/master/src/System.Net.Primitives/src/System/Net/IPEndPoint.cs. If/when + /// that feature makes it into .NET Standard this method can be replaced. + /// + public static string GetSocketString(IPEndPoint endPoint) { - /// - /// Specifies the minimum acceptable value for the Port property. - /// - public const int MinPort = 0x00000000; - - /// - /// Specifies the maximum acceptable value for the Port property. - /// - public const int MaxPort = 0x0000FFFF; - - /// - /// This code is based on the IPEndPoint.ToString method in the dotnet source code at - /// https://github.com/dotnet/corefx/blob/master/src/System.Net.Primitives/src/System/Net/IPEndPoint.cs. - /// If/when that feature makes it into .NET Standard this method can be replaced. - /// - public static string GetSocketString(IPEndPoint endPoint) - { - var address = endPoint.Address.ToString(); - var port = endPoint.Port.ToString(NumberFormatInfo.InvariantInfo); + var address = endPoint.Address.ToString(); + var port = endPoint.Port.ToString(NumberFormatInfo.InvariantInfo); - return endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 ? $"[{address}]:{port}" : $"{address}:{port}"; - } + return endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 ? $"[{address}]:{port}" : $"{address}:{port}"; + } - /// - /// This code is based on the IPEndPoint.TryParse method in the dotnet source code at - /// https://github.com/dotnet/corefx/blob/master/src/System.Net.Primitives/src/System/Net/IPEndPoint.cs. - /// If/when that feature makes it into .NET Standard this method can be replaced. - /// - /// The end point string to parse. - /// If the parse is successful this output parameter will contain the IPv4 or IPv6 end point. - /// Returns true if the string could be successfully parsed as an IPv4 or IPv6 end point. False if not. - public static bool TryParseIPEndPoint(string s, out IPEndPoint result) - => TryParseIPEndPoint(s.AsSpan(), out result); - - /// - /// This code is based on the IPEndPoint.TryParse method in the dotnet source code at - /// https://github.com/dotnet/corefx/blob/master/src/System.Net.Primitives/src/System/Net/IPEndPoint.cs. - /// If/when that feature makes it into .NET Standard this method can be replaced. - /// - /// The end point string to parse. - /// If the parse is successful this output parameter will contain the IPv4 or IPv6 end point. - /// Returns true if the string could be successfully parsed as an IPv4 or IPv6 end point. False if not. - public static bool TryParseIPEndPoint(ReadOnlySpan s, out IPEndPoint result) - { - result = null; - int addressLength = s.Length; - int lastColonPos = s.LastIndexOf(':'); + /// + /// This code is based on the IPEndPoint.TryParse method in the dotnet source code at + /// https://github.com/dotnet/corefx/blob/master/src/System.Net.Primitives/src/System/Net/IPEndPoint.cs. If/when + /// that feature makes it into .NET Standard this method can be replaced. + /// + /// The end point string to parse. + /// + /// If the parse is successful this output parameter will contain the IPv4 or IPv6 end point. + /// + /// + /// Returns true if the string could be successfully parsed as an IPv4 or IPv6 end point. False if not. + /// + public static bool TryParseIPEndPoint(ReadOnlySpan s, [NotNullWhen(true)] out IPEndPoint? result) + { + result = null; + var addressLength = s.Length; + var lastColonPos = s.LastIndexOf(':'); - if (lastColonPos > 0) + if (lastColonPos > 0) + { + if (s[lastColonPos - 1] == ']') { - if (s[lastColonPos - 1] == ']') - { - addressLength = lastColonPos; - } - else if (s.Slice(0, lastColonPos).LastIndexOf(':') == -1) - { - addressLength = lastColonPos; - } + addressLength = lastColonPos; } - -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - if (IPAddress.TryParse(s.Slice(0, addressLength), out IPAddress address)) -#else - if (IPAddress.TryParse(s.Slice(0, addressLength).ToString(), out IPAddress address)) -#endif + else if (s.Slice(0, lastColonPos).LastIndexOf(':') == -1) { - uint port = 0; - if (addressLength == s.Length || -#if NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER - (uint.TryParse(s.Slice(addressLength + 1), NumberStyles.None, CultureInfo.InvariantCulture, out port) && port <= MaxPort)) -#else - (uint.TryParse(s.Slice(addressLength + 1).ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out port) && port <= MaxPort)) -#endif - { - result = new IPEndPoint(address, (int)port); - return true; - } + addressLength = lastColonPos; } - - return false; } - public static IPEndPoint ParseSocketString(string s) + if (IPAddress.TryParse(s.Slice(0, addressLength), out var address)) { - if (TryParseIPEndPoint(s, out var ipEndPoint)) - { - return ipEndPoint; - } - else + uint port = 0; + if (addressLength == s.Length || + (uint.TryParse(s.Slice(addressLength + 1), NumberStyles.None, CultureInfo.InvariantCulture, out port) && port <= MaxPort)) { - throw new ApplicationException($"Could not parse IP end point from {s}."); + result = new IPEndPoint(address, (int)port); + return true; } } - public static string ParseHostFromSocket(string socket) + return false; + } + + public static IPEndPoint ParseSocketString(string s) + { + if (TryParseIPEndPoint(s, out var ipEndPoint)) + { + return ipEndPoint; + } + else { - string host = socket; + throw new ApplicationException($"Could not parse IP end point from {s}."); + } + } - if (!string.IsNullOrWhiteSpace(socket) && socket.IndexOf(':') != -1) - { - host = socket.Substring(0, socket.LastIndexOf(':')).Trim(); - } + public static string ParseHostFromSocket(string socket) + { + string host = socket; - return host; + if (!string.IsNullOrWhiteSpace(socket) && socket.IndexOf(':') != -1) + { + host = socket.Substring(0, socket.LastIndexOf(':')).Trim(); } - /// - /// For IPv6 addresses with port the string format is of the form: - /// [2a02:8084:6981:7880:54a9:d238:b2ee:ceb]:6060 - /// Without a port the form is: - /// 2a02:8084:6981:7880:54a9:d238:b2ee:ceb - /// - /// The socket string to check - /// The socket string's explicit port number or 0 if it does not have one. - public static int ParsePortFromSocket(string socket) - { - int port = 0; + return host; + } - int lastColonPos = socket.LastIndexOf(':'); + /// + /// For IPv6 addresses with port the string format is of the form: [2a02:8084:6981:7880:54a9:d238:b2ee:ceb]:6060 + /// Without a port the form is: 2a02:8084:6981:7880:54a9:d238:b2ee:ceb + /// + /// The socket string to check + /// The socket string's explicit port number or 0 if it does not have one. + public static int ParsePortFromSocket(string socket) + { + int port = 0; - // Look to see if this is an IPv6 address with a port. - if (lastColonPos > 0) + int lastColonPos = socket.LastIndexOf(':'); + + // Look to see if this is an IPv6 address with a port. + if (lastColonPos > 0) + { + if (socket[lastColonPos - 1] == ']') { - if (socket[lastColonPos - 1] == ']') - { - // This is an IPv6 address WITH a port. - } - // Look to see if this is IPv4 with a port (IPv6 will have another colon) - // If it's a host name there will also not be another ':'. - else if (socket.AsSpan(0, lastColonPos).LastIndexOf(':') != -1) - { - // This is an IPv6 address WITHOUT a port. - lastColonPos = -1; - } + // This is an IPv6 address WITH a port. } - - if (!string.IsNullOrWhiteSpace(socket) && lastColonPos != -1) + // Look to see if this is IPv4 with a port (IPv6 will have another colon) + // If it's a host name there will also not be another ':'. + else if (socket.AsSpan(0, lastColonPos).LastIndexOf(':') != -1) { - port = Convert.ToInt32(socket.Substring(lastColonPos + 1).Trim()); + // This is an IPv6 address WITHOUT a port. + lastColonPos = -1; } + } - return port; + if (!string.IsNullOrWhiteSpace(socket) && lastColonPos != -1) + { + port = Convert.ToInt32(socket.Substring(lastColonPos + 1).Trim()); } - /// - /// (convenience method) check if string can be parsed as IPAddress - /// - /// string to check - /// true/false - public static bool IsIPAddress(string socket) + return port; + } + + /// + /// (convenience method) check if string can be parsed as IPAddress + /// + /// string to check + /// true/false + public static bool IsIPAddress(string socket) + { + if (string.IsNullOrWhiteSpace(socket)) { - if (string.IsNullOrWhiteSpace(socket)) - { - return false; - } - else - { - IPAddress ipaddr; - return IPAddress.TryParse(socket, out ipaddr); - } + return false; } + else + { + return IPAddress.TryParse(socket, out var ipaddr); + } + } - /// - /// Checks the Contact SIP URI host and if it is recognised as a private address it is replaced with the socket - /// the SIP message was received on. - /// - /// Private address space blocks RFC 1597. - /// 10.0.0.0 - 10.255.255.255 - /// 172.16.0.0 - 172.31.255.255 - /// 192.168.0.0 - 192.168.255.255 - /// - /// - public static bool IsPrivateAddress(string host) + /// + /// Checks the Contact SIP URI host and if it is recognised as a private address it is replaced with the socket the + /// SIP message was received on. + /// Private address space blocks RFC 1597. 10.0.0.0 - 10.255.255.255 172.16.0.0 - 172.31.255.255 192.168.0.0 - + /// 192.168.255.255 + /// + /// + public static bool IsPrivateAddress(string host) + { + if (IPAddress.TryParse(host, out var ipAddress)) { - if (IPAddress.TryParse(host, out var ipAddress)) + if (IPAddress.IsLoopback(ipAddress) || ipAddress.IsIPv6LinkLocal || ipAddress.IsIPv6SiteLocal) { - if (IPAddress.IsLoopback(ipAddress) || ipAddress.IsIPv6LinkLocal || ipAddress.IsIPv6SiteLocal) + return true; + } + else if (ipAddress.AddressFamily == AddressFamily.InterNetwork) + { + var addrBytes = ipAddress.GetAddressBytes(); + if ((addrBytes[0] == 10) || + (addrBytes[0] == 172 && (addrBytes[1] >= 16 && addrBytes[1] <= 31)) || + (addrBytes[0] == 192 && addrBytes[1] == 168)) { return true; } - else if (ipAddress.AddressFamily == AddressFamily.InterNetwork) - { - byte[] addrBytes = ipAddress.GetAddressBytes(); - if ((addrBytes[0] == 10) || - (addrBytes[0] == 172 && (addrBytes[1] >= 16 && addrBytes[1] <= 31)) || - (addrBytes[0] == 192 && addrBytes[1] == 168)) - { - return true; - } - } } + } - return false; + return false; + } + + /// + /// Check if contains a hostname or ip-address and ip-port accepts IPv4 and IPv6 + /// and IPv6 mapped IPv4 addresses return detected values in and + /// adapted from: http://stackoverflow.com/questions/2727609/best-way-to-create-ipendpoint-from-string + /// + /// + /// rj2: I had the requirement of parsing an IPEndpoint with IPv6, v4 and hostnames and getting them as string and + /// int + /// + /// string to check + /// + /// host-portion of , if host can be parsed as IPAddress, then + /// is IPAddress.ToString + /// + /// port-portion of + /// true if host-portion of endpoint string is valid ip-address + /// if is null/empty + /// if host looks like ip-address but can't be parsed + public static bool Parse(string endpointstring, out string host, out int port) + { + bool rc = false; + if (string.IsNullOrWhiteSpace(endpointstring)) + { + throw new ArgumentException("Endpoint descriptor must not be empty."); } - /// - /// Check if contains a hostname or ip-address and ip-port - /// accepts IPv4 and IPv6 and IPv6 mapped IPv4 addresses - /// return detected values in and - /// - /// adapted from: http://stackoverflow.com/questions/2727609/best-way-to-create-ipendpoint-from-string - /// - /// - /// rj2: I had the requirement of parsing an IPEndpoint with IPv6, v4 and hostnames and getting them as string and int - /// - /// string to check - /// host-portion of , if host can be parsed as IPAddress, then is IPAddress.ToString - /// port-portion of - /// true if host-portion of endpoint string is valid ip-address - /// if is null/empty - /// if host looks like ip-address but can't be parsed - public static bool Parse(string endpointstring, out string host, out int port) + string[] values; + if (endpointstring.IndexOf(';') > 0) { - bool rc = false; - if (string.IsNullOrWhiteSpace(endpointstring)) - { - throw new ArgumentException("Endpoint descriptor must not be empty."); - } + values = endpointstring.Substring(0, endpointstring.IndexOf(';')).Split(new char[] { ':' }); + } + else + { + values = endpointstring.Split(new char[] { ':' }); + } - string[] values = null; - if (endpointstring.IndexOf(';') > 0) + IPAddress? ipaddr; + port = -1; + + //check if we have an IPv6 or ports + if (values.Length <= 2) // ipv4 or hostname + { + if (values.Length == 1) { - values = endpointstring.Substring(0, endpointstring.IndexOf(';')).Split(new char[] { ':' }); + //no port is specified, default + port = -1; } else { - values = endpointstring.Split(new char[] { ':' }); + port = getPort(values[1]); } - IPAddress ipaddr; - port = -1; - - //check if we have an IPv6 or ports - if (values.Length <= 2) // ipv4 or hostname + host = values[0]; + //try to use the address as IPv4, otherwise get hostname + if (!IPAddress.TryParse(values[0], out ipaddr)) { - if (values.Length == 1) - { - //no port is specified, default - port = -1; - } - else - { - port = getPort(values[1]); - } - host = values[0]; - //try to use the address as IPv4, otherwise get hostname - if (!IPAddress.TryParse(values[0], out ipaddr)) - { - host = values[0]; - } - else - { - host = ipaddr.ToString(); - rc = true; - } } - else if (values.Length > 2) //ipv6 + else { - //could [a:b:c]:d - if (values[0].StartsWith("[") && values[values.Length - 2].EndsWith("]")) + host = ipaddr.ToString(); + rc = true; + } + } + else if (values.Length > 2) //ipv6 + { + //could [a:b:c]:d + if (values[0].StartsWith("[") && values[values.Length - 2].EndsWith("]")) + { + string ipaddressstring = string.Join(":", values.Take(values.Length - 1).ToArray()); + ipaddr = IPAddress.Parse(ipaddressstring); + port = getPort(values[values.Length - 1]); + host = ipaddr.ToString(); + } + else //[a:b:c] or a:b:c + { + if (endpointstring.IndexOf(';') > 0) { - string ipaddressstring = string.Join(":", values.Take(values.Length - 1).ToArray()); - ipaddr = IPAddress.Parse(ipaddressstring); - port = getPort(values[values.Length - 1]); - host = ipaddr.ToString(); + ipaddr = IPAddress.Parse(endpointstring.Substring(0, endpointstring.IndexOf(';'))); } - else //[a:b:c] or a:b:c + else { - if (endpointstring.IndexOf(';') > 0) - { - ipaddr = IPAddress.Parse(endpointstring.Substring(0, endpointstring.IndexOf(';'))); - } - else - { - ipaddr = IPAddress.Parse(endpointstring); - } - - host = ipaddr.ToString(); - port = -1; + ipaddr = IPAddress.Parse(endpointstring); } - rc = true; - } - else - { - throw new FormatException($"Invalid endpoint ipaddress '{endpointstring}'"); + + host = ipaddr.ToString(); + port = -1; } + rc = true; + } + else + { + throw new FormatException($"Invalid endpoint ipaddress '{endpointstring}'"); + } - return rc; + return rc; + } + + public static IPEndPoint Parse(string endpointstring, int defaultport = -1) + { + if (endpointstring.IsNullOrBlank()) + { + throw new ArgumentException("Endpoint descriptor must not be empty."); } - public static IPEndPoint Parse(string endpointstring, int defaultport = -1) + if (defaultport is not (-1) and + (< IPEndPoint.MinPort + or > IPEndPoint.MaxPort)) { - if (endpointstring.IsNullOrBlank()) + throw new ArgumentException($"Invalid default port '{defaultport}'"); + } + + var values = endpointstring.Split(':'); + IPAddress? ipaddr; + var port = -1; + + //check if we have an IPv6 or ports + if (values.Length <= 2) // ipv4 or hostname + { + if (values.Length == 1) { - throw new ArgumentException("Endpoint descriptor must not be empty."); + //no port is specified, default + port = defaultport; } - - if (defaultport != -1 && - (defaultport < IPEndPoint.MinPort - || defaultport > IPEndPoint.MaxPort)) + else { - throw new ArgumentException($"Invalid default port '{defaultport}'"); + port = getPort(values[1]); } - string[] values = endpointstring.Split(new char[] { ':' }); - IPAddress ipaddr; - int port = -1; - - //check if we have an IPv6 or ports - if (values.Length <= 2) // ipv4 or hostname + //try to use the address as IPv4, otherwise get hostname + if (!IPAddress.TryParse(values[0], out ipaddr)) { - if (values.Length == 1) + try { - //no port is specified, default - port = defaultport; + ipaddr = getIPfromHost(values[0]); } - else - { - port = getPort(values[1]); - } - - //try to use the address as IPv4, otherwise get hostname - if (!IPAddress.TryParse(values[0], out ipaddr)) + catch { - try - { - ipaddr = getIPfromHost(values[0]); - } - catch - { - throw new FormatException($"Invalid endpoint ipaddress '{endpointstring}'"); - } + throw new FormatException($"Invalid endpoint ipaddress '{endpointstring}'"); } } - else if (values.Length > 2) //ipv6 + } + else if (values.Length > 2) //ipv6 + { + //could [a:b:c]:d + if (values[0] is { Length: > 0 } start && start[0] == '[' && values[^2] is { Length: > 0 } end && end[^1] == ']') { - //could [a:b:c]:d - if (values[0].StartsWith("[") && values[values.Length - 2].EndsWith("]")) - { - string ipaddressstring = string.Join(":", values.Take(values.Length - 1).ToArray()); - ipaddr = IPAddress.Parse(ipaddressstring); - port = getPort(values[values.Length - 1]); - } - else //[a:b:c] or a:b:c - { - ipaddr = IPAddress.Parse(endpointstring); - port = defaultport; + // Join all parts except the last as IPv6 address string + using var vsb = new ValueStringBuilder(stackalloc char[128]); + vsb.Append(values[0]); + for (int i = 1; i < values.Length - 1; i++) + { + vsb.Append(':'); + vsb.Append(values[i]); } + ipaddr = IPAddress.Parse(vsb.AsSpan()); + port = getPort(values[values.Length - 1]); } - else + else //[a:b:c] or a:b:c { - throw new FormatException($"Invalid endpoint ipaddress '{endpointstring}'"); + ipaddr = IPAddress.Parse(endpointstring); + port = defaultport; } - - if (port == -1) - { - port = 0; - } - - return new IPEndPoint(ipaddr, port); + } + else + { + throw new FormatException($"Invalid endpoint ipaddress '{endpointstring}'"); } - private static int getPort(string p) + if (port == -1) { - int port; + port = 0; + } - if (!int.TryParse(p, out port) - || port < IPEndPoint.MinPort - || port > IPEndPoint.MaxPort) - { - throw new FormatException($"Invalid end point port '{p}'"); - } + return new IPEndPoint(ipaddr, port); + } - return port; + private static int getPort(string p) + { + int port; + + if (!int.TryParse(p, out port) + || port < IPEndPoint.MinPort + || port > IPEndPoint.MaxPort) + { + throw new FormatException($"Invalid end point port '{p}'"); } - private static IPAddress getIPfromHost(string p) + return port; + } + + private static IPAddress getIPfromHost(string p) + { + try { - try - { - var hosts = Dns.GetHostAddresses(p); + var hosts = Dns.GetHostAddresses(p); - if (hosts == null || hosts.Length == 0) - { - throw new ArgumentException($"Host not found: {p}"); - } - return hosts[0]; - } - catch + if (hosts is null || hosts.Length == 0) { throw new ArgumentException($"Host not found: {p}"); } + return hosts[0]; } - - /// - /// Returns an IPv4 end point from a socket address in 10.0.0.1:5060 format. - /// > - public static IPEndPoint GetIPEndPoint(string IPSocket) + catch { - return Parse(IPSocket); + throw new ArgumentException($"Host not found: {p}"); } } + + /// + /// Returns an IPv4 end point from a socket address in 10.0.0.1:5060 format. + /// + public static IPEndPoint GetIPEndPoint(string IPSocket) + { + return Parse(IPSocket); + } } diff --git a/src/SIPSorcery/sys/Net/NetConvert.cs b/src/SIPSorcery/sys/Net/NetConvert.cs index 7d79bc4b04..5835e8efe6 100644 --- a/src/SIPSorcery/sys/Net/NetConvert.cs +++ b/src/SIPSorcery/sys/Net/NetConvert.cs @@ -25,7 +25,7 @@ public class NetConvert /// The buffer to parse the value from. /// The position in the buffer to start the parse from. /// A UInt16 value. - public static ushort ParseUInt16(byte[] buffer, int posn) + public static ushort ParseUInt16(ReadOnlySpan buffer, int posn) { return (ushort)(buffer[posn] << 8 | buffer[posn + 1]); } @@ -36,7 +36,7 @@ public static ushort ParseUInt16(byte[] buffer, int posn) /// The buffer to parse the value from. /// The position in the buffer to start the parse from. /// A UInt32 value. - public static uint ParseUInt32(byte[] buffer, int posn) + public static uint ParseUInt32(ReadOnlySpan buffer, int posn) { return (uint)(buffer[posn] << 24 | buffer[posn + 1] << 16 | buffer[posn + 2] << 8 | buffer[posn + 3]); } @@ -47,7 +47,7 @@ public static uint ParseUInt32(byte[] buffer, int posn) /// The buffer to parse the value from. /// The position in the buffer to start the parse from. /// A UInt64 value. - public static ulong ParseUInt64(byte[] buffer, int posn) + public static ulong ParseUInt64(ReadOnlySpan buffer, int posn) { return (ulong)buffer[posn] << 56 | diff --git a/src/SIPSorcery/sys/Net/NetServices.cs b/src/SIPSorcery/sys/Net/NetServices.cs index c5824d713f..19cae68c74 100644 --- a/src/SIPSorcery/sys/Net/NetServices.cs +++ b/src/SIPSorcery/sys/Net/NetServices.cs @@ -18,340 +18,326 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; -namespace SIPSorcery.Sys +namespace SIPSorcery.Sys; + +/// +/// Helper class to provide network services. +/// +public static class NetServices { + private const int RTP_RECEIVE_BUFFER_SIZE = 1000000; + private const int RTP_SEND_BUFFER_SIZE = 1000000; + + /// + /// The maximum number of re-attempts that will be made when trying to bind a UDP socket. + /// + private const int MAXIMUM_UDP_PORT_BIND_ATTEMPTS = 25; + + /// + /// IP address to use when getting default IP address from OS. + /// No connection is established. + /// + private const string INTERNET_IPADDRESS = "8.8.8.8"; + + /// + /// IP address to use when getting default IPv6 address from OS. + /// No connection is established. + /// + private const string INTERNET_IPv6ADDRESS = "2001:4860:4860::8888"; + /// - /// Helper class to provide network services. + /// Port to use when doing a Udp.Connect to determine local IP + /// address (port 0 does not work on MacOS). /// - public class NetServices + private const int NETWORK_TEST_PORT = 5060; + + /// + /// The amount of time to leave the result of a local IP address + /// determination in the cache. + /// + private const int LOCAL_ADDRESS_CACHE_LIFETIME_SECONDS = 300; + + private static readonly ILogger logger = LogFactory.CreateLogger(typeof(NetServices).FullName!); + + /// + /// Doing the same check as here https://github.com/dotnet/corefx/blob/e99ec129cfd594d53f4390bf97d1d736cff6f860/src/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs#L19. + /// Which is checking if a dual mode socket can use the *ReceiveFrom* methods in order to + /// be able to get the remote destination end point. + /// To date the only case this has cropped up for is Mac OS as per https://github.com/sipsorcery/sipsorcery/issues/207. + /// + private static bool? _supportsDualModeIPv4PacketInfo; + public static bool SupportsDualModeIPv4PacketInfo { - private const int RTP_RECEIVE_BUFFER_SIZE = 1000000; - private const int RTP_SEND_BUFFER_SIZE = 1000000; - - /// - /// The maximum number of re-attempts that will be made when trying to bind a UDP socket. - /// - private const int MAXIMUM_UDP_PORT_BIND_ATTEMPTS = 25; - - /// - /// IP address to use when getting default IP address from OS. - /// No connection is established. - /// - private const string INTERNET_IPADDRESS = "8.8.8.8"; - - /// - /// IP address to use when getting default IPv6 address from OS. - /// No connection is established. - /// - private const string INTERNET_IPv6ADDRESS = "2001:4860:4860::8888"; - - /// - /// Port to use when doing a Udp.Connect to determine local IP - /// address (port 0 does not work on MacOS). - /// - private const int NETWORK_TEST_PORT = 5060; - - /// - /// The amount of time to leave the result of a local IP address - /// determination in the cache. - /// - private const int LOCAL_ADDRESS_CACHE_LIFETIME_SECONDS = 300; - - private static readonly ILogger logger = LogFactory.CreateLogger(); - - /// - /// Doing the same check as here https://github.com/dotnet/corefx/blob/e99ec129cfd594d53f4390bf97d1d736cff6f860/src/System.Net.Sockets/src/System/Net/Sockets/SocketPal.Unix.cs#L19. - /// Which is checking if a dual mode socket can use the *ReceiveFrom* methods in order to - /// be able to get the remote destination end point. - /// To date the only case this has cropped up for is Mac OS as per https://github.com/sipsorcery/sipsorcery/issues/207. - /// - private static bool? _supportsDualModeIPv4PacketInfo = null; - public static bool SupportsDualModeIPv4PacketInfo + get { - get + if (!_supportsDualModeIPv4PacketInfo.HasValue) { - if (!_supportsDualModeIPv4PacketInfo.HasValue) + try { - try - { - _supportsDualModeIPv4PacketInfo = DoCheckSupportsDualModeIPv4PacketInfo(); - } - catch - { - _supportsDualModeIPv4PacketInfo = false; - } + _supportsDualModeIPv4PacketInfo = DoCheckSupportsDualModeIPv4PacketInfo(); + } + catch + { + _supportsDualModeIPv4PacketInfo = false; } - - return _supportsDualModeIPv4PacketInfo.Value; } + + return _supportsDualModeIPv4PacketInfo.Value; } + } - /// - /// A lookup collection to cache the local IP address for a destination address. The collection will cache results of - /// asking the Operating System which local address to use for a destination address. The cache saves a relatively - /// expensive call to create a socket and ask the OS for a route lookup. - /// - private static ConcurrentDictionary> m_localAddressTable = - new ConcurrentDictionary>(); + /// + /// A lookup collection to cache the local IP address for a destination address. The collection will cache results of + /// asking the Operating System which local address to use for a destination address. The cache saves a relatively + /// expensive call to create a socket and ask the OS for a route lookup. + /// + private static ConcurrentDictionary m_localAddressTable = + new ConcurrentDictionary(); - static NetServices() + static NetServices() + { + try { - try - { - NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; - } - catch (PlatformNotSupportedException ex) - { - // Some runtimes (notably Unity's Windows-shipped Mono, see - // issue #1614) implement the NetworkChange type but throw - // when anything subscribes. Without this guard the static - // ctor — and therefore the first RTCPeerConnection() — fails - // with TypeInitializationException. The behavioural cost is - // that m_localAddressTable will not be invalidated when - // adapters come and go on those runtimes; entries stay - // cached for the process lifetime. - logger.LogWarning(ex, "NetworkChange.NetworkAddressChanged is not supported on this runtime; the local-address cache will not auto-invalidate on adapter changes."); - } + NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; } - - private static void OnNetworkAddressChanged(object sender, EventArgs e) + catch (PlatformNotSupportedException ex) { - // Clear cached addresses if the state of the local network interfaces change. - m_localAddressTable.Clear(); - _localIPAddresses = null; - _internetDefaultAddress = null; - _internetDefaultIPv6Address = null; + // Some runtimes (notably Unity's Windows-shipped Mono, see + // issue #1614) implement the NetworkChange type but throw + // when anything subscribes. Without this guard the static + // ctor — and therefore the first RTCPeerConnection() — fails + // with TypeInitializationException. The behavioural cost is + // that m_localAddressTable will not be invalidated when + // adapters come and go on those runtimes; entries stay + // cached for the process lifetime. + logger.LogNetworkAddressChangedUnsupported(ex); } + } - /// - /// The list of IP addresses that this machine can use. - /// - public static List LocalIPAddresses + private static void OnNetworkAddressChanged(object? sender, EventArgs e) + { + // Clear cached addresses if the state of the local network interfaces change. + m_localAddressTable.Clear(); + _localIPAddresses = null; + _internetDefaultAddress = null; + _internetDefaultIPv6Address = null; + } + + /// + /// The list of IP addresses that this machine can use. + /// + public static List LocalIPAddresses + { + get { - get + if (_localIPAddresses is null) { - if (_localIPAddresses == null) - { - _localIPAddresses = NetServices.GetAllLocalIPAddresses(); - } + _localIPAddresses = NetServices.GetAllLocalIPAddresses(); + } - return _localIPAddresses; + return _localIPAddresses; - // Using this call seems to be the recommended way to get the local IP addresses. - // https://docs.microsoft.com/en-us/dotnet/api/system.net.dns.gethostaddresses?view=netcore-3.1 - // Unfortunately this does not work on WSL2 prior to .net5.0 see https://github.com/dotnet/runtime/issues/37785 - //return Dns.GetHostAddresses(string.Empty).ToList(); - } + // Using this call seems to be the recommended way to get the local IP addresses. + // https://docs.microsoft.com/en-us/dotnet/api/system.net.dns.gethostaddresses?view=netcore-3.1 + // Unfortunately this does not work on WSL2 prior to .net5.0 see https://github.com/dotnet/runtime/issues/37785 + //return Dns.GetHostAddresses(string.Empty).ToList(); } - private static List _localIPAddresses = null; + } + private static List? _localIPAddresses; - /// - /// The local IP address this machine uses to communicate with the Internet. - /// - public static IPAddress InternetDefaultAddress + /// + /// The local IP address this machine uses to communicate with the Internet. + /// + public static IPAddress? InternetDefaultAddress + { + get { - get - { - if (_internetDefaultAddress == null) - { - _internetDefaultAddress = GetLocalAddressForInternet(); - } + _internetDefaultAddress ??= GetLocalAddressForInternet(); - return _internetDefaultAddress; - } + return _internetDefaultAddress; } - private static IPAddress _internetDefaultAddress = null; + } + private static IPAddress? _internetDefaultAddress; - /// - /// The local IPv6 address this machine uses to communicate with the Internet. - /// - public static IPAddress InternetDefaultIPv6Address + /// + /// The local IPv6 address this machine uses to communicate with the Internet. + /// + public static IPAddress? InternetDefaultIPv6Address + { + get { - get - { - if (_internetDefaultIPv6Address == null) - { - _internetDefaultIPv6Address = GetLocalIPv6AddressForInternet(); - } + _internetDefaultIPv6Address ??= GetLocalIPv6AddressForInternet(); - return _internetDefaultIPv6Address; - } + return _internetDefaultIPv6Address; } - private static IPAddress _internetDefaultIPv6Address = null; + } + private static IPAddress? _internetDefaultIPv6Address; - /// - /// Checks whether an IP address can be used on the underlying System. - /// - /// The bind address to use. - private static void CheckBindAddressAndThrow(IPAddress bindAddress) + /// + /// Checks whether an IP address can be used on the underlying System. + /// + /// The bind address to use. + private static void CheckBindAddressAndThrow(IPAddress bindAddress) + { + if (bindAddress is { } && bindAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { - if (bindAddress != null && bindAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) - { - throw new ApplicationException("A UDP socket cannot be created on an IPv6 address due to lack of OS support."); - } - else if (bindAddress != null && bindAddress.AddressFamily == AddressFamily.InterNetwork && !Socket.OSSupportsIPv4) - { - throw new ApplicationException("A UDP socket cannot be created on an IPv4 address due to lack of OS support."); - } + throw new SipSorceryException("A UDP socket cannot be created on an IPv6 address due to lack of OS support."); } - - /// - /// Attempts to create and bind a UDP socket. The socket is always created with the ExclusiveAddressUse socket option - /// set to accommodate a Windows 10 .Net Core socket bug where the same port can be bound to two different - /// sockets, see https://github.com/dotnet/runtime/issues/36618. - /// - /// The port to attempt to bind on. Set to 0 to request the underlying OS to select a port. - /// Optional. If specified the UDP socket will attempt to bind using this specific address. - /// If not specified the broadest possible address will be chosen. Either IPAddress.Any or IPAddress.IPv6Any. - /// If true the method will only return successfully if it is able to bind on an - /// even numbered port. - /// If true then IPv6 sockets will be created as dual mode IPv4/IPv6 on supporting systems. - /// A bound socket if successful or throws an ApplicationException if unable to bind. - public static Socket CreateBoundUdpSocket(int port, IPAddress bindAddress, bool requireEvenPort = false, bool useDualMode = true) + else if (bindAddress is { } && bindAddress.AddressFamily == AddressFamily.InterNetwork && !Socket.OSSupportsIPv4) { - return CreateBoundSocket(port, bindAddress, ProtocolType.Udp, requireEvenPort, useDualMode); + throw new SipSorceryException("A UDP socket cannot be created on an IPv4 address due to lack of OS support."); } + } - /// - /// Attempts to create and bind a TCP socket. The socket is always created with the ExclusiveAddressUse socket option - /// set to accommodate a Windows 10 .Net Core socket bug where the same port can be bound to two different - /// sockets, see https://github.com/dotnet/runtime/issues/36618. - /// - /// The port to attempt to bind on. Set to 0 to request the underlying OS to select a port. - /// Optional. If specified the TCP socket will attempt to bind using this specific address. - /// If not specified the broadest possible address will be chosen. Either IPAddress.Any or IPAddress.IPv6Any. - /// If true the method will only return successfully if it is able to bind on an - /// even numbered port. - /// If true then IPv6 sockets will be created as dual mode IPv4/IPv6 on supporting systems. - /// A bound socket if successful or throws an ApplicationException if unable to bind. - public static Socket CreateBoundTcpSocket(int port, IPAddress bindAddress, bool requireEvenPort = false, bool useDualMode = true) + /// + /// Attempts to create and bind a UDP socket. The socket is always created with the ExclusiveAddressUse socket option + /// set to accommodate a Windows 10 .Net Core socket bug where the same port can be bound to two different + /// sockets, see https://github.com/dotnet/runtime/issues/36618. + /// + /// The port to attempt to bind on. Set to 0 to request the underlying OS to select a port. + /// Optional. If specified the UDP socket will attempt to bind using this specific address. + /// If not specified the broadest possible address will be chosen. Either IPAddress.Any or IPAddress.IPv6Any. + /// If true the method will only return successfully if it is able to bind on an + /// even numbered port. + /// If true then IPv6 sockets will be created as dual mode IPv4/IPv6 on supporting systems. + /// A bound socket if successful or throws an SipSorceryException if unable to bind. + public static Socket? CreateBoundUdpSocket(int port, IPAddress bindAddress, bool requireEvenPort = false, bool useDualMode = true) + { + return CreateBoundSocket(port, bindAddress, ProtocolType.Udp, requireEvenPort, useDualMode); + } + + /// + /// Attempts to create and bind a TCP socket. The socket is always created with the ExclusiveAddressUse socket option + /// set to accommodate a Windows 10 .Net Core socket bug where the same port can be bound to two different + /// sockets, see https://github.com/dotnet/runtime/issues/36618. + /// + /// The port to attempt to bind on. Set to 0 to request the underlying OS to select a port. + /// Optional. If specified the TCP socket will attempt to bind using this specific address. + /// If not specified the broadest possible address will be chosen. Either IPAddress.Any or IPAddress.IPv6Any. + /// If true the method will only return successfully if it is able to bind on an + /// even numbered port. + /// If true then IPv6 sockets will be created as dual mode IPv4/IPv6 on supporting systems. + /// A bound socket if successful or throws an SipSorceryException if unable to bind. + public static Socket? CreateBoundTcpSocket(int port, IPAddress bindAddress, bool requireEvenPort = false, bool useDualMode = true) + { + return CreateBoundSocket(port, bindAddress, ProtocolType.Tcp, requireEvenPort, useDualMode); + } + + /// + /// Attempts to create and bind a socket with defined protocol. The socket is always created with the ExclusiveAddressUse socket option + /// set to accommodate a Windows 10 .Net Core socket bug where the same port can be bound to two different + /// sockets, see https://github.com/dotnet/runtime/issues/36618. + /// + /// The port to attempt to bind on. Set to 0 to request the underlying OS to select a port. + /// Optional. If specified the socket will attempt to bind using this specific address. + /// If not specified the broadest possible address will be chosen. Either IPAddress.Any or IPAddress.IPv6Any. + /// Optional. If specified the socket procotol + /// If true the method will only return successfully if it is able to bind on an + /// even numbered port. + /// If true then IPv6 sockets will be created as dual mode IPv4/IPv6 on supporting systems. + /// A bound socket if successful or throws an SipSorceryException if unable to bind. + public static Socket? CreateBoundSocket(int port, IPAddress bindAddress, ProtocolType protocolType, bool requireEvenPort = false, bool useDualMode = true) + { + if (requireEvenPort && port != 0 && port % 2 != 0) { - return CreateBoundSocket(port, bindAddress, ProtocolType.Tcp, requireEvenPort, useDualMode); + throw new ArgumentException("Cannot specify both require even port and a specific non-even port to bind on. Set port to 0."); } - /// - /// Attempts to create and bind a socket with defined protocol. The socket is always created with the ExclusiveAddressUse socket option - /// set to accommodate a Windows 10 .Net Core socket bug where the same port can be bound to two different - /// sockets, see https://github.com/dotnet/runtime/issues/36618. - /// - /// The port to attempt to bind on. Set to 0 to request the underlying OS to select a port. - /// Optional. If specified the socket will attempt to bind using this specific address. - /// If not specified the broadest possible address will be chosen. Either IPAddress.Any or IPAddress.IPv6Any. - /// Optional. If specified the socket procotol - /// If true the method will only return successfully if it is able to bind on an - /// even numbered port. - /// If true then IPv6 sockets will be created as dual mode IPv4/IPv6 on supporting systems. - /// A bound socket if successful or throws an ApplicationException if unable to bind. - public static Socket CreateBoundSocket(int port, IPAddress bindAddress, ProtocolType protocolType, bool requireEvenPort = false, bool useDualMode = true) + if (bindAddress is null) { - if (requireEvenPort && port != 0 && port % 2 != 0) - { - throw new ArgumentException("Cannot specify both require even port and a specific non-even port to bind on. Set port to 0."); - } + bindAddress = (Socket.OSSupportsIPv6 && SupportsDualModeIPv4PacketInfo) ? IPAddress.IPv6Any : IPAddress.Any; + } - if (bindAddress == null) - { - bindAddress = (Socket.OSSupportsIPv6 && SupportsDualModeIPv4PacketInfo) ? IPAddress.IPv6Any : IPAddress.Any; - } + var logEp = new IPEndPoint(bindAddress, port); - IPEndPoint logEp = new IPEndPoint(bindAddress, port); - logger.LogDebug("CreateBoundSocket attempting to create and bind socket(s) on {logEp} using protocol {protocolType}.", logEp, protocolType); + logger.LogCreateBoundSocketStart(logEp, protocolType); - CheckBindAddressAndThrow(bindAddress); + CheckBindAddressAndThrow(bindAddress); - int bindAttempts = 0; - AddressFamily addressFamily = bindAddress.AddressFamily; - bool success = false; - Socket socket = null; + var bindAttempts = 0; + var addressFamily = bindAddress.AddressFamily; + var success = false; + Socket? socket = null; - while (bindAttempts < MAXIMUM_UDP_PORT_BIND_ATTEMPTS) + while (bindAttempts < MAXIMUM_UDP_PORT_BIND_ATTEMPTS) + { + try { - try - { - socket = CreateSocket(addressFamily, protocolType, useDualMode); - BindSocket(socket, bindAddress, port); - int boundPort = (socket.LocalEndPoint as IPEndPoint).Port; + socket = CreateSocket(addressFamily, protocolType, useDualMode); + BindSocket(socket, bindAddress, port); + var localEndPoint = socket.LocalEndPoint as IPEndPoint; + Debug.Assert(localEndPoint is { }); + var boundPort = localEndPoint.Port; - if (requireEvenPort && boundPort % 2 != 0 && boundPort == IPEndPoint.MaxPort) + if (requireEvenPort && boundPort % 2 != 0 && boundPort == IPEndPoint.MaxPort) + { + logger.LogCreateBoundSocketEvenPortClose(socket.LocalEndPoint); + success = false; + } + else + { + if (requireEvenPort && boundPort % 2 != 0) { - logger.LogDebug("CreateBoundSocket even port required, closing socket on {LocalEndPoint}, max port reached request new bind.", socket.LocalEndPoint); - success = false; + logger.LogCreateBoundSocketEvenPortRetry(socket.LocalEndPoint, boundPort + 1); + + // Close the socket, create a new one and try binding on the next consecutive port. + socket.Close(); + socket = CreateSocket(addressFamily, protocolType, useDualMode); + BindSocket(socket, bindAddress, boundPort + 1); } else { - if (requireEvenPort && boundPort % 2 != 0) + if (addressFamily == AddressFamily.InterNetworkV6) { - logger.LogDebug("CreateBoundSocket even port required, closing socket on {LocalEndPoint} and retrying on {NextPort}.", socket.LocalEndPoint, boundPort + 1); - - // Close the socket, create a new one and try binding on the next consecutive port. - socket.Close(); - socket = CreateSocket(addressFamily, protocolType, useDualMode); - BindSocket(socket, bindAddress, boundPort + 1); + logger.LogCreateBoundSocketSuccessDualMode(socket.LocalEndPoint, socket.DualMode); } else { - if (addressFamily == AddressFamily.InterNetworkV6) - { - logger.LogDebug("CreateBoundSocket successfully bound on {LocalEndPoint}, dual mode {DualMode}.", socket.LocalEndPoint, socket.DualMode); - } - else - { - logger.LogDebug("CreateBoundSocket successfully bound on {LocalEndPoint}.", socket.LocalEndPoint); - } + logger.LogCreateBoundSocketSuccess(socket.LocalEndPoint); } - - success = true; - } - } - catch (SocketException sockExcp) - { - if (sockExcp.SocketErrorCode == SocketError.AddressAlreadyInUse) - { - // Try again if the port is already in use. - logger.LogWarning("Address already in use exception attempting to bind socket, attempt {BindAttempts}.", bindAttempts); - success = false; - } - else if (sockExcp.SocketErrorCode == SocketError.AccessDenied) - { - // This exception seems to be interchangeable with address already in use. Perhaps a race condition with another process - // attempting to bind at the same time. - logger.LogWarning("Access denied exception attempting to bind socket, attempt {BindAttempts}.", bindAttempts); - success = false; - } - else - { - logger.LogError(sockExcp, "SocketException in NetServices.CreateBoundSocket. {ErrorMessage}", sockExcp.Message); - throw; } + + success = true; } - catch (Exception excp) + } + catch (SocketException sockExcp) + { + if (sockExcp.SocketErrorCode == SocketError.AddressAlreadyInUse) { - logger.LogError(excp, "Exception in NetServices.CreateBoundSocket attempting the initial socket bind on address {BindAddress}.", bindAddress); - throw; + // Try again if the port is already in use. + logger.LogSocketBindAddressInUse(bindAttempts); + success = false; } - finally + else if (sockExcp.SocketErrorCode == SocketError.AccessDenied) { - if (!success) - { - socket?.Close(); - } + // This exception seems to be interchangeable with address already in use. Perhaps a race condition with another process + // attempting to bind at the same time. + logger.LogSocketBindAccessDenied(bindAttempts); + success = false; } - - if (success || port != 0) + else { - // If the bind was requested on a specific port there is no need to try again. - break; + logger.LogSocketBindException(sockExcp.Message, sockExcp); + throw; } - else + } + catch (Exception excp) + { + logger.LogSocketBindInitialException(bindAddress, excp); + throw; + } + finally + { + if (!success) { - bindAttempts++; + socket?.Close(); } } @@ -367,493 +353,547 @@ public static Socket CreateBoundSocket(int port, IPAddress bindAddress, Protocol try { + Debug.Assert(socket is not null); socket.IOControl(SIO_UDP_CONNRESET, new byte[] { 0, 0, 0, 0 }, null); } catch (SocketException excp) { - logger.LogWarning(excp, "CreateBoundSocket was unable to disable UDP connection reset handling on {logEp}. Continuing with bound socket.", logEp); + logger.LogUdpConnectionResetDisableFailed(logEp, excp); } catch (NotSupportedException excp) { - logger.LogWarning(excp, "CreateBoundSocket does not support disabling UDP connection reset handling on {logEp}. Continuing with bound socket.", logEp); + logger.LogUdpConnectionResetDisableNotSupported(logEp, excp); } } - - return socket; + + break; + } + else if (port != 0) + { + // If a specific port is requested, only one bind attempt is made. + break; } else { - throw new ApplicationException($"Unable to bind socket using end point {logEp}."); + bindAttempts++; } } - private static void BindSocket(Socket socket, IPAddress bindAddress, int port) + if (success) { - // Nasty code warning. On Windows Subsystem for Linux (WSL) on Windows 10 - // the OS lets a socket bind on an IPv6 dual mode port even if there - // is an IPv4 socket bound to the same port. To prevent this occurring - // a test IPv4 socket bind is carried out. - // This happen even if the exclusive address socket option is set. - // See https://github.com/dotnet/runtime/issues/36618. - if (port != 0 && - socket.AddressFamily == AddressFamily.InterNetworkV6 && - socket.DualMode && IPAddress.IPv6Any.Equals(bindAddress) && - Environment.OSVersion.Platform == PlatformID.Unix && - RuntimeInformation.OSDescription.Contains("Microsoft")) - { - // Create a dummy IPv4 socket and attempt to bind it to the same port - // to check the port isn't already in use. - if (Socket.OSSupportsIPv4) - { - logger.LogDebug("WSL detected, carrying out bind check on 0.0.0.0:{Port}.", port); - - using (Socket testSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) - { - testSocket.Bind(new IPEndPoint(IPAddress.Any, port)); - testSocket.Close(); - } - } - } - - socket.Bind(new IPEndPoint(bindAddress, port)); + return socket; } - - private static Socket CreateSocket(AddressFamily addressFamily, ProtocolType protocol, bool useDualMode = true) + else { - var sock = new Socket(addressFamily, protocol == ProtocolType.Tcp ? SocketType.Stream : SocketType.Dgram, protocol); - sock.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); + throw new SipSorceryException($"Unable to bind socket using end point {bindAddress}:{port}."); + } + } - if (addressFamily == AddressFamily.InterNetworkV6) + private static void BindSocket(Socket socket, IPAddress bindAddress, int port) + { + // Nasty code warning. On Windows Subsystem for Linux (WSL) on Windows 10 + // the OS lets a socket bind on an IPv6 dual mode port even if there + // is an IPv4 socket bound to the same port. To prevent this occurring + // a test IPv4 socket bind is carried out. + // This happen even if the exclusive address socket option is set. + // See https://github.com/dotnet/runtime/issues/36618. + if (port != 0 && + socket.AddressFamily == AddressFamily.InterNetworkV6 && + socket.DualMode && IPAddress.IPv6Any.Equals(bindAddress) && + Environment.OSVersion.Platform == PlatformID.Unix && + RuntimeInformation.OSDescription.Contains("Microsoft")) + { + // Create a dummy IPv4 socket and attempt to bind it to the same port + // to check the port isn't already in use. + if (Socket.OSSupportsIPv4) { - if (!useDualMode) - { - sock.DualMode = false; - } - else + logger.LogWSLBindCheck(port); + + using (var testSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) { - sock.DualMode = SupportsDualModeIPv4PacketInfo; + testSocket.Bind(new IPEndPoint(IPAddress.Any, port)); + testSocket.Close(); } } - return sock; } - /// - /// Attempts to create and bind a new RTP UDP Socket, and optionally an control (RTCP), socket(s). - /// The RTP and control sockets created are IPv4 and IPv6 dual mode sockets which means they can send and receive - /// either IPv4 or IPv6 packets. - /// - /// True if a control (RTCP) socket should be created. Set to false if RTP - /// and RTCP are being multiplexed on the same connection. - /// Optional. If null The RTP and control sockets will be created as IPv4 and IPv6 dual mode - /// sockets which means they can send and receive either IPv4 or IPv6 packets. If the bind address is specified an attempt - /// will be made to bind the RTP and optionally control listeners on it. - /// Optional. If 0 the choice of port will be left up to the Operating System. If specified - /// a single attempt will be made to bind on the port. - /// Optional. If non-null the choice of port will be left up to the PortRange. Multiple ports will be - /// tried before giving up. The parameter bindPort is ignored. - /// An output parameter that will contain the allocated RTP socket. - /// An output parameter that will contain the allocated control (RTCP) socket. - public static void CreateRtpSocket( - bool createControlSocket, - IPAddress bindAddress, - int bindPort, - PortRange portRange, - out Socket rtpSocket, - out Socket controlSocket) - { - CreateRtpSocket(createControlSocket, ProtocolType.Udp, bindAddress, bindPort, portRange, true, true, out rtpSocket, out controlSocket); - } + socket.Bind(new IPEndPoint(bindAddress, port)); + } - /// - /// Attempts to create and bind a new RTP Socket with protocol, and optionally an control (RTCP), socket(s). - /// The RTP and control sockets created are IPv4 and IPv6 dual mode sockets which means they can send and receive - /// either IPv4 or IPv6 packets. - /// - /// True if a control (RTCP) socket should be created. Set to false if RTP - /// and RTCP are being multiplexed on the same connection. - /// Procotol used by socket - /// Optional. If null The RTP and control sockets will be created as IPv4 and IPv6 dual mode - /// sockets which means they can send and receive either IPv4 or IPv6 packets. If the bind address is specified an attempt - /// will be made to bind the RTP and optionally control listeners on it. - /// Optional. If 0 the choice of port will be left up to the Operating System. If specified - /// a single attempt will be made to bind on the port. - /// Optional. If non-null the choice of port will be left up to the PortRange. Multiple ports will be - /// tried before giving up. The parameter bindPort is ignored. - /// - /// - /// An output parameter that will contain the allocated RTP socket. - /// An output parameter that will contain the allocated control (RTCP) socket. - public static void CreateRtpSocket( - bool createControlSocket, - ProtocolType protocolType, - IPAddress bindAddress, - int bindPort, - PortRange portRange, - bool requireEvenPort, - bool useDualMode, - out Socket rtpSocket, - out Socket controlSocket) + private static Socket CreateSocket(AddressFamily addressFamily, ProtocolType protocol, bool useDualMode = true) + { + var sock = new Socket(addressFamily, protocol == ProtocolType.Tcp ? SocketType.Stream : SocketType.Dgram, protocol); + sock.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); + + if (addressFamily == AddressFamily.InterNetworkV6) { - if (bindAddress == null) + if (!useDualMode) { - bindAddress = (Socket.OSSupportsIPv6 && SupportsDualModeIPv4PacketInfo) ? IPAddress.IPv6Any : IPAddress.Any; + sock.DualMode = false; } + else + { + sock.DualMode = SupportsDualModeIPv4PacketInfo; + } + } + return sock; + } + + /// + /// Attempts to create and bind a new RTP UDP Socket, and optionally an control (RTCP), socket(s). + /// The RTP and control sockets created are IPv4 and IPv6 dual mode sockets which means they can send and receive + /// either IPv4 or IPv6 packets. + /// + /// True if a control (RTCP) socket should be created. Set to false if RTP + /// and RTCP are being multiplexed on the same connection. + /// Optional. If null The RTP and control sockets will be created as IPv4 and IPv6 dual mode + /// sockets which means they can send and receive either IPv4 or IPv6 packets. If the bind address is specified an attempt + /// will be made to bind the RTP and optionally control listeners on it. + /// Optional. If 0 the choice of port will be left up to the Operating System. If specified + /// a single attempt will be made to bind on the port. + /// Optional. If non-null the choice of port will be left up to the PortRange. Multiple ports will be + /// tried before giving up. The parameter bindPort is ignored. + /// An output parameter that will contain the allocated RTP socket. + /// An output parameter that will contain the allocated control (RTCP) socket. + public static void CreateRtpSocket( + bool createControlSocket, + IPAddress? bindAddress, + int bindPort, + PortRange? portRange, + out Socket? rtpSocket, + out Socket? controlSocket) + { + CreateRtpSocket(createControlSocket, ProtocolType.Udp, bindAddress, bindPort, portRange, true, true, out rtpSocket, out controlSocket); + } + + /// + /// Attempts to create and bind a new RTP Socket with protocol, and optionally an control (RTCP), socket(s). + /// The RTP and control sockets created are IPv4 and IPv6 dual mode sockets which means they can send and receive + /// either IPv4 or IPv6 packets. + /// + /// True if a control (RTCP) socket should be created. Set to false if RTP + /// and RTCP are being multiplexed on the same connection. + /// Procotol used by socket + /// Optional. If null The RTP and control sockets will be created as IPv4 and IPv6 dual mode + /// sockets which means they can send and receive either IPv4 or IPv6 packets. If the bind address is specified an attempt + /// will be made to bind the RTP and optionally control listeners on it. + /// Optional. If 0 the choice of port will be left up to the Operating System. If specified + /// a single attempt will be made to bind on the port. + /// Optional. If non-null the choice of port will be left up to the PortRange. Multiple ports will be + /// tried before giving up. The parameter bindPort is ignored. + /// + /// + /// An output parameter that will contain the allocated RTP socket. + /// An output parameter that will contain the allocated control (RTCP) socket. + public static void CreateRtpSocket( + bool createControlSocket, + ProtocolType protocolType, + IPAddress? bindAddress, + int bindPort, + PortRange? portRange, + bool requireEvenPort, + bool useDualMode, + out Socket? rtpSocket, + out Socket? controlSocket) + { + bindAddress ??= (Socket.OSSupportsIPv6 && SupportsDualModeIPv4PacketInfo) + ? IPAddress.IPv6Any + : IPAddress.Any; - CheckBindAddressAndThrow(bindAddress); + CheckBindAddressAndThrow(bindAddress); - IPEndPoint bindEP = new IPEndPoint(bindAddress, bindPort); - logger.LogDebug("CreateRtpSocket attempting to create and bind RTP socket(s) on {bindEP}.", bindEP); + var bindEP = new IPEndPoint(bindAddress, bindPort); + logger.LogCreateRtpSocketStart(bindEP); - rtpSocket = null; - controlSocket = null; - int bindAttempts = 0; + rtpSocket = null; + controlSocket = null; + var bindAttempts = 0; - while (bindAttempts < MAXIMUM_UDP_PORT_BIND_ATTEMPTS) + while (bindAttempts < MAXIMUM_UDP_PORT_BIND_ATTEMPTS) + { + try { - try + if (portRange is { }) { - if (portRange != null) - { - bindPort = portRange.GetNextPort(); - } - rtpSocket = CreateBoundSocket(bindPort, bindAddress, protocolType, requireEvenPort, useDualMode); - rtpSocket.ReceiveBufferSize = RTP_RECEIVE_BUFFER_SIZE; - rtpSocket.SendBufferSize = RTP_SEND_BUFFER_SIZE; + bindPort = portRange.GetNextPort(); + } + rtpSocket = CreateBoundSocket(bindPort, bindAddress, protocolType, requireEvenPort, useDualMode); + Debug.Assert(rtpSocket is { }); + rtpSocket.ReceiveBufferSize = RTP_RECEIVE_BUFFER_SIZE; + rtpSocket.SendBufferSize = RTP_SEND_BUFFER_SIZE; - if (createControlSocket) - { - // For legacy VoIP the RTP and Control sockets need to be consecutive with the RTP port being - // an even number. - int rtpPort = (rtpSocket.LocalEndPoint as IPEndPoint).Port; - int controlPort = rtpPort + 1; + if (createControlSocket) + { + // For legacy VoIP the RTP and Control sockets need to be consecutive with the RTP port being + // an even number. + var localEndPoint = (rtpSocket.LocalEndPoint as IPEndPoint); + Debug.Assert(localEndPoint is { }); + var controlPort = localEndPoint.Port + 1; - // Hopefully the next OS port allocation will be back in range. - if (controlPort <= IPEndPoint.MaxPort) - { - // This bind is being attempted on a specific port and can therefore legitimately fail if the port is already in use. - // Certain expected failure are caught and the attempt to bind two consecutive port will be re-attempted. - controlSocket = CreateBoundSocket(controlPort, bindAddress, protocolType); - controlSocket.ReceiveBufferSize = RTP_RECEIVE_BUFFER_SIZE; - controlSocket.SendBufferSize = RTP_SEND_BUFFER_SIZE; - } + // Hopefully the next OS port allocation will be back in range. + if (controlPort <= IPEndPoint.MaxPort) + { + // This bind is being attempted on a specific port and can therefore legitimately fail if the port is already in use. + // Certain expected failure are caught and the attempt to bind two consecutive port will be re-attempted. + controlSocket = CreateBoundSocket(controlPort, bindAddress, protocolType); + Debug.Assert(controlSocket is { }); + controlSocket.ReceiveBufferSize = RTP_RECEIVE_BUFFER_SIZE; + controlSocket.SendBufferSize = RTP_SEND_BUFFER_SIZE; } } - catch (ApplicationException) { } + } + catch (SipSorceryException) { } - if ((rtpSocket != null && (!createControlSocket || controlSocket != null)) || (bindPort != 0 && portRange == null)) - { - // If a specific bind port was specified only a single attempt to create the socket is made. - break; - } - else - { - rtpSocket?.Close(); - controlSocket?.Close(); - bindAttempts++; + if ((rtpSocket is { } && (!createControlSocket || controlSocket is { })) || (bindPort != 0 && portRange is null)) + { + // If a specific bind port was specified only a single attempt to create the socket is made. + break; + } + else + { + rtpSocket?.Close(); + controlSocket?.Close(); + bindAttempts++; - rtpSocket = null; - controlSocket = null; + rtpSocket = null; + controlSocket = null; - logger.LogWarning("CreateRtpSocket failed to create and bind RTP socket(s) on {bindEP}, bind attempt {bindAttempts}.", bindEP, bindAttempts); - } + logger.LogCreateRtpSocketBindFailed(bindEP, bindAttempts); } + } - if (createControlSocket && rtpSocket != null && controlSocket != null) + if (rtpSocket?.LocalEndPoint is IPEndPoint rtpEndPoint) + { + if (!createControlSocket) { - if (rtpSocket.LocalEndPoint.AddressFamily == AddressFamily.InterNetworkV6) + if (rtpEndPoint.AddressFamily == AddressFamily.InterNetworkV6) { - logger.LogDebug("Successfully bound RTP socket {LocalEndPoint} (dual mode {DualMode}) and control socket {ControlEndPoint} (dual mode {ControlDualMode}).", - rtpSocket.LocalEndPoint, rtpSocket.DualMode, controlSocket.LocalEndPoint, controlSocket.DualMode); + logger.LogCreateRtpSocketSingleSuccessDualMode(rtpEndPoint, rtpSocket.DualMode); } else { - logger.LogDebug("Successfully bound RTP socket {LocalEndPoint} and control socket {ControlEndPoint}.", - rtpSocket.LocalEndPoint, controlSocket.LocalEndPoint); + logger.LogCreateRtpSocketSingleSuccess(rtpEndPoint); } + + return; } - else if (!createControlSocket && rtpSocket != null) + else { - if (rtpSocket.LocalEndPoint.AddressFamily == AddressFamily.InterNetworkV6) - { - logger.LogDebug("Successfully bound RTP socket {LocalEndPoint} (dual mode {DualMode}).", rtpSocket.LocalEndPoint, rtpSocket.DualMode); - } - else + if (controlSocket?.LocalEndPoint is IPEndPoint controlEndPoint) { - logger.LogDebug("Successfully bound RTP socket {LocalEndPoint}.", rtpSocket.LocalEndPoint); + if (rtpEndPoint.AddressFamily == AddressFamily.InterNetworkV6) + { + logger.LogCreateRtpSocketSuccessDualMode(rtpEndPoint, rtpSocket.DualMode, controlEndPoint, controlSocket.DualMode); + } + else + { + logger.LogCreateRtpSocketSuccess(rtpEndPoint, controlEndPoint); + } + + return; } } - else - { - throw new ApplicationException($"Failed to create and bind RTP socket using bind address {bindAddress}:{bindPort}(portRange=[{portRange?.StartPort},{portRange?.EndPort}],useDualMode={useDualMode},requireEvenPort={requireEvenPort},createControlSocket={createControlSocket},protocolType={protocolType})."); - } } - /// - /// Dual mode sockets are created by default if an IPv6 bind address was specified. - /// Dual mode needs to be disabled for Mac OS sockets as they don't support the use - /// of dual mode and the receive methods that return packet information. Packet info - /// is needed to get the remote recipient. - /// - /// True if the underlying OS supports dual mode IPv6 sockets WITH the socket ReceiveFrom methods - /// which are required to get the remote end point. False if not - private static bool DoCheckSupportsDualModeIPv4PacketInfo() + throw new SipSorceryException($"Failed to create and bind RTP socket using bind address {bindAddress}:{bindPort}(portRange=[{portRange?.StartPort},{portRange?.EndPort}],useDualMode={useDualMode},requireEvenPort={requireEvenPort},createControlSocket={createControlSocket},protocolType={protocolType})."); + } + + /// + /// Dual mode sockets are created by default if an IPv6 bind address was specified. + /// Dual mode needs to be disabled for Mac OS sockets as they don't support the use + /// of dual mode and the receive methods that return packet information. Packet info + /// is needed to get the remote recipient. + /// + /// True if the underlying OS supports dual mode IPv6 sockets WITH the socket ReceiveFrom methods + /// which are required to get the remote end point. False if not + private static bool DoCheckSupportsDualModeIPv4PacketInfo() + { + var hasDualModeReceiveSupport = true; + + if (!Socket.OSSupportsIPv6) { - bool hasDualModeReceiveSupport = true; + hasDualModeReceiveSupport = false; + } + else + { + var testSocket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + testSocket.DualMode = true; + + try + { + testSocket.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); + testSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1); + var buf = new byte[1]; + EndPoint remoteEP = new IPEndPoint(IPAddress.IPv6Any, 0); - if (!Socket.OSSupportsIPv6) + testSocket.BeginReceiveFrom(buf, 0, buf.Length, SocketFlags.None, ref remoteEP, (ar) => { try { testSocket.EndReceiveFrom(ar, ref remoteEP); } catch { } }, null); + hasDualModeReceiveSupport = true; + } + catch (PlatformNotSupportedException platExcp) { + logger.LogDualModeSupportCheckFailed(platExcp.Message, platExcp); hasDualModeReceiveSupport = false; } - else + catch (Exception excp) { - var testSocket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); - testSocket.DualMode = true; - - try - { - testSocket.Bind(new IPEndPoint(IPAddress.IPv6Any, 0)); - testSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1); - byte[] buf = new byte[1]; - EndPoint remoteEP = new IPEndPoint(IPAddress.IPv6Any, 0); - - testSocket.BeginReceiveFrom(buf, 0, buf.Length, SocketFlags.None, ref remoteEP, (ar) => { try { testSocket.EndReceiveFrom(ar, ref remoteEP); } catch { } }, null); - hasDualModeReceiveSupport = true; - } - catch (PlatformNotSupportedException platExcp) - { - logger.LogWarning(platExcp, "A socket 'receive from' attempt on a dual mode socket failed (dual mode RTP sockets will not be used) with a platform exception {Message}", platExcp.Message); - hasDualModeReceiveSupport = false; - } - catch (Exception excp) - { - logger.LogWarning(excp, "A socket 'receive from' attempt on a dual mode socket failed (dual mode RTP sockets will not be used) with {Message}", excp.Message); - hasDualModeReceiveSupport = false; - } - finally - { - testSocket.Close(); - } + logger.LogDualModeSupportCheckFailed(excp.Message, excp); + hasDualModeReceiveSupport = false; + } + finally + { + testSocket.Close(); } + } + + return hasDualModeReceiveSupport; + } - return hasDualModeReceiveSupport; + /// + /// This method utilises the OS routing table to determine the local IP address to connect to a destination end point. + /// It selects the correct local IP address, on a potentially multi-honed host, to communicate with a destination IP address. + /// See https://github.com/sipsorcery/sipsorcery/issues/97 for elaboration. + /// + /// The remote destination to find a local IP address for. + /// The local IP address to use to connect to the remote end point. + public static IPAddress? GetLocalAddressForRemote(IPAddress destination) + { + if (destination is null || IPAddress.Any.Equals(destination) || IPAddress.IPv6Any.Equals(destination)) + { + return null; } - /// - /// This method utilises the OS routing table to determine the local IP address to connect to a destination end point. - /// It selects the correct local IP address, on a potentially multi-honed host, to communicate with a destination IP address. - /// See https://github.com/sipsorcery/sipsorcery/issues/97 for elaboration. - /// - /// The remote destination to find a local IP address for. - /// The local IP address to use to connect to the remote end point. - public static IPAddress GetLocalAddressForRemote(IPAddress destination) + if (m_localAddressTable.TryGetValue(destination, out var cachedAddress)) { - if (destination == null || IPAddress.Any.Equals(destination) || IPAddress.IPv6Any.Equals(destination)) + if (DateTime.Now.Subtract(cachedAddress.dateTime).TotalSeconds < LOCAL_ADDRESS_CACHE_LIFETIME_SECONDS) + { + // Cached item is valid, return the value + return cachedAddress.address; + } + else { - return null; + m_localAddressTable.TryRemove(destination, out _); } + } + + var localAddress = default(IPAddress); - if (m_localAddressTable.TryGetValue(destination, out var cachedAddress)) + if (destination.AddressFamily == AddressFamily.InterNetwork || destination.IsIPv4MappedToIPv6) + { + using (var udpClient = new UdpClient()) { - if (DateTime.Now.Subtract(cachedAddress.Item2).TotalSeconds < LOCAL_ADDRESS_CACHE_LIFETIME_SECONDS) + try { - // Cached item is valid, return the value - return cachedAddress.Item1; + udpClient.Connect(destination.MapToIPv4(), NETWORK_TEST_PORT); + localAddress = (udpClient.Client.LocalEndPoint as IPEndPoint)?.Address; } - else + catch (SocketException) { - m_localAddressTable.TryRemove(destination, out _); + // Socket exception is thrown if the OS cannot find a suitable entry in the routing table. } } - - IPAddress localAddress = null; - - if (destination.AddressFamily == AddressFamily.InterNetwork || destination.IsIPv4MappedToIPv6) + } + else + { + using (var udpClient = new UdpClient(AddressFamily.InterNetworkV6)) { - using (UdpClient udpClient = new UdpClient()) + try { - try - { - udpClient.Connect(destination.MapToIPv4(), NETWORK_TEST_PORT); - localAddress = (udpClient.Client.LocalEndPoint as IPEndPoint)?.Address; - } - catch (SocketException) - { - // Socket exception is thrown if the OS cannot find a suitable entry in the routing table. - } + udpClient.Connect(destination, NETWORK_TEST_PORT); + localAddress = (udpClient.Client.LocalEndPoint as IPEndPoint)?.Address; } - } - else - { - using (UdpClient udpClient = new UdpClient(AddressFamily.InterNetworkV6)) + catch (SocketException) { - try - { - udpClient.Connect(destination, NETWORK_TEST_PORT); - localAddress = (udpClient.Client.LocalEndPoint as IPEndPoint)?.Address; - } - catch (SocketException) - { - // Socket exception is thrown if the OS cannot find a suitable entry in the routing table. - } + // Socket exception is thrown if the OS cannot find a suitable entry in the routing table. } - - } - - if (localAddress != null) - { - m_localAddressTable.TryAdd(destination, new Tuple(localAddress, DateTime.Now)); } - return localAddress; } - /// - /// Gets the default local address for this machine for communicating with the Internet. - /// - /// The local address this machine should use for communicating with the Internet. - public static IPAddress GetLocalAddressForInternet() + if (localAddress is { }) { - var internetAddress = IPAddress.Parse(INTERNET_IPADDRESS); - return GetLocalAddressForRemote(internetAddress); + m_localAddressTable.TryAdd(destination, (localAddress, DateTime.Now)); } - /// - /// Gets the default local IPv6 address for this machine for communicating with the Internet. - /// - /// The local address this machine should use for communicating with the Internet. - public static IPAddress GetLocalIPv6AddressForInternet() + return localAddress; + } + + /// + /// Gets the default local address for this machine for communicating with the Internet. + /// + /// The local address this machine should use for communicating with the Internet. + public static IPAddress? GetLocalAddressForInternet() + { + var internetAddress = IPAddress.Parse(INTERNET_IPADDRESS); + return GetLocalAddressForRemote(internetAddress); + } + + /// + /// Gets the default local IPv6 address for this machine for communicating with the Internet. + /// + /// The local address this machine should use for communicating with the Internet. + public static IPAddress? GetLocalIPv6AddressForInternet() + { + var internetAddress = IPAddress.Parse(INTERNET_IPv6ADDRESS); + return GetLocalAddressForRemote(internetAddress); + } + + /// + /// Determines the local IP address to use to connection a remote address and + /// returns all the local addresses (IPv4 and IPv6) that are bound to the same + /// interface. The main (and probably sole) use case for this method is + /// gathering host candidates for a WebRTC ICE session. Rather than selecting + /// ALL local IP addresses only those on the interface needed to connect to + /// the destination are returned. + /// + /// Optional. If not specified the interface that + /// connects to the Internet will be used. + /// By default only the single interface that is used to + /// connect to the destination address (or internet address if it's null) will be + /// used to get the list of IP addresses. This default behaviour is to shield all local + /// IP addresses being included in ICE candidates. In some circumstances, and after + /// weighing up the security concerns, it's very useful to include all interfaces in + /// when generating the address list. Setting this parameter to true will cause all + /// interfaces to be used irrespective of the destination address. + /// A list of local IP addresses on the identified interface(s). + public static IEnumerable GetLocalAddressesOnInterface(IPAddress? destination, bool includeAllInterfaces = false) + { +#if ANDROID + var ipAddresses = GetLocalAddressAndroid(); + if (includeAllInterfaces) { - var internetAddress = IPAddress.Parse(INTERNET_IPv6ADDRESS); - return GetLocalAddressForRemote(internetAddress); + return ipAddresses; } - /// - /// Determines the local IP address to use to connection a remote address and - /// returns all the local addresses (IPv4 and IPv6) that are bound to the same - /// interface. The main (and probably sole) use case for this method is - /// gathering host candidates for a WebRTC ICE session. Rather than selecting - /// ALL local IP addresses only those on the interface needed to connect to - /// the destination are returned. - /// - /// Optional. If not specified the interface that - /// connects to the Internet will be used. - /// By default only the single interface that is used to - /// connect to the destination address (or internet address if it's null) will be - /// used to get the list of IP addresses. This default behaviour is to shield all local - /// IP addresses being included in ICE candidates. In some circumstances, and after - /// weighing up the security concerns, it's very useful to include all interfaces in - /// when generating the address list. Setting this parameter to true will cause all - /// interfaces to be used irrespective of the destination address. - /// A list of local IP addresses on the identified interface(s). - public static List GetLocalAddressesOnInterface(IPAddress destination, bool includeAllInterfaces = false) + return new List() { GetLocalAddressForRemote(destination ?? IPAddress.Parse(INTERNET_IPADDRESS)) }; +#else + var localAddress = GetLocalAddressForRemote(destination ?? IPAddress.Parse(INTERNET_IPADDRESS)); + var adapters = NetworkInterface.GetAllNetworkInterfaces(); + foreach (var n in adapters) { -#if ANDROID - var ipAddresses = GetLocalAddressAndroid(); - if (includeAllInterfaces) + // AC 5 Jun 2020: Network interface status is reported as Unknown on WSL. + if (n.OperationalStatus is OperationalStatus.Up or OperationalStatus.Unknown) { - return ipAddresses; - } - - return new List() { GetLocalAddressForRemote(destination ?? IPAddress.Parse(INTERNET_IPADDRESS)) }; -#else - IPAddress localAddress = GetLocalAddressForRemote(destination ?? IPAddress.Parse(INTERNET_IPADDRESS)); - List localAddresses = new List(); + var ipProps = n.GetIPProperties(); - NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces(); - foreach (NetworkInterface n in adapters) - { - // AC 5 Jun 2020: Network interface status is reported as Unknown on WSL. - if (n.OperationalStatus == OperationalStatus.Up || n.OperationalStatus == OperationalStatus.Unknown) + if (includeAllInterfaces) { - IPInterfaceProperties ipProps = n.GetIPProperties(); - - if (includeAllInterfaces) + foreach (var unicastAddress in ipProps.UnicastAddresses) { - localAddresses.AddRange(ipProps.UnicastAddresses.Select(x => x.Address)); + yield return unicastAddress.Address; } - else if (localAddress == null || ipProps.UnicastAddresses.Any(x => x.Address.Equals(localAddress))) + } + else + { + var hasMatchingAddress = localAddress is null; + if (!hasMatchingAddress) + { + foreach (var unicastAddress in ipProps.UnicastAddresses) + { + if (unicastAddress.Address.Equals(localAddress)) + { + hasMatchingAddress = true; + break; + } + } + } + + if (hasMatchingAddress) { // Use this interface if it has the local IP address for the destination. // If the local address couldn't be determined use the first available interface. - localAddresses.AddRange(ipProps.UnicastAddresses.Select(x => x.Address)); + foreach (var unicastAddress in ipProps.UnicastAddresses) + { + yield return unicastAddress.Address; + } break; } } } - return localAddresses; -#endif } +#endif + } - /// - /// Gets all the IP addresses for all active interfaces on the machine. - /// - /// A list of all local IP addresses. - private static List GetAllLocalIPAddresses() - { + /// + /// Gets all the IP addresses for all active interfaces on the machine. + /// + /// A list of all local IP addresses. + private static List GetAllLocalIPAddresses() + { #if ANDROID - return GetLocalAddressAndroid(); + return GetLocalAddressAndroid(); #else - List localAddresses = new List(); + var localAddresses = new List(); - NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces(); - foreach (NetworkInterface n in adapters) + var adapters = NetworkInterface.GetAllNetworkInterfaces(); + foreach (var n in adapters) + { + // AC 5 Jun 2020: Network interface status is reported as Unknown on WSL. + if (n.OperationalStatus is OperationalStatus.Up or OperationalStatus.Unknown) { - // AC 5 Jun 2020: Network interface status is reported as Unknown on WSL. - if (n.OperationalStatus == OperationalStatus.Up || n.OperationalStatus == OperationalStatus.Unknown) + var ipProps = n.GetIPProperties(); + foreach (var unicastAddr in ipProps.UnicastAddresses) { - IPInterfaceProperties ipProps = n.GetIPProperties(); - foreach (var unicastAddr in ipProps.UnicastAddresses) - { - localAddresses.Add(unicastAddr.Address); - } + localAddresses.Add(unicastAddr.Address); } } + } - return localAddresses; + return localAddresses; #endif - } + } #if ANDROID - public static List GetLocalAddressAndroid() + public static List GetLocalAddressAndroid() + { + var inetEnum = Java.Net.NetworkInterface.NetworkInterfaces; + if (inetEnum is null) + { + return new List(); + } + + var ipAddresses = new List(); + foreach (var interfaces in Java.Util.Collections.List(inetEnum)) { - var inetEnum = Java.Net.NetworkInterface.NetworkInterfaces; - if (inetEnum is null) + var addresses = (interfaces as Java.Net.NetworkInterface)?.InetAddresses; + if (addresses is null) { - return new List(); + continue; } - var ipAddresses = new List(); - foreach (var interfaces in Java.Util.Collections.List(inetEnum)) + foreach (Java.Net.InetAddress address in Java.Util.Collections.List(addresses)) { - var addresses = (interfaces as Java.Net.NetworkInterface)?.InetAddresses; - if (addresses == null) + if (address.HostAddress is null || address.IsLoopbackAddress || !(address is Java.Net.Inet4Address)) { continue; } + ipAddresses.Add(IPAddress.Parse(address.HostAddress)); + } + } + + return ipAddresses; + } +#endif - foreach (Java.Net.InetAddress address in Java.Util.Collections.List(addresses)) + /// + /// Check if the OS has an active IPv6 address configured. + /// + public static bool HasActiveIPv6Address() + { + foreach (var ni in NetworkInterface.GetAllNetworkInterfaces()) + { + if (ni.OperationalStatus == OperationalStatus.Up) + { + var ipProps = ni.GetIPProperties(); + foreach (var addr in ipProps.UnicastAddresses) { - if (address.HostAddress == null || address.IsLoopbackAddress || !(address is Java.Net.Inet4Address)) + if (addr.Address.AddressFamily == AddressFamily.InterNetworkV6) { - continue; + return true; } - ipAddresses.Add(IPAddress.Parse(address.HostAddress)); } } - - return ipAddresses; } -#endif - /// - /// Check if the OS has an active IPv6 address configured. - /// - public static bool HasActiveIPv6Address() - { - return NetworkInterface.GetAllNetworkInterfaces() - .Where(ni => ni.OperationalStatus == OperationalStatus.Up) - .SelectMany(ni => ni.GetIPProperties().UnicastAddresses) - .Any(addr => addr.Address.AddressFamily == AddressFamily.InterNetworkV6); - } + return false; } } diff --git a/src/SIPSorcery/sys/Net/PortRange.cs b/src/SIPSorcery/sys/Net/PortRange.cs index 506ba7fca0..c0cc7bda75 100644 --- a/src/SIPSorcery/sys/Net/PortRange.cs +++ b/src/SIPSorcery/sys/Net/PortRange.cs @@ -16,103 +16,104 @@ // ============================================================================ using System; +using System.Diagnostics; using System.Net; -namespace SIPSorcery.Sys +namespace SIPSorcery.Sys; + +/// +/// Class to manage port allocation for Rtp Ports. Ports are always even, because +/// due Rtp data and control ports are always with data on even port and +/// control (if any) on data_port + 1 +/// +/// There are two operation modes: +/// - The sequential mode which hands out all ports within the assigned range and +/// wraps around if the last port was assigned +/// - The shuffled mode: Ports are handed out evenly distributed within the assigned +/// port range. +/// +public class PortRange { + private readonly Random? m_random; + private readonly bool m_shuffle; + public readonly int StartPort; + public readonly int EndPort; + private int m_nextPort; + /// - /// Class to manage port allocation for Rtp Ports. Ports are always even, because - /// due Rtp data and control ports are always with data on even port and - /// control (if any) on data_port + 1 - /// - /// There are two operation modes: - /// - The sequential mode which hands out all ports within the assigned range and - /// wraps around if the last port was assigned - /// - The shuffled mode: Ports are handed out evenly distributed within the assigned - /// port range. + /// Initializes a new PortRange. /// - public class PortRange + /// Inclusive, lowest port within this portrange. must be an even number + /// Inclusive, highest port within this portrange. + /// optional, if set, the ports are assigned in a pseudorandom order. + /// optional, the seed for the pseudorandom order. + /// + public PortRange(int startPort, int endPort, bool shuffle = false, int? randomSeed = null) { - private readonly Random m_random; - private readonly bool m_shuffle; - public readonly int StartPort; - public readonly int EndPort; - private int m_nextPort; + if (startPort is <= 0 or > IPEndPoint.MaxPort) + { + throw new ArgumentException($"startPort must be greater than 0 and less than or equal {IPEndPoint.MaxPort}"); + } + if (endPort is <= 0 or > IPEndPoint.MaxPort) + { + throw new ArgumentException($"endPort must be greater than 0 and less than or equal {IPEndPoint.MaxPort}"); + } + if (endPort - startPort < 2) + { + throw new ArgumentException($"endPort({endPort}) - startPort({startPort}) must be at least 2, but is {endPort - startPort}"); + } + if (startPort % 2 == 1) + { + throw new ArgumentException("startPort must be even"); + } + if (endPort % 2 == 0) + { + endPort -= 1;// correct end-port to odd if even -> RtpPort are always handed out in pairs + } + StartPort = startPort; + EndPort = endPort; + m_shuffle = shuffle; + if (shuffle) + { + m_random = randomSeed.HasValue ? new Random(randomSeed.Value) : new Random(); + m_nextPort = m_random.Next(StartPort, EndPort + 1) // The + 1 is needed to get an even distribution because Random.Next(start, end) is inclusive start but exclusive the end + & 0x0000_FFFE; // AND with IPEndPoint.MaxPort but last bit is set to zero to always have an even port + } + else + { + m_nextPort = startPort; + } + } - /// - /// Initializes a new PortRange. - /// - /// Inclusive, lowest port within this portrange. must be an even number - /// Inclusive, highest port within this portrange. - /// optional, if set, the ports are assigned in a pseudorandom order. - /// optional, the seed for the pseudorandom order. - /// - public PortRange(int startPort, int endPort, bool shuffle = false, int? randomSeed = null) + /// + /// Calculates the next port which should be tried. + /// No guarantee is made, that the returned port can also be bound to; actual check is still needed. + /// Caller of this method should try to bind to the socket and if not successful, try again for x times + /// before giving up. + /// + /// This method is thread-safe + /// + /// port from the portrange + public virtual int GetNextPort() + { + lock (this) { - if (startPort <= 0 || startPort > IPEndPoint.MaxPort) - { - throw new ArgumentException($"startPort must be greater than 0 and less than or equal {IPEndPoint.MaxPort}"); - } - if (endPort <= 0 || endPort > IPEndPoint.MaxPort) - { - throw new ArgumentException($"endPort must be greater than 0 and less than or equal {IPEndPoint.MaxPort}"); - } - if (endPort - startPort < 2) - { - throw new ArgumentException($"endPort({endPort}) - startPort({startPort}) must be at least 2, but is {endPort - startPort}"); - } - if (startPort % 2 == 1) - { - throw new ArgumentException("startPort must be even"); - } - if (endPort % 2 == 0) + var res = m_nextPort; + if (m_shuffle) { - endPort -= 1;// correct end-port to odd if even -> RtpPort are always handed out in pairs - } - StartPort = startPort; - EndPort = endPort; - m_shuffle = shuffle; - if (shuffle) - { - m_random = randomSeed.HasValue ? new Random(randomSeed.Value) : new Random(); + Debug.Assert(m_random is { }); m_nextPort = m_random.Next(StartPort, EndPort + 1) // The + 1 is needed to get an even distribution because Random.Next(start, end) is inclusive start but exclusive the end & 0x0000_FFFE; // AND with IPEndPoint.MaxPort but last bit is set to zero to always have an even port } else { - m_nextPort = startPort; - } - } - - /// - /// Calculates the next port which should be tried. - /// No guarantee is made, that the returned port can also be bound to; actual check is still needed. - /// Caller of this method should try to bind to the socket and if not successful, try again for x times - /// before giving up. - /// - /// This method is thread-safe - /// - /// port from the portrange - public virtual int GetNextPort() - { - lock (this) - { - var res = m_nextPort; - if (m_shuffle) - { - m_nextPort = m_random.Next(StartPort, EndPort + 1) // The + 1 is needed to get an even distribution because Random.Next(start, end) is inclusive start but exclusive the end - & 0x0000_FFFE; // AND with IPEndPoint.MaxPort but last bit is set to zero to always have an even port - } - else + m_nextPort = m_nextPort + 2; + if (m_nextPort > EndPort) { - m_nextPort = m_nextPort + 2; - if (m_nextPort > EndPort) - { - m_nextPort = StartPort; - } + m_nextPort = StartPort; } - return res; } + return res; } } } diff --git a/src/SIPSorcery/sys/Net/SocketConnection.cs b/src/SIPSorcery/sys/Net/SocketConnection.cs new file mode 100644 index 0000000000..937e527bd9 --- /dev/null +++ b/src/SIPSorcery/sys/Net/SocketConnection.cs @@ -0,0 +1,180 @@ +//----------------------------------------------------------------------------- +// Filename: RTPChannel.cs +// +// Description: Communications channel to send and receive RTP and RTCP packets +// and whatever else happens to be multiplexed. +// +// Author(s): +// Aaron Clauson (aaron@sipsorcery.com) +// +// History: +// 27 Feb 2012 Aaron Clauson Created, Hobart, Australia. +// 06 Dec 2019 Aaron Clauson Simplify by removing all frame logic and reduce responsibility +// to only managing sending and receiving of packets. +// 28 Dec 2019 Aaron Clauson Added RTCP reporting as per RFC3550. +// +// License: +// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. +//----------------------------------------------------------------------------- + +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SIPSorcery.Net; + +namespace SIPSorcery.Sys; + +public abstract class SocketConnection +{ + /// + /// MTU is 1452 bytes so this should be heaps [AC 03 Nov 2024: turns out it's not when considering UDP fragmentation can + /// result in a max UDP payload of 65535 - 8 (header) = 65527 bytes]. + /// An issue was reported with a real World WeBRTC implementation producing UDP packet sizes of 2144 byes #1045. Consequently + /// updated from 2048 to 3000. + /// + protected const int RECEIVE_BUFFER_SIZE = 3000; + + protected static ILogger logger = LogFactory.CreateLogger(); + + protected SocketConnection(Socket socket, int mtu = RECEIVE_BUFFER_SIZE) + { + Mtu = mtu; + + Socket = socket; + if (Socket.LocalEndPoint is not IPEndPoint localEndPoint) + { + throw new InvalidOperationException($"The socket is required to have a LocalEndPoint of type IPEndpoint and it was {(Socket.LocalEndPoint is null ? "" : Socket.LocalEndPoint.GetType().FullName)}"); + } + + LocalEndPoint = localEndPoint; + AddressFamily = localEndPoint.AddressFamily; + } + + public Socket Socket { get; } + + public virtual bool IsClosed => CancellationTokenSource.IsCancellationRequested; + + public virtual bool IsRunningReceive { get; protected set; } + + protected virtual bool IsClosing { get; set; } + + /// + /// Returns true if the RTP socket supports dual mode IPv4 and IPv6. If the control + /// socket exists it will be the same. + /// + public bool IsDualMode => Socket is { AddressFamily: AddressFamily.InterNetworkV6 } && Socket.DualMode; + + protected int Mtu { get; } + + protected IPEndPoint LocalEndPoint { get; } + + protected AddressFamily AddressFamily { get; } + + protected CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource(); + + /// + /// Fires when a new packet has been received on the UDP socket. + /// + public event Action>? OnPacketReceived; + + /// + /// Fires when there is an error attempting to receive on the UDP socket. + /// + public event Action? OnClosed; + + public abstract void BeginReceiveFrom(); + + public SocketError SendTo(IPEndPoint dstEndPoint, ReadOnlyMemory buffer, IDisposable? memoryOwner) + { + if (IsClosed) + { + return SocketError.Disconnecting; + } + else if (dstEndPoint is null) + { + throw new ArgumentException("An empty destination was specified to Send in RTP connection.", nameof(dstEndPoint)); + } + else if (buffer.IsEmpty) + { + throw new ArgumentException("The buffer must be set and non empty for Send in RTP connection.", nameof(buffer)); + } + else if (IPAddress.Any.Equals(dstEndPoint.Address) || IPAddress.IPv6Any.Equals(dstEndPoint.Address)) + { + logger.LogRtpDestinationAddressInvalid(dstEndPoint.Address); + return SocketError.DestinationAddressRequired; + } + + //Prevent Send to IPV4 while socket is IPV6 (Mono Error) + if (dstEndPoint.AddressFamily == AddressFamily.InterNetwork && Socket.AddressFamily != dstEndPoint.AddressFamily) + { + dstEndPoint = new IPEndPoint(dstEndPoint.Address.MapToIPv6(), dstEndPoint.Port); + } + + try + { + _ = SendToCoreAsync(dstEndPoint, buffer, memoryOwner); + + BeginReceiveFrom(); + + return SocketError.Success; + } + catch (OperationCanceledException) when (CancellationTokenSource.IsCancellationRequested) + { + // Ignore cancelled operations when the connection is closed. + return SocketError.Disconnecting; + } + catch (ObjectDisposedException) + { + // Thrown when socket is closed. Can be safely ignored. + return SocketError.Disconnecting; + } + catch (SocketException sockExcp) + { + // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during + // normal RTP operation. For example: + // - the RTP connection may start sending before the remote socket starts listening, + // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old + // or new socket during the transition. + logger.LogRtpChannelSocketError(sockExcp.SocketErrorCode); + return sockExcp.SocketErrorCode; + } + catch (Exception excp) + { + logger.LogRtpChannelGeneralException(excp); + return SocketError.Fault; + } + } + + protected abstract Task SendToCoreAsync(IPEndPoint dstEndPoint, ReadOnlyMemory buffer, IDisposable? memoryOwner); + + /// + /// Closes the socket and stops any new receives from being initiated. + /// + public virtual void Close(string? reason) + { + if (!IsClosed) + { + CancellationTokenSource.Cancel(); + + if (Socket is { }) + { + if (Socket.Connected) + { + Socket.Disconnect(false); + } + + Socket.Close(); + } + + OnClosed?.Invoke(reason); + } + } + + protected virtual void CallOnPacketReceivedCallback(int localPort, IPEndPoint? remoteEndPoint, ReadOnlyMemory packet) + { + OnPacketReceived?.Invoke(this, localPort, remoteEndPoint, packet); + } +} diff --git a/src/SIPSorcery/sys/Net/SocketTcpConnection.cs b/src/SIPSorcery/sys/Net/SocketTcpConnection.cs new file mode 100644 index 0000000000..eb7aad8149 --- /dev/null +++ b/src/SIPSorcery/sys/Net/SocketTcpConnection.cs @@ -0,0 +1,222 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using SIPSorcery.Net; + +namespace SIPSorcery.Sys; + +public class SocketTcpConnection : SocketConnection +{ + private readonly Pipe m_pipe = new(new PipeOptions(useSynchronizationContext: false)); + + public SocketTcpConnection(Socket socket, int mtu = RECEIVE_BUFFER_SIZE) : base(socket, mtu) + { + } + + public override void BeginReceiveFrom() + { + if (IsClosed || IsClosing || !Socket.Connected) + { + return; + } + + if (IsRunningReceive) + { + return; + } + + IsRunningReceive = true; + + try + { + _ = BeginReceiveFromCoreAsync(); + + async Task BeginReceiveFromCoreAsync() + { + try + { + while (!IsClosed) + { + try + { + var buffer = m_pipe.Writer.GetMemory(Mtu); + + var bytesRead = await ReadBytesAsync(buffer).ConfigureAwait(false); + + if (bytesRead > 0) + { + m_pipe.Writer.Advance(bytesRead); + await m_pipe.Writer.FlushAsync().ConfigureAwait(false); + + var localEndPoint = Socket.LocalEndPoint as IPEndPoint; + var remoteEndPoint = Socket.RemoteEndPoint as IPEndPoint; + Debug.Assert(localEndPoint is { }); + Debug.Assert(remoteEndPoint is { }); + ProcessRawBuffer(m_pipe.Reader, localEndPoint, remoteEndPoint); + } + } + catch (OperationCanceledException) when (CancellationTokenSource.IsCancellationRequested) + { + // Ignore cancelled operations when the connection is closed. + } + catch (IOException ex) when (ex.InnerException is SocketException sockEx) + { + logger.LogIceSocketWarning(sockEx.SocketErrorCode, sockEx.Message, sockEx); + } + catch (Exception excp) + { + logger.LogIceSocketReceiveError(excp.Message, excp); + Close(excp.Message); + break; + } + } + } + finally + { + IsRunningReceive = false; + } + } + } + catch (OperationCanceledException) when (CancellationTokenSource.IsCancellationRequested) + { + // Ignore cancelled operations when the connection is closed. + } + catch (Exception excp) + { + Close(excp.Message); + } + } + + protected override async Task SendToCoreAsync(IPEndPoint dstEndPoint, ReadOnlyMemory buffer, IDisposable? memoryOwner) + { + if (!Socket.Connected + || Socket.RemoteEndPoint is not IPEndPoint remoteEndPoint + || remoteEndPoint.Port != dstEndPoint.Port + || !remoteEndPoint.Address.Equals(dstEndPoint.Address)) + { + if (Socket.Connected) + { + logger.LogTcpDisconnectRequest(); + await Socket.DisconnectAsync(true).ConfigureAwait(false); + } + + await Socket.ConnectAsync(dstEndPoint).ConfigureAwait(false); + + logger.LogTcpSendStatus(Socket.Connected, dstEndPoint); + } + + try + { + await Socket.SendToAsync(buffer, SocketFlags.None, dstEndPoint).ConfigureAwait(false); + } + catch (OperationCanceledException) when (CancellationTokenSource.IsCancellationRequested) + { + // Ignore cancelled operations when the connection is closed. + } + catch (ObjectDisposedException) + { + // Thrown when socket is closed. Can be safely ignored. + } + catch (SocketException ex) + { + // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during + // normal RTP operation. For example: + // - the RTP connection may start sending before the remote socket starts listening, + // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old + // or new socket during the transition. + logger.LogRtpChannelSocketError(ex.SocketErrorCode); + } + catch (Exception ex) + { + logger.LogRtpChannelGeneralException(ex); + } + finally + { + memoryOwner?.Dispose(); + } + } + + protected virtual async Task ReadBytesAsync(Memory buffer) + { + Debug.Assert(Socket.RemoteEndPoint is { }); + + var result = await Socket.ReceiveFromAsync( + buffer, + SocketFlags.None, + Socket.RemoteEndPoint, + CancellationTokenSource.Token).ConfigureAwait(false); + + var bytesRead = result.ReceivedBytes; + return bytesRead; + } + + protected void ProcessRawBuffer(PipeReader reader, IPEndPoint localEndPoint, IPEndPoint? remoteEndPoint) + { + var rentedBuffer = default(byte[]); + + try + { + Span stunMessageLengthBytes = stackalloc byte[2]; + + while (reader.TryRead(out var readResult) && readResult.Buffer.Length > STUNHeader.STUN_HEADER_LENGTH) + { + // TODO: If we miss any package because slow internet connection + // and initial byte in buffer is not a STUNHeader (starts with 0x00 0x00) + // and our receive buffer is full, we need a way to discard whole buffer + // or check for 0x00 0x00 start again. + + readResult.Buffer.Slice(2, 2).CopyTo(stunMessageLengthBytes); + var stunMessageLength = BinaryPrimitives.ReadUInt16BigEndian(stunMessageLengthBytes); + + var stunMsgBytes = (STUNHeader.STUN_HEADER_LENGTH + stunMessageLength + 3) & ~3; + + //We have the packet count all inside current receiving buffer + if (readResult.Buffer.Length >= stunMsgBytes) + { + var messageSequence = readResult.Buffer.Slice(0, stunMsgBytes); + var stunMessageBuffer = ReadBuffer(messageSequence, ref rentedBuffer); + + CallOnPacketReceivedCallback(localEndPoint.Port, remoteEndPoint, stunMessageBuffer); + + reader.AdvanceTo(messageSequence.End); + } + + static ReadOnlyMemory ReadBuffer(ReadOnlySequence buffer, ref byte[]? rentedBuffer) + { + if (buffer.IsSingleSegment) + { + return buffer.First.Slice(0, (int)buffer.Length); + } + + if (rentedBuffer is { } && rentedBuffer.Length < buffer.Length) + { + ArrayPool.Shared.Return(rentedBuffer); + rentedBuffer = null; + } + + if (rentedBuffer is null) + { + rentedBuffer = ArrayPool.Shared.Rent((int)buffer.Length); + } + + buffer.CopyTo(rentedBuffer); + + return rentedBuffer.AsMemory(0, (int)buffer.Length); + } + } + } + finally + { + if (rentedBuffer is { }) + { + ArrayPool.Shared.Return(rentedBuffer); + } + } + } +} diff --git a/src/SIPSorcery/sys/Net/SocketTlsConnection.cs b/src/SIPSorcery/sys/Net/SocketTlsConnection.cs new file mode 100644 index 0000000000..8bc5fe46db --- /dev/null +++ b/src/SIPSorcery/sys/Net/SocketTlsConnection.cs @@ -0,0 +1,112 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using SIPSorcery.Net; + +namespace SIPSorcery.Sys; + +internal sealed class SocketTlsConnection : SocketTcpConnection +{ + private SslStream? m_sslStream; + private readonly SslClientAuthenticationOptions m_sslClientAuthenticationOptions; + private readonly SemaphoreSlim m_sslStreamLock = new(1); + + public SocketTlsConnection(Socket socket, string targetHost, SslClientAuthenticationOptions? sslClientAuthenticationOptions, int mtu = RECEIVE_BUFFER_SIZE) : base(socket, mtu) + { + m_sslClientAuthenticationOptions = SslClientAuthenticationOptions.CreateFrom(sslClientAuthenticationOptions); + m_sslClientAuthenticationOptions.TargetHost ??= targetHost; + } + + protected override async Task SendToCoreAsync(IPEndPoint dstEndPoint, ReadOnlyMemory buffer, IDisposable? memoryOwner) + { + await m_sslStreamLock.WaitAsync().ConfigureAwait(false); + + try + { + if (m_sslStream is null + || !Socket.Connected + || Socket.RemoteEndPoint is not IPEndPoint remoteEndPoint + || remoteEndPoint.Port != dstEndPoint.Port + || !remoteEndPoint.Address.Equals(dstEndPoint.Address)) + { + if (m_sslStream is { }) + { + await m_sslStream.DisposeAsync().ConfigureAwait(false); + } + + if (Socket.Connected) + { + logger.LogTcpDisconnectRequest(); + await Socket.DisconnectAsync(true, CancellationTokenSource.Token).ConfigureAwait(false); + } + + await Socket.ConnectAsync(dstEndPoint, CancellationTokenSource.Token).ConfigureAwait(false); + + logger.LogTcpSendStatus(Socket.Connected, dstEndPoint); + + m_sslStream = new SslStream(new NetworkStream(Socket, ownsSocket: false), leaveInnerStreamOpen: false); + + await m_sslStream.AuthenticateAsClientAsync(m_sslClientAuthenticationOptions, CancellationTokenSource.Token).ConfigureAwait(false); + } + + Debug.Assert(m_sslStream is { }); + + await m_sslStream.WriteAsync(buffer, CancellationTokenSource.Token).ConfigureAwait(false); + await m_sslStream.FlushAsync(CancellationTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (CancellationTokenSource.IsCancellationRequested) + { + // Ignore cancelled operations when the connection is closed. + } + catch (IOException ex) when (ex.InnerException is SocketException sockEx) + { + // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during + // normal RTP operation. For example: + // - the RTP connection may start sending before the remote socket starts listening, + // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old + // or new socket during the transition. + logger.LogRtpChannelSocketError(sockEx.SocketErrorCode); + } + catch (Exception ex) + { + logger.LogRtpChannelGeneralException(ex); + } + finally + { + m_sslStreamLock.Release(); + memoryOwner?.Dispose(); + } + } + + protected override async Task ReadBytesAsync(Memory buffer) + { + await m_sslStreamLock.WaitAsync(CancellationTokenSource.Token).ConfigureAwait(false); + + try + { + Debug.Assert(m_sslStream is { }); + + return await m_sslStream.ReadAsync(buffer, CancellationTokenSource.Token).ConfigureAwait(false); + } + finally + { + m_sslStreamLock.Release(); + } + } + + public override void Close(string? reason) + { + if (!IsClosed) + { + m_sslStream?.Dispose(); + m_sslStream = null; + + base.Close(reason); + } + } +} diff --git a/src/SIPSorcery/sys/Net/SocketUdpConnection.cs b/src/SIPSorcery/sys/Net/SocketUdpConnection.cs new file mode 100644 index 0000000000..68d4277b56 --- /dev/null +++ b/src/SIPSorcery/sys/Net/SocketUdpConnection.cs @@ -0,0 +1,173 @@ +//----------------------------------------------------------------------------- +// Filename: RTPChannel.cs +// +// Description: Communications channel to send and receive RTP and RTCP packets +// and whatever else happens to be multiplexed. +// +// Author(s): +// Aaron Clauson (aaron@sipsorcery.com) +// +// History: +// 27 Feb 2012 Aaron Clauson Created, Hobart, Australia. +// 06 Dec 2019 Aaron Clauson Simplify by removing all frame logic and reduce responsibility +// to only managing sending and receiving of packets. +// 28 Dec 2019 Aaron Clauson Added RTCP reporting as per RFC3550. +// +// License: +// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. +//----------------------------------------------------------------------------- + +using System; +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using SIPSorcery.Net; + +namespace SIPSorcery.Sys; + +/// +/// A basic UDP socket manager. The RTP channel may need both an RTP and Control socket. This class encapsulates +/// the common logic for UDP socket management. +/// +/// +/// .NET Framework Socket source: +/// https://referencesource.microsoft.com/#system/net/system/net/Sockets/Socket.cs +/// .NET Core Socket source: +/// https://github.com/dotnet/runtime/blob/master/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs +/// Mono Socket source: +/// https://github.com/mono/mono/blob/master/mcs/class/System/System.Net.Sockets/Socket.cs +/// +public class SocketUdpConnection : SocketConnection +{ + public SocketUdpConnection(Socket socket, int mtu = RECEIVE_BUFFER_SIZE) : base(socket, mtu) + { + } + + protected override async Task SendToCoreAsync(IPEndPoint dstEndPoint, ReadOnlyMemory buffer, IDisposable? memoryOwner) + { + try + { + await Socket.SendToAsync(buffer, SocketFlags.None, dstEndPoint).ConfigureAwait(false); + } + catch (OperationCanceledException) when (CancellationTokenSource.IsCancellationRequested) + { + // Ignore cancelled operations when the connection is closed. + } + catch (ObjectDisposedException) + { + // Thrown when socket is closed. Can be safely ignored. + } + catch (SocketException sockExcp) + { + // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during + // normal RTP operation. For example: + // - the RTP connection may start sending before the remote socket starts listening, + // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old + // or new socket during the transition. + logger.LogRtpChannelSocketError(sockExcp.SocketErrorCode); + } + catch (Exception excp) + { + logger.LogRtpChannelGeneralException(excp); + } + finally + { + memoryOwner?.Dispose(); + } + } + + /// + /// Starts the receive. This method returns immediately. An event will be fired in the corresponding "End" event to + /// return any data received. + /// + public override void BeginReceiveFrom() + { + //Prevent call BeginReceiveFrom if it is already running + if (IsRunningReceive || IsClosed || IsClosing) + { + return; + } + + IsRunningReceive = true; + + _ = BeginReceiveFromCoreAsync(); + + async Task BeginReceiveFromCoreAsync() + { + var buffer = ArrayPool.Shared.Rent(Mtu); + + try + { + while (!IsClosed) + { + try + { + EndPoint remoteEP = AddressFamily == AddressFamily.InterNetwork + ? new IPEndPoint(IPAddress.Any, 0) + : new IPEndPoint(IPAddress.IPv6Any, 0); + + var result = await Socket.ReceiveFromAsync( + buffer, + SocketFlags.None, + remoteEP, + CancellationTokenSource.Token + ).ConfigureAwait(false); + + if (result.ReceivedBytes > 0) + { + CallOnPacketReceivedCallback( + LocalEndPoint.Port, + result.RemoteEndPoint as IPEndPoint, + buffer.AsMemory(0, result.ReceivedBytes) + ); + } + } + catch (OperationCanceledException) when (CancellationTokenSource.IsCancellationRequested) + { + // Ignore cancelled operations when the connection is closed. + } + catch (ObjectDisposedException) + { + // Thrown when socket is closed. Can be safely ignored. + } + catch (SocketException sockExcp) + { + // This exception can be thrown in response to an ICMP packet. The problem is the ICMP packet can be a false positive. + // For example if the remote RTP socket has not yet been opened the remote host could generate an ICMP packet for the + // initial RTP packets. Experience has shown that it's not safe to close an RTP connection based solely on ICMP packets. + + // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during + // normal RTP operation. For example: + // - the RTP connection may start sending before the remote socket starts listening, + // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old + // or new socket during the transition. + // It also seems that once a UDP socket pair have exchanged packets and the remote party closes the socket exception will occur + // in the BeginReceive method (very handy). Follow-up, this doesn't seem to be the case, the socket exception can occur in + // BeginReceive before any packets have been exchanged. This means it's not safe to close if BeginReceive gets an ICMP + // error since the remote party may not have initialised their socket yet. + + logger.LogRtpSocketException(sockExcp.SocketErrorCode, sockExcp.Message); + } + catch (Exception excp) + { + // From https://github.com/dotnet/corefx/blob/e99ec129cfd594d53f4390bf97d1d736cff6f860/src/System.Net.Sockets/src/System/Net/Sockets/Socket.cs#L3262 + // the BeginReceiveFrom will only throw if there is an problem with the arguments or the socket has been disposed of. In that + // case the socket can be considered to be unusable and there's no point trying another receive. + + logger.LogRtpChannelBeginReceiveError(excp.Message, excp); + + IsRunningReceive = false; + + Close(excp.Message); + } + } + } + finally + { + IsRunningReceive = false; + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/src/SIPSorcery/sys/Net/SysNetLoggingExtensions.cs b/src/SIPSorcery/sys/Net/SysNetLoggingExtensions.cs new file mode 100644 index 0000000000..65be4c7bef --- /dev/null +++ b/src/SIPSorcery/sys/Net/SysNetLoggingExtensions.cs @@ -0,0 +1,204 @@ +using System; +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; + +namespace SIPSorcery.Sys; + +internal static partial class SysNetLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "CreateBoundSocketStart", + Level = LogLevel.Debug, + Message = "CreateBoundSocket attempting to create and bind socket(s) on {BindEndPoint} using protocol {Protocol}.")] + public static partial void LogCreateBoundSocketStart( + this ILogger logger, + EndPoint? bindEndPoint, + ProtocolType protocol); + + [LoggerMessage( + EventId = 0, + EventName = "CreateBoundSocketEvenPortClose", + Level = LogLevel.Debug, + Message = "CreateBoundSocket even port required, closing socket on {LocalEndPoint}, max port reached request new bind.")] + public static partial void LogCreateBoundSocketEvenPortClose( + this ILogger logger, + EndPoint? localEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "CreateBoundSocketEvenPortRetry", + Level = LogLevel.Debug, + Message = "CreateBoundSocket even port required, closing socket on {LocalEndPoint} and retrying on {NextPort}.")] + public static partial void LogCreateBoundSocketEvenPortRetry( + this ILogger logger, + EndPoint? localEndPoint, + int nextPort); + + [LoggerMessage( + EventId = 0, + EventName = "CreateBoundSocketSuccessDualMode", + Level = LogLevel.Debug, + Message = "CreateBoundSocket successfully bound on {LocalEndPoint}, dual mode {DualMode}.")] + public static partial void LogCreateBoundSocketSuccessDualMode( + this ILogger logger, + EndPoint? localEndPoint, + bool dualMode); + + [LoggerMessage( + EventId = 0, + EventName = "CreateBoundSocketSuccess", + Level = LogLevel.Debug, + Message = "CreateBoundSocket successfully bound on {LocalEndPoint}.")] + public static partial void LogCreateBoundSocketSuccess( + this ILogger logger, + EndPoint? localEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "WSLBindCheck", + Level = LogLevel.Debug, + Message = "WSL detected, carrying out bind check on 0.0.0.0:{Port}.")] + public static partial void LogWSLBindCheck( + this ILogger logger, + int port); + + [LoggerMessage( + EventId = 0, + EventName = "CreateRtpSocketStart", + Level = LogLevel.Debug, + Message = "CreateRtpSocket attempting to create and bind RTP socket(s) on {BindEndPoint}.")] + public static partial void LogCreateRtpSocketStart( + this ILogger logger, + IPEndPoint bindEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "CreateRtpSocketBindFailed", + Level = LogLevel.Warning, + Message = "CreateRtpSocket failed to create and bind RTP socket(s) on {BindEndPoint}, bind attempt {BindAttempt}.")] + public static partial void LogCreateRtpSocketBindFailed( + this ILogger logger, + IPEndPoint bindEndPoint, + int bindAttempt); + + [LoggerMessage( + EventId = 0, + EventName = "CreateRtpSocketSuccessDualMode", + Level = LogLevel.Debug, + Message = "Successfully bound RTP socket {LocalEndPoint} (dual mode {DualMode}) and control socket {ControlEndPoint} (dual mode {ControlDualMode}).")] + public static partial void LogCreateRtpSocketSuccessDualMode( + this ILogger logger, + EndPoint localEndPoint, + bool dualMode, + EndPoint controlEndPoint, + bool controlDualMode); + + [LoggerMessage( + EventId = 0, + EventName = "CreateRtpSocketSuccess", + Level = LogLevel.Debug, + Message = "Successfully bound RTP socket {LocalEndPoint} and control socket {ControlEndPoint}.")] + public static partial void LogCreateRtpSocketSuccess( + this ILogger logger, + EndPoint localEndPoint, + EndPoint controlEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "CreateRtpSocketSingleSuccessDualMode", + Level = LogLevel.Debug, + Message = "Successfully bound RTP socket {LocalEndPoint} (dual mode {DualMode}).")] + public static partial void LogCreateRtpSocketSingleSuccessDualMode( + this ILogger logger, + EndPoint localEndPoint, + bool dualMode); + + [LoggerMessage( + EventId = 0, + EventName = "CreateRtpSocketSingleSuccess", + Level = LogLevel.Debug, + Message = "Successfully bound RTP socket {LocalEndPoint}.")] + public static partial void LogCreateRtpSocketSingleSuccess( + this ILogger logger, + EndPoint localEndPoint); + + [LoggerMessage( + EventId = 0, + EventName = "DualModeSupportCheckFailed", + Level = LogLevel.Warning, + Message = "A socket 'receive from' attempt on a dual mode socket failed (dual mode RTP sockets will not be used) with {Message}")] + public static partial void LogDualModeSupportCheckFailed( + this ILogger logger, + string message, + Exception ex); + + [LoggerMessage( + EventId = 0, + EventName = "SocketBindAddressInUse", + Level = LogLevel.Warning, + Message = "Address already in use exception attempting to bind socket, attempt {bindAttempts}.")] + public static partial void LogSocketBindAddressInUse( + this ILogger logger, + int bindAttempts); + + [LoggerMessage( + EventId = 0, + EventName = "SocketBindAccessDenied", + Level = LogLevel.Warning, + Message = "Access denied exception attempting to bind socket, attempt {bindAttempts}.")] + public static partial void LogSocketBindAccessDenied( + this ILogger logger, + int bindAttempts); + + [LoggerMessage( + EventId = 0, + EventName = "SocketBindException", + Level = LogLevel.Error, + Message = "SocketException in NetServices.CreateBoundSocket. {errorMessage}")] + public static partial void LogSocketBindException( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "SocketBindInitialException", + Level = LogLevel.Error, + Message = "Exception in NetServices.CreateBoundSocket attempting the initial socket bind on address {bindAddress}.")] + public static partial void LogSocketBindInitialException( + this ILogger logger, + IPAddress bindAddress, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "UdpConnectionResetDisableFailed", + Level = LogLevel.Warning, + Message = "CreateBoundSocket was unable to disable UDP connection reset handling on {LogEndPoint}. Continuing with bound socket.")] + public static partial void LogUdpConnectionResetDisableFailed( + this ILogger logger, + EndPoint logEndPoint, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "UdpConnectionResetDisableNotSupported", + Level = LogLevel.Warning, + Message = "CreateBoundSocket does not support disabling UDP connection reset handling on {LogEndPoint}. Continuing with bound socket.")] + public static partial void LogUdpConnectionResetDisableNotSupported( + this ILogger logger, + EndPoint logEndPoint, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "NetworkCertificateError", + Level = LogLevel.Error, + Message = "Exception loading network certificate. {ErrorMessage}")] + public static partial void LogNetworkCertificateError( + this ILogger logger, + string errorMessage, + Exception exception); +} diff --git a/src/SIPSorcery/net/RTP/UdpReceiver.cs b/src/SIPSorcery/sys/Net/UdpReceiver.cs similarity index 92% rename from src/SIPSorcery/net/RTP/UdpReceiver.cs rename to src/SIPSorcery/sys/Net/UdpReceiver.cs index ec4a33fc13..a3ca56f70f 100644 --- a/src/SIPSorcery/net/RTP/UdpReceiver.cs +++ b/src/SIPSorcery/sys/Net/UdpReceiver.cs @@ -15,6 +15,7 @@ //----------------------------------------------------------------------------- using System; +using System.Diagnostics; using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Logging; @@ -46,13 +47,13 @@ public class UdpReceiver ///
protected const int RECEIVE_BUFFER_SIZE = 3000; - protected static readonly ILogger logger = LogFactory.CreateLogger(); + protected static ILogger logger = LogFactory.CreateLogger(); protected readonly Socket m_socket; protected byte[] m_recvBuffer; protected bool m_isClosed; protected bool m_isRunningReceive; - protected IPEndPoint m_localEndPoint; + protected IPEndPoint? m_localEndPoint; protected AddressFamily m_addressFamily; public virtual bool IsClosed @@ -90,18 +91,20 @@ protected set /// /// Fires when a new packet has been received on the UDP socket. /// - public event PacketReceivedDelegate OnPacketReceived; + public event PacketReceivedDelegate? OnPacketReceived; /// /// Fires when there is an error attempting to receive on the UDP socket. /// - public event Action OnClosed; + public event Action? OnClosed; public UdpReceiver(Socket socket, int mtu = RECEIVE_BUFFER_SIZE) { m_socket = socket; - m_localEndPoint = m_socket.LocalEndPoint as IPEndPoint; + Debug.Assert(m_socket is not null); + m_localEndPoint = (IPEndPoint?)m_socket.LocalEndPoint; m_recvBuffer = new byte[mtu]; + Debug.Assert(m_socket.LocalEndPoint is not null); m_addressFamily = m_socket.LocalEndPoint.AddressFamily; } @@ -180,7 +183,11 @@ protected virtual void EndReceiveFrom(IAsyncResult ar) byte[] packetBuffer = new byte[bytesRead]; // TODO: When .NET Framework support is dropped switch to using a slice instead of a copy. Buffer.BlockCopy(m_recvBuffer, 0, packetBuffer, 0, bytesRead); - CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEP as IPEndPoint, packetBuffer); + + var remoteEndPoint = remoteEP as IPEndPoint; + Debug.Assert(m_localEndPoint is not null); + Debug.Assert(remoteEndPoint is not null); + CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEndPoint, packetBuffer); } } else @@ -205,7 +212,11 @@ protected virtual void EndReceiveFrom(IAsyncResult ar) byte[] packetBufferSync = new byte[bytesReadSync]; // TODO: When .NET Framework support is dropped switch to using a slice instead of a copy. Buffer.BlockCopy(m_recvBuffer, 0, packetBufferSync, 0, bytesReadSync); - CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEP as IPEndPoint, packetBufferSync); + + var remoteEndPoint = remoteEP as IPEndPoint; + Debug.Assert(m_localEndPoint is not null); + Debug.Assert(remoteEndPoint is not null); + CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEndPoint, packetBufferSync); } else { diff --git a/src/SIPSorcery/sys/ProtocolTypeExtensions.cs b/src/SIPSorcery/sys/ProtocolTypeExtensions.cs new file mode 100644 index 0000000000..28039eb214 --- /dev/null +++ b/src/SIPSorcery/sys/ProtocolTypeExtensions.cs @@ -0,0 +1,39 @@ +namespace System.Net.Sockets; + +internal static class ProtocolTypeExtensions +{ + public static string ToLowerString(this ProtocolType protocolType) + { + return protocolType switch + { + ProtocolType.IP => "ip", + + ProtocolType.Icmp => "icmp", + ProtocolType.Igmp => "igmp", + ProtocolType.Ggp => "ggp", + + ProtocolType.IPv4 => "ipv4", + ProtocolType.Tcp => "tcp", + ProtocolType.Pup => "pup", + ProtocolType.Udp => "udp", + ProtocolType.Idp => "idp", + ProtocolType.IPv6 => "ipv6", + ProtocolType.IPv6RoutingHeader => "routing", + ProtocolType.IPv6FragmentHeader => "fragment", + ProtocolType.IPSecEncapsulatingSecurityPayload => "ipsecencapsulatingsecuritypayload", + ProtocolType.IPSecAuthenticationHeader => "ipsecauthenticationheader", + ProtocolType.IcmpV6 => "icmpv6", + ProtocolType.IPv6NoNextHeader => "nonext", + ProtocolType.IPv6DestinationOptions => "dstopts", + ProtocolType.ND => "nd", + ProtocolType.Raw => "raw", + + ProtocolType.Ipx => "ipx", + ProtocolType.Spx => "spx", + ProtocolType.SpxII => "spx2", + ProtocolType.Unknown => "unknown", + + _ => protocolType.ToString().ToLowerInvariant() + }; + } +} diff --git a/src/SIPSorcery/sys/ReadOnlySpanExtensions.cs b/src/SIPSorcery/sys/ReadOnlySpanExtensions.cs new file mode 100644 index 0000000000..4136a2a87e --- /dev/null +++ b/src/SIPSorcery/sys/ReadOnlySpanExtensions.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SIPSorcery.Sys; + +internal static class ReadOnlySpanExtensions +{ + extension(global::System.ReadOnlySpan value) + { + public bool IsEmptyOrWhiteSpace() => value.IsEmpty || value.IsWhiteSpace(); + } +} diff --git a/src/SIPSorcery/sys/SearchValues.cs b/src/SIPSorcery/sys/SearchValues.cs new file mode 100644 index 0000000000..b9ad1ecf64 --- /dev/null +++ b/src/SIPSorcery/sys/SearchValues.cs @@ -0,0 +1,86 @@ +using System; + +namespace SIPSorcery.Sys; + +internal static class SearchValues +{ + public static +#if NET8_0_OR_GREATER + global::System.Buffers.SearchValues +#else + ReadOnlySpan +#endif + DigitChars +#if NET8_0_OR_GREATER + { get; } = global::System.Buffers.SearchValues.Create( +#else + => +#endif + "0123456789" +#if NET8_0_OR_GREATER + ) +#else + .AsSpan() +#endif + ; + + public static +#if NET8_0_OR_GREATER + global::System.Buffers.SearchValues +#else + ReadOnlySpan +#endif + WhiteSpaceChars +#if NET8_0_OR_GREATER + { get; } = global::System.Buffers.SearchValues.Create( +#else + => +#endif + "\t\n\v\f\r\u0020\u0085\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000" +#if NET8_0_OR_GREATER + ) +#else + .AsSpan() +#endif + ; + + public static +#if NET8_0_OR_GREATER + global::System.Buffers.SearchValues +#else + ReadOnlySpan +#endif + NewLineChars +#if NET8_0_OR_GREATER + { get; } = global::System.Buffers.SearchValues.Create( +#else + => +#endif + "\r\n\f\u0085\u2028\u2029" +#if NET8_0_OR_GREATER + ) +#else + .AsSpan() +#endif + ; + + public static +#if NET8_0_OR_GREATER + global::System.Buffers.SearchValues +#else + ReadOnlySpan +#endif + InvalidHostNameChars +#if NET8_0_OR_GREATER + { get; } = global::System.Buffers.SearchValues.Create( +#else + => +#endif + " \t\r\n\f\v/:" +#if NET8_0_OR_GREATER + ) +#else + .AsSpan() +#endif + ; +} diff --git a/src/SIPSorcery/sys/SipSorceryException.cs b/src/SIPSorcery/sys/SipSorceryException.cs new file mode 100644 index 0000000000..12f2076420 --- /dev/null +++ b/src/SIPSorcery/sys/SipSorceryException.cs @@ -0,0 +1,32 @@ +using System; + +namespace SIPSorcery; + +/// +/// Represents errors that occur during SIP Sorcery operations. +/// +/// +/// This exception is thrown to indicate failures specific to SIP Sorcery functionality. Use this type to +/// catch and handle errors related to SIP Sorcery components separately from other exceptions. For more information +/// about the error, inspect the exception's message and inner exception properties. +/// +public class SipSorceryException : ApplicationException +{ + /// Initializes a new instance of the class. + public SipSorceryException() + { + } + + /// Initializes a new instance of the class with a specified error message. + /// The message that describes the error. + public SipSorceryException(string? message) : base(message) + { + } + + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. + public SipSorceryException(string? message, Exception? innerException) : base(message, innerException) + { + } +} diff --git a/src/SIPSorcery/sys/SipSorceryJsonSerializerContext.cs b/src/SIPSorcery/sys/SipSorceryJsonSerializerContext.cs new file mode 100644 index 0000000000..410b8b837d --- /dev/null +++ b/src/SIPSorcery/sys/SipSorceryJsonSerializerContext.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; +using SIPSorcery.Net; + +namespace SIPSorcery.Sys; + +[JsonSerializable(typeof(RTCIceCandidateInit))] +[JsonSerializable(typeof(RTCSessionDescriptionInit))] +[JsonSerializable(typeof(SctpTransportCookie))] +[JsonSourceGenerationOptions( + WriteIndented = false, + AllowTrailingCommas = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + Converters = [typeof(JsonStringEnumConverter)])] +internal sealed partial class SipSorceryJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/src/SIPSorcery/sys/SslClientAuthenticationOptions.cs b/src/SIPSorcery/sys/SslClientAuthenticationOptions.cs new file mode 100644 index 0000000000..473536a455 --- /dev/null +++ b/src/SIPSorcery/sys/SslClientAuthenticationOptions.cs @@ -0,0 +1,23 @@ +#if !(NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER) +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace System.Net.Security; + +public class SslClientAuthenticationOptions +{ + public LocalCertificateSelectionCallback? LocalCertificateSelectionCallback { get; set; } + + public RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; set; } + + public string? TargetHost { get; set; } + + public X509CertificateCollection? ClientCertificates { get; set; } + + public EncryptionPolicy EncryptionPolicy { get; set; } + + public SslProtocols EnabledSslProtocols { get; set; } + + public X509RevocationMode CertificateRevocationCheckMode { get; set; } +} +#endif diff --git a/src/SIPSorcery/sys/SslClientAuthenticationOptionsExtensions.cs b/src/SIPSorcery/sys/SslClientAuthenticationOptionsExtensions.cs new file mode 100644 index 0000000000..4fb8f700c0 --- /dev/null +++ b/src/SIPSorcery/sys/SslClientAuthenticationOptionsExtensions.cs @@ -0,0 +1,41 @@ +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace System.Net.Security; + +public static class SslClientAuthenticationOptionsExtensions +{ + extension(SslClientAuthenticationOptions) + { + public static SslClientAuthenticationOptions CreateFrom(SslClientAuthenticationOptions? sslClientAuthenticationOptions) + { + var newSslClientAuthenticationOptions = new SslClientAuthenticationOptions + { + EnabledSslProtocols = SslProtocols.None, + CertificateRevocationCheckMode = X509RevocationMode.NoCheck + }; + + if (sslClientAuthenticationOptions is { }) + { + newSslClientAuthenticationOptions.ClientCertificates = sslClientAuthenticationOptions.ClientCertificates; + newSslClientAuthenticationOptions.EnabledSslProtocols = sslClientAuthenticationOptions.EnabledSslProtocols; + newSslClientAuthenticationOptions.EncryptionPolicy = sslClientAuthenticationOptions.EncryptionPolicy; + newSslClientAuthenticationOptions.LocalCertificateSelectionCallback = sslClientAuthenticationOptions.LocalCertificateSelectionCallback; + newSslClientAuthenticationOptions.RemoteCertificateValidationCallback = sslClientAuthenticationOptions.RemoteCertificateValidationCallback; + newSslClientAuthenticationOptions.TargetHost = sslClientAuthenticationOptions.TargetHost; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + newSslClientAuthenticationOptions.ApplicationProtocols = sslClientAuthenticationOptions.ApplicationProtocols; + newSslClientAuthenticationOptions.ApplicationProtocols = sslClientAuthenticationOptions.ApplicationProtocols; +#if NET8_0_OR_GREATER + newSslClientAuthenticationOptions.AllowTlsResume = sslClientAuthenticationOptions.AllowTlsResume; + newSslClientAuthenticationOptions.CertificateChainPolicy = sslClientAuthenticationOptions.CertificateChainPolicy; + newSslClientAuthenticationOptions.CipherSuitesPolicy = sslClientAuthenticationOptions.CipherSuitesPolicy; + newSslClientAuthenticationOptions.ClientCertificateContext = sslClientAuthenticationOptions.ClientCertificateContext; +#endif +#endif + } + + return newSslClientAuthenticationOptions; + } + } +} diff --git a/src/SIPSorcery/sys/SslStreamExtensions.cs b/src/SIPSorcery/sys/SslStreamExtensions.cs new file mode 100644 index 0000000000..9abddb3923 --- /dev/null +++ b/src/SIPSorcery/sys/SslStreamExtensions.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; +using System.IO; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace SIPSorcery.Sys; + +internal static class SslStreamExtensions +{ +#if !NETSTANDARD2_1_OR_GREATER || !NETCOREAPP3_1_OR_GREATER + public static SslStream Create(Stream innerStream, bool leaveInnerStreamOpen, SslClientAuthenticationOptions sslClientAuthenticationOptions) + { + return new SslStream( + innerStream, + leaveInnerStreamOpen, + sslClientAuthenticationOptions.RemoteCertificateValidationCallback, + sslClientAuthenticationOptions.LocalCertificateSelectionCallback, + sslClientAuthenticationOptions.EncryptionPolicy); + } + + public static Task AuthenticateAsClientAsync(this SslStream sslStream, SslClientAuthenticationOptions sslClientAuthenticationOptions, CancellationToken cancellationToken = default) + { + return sslStream.AuthenticateAsClientAsync( +#nullable disable + sslClientAuthenticationOptions.TargetHost, +#nullable restore + sslClientAuthenticationOptions.ClientCertificates, + sslClientAuthenticationOptions.EnabledSslProtocols, + sslClientAuthenticationOptions.CertificateRevocationCheckMode != X509RevocationMode.Online); + } +#else + public static SslStream Create(Stream innerStream, bool leaveInnerStreamOpen, global::System.Net.Security.SslClientAuthenticationOptions sslClientAuthenticationOptions) + { + return new SslStream(innerStream, leaveInnerStreamOpen: leaveInnerStreamOpen); + } +#endif +} diff --git a/src/SIPSorcery/sys/SysLoggingExtensions.cs b/src/SIPSorcery/sys/SysLoggingExtensions.cs new file mode 100644 index 0000000000..4ae083f2e7 --- /dev/null +++ b/src/SIPSorcery/sys/SysLoggingExtensions.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace SIPSorcery.Sys; + +internal static partial class SysLoggingExtensions +{ + [LoggerMessage( + EventId = 0, + EventName = "StorageTypeUnknown", + Level = LogLevel.Error, + Message = "StorageTypesConverter {storageType} unknown.")] + internal static partial void LogStorageTypeUnknown( + this ILogger logger, + string storageType); + + [LoggerMessage( + EventId = 0, + EventName = "TimerDisposalError", + Level = LogLevel.Error, + Message = "Exception disposing timer. {ErrorMessage}")] + public static partial void LogTimerDisposalError( + this ILogger logger, + string errorMessage, + Exception exception); + + [LoggerMessage( + EventId = 0, + EventName = "NetworkAddressChangedUnsupported", + Level = LogLevel.Warning, + Message = "NetworkChange.NetworkAddressChanged is not supported on this runtime; the local-address cache will not auto-invalidate on adapter changes.")] + internal static partial void LogNetworkAddressChangedUnsupported( + this ILogger logger, + Exception exception); +} diff --git a/src/SIPSorcery/sys/TaskExtensions.cs b/src/SIPSorcery/sys/TaskExtensions.cs new file mode 100644 index 0000000000..d3cc305bcf --- /dev/null +++ b/src/SIPSorcery/sys/TaskExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace SIPSorcery.Sys; + +internal static class TaskExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Task WaitAsync(this Task task, TimeSpan? timeout) + => timeout is { } timeoutValue + ? task.WaitAsync(timeoutValue, CancellationToken.None) + : task; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Task WaitAsync(this Task task, TimeSpan? timeout) + => timeout is { } timeoutValue + ? task.WaitAsync(timeoutValue, CancellationToken.None) + : task; +} diff --git a/src/SIPSorcery/sys/TypeExtensions.cs b/src/SIPSorcery/sys/TypeExtensions.cs index 51935f2033..7867cc6375 100644 --- a/src/SIPSorcery/sys/TypeExtensions.cs +++ b/src/SIPSorcery/sys/TypeExtensions.cs @@ -17,238 +17,229 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Net; -namespace SIPSorcery.Sys +namespace SIPSorcery.Sys; + +public static class TypeExtensions { - public static class TypeExtensions + // The Trim method only trims 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x0085, 0x2028, and 0x2029. + // This array adds in control characters. + public static readonly char[] WhiteSpaceChars = [ + (char)0x00, (char)0x01, (char)0x02, (char)0x03, (char)0x04, (char)0x05, + (char)0x06, (char)0x07, (char)0x08, (char)0x09, (char)0x0a, (char)0x0b, (char)0x0c, (char)0x0d, (char)0x0e, (char)0x0f, + (char)0x10, (char)0x11, (char)0x12, (char)0x13, (char)0x14, (char)0x15, (char)0x16, (char)0x17, (char)0x18, (char)0x19, (char)0x20, + (char)0x1a, (char)0x1b, (char)0x1c, (char)0x1d, (char)0x1e, (char)0x1f, (char)0x7f, (char)0x85, (char)0x2028, (char)0x2029 + ]; + + private static readonly sbyte[] _hexDigits = [ + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + 0,1,2,3,4,5,6,7,8,9,-1,-1,-1,-1,-1,-1, + -1,0xa,0xb,0xc,0xd,0xe,0xf,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,0xa,0xb,0xc,0xd,0xe,0xf,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + ]; + + private static readonly char[] hexmap = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + /// + /// Gets a value that indicates whether or not the string is empty. + /// + public static bool IsNullOrBlank(this string s) { - // The Trim method only trims 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x0085, 0x2028, and 0x2029. - // This array adds in control characters. - public static readonly char[] WhiteSpaceChars = new char[] { (char)0x00, (char)0x01, (char)0x02, (char)0x03, (char)0x04, (char)0x05, - (char)0x06, (char)0x07, (char)0x08, (char)0x09, (char)0x0a, (char)0x0b, (char)0x0c, (char)0x0d, (char)0x0e, (char)0x0f, - (char)0x10, (char)0x11, (char)0x12, (char)0x13, (char)0x14, (char)0x15, (char)0x16, (char)0x17, (char)0x18, (char)0x19, (char)0x20, - (char)0x1a, (char)0x1b, (char)0x1c, (char)0x1d, (char)0x1e, (char)0x1f, (char)0x7f, (char)0x85, (char)0x2028, (char)0x2029 }; - - private static readonly sbyte[] _hexDigits = - { -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - 0,1,2,3,4,5,6,7,8,9,-1,-1,-1,-1,-1,-1, - -1,0xa,0xb,0xc,0xd,0xe,0xf,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,0xa,0xb,0xc,0xd,0xe,0xf,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, - -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, }; - - private static readonly char[] hexmap = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; - - /// - /// Gets a value that indicates whether or not the string is empty. - /// - public static bool IsNullOrBlank(this string s) + if (s is null || s.AsSpan().Trim(WhiteSpaceChars).Length == 0) { - if (s == null || s.AsSpan().Trim(WhiteSpaceChars).Length == 0) - { - return true; - } + return true; + } + return false; + } + + public static bool NotNullOrBlank([NotNullWhen(true)] this string? s) + { + if (s is null || s.AsSpan().Trim(WhiteSpaceChars).Length == 0) + { return false; } - public static bool NotNullOrBlank(this string s) - { - if (s == null || s.AsSpan().Trim(WhiteSpaceChars).Length == 0) - { - return false; - } + return true; + } - return true; - } + [Obsolete("Use ToUnixTime.")] + public static long GetEpoch(this DateTime dateTime) + { + var unixTime = dateTime.ToUniversalTime() - + new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - [Obsolete("Use ToUnixTime.")] - public static long GetEpoch(this DateTime dateTime) - { - var unixTime = dateTime.ToUniversalTime() - - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + return Convert.ToInt64(unixTime.TotalSeconds); + } - return Convert.ToInt64(unixTime.TotalSeconds); - } + public static long ToUnixTime(this DateTime dateTime) + { + return new DateTimeOffset(dateTime.ToUniversalTime()).ToUnixTimeSeconds(); + } - public static long ToUnixTime(this DateTime dateTime) + /// + /// Returns a slice from a string that is delimited by the first instance of a start and end character. The + /// delimiting characters are not included. + /// "sip:127.0.0.1:5060;connid=1234".slice(':', ';') => "127.0.0.1:5060" + /// + /// The input string to extract the slice from. + /// + /// The character to start the slice from. The first instance of the character found is used. + /// + /// + /// The character to end the slice on. The first instance of the character found is used. + /// + /// A slice of the input string or null if the slice is not possible. + public static string? Slice(this string s, char startDelimiter, char endDelimeter) + { + if (String.IsNullOrEmpty(s)) { - return new DateTimeOffset(dateTime.ToUniversalTime()).ToUnixTimeSeconds(); + return null; } - - /// - /// Returns a slice from a string that is delimited by the first instance of a - /// start and end character. The delimiting characters are not included. - /// - /// - /// "sip:127.0.0.1:5060;connid=1234".slice(':', ';') => "127.0.0.1:5060" - /// - /// - /// The input string to extract the slice from. - /// The character to start the slice from. The first instance of the character found is used. - /// The character to end the slice on. The first instance of the character found is used. - /// A slice of the input string or null if the slice is not possible. - public static string Slice(this string s, char startDelimiter, char endDelimeter) + else { - if (String.IsNullOrEmpty(s)) + int startPosn = s.IndexOf(startDelimiter); + int endPosn = s.IndexOf(endDelimeter) - 1; + + if (endPosn > startPosn) { - return null; + return s.Substring(startPosn + 1, endPosn - startPosn); } else { - int startPosn = s.IndexOf(startDelimiter); - int endPosn = s.IndexOf(endDelimeter) - 1; - - if (endPosn > startPosn) - { - return s.Substring(startPosn + 1, endPosn - startPosn); - } - else - { - return null; - } + return null; } } + } - public static string HexStr(this byte[] buffer, char? separator = null) + public static string HexStr(this ReadOnlySpan buffer, char? separator = null, bool lowercase = false) + { + using var sb = new ValueStringBuilder(stackalloc char[256]); + sb.Append(buffer, separator, lowercase); + return sb.ToString(); + } + + public static byte[] ParseHexStr(string hexStr) + { +#if NET8_0_OR_GREATER + if (hexStr.AsSpan().ContainsAny(SearchValues.DigitChars)) { - return buffer.HexStr(buffer.Length, separator); + return Convert.FromHexString(hexStr); } - - public static string HexStr(this byte[] buffer, int length, char? separator = null) +#else +#if NET5_0_OR_GREATER + // Check if string contains whitespace + var hasWhitespace = false; + for (int i = 0; i < hexStr.Length; i++) { - if (separator is { } s) + if (char.IsWhiteSpace(hexStr[i])) { - int numberOfChars = length * 3 - 1; -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - return string.Create(numberOfChars, (buffer, length, s), PopulateNewStringWithSeparator); -#else - var rv = new char[numberOfChars]; - PopulateNewStringWithSeparator(rv, (buffer, length, s)); - return new string(rv); -#endif + hasWhitespace = true; + break; } - else - { - int numberOfChars = length * 2; -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - return string.Create(numberOfChars, (buffer, length), PopulateNewStringWithoutSeparator); -#else - var rv = new char[numberOfChars]; - PopulateNewStringWithoutSeparator(rv, (buffer, length)); - return new string(rv); + } + + if (!hasWhitespace) + { + return Convert.FromHexString(hexStr); + } #endif - } +#endif + + // Fallback implementation + var buffer = new byte[hexStr.Length / 2 + 1]; + var chars = hexStr.AsSpan(); + var bufferIndex = 0; + + // Split by whitespace and process each token + foreach (var tokenRange in chars.SplitAny(SearchValues.WhiteSpaceChars)) + { + var token = chars[tokenRange]; - static void PopulateNewStringWithSeparator(Span chars, (byte[] buffer, int length, char separator) state) + // Process pairs of hex digits + for (int i = 0; i < token.Length; i += 2) { - var (buffer, length, s) = state; - for (int i = 0, j = 0; i < length; i++) + if (i + 1 < token.Length) { - var val = buffer[i]; - chars[j++] = char.ToUpperInvariant(hexmap[val >> 4]); - chars[j++] = char.ToUpperInvariant(hexmap[val & 15]); - if (j < chars.Length) + var c1 = _hexDigits[token[i]]; + var c2 = _hexDigits[token[i + 1]]; + + if (c1 == -1 || c2 == -1) { - chars[j++] = s; + break; } - } - } - static void PopulateNewStringWithoutSeparator(Span chars, (byte[] buffer, int length) state) - { - var (buffer, length) = state; - for (int i = 0, j = 0; i < length; i++) - { - var val = buffer[i]; - chars[j++] = char.ToUpperInvariant(hexmap[val >> 4]); - chars[j++] = char.ToUpperInvariant(hexmap[val & 15]); + var n = (sbyte)(c1 << 4); + n |= c2; + buffer[bufferIndex++] = (byte)n; } } } - public static byte[] ParseHexStr(string hexStr) + if (bufferIndex < buffer.Length) { - List buffer = new List(); - var chars = hexStr.ToCharArray(); - int posn = 0; - while (posn < hexStr.Length) - { - while (char.IsWhiteSpace(chars[posn])) - { - posn++; - } - sbyte c = _hexDigits[chars[posn++]]; - if (c == -1) - { - break; - } - sbyte n = (sbyte)(c << 4); - c = _hexDigits[chars[posn++]]; - if (c == -1) - { - break; - } - n |= c; - buffer.Add((byte)n); - } - return buffer.ToArray(); + Array.Resize(ref buffer, bufferIndex); } - public static bool IsPrivate(this IPAddress address) - { - return IPSocket.IsPrivateAddress(address.ToString()); - } + return buffer; + } + public static bool IsPrivate(this IPAddress address) + { + return IPSocket.IsPrivateAddress(address.ToString()); + } - /// - /// Purpose of this extension is to allow deconstruction of a list into a fixed size tuple. - /// - /// - /// (var field0, var field1) = "a b c".Split(); - /// - public static void Deconstruct(this IList list, out T first, out T second) - { - first = list.Count > 0 ? list[0] : default(T); - second = list.Count > 1 ? list[1] : default(T); - } - /// - /// Purpose of this extension is to allow deconstruction of a list into a fixed size tuple. - /// - /// - /// (var field0, var field1, var field2) = "a b c".Split(); - /// - public static void Deconstruct(this IList list, out T first, out T second, out T third) - { - first = list.Count > 0 ? list[0] : default(T); - second = list.Count > 1 ? list[1] : default(T); - third = list.Count > 2 ? list[2] : default(T); - } + /// + /// Purpose of this extension is to allow deconstruction of a list into a fixed size tuple. + /// + /// + /// (var field0, var field1) = "a b c".Split(); + /// + public static void Deconstruct(this IList list, out T? first, out T? second) + { + first = list.Count > 0 ? list[0] : default(T); + second = list.Count > 1 ? list[1] : default(T); + } - /// - /// Purpose of this extension is to allow deconstruction of a list into a fixed size tuple. - /// - /// - /// (var field0, var field1, var field2, var field3) = "a b c d".Split(); - /// - public static void Deconstruct(this IList list, out T first, out T second, out T third, out T fourth) - { - first = list.Count > 0 ? list[0] : default(T); - second = list.Count > 1 ? list[1] : default(T); - third = list.Count > 2 ? list[2] : default(T); - fourth = list.Count > 3 ? list[3] : default(T); - } + /// + /// Purpose of this extension is to allow deconstruction of a list into a fixed size tuple. + /// + /// + /// (var field0, var field1, var field2) = "a b c".Split(); + /// + public static void Deconstruct(this IList list, out T? first, out T? second, out T? third) + { + first = list.Count > 0 ? list[0] : default(T); + second = list.Count > 1 ? list[1] : default(T); + third = list.Count > 2 ? list[2] : default(T); + } + + /// + /// Purpose of this extension is to allow deconstruction of a list into a fixed size tuple. + /// + /// + /// (var field0, var field1, var field2, var field3) = "a b c d".Split(); + /// + public static void Deconstruct(this IList list, out T? first, out T? second, out T? third, out T? fourth) + { + first = list.Count > 0 ? list[0] : default(T); + second = list.Count > 1 ? list[1] : default(T); + third = list.Count > 2 ? list[2] : default(T); + fourth = list.Count > 3 ? list[3] : default(T); } } diff --git a/src/SIPSorcery/sys/ValueStringBuilder.AppendSpanFormattable.cs b/src/SIPSorcery/sys/ValueStringBuilder.AppendSpanFormattable.cs new file mode 100644 index 0000000000..5af076c416 --- /dev/null +++ b/src/SIPSorcery/sys/ValueStringBuilder.AppendSpanFormattable.cs @@ -0,0 +1,21 @@ +using System; +using System.Runtime.CompilerServices; + +namespace SIPSorcery.Sys; + +#if NET6_0_OR_GREATER +internal ref partial struct ValueStringBuilder +{ + internal void AppendSpanFormattable(T value, string? format = null, IFormatProvider? provider = null) where T : ISpanFormattable + { + if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider)) + { + _pos += charsWritten; + } + else + { + Append(value.ToString(format, provider)); + } + } +} +#endif diff --git a/src/SIPSorcery/sys/ValueStringBuilder.Bytes.cs b/src/SIPSorcery/sys/ValueStringBuilder.Bytes.cs new file mode 100644 index 0000000000..d5d1d02ae9 --- /dev/null +++ b/src/SIPSorcery/sys/ValueStringBuilder.Bytes.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.InteropServices; + +namespace SIPSorcery.Sys; + +internal ref partial struct ValueStringBuilder +{ + private static readonly char[] upperHexmap = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + private static readonly char[] lowerHexmap = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + public void Append(byte[]? bytes, char? separator = null) + { + if (bytes is { Length: > 0 }) + { + Append(bytes.AsSpan(), separator); + } + } + + public void Append(ReadOnlySpan bytes, char? separator = null, bool lowercase = false) + { + var hexmap = lowercase ? lowerHexmap : upperHexmap; + + if (bytes.IsEmpty) + { + return; + } + + if (separator is { } s) + { + for (int i = 0; i < bytes.Length;) + { + var b = bytes[i]; + Append(hexmap[(int)b >> 4]); + Append(hexmap[(int)b & 0b1111]); + if (++i < bytes.Length) + { + Append(s); + } + } + } + else + { + for (var i = 0; i < bytes.Length; i++) + { + var b = bytes[i]; + Append(hexmap[(int)b >> 4]); + Append(hexmap[(int)b & 0b1111]); + } + } + } +} diff --git a/src/SIPSorcery/sys/ValueStringBuilder.IPAddress.cs b/src/SIPSorcery/sys/ValueStringBuilder.IPAddress.cs new file mode 100644 index 0000000000..32db13e9ae --- /dev/null +++ b/src/SIPSorcery/sys/ValueStringBuilder.IPAddress.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text; + +namespace SIPSorcery.Sys; + +internal ref partial struct ValueStringBuilder +{ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET8_0_OR_GREATER + public void Append(IPAddress value) => AppendSpanFormattable(value, null, null); +#else + public void Append(IPAddress value) => Append(value.ToString()); +#endif +} diff --git a/src/SIPSorcery/sys/ValueStringBuilder.cs b/src/SIPSorcery/sys/ValueStringBuilder.cs new file mode 100644 index 0000000000..579c760378 --- /dev/null +++ b/src/SIPSorcery/sys/ValueStringBuilder.cs @@ -0,0 +1,393 @@ +// Based on System.Text.ValueStringBuilder - System.Console + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SIPSorcery.Sys; + +internal ref partial struct ValueStringBuilder +{ + private char[]? _arrayToReturnToPool; + private Span _chars; + private int _pos; + + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } + + public ValueStringBuilder(int initialCapacity) + { + _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _chars = _arrayToReturnToPool; + _pos = 0; + } + + public int Length + { + get => _pos; + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= _chars.Length); + _pos = value; + } + } + + public int Capacity => _chars.Length; + + public ReadOnlySpan Chars => _chars; + + public void EnsureCapacity(int capacity) + { + // This is not expected to be called this with negative capacity + Debug.Assert(capacity >= 0); + + // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception. + if ((uint)capacity > (uint)_chars.Length) + { + Grow(capacity - _pos); + } + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)" + /// + public ref char GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_chars); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null char after + public ref char GetPinnableReference(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return ref MemoryMarshal.GetReference(_chars); + } + + public ref char this[int index] + { + get + { + Debug.Assert(index < _pos); + return ref _chars[index]; + } + } + + public override string ToString() + { + var s = _chars.Slice(0, _pos).ToString(); + Dispose(); + return s; + } + + /// Returns the underlying storage of the builder. + public Span RawChars => _chars; + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null char after + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return _chars.Slice(0, _pos); + } + + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); + + public bool TryCopyTo(Span destination, out int charsWritten) + { + if (_chars.Slice(0, _pos).TryCopyTo(destination)) + { + charsWritten = _pos; + Dispose(); + return true; + } + else + { + charsWritten = 0; + Dispose(); + return false; + } + } + + public void Insert(int index, char value, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + var remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + _chars.Slice(index, count).Fill(value); + _pos += count; + } + + public void Insert(int index, string? s) + { + if (s is null) + { + return; + } + + var count = s.Length; + + if (_pos > (_chars.Length - count)) + { + Grow(count); + } + + var remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + s.AsSpan().CopyTo(_chars.Slice(index)); + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(bool value) => Append(value ? "true" : "false"); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + var pos = _pos; + var chars = _chars; + if ((uint)pos < (uint)chars.Length) + { + chars[pos] = c; + _pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string? s) + { + if (s is null) + { + return; + } + + var pos = _pos; + if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. + { + _chars[pos] = s[0]; + _pos = pos + 1; + } + else + { + AppendSlow(s); + } + } + + private void AppendSlow(string s) + { + var pos = _pos; + if (pos > _chars.Length - s.Length) + { + Grow(s.Length); + } + + s.AsSpan().CopyTo(_chars.Slice(pos)); + _pos += s.Length; + } + + public void Append(char c, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + var dst = _chars.Slice(_pos, count); + for (var i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + _pos += count; + } + + public void Append(scoped ReadOnlySpan value) + { + var pos = _pos; + if (pos > _chars.Length - value.Length) + { + Grow(value.Length); + } + + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + var origPos = _pos; + if (origPos > _chars.Length - length) + { + Grow(length); + } + + _pos = origPos + length; + return _chars.Slice(origPos, length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(int value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(int value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(uint value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(uint value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(ushort value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(ushort value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(ushort? value, string? format = null, IFormatProvider? provider = null) + { + if (value is { } v) + { + AppendSpanFormattable(v, format, provider); + } + } +#else + public void Append(ushort? value, string? format = null, IFormatProvider? provider = null) + { + if (value is { } v) + { + Append(v.ToString(format, provider)); + } + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(long value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(long value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(ulong value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(ulong value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(float value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(float value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(double value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(double value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET6_0_OR_GREATER + public void Append(decimal value, string? format = null, IFormatProvider? provider = null) => AppendSpanFormattable(value, format, provider); +#else + public void Append(decimal value, string? format = null, IFormatProvider? provider = null) => Append(value.ToString(format, provider)); +#endif + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) + { + Grow(1); + Append(c); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int additionalCapacityBeyondPos) + { + if (additionalCapacityBeyondPos == 0) + { + additionalCapacityBeyondPos = 2 * Capacity - _pos; // Default to double the current capacity minus the current position. + } + + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); + + const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength + + // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try + // to double the size if possible, bounding the doubling to not go beyond the max array length. + var newCapacity = (int)Math.Max( + (uint)(_pos + additionalCapacityBeyondPos), + Math.Min((uint)_chars.Length * 2, ArrayMaxLength)); + + // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative. + // This could also go negative if the actual required length wraps around. + var poolArray = ArrayPool.Shared.Rent(newCapacity); + + _chars.Slice(0, _pos).CopyTo(poolArray); + + var toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + var toReturn = _arrayToReturnToPool; + this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } +} diff --git a/src/SIPSorceryMedia.Abstractions/Encoders/IAudioEncoder.cs b/src/SIPSorceryMedia.Abstractions/Encoders/IAudioEncoder.cs index 2d19e31a19..f045db7ac2 100644 --- a/src/SIPSorceryMedia.Abstractions/Encoders/IAudioEncoder.cs +++ b/src/SIPSorceryMedia.Abstractions/Encoders/IAudioEncoder.cs @@ -14,6 +14,8 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; +using System.Buffers; using System.Collections.Generic; namespace SIPSorceryMedia.Abstractions; @@ -30,14 +32,14 @@ public interface IAudioEncoder ///
/// An array of 16 bit signed audio samples. /// The audio format to encode the PCM sample to. - /// A byte array containing the encoded sample. - byte[] EncodeAudio(short[] pcm, AudioFormat format); + /// A of to receieve the encoded sample. + void EncodeAudio(ReadOnlySpan pcm, AudioFormat format, IBufferWriter destination); /// /// Decodes to 16bit signed PCM samples. /// - /// The byte array containing the encoded sample. + /// The span containing the encoded sample. /// The audio format of the encoded sample. - /// An array containing the 16 bit signed PCM samples. - short[] DecodeAudio(byte[] encodedSample, AudioFormat format); -} \ No newline at end of file + /// A of to receive the decoded PCM samples. + void DecodeAudio(ReadOnlySpan encodedSample, AudioFormat format, IBufferWriter destination); +} diff --git a/src/SIPSorceryMedia.Abstractions/Encoders/RawImage.cs b/src/SIPSorceryMedia.Abstractions/Encoders/RawImage.cs index fd3c1554ce..e8eb9080c8 100644 --- a/src/SIPSorceryMedia.Abstractions/Encoders/RawImage.cs +++ b/src/SIPSorceryMedia.Abstractions/Encoders/RawImage.cs @@ -39,7 +39,7 @@ public class RawImage /// /// Pointer to an array of bytes that contains the pixel data. /// - public IntPtr Sample { get; set; } + public nint Sample { get; set; } /// /// The pixel format of the image @@ -52,9 +52,9 @@ public class RawImage /// For performance reasons it's better to use directly Sample /// /// - public byte[] GetBuffer() + public byte[]? GetBuffer() { - byte[] result = null; + byte[]? result = null; if ((Height > 0) && (Stride > 0)) { diff --git a/src/SIPSorceryMedia.Abstractions/Enums/AudioCodecsEnum.cs b/src/SIPSorceryMedia.Abstractions/Enums/AudioCodecsEnum.cs index d70b6de7f3..a3479262f0 100644 --- a/src/SIPSorceryMedia.Abstractions/Enums/AudioCodecsEnum.cs +++ b/src/SIPSorceryMedia.Abstractions/Enums/AudioCodecsEnum.cs @@ -18,6 +18,8 @@ namespace SIPSorceryMedia.Abstractions; public enum AudioCodecsEnum { + Unknown, + PCMU, GSM, G723, @@ -34,6 +36,4 @@ public enum AudioCodecsEnum OPUS, PCM_S16LE, // PCM signed 16-bit little-endian (equivalent to FFmpeg s16le). For use with Azure, not likely to be supported in VoIP/WebRTC. - - Unknown } diff --git a/src/SIPSorceryMedia.Abstractions/Enums/VideoCodecsEnum.cs b/src/SIPSorceryMedia.Abstractions/Enums/VideoCodecsEnum.cs index 795b10a541..680d510575 100644 --- a/src/SIPSorceryMedia.Abstractions/Enums/VideoCodecsEnum.cs +++ b/src/SIPSorceryMedia.Abstractions/Enums/VideoCodecsEnum.cs @@ -18,6 +18,8 @@ namespace SIPSorceryMedia.Abstractions; public enum VideoCodecsEnum { + Unknown, + CELB, JPEG, NV, @@ -30,6 +32,4 @@ public enum VideoCodecsEnum AV1, H264, H265, - - Unknown } diff --git a/src/SIPSorceryMedia.Abstractions/Frames/EncodedAudioFrame.cs b/src/SIPSorceryMedia.Abstractions/Frames/EncodedAudioFrame.cs index 513e67500e..bfd51249de 100644 --- a/src/SIPSorceryMedia.Abstractions/Frames/EncodedAudioFrame.cs +++ b/src/SIPSorceryMedia.Abstractions/Frames/EncodedAudioFrame.cs @@ -14,6 +14,8 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; + namespace SIPSorceryMedia.Abstractions; /// @@ -21,7 +23,7 @@ namespace SIPSorceryMedia.Abstractions; /// public class EncodedAudioFrame { - public EncodedAudioFrame(int mediaStreamIndex, AudioFormat mediaformat, uint durationMilliSeconds, byte[] encodedMedia) + public EncodedAudioFrame(int mediaStreamIndex, AudioFormat mediaformat, uint durationMilliSeconds, ReadOnlyMemory encodedMedia) { MediaStreamIndex = mediaStreamIndex; AudioFormat = mediaformat; @@ -35,5 +37,5 @@ public EncodedAudioFrame(int mediaStreamIndex, AudioFormat mediaformat, uint dur public uint DurationMilliSeconds { get; } - public byte[] EncodedAudio { get; } + public ReadOnlyMemory EncodedAudio { get; } } diff --git a/src/SIPSorceryMedia.Abstractions/LogFactory.cs b/src/SIPSorceryMedia.Abstractions/LogFactory.cs index 011483c14c..528b43ef9f 100644 --- a/src/SIPSorceryMedia.Abstractions/LogFactory.cs +++ b/src/SIPSorceryMedia.Abstractions/LogFactory.cs @@ -8,21 +8,10 @@ public class LogFactory { private ILoggerFactory _factory = NullLoggerFactory.Instance; - public event Action OnFactorySet; + public event Action? OnFactorySet; - private static LogFactory _appLog; - public static LogFactory Instance - { - get - { - if (_appLog == null) - { - _appLog = new LogFactory(); - } - - return _appLog; - } - } + private static LogFactory? _appLog; + public static LogFactory Instance => (_appLog ??= new()); private LogFactory() { } @@ -33,9 +22,9 @@ public static ILogger CreateLogger(string categoryName) => public static ILogger CreateLogger() => Instance._factory.CreateLogger(); - public static void Set(ILoggerFactory factory) + public static void Set(ILoggerFactory? factory) { - Instance._factory = factory; + Instance._factory = factory ?? NullLoggerFactory.Instance; Instance.OnFactorySet?.Invoke(); } } diff --git a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IAudioSink.cs b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IAudioSink.cs index a58997e37a..a04fcd6a6b 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IAudioSink.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IAudioSink.cs @@ -44,4 +44,4 @@ public interface IAudioSink Task StartAudioSink(); Task CloseAudioSink(); -} \ No newline at end of file +} diff --git a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IAudioSource.cs b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IAudioSource.cs index 10139f1fae..bf85aa5dee 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IAudioSource.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IAudioSource.cs @@ -52,4 +52,4 @@ public interface IAudioSource bool HasEncodedAudioSubscribers(); bool IsAudioSourcePaused(); -} \ No newline at end of file +} diff --git a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/ITextSource.cs b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/ITextSource.cs index cf28d5dca1..34a63cf751 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/ITextSource.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/ITextSource.cs @@ -15,15 +15,14 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Threading.Tasks; namespace SIPSorceryMedia.Abstractions; -public delegate void EncodedTextSampleDelegate(byte[] sample); - public interface ITextSource { - event EncodedTextSampleDelegate OnTextSourceEncodedSample; + event Action OnTextSourceEncodedSample; Task CloseText(); TextFormat GetTextSourceFormat(); void SetTextSourceFormat(TextFormat textFormat); diff --git a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IVideoSink.cs b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IVideoSink.cs index 2f15c8edde..8ea503e05b 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IVideoSink.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IVideoSink.cs @@ -16,6 +16,7 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; @@ -31,13 +32,13 @@ public interface IVideoSink /// /// This event will be fired by the sink after is decodes a video frame from the RTP stream. /// - event VideoSinkSampleDecodedDelegate OnVideoSinkDecodedSample; + event VideoSinkSampleDecodedDelegate? OnVideoSinkDecodedSample; - event VideoSinkSampleDecodedFasterDelegate OnVideoSinkDecodedSampleFaster; // Avoid to use byte[] to improve performance + event VideoSinkSampleDecodedFasterDelegate? OnVideoSinkDecodedSampleFaster; // Avoid to use byte[] to improve performance void GotVideoRtp(IPEndPoint remoteEndPoint, uint ssrc, uint seqnum, uint timestamp, int payloadID, bool marker, byte[] payload); - void GotVideoFrame(IPEndPoint remoteEndPoint, uint timestamp, byte[] payload, VideoFormat format); + void GotVideoFrame(IPEndPoint remoteEndPoint, uint timestamp, ReadOnlyMemory payload, VideoFormat format); List GetVideoSinkFormats(); @@ -52,4 +53,4 @@ public interface IVideoSink Task StartVideoSink(); Task CloseVideoSink(); -} \ No newline at end of file +} diff --git a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IVideoSource.cs b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IVideoSource.cs index c50c21a620..921e5f4605 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IVideoSource.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/IVideoSource.cs @@ -31,9 +31,9 @@ public interface IVideoSource event RawVideoSampleDelegate OnVideoSourceRawSample; - event RawVideoSampleFasterDelegate OnVideoSourceRawSampleFaster; // Avoid to use byte[] to improve performance + event RawVideoSampleFasterDelegate? OnVideoSourceRawSampleFaster; // Avoid to use byte[] to improve performance - event SourceErrorDelegate OnVideoSourceError; + event SourceErrorDelegate? OnVideoSourceError; Task PauseVideo(); diff --git a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/MediaEndPoints.cs b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/MediaEndPoints.cs index 7d6fe9bad0..1e2e1b092e 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaEndPoints/MediaEndPoints.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaEndPoints/MediaEndPoints.cs @@ -15,19 +15,20 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- -namespace SIPSorceryMedia.Abstractions; +using System; -public delegate void EncodedSampleDelegate(uint durationRtpUnits, byte[] sample); +namespace SIPSorceryMedia.Abstractions; +public delegate void EncodedSampleDelegate(uint durationRtpUnits, ReadOnlyMemory sample); public delegate void SourceErrorDelegate(string errorMessage); public class MediaEndPoints { - public IAudioSource AudioSource { get; set; } - public IAudioSink AudioSink { get; set; } - public IVideoSource VideoSource { get; set; } - public IVideoSink VideoSink { get; set; } - public ITextSource TextSource { get; set; } - public ITextSink TextSink { get; set; } + public IAudioSource? AudioSource { get; set; } + public IAudioSink? AudioSink { get; set; } + public IVideoSource? VideoSource { get; set; } + public IVideoSink? VideoSink { get; set; } + public ITextSource? TextSource { get; set; } + public ITextSink? TextSink { get; set; } } diff --git a/src/SIPSorceryMedia.Abstractions/MediaFormatManager.cs b/src/SIPSorceryMedia.Abstractions/MediaFormatManager.cs index 0a984c919e..06c03614f5 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaFormatManager.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaFormatManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; namespace SIPSorceryMedia.Abstractions { @@ -8,7 +7,7 @@ public class MediaFormatManager { public readonly List SupportedFormats = new List(); - public T SelectedFormat { get; private set; } + public T? SelectedFormat { get; private set; } private List _filteredFormats = new List(); public MediaFormatManager(List supportedFormats) @@ -30,13 +29,21 @@ public List GetSourceFormats() /// Function to determine which formats the source formats should be restricted to. public void RestrictFormats(Func filter) { - if (filter == null) + if (filter is null) { _filteredFormats = new List(SupportedFormats); } else { - _filteredFormats = _filteredFormats.Where(x => filter(x)).ToList(); + var filtered = new List(_filteredFormats.Count); + foreach (var item in _filteredFormats) + { + if (filter(item)) + { + filtered.Add(item); + } + } + _filteredFormats = filtered; } } diff --git a/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioCommonlyUsedFormats.cs b/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioCommonlyUsedFormats.cs index 424ce9ae6b..54e60e19c4 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioCommonlyUsedFormats.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioCommonlyUsedFormats.cs @@ -27,5 +27,5 @@ public static class AudioCommonlyUsedFormats /// /// The Opus audio format typical used for WebRTC scenarios. /// - public static AudioFormat OpusWebRTC => new AudioFormat(111, AudioCodecsEnum.OPUS.ToString(), OPUS_SAMPLE_RATE, OPUS_CHANNELS, "useinbandfec=1"); + public static AudioFormat OpusWebRTC => new AudioFormat(111, nameof(AudioCodecsEnum.OPUS), OPUS_SAMPLE_RATE, OPUS_CHANNELS, "useinbandfec=1"); } \ No newline at end of file diff --git a/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioFormat.cs b/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioFormat.cs index afc15fc8d0..000092c2e0 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioFormat.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioFormat.cs @@ -79,7 +79,7 @@ public struct AudioFormat /// In the case below this filed should be set as "emphasis=50-15". /// a=fmtp:97 emphasis=50-15 /// - public string Parameters { get; set; } + public string? Parameters { get; set; } private bool _isNonEmpty; @@ -98,7 +98,7 @@ public AudioFormat( int formatID, int clockRate = DEFAULT_CLOCK_RATE, int channelCount = DEFAULT_CHANNEL_COUNT, - string parameters = null) : + string? parameters = null) : this(codec, formatID, clockRate, clockRate, channelCount, parameters) { } @@ -111,8 +111,8 @@ public AudioFormat( int clockRate, int rtpClockRate, int channelCount, - string parameters) - : this(formatID, codec.ToString(), clockRate, rtpClockRate, channelCount, parameters) + string? parameters) + : this(formatID, codec.ToStringFast(), clockRate, rtpClockRate, channelCount, parameters) { } /// @@ -123,7 +123,7 @@ public AudioFormat( string formatName, int clockRate = DEFAULT_CLOCK_RATE, int channelCount = DEFAULT_CHANNEL_COUNT, - string parameters = null) : + string? parameters = null) : this(formatID, formatName, clockRate, clockRate, channelCount, parameters) { } @@ -134,33 +134,33 @@ public AudioFormat(AudioFormat format) /// /// Creates a new audio format based on a dynamic codec (or an unsupported well known codec). /// - public AudioFormat(int formatID, string formatName, int clockRate, int rtpClockRate, int channelCount, string parameters) + public AudioFormat(int formatID, string formatName, int clockRate, int rtpClockRate, int channelCount, string? parameters) { if (formatID < 0) { // Note format ID's less than the dynamic start range are allowed as the codec list // does not currently support all well known codecs. - throw new ApplicationException("The format ID for an AudioFormat must be greater than 0."); + throw new SipSorceryMediaException("The format ID for an AudioFormat must be greater than 0."); } else if (formatID > DYNAMIC_ID_MAX) { - throw new ApplicationException($"The format ID for an AudioFormat exceeded the maximum allowed vale of {DYNAMIC_ID_MAX}."); + throw new SipSorceryMediaException($"The format ID for an AudioFormat exceeded the maximum allowed vale of {DYNAMIC_ID_MAX}."); } else if (string.IsNullOrWhiteSpace(formatName)) { - throw new ApplicationException($"The format name must be provided for an AudioFormat."); + throw new SipSorceryMediaException($"The format name must be provided for an AudioFormat."); } else if (clockRate <= 0) { - throw new ApplicationException($"The clock rate for an AudioFormat must be greater than 0."); + throw new SipSorceryMediaException($"The clock rate for an AudioFormat must be greater than 0."); } else if (rtpClockRate <= 0) { - throw new ApplicationException($"The RTP clock rate for an AudioFormat must be greater than 0."); + throw new SipSorceryMediaException($"The RTP clock rate for an AudioFormat must be greater than 0."); } else if (channelCount <= 0) { - throw new ApplicationException($"The channel count for an AudioFormat must be greater than 0."); + throw new SipSorceryMediaException($"The channel count for an AudioFormat must be greater than 0."); } FormatID = formatID; @@ -171,7 +171,7 @@ public AudioFormat(int formatID, string formatName, int clockRate, int rtpClockR Parameters = parameters; _isNonEmpty = true; - if (Enum.TryParse(FormatName, true, out var audioCodec)) + if (AudioCodecsEnumExtensions.TryParse(FormatName, out var audioCodec, true)) { Codec = audioCodec; } @@ -182,4 +182,14 @@ public AudioFormat(int formatID, string formatName, int clockRate, int rtpClockR } public bool IsEmpty() => !_isNonEmpty; + + public override bool Equals(object? obj) + { + throw new InvalidOperationException("AudioFormat equality is not supported."); + } + + public override int GetHashCode() + { + throw new InvalidOperationException("Using AudioFormat as a key is not supported."); + } } diff --git a/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioVideoWellKnown.cs b/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioVideoWellKnown.cs index decbd0fa6d..56ea2ab44c 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioVideoWellKnown.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaFormats/AudioVideoWellKnown.cs @@ -14,13 +14,14 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System.Collections.Frozen; using System.Collections.Generic; namespace SIPSorceryMedia.Abstractions; public static class AudioVideoWellKnown { - public static Dictionary WellKnownAudioFormats = + public static readonly FrozenDictionary WellKnownAudioFormats = new Dictionary { { SDPWellKnownMediaFormatsEnum.PCMU, new AudioFormat(AudioCodecsEnum.PCMU, 0, 8000, 1)}, { SDPWellKnownMediaFormatsEnum.GSM, new AudioFormat(AudioCodecsEnum.GSM, 3, 8000, 1)}, @@ -39,9 +40,9 @@ public static class AudioVideoWellKnown { SDPWellKnownMediaFormatsEnum.DVI4_11K, new AudioFormat(AudioCodecsEnum.DVI4, 16, 11025, 1)}, { SDPWellKnownMediaFormatsEnum.DVI4_22K, new AudioFormat(AudioCodecsEnum.DVI4, 17, 22050, 1)}, { SDPWellKnownMediaFormatsEnum.G729, new AudioFormat(AudioCodecsEnum.G729, 18, 8000, 1)}, - }; + }.ToFrozenDictionary(); - public static Dictionary WellKnownVideoFormats = + public static readonly FrozenDictionary WellKnownVideoFormats = new Dictionary { { SDPWellKnownMediaFormatsEnum.CELB, new VideoFormat(VideoCodecsEnum.CELB, 24, 90000)}, { SDPWellKnownMediaFormatsEnum.JPEG, new VideoFormat(VideoCodecsEnum.JPEG, 26, 90000)}, @@ -50,5 +51,5 @@ public static class AudioVideoWellKnown { SDPWellKnownMediaFormatsEnum.MPV, new VideoFormat(VideoCodecsEnum.MPV, 32, 90000)}, { SDPWellKnownMediaFormatsEnum.MP2T, new VideoFormat(VideoCodecsEnum.MP2T, 33, 90000)}, { SDPWellKnownMediaFormatsEnum.H263, new VideoFormat(VideoCodecsEnum.H263, 34, 90000)} - }; -} \ No newline at end of file + }.ToFrozenDictionary(); +} diff --git a/src/SIPSorceryMedia.Abstractions/MediaFormats/TextFormat.cs b/src/SIPSorceryMedia.Abstractions/MediaFormats/TextFormat.cs index ae010fac9d..fc7244c50a 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaFormats/TextFormat.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaFormats/TextFormat.cs @@ -31,32 +31,32 @@ public TextFormat(TextFormat format) : this(format.FormatID, format.FormatName, format.ClockRate, format.Parameters) { } - public TextFormat(TextCodecsEnum codec, int formatID, int clockRate = DEFAULT_CLOCK_RATE, string parameters = null) - : this(formatID, codec.ToString(), clockRate, parameters) + public TextFormat(TextCodecsEnum codec, int formatID, int clockRate = DEFAULT_CLOCK_RATE, string? parameters = null) + : this(formatID, codec.ToStringFast(), clockRate, parameters) { } /// /// Creates a new text format based on a dynamic codec (or an unsupported well known codec). /// - public TextFormat(int formatID, string formatName, int clockRate = DEFAULT_CLOCK_RATE, string parameters = null) + public TextFormat(int formatID, string formatName, int clockRate = DEFAULT_CLOCK_RATE, string? parameters = null) { if (formatID < 0) { // Note format ID's less than the dynamic start range are allowed as the codec list // does not currently support all well known codecs. - throw new ApplicationException("The format ID for an TextFormat must be greater than 0."); + throw new SipSorceryMediaException("The format ID for an TextFormat must be greater than 0."); } else if (formatID > DYNAMIC_ID_MAX) { - throw new ApplicationException($"The format ID for an TextFormat exceeded the maximum allowed vale of {DYNAMIC_ID_MAX}."); + throw new SipSorceryMediaException($"The format ID for an TextFormat exceeded the maximum allowed vale of {DYNAMIC_ID_MAX}."); } else if (string.IsNullOrWhiteSpace(formatName)) { - throw new ApplicationException($"The format name must be provided for a TextFormat."); + throw new SipSorceryMediaException($"The format name must be provided for a TextFormat."); } else if (clockRate <= 0) { - throw new ApplicationException($"The clock rate for a TextFormat must be greater than 0."); + throw new SipSorceryMediaException($"The clock rate for a TextFormat must be greater than 0."); } FormatID = formatID; @@ -64,7 +64,7 @@ public TextFormat(int formatID, string formatName, int clockRate = DEFAULT_CLOCK ClockRate = clockRate; Parameters = parameters; - if (Enum.TryParse(FormatName, out var textCodec)) + if (TextCodecsEnumExtensions.TryParse(FormatName, out var textCodec)) { Codec = textCodec; } @@ -106,5 +106,5 @@ public TextFormat(int formatID, string formatName, int clockRate = DEFAULT_CLOCK /// Example: /// a=fmtp:100 98/98/98 /// - public string Parameters { get; set; } + public string? Parameters { get; set; } } diff --git a/src/SIPSorceryMedia.Abstractions/MediaFormats/VideoFormat.cs b/src/SIPSorceryMedia.Abstractions/MediaFormats/VideoFormat.cs index 315a3c7e01..045a551a8e 100644 --- a/src/SIPSorceryMedia.Abstractions/MediaFormats/VideoFormat.cs +++ b/src/SIPSorceryMedia.Abstractions/MediaFormats/VideoFormat.cs @@ -14,8 +14,6 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- -using System; - namespace SIPSorceryMedia.Abstractions; public struct VideoFormat @@ -59,7 +57,7 @@ public struct VideoFormat /// Example: /// a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f" /// - public string Parameters { get; set; } + public string? Parameters { get; set; } private bool _isNonEmpty; @@ -73,8 +71,8 @@ public VideoFormat(SDPWellKnownMediaFormatsEnum wellKnown) : /// /// Creates a new video format based on a well known codec. /// - public VideoFormat(VideoCodecsEnum codec, int formatID, int clockRate = DEFAULT_CLOCK_RATE, string parameters = null) - : this(formatID, codec.ToString(), clockRate, parameters) + public VideoFormat(VideoCodecsEnum codec, int formatID, int clockRate = DEFAULT_CLOCK_RATE, string? parameters = null) + : this(formatID, codec.ToStringFast(), clockRate, parameters) { } public VideoFormat(VideoFormat format) @@ -84,25 +82,25 @@ public VideoFormat(VideoFormat format) /// /// Creates a new video format based on a dynamic codec (or an unsupported well known codec). /// - public VideoFormat(int formatID, string formatName, int clockRate = DEFAULT_CLOCK_RATE, string parameters = null) + public VideoFormat(int formatID, string formatName, int clockRate = DEFAULT_CLOCK_RATE, string? parameters = null) { if (formatID < 0) { // Note format ID's less than the dynamic start range are allowed as the codec list // does not currently support all well known codecs. - throw new ApplicationException("The format ID for an VideoFormat must be greater than 0."); + throw new SipSorceryMediaException("The format ID for an VideoFormat must be greater than 0."); } else if (formatID > DYNAMIC_ID_MAX) { - throw new ApplicationException($"The format ID for an VideoFormat exceeded the maximum allowed vale of {DYNAMIC_ID_MAX}."); + throw new SipSorceryMediaException($"The format ID for an VideoFormat exceeded the maximum allowed vale of {DYNAMIC_ID_MAX}."); } else if (string.IsNullOrWhiteSpace(formatName)) { - throw new ApplicationException($"The format name must be provided for a VideoFormat."); + throw new SipSorceryMediaException($"The format name must be provided for a VideoFormat."); } else if (clockRate <= 0) { - throw new ApplicationException($"The clock rate for a VideoFormat must be greater than 0."); + throw new SipSorceryMediaException($"The clock rate for a VideoFormat must be greater than 0."); } FormatID = formatID; @@ -111,7 +109,7 @@ public VideoFormat(int formatID, string formatName, int clockRate = DEFAULT_CLOC Parameters = parameters; _isNonEmpty = true; - if (Enum.TryParse(FormatName, true, out var videoCodec)) + if (VideoCodecsEnumExtensions.TryParse(FormatName, out var videoCodec)) { Codec = videoCodec; } @@ -122,4 +120,4 @@ public VideoFormat(int formatID, string formatName, int clockRate = DEFAULT_CLOC } public bool IsEmpty() => !_isNonEmpty; -} \ No newline at end of file +} diff --git a/src/SIPSorceryMedia.Abstractions/PixelConverter.cs b/src/SIPSorceryMedia.Abstractions/PixelConverter.cs index 9b9043a31e..d5bd3c5ca6 100644 --- a/src/SIPSorceryMedia.Abstractions/PixelConverter.cs +++ b/src/SIPSorceryMedia.Abstractions/PixelConverter.cs @@ -1,842 +1,791 @@ -using System; -using System.Collections.Generic; -#if NET8_0_OR_GREATER -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -#endif -using System.Threading.Tasks; - -namespace SIPSorceryMedia.Abstractions -{ - public class PixelConverter - { - private static readonly Dictionary _optDOP = new Dictionary(); - - [Obsolete("Use overload with stride parameter in order to deal with uneven dimensions.")] - public static byte[] ToI420(int width, int height, byte[] sample, VideoPixelFormatsEnum pixelFormat) - { - switch (pixelFormat) - { - case VideoPixelFormatsEnum.I420: - return sample; - case VideoPixelFormatsEnum.Bgra: - return PixelConverter.RGBAtoI420(sample, width, height, width * 4); - case VideoPixelFormatsEnum.Bgr: - return PixelConverter.BGRtoI420(sample, width, height, width * 3); - case VideoPixelFormatsEnum.Rgb: - return PixelConverter.RGBtoI420(sample, width, height, width * 3); - default: - throw new ApplicationException($"Pixel format {pixelFormat} does not have an I420 conversion implemented."); - } - } - - /// - /// Attempts to convert an image buffer into an I420 format. - /// - /// The width of the image in pixels. - /// The height of the image in pixels. - /// The stride of the image. Currently this method can only convert RGB and BGR - /// formats. For those formats the stride is typically: width x bytes per pixel. For example for - /// a 640x480 RGB sample stride=640x3. For a 640x480 BGRA sample stride=640x4. Note in some cases - /// the stride could be greater than the width x bytes per pixel. - /// The buffer containing the image data. - /// The pixel format of the image. - /// If successful a buffer containing an I420 formatted image sample. - public static byte[] ToI420(int width, int height, int stride, byte[] sample, VideoPixelFormatsEnum pixelFormat) - { - switch (pixelFormat) - { - case VideoPixelFormatsEnum.I420: - // No conversion needed. - return sample; - case VideoPixelFormatsEnum.Bgra: - return PixelConverter.BGRAtoI420(sample, width, height, stride); - case VideoPixelFormatsEnum.Bgr: - return PixelConverter.BGRtoI420(sample, width, height, stride); - case VideoPixelFormatsEnum.Rgba: - return PixelConverter.RGBAtoI420(sample, width, height, stride); - case VideoPixelFormatsEnum.Rgb: - return PixelConverter.RGBtoI420(sample, width, height, stride); - case VideoPixelFormatsEnum.NV12: - return PixelConverter.NV12toI420(sample, width, height); - default: - throw new ApplicationException($"Pixel format {pixelFormat} does not have an I420 conversion implemented."); - } - } - - [Obsolete("Use overload with stride parameter in order to deal with uneven dimensions.")] - public static byte[] RGBAtoI420(byte[] rgba, int width, int height) - { - return RGBAtoI420(rgba, width, height, width * 4); - } - - /// - /// Converts an RGBA sample to an I420 formatted sample. - /// - /// The RGBA image sample. - /// The width in pixels of the RGBA sample. - /// The height in pixels of the RGBA sample. - /// The stride of the RGBA sample. - /// The degree of parallelism for converting. - /// An I420 buffer representing the source image. - /// - /// https://docs.microsoft.com/en-us/previous-versions/visualstudio/hh394035(v=vs.105) - /// http://qiita.com/gomachan7/items/54d43693f943a0986e95 - /// - public static byte[] RGBAtoI420(byte[] rgba, int width, int height, int stride, int dop = 1) - { - if (rgba == null || rgba.Length < (stride * height)) - { - throw new ApplicationException($"RGBA buffer supplied to RGBAtoI420 was too small, expected {stride * height} but got {rgba?.Length}."); - } - - int ySize = width * height; - int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; - int uOffset = ySize; - int vOffset = ySize + uvSize / 2; - //int posn = 0; - - byte[] buffer = new byte[ySize + uvSize]; - - if (!_optDOP.ContainsKey(dop)) +using System; +using System.Collections.Generic; +#if NET8_0_OR_GREATER +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +#endif +using System.Threading.Tasks; + +namespace SIPSorceryMedia.Abstractions +{ + public class PixelConverter + { + private static readonly Dictionary _optDOP = new Dictionary(); + + [Obsolete("Use overload with stride parameter in order to deal with uneven dimensions.")] + public static byte[] ToI420(int width, int height, byte[] sample, VideoPixelFormatsEnum pixelFormat) + { + switch (pixelFormat) { - _optDOP[dop] = new ParallelOptions() { MaxDegreeOfParallelism = dop }; - } - - Parallel.For(0, height, _optDOP[dop], (row) => - { - int u, v, y; - int r, g, b; - - for (int col = 0; col < width; col++) - { - r = rgba[row * stride + col * 4] & 0xff; - g = rgba[row * stride + col * 4 + 1] & 0xff; - b = rgba[row * stride + col * 4 + 2] & 0xff; - //posn++; // Skip transparency byte. - - y = (int)(0.299 * r + 0.587 * g + 0.114 * b); - u = (int)(-0.147 * r - 0.289 * g + 0.436 * b) + 128; - v = (int)(0.615 * r - 0.515 * g - 0.100 * b) + 128; - - buffer[col + row * width] = (byte)(y > 255 ? 255 : y < 0 ? 0 : y); - - int uvposn = col / 2 + row / 2 * width / 2; - - buffer[uOffset + uvposn] = (byte)(u > 255 ? 255 : u < 0 ? 0 : u); - buffer[vOffset + uvposn] = (byte)(v > 255 ? 255 : v < 0 ? 0 : v); - } - }); - - return buffer; - } - - [Obsolete("Use overload with stride parameter in order to deal with uneven dimensions.")] - public static byte[] RGBtoI420(byte[] rgb, int width, int height) - { - return RGBtoI420(rgb, width, height, width * 3); - } - - /// - /// Converts an RGB sample to an I420 formatted sample. - /// - /// The RGB image sample. - /// The width in pixels of the RGB sample. - /// The height in pixels of the RGB sample. - /// The stride of the RGB sample. - /// The degree of parallelism for converting. - /// An I420 buffer representing the source image. - public static byte[] RGBtoI420(byte[] rgb, int width, int height, int stride, int dop = 1) - { - if (rgb == null || rgb.Length < (stride * height)) - { - throw new ApplicationException($"RGB buffer supplied to RGBtoI420 was too small, expected {stride * height} but got {rgb?.Length}."); - } - - int ySize = width * height; - int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; - int uOffset = ySize; - int vOffset = ySize + uvSize / 2; - //int posn = 0; - - byte[] buffer = new byte[ySize + uvSize]; - + case VideoPixelFormatsEnum.I420: + return sample; + case VideoPixelFormatsEnum.Bgra: + return PixelConverter.RGBAtoI420(sample, width, height, width * 4); + case VideoPixelFormatsEnum.Bgr: + return PixelConverter.BGRtoI420(sample, width, height, width * 3); + case VideoPixelFormatsEnum.Rgb: + return PixelConverter.RGBtoI420(sample, width, height, width * 3); + default: + throw new ApplicationException($"Pixel format {pixelFormat} does not have an I420 conversion implemented."); + } + } + + /// + /// Attempts to convert an image buffer into an I420 format. + /// + /// The width of the image in pixels. + /// The height of the image in pixels. + /// The stride of the image. Currently this method can only convert RGB and BGR + /// formats. For those formats the stride is typically: width x bytes per pixel. For example for + /// a 640x480 RGB sample stride=640x3. For a 640x480 BGRA sample stride=640x4. Note in some cases + /// the stride could be greater than the width x bytes per pixel. + /// The buffer containing the image data. + /// The pixel format of the image. + /// If successful a buffer containing an I420 formatted image sample. + public static byte[] ToI420(int width, int height, int stride, byte[] sample, VideoPixelFormatsEnum pixelFormat) + => pixelFormat switch + { + VideoPixelFormatsEnum.I420 => sample,// No conversion needed. + VideoPixelFormatsEnum.Bgra => PixelConverter.BGRAtoI420(sample, width, height, stride), + VideoPixelFormatsEnum.Bgr => PixelConverter.BGRtoI420(sample, width, height, stride), + VideoPixelFormatsEnum.Rgba => PixelConverter.RGBAtoI420(sample, width, height, stride), + VideoPixelFormatsEnum.Rgb => PixelConverter.RGBtoI420(sample, width, height, stride), + VideoPixelFormatsEnum.NV12 => PixelConverter.NV12toI420(sample, width, height), + _ => throw new SipSorceryMediaException($"Pixel format {pixelFormat} does not have an I420 conversion implemented."), + }; + + /// + /// Converts an RGBA sample to an I420 formatted sample. + /// + /// The RGBA image sample. + /// The width in pixels of the RGBA sample. + /// The height in pixels of the RGBA sample. + /// The stride of the RGBA sample. + /// The degree of parallelism for converting. + /// An I420 buffer representing the source image. + /// + /// https://docs.microsoft.com/en-us/previous-versions/visualstudio/hh394035(v=vs.105) + /// http://qiita.com/gomachan7/items/54d43693f943a0986e95 + /// + public static byte[] RGBAtoI420(byte[] rgba, int width, int height, int stride, int dop = 1) + { + if (rgba is null || rgba.Length < (stride * height)) + { + throw new SipSorceryMediaException($"RGBA buffer supplied to RGBAtoI420 was too small, expected {stride * height} but got {rgba?.Length}."); + } + + int ySize = width * height; + int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; + int uOffset = ySize; + int vOffset = ySize + uvSize / 2; + //int posn = 0; + + byte[] buffer = new byte[ySize + uvSize]; + if (!_optDOP.ContainsKey(dop)) { _optDOP[dop] = new ParallelOptions() { MaxDegreeOfParallelism = dop }; - } - - Parallel.For(0, height, _optDOP[dop], (row) => - { - int u, v, y; - int r, g, b; - - for (int col = 0; col < width; col++) - { - r = rgb[row * stride + col * 3] & 0xff; - g = rgb[row * stride + col * 3 + 1] & 0xff; - b = rgb[row * stride + col * 3 + 2] & 0xff; - - y = (int)(0.299 * r + 0.587 * g + 0.114 * b); - u = (int)(-0.147 * r - 0.289 * g + 0.436 * b) + 128; - v = (int)(0.615 * r - 0.515 * g - 0.100 * b) + 128; - - buffer[col + row * width] = (byte)(y > 255 ? 255 : y < 0 ? 0 : y); - - int uvposn = col / 2 + row / 2 * width / 2; - - buffer[uOffset + uvposn] = (byte)(u > 255 ? 255 : u < 0 ? 0 : u); - buffer[vOffset + uvposn] = (byte)(v > 255 ? 255 : v < 0 ? 0 : v); - } - }); - - return buffer; - } - - [Obsolete("Use overload with stride parameter in order to deal with uneven dimensions.")] - public static byte[] BGRtoI420(byte[] bgr, int width, int height) - { - return BGRtoI420(bgr, width, height, width * 3); - } - - /// - /// Converts a BGR sample to an I420 formatted sample. - /// - /// The BGR image sample. - /// The width in pixels of the BGR sample. - /// The height in pixels of the BGR sample. - /// The stride of the BGR sample. - /// The degree of parallelism for converting. - /// An I420 buffer representing the source image. - public static byte[] BGRtoI420(byte[] bgr, int width, int height, int stride, int dop = 1) - { - if (bgr == null || bgr.Length < (stride * height)) - { - throw new ApplicationException($"BGR buffer supplied to BGRtoI420 was too small, expected {stride * height} but got {bgr?.Length}."); - } - - int ySize = width * height; - int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; - int uOffset = ySize; - int vOffset = ySize + uvSize / 2; - //int posn = 0; - - byte[] buffer = new byte[ySize + uvSize]; - + } + + Parallel.For(0, height, _optDOP[dop], (row) => + { + int u, v, y; + int r, g, b; + + for (int col = 0; col < width; col++) + { + r = rgba[row * stride + col * 4] & 0xff; + g = rgba[row * stride + col * 4 + 1] & 0xff; + b = rgba[row * stride + col * 4 + 2] & 0xff; + //posn++; // Skip transparency byte. + + y = (int)(0.299 * r + 0.587 * g + 0.114 * b); + u = (int)(-0.147 * r - 0.289 * g + 0.436 * b) + 128; + v = (int)(0.615 * r - 0.515 * g - 0.100 * b) + 128; + + buffer[col + row * width] = (byte)(y > 255 ? 255 : y < 0 ? 0 : y); + + int uvposn = col / 2 + row / 2 * width / 2; + + buffer[uOffset + uvposn] = (byte)(u > 255 ? 255 : u < 0 ? 0 : u); + buffer[vOffset + uvposn] = (byte)(v > 255 ? 255 : v < 0 ? 0 : v); + } + }); + + return buffer; + } + + /// + /// Converts an RGB sample to an I420 formatted sample. + /// + /// The RGB image sample. + /// The width in pixels of the RGB sample. + /// The height in pixels of the RGB sample. + /// The stride of the RGB sample. + /// The degree of parallelism for converting. + /// An I420 buffer representing the source image. + public static byte[] RGBtoI420(byte[] rgb, int width, int height, int stride, int dop = 1) + { + if (rgb is null || rgb.Length < (stride * height)) + { + throw new SipSorceryMediaException($"RGB buffer supplied to RGBtoI420 was too small, expected {stride * height} but got {rgb?.Length}."); + } + + int ySize = width * height; + int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; + int uOffset = ySize; + int vOffset = ySize + uvSize / 2; + //int posn = 0; + + byte[] buffer = new byte[ySize + uvSize]; + if (!_optDOP.ContainsKey(dop)) { _optDOP[dop] = new ParallelOptions() { MaxDegreeOfParallelism = dop }; - } - - Parallel.For(0, height, _optDOP[dop], (row) => - { - int u, v, y; - int r, g, b; - - for (int col = 0; col < width; col++) - { - b = bgr[row * stride + col * 3] & 0xff; - g = bgr[row * stride + col * 3 + 1] & 0xff; - r = bgr[row * stride + col * 3 + 2] & 0xff; - - y = (int)(0.299 * r + 0.587 * g + 0.114 * b); - u = (int)(-0.147 * r - 0.289 * g + 0.436 * b) + 128; - v = (int)(0.615 * r - 0.515 * g - 0.100 * b) + 128; - - buffer[col + row * width] = (byte)(y > 255 ? 255 : y < 0 ? 0 : y); - - int uvposn = col / 2 + row / 2 * width / 2; - - buffer[uOffset + uvposn] = (byte)(u > 255 ? 255 : u < 0 ? 0 : u); - buffer[vOffset + uvposn] = (byte)(v > 255 ? 255 : v < 0 ? 0 : v); - } - }); - - return buffer; - } - - /// - /// Converts a BGRA sample to an I420 formatted sample. - /// - /// The BGRA image sample. - /// The width in pixels of the BGRA sample. - /// The height in pixels of the BGRA sample. - /// The stride of the BGRA sample. - /// The degree of parallelism for converting. - /// An I420 buffer representing the source image. - public static byte[] BGRAtoI420(byte[] bgra, int width, int height, int stride, int dop = 1) - { - if (bgra == null || bgra.Length < (stride * height)) - { - throw new ApplicationException($"BGRA buffer supplied to BGRAtoI420 was too small, expected {stride * height} but got {bgra?.Length}."); - } - - int ySize = width * height; - int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; - int uOffset = ySize; - int vOffset = ySize + uvSize / 2; - - byte[] buffer = new byte[ySize + uvSize]; - + } + + Parallel.For(0, height, _optDOP[dop], (row) => + { + int u, v, y; + int r, g, b; + + for (int col = 0; col < width; col++) + { + r = rgb[row * stride + col * 3] & 0xff; + g = rgb[row * stride + col * 3 + 1] & 0xff; + b = rgb[row * stride + col * 3 + 2] & 0xff; + + y = (int)(0.299 * r + 0.587 * g + 0.114 * b); + u = (int)(-0.147 * r - 0.289 * g + 0.436 * b) + 128; + v = (int)(0.615 * r - 0.515 * g - 0.100 * b) + 128; + + buffer[col + row * width] = (byte)(y > 255 ? 255 : y < 0 ? 0 : y); + + int uvposn = col / 2 + row / 2 * width / 2; + + buffer[uOffset + uvposn] = (byte)(u > 255 ? 255 : u < 0 ? 0 : u); + buffer[vOffset + uvposn] = (byte)(v > 255 ? 255 : v < 0 ? 0 : v); + } + }); + + return buffer; + } + + /// + /// Converts a BGR sample to an I420 formatted sample. + /// + /// The BGR image sample. + /// The width in pixels of the BGR sample. + /// The height in pixels of the BGR sample. + /// The stride of the BGR sample. + /// The degree of parallelism for converting. + /// An I420 buffer representing the source image. + public static byte[] BGRtoI420(byte[] bgr, int width, int height, int stride, int dop = 1) + { + if (bgr is null || bgr.Length < (stride * height)) + { + throw new SipSorceryMediaException($"BGR buffer supplied to BGRtoI420 was too small, expected {stride * height} but got {bgr?.Length}."); + } + + int ySize = width * height; + int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; + int uOffset = ySize; + int vOffset = ySize + uvSize / 2; + //int posn = 0; + + byte[] buffer = new byte[ySize + uvSize]; + if (!_optDOP.ContainsKey(dop)) { _optDOP[dop] = new ParallelOptions() { MaxDegreeOfParallelism = dop }; - } - - Parallel.For(0, height, _optDOP[dop], (row) => - { - int u, v, y; - int r, g, b; - - for (int col = 0; col < width; col++) - { - // BGRA: Byte order is Blue, Green, Red, Alpha. - b = bgra[row * stride + col * 4] & 0xff; - g = bgra[row * stride + col * 4 + 1] & 0xff; - r = bgra[row * stride + col * 4 + 2] & 0xff; - // Alpha at index 3 is ignored. - - y = (int)(0.299 * r + 0.587 * g + 0.114 * b); - u = (int)(-0.147 * r - 0.289 * g + 0.436 * b) + 128; - v = (int)(0.615 * r - 0.515 * g - 0.100 * b) + 128; - - buffer[col + row * width] = (byte)(y > 255 ? 255 : y < 0 ? 0 : y); - - int uvposn = (col / 2) + (row / 2) * (width / 2); - buffer[uOffset + uvposn] = (byte)(u > 255 ? 255 : u < 0 ? 0 : u); - buffer[vOffset + uvposn] = (byte)(v > 255 ? 255 : v < 0 ? 0 : v); - } - }); - - return buffer; - } - - - [Obsolete("Use overload with stride parameter in order to deal with uneven dimensions.")] - public static byte[] I420toRGB(byte[] data, int width, int height) - { - return I420toRGB(data, width, height, out _); - } - - /// - /// Converts an I420 sample to an RGB formatted sample. - /// - /// The I420 image sample. - /// The width in pixels of the I420 sample. - /// The height in pixels of the I420 sample. - /// The stride to use for the desintation RGB sample. - /// The degree of parallelism for converting. - /// An RGB buffer representing the source image. - public static byte[] I420toRGB(byte[] data, int width, int height, out int stride, int dop = 1) - { - int ySize = width * height; - int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; - if (data == null || data.Length < (ySize + uvSize)) - { - throw new ApplicationException($"I420 buffer supplied to I420toRGB was too small, expected {ySize + uvSize} but got {data?.Length}."); - } - - int uOffset = ySize; - int vOffset = ySize + ySize / 4; - int lclStride = stride = (width * 3 + 3) / 4 * 4; - byte[] rgb = new byte[height * stride]; - //int posn = 0; - + } + + Parallel.For(0, height, _optDOP[dop], (row) => + { + int u, v, y; + int r, g, b; + + for (int col = 0; col < width; col++) + { + b = bgr[row * stride + col * 3] & 0xff; + g = bgr[row * stride + col * 3 + 1] & 0xff; + r = bgr[row * stride + col * 3 + 2] & 0xff; + + y = (int)(0.299 * r + 0.587 * g + 0.114 * b); + u = (int)(-0.147 * r - 0.289 * g + 0.436 * b) + 128; + v = (int)(0.615 * r - 0.515 * g - 0.100 * b) + 128; + + buffer[col + row * width] = (byte)(y > 255 ? 255 : y < 0 ? 0 : y); + + int uvposn = col / 2 + row / 2 * width / 2; + + buffer[uOffset + uvposn] = (byte)(u > 255 ? 255 : u < 0 ? 0 : u); + buffer[vOffset + uvposn] = (byte)(v > 255 ? 255 : v < 0 ? 0 : v); + } + }); + + return buffer; + } + + /// + /// Converts a BGRA sample to an I420 formatted sample. + /// + /// The BGRA image sample. + /// The width in pixels of the BGRA sample. + /// The height in pixels of the BGRA sample. + /// The stride of the BGRA sample. + /// The degree of parallelism for converting. + /// An I420 buffer representing the source image. + public static byte[] BGRAtoI420(byte[] bgra, int width, int height, int stride, int dop = 1) + { + if (bgra is null || bgra.Length < (stride * height)) + { + throw new SipSorceryMediaException($"BGRA buffer supplied to BGRAtoI420 was too small, expected {stride * height} but got {bgra?.Length}."); + } + + int ySize = width * height; + int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; + int uOffset = ySize; + int vOffset = ySize + uvSize / 2; + + byte[] buffer = new byte[ySize + uvSize]; + if (!_optDOP.ContainsKey(dop)) { _optDOP[dop] = new ParallelOptions() { MaxDegreeOfParallelism = dop }; - } - - Parallel.For(0, height, _optDOP[dop], (row) => - { - int u, v, y; - int r, g, b; - - for (int col = 0; col < width; col++) - { - y = data[col + row * width]; - int uvposn = col / 2 + row / 2 * width / 2; - - u = data[uOffset + uvposn] - 128; - v = data[vOffset + uvposn] - 128; - - r = (int)(y + 1.140 * v); - g = (int)(y - 0.395 * u - 0.581 * v); - b = (int)(y + 2.302 * u); - - rgb[row * lclStride + col * 3] = (byte)(r > 255 ? 255 : r < 0 ? 0 : r); - rgb[row * lclStride + col * 3 + 1] = (byte)(g > 255 ? 255 : g < 0 ? 0 : g); - rgb[row * lclStride + col * 3 + 2] = (byte)(b > 255 ? 255 : b < 0 ? 0 : b); - } - }); - - return rgb; - } - - [Obsolete("Use overload with stride parameter in order to deal with uneven dimensions.")] - public static byte[] I420toBGR(byte[] data, int width, int height) - { - return I420toBGR(data, width, height, out _); - } - - /// - /// Converts an I420 sample to an BGR formatted sample. - /// - /// The I420 image sample. - /// The width in pixels of the I420 sample. - /// The height in pixels of the I420 sample. - /// The stride to use for the desintation BGR sample. - /// The degree of parallelism for converting. - /// A BGR buffer representing the source image. - public static byte[] I420toBGR(byte[] data, int width, int height, out int stride, int dop = 1) - { - int ySize = width * height; - int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; - if (data == null || data.Length < (ySize + uvSize)) - { - throw new ApplicationException($"I420 buffer supplied to I420toBGR was too small, expected {ySize + uvSize} but got {data?.Length}."); - } - - int uOffset = ySize; - int vOffset = ySize + uvSize / 2; - var lclStride = stride = (width * 3 + 3) / 4 * 4; - byte[] bgr = new byte[height * stride]; - //int posn = 0; - + } + + Parallel.For(0, height, _optDOP[dop], (row) => + { + int u, v, y; + int r, g, b; + + for (int col = 0; col < width; col++) + { + // BGRA: Byte order is Blue, Green, Red, Alpha. + b = bgra[row * stride + col * 4] & 0xff; + g = bgra[row * stride + col * 4 + 1] & 0xff; + r = bgra[row * stride + col * 4 + 2] & 0xff; + // Alpha at index 3 is ignored. + + y = (int)(0.299 * r + 0.587 * g + 0.114 * b); + u = (int)(-0.147 * r - 0.289 * g + 0.436 * b) + 128; + v = (int)(0.615 * r - 0.515 * g - 0.100 * b) + 128; + + buffer[col + row * width] = (byte)(y > 255 ? 255 : y < 0 ? 0 : y); + + int uvposn = (col / 2) + (row / 2) * (width / 2); + buffer[uOffset + uvposn] = (byte)(u > 255 ? 255 : u < 0 ? 0 : u); + buffer[vOffset + uvposn] = (byte)(v > 255 ? 255 : v < 0 ? 0 : v); + } + }); + + return buffer; + } + + /// + /// Converts an I420 sample to an RGB formatted sample. + /// + /// The I420 image sample. + /// The width in pixels of the I420 sample. + /// The height in pixels of the I420 sample. + /// The stride to use for the desintation RGB sample. + /// The degree of parallelism for converting. + /// An RGB buffer representing the source image. + public static byte[] I420toRGB(byte[] data, int width, int height, out int stride, int dop = 1) + { + int ySize = width * height; + int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; + if (data is null || data.Length < (ySize + uvSize)) + { + throw new SipSorceryMediaException($"I420 buffer supplied to I420toRGB was too small, expected {ySize + uvSize} but got {data?.Length}."); + } + + int uOffset = ySize; + int vOffset = ySize + ySize / 4; + int lclStride = stride = (width * 3 + 3) / 4 * 4; + byte[] rgb = new byte[height * stride]; + //int posn = 0; + if (!_optDOP.ContainsKey(dop)) { _optDOP[dop] = new ParallelOptions() { MaxDegreeOfParallelism = dop }; - } - - Parallel.For(0, height, _optDOP[dop], (row) => - { - int u, v, y; - int r, g, b; - - for (int col = 0; col < width; col++) - { - y = data[col + row * width]; - int uvposn = col / 2 + row / 2 * width / 2; - - u = data[uOffset + uvposn] - 128; - v = data[vOffset + uvposn] - 128; - - b = (int)(y + 1.140 * v); - g = (int)(y - 0.395 * u - 0.581 * v); - r = (int)(y + 2.302 * u); - - bgr[row * lclStride + col * 3] = (byte)(r > 255 ? 255 : r < 0 ? 0 : r); - bgr[row * lclStride + col * 3 + 1] = (byte)(g > 255 ? 255 : g < 0 ? 0 : g); - bgr[row * lclStride + col * 3 + 2] = (byte)(b > 255 ? 255 : b < 0 ? 0 : b); - } - }); - - return bgr; - } - - [Obsolete("Use overload with stride parameter in order to deal with uneven dimensions.")] - public static byte[] NV12toBGR(byte[] data, int width, int height) - { - return NV12toBGR(data, width, height, width * 3); - } - - /// - /// Converts an NV12 sample to an BGR formatted sample. - /// - /// The NV12 image sample. - /// The width in pixels of the NV12 sample. - /// The height in pixels of the NV12 sample. - /// The stride to use for the desintation BGR sample. - /// The degree of parallelism for converting. - /// A BGR buffer representing the source image. - public static byte[] NV12toBGR(byte[] data, int width, int height, int stride, int dop = 1) - { - int ySize = width * height; - int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; - if (data == null || data.Length < (ySize + uvSize)) - { - throw new ApplicationException($"NV12 buffer supplied to NV12toBGR was too small, expected {ySize + uvSize} but got {data?.Length}."); - } - - int uvOffset = ySize; - byte[] bgr = new byte[height * stride]; - //int posn = 0; - + } + + Parallel.For(0, height, _optDOP[dop], (row) => + { + int u, v, y; + int r, g, b; + + for (int col = 0; col < width; col++) + { + y = data[col + row * width]; + int uvposn = col / 2 + row / 2 * width / 2; + + u = data[uOffset + uvposn] - 128; + v = data[vOffset + uvposn] - 128; + + r = (int)(y + 1.140 * v); + g = (int)(y - 0.395 * u - 0.581 * v); + b = (int)(y + 2.302 * u); + + rgb[row * lclStride + col * 3] = (byte)(r > 255 ? 255 : r < 0 ? 0 : r); + rgb[row * lclStride + col * 3 + 1] = (byte)(g > 255 ? 255 : g < 0 ? 0 : g); + rgb[row * lclStride + col * 3 + 2] = (byte)(b > 255 ? 255 : b < 0 ? 0 : b); + } + }); + + return rgb; + } + + /// + /// Converts an I420 sample to an BGR formatted sample. + /// + /// The I420 image sample. + /// The width in pixels of the I420 sample. + /// The height in pixels of the I420 sample. + /// The stride to use for the desintation BGR sample. + /// The degree of parallelism for converting. + /// A BGR buffer representing the source image. + public static byte[] I420toBGR(byte[] data, int width, int height, out int stride, int dop = 1) + { + int ySize = width * height; + int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; + if (data is null || data.Length < (ySize + uvSize)) + { + throw new SipSorceryMediaException($"I420 buffer supplied to I420toBGR was too small, expected {ySize + uvSize} but got {data?.Length}."); + } + + int uOffset = ySize; + int vOffset = ySize + uvSize / 2; + var lclStride = stride = (width * 3 + 3) / 4 * 4; + byte[] bgr = new byte[height * stride]; + //int posn = 0; + if (!_optDOP.ContainsKey(dop)) { _optDOP[dop] = new ParallelOptions() { MaxDegreeOfParallelism = dop }; - } - - Parallel.For(0, height, _optDOP[dop], (row) => - { - int u, v, y; - int r, g, b; - - for (int col = 0; col < width; col++) - { - y = data[col + row * width]; - int uvposn = row / 2 * width + col / 2 * 2; - - u = data[uvOffset + uvposn] - 128; - v = data[uvOffset + uvposn + 1] - 128; - - r = (int)(y + 1.140 * v); - g = (int)(y - 0.395 * u - 0.581 * v); - b = (int)(y + 2.302 * u); - - bgr[row * stride + col * 3] = (byte)(b > 255 ? 255 : b < 0 ? 0 : b); - bgr[row * stride + col * 3 + 1] = (byte)(g > 255 ? 255 : g < 0 ? 0 : g); - bgr[row * stride + col * 3 + 2] = (byte)(r > 255 ? 255 : r < 0 ? 0 : r); - } - }); - - return bgr; - } - - /// - /// Converts an NV12 sample to an I420 formatted sample. - /// NV12: Y plane followed by interleaved UV plane (UVUVUV...). - /// I420: Y plane followed by U plane, then V plane (planar format). - /// - /// The NV12 image sample. - /// The width in pixels of the NV12 sample. - /// The height in pixels of the NV12 sample. - /// The degree of parallelism for converting. - /// An I420 buffer representing the source image. - public static byte[] NV12toI420(byte[] nv12, int width, int height, int dop = 1) - { - int ySize = width * height; - int uvWidth = (width + 1) / 2; - int uvHeight = (height + 1) / 2; - int uvSize = uvWidth * uvHeight * 2; - - if (nv12 == null || nv12.Length < (ySize + uvSize)) - { - throw new ApplicationException($"NV12 buffer supplied to NV12toI420 was too small, expected {ySize + uvSize} but got {nv12?.Length}."); - } - - byte[] i420 = new byte[ySize + uvSize]; - - // Copy Y plane (same layout in both formats). - Buffer.BlockCopy(nv12, 0, i420, 0, ySize); - - int nv12UvOffset = ySize; - int i420UOffset = ySize; - int i420VOffset = ySize + uvWidth * uvHeight; - -#if NET8_0_OR_GREATER - // Use SIMD for de-interleaving UV plane when available - DeinterleaveUVSimd(nv12, nv12UvOffset, i420, i420UOffset, i420VOffset, uvWidth, uvHeight); -#else + } + + Parallel.For(0, height, _optDOP[dop], (row) => + { + int u, v, y; + int r, g, b; + + for (int col = 0; col < width; col++) + { + y = data[col + row * width]; + int uvposn = col / 2 + row / 2 * width / 2; + + u = data[uOffset + uvposn] - 128; + v = data[vOffset + uvposn] - 128; + + b = (int)(y + 1.140 * v); + g = (int)(y - 0.395 * u - 0.581 * v); + r = (int)(y + 2.302 * u); + + bgr[row * lclStride + col * 3] = (byte)(r > 255 ? 255 : r < 0 ? 0 : r); + bgr[row * lclStride + col * 3 + 1] = (byte)(g > 255 ? 255 : g < 0 ? 0 : g); + bgr[row * lclStride + col * 3 + 2] = (byte)(b > 255 ? 255 : b < 0 ? 0 : b); + } + }); + + return bgr; + } + + /// + /// Converts an NV12 sample to an BGR formatted sample. + /// + /// The NV12 image sample. + /// The width in pixels of the NV12 sample. + /// The height in pixels of the NV12 sample. + /// The stride to use for the desintation BGR sample. + /// The degree of parallelism for converting. + /// A BGR buffer representing the source image. + public static byte[] NV12toBGR(byte[] data, int width, int height, int stride, int dop = 1) + { + int ySize = width * height; + int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; + if (data is null || data.Length < (ySize + uvSize)) + { + throw new SipSorceryMediaException($"NV12 buffer supplied to NV12toBGR was too small, expected {ySize + uvSize} but got {data?.Length}."); + } + + int uvOffset = ySize; + byte[] bgr = new byte[height * stride]; + //int posn = 0; + if (!_optDOP.ContainsKey(dop)) { _optDOP[dop] = new ParallelOptions() { MaxDegreeOfParallelism = dop }; - } - - // De-interleave UV plane: NV12 has UV interleaved, I420 has separate U and V planes. - Parallel.For(0, uvHeight, _optDOP[dop], (row) => - { - for (int col = 0; col < uvWidth; col++) - { - int nv12Posn = nv12UvOffset + row * uvWidth * 2 + col * 2; - int i420UPosn = i420UOffset + row * uvWidth + col; - int i420VPosn = i420VOffset + row * uvWidth + col; - - i420[i420UPosn] = nv12[nv12Posn]; // U - i420[i420VPosn] = nv12[nv12Posn + 1]; // V - } - }); -#endif - - return i420; - } - -#if NET8_0_OR_GREATER - /// - /// SIMD-optimized de-interleave of UV plane from NV12 format (UVUVUV...) to I420 format (separate U and V planes). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void DeinterleaveUVSimd(byte[] src, int srcOffset, byte[] dst, int dstUOffset, int dstVOffset, int uvWidth, int uvHeight) - { - int totalUV = uvWidth * uvHeight; - int i = 0; - - ref byte srcRef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(src), srcOffset); - ref byte dstURef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(dst), dstUOffset); - ref byte dstVRef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(dst), dstVOffset); - - // Process 32 UV pairs at a time (64 bytes) using Vector256 - if (Vector256.IsHardwareAccelerated) - { - // Indices for de-interleaving: extract U values (even positions) and V values (odd positions) - // For byte pairs: [U0,V0,U1,V1,U2,V2,...] -> U: [U0,U1,U2,...], V: [V0,V1,V2,...] - for (; i <= totalUV - 32; i += 32) - { - // Load 64 bytes (32 UV pairs) - var uv0 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i * 2)); - var uv1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i * 2 + 32)); - - // Use shuffle to de-interleave - extract even bytes (U) and odd bytes (V) - var (u0, v0) = DeinterleaveVector256(uv0); - var (u1, v1) = DeinterleaveVector256(uv1); - - // Combine into 256-bit vectors - var u = Vector256.Create(u0, u1); - var v = Vector256.Create(v0, v1); - - u.StoreUnsafe(ref Unsafe.Add(ref dstURef, i)); - v.StoreUnsafe(ref Unsafe.Add(ref dstVRef, i)); - } - } - - // Process 16 UV pairs at a time (32 bytes) using Vector128 - if (Vector128.IsHardwareAccelerated) - { - for (; i <= totalUV - 16; i += 16) - { - // Load 32 bytes (16 UV pairs) - var uv0 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i * 2)); - var uv1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i * 2 + 16)); - - var (u0, v0) = DeinterleaveVector128(uv0); - var (u1, v1) = DeinterleaveVector128(uv1); - - var u = Vector128.Create(u0, u1); - var v = Vector128.Create(v0, v1); - - u.StoreUnsafe(ref Unsafe.Add(ref dstURef, i)); - v.StoreUnsafe(ref Unsafe.Add(ref dstVRef, i)); - } - } - - // Handle remaining elements with scalar code - for (; i < totalUV; i++) - { - Unsafe.Add(ref dstURef, i) = Unsafe.Add(ref srcRef, i * 2); - Unsafe.Add(ref dstVRef, i) = Unsafe.Add(ref srcRef, i * 2 + 1); - } - } - - /// - /// De-interleave 16 byte pairs from a Vector256 into two Vector128 containing U and V values. - /// Input: [U0,V0,U1,V1,U2,V2,...,U15,V15] - /// Output: U=[U0,U1,...,U15], V=[V0,V1,...,V15] - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (Vector128 u, Vector128 v) DeinterleaveVector256(Vector256 uv) - { - // Extract low and high 128-bit halves - var low = uv.GetLower(); // [U0,V0,U1,V1,U2,V2,U3,V3,U4,V4,U5,V5,U6,V6,U7,V7] - var high = uv.GetUpper(); // [U8,V8,U9,V9,U10,V10,U11,V11,U12,V12,U13,V13,U14,V14,U15,V15] - - var (uLow, vLow) = DeinterleaveVector128(low); - var (uHigh, vHigh) = DeinterleaveVector128(high); - - // Combine halves - var u = Vector128.Create(uLow, uHigh); - var v = Vector128.Create(vLow, vHigh); - - return (u, v); - } - - /// - /// De-interleave 8 byte pairs from a Vector128 into two Vector64 containing U and V values. - /// Input: [U0,V0,U1,V1,U2,V2,U3,V3,U4,V4,U5,V5,U6,V6,U7,V7] - /// Output: U=[U0,U1,U2,U3,U4,U5,U6,U7], V=[V0,V1,V2,V3,V4,V5,V6,V7] - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (Vector64 u, Vector64 v) DeinterleaveVector128(Vector128 uv) - { - // Shuffle bytes to gather all U values in low 64 bits and V values in high 64 bits - // This shuffle pattern extracts even indices (U) to the first 8 bytes and odd indices (V) to the last 8 bytes - var shuffleIndices = Vector128.Create( - (byte)0, 2, 4, 6, 8, 10, 12, 14, // U indices (even positions) - 1, 3, 5, 7, 9, 11, 13, 15 // V indices (odd positions) - ); - - var shuffled = Vector128.Shuffle(uv, shuffleIndices); - - return (shuffled.GetLower(), shuffled.GetUpper()); - } -#endif - - /// - /// Converts an I420 sample to an NV12 formatted sample. - /// I420: Y plane followed by U plane, then V plane (planar format). - /// NV12: Y plane followed by interleaved UV plane (UVUVUV...). - /// - /// The I420 image sample. - /// The width in pixels of the I420 sample. - /// The height in pixels of the I420 sample. - /// The degree of parallelism for converting. - /// An NV12 buffer representing the source image. - public static byte[] I420toNV12(byte[] i420, int width, int height, int dop = 1) - { - int ySize = width * height; - int uvWidth = (width + 1) / 2; - int uvHeight = (height + 1) / 2; - int uvSize = uvWidth * uvHeight * 2; - - if (i420 == null || i420.Length < (ySize + uvSize)) - { - throw new ApplicationException($"I420 buffer supplied to I420toNV12 was too small, expected {ySize + uvSize} but got {i420?.Length}."); - } - - byte[] nv12 = new byte[ySize + uvSize]; - - // Copy Y plane (same layout in both formats). - Buffer.BlockCopy(i420, 0, nv12, 0, ySize); - - int i420UOffset = ySize; - int i420VOffset = ySize + uvWidth * uvHeight; - int nv12UvOffset = ySize; - -#if NET8_0_OR_GREATER - // Use SIMD for interleaving U and V planes when available - InterleaveUVSimd(i420, i420UOffset, i420VOffset, nv12, nv12UvOffset, uvWidth, uvHeight); -#else + } + + Parallel.For(0, height, _optDOP[dop], (row) => + { + int u, v, y; + int r, g, b; + + for (int col = 0; col < width; col++) + { + y = data[col + row * width]; + int uvposn = row / 2 * width + col / 2 * 2; + + u = data[uvOffset + uvposn] - 128; + v = data[uvOffset + uvposn + 1] - 128; + + r = (int)(y + 1.140 * v); + g = (int)(y - 0.395 * u - 0.581 * v); + b = (int)(y + 2.302 * u); + + bgr[row * stride + col * 3] = (byte)(b > 255 ? 255 : b < 0 ? 0 : b); + bgr[row * stride + col * 3 + 1] = (byte)(g > 255 ? 255 : g < 0 ? 0 : g); + bgr[row * stride + col * 3 + 2] = (byte)(r > 255 ? 255 : r < 0 ? 0 : r); + } + }); + + return bgr; + } + + /// + /// Converts an NV12 sample to an I420 formatted sample. + /// NV12: Y plane followed by interleaved UV plane (UVUVUV...). + /// I420: Y plane followed by U plane, then V plane (planar format). + /// + /// The NV12 image sample. + /// The width in pixels of the NV12 sample. + /// The height in pixels of the NV12 sample. + /// The degree of parallelism for converting. + /// An I420 buffer representing the source image. + public static byte[] NV12toI420(byte[] nv12, int width, int height, int dop = 1) + { + int ySize = width * height; + int uvWidth = (width + 1) / 2; + int uvHeight = (height + 1) / 2; + int uvSize = uvWidth * uvHeight * 2; + + if (nv12 == null || nv12.Length < (ySize + uvSize)) + { + throw new ApplicationException($"NV12 buffer supplied to NV12toI420 was too small, expected {ySize + uvSize} but got {nv12?.Length}."); + } + + byte[] i420 = new byte[ySize + uvSize]; + + // Copy Y plane (same layout in both formats). + Buffer.BlockCopy(nv12, 0, i420, 0, ySize); + + int nv12UvOffset = ySize; + int i420UOffset = ySize; + int i420VOffset = ySize + uvWidth * uvHeight; + +#if NET8_0_OR_GREATER + // Use SIMD for de-interleaving UV plane when available + DeinterleaveUVSimd(nv12, nv12UvOffset, i420, i420UOffset, i420VOffset, uvWidth, uvHeight); +#else if (!_optDOP.ContainsKey(dop)) + _optDOP[dop] = new ParallelOptions() { MaxDegreeOfParallelism = dop }; + + // De-interleave UV plane: NV12 has UV interleaved, I420 has separate U and V planes. + Parallel.For(0, uvHeight, _optDOP[dop], (row) => + { + for (int col = 0; col < uvWidth; col++) + { + int nv12Posn = nv12UvOffset + row * uvWidth * 2 + col * 2; + int i420UPosn = i420UOffset + row * uvWidth + col; + int i420VPosn = i420VOffset + row * uvWidth + col; + + i420[i420UPosn] = nv12[nv12Posn]; // U + i420[i420VPosn] = nv12[nv12Posn + 1]; // V + } + }); +#endif + + return i420; + } + +#if NET8_0_OR_GREATER + /// + /// SIMD-optimized de-interleave of UV plane from NV12 format (UVUVUV...) to I420 format (separate U and V planes). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DeinterleaveUVSimd(byte[] src, int srcOffset, byte[] dst, int dstUOffset, int dstVOffset, int uvWidth, int uvHeight) + { + int totalUV = uvWidth * uvHeight; + int i = 0; + + ref byte srcRef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(src), srcOffset); + ref byte dstURef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(dst), dstUOffset); + ref byte dstVRef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(dst), dstVOffset); + + // Process 32 UV pairs at a time (64 bytes) using Vector256 + if (Vector256.IsHardwareAccelerated) + { + // Indices for de-interleaving: extract U values (even positions) and V values (odd positions) + // For byte pairs: [U0,V0,U1,V1,U2,V2,...] -> U: [U0,U1,U2,...], V: [V0,V1,V2,...] + for (; i <= totalUV - 32; i += 32) + { + // Load 64 bytes (32 UV pairs) + var uv0 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i * 2)); + var uv1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i * 2 + 32)); + + // Use shuffle to de-interleave - extract even bytes (U) and odd bytes (V) + var (u0, v0) = DeinterleaveVector256(uv0); + var (u1, v1) = DeinterleaveVector256(uv1); + + // Combine into 256-bit vectors + var u = Vector256.Create(u0, u1); + var v = Vector256.Create(v0, v1); + + u.StoreUnsafe(ref Unsafe.Add(ref dstURef, i)); + v.StoreUnsafe(ref Unsafe.Add(ref dstVRef, i)); + } + } + + // Process 16 UV pairs at a time (32 bytes) using Vector128 + if (Vector128.IsHardwareAccelerated) + { + for (; i <= totalUV - 16; i += 16) + { + // Load 32 bytes (16 UV pairs) + var uv0 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i * 2)); + var uv1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i * 2 + 16)); + + var (u0, v0) = DeinterleaveVector128(uv0); + var (u1, v1) = DeinterleaveVector128(uv1); + + var u = Vector128.Create(u0, u1); + var v = Vector128.Create(v0, v1); + + u.StoreUnsafe(ref Unsafe.Add(ref dstURef, i)); + v.StoreUnsafe(ref Unsafe.Add(ref dstVRef, i)); + } + } + + // Handle remaining elements with scalar code + for (; i < totalUV; i++) { + Unsafe.Add(ref dstURef, i) = Unsafe.Add(ref srcRef, i * 2); + Unsafe.Add(ref dstVRef, i) = Unsafe.Add(ref srcRef, i * 2 + 1); + } + } + + /// + /// De-interleave 16 byte pairs from a Vector256 into two Vector128 containing U and V values. + /// Input: [U0,V0,U1,V1,U2,V2,...,U15,V15] + /// Output: U=[U0,U1,...,U15], V=[V0,V1,...,V15] + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (Vector128 u, Vector128 v) DeinterleaveVector256(Vector256 uv) + { + // Extract low and high 128-bit halves + var low = uv.GetLower(); // [U0,V0,U1,V1,U2,V2,U3,V3,U4,V4,U5,V5,U6,V6,U7,V7] + var high = uv.GetUpper(); // [U8,V8,U9,V9,U10,V10,U11,V11,U12,V12,U13,V13,U14,V14,U15,V15] + + var (uLow, vLow) = DeinterleaveVector128(low); + var (uHigh, vHigh) = DeinterleaveVector128(high); + + // Combine halves + var u = Vector128.Create(uLow, uHigh); + var v = Vector128.Create(vLow, vHigh); + + return (u, v); + } + + /// + /// De-interleave 8 byte pairs from a Vector128 into two Vector64 containing U and V values. + /// Input: [U0,V0,U1,V1,U2,V2,U3,V3,U4,V4,U5,V5,U6,V6,U7,V7] + /// Output: U=[U0,U1,U2,U3,U4,U5,U6,U7], V=[V0,V1,V2,V3,V4,V5,V6,V7] + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (Vector64 u, Vector64 v) DeinterleaveVector128(Vector128 uv) + { + // Shuffle bytes to gather all U values in low 64 bits and V values in high 64 bits + // This shuffle pattern extracts even indices (U) to the first 8 bytes and odd indices (V) to the last 8 bytes + var shuffleIndices = Vector128.Create( + (byte)0, 2, 4, 6, 8, 10, 12, 14, // U indices (even positions) + 1, 3, 5, 7, 9, 11, 13, 15 // V indices (odd positions) + ); + + var shuffled = Vector128.Shuffle(uv, shuffleIndices); + + return (shuffled.GetLower(), shuffled.GetUpper()); + } +#endif + + /// + /// Converts an I420 sample to an NV12 formatted sample. + /// I420: Y plane followed by U plane, then V plane (planar format). + /// NV12: Y plane followed by interleaved UV plane (UVUVUV...). + /// + /// The I420 image sample. + /// The width in pixels of the I420 sample. + /// The height in pixels of the I420 sample. + /// The degree of parallelism for converting. + /// An NV12 buffer representing the source image. + public static byte[] I420toNV12(byte[] i420, int width, int height, int dop = 1) + { + int ySize = width * height; + int uvWidth = (width + 1) / 2; + int uvHeight = (height + 1) / 2; + int uvSize = uvWidth * uvHeight * 2; + + if (i420 == null || i420.Length < (ySize + uvSize)) + { + throw new ApplicationException($"I420 buffer supplied to I420toNV12 was too small, expected {ySize + uvSize} but got {i420?.Length}."); + } + + byte[] nv12 = new byte[ySize + uvSize]; + + // Copy Y plane (same layout in both formats). + Buffer.BlockCopy(i420, 0, nv12, 0, ySize); + + int i420UOffset = ySize; + int i420VOffset = ySize + uvWidth * uvHeight; + int nv12UvOffset = ySize; + +#if NET8_0_OR_GREATER + // Use SIMD for interleaving U and V planes when available + InterleaveUVSimd(i420, i420UOffset, i420VOffset, nv12, nv12UvOffset, uvWidth, uvHeight); +#else + if (!_optDOP.ContainsKey(dop)) _optDOP[dop] = new ParallelOptions() { MaxDegreeOfParallelism = dop }; - } - - // Interleave UV plane: I420 has separate U and V planes, NV12 has UV interleaved. - Parallel.For(0, uvHeight, _optDOP[dop], (row) => - { - for (int col = 0; col < uvWidth; col++) - { - int i420UPosn = i420UOffset + row * uvWidth + col; - int i420VPosn = i420VOffset + row * uvWidth + col; - int nv12Posn = nv12UvOffset + row * uvWidth * 2 + col * 2; - - nv12[nv12Posn] = i420[i420UPosn]; // U - nv12[nv12Posn + 1] = i420[i420VPosn]; // V - } - }); -#endif - - return nv12; - } - -#if NET8_0_OR_GREATER - /// - /// SIMD-optimized interleave of separate U and V planes from I420 format to NV12 format (UVUVUV...). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void InterleaveUVSimd(byte[] src, int srcUOffset, int srcVOffset, byte[] dst, int dstOffset, int uvWidth, int uvHeight) - { - int totalUV = uvWidth * uvHeight; - int i = 0; - - ref byte srcURef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(src), srcUOffset); - ref byte srcVRef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(src), srcVOffset); - ref byte dstRef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(dst), dstOffset); - - // Process 32 U/V values at a time using Vector256 - if (Vector256.IsHardwareAccelerated) - { - for (; i <= totalUV - 32; i += 32) - { - // Load 32 U values and 32 V values - var u = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcURef, i)); - var v = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcVRef, i)); - - // Interleave U and V values - var (uv0, uv1) = InterleaveVector256(u, v); - - // Store 64 bytes (32 UV pairs) - uv0.StoreUnsafe(ref Unsafe.Add(ref dstRef, i * 2)); - uv1.StoreUnsafe(ref Unsafe.Add(ref dstRef, i * 2 + 32)); - } - } - - // Process 16 U/V values at a time using Vector128 - if (Vector128.IsHardwareAccelerated) - { - for (; i <= totalUV - 16; i += 16) - { - // Load 16 U values and 16 V values - var u = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcURef, i)); - var v = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcVRef, i)); - - // Interleave U and V values - var (uv0, uv1) = InterleaveVector128(u, v); - - // Store 32 bytes (16 UV pairs) - uv0.StoreUnsafe(ref Unsafe.Add(ref dstRef, i * 2)); - uv1.StoreUnsafe(ref Unsafe.Add(ref dstRef, i * 2 + 16)); - } - } - - // Handle remaining elements with scalar code - for (; i < totalUV; i++) - { - Unsafe.Add(ref dstRef, i * 2) = Unsafe.Add(ref srcURef, i); - Unsafe.Add(ref dstRef, i * 2 + 1) = Unsafe.Add(ref srcVRef, i); - } - } - - /// - /// Interleave two Vector256 of U and V values into two Vector256 of interleaved UV pairs. - /// Input: U=[U0,U1,...,U31], V=[V0,V1,...,V31] - /// Output: UV0=[U0,V0,U1,V1,...,U15,V15], UV1=[U16,V16,...,U31,V31] - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (Vector256 uv0, Vector256 uv1) InterleaveVector256(Vector256 u, Vector256 v) - { - // Get low and high halves - var uLow = u.GetLower(); // U0-U15 - var uHigh = u.GetUpper(); // U16-U31 - var vLow = v.GetLower(); // V0-V15 - var vHigh = v.GetUpper(); // V16-V31 - - // Interleave low halves -> first 32 bytes - var (uv0Low, uv0High) = InterleaveVector128ToTwo(uLow, vLow); - var uv0 = Vector256.Create(uv0Low, uv0High); - - // Interleave high halves -> second 32 bytes - var (uv1Low, uv1High) = InterleaveVector128ToTwo(uHigh, vHigh); - var uv1 = Vector256.Create(uv1Low, uv1High); - - return (uv0, uv1); - } - - /// - /// Interleave two Vector128 of U and V values into two Vector128 of interleaved UV pairs. - /// Input: U=[U0,U1,...,U15], V=[V0,V1,...,V15] - /// Output: UV0=[U0,V0,U1,V1,...,U7,V7], UV1=[U8,V8,...,U15,V15] - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (Vector128 uv0, Vector128 uv1) InterleaveVector128(Vector128 u, Vector128 v) - { - return InterleaveVector128ToTwo(u, v); - } - - /// - /// Interleave two Vector128 of 16 bytes each into two Vector128 of interleaved pairs. - /// Input: A=[A0,A1,...,A15], B=[B0,B1,...,B15] - /// Output: Out0=[A0,B0,A1,B1,...,A7,B7], Out1=[A8,B8,...,A15,B15] - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (Vector128 out0, Vector128 out1) InterleaveVector128ToTwo(Vector128 a, Vector128 b) - { - // Create interleave shuffle patterns for low and high halves - // Low: takes elements 0-7 from A and B, interleaves them - // Pattern for first 8 pairs: A0,B0,A1,B1,A2,B2,A3,B3,A4,B4,A5,B5,A6,B6,A7,B7 - var shuffleLowA = Vector128.Create((byte)0, 255, 1, 255, 2, 255, 3, 255, 4, 255, 5, 255, 6, 255, 7, 255); - var shuffleLowB = Vector128.Create((byte)255, 0, 255, 1, 255, 2, 255, 3, 255, 4, 255, 5, 255, 6, 255, 7); - - // Pattern for second 8 pairs: A8,B8,A9,B9,...,A15,B15 - var shuffleHighA = Vector128.Create((byte)8, 255, 9, 255, 10, 255, 11, 255, 12, 255, 13, 255, 14, 255, 15, 255); - var shuffleHighB = Vector128.Create((byte)255, 8, 255, 9, 255, 10, 255, 11, 255, 12, 255, 13, 255, 14, 255, 15); - - // Shuffle and OR to combine - var out0 = Vector128.Shuffle(a, shuffleLowA) | Vector128.Shuffle(b, shuffleLowB); - var out1 = Vector128.Shuffle(a, shuffleHighA) | Vector128.Shuffle(b, shuffleHighB); - - return (out0, out1); - } -#endif - } -} + + // Interleave UV plane: I420 has separate U and V planes, NV12 has UV interleaved. + Parallel.For(0, uvHeight, _optDOP[dop], (row) => + { + for (int col = 0; col < uvWidth; col++) + { + int i420UPosn = i420UOffset + row * uvWidth + col; + int i420VPosn = i420VOffset + row * uvWidth + col; + int nv12Posn = nv12UvOffset + row * uvWidth * 2 + col * 2; + + nv12[nv12Posn] = i420[i420UPosn]; // U + nv12[nv12Posn + 1] = i420[i420VPosn]; // V + } + }); +#endif + + return nv12; + } + +#if NET8_0_OR_GREATER + /// + /// SIMD-optimized interleave of separate U and V planes from I420 format to NV12 format (UVUVUV...). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InterleaveUVSimd(byte[] src, int srcUOffset, int srcVOffset, byte[] dst, int dstOffset, int uvWidth, int uvHeight) + { + int totalUV = uvWidth * uvHeight; + int i = 0; + + ref byte srcURef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(src), srcUOffset); + ref byte srcVRef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(src), srcVOffset); + ref byte dstRef = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(dst), dstOffset); + + // Process 32 U/V values at a time using Vector256 + if (Vector256.IsHardwareAccelerated) + { + for (; i <= totalUV - 32; i += 32) + { + // Load 32 U values and 32 V values + var u = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcURef, i)); + var v = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcVRef, i)); + + // Interleave U and V values + var (uv0, uv1) = InterleaveVector256(u, v); + + // Store 64 bytes (32 UV pairs) + uv0.StoreUnsafe(ref Unsafe.Add(ref dstRef, i * 2)); + uv1.StoreUnsafe(ref Unsafe.Add(ref dstRef, i * 2 + 32)); + } + } + + // Process 16 U/V values at a time using Vector128 + if (Vector128.IsHardwareAccelerated) + { + for (; i <= totalUV - 16; i += 16) + { + // Load 16 U values and 16 V values + var u = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcURef, i)); + var v = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcVRef, i)); + + // Interleave U and V values + var (uv0, uv1) = InterleaveVector128(u, v); + + // Store 32 bytes (16 UV pairs) + uv0.StoreUnsafe(ref Unsafe.Add(ref dstRef, i * 2)); + uv1.StoreUnsafe(ref Unsafe.Add(ref dstRef, i * 2 + 16)); + } + } + + // Handle remaining elements with scalar code + for (; i < totalUV; i++) + { + Unsafe.Add(ref dstRef, i * 2) = Unsafe.Add(ref srcURef, i); + Unsafe.Add(ref dstRef, i * 2 + 1) = Unsafe.Add(ref srcVRef, i); + } + } + + /// + /// Interleave two Vector256 of U and V values into two Vector256 of interleaved UV pairs. + /// Input: U=[U0,U1,...,U31], V=[V0,V1,...,V31] + /// Output: UV0=[U0,V0,U1,V1,...,U15,V15], UV1=[U16,V16,...,U31,V31] + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (Vector256 uv0, Vector256 uv1) InterleaveVector256(Vector256 u, Vector256 v) + { + // Get low and high halves + var uLow = u.GetLower(); // U0-U15 + var uHigh = u.GetUpper(); // U16-U31 + var vLow = v.GetLower(); // V0-V15 + var vHigh = v.GetUpper(); // V16-V31 + + // Interleave low halves -> first 32 bytes + var (uv0Low, uv0High) = InterleaveVector128ToTwo(uLow, vLow); + var uv0 = Vector256.Create(uv0Low, uv0High); + + // Interleave high halves -> second 32 bytes + var (uv1Low, uv1High) = InterleaveVector128ToTwo(uHigh, vHigh); + var uv1 = Vector256.Create(uv1Low, uv1High); + + return (uv0, uv1); + } + + /// + /// Interleave two Vector128 of U and V values into two Vector128 of interleaved UV pairs. + /// Input: U=[U0,U1,...,U15], V=[V0,V1,...,V15] + /// Output: UV0=[U0,V0,U1,V1,...,U7,V7], UV1=[U8,V8,...,U15,V15] + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (Vector128 uv0, Vector128 uv1) InterleaveVector128(Vector128 u, Vector128 v) + { + return InterleaveVector128ToTwo(u, v); + } + + /// + /// Interleave two Vector128 of 16 bytes each into two Vector128 of interleaved pairs. + /// Input: A=[A0,A1,...,A15], B=[B0,B1,...,B15] + /// Output: Out0=[A0,B0,A1,B1,...,A7,B7], Out1=[A8,B8,...,A15,B15] + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (Vector128 out0, Vector128 out1) InterleaveVector128ToTwo(Vector128 a, Vector128 b) + { + // Create interleave shuffle patterns for low and high halves + // Low: takes elements 0-7 from A and B, interleaves them + // Pattern for first 8 pairs: A0,B0,A1,B1,A2,B2,A3,B3,A4,B4,A5,B5,A6,B6,A7,B7 + var shuffleLowA = Vector128.Create((byte)0, 255, 1, 255, 2, 255, 3, 255, 4, 255, 5, 255, 6, 255, 7, 255); + var shuffleLowB = Vector128.Create((byte)255, 0, 255, 1, 255, 2, 255, 3, 255, 4, 255, 5, 255, 6, 255, 7); + + // Pattern for second 8 pairs: A8,B8,A9,B9,...,A15,B15 + var shuffleHighA = Vector128.Create((byte)8, 255, 9, 255, 10, 255, 11, 255, 12, 255, 13, 255, 14, 255, 15, 255); + var shuffleHighB = Vector128.Create((byte)255, 8, 255, 9, 255, 10, 255, 11, 255, 12, 255, 13, 255, 14, 255, 15); + + // Shuffle and OR to combine + var out0 = Vector128.Shuffle(a, shuffleLowA) | Vector128.Shuffle(b, shuffleLowB); + var out1 = Vector128.Shuffle(a, shuffleHighA) | Vector128.Shuffle(b, shuffleHighB); + + return (out0, out1); + } +#endif + } +} diff --git a/src/SIPSorceryMedia.Abstractions/SIPSorceryMedia.Abstractions.csproj b/src/SIPSorceryMedia.Abstractions/SIPSorceryMedia.Abstractions.csproj index ccf6eea6cf..4e658aed04 100644 --- a/src/SIPSorceryMedia.Abstractions/SIPSorceryMedia.Abstractions.csproj +++ b/src/SIPSorceryMedia.Abstractions/SIPSorceryMedia.Abstractions.csproj @@ -2,16 +2,16 @@ SIPSorceryMedia.Abstractions - netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net8.0;net10.0 + netstandard2.0;net8.0 true true - 12.0 + 14.0 $(NoWarn);CS1591;CS1573 - true - True + true + True Aaron Clauson - Copyright © 2020-2026 Aaron Clauson + Copyright © 2020-2025 Aaron Clauson SIP Sorcery PTY LTD BSD-3-Clause SIPSorceryMedia.Abstractions @@ -48,16 +48,26 @@ - + - + + + + + + + + + + true snupkg + enable diff --git a/src/SIPSorceryMedia.Abstractions/sys/EnumExtensions.cs b/src/SIPSorceryMedia.Abstractions/sys/EnumExtensions.cs new file mode 100644 index 0000000000..3648c84ac4 --- /dev/null +++ b/src/SIPSorceryMedia.Abstractions/sys/EnumExtensions.cs @@ -0,0 +1,6 @@ +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] +[assembly: global::NetEscapades.EnumGenerators.EnumExtensions()] diff --git a/src/SIPSorceryMedia.Abstractions/sys/SipSorceryMediaException.cs b/src/SIPSorceryMedia.Abstractions/sys/SipSorceryMediaException.cs new file mode 100644 index 0000000000..8678da7c9d --- /dev/null +++ b/src/SIPSorceryMedia.Abstractions/sys/SipSorceryMediaException.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace SIPSorceryMedia +{ + /// + /// Represents errors that occur during media processing in the SIP Sorcery media library. + /// + /// + /// Use this exception to indicate failures related to media operations, such as audio or video + /// processing errors, within the SIP Sorcery framework. This exception can be caught to handle media-specific error + /// conditions separately from other exception types. + /// + public class SipSorceryMediaException : ApplicationException + { + /// Initializes a new instance of the class. + public SipSorceryMediaException() + { + } + + /// Initializes a new instance of the class with a specified error message. + /// The message that describes the error. + public SipSorceryMediaException(string? message) : base(message) + { + } + + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. + public SipSorceryMediaException(string? message, Exception? innerException) : base(message, innerException) + { + } + } +} diff --git a/src/SIPSorceryMedia.FFmpeg/BasicBufferShort.cs b/src/SIPSorceryMedia.FFmpeg/BasicBufferShort.cs index f34d0df85e..d56dbeca9a 100644 --- a/src/SIPSorceryMedia.FFmpeg/BasicBufferShort.cs +++ b/src/SIPSorceryMedia.FFmpeg/BasicBufferShort.cs @@ -25,10 +25,10 @@ public BasicBufferShort(int capacity) public void Write(short[] toWrite) { // Write the data in chunks - int sourceIndex = 0; + var sourceIndex = 0; while (sourceIndex < toWrite.Length) { - int count = Math.Min(toWrite.Length - sourceIndex, capacity - writeIndex); + var count = Math.Min(toWrite.Length - sourceIndex, capacity - writeIndex); Array.Copy(toWrite, sourceIndex, data, writeIndex, count); writeIndex = (writeIndex + count) % capacity; sourceIndex += count; @@ -65,14 +65,14 @@ public void Clear() available = 0; } - public short[] Read(int count) + public void Read(int count, Span buffer) { - short[] returnVal = new short[count]; + var returnVal = new short[count]; // Read the data in chunks - int sourceIndex = 0; + var sourceIndex = 0; while (sourceIndex < count) { - int readCount = Math.Min(count - sourceIndex, capacity - readIndex); + var readCount = Math.Min(count - sourceIndex, capacity - readIndex); Array.Copy(data, readIndex, returnVal, sourceIndex, readCount); readIndex = (readIndex + readCount) % capacity; sourceIndex += readCount; @@ -84,7 +84,6 @@ public short[] Read(int count) writeIndex = (readIndex + 1) % capacity; available = 0; } - return returnVal; } /// @@ -93,7 +92,7 @@ public short[] Read(int count) /// public short Read() { - short returnVal = data[readIndex]; + var returnVal = data[readIndex]; readIndex = (readIndex + 1) % capacity; available -= 1; if (available < 0) @@ -111,14 +110,14 @@ public short Read() /// public short[] Peek(int count) { - int toRead = Math.Min(available, count); - short[] returnVal = new short[toRead]; + var toRead = Math.Min(available, count); + var returnVal = new short[toRead]; // Read the data in chunks - int sourceIndex = 0; - int localReadIndex = readIndex; + var sourceIndex = 0; + var localReadIndex = readIndex; while (sourceIndex < toRead) { - int readCount = Math.Min(toRead - sourceIndex, capacity - localReadIndex); + var readCount = Math.Min(toRead - sourceIndex, capacity - localReadIndex); Array.Copy(data, localReadIndex, returnVal, sourceIndex, readCount); localReadIndex = (localReadIndex + readCount) % capacity; sourceIndex += readCount; diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegAudioDecoder.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegAudioDecoder.cs index 0b0b161aca..e72f790da2 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegAudioDecoder.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegAudioDecoder.cs @@ -218,16 +218,16 @@ public async Task Close() private unsafe void RunDecodeLoop() { - bool needToRestartAudio = false; + var needToRestartAudio = false; AVPacket* pkt = ffmpeg.av_packet_alloc(); AVFrame* avFrame = ffmpeg.av_frame_alloc(); - int eagain = ffmpeg.AVERROR(ffmpeg.EAGAIN); + var eagain = ffmpeg.AVERROR(ffmpeg.EAGAIN); int error; - bool canContinue = true; - bool managePacket = true; + var canContinue = true; + var managePacket = true; double firts_dpts = 0; @@ -249,12 +249,18 @@ private unsafe void RunDecodeLoop() { managePacket = false; if (error == eagain) + { ffmpeg.av_packet_unref(pkt); + } else + { canContinue = false; + } } else + { managePacket = true; + } if (managePacket) { @@ -266,7 +272,7 @@ private unsafe void RunDecodeLoop() return; } - int recvRes = ffmpeg.avcodec_receive_frame(_audDecCtx, avFrame); + var recvRes = ffmpeg.avcodec_receive_frame(_audDecCtx, avFrame); while (recvRes >= 0) { @@ -282,16 +288,20 @@ private unsafe void RunDecodeLoop() original_dpts = dpts; if (firts_dpts == 0) + { firts_dpts = dpts; + } dpts -= firts_dpts; } - int sleep = (int)(dpts * 1000 - DateTime.Now.Subtract(startTime).TotalMilliseconds); + var sleep = (int)(dpts * 1000 - DateTime.Now.Subtract(startTime).TotalMilliseconds); //Console.WriteLine($"sleep {sleep} {Math.Min(_maxAudioFrameSpace, sleep)} - firts_dpts:{firts_dpts} - dpts:{dpts} - original_dpts:{original_dpts}"); if (sleep > Helper.MIN_SLEEP_MILLISECONDS) + { ffmpeg.av_usleep((uint)(Math.Min(_maxAudioFrameSpace, sleep) * 1000)); + } } recvRes = ffmpeg.avcodec_receive_frame(_audDecCtx, avFrame); diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegAudioSource.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegAudioSource.cs index a54e069d09..7cdb386b02 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegAudioSource.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegAudioSource.cs @@ -1,7 +1,9 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using CommunityToolkit.HighPerformance.Buffers; using FFmpeg.AutoGen; using Microsoft.Extensions.Logging; using SIPSorceryMedia.Abstractions; @@ -86,7 +88,10 @@ public bool InitialiseDecoder() public List GetAudioSourceFormats() { if (_audioFormatManager != null) + { return _audioFormatManager.GetSourceFormats(); + } + return new List(); } @@ -100,7 +105,9 @@ public void SetAudioSourceFormat(AudioFormat audioFormat) public void RestrictFormats(Func filter) { if (_audioFormatManager != null) + { _audioFormatManager.RestrictFormats(filter); + } } public void ExternalAudioSourceRawSample(AudioSamplingRatesEnum samplingRate, uint durationMilliseconds, short[] sample) => throw new NotImplementedException(); @@ -119,8 +126,10 @@ public void RestrictFormats(Func filter) private unsafe void AudioDecoder_OnAudioFrame(ref AVFrame avFrame) { - if ( (OnAudioSourceEncodedSample == null) || (_audioDecoder == null) ) + if ((OnAudioSourceEncodedSample == null) || (_audioDecoder == null)) + { return; + } // Avoid to create several times buffer of the same size if (_currentNbSamples < avFrame.nb_samples) @@ -143,27 +152,61 @@ private unsafe void AudioDecoder_OnAudioFrame(ref AVFrame avFrame) // Convert audio int dstSampleCount; fixed (byte* pBuffer = buffer) + { dstSampleCount = ffmpeg.swr_convert(_audioDecoder._swrContext, &pBuffer, bufferSizeInSamples, avFrame.extended_data, avFrame.nb_samples); + } - if(dstSampleCount < 0) + if (dstSampleCount < 0) { OnAudioSourceError?.Invoke("Cannot convert audio"); Dispose(); return; } - if(dstSampleCount > 0) + if (dstSampleCount > 0) { // FFmpeg AV_SAMPLE_FMT_S16 will store the bytes in the correct endianess for the underlying platform. - short[] pcm = buffer.Take(dstSampleCount * 2).Where((x, i) => i % 2 == 0).Select((y, i) => BitConverter.ToInt16(buffer, i * 2)).ToArray(); + var pcm = buffer.Take(dstSampleCount * 2).Where((x, i) => i % 2 == 0).Select((y, i) => BitConverter.ToInt16(buffer, i * 2)).ToArray(); _incomingSamples.Write(pcm); - while (_incomingSamples.Available() >= frameSize) + if (_incomingSamples.Available() >= frameSize) { - var pcmFrame = _incomingSamples.Read(frameSize); - var encodedSample = _audioEncoder.EncodeAudio(pcmFrame, _audioFormatManager.SelectedFormat); - if(encodedSample.Length > 0) - OnAudioSourceEncodedSample?.Invoke((uint) (pcmFrame.Length * _audioFormatManager.SelectedFormat.RtpClockRate / _audioFormatManager.SelectedFormat.ClockRate ), encodedSample); + var pcmFrame = ArrayPool.Shared.Rent(frameSize); + try + { + while (_incomingSamples.Available() >= frameSize) + { + _incomingSamples.Read(frameSize, pcmFrame); + using var encodedBuffer = new ArrayPoolBufferWriter(); + _audioEncoder.EncodeAudio(pcmFrame.AsSpan(0, frameSize), _audioFormatManager.SelectedFormat, encodedBuffer); + + var encodedMemory = encodedBuffer.WrittenMemory; + + var onAudioSourceEncodedSample = OnAudioSourceEncodedSample; + var onAudioSourceEncodedFrameReady = OnAudioSourceEncodedFrameReady; + + if (!encodedMemory.IsEmpty && (onAudioSourceEncodedSample is { } || onAudioSourceEncodedFrameReady is { })) + { + onAudioSourceEncodedSample?.Invoke((uint)encodedMemory.Length, encodedMemory); + + if (onAudioSourceEncodedFrameReady is { }) + { + var audioFormat = _audioFormatManager.SelectedFormat; + var numChannels = audioFormat.ChannelCount; + var sampleRate = audioFormat.ClockRate; + var frames = pcm.Length / numChannels; + var durationMsD = sampleRate > 0 ? (frames / (double)sampleRate) * 1000.0 : 0; + var durationMs = (uint)Math.Round(durationMsD); + var encodedFrame = new EncodedAudioFrame(0, audioFormat, durationMs, encodedMemory); + onAudioSourceEncodedFrameReady(encodedFrame); + } + } + } + } + finally + { + ArrayPool.Shared.Return(pcmFrame); + } } } } @@ -188,8 +231,10 @@ public async Task Close() { _isClosed = true; - if(_audioDecoder != null) + if (_audioDecoder != null) + { await _audioDecoder.Close(); + } Dispose(); } @@ -202,7 +247,9 @@ public Task Pause() _isPaused = true; if (_audioDecoder != null) + { _audioDecoder.Pause(); + } } return Task.CompletedTask; @@ -214,7 +261,9 @@ public Task Resume() { _isPaused = false; if (_audioDecoder != null) + { _audioDecoder.Resume(); + } } return Task.CompletedTask; } diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegCameraManager.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegCameraManager.cs index abfa5c58ec..105677cc03 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegCameraManager.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegCameraManager.cs @@ -13,7 +13,7 @@ public unsafe class FFmpegCameraManager { List? result = null; - string inputFormat = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dshow" + var inputFormat = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dshow" : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "v4l2" : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "avfoundation" : throw new NotSupportedException($"Cannot find adequate input format - OSArchitecture:[{RuntimeInformation.OSArchitecture}] - OSDescription:[{RuntimeInformation.OSDescription}]"); @@ -32,11 +32,11 @@ public unsafe class FFmpegCameraManager AVDeviceInfoList* avDeviceInfoList = null; ffmpeg.avdevice_list_input_sources(avInputFormat, null, null, &avDeviceInfoList).ThrowExceptionIfError(); - int nDevices = avDeviceInfoList->nb_devices; + var nDevices = avDeviceInfoList->nb_devices; var avDevices = avDeviceInfoList->devices; result = new List(); - for (int i = 0; i < nDevices; i++) + for (var i = 0; i < nDevices; i++) { var avDevice = avDevices[i]; var name = Marshal.PtrToStringAnsi((IntPtr)avDevice->device_description); diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegCameraSource.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegCameraSource.cs index d6e68f9607..22acb98f2d 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegCameraSource.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegCameraSource.cs @@ -39,7 +39,7 @@ public unsafe FFmpegCameraSource(Camera camera) _sourcePixFmts = _camera.AvailableFormats?.Select(f => f.PixelFormat).Distinct().ToArray(); - string inputFormat = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dshow" + var inputFormat = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dshow" : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "v4l2" : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "avfoundation" : throw new NotSupportedException($"Cannot find adequate input format - OSArchitecture:[{RuntimeInformation.OSArchitecture}] - OSDescription:[{RuntimeInformation.OSDescription}]"); @@ -154,12 +154,16 @@ internal override async void OnNegotiatedPixelFormat(AVPixelFormat ongoingFmt, A base.OnNegotiatedPixelFormat(ongoingFmt, chosenPixFmt); if (ongoingFmt == chosenPixFmt) + { return; + } var formats = _filteredFormats ?? _camera.AvailableFormats; if (formats == null) + { return; + } var chosenfmt = formats?.FirstOrDefault(f => f.PixelFormat == chosenPixFmt); @@ -187,7 +191,9 @@ internal static class FFmpegCameraExtensions if (c.Equals(default(Camera.CameraFormat)) || c.FPS == 0 || c.Width == 0 || c.Height == 0 ) + { return null; + } return new Dictionary() { diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegFileSource.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegFileSource.cs index a2af4f0e75..ab8fd388a1 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegFileSource.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegFileSource.cs @@ -39,7 +39,9 @@ public unsafe FFmpegFileSource(string path, bool repeat, IAudioEncoder? audioEnc if (!File.Exists(path)) { if (!Uri.TryCreate(path, UriKind.Absolute, out Uri? result)) + { throw new ApplicationException($"Requested path is not a valid file path or not a valid Uri: {path}."); + } } if ((audioEncoder != null)) @@ -73,7 +75,7 @@ private void _FFmpegVideoSource_OnVideoSourceError(string errorMessage) OnVideoSourceError?.Invoke(errorMessage); } - private void _FFmpegVideoSource_OnVideoSourceEncodedSample(uint durationRtpUnits, byte[] sample) + private void _FFmpegVideoSource_OnVideoSourceEncodedSample(uint durationRtpUnits, ReadOnlyMemory sample) { OnVideoSourceEncodedSample?.Invoke(durationRtpUnits, sample); } @@ -83,7 +85,7 @@ private void _FFmpegVideoSource_OnVideoSourceRawSampleFaster(uint durationMillis OnVideoSourceRawSampleFaster?.Invoke(durationMilliseconds, imageRawSample); } - private void _FFmpegAudioSource_OnAudioSourceEncodedSample(uint durationRtpUnits, byte[] sample) + private void _FFmpegAudioSource_OnAudioSourceEncodedSample(uint durationRtpUnits, ReadOnlyMemory sample) { OnAudioSourceEncodedSample?.Invoke(durationRtpUnits, sample); } @@ -98,7 +100,10 @@ private void _FFmpegAudioSource_OnAudioSourceRawSample(AudioSamplingRatesEnum sa public List GetAudioSourceFormats() { if (_FFmpegAudioSource != null) + { return _FFmpegAudioSource.GetAudioSourceFormats(); + } + return new List(); } public void SetAudioSourceFormat(AudioFormat audioFormat) @@ -112,19 +117,25 @@ public void SetAudioSourceFormat(AudioFormat audioFormat) public void RestrictFormats(Func filter) { if (_FFmpegAudioSource != null) + { _FFmpegAudioSource.RestrictFormats(filter); + } } public void ExternalAudioSourceRawSample(AudioSamplingRatesEnum samplingRate, uint durationMilliseconds, short[] sample) => throw new NotImplementedException(); public bool HasEncodedAudioSubscribers() { - Boolean result = OnAudioSourceEncodedSample != null; + var result = OnAudioSourceEncodedSample != null; if (_FFmpegAudioSource != null) { if (result) + { _FFmpegAudioSource.OnAudioSourceEncodedSample += _FFmpegAudioSource_OnAudioSourceEncodedSample; + } else + { _FFmpegAudioSource.OnAudioSourceEncodedSample -= _FFmpegAudioSource_OnAudioSourceEncodedSample; + } } return result; @@ -132,13 +143,17 @@ public bool HasEncodedAudioSubscribers() public bool HasRawAudioSubscribers() { - Boolean result = OnAudioSourceRawSample!= null; + var result = OnAudioSourceRawSample!= null; if (_FFmpegAudioSource != null) { if (result) + { _FFmpegAudioSource.OnAudioSourceRawSample += _FFmpegAudioSource_OnAudioSourceRawSample; + } else + { _FFmpegAudioSource.OnAudioSourceRawSample -= _FFmpegAudioSource_OnAudioSourceRawSample; + } } return result; @@ -153,7 +168,10 @@ public bool HasRawAudioSubscribers() public List GetVideoSourceFormats() { if (_FFmpegVideoSource != null) + { return _FFmpegVideoSource.GetVideoSourceFormats(); + } + return new List(); } @@ -169,7 +187,9 @@ public void SetVideoSourceFormat(VideoFormat videoFormat) public void RestrictFormats(Func filter) { if (_FFmpegVideoSource != null) + { _FFmpegVideoSource.RestrictFormats(filter); + } } public void ForceKeyFrame() => _FFmpegVideoSource?.ForceKeyFrame(); @@ -179,13 +199,17 @@ public void RestrictFormats(Func filter) public bool HasEncodedVideoSubscribers() { - Boolean result = OnVideoSourceEncodedSample != null; + var result = OnVideoSourceEncodedSample != null; if (_FFmpegVideoSource != null) { if (result) + { _FFmpegVideoSource.OnVideoSourceEncodedSample += _FFmpegVideoSource_OnVideoSourceEncodedSample; + } else + { _FFmpegVideoSource.OnVideoSourceEncodedSample -= _FFmpegVideoSource_OnVideoSourceEncodedSample; + } } return result; @@ -193,13 +217,17 @@ public bool HasEncodedVideoSubscribers() public bool HasRawVideoSubscribers() { - Boolean result = OnVideoSourceRawSampleFaster != null; + var result = OnVideoSourceRawSampleFaster != null; if (_FFmpegVideoSource != null) { if (result) + { _FFmpegVideoSource.OnVideoSourceRawSampleFaster += _FFmpegVideoSource_OnVideoSourceRawSampleFaster; + } else + { _FFmpegVideoSource.OnVideoSourceRawSampleFaster -= _FFmpegVideoSource_OnVideoSourceRawSampleFaster; + } } return result; diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegMicrophoneSource.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegMicrophoneSource.cs index 499f473580..2c82fcc06e 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegMicrophoneSource.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegMicrophoneSource.cs @@ -12,7 +12,7 @@ public class FFmpegMicrophoneSource : FFmpegAudioSource public unsafe FFmpegMicrophoneSource(string path, IAudioEncoder audioEncoder, uint frameSize = 960) : base(audioEncoder, frameSize) { - string inputFormat = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dshow" + var inputFormat = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dshow" : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "alsa" : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "avfoundation" : throw new NotSupportedException($"Cannot find adequate input format - OSArchitecture:[{RuntimeInformation.OSArchitecture}] - OSDescription:[{RuntimeInformation.OSDescription}]"); diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegScreenSource.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegScreenSource.cs index 1681993535..2a1db9ef59 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegScreenSource.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegScreenSource.cs @@ -54,7 +54,9 @@ public unsafe FFmpegScreenSource(string path, Rectangle rect, int frameRate = 20 }; } else + { throw new NotSupportedException($"Cannot find adequate input format - OSArchitecture:[{RuntimeInformation.OSArchitecture}] - OSDescription:[{RuntimeInformation.OSDescription}]"); + } AVInputFormat* aVInputFormat = ffmpeg.av_find_input_format(inputFormat); diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegVideoDecoder.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegVideoDecoder.cs index f134e5c3e6..63b01e87a5 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegVideoDecoder.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegVideoDecoder.cs @@ -80,10 +80,12 @@ public unsafe Boolean InitialiseSource(Dictionary? decoderOption if(decoderOptions != null) { - foreach (String key in decoderOptions.Keys) + foreach (var key in decoderOptions.Keys) { if(ffmpeg.av_dict_set(&options, key, decoderOptions[key], 0) < 0) + { logger.LogWarning($"Cannot set option [{key}]=[{decoderOptions[key]}]"); + } } } @@ -206,17 +208,17 @@ public async Task Close() private void RunDecodeLoop() { - bool needToRestartVideo = false; + var needToRestartVideo = false; unsafe { AVPacket* pkt = null; AVFrame* avFrame = ffmpeg.av_frame_alloc(); - int eagain = ffmpeg.AVERROR(ffmpeg.EAGAIN); + var eagain = ffmpeg.AVERROR(ffmpeg.EAGAIN); int error; - bool canContinue = true; - bool managePacket = true; + var canContinue = true; + var managePacket = true; double firts_dpts = 0; @@ -237,12 +239,18 @@ private void RunDecodeLoop() { managePacket = false; if (error == eagain) + { ffmpeg.av_packet_unref(pkt); + } else + { canContinue = false; + } } else + { managePacket = true; + } if (managePacket) { @@ -254,12 +262,12 @@ private void RunDecodeLoop() return; } - int recvRes = ffmpeg.avcodec_receive_frame(_vidDecCtx, avFrame); + var recvRes = ffmpeg.avcodec_receive_frame(_vidDecCtx, avFrame); while (recvRes >= 0) { //Console.WriteLine($"video number samples {frame->nb_samples}, pts={frame->pts}, dts={(int)(_videoTimebase * frame->pts * 1000)}, width {frame->width}, height {frame->height}."); - long pts = avFrame->pts; + var pts = avFrame->pts; OnVideoFrame?.Invoke(avFrame); @@ -270,14 +278,16 @@ private void RunDecodeLoop() { dpts = _videoTimebase * pts; if (firts_dpts == 0) + { firts_dpts = dpts; + } dpts -= firts_dpts; } //Console.WriteLine($"Decoded video frame {frame->width}x{frame->height}, ts {frame->best_effort_timestamp}, delta {frame->best_effort_timestamp - prevVidTs}, dpts {dpts}."); - int sleep = (int)(dpts * 1000 - DateTime.Now.Subtract(startTime).TotalMilliseconds); + var sleep = (int)(dpts * 1000 - DateTime.Now.Subtract(startTime).TotalMilliseconds); if (sleep > Helper.MIN_SLEEP_MILLISECONDS) { ffmpeg.av_usleep((uint)(Math.Min(_maxVideoFrameSpace, sleep) * 1000)); diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegVideoEncoder.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegVideoEncoder.cs index 6fddfe2964..a32cbb49b6 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegVideoEncoder.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegVideoEncoder.cs @@ -73,7 +73,7 @@ public FFmpegVideoEncoder(Dictionary? encoderOptions = null, AVH public byte[]? EncodeVideoFaster(RawImage rawImage, VideoCodecsEnum codec) { - byte* pSample = (byte*)rawImage.Sample; + var pSample = (byte*)rawImage.Sample; return EncodeVideo(rawImage.Width, rawImage.Height, pSample, rawImage.PixelFormat, codec); } @@ -107,9 +107,11 @@ public IEnumerable DecodeVideoFaster(byte[] encodedSample, VideoPixelF throw new NotImplementedException($"Codec {codec} is not supported by the FFmpeg video decoder."); } - var decodedFrames = DecodeFaster(codecID.Value, encodedSample, out int width, out int height); + var decodedFrames = DecodeFaster(codecID.Value, encodedSample, out var width, out var height); if (decodedFrames != null && decodedFrames.Count > 0) + { return decodedFrames; + } return new List(); } @@ -163,7 +165,9 @@ public bool SetCodec(AVCodecID cdc, string name, Dictionary? opt (_specificEncoders ??= [])[cdc] = (IntPtr)codec; if (opts != null) + { (_codecOptionsByName ??= [])[name] = opts; + } return codec != null; } @@ -171,7 +175,9 @@ public bool SetCodec(AVCodecID cdc, string name, Dictionary? opt private AVCodec* GetCodec(AVCodecID codecID, string? wrapName, bool isEncoder = true) { if (wrapName == null) + { return null; + } IntPtr? iterator = null; var cdc = ffmpeg.av_codec_iterate((void**)&iterator); @@ -183,7 +189,9 @@ public bool SetCodec(AVCodecID cdc, string name, Dictionary? opt || (!isEncoder && ffmpeg.av_codec_is_decoder(cdc) != 0) ) ) + { break; + } cdc = ffmpeg.av_codec_iterate((void**)&iterator); } @@ -191,7 +199,9 @@ public bool SetCodec(AVCodecID cdc, string name, Dictionary? opt (_specificEncoders ??= [])[codecID] = (IntPtr)cdc; if (cdc == null) + { logger.LogWarning("Codec not found for {id} with wrapper {wrap}", codecID, _wrapName); + } return cdc; } @@ -201,7 +211,9 @@ public bool SetCodec(AVCodecID cdc, string name, Dictionary? opt AVCodec* codec = null; if (_specificEncoders?.TryGetValue(codecID, out var cdc) ?? false) + { codec = (AVCodec*)cdc; + } if (codec == null) { @@ -226,10 +238,14 @@ public bool SetCodec(AVCodecID cdc, string name, Dictionary? opt private Dictionary GetCodecOptions(string? name) { if (!string.IsNullOrWhiteSpace(name) - && (_codecOptionsByName?.TryGetValue(name!, out var opt) ?? false)) + && (_codecOptionsByName?.TryGetValue(name, out var opt) ?? false)) + { return opt; + } else + { return _codecOptions; + } } public void InitialiseEncoder(AVCodecID codecID, int width, int height, int fps) @@ -263,17 +279,40 @@ public void InitialiseEncoder(AVCodecID codecID, int width, int height, int fps) _encoderContext->pix_fmt = _negotiatedPixFmt ?? codec->pix_fmts[0]; - if (_bit_rate != null) _encoderContext->bit_rate = (long)_bit_rate; - if (_bit_rate_tolerance != null) _encoderContext->bit_rate_tolerance = (int)_bit_rate_tolerance; - if (_rc_min_rate != null) _encoderContext->rc_min_rate = (long)_rc_min_rate; - if (_rc_max_rate != null) _encoderContext->rc_max_rate = (long)_rc_max_rate; - if (_thread_count != null) _encoderContext->thread_count = (int)_thread_count; + if (_bit_rate != null) + { + _encoderContext->bit_rate = (long)_bit_rate; + } + + if (_bit_rate_tolerance != null) + { + _encoderContext->bit_rate_tolerance = (int)_bit_rate_tolerance; + } + + if (_rc_min_rate != null) + { + _encoderContext->rc_min_rate = (long)_rc_min_rate; + } + + if (_rc_max_rate != null) + { + _encoderContext->rc_max_rate = (long)_rc_max_rate; + } + + if (_thread_count != null) + { + _encoderContext->thread_count = (int)_thread_count; + } // Set Key frame interval if (fps < 5) + { _encoderContext->gop_size = 1; + } else + { _encoderContext->gop_size = fps; + } try { @@ -311,7 +350,9 @@ public void InitialiseEncoder(AVCodecID codecID, int width, int height, int fps) { var ok = ffmpeg.av_opt_set(_encoderContext->priv_data, option.Key, option.Value, ffmpeg.AV_OPT_SEARCH_CHILDREN); if (ok < 0) + { logger.LogWarning("Failed to set encoder option \"{key}\"=\"{val}\", Skipping this option. {msg}", option.Key, option.Value, FFmpegInit.av_strerror(ok)); + } } ffmpeg.avcodec_open2(_encoderContext, codec, null).ThrowExceptionIfError(); @@ -475,9 +516,9 @@ private bool CheckDropFrame() } // Calculate frame interval in ticks based on Stopwatch frequency. // frameIntervalMs = 1000 / framerate, convert ms to ticks: frameIntervalTicks = frameIntervalMs * Stopwatch.Frequency / 1000 - long frameIntervalTicks = (long)(1000.0 / _encoderContext->framerate.num * Stopwatch.Frequency / 1000); + var frameIntervalTicks = (long)(1000.0 / _encoderContext->framerate.num * Stopwatch.Frequency / 1000); - long nowTicks = _frameTimer.ElapsedTicks; + var nowTicks = _frameTimer.ElapsedTicks; if (_lastFrameTicks != 0) { if (nowTicks - _lastFrameTicks < frameIntervalTicks) @@ -554,8 +595,8 @@ private bool CheckDropFrame() } lock (_encoderLock) { - int width = avFrame->width; - int height = avFrame->height; + var width = avFrame->width; + var height = avFrame->height; if (!_isEncoderInitialised) { @@ -595,7 +636,7 @@ private bool CheckDropFrame() try { ffmpeg.avcodec_send_frame(_encoderContext, avFrame).ThrowExceptionIfError(); - int error = ffmpeg.avcodec_receive_packet(_encoderContext, pPacket); + var error = ffmpeg.avcodec_receive_packet(_encoderContext, pPacket); if (error == 0) { @@ -605,13 +646,13 @@ private bool CheckDropFrame() { // TODO: Work out how to use the FFmpeg H264 bit stream parser to extract the NALs. // Currently it's being done in the RTPSession class. - byte[] arr = new byte[pPacket->size]; + var arr = new byte[pPacket->size]; Marshal.Copy((IntPtr)pPacket->data, arr, 0, pPacket->size); return arr; } else { - byte[] arr = new byte[pPacket->size]; + var arr = new byte[pPacket->size]; Marshal.Copy((IntPtr)pPacket->data, arr, 0, pPacket->size); return arr; } @@ -674,11 +715,17 @@ public void AdjustStream(int bitrate, int fps) { if (_encoderContext == null) + { return; + } + lock (_encoderLock) { if (_encoderContext == null) + { return; + } + _encoderContext->bit_rate = bitrate; _encoderContext->framerate.num = fps; _encoderContext->gop_size = Math.Max(5, fps * 2); @@ -740,7 +787,9 @@ public void AdjustStream(int bitrate, int fps) height = 0; if (_isDecoderInitialised && _codecID != codecID) + { ResetDecoder(); + } if (!_isDecoderInitialised) { @@ -760,7 +809,7 @@ public void AdjustStream(int bitrate, int fps) ffmpeg.av_frame_unref(_frame); ffmpeg.av_frame_unref(_gpuFrame); - int recvRes = ffmpeg.avcodec_receive_frame(_decoderContext, _frame); + var recvRes = ffmpeg.avcodec_receive_frame(_decoderContext, _frame); while (recvRes == 0) { @@ -893,28 +942,39 @@ internal bool NegotiatePixelFormat(AVCodecID codecid, int width, int height, int lock (_encoderLock) { if (_isEncoderInitialised) + { ResetEncoder(); + } InitialiseEncoder(codecid, width, height, frameRate); if (logger.IsEnabled(LogLevel.Trace)) + { logger.LogTrace("Negotiating pixel format for codec [{name}].", GetNameString(_encoderContext->codec->name)); + } var fmts = _encoderContext->codec->pix_fmts; while (*fmts != AVPixelFormat.AV_PIX_FMT_NONE && sourcePixFmts?.Length > 0 && !sourcePixFmts.Contains(*fmts)) { if (logger.IsEnabled(LogLevel.Trace)) + { logger.LogTrace("Skipping unsupported pixel format {fmt}.", *fmts); + } + fmts++; } var ret = false; fmt = *fmts; if (fmt == AVPixelFormat.AV_PIX_FMT_NONE) + { fmt = _encoderContext->codec->pix_fmts[0]; + } else + { ret = true; + } ResetEncoder(); diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegVideoEndPoint.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegVideoEndPoint.cs index a154e32d9a..fd7e82fb0b 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegVideoEndPoint.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegVideoEndPoint.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; @@ -79,7 +80,9 @@ public bool SetDecoderForCodec(VideoCodecsEnum codec, string name, Dictionary payload, VideoFormat format) { - if ( (!_isClosed) && (payload != null) && (OnVideoSinkDecodedSampleFaster != null) ) + if ( (!_isClosed) && (!payload.IsEmpty) && (OnVideoSinkDecodedSampleFaster != null) ) { if (_videoFormatManager.SelectedFormat.Codec != format.Codec) { @@ -113,8 +116,8 @@ public void GotVideoFrame(IPEndPoint remoteEndPoint, uint timestamp, byte[] payl AVCodecID? codecID = FFmpegConvert.GetAVCodecID(_videoFormatManager.SelectedFormat.Codec); if(codecID != null) - { - var imageRawSamples = _ffmpegEncoder.DecodeFaster(codecID.Value, payload, out var width, out var height); + { + var imageRawSamples = _ffmpegEncoder.DecodeFaster(codecID.Value, payload.ToArray(), out var width, out var height); if (imageRawSamples == null || width == 0 || height == 0) { diff --git a/src/SIPSorceryMedia.FFmpeg/FFmpegVideoSource.cs b/src/SIPSorceryMedia.FFmpeg/FFmpegVideoSource.cs index bbd2d47a43..17b371e6e3 100644 --- a/src/SIPSorceryMedia.FFmpeg/FFmpegVideoSource.cs +++ b/src/SIPSorceryMedia.FFmpeg/FFmpegVideoSource.cs @@ -126,7 +126,9 @@ public bool SetEncoderForCodec(VideoCodecsEnum codec, string name, Dictionarywidth; var height = frame->height; @@ -171,7 +173,9 @@ private unsafe void VideoDecoder_OnVideoFrame(AVFrame* frame) } if (!NegotiatePixelFormat(aVCodecId, width, height, frameRate, srcfmt)) + { return; + } // Manage Raw Sample if (OnVideoSourceRawSampleFaster != null) @@ -231,10 +235,12 @@ private unsafe void VideoDecoder_OnVideoFrame(AVFrame* frame) // let the encoder decide on I-frames if (readyFrame->pict_type == AVPictureType.AV_PICTURE_TYPE_I) + { readyFrame->pict_type = AVPictureType.AV_PICTURE_TYPE_NONE; + } // Now a frame in the correct pixel format is availble so it can be encoded. - byte[]? encodedSample = _videoEncoder.Encode(aVCodecId.Value, readyFrame, frameRate, _forceKeyFrame); + var encodedSample = _videoEncoder.Encode(aVCodecId.Value, readyFrame, frameRate, _forceKeyFrame); if (encodedSample != null) { @@ -253,7 +259,9 @@ private unsafe void VideoDecoder_OnVideoFrame(AVFrame* frame) internal virtual bool NegotiatePixelFormat(AVCodecID? codecid, int width, int height, int frameRate, AVPixelFormat srcfmt) { if (_negotiatedPixFmt != null && _negotiatedPixFmt != AVPixelFormat.AV_PIX_FMT_NONE) + { return true; + } if (_videoEncoder != null && codecid != null) { @@ -296,7 +304,10 @@ public async Task Close() { _isClosed = true; if (_videoDecoder != null) + { await _videoDecoder.Close(); + } + Dispose(); } } @@ -307,7 +318,9 @@ public Task Pause() { _isPaused = true; if (_videoDecoder != null) + { _videoDecoder?.Pause(); + } } return Task.CompletedTask; @@ -319,7 +332,9 @@ public Task Resume() { _isPaused = false; if (_videoDecoder != null) + { _videoDecoder.Resume(); + } } return Task.CompletedTask; } diff --git a/src/SIPSorceryMedia.FFmpeg/FfmpegInit.cs b/src/SIPSorceryMedia.FFmpeg/FfmpegInit.cs index 2b27a64e0e..e793144260 100644 --- a/src/SIPSorceryMedia.FFmpeg/FfmpegInit.cs +++ b/src/SIPSorceryMedia.FFmpeg/FfmpegInit.cs @@ -33,7 +33,7 @@ public static String GetStoredLogs(Boolean clear = true) { if(clear) { - String log = storedLogs; + var log = storedLogs; storedLogs = ""; return log; } @@ -49,11 +49,16 @@ public static void UseSpecificLogCallback(Boolean storeLogs = true) { // We clear previous stored logs if (storeLogs) + { ClearStoredLogs(); + } logCallback = (p0, level, format, vl) => { - if ( (!storeLogs) && (level > ffmpeg.av_log_get_level())) return; + if ( (!storeLogs) && (level > ffmpeg.av_log_get_level())) + { + return; + } var lineSize = 1024; var lineBuffer = stackalloc byte[lineSize]; @@ -62,7 +67,9 @@ public static void UseSpecificLogCallback(Boolean storeLogs = true) var line = Marshal.PtrToStringAnsi((IntPtr)lineBuffer); //Console.Write(line); if (storeLogs) + { storedLogs += line; + } }; ffmpeg.av_log_set_callback(logCallback); } @@ -113,13 +120,15 @@ internal static void SetFFmpegBinariesPath(string path) internal static void RegisterFFmpegBinaries(String? libPath = null) { if (registered) + { return; + } if (libPath == null) { // search the system path, handle with and without .exe extension - string ffmpegExecutable = "ffmpeg"; - string? path = Environment.GetEnvironmentVariable("PATH")? + var ffmpegExecutable = "ffmpeg"; + var path = Environment.GetEnvironmentVariable("PATH")? .Split([';'], StringSplitOptions.RemoveEmptyEntries) .Where(s => File.Exists(Path.Combine(s, ffmpegExecutable)) || File.Exists(Path.Combine(s, $"{ffmpegExecutable}.exe"))) .FirstOrDefault(); diff --git a/src/SIPSorceryMedia.FFmpeg/Interop/MacOs/AvFoundation.cs b/src/SIPSorceryMedia.FFmpeg/Interop/MacOs/AvFoundation.cs index 6c5d44ee77..5dd993eeab 100644 --- a/src/SIPSorceryMedia.FFmpeg/Interop/MacOs/AvFoundation.cs +++ b/src/SIPSorceryMedia.FFmpeg/Interop/MacOs/AvFoundation.cs @@ -14,7 +14,7 @@ internal class AvFoundation private static unsafe String GetAvFoundationLogsAboutDevicesList() { - String inputFormat = "avfoundation"; + var inputFormat = "avfoundation"; AVInputFormat* avInputFormat = ffmpeg.av_find_input_format(inputFormat); AVFormatContext* pFormatCtx = ffmpeg.avformat_alloc_context(); @@ -36,13 +36,13 @@ private static unsafe String GetAvFoundationLogsAboutDevicesList() static public unsafe List? GetMonitors() { - String logs = GetAvFoundationLogsAboutDevicesList(); + var logs = GetAvFoundationLogsAboutDevicesList(); return ParseAvFoundationLogsForMonitors(logs); } static public unsafe List? GetCameraDevices() { - String logs = GetAvFoundationLogsAboutDevicesList(); + var logs = GetAvFoundationLogsAboutDevicesList(); return ParseAvFoundationLogsForCameras(logs); } @@ -55,22 +55,26 @@ private static unsafe String GetAvFoundationLogsAboutDevicesList() // Do we have at least a video device ? if (logs.Contains(AVFOUNDATION_VIDEO_DEVICE_LOG_OUTPUT)) { - String[] lines = logs.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + var lines = logs.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); String? header = null; int index; - foreach (String line in lines) + foreach (var line in lines) { // If we reach audio devices, we have finish the parsing if (line.Contains(AVFOUNDATION_AUDIO_DEVICE_LOG_OUTPUT)) + { break; + } // Get "header" if (header == null) { index = line.IndexOf(AVFOUNDATION_VIDEO_DEVICE_LOG_OUTPUT); if (index > 0) + { header = line.Substring(0, index); + } } else { @@ -80,12 +84,12 @@ private static unsafe String GetAvFoundationLogsAboutDevicesList() if (line.Contains(header)) { // remove header - String ln = line.Replace(header, ""); + var ln = line.Replace(header, ""); if (ln.StartsWith("[")) { index = ln.IndexOf("]"); - string name = ln.Substring(index + 2); - string path = $"{ln.Substring(1, index - 1)}:"; + var name = ln.Substring(index + 2); + var path = $"{ln.Substring(1, index - 1)}:"; Monitor monitor = new Monitor { @@ -95,7 +99,9 @@ private static unsafe String GetAvFoundationLogsAboutDevicesList() }; if (result == null) + { result = new List(); + } result.Add(monitor); } @@ -119,22 +125,26 @@ private static unsafe String GetAvFoundationLogsAboutDevicesList() // Do we have at least a video device ? if (logs.Contains(AVFOUNDATION_VIDEO_DEVICE_LOG_OUTPUT)) { - String[] lines = logs.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + var lines = logs.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); String? header = null; int index; - foreach (String line in lines) + foreach (var line in lines) { // If we reach audio devices, we have finish the parsing if (line.Contains(AVFOUNDATION_AUDIO_DEVICE_LOG_OUTPUT)) + { break; + } // Get "header" - if(header == null) + if (header == null) { index = line.IndexOf(AVFOUNDATION_VIDEO_DEVICE_LOG_OUTPUT); if (index > 0) + { header = line.Substring(0, index); + } } else { @@ -144,12 +154,12 @@ private static unsafe String GetAvFoundationLogsAboutDevicesList() if (line.Contains(header)) { // remove header - String ln = line.Replace(header, ""); + var ln = line.Replace(header, ""); if(ln.StartsWith("[")) { index = ln.IndexOf("]"); - string name = ln.Substring(index+2); - string path = $"{ln.Substring(1, index - 1)}:"; + var name = ln.Substring(index+2); + var path = $"{ln.Substring(1, index - 1)}:"; Camera camera = new Camera { @@ -158,7 +168,9 @@ private static unsafe String GetAvFoundationLogsAboutDevicesList() }; if (result == null) + { result = new List(); + } result.Add(camera); } diff --git a/src/SIPSorceryMedia.FFmpeg/Interop/Win32/DShow.cs b/src/SIPSorceryMedia.FFmpeg/Interop/Win32/DShow.cs index 5ca0053644..5387cef31a 100644 --- a/src/SIPSorceryMedia.FFmpeg/Interop/Win32/DShow.cs +++ b/src/SIPSorceryMedia.FFmpeg/Interop/Win32/DShow.cs @@ -39,7 +39,9 @@ internal class DShow ffmpeg.avdevice_free_list_devices(&dvls); if (GetDShowCameras(devNames) is var add && add is not null) + { _cachedCameras.AddRange(add); + } return _cachedCameras; } @@ -148,11 +150,16 @@ private static unsafe void UseSpecificLogCallback() { // We clear previous stored logs if (!string.IsNullOrEmpty(storedLogs)) + { storedLogs = string.Empty; + } av_log_set_callback_callback logCallback = (p0, level, format, vl) => { - if (level > ffmpeg.av_log_get_level()) return; + if (level > ffmpeg.av_log_get_level()) + { + return; + } var lineSize = 4096; var lineBuffer = stackalloc byte[lineSize]; diff --git a/src/SIPSorceryMedia.FFmpeg/Interop/Win32/User32.cs b/src/SIPSorceryMedia.FFmpeg/Interop/Win32/User32.cs index d0d920c61a..5ffabca9a4 100644 --- a/src/SIPSorceryMedia.FFmpeg/Interop/Win32/User32.cs +++ b/src/SIPSorceryMedia.FFmpeg/Interop/Win32/User32.cs @@ -42,13 +42,13 @@ struct MonitorInfo public static List GetMonitors() { List result = new List (); - int index = 0; + var index = 0; EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, delegate (IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData) { MonitorInfo mi = new MonitorInfo(); mi.size = (uint)Marshal.SizeOf(mi); - bool success = GetMonitorInfo(hMonitor, ref mi); + var success = GetMonitorInfo(hMonitor, ref mi); Monitor monitor = new Monitor { diff --git a/src/SIPSorceryMedia.FFmpeg/Interop/X11/XLib.cs b/src/SIPSorceryMedia.FFmpeg/Interop/X11/XLib.cs index a95dade3bc..e797ae42a4 100644 --- a/src/SIPSorceryMedia.FFmpeg/Interop/X11/XLib.cs +++ b/src/SIPSorceryMedia.FFmpeg/Interop/X11/XLib.cs @@ -1,4 +1,4 @@ - + using System; using System.Collections.Generic; @@ -20,7 +20,9 @@ internal class XLib public static unsafe Display XOpenDisplay(sbyte* display) { lock (displayLock) + { return sys_XOpenDisplay(display); + } } [DllImport(lib_x11, EntryPoint = "XCloseDisplay")] @@ -43,11 +45,11 @@ public static List GetMonitors() { IntPtr display = XLib.XOpenDisplay(null); IntPtr rootWindow = XLib.XDefaultRootWindow(display); - XRRMonitorInfo* monitors = XRandr.XRRGetMonitors(display, rootWindow, true, out int count); - for (int i = 0; i < count; i++) + XRRMonitorInfo* monitors = XRandr.XRRGetMonitors(display, rootWindow, true, out var count); + for (var i = 0; i < count; i++) { XRRMonitorInfo monitor = monitors[i]; - string nameAddition = monitor.Name == null ? "" : $" ({new string(monitor.Name)})"; + var nameAddition = monitor.Name == null ? "" : $" ({new string(monitor.Name)})"; Monitor m = new Monitor { diff --git a/src/SIPSorceryMedia.FFmpeg/SIPSorceryMedia.FFmpeg.csproj b/src/SIPSorceryMedia.FFmpeg/SIPSorceryMedia.FFmpeg.csproj index 0591ad8bb2..1258b8133d 100644 --- a/src/SIPSorceryMedia.FFmpeg/SIPSorceryMedia.FFmpeg.csproj +++ b/src/SIPSorceryMedia.FFmpeg/SIPSorceryMedia.FFmpeg.csproj @@ -3,7 +3,7 @@ true enable - preview + 14.0 LGPL-2.1-only True true @@ -13,13 +13,20 @@ - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + diff --git a/src/SIPSorceryMedia.FFmpeg/VideoFrameConverter.cs b/src/SIPSorceryMedia.FFmpeg/VideoFrameConverter.cs index 852ac0e439..33242ef022 100644 --- a/src/SIPSorceryMedia.FFmpeg/VideoFrameConverter.cs +++ b/src/SIPSorceryMedia.FFmpeg/VideoFrameConverter.cs @@ -80,12 +80,18 @@ public VideoFrameConverter( private void EnsureNotDisposed() { - if (IsDisposed) { throw new ObjectDisposedException(nameof(VideoFrameConverter)); } + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(VideoFrameConverter)); + } } public void Dispose() { - if (IsDisposed) { return; } + if (IsDisposed) + { + return; + } Marshal.FreeHGlobal(_convertedFrameBufferPtr); _convertedFrameBufferPtr = IntPtr.Zero; @@ -152,8 +158,8 @@ public byte[] ConvertToBuffer(byte[] srcData) ffmpeg.sws_scale(_pConvertContext, src, srcStride, 0, _srcHeight, _dstData, _dstLinesize).ThrowExceptionIfError(); - int outputBufferSize = ffmpeg.av_image_get_buffer_size(_dstPixelFormat, _dstWidth, _dstHeight, 1); - byte[] outputBuffer = new byte[outputBufferSize]; + var outputBufferSize = ffmpeg.av_image_get_buffer_size(_dstPixelFormat, _dstWidth, _dstHeight, 1); + var outputBuffer = new byte[outputBufferSize]; fixed (byte* pOutData = outputBuffer) { @@ -206,8 +212,8 @@ public byte[] ConvertFrame(ref AVFrame frame) ffmpeg.sws_scale(_pConvertContext, frame.data, frame.linesize, 0, frame.height, _dstData, _dstLinesize).ThrowExceptionIfError(); - int outputBufferSize = ffmpeg.av_image_get_buffer_size(_dstPixelFormat, _dstWidth, _dstHeight, 1); - byte[] outputBuffer = new byte[outputBufferSize]; + var outputBufferSize = ffmpeg.av_image_get_buffer_size(_dstPixelFormat, _dstWidth, _dstHeight, 1); + var outputBuffer = new byte[outputBufferSize]; fixed (byte* pOutData = outputBuffer) { diff --git a/src/SIPSorceryMedia.Windows/SIPSorceryMedia.Windows.csproj b/src/SIPSorceryMedia.Windows/SIPSorceryMedia.Windows.csproj index 28cdc51b43..28f12130a6 100644 --- a/src/SIPSorceryMedia.Windows/SIPSorceryMedia.Windows.csproj +++ b/src/SIPSorceryMedia.Windows/SIPSorceryMedia.Windows.csproj @@ -1,6 +1,7 @@  + @@ -15,6 +16,7 @@ SIPSorceryMedia.Windows net10.0-windows10.0.17763.0 10.0.17763.0 + 14.0 true Aaron Clauson Copyright © 2020-2026 Aaron Clauson @@ -68,10 +70,23 @@ + + + + true snupkg + enable + + + + 9999 + + + + 9999 diff --git a/src/SIPSorceryMedia.Windows/WindowsAudioEndPoint.cs b/src/SIPSorceryMedia.Windows/WindowsAudioEndPoint.cs index 194ee8b34e..64a9a57195 100644 --- a/src/SIPSorceryMedia.Windows/WindowsAudioEndPoint.cs +++ b/src/SIPSorceryMedia.Windows/WindowsAudioEndPoint.cs @@ -20,17 +20,22 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Net; +using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.HighPerformance.Buffers; using Microsoft.Extensions.Logging; using NAudio.Wave; +using SIPSorcery.Sys; using SIPSorceryMedia.Abstractions; namespace SIPSorceryMedia.Windows { - public class WindowsAudioEndPoint : IAudioEndPoint + public class WindowsAudioEndPoint : IAudioEndPoint, IDisposable { private const int DEVICE_BITS_PER_SAMPLE = 16; private const int DEFAULT_DEVICE_CHANNELS = 1; @@ -39,6 +44,14 @@ public class WindowsAudioEndPoint : IAudioEndPoint private const int AUDIO_INPUTDEVICE_INDEX = -1; private const int AUDIO_OUTPUTDEVICE_INDEX = -1; + // Audio buffer sizing constants for optimal memory allocation + private const int DEFAULT_AUDIO_BUFFER_SIZE = 4096; // 4KB - optimal for typical audio frames + private const int LARGE_AUDIO_BUFFER_SIZE = 8192; // 8KB - for larger frames + private const int MAX_AUDIO_BUFFER_SIZE = 16384; // 16KB - maximum size cap + + // Adaptive sizing tracking - learns from observed buffer usage patterns + private static int _maxObservedBufferSize = DEFAULT_AUDIO_BUFFER_SIZE; + /// /// Microphone input is sampled at 8KHz. /// @@ -48,23 +61,25 @@ public class WindowsAudioEndPoint : IAudioEndPoint private ILogger logger = SIPSorcery.LogFactory.CreateLogger(); - private WaveFormat _waveSinkFormat; - private WaveFormat _waveSourceFormat; + private WaveFormat? _waveSinkFormat; + private WaveFormat? _waveSourceFormat; + + private bool _isDisposed; /// /// Audio render device. /// - private WaveOutEvent _waveOutEvent; + private WaveOutEvent? _waveOutEvent; /// /// Buffer for audio samples to be rendered. /// - private BufferedWaveProvider _waveProvider; + private Sys.BufferedWaveProvider? _waveProvider; /// /// Audio capture device. /// - private WaveInEvent _waveInEvent; + private WaveInEvent? _waveInEvent; private IAudioEncoder _audioEncoder; private MediaFormatManager _audioFormatManager; @@ -74,42 +89,41 @@ public class WindowsAudioEndPoint : IAudioEndPoint private int _audioInDeviceIndex; private bool _disableSource; - protected bool _isAudioSourceStarted; - protected bool _isAudioSinkStarted; - protected bool _isAudioSourcePaused; - protected bool _isAudioSinkPaused; - protected bool _isAudioSourceClosed; - protected bool _isAudioSinkClosed; + protected int _isAudioSourceStarted; + protected int _isAudioSinkStarted; + protected int _isAudioSourcePaused; + protected int _isAudioSinkPaused; + protected int _isAudioSourceClosed; + protected int _isAudioSinkClosed; /// - /// Obsolete. Use the event instead. + /// Obsolete. Use the event instead. /// - public event EncodedSampleDelegate OnAudioSourceEncodedSample; + [Obsolete("Use OnAudioSourceEncodedFrameReady instead.")] + public event EncodedSampleDelegate? OnAudioSourceEncodedSample; /// /// Event handler for when an encoded audio frame is ready to be sent to the RTP transport layer. /// The sample contained in this event is already encoded with the chosen audio format (codec) and ready for transmission. /// - public event Action OnAudioSourceEncodedFrameReady; + public event Action? OnAudioSourceEncodedFrameReady; /// /// This audio source DOES NOT generate raw samples. Subscribe to the encoded samples event /// to get samples ready for passing to the RTP transport layer. /// [Obsolete("The audio source only generates encoded samples.")] - public event RawAudioSampleDelegate OnAudioSourceRawSample { add { } remove { } } + public event RawAudioSampleDelegate? OnAudioSourceRawSample { add { } remove { } } - public event SourceErrorDelegate OnAudioSourceError; + public event SourceErrorDelegate? OnAudioSourceError; - public event SourceErrorDelegate OnAudioSinkError; + public event SourceErrorDelegate? OnAudioSinkError; /// /// Creates a new basic RTP session that captures and renders audio to/from the default system devices. /// /// An audio encoder that can be used to encode and decode /// specific audio codecs. - /// Optional. An external source to use in combination with the source - /// provided by this end point. The application will need to signal which source is active. /// Set to true to disable the use of the audio source functionality, i.e. /// don't capture input from the microphone. /// Set to true to disable the use of the audio sink functionality, i.e. @@ -132,7 +146,7 @@ public WindowsAudioEndPoint(IAudioEncoder audioEncoder, if (!_disableSink) { - InitPlaybackDevice(_audioOutDeviceIndex, DefaultAudioPlaybackRate.GetHashCode(), DEFAULT_DEVICE_CHANNELS); + InitPlaybackDevice(_audioOutDeviceIndex, (int)DefaultAudioPlaybackRate, DEFAULT_DEVICE_CHANNELS); if (audioEncoder.SupportedFormats?.Count == 1) { @@ -151,50 +165,101 @@ public WindowsAudioEndPoint(IAudioEncoder audioEncoder, } } - public void RestrictFormats(Func filter) => _audioFormatManager.RestrictFormats(filter); - public List GetAudioSourceFormats() => _audioFormatManager.GetSourceFormats(); - public List GetAudioSinkFormats() => _audioFormatManager.GetSourceFormats(); + public void RestrictFormats(Func filter) + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + _audioFormatManager.RestrictFormats(filter); + } + + public List GetAudioSourceFormats() + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + return _audioFormatManager.GetSourceFormats(); + } + + public List GetAudioSinkFormats() + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + return _audioFormatManager.GetSourceFormats(); + } + + public bool HasEncodedAudioSubscribers() + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + return OnAudioSourceEncodedSample is { }; + } + + public bool IsAudioSourcePaused() + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + return Interlocked.CompareExchange(ref _isAudioSourcePaused, 0, 0) == 1; + } + + public bool IsAudioSinkPaused() + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + return Interlocked.CompareExchange(ref _isAudioSinkPaused, 0, 0) == 1; + } + + public void ExternalAudioSourceRawSample(AudioSamplingRatesEnum samplingRate, uint durationMilliseconds, short[] sample) + { + ObjectDisposedException.ThrowIf(_isDisposed, this); - public bool HasEncodedAudioSubscribers() => OnAudioSourceEncodedSample != null; - public bool IsAudioSourcePaused() => _isAudioSourcePaused; - public bool IsAudioSinkPaused() => _isAudioSinkPaused; - public void ExternalAudioSourceRawSample(AudioSamplingRatesEnum samplingRate, uint durationMilliseconds, short[] sample) => throw new NotImplementedException(); + } public void SetAudioSourceFormat(AudioFormat audioFormat) { + ObjectDisposedException.ThrowIf(_isDisposed, this); + _audioFormatManager.SetSelectedFormat(audioFormat); if (!_disableSource) { - if (_waveSourceFormat.SampleRate != _audioFormatManager.SelectedFormat.ClockRate) + var selectedFormat = _audioFormatManager.SelectedFormat; + Debug.Assert(_waveSourceFormat is { }); + Debug.Assert(selectedFormat is { }); + if (_waveSourceFormat.SampleRate != selectedFormat.ClockRate) { // Reinitialise the audio capture device. - logger.LogDebug($"Windows audio end point adjusting capture rate from {_waveSourceFormat.SampleRate} to {_audioFormatManager.SelectedFormat.ClockRate}."); + logger.LogAudioCaptureRateAdjusted(_waveSourceFormat.SampleRate, selectedFormat.ClockRate); - InitCaptureDevice(_audioInDeviceIndex, _audioFormatManager.SelectedFormat.ClockRate, _audioFormatManager.SelectedFormat.ChannelCount); + InitCaptureDevice(_audioInDeviceIndex, selectedFormat.ClockRate, selectedFormat.ChannelCount); } } } public void SetAudioSinkFormat(AudioFormat audioFormat) { + ObjectDisposedException.ThrowIf(_isDisposed, this); + _audioFormatManager.SetSelectedFormat(audioFormat); if (!_disableSink) { - if (_waveSinkFormat.SampleRate != _audioFormatManager.SelectedFormat.ClockRate) + var selectedFormat = _audioFormatManager.SelectedFormat; + Debug.Assert(_waveSinkFormat is { }); + Debug.Assert(selectedFormat is { }); + if (_waveSinkFormat.SampleRate != selectedFormat.ClockRate) { // Reinitialise the audio output device. - logger.LogDebug($"Windows audio end point adjusting playback rate from {_waveSinkFormat.SampleRate} to {_audioFormatManager.SelectedFormat.ClockRate}."); + logger.LogAudioPlaybackRateAdjusted(_waveSinkFormat.SampleRate, selectedFormat.ClockRate); - InitPlaybackDevice(_audioOutDeviceIndex, _audioFormatManager.SelectedFormat.ClockRate, _audioFormatManager.SelectedFormat.ChannelCount); + InitPlaybackDevice(_audioOutDeviceIndex, selectedFormat.ClockRate, selectedFormat.ChannelCount); } } } public MediaEndPoints ToMediaEndPoints() { + ObjectDisposedException.ThrowIf(_isDisposed, this); + return new MediaEndPoints { AudioSource = _disableSource ? null : this, @@ -207,12 +272,14 @@ public MediaEndPoints ToMediaEndPoints() /// public Task Start() { - if (!_isAudioSourceStarted && _waveInEvent != null) + ObjectDisposedException.ThrowIf(_isDisposed, this); + + if (Interlocked.CompareExchange(ref _isAudioSourceStarted, 0, 0) == 0 && _waveInEvent is { }) { StartAudio(); } - if (!_isAudioSinkStarted && _waveOutEvent != null) + if (Interlocked.CompareExchange(ref _isAudioSinkStarted, 0, 0) == 0 && _waveOutEvent is { }) { StartAudioSink(); } @@ -225,12 +292,14 @@ public Task Start() /// public Task Close() { - if (!_isAudioSourceClosed && _waveInEvent != null) + ObjectDisposedException.ThrowIf(_isDisposed, this); + + if (Interlocked.CompareExchange(ref _isAudioSourceClosed, 0, 0) == 0 && _waveInEvent is { }) { CloseAudio(); } - if (!_isAudioSinkClosed && _waveOutEvent != null) + if (Interlocked.CompareExchange(ref _isAudioSinkClosed, 0, 0) == 0 && _waveOutEvent is { }) { CloseAudioSink(); } @@ -240,12 +309,15 @@ public Task Close() public Task Pause() { - if (!_isAudioSourcePaused && _waveInEvent != null) + ObjectDisposedException.ThrowIf(_isDisposed, this); + + if (Interlocked.CompareExchange(ref _isAudioSourcePaused, 0, 0) == 0 && _waveInEvent is { }) { PauseAudio(); } - if (!_isAudioSinkPaused && _waveOutEvent != null) + if (Interlocked.CompareExchange(ref _isAudioSinkPaused, 0, 0) == 0 && _waveOutEvent is { } +) { PauseAudioSink(); } @@ -255,12 +327,14 @@ public Task Pause() public Task Resume() { - if (_isAudioSourcePaused && _waveInEvent != null) + ObjectDisposedException.ThrowIf(_isDisposed, this); + + if (Interlocked.CompareExchange(ref _isAudioSourcePaused, 0, 0) == 1 && _waveInEvent is { }) { ResumeAudio(); } - if (_isAudioSinkPaused && _waveOutEvent != null) + if (Interlocked.CompareExchange(ref _isAudioSinkPaused, 0, 0) == 1 && _waveOutEvent is { }) { ResumeAudioSink(); } @@ -272,7 +346,13 @@ private void InitPlaybackDevice(int audioOutDeviceIndex, int audioSinkSampleRate { try { - _waveOutEvent?.Stop(); + // Dispose existing playback device if present + if (_waveOutEvent is { } waveOutEvent) + { + _waveOutEvent = null; + waveOutEvent.Stop(); + waveOutEvent.Dispose(); + } _waveSinkFormat = new WaveFormat( audioSinkSampleRate, @@ -280,15 +360,17 @@ private void InitPlaybackDevice(int audioOutDeviceIndex, int audioSinkSampleRate channels); // Playback device. + // For playback devices, we'll rely on WaveOutEvent's internal validation + // since the static DeviceCount property may not be available in this NAudio version _waveOutEvent = new WaveOutEvent(); _waveOutEvent.DeviceNumber = audioOutDeviceIndex; - _waveProvider = new BufferedWaveProvider(_waveSinkFormat); + _waveProvider = new Sys.BufferedWaveProvider(_waveSinkFormat); _waveProvider.DiscardOnBufferOverflow = true; _waveOutEvent.Init(_waveProvider); } catch (Exception excp) { - logger.LogWarning(0, excp, "WindowsAudioEndPoint failed to initialise playback device."); + logger.LogPlaybackDeviceInitFailed(excp); OnAudioSinkError?.Invoke($"WindowsAudioEndPoint failed to initialise playback device. {excp.Message}"); } } @@ -297,38 +379,49 @@ private void InitCaptureDevice(int audioInDeviceIndex, int audioSourceSampleRate { if (WaveInEvent.DeviceCount > 0) { - if (WaveInEvent.DeviceCount > audioInDeviceIndex) + // Handle default device (-1) and validate range properly + if (audioInDeviceIndex >= -1 && audioInDeviceIndex < WaveInEvent.DeviceCount) { - if (_waveInEvent != null) + try { - _waveInEvent.DataAvailable -= LocalAudioSampleAvailable; - _waveInEvent.StopRecording(); + if (_waveInEvent is { } waveInEvent) + { + _waveInEvent = null; + waveInEvent.DataAvailable -= LocalAudioSampleAvailable; + waveInEvent.StopRecording(); + waveInEvent.Dispose(); + } + + _waveSourceFormat = new WaveFormat( + audioSourceSampleRate, + DEVICE_BITS_PER_SAMPLE, + audioSourceChannels); + + _waveInEvent = new WaveInEvent(); + + // Note NAudio recommends a buffer size of 100ms but codecs like Opus can only handle 20ms buffers. + _waveInEvent.BufferMilliseconds = DEFAULT_PLAYBACK_BUFFER_MILLISECONDS; + + _waveInEvent.NumberOfBuffers = INPUT_BUFFERS; + _waveInEvent.DeviceNumber = audioInDeviceIndex; + _waveInEvent.WaveFormat = _waveSourceFormat; + _waveInEvent.DataAvailable += LocalAudioSampleAvailable; + } + catch (Exception ex) + { + logger.LogAudioCaptureDeviceInitFailed(audioInDeviceIndex, ex); + OnAudioSourceError?.Invoke($"Failed to initialize audio capture device: {ex.Message}"); } - - _waveSourceFormat = new WaveFormat( - audioSourceSampleRate, - DEVICE_BITS_PER_SAMPLE, - audioSourceChannels); - - _waveInEvent = new WaveInEvent(); - - // Note NAudio recommends a buffer size of 100ms but codecs like Opus can only handle 20ms buffers. - _waveInEvent.BufferMilliseconds = DEFAULT_PLAYBACK_BUFFER_MILLISECONDS; - - _waveInEvent.NumberOfBuffers = INPUT_BUFFERS; - _waveInEvent.DeviceNumber = audioInDeviceIndex; - _waveInEvent.WaveFormat = _waveSourceFormat; - _waveInEvent.DataAvailable += LocalAudioSampleAvailable; } else { - logger.LogWarning($"The requested audio input device index {audioInDeviceIndex} exceeds the maximum index of {WaveInEvent.DeviceCount - 1}."); - OnAudioSourceError?.Invoke($"The requested audio input device index {audioInDeviceIndex} exceeds the maximum index of {WaveInEvent.DeviceCount - 1}."); + logger.LogAudioInputDeviceIndexExceeded(audioInDeviceIndex, WaveInEvent.DeviceCount - 1); + OnAudioSourceError?.Invoke($"The requested audio input device index {audioInDeviceIndex} exceeds the maximum index of {WaveInEvent.DeviceCount - 1}. Use -1 for default device."); } } else { - logger.LogWarning("No audio capture devices are available."); + logger.LogNoAudioCaptureDevices(); OnAudioSourceError?.Invoke("No audio capture devices are available."); } } @@ -336,34 +429,38 @@ private void InitCaptureDevice(int audioInDeviceIndex, int audioSourceSampleRate /// /// Event handler for audio sample being supplied by local capture device. /// - private void LocalAudioSampleAvailable(object sender, WaveInEventArgs args) + private void LocalAudioSampleAvailable(object? sender, WaveInEventArgs args) { // Note NAudio.Wave.WaveBuffer.ShortBuffer does not take into account little endian. // https://github.com/naudio/NAudio/blob/master/NAudio/Wave/WaveOutputs/WaveBuffer.cs - byte[] buffer = args.Buffer.Take(args.BytesRecorded).ToArray(); - short[] pcm = buffer.Where((x, i) => i % 2 == 0).Select((y, i) => BitConverter.ToInt16(buffer, i * 2)).ToArray(); - byte[] encodedSample = _audioEncoder.EncodeAudio(pcm, _audioFormatManager.SelectedFormat); - - OnAudioSourceEncodedSample?.Invoke((uint)encodedSample.Length, encodedSample); + var pcm = MemoryMarshal.Cast(args.Buffer.AsSpan(0, args.BytesRecorded)); + + // Use adaptive buffer sizing for optimal memory allocation + using var buffer = new ArrayPoolBufferWriter(GetOptimalBufferSize(pcm.Length)); + _audioEncoder.EncodeAudio(pcm, _audioFormatManager.SelectedFormat, buffer); + + var encodedMemory = buffer.WrittenMemory; - if (OnAudioSourceEncodedFrameReady != null) + var onAudioSourceEncodedSample = OnAudioSourceEncodedSample; + var onAudioSourceEncodedFrameReady = OnAudioSourceEncodedFrameReady; + + if (!encodedMemory.IsEmpty && (onAudioSourceEncodedSample is { } || onAudioSourceEncodedFrameReady is { })) { - var encodedAudioFrame = new EncodedAudioFrame(0, - _audioFormatManager.SelectedFormat, - GetEncodSampleDurationMs(pcm.Length, _audioFormatManager.SelectedFormat), - encodedSample); - OnAudioSourceEncodedFrameReady(encodedAudioFrame); - } - } + onAudioSourceEncodedSample?.Invoke((uint)encodedMemory.Length, encodedMemory); - private uint GetEncodSampleDurationMs(int totalPcmSamples, AudioFormat audioFormat) - { - int numChannels = audioFormat.ChannelCount; - int sampleRate = audioFormat.ClockRate; - int frames = totalPcmSamples / numChannels; - double durationMsD = sampleRate > 0 ? (frames / (double)sampleRate) * 1000.0 : 0; - return (uint)Math.Round(durationMsD); + if (onAudioSourceEncodedFrameReady is { }) + { + var audioFormat = _audioFormatManager.SelectedFormat; + var numChannels = audioFormat.ChannelCount; + var sampleRate = audioFormat.ClockRate; + var frames = pcm.Length / numChannels; + var durationMsD = sampleRate > 0 ? (frames / (double)sampleRate) * 1000.0 : 0; + var durationMs = (uint)Math.Round(durationMsD); + var encodedFrame = new EncodedAudioFrame(0, audioFormat, durationMs, encodedMemory); + onAudioSourceEncodedFrameReady(encodedFrame); + } + } } /// @@ -372,10 +469,9 @@ private uint GetEncodSampleDurationMs(int totalPcmSamples, AudioFormat audioForm /// Raw PCM sample from remote party. public void GotAudioSample(byte[] pcmSample) { - if (_waveProvider != null) - { - _waveProvider.AddSamples(pcmSample, 0, pcmSample.Length); - } + ObjectDisposedException.ThrowIf(_isDisposed, this); + + _waveProvider?.AddSamples(pcmSample.AsSpan()); } /// @@ -384,12 +480,9 @@ public void GotAudioSample(byte[] pcmSample) [Obsolete("Use GotEncodedMediaFrame instead.")] public void GotAudioRtp(IPEndPoint remoteEndPoint, uint ssrc, uint seqnum, uint timestamp, int payloadID, bool marker, byte[] payload) { - if (_waveProvider != null && _audioEncoder != null) - { - var pcmSample = _audioEncoder.DecodeAudio(payload, _audioFormatManager.SelectedFormat); - byte[] pcmBytes = pcmSample.SelectMany(BitConverter.GetBytes).ToArray(); - _waveProvider?.AddSamples(pcmBytes, 0, pcmBytes.Length); - } + ObjectDisposedException.ThrowIf(_isDisposed, this); + + ProcessDecodedAudio(payload.AsMemory(), _audioFormatManager.SelectedFormat); } /// @@ -398,35 +491,60 @@ public void GotAudioRtp(IPEndPoint remoteEndPoint, uint ssrc, uint seqnum, uint /// Encoded audio frame received from the remote party. public void GotEncodedMediaFrame(EncodedAudioFrame encodedMediaFrame) { - var audioFormat = encodedMediaFrame.AudioFormat; + ObjectDisposedException.ThrowIf(_isDisposed, this); - if (_waveProvider != null && _audioEncoder != null && !audioFormat.IsEmpty()) + ProcessDecodedAudio(encodedMediaFrame.EncodedAudio, encodedMediaFrame.AudioFormat); + } + + /// + /// Common helper method to process decoded PCM audio samples and add them to the wave provider. + /// + /// The encoded audio data to decode. + /// The audio format for decoding. + private void ProcessDecodedAudio(ReadOnlyMemory encodedAudio, AudioFormat audioFormat) + { + if (_waveProvider is { } && _audioEncoder is { } && !audioFormat.IsEmpty()) { - var pcmSample = _audioEncoder.DecodeAudio(encodedMediaFrame.EncodedAudio, audioFormat); - byte[] pcmBytes = pcmSample.SelectMany(BitConverter.GetBytes).ToArray(); - _waveProvider?.AddSamples(pcmBytes, 0, pcmBytes.Length); + // Use optimal buffer sizing for PCM shorts buffer (decode typically expands data 3-4x) + var estimatedPcmShorts = encodedAudio.Length * 4; // Conservative estimate for decoded size + using var pcmShortsBuffer = new ArrayPoolBufferWriter(GetOptimalBufferSize(estimatedPcmShorts)); + _audioEncoder.DecodeAudio(encodedAudio.Span, audioFormat, pcmShortsBuffer); + + var pcmSpan = pcmShortsBuffer.WrittenSpan; + if (pcmSpan.IsEmpty) + { + return; + } + + var pcmBytes = MemoryMarshal.Cast(pcmSpan); + _waveProvider.AddSamples(pcmBytes); } } public Task PauseAudioSink() { - _isAudioSinkPaused = true; + ObjectDisposedException.ThrowIf(_isDisposed, this); + + Interlocked.Exchange(ref _isAudioSinkPaused, 1); _waveOutEvent?.Pause(); return Task.CompletedTask; } public Task ResumeAudioSink() { - _isAudioSinkPaused = false; + ObjectDisposedException.ThrowIf(_isDisposed, this); + + Interlocked.Exchange(ref _isAudioSinkPaused, 0); _waveOutEvent?.Play(); return Task.CompletedTask; } public Task StartAudioSink() { - if (!_isAudioSinkStarted) + ObjectDisposedException.ThrowIf(_isDisposed, this); + + if (Interlocked.CompareExchange(ref _isAudioSinkStarted, 1, 0) == 0) { - _isAudioSinkStarted = true; _waveOutEvent?.Play(); } return Task.CompletedTask; @@ -434,10 +552,10 @@ public Task StartAudioSink() public Task CloseAudioSink() { - if (!_isAudioSinkClosed) - { - _isAudioSinkClosed = true; + ObjectDisposedException.ThrowIf(_isDisposed, this); + if (Interlocked.CompareExchange(ref _isAudioSinkClosed, 1, 0) == 0) + { _waveOutEvent?.Stop(); } @@ -449,7 +567,9 @@ public Task CloseAudioSink() /// public Task PauseAudio() { - _isAudioSourcePaused = true; + ObjectDisposedException.ThrowIf(_isDisposed, this); + + Interlocked.Exchange(ref _isAudioSourcePaused, 1); _waveInEvent?.StopRecording(); return Task.CompletedTask; @@ -460,7 +580,9 @@ public Task PauseAudio() /// public Task ResumeAudio() { - _isAudioSourcePaused = false; + ObjectDisposedException.ThrowIf(_isDisposed, this); + + Interlocked.Exchange(ref _isAudioSourcePaused, 0); _waveInEvent?.StartRecording(); return Task.CompletedTask; @@ -471,9 +593,10 @@ public Task ResumeAudio() /// public Task StartAudio() { - if (!_isAudioSourceStarted) + ObjectDisposedException.ThrowIf(_isDisposed, this); + + if (Interlocked.CompareExchange(ref _isAudioSourceStarted, 1, 0) == 0) { - _isAudioSourceStarted = true; _waveInEvent?.StartRecording(); } @@ -485,11 +608,11 @@ public Task StartAudio() ///
public Task CloseAudio() { - if (!_isAudioSourceClosed) - { - _isAudioSourceClosed = true; + ObjectDisposedException.ThrowIf(_isDisposed, this); - if (_waveInEvent != null) + if (Interlocked.CompareExchange(ref _isAudioSourceClosed, 1, 0) == 0) + { + if (_waveInEvent is { }) { _waveInEvent.DataAvailable -= LocalAudioSampleAvailable; _waveInEvent.StopRecording(); @@ -498,5 +621,60 @@ public Task CloseAudio() return Task.CompletedTask; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed && disposing) + { + _isDisposed = true; + + if (_waveInEvent is { } waveInEvent) + { + _waveInEvent = null; + waveInEvent.DataAvailable -= LocalAudioSampleAvailable; + waveInEvent.Dispose(); + } + + if (_waveOutEvent is { } waveOutEvent) + { + _waveOutEvent = null; + waveOutEvent.Dispose(); + } + } + } + + /// + /// Calculates optimal buffer size for audio processing based on PCM sample count. + /// Uses adaptive sizing to learn from usage patterns and reduce buffer fragmentation. + /// + /// Number of PCM samples to be encoded + /// Optimal buffer size for PooledSegmentedBuffer allocation + private static int GetOptimalBufferSize(int pcmSampleCount) + { + // Estimate encoded size (typically 25-50% of PCM for modern codecs like Opus) + var estimatedEncodedSize = pcmSampleCount * sizeof(short) / 3; // Conservative estimate + + var optimalSize = estimatedEncodedSize switch + { + <= 2048 => DEFAULT_AUDIO_BUFFER_SIZE, // 4KB for normal frames + <= 4096 => LARGE_AUDIO_BUFFER_SIZE, // 8KB for large frames + _ => MAX_AUDIO_BUFFER_SIZE // 16KB for very large frames + }; + + // Adaptive sizing: learn from observed patterns to reduce future fragmentation + if (estimatedEncodedSize > _maxObservedBufferSize && estimatedEncodedSize <= MAX_AUDIO_BUFFER_SIZE) + { + _maxObservedBufferSize = estimatedEncodedSize; + } + + // Use the larger of calculated optimal size or previously observed maximum + return Math.Max(optimalSize, _maxObservedBufferSize); + } } } diff --git a/src/SIPSorceryMedia.Windows/WindowsMediaLoggingExtensions.cs b/src/SIPSorceryMedia.Windows/WindowsMediaLoggingExtensions.cs new file mode 100644 index 0000000000..ba28eb5331 --- /dev/null +++ b/src/SIPSorceryMedia.Windows/WindowsMediaLoggingExtensions.cs @@ -0,0 +1,85 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace SIPSorceryMedia.Windows +{ + internal static partial class WindowsMediaLoggingExtensions + { + [LoggerMessage( + EventId = 1, + EventName = "AudioCaptureRateAdjusted", + Level = LogLevel.Debug, + Message = "Windows audio end point adjusting capture rate from {oldRate} to {newRate}.")] + public static partial void LogAudioCaptureRateAdjusted(this ILogger logger, int oldRate, int newRate); + + [LoggerMessage( + EventId = 2, + EventName = "AudioPlaybackRateAdjusted", + Level = LogLevel.Debug, + Message = "Windows audio end point adjusting playback rate from {oldRate} to {newRate}.")] + public static partial void LogAudioPlaybackRateAdjusted(this ILogger logger, int oldRate, int newRate); + + [LoggerMessage( + EventId = 3, + EventName = "PlaybackDeviceInitFailed", + Level = LogLevel.Warning, + Message = "WindowsAudioEndPoint failed to initialise playback device.")] + public static partial void LogPlaybackDeviceInitFailed(this ILogger logger, Exception exception); + + [LoggerMessage( + EventId = 4, + EventName = "AudioInputDeviceIndexExceeded", + Level = LogLevel.Warning, + Message = "The requested audio input device index {AudioInDeviceIndex} exceeds the maximum index of {maxIndex}.")] + public static partial void LogAudioInputDeviceIndexExceeded(this ILogger logger, int audioInDeviceIndex, int maxIndex); + + [LoggerMessage( + EventId = 5, + EventName = "NoAudioCaptureDevices", + Level = LogLevel.Warning, + Message = "No audio capture devices are available.")] + public static partial void LogNoAudioCaptureDevices(this ILogger logger); + + [LoggerMessage( + EventId = 6, + EventName = "AudioCaptureDeviceInitFailed", + Level = LogLevel.Warning, + Message = "Failed to initialize audio capture device {DeviceIndex}.")] + public static partial void LogAudioCaptureDeviceInitFailed(this ILogger logger, int deviceIndex, Exception exception); + + [LoggerMessage( + EventId = 7, + EventName = "VideoCaptureDeviceFormat", + Level = LogLevel.Debug, + Message = "Video Capture device {deviceName} format {Width}x{Height} {Fps:0.##}fps {PixelFormat}")] + public static partial void LogVideoCaptureDeviceFormat(this ILogger logger, string deviceName, uint width, uint height, float fps, string pixelFormat); + + [LoggerMessage( + EventId = 8, + EventName = "VideoDeviceNotFound", + Level = LogLevel.Warning, + Message = "Could not find video capture device for specified ID {VideoDeviceID}, using default device.")] + public static partial void LogVideoDeviceNotFound(this ILogger logger, string videoDeviceID); + + [LoggerMessage( + EventId = 9, + EventName = "VideoDeviceSelected", + Level = LogLevel.Information, + Message = "Video capture device {DeviceName} selected.")] + public static partial void LogVideoDeviceSelected(this ILogger logger, string deviceName); + + [LoggerMessage( + EventId = 10, + EventName = "VideoFormatNotSupported", + Level = LogLevel.Warning, + Message = "The video capture device did not support the requested format (or better) {Width}x{Height} {Fps}fps. Using default mode.")] + public static partial void LogVideoFormatNotSupported(this ILogger logger, uint width, uint height, uint fps); + + [LoggerMessage( + EventId = 11, + EventName = "VpxDecodeFailed", + Level = LogLevel.Warning, + Message = "VPX decode of video sample failed.")] + public static partial void LogVpxDecodeFailed(this ILogger logger); + } +} diff --git a/src/SIPSorceryMedia.Windows/WindowsVideoEndPoint.cs b/src/SIPSorceryMedia.Windows/WindowsVideoEndPoint.cs index a2ea1f0648..84c505b527 100644 --- a/src/SIPSorceryMedia.Windows/WindowsVideoEndPoint.cs +++ b/src/SIPSorceryMedia.Windows/WindowsVideoEndPoint.cs @@ -14,15 +14,17 @@ // BDS BY-NC-SA restriction, see included LICENSE.md file. //----------------------------------------------------------------------------- -using Microsoft.Extensions.Logging; -using SIPSorceryMedia.Abstractions; using System; +using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SIPSorceryMedia.Abstractions; using Windows.Devices.Enumeration; using Windows.Graphics.Imaging; using Windows.Media.Capture; @@ -68,9 +70,9 @@ public class WindowsVideoEndPoint : IVideoEndPoint, IDisposable private bool _isPaused; private bool _isClosed; private MediaCapture _mediaCapture; - private MediaFrameReader _mediaFrameReader; - private SoftwareBitmap _backBuffer; - private string _videoDeviceID; + private MediaFrameReader? _mediaFrameReader; + private SoftwareBitmap? _backBuffer; + private string? _videoDeviceID; private uint _width = 0; private uint _height = 0; private uint _fpsNumerator = 0; @@ -84,36 +86,36 @@ public class WindowsVideoEndPoint : IVideoEndPoint, IDisposable /// event is fired after the sample /// has been encoded and is ready for transmission. ///
- public event RawVideoSampleDelegate OnVideoSourceRawSample; + public event RawVideoSampleDelegate? OnVideoSourceRawSample; #pragma warning disable 0067 /// /// Event Not used in this component - use instead /// - public event RawVideoSampleFasterDelegate OnVideoSourceRawSampleFaster; + public event RawVideoSampleFasterDelegate? OnVideoSourceRawSampleFaster; #pragma warning restore 0067 /// /// This event will be fired whenever a video sample is encoded and is ready to transmit to the remote party. /// - public event EncodedSampleDelegate OnVideoSourceEncodedSample; + public event EncodedSampleDelegate? OnVideoSourceEncodedSample; /// /// This event is fired after the sink decodes a video frame from the remote party. /// - public event VideoSinkSampleDecodedDelegate OnVideoSinkDecodedSample; + public event VideoSinkSampleDecodedDelegate? OnVideoSinkDecodedSample; #pragma warning disable 0067 /// /// Event Not used in this component - use instead /// - public event VideoSinkSampleDecodedFasterDelegate OnVideoSinkDecodedSampleFaster; + public event VideoSinkSampleDecodedFasterDelegate? OnVideoSinkDecodedSampleFaster; #pragma warning restore 0067 /// /// This event will be fired if there is a problem acquiring the capture device. /// - public event SourceErrorDelegate OnVideoSourceError; + public event SourceErrorDelegate? OnVideoSourceError; /// /// Attempts to create a new video source from a local video capture device. @@ -129,7 +131,7 @@ public class WindowsVideoEndPoint : IVideoEndPoint, IDisposable /// rate. If the attempt fails an exception is thrown. If not specified the device's default frame rate will /// be used. public WindowsVideoEndPoint(IVideoEncoder videoEncoder, - string videoDeviceID = null, + string? videoDeviceID = null, uint width = 0, uint height = 0, uint fps = 0) @@ -156,14 +158,14 @@ public WindowsVideoEndPoint(IVideoEncoder videoEncoder, public List GetVideoSinkFormats() => _videoFormatManager.GetSourceFormats(); public void SetVideoSinkFormat(VideoFormat videoFormat) => _videoFormatManager.SetSelectedFormat(videoFormat); public void ExternalVideoSourceRawSample(uint durationMilliseconds, int width, int height, byte[] sample, VideoPixelFormatsEnum pixelFormat) => - throw new ApplicationException("The Windows Video End Point does not support external samples. Use the video end point from SIPSorceryMedia.Encoders."); + throw new SipSorceryMediaException("The Windows Video End Point does not support external samples. Use the video end point from SIPSorceryMedia.Encoders."); public void ExternalVideoSourceRawSampleFaster(uint durationMilliseconds, RawImage rawImage) => - throw new ApplicationException("The Windows Video End Point does not support external samples. Use the video end point from SIPSorceryMedia.Encoders."); + throw new SipSorceryMediaException("The Windows Video End Point does not support external samples. Use the video end point from SIPSorceryMedia.Encoders."); public void ForceKeyFrame() => _forceKeyFrame = true; public void GotVideoRtp(IPEndPoint remoteEndPoint, uint ssrc, uint seqnum, uint timestamp, int payloadID, bool marker, byte[] payload) => - throw new ApplicationException("The Windows Video End Point requires full video frames rather than individual RTP packets."); + throw new SipSorceryMediaException("The Windows Video End Point requires full video frames rather than individual RTP packets."); public bool HasEncodedVideoSubscribers() => OnVideoSourceEncodedSample != null; public bool IsVideoSourcePaused() => _isPaused; @@ -227,22 +229,27 @@ private async Task InitialiseDevice(uint width, uint height, uint fps) if (_videoDeviceID != null) { var vidCapDevices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture).AsTask().ConfigureAwait(false); - var vidDevice = vidCapDevices.FirstOrDefault(x => x.Id == _videoDeviceID || x.Name == _videoDeviceID); + FindDeviceByIdOrName(vidCapDevices, _videoDeviceID); - if (vidDevice == null) + void FindDeviceByIdOrName(IEnumerable devices, string idOrName) { - logger.LogWarning("Could not find video capture device for specified ID {VideoDeviceId}, using default device.", _videoDeviceID); + foreach (var device in devices) + { + if (device.Id == idOrName || device.Name == idOrName) + { + mediaCaptureSettings.VideoDeviceId = device.Id; + logger.LogVideoDeviceSelected(device.Name); + return; + } } - else - { - logger.LogInformation("Video capture device {VideoDeviceName} selected.", vidDevice.Name); - mediaCaptureSettings.VideoDeviceId = vidDevice.Id; + + logger.LogVideoDeviceNotFound(_videoDeviceID); } } await _mediaCapture.InitializeAsync(mediaCaptureSettings).AsTask().ConfigureAwait(false); - MediaFrameSourceInfo colorSourceInfo = null; + MediaFrameSourceInfo? colorSourceInfo = null; foreach (var srcInfo in _mediaCapture.FrameSources) { if (srcInfo.Value.Info.MediaStreamType == MediaStreamType.VideoRecord && @@ -253,39 +260,45 @@ private async Task InitialiseDevice(uint width, uint height, uint fps) } } + Debug.Assert(colorSourceInfo is not null, "No suitable video capture source was found on the system."); var colorFrameSource = _mediaCapture.FrameSources[colorSourceInfo.Id]; - var preferredFormat = colorFrameSource.SupportedFormats.Where(format => + MediaFrameFormat? FindPreferredFormat(IEnumerable formats, uint width, uint height, uint fps) { - return format.VideoFormat.Width >= _width && - format.VideoFormat.Width >= _height && - (format.FrameRate.Numerator / format.FrameRate.Denominator) >= fps - && format.Subtype == MF_NV12_PIXEL_FORMAT; - }).FirstOrDefault(); + MediaFrameFormat? firstAny = null; + foreach (var format in formats) + { + if (format.VideoFormat.Width >= width && + format.VideoFormat.Width >= height && + (format.FrameRate.Numerator / format.FrameRate.Denominator) >= fps) + { + if (format.Subtype == MF_NV12_PIXEL_FORMAT) + { + return format; + } - if (preferredFormat == null) + if (firstAny is null) { - // Try again without the pixel format. - preferredFormat = colorFrameSource.SupportedFormats.Where(format => - { - return format.VideoFormat.Width >= _width && - format.VideoFormat.Width >= _height && - (format.FrameRate.Numerator / format.FrameRate.Denominator) >= fps; - }).FirstOrDefault(); + firstAny = format; + } + } + } + return firstAny; } - if (preferredFormat == null) + var preferredFormat = FindPreferredFormat(colorFrameSource.SupportedFormats, _width, _height, fps); + + if (preferredFormat is null) { // Still can't get what we want. Log a warning message and take the default. - logger.LogWarning("The video capture device did not support the requested format (or better) {Width}x{Height} {Fps}fps. Using default mode.", - _width, _height, fps); + logger.LogVideoFormatNotSupported(_width, _height, fps); - preferredFormat = colorFrameSource.SupportedFormats.First(); + preferredFormat = colorFrameSource.SupportedFormats[0]; } - if (preferredFormat == null) + if (preferredFormat is null) { - throw new ApplicationException("The video capture device does not support a compatible video format for the requested parameters."); + throw new SipSorceryMediaException("The video capture device does not support a compatible video format for the requested parameters."); } await colorFrameSource.SetFormatAsync(preferredFormat).AsTask().ConfigureAwait(false); @@ -311,18 +324,19 @@ private async Task InitialiseDevice(uint width, uint height, uint fps) return true; } - public void GotVideoFrame(IPEndPoint remoteEndPoint, uint timestamp, byte[] frame, VideoFormat format) + public void GotVideoFrame(IPEndPoint remoteEndPoint, uint timestamp, ReadOnlyMemory frame, VideoFormat format) { if (!_isClosed) { //DateTime startTime = DateTime.Now; //List decodedFrames = _vp8Decoder.Decode(frame, frame.Length, out var width, out var height); - var decodedFrames = _videoEncoder.DecodeVideo(frame, EncoderInputFormat, _videoFormatManager.SelectedFormat.Codec); + // TODO: use ReadOnlySequence without ToArray() to avoid unnecessary copying of the frame data. + var decodedFrames = _videoEncoder.DecodeVideo(frame.ToArray(), EncoderInputFormat, _videoFormatManager.SelectedFormat.Codec); if (decodedFrames == null) { - logger.LogWarning("VPX decode of video sample failed."); + logger.LogVpxDecodeFailed(); } else { @@ -331,7 +345,7 @@ public void GotVideoFrame(IPEndPoint remoteEndPoint, uint timestamp, byte[] fram // Windows bitmaps expect BGR when supplying System.Drawing.Imaging.PixelFormat.Format24bppRgb. //byte[] bgr = PixelConverter.I420toBGR(decodedFrame.Sample, (int)decodedFrame.Width, (int)decodedFrame.Height); //Console.WriteLine($"VP8 decode took {DateTime.Now.Subtract(startTime).TotalMilliseconds}ms."); - OnVideoSinkDecodedSample(decodedFrame.Sample, decodedFrame.Width, decodedFrame.Height, (int)(decodedFrame.Width * 3), VideoPixelFormatsEnum.Bgr); + OnVideoSinkDecodedSample?.Invoke(decodedFrame.Sample, decodedFrame.Width, decodedFrame.Height, (int)(decodedFrame.Width * 3), VideoPixelFormatsEnum.Bgr); } } } @@ -376,6 +390,8 @@ public async Task StartVideo() await InitialiseVideoSourceDevice().ConfigureAwait(false); } + Debug.Assert(_mediaFrameReader is not null); + await _mediaFrameReader.StartAsync().AsTask().ConfigureAwait(false); } } @@ -405,13 +421,20 @@ public async Task CloseVideo() /// /// Attempts to list the system video capture devices and supported video modes. /// - public static async Task> GetVideoCatpureDevices() + public static async Task?> GetVideoCatpureDevices() { var vidCapDevices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture); if (vidCapDevices != null) { - return vidCapDevices.Select(x => new VideoCaptureDeviceInfo { ID = x.Id, Name = x.Name }).ToList(); + var result = new List(vidCapDevices.Count); + + foreach (var x in vidCapDevices) + { + result.Add(new VideoCaptureDeviceInfo { ID = x.Id, Name = x.Name }); + } + + return result; } else { @@ -446,18 +469,24 @@ public static async Task ListDevicesAndFormats() VideoDeviceId = vidCapDevice.Id }; - MediaCapture mediaCapture = new MediaCapture(); + var mediaCapture = new MediaCapture(); await mediaCapture.InitializeAsync(mediaCaptureSettings); - foreach (var srcFmtList in mediaCapture.FrameSources.Values.Select(x => x.SupportedFormats).Select(y => y.ToList())) + foreach (var kvp in mediaCapture.FrameSources) { - foreach (var srcFmt in srcFmtList) + var supportedFormats = kvp.Value.SupportedFormats; + foreach (var srcFmt in supportedFormats) { var vidFmt = srcFmt.VideoFormat; - float vidFps = vidFmt.MediaFrameFormat.FrameRate.Numerator / vidFmt.MediaFrameFormat.FrameRate.Denominator; + float vidFps = 0f; + uint num = vidFmt.MediaFrameFormat.FrameRate.Numerator; + uint den = vidFmt.MediaFrameFormat.FrameRate.Denominator; + if (den != 0) + { + vidFps = (float)num / den; + } string pixFmt = vidFmt.MediaFrameFormat.Subtype == MF_I420_PIXEL_FORMAT ? "I420" : vidFmt.MediaFrameFormat.Subtype; - logger.LogDebug("Video Capture device {VideoDeviceName} format {Width}x{Height} {Fps:0.##}fps {PixelFormat}", - vidCapDevice.Name, vidFmt.Width, vidFmt.Height, vidFps, pixFmt); + logger.LogVideoCaptureDeviceFormat(vidCapDevice.Name, vidFmt.Width, vidFmt.Height, vidFps, pixFmt); } } } @@ -470,17 +499,14 @@ public static async Task ListDevicesAndFormats() /// A list of supported video frame formats for the specified webcam. public static async Task> GetDeviceFrameFormats(string deviceName) { - if(string.IsNullOrEmpty(deviceName)) - { - throw new ArgumentNullException(nameof(deviceName), "A webcam name must be specified to get the video formats for."); - } + ArgumentNullException.ThrowIfNullOrEmpty(deviceName); List formats = new List(); var vidCapDevices = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture); foreach (var vidCapDevice in vidCapDevices) { - if(string.Equals(vidCapDevice.Name, deviceName, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(vidCapDevice.Name, deviceName, StringComparison.OrdinalIgnoreCase)) { var mediaCaptureSettings = new MediaCaptureInitializationSettings() { @@ -489,12 +515,13 @@ public static async Task> GetDeviceFrameFormats(stri VideoDeviceId = vidCapDevice.Id }; - MediaCapture mediaCapture = new MediaCapture(); + var mediaCapture = new MediaCapture(); await mediaCapture.InitializeAsync(mediaCaptureSettings); - foreach (var srcFmtList in mediaCapture.FrameSources.Values.Select(x => x.SupportedFormats).Select(y => y.ToList())) + foreach (var kvp in mediaCapture.FrameSources) { - foreach (var srcFmt in srcFmtList) + var supportedFormats = kvp.Value.SupportedFormats; + foreach (var srcFmt in supportedFormats) { formats.Add(srcFmt.VideoFormat); } @@ -541,8 +568,8 @@ private async void FrameArrivedHandler(MediaFrameReader sender, MediaFrameArrive if (softwareBitmap != null) { - int width = softwareBitmap.PixelWidth; - int height = softwareBitmap.PixelHeight; + var width = softwareBitmap.PixelWidth; + var height = softwareBitmap.PixelHeight; if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Nv12) { @@ -552,7 +579,7 @@ private async void FrameArrivedHandler(MediaFrameReader sender, MediaFrameArrive // Swap the processed frame to _backBuffer and dispose of the unused image. softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap); - using (BitmapBuffer buffer = _backBuffer.LockBuffer(BitmapBufferAccessMode.Read)) + using (var buffer = _backBuffer.LockBuffer(BitmapBufferAccessMode.Read)) { using (var reference = buffer.CreateReference()) { @@ -561,7 +588,7 @@ private async void FrameArrivedHandler(MediaFrameReader sender, MediaFrameArrive byte* dataInBytes; uint capacity; reference.As().GetBuffer(out dataInBytes, out capacity); - byte[] nv12Buffer = new byte[capacity]; + var nv12Buffer = new byte[capacity]; Marshal.Copy((IntPtr)dataInBytes, nv12Buffer, 0, (int)capacity); if (OnVideoSourceEncodedSample != null) @@ -572,8 +599,8 @@ private async void FrameArrivedHandler(MediaFrameReader sender, MediaFrameArrive if (encodedBuffer != null) { - uint fps = (_fpsDenominator > 0 && _fpsNumerator > 0) ? _fpsNumerator / _fpsDenominator : DEFAULT_FRAMES_PER_SECOND; - uint durationRtpTS = VIDEO_SAMPLING_RATE / fps; + var fps = (_fpsDenominator > 0 && _fpsNumerator > 0) ? _fpsNumerator / _fpsDenominator : DEFAULT_FRAMES_PER_SECOND; + var durationRtpTS = VIDEO_SAMPLING_RATE / fps; OnVideoSourceEncodedSample.Invoke(durationRtpTS, encodedBuffer); } @@ -586,7 +613,7 @@ private async void FrameArrivedHandler(MediaFrameReader sender, MediaFrameArrive if (OnVideoSourceRawSample != null) { - uint frameSpacing = 0; + var frameSpacing = 0u; if (_lastFrameAt != DateTime.MinValue) { frameSpacing = Convert.ToUInt32(DateTime.Now.Subtract(_lastFrameAt).TotalMilliseconds); @@ -687,8 +714,7 @@ private void PrintFrameSourceInfo(MediaFrameSource frameSource) string pixFmt = frameSource.CurrentFormat.Subtype; string deviceName = frameSource.Info.DeviceInformation.Name; - logger.LogInformation("Video capture device {VideoDeviceName} successfully initialised: {Width}x{Height} {Fps:0.##}fps pixel format {PixelFormat}.", - deviceName, width, height, fps, pixFmt); + logger.LogVideoCaptureDeviceFormat(deviceName, width, height, (float)fps, pixFmt); } public void Dispose() diff --git a/src/SIPSorceryMedia.Windows/sys/BufferedWaveProvider.cs b/src/SIPSorceryMedia.Windows/sys/BufferedWaveProvider.cs new file mode 100644 index 0000000000..a7ca5ae152 --- /dev/null +++ b/src/SIPSorceryMedia.Windows/sys/BufferedWaveProvider.cs @@ -0,0 +1,140 @@ +using NAudio.Wave; +using System; + +namespace SIPSorceryMedia.Windows.Sys +{ + /// + /// Provides a buffered store of samples + /// Read method will return queued samples or fill buffer with zeroes + /// Now backed by a circular buffer + /// + internal sealed class BufferedWaveProvider : IWaveProvider + { + private CircularBuffer? circularBuffer; + private readonly WaveFormat waveFormat; + + /// + /// Creates a new buffered WaveProvider + /// + /// WaveFormat + public BufferedWaveProvider(WaveFormat waveFormat) + { + this.waveFormat = waveFormat; + BufferLength = waveFormat.AverageBytesPerSecond * 5; + ReadFully = true; + } + + /// + /// If true, always read the amount of data requested, padding with zeroes if necessary + /// By default is set to true + /// + public bool ReadFully { get; set; } + + /// + /// Buffer length in bytes + /// + public int BufferLength { get; set; } + + /// + /// Buffer duration + /// + public TimeSpan BufferDuration + { + get + { + return TimeSpan.FromSeconds((double)BufferLength / WaveFormat.AverageBytesPerSecond); + } + set + { + BufferLength = (int)(value.TotalSeconds * WaveFormat.AverageBytesPerSecond); + } + } + + /// + /// If true, when the buffer is full, start throwing away data + /// if false, AddSamples will throw an exception when buffer is full + /// + public bool DiscardOnBufferOverflow { get; set; } + + /// + /// The number of buffered bytes + /// + public int BufferedBytes + { + get + { + return circularBuffer is null ? 0 : circularBuffer.Count; + } + } + + /// + /// Buffered Duration + /// + public TimeSpan BufferedDuration + { + get { return TimeSpan.FromSeconds((double)BufferedBytes / WaveFormat.AverageBytesPerSecond); } + } + + /// + /// Gets the WaveFormat + /// + public WaveFormat WaveFormat + { + get { return waveFormat; } + } + + /// + /// Adds samples. Takes a copy of buffer, so that buffer can be reused if necessary + /// + public void AddSamples(ReadOnlySpan buffer) + { + // create buffer here to allow user to customise buffer length + if (circularBuffer is null) + { + circularBuffer = new CircularBuffer(BufferLength); + } + + var written = circularBuffer.Write(buffer); + if (written < buffer.Length && !DiscardOnBufferOverflow) + { + throw new InvalidOperationException("Buffer full"); + } + } + + /// + /// Adds samples. Takes a copy of buffer, so that buffer can be reused if necessary + /// + public void AddSamples(byte[] buffer, int offset, int count) + { + AddSamples(buffer.AsSpan(offset, count)); + } + + /// + /// Reads from this WaveProvider + /// Will always return count bytes, since we will zero-fill the buffer if not enough available + /// + public int Read(byte[] buffer, int offset, int count) + { + var read = 0; + if (circularBuffer is { }) // not yet created + { + read = circularBuffer.Read(buffer, offset, count); + } + if (ReadFully && read < count) + { + // zero the end of the buffer + Array.Clear(buffer, offset + read, count - read); + read = count; + } + return read; + } + + /// + /// Discards all audio from the buffer + /// + public void ClearBuffer() + { + circularBuffer?.Reset(); + } + } +} diff --git a/src/SIPSorceryMedia.Windows/sys/CircularBuffer.cs b/src/SIPSorceryMedia.Windows/sys/CircularBuffer.cs new file mode 100644 index 0000000000..63f4e934f1 --- /dev/null +++ b/src/SIPSorceryMedia.Windows/sys/CircularBuffer.cs @@ -0,0 +1,175 @@ +using System; +using System.Diagnostics; + +namespace SIPSorceryMedia.Windows.Sys +{ + /// + /// A very basic circular buffer implementation + /// + internal sealed class CircularBuffer + { + private readonly byte[] buffer; + private readonly object lockObject; + private int writePosition; + private int readPosition; + private int byteCount; + + /// + /// Create a new circular buffer + /// + /// Max buffer size in bytes + public CircularBuffer(int size) + { + buffer = new byte[size]; + lockObject = new object(); + } + + /// + /// Write data to the buffer + /// + /// Data to write + /// number of bytes written + public int Write(ReadOnlySpan data) + { + lock (lockObject) + { + var count = data.Length; + var bytesWritten = 0; + if (count > buffer.Length - byteCount) + { + count = buffer.Length - byteCount; + } + + if (count == 0) + { + return 0; + } + + // write to end + var writeToEnd = Math.Min(buffer.Length - writePosition, count); + data.Slice(0, writeToEnd).CopyTo(buffer.AsSpan(writePosition)); + writePosition += writeToEnd; + writePosition %= buffer.Length; + bytesWritten += writeToEnd; + + if (bytesWritten < count) + { + Debug.Assert(writePosition == 0); + // must have wrapped round. Write to start + data.Slice(bytesWritten, count - bytesWritten).CopyTo(buffer.AsSpan(writePosition)); + writePosition += (count - bytesWritten); + bytesWritten = count; + } + byteCount += bytesWritten; + return bytesWritten; + } + } + + /// + /// Write data to the buffer + /// + /// Data to write + /// Offset into data + /// Number of bytes to write + /// number of bytes written + public int Write(byte[] data, int offset, int count) + { + return Write(data.AsSpan(offset, count)); + } + + /// + /// Read from the buffer + /// + /// Buffer to read into + /// Offset into read buffer + /// Bytes to read + /// Number of bytes actually read + public int Read(byte[] data, int offset, int count) + { + lock (lockObject) + { + if (count > byteCount) + { + count = byteCount; + } + var bytesRead = 0; + var readToEnd = Math.Min(buffer.Length - readPosition, count); + Array.Copy(buffer, readPosition, data, offset, readToEnd); + bytesRead += readToEnd; + readPosition += readToEnd; + readPosition %= buffer.Length; + + if (bytesRead < count) + { + // must have wrapped round. Read from start + Debug.Assert(readPosition == 0); + Array.Copy(buffer, readPosition, data, offset + bytesRead, count - bytesRead); + readPosition += (count - bytesRead); + bytesRead = count; + } + + byteCount -= bytesRead; + Debug.Assert(byteCount >= 0); + return bytesRead; + } + } + + /// + /// Maximum length of this circular buffer + /// + public int MaxLength => buffer.Length; + + /// + /// Number of bytes currently stored in the circular buffer + /// + public int Count + { + get + { + lock (lockObject) + { + return byteCount; + } + } + } + + /// + /// Resets the buffer + /// + public void Reset() + { + lock (lockObject) + { + ResetInner(); + } + } + + private void ResetInner() + { + byteCount = 0; + readPosition = 0; + writePosition = 0; + } + + /// + /// Advances the buffer, discarding bytes + /// + /// Bytes to advance + public void Advance(int count) + { + lock (lockObject) + { + if (count >= byteCount) + { + ResetInner(); + } + else + { + byteCount -= count; + readPosition += count; + readPosition %= MaxLength; + } + } + } + } +} diff --git a/test/FFmpegFileAndDevicesTest/Program.cs b/test/FFmpegFileAndDevicesTest/Program.cs index 9e87b31a55..fe9f61c419 100644 --- a/test/FFmpegFileAndDevicesTest/Program.cs +++ b/test/FFmpegFileAndDevicesTest/Program.cs @@ -284,12 +284,12 @@ static private Task CreatePeerConnection() return Task.FromResult(PeerConnection); } - private static void AudioSource_OnAudioSourceEncodedSample(uint durationRtpUnits, byte[] sample) + private static void AudioSource_OnAudioSourceEncodedSample(uint durationRtpUnits, ReadOnlyMemory sample) { PeerConnection.SendAudio(durationRtpUnits, sample); if (audioSink != null) - audioSink.GotAudioRtp(null, 0, 0, 0, 0, false, sample); + audioSink.GotAudioRtp(null, 0, 0, 0, 0, false, sample.ToArray()); } /// diff --git a/test/SIPSorceryMedia.Abstractions.UnitTest/PixelConverterTest.cs b/test/SIPSorceryMedia.Abstractions.UnitTest/PixelConverterTest.cs index ea85080283..6d5e1a7008 100644 --- a/test/SIPSorceryMedia.Abstractions.UnitTest/PixelConverterTest.cs +++ b/test/SIPSorceryMedia.Abstractions.UnitTest/PixelConverterTest.cs @@ -109,7 +109,7 @@ public unsafe void ConvertKnownI420ToBGRTest() [Fact] public unsafe void ConvertKnownNV12ToBGRTest() { - logger.LogDebug("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name); + logger.LogDebug("--> " + TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); int width = 640; @@ -153,7 +153,7 @@ public unsafe void WrongSizeI420ToBGRTest() int height = 405; byte[] i420 = new byte[width * height * 3/2]; - Assert.Throws(() => PixelConverter.I420toBGR(i420, width, height, out _)); + Assert.ThrowsAny(() => PixelConverter.I420toBGR(i420, width, height, out _)); } /// diff --git a/test/SIPSorceryMedia.Abstractions.UnitTest/SIPSorceryMedia.Abstractions.UnitTest.csproj b/test/SIPSorceryMedia.Abstractions.UnitTest/SIPSorceryMedia.Abstractions.UnitTest.csproj old mode 100755 new mode 100644 index ef42e26880..880345736f --- a/test/SIPSorceryMedia.Abstractions.UnitTest/SIPSorceryMedia.Abstractions.UnitTest.csproj +++ b/test/SIPSorceryMedia.Abstractions.UnitTest/SIPSorceryMedia.Abstractions.UnitTest.csproj @@ -1,7 +1,7 @@  - net8 + net472;net8.0;net10.0 false true @@ -19,6 +19,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/test/integration/SIPSorcery.IntegrationTests.csproj b/test/integration/SIPSorcery.IntegrationTests.csproj old mode 100755 new mode 100644 diff --git a/test/integration/net/DNS/DNSUnitTest.cs b/test/integration/net/DNS/DNSUnitTest.cs index c047d6fdb9..77b4e24f4b 100644 --- a/test/integration/net/DNS/DNSUnitTest.cs +++ b/test/integration/net/DNS/DNSUnitTest.cs @@ -21,7 +21,7 @@ using Microsoft.Extensions.Logging; using SIPSorcery.SIP; using SIPSorcery.Sys; -using SIPSorcery.UnitTests; +using SIPSorcery.UnitTests; using Xunit; namespace SIPSorcery.Net.IntegrationTests @@ -44,8 +44,8 @@ public DNSUnitTest(Xunit.Abstractions.ITestOutputHelper output) //[Fact] public async Task LookupAnyRecordTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); //DNSResponse result = DNSManager.Lookup("dns.google", QType.ANY, 100, null, false, false); var result = await SIPDns.LookupClient.QueryAsync("dns.google", QueryType.ANY); @@ -76,8 +76,8 @@ public async Task LookupAnyRecordTest() //[Fact] public async Task LookupAnyRecordAsyncCacheTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); //1.queue dns lookup for async resolution //DNSResponse result = DNSManager.Lookup("dns.google", QType.ANY, 1, null, false, true); @@ -113,7 +113,7 @@ public async Task LookupAnyRecordAsyncCacheTest() //[Fact(Skip = "Need to investigate why this fails on Appveyor Windows CI.")] public async Task LookupARecordMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); //DNSResponse result = DNSManager.Lookup("www.sipsorcery.com", QType.A, 10, null, false, false); @@ -136,7 +136,7 @@ public async Task LookupARecordMethod() [Fact] public async Task LookupAAAARecordMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); //DNSResponse result = DNSManager.Lookup("www.google.com", QType.AAAA, 10, null, false, false); @@ -172,8 +172,8 @@ public async Task LookupAAAARecordMethod() [Fact] public async Task LookupSrvRecordMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); //DNSResponse result = DNSManager.Lookup(SIPDNSConstants.SRV_SIP_UDP_QUERY_PREFIX + "sipsorcery.com", QType.SRV, 10, null, false, false); var result = await SIPDns.LookupClient.QueryAsync($"{SIPDNSConstants.SRV_SIP_UDP_QUERY_PREFIX}sipsorcery.com", QueryType.SRV); @@ -193,8 +193,8 @@ public async Task LookupSrvRecordMethod() [Fact] public void LookupCurrentHostNameMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); string localHostname = System.Net.Dns.GetHostName(); diff --git a/test/integration/net/DtlsSrtp/DtlsSrtpTransportUnitTest.cs b/test/integration/net/DtlsSrtp/DtlsSrtpTransportUnitTest.cs index 3e9cc94fa4..4a681df0ba 100644 --- a/test/integration/net/DtlsSrtp/DtlsSrtpTransportUnitTest.cs +++ b/test/integration/net/DtlsSrtp/DtlsSrtpTransportUnitTest.cs @@ -92,10 +92,13 @@ public async Task DoHandshakeUnitTest() dtlsClientTransport.WriteToRecvStream(buf); }; - var serverTask = Task.Run(() => - dtlsServerTransport.DoHandshake(out _)); - var clientTask = Task.Run(() => - dtlsClientTransport.DoHandshake(out _)); + string serverError = null; + string clientError = null; + + var serverTask = Task.Run(() => + dtlsServerTransport.DoHandshake(out serverError)); + var clientTask = Task.Run(() => + dtlsClientTransport.DoHandshake(out clientError)); var timeoutTask = Task.Delay(TimeSpan.FromMilliseconds(timeout)); var winner = await Task.WhenAny(serverTask, clientTask, timeoutTask); @@ -105,8 +108,20 @@ public async Task DoHandshakeUnitTest() Assert.Fail($"Test timed out after {timeout}ms."); } - Assert.True(await serverTask); - Assert.True(await clientTask); + var serverResult = await serverTask; + var clientResult = await clientTask; + + if (!serverResult) + { + logger.LogError("Server handshake failed: {Error}", serverError ?? "Unknown error"); + } + if (!clientResult) + { + logger.LogError("Client handshake failed: {Error}", clientError ?? "Unknown error"); + } + + Assert.True(serverResult, $"Server handshake failed: {serverError}"); + Assert.True(clientResult, $"Client handshake failed: {clientError}"); logger.LogDebug("DTLS client fingerprint : {Fingerprint}", DtlsUtils.Fingerprint(dtlsClient.Certificate)); //logger.LogDebug($"DTLS client server fingerprint: {dtlsClient.ServerFingerprint}."); diff --git a/test/integration/net/ICE/MockTurnServer.cs b/test/integration/net/ICE/MockTurnServer.cs index 629403c48a..fb20013fe2 100644 --- a/test/integration/net/ICE/MockTurnServer.cs +++ b/test/integration/net/ICE/MockTurnServer.cs @@ -62,7 +62,7 @@ public MockTurnServer(IPAddress listenAddress, int port) private void OnPacketReceived(UdpReceiver receiver, int localPort, IPEndPoint remoteEndPoint, byte[] packet) { - STUNMessage stunMessage = STUNMessage.ParseSTUNMessage(packet, packet.Length); + STUNMessage stunMessage = STUNMessage.ParseSTUNMessage(packet); switch (stunMessage.Header.MessageType) { @@ -92,7 +92,9 @@ private void OnPacketReceived(UdpReceiver receiver, int localPort, IPEndPoint re allocateResponse.AddXORMappedAddressAttribute(remoteEndPoint.Address, remoteEndPoint.Port); allocateResponse.AddXORAddressAttribute(STUNAttributeTypesEnum.XORRelayedAddress, _relayEndPoint.Address, _relayEndPoint.Port); - _clientSocket.SendTo(allocateResponse.ToByteBuffer(null, false), remoteEndPoint); + var allocateResponseBuffer = new byte[allocateResponse.GetByteBufferSize(null, false)]; + allocateResponse.WriteToBuffer(allocateResponseBuffer, null, false); + _clientSocket.SendTo(allocateResponseBuffer, remoteEndPoint); break; case STUNMessageTypesEnum.BindingRequest: @@ -102,7 +104,9 @@ private void OnPacketReceived(UdpReceiver receiver, int localPort, IPEndPoint re STUNMessage stunResponse = new STUNMessage(STUNMessageTypesEnum.BindingSuccessResponse); stunResponse.Header.TransactionId = stunMessage.Header.TransactionId; stunResponse.AddXORMappedAddressAttribute(remoteEndPoint.Address, remoteEndPoint.Port); - _clientSocket.SendTo(stunResponse.ToByteBuffer(null, false), remoteEndPoint); + var stunResponseBuffer = new byte[stunResponse.GetByteBufferSize(null, false)]; + stunResponse.WriteToBuffer(stunResponseBuffer, null, false); + _clientSocket.SendTo(stunResponseBuffer, remoteEndPoint); break; case STUNMessageTypesEnum.CreatePermission: @@ -111,7 +115,9 @@ private void OnPacketReceived(UdpReceiver receiver, int localPort, IPEndPoint re STUNMessage permResponse = new STUNMessage(STUNMessageTypesEnum.CreatePermissionSuccessResponse); permResponse.Header.TransactionId = stunMessage.Header.TransactionId; - _clientSocket.SendTo(permResponse.ToByteBuffer(null, false), remoteEndPoint); + var permResponseBuffer = new byte[permResponse.GetByteBufferSize(null, false)]; + permResponse.WriteToBuffer(permResponseBuffer, null, false); + _clientSocket.SendTo(permResponseBuffer, remoteEndPoint); break; case STUNMessageTypesEnum.SendIndication: @@ -122,7 +128,7 @@ private void OnPacketReceived(UdpReceiver receiver, int localPort, IPEndPoint re logger.LogDebug("MockTurnServer relaying {BufferLength} bytes to {DestinationEndPoint}.", buffer.Length, destEP); - _relaySocket.SendTo(buffer, destEP); + _relaySocket.SendTo(buffer.ToArray(), destEP); break; @@ -146,7 +152,9 @@ private void OnRelayPacketReceived(UdpReceiver receiver, int localPort, IPEndPoi dataInd.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Data, packet)); dataInd.AddXORPeerAddressAttribute(remoteEndPoint.Address, remoteEndPoint.Port); - _clientSocket.SendTo(dataInd.ToByteBuffer(null, false), _clientEndPoint); + var dataIndBuffer = new byte[dataInd.GetByteBufferSize(null, false)]; + dataInd.WriteToBuffer(dataIndBuffer, null, false); + _clientSocket.SendTo(dataIndBuffer, _clientEndPoint); } public void Dispose() diff --git a/test/integration/net/ICE/RtpIceChannelIntegrationTest.cs b/test/integration/net/ICE/RtpIceChannelIntegrationTest.cs index 0a3524e74e..f0efcc6851 100644 --- a/test/integration/net/ICE/RtpIceChannelIntegrationTest.cs +++ b/test/integration/net/ICE/RtpIceChannelIntegrationTest.cs @@ -128,10 +128,10 @@ public void SortChecklistUnitTest() logger.LogDebug("{HostCandidate}", hostCandidate.ToString()); } - var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999"); + var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999".AsSpan()); rtpIceChannel.AddRemoteCandidate(remoteCandidate); - var remoteCandidate2 = RTCIceCandidate.Parse("candidate:408132417 1 udp 2113937150 192.168.11.51 51268 typ host generation 0 ufrag CI7o network-cost 999"); + var remoteCandidate2 = RTCIceCandidate.Parse("candidate:408132417 1 udp 2113937150 192.168.11.51 51268 typ host generation 0 ufrag CI7o network-cost 999".AsSpan()); rtpIceChannel.AddRemoteCandidate(remoteCandidate2); foreach (var entry in rtpIceChannel._checklist) @@ -160,10 +160,10 @@ public async Task ChecklistConstructionUnitTest() logger.LogDebug("host candidate: {HostCandidate}", hostCandidate); } - var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999"); + var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999".AsSpan()); rtpIceChannel.AddRemoteCandidate(remoteCandidate); - var remoteCandidate2 = RTCIceCandidate.Parse("candidate:408132417 1 udp 2113937150 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999"); + var remoteCandidate2 = RTCIceCandidate.Parse("candidate:408132417 1 udp 2113937150 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999".AsSpan()); rtpIceChannel.AddRemoteCandidate(remoteCandidate2); await Task.Delay(500); @@ -196,7 +196,7 @@ public async Task ChecklistProcessingUnitTest() logger.LogDebug("host candidate: {HostCandidate}", hostCandidate); } - var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999"); + var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999".AsSpan()); rtpIceChannel.AddRemoteCandidate(remoteCandidate); rtpIceChannel.SetRemoteCredentials("CI7o", "xxxxxxxxxxxx"); @@ -231,7 +231,7 @@ public async Task ChecklistProcessingToFailStateUnitTest() logger.LogDebug("host candidate: {HostCandidate}", hostCandidate); } - var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999"); + var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999".AsSpan()); rtpIceChannel.AddRemoteCandidate(remoteCandidate); rtpIceChannel.SetRemoteCredentials("CI7o", "xxxxxxxxxxxx"); @@ -613,9 +613,9 @@ public void AddMulitpleIceServersTest() foreach(var pair in rtpIceChannel._iceServerResolver.IceServers) { - logger.LogDebug("ICE server {ServerKey}, tx ID {TransactionID}", pair.Key, pair.Value._id); + logger.LogDebug("ICE server {ServerKey}, tx ID {TransactionID}", pair.Key, pair.Value.Id); - Assert.Equal(1, rtpIceChannel._iceServerResolver.IceServers.Values.Count(x => x._id == pair.Value._id)); + Assert.Equal(1, rtpIceChannel._iceServerResolver.IceServers.Values.Count(x => x.Id == pair.Value.Id)); } } } diff --git a/test/integration/net/STUN/STUNDnsUnitTest.cs b/test/integration/net/STUN/STUNDnsUnitTest.cs index 7b71d20078..74868c342d 100644 --- a/test/integration/net/STUN/STUNDnsUnitTest.cs +++ b/test/integration/net/STUN/STUNDnsUnitTest.cs @@ -39,8 +39,8 @@ public STUNDnsUnitTest(Xunit.Abstractions.ITestOutputHelper output) [Fact] public async Task LookupLocalhostTestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); STUNUri.TryParse("localhost", out var stunUri); var result = await STUNDns.Resolve(stunUri); @@ -57,7 +57,7 @@ public async Task LookupLocalhostTestMethod() [Fact] public async Task LookupLocalhostIPv6TestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); STUNUri.TryParse("localhost", out var stunUri); @@ -87,8 +87,8 @@ public async Task LookupLocalhostIPv6TestMethod() [Fact] public async Task LookupPrivateNetworkHostTestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); string localHostname = Dns.GetHostName(); @@ -115,8 +115,8 @@ public async Task LookupPrivateNetworkHostTestMethod() [Fact] public async Task LookupPrivateNetworkHostIPv6TestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); string localHostname = Dns.GetHostName(); @@ -157,8 +157,8 @@ public async Task LookupPrivateNetworkHostIPv6TestMethod() [Fact] public async Task LookupHostWithExplicitPortTestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); STUNUri.TryParse("stun.l.google.com:19302", out var stunUri); var result = await STUNDns.Resolve(stunUri); @@ -174,7 +174,7 @@ public async Task LookupHostWithExplicitPortTestMethod() [Fact] public async Task LookupHostPreferIPv6TestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); STUNUri.TryParse("www.google.com", out var stunUri); @@ -195,7 +195,7 @@ public async Task LookupHostPreferIPv6TestMethod() [Fact] public async Task LookupHostPreferIPv6FallbackTestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); STUNUri.TryParse("www.sipsorcery.com", out var stunUri); @@ -213,8 +213,8 @@ public async Task LookupHostPreferIPv6FallbackTestMethod() [Fact] public async Task LookupWithSRVTestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); STUNUri.TryParse("sipsorcery.com", out var stunUri); var result = await STUNDns.Resolve(stunUri); @@ -230,8 +230,8 @@ public async Task LookupWithSRVTestMethod() [Fact] public async Task LookupWithSRVTestPreferIPv6Method() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); STUNUri.TryParse("sipsorcery.com", out var stunUri); var result = await STUNDns.Resolve(stunUri, true); @@ -250,7 +250,7 @@ public async Task LookupWithSRVTestPreferIPv6Method() [Fact] public async Task LookupNonExistentHostTestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); STUNUri.TryParse("idontexist", out var stunUri); @@ -267,7 +267,7 @@ public async Task LookupNonExistentHostTestMethod() [Fact] public async Task LookupNonExistentCanoncialHostTestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); STUNUri.TryParse("somehost.fsdfergerw.com", out var stunUri); diff --git a/test/integration/net/WebRTC/RTCPeerConnectionUnitTest.cs b/test/integration/net/WebRTC/RTCPeerConnectionUnitTest.cs index 70f1353f15..664655fb4a 100755 --- a/test/integration/net/WebRTC/RTCPeerConnectionUnitTest.cs +++ b/test/integration/net/WebRTC/RTCPeerConnectionUnitTest.cs @@ -12,6 +12,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -78,7 +79,7 @@ public void GenerateLocalOfferWithAudioTrackUnitTest() pc.addTrack(audioTrack); var offer = pc.createOffer(new RTCOfferOptions()); - SDP offerSDP = SDP.ParseSDPDescription(offer.sdp); + SDP offerSDP = SDP.ParseSDPDescription(offer.sdp.AsSpan()); Assert.NotNull(offer); Assert.NotNull(offer.sdp); @@ -197,7 +198,7 @@ public void CheckAudioVideoMediaIdentifierTagsAreReusedForAnswerUnitTest() MediaStreamTrack localVideoTrack = new MediaStreamTrack(SDPMediaTypesEnum.video, false, new List { new SDPAudioVideoMediaFormat(SDPMediaTypesEnum.video, 96, "VP8", 90000) }); pc.addTrack(localVideoTrack); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -255,7 +256,7 @@ public void CheckDataChannelMediaIdentifierTagsAreReusedForAnswerUnitTest() RTCPeerConnection pc = new RTCPeerConnection(null); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -372,7 +373,7 @@ public void CheckDataChannelVideoAndAudioAreWellManagedInAnswerUnitTest() RTCPeerConnection pc = new RTCPeerConnection(null); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -517,7 +518,7 @@ public void CheckMediaIdentifierTagOrderRemainsForAnswerUnitTest() MediaStreamTrack localVideoTrack = new MediaStreamTrack(SDPMediaTypesEnum.video, false, new List { new SDPAudioVideoMediaFormat(SDPMediaTypesEnum.video, 96, "VP8", 90000) }); pc.addTrack(localVideoTrack); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -641,7 +642,7 @@ public void CheckMediaFormatNegotiationUnitTest() MediaStreamTrack localVideoTrack = new MediaStreamTrack(SDPMediaTypesEnum.video, false, new List { new SDPAudioVideoMediaFormat(SDPMediaTypesEnum.video, 96, "VP8", 90000) }); pc.addTrack(localVideoTrack); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -717,7 +718,7 @@ public void CheckNoAudioNegotiationUnitTest() MediaStreamTrack localVideoTrack = new MediaStreamTrack(SDPMediaTypesEnum.video, false, new List { new SDPAudioVideoMediaFormat(SDPMediaTypesEnum.video, 96, "VP8", 90000) }); pc.addTrack(localVideoTrack); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -820,7 +821,7 @@ public void Check_Inactive_Audio_Negotiation_Test() MediaStreamTrack localVideoTrack = new MediaStreamTrack(SDPMediaTypesEnum.video, false, new List { new SDPAudioVideoMediaFormat(SDPMediaTypesEnum.video, 96, "VP8", 90000) }); pc.addTrack(localVideoTrack); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -998,7 +999,7 @@ public void CheckAnswerForGStreamerOfferUnitTest() MediaStreamTrack localVideoTrack = new MediaStreamTrack(SDPMediaTypesEnum.video, false, new List { new SDPAudioVideoMediaFormat(SDPMediaTypesEnum.video, 100, "H264", 90000) }); pc.addTrack(localVideoTrack); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -1145,7 +1146,7 @@ public void AnswerShouldNotContainAbsSendTimeIfOfferDidNot() MediaStreamTrack localVideoTrack = new MediaStreamTrack(SDPMediaTypesEnum.video, false, new List { new SDPAudioVideoMediaFormat(SDPMediaTypesEnum.video, 96, "VP8", 90000) }, headerExtensions: videoExtensions); pc.addTrack(localVideoTrack); - var offer = SDP.ParseSDPDescription(offerSdp); + var offer = SDP.ParseSDPDescription(offerSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); diff --git a/test/unit/App.config b/test/unit/App.config new file mode 100644 index 0000000000..a466fac807 --- /dev/null +++ b/test/unit/App.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/unit/CryptoHelper.cs b/test/unit/CryptoHelper.cs new file mode 100644 index 0000000000..d716dc0cdb --- /dev/null +++ b/test/unit/CryptoHelper.cs @@ -0,0 +1,21 @@ +using System.Security.Cryptography; +using SIPSorcery.Sys; + +namespace SIPSorcery.UnitTests; + +// TODO: When .NET Standard and Framework support are deprecated these pragmas can be removed. +#pragma warning disable SYSLIB0021 +internal class CryptoHelper +{ + /// + /// Gets the HSA256 hash of an arbitrary buffer. + /// + /// The buffer to hash. + /// A hex string representing the hashed buffer. + public static string GetSHA256Hash(byte[] buffer) + { + using var sha256 = new SHA256Managed(); + return sha256.ComputeHash(buffer).HexStr(); + } +} +#pragma warning restore SYSLIB0021 diff --git a/test/unit/Initialise.cs b/test/unit/Initialise.cs index 570264ef6d..9d3b3819cd 100644 --- a/test/unit/Initialise.cs +++ b/test/unit/Initialise.cs @@ -124,7 +124,7 @@ public class MockSIPUriResolver { public static Task ResolveSIPUri(SIPURI uri, bool preferIPv6) { - if (IPSocket.TryParseIPEndPoint(uri.Host, out var ipEndPoint)) + if (IPSocket.TryParseIPEndPoint(uri.Host.AsSpan(), out var ipEndPoint)) { return Task.FromResult(new SIPEndPoint(uri.Protocol, ipEndPoint)); } diff --git a/test/unit/SIPSorcery.UnitTests.csproj b/test/unit/SIPSorcery.UnitTests.csproj old mode 100755 new mode 100644 diff --git a/test/unit/app/media/VideoTestPatternSourceUnitTest.cs b/test/unit/app/media/VideoTestPatternSourceUnitTest.cs index db5b09374a..e22682ae9b 100644 --- a/test/unit/app/media/VideoTestPatternSourceUnitTest.cs +++ b/test/unit/app/media/VideoTestPatternSourceUnitTest.cs @@ -16,7 +16,7 @@ using Microsoft.Extensions.Logging; using Xunit; using SIPSorcery.Media; -using SIPSorcery.UnitTests; +using SIPSorcery.UnitTests; namespace SIPSorcery.SIP.App.UnitTests { @@ -37,7 +37,7 @@ public VideoTestPatternSourceUnitTest(Xunit.Abstractions.ITestOutputHelper outpu [Fact] public void CanInstantiateUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); VideoTestPatternSource testPatternSource = new VideoTestPatternSource(); diff --git a/test/unit/core/SIP/Channels/SIPUDPChannelUnitTest.cs b/test/unit/core/SIP/Channels/SIPUDPChannelUnitTest.cs index d12d7d244f..c201c28c40 100644 --- a/test/unit/core/SIP/Channels/SIPUDPChannelUnitTest.cs +++ b/test/unit/core/SIP/Channels/SIPUDPChannelUnitTest.cs @@ -36,8 +36,8 @@ public SIPUDPChannelUnitTest(Xunit.Abstractions.ITestOutputHelper output) [Fact] public void CreateChannelUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var udpChan = new SIPUDPChannel(IPAddress.Any, 0); @@ -54,8 +54,8 @@ public void CreateChannelUnitTest() [Fact] public async Task InterChannelCommsUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var udpChan1 = new SIPUDPChannel(IPAddress.Any, 0); logger.LogDebug("Listening end point {ListeningSIPEndPoint}.", udpChan1.ListeningSIPEndPoint); @@ -116,8 +116,8 @@ public async Task InterChannelCommsUnitTest() [Fact] public void GetDefaultContactURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var udpChan = new SIPUDPChannel(IPAddress.Any, 0); diff --git a/test/unit/core/SIP/SIPAuthorisationDigestUnitTest.cs b/test/unit/core/SIP/SIPAuthorisationDigestUnitTest.cs index c60fc12615..ffe8c143c8 100644 --- a/test/unit/core/SIP/SIPAuthorisationDigestUnitTest.cs +++ b/test/unit/core/SIP/SIPAuthorisationDigestUnitTest.cs @@ -1,26 +1,26 @@ -//----------------------------------------------------------------------------- -// Author(s): -// Aaron Clauson -// -// History: -// -// -// License: -// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// Author(s): +// Aaron Clauson +// +// History: +// +// +// License: +// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. +//----------------------------------------------------------------------------- using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; using Xunit; using SIPSorcery.SIP.App; -using SIPSorcery.UnitTests; - -namespace SIPSorcery.SIP.UnitTests -{ - [Trait("Category", "unit")] - public class SIPAuthorisationDigestUnitTest - { +using SIPSorcery.UnitTests; + +namespace SIPSorcery.SIP.UnitTests +{ + [Trait("Category", "unit")] + public class SIPAuthorisationDigestUnitTest + { private Microsoft.Extensions.Logging.ILogger logger = null; private class MockSIPAccount : ISIPAccount @@ -38,262 +38,262 @@ public MockSIPAccount(string username, string password) SIPUsername = username; SIPPassword = password; } - } - - public SIPAuthorisationDigestUnitTest(Xunit.Abstractions.ITestOutputHelper output) - { - logger = SIPSorcery.UnitTests.TestLogHelper.InitTestLogger(output); - } - - [Fact] - public void KnownDigestTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "asterisk", "aaronxten2", "password", "sip:303@bluesipd", "17190028", "INVITE"); - - string digest = authRequest.GetDigest(); - - logger.LogDebug("Digest = {digest}.", digest); + } + + public SIPAuthorisationDigestUnitTest(Xunit.Abstractions.ITestOutputHelper output) + { + logger = SIPSorcery.UnitTests.TestLogHelper.InitTestLogger(output); + } + + [Fact] + public void KnownDigestTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "asterisk", "aaronxten2", "password", "sip:303@bluesipd", "17190028", "INVITE"); + + string digest = authRequest.GetDigest(); + + logger.LogDebug("Digest = {digest}.", digest); logger.LogDebug("{AuthRequest}", authRequest.ToString()); - + Assert.Equal("06b931d79a06b4e9426b7efbbd6c8da2", digest); - Assert.Equal(DigestAlgorithmsEnum.MD5, authRequest.DigestAlgorithm); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void KnownDigestTestObscureChars() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "sip.blueface.ie", "aaronnetgear", "!\"$%^&*()_-+=}[{]~#@':;?><,.", "sip:sip.blueface.ie:5060", "1430352056", "REGISTER"); - - string digest = authRequest.GetDigest(); - - logger.LogDebug("Digest = {digest}.", digest); - logger.LogDebug("{AuthRequest}", authRequest.ToString()); - - Assert.True(digest == "500fd998b609a0f24b45edfe190f2a17", "The digest was incorrect."); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void KnownDigestTestObscureChars2() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "sip.blueface.ie", "aaronxten", "_*!$%^()\"", "sip:sip.blueface.ie", "1263192143", "REGISTER"); - - string digest = authRequest.GetDigest(); - - logger.LogDebug("Digest = {digest}.", digest); - logger.LogDebug("{AuthRequest}", authRequest.ToString()); - - Assert.True(digest == "54b08b70ed1976068b9e18d38ea59849", "The digest was incorrect."); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void KnownDigestTest2() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "asterisk", "aaronxten2", "password", "sip:303@213.168.225.133", "4a4ad124", "INVITE"); - - string digest = authRequest.GetDigest(); - - logger.LogDebug("Digest = {digest}.", digest); - logger.LogDebug("{AuthRequest}", authRequest.ToString()); - - Assert.True(true, "True was false."); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void KnownRegisterDigestTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "asterisk", "aaron", "password", "sip:blueface", "1c8192c9", "REGISTER"); - - string digest = authRequest.GetDigest(); - - logger.LogDebug("Digest = {digest}.", digest); - logger.LogDebug("{AuthRequest}", authRequest.ToString()); - - Assert.True("08881d1d56c0b21f11d19f4067da7045" == digest, "Digest was incorrect."); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void KnownRegisterDigestTest2() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "asterisk", "aaron", "password", "sip:blueface", "1c3c7a97", "REGISTER"); - - string digest = authRequest.GetDigest(); - - logger.LogDebug("Digest = {digest}.", digest); - logger.LogDebug("{AuthRequest}", authRequest.ToString()); - - Assert.True("1ef20beed71043225873e4f6712e4922" == digest, "Digest was incorrect."); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void ParseWWWAuthenticateDigestTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"Digest realm=""aol.com"",nonce=""48e7541d4339e27ee7b520a4bf8a8e3c4fffcb90"",qop=""auth"",opaque=""004533235332435434ffac663e"",algorithm=MD5"); - - Assert.True(authRequest.Realm == "aol.com", "The authorisation realm was not parsed correctly."); - Assert.True(authRequest.Nonce == "48e7541d4339e27ee7b520a4bf8a8e3c4fffcb90", "The authorisation nonce was not parsed correctly."); - Assert.True(authRequest.Qop == "auth", "The authorisation qop was not parsed correctly."); - Assert.True(authRequest.Opaque == "004533235332435434ffac663e", "The authorisation opaque was not parsed correctly."); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void KnownWWWAuthenticateDigestTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"Digest realm=""aol.com"",nonce=""48e757f3b95250379d63fe29f777984a93831b80"",qop=""auth"",opaque=""004533235332435434ffac663e"",algorithm=MD5"); - authRequest.SetCredentials("user@aim.com", "password", "sip:01135312222222@sip.aol.com;transport=udp", "INVITE"); - authRequest.Cnonce = "e66ea40d700e8ab69509df4893f4a821"; - - string digest = authRequest.GetDigest(); - authRequest.Response = digest; - - logger.LogDebug("Digest = {digest}.", digest); - logger.LogDebug("{AuthRequest}", authRequest.ToString()); - - Assert.True("6221ea0348e2d5229dd1f3825d633295" == digest, "Digest was incorrect."); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void AuthenticateHeaderToStringTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"Digest realm=""aol.com"",nonce=""48e7541d4339e27ee7b520a4bf8a8e3c4fffcb90"",qop=""auth"",opaque=""004533235332435434ffac663e"",algorithm=MD5"); - authRequest.SetCredentials("user@aim.com", "password", "sip:01135312222222@sip.aol.com;transport=udp", "INVITE"); - authRequest.Cnonce = "cf2e005f1801550717cc8c59193aa9f4"; - - string digest = authRequest.GetDigest(); - authRequest.Response = digest; - - logger.LogDebug("Digest = {digest}.", digest); - logger.LogDebug("{AuthRequest}", authRequest.ToString()); - - Assert.True(authRequest.ToString() == @"Digest username=""user@aim.com"",realm=""aol.com"",nonce=""48e7541d4339e27ee7b520a4bf8a8e3c4fffcb90"",uri=""sip:01135312222222@sip.aol.com;transport=udp"",response=""18ad0e62fcc9d7f141a72078c4a0784f"",cnonce=""cf2e005f1801550717cc8c59193aa9f4"",nc=00000001,qop=auth,opaque=""004533235332435434ffac663e"",algorithm=MD5", "The authorisation header was not put to a string correctly."); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void KnownQOPUnitTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, "Digest realm=\"jnctn.net\", nonce=\"4a597e1c0000a1636739088e9151ef2f319af257c8f585f1\", qop=\"auth\""); - authRequest.SetCredentials("user", "password", "sip:user.onsip.com", "REGISTER"); - authRequest.Cnonce = "d3a1ca6af34e72e2461b794f48d5045d"; - - string digest = authRequest.GetDigest(); - authRequest.Response = digest; - - logger.LogDebug("Digest = {digest}.", digest); - logger.LogDebug("{AuthRequest}", authRequest.ToString()); - - Assert.True(authRequest.Response == "7709215c1d58c1912dc59d1e8b5b6248", "The authentication response digest was not generated properly."); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void KnownOpaqueTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"digest realm=""Syndeo Corporation"", nonce=""1265068315059e3bbf3052cf13ea5ca22fb71669a7"", opaque=""09c0f23f71f89ce53baab5664c09cbfa"", algorithm=MD5"); - authRequest.SetCredentials("user", "pass", "sip:sip.ribbit.com", "REGISTER"); - - string digest = authRequest.GetDigest(); - - logger.LogDebug("Digest = {digest}.", digest); - logger.LogDebug("{AuthRequest}", authRequest.ToString()); - - Assert.True(true, "True was false."); - - logger.LogDebug("-----------------------------------------"); - } - - [Fact] - public void GenerateDigestTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"digest realm=""sipsorcery.com"", nonce=""1265068315059e3bbf3052cf13ea5ca22fb71669a7"", opaque=""09c0f23f71f89ce53baab5664c09cbfa"", algorithm=MD5"); - authRequest.SetCredentials("username", "password", "sip:sipsorcery.com", "REGISTER"); - - string digest = authRequest.GetDigest(); - - logger.LogDebug("Digest = {digest}.", digest); - logger.LogDebug("{AuthRequest}", authRequest.ToString()); - - Assert.Equal("b1ea9d6b32e8dd0023a3feec14b16177", digest); - - logger.LogDebug("-----------------------------------------"); + Assert.Equal(DigestAlgorithmsEnum.MD5, authRequest.DigestAlgorithm); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void KnownDigestTestObscureChars() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "sip.blueface.ie", "aaronnetgear", "!\"$%^&*()_-+=}[{]~#@':;?><,.", "sip:sip.blueface.ie:5060", "1430352056", "REGISTER"); + + string digest = authRequest.GetDigest(); + + logger.LogDebug("Digest = {digest}.", digest); + logger.LogDebug("{AuthRequest}", authRequest.ToString()); + + Assert.True(digest == "500fd998b609a0f24b45edfe190f2a17", "The digest was incorrect."); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void KnownDigestTestObscureChars2() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "sip.blueface.ie", "aaronxten", "_*!$%^()\"", "sip:sip.blueface.ie", "1263192143", "REGISTER"); + + string digest = authRequest.GetDigest(); + + logger.LogDebug("Digest = {digest}.", digest); + logger.LogDebug("{AuthRequest}", authRequest.ToString()); + + Assert.True(digest == "54b08b70ed1976068b9e18d38ea59849", "The digest was incorrect."); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void KnownDigestTest2() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "asterisk", "aaronxten2", "password", "sip:303@213.168.225.133", "4a4ad124", "INVITE"); + + string digest = authRequest.GetDigest(); + + logger.LogDebug("Digest = {digest}.", digest); + logger.LogDebug("{AuthRequest}", authRequest.ToString()); + + Assert.True(true, "True was false."); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void KnownRegisterDigestTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "asterisk", "aaron", "password", "sip:blueface", "1c8192c9", "REGISTER"); + + string digest = authRequest.GetDigest(); + + logger.LogDebug("Digest = {digest}.", digest); + logger.LogDebug("{AuthRequest}", authRequest.ToString()); + + Assert.True("08881d1d56c0b21f11d19f4067da7045" == digest, "Digest was incorrect."); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void KnownRegisterDigestTest2() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "asterisk", "aaron", "password", "sip:blueface", "1c3c7a97", "REGISTER"); + + string digest = authRequest.GetDigest(); + + logger.LogDebug("Digest = {digest}.", digest); + logger.LogDebug("{AuthRequest}", authRequest.ToString()); + + Assert.True("1ef20beed71043225873e4f6712e4922" == digest, "Digest was incorrect."); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void ParseWWWAuthenticateDigestTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"Digest realm=""aol.com"",nonce=""48e7541d4339e27ee7b520a4bf8a8e3c4fffcb90"",qop=""auth"",opaque=""004533235332435434ffac663e"",algorithm=MD5"); + + Assert.True(authRequest.Realm == "aol.com", "The authorisation realm was not parsed correctly."); + Assert.True(authRequest.Nonce == "48e7541d4339e27ee7b520a4bf8a8e3c4fffcb90", "The authorisation nonce was not parsed correctly."); + Assert.True(authRequest.Qop == "auth", "The authorisation qop was not parsed correctly."); + Assert.True(authRequest.Opaque == "004533235332435434ffac663e", "The authorisation opaque was not parsed correctly."); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void KnownWWWAuthenticateDigestTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"Digest realm=""aol.com"",nonce=""48e757f3b95250379d63fe29f777984a93831b80"",qop=""auth"",opaque=""004533235332435434ffac663e"",algorithm=MD5"); + authRequest.SetCredentials("user@aim.com", "password", "sip:01135312222222@sip.aol.com;transport=udp", "INVITE"); + authRequest.Cnonce = "e66ea40d700e8ab69509df4893f4a821"; + + string digest = authRequest.GetDigest(); + authRequest.Response = digest; + + logger.LogDebug("Digest = {digest}.", digest); + logger.LogDebug("{AuthRequest}", authRequest.ToString()); + + Assert.True("6221ea0348e2d5229dd1f3825d633295" == digest, "Digest was incorrect."); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void AuthenticateHeaderToStringTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"Digest realm=""aol.com"",nonce=""48e7541d4339e27ee7b520a4bf8a8e3c4fffcb90"",qop=""auth"",opaque=""004533235332435434ffac663e"",algorithm=MD5"); + authRequest.SetCredentials("user@aim.com", "password", "sip:01135312222222@sip.aol.com;transport=udp", "INVITE"); + authRequest.Cnonce = "cf2e005f1801550717cc8c59193aa9f4"; + + string digest = authRequest.GetDigest(); + authRequest.Response = digest; + + logger.LogDebug("Digest = {digest}.", digest); + logger.LogDebug("{AuthRequest}", authRequest.ToString()); + + Assert.True(authRequest.ToString() == @"Digest username=""user@aim.com"",realm=""aol.com"",nonce=""48e7541d4339e27ee7b520a4bf8a8e3c4fffcb90"",uri=""sip:01135312222222@sip.aol.com;transport=udp"",response=""18ad0e62fcc9d7f141a72078c4a0784f"",cnonce=""cf2e005f1801550717cc8c59193aa9f4"",nc=00000001,qop=auth,opaque=""004533235332435434ffac663e"",algorithm=MD5", "The authorisation header was not put to a string correctly."); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void KnownQOPUnitTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, "Digest realm=\"jnctn.net\", nonce=\"4a597e1c0000a1636739088e9151ef2f319af257c8f585f1\", qop=\"auth\""); + authRequest.SetCredentials("user", "password", "sip:user.onsip.com", "REGISTER"); + authRequest.Cnonce = "d3a1ca6af34e72e2461b794f48d5045d"; + + string digest = authRequest.GetDigest(); + authRequest.Response = digest; + + logger.LogDebug("Digest = {digest}.", digest); + logger.LogDebug("{AuthRequest}", authRequest.ToString()); + + Assert.True(authRequest.Response == "7709215c1d58c1912dc59d1e8b5b6248", "The authentication response digest was not generated properly."); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void KnownOpaqueTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"digest realm=""Syndeo Corporation"", nonce=""1265068315059e3bbf3052cf13ea5ca22fb71669a7"", opaque=""09c0f23f71f89ce53baab5664c09cbfa"", algorithm=MD5"); + authRequest.SetCredentials("user", "pass", "sip:sip.ribbit.com", "REGISTER"); + + string digest = authRequest.GetDigest(); + + logger.LogDebug("Digest = {digest}.", digest); + logger.LogDebug("{AuthRequest}", authRequest.ToString()); + + Assert.True(true, "True was false."); + + logger.LogDebug("-----------------------------------------"); } - [Fact] - public void KnownHA1Digest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - var digest = HTTPDigest.DigestCalcHA1("user", "sipsorcery.cloud", "password"); - - logger.LogDebug("Digest = {digest}.", digest); - - Assert.Equal("f5732e14bef238badb2b4cb987d415f6", digest); - - logger.LogDebug("-----------------------------------------"); + [Fact] + public void GenerateDigestTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + SIPAuthorisationDigest authRequest = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"digest realm=""sipsorcery.com"", nonce=""1265068315059e3bbf3052cf13ea5ca22fb71669a7"", opaque=""09c0f23f71f89ce53baab5664c09cbfa"", algorithm=MD5"); + authRequest.SetCredentials("username", "password", "sip:sipsorcery.com", "REGISTER"); + + string digest = authRequest.GetDigest(); + + logger.LogDebug("Digest = {digest}.", digest); + logger.LogDebug("{AuthRequest}", authRequest.ToString()); + + Assert.Equal("b1ea9d6b32e8dd0023a3feec14b16177", digest); + + logger.LogDebug("-----------------------------------------"); + } + + [Fact] + public void KnownHA1Digest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + var digest = HTTPDigest.DigestCalcHA1("user", "sipsorcery.cloud", "password"); + + logger.LogDebug("Digest = {digest}.", digest); + + Assert.Equal("f5732e14bef238badb2b4cb987d415f6", digest); + + logger.LogDebug("-----------------------------------------"); } /// /// Tests that a known digest for MD5 and SHA256 are correctly generated. /// - [Fact] - public void KnownMD5AndSHA256DigestTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + [Fact] + public void KnownMD5AndSHA256DigestTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); { SIPAuthorisationDigest authDigest = new SIPAuthorisationDigest(SIPAuthorisationHeadersEnum.ProxyAuthorization, "asterisk", "aaronxten2", "password", @@ -320,19 +320,19 @@ public void KnownMD5AndSHA256DigestTest() Assert.Equal("06b931d79a06b4e9426b7efbbd6c8da2", digest); Assert.Equal(DigestAlgorithmsEnum.MD5, authDigest.DigestAlgorithm); } - - logger.LogDebug("-----------------------------------------"); + + logger.LogDebug("-----------------------------------------"); } /// /// Tests that the MD5 test vector from RFC7616 https://datatracker.ietf.org/doc/html/rfc7616#section-3.9.1 /// is correctly generated. /// - [Fact] - public void HttpDigestMD5TestVector() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + [Fact] + public void HttpDigestMD5TestVector() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var md5DigestReq = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"Digest @@ -351,19 +351,19 @@ public void HttpDigestMD5TestVector() logger.LogDebug("Auth Header={md5DigestReq}.", md5DigestReq); Assert.Equal("8ca523f5e9506fed4657c9700eebdbec", md5Digest); - - logger.LogDebug("-----------------------------------------"); + + logger.LogDebug("-----------------------------------------"); } /// /// Tests that the SHA256 test vector from RFC7616 https://datatracker.ietf.org/doc/html/rfc7616#section-3.9.1 /// is correctly generated. /// - [Fact] - public void HttpDigestSHA256TestVector() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + [Fact] + public void HttpDigestSHA256TestVector() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var digestReq = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.WWWAuthenticate, @"Digest @@ -382,8 +382,8 @@ public void HttpDigestSHA256TestVector() logger.LogDebug("Auth Header={digestReq}.", digestReq); Assert.Equal("753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1", digest); - - logger.LogDebug("-----------------------------------------"); + + logger.LogDebug("-----------------------------------------"); } /// @@ -509,6 +509,6 @@ public void ParseDigestSHA256AlgorithmTest() Assert.Equal(DigestAlgorithmsEnum.SHA256, authRequest.DigestAlgorithm); Assert.Equal("example.com", authRequest.Realm); Assert.Equal("abc123", authRequest.Nonce); - } - } -} + } + } +} diff --git a/test/unit/core/SIP/SIPRequestUnitTest.cs b/test/unit/core/SIP/SIPRequestUnitTest.cs index c2d3203a06..d4dfda964d 100644 --- a/test/unit/core/SIP/SIPRequestUnitTest.cs +++ b/test/unit/core/SIP/SIPRequestUnitTest.cs @@ -751,7 +751,7 @@ public void BinarySerialisationRoundTripTest() string bodyHash = null; using(var sha256 = new SHA256Managed()) { - bodyHash = sha256.ComputeHash(req.BodyBuffer).HexStr(); + bodyHash = TypeExtensions.HexStr(sha256.ComputeHash(req.BodyBuffer)); } logger.LogDebug("{Req}", req.ToString()); @@ -763,7 +763,7 @@ public void BinarySerialisationRoundTripTest() string rndTripBodyHash = null; using (var sha256 = new SHA256Managed()) { - rndTripBodyHash = sha256.ComputeHash(rndTripReq.BodyBuffer).HexStr(); + rndTripBodyHash = TypeExtensions.HexStr(sha256.ComputeHash(rndTripReq.BodyBuffer)); } logger.LogDebug("{RndTripRequest}", rndTripReq.ToString()); diff --git a/test/unit/core/SIP/SIPResponseUnitTest.cs b/test/unit/core/SIP/SIPResponseUnitTest.cs index f2f74fd141..3ec1b1d3d7 100755 --- a/test/unit/core/SIP/SIPResponseUnitTest.cs +++ b/test/unit/core/SIP/SIPResponseUnitTest.cs @@ -254,7 +254,7 @@ public void BinarySerialisationRoundTripTest() string bodyHash = null; using (var sha256 = new SHA256Managed()) { - bodyHash = sha256.ComputeHash(resp.BodyBuffer).HexStr(); + bodyHash = TypeExtensions.HexStr(sha256.ComputeHash(resp.BodyBuffer)); } logger.LogDebug("{Resp}", resp.ToString()); @@ -266,7 +266,7 @@ public void BinarySerialisationRoundTripTest() string rndTripBodyHash = null; using (var sha256 = new SHA256Managed()) { - rndTripBodyHash = sha256.ComputeHash(rndTripResp.BodyBuffer).HexStr(); + rndTripBodyHash = TypeExtensions.HexStr(sha256.ComputeHash(rndTripResp.BodyBuffer)); } logger.LogDebug("{RndTripResp}", rndTripResp.ToString()); diff --git a/test/unit/core/SIP/SIPURIUnitTest.cs b/test/unit/core/SIP/SIPURIUnitTest.cs old mode 100755 new mode 100644 index 13679f9504..96e798b713 --- a/test/unit/core/SIP/SIPURIUnitTest.cs +++ b/test/unit/core/SIP/SIPURIUnitTest.cs @@ -12,7 +12,7 @@ using System.Net; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -using SIPSorcery.UnitTests; +using SIPSorcery.UnitTests; using Xunit; namespace SIPSorcery.SIP.UnitTests @@ -30,16 +30,16 @@ public SIPURIUnitTest(Xunit.Abstractions.ITestOutputHelper output) [Fact] public void SampleTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); Assert.True(true, "True was false."); } [Fact] public void ParseHostOnlyURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:sip.domain.com"); @@ -52,8 +52,8 @@ public void ParseHostOnlyURIUnitTest() [Fact] public void ParseHostAndUserURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:user@sip.domain.com"); @@ -66,8 +66,8 @@ public void ParseHostAndUserURIUnitTest() [Fact] public void ParseWithParamURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:user@sip.domain.com;param=1234"); @@ -100,8 +100,8 @@ public void ParseWithParamAndPortURIUnitTest() [Fact] public void ParseWithHeaderURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:user@sip.domain.com?header=1234"); @@ -115,8 +115,8 @@ public void ParseWithHeaderURIUnitTest() [Fact] public void SpaceInHostNameURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:Blue Face"); @@ -129,8 +129,8 @@ public void SpaceInHostNameURIUnitTest() [Fact] public void ContactAsteriskURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("*"); @@ -143,8 +143,8 @@ public void ContactAsteriskURIUnitTest() [Fact] public void AreEqualNoParamsURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = SIPURI.ParseSIPURI("sip:abcd@adcb.com"); SIPURI sipURI2 = SIPURI.ParseSIPURI("sip:abcd@adcb.com"); @@ -157,8 +157,8 @@ public void AreEqualNoParamsURIUnitTest() [Fact] public void AreEqualIPAddressNoParamsURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = SIPURI.ParseSIPURI("sip:abcd@192.168.1.101"); SIPURI sipURI2 = SIPURI.ParseSIPURI("sip:abcd@192.168.1.101"); @@ -171,8 +171,8 @@ public void AreEqualIPAddressNoParamsURIUnitTest() [Fact] public void AreEqualWithParamsURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key1=value1;key2=value2"); SIPURI sipURI2 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key2=value2;key1=value1"); @@ -186,8 +186,8 @@ public void AreEqualWithParamsURIUnitTest() [Fact] public void NotEqualWithParamsURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key1=value1;key2=value2"); SIPURI sipURI2 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key2=value2;key1=value2"); @@ -200,8 +200,8 @@ public void NotEqualWithParamsURIUnitTest() [Fact] public void AreEqualWithHeadersURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key1=value1;key2=value2?header1=value1&header2=value2"); SIPURI sipURI2 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key2=value2;key1=value1?header2=value2&header1=value1"); @@ -214,8 +214,8 @@ public void AreEqualWithHeadersURIUnitTest() [Fact] public void NotEqualWithHeadersURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key1=value1;key2=value2?header1=value2&header2=value2"); SIPURI sipURI2 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key2=value2;key1=value1?header2=value2&header1=value1"); @@ -231,8 +231,8 @@ public void NotEqualWithHeadersURIUnitTest() [Fact] public void UriWithParameterEqualityURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key1=value1"); SIPURI sipURI2 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key1=value1"); @@ -245,8 +245,8 @@ public void UriWithParameterEqualityURIUnitTest() [Fact] public void UriWithDifferentParamsEqualURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key1=value1"); SIPURI sipURI2 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key1=value2"); @@ -259,8 +259,8 @@ public void UriWithDifferentParamsEqualURIUnitTest() [Fact] public void UriWithSameParamsInDifferentOrderURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key2=value2;key1=value1"); SIPURI sipURI2 = SIPURI.ParseSIPURI("sip:abcd@adcb.com;key1=value1;key2=value2"); @@ -273,8 +273,8 @@ public void UriWithSameParamsInDifferentOrderURIUnitTest() [Fact] public void AreEqualNullURIsUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = null; SIPURI sipURI2 = null; @@ -287,8 +287,8 @@ public void AreEqualNullURIsUnitTest() [Fact] public void NotEqualOneNullURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = SIPURI.ParseSIPURI("sip:abcd@adcb.com"); SIPURI sipURI2 = null; @@ -301,8 +301,8 @@ public void NotEqualOneNullURIUnitTest() [Fact] public void AreEqualNullEqualsOverloadUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = null; @@ -314,8 +314,8 @@ public void AreEqualNullEqualsOverloadUnitTest() [Fact] public void AreEqualNullNotEqualsOverloadUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI1 = null; @@ -327,8 +327,8 @@ public void AreEqualNullNotEqualsOverloadUnitTest() [Fact] public void UnknownSchemeUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); Assert.Throws(() => SIPURI.ParseSIPURI("mailto:1234565")); @@ -338,8 +338,8 @@ public void UnknownSchemeUnitTest() [Fact] public void KnownSchemesUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); foreach (var value in System.Enum.GetValues(typeof(SIPSchemesEnum))) { @@ -352,8 +352,8 @@ public void KnownSchemesUnitTest() [Fact] public void ParamsInUserPortionURIWithUserPhoneTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:C=on;t=DLPAN@10.0.0.1:5060;lr;user=phone"); @@ -369,8 +369,8 @@ public void ParamsInUserPortionURIWithUserPhoneTest() [Fact] public void OneParamInUserPortionURIWithUserPhoneTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:C=on@10.0.0.1:5060;lr;user=phone"); @@ -385,8 +385,8 @@ public void OneParamInUserPortionURIWithUserPhoneTest() [Fact] public void ParamsInUserPortionURIPhoneNumWithParamsTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:+41999999999;cpc=ordinary@10.0.0.1:5060;transport=udp;user=phone"); @@ -401,8 +401,8 @@ public void ParamsInUserPortionURIPhoneNumWithParamsTest() [Fact] public void ParamsInUserPortionURITest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:C=on;t=DLPAN@10.0.0.1:5060;lr"); @@ -415,8 +415,8 @@ public void ParamsInUserPortionURITest() [Fact] public void SwitchTagParameterUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:joebloggs@sip.mysipswitch.com;switchtag=119651"); @@ -430,8 +430,8 @@ public void SwitchTagParameterUnitTest() [Fact] public void LongUserUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:EhZgKgLM9CwGqYDAECqDpL5MNrM_sKN5NurN5q_pssAk4oxhjKEMT4@10.0.0.1:5060"); @@ -444,8 +444,8 @@ public void LongUserUnitTest() [Fact] public void ParsePartialURINoSchemeUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURIRelaxed("sip.domain.com"); @@ -460,8 +460,8 @@ public void ParsePartialURINoSchemeUnitTest() [Fact] public void ParsePartialURISIPSSchemeUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURIRelaxed("sips:sip.domain.com:1234"); @@ -475,8 +475,8 @@ public void ParsePartialURISIPSSchemeUnitTest() [Fact] public void ParsePartialURIWithUserUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURIRelaxed("sip:joe.bloggs@sip.domain.com:1234;transport=tcp"); @@ -494,8 +494,8 @@ public void ParsePartialURIWithUserUnitTest() [Fact] public void ParseHoHostUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); Assert.Throws(() => SIPURI.ParseSIPURI("sip:;transport=UDP")); @@ -505,8 +505,8 @@ public void ParseHoHostUnitTest() [Fact] public void UDPProtocolToStringTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = new SIPURI(SIPSchemesEnum.sip, SIPEndPoint.ParseSIPEndPoint("127.0.0.1")); logger.LogDebug("{sipURI}", sipURI.ToString()); @@ -517,8 +517,8 @@ public void UDPProtocolToStringTest() [Fact] public void ParseUDPProtocolToStringTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURIRelaxed("127.0.0.1"); logger.LogDebug("{sipURI}", sipURI.ToString()); @@ -529,8 +529,8 @@ public void ParseUDPProtocolToStringTest() [Fact] public void ParseBigURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURIRelaxed("TRUNKa1d2ce524d44cd54f39ac78bcdba85c7@65.98.14.50:5069"); logger.LogDebug("{sipURI}", sipURI.ToString()); @@ -541,8 +541,8 @@ public void ParseBigURIUnitTest() [Fact] public void ParseMalformedContactUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); Assert.Throws(() => SIPURI.ParseSIPURIRelaxed("sip:twolmsted@24.183.120.253, sip:5060")); @@ -552,8 +552,8 @@ public void ParseMalformedContactUnitTest() [Fact] public void NoPortIPv4CanonicalAddressToStringTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:127.0.0.1"); logger.LogDebug("SIP URI {sipURI}", sipURI); @@ -571,8 +571,8 @@ public void NoPortIPv4CanonicalAddressToStringTest() [Fact] public void ParseIPv6UnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:[::1]"); @@ -598,8 +598,8 @@ public void ParseIPv6UnitTest() [Fact] public void ParseIPv6WithExplicitPortUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURI = SIPURI.ParseSIPURI("sip:[::1]:6060"); @@ -618,8 +618,8 @@ public void ParseIPv6WithExplicitPortUnitTest() [Fact] public void IPv6UriPortToNoPortCanonicalAddressUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI sipURINoPort = SIPURI.ParseSIPURI("sip:[::1]"); SIPURI sipURIWIthPort = SIPURI.ParseSIPURI("sip:[::1]:5060"); @@ -662,8 +662,8 @@ public void IPv6UriPortToNoPortCanonicalAddressUnitTest() [Fact] public void UriConstructorWithIPv6AddressUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI ipv6Uri = new SIPURI(SIPSchemesEnum.sip, IPAddress.IPv6Loopback, 6060); @@ -678,8 +678,8 @@ public void UriConstructorWithIPv6AddressUnitTest() [Fact] public void InvalidIPv6UriThrowUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI ipv6Uri = new SIPURI(SIPSchemesEnum.sip, IPAddress.IPv6Loopback, 6060); @@ -695,8 +695,8 @@ public void InvalidIPv6UriThrowUnitTest() [Fact] public void ParseIPv4MappedAddressUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI ipv6Uri = new SIPURI(SIPSchemesEnum.sip, IPAddress.IPv6Loopback, 6060); @@ -713,8 +713,8 @@ public void ParseIPv4MappedAddressUnitTest() [Fact] public void ParseReplacesHeaderUriUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI referToUri = SIPURI.ParseSIPURI("sip:1@127.0.0.1?Replaces=84929ZTg0Zjk1Y2UyM2Q1OWJjYWNlZmYyYTI0Njg1YjgwMzI%3Bto-tag%3D8787f9cc94bb4bb19c089af17e5a94f7%3Bfrom-tag%3Dc2b89404"); @@ -730,8 +730,8 @@ public void ParseReplacesHeaderUriUnitTest() [Fact] public void MangleUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@192.168.0.50:5060?Replaces=xyz"); SIPURI mangled = SIPURI.Mangle(uri, IPSocket.Parse("67.222.131.147:5090")); @@ -750,8 +750,8 @@ public void MangleUnitTest() [Fact] public void MangleNoPortUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@192.168.0.50?Replaces=xyz"); SIPURI mangled = SIPURI.Mangle(uri, IPSocket.Parse("67.222.131.147:5090")); @@ -771,8 +771,8 @@ public void MangleNoPortUnitTest() [Fact] public void MangleReceiveOnIPv6UnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@192.168.0.50:5060?Replaces=xyz"); SIPURI mangled = SIPURI.Mangle(uri, IPSocket.Parse("[2001:730:3ec2::10]:5090")); @@ -792,8 +792,8 @@ public void MangleReceiveOnIPv6UnitTest() [Fact] public void NoMangleSameAddressUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@192.168.0.50:5060?Replaces=xyz"); SIPURI mangled = SIPURI.Mangle(uri, IPSocket.Parse("192.168.0.50:5060")); @@ -809,8 +809,8 @@ public void NoMangleSameAddressUnitTest() [Fact] public void NoManglePublicIPv4UnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@67.222.131.149:5060?Replaces=xyz"); SIPURI mangled = SIPURI.Mangle(uri, IPSocket.Parse("67.222.131.147:5060")); @@ -826,8 +826,8 @@ public void NoManglePublicIPv4UnitTest() [Fact] public void NoMangleHostnameUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@sipsorcery.com:5060?Replaces=xyz"); SIPURI mangled = SIPURI.Mangle(uri, IPSocket.Parse("67.222.131.147:5060")); @@ -843,8 +843,8 @@ public void NoMangleHostnameUnitTest() [Fact] public void NoMangleIPv6UnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@[2001:730:3ec2::10]:5060?Replaces=xyz"); SIPURI mangled = SIPURI.Mangle(uri, IPSocket.Parse("67.222.131.147:5060")); @@ -860,8 +860,8 @@ public void NoMangleIPv6UnitTest() [Fact] public void DefaultUdpPortUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@[2001:730:3ec2::10]:5060?Replaces=xyz"); @@ -878,8 +878,8 @@ public void DefaultUdpPortUnitTest() [Fact] public void DefaultUdpPortWhenNotSetUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@[2001:730:3ec2::10]?Replaces=xyz"); @@ -895,8 +895,8 @@ public void DefaultUdpPortWhenNotSetUnitTest() [Fact] public void NonDefaultUdpPortUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@[2001:730:3ec2::10]:5080?Replaces=xyz"); @@ -912,8 +912,8 @@ public void NonDefaultUdpPortUnitTest() [Fact] public void DefaultTcpPortUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@[2001:730:3ec2::10]:5060;transport=tcp?Replaces=xyz"); @@ -929,8 +929,8 @@ public void DefaultTcpPortUnitTest() [Fact] public void DefaultTlsPortUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sips:user@[2001:730:3ec2::10]:5061?Replaces=xyz"); @@ -946,8 +946,8 @@ public void DefaultTlsPortUnitTest() [Fact] public void DefaultWebSocketPortUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@[2001:730:3ec2::10]:80;transport=ws?Replaces=xyz"); @@ -963,8 +963,8 @@ public void DefaultWebSocketPortUnitTest() [Fact] public void DefaultSecureWebSocketPortUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPURI uri = SIPURI.ParseSIPURI("sip:user@[2001:730:3ec2::10]:443;transport=wss?Replaces=xyz"); @@ -980,8 +980,8 @@ public void DefaultSecureWebSocketPortUnitTest() [Fact] public void ParseTelSchemeURIUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var telStr = "tel:+1-(201) 555 0123;phone-context=example.com"; SIPURI sipURI = SIPURI.ParseSIPURI(telStr); diff --git a/test/unit/core/SIP/SIPUserFieldUnitTest.cs b/test/unit/core/SIP/SIPUserFieldUnitTest.cs index 5bb7edcd3c..42aa0a85b8 100644 --- a/test/unit/core/SIP/SIPUserFieldUnitTest.cs +++ b/test/unit/core/SIP/SIPUserFieldUnitTest.cs @@ -10,7 +10,7 @@ //----------------------------------------------------------------------------- using Microsoft.Extensions.Logging; -using SIPSorcery.UnitTests; +using SIPSorcery.UnitTests; using Xunit; namespace SIPSorcery.SIP.UnitTests @@ -28,8 +28,8 @@ public SIPUserFieldUnitTest(Xunit.Abstractions.ITestOutputHelper output) [Fact] public void ParamsInUserPortionURITest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); SIPUserField userField = SIPUserField.ParseSIPUserField(""); @@ -45,8 +45,8 @@ public void ParamsInUserPortionURITest() [Fact] public void ParseSIPUserFieldUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var userField = SIPUserField.ParseSIPUserField("\"Jane Doe\" "); @@ -62,8 +62,8 @@ public void ParseSIPUserFieldUnitTest() [Fact] public void ParseSIPUserFieldNoAnglesUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var userField = SIPUserField.ParseSIPUserField("sip:jane@doe.com"); @@ -79,8 +79,8 @@ public void ParseSIPUserFieldNoAnglesUnitTest() [Fact] public void ParseWithHeaderParametersUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var userField = SIPUserField.ParseSIPUserField("\"Jane Doe\" p=1;q=2"); @@ -99,8 +99,8 @@ public void ParseWithHeaderParametersUnitTest() [Fact] public void ParseWithHeaderAndURIParametersUnitTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var userField = SIPUserField.ParseSIPUserField("\"Jane Doe\" p=1;q=2"); diff --git a/test/unit/core/SIP/TortureTests.cs b/test/unit/core/SIP/TortureTests.cs index 668056f0c0..9cfb9d5d8b 100644 --- a/test/unit/core/SIP/TortureTests.cs +++ b/test/unit/core/SIP/TortureTests.cs @@ -9,6 +9,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.IO; using System.Net; using System.Net.Sockets; @@ -307,7 +308,7 @@ public void RFC5118_4_6() Assert.True(IPAddress.TryParse(sipRequest.URI.HostAddress, out ip6)); Assert.Equal(AddressFamily.InterNetworkV6, ip6.AddressFamily); Assert.False(string.IsNullOrWhiteSpace(sipRequest.Body)); - SDP sdp = SDP.ParseSDPDescription(sipRequest.Body); + SDP sdp = SDP.ParseSDPDescription(sipRequest.Body.AsSpan()); Assert.NotNull(sdp); Assert.NotNull(sdp.Connection); Assert.True(IPAddress.TryParse(sdp.Connection.ConnectionAddress, out ip6)); @@ -390,7 +391,7 @@ public void RFC5118_4_8() Assert.True(IPAddress.TryParse(sipRequest.URI.HostAddress, out ip6)); Assert.Equal(AddressFamily.InterNetworkV6, ip6.AddressFamily); Assert.False(string.IsNullOrWhiteSpace(sipRequest.Body)); - SDP sdp = SDP.ParseSDPDescription(sipRequest.Body); + SDP sdp = SDP.ParseSDPDescription(sipRequest.Body.AsSpan()); Assert.NotNull(sdp); //Assert.NotNull(sdp.Connection); //Assert.True(IPAddress.TryParse(sdp.Connection.ConnectionAddress, out ip4)); @@ -441,7 +442,7 @@ public void RFC5118_4_9() Assert.Equal(AddressFamily.InterNetworkV6, ip6.AddressFamily); Assert.False(IPAddress.TryParse(sipRequest.URI.HostAddress, out ip6)); Assert.False(string.IsNullOrWhiteSpace(sipRequest.Body)); - SDP sdp = SDP.ParseSDPDescription(sipRequest.Body); + SDP sdp = SDP.ParseSDPDescription(sipRequest.Body.AsSpan()); Assert.NotNull(sdp); Assert.NotNull(sdp.Connection); Assert.True(IPAddress.TryParse(sdp.Connection.ConnectionAddress, out ip6)); diff --git a/test/unit/net/ICE/RTCIceCandidateUnitTest.cs b/test/unit/net/ICE/RTCIceCandidateUnitTest.cs index 3f791e92db..4369268edb 100644 --- a/test/unit/net/ICE/RTCIceCandidateUnitTest.cs +++ b/test/unit/net/ICE/RTCIceCandidateUnitTest.cs @@ -10,6 +10,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Net; using Microsoft.Extensions.Logging; using SIPSorcery.UnitTests; @@ -36,7 +37,7 @@ public void ParseHostCandidateUnitTest() logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); - var candidate = RTCIceCandidate.Parse("1390596646 1 udp 1880747346 192.168.11.50 61680 typ host generation 0"); + var candidate = RTCIceCandidate.Parse("1390596646 1 udp 1880747346 192.168.11.50 61680 typ host generation 0".AsSpan()); Assert.NotNull(candidate); Assert.Equal(RTCIceCandidateType.host, candidate.type); @@ -54,7 +55,7 @@ public void Parse_IPv6_Host_Candidate_UnitTest() logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); - var candidate = RTCIceCandidate.Parse("1390596646 1 udp 1880747346 [::1] 61680 typ host generation 0"); + var candidate = RTCIceCandidate.Parse("1390596646 1 udp 1880747346 [::1] 61680 typ host generation 0".AsSpan()); Assert.NotNull(candidate); Assert.Equal(RTCIceCandidateType.host, candidate.type); @@ -73,7 +74,7 @@ public void Parse_IPv6_Host_NoBrackets_Candidate_UnitTest() logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); - var candidate = RTCIceCandidate.Parse("1390596646 1 udp 1880747346 ::1 61680 typ host generation 0"); + var candidate = RTCIceCandidate.Parse("1390596646 1 udp 1880747346 ::1 61680 typ host generation 0".AsSpan()); Assert.NotNull(candidate); Assert.Equal(RTCIceCandidateType.host, candidate.type); @@ -92,7 +93,7 @@ public void ParseSvrRflxCandidateUnitTest() logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); - var candidate = RTCIceCandidate.Parse("842163049 1 udp 1677729535 8.8.8.8 12767 typ srflx raddr 0.0.0.0 rport 0 generation 0 network-cost 999"); + var candidate = RTCIceCandidate.Parse("842163049 1 udp 1677729535 8.8.8.8 12767 typ srflx raddr 0.0.0.0 rport 0 generation 0 network-cost 999".AsSpan()); Assert.NotNull(candidate); Assert.Equal(RTCIceCandidateType.srflx, candidate.type); @@ -159,7 +160,7 @@ public void ToJsonUnitTest() logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); - var candidate = RTCIceCandidate.Parse("1390596646 1 udp 1880747346 192.168.11.50 61680 typ host generation 0"); + var candidate = RTCIceCandidate.Parse("1390596646 1 udp 1880747346 192.168.11.50 61680 typ host generation 0".AsSpan()); Assert.NotNull(candidate); Assert.Equal(RTCIceCandidateType.host, candidate.type); @@ -174,7 +175,7 @@ public void ToJsonUnitTest() Assert.Equal(0, init.sdpMLineIndex); Assert.Equal("0", init.sdpMid); - var initCandidate = RTCIceCandidate.Parse(init.candidate); + var initCandidate = RTCIceCandidate.Parse(init.candidate.AsSpan()); Assert.Equal(RTCIceCandidateType.host, initCandidate.type); Assert.Equal(RTCIceProtocol.udp, initCandidate.protocol); diff --git a/test/unit/net/RTCP/RTCFeedbackUnitTest.cs b/test/unit/net/RTCP/RTCFeedbackUnitTest.cs index f8a0aeeddb..56c1b3ac58 100644 --- a/test/unit/net/RTCP/RTCFeedbackUnitTest.cs +++ b/test/unit/net/RTCP/RTCFeedbackUnitTest.cs @@ -44,7 +44,8 @@ public void RoundtripPictureLossIndicationReportUnitTest() uint mediaSsrc = 44; RTCPFeedback rtcpPli = new RTCPFeedback(senderSsrc, mediaSsrc, PSFBFeedbackTypesEnum.PLI); - byte[] buffer = rtcpPli.GetBytes(); + var buffer = new byte[rtcpPli.GetByteCount()]; + rtcpPli.WriteBytes(buffer); logger.LogDebug("Serialised PLI feedback report: {Buffer}", BufferUtils.HexStr(buffer)); @@ -79,7 +80,8 @@ public void RoundtripREMBUnitTest() BitrateMantissa = 222242u, FeedbackSSRC = 0x4a8eec30 }; - byte[] buffer = rtcpREMB.GetBytes(); + var buffer = new byte[rtcpREMB.GetByteCount()]; + rtcpREMB.WriteBytes(buffer); logger.LogDebug("Serialised REMB: {Buffer}", BufferUtils.HexStr(buffer)); @@ -118,12 +120,14 @@ public void RoundtripREMBUnitTestMultipleSsrcs() BitrateMantissa = 222242u, FeedbackSSRCs = new uint[]{0x4a8eec30,0x4a8eec44,0x4a8eec58} }; - byte[] buffer = rtcpREMB.GetBytes(); + var buffer = new byte[rtcpREMB.GetByteCount()]; + rtcpREMB.WriteBytes(buffer); logger.LogDebug("Serialised REMB: {Buffer}", BufferUtils.HexStr(buffer)); RTCPFeedback parsedREMB = new RTCPFeedback(buffer); - var parsedBuffer = parsedREMB.GetBytes(); + var parsedBuffer = new byte[parsedREMB.GetByteCount()]; + parsedREMB.WriteBytes(parsedBuffer); Assert.Equal(parsedBuffer, buffer); Assert.Equal(RTCPReportTypesEnum.PSFB, parsedREMB.Header.PacketType); Assert.Equal(PSFBFeedbackTypesEnum.AFB, parsedREMB.Header.PayloadFeedbackMessageType); diff --git a/test/unit/net/RTCP/RTCPByeUnitTest.cs b/test/unit/net/RTCP/RTCPByeUnitTest.cs index b4c957e495..340d8817a9 100644 --- a/test/unit/net/RTCP/RTCPByeUnitTest.cs +++ b/test/unit/net/RTCP/RTCPByeUnitTest.cs @@ -42,7 +42,8 @@ public void RoundtripRTCPByeUnitTest() uint ssrc = 23; RTCPBye bye = new RTCPBye(ssrc, null); - byte[] buffer = bye.GetBytes(); + byte[] buffer = new byte[bye.GetByteCount()]; + bye.WriteBytes(buffer); RTCPBye parsedBye = new RTCPBye(buffer); @@ -64,7 +65,8 @@ public void RoundtripByeWithReasonUnitTest() string reason = "x"; RTCPBye bye = new RTCPBye(ssrc, reason); - byte[] buffer = bye.GetBytes(); + byte[] buffer = new byte[bye.GetByteCount()]; + bye.WriteBytes(buffer); RTCPBye parsedBye = new RTCPBye(buffer); @@ -87,7 +89,8 @@ public void RoundtripRTCPByeOnBoundaryUnitTest() string reason = "1234567"; RTCPBye bye = new RTCPBye(ssrc, reason); - byte[] buffer = bye.GetBytes(); + byte[] buffer = new byte[bye.GetByteCount()]; + bye.WriteBytes(buffer); RTCPBye parsedBye = new RTCPBye(buffer); @@ -110,7 +113,8 @@ public void RoundtripByeWithTimeoutReasonUnitTest() string reason = RTCPSession.NO_ACTIVITY_TIMEOUT_REASON; RTCPBye bye = new RTCPBye(ssrc, reason); - byte[] buffer = bye.GetBytes(); + byte[] buffer = new byte[bye.GetByteCount()]; + bye.WriteBytes(buffer); RTCPBye parsedBye = new RTCPBye(buffer); diff --git a/test/unit/net/RTCP/RTCPCompoundPacketUnitTest.cs b/test/unit/net/RTCP/RTCPCompoundPacketUnitTest.cs index d3a2770ed5..2d228bfa74 100644 --- a/test/unit/net/RTCP/RTCPCompoundPacketUnitTest.cs +++ b/test/unit/net/RTCP/RTCPCompoundPacketUnitTest.cs @@ -64,7 +64,8 @@ public void RoundtripRTCPCompoundPacketUnitTest() RTCPCompoundPacket compoundPacket = new RTCPCompoundPacket(sr, sdesReport); - byte[] buffer = compoundPacket.GetBytes(); + var buffer = new byte[compoundPacket.GetByteCount()]; + compoundPacket.WriteBytes(buffer); RTCPCompoundPacket parsedCP = new RTCPCompoundPacket(buffer); RTCPSenderReport parsedSR = parsedCP.SenderReport; diff --git a/test/unit/net/RTCP/RTCPHeaderUnitTest.cs b/test/unit/net/RTCP/RTCPHeaderUnitTest.cs index c56e4f7b5a..2ff1aee238 100644 --- a/test/unit/net/RTCP/RTCPHeaderUnitTest.cs +++ b/test/unit/net/RTCP/RTCPHeaderUnitTest.cs @@ -13,6 +13,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; using SIPSorcery.UnitTests; @@ -33,11 +34,12 @@ public RTCPHeaderUnitTest(Xunit.Abstractions.ITestOutputHelper output) [Fact] public void GetRTCPHeaderTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); RTCPHeader rtcpHeader = new RTCPHeader(RTCPReportTypesEnum.SR, 1); - byte[] headerBuffer = rtcpHeader.GetHeader(0, 0); + byte[] headerBuffer = new byte[rtcpHeader.GetByteCount()]; + rtcpHeader.WriteBytes(headerBuffer.AsSpan()); int byteNum = 1; foreach (byte headerByte in headerBuffer) @@ -50,11 +52,14 @@ public void GetRTCPHeaderTest() [Fact] public void RTCPHeaderRoundTripTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); RTCPHeader src = new RTCPHeader(RTCPReportTypesEnum.SR, 1); - byte[] headerBuffer = src.GetHeader(17, 54443); + src.SetReceptionReportCount(17); + src.SetLength(54443); + byte[] headerBuffer = new byte[src.GetByteCount()]; + src.WriteBytes(headerBuffer.AsSpan()); RTCPHeader dst = new RTCPHeader(headerBuffer); logger.LogDebug("Version: {SrcVersion}, {DstVersion}", src.Version, dst.Version); @@ -63,7 +68,7 @@ public void RTCPHeaderRoundTripTest() logger.LogDebug("PacketType: {SrcPacketType}, {DstPacketType}", src.PacketType, dst.PacketType); logger.LogDebug("Length: {SrcLength}, {DstLength}", src.Length, dst.Length); - logger.LogDebug("Raw Header: {RawHeader}", headerBuffer.HexStr(headerBuffer.Length)); + logger.LogDebug("Raw Header: {RawHeader}", headerBuffer.AsSpan(0, headerBuffer.Length).HexStr()); Assert.True(src.Version == dst.Version, "Version was mismatched."); Assert.True(src.PaddingFlag == dst.PaddingFlag, "PaddingFlag was mismatched."); diff --git a/test/unit/net/RTCP/RTCPReceiverReportUnitTest.cs b/test/unit/net/RTCP/RTCPReceiverReportUnitTest.cs index 2a52d0feb9..e4cb0e9762 100644 --- a/test/unit/net/RTCP/RTCPReceiverReportUnitTest.cs +++ b/test/unit/net/RTCP/RTCPReceiverReportUnitTest.cs @@ -55,7 +55,8 @@ public void RoundtripRTCPReceiverResportUnitTest() var rr = new ReceptionReportSample(rrSsrc, fractionLost, packetsLost, highestSeqNum, jitter, lastSRTimestamp, delaySinceLastSR); var receiverReport = new RTCPReceiverReport(ssrc, new List { rr }); - byte[] buffer = receiverReport.GetBytes(); + var buffer = new byte[receiverReport.GetByteCount()]; + receiverReport.WriteBytes(buffer); RTCPReceiverReport parsedRR = new RTCPReceiverReport(buffer); diff --git a/test/unit/net/RTCP/RTCPSDesReportUnitTest.cs b/test/unit/net/RTCP/RTCPSDesReportUnitTest.cs index 9954250e37..faa190b307 100644 --- a/test/unit/net/RTCP/RTCPSDesReportUnitTest.cs +++ b/test/unit/net/RTCP/RTCPSDesReportUnitTest.cs @@ -43,7 +43,8 @@ public void RoundtripRTCPSDesReportUnitTest() string cname = "abc"; RTCPSDesReport sdesReport = new RTCPSDesReport(ssrc, cname); - byte[] buffer = sdesReport.GetBytes(); + var buffer = new byte[sdesReport.GetByteCount()]; + sdesReport.WriteBytes(buffer); RTCPSDesReport parsedReport = new RTCPSDesReport(buffer); @@ -66,7 +67,8 @@ public void RoundtripRTCPSDesReportNotOnBoundaryUnitTest() string cname = "ab1234"; RTCPSDesReport sdesReport = new RTCPSDesReport(ssrc, cname); - byte[] buffer = sdesReport.GetBytes(); + var buffer = new byte[sdesReport.GetByteCount()]; + sdesReport.WriteBytes(buffer); RTCPSDesReport parsedReport = new RTCPSDesReport(buffer); @@ -89,7 +91,8 @@ public void RoundtripRTCPSDesReportOnBoundaryUnitTest() string cname = "ab123"; // 5 bytes + 1 byte for the item null termination. RTCPSDesReport sdesReport = new RTCPSDesReport(ssrc, cname); - byte[] buffer = sdesReport.GetBytes(); + var buffer = new byte[sdesReport.GetByteCount()]; + sdesReport.WriteBytes(buffer); RTCPSDesReport parsedReport = new RTCPSDesReport(buffer); diff --git a/test/unit/net/RTCP/RTCPSenderReportUnitTest.cs b/test/unit/net/RTCP/RTCPSenderReportUnitTest.cs index 351f3b354c..1c5207b168 100644 --- a/test/unit/net/RTCP/RTCPSenderReportUnitTest.cs +++ b/test/unit/net/RTCP/RTCPSenderReportUnitTest.cs @@ -59,7 +59,8 @@ public void RoundtripRTCPSenderResportUnitTest() ReceptionReportSample rr = new ReceptionReportSample(rrSsrc, fractionLost, packetsLost, highestSeqNum, jitter, lastSRTimestamp, delaySinceLastSR); var sr = new RTCPSenderReport(ssrc, ntpTs, rtpTs, packetCount, octetCount, new List { rr }); - byte[] buffer = sr.GetBytes(); + var buffer = new byte[sr.GetByteCount()]; + sr.WriteBytes(buffer); RTCPSenderReport parsedSR = new RTCPSenderReport(buffer); diff --git a/test/unit/net/RTP/AV1PacketiserUnitTest.cs b/test/unit/net/RTP/AV1PacketiserUnitTest.cs index 54167c7495..9ef84e15ea 100644 --- a/test/unit/net/RTP/AV1PacketiserUnitTest.cs +++ b/test/unit/net/RTP/AV1PacketiserUnitTest.cs @@ -14,90 +14,103 @@ //----------------------------------------------------------------------------- using System; +using System.Buffers; using System.Linq; +using CommunityToolkit.HighPerformance.Buffers; using SIPSorcery.Net; using SIPSorceryMedia.Abstractions; using Xunit; -namespace SIPSorcery.Net.UnitTests +namespace SIPSorcery.Net.UnitTests; + +[Trait("Category", "unit")] +public class AV1PacketiserUnitTest { - [Trait("Category", "unit")] - public class AV1PacketiserUnitTest + [Fact] + public void PacketiseAndDepacketiseAggregatedAv1FrameUnitTest() { - [Fact] - public void PacketiseAndDepacketiseAggregatedAv1FrameUnitTest() - { - var temporalDelimiter = CreateObu(AV1Packetiser.AV1ObuType.TemporalDelimiter, Array.Empty()); - var sequenceHeader = CreateObu(AV1Packetiser.AV1ObuType.SequenceHeader, new byte[] { 0x01, 0x02, 0x03 }); - var frame = CreateObu(AV1Packetiser.AV1ObuType.Frame, new byte[] { 0x11, 0x22, 0x33, 0x44 }); - var sourceTemporalUnit = temporalDelimiter.Concat(sequenceHeader).Concat(frame).ToArray(); - var expectedTemporalUnit = sequenceHeader.Concat(frame).ToArray(); + // Arrange + + var temporalDelimiter = CreateObu(AV1Packetiser.AV1ObuType.TemporalDelimiter, Array.Empty()); + var sequenceHeader = CreateObu(AV1Packetiser.AV1ObuType.SequenceHeader, new byte[] { 0x01, 0x02, 0x03 }); + var frame = CreateObu(AV1Packetiser.AV1ObuType.Frame, new byte[] { 0x11, 0x22, 0x33, 0x44 }); + var sourceTemporalUnit = temporalDelimiter.Concat(sequenceHeader).Concat(frame).ToArray(); + var expectedTemporalUnit = sequenceHeader.Concat(frame).ToArray(); - var packets = AV1Packetiser.Packetize(sourceTemporalUnit, 1200); + // Act - Assert.Single(packets); + var packets = AV1Packetiser.Packetize(sourceTemporalUnit, 1200); - var framer = new RtpVideoFramer(VideoCodecsEnum.AV1, 4096); - byte[] reconstructedFrame = null; + // Assert - for (ushort i = 0; i < packets.Count; i++) + Assert.Single(packets); + + var framer = new RtpVideoFramer(VideoCodecsEnum.AV1, 4096); + using var bufferWriter = new ArrayPoolBufferWriter(); + bool gotFrame = false; + + for (ushort i = 0; i < packets.Count; i++) + { + var header = new RTPHeader { - var packet = new RTPPacket - { - Header = new RTPHeader - { - SequenceNumber = i, - Timestamp = 90000, - MarkerBit = packets[i].IsLast ? 1 : 0 - }, - Payload = packets[i].Payload - }; - - reconstructedFrame = framer.GotRtpPacket(packet); - } - - Assert.NotNull(reconstructedFrame); - Assert.Equal(expectedTemporalUnit, reconstructedFrame); + SequenceNumber = i, + Timestamp = 90000, + MarkerBit = packets[i].IsLast ? 1 : 0 + }; + var packet = new RTPPacket(header, packets[i].Payload); + + gotFrame = framer.GotRtpPacket(bufferWriter, packet); } - [Fact] - public void PacketiseAndDepacketiseFragmentedAv1FrameUnitTest() - { - var payload = Enumerable.Range(0, 1500).Select(x => (byte)(x % 251)).ToArray(); - var frame = CreateObu(AV1Packetiser.AV1ObuType.Frame, payload); + Assert.True(gotFrame); + Assert.Equal(expectedTemporalUnit, bufferWriter.WrittenMemory.ToArray()); + } - var packets = AV1Packetiser.Packetize(frame, 200); + [Fact] + public void PacketiseAndDepacketiseFragmentedAv1FrameUnitTest() + { + // Arrange - Assert.True(packets.Count > 1); + var payload = Enumerable.Range(0, 1500).Select(x => (byte)(x % 251)).ToArray(); + var frame = CreateObu(AV1Packetiser.AV1ObuType.Frame, payload); - var framer = new RtpVideoFramer(VideoCodecsEnum.AV1, 4096); - byte[] reconstructedFrame = null; + // Act - for (ushort i = 0; i < packets.Count; i++) - { - var packet = new RTPPacket - { - Header = new RTPHeader - { - SequenceNumber = i, - Timestamp = 180000, - MarkerBit = packets[i].IsLast ? 1 : 0 - }, - Payload = packets[i].Payload - }; - - reconstructedFrame = framer.GotRtpPacket(packet); - } - - Assert.NotNull(reconstructedFrame); - Assert.Equal(frame, reconstructedFrame); - } + var packets = AV1Packetiser.Packetize(frame, 200); + + // Assert + + Assert.True(packets.Count > 1); - private static byte[] CreateObu(AV1Packetiser.AV1ObuType obuType, byte[] payload) + var framer = new RtpVideoFramer(VideoCodecsEnum.AV1, 4096); + using var bufferWriter = new ArrayPoolBufferWriter(); + bool gotFrame = false; + + for (ushort i = 0; i < packets.Count; i++) { - byte obuHeader = (byte)(((byte)obuType << 3) | 0x02); - var leb128Size = AV1Packetiser.WriteLeb128(payload.Length); - return new[] { obuHeader }.Concat(leb128Size).Concat(payload).ToArray(); + var header = new RTPHeader + { + SequenceNumber = i, + Timestamp = 180000, + MarkerBit = packets[i].IsLast ? 1 : 0 + }; + var packet = new RTPPacket(header, packets[i].Payload); + + gotFrame = framer.GotRtpPacket(bufferWriter, packet); } + + Assert.True(gotFrame); + Assert.Equal(frame, bufferWriter.WrittenMemory.ToArray()); + } + + private static byte[] CreateObu(AV1Packetiser.AV1ObuType obuType, byte[] payload) + { + byte obuHeader = (byte)(((byte)obuType << 3) | 0x02); + int leb128Length = AV1Packetiser.GetLeb128Length(payload.Length); + var obu = new byte[1 + leb128Length + payload.Length]; + obu[0] = obuHeader; + AV1Packetiser.WriteLeb128(obu.AsSpan(1), payload.Length); + payload.CopyTo(obu.AsSpan(1 + leb128Length)); + return obu; } } diff --git a/test/unit/net/RTP/RTPHeaderUnitTest.cs b/test/unit/net/RTP/RTPHeaderUnitTest.cs index 34e2beb22f..79844e140b 100644 --- a/test/unit/net/RTP/RTPHeaderUnitTest.cs +++ b/test/unit/net/RTP/RTPHeaderUnitTest.cs @@ -9,6 +9,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Linq; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; @@ -35,7 +36,9 @@ public void GetHeaderTest() logger.BeginScope(TestHelper.GetCurrentMethodName()); RTPHeader rtpHeader = new RTPHeader(); - byte[] headerBuffer = rtpHeader.GetHeader(1, 0, 1); + rtpHeader.SetHeader(1, 0, 1); + byte[] headerBuffer = new byte[rtpHeader.GetByteCount()]; + rtpHeader.WriteBytes(headerBuffer.AsSpan()); int byteNum = 1; foreach (byte headerByte in headerBuffer) @@ -52,7 +55,9 @@ public void HeaderRoundTripTest() logger.BeginScope(TestHelper.GetCurrentMethodName()); RTPHeader src = new RTPHeader(); - byte[] headerBuffer = src.GetHeader(1, 0, 1); + src.SetHeader(1, 0, 1); + byte[] headerBuffer = new byte[src.GetByteCount()]; + src.WriteBytes(headerBuffer.AsSpan()); RTPHeader dst = new RTPHeader(headerBuffer); logger.LogDebug("Versions: {SrcVersion}, {DstVersion}", src.Version, dst.Version); @@ -92,7 +97,9 @@ public void CustomisedHeaderRoundTripTest() src.CSRCCount = 3; src.PayloadType = (int)SDPWellKnownMediaFormatsEnum.PCMA; - byte[] headerBuffer = src.GetHeader(1, 0, 1); + src.SetHeader(1, 0, 1); + byte[] headerBuffer = new byte[src.GetByteCount()]; + src.WriteBytes(headerBuffer.AsSpan()); RTPHeader dst = new RTPHeader(headerBuffer); @@ -221,7 +228,7 @@ public void ParseHeaderExtensions() Assert.Equal(13, extension.Id); Assert.Equal(RTPHeaderExtensionType.OneByte, extension.Type); - var expectedValue = new byte[] {0xb3, 0x85, 0xb0, 0x8f, 0xc, 0x13, 0x9d, 0xe5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}; + var expectedValue = new byte[] { 0xb3, 0x85, 0xb0, 0x8f, 0xc, 0x13, 0x9d, 0xe5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 }; Assert.Equal(expectedValue, extension.Data); } @@ -241,9 +248,20 @@ public void should_empty_rtp_header_extensions() }; var packet = new RTPPacket(rtpPayload); var header = packet.Header; - var extensions= header.GetHeaderExtensions().ToList(); + var extensions = header.GetHeaderExtensions().ToList(); Assert.NotNull(extensions); Assert.Empty(extensions); } + + private byte[] GetHeader(RTPHeader header, UInt16 sequenceNumber, uint timestamp, uint syncSource) + { + header.SequenceNumber = sequenceNumber; + header.Timestamp = timestamp; + header.SyncSource = syncSource; + + var bytes = new byte[header.GetByteCount()]; + header.WriteBytes(bytes); + return bytes; + } } } diff --git a/test/unit/net/RTP/RTPSessionCreateAnswerUnitTest.cs b/test/unit/net/RTP/RTPSessionCreateAnswerUnitTest.cs index e72f999f05..c2e6794457 100644 --- a/test/unit/net/RTP/RTPSessionCreateAnswerUnitTest.cs +++ b/test/unit/net/RTP/RTPSessionCreateAnswerUnitTest.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: RTPSessionCreateAnswerUnitTest.cs // // Description: Characterization tests for RTPSession.CreateAnswer @@ -37,7 +37,7 @@ public RTPSessionCreateAnswerUnitTest(Xunit.Abstractions.ITestOutputHelper outpu } /// - /// CreateAnswer must throw ApplicationException when no remote + /// CreateAnswer must throw SipSorceryException when no remote /// description has been set. Mirrors the WebRTC createAnswer /// contract. /// @@ -46,7 +46,7 @@ public void NoRemoteDescriptionSet_Throws() { using (var session = new RtpSessionBuilder().WithAudioTrack().Build()) { - Assert.Throws(() => session.CreateAnswer(IPAddress.Loopback)); + Assert.Throws(() => session.CreateAnswer(IPAddress.Loopback)); } } diff --git a/test/unit/net/RTP/RTPSessionRenegotiationUnitTest.cs b/test/unit/net/RTP/RTPSessionRenegotiationUnitTest.cs index 815315dc98..aaae83708f 100644 --- a/test/unit/net/RTP/RTPSessionRenegotiationUnitTest.cs +++ b/test/unit/net/RTP/RTPSessionRenegotiationUnitTest.cs @@ -12,6 +12,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -78,7 +79,7 @@ public void VideoRejectedByReInviteClosesRtcpSession() a=rtpmap:96 VP8/90000 a=sendrecv"; - var initialOffer = SDP.ParseSDPDescription(initialOfferSdp); + var initialOffer = SDP.ParseSDPDescription(initialOfferSdp.AsSpan()); var result = rtpSession.SetRemoteDescription(SdpType.offer, initialOffer); Assert.Equal(SetDescriptionResultEnum.OK, result); @@ -104,7 +105,7 @@ public void VideoRejectedByReInviteClosesRtcpSession() m=video 0 RTP/AVP 96 a=rtpmap:96 VP8/90000"; - var reInviteOffer = SDP.ParseSDPDescription(reInviteOfferSdp); + var reInviteOffer = SDP.ParseSDPDescription(reInviteOfferSdp.AsSpan()); result = rtpSession.SetRemoteDescription(SdpType.offer, reInviteOffer); Assert.Equal(SetDescriptionResultEnum.OK, result); @@ -160,7 +161,7 @@ public void BothStreamsActiveAfterReInviteKeepsRtcpRunning() a=rtpmap:96 VP8/90000 a=sendrecv"; - var offer = SDP.ParseSDPDescription(offerSdp); + var offer = SDP.ParseSDPDescription(offerSdp.AsSpan()); var result = rtpSession.SetRemoteDescription(SdpType.offer, offer); Assert.Equal(SetDescriptionResultEnum.OK, result); rtpSession.Start(); @@ -179,7 +180,7 @@ public void BothStreamsActiveAfterReInviteKeepsRtcpRunning() a=rtpmap:96 VP8/90000 a=sendrecv"; - var reInvite = SDP.ParseSDPDescription(reInviteSdp); + var reInvite = SDP.ParseSDPDescription(reInviteSdp.AsSpan()); result = rtpSession.SetRemoteDescription(SdpType.offer, reInvite); Assert.Equal(SetDescriptionResultEnum.OK, result); diff --git a/test/unit/net/RTP/RTPSessionSdesCryptoNegotiationUnitTest.cs b/test/unit/net/RTP/RTPSessionSdesCryptoNegotiationUnitTest.cs index 9f51347821..da84ae41ea 100644 --- a/test/unit/net/RTP/RTPSessionSdesCryptoNegotiationUnitTest.cs +++ b/test/unit/net/RTP/RTPSessionSdesCryptoNegotiationUnitTest.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: RTPSessionSdesCryptoNegotiationUnitTest.cs // // Description: Characterization tests for SDP-driven SRTP (SDES) crypto @@ -249,7 +249,7 @@ public void RejectedMediaPortZeroWithoutCrypto_CreateAnswerThrows() Assert.Equal(SetDescriptionResultEnum.OK, session.SetRemoteDescription(SdpType.offer, offer)); - Assert.Throws( + Assert.Throws( () => session.CreateAnswer(IPAddress.Loopback)); } } diff --git a/test/unit/net/RTP/RTPSessionUnitTest.cs b/test/unit/net/RTP/RTPSessionUnitTest.cs index f20ffb6dcf..4a0374f1f2 100644 --- a/test/unit/net/RTP/RTPSessionUnitTest.cs +++ b/test/unit/net/RTP/RTPSessionUnitTest.cs @@ -378,7 +378,7 @@ public void CheckDuplicateBindPortFailsUnitTest() RTPSession duplicateSession = new RTPSession(false, false, false, IPAddress.Loopback, rtpEndPoint.Port); MediaStreamTrack duplicateTrack = new MediaStreamTrack(SDPMediaTypesEnum.audio, false, new List { new SDPAudioVideoMediaFormat(SDPWellKnownMediaFormatsEnum.PCMU) }); - Assert.Throws(() => duplicateSession.addTrack(duplicateTrack)); + Assert.Throws(() => duplicateSession.addTrack(duplicateTrack)); localSession.Close(null); } @@ -421,7 +421,7 @@ public void MediaOrderMatchesRemoteOfferUnitTest() }); rtpSession.addTrack(localAudioTrack); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -468,7 +468,7 @@ public void SetRemoteSDPNoMediaStreamAttributeUnitTest() MediaStreamTrack localAudioTrack = new MediaStreamTrack(new AudioFormat(SDPWellKnownMediaFormatsEnum.PCMU)); rtpSession.addTrack(localAudioTrack); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -514,7 +514,7 @@ public void CheckSelectedAudioFormatAttributeUnitTest() MediaStreamTrack localAudioTrack = new MediaStreamTrack(SDPWellKnownMediaFormatsEnum.PCMA, SDPWellKnownMediaFormatsEnum.G723); rtpSession.addTrack(localAudioTrack); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); @@ -548,7 +548,7 @@ public void CheckSelectedTextFormatParsedSDPUnitTest() rtpSession.addTrack(localTextTrack); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug($"Remote offer: {offer}"); @@ -589,7 +589,7 @@ public void ModifiedWellKnownFormatIDUnitTest() Assert.Equal(8, rtpSession.AudioStream.LocalTrack.Capabilities.Single(x => x.Name() == "PCMA").ID); - var offer = SDP.ParseSDPDescription(remoteSdp); + var offer = SDP.ParseSDPDescription(remoteSdp.AsSpan()); logger.LogDebug("Remote offer: {RemoteOffer}", offer); var result = rtpSession.SetRemoteDescription(SIP.App.SdpType.offer, offer); diff --git a/test/unit/net/RTP/ReorderBufferUnitTest.cs b/test/unit/net/RTP/ReorderBufferUnitTest.cs index 3af09363a0..e30c4ab2d1 100644 --- a/test/unit/net/RTP/ReorderBufferUnitTest.cs +++ b/test/unit/net/RTP/ReorderBufferUnitTest.cs @@ -191,7 +191,7 @@ private void AssertSequenceNumber(RTPReorderBuffer buffer, ushort expected) { } private RTPPacket CreatePacket(ushort seq, DateTime datetime = default) { - return new RTPPacket() { Header = new RTPHeader() { SequenceNumber = seq, ReceivedTime = datetime } }; + return new RTPPacket(new RTPHeader() { SequenceNumber = seq, ReceivedTime = datetime }, ReadOnlyMemory.Empty); } } } diff --git a/test/unit/net/RTSP/RTSPMessageUnitTest.cs b/test/unit/net/RTSP/RTSPMessageUnitTest.cs index d8b754a29b..173e39e795 100644 --- a/test/unit/net/RTSP/RTSPMessageUnitTest.cs +++ b/test/unit/net/RTSP/RTSPMessageUnitTest.cs @@ -1,60 +1,60 @@ -//----------------------------------------------------------------------------- -// Filename: RTSPMessageUnitTest.cs -// -// Description: Unit tests for the RTSPMessage class. -// -// Author(s): -// Aaron Clauson (aaron@sipsorcery.com) -// -// History: -// 20 Jan 2014 Aaron Clauson Created, Hobart, Australia. +//----------------------------------------------------------------------------- +// Filename: RTSPMessageUnitTest.cs +// +// Description: Unit tests for the RTSPMessage class. +// +// Author(s): +// Aaron Clauson (aaron@sipsorcery.com) // +// History: +// 20 Jan 2014 Aaron Clauson Created, Hobart, Australia. +// // License: -// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. -//----------------------------------------------------------------------------- - -using System; -using System.Text; -using Microsoft.Extensions.Logging; -using SIPSorcery.UnitTests; -using Xunit; - -namespace SIPSorcery.Net.UnitTests -{ - [Trait("Category", "unit")] - public class RTSPMessageUnitTest - { - private Microsoft.Extensions.Logging.ILogger logger = null; - - public RTSPMessageUnitTest(Xunit.Abstractions.ITestOutputHelper output) - { - logger = SIPSorcery.UnitTests.TestLogHelper.InitTestLogger(output); +// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. +//----------------------------------------------------------------------------- + +using System; +using System.Text; +using Microsoft.Extensions.Logging; +using SIPSorcery.UnitTests; +using Xunit; + +namespace SIPSorcery.Net.UnitTests +{ + [Trait("Category", "unit")] + public class RTSPMessageUnitTest + { + private Microsoft.Extensions.Logging.ILogger logger = null; + + public RTSPMessageUnitTest(Xunit.Abstractions.ITestOutputHelper output) + { + logger = SIPSorcery.UnitTests.TestLogHelper.InitTestLogger(output); } - private string m_CRLF = SIP.SIPConstants.CRLF; - - /// - /// Tests that an RTSP request with headers and a body is correctly serialised and parsed. - /// - [Fact] - public void RTSPRequestWIthStandardHeadersParseTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - int cseq = 23; - string session = Guid.NewGuid().ToString(); + private string m_CRLF = SIP.SIPConstants.CRLF; + + /// + /// Tests that an RTSP request with headers and a body is correctly serialised and parsed. + /// + [Fact] + public void RTSPRequestWIthStandardHeadersParseTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + int cseq = 23; + string session = Guid.NewGuid().ToString(); string body = $"v=0{m_CRLF}o=- 2890844526 2890842807 IN IP4 192.16.24.202{m_CRLF}s=RTSP Session{m_CRLF}m=audio 3456 RTP/AVP 0{m_CRLF}a=control:rtsp://live.example.com/concert/audio{m_CRLF}c=IN IP4 224.2.0.1/16"; - - RTSPResponse describeResponse = new RTSPResponse(RTSPResponseStatusCodesEnum.OK, null); - describeResponse.Header = new RTSPHeader(cseq, session); - describeResponse.Body = body; - - byte[] buffer = Encoding.UTF8.GetBytes(describeResponse.ToString()); - RTSPMessage rtspMessage = RTSPMessage.ParseRTSPMessage(buffer, null, null); - - Assert.Equal(RTSPMessageTypesEnum.Response, rtspMessage.RTSPMessageType); - Assert.Equal(body, rtspMessage.Body); - } - } -} + + RTSPResponse describeResponse = new RTSPResponse(RTSPResponseStatusCodesEnum.OK, null); + describeResponse.Header = new RTSPHeader(cseq, session); + describeResponse.Body = body; + + byte[] buffer = Encoding.UTF8.GetBytes(describeResponse.ToString()); + RTSPMessage rtspMessage = RTSPMessage.ParseRTSPMessage(buffer, null, null); + + Assert.Equal(RTSPMessageTypesEnum.Response, rtspMessage.RTSPMessageType); + Assert.Equal(body, rtspMessage.Body); + } + } +} diff --git a/test/unit/net/SCTP/SctpAssociationUnitTest.cs b/test/unit/net/SCTP/SctpAssociationUnitTest.cs index 47490d3ea0..b5df3c626b 100644 --- a/test/unit/net/SCTP/SctpAssociationUnitTest.cs +++ b/test/unit/net/SCTP/SctpAssociationUnitTest.cs @@ -20,6 +20,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; +using SIPSorcery.Sys.UnitTests; using SIPSorcery.UnitTests; using Xunit; @@ -166,9 +167,9 @@ public async Task SendLargeFragmentedDataChunk() byte[] dummyData = new byte[SctpAssociation.DEFAULT_ADVERTISED_RECEIVE_WINDOW]; Crypto.GetRandomBytes(dummyData); - string sha256Hash = Crypto.GetSHA256Hash(dummyData); + string sha256Hash = SIPSorcery.UnitTests.CryptoHelper.GetSHA256Hash(dummyData); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - bAssoc.OnData += (frame) => tcs.TrySetResult(Crypto.GetSHA256Hash(frame.UserData)); + bAssoc.OnData += (frame) => tcs.TrySetResult(SIPSorcery.UnitTests.CryptoHelper.GetSHA256Hash(frame.UserData)); aAssoc.SendData(0, 0, dummyData); var timeoutTask = Task.Delay(TimeSpan.FromSeconds(3)); @@ -237,7 +238,7 @@ internal static (SctpAssociation a, SctpAssociation b) GetConnectedAssociations( } else { - throw new ApplicationException("GetConnectedAssociations failed to connect associations."); + throw new SipSorceryException("GetConnectedAssociations failed to connect associations."); } } } @@ -269,14 +270,15 @@ public void Listen() { if (_input.TryTake(out var buffer, 1000)) { - SctpPacket pkt = SctpPacket.Parse(buffer, 0, buffer.Length); + SctpPacket pkt = SctpPacket.Parse(buffer.AsSpan()); // Process packet. if (pkt.Chunks.Any(x => x.KnownType == SctpChunkType.INIT)) { var initAckPacket = base.GetInitAck(pkt, null); - var initAckBuffer = initAckPacket.GetBytes(); - Send(null, initAckBuffer, 0, initAckBuffer.Length); + var initAckBuffer = new byte[initAckPacket.GetByteCount()]; + initAckPacket.WriteBytes(initAckBuffer.AsSpan()); + Send(null, initAckBuffer.AsMemory()); } else if (pkt.Chunks.Any(x => x.KnownType == SctpChunkType.COOKIE_ECHO)) { @@ -284,7 +286,7 @@ public void Listen() var cookie = base.GetCookie(pkt); if (cookie.IsEmpty()) { - throw new ApplicationException($"MockB2BSctpTransport gave itself an invalid INIT cookie."); + throw new SipSorceryException($"MockB2BSctpTransport gave itself an invalid INIT cookie."); } else { @@ -299,9 +301,9 @@ public void Listen() } } - public override void Send(string associationID, byte[] buffer, int offset, int length) + public override void Send(string associationID, ReadOnlyMemory buffer, IDisposable memoryOwner = null) { - _output.Add(buffer.Skip(offset).Take(length).ToArray()); + _output.Add(buffer.ToArray()); } public void Close() diff --git a/test/unit/net/SCTP/SctpChunkUnitTest.cs b/test/unit/net/SCTP/SctpChunkUnitTest.cs index 346630fce9..e679cac32c 100644 --- a/test/unit/net/SCTP/SctpChunkUnitTest.cs +++ b/test/unit/net/SCTP/SctpChunkUnitTest.cs @@ -13,6 +13,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Linq; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; @@ -45,15 +46,15 @@ public void RoundtripHeartBeatChunk() ChunkValue = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 } }; - byte[] buffer = new byte[heartbeatChunk.GetChunkLength(true)]; + byte[] buffer = new byte[heartbeatChunk.GetByteCount(true)]; - heartbeatChunk.WriteTo(buffer, 0); + heartbeatChunk.WriteBytes(buffer.AsSpan()); - var rndTripChunk = SctpChunk.Parse(buffer, 0); + var rndTripChunk = SctpChunk.Parse(buffer.AsSpan()); Assert.Equal(SctpChunkType.HEARTBEAT, rndTripChunk.KnownType); Assert.Equal(0, rndTripChunk.ChunkFlags); - Assert.Equal("0102030405", rndTripChunk.ChunkValue.HexStr()); + Assert.Equal("0102030405", TypeExtensions.HexStr(rndTripChunk.ChunkValue)); } /// @@ -62,9 +63,9 @@ public void RoundtripHeartBeatChunk() [Fact] public void ParseSACKChunk() { - var sackBuffer = BufferUtils.ParseHexStr("13881388E48092946AB2050003000014D19244F60002000000000001A7498379"); + var sackBuffer = TypeExtensions.ParseHexStr("13881388E48092946AB2050003000014D19244F60002000000000001A7498379"); - var sackPkt = SctpPacket.Parse(sackBuffer, 0, sackBuffer.Length); + var sackPkt = SctpPacket.Parse(sackBuffer.AsSpan()); Assert.NotNull(sackPkt); Assert.Single(sackPkt.Chunks); diff --git a/test/unit/net/SCTP/SctpDataReceiverUnitTest.cs b/test/unit/net/SCTP/SctpDataReceiverUnitTest.cs index deb35614b3..63d4dfb926 100644 --- a/test/unit/net/SCTP/SctpDataReceiverUnitTest.cs +++ b/test/unit/net/SCTP/SctpDataReceiverUnitTest.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: SctpDataReceiverUnitTest.cs // // Description: Unit tests for the SctpDataReceiver class. @@ -42,7 +42,7 @@ public void SinglePacketFrame() var sortedFrames = receiver.OnDataChunk(chunk); Assert.Single(sortedFrames); - Assert.Equal("00", sortedFrames.Single().UserData.HexStr()); + Assert.Equal("00", TypeExtensions.HexStr(sortedFrames.Single().UserData)); Assert.Equal(0U, receiver.CumulativeAckTSN); Assert.Equal(0, receiver.ForwardTSNCount); } @@ -69,7 +69,7 @@ public void ThreeFragments() Assert.Empty(sortFrames1); Assert.Empty(sortFrames2); Assert.Single(sortFrames3); - Assert.Equal("000102", sortFrames3.Single().UserData.HexStr()); + Assert.Equal("000102", TypeExtensions.HexStr(sortFrames3.Single().UserData)); Assert.Equal(0, receiver.ForwardTSNCount); } @@ -96,7 +96,7 @@ public void ThreeFragmentsOutOfOrder() Assert.Empty(sortFrames1); Assert.Empty(sortFrames2); Assert.Single(sortFrames3); - Assert.Equal("000102", sortFrames3.Single().UserData.HexStr()); + Assert.Equal("000102", TypeExtensions.HexStr(sortFrames3.Single().UserData)); Assert.Equal(0, receiver.ForwardTSNCount); } @@ -123,7 +123,7 @@ public void ThreeFragmentsBeginLast() Assert.Empty(sortFrames1); Assert.Empty(sortFrames2); Assert.Single(sortFrames3); - Assert.Equal("000102", sortFrames3.Single().UserData.HexStr()); + Assert.Equal("000102", TypeExtensions.HexStr(sortFrames3.Single().UserData)); Assert.Equal(0, receiver.ForwardTSNCount); } @@ -157,7 +157,7 @@ public void FragmentWithTSNWrap() Assert.Empty(sFrames3); Assert.Empty(sFrames4); Assert.Single(sFrames5); - Assert.Equal("0001020304", sFrames5.Single().UserData.HexStr()); + Assert.Equal("0001020304", TypeExtensions.HexStr(sFrames5.Single().UserData)); Assert.Equal(0, receiver.ForwardTSNCount); } @@ -202,7 +202,7 @@ public void FragmentWithTSNWrapAndOutOfOrder() Assert.Single(sframes6); Assert.Single(sframes9); Assert.Single(sframes5); - Assert.Equal("0001020304", sframes5.Single().UserData.HexStr()); + Assert.Equal("0001020304", TypeExtensions.HexStr(sframes5.Single().UserData)); Assert.Equal(2, receiver.ForwardTSNCount); } @@ -232,7 +232,7 @@ public void FragmentWithExpectedTSNWrap() Assert.Empty(sframes3); Assert.Empty(sframes4); Assert.Single(sframes5); - Assert.Equal("0001020304", sframes5.Single().UserData.HexStr()); + Assert.Equal("0001020304", TypeExtensions.HexStr(sframes5.Single().UserData)); Assert.Equal(0, receiver.ForwardTSNCount); } @@ -293,7 +293,7 @@ public void CheckExpiryWithSinglePacketChunksUnordered() var sortedFrames = receiver.OnDataChunk(chunk); Assert.Single(sortedFrames); - Assert.Equal("55", sortedFrames.Single().UserData.HexStr()); + Assert.Equal("55", TypeExtensions.HexStr(sortedFrames.Single().UserData)); Assert.Equal(0, receiver.ForwardTSNCount); Assert.Equal(tsn - 1, receiver.CumulativeAckTSN); } @@ -319,7 +319,7 @@ public void CheckExpiryWithSinglePacketChunksOrdered() var sortedFrames = receiver.OnDataChunk(chunk); Assert.Single(sortedFrames); - Assert.Equal("55", sortedFrames.Single().UserData.HexStr()); + Assert.Equal("55", TypeExtensions.HexStr(sortedFrames.Single().UserData)); Assert.Equal(0, receiver.ForwardTSNCount); Assert.Equal(tsn - 1, receiver.CumulativeAckTSN); } @@ -343,13 +343,13 @@ public void ThreeStreamPackets() Assert.Single(sortFrames1); Assert.Equal(0, sortFrames1.Single().StreamSeqNum); - Assert.Equal("00", sortFrames1.Single().UserData.HexStr()); + Assert.Equal("00", TypeExtensions.HexStr(sortFrames1.Single().UserData)); Assert.Single(sortFrames2); Assert.Equal(1, sortFrames2.Single().StreamSeqNum); - Assert.Equal("01", sortFrames2.Single().UserData.HexStr()); + Assert.Equal("01", TypeExtensions.HexStr(sortFrames2.Single().UserData)); Assert.Single(sortFrames3); Assert.Equal(2, sortFrames3.Single().StreamSeqNum); - Assert.Equal("02", sortFrames3.Single().UserData.HexStr()); + Assert.Equal("02", TypeExtensions.HexStr(sortFrames3.Single().UserData)); Assert.Equal(0, receiver.ForwardTSNCount); } @@ -377,9 +377,9 @@ public void StreamPacketsReceviedOutOfOrder() Assert.Empty(sortFrames2); Assert.Equal(3, sortFrames3.Count); Assert.Equal(0, sortFrames3.First().StreamSeqNum); - Assert.Equal("00", sortFrames3.First().UserData.HexStr()); + Assert.Equal("00", TypeExtensions.HexStr(sortFrames3.First().UserData)); Assert.Equal(2, sortFrames3.Last().StreamSeqNum); - Assert.Equal("02", sortFrames3.Last().UserData.HexStr()); + Assert.Equal("02", TypeExtensions.HexStr(sortFrames3.Last().UserData)); Assert.Equal(0, receiver.ForwardTSNCount); } diff --git a/test/unit/net/SCTP/SctpDataSenderUnitTest.cs b/test/unit/net/SCTP/SctpDataSenderUnitTest.cs index 5aff0b5be3..a7e76fe7c9 100755 --- a/test/unit/net/SCTP/SctpDataSenderUnitTest.cs +++ b/test/unit/net/SCTP/SctpDataSenderUnitTest.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: SctpDataSenderUnitTest.cs // // Description: Unit tests for the SctpDataSender class. @@ -54,7 +54,7 @@ public async Task SmallBufferSend() Assert.Single(outStm); byte[] sendBuffer = outStm.Single(); - SctpPacket pkt = SctpPacket.Parse(sendBuffer, 0, sendBuffer.Length); + SctpPacket pkt = SctpPacket.Parse(sendBuffer.AsSpan()); Assert.NotNull(pkt); Assert.NotNull(pkt.Chunks.Single() as SctpDataChunk); diff --git a/test/unit/net/SCTP/SctpHeaderUnitTest.cs b/test/unit/net/SCTP/SctpHeaderUnitTest.cs index 5886ee2a76..691b9853c4 100644 --- a/test/unit/net/SCTP/SctpHeaderUnitTest.cs +++ b/test/unit/net/SCTP/SctpHeaderUnitTest.cs @@ -13,6 +13,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using Microsoft.Extensions.Logging; using SIPSorcery.UnitTests; using Xunit; @@ -52,7 +53,7 @@ public void RoundtripSctpHeader() header.WriteToBuffer(buffer, 0); - var rndTripHeader = SctpHeader.Parse(buffer, 0); + var rndTripHeader = SctpHeader.Parse(buffer.AsSpan()); Assert.Equal(srcPort, rndTripHeader.SourcePort); Assert.Equal(dstPort, rndTripHeader.DestinationPort); @@ -69,7 +70,7 @@ public void ParseUsrSctpInitHeader() byte[] buffer = { 0xdf, 0x90, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x6a, 0xb8, 0x0e, 0x99 }; - var sctpHdr = SctpHeader.Parse(buffer, 0); + var sctpHdr = SctpHeader.Parse(buffer.AsSpan()); Assert.Equal(57232, sctpHdr.SourcePort); Assert.Equal(7, sctpHdr.DestinationPort); diff --git a/test/unit/net/SCTP/SctpPacketUnitTest.cs b/test/unit/net/SCTP/SctpPacketUnitTest.cs index eb8bf9f036..458009a9d4 100644 --- a/test/unit/net/SCTP/SctpPacketUnitTest.cs +++ b/test/unit/net/SCTP/SctpPacketUnitTest.cs @@ -13,6 +13,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Linq; using Microsoft.Extensions.Logging; using SIPSorcery.Sys; @@ -50,9 +51,9 @@ public void ParseUsrSctpInitPacket() 0x69, 0x81, 0x78, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x05, 0x00, 0x08, 0xc0, 0xa8, 0x0b, 0x32, 0x00, 0x05, 0x00, 0x08, 0xc0, 0xa8, 0x00, 0x32 }; - Assert.True(SctpPacket.IsValid(buffer, 0, buffer.Length, 0U)); + Assert.True(SctpPacket.IsValid(buffer.AsSpan(), 0U)); - var sctpPkt = SctpPacket.Parse(buffer, 0, buffer.Length); + var sctpPkt = SctpPacket.Parse(buffer.AsSpan()); Assert.NotNull(sctpPkt); Assert.Equal(57232, sctpPkt.Header.SourcePort); @@ -123,9 +124,9 @@ public void ParseUsrSctpInitAckPacket() 0x00, 0x05, 0x00, 0x08, 0xc0, 0xa8, 0x00, 0x32, 0xc5, 0x35, 0x15, 0xc8, 0x35, 0x57, 0x0a, 0xd5, 0x96, 0x29, 0xc8, 0xbf, 0x38, 0x7b, 0xc2, 0x16, 0xe9, 0x4c, 0x81, 0xbe }; - Assert.True(SctpPacket.IsValid(buffer, 0, buffer.Length, 0xe31c5536U)); + Assert.True(SctpPacket.IsValid(buffer.AsSpan(), 0xe31c5536U)); - var sctpPkt = SctpPacket.Parse(buffer, 0, buffer.Length); + var sctpPkt = SctpPacket.Parse(buffer.AsSpan()); Assert.NotNull(sctpPkt); Assert.Equal(7, sctpPkt.Header.SourcePort); @@ -156,10 +157,10 @@ public void ParseUsrSctpCookieEchoPacket() 0x4b, 0x41, 0x4d, 0x45, 0x2d, 0x42, 0x53, 0x44, 0x20, 0x31, 0x2e, 0x31, 0x00, 0x00, 0x00, 0x00 }; // Checksum does not match because the original cookie was too long and was truncated for testing purposes. - Assert.False(SctpPacket.VerifyChecksum(buffer, 0, buffer.Length)); - Assert.Equal(0xcd6e6150U, SctpPacket.GetVerificationTag(buffer, 0, buffer.Length)); + Assert.False(SctpPacket.VerifyChecksum(buffer.AsSpan())); + Assert.Equal(0xcd6e6150U, SctpPacket.GetVerificationTag(buffer.AsSpan())); - var sctpPkt = SctpPacket.Parse(buffer, 0, buffer.Length); + var sctpPkt = SctpPacket.Parse(buffer.AsSpan()); Assert.NotNull(sctpPkt); Assert.Equal(57232, sctpPkt.Header.SourcePort); @@ -171,7 +172,7 @@ public void ParseUsrSctpCookieEchoPacket() var cookieEchoChunk = sctpPkt.Chunks.First(); - Assert.Equal(0x14, cookieEchoChunk.GetChunkLength(true)); + Assert.Equal(0x14, cookieEchoChunk.GetByteCount(true)); Assert.Equal(0x10, cookieEchoChunk.ChunkValue.Length); } @@ -185,9 +186,9 @@ public void ParseUsrSctpCookieAckPacket() byte[] buffer = { 0x00, 0x07, 0xdf, 0x90, 0xe3, 0x1c, 0x55, 0x36, 0xb2, 0x04, 0xdf, 0x21, 0x0b, 0x00, 0x00, 0x04 }; - Assert.True(SctpPacket.IsValid(buffer, 0, buffer.Length, 0xe31c5536U)); + Assert.True(SctpPacket.IsValid(buffer.AsSpan(), 0xe31c5536U)); - var sctpPkt = SctpPacket.Parse(buffer, 0, buffer.Length); + var sctpPkt = SctpPacket.Parse(buffer.AsSpan()); Assert.NotNull(sctpPkt); Assert.Equal(7, sctpPkt.Header.SourcePort); @@ -199,7 +200,7 @@ public void ParseUsrSctpCookieAckPacket() var cookieAckChunk = sctpPkt.Chunks.First(); - Assert.Equal(4, cookieAckChunk.GetChunkLength(true)); + Assert.Equal(4, cookieAckChunk.GetByteCount(true)); } /// @@ -221,18 +222,19 @@ public void RoundTripInitPacket() 0x69, 0x81, 0x78, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x05, 0x00, 0x08, 0xc0, 0xa8, 0x0b, 0x32, 0x00, 0x05, 0x00, 0x08, 0xc0, 0xa8, 0x00, 0x32 }; - Assert.True(SctpPacket.IsValid(buffer, 0, buffer.Length, 0x0U)); + Assert.True(SctpPacket.IsValid(buffer.AsSpan(), 0x0U)); - var initPkt = SctpPacket.Parse(buffer, 0, buffer.Length); + var initPkt = SctpPacket.Parse(buffer.AsSpan()); - var rndTripBuffer = initPkt.GetBytes(); + byte[] rndTripBuffer = new byte[initPkt.GetByteCount()]; + initPkt.WriteBytes(rndTripBuffer.AsSpan()); logger.LogDebug("Before: {BufferHexStr}", buffer.HexStr()); logger.LogDebug("After : {RndTripBufferHexStr}", rndTripBuffer.HexStr()); - Assert.True(SctpPacket.IsValid(rndTripBuffer, 0, rndTripBuffer.Length, 0x0U)); + Assert.True(SctpPacket.IsValid(rndTripBuffer.AsSpan(), 0x0U)); - var sctpPkt = SctpPacket.Parse(rndTripBuffer, 0, rndTripBuffer.Length); + var sctpPkt = SctpPacket.Parse(rndTripBuffer.AsSpan()); Assert.NotNull(sctpPkt); Assert.Equal(57232, sctpPkt.Header.SourcePort); @@ -265,9 +267,9 @@ public void ParseUsrSctpHeartbeatPacket() 0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0xc0, 0xa8, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - Assert.True(SctpPacket.IsValid(buffer, 0, buffer.Length, 0x054a3af0U)); + Assert.True(SctpPacket.IsValid(buffer.AsSpan(), 0x054a3af0U)); - var sctpPkt = SctpPacket.Parse(buffer, 0, buffer.Length); + var sctpPkt = SctpPacket.Parse(buffer.AsSpan()); Assert.NotNull(sctpPkt); Assert.Equal(7, sctpPkt.Header.SourcePort); @@ -279,7 +281,7 @@ public void ParseUsrSctpHeartbeatPacket() var heartbeatChunk = sctpPkt.Chunks.First(); - Assert.Equal(44, heartbeatChunk.GetChunkLength(true)); + Assert.Equal(44, heartbeatChunk.GetByteCount(true)); } /// @@ -294,18 +296,19 @@ public void RoundTripHeartbeatPacket() 0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0xc0, 0xa8, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - Assert.True(SctpPacket.IsValid(buffer, 0, buffer.Length, 0x054a3af0U)); + Assert.True(SctpPacket.IsValid(buffer.AsSpan(), 0x054a3af0U)); - var heartbeatPkt = SctpPacket.Parse(buffer, 0, buffer.Length); + var heartbeatPkt = SctpPacket.Parse(buffer.AsSpan()); - var rndTripBuffer = heartbeatPkt.GetBytes(); + var rndTripBuffer = new byte[heartbeatPkt.GetByteCount()]; + heartbeatPkt.WriteBytes(rndTripBuffer.AsSpan()); logger.LogDebug("Before: {BufferHexStr}", buffer.HexStr()); logger.LogDebug("After : {RndTripBufferHexStr}", rndTripBuffer.HexStr()); - Assert.True(SctpPacket.IsValid(rndTripBuffer, 0, rndTripBuffer.Length, 0x054a3af0U)); + Assert.True(SctpPacket.IsValid(rndTripBuffer.AsSpan(), 0x054a3af0U)); - var sctpPkt = SctpPacket.Parse(rndTripBuffer, 0, buffer.Length); + var sctpPkt = SctpPacket.Parse(rndTripBuffer.AsSpan()); Assert.NotNull(sctpPkt); Assert.Equal(7, sctpPkt.Header.SourcePort); @@ -317,7 +320,7 @@ public void RoundTripHeartbeatPacket() var heartbeatChunk = sctpPkt.Chunks.First(); - Assert.Equal(44, heartbeatChunk.GetChunkLength(true)); + Assert.Equal(44, heartbeatChunk.GetByteCount(true)); } /// @@ -331,9 +334,9 @@ public void ParseUsrSctpDataPacket() 0x00, 0x07, 0x11, 0x5c, 0x2e, 0x0b, 0x82, 0xc7, 0x5d, 0x2c, 0xeb, 0xa7, 0x00, 0x07, 0x00, 0x13, 0xdf, 0x08, 0xc1, 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x69, 0x0a, 0x00}; - Assert.True(SctpPacket.IsValid(buffer, 0, buffer.Length, 0x2e0b82c7U)); + Assert.True(SctpPacket.IsValid(buffer.AsSpan(), 0x2e0b82c7U)); - var sctpPkt = SctpPacket.Parse(buffer, 0, buffer.Length); + var sctpPkt = SctpPacket.Parse(buffer.AsSpan()); Assert.NotNull(sctpPkt); Assert.Equal(7, sctpPkt.Header.SourcePort); @@ -346,7 +349,7 @@ public void ParseUsrSctpDataPacket() var dataChunk = sctpPkt.Chunks.First() as SctpDataChunk; - Assert.Equal(20, dataChunk.GetChunkLength(true)); + Assert.Equal(20, dataChunk.GetByteCount(true)); Assert.Equal("68690A", dataChunk.UserData.HexStr()); Assert.Equal(3741893047, dataChunk.TSN); } @@ -361,18 +364,19 @@ public void RoundTripDataPacket() 0x00, 0x07, 0x11, 0x5c, 0x2e, 0x0b, 0x82, 0xc7, 0x5d, 0x2c, 0xeb, 0xa7, 0x00, 0x07, 0x00, 0x13, 0xdf, 0x08, 0xc1, 0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x69, 0x0a, 0x00}; - Assert.True(SctpPacket.IsValid(buffer, 0, buffer.Length, 0x2e0b82c7U)); + Assert.True(SctpPacket.IsValid(buffer.AsSpan(), 0x2e0b82c7U)); - var dataPkt = SctpPacket.Parse(buffer, 0, buffer.Length); + var dataPkt = SctpPacket.Parse(buffer.AsSpan()); - var rndTripBuffer = dataPkt.GetBytes(); + var rndTripBuffer = new byte[dataPkt.GetByteCount()]; + dataPkt.WriteBytes(rndTripBuffer.AsSpan()); logger.LogDebug("Before: {BufferHexStr}", buffer.HexStr()); logger.LogDebug("After : {RndTripBufferHexStr}", rndTripBuffer.HexStr()); - Assert.True(SctpPacket.IsValid(rndTripBuffer, 0, rndTripBuffer.Length, 0x2e0b82c7U)); + Assert.True(SctpPacket.IsValid(rndTripBuffer.AsSpan(), 0x2e0b82c7U)); - var sctpPkt = SctpPacket.Parse(rndTripBuffer, 0, buffer.Length); + var sctpPkt = SctpPacket.Parse(rndTripBuffer.AsSpan()); Assert.NotNull(sctpPkt); Assert.Equal(7, sctpPkt.Header.SourcePort); @@ -384,7 +388,7 @@ public void RoundTripDataPacket() var dataChunk = sctpPkt.Chunks.First() as SctpDataChunk; - Assert.Equal(20, dataChunk.GetChunkLength(true)); + Assert.Equal(20, dataChunk.GetByteCount(true)); Assert.Equal("68690A", dataChunk.UserData.HexStr()); Assert.Equal(3741893047, dataChunk.TSN); } @@ -406,9 +410,9 @@ public void ParseUsrSctpAbortPacket() 0x65, 0x71, 0x75, 0x61, 0x6c, 0x20, 0x74, 0x68, 0x61, 0x6e, 0x20, 0x54, 0x53, 0x4e, 0x20, 0x63, 0x36, 0x65, 0x33, 0x61, 0x64, 0x33, 0x63, 0x00}; - Assert.True(SctpPacket.IsValid(buffer, 0, buffer.Length, 0x93c9d98aU)); + Assert.True(SctpPacket.IsValid(buffer.AsSpan(), 0x93c9d98aU)); - var sctpPkt = SctpPacket.Parse(buffer, 0, buffer.Length); + var sctpPkt = SctpPacket.Parse(buffer.AsSpan()); Assert.NotNull(sctpPkt); Assert.Equal(7, sctpPkt.Header.SourcePort); diff --git a/test/unit/net/SCTP/SctpTransportUnitTest.cs b/test/unit/net/SCTP/SctpTransportUnitTest.cs index b03a72d2a9..1f0f8af35e 100644 --- a/test/unit/net/SCTP/SctpTransportUnitTest.cs +++ b/test/unit/net/SCTP/SctpTransportUnitTest.cs @@ -13,6 +13,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Linq; using System.Text; using Microsoft.Extensions.Logging; @@ -90,7 +91,7 @@ public SctpPacket GetInitAck(SctpPacket initPacket) return base.GetCookieHMAC(buffer); } - public override void Send(string associationID, byte[] buffer, int offset, int length) + public override void Send(string associationID, ReadOnlyMemory buffer, IDisposable memoryOwner = null) { } } } diff --git a/test/unit/net/SDP/KeyParameterUnitTest.cs b/test/unit/net/SDP/KeyParameterUnitTest.cs index baca18e0a2..3b8ed4ae0e 100644 --- a/test/unit/net/SDP/KeyParameterUnitTest.cs +++ b/test/unit/net/SDP/KeyParameterUnitTest.cs @@ -96,31 +96,31 @@ public void ParseTest() logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); - SDPSecurityDescription.KeyParameter kp1 = SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4"); + SDPSecurityDescription.KeyParameter kp1 = SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4".AsSpan()); Assert.Equal("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4", kp1.ToString()); - Assert.Equal(kp1.ToString(), SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4").ToString()); - Assert.Equal(kp1.ToString(), SDPSecurityDescription.KeyParameter.Parse(kp1.ToString()).ToString()); + Assert.Equal(kp1.ToString(), SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4".AsSpan()).ToString()); + Assert.Equal(kp1.ToString(), SDPSecurityDescription.KeyParameter.Parse(kp1.ToString().AsSpan()).ToString()); Assert.Equal(4u, kp1.MkiLength); Assert.Equal(1u, kp1.MkiValue); Assert.Equal((ulong)Math.Pow(2, 20), kp1.LifeTime); Assert.Equal("2^20", kp1.LifeTimeString); Assert.Equal("MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm", kp1.KeySaltBase64); - SDPSecurityDescription.KeyParameter kp2 = SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20"); + SDPSecurityDescription.KeyParameter kp2 = SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20".AsSpan()); Assert.Equal((ulong)Math.Pow(2, 20), kp2.LifeTime); Assert.Equal("2^20", kp2.LifeTimeString); Assert.Equal(0u, kp2.MkiLength); Assert.Equal(0u, kp2.MkiValue); - SDPSecurityDescription.KeyParameter kp3 = SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|1:4"); + SDPSecurityDescription.KeyParameter kp3 = SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|1:4".AsSpan()); Assert.Equal(0uL, kp3.LifeTime); Assert.Equal(4u, actual: kp3.MkiLength); Assert.Equal(1u, kp3.MkiValue); - SDPSecurityDescription.KeyParameter kp4 = SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|1:4|2^20"); + SDPSecurityDescription.KeyParameter kp4 = SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|1:4|2^20".AsSpan()); Assert.Equal("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4", kp4.ToString()); - Assert.Equal(SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4").ToString(), kp4.ToString()); - Assert.Equal(SDPSecurityDescription.KeyParameter.Parse(kp4.ToString()).ToString(), kp4.ToString()); + Assert.Equal(SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4".AsSpan()).ToString(), kp4.ToString()); + Assert.Equal(SDPSecurityDescription.KeyParameter.Parse(kp4.ToString().AsSpan()).ToString(), kp4.ToString()); Assert.Equal(4u, kp4.MkiLength); Assert.Equal(1u, kp4.MkiValue); Assert.Equal((ulong)Math.Pow(2, 20), kp4.LifeTime); @@ -128,17 +128,16 @@ public void ParseTest() Assert.Equal("MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm", kp4.KeySaltBase64); SDPSecurityDescription.KeyParameter kp5 = KeyParameterFactory.Create("ĀĀ\0\0\0\0\0\0\0\0\0\0\0\0\0\0", "ĀĀĀ\0\0\0\0\0\0\0\0\0\0\0"); - Assert.Equal(SDPSecurityDescription.KeyParameter.Parse(kp5.ToString()).ToString(), kp5.ToString()); + Assert.Equal(SDPSecurityDescription.KeyParameter.Parse(kp5.ToString().AsSpan()).ToString(), kp5.ToString()); - Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse(null)); - Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("")); + Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse(default)); - Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4;inline:QUJjZGVmMTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5|2^20|2:4")); - Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|20^2|1:4")); - Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^0|1:4")); - Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTINDU2Nzg5QUJjZGVm|2^20|1:4")); - Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|14")); - Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline: MTIzNDU2Nzg5QUJDREUwMTINDU2Nzg5QUJjZGVm|2^20|1:4")); + Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4;inline:QUJjZGVmMTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5|2^20|2:4".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|20^2|1:4".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^0|1:4".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTINDU2Nzg5QUJjZGVm|2^20|1:4".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|14".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.KeyParameter.Parse("inline: MTIzNDU2Nzg5QUJDREUwMTINDU2Nzg5QUJjZGVm|2^20|1:4".AsSpan())); } } } diff --git a/test/unit/net/SDP/SDPMediaAnnouncementUnitTests.cs b/test/unit/net/SDP/SDPMediaAnnouncementUnitTests.cs index 92b856b21f..284d88dfb2 100644 --- a/test/unit/net/SDP/SDPMediaAnnouncementUnitTests.cs +++ b/test/unit/net/SDP/SDPMediaAnnouncementUnitTests.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Net; using Microsoft.Extensions.Logging; -using SIPSorcery.UnitTests; +using SIPSorcery.UnitTests; using Xunit; namespace SIPSorcery.Net.UnitTests @@ -23,8 +23,8 @@ public SDPMediaAnnouncementUnitTests(Xunit.Abstractions.ITestOutputHelper output [Fact] public void InvalidPortInRemoteOfferTest() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var remoteOffer = new SDP(); diff --git a/test/unit/net/SDP/SDPSecurityDescriptionUnitTest.cs b/test/unit/net/SDP/SDPSecurityDescriptionUnitTest.cs index 463e12e993..c3a3c1a7b0 100644 --- a/test/unit/net/SDP/SDPSecurityDescriptionUnitTest.cs +++ b/test/unit/net/SDP/SDPSecurityDescriptionUnitTest.cs @@ -26,10 +26,10 @@ public void ParseTest() logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); - SDPSecurityDescription c1 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz|2^20|1:4 FEC_ORDER=FEC_SRTP"); + SDPSecurityDescription c1 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz|2^20|1:4 FEC_ORDER=FEC_SRTP".AsSpan()); Assert.Equal("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz|2^20|1:4 FEC_ORDER=FEC_SRTP", c1.ToString()); - Assert.Equal(SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz|2^20|1:4 FEC_ORDER=FEC_SRTP").ToString(), c1.ToString()); - Assert.Equal(SDPSecurityDescription.Parse(c1.ToString()).ToString(), c1.ToString()); + Assert.Equal(SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz|2^20|1:4 FEC_ORDER=FEC_SRTP".AsSpan()).ToString(), c1.ToString()); + Assert.Equal(SDPSecurityDescription.Parse(c1.ToString().AsSpan()).ToString(), c1.ToString()); Assert.Equal(1u, c1.Tag); Assert.Equal(4u, c1.KeyParams[0].MkiLength); Assert.Equal(1u, c1.KeyParams[0].MkiValue); @@ -38,7 +38,7 @@ public void ParseTest() Assert.Equal("WVNfX19zZW1jdGwgKCkgewkyMjA7fQp9CnVubGVz", c1.KeyParams[0].KeySaltBase64); Assert.Equal("FEC_ORDER=FEC_SRTP", c1.SessionParam.ToString()); - SDPSecurityDescription c2 = SDPSecurityDescription.Parse("a=crypto:2 F8_128_HMAC_SHA1_80 inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4;inline:QUJjZGVmMTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5|2^20|2:4 FEC_ORDER=FEC_SRTP"); + SDPSecurityDescription c2 = SDPSecurityDescription.Parse("a=crypto:2 F8_128_HMAC_SHA1_80 inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4;inline:QUJjZGVmMTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5|2^20|2:4 FEC_ORDER=FEC_SRTP".AsSpan()); Assert.Equal("a=crypto:2 F8_128_HMAC_SHA1_80 inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4;inline:QUJjZGVmMTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5|2^20|2:4 FEC_ORDER=FEC_SRTP", c2.ToString()); Assert.Equal(2, c2.KeyParams.Count); Assert.Equal(2u, c2.Tag); @@ -54,10 +54,10 @@ public void ParseTest() Assert.Equal("QUJjZGVmMTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5", c2.KeyParams[1].KeySaltBase64); Assert.Equal("FEC_ORDER=FEC_SRTP", c2.SessionParam.ToString()); - SDPSecurityDescription c3 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4"); + SDPSecurityDescription c3 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4".AsSpan()); Assert.Equal("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4", c3.ToString()); - Assert.Equal(SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4").ToString(), c3.ToString()); - Assert.Equal(SDPSecurityDescription.Parse(c3.ToString()).ToString(), c3.ToString()); + Assert.Equal(SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4".AsSpan()).ToString(), c3.ToString()); + Assert.Equal(SDPSecurityDescription.Parse(c3.ToString().AsSpan()).ToString(), c3.ToString()); Assert.Equal(1u, c3.Tag); Assert.Equal(4u, c3.KeyParams[0].MkiLength); Assert.Equal(1u, c3.KeyParams[0].MkiValue); @@ -67,7 +67,7 @@ public void ParseTest() Assert.Null(c3.SessionParam); // Simple inline key without optional parameters (RFC 4568 basic example) - SDPSecurityDescription c4 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR"); + SDPSecurityDescription c4 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR".AsSpan()); Assert.Equal("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR", c4.ToString()); Assert.Equal(1u, c4.Tag); Assert.Equal(0u, c4.KeyParams[0].MkiLength); @@ -77,27 +77,27 @@ public void ParseTest() Assert.Null(c4.SessionParam); // Lifetime without MKI - SDPSecurityDescription c5 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20"); + SDPSecurityDescription c5 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20".AsSpan()); Assert.Equal("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20", c5.ToString()); Assert.Equal((ulong)Math.Pow(2, 20), c5.KeyParams[0].LifeTime); Assert.Equal("2^20", c5.KeyParams[0].LifeTimeString); Assert.Equal(0u, c5.KeyParams[0].MkiLength); // MKI without lifetime - SDPSecurityDescription c6 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|1:32"); + SDPSecurityDescription c6 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|1:32".AsSpan()); Assert.Equal("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|1:32", c6.ToString()); Assert.Equal(32u, c6.KeyParams[0].MkiLength); Assert.Equal(1u, c6.KeyParams[0].MkiValue); Assert.Equal(0ul, c6.KeyParams[0].LifeTime); // AES_256_CM_HMAC_SHA1_80 crypto suite - SDPSecurityDescription c7 = SDPSecurityDescription.Parse("a=crypto:1 AES_256_CM_HMAC_SHA1_80 inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj/Jl+xZQ4qrEUAzDN67dOAQ=="); + SDPSecurityDescription c7 = SDPSecurityDescription.Parse("a=crypto:1 AES_256_CM_HMAC_SHA1_80 inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj/Jl+xZQ4qrEUAzDN67dOAQ==".AsSpan()); Assert.Equal(SDPSecurityDescription.CryptoSuites.AES_256_CM_HMAC_SHA1_80, c7.CryptoSuite); Assert.NotNull(c7.KeyParams[0].Key); Assert.NotNull(c7.KeyParams[0].Salt); // AEAD_AES_256_GCM crypto suite (different salt offset) - needs 32 byte key + 12 byte salt = 44 bytes - SDPSecurityDescription c8 = SDPSecurityDescription.Parse("a=crypto:1 AEAD_AES_256_GCM inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj/Jl+xZQ4qrEUAzDN67dOAUm8tQ=="); + SDPSecurityDescription c8 = SDPSecurityDescription.Parse("a=crypto:1 AEAD_AES_256_GCM inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj/Jl+xZQ4qrEUAzDN67dOAUm8tQ==".AsSpan()); Assert.Equal(SDPSecurityDescription.CryptoSuites.AEAD_AES_256_GCM, c8.CryptoSuite); Assert.NotNull(c8.KeyParams[0].Key); Assert.Equal(32, c8.KeyParams[0].Key.Length); @@ -105,58 +105,58 @@ public void ParseTest() Assert.Equal(12, c8.KeyParams[0].Salt.Length); // Numeric lifetime format (instead of 2^n) - SDPSecurityDescription c9 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|1048576"); + SDPSecurityDescription c9 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|1048576".AsSpan()); Assert.Equal((ulong)Math.Pow(2, 20), c9.KeyParams[0].LifeTime); // Different session parameters - KDR - SDPSecurityDescription c10 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR KDR=10"); + SDPSecurityDescription c10 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR KDR=10".AsSpan()); Assert.NotNull(c10.SessionParam); Assert.Equal(SDPSecurityDescription.SessionParameter.SrtpSessionParams.kdr, c10.SessionParam.SrtpSessionParam); Assert.Equal(10ul, c10.SessionParam.Kdr); // Different session parameters - WSH - SDPSecurityDescription c11 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR WSH=128"); + SDPSecurityDescription c11 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR WSH=128".AsSpan()); Assert.NotNull(c11.SessionParam); Assert.Equal(SDPSecurityDescription.SessionParameter.SrtpSessionParams.wsh, c11.SessionParam.SrtpSessionParam); Assert.Equal(128ul, c11.SessionParam.Wsh); // Different session parameters - UNENCRYPTED_SRTP - SDPSecurityDescription c12 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR UNENCRYPTED_SRTP"); + SDPSecurityDescription c12 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR UNENCRYPTED_SRTP".AsSpan()); Assert.NotNull(c12.SessionParam); Assert.Equal(SDPSecurityDescription.SessionParameter.SrtpSessionParams.UNENCRYPTED_SRTP, c12.SessionParam.SrtpSessionParam); // Different session parameters - UNENCRYPTED_SRTCP - SDPSecurityDescription c13 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR UNENCRYPTED_SRTCP"); + SDPSecurityDescription c13 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR UNENCRYPTED_SRTCP".AsSpan()); Assert.NotNull(c13.SessionParam); Assert.Equal(SDPSecurityDescription.SessionParameter.SrtpSessionParams.UNENCRYPTED_SRTCP, c13.SessionParam.SrtpSessionParam); // Different session parameters - UNAUTHENTICATED_SRTP - SDPSecurityDescription c14 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR UNAUTHENTICATED_SRTP"); + SDPSecurityDescription c14 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR UNAUTHENTICATED_SRTP".AsSpan()); Assert.NotNull(c14.SessionParam); Assert.Equal(SDPSecurityDescription.SessionParameter.SrtpSessionParams.UNAUTHENTICATED_SRTP, c14.SessionParam.SrtpSessionParam); // High tag value (edge case) - SDPSecurityDescription c15 = SDPSecurityDescription.Parse("a=crypto:999999999 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR"); + SDPSecurityDescription c15 = SDPSecurityDescription.Parse("a=crypto:999999999 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR".AsSpan()); Assert.Equal(999999999u, c15.Tag); // Different lifetime exponents - SDPSecurityDescription c16 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^48"); + SDPSecurityDescription c16 = SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^48".AsSpan()); Assert.Equal((ulong)Math.Pow(2, 48), c16.KeyParams[0].LifeTime); Assert.Equal("2^48", c16.KeyParams[0].LifeTimeString); // AES_192_CM_HMAC_SHA1_80 crypto suite - SDPSecurityDescription c17 = SDPSecurityDescription.Parse("a=crypto:1 AES_192_CM_HMAC_SHA1_80 inline:0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123"); + SDPSecurityDescription c17 = SDPSecurityDescription.Parse("a=crypto:1 AES_192_CM_HMAC_SHA1_80 inline:0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123".AsSpan()); Assert.Equal(SDPSecurityDescription.CryptoSuites.AES_192_CM_HMAC_SHA1_80, c17.CryptoSuite); Assert.Null(SDPSecurityDescription.Parse(null)); - Assert.Null(SDPSecurityDescription.Parse("")); - - Assert.Throws(() => SDPSecurityDescription.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4;inline:QUJjZGVmMTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5|2^20|2:4")); - Assert.Throws(() => SDPSecurityDescription.Parse("a=crypto: AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4")); - Assert.Throws(() => SDPSecurityDescription.Parse("a=crypto:1 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4")); - Assert.Throws(() => SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 ")); - Assert.Throws(() => SDPSecurityDescription.Parse("a=crypto:1 AES_CM_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4")); - Assert.Throws(() => SDPSecurityDescription.Parse("a=crypto:1 1 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4")); + Assert.Null(SDPSecurityDescription.Parse("".AsSpan())); + + Assert.Throws(() => SDPSecurityDescription.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4;inline:QUJjZGVmMTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5|2^20|2:4".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.Parse("a=crypto: AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.Parse("a=crypto:1 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.Parse("a=crypto:1 AES_CM_128_HMAC_SHA1_80 ".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.Parse("a=crypto:1 AES_CM_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.Parse("a=crypto:1 1 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:4".AsSpan())); } [Fact] @@ -211,7 +211,7 @@ public void ParseCryptoSIPMessage() Assert.Equal(SIPMethodsEnum.INVITE, sipRequest.Method); Assert.Equal(SIPProtocolsEnum.tls, sipRequest.URI.Protocol); - SDP sdp = SDP.ParseSDPDescription(sipRequest.Body); + SDP sdp = SDP.ParseSDPDescription(sipRequest.Body.AsSpan()); Assert.NotNull(sdp); //Assert.Equal("10.2.19.102", sdp.Connection.ConnectionAddress); Assert.Equal("-", sdp.Username); diff --git a/test/unit/net/SDP/SDPUnitTests.cs b/test/unit/net/SDP/SDPUnitTests.cs index 5ac85218f1..d9c3eb0ed9 100755 --- a/test/unit/net/SDP/SDPUnitTests.cs +++ b/test/unit/net/SDP/SDPUnitTests.cs @@ -9,6 +9,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -43,7 +44,7 @@ public void ParseSDPUnitTest() string sdpStr = $"v=0{m_CRLF}o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF}s=session{m_CRLF}c=IN IP4 10.0.0.4{m_CRLF}t=0 0{m_CRLF}m=audio 12228 RTP/AVP 0 101{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=rtpmap:101 telephone-event/8000{m_CRLF}a=fmtp:101 0-16{m_CRLF}a=silenceSupp:off - - - -{m_CRLF}a=ptime:20{m_CRLF}a=sendrecv"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); @@ -65,7 +66,7 @@ public void ParseBriaSDPUnitTest() string sdpStr = $"v=0o=- 5 2 IN IP4 10.1.1.2{m_CRLF}s=CounterPath Bria{m_CRLF}c=IN IP4 144.137.16.240{m_CRLF}t=0 0{m_CRLF}m=audio 34640 RTP/AVP 0 8 101{m_CRLF}a=sendrecv{m_CRLF}a=rtpmap:101 telephone-event/8000{m_CRLF}a=fmtp:101 0-15{m_CRLF}a=alt:1 1 : STu/ZtOu 7hiLQmUp 10.1.1.2 34640"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); @@ -87,7 +88,7 @@ public void ParseTelephoneEventSDPUnitTest() string sdpStr = $" v=0{m_CRLF} o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF} s=session{m_CRLF} c=IN IP4 10.0.0.4{m_CRLF} t=0 0{m_CRLF} m=audio 12228 RTP/AVP 0 101{m_CRLF} a=rtpmap:0 PCMU/8000{m_CRLF} a=rtpmap:101 telephone-event/8000{m_CRLF} a=fmtp:101 0-16{m_CRLF} a=silenceSupp:off - - - -{m_CRLF} a=ptime:20{m_CRLF} a=sendrecv"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); @@ -109,7 +110,7 @@ public void ParseBadFormatBriaSDPUnitTest() logger.BeginScope(TestHelper.GetCurrentMethodName()); string sdpStr = " v=0\r\no=- 5 2 IN IP4 10.1.1.2\r\n s=CounterPath Bria\r\nc=IN IP4 144.137.16.240\r\nt=0 0\r\n m=audio 34640 RTP/AVP 0 8 101\r\na=sendrecv\r\na=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-15\r\na=alt:1 1 : STu/ZtOu 7hiLQmUp 10.1.1.2 34640\r\n"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Debug.WriteLine(sdp.ToString()); @@ -126,7 +127,7 @@ public void ParseICESessionAttributesUnitTest() string sdpStr = $"v=0{m_CRLF}o=jdoe 2890844526 2890842807 IN IP4 10.0.1.1{m_CRLF}s={m_CRLF}c=IN IP4 192.0.2.3{m_CRLF}t=0 0{m_CRLF}a=ice-pwd:asd88fgpdd777uzjYhagZg{m_CRLF}a=ice-ufrag:8hhY{m_CRLF}m=audio 45664 RTP/AVP 0{m_CRLF}b=RS:0{m_CRLF}b=RR:0{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=candidate:1 1 UDP 2130706431 10.0.1.1 8998 typ host{m_CRLF}a=candidate:2 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr 10.0.1.1 rport 8998"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Debug.WriteLine(sdp.ToString()); @@ -146,7 +147,7 @@ public void ParseMultipleMediaAnnouncementsUnitTest() string sdpStr = $"v=0{m_CRLF}o=- 13064410510996677 3 IN IP4 10.1.1.2{m_CRLF}s=Bria 4 release 4.1.1 stamp 74246{m_CRLF}c=IN IP4 10.1.1.2{m_CRLF}b=AS:2064{m_CRLF}t=0 0{m_CRLF}m=audio 49290 RTP/AVP 0{m_CRLF}a=sendrecv{m_CRLF}m=video 56674 RTP/AVP 96{m_CRLF}b=TIAS:2000000{m_CRLF}a=rtpmap:96 VP8/90000{m_CRLF}a=sendrecv{m_CRLF}a=rtcp-fb:* nack pli{m_CRLF}m=text 60216 RTP/AVP 98{m_CRLF}mid:1{m_CRLF}a=rtpmap:98 T140/1000{m_CRLF}a=sendrecv{m_CRLF}a=ssrc:1679134341 cname:de431dae-58f3-4191-9efe-5d86c1235b60"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Debug.WriteLine(sdp.ToString()); @@ -167,7 +168,7 @@ public void ParseAudioAndVideoConnectionsUnitTest() string sdpStr = $"v=0{m_CRLF}o=Cisco-SIPUA 6396 0 IN IP4 101.180.234.134{m_CRLF}s=SIP Call{m_CRLF}t=0 0{m_CRLF}m=audio 19586 RTP/AVP 0{m_CRLF}c=IN IP4 101.180.234.134{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=sendrecv{m_CRLF}m=video 0 RTP/AVP 96{m_CRLF}c=IN IP4 10.0.0.10{m_CRLF}m=text 11000 RTP/AVP 98 100{m_CRLF}a=rtpmap:98 t140/1000{m_CRLF}a=fmtp:100 98/98"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); @@ -188,7 +189,7 @@ public void ParseMediaTypeImageUnitTest() string sdpStr = $"v=0{m_CRLF}o=OfficeMasterDirectSIP 806542878 806542879 IN IP4 10.2.0.110{m_CRLF}s=FOIP Call{m_CRLF}c=IN IP4 10.2.0.110{m_CRLF}t=0 0{m_CRLF}m=image 50594 udptl t38{m_CRLF}a=T38FaxRateManagement:transferredTCF{m_CRLF}a=T38FaxVersion:0"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); @@ -210,7 +211,7 @@ public void ParseEdgeBrowserSdpUnitTest() string sdpStr = $"v=0{m_CRLF}o=- 8028343537520473029 0 IN IP4 127.0.0.1{m_CRLF}s=-{m_CRLF}t=0 0{m_CRLF}a=msid-semantic: WMS{m_CRLF}a=group:BUNDLE audio{m_CRLF}m=audio 7038 UDP/TLS/RTP/SAVPF 0{m_CRLF}c=IN IP4 10.0.75.1{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=rtcp:9 IN IP4 0.0.0.0{m_CRLF}a=setup:active{m_CRLF}a=mid:audio{m_CRLF}a=maxptime:60{m_CRLF}a=recvonly{m_CRLF}a=ice-ufrag:1Fs+{m_CRLF}a=ice-pwd:oiLbCgce1c9xzyamdrWtn9Q/{m_CRLF}a=fingerprint:sha-256 B0:1F:2C:72:8F:1A:14:CD:92:15:47:F0:C3:0A:69:F9:A9:43:35:EE:10:CB:F0:11:18:B8:0E:F9:A6:95:5F:B1{m_CRLF}a=candidate:1 1 udp 2130706431 10.0.75.1 7038 typ host{m_CRLF}a=candidate:2 1 udp 2130705919 172.22.240.1 31136 typ host{m_CRLF}a=candidate:3 1 udp 2130705407 172.22.48.1 21390 typ host{m_CRLF}a=candidate:4 1 udp 2130704895 192.168.11.50 26878 typ host{m_CRLF}a=candidate:5 1 tcp 1684797439 10.0.75.1 7038 typ srflx raddr 10.0.75.1 rport 7038 tcptype active{m_CRLF}a=rtcp-mux{m_CRLF}m=video 0 UDP/TLS/RTP/SAVPF{m_CRLF}c=IN IP4 0.0.0.0{m_CRLF}a=inactive"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Debug.WriteLine(sdp.ToString()); @@ -232,7 +233,7 @@ public void ParseIPv6SDPUnitTest() string sdpStr = $"v=0{m_CRLF}o=nasa1 971731711378798081 0 IN IP6 2201:056D::112E:144A:1E24{m_CRLF}s=(Almost) live video feed from Mars-II satellite{m_CRLF}p=+1 713 555 1234{m_CRLF}c=IN IP6 FF1E:03AD::7F2E:172A:1E24{m_CRLF}t=3338481189 3370017201{m_CRLF}m=audio 6000 RTP/AVP 2{m_CRLF}a=rtpmap:2 G726-32/8000{m_CRLF}m=video 6024 RTP/AVP 107{m_CRLF}a=rtpmap:107 H263-1998/90000"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); @@ -287,7 +288,7 @@ public void GetFirstMediaSteamStatusUnitTest() string sdpStr = $"v=0{m_CRLF}o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF}s=session{m_CRLF}c=IN IP4 10.0.0.4{m_CRLF}t=0 0{m_CRLF}m=audio 12228 RTP/AVP 0 101{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=rtpmap:101 telephone-event/8000{m_CRLF}a=fmtp:101 0-16{m_CRLF}a=silenceSupp:off - - - -{m_CRLF}a=ptime:20{m_CRLF}a=sendrecv"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Assert.Equal(MediaStreamStatusEnum.SendRecv, sdp.Media.First().MediaStreamStatus); } @@ -305,7 +306,7 @@ public void GetFirstMediaSteamStatusNonDefaultUnitTest() string sdpStr = $"v=0{m_CRLF}o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF}s=session{m_CRLF}c=IN IP4 10.0.0.4{m_CRLF}t=0 0{m_CRLF}m=audio 12228 RTP/AVP 0 101{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=rtpmap:101 telephone-event/8000{m_CRLF}a=fmtp:101 0-16{m_CRLF}a=silenceSupp:off - - - -{m_CRLF}a=ptime:20{m_CRLF}a=sendonly"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Assert.Equal(MediaStreamStatusEnum.SendOnly, sdp.Media.First().MediaStreamStatus); } @@ -322,7 +323,7 @@ public void GetSessionMediaSteamStatusUnitTest() string sdpStr = $"v=0{m_CRLF}o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF}s=session{m_CRLF}c=IN IP4 10.0.0.4{m_CRLF}t=0 0{m_CRLF}a=recvonly{m_CRLF}m=audio 12228 RTP/AVP 0 101{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=rtpmap:101 telephone-event/8000{m_CRLF}a=fmtp:101 0-16{m_CRLF}a=silenceSupp:off - - - -{m_CRLF}a=ptime:20"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Assert.Equal(MediaStreamStatusEnum.RecvOnly, sdp.SessionMediaStreamStatus); Assert.Equal(MediaStreamStatusEnum.RecvOnly, sdp.Media.First().MediaStreamStatus); @@ -341,7 +342,7 @@ public void GetAnnMediaSteamDiffToStreamStatusUnitTest() string sdpStr = $"v=0{m_CRLF}o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF}s=session{m_CRLF}c=IN IP4 10.0.0.4{m_CRLF}t=0 0{m_CRLF}a=recvonly{m_CRLF}m=audio 12228 RTP/AVP 0 101{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=rtpmap:101 telephone-event/8000{m_CRLF}a=fmtp:101 0-16{m_CRLF}a=silenceSupp:off - - - -{m_CRLF}a=ptime:20{m_CRLF}a=sendonly"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Assert.Equal(MediaStreamStatusEnum.RecvOnly, sdp.SessionMediaStreamStatus); Assert.Equal(MediaStreamStatusEnum.SendOnly, sdp.Media.First().MediaStreamStatus); @@ -360,7 +361,7 @@ public void GetAnnMediaSteamNotreamStatusAttributesUnitTest() string sdpStr = $"v=0{m_CRLF}o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF}s=session{m_CRLF}c=IN IP4 10.0.0.4{m_CRLF}t=0 0{m_CRLF}m=audio 12228 RTP/AVP 0 101{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=rtpmap:101 telephone-event/8000{m_CRLF}a=fmtp:101 0-16{m_CRLF}a=silenceSupp:off - - - -{m_CRLF}a=ptime:20"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Assert.Null(sdp.SessionMediaStreamStatus); Assert.Equal(MediaStreamStatusEnum.SendRecv, sdp.Media.First().MediaStreamStatus); @@ -378,11 +379,11 @@ public void AnnouncementMediaSteamStatuRoundtripUnitTest() string sdpStr = $"v=0{m_CRLF}o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF}s=session{m_CRLF}c=IN IP4 10.0.0.4{m_CRLF}t=0 0{m_CRLF}m=audio 12228 RTP/AVP 0 101{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=rtpmap:101 telephone-event/8000{m_CRLF}a=fmtp:101 0-16{m_CRLF}a=silenceSupp:off - - - -{m_CRLF}a=ptime:20{m_CRLF}a=sendonly"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP sdpRoundTrip = SDP.ParseSDPDescription(sdp.ToString()); + SDP sdpRoundTrip = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal(MediaStreamStatusEnum.SendOnly, sdpRoundTrip.Media.First().MediaStreamStatus); } @@ -399,11 +400,11 @@ public void SessionMediaSteamStatusRoundTripUnitTest() string sdpStr = $"v=0{m_CRLF}o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF}s=session{m_CRLF}c=IN IP4 10.0.0.4{m_CRLF}t=0 0{m_CRLF}a=recvonly{m_CRLF}m=audio 12228 RTP/AVP 0 101{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=rtpmap:101 telephone-event/8000{m_CRLF}a=fmtp:101 0-16{m_CRLF}a=silenceSupp:off - - - -{m_CRLF}a=ptime:20"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP sdpRoundTrip = SDP.ParseSDPDescription(sdp.ToString()); + SDP sdpRoundTrip = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal(MediaStreamStatusEnum.RecvOnly, sdpRoundTrip.SessionMediaStreamStatus); } @@ -449,11 +450,11 @@ public void ParseWebRtcSDPUnitTest() a=mid:video a=rtpmap:100 VP8/90000"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString()); + SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal("BUNDLE audio video", sdp.Group); Assert.Equal("BUNDLE audio video", rndTripSdp.Group); @@ -645,11 +646,11 @@ public void ParseChromeOfferSDPUnitTest() a=ssrc:1316862390 label:e9e6e397-1589-4df3-bd6a-53124925325a a=sendrecv"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString()); + SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal("BUNDLE 0 1", sdp.Group); Assert.Equal("BUNDLE 0 1", rndTripSdp.Group); @@ -690,11 +691,11 @@ public void ParseDataChannelOnlyOfferSDPUnitTest() a=sctp-port:5000 a=max-message-size:262144"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString()); + SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal("BUNDLE 0", rndTripSdp.Group); Assert.Single(rndTripSdp.Media); @@ -728,11 +729,11 @@ public void ParsePionDataChannelOnlyOfferSDPUnitTest() a=sendrecv a=sctpmap:5000 webrtc-datachannel 1024"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString()); + SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal("BUNDLE 0", rndTripSdp.Group); Assert.Single(rndTripSdp.Media); @@ -766,11 +767,11 @@ public void ParseMediaFormatWithHyphenNameUnitTest() a=fmtp:96 QCIF=3 a=sendrecv"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString()); + SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal(96, rndTripSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.video).Single().MediaFormats.Single().Key); Assert.Equal("H263-1998", rndTripSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.video).Single().MediaFormats.Single().Value.Name()); @@ -797,11 +798,11 @@ public void ParseMediaFormatWithFowardSlashUnitTest() a=fmtp:111 minptime=10;useinbandfec=1 a=sendrecv"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString()); + SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal(111, rndTripSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.audio).Single().MediaFormats.Single().Key); Assert.Equal("opus", rndTripSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.audio).Single().MediaFormats.Single().Value.Name()); @@ -839,7 +840,7 @@ public void ParseOfferWithFmtpPreceedingRtmapTest() a=setup:active a=ssrc:2404235415 cname:{7c06c5db-d3db-4891-b729-df4919014c3f}"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Assert.Equal(96, sdp.Media.Where(x => x.Media == SDPMediaTypesEnum.video).Single().MediaFormats.Single().Key); Assert.Equal("VP8", sdp.Media.Where(x => x.Media == SDPMediaTypesEnum.video).Single().MediaFormats.Single().Value.Name()); @@ -872,11 +873,11 @@ public void ParseMcpttTest() m=application 55317 udp MCPTT a=fmtp:MCPTT mc_queueing;mc_priority=4"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString()); + SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal("MCPTT", rndTripSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.application).Single().ApplicationMediaFormats.Single().Key); Assert.Equal("mc_queueing;mc_priority=4", rndTripSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.application).Single().ApplicationMediaFormats.Single().Value.Fmtp); @@ -913,11 +914,11 @@ public void DescriptionAttributeRoundTripTest() a=sendrecv "; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString()); + SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal("A session description", rndTripSdp.SessionDescription); Assert.Equal("speech", rndTripSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.audio).Single().MediaDescription); @@ -953,11 +954,11 @@ public void TIASBandwidthAttributeRoundTripTest() a=sendrecv "; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString()); + SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal(256000U, rndTripSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.video).Single().TIASBandwidth); } @@ -1036,11 +1037,11 @@ public void ParseFireFoxOfferSDPUnitTest() a=ssrc:777490417 cname:{7eee5d94-87f0-4e5e-a6ae-ac6f067c4782} a=ssrc-group:FID 3366495178 777490417"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug("{sdp}", sdp.ToString()); - SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString()); + SDP rndTripSdp = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal("BUNDLE 0", sdp.Group); Assert.Equal("BUNDLE 0", rndTripSdp.Group); @@ -1065,7 +1066,7 @@ public void AnnoucementMediaCheckTest() string sdpStr = $"v=0{m_CRLF}o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF}s=session{m_CRLF}c=IN IP4 10.0.0.4{m_CRLF}t=0 0{m_CRLF}m=message 57102 TCP/MSRP *{m_CRLF}a=accept-types:text/plain text/x-msrp-heartbeat{m_CRLF}a=path:msrp://192.168.0.105:57102/10vMB2Ee;tcp"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); Assert.Null(sdp.SessionMediaStreamStatus); Assert.Equal(SDPMediaTypesEnum.message, sdp.Media.First().Media); @@ -1100,7 +1101,7 @@ public void Media_Formats_Order_Test() logger.LogDebug("{sdp}", sdp.ToString()); - var sdpParsed = SDP.ParseSDPDescription(sdp.ToString()); + var sdpParsed = SDP.ParseSDPDescription(sdp.ToString().AsSpan()); Assert.Equal(8, sdpParsed.Media.Where(x => x.Media == SDPMediaTypesEnum.audio).First().MediaFormats.First().Key); } @@ -1120,7 +1121,7 @@ public void Parse_Number_Of_Ports_Unit_Test() string sdpStr = $"v=0{m_CRLF}o=root 3285 3285 IN IP4 10.0.0.4{m_CRLF}s=session{m_CRLF}c=IN IP4 10.0.0.4{m_CRLF}t=0 0{m_CRLF}m=audio 12228/2 RTP/AVP 0 101{m_CRLF}a=rtpmap:0 PCMU/8000{m_CRLF}a=rtpmap:101 telephone-event/8000{m_CRLF}a=fmtp:101 0-16{m_CRLF}a=silenceSupp:off - - - -{m_CRLF}a=ptime:20{m_CRLF}a=sendrecv"; - SDP sdp = SDP.ParseSDPDescription(sdpStr); + SDP sdp = SDP.ParseSDPDescription(sdpStr.AsSpan()); logger.LogDebug(sdp.ToString()); diff --git a/test/unit/net/SDP/SessionParameterUnitTest.cs b/test/unit/net/SDP/SessionParameterUnitTest.cs index 6b2fb38d40..44389ae850 100644 --- a/test/unit/net/SDP/SessionParameterUnitTest.cs +++ b/test/unit/net/SDP/SessionParameterUnitTest.cs @@ -149,26 +149,26 @@ public void ParseTest() SDPSecurityDescription.SessionParameter sessionParameterKdr = SessionParameterFactory.Create(SDPSecurityDescription.SessionParameter.SrtpSessionParams.kdr, 4); string sKdr = sessionParameterKdr.ToString(); - Assert.Equal(sKdr, SDPSecurityDescription.SessionParameter.Parse(sKdr).ToString()); - Assert.Equal("KDR=4", SDPSecurityDescription.SessionParameter.Parse(sKdr).ToString()); + Assert.Equal(sKdr, SDPSecurityDescription.SessionParameter.Parse(sKdr.AsSpan()).ToString()); + Assert.Equal("KDR=4", SDPSecurityDescription.SessionParameter.Parse(sKdr.AsSpan()).ToString()); SDPSecurityDescription.SessionParameter sessionParameterWsh = SessionParameterFactory.Create(SDPSecurityDescription.SessionParameter.SrtpSessionParams.wsh, 64); string sWsh = sessionParameterWsh.ToString(); - Assert.Equal(sWsh, SDPSecurityDescription.SessionParameter.Parse(sWsh).ToString()); - Assert.Equal("WSH=64", SDPSecurityDescription.SessionParameter.Parse(sWsh).ToString()); + Assert.Equal(sWsh, SDPSecurityDescription.SessionParameter.Parse(sWsh.AsSpan()).ToString()); + Assert.Equal("WSH=64", SDPSecurityDescription.SessionParameter.Parse(sWsh.AsSpan()).ToString()); SDPSecurityDescription.SessionParameter sessionParameterFecOrder = SessionParameterFactory.Create(SDPSecurityDescription.SessionParameter.SrtpSessionParams.fec_order, (uint)SDPSecurityDescription.SessionParameter.FecTypes.FEC_SRTP); string sFecOrder = sessionParameterFecOrder.ToString(); - Assert.Equal(sFecOrder, SDPSecurityDescription.SessionParameter.Parse(sFecOrder).ToString()); - Assert.Equal(sFecOrder, SDPSecurityDescription.SessionParameter.Parse("FEC_ORDER=FEC_SRTP").ToString()); + Assert.Equal(sFecOrder, SDPSecurityDescription.SessionParameter.Parse(sFecOrder.AsSpan()).ToString()); + Assert.Equal(sFecOrder, SDPSecurityDescription.SessionParameter.Parse("FEC_ORDER=FEC_SRTP".AsSpan()).ToString()); sessionParameterFecOrder.FecOrder = SDPSecurityDescription.SessionParameter.FecTypes.SRTP_FEC; Assert.NotEqual(sFecOrder, sessionParameterFecOrder.ToString()); sFecOrder = sessionParameterFecOrder.ToString(); - Assert.Equal(sFecOrder, SDPSecurityDescription.SessionParameter.Parse(sFecOrder).ToString()); + Assert.Equal(sFecOrder, SDPSecurityDescription.SessionParameter.Parse(sFecOrder.AsSpan()).ToString()); SDPSecurityDescription.SessionParameter sessionParameterFecKey = SessionParameterFactory.Create(SDPSecurityDescription.SessionParameter.SrtpSessionParams.fec_key); - sessionParameterFecKey.FecKey = SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4"); + sessionParameterFecKey.FecKey = SDPSecurityDescription.KeyParameter.Parse("inline:MTIzNDU2Nzg5QUJDREUwMTIzNDU2Nzg5QUJjZGVm|2^20|1:4".AsSpan()); string FecKey = sessionParameterFecKey.ToString(); Assert.StartsWith(SDPSecurityDescription.SessionParameter.FEC_KEY_PREFIX, FecKey); Assert.EndsWith("1:4", FecKey); @@ -179,15 +179,15 @@ public void ParseTest() string sUn1 = sessionParameterUn1.ToString(); string sUn2 = sessionParameterUn2.ToString(); string sUn3 = sessionParameterUn3.ToString(); - Assert.Equal(sUn1, SDPSecurityDescription.SessionParameter.Parse(sUn1).ToString()); - Assert.NotEqual(SDPSecurityDescription.SessionParameter.SrtpSessionParams.UNENCRYPTED_SRTP, SDPSecurityDescription.SessionParameter.Parse(sUn2).SrtpSessionParam); - Assert.Equal(SDPSecurityDescription.SessionParameter.SrtpSessionParams.UNENCRYPTED_SRTP, SDPSecurityDescription.SessionParameter.Parse(sUn3).SrtpSessionParam); + Assert.Equal(sUn1, SDPSecurityDescription.SessionParameter.Parse(sUn1.AsSpan()).ToString()); + Assert.NotEqual(SDPSecurityDescription.SessionParameter.SrtpSessionParams.UNENCRYPTED_SRTP, SDPSecurityDescription.SessionParameter.Parse(sUn2.AsSpan()).SrtpSessionParam); + Assert.Equal(SDPSecurityDescription.SessionParameter.SrtpSessionParams.UNENCRYPTED_SRTP, SDPSecurityDescription.SessionParameter.Parse(sUn3.AsSpan()).SrtpSessionParam); - Assert.Null(SDPSecurityDescription.SessionParameter.Parse(null)); - Assert.Null(SDPSecurityDescription.SessionParameter.Parse("")); + Assert.Null(SDPSecurityDescription.SessionParameter.Parse(default)); + Assert.Null(SDPSecurityDescription.SessionParameter.Parse("".AsSpan())); - Assert.Throws(() => SDPSecurityDescription.SessionParameter.Parse("wsh=64")); - Assert.Throws(() => SDPSecurityDescription.SessionParameter.Parse("ĀĀ\0\0\0\0\0\0\0\0\0\0\0\0\0\0")); + Assert.Throws(() => SDPSecurityDescription.SessionParameter.Parse("wsh=64".AsSpan())); + Assert.Throws(() => SDPSecurityDescription.SessionParameter.Parse("ĀĀ\0\0\0\0\0\0\0\0\0\0\0\0\0\0".AsSpan())); } } } diff --git a/test/unit/net/STUN/STUNCheckIntegrityUnitTest.cs b/test/unit/net/STUN/STUNCheckIntegrityUnitTest.cs index ea2c54cfc3..d83af1fab5 100644 --- a/test/unit/net/STUN/STUNCheckIntegrityUnitTest.cs +++ b/test/unit/net/STUN/STUNCheckIntegrityUnitTest.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: STUNCheckIntegrityUnitTest.cs // // Description: Unit tests for STUNMessage.CheckIntegrity with and without @@ -8,6 +8,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Text; using Xunit; @@ -31,9 +32,10 @@ public void CheckIntegrityWithoutFingerprint() msg.AddUsernameAttribute("testuser"); // Serialize WITH integrity but WITHOUT fingerprint - var buffer = msg.ToByteBufferStringKey(key, false); + var buffer = new byte[msg.GetByteBufferSizeStringKey(key, false)]; + msg.WriteToBufferStringKey(buffer, key, false); - var parsed = STUNMessage.ParseSTUNMessage(buffer, buffer.Length); + var parsed = STUNMessage.ParseSTUNMessage(buffer); Assert.False(parsed.isFingerprintValid, "No FINGERPRINT was sent"); Assert.True(parsed.CheckIntegrity(Encoding.UTF8.GetBytes(key)), @@ -54,9 +56,10 @@ public void CheckIntegrityWithFingerprint() msg.AddUsernameAttribute("testuser"); // Serialize WITH both integrity and fingerprint - var buffer = msg.ToByteBufferStringKey(key, true); + var buffer = new byte[msg.GetByteBufferSizeStringKey(key, true)]; + msg.WriteToBufferStringKey(buffer, key, true); - var parsed = STUNMessage.ParseSTUNMessage(buffer, buffer.Length); + var parsed = STUNMessage.ParseSTUNMessage(buffer); Assert.True(parsed.isFingerprintValid); Assert.True(parsed.CheckIntegrity(Encoding.UTF8.GetBytes(key))); @@ -69,12 +72,15 @@ public void CheckIntegrityWithFingerprint() [Fact] public void CheckIntegrityFailsWithWrongKey() { + string key = "correctkey"; + var msg = new STUNMessage(STUNMessageTypesEnum.BindingRequest); msg.Header.TransactionId = Encoding.ASCII.GetBytes("abcdefghijkl"); msg.AddUsernameAttribute("testuser"); - var buffer = msg.ToByteBufferStringKey("correctkey", false); - var parsed = STUNMessage.ParseSTUNMessage(buffer, buffer.Length); + var buffer = new byte[msg.GetByteBufferSizeStringKey(key, false)]; + msg.WriteToBufferStringKey(buffer, key, false); + var parsed = STUNMessage.ParseSTUNMessage(buffer); Assert.False(parsed.CheckIntegrity(Encoding.UTF8.GetBytes("wrongkey"))); } diff --git a/test/unit/net/STUN/STUNClientUnitTest.cs b/test/unit/net/STUN/STUNClientUnitTest.cs index cca69bbac3..f675e9cf02 100644 --- a/test/unit/net/STUN/STUNClientUnitTest.cs +++ b/test/unit/net/STUN/STUNClientUnitTest.cs @@ -14,7 +14,7 @@ //----------------------------------------------------------------------------- using Microsoft.Extensions.Logging; -using SIPSorcery.UnitTests; +using SIPSorcery.UnitTests; using Xunit; namespace SIPSorcery.Net.UnitTests @@ -35,8 +35,8 @@ public STUNClientUnitTest(Xunit.Abstractions.ITestOutputHelper output) [Fact(Skip = "STUN server isn't kept running all the time.")] public void GetPublicIPStunClientTestMethod() { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); var publicIP = STUNClient.GetPublicIPAddress("stun.sipsorcery.com"); diff --git a/test/unit/net/STUN/STUNErrorCodeAttributeUnitTest.cs b/test/unit/net/STUN/STUNErrorCodeAttributeUnitTest.cs index 434096d478..8a6ee8975d 100644 --- a/test/unit/net/STUN/STUNErrorCodeAttributeUnitTest.cs +++ b/test/unit/net/STUN/STUNErrorCodeAttributeUnitTest.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: STUNErrorCodeAttributeUnitTest.cs // // Description: Unit tests for the STUNErrorCodeAttribute class. @@ -48,7 +48,7 @@ public void IntStringConstructorPopulatesValueField() { var attr = new STUNErrorCodeAttribute(401, "Unauthorized"); - Assert.NotNull(attr.Value); + Assert.False(attr.Value.IsEmpty); // Value should be: 2 reserved bytes + 1 class + 1 number + reason phrase var expectedLength = 4 + Encoding.UTF8.GetByteCount("Unauthorized"); @@ -70,7 +70,8 @@ public void ErrorResponseSerializesToByteBuffer() msg.Attributes.Add(new STUNErrorCodeAttribute(401, "Unauthorized")); // This previously threw ArgumentException due to null Value / zero PaddedLength - byte[] buffer = msg.ToByteBuffer(null, false); + byte[] buffer = new byte[msg.GetByteBufferSize(null, false)]; + msg.WriteToBuffer(buffer, null, false); Assert.NotNull(buffer); Assert.True(buffer.Length > 20); // At least STUN header + error attribute @@ -87,14 +88,15 @@ public void ErrorAttributeRoundTrips() msg.Header.TransactionId = new byte[12]; msg.Attributes.Add(new STUNErrorCodeAttribute(437, "Allocation Mismatch")); - byte[] buffer = msg.ToByteBuffer(null, false); - var parsed = STUNMessage.ParseSTUNMessage(buffer, buffer.Length); + byte[] buffer = new byte[msg.GetByteBufferSize(null, false)]; + msg.WriteToBuffer(buffer, null, false); + var parsed = STUNMessage.ParseSTUNMessage(buffer); Assert.NotNull(parsed); Assert.Single(parsed.Attributes); Assert.Equal(STUNAttributeTypesEnum.ErrorCode, parsed.Attributes[0].AttributeType); - var errorAttr = new STUNErrorCodeAttribute(parsed.Attributes[0].Value); + var errorAttr = new STUNErrorCodeAttribute(parsed.Attributes[0].Value.ToArray()); Assert.Equal(437, errorAttr.ErrorCode); Assert.Equal(4, errorAttr.ErrorClass); Assert.Equal(37, errorAttr.ErrorNumber); diff --git a/test/unit/net/STUN/STUNFingerprintBufferUnitTest.cs b/test/unit/net/STUN/STUNFingerprintBufferUnitTest.cs index 6de316670c..3feae67b07 100644 --- a/test/unit/net/STUN/STUNFingerprintBufferUnitTest.cs +++ b/test/unit/net/STUN/STUNFingerprintBufferUnitTest.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: STUNFingerprintBufferUnitTest.cs // // Description: Unit tests for ParseSTUNMessage fingerprint validation with @@ -32,45 +32,12 @@ public void FingerprintValidWithExactBuffer() msg.AddUsernameAttribute("xxxx:yyyy"); msg.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Priority, BitConverter.GetBytes(1U))); - var exact = msg.ToByteBufferStringKey(key, true); - var parsed = STUNMessage.ParseSTUNMessage(exact, exact.Length); + var exact = new byte[msg.GetByteBufferSizeStringKey(key, true)]; + msg.WriteToBufferStringKey(exact, key, true); + var parsed = STUNMessage.ParseSTUNMessage(exact); Assert.True(parsed.isFingerprintValid); Assert.True(parsed.CheckIntegrity(Encoding.UTF8.GetBytes(key))); } - - /// - /// Verifies that fingerprint validation works when the message is in an - /// oversized buffer (e.g., a pooled UDP receive buffer). Previously, - /// ParseSTUNMessage used buffer.Length instead of bufferLength for the - /// CRC computation, causing fingerprint validation to fail. - /// - [Fact] - public void FingerprintValidWithOversizedBuffer() - { - string key = "SKYKPPYLTZOAVCLTGHDUODANRKSPOVQVKXJULOGG"; - - var msg = new STUNMessage(STUNMessageTypesEnum.BindingRequest); - msg.Header.TransactionId = Encoding.ASCII.GetBytes("abcdefghijkl"); - msg.AddUsernameAttribute("xxxx:yyyy"); - msg.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Priority, BitConverter.GetBytes(1U))); - - var exact = msg.ToByteBufferStringKey(key, true); - - // Simulate a pooled receive buffer: copy message into a larger array - // with trailing garbage bytes. - var oversized = new byte[exact.Length + 200]; - Buffer.BlockCopy(exact, 0, oversized, 0, exact.Length); - var trailing = new byte[200]; - new Random(42).NextBytes(trailing); - Buffer.BlockCopy(trailing, 0, oversized, exact.Length, trailing.Length); - - var parsed = STUNMessage.ParseSTUNMessage(oversized, exact.Length); - - Assert.True(parsed.isFingerprintValid, - "Fingerprint should be valid even with oversized buffer"); - Assert.True(parsed.CheckIntegrity(Encoding.UTF8.GetBytes(key)), - "Integrity should be valid even with oversized buffer"); - } } } diff --git a/test/unit/net/STUN/STUNUnitTest.cs b/test/unit/net/STUN/STUNUnitTest.cs old mode 100755 new mode 100644 index 0ae2264d57..1d18709cce --- a/test/unit/net/STUN/STUNUnitTest.cs +++ b/test/unit/net/STUN/STUNUnitTest.cs @@ -1,294 +1,291 @@ -//----------------------------------------------------------------------------- -// Author(s): -// Aaron Clauson -// -// History: +//----------------------------------------------------------------------------- +// Author(s): +// Aaron Clauson // +// History: // +// // License: -// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. -//----------------------------------------------------------------------------- - -using System; -using System.Linq; -using System.Net; -using System.Text; -using Microsoft.Extensions.Logging; +// BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. +//----------------------------------------------------------------------------- + +using System; +using System.Buffers.Binary; +using System.Linq; +using System.Net; +using System.Text; +using Microsoft.Extensions.Logging; using SIPSorcery.Sys; -using SIPSorcery.UnitTests; -using Xunit; - -namespace SIPSorcery.Net.UnitTests -{ - [Trait("Category", "unit")] - public class STUNUnitTest - { - private Microsoft.Extensions.Logging.ILogger logger = null; - - public STUNUnitTest(Xunit.Abstractions.ITestOutputHelper output) - { - logger = SIPSorcery.UnitTests.TestLogHelper.InitTestLogger(output); - } - - /// - /// Parse a STUN request received from the Chrome browser's WebRTC stack. - /// - [Fact] - public void ParseWebRTCSTUNRequestTestMethod() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - byte[] stunReq = new byte[]{ 0x00, 0x01, 0x00, 0x60, 0x21, 0x12, 0xa4, 0x42, 0x66, 0x55, 0x55, 0x43, 0x4b, 0x48, 0x74, 0x73, 0x68, 0x4e, 0x71, 0x56, +using SIPSorcery.UnitTests; +using Xunit; + +namespace SIPSorcery.Net.UnitTests +{ + [Trait("Category", "unit")] + public class STUNUnitTest + { + private Microsoft.Extensions.Logging.ILogger logger = null; + + public STUNUnitTest(Xunit.Abstractions.ITestOutputHelper output) + { + logger = SIPSorcery.UnitTests.TestLogHelper.InitTestLogger(output); + } + + /// + /// Parse a STUN request received from the Chrome browser's WebRTC stack. + /// + [Fact] + public void ParseWebRTCSTUNRequestTestMethod() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + byte[] stunReq = new byte[]{ 0x00, 0x01, 0x00, 0x60, 0x21, 0x12, 0xa4, 0x42, 0x66, 0x55, 0x55, 0x43, 0x4b, 0x48, 0x74, 0x73, 0x68, 0x4e, 0x71, 0x56, // Att1: - 0x00, 0x06, 0x00, 0x21, - 0x6d, 0x30, 0x71, 0x47, 0x77, 0x53, 0x71, 0x2f, 0x48, 0x56, 0x48, 0x71, 0x41, 0x62, 0x4b, 0x62, 0x3a, 0x73, 0x64, 0x43, - 0x48, 0x59, 0x6b, 0x35, 0x6e, 0x46, 0x34, 0x79, 0x44, 0x77, 0x55, 0x39, 0x53, 0x00, 0x00, 0x00, - // Att2 - 0x80, 0x2a, 0x00, 0x08, - 0xa0, 0x36, 0xc9, 0x6c, 0x30, 0xc6, 0x2f, 0xd2, 0x00, 0x25, 0x00, 0x00, 0x00, 0x24, 0x00, 0x04, - 0x6e, 0x7f, 0x1e, 0xff, 0x00, 0x08, 0x00, 0x14, 0x81, 0x4a, 0x4f, 0xaf, 0x3d, 0x99, 0x30, 0x67, - 0x66, 0xb9, 0x48, 0x67, 0x83, 0x72, 0xd5, 0xa0, 0x7a, 0x87, 0xb5, 0x3f, 0x80, 0x28, 0x00, 0x04, - 0x49, 0x7e, 0x51, 0x17 }; - - STUNMessage stunMessage = STUNMessage.ParseSTUNMessage(stunReq, stunReq.Length); - STUNHeader stunHeader = stunMessage.Header; - + 0x00, 0x06, 0x00, 0x21, + 0x6d, 0x30, 0x71, 0x47, 0x77, 0x53, 0x71, 0x2f, 0x48, 0x56, 0x48, 0x71, 0x41, 0x62, 0x4b, 0x62, 0x3a, 0x73, 0x64, 0x43, + 0x48, 0x59, 0x6b, 0x35, 0x6e, 0x46, 0x34, 0x79, 0x44, 0x77, 0x55, 0x39, 0x53, 0x00, 0x00, 0x00, + // Att2 + 0x80, 0x2a, 0x00, 0x08, + 0xa0, 0x36, 0xc9, 0x6c, 0x30, 0xc6, 0x2f, 0xd2, 0x00, 0x25, 0x00, 0x00, 0x00, 0x24, 0x00, 0x04, + 0x6e, 0x7f, 0x1e, 0xff, 0x00, 0x08, 0x00, 0x14, 0x81, 0x4a, 0x4f, 0xaf, 0x3d, 0x99, 0x30, 0x67, + 0x66, 0xb9, 0x48, 0x67, 0x83, 0x72, 0xd5, 0xa0, 0x7a, 0x87, 0xb5, 0x3f, 0x80, 0x28, 0x00, 0x04, + 0x49, 0x7e, 0x51, 0x17 }; + + STUNMessage stunMessage = STUNMessage.ParseSTUNMessage(stunReq.AsSpan()); + STUNHeader stunHeader = stunMessage.Header; + logger.LogDebug("Request type = {MessageType}.", stunHeader.MessageType); logger.LogDebug("Length = {MessageLength}.", stunHeader.MessageLength); logger.LogDebug("Transaction ID = {TransactionId}.", BitConverter.ToString(stunHeader.TransactionId)); - - Assert.Equal(STUNMessageTypesEnum.BindingRequest, stunHeader.MessageType); - Assert.Equal(96, stunHeader.MessageLength); - Assert.Equal(6, stunMessage.Attributes.Count); - } - - /// - /// Tests that a binding request with a username attribute is correctly output to a byte array. - /// - [Fact] - public void BindingRequestWithUsernameToBytesUnitTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - STUNMessage initMessage = new STUNMessage(STUNMessageTypesEnum.BindingRequest); - initMessage.AddUsernameAttribute("someusernamex"); - byte[] stunMessageBytes = initMessage.ToByteBuffer(null, false); - + + Assert.Equal(STUNMessageTypesEnum.BindingRequest, stunHeader.MessageType); + Assert.Equal(96, stunHeader.MessageLength); + Assert.Equal(6, stunMessage.Attributes.Count); + } + + /// + /// Tests that a binding request with a username attribute is correctly output to a byte array. + /// + [Fact] + public void BindingRequestWithUsernameToBytesUnitTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + STUNMessage initMessage = new STUNMessage(STUNMessageTypesEnum.BindingRequest); + initMessage.AddUsernameAttribute("someusernamex"); + byte[] stunMessageBytes = new byte[initMessage.GetByteBufferSize(default, false)]; + initMessage.WriteToBuffer(stunMessageBytes, default, false); + logger.LogDebug("STUN message bytes: {StunMessageBytes}", BitConverter.ToString(stunMessageBytes)); - - Assert.True(stunMessageBytes.Length % 4 == 0); - } - - /// - /// Parse a STUN response received from the Chrome browser's WebRTC stack. - /// - [Fact] - public void ParseWebRTCSTUNResponseTestMethod() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - byte[] stunResp = new byte[]{ 0x01, 0x01, 0x00, 0x2c, 0x21, 0x12, 0xa4, 0x42, 0x6a, 0x45, 0x38, 0x2b, 0x4e, 0x5a, 0x4b, 0x50, - 0x64, 0x31, 0x70, 0x38, 0x00, 0x20, 0x00, 0x08, 0x00, 0x01, 0xe0, 0xda, 0xe1, 0xba, 0x85, 0x3f, - 0x00, 0x08, 0x00, 0x14, 0x24, 0x37, 0x24, 0xa0, 0x05, 0x2d, 0x88, 0x97, 0xce, 0xa6, 0x4e, 0x90, - 0x69, 0xf6, 0x39, 0x07, 0x7d, 0xb1, 0x6e, 0x71, 0x80, 0x28, 0x00, 0x04, 0xde, 0x6a, 0x05, 0xac}; - - STUNMessage stunMessage = STUNMessage.ParseSTUNMessage(stunResp, stunResp.Length); - - STUNHeader stunHeader = stunMessage.Header; - + + Assert.True(stunMessageBytes.Length % 4 == 0); + } + + /// + /// Parse a STUN response received from the Chrome browser's WebRTC stack. + /// + [Fact] + public void ParseWebRTCSTUNResponseTestMethod() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + byte[] stunResp = new byte[]{ 0x01, 0x01, 0x00, 0x2c, 0x21, 0x12, 0xa4, 0x42, 0x6a, 0x45, 0x38, 0x2b, 0x4e, 0x5a, 0x4b, 0x50, + 0x64, 0x31, 0x70, 0x38, 0x00, 0x20, 0x00, 0x08, 0x00, 0x01, 0xe0, 0xda, 0xe1, 0xba, 0x85, 0x3f, + 0x00, 0x08, 0x00, 0x14, 0x24, 0x37, 0x24, 0xa0, 0x05, 0x2d, 0x88, 0x97, 0xce, 0xa6, 0x4e, 0x90, + 0x69, 0xf6, 0x39, 0x07, 0x7d, 0xb1, 0x6e, 0x71, 0x80, 0x28, 0x00, 0x04, 0xde, 0x6a, 0x05, 0xac}; + + STUNMessage stunMessage = STUNMessage.ParseSTUNMessage(stunResp.AsSpan()); + + STUNHeader stunHeader = stunMessage.Header; + logger.LogDebug("Request type = {MessageType}.", stunHeader.MessageType); logger.LogDebug("Length = {MessageLength}.", stunHeader.MessageLength); logger.LogDebug("Transaction ID = {TransactionId}.", BitConverter.ToString(stunHeader.TransactionId)); - - foreach (STUNAttribute attribute in stunMessage.Attributes) - { - if (attribute.AttributeType == STUNAttributeTypesEnum.Username) - { - logger.LogDebug(" {AttributeType} {AttributeValue}.", attribute.AttributeType, Encoding.UTF8.GetString(attribute.Value)); - } - else - { + + foreach (STUNAttribute attribute in stunMessage.Attributes) + { + if (attribute.AttributeType == STUNAttributeTypesEnum.Username) + { + logger.LogDebug(" {AttributeType} {AttributeValue}.", attribute.AttributeType, Encoding.UTF8.GetString(attribute.Value.ToArray())); + } + else + { logger.LogDebug(" {AttributeType} {AttributeValue}.", attribute.AttributeType, attribute.Value); - } - } - - Assert.Equal(STUNMessageTypesEnum.BindingSuccessResponse, stunHeader.MessageType); - Assert.Equal(44, stunHeader.MessageLength); - Assert.Equal(3, stunMessage.Attributes.Count); - } - - /// - /// Tests that parsing an XOR-MAPPED-ADDRESS attribute correctly extracts the IP Address and Port. - /// - [Fact] - public void ParseXORMappedAddressAttributeTestMethod() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - byte[] stunAttribute = new byte[] { 0x00, 0x01, 0xe0, 0xda, 0xe1, 0xba, 0x85, 0x3f }; - - STUNXORAddressAttribute xorAddressAttribute = new STUNXORAddressAttribute(STUNAttributeTypesEnum.XORMappedAddress, stunAttribute, null); - - Assert.Equal(49608, xorAddressAttribute.Port); - Assert.Equal("192.168.33.125", xorAddressAttribute.Address.ToString()); - } - - /// - /// Tests that putting an XOR-MAPPED-ADDRESS attribute to a byte buffer works correctly. - /// - [Fact] - public void PutXORMappedAddressAttributeToBufferTestMethod() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - STUNXORAddressAttribute xorAddressAttribute = new STUNXORAddressAttribute(STUNAttributeTypesEnum.XORMappedAddress, 49608, IPAddress.Parse("192.168.33.125"), null); - - byte[] buffer = new byte[12]; - xorAddressAttribute.ToByteBuffer(buffer, 0); - - Assert.Equal(0x00, buffer[0]); - Assert.Equal(0x20, buffer[1]); - Assert.Equal(0x00, buffer[2]); - Assert.Equal(0x08, buffer[3]); - Assert.Equal(0x00, buffer[4]); - Assert.Equal(0x01, buffer[5]); - Assert.Equal(0xe0, buffer[6]); - Assert.Equal(0xda, buffer[7]); - Assert.Equal(0xe1, buffer[8]); - Assert.Equal(0xba, buffer[9]); - Assert.Equal(0x85, buffer[10]); - Assert.Equal(0x3f, buffer[11]); - } - - /// - /// Tests that putting a STUN response to a byte buffer works correctly. - /// - [Fact] - public void PutResponseToBufferTestMethod() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - STUNMessage stunResponse = new STUNMessage(STUNMessageTypesEnum.BindingSuccessResponse); - stunResponse.Header.TransactionId = Guid.NewGuid().ToByteArray().Take(12).ToArray(); - //stunResponse.AddFingerPrintAttribute(); - stunResponse.AddXORMappedAddressAttribute(IPAddress.Parse("127.0.0.1"), 1234); - - byte[] buffer = stunResponse.ToByteBuffer(null, true); - } - - /// - /// Tests that the message integrity attribute is being correctly generated. The original STUN request packet - /// was capture on the wire from the Google Chrome WebRTC stack. - /// - [Fact] - public void TestMessageIntegrityAttributeForBindingRequest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - byte[] stunReq = new byte[]{ - 0x00, 0x01, 0x00, 0x60, 0x21, 0x12, 0xa4, 0x42, 0x69, 0x64, 0x38, 0x2b, 0x4c, 0x45, 0x44, 0x57, - 0x4d, 0x31, 0x64, 0x30, 0x00, 0x06, 0x00, 0x21, 0x75, 0x4f, 0x35, 0x73, 0x69, 0x31, 0x75, 0x61, - 0x37, 0x63, 0x59, 0x34, 0x74, 0x38, 0x4d, 0x4d, 0x3a, 0x4c, 0x77, 0x38, 0x2f, 0x30, 0x43, 0x31, - 0x43, 0x72, 0x76, 0x68, 0x5a, 0x43, 0x31, 0x67, 0x62, 0x00, 0x00, 0x00, 0x80, 0x2a, 0x00, 0x08, - 0xc0, 0x3d, 0xf5, 0x13, 0x40, 0xf4, 0x22, 0x46, 0x00, 0x25, 0x00, 0x00, 0x00, 0x24, 0x00, 0x04, - 0x6e, 0x7f, 0x1e, 0xff, 0x00, 0x08, 0x00, 0x14, 0x55, 0x82, 0x69, 0xde, 0x17, 0x55, 0xcc, 0x66, - 0x29, 0x23, 0xe6, 0x7d, 0xec, 0x87, 0x6c, 0x07, 0x3a, 0xd6, 0x78, 0x15, 0x80, 0x28, 0x00, 0x04, - 0x1c, 0xae, 0x89, 0x2e}; - - STUNMessage stunMessage = STUNMessage.ParseSTUNMessage(stunReq, stunReq.Length); - STUNHeader stunHeader = stunMessage.Header; - + } + } + + Assert.Equal(STUNMessageTypesEnum.BindingSuccessResponse, stunHeader.MessageType); + Assert.Equal(44, stunHeader.MessageLength); + Assert.Equal(3, stunMessage.Attributes.Count); + } + + /// + /// Tests that parsing an XOR-MAPPED-ADDRESS attribute correctly extracts the IP Address and Port. + /// + [Fact] + public void ParseXORMappedAddressAttributeTestMethod() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + byte[] stunAttribute = new byte[] { 0x00, 0x01, 0xe0, 0xda, 0xe1, 0xba, 0x85, 0x3f }; + + STUNXORAddressAttribute xorAddressAttribute = new STUNXORAddressAttribute(STUNAttributeTypesEnum.XORMappedAddress, stunAttribute, null); + + Assert.Equal(49608, xorAddressAttribute.Port); + Assert.Equal("192.168.33.125", xorAddressAttribute.Address.ToString()); + } + + /// + /// Tests that putting an XOR-MAPPED-ADDRESS attribute to a byte buffer works correctly. + /// + [Theory] + [InlineData("192.168.33.125", new byte[] { 0x00, 0x20, 0x00, 0x08, 0x00, 0x01, 0xe0, 0xda, 0xe1, 0xba, 0x85, 0x3f, })] + [InlineData("fe80::464c:5d73:4576:a13c%9", new byte[] { 0x00, 0x20, 0x00, 0x14, 0x00, 0x02, 0xE0, 0xDA, 0xDF, 0x92, 0xA4, 0x42, })] + public void PutXORMappedAddressAttributeToBufferTestMethod(string ipAddress, byte[] expectedResult) + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + STUNXORAddressAttribute xorAddressAttribute = new STUNXORAddressAttribute(STUNAttributeTypesEnum.XORMappedAddress, 49608, IPAddress.Parse(ipAddress), null); + + byte[] buffer = new byte[12]; + xorAddressAttribute.WriteBytes(buffer.AsSpan()); + + Assert.Equal(expectedResult, buffer); + } + + /// + /// Tests that putting a STUN response to a byte buffer works correctly. + /// + [Fact] + public void PutResponseToBufferTestMethod() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + STUNMessage stunResponse = new STUNMessage(STUNMessageTypesEnum.BindingSuccessResponse); + stunResponse.Header.TransactionId = Guid.NewGuid().ToByteArray().Take(12).ToArray(); + //stunResponse.AddFingerPrintAttribute(); + stunResponse.AddXORMappedAddressAttribute(IPAddress.Parse("127.0.0.1"), 1234); + + var buffer = new byte[stunResponse.GetByteBufferSize(default, true)]; + stunResponse.WriteToBuffer(buffer.AsSpan(), default, true); + } + + /// + /// Tests that the message integrity attribute is being correctly generated. The original STUN request packet + /// was capture on the wire from the Google Chrome WebRTC stack. + /// + [Fact] + public void TestMessageIntegrityAttributeForBindingRequest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + byte[] stunReq = new byte[]{ + 0x00, 0x01, 0x00, 0x60, 0x21, 0x12, 0xa4, 0x42, 0x69, 0x64, 0x38, 0x2b, 0x4c, 0x45, 0x44, 0x57, + 0x4d, 0x31, 0x64, 0x30, 0x00, 0x06, 0x00, 0x21, 0x75, 0x4f, 0x35, 0x73, 0x69, 0x31, 0x75, 0x61, + 0x37, 0x63, 0x59, 0x34, 0x74, 0x38, 0x4d, 0x4d, 0x3a, 0x4c, 0x77, 0x38, 0x2f, 0x30, 0x43, 0x31, + 0x43, 0x72, 0x76, 0x68, 0x5a, 0x43, 0x31, 0x67, 0x62, 0x00, 0x00, 0x00, 0x80, 0x2a, 0x00, 0x08, + 0xc0, 0x3d, 0xf5, 0x13, 0x40, 0xf4, 0x22, 0x46, 0x00, 0x25, 0x00, 0x00, 0x00, 0x24, 0x00, 0x04, + 0x6e, 0x7f, 0x1e, 0xff, 0x00, 0x08, 0x00, 0x14, 0x55, 0x82, 0x69, 0xde, 0x17, 0x55, 0xcc, 0x66, + 0x29, 0x23, 0xe6, 0x7d, 0xec, 0x87, 0x6c, 0x07, 0x3a, 0xd6, 0x78, 0x15, 0x80, 0x28, 0x00, 0x04, + 0x1c, 0xae, 0x89, 0x2e}; + + STUNMessage stunMessage = STUNMessage.ParseSTUNMessage(stunReq.AsSpan()); + STUNHeader stunHeader = stunMessage.Header; + logger.LogDebug("Request type = {MessageType}.", stunHeader.MessageType); logger.LogDebug("Length = {MessageLength}.", stunHeader.MessageLength); logger.LogDebug("Transaction ID = {TransactionId}.", BitConverter.ToString(stunHeader.TransactionId)); - - Assert.Equal(STUNMessageTypesEnum.BindingRequest, stunHeader.MessageType); - Assert.Equal(96, stunHeader.MessageLength); - Assert.Equal(6, stunMessage.Attributes.Count); - Assert.Equal("69-64-38-2B-4C-45-44-57-4D-31-64-30", BitConverter.ToString(stunMessage.Header.TransactionId)); - - stunMessage.Attributes.Remove(stunMessage.Attributes.Where(x => x.AttributeType == STUNAttributeTypesEnum.MessageIntegrity).Single()); - stunMessage.Attributes.Remove(stunMessage.Attributes.Where(x => x.AttributeType == STUNAttributeTypesEnum.FingerPrint).Single()); - - byte[] buffer = stunMessage.ToByteBufferStringKey("r89XhWC9k2kW4Pns75vmwHIa", true); - - Assert.Equal(BitConverter.ToString(stunReq), BitConverter.ToString(buffer)); - } - - /// - /// Parse a STUN response received from the Coturn TURN server. - /// - [Fact] - public void ParseCoturnSTUNResponseTestMethod() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); - logger.BeginScope(TestHelper.GetCurrentMethodName()); - - byte[] stunResp = new byte[]{ 0x01, 0x01, 0x00, 0x44, 0x21, 0x12, 0xa4, 0x42, 0x6b, 0x4c, 0xf3, 0x18, 0xd0, 0xa7, 0xf5, 0x40, - 0x97, 0x30, 0x3a, 0x27, 0x00, 0x20, 0x00, 0x08, 0x00, 0x01, 0x9e, 0x90, 0x1a, 0xb5, 0x08, 0xf3, - 0x00, 0x01, 0x00, 0x08, 0x00, 0x01, 0xbf, 0x82, 0x3b, 0xa7, 0xac, 0xb1, 0x80, 0x2b, 0x00, 0x08, - 0x00, 0x01, 0x0d, 0x96, 0x67, 0x1d, 0x42, 0xf3, 0x80, 0x22, 0x00, 0x1a, 0x43, 0x6f, 0x74, 0x75, - 0x72, 0x6e, 0x2d, 0x34, 0x2e, 0x35, 0x2e, 0x30, 0x2e, 0x33, 0x20, 0x27, 0x64, 0x61, 0x6e, 0x20, - 0x45, 0x69, 0x64, 0x65, 0x72, 0x27, 0x77, 0x75}; - - STUNMessage stunMessage = STUNMessage.ParseSTUNMessage(stunResp, stunResp.Length); - - STUNHeader stunHeader = stunMessage.Header; - + + Assert.Equal(STUNMessageTypesEnum.BindingRequest, stunHeader.MessageType); + Assert.Equal(96, stunHeader.MessageLength); + Assert.Equal(6, stunMessage.Attributes.Count); + Assert.Equal("69-64-38-2B-4C-45-44-57-4D-31-64-30", BitConverter.ToString(stunMessage.Header.TransactionId)); + + stunMessage.Attributes.Remove(stunMessage.Attributes.Where(x => x.AttributeType == STUNAttributeTypesEnum.MessageIntegrity).Single()); + stunMessage.Attributes.Remove(stunMessage.Attributes.Where(x => x.AttributeType == STUNAttributeTypesEnum.FingerPrint).Single()); + + const string messageIntegrityKey = "r89XhWC9k2kW4Pns75vmwHIa"; + var buffer = new byte[stunMessage.GetByteBufferSizeStringKey(messageIntegrityKey, true)]; + stunMessage.WriteToBufferStringKey(buffer.AsSpan(), messageIntegrityKey, true); + + Assert.Equal(BitConverter.ToString(stunReq), BitConverter.ToString(buffer)); + } + + /// + /// Parse a STUN response received from the Coturn TURN server. + /// + [Fact] + public void ParseCoturnSTUNResponseTestMethod() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + logger.BeginScope(TestHelper.GetCurrentMethodName()); + + byte[] stunResp = new byte[]{ 0x01, 0x01, 0x00, 0x44, 0x21, 0x12, 0xa4, 0x42, 0x6b, 0x4c, 0xf3, 0x18, 0xd0, 0xa7, 0xf5, 0x40, + 0x97, 0x30, 0x3a, 0x27, 0x00, 0x20, 0x00, 0x08, 0x00, 0x01, 0x9e, 0x90, 0x1a, 0xb5, 0x08, 0xf3, + 0x00, 0x01, 0x00, 0x08, 0x00, 0x01, 0xbf, 0x82, 0x3b, 0xa7, 0xac, 0xb1, 0x80, 0x2b, 0x00, 0x08, + 0x00, 0x01, 0x0d, 0x96, 0x67, 0x1d, 0x42, 0xf3, 0x80, 0x22, 0x00, 0x1a, 0x43, 0x6f, 0x74, 0x75, + 0x72, 0x6e, 0x2d, 0x34, 0x2e, 0x35, 0x2e, 0x30, 0x2e, 0x33, 0x20, 0x27, 0x64, 0x61, 0x6e, 0x20, + 0x45, 0x69, 0x64, 0x65, 0x72, 0x27, 0x77, 0x75}; + + STUNMessage stunMessage = STUNMessage.ParseSTUNMessage(stunResp.AsSpan()); + + STUNHeader stunHeader = stunMessage.Header; + logger.LogDebug("Request type = {MessageType}.", stunHeader.MessageType); logger.LogDebug("Length = {MessageLength}.", stunHeader.MessageLength); logger.LogDebug("Transaction ID = {TransactionId}.", BitConverter.ToString(stunHeader.TransactionId)); - - foreach (STUNAttribute attribute in stunMessage.Attributes) - { - if (attribute.AttributeType == STUNAttributeTypesEnum.MappedAddress) - { + + foreach (STUNAttribute attribute in stunMessage.Attributes) + { + if (attribute.AttributeType == STUNAttributeTypesEnum.MappedAddress) + { STUNAddressAttribute addressAttribute = new STUNAddressAttribute(attribute.Value); logger.LogDebug(" {AttributeType} {Address}:{Port}.", attribute.AttributeType, addressAttribute.Address, addressAttribute.Port); - - Assert.Equal("59.167.172.177", addressAttribute.Address.ToString()); - Assert.Equal(49026, addressAttribute.Port); - } - else if (attribute.AttributeType == STUNAttributeTypesEnum.XORMappedAddress) - { + + Assert.Equal("59.167.172.177", addressAttribute.Address.ToString()); + Assert.Equal(49026, addressAttribute.Port); + } + else if (attribute.AttributeType == STUNAttributeTypesEnum.XORMappedAddress) + { STUNXORAddressAttribute xorAddressAttribute = new STUNXORAddressAttribute(STUNAttributeTypesEnum.XORMappedAddress, attribute.Value, stunHeader.TransactionId); logger.LogDebug(" {AttributeType} {Address}:{Port}.", attribute.AttributeType, xorAddressAttribute.Address, xorAddressAttribute.Port); - - Assert.Equal("59.167.172.177", xorAddressAttribute.Address.ToString()); - Assert.Equal(49026, xorAddressAttribute.Port); - } - - else + + Assert.Equal("59.167.172.177", xorAddressAttribute.Address.ToString()); + Assert.Equal(49026, xorAddressAttribute.Port); + } + + else { logger.LogDebug(" {AttributeType} {AttributeValue}.", attribute.AttributeType, attribute.Value); - } - } - - Assert.Equal(STUNMessageTypesEnum.BindingSuccessResponse, stunHeader.MessageType); + } + } + + Assert.Equal(STUNMessageTypesEnum.BindingSuccessResponse, stunHeader.MessageType); } - /// - /// Tests that the fingerprint and hmac attributes get generated correctly. - /// - [Fact] - public void GenerateHmacAndFingerprintTestMethod() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + /// + /// Tests that the fingerprint and hmac attributes get generated correctly. + /// + [Fact] + public void GenerateHmacAndFingerprintTestMethod() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); - string icePassword = "SKYKPPYLTZOAVCLTGHDUODANRKSPOVQVKXJULOGG"; - + string icePassword = "SKYKPPYLTZOAVCLTGHDUODANRKSPOVQVKXJULOGG"; + STUNMessage msg = new STUNMessage(STUNMessageTypesEnum.BindingSuccessResponse); msg.Header.TransactionId = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; msg.AddXORMappedAddressAttribute(IPAddress.Loopback, 55477); - var buffer = msg.ToByteBufferStringKey(icePassword, true); + var buffer = new byte[msg.GetByteBufferSizeStringKey(icePassword, true)]; + msg.WriteToBufferStringKey(buffer.AsSpan(), icePassword, true); string hmac = "HMAC: "; for (int i = 36; i < 56; i++) @@ -300,11 +297,11 @@ public void GenerateHmacAndFingerprintTestMethod() logger.LogDebug("Fingerprint: {Byte1:X2} {Byte2:X2} {Byte3:X2} {Byte4:X2}.", buffer[buffer.Length - 4], buffer[buffer.Length - 3], buffer[buffer.Length - 2], buffer[buffer.Length - 1]); } - /// - /// Tests that the STUN header class type is correctly determined from the message type. - /// - [Fact] - public void CheckCLassForSTUNMessageTypeUnitTest() + /// + /// Tests that the STUN header class type is correctly determined from the message type. + /// + [Fact] + public void CheckCLassForSTUNMessageTypeUnitTest() { Assert.Equal(STUNClassTypesEnum.Request, (new STUNHeader(STUNMessageTypesEnum.BindingRequest).MessageClass)); Assert.Equal(STUNClassTypesEnum.Request, (new STUNHeader(STUNMessageTypesEnum.Allocate).MessageClass)); @@ -326,13 +323,13 @@ public void CheckCLassForSTUNMessageTypeUnitTest() Assert.Equal(STUNClassTypesEnum.Indication, (new STUNHeader(STUNMessageTypesEnum.SendIndication).MessageClass)); } - /// - /// Tests that a locally signed STUN request can be verified. - /// - [Fact] - public void IntegrityCheckUnitTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + /// + /// Tests that a locally signed STUN request can be verified. + /// + [Fact] + public void IntegrityCheckUnitTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); string icePassword = "SKYKPPYLTZOAVCLTGHDUODANRKSPOVQVKXJULOGG"; @@ -340,26 +337,27 @@ public void IntegrityCheckUnitTest() STUNMessage stunRequest = new STUNMessage(STUNMessageTypesEnum.BindingRequest); stunRequest.Header.TransactionId = Encoding.ASCII.GetBytes(Crypto.GetRandomString(STUNHeader.TRANSACTION_ID_LENGTH)); stunRequest.AddUsernameAttribute("xxxx:yyyy"); - stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Priority, BitConverter.GetBytes(1))); + stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Priority, BitConverter.GetBytes(1))); - var buffer = stunRequest.ToByteBufferStringKey(icePassword, true); + var buffer = new byte[stunRequest.GetByteBufferSizeStringKey(icePassword, true)]; + stunRequest.WriteToBufferStringKey(buffer.AsSpan(), icePassword, true); - //logger.LogDebug($"HMAC: {buffer.Skip(buffer.Length - ).Take(20).ToArray().HexStr()}."); + //logger.LogDebug($"HMAC: {buffer.Skip(buffer.Length - ).Take(20).ToArray().HexStr()}."); //logger.LogDebug($"Fingerprint: {buffer.Skip(buffer.Length -4).ToArray().HexStr()}."); - STUNMessage rndTripReq = STUNMessage.ParseSTUNMessage(buffer, buffer.Length); + STUNMessage rndTripReq = STUNMessage.ParseSTUNMessage(buffer.AsSpan()); Assert.True(rndTripReq.isFingerprintValid); - Assert.True(rndTripReq.CheckIntegrity(System.Text.Encoding.UTF8.GetBytes(icePassword))); + Assert.True(rndTripReq.CheckIntegrity(System.Text.Encoding.UTF8.GetBytes(icePassword))); } - /// - /// Tests that a known STUN request can be verified. - /// - [Fact] - public void KnownSTUNBindingRequestIntegrityCheckUnitTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + /// + /// Tests that a known STUN request can be verified. + /// + [Fact] + public void KnownSTUNBindingRequestIntegrityCheckUnitTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); string icePassword = "DVJSBHBUIBFSZFKVECMPRISQ"; @@ -367,10 +365,10 @@ public void KnownSTUNBindingRequestIntegrityCheckUnitTest() byte[] buffer = TypeExtensions.ParseHexStr( $"0001003C2112A4424A5655444B44544753454455000600095A4C45423A4554454F00000000240008CC3A28000000000000080014B295EDA4BC88A0BC885D745644D36E51FE3CBD1880280004EDF60FF7"); - STUNMessage stunRequest = STUNMessage.ParseSTUNMessage(buffer, buffer.Length); + STUNMessage stunRequest = STUNMessage.ParseSTUNMessage(buffer.AsSpan()); Assert.True(stunRequest.isFingerprintValid); - Assert.True(stunRequest.CheckIntegrity(System.Text.Encoding.UTF8.GetBytes(icePassword))); + Assert.True(stunRequest.CheckIntegrity(System.Text.Encoding.UTF8.GetBytes(icePassword))); } /// @@ -383,9 +381,10 @@ public void CheckPriorityAttributeLengthUnitTest() stunRequest.AddUsernameAttribute("dummy:dummy"); stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Priority, BitConverter.GetBytes(1234U))); stunRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.UseCandidate, null)); - byte[] stunReqBytes = stunRequest.ToByteBufferStringKey("dummy", true); + byte[] stunReqBytes = new byte[stunRequest.GetByteBufferSizeStringKey("dummy", true)]; ; + stunRequest.WriteToBufferStringKey(stunReqBytes.AsSpan(), "dummy", true); - var stunReq = STUNMessage.ParseSTUNMessage(stunReqBytes, stunReqBytes.Length); + var stunReq = STUNMessage.ParseSTUNMessage(stunReqBytes.AsSpan()); Assert.Equal(4, stunReq.Attributes.Single(x => x.AttributeType == STUNAttributeTypesEnum.Priority).Value.Length); } @@ -408,32 +407,32 @@ public void ParseBindingRequestWithIceControlledAttribute() 0x07, 0x8a, 0x49, 0x2e }; - var stunReq = STUNMessage.ParseSTUNMessage(buffer, buffer.Length); + var stunReq = STUNMessage.ParseSTUNMessage(buffer.AsSpan()); Assert.NotNull(stunReq); - Assert.Equal(1853882367U, - NetConvert.ParseUInt32(stunReq.Attributes.Single(x => x.AttributeType == STUNAttributeTypesEnum.Priority).Value, 0)); + Assert.Equal(1853882367U, + BinaryPrimitives.ReadUInt32BigEndian(stunReq.Attributes.Single(x => x.AttributeType == STUNAttributeTypesEnum.Priority).Value.Span)); Assert.Equal(8, stunReq.Attributes.Single(x => x.AttributeType == STUNAttributeTypesEnum.IceControlled).PaddedLength); - Assert.Equal(0x27ff2a171b888ffeU, - NetConvert.ParseUInt64(stunReq.Attributes.Single(x => x.AttributeType == STUNAttributeTypesEnum.IceControlled).Value, 0)); + Assert.Equal(0x27ff2a171b888ffeU, + BinaryPrimitives.ReadUInt64BigEndian(stunReq.Attributes.Single(x => x.AttributeType == STUNAttributeTypesEnum.IceControlled).Value.Span)); } - /// - /// Used as an ad-hoc way to parse STUN messages. - /// - [Fact] - public void ParseStunMessageUnitTest() - { - logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); + /// + /// Used as an ad-hoc way to parse STUN messages. + /// + [Fact] + public void ParseStunMessageUnitTest() + { + logger.LogDebug("--> {MethodName}", TestHelper.GetCurrentMethodName()); logger.BeginScope(TestHelper.GetCurrentMethodName()); byte[] buffer = TypeExtensions.ParseHexStr( "000100542112a4424f585055434d4e54425a4f4a00060015435242617a4d64534248616a494774433a45544d5300000000240004ff200000802a000852c0aba195cf65190025000000080014b05baf6be589d5ab202e9153547457eb1a20244c8028000464f37f6c"); - STUNMessage stunRequest = STUNMessage.ParseSTUNMessage(buffer, buffer.Length); + STUNMessage stunRequest = STUNMessage.ParseSTUNMessage(buffer.AsSpan()); Assert.True(stunRequest.isFingerprintValid); - //Assert.True(stunRequest.CheckIntegrity(System.Text.Encoding.UTF8.GetBytes(icePassword))); + //Assert.True(stunRequest.CheckIntegrity(System.Text.Encoding.UTF8.GetBytes(icePassword))); } - } -} + } +} diff --git a/test/unit/net/TURN/TurnServerUnitTest.cs b/test/unit/net/TURN/TurnServerUnitTest.cs index a7f134fff5..0386bb9f70 100644 --- a/test/unit/net/TURN/TurnServerUnitTest.cs +++ b/test/unit/net/TURN/TurnServerUnitTest.cs @@ -96,7 +96,8 @@ private static async Task ConnectTcpClient(int port) private static async Task SendStunMessage(NetworkStream stream, STUNMessage msg, byte[] hmacKey = null, bool addFingerprint = false) { - var bytes = msg.ToByteBuffer(hmacKey, addFingerprint); + var bytes = new byte[msg.GetByteBufferSize(hmacKey, addFingerprint)]; + msg.WriteToBuffer(bytes, hmacKey, addFingerprint); await stream.WriteAsync(bytes, 0, bytes.Length); await stream.FlushAsync(); } @@ -129,7 +130,7 @@ private static async Task ReceiveStunMessage(NetworkStream stream, totalRead += read; } - return STUNMessage.ParseSTUNMessage(fullMsg, fullMsg.Length); + return STUNMessage.ParseSTUNMessage(fullMsg); } private static STUNMessage BuildAllocateRequest() @@ -147,7 +148,7 @@ private static STUNMessage BuildAuthenticatedAllocateRequest(byte[] hmacKey, str msg.Attributes.Add(new STUNAttribute( STUNAttributeTypesEnum.RequestedTransport, STUNAttributeConstants.UdpTransportType)); - msg.AddUsernameAttribute(username); + msg.AddUsernameAttribute(username.AsSpan()); msg.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Realm, Encoding.UTF8.GetBytes(realm))); msg.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.Nonce, @@ -180,14 +181,14 @@ public async Task AllocateReturns401WithoutCredentials() a => a.AttributeType == STUNAttributeTypesEnum.ErrorCode); Assert.NotNull(errorAttr); Assert.True(errorAttr.Value.Length >= 4); - int errorCode = errorAttr.Value[2] * 100 + errorAttr.Value[3]; + int errorCode = errorAttr.Value.Span[2] * 100 + errorAttr.Value.Span[3]; Assert.Equal(401, errorCode); // Should contain REALM var realmAttr = response.Attributes.FirstOrDefault( a => a.AttributeType == STUNAttributeTypesEnum.Realm); Assert.NotNull(realmAttr); - Assert.Equal(TEST_REALM, Encoding.UTF8.GetString(realmAttr.Value)); + Assert.Equal(TEST_REALM, Encoding.UTF8.GetString(realmAttr.Value.ToArray())); // Should contain NONCE var nonceAttr = response.Attributes.FirstOrDefault( @@ -215,7 +216,7 @@ public async Task AuthenticatedAllocateSucceeds() Assert.Equal(STUNMessageTypesEnum.AllocateErrorResponse, response1.Header.MessageType); var nonceAttr = response1.Attributes.First(a => a.AttributeType == STUNAttributeTypesEnum.Nonce); - var nonce = Encoding.UTF8.GetString(nonceAttr.Value); + var nonce = Encoding.UTF8.GetString(nonceAttr.Value.ToArray()); // Step 2: Send authenticated allocate var request2 = BuildAuthenticatedAllocateRequest(hmacKey, TEST_USERNAME, TEST_REALM, nonce); @@ -260,7 +261,7 @@ public async Task WrongPasswordFails() await SendStunMessage(stream, request1); var response1 = await ReceiveStunMessage(stream); var nonce = Encoding.UTF8.GetString( - response1.Attributes.First(a => a.AttributeType == STUNAttributeTypesEnum.Nonce).Value); + response1.Attributes.First(a => a.AttributeType == STUNAttributeTypesEnum.Nonce).Value.ToArray()); // Send with wrong credentials var request2 = BuildAuthenticatedAllocateRequest(wrongKey, TEST_USERNAME, TEST_REALM, nonce); @@ -660,7 +661,7 @@ private async Task AllocateWithAuth(NetworkStream stream, byte[] hm var nonceAttr = response1.Attributes.First( a => a.AttributeType == STUNAttributeTypesEnum.Nonce); - var nonce = Encoding.UTF8.GetString(nonceAttr.Value); + var nonce = Encoding.UTF8.GetString(nonceAttr.Value.ToArray()); // Step 2: Authenticated request var request2 = BuildAuthenticatedAllocateRequest(hmacKey, TEST_USERNAME, TEST_REALM, nonce); diff --git a/test/unit/net/WebRTC/RTCPeerConnectionAnswerUnitTest.cs b/test/unit/net/WebRTC/RTCPeerConnectionAnswerUnitTest.cs index c358851e9d..9ac0601bcc 100644 --- a/test/unit/net/WebRTC/RTCPeerConnectionAnswerUnitTest.cs +++ b/test/unit/net/WebRTC/RTCPeerConnectionAnswerUnitTest.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: RTCPeerConnectionAnswerUnitTest.cs // // Description: Unit tests for RTCPeerConnection.createAnswer(). @@ -10,6 +10,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -71,7 +72,7 @@ public void AnswerSdpSetupAttributeNotActpass() // Parse the answer SDP and verify every media announcement has // setup:active or setup:passive, never actpass. - SDP answerSdp = SDP.ParseSDPDescription(answer.sdp); + SDP answerSdp = SDP.ParseSDPDescription(answer.sdp.AsSpan()); Assert.NotEmpty(answerSdp.Media); foreach (var media in answerSdp.Media) diff --git a/test/unit/net/WebRTC/RTCPeerConnectionCreateAnswerUnitTest.cs b/test/unit/net/WebRTC/RTCPeerConnectionCreateAnswerUnitTest.cs index 7b174b9177..c2ab0cd66e 100644 --- a/test/unit/net/WebRTC/RTCPeerConnectionCreateAnswerUnitTest.cs +++ b/test/unit/net/WebRTC/RTCPeerConnectionCreateAnswerUnitTest.cs @@ -1,4 +1,4 @@ -//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- // Filename: RTCPeerConnectionCreateAnswerUnitTest.cs // // Description: Characterization tests for RTCPeerConnection.createAnswer. @@ -52,7 +52,7 @@ public void NoRemoteDescriptionSet_Throws() { using (var pc = new PeerConnectionBuilder().WithAudioTrack().Build()) { - Assert.Throws(() => pc.createAnswer(null)); + Assert.Throws(() => pc.createAnswer(null)); } } diff --git a/test/unit/net/WebRTC/RTCSessionDescriptionInitUnitTest.cs b/test/unit/net/WebRTC/RTCSessionDescriptionInitUnitTest.cs index e883199ccd..b3fca5263d 100644 --- a/test/unit/net/WebRTC/RTCSessionDescriptionInitUnitTest.cs +++ b/test/unit/net/WebRTC/RTCSessionDescriptionInitUnitTest.cs @@ -10,6 +10,7 @@ // BSD 3-Clause "New" or "Revised" License, see included LICENSE.md file. //----------------------------------------------------------------------------- +using System; using System.Collections.Generic; using Microsoft.Extensions.Logging; using Xunit; @@ -51,7 +52,7 @@ public void JsonRoundtripUnitTest() Assert.Equal(RTCSdpType.offer, init.type); Assert.NotNull(init.sdp); - SDP sdp = SDP.ParseSDPDescription(init.sdp); + SDP sdp = SDP.ParseSDPDescription(init.sdp.AsSpan()); Assert.Equal(0, sdp.Version); } diff --git a/test/unit/sys/BufferUtilsUnitTest.cs b/test/unit/sys/BufferUtilsUnitTest.cs index f568705a1e..1cc693ca34 100644 --- a/test/unit/sys/BufferUtilsUnitTest.cs +++ b/test/unit/sys/BufferUtilsUnitTest.cs @@ -127,7 +127,7 @@ public void HexStrUnitTest() logger.LogDebug("HexStr result: {HexStrResult}", BitConverter.ToString(buffer).Replace("-", "")); - Assert.Equal("010203", buffer.HexStr()); + Assert.Equal("010203", TypeExtensions.HexStr(buffer)); } [Fact] @@ -140,7 +140,7 @@ public void HexStrWithSeparatorUnitTest() logger.LogDebug("HexStr result: {HexStrResult}", BitConverter.ToString(buffer).Replace("-", ":")); - Assert.Equal("01:02:03", buffer.HexStr(':')); + Assert.Equal("01:02:03", TypeExtensions.HexStr(buffer, ':')); } } } diff --git a/test/unit/sys/TypeExtensionsUnitTest.cs b/test/unit/sys/TypeExtensionsUnitTest.cs index 1b4c747625..9c69be0ded 100755 --- a/test/unit/sys/TypeExtensionsUnitTest.cs +++ b/test/unit/sys/TypeExtensionsUnitTest.cs @@ -64,9 +64,9 @@ public void HexStrTest() byte[] buffer = { 0x00, 0x01, 0x02, 0x03 }; - logger.LogDebug("Hex string: {HexString}.", buffer.HexStr()); + logger.LogDebug("Hex string: {HexString}.", TypeExtensions.HexStr(buffer)); - Assert.Equal("00010203", buffer.HexStr()); + Assert.Equal("00010203", TypeExtensions.HexStr(buffer)); } [Fact] @@ -105,9 +105,9 @@ public void ParseHexStrTest() byte[] buffer = TypeExtensions.ParseHexStr("00010203"); - logger.LogDebug("Hex string: {HexString}.", buffer.HexStr()); + logger.LogDebug("Hex string: {HexString}.", TypeExtensions.HexStr(buffer)); - Assert.Equal("00010203", buffer.HexStr()); + Assert.Equal("00010203", TypeExtensions.HexStr(buffer)); } } } diff --git a/test/unit/sys/net/NetServicesUnitTest.cs b/test/unit/sys/net/NetServicesUnitTest.cs index e7f6c554b1..deefbca4cb 100755 --- a/test/unit/sys/net/NetServicesUnitTest.cs +++ b/test/unit/sys/net/NetServicesUnitTest.cs @@ -414,7 +414,7 @@ public void CheckFailsOnDuplicateForIP4AnyThenIPv6AnyUnitTest() Assert.NotNull(rtpSocket); Assert.NotNull(controlSocket); - Assert.Throws(() => NetServices.CreateBoundUdpSocket((rtpSocket.LocalEndPoint as IPEndPoint).Port, IPAddress.IPv6Any, false, true)); + Assert.Throws(() => NetServices.CreateBoundUdpSocket((rtpSocket.LocalEndPoint as IPEndPoint).Port, IPAddress.IPv6Any, false, true)); rtpSocket.Close(); controlSocket.Close(); @@ -453,7 +453,7 @@ public void CheckFailsOnDuplicateForIP6AnyThenIPv4AnyUnitTest() Assert.NotNull(rtpSocket); Assert.NotNull(controlSocket); - Assert.Throws(() => NetServices.CreateBoundUdpSocket((rtpSocket.LocalEndPoint as IPEndPoint).Port, IPAddress.Any)); + Assert.Throws(() => NetServices.CreateBoundUdpSocket((rtpSocket.LocalEndPoint as IPEndPoint).Port, IPAddress.Any)); rtpSocket.Close(); controlSocket.Close(); diff --git a/test/unit/net/RTP/UdpReceiverConnectionResetUnitTest.cs b/test/unit/sys/net/UdpReceiverConnectionResetUnitTest.cs similarity index 100% rename from test/unit/net/RTP/UdpReceiverConnectionResetUnitTest.cs rename to test/unit/sys/net/UdpReceiverConnectionResetUnitTest.cs diff --git a/test/unit/net/RTP/UdpReceiverUnitTest.cs b/test/unit/sys/net/UdpReceiverUnitTest.cs similarity index 100% rename from test/unit/net/RTP/UdpReceiverUnitTest.cs rename to test/unit/sys/net/UdpReceiverUnitTest.cs