public async void ChecklistProcessingToFailStateUnitTest() { logger.LogDebug("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name); logger.BeginScope(System.Reflection.MethodBase.GetCurrentMethod().Name); var rtpIceChannel = new RtpIceChannel(null, RTCIceComponent.rtp, null); rtpIceChannel.StartGathering(); Assert.NotNull(rtpIceChannel); Assert.NotEmpty(rtpIceChannel.Candidates); foreach (var hostCandidate in rtpIceChannel.Candidates) { logger.LogDebug($"host candidate: {hostCandidate}"); } var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999"); rtpIceChannel.AddRemoteCandidate(remoteCandidate); rtpIceChannel.SetRemoteCredentials("CI7o", "xxxxxxxxxxxx"); logger.LogDebug($"ICE session retry interval {rtpIceChannel.RTO}ms."); await Task.Delay(1000); rtpIceChannel._checklist.Single().FirstCheckSentAt = DateTime.Now.Subtract(TimeSpan.FromSeconds(RtpIceChannel.FAILED_TIMEOUT_PERIOD)); await Task.Delay(1000); Assert.Equal(ChecklistEntryState.Failed, rtpIceChannel._checklist.Single().State); Assert.Equal(ChecklistState.Failed, rtpIceChannel._checklistState); Assert.Equal(RTCIceConnectionState.failed, rtpIceChannel.IceConnectionState); }
public async void ChecklistProcessingUnitTest() { logger.LogDebug("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name); logger.BeginScope(System.Reflection.MethodBase.GetCurrentMethod().Name); var rtpIceChannel = new RtpIceChannel(null, RTCIceComponent.rtp, null); rtpIceChannel.StartGathering(); Assert.NotNull(rtpIceChannel); Assert.NotEmpty(rtpIceChannel.Candidates); foreach (var hostCandidate in rtpIceChannel.Candidates) { logger.LogDebug($"host candidate: {hostCandidate}"); } var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999"); rtpIceChannel.AddRemoteCandidate(remoteCandidate); rtpIceChannel.SetRemoteCredentials("CI7o", "xxxxxxxxxxxx"); rtpIceChannel.StartGathering(); await Task.Delay(2000); var checklistEntry = rtpIceChannel._checklist.Single(); logger.LogDebug($"Checklist entry state {checklistEntry.State}, last check sent at {checklistEntry.LastCheckSentAt}."); Assert.Equal(ChecklistEntryState.InProgress, checklistEntry.State); }
public async void ChecklistProcessingToFailStateUnitTest() { logger.LogDebug("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name); logger.BeginScope(System.Reflection.MethodBase.GetCurrentMethod().Name); var rtpIceChannel = new RtpIceChannel(null, RTCIceComponent.rtp, null); rtpIceChannel.StartGathering(); Assert.NotNull(rtpIceChannel); Assert.NotEmpty(rtpIceChannel.Candidates); foreach (var hostCandidate in rtpIceChannel.Candidates) { logger.LogDebug($"host candidate: {hostCandidate}"); } var remoteCandidate = RTCIceCandidate.Parse("candidate:408132416 1 udp 2113937151 192.168.11.50 51268 typ host generation 0 ufrag CI7o network-cost 999"); rtpIceChannel.AddRemoteCandidate(remoteCandidate); rtpIceChannel.SetRemoteCredentials("CI7o", "xxxxxxxxxxxx"); logger.LogDebug($"ICE session retry interval {rtpIceChannel.RTO}ms."); // The defaults are 5 STUN requests and for a checklist with one entry they will be 500ms apart. await Task.Delay(4000); Assert.Equal(ChecklistEntryState.Failed, rtpIceChannel._checklist.Single().State); Assert.Equal(ChecklistState.Failed, rtpIceChannel._checklistState); Assert.Equal(RTCIceConnectionState.failed, rtpIceChannel.IceConnectionState); }
public async void CheckSuccessfulConnectionForRelayCandidatesUnitTest() { logger.LogDebug("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name); logger.BeginScope(System.Reflection.MethodBase.GetCurrentMethod().Name); using (MockTurnServer mockTurnServer = new MockTurnServer()) { var iceServers = new List <RTCIceServer> { new RTCIceServer { urls = $"turn:{mockTurnServer.ListeningEndPoint}", } }; var rtpIceChannelRelay = new RtpIceChannel(null, RTCIceComponent.rtp, iceServers, RTCIceTransportPolicy.relay); rtpIceChannelRelay.IsController = true; logger.LogDebug($"RTP ICE channel RTP socket local end point {rtpIceChannelRelay.RTPLocalEndPoint}."); var rtpIceChannelHost = new RtpIceChannel(); logger.LogDebug($"RTP ICE channel RTP socket local end point {rtpIceChannelHost.RTPLocalEndPoint}."); rtpIceChannelRelay.StartGathering(); rtpIceChannelHost.StartGathering(); // Need to give some time for the relay channel to connect to the mock TURN server. await Task.Delay(200); Assert.Single(rtpIceChannelRelay.Candidates); // Should only have the single local relay candidate. Assert.NotEmpty(rtpIceChannelHost.Candidates); Assert.Equal(RTCIceGatheringState.complete, rtpIceChannelRelay.IceGatheringState); Assert.Equal(RTCIceConnectionState.@new, rtpIceChannelRelay.IceConnectionState); Assert.Equal(RTCIceGatheringState.complete, rtpIceChannelHost.IceGatheringState); Assert.Equal(RTCIceConnectionState.@new, rtpIceChannelHost.IceConnectionState); // Exchange ICE user and passwords. rtpIceChannelRelay.SetRemoteCredentials(rtpIceChannelHost.LocalIceUser, rtpIceChannelHost.LocalIcePassword); rtpIceChannelHost.SetRemoteCredentials(rtpIceChannelRelay.LocalIceUser, rtpIceChannelRelay.LocalIcePassword); Assert.Equal(RTCIceConnectionState.checking, rtpIceChannelRelay.IceConnectionState); Assert.Equal(RTCIceConnectionState.checking, rtpIceChannelHost.IceConnectionState); // Exchange ICE candidates. rtpIceChannelRelay.Candidates.ForEach(x => rtpIceChannelHost.AddRemoteCandidate(x)); rtpIceChannelHost.Candidates.ForEach(x => rtpIceChannelRelay.AddRemoteCandidate(x)); await Task.Delay(1000); Assert.Equal(RTCIceConnectionState.connected, rtpIceChannelRelay.IceConnectionState); Assert.Equal(RTCIceConnectionState.connected, rtpIceChannelHost.IceConnectionState); Assert.NotNull(rtpIceChannelRelay.NominatedEntry); Assert.NotNull(rtpIceChannelHost.NominatedEntry); } }
public async void CheckSuccessfulConnectionForHostCandidatesUnitTest() { logger.LogDebug("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name); logger.BeginScope(System.Reflection.MethodBase.GetCurrentMethod().Name); var rtpIceChannelA = new RtpIceChannel(); rtpIceChannelA.IsController = true; logger.LogDebug($"RTP ICE channel RTP socket local end point {rtpIceChannelA.RTPLocalEndPoint}."); var rtpIceChannelB = new RtpIceChannel(); logger.LogDebug($"RTP ICE channel RTP socket local end point {rtpIceChannelB.RTPLocalEndPoint}."); rtpIceChannelA.StartGathering(); rtpIceChannelB.StartGathering(); Assert.NotEmpty(rtpIceChannelA.Candidates); Assert.NotEmpty(rtpIceChannelB.Candidates); // Because there are no ICE servers gathering completes after the host candidates are gathered. Assert.Equal(RTCIceGatheringState.complete, rtpIceChannelA.IceGatheringState); Assert.Equal(RTCIceGatheringState.complete, rtpIceChannelB.IceGatheringState); Assert.Equal(RTCIceConnectionState.@new, rtpIceChannelA.IceConnectionState); Assert.Equal(RTCIceConnectionState.@new, rtpIceChannelB.IceConnectionState); // Exchange ICE user and passwords. rtpIceChannelA.SetRemoteCredentials(rtpIceChannelB.LocalIceUser, rtpIceChannelB.LocalIcePassword); rtpIceChannelB.SetRemoteCredentials(rtpIceChannelA.LocalIceUser, rtpIceChannelA.LocalIcePassword); Assert.Equal(RTCIceConnectionState.checking, rtpIceChannelA.IceConnectionState); Assert.Equal(RTCIceConnectionState.checking, rtpIceChannelB.IceConnectionState); // Give the RTP channel listeners time to start. await Task.Delay(500); // Exchange ICE candidates. rtpIceChannelA.Candidates.ForEach(x => rtpIceChannelB.AddRemoteCandidate(x)); rtpIceChannelB.Candidates.ForEach(x => rtpIceChannelA.AddRemoteCandidate(x)); // Give the RTP ICE channel checklists time to send the first few checks. await Task.Delay(4000); Assert.Equal(RTCIceConnectionState.connected, rtpIceChannelA.IceConnectionState); Assert.Equal(RTCIceConnectionState.connected, rtpIceChannelB.IceConnectionState); Assert.NotNull(rtpIceChannelA.NominatedEntry); Assert.NotNull(rtpIceChannelB.NominatedEntry); }
public async void CheckPeerReflexiveReplacedByHostCandidatesUnitTest() { logger.LogDebug("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name); logger.BeginScope(System.Reflection.MethodBase.GetCurrentMethod().Name); var rtpIceChannelA = new RtpIceChannel(); rtpIceChannelA.IsController = true; logger.LogDebug($"RTP ICE channel RTP socket local end point {rtpIceChannelA.RTPLocalEndPoint}."); var rtpIceChannelB = new RtpIceChannel(); logger.LogDebug($"RTP ICE channel RTP socket local end point {rtpIceChannelB.RTPLocalEndPoint}."); // Set up the triggers so the test can proceed at the right pace. ManualResetEventSlim connected = new ManualResetEventSlim(); rtpIceChannelA.OnIceConnectionStateChange += (state) => { if (state == RTCIceConnectionState.connected) { connected.Set(); } }; rtpIceChannelA.StartGathering(); rtpIceChannelB.StartGathering(); Assert.NotEmpty(rtpIceChannelA.Candidates); Assert.NotEmpty(rtpIceChannelB.Candidates); // Because there are no ICE servers gathering completes after the host candidates are gathered. Assert.Equal(RTCIceGatheringState.complete, rtpIceChannelA.IceGatheringState); Assert.Equal(RTCIceGatheringState.complete, rtpIceChannelB.IceGatheringState); Assert.Equal(RTCIceConnectionState.@new, rtpIceChannelA.IceConnectionState); Assert.Equal(RTCIceConnectionState.@new, rtpIceChannelB.IceConnectionState); // Exchange ICE user and passwords. //rtpIceChannelA.SetRemoteCredentials(rtpIceChannelB.LocalIceUser, rtpIceChannelB.LocalIcePassword); rtpIceChannelB.SetRemoteCredentials(rtpIceChannelA.LocalIceUser, rtpIceChannelA.LocalIcePassword); Assert.Equal(RTCIceConnectionState.@new, rtpIceChannelA.IceConnectionState); Assert.Equal(RTCIceConnectionState.checking, rtpIceChannelB.IceConnectionState); // Only give the non-controlling peer the remote candidates. rtpIceChannelA.Candidates.ForEach(x => rtpIceChannelB.AddRemoteCandidate(x)); //rtpIceChannelB.Candidates.ForEach(x => rtpIceChannelA.AddRemoteCandidate(x)); // Want channel B to send checks to A so that it create peer reflexive candidates. int retries = 0; while (rtpIceChannelA._remoteCandidates.Count == 0 && retries < 5) { logger.LogDebug("Waiting for channel A to acquire peer reflexive candidates."); retries++; await Task.Delay(500); } Assert.True(rtpIceChannelA._remoteCandidates.Count > 0); logger.LogDebug("Adding remote candidates from B to A."); rtpIceChannelB.Candidates.ForEach(x => rtpIceChannelA.AddRemoteCandidate(x)); // This pause is so that channel A can process the new remote candidates supplied by B. // These candidates are host candidates and should replace the peer reflexive candidates // that were automatically created previously. await Task.Delay(1000); logger.LogDebug("Setting remote credentials for channel A."); rtpIceChannelA.SetRemoteCredentials(rtpIceChannelB.LocalIceUser, rtpIceChannelB.LocalIcePassword); Assert.True(connected.Wait(3000)); Assert.Equal(RTCIceConnectionState.connected, rtpIceChannelA.IceConnectionState); Assert.Equal(RTCIceConnectionState.connected, rtpIceChannelB.IceConnectionState); Assert.NotNull(rtpIceChannelA.NominatedEntry); Assert.NotNull(rtpIceChannelB.NominatedEntry); Assert.Equal(RTCIceCandidateType.host, rtpIceChannelA.NominatedEntry.LocalCandidate.type); Assert.Equal(RTCIceCandidateType.host, rtpIceChannelB.NominatedEntry.LocalCandidate.type); }
/// <summary> /// Updates the session after receiving the remote SDP. /// At this point check that the codecs match. We currently only support: /// - Audio: PCMU, /// - Video: VP8. /// If they are not available there's no point carrying on. /// </summary> /// <param name="sessionDescription">The answer/offer SDP from the remote party.</param> public SetDescriptionResultEnum setRemoteDescription(RTCSessionDescriptionInit init) { RTCSessionDescription description = new RTCSessionDescription { type = init.type, sdp = SDP.ParseSDPDescription(init.sdp) }; remoteDescription = description; SDP remoteSdp = SDP.ParseSDPDescription(init.sdp); SdpType sdpType = (init.type == RTCSdpType.offer) ? SdpType.offer : SdpType.answer; var setResult = base.SetRemoteDescription(sdpType, remoteSdp); if (setResult == SetDescriptionResultEnum.OK) { string remoteIceUser = remoteSdp.IceUfrag; string remoteIcePassword = remoteSdp.IcePwd; string dtlsFingerprint = remoteSdp.DtlsFingerprint; var audioAnnounce = remoteSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.audio).FirstOrDefault(); if (audioAnnounce != null) { remoteIceUser = remoteIceUser ?? audioAnnounce.IceUfrag; remoteIcePassword = remoteIcePassword ?? audioAnnounce.IcePwd; dtlsFingerprint = dtlsFingerprint ?? audioAnnounce.DtlsFingerprint; } var videoAnnounce = remoteSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.video).FirstOrDefault(); if (videoAnnounce != null) { if (remoteIceUser == null || remoteIcePassword == null || dtlsFingerprint == null) { remoteIceUser = remoteIceUser ?? videoAnnounce.IceUfrag; remoteIcePassword = remoteIcePassword ?? videoAnnounce.IcePwd; dtlsFingerprint = dtlsFingerprint ?? videoAnnounce.DtlsFingerprint; } } SdpSessionID = remoteSdp.SessionId; if (init.type == RTCSdpType.answer) { _rtpIceChannel.IsController = true; // Set DTLS role to be server. IceRole = IceRolesEnum.passive; } else { // Set DTLS role to be server. // As of 20 Jun 2020 the DTLS handshake logic is based on OpenSSL and the mechanism // used hands over the socket handle to the C++ class. This logic works a lot better // when acting as the server in the handshake. IceRole = IceRolesEnum.passive; } if (remoteIceUser != null && remoteIcePassword != null) { _rtpIceChannel.SetRemoteCredentials(remoteIceUser, remoteIcePassword); } if (!string.IsNullOrWhiteSpace(dtlsFingerprint)) { dtlsFingerprint = dtlsFingerprint.Trim().ToLower(); if (dtlsFingerprint.Length < DTLS_FINGERPRINT_DIGEST.Length + 1) { logger.LogWarning($"The DTLS fingerprint was too short."); return(SetDescriptionResultEnum.DtlsFingerprintDigestNotSupported); } else if (!dtlsFingerprint.StartsWith(DTLS_FINGERPRINT_DIGEST)) { logger.LogWarning($"The DTLS fingerprint was supplied with an unsupported digest function (supported one is {DTLS_FINGERPRINT_DIGEST}): {dtlsFingerprint}."); return(SetDescriptionResultEnum.DtlsFingerprintDigestNotSupported); } else { dtlsFingerprint = dtlsFingerprint.Substring(DTLS_FINGERPRINT_DIGEST.Length + 1).Trim().Replace(":", "").Replace(" ", ""); RemotePeerDtlsFingerprint = ByteBufferInfo.ParseHexStr(dtlsFingerprint); logger.LogDebug($"The DTLS fingerprint for the remote peer's SDP {ByteBufferInfo.HexStr(RemotePeerDtlsFingerprint)}."); } } else { logger.LogWarning("The DTLS fingerprint was missing from the remote party's session description."); 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) { foreach (var iceCandidate in remoteSdp.IceCandidates) { addIceCandidate(new RTCIceCandidateInit { candidate = iceCandidate }); } } foreach (var media in remoteSdp.Media) { if (media.IceCandidates != null) { foreach (var iceCandidate in media.IceCandidates) { addIceCandidate(new RTCIceCandidateInit { candidate = iceCandidate }); } } } signalingState = RTCSignalingState.have_remote_offer; onsignalingstatechange?.Invoke(); } return(setResult); }
/// <summary> /// Updates the session after receiving the remote SDP. /// At this point check that the codecs match. We currently only support: /// - Audio: PCMU, /// - Video: VP8. /// If they are not available there's no point carrying on. /// </summary> /// <param name="sessionDescription">The answer/offer SDP from the remote party.</param> public SetDescriptionResultEnum setRemoteDescription(RTCSessionDescriptionInit init) { RTCSessionDescription description = new RTCSessionDescription { type = init.type, sdp = SDP.ParseSDPDescription(init.sdp) }; remoteDescription = description; SDP remoteSdp = SDP.ParseSDPDescription(init.sdp); SdpType sdpType = (init.type == RTCSdpType.offer) ? SdpType.offer : SdpType.answer; var setResult = base.SetRemoteDescription(sdpType, remoteSdp); if (setResult == SetDescriptionResultEnum.OK) { string remoteIceUser = remoteSdp.IceUfrag; string remoteIcePassword = remoteSdp.IcePwd; string dtlsFingerprint = remoteSdp.DtlsFingerprint; var audioAnnounce = remoteSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.audio).FirstOrDefault(); if (audioAnnounce != null) { remoteIceUser = remoteIceUser ?? audioAnnounce.IceUfrag; remoteIcePassword = remoteIcePassword ?? audioAnnounce.IcePwd; dtlsFingerprint = dtlsFingerprint ?? audioAnnounce.DtlsFingerprint; } var videoAnnounce = remoteSdp.Media.Where(x => x.Media == SDPMediaTypesEnum.video).FirstOrDefault(); if (videoAnnounce != null) { if (remoteIceUser == null || remoteIcePassword == null || dtlsFingerprint == null) { remoteIceUser = remoteIceUser ?? videoAnnounce.IceUfrag; remoteIcePassword = remoteIcePassword ?? videoAnnounce.IcePwd; dtlsFingerprint = dtlsFingerprint ?? videoAnnounce.DtlsFingerprint; } } SdpSessionID = remoteSdp.SessionId; if (init.type == RTCSdpType.answer) { _rtpIceChannel.IsController = true; // Set DTLS role to be server. IceRole = IceRolesEnum.passive; } else { // Set DTLS role as client. IceRole = IceRolesEnum.active; } if (remoteIceUser != null && remoteIcePassword != null) { _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 invalid or not supported."); return(SetDescriptionResultEnum.DtlsFingerprintDigestNotSupported); } } else { logger.LogWarning("The DTLS fingerprint was missing from the remote party's session description."); 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) { foreach (var iceCandidate in remoteSdp.IceCandidates) { addIceCandidate(new RTCIceCandidateInit { candidate = iceCandidate }); } } foreach (var media in remoteSdp.Media) { if (media.IceCandidates != null) { foreach (var iceCandidate in media.IceCandidates) { addIceCandidate(new RTCIceCandidateInit { candidate = iceCandidate }); } } } signalingState = RTCSignalingState.have_remote_offer; onsignalingstatechange?.Invoke(); } return(setResult); }