diff --git a/src/net/RTCP/RTCPCompoundPacket.cs b/src/net/RTCP/RTCPCompoundPacket.cs index b839e3007..e9a6005b9 100644 --- a/src/net/RTCP/RTCPCompoundPacket.cs +++ b/src/net/RTCP/RTCPCompoundPacket.cs @@ -37,7 +37,10 @@ public class RTCPCompoundPacket public RTCPSenderReport SenderReport { get; private set; } public RTCPReceiverReport ReceiverReport { get; private set; } public RTCPSDesReport SDesReport { get; private set; } + public RTCPHeader PLI { get; private set; } public RTCPBye Bye { get; set; } + public RTCPFIR FIR { get; private set; } + public RTCPFeedback NACK { get; private set; } public RTCPCompoundPacket(RTCPSenderReport senderReport, RTCPSDesReport sdesReport) { @@ -97,11 +100,24 @@ public RTCPCompoundPacket(byte[] packet) // TODO: Interpret Generic RTP feedback reports. var rtpfbHeader = new RTCPHeader(buffer); offset += rtpfbHeader.Length * 4 + 4; + if(rtpfbHeader.FeedbackMessageType == RTCPFeedbackTypesEnum.NACK) + { + NACK = new RTCPFeedback(buffer); + } break; case (byte)RTCPReportTypesEnum.PSFB: // TODO: Interpret Payload specific feedback reports. var psfbHeader = new RTCPHeader(buffer); offset += psfbHeader.Length * 4 + 4; + if(psfbHeader.PayloadFeedbackMessageType == PSFBFeedbackTypesEnum.PLI) + { + PLI = psfbHeader; + } + break; + case (byte)RTCPReportTypesEnum.FIR: + FIR = new RTCPFIR(buffer); + int FIRLength = (FIR != null) ? FIR.GetBytes().Length : Int32.MaxValue; + offset += FIRLength; break; default: logger.LogWarning($"RTCPCompoundPacket did not recognise packet type ID {packetTypeID}."); diff --git a/src/net/RTCP/RTCPFIR.cs b/src/net/RTCP/RTCPFIR.cs new file mode 100644 index 000000000..bcae7fded --- /dev/null +++ b/src/net/RTCP/RTCPFIR.cs @@ -0,0 +1,152 @@ +//----------------------------------------------------------------------------- +// Filename: RTCPFIR.cs +// +// Description: RTCP FIR packet as defined in RFC2032 +// +// FIR RTCP Packet +// 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| MBZ | PT=RTCP_FIR=196 | length | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC/CSRC | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +//----------------------------------------------------------------------------- + +using System; +using System.Text; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net +{ + /// + /// RTCP Goodbye packet as defined in RFC3550. The BYE packet indicates + /// that one or more sources are no longer active. + /// + public class RTCPFIR + { + 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 RTCPFIR(uint ssrc, string reason) + { + Header = new RTCPHeader(RTCPReportTypesEnum.FIR, 1); + SSRC = ssrc; + + if (reason != null) + { + 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); + } + } + } + + /// + /// Create a new RTCP Goodbye packet from a serialised byte array. + /// + /// The byte array holding the Goodbye packet. + public RTCPFIR(byte[] packet) + { + if (packet.Length < MIN_PACKET_SIZE) + { + throw new ApplicationException("The packet did not contain the minimum number of bytes for an RTCP Goodbye packet."); + } + + Header = new RTCPHeader(packet); + + if (BitConverter.IsLittleEndian) + { + SSRC = NetConvert.DoReverseEndian(BitConverter.ToUInt32(packet, 4)); + } + else + { + SSRC = BitConverter.ToUInt32(packet, 4); + } + + 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); + } + } + } + + /// + /// Gets the raw bytes for the Goodbye packet. + /// + /// A byte array. + public byte[] GetBytes() + { + 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)); + + Buffer.BlockCopy(Header.GetBytes(), 0, buffer, 0, RTCPHeader.HEADER_BYTES_LENGTH); + int payloadIndex = RTCPHeader.HEADER_BYTES_LENGTH; + + if (BitConverter.IsLittleEndian) + { + Buffer.BlockCopy(BitConverter.GetBytes(NetConvert.DoReverseEndian(SSRC)), 0, buffer, payloadIndex, 4); + } + else + { + Buffer.BlockCopy(BitConverter.GetBytes(SSRC), 0, buffer, payloadIndex, 4); + } + + if (reasonLength > 0) + { + buffer[payloadIndex + 4] = (byte)reasonLength; + Buffer.BlockCopy(reasonBytes, 0, buffer, payloadIndex + 5, reasonBytes.Length); + } + + return buffer; + } + + /// + /// 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) + { + reasonLength += 1; + } + + int nonPaddedSize = reasonLength + SSRC_SIZE; + + if (nonPaddedSize % 4 == 0) + { + return nonPaddedSize; + } + else + { + return nonPaddedSize + 4 - (nonPaddedSize % 4); + } + } + } +} diff --git a/src/net/RTCP/RTCPFeedback.cs b/src/net/RTCP/RTCPFeedback.cs index d856926cc..31c2e4058 100644 --- a/src/net/RTCP/RTCPFeedback.cs +++ b/src/net/RTCP/RTCPFeedback.cs @@ -149,7 +149,22 @@ public RTCPFeedback(byte[] packet) MediaSSRC = BitConverter.ToUInt32(packet, 8); } + // TODO: Depending on the report type additional parameters will need to be deserialised. + if (Header.FeedbackMessageType == RTCPFeedbackTypesEnum.NACK) + { + int payloadIndex = RTCPHeader.HEADER_BYTES_LENGTH; + if (BitConverter.IsLittleEndian) + { + PID = NetConvert.DoReverseEndian(BitConverter.ToUInt16(packet, payloadIndex + 6)); + BLP = NetConvert.DoReverseEndian(BitConverter.ToUInt16(packet, payloadIndex + 8)); + } + else + { + SenderSSRC = BitConverter.ToUInt16(packet, payloadIndex + 6); + MediaSSRC = BitConverter.ToUInt16(packet, payloadIndex + 8); + } + } } public byte[] GetBytes() diff --git a/src/net/RTCP/RTCPHeader.cs b/src/net/RTCP/RTCPHeader.cs index a6ccf244d..c0f94f49e 100644 --- a/src/net/RTCP/RTCPHeader.cs +++ b/src/net/RTCP/RTCPHeader.cs @@ -42,6 +42,7 @@ namespace SIPSorcery.Net /// public enum RTCPReportTypesEnum : byte { + FIR = 192, // FIR SR = 200, // Send Report. RR = 201, // Receiver Report. SDES = 202, // Session Description. diff --git a/src/net/RTP/RTPSession.cs b/src/net/RTP/RTPSession.cs index 73c106e58..fef95b2e6 100644 --- a/src/net/RTP/RTPSession.cs +++ b/src/net/RTP/RTPSession.cs @@ -19,6 +19,7 @@ //----------------------------------------------------------------------------- using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net; @@ -133,7 +134,7 @@ public enum SetDescriptionResultEnum /// public class RTPSession : IMediaSession, IDisposable { - private const int RTP_MAX_PAYLOAD = 1400; + public int RTP_MAX_PAYLOAD { get; set; } = 1400; /// /// From libsrtp: SRTP_MAX_TRAILER_LEN is the maximum length of the SRTP trailer @@ -187,6 +188,8 @@ public class RTPSession : IMediaSession, IDisposable internal Dictionary m_rtpChannels = new Dictionary(); private SrtpHandler m_srtpHandler = null; + private List videoChannelRTPPacketBuffer = new List(); + public int NACKPayloadType { get; set; } = 101; /// /// Track if current remote description is invalid (used in Renegotiation logic) @@ -379,6 +382,12 @@ public bool HasVideo /// public event Action OnRtcpBye; + + + public event Action OnFullIntraRequest; + + public event Action OnPictureLossIndication; + /// /// 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. @@ -2150,6 +2159,20 @@ protected void OnReceive(int localPort, IPEndPoint remoteEndPoint, byte[] buffer VideoRemoteTrack.Ssrc = 0; } } + else if (rtcpPkt.FIR != null) + { + logger.LogDebug($"RTCP FIR received for SSRC {rtcpPkt.FIR.SSRC}"); + OnFullIntraRequest?.Invoke(); + } + else if (rtcpPkt.PLI != null) + { + logger.LogDebug($"RTCP PSFB received"); + OnPictureLossIndication?.Invoke(); + } + else if(rtcpPkt.NACK != null) + { + OnNackRecevied(rtcpPkt.NACK); + } else if (!IsClosed) { var rtcpSession = GetRtcpSession(rtcpPkt); @@ -2594,6 +2617,10 @@ private void SendRtpPacket(RTPChannel rtpChannel, IPEndPoint dstRtpSocket, byte[ if (m_srtpProtect == null) { rtpChannel.Send(RTPChannelSocketsEnum.RTP, dstRtpSocket, rtpBuffer); + if (ssrc == VideoLocalTrack.Ssrc) + { + PushToBufferVideoChannelPacketBuffer(rtpPacket); + } } else { @@ -2606,6 +2633,10 @@ private void SendRtpPacket(RTPChannel rtpChannel, IPEndPoint dstRtpSocket, byte[ else { rtpChannel.Send(RTPChannelSocketsEnum.RTP, dstRtpSocket, rtpBuffer.Take(outBufLen).ToArray()); + if (ssrc == VideoLocalTrack.Ssrc) + { + PushToBufferVideoChannelPacketBuffer(rtpPacket); + } } } m_lastRtpTimestamp = timestamp; @@ -2708,5 +2739,37 @@ public virtual void Dispose() { Close("disposed"); } + + private void PushToBufferVideoChannelPacketBuffer(RTPPacket packet) + { + if (videoChannelRTPPacketBuffer.Count < 60) + { + videoChannelRTPPacketBuffer.Add(packet); + } + else + { + videoChannelRTPPacketBuffer.RemoveAt(0); + videoChannelRTPPacketBuffer.Add(packet); + } + } + + private void OnNackRecevied(RTCPFeedback NACK) + { + if(NACK.SenderSSRC == VideoLocalTrack.Ssrc || NACK.MediaSSRC == VideoLocalTrack.Ssrc) + { + var seq = NACK.PID; + var bitMask = new BitArray(BitConverter.GetBytes(NACK.BLP)); + for (int i = 0; i < bitMask.Length; i++) + { + if (bitMask[i]) + { + var packet = videoChannelRTPPacketBuffer.First(x => x.Header.SequenceNumber == seq + i + 1); + var videoChannel = GetRtpChannel(SDPMediaTypesEnum.video); + + SendRtpPacket(videoChannel, VideoDestinationEndPoint, packet.Payload, packet.Header.Timestamp, packet.Header.MarkerBit, NACKPayloadType, VideoLocalTrack.Ssrc, packet.Header.SequenceNumber, VideoRtcpSession); + } + } + } + } } } diff --git a/src/net/WebRTC/RTCPeerConnection.cs b/src/net/WebRTC/RTCPeerConnection.cs index c97ca6c12..2c3797f2a 100644 --- a/src/net/WebRTC/RTCPeerConnection.cs +++ b/src/net/WebRTC/RTCPeerConnection.cs @@ -159,6 +159,9 @@ public class RTCPeerConnection : RTPSession, IRTCPeerConnection private const string NORMAL_CLOSE_REASON = "normal"; private const ushort SCTP_DEFAULT_PORT = 5000; private const string UNKNOWN_DATACHANNEL_ERROR = "unknown"; + private const string RTCP_FIR = "a=rtcp-fb:* ccm fir"; // Indicates the media announcement is using multiplexed RTCP. + private const string RTP_MAP_RETRANSMIT = "a=rtpmap:{0} rtx/90000"; + private const string FMTP_RETRANSMIT = "a=fmtp:{0} apt={1};rtx-time=200"; /// /// The period to wait for the SCTP association to complete before giving up. @@ -176,6 +179,7 @@ public class RTCPeerConnection : RTPSession, IRTCPeerConnection public string SessionID { get; private set; } public string SdpSessionID { get; private set; } public string LocalSdpSessionID { get; private set; } + public bool FIREnabled { get; set; } private RtpIceChannel _rtpIceChannel; @@ -379,12 +383,15 @@ public RTCPeerConnection() : this(null) { } + /// /// Constructor to create a new RTC peer connection instance. /// /// Optional. - public RTCPeerConnection(RTCConfiguration configuration, int bindPort = 0) : - base(true, true, true, configuration?.X_BindAddress, bindPort) + /// Optional. Default = 0 + /// Optional. Default = true + public RTCPeerConnection(RTCConfiguration configuration, int bindPort = 0, bool isSecure = true) : + base(true, true, isSecure, configuration?.X_BindAddress, bindPort) { if (_configuration != null && _configuration.iceTransportPolicy == RTCIceTransportPolicy.relay && @@ -472,7 +479,7 @@ public RTCPeerConnection(RTCConfiguration configuration, int bindPort = 0) : OnRtpClosed += Close; OnRtcpBye += Close; - + //Cancel Negotiation Task Event to Prevent Duplicated Calls onnegotiationneeded += CancelOnNegotiationNeededTask; @@ -1102,6 +1109,13 @@ void AddIceCandidates(SDPMediaAnnouncement announcement) announcement.IceOptions = ICE_OPTIONS; announcement.IceRole = IceRole; announcement.DtlsFingerprint = dtlsFingerprint; + if (FIREnabled) + { + announcement.AddExtra(RTCP_FIR); + announcement.AddExtra(string.Format(RTP_MAP_RETRANSMIT, NACKPayloadType)); + announcement.AddExtra(String.Format(FMTP_RETRANSMIT, NACKPayloadType, videoCapabilities[0].ID)); + } + if (iceCandidatesAdded == false && !excludeIceCandidates) {