diff --git a/com.unity.renderstreaming/Runtime/Scripts/PeerConnection.cs b/com.unity.renderstreaming/Runtime/Scripts/PeerConnection.cs index 72316752d..fbe622ebb 100644 --- a/com.unity.renderstreaming/Runtime/Scripts/PeerConnection.cs +++ b/com.unity.renderstreaming/Runtime/Scripts/PeerConnection.cs @@ -1,52 +1,104 @@ using System; +using System.Collections; +using System.Collections.Generic; using Unity.WebRTC; +using UnityEngine; +using UnityEngine.Assertions; namespace Unity.RenderStreaming { internal class PeerConnection : IDisposable { - public readonly RTCPeerConnection peer; - public readonly bool polite; + public delegate void OnConnectEvent(); - public bool makingOffer; - public bool ignoreOffer; - public bool srdAnswerPending; - public bool makingAnswer; + public delegate void OnDisconnectEvent(); - bool disposed = false; + public delegate void OnDataChannelEvent(RTCDataChannel channel); + + public delegate void OnTrackEvent(RTCTrackEvent trackEvent); + + public delegate void SendOfferEvent(RTCSessionDescription description); + + public delegate void SendAnswerEvent(RTCSessionDescription description); + + public delegate void SendCandidateEvent(RTCIceCandidate candidate); + + public OnConnectEvent OnConnectHandler; + public OnDisconnectEvent OnDisconnectHandler; + public OnDataChannelEvent OnDataChannelHandler; + public OnTrackEvent OnTrackEventHandler; + public SendOfferEvent SendOfferHandler; + public SendAnswerEvent SendAnswerHandler; + public SendCandidateEvent SendCandidateHandler; + + public RTCPeerConnection peer => _peer; /// - /// + /// /// public bool waitingAnswer { get => _waitingAnswer; - set { + private set + { _waitingAnswer = value; - timeSinceStartWaitingAnswer = - _waitingAnswer ? UnityEngine.Time.realtimeSinceStartup : 0; + _timeSinceStartWaitingAnswer = + _waitingAnswer ? Time.realtimeSinceStartup : 0; } } - /// - /// see Time.realtimeSinceStartup - /// - public float timeSinceStartWaitingAnswer { get; private set; } + private readonly RTCPeerConnection _peer; + private readonly bool _polite; + private readonly Func _startCoroutine; + private readonly Action _stopCoroutine; + private readonly HashSet> _processingCoroutineList = new HashSet>(); + // resend offer + private readonly float _resendInterval; private bool _waitingAnswer; + private float _timeSinceStartWaitingAnswer; + + // processing set description + private bool _processingSetDescription; + + // processing got description + private bool _ignoreOffer; + private bool _srdAnswerPending; + - public PeerConnection(RTCPeerConnection peer, bool polite) + private bool _disposed = false; + + public PeerConnection(bool polite, RTCConfiguration config, float resendInterval, Func startCoroutine, Action stopCoroutine) { - this.peer = peer; - this.polite = polite; + _polite = polite; + _resendInterval = resendInterval; + _startCoroutine = startCoroutine; + _stopCoroutine = stopCoroutine; + + _peer = new RTCPeerConnection(ref config); + _peer.OnDataChannel = channel => OnDataChannelHandler?.Invoke(channel); + _peer.OnIceCandidate = candidate => SendCandidateHandler?.Invoke(candidate); + _peer.OnTrack = trackEvent => OnTrackEventHandler?.Invoke(trackEvent); + _peer.OnConnectionStateChange = state => + { + switch (state) + { + case RTCPeerConnectionState.Connected: + OnConnectHandler?.Invoke(); + break; + case RTCPeerConnectionState.Disconnected: + OnDisconnectHandler?.Invoke(); + break; + } + }; + _peer.OnNegotiationNeeded = () => StartCoroutine(OnNegotiationNeeded()); } - /// - /// - /// - public void RestartTimerForWaitingAnswer() + private void StartCoroutine(IEnumerator enumerator) { - timeSinceStartWaitingAnswer = UnityEngine.Time.realtimeSinceStartup; + var co = _startCoroutine(enumerator); + _processingCoroutineList.RemoveWhere(x => !x.TryGetTarget(out _)); + _processingCoroutineList.Add(new WeakReference(co)); } ~PeerConnection() @@ -56,30 +108,192 @@ public void RestartTimerForWaitingAnswer() public override string ToString() { - var str = polite ? "polite" : "impolite"; - return $"[{str}-{base.ToString()}]"; + var str = _polite ? "polite" : "impolite"; + return + $"[{str}-{nameof(PeerConnection)} {nameof(_peer.ConnectionState)}:{_peer.ConnectionState} {nameof(_peer.IceConnectionState)}:{_peer.IceConnectionState} {nameof(_peer.SignalingState)}:{_peer.SignalingState} {nameof(_peer.GatheringState)}:{_peer.GatheringState}]"; } public void Dispose() { - if (peer == null) + if (_disposed) + return; + + foreach (var weakCo in _processingCoroutineList) { + if (weakCo.TryGetTarget(out var co)) + { + _stopCoroutine?.Invoke(co); + } + } + _processingCoroutineList.Clear(); + + if (_peer != null) + { + _peer.OnTrack = null; + _peer.OnDataChannel = null; + _peer.OnIceCandidate = null; + _peer.OnNegotiationNeeded = null; + _peer.OnConnectionStateChange = null; + _peer.OnIceConnectionChange = null; + _peer.OnIceGatheringStateChange = null; + _peer.Dispose(); + } + + _disposed = true; + GC.SuppressFinalize(this); + } + + private IEnumerator OnNegotiationNeeded() + { + var waitProcessSetDescription = new WaitWhile(() => _processingSetDescription); + yield return waitProcessSetDescription; + SendOffer(); + } + + public bool IsConnected() + { + return _peer.ConnectionState == RTCPeerConnectionState.Connected; + } + + public bool IsStable() + { + return _peer.SignalingState == RTCSignalingState.Stable || + (_peer.SignalingState == RTCSignalingState.HaveLocalOffer && _srdAnswerPending); + } + + public void SendOffer() + { + if (_processingSetDescription) + { + Debug.LogWarning($"{this} already processing other set description"); return; } - if (disposed) + + if (!IsStable()) + { + if (!_waitingAnswer) + { + throw new InvalidOperationException( + $"{this} sendoffer needs in stable state, current state is {_peer.SignalingState}"); + } + + var timeout = _timeSinceStartWaitingAnswer + _resendInterval; + + if (timeout < Time.realtimeSinceStartup) + { + SendOfferHandler?.Invoke(_peer.LocalDescription); + _timeSinceStartWaitingAnswer = Time.realtimeSinceStartup; + } return; + } - peer.OnTrack = null; - peer.OnDataChannel = null; - peer.OnIceCandidate = null; - peer.OnNegotiationNeeded = null; - peer.OnConnectionStateChange = null; - peer.OnIceConnectionChange = null; - peer.OnIceGatheringStateChange = null; - peer.Dispose(); + StartCoroutine(SendOfferCoroutine()); + } - disposed = true; - GC.SuppressFinalize(this); + private IEnumerator SendOfferCoroutine() + { + Assert.AreEqual(_peer.SignalingState, RTCSignalingState.Stable); + Assert.AreEqual(_processingSetDescription, false); + Assert.AreEqual(waitingAnswer, false); + + _processingSetDescription = true; + + var opLocalDesc = _peer.SetLocalDescription(); + yield return opLocalDesc; + + if (opLocalDesc.IsError) + { + Debug.LogError($"{this} {opLocalDesc.Error.message}"); + _processingSetDescription = false; + yield break; + } + + Assert.AreEqual(_peer.LocalDescription.type, RTCSdpType.Offer); + Assert.AreEqual(_peer.SignalingState, RTCSignalingState.HaveLocalOffer); + _processingSetDescription = false; + waitingAnswer = true; + + SendOfferHandler?.Invoke(_peer.LocalDescription); + } + + public void SendAnswer() + { + if (_processingSetDescription) + { + Debug.LogWarning($"{this} already processing other set description"); + return; + } + + StartCoroutine(SendAnswerCoroutine()); + } + + private IEnumerator SendAnswerCoroutine() + { + Assert.AreEqual(_peer.SignalingState, RTCSignalingState.HaveRemoteOffer); + Assert.AreEqual(_processingSetDescription, false); + + _processingSetDescription = true; + + var opLocalDesc = _peer.SetLocalDescription(); + yield return opLocalDesc; + + if (opLocalDesc.IsError) + { + Debug.LogError($"{this} {opLocalDesc.Error.message}"); + _processingSetDescription = false; + yield break; + } + + Assert.AreEqual(_peer.LocalDescription.type, RTCSdpType.Answer); + Assert.AreEqual(_peer.SignalingState, RTCSignalingState.Stable); + _processingSetDescription = false; + + SendAnswerHandler?.Invoke(_peer.LocalDescription); + } + + public IEnumerator OnGotDescription(RTCSessionDescription description, Action onComplete) + { + var waitOtherProcess = new WaitWhile(() => _processingSetDescription); + yield return waitOtherProcess; + + _ignoreOffer = description.type == RTCSdpType.Offer && !_polite && (_processingSetDescription || !IsStable()); + + if (_ignoreOffer) + { + Debug.LogWarning($"{this} glare - ignoreOffer."); + yield break; + } + + waitingAnswer = false; + _srdAnswerPending = description.type == RTCSdpType.Answer; + _processingSetDescription = true; + + var remoteDescOp = _peer.SetRemoteDescription(ref description); + yield return remoteDescOp; + if (remoteDescOp.IsError) + { + Debug.LogError($"{this} {remoteDescOp.Error.message}"); + _srdAnswerPending = false; + _processingSetDescription = false; + yield break; + } + + _srdAnswerPending = false; + _processingSetDescription = false; + onComplete?.Invoke(); + } + + public bool OnGotIceCandidate(RTCIceCandidate candidate) + { + if (!_peer.AddIceCandidate(candidate)) + { + if (!_ignoreOffer) + Debug.LogWarning($"{this} this candidate can't accept on state."); + + return false; + } + + return true; } } } diff --git a/com.unity.renderstreaming/Runtime/Scripts/RenderStreaming.cs b/com.unity.renderstreaming/Runtime/Scripts/RenderStreaming.cs index 9eac06e2d..2584227b7 100644 --- a/com.unity.renderstreaming/Runtime/Scripts/RenderStreaming.cs +++ b/com.unity.renderstreaming/Runtime/Scripts/RenderStreaming.cs @@ -162,6 +162,7 @@ private void _Run( config = _conf, signaling = _signaling, startCoroutine = StartCoroutine, + stopCoroutine = StopCoroutine, resentOfferInterval = interval, }; var _handlers = (handlers ?? this.handlers.AsEnumerable()).Where(_ => _ != null); diff --git a/com.unity.renderstreaming/Runtime/Scripts/RenderStreamingInternal.cs b/com.unity.renderstreaming/Runtime/Scripts/RenderStreamingInternal.cs index d73818bdc..b4fb998e9 100644 --- a/com.unity.renderstreaming/Runtime/Scripts/RenderStreamingInternal.cs +++ b/com.unity.renderstreaming/Runtime/Scripts/RenderStreamingInternal.cs @@ -29,6 +29,11 @@ internal struct RenderStreamingDependencies /// public Func startCoroutine; + /// + /// + /// + public Action stopCoroutine; + /// /// unit is second; /// @@ -90,6 +95,7 @@ internal class RenderStreamingInternal : IDisposable, private readonly ISignaling _signaling; private RTCConfiguration _config; private readonly Func _startCoroutine; + private readonly Action _stopCoroutine; private readonly Dictionary _mapConnectionIdAndPeer = new Dictionary(); private bool _runningResendCoroutine; @@ -118,6 +124,7 @@ public RenderStreamingInternal(ref RenderStreamingDependencies dependencies) _config = dependencies.config; _startCoroutine = dependencies.startCoroutine; + _stopCoroutine = dependencies.stopCoroutine; _resendInterval = dependencies.resentOfferInterval; _signaling = dependencies.signaling; _signaling.OnStart += OnStart; @@ -190,23 +197,12 @@ public bool ExistConnection(string connectionId) public bool IsConnected(string connectionId) { - if (!_mapConnectionIdAndPeer.TryGetValue(connectionId, out var peer)) - return false; - - return peer.peer.ConnectionState == RTCPeerConnectionState.Connected; + return _mapConnectionIdAndPeer.TryGetValue(connectionId, out var peer) && peer.IsConnected(); } public bool IsStable(string connectionId) { - if (!_mapConnectionIdAndPeer.TryGetValue(connectionId, out var peer)) - return false; - - if (peer.makingOffer || peer.waitingAnswer) - { - return false; - } - - return peer.peer.SignalingState == RTCSignalingState.Stable; + return _mapConnectionIdAndPeer.TryGetValue(connectionId, out var peer) && peer.IsStable(); } /// @@ -301,18 +297,7 @@ public void SendOffer(string connectionId) { if (!_mapConnectionIdAndPeer.TryGetValue(connectionId, out var pc)) return; - if (!IsStable(connectionId)) - { - if (!pc.waitingAnswer) - { - throw new InvalidOperationException( - $"{pc} sendoffer needs in stable state, current state is {pc.peer.SignalingState}"); - } - - _signaling.SendOffer(connectionId, pc.peer.LocalDescription); - return; - } - _startCoroutine(SendOfferCoroutine(connectionId, pc)); + pc.SendOffer(); } /// @@ -321,22 +306,18 @@ public void SendOffer(string connectionId) /// public void SendAnswer(string connectionId) { - _startCoroutine(SendAnswerCoroutine(connectionId, _mapConnectionIdAndPeer[connectionId])); + if (!_mapConnectionIdAndPeer.TryGetValue(connectionId, out var pc)) + return; + pc.SendAnswer(); } IEnumerator ResendOfferCoroutine() { while (_runningResendCoroutine) { - foreach (var pair in _mapConnectionIdAndPeer.Where(x => x.Value.waitingAnswer)) + foreach (var peer in _mapConnectionIdAndPeer.Where(x => x.Value.waitingAnswer)) { - float timeout = pair.Value.timeSinceStartWaitingAnswer + _resendInterval; - - if (timeout < Time.realtimeSinceStartup) - { - _signaling.SendOffer(pair.Key, pair.Value.peer.LocalDescription); - pair.Value.RestartTimerForWaitingAnswer(); - } + peer.Value.SendOffer(); } yield return 0; } @@ -368,25 +349,19 @@ PeerConnection CreatePeerConnection(string connectionId, bool polite) { if (_mapConnectionIdAndPeer.TryGetValue(connectionId, out var peer)) { - peer.peer.Close(); + peer.Dispose(); } - var pc = new RTCPeerConnection(); - peer = new PeerConnection(pc, polite); + peer = new PeerConnection(polite, _config, _resendInterval, _startCoroutine, _stopCoroutine); _mapConnectionIdAndPeer[connectionId] = peer; - pc.SetConfiguration(ref _config); - pc.OnDataChannel = channel => { OnDataChannel(connectionId, channel); }; - pc.OnIceCandidate = candidate => - { - _signaling.SendCandidate(connectionId, candidate); - }; - pc.OnConnectionStateChange = state => OnConnectionStateChange(connectionId, state); - pc.OnTrack = trackEvent => - { - onAddTransceiver?.Invoke(connectionId, trackEvent.Transceiver); - }; - pc.OnNegotiationNeeded = () => _startCoroutine(OnNegotiationNeeded(connectionId)); + peer.OnConnectHandler += () => onConnect?.Invoke(connectionId); + peer.OnDisconnectHandler += () => onDisconnect?.Invoke(connectionId); + peer.OnDataChannelHandler += channel => onAddChannel?.Invoke(connectionId, channel);; + peer.OnTrackEventHandler += e => onAddTransceiver?.Invoke(connectionId, e.Transceiver); + peer.SendOfferHandler += desc => _signaling?.SendOffer(connectionId, desc); + peer.SendAnswerHandler += desc => _signaling?.SendAnswer(connectionId, desc); + peer.SendCandidateHandler += candidate => _signaling?.SendCandidate(connectionId, candidate); return peer; } @@ -401,62 +376,6 @@ void DeletePeerConnection(string connectionId) _mapConnectionIdAndPeer.Remove(connectionId); } - void OnDataChannel(string connectionId, RTCDataChannel channel) - { - onAddChannel?.Invoke(connectionId, channel); - } - - void OnConnectionStateChange(string connectionId, RTCPeerConnectionState state) - { - switch (state) - { - case RTCPeerConnectionState.Connected: - onConnect?.Invoke(connectionId); - break; - case RTCPeerConnectionState.Disconnected: - onDisconnect?.Invoke(connectionId); - break; - } - } - - IEnumerator OnNegotiationNeeded(string connectionId) - { - yield return new WaitWhile(() => !IsStable(connectionId)); - SendOffer(connectionId); - } - - IEnumerator SendOfferCoroutine(string connectionId, PeerConnection pc) - { - // waiting other setLocalDescription process - yield return new WaitWhile(() => !IsStable(connectionId)); - - if (!ExistConnection(connectionId)) - yield break; - - Assert.AreEqual(pc.peer.SignalingState, RTCSignalingState.Stable, - $"{pc} negotiationneeded always fires in stable state"); - Assert.AreEqual(pc.makingOffer, false, $"{pc} negotiationneeded not already in progress"); - - pc.makingOffer = true; - var opLocalDesc = pc.peer.SetLocalDescription(); - yield return opLocalDesc; - - if (opLocalDesc.IsError) - { - Debug.LogError($"{pc} {opLocalDesc.Error.message}"); - pc.makingOffer = false; - yield break; - } - - Assert.AreEqual(pc.peer.SignalingState, RTCSignalingState.HaveLocalOffer, - $"{pc} negotiationneeded not racing with onmessage"); - Assert.AreEqual(pc.peer.LocalDescription.type, RTCSdpType.Offer, $"{pc} negotiationneeded SLD worked"); - pc.makingOffer = false; - pc.waitingAnswer = true; - - _signaling.SendOffer(connectionId, pc.peer.LocalDescription); - } - void OnAnswer(ISignaling signaling, DescData e) { if (!_mapConnectionIdAndPeer.TryGetValue(e.connectionId, out var pc)) @@ -465,36 +384,8 @@ void OnAnswer(ISignaling signaling, DescData e) return; } - _startCoroutine(GotAnswerCoroutine(e.connectionId, pc, e.sdp)); - } - - IEnumerator GotAnswerCoroutine(string connectionId, PeerConnection pc, string sdp) - { - var description = new RTCSessionDescription(); - description.type = RTCSdpType.Answer; - description.sdp = sdp; - - // waiting other setLocalDescription process - yield return new WaitWhile(() => pc.makingOffer || pc.makingAnswer); - - pc.waitingAnswer = false; - pc.srdAnswerPending = true; - - var opRemoteDesc = pc.peer.SetRemoteDescription(ref description); - yield return opRemoteDesc; - - if (opRemoteDesc.IsError) - { - Debug.LogError($"{pc} {opRemoteDesc.Error.message}"); - pc.srdAnswerPending = false; - yield break; - } - - Assert.AreEqual(pc.peer.RemoteDescription.type, RTCSdpType.Answer, $"{pc} Answer was set"); - Assert.AreEqual(pc.peer.SignalingState, RTCSignalingState.Stable, $"{pc} answered"); - pc.srdAnswerPending = false; - - onGotAnswer?.Invoke(connectionId, sdp); + RTCSessionDescription description = new RTCSessionDescription {type = RTCSdpType.Answer, sdp = e.sdp}; + _startCoroutine(pc.OnGotDescription(description, () => onGotAnswer?.Invoke(e.connectionId, e.sdp))); } void OnIceCandidate(ISignaling signaling, CandidateData e) @@ -508,11 +399,7 @@ void OnIceCandidate(ISignaling signaling, CandidateData e) { candidate = e.candidate, sdpMLineIndex = e.sdpMLineIndex, sdpMid = e.sdpMid }; - - if (!pc.peer.AddIceCandidate(new RTCIceCandidate(option)) && !pc.ignoreOffer) - { - Debug.LogWarning($"{pc} this candidate can't accept current signaling state {pc.peer.SignalingState}."); - } + pc.OnGotIceCandidate(new RTCIceCandidate(option)); } void OnOffer(ISignaling signaling, DescData e) @@ -523,64 +410,8 @@ void OnOffer(ISignaling signaling, DescData e) pc = CreatePeerConnection(connectionId, e.polite); } - _startCoroutine(GotOfferCoroutine(connectionId, pc, e.sdp)); - } - - IEnumerator GotOfferCoroutine(string connectionId, PeerConnection pc, string sdp) - { - RTCSessionDescription description; - description.type = RTCSdpType.Offer; - description.sdp = sdp; - - var isStable = - pc.peer.SignalingState == RTCSignalingState.Stable || - (pc.peer.SignalingState == RTCSignalingState.HaveLocalOffer && pc.srdAnswerPending); - pc.ignoreOffer = !pc.polite && (pc.makingOffer || !isStable); - if (pc.ignoreOffer || pc.makingAnswer) - { - Debug.LogWarning($"{pc} glare - ignoreOffer {nameof(pc.peer.SignalingState)}:{pc.peer.SignalingState}"); - yield break; - } - - // waiting other setRemoteDescription process - yield return new WaitWhile(() => pc.srdAnswerPending); - pc.waitingAnswer = false; - - var opRemoteDesc = pc.peer.SetRemoteDescription(ref description); - yield return opRemoteDesc; - - if (opRemoteDesc.IsError) - { - Debug.LogError($"{pc} {opRemoteDesc.Error.message}"); - yield break; - } - - Assert.AreEqual(pc.peer.RemoteDescription.type, RTCSdpType.Offer, $"{pc} SRD worked"); - Assert.AreEqual(pc.peer.SignalingState, RTCSignalingState.HaveRemoteOffer, $"{pc} Remote offer"); - - onGotOffer?.Invoke(connectionId, sdp); - } - - IEnumerator SendAnswerCoroutine(string connectionId, PeerConnection pc) - { - pc.makingAnswer = true; - - var opLocalDesc = pc.peer.SetLocalDescription(); - yield return opLocalDesc; - - if (opLocalDesc.IsError) - { - Debug.LogError($"{pc} {opLocalDesc.Error.message}"); - pc.makingAnswer = false; - yield break; - } - - Assert.AreEqual(pc.peer.LocalDescription.type, RTCSdpType.Answer, $"{pc} onmessage SLD worked"); - Assert.AreEqual(pc.peer.SignalingState, RTCSignalingState.Stable, - $"{pc} onmessage not racing with negotiationneeded"); - pc.makingAnswer = false; - - _signaling.SendAnswer(connectionId, pc.peer.LocalDescription); + RTCSessionDescription description = new RTCSessionDescription {type = RTCSdpType.Offer, sdp = e.sdp}; + _startCoroutine(pc.OnGotDescription(description, () => onGotOffer?.Invoke(connectionId, e.sdp))); } } } diff --git a/com.unity.renderstreaming/Tests/Runtime/InputSystem/InputRemotingTest.cs b/com.unity.renderstreaming/Tests/Runtime/InputSystem/InputRemotingTest.cs index ae9814bb9..2bffbbd2f 100644 --- a/com.unity.renderstreaming/Tests/Runtime/InputSystem/InputRemotingTest.cs +++ b/com.unity.renderstreaming/Tests/Runtime/InputSystem/InputRemotingTest.cs @@ -63,6 +63,7 @@ private RenderStreamingDependencies CreateDependencies() iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } }, }, startCoroutine = _test.component.StartCoroutine, + stopCoroutine = _test.component.StopCoroutine, resentOfferInterval = ResendOfferInterval, }; } @@ -176,7 +177,7 @@ public void Receiver() receiverDisposer.Dispose(); } - /// todo(kazuki): This test is failed for timeout on macOS + /// todo(kazuki): This test is failed for timeout on macOS [UnityTest, Timeout(3000)] [UnityPlatform(exclude = new[] { RuntimePlatform.OSXPlayer })] public IEnumerator AddDevice() diff --git a/com.unity.renderstreaming/Tests/Runtime/PeerConnectionTest.cs b/com.unity.renderstreaming/Tests/Runtime/PeerConnectionTest.cs new file mode 100644 index 000000000..a1a6ae201 --- /dev/null +++ b/com.unity.renderstreaming/Tests/Runtime/PeerConnectionTest.cs @@ -0,0 +1,964 @@ +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using Unity.WebRTC; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.RenderStreaming.RuntimeTest +{ + class PeerConnectionTest + { + class MyMonoBehaviourTest : MonoBehaviour, IMonoBehaviourTest + { + public bool IsTestFinished + { + get { return true; } + } + } + + private const float ResendOfferInterval = 1.0f; + private MonoBehaviourTest test; + private RTCConfiguration config; + + [SetUp] + public void SetUp() + { + test = new MonoBehaviourTest(); + config = new RTCConfiguration + { + iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } }, + }; + } + + [TearDown] + public void TearDown() + { + test.component.StopAllCoroutines(); + Object.Destroy(test.gameObject); + } + + [Test, Timeout(5000)] + public void Construct() + { + var peer = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer, Is.Not.Null); + var rtcPeer = peer.peer; + Assert.That(rtcPeer, Is.Not.Null); + Assert.That(rtcPeer.OnTrack, Is.Not.Null); + Assert.That(rtcPeer.OnIceCandidate, Is.Not.Null); + Assert.That(rtcPeer.OnNegotiationNeeded, Is.Not.Null); + Assert.That(rtcPeer.OnDataChannel, Is.Not.Null); + Assert.That(rtcPeer.OnConnectionStateChange, Is.Not.Null); + + peer.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator CalledSendOfferWhenAddTrack() + { + var peer = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer, Is.Not.Null); + + var isGotSendOffer = false; + RTCSessionDescription offerDesc = default; + peer.SendOfferHandler += description => + { + isGotSendOffer = true; + offerDesc = description; + }; + + var track = new AudioStreamTrack(); + peer.peer.AddTrack(track); + + yield return new WaitUntil(() => isGotSendOffer); + Assert.That(isGotSendOffer, Is.True); + Assert.That(offerDesc.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc.sdp, Is.Not.Null.Or.Empty); + + track.Dispose(); + peer.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator CalledSendOfferWhenAddTransceiver() + { + var peer = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer, Is.Not.Null); + + var isGotSendOffer = false; + RTCSessionDescription offerDesc = default; + peer.SendOfferHandler += description => + { + isGotSendOffer = true; + offerDesc = description; + }; + + var transceiver = peer.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer); + Assert.That(isGotSendOffer, Is.True); + Assert.That(offerDesc.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc.sdp, Is.Not.Null.Or.Empty); + + transceiver.Dispose(); + peer.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator CalledSendOfferWhenCreateChannel() + { + var peer = new PeerConnection( + true, + config, + ResendOfferInterval, + test.component.StartCoroutine, + test.component.StopCoroutine + ); + + var isGotSendOffer = false; + RTCSessionDescription offerDesc = default; + peer.SendOfferHandler += description => + { + isGotSendOffer = true; + offerDesc = description; + }; + + var channel = peer.peer.CreateDataChannel("test"); + Assert.That(channel, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer); + Assert.That(isGotSendOffer, Is.True); + Assert.That(offerDesc.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc.sdp, Is.Not.Null.Or.Empty); + + channel.Dispose(); + peer.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator CalledSendOfferTwiceIfGetAnswerNotYet() + { + var peer = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer, Is.Not.Null); + + var sendOfferCount = 0; + RTCSessionDescription offerDesc = default; + peer.SendOfferHandler += description => + { + sendOfferCount++; + offerDesc = description; + }; + + var track = new AudioStreamTrack(); + peer.peer.AddTrack(track); + + while (sendOfferCount <= 2) + { + yield return new WaitForSeconds(ResendOfferInterval); + Assert.That(peer.waitingAnswer, Is.True); + peer.SendOffer(); + } + + yield return new WaitUntil(() => sendOfferCount > 2); + Assert.That(sendOfferCount, Is.GreaterThan(2)); + Assert.That(offerDesc.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc.sdp, Is.Not.Null.Or.Empty); + + track.Dispose(); + peer.Dispose(); + } + + [UnityTest, Timeout(5000)] + [TestCase(true, ExpectedResult = null)] + [TestCase(false, ExpectedResult = null)] + public IEnumerator CalledSendAnswerWhenGotDescriptionWhenStable(bool polite) + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(polite, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer = false; + RTCSessionDescription offerDesc = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer = true; + offerDesc = description; + }; + + var isGotSendAnswer = false; + RTCSessionDescription answerDesc = default; + peer2.SendAnswerHandler += description => + { + isGotSendAnswer = true; + answerDesc = description; + }; + + var track = new AudioStreamTrack(); + peer1.peer.AddTrack(track); + + yield return new WaitUntil(() => isGotSendOffer); + Assert.That(isGotSendOffer, Is.True); + Assert.That(offerDesc.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc.sdp, Is.Not.Null.Or.Empty); + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc, () => isComplete = true); + Assert.That(isComplete, Is.True); + peer2.SendAnswer(); + + yield return new WaitUntil(() => isGotSendAnswer); + Assert.That(isGotSendAnswer, Is.True); + Assert.That(answerDesc.type, Is.EqualTo(RTCSdpType.Answer)); + Assert.That(answerDesc.sdp, Is.Not.Null.Or.Empty); + + track.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator CalledSendAnswerWhenGotDescriptionThatHaveLocalOfferInPolite() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer1 = false; + RTCSessionDescription offerDesc1 = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer1 = true; + offerDesc1 = description; + }; + + var isGotSendOffer2 = false; + RTCSessionDescription offerDesc2 = default; + peer2.SendOfferHandler += description => + { + isGotSendOffer2 = true; + offerDesc2 = description; + }; + + var isGotSendAnswer = false; + RTCSessionDescription answerDesc = default; + peer2.SendAnswerHandler += description => + { + isGotSendAnswer = true; + answerDesc = description; + }; + + var track1 = new AudioStreamTrack(); + peer1.peer.AddTrack(track1); + var track2 = new AudioStreamTrack(); + peer2.peer.AddTrack(track2); + + yield return new WaitUntil(() => isGotSendOffer1); + Assert.That(isGotSendOffer1, Is.True); + Assert.That(offerDesc1.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc1.sdp, Is.Not.Null.Or.Empty); + + yield return new WaitUntil(() => isGotSendOffer2); + Assert.That(isGotSendOffer1, Is.True); + Assert.That(offerDesc2.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc2.sdp, Is.Not.Null.Or.Empty); + Assert.That(peer2.peer.SignalingState, Is.EqualTo(RTCSignalingState.HaveLocalOffer)); + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc1, () => isComplete = true); + Assert.That(isComplete, Is.True); + peer2.SendAnswer(); + + yield return new WaitUntil(() => isGotSendAnswer); + Assert.That(isGotSendAnswer, Is.True); + Assert.That(answerDesc.type, Is.EqualTo(RTCSdpType.Answer)); + Assert.That(answerDesc.sdp, Is.Not.Null.Or.Empty); + Assert.That(peer2.peer.SignalingState, Is.EqualTo(RTCSignalingState.Stable)); + + track1.Dispose(); + track2.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator NotCalledSendAnswerWhenGotDescriptionThatHaveLocalOfferInImpolite() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(false, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer1 = false; + RTCSessionDescription offerDesc1 = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer1 = true; + offerDesc1 = description; + }; + + var isGotSendOffer2 = false; + RTCSessionDescription offerDesc2 = default; + peer2.SendOfferHandler += description => + { + isGotSendOffer2 = true; + offerDesc2 = description; + }; + + var track1 = new AudioStreamTrack(); + peer1.peer.AddTrack(track1); + var track2 = new AudioStreamTrack(); + peer2.peer.AddTrack(track2); + + yield return new WaitUntil(() => isGotSendOffer1); + Assert.That(isGotSendOffer1, Is.True); + Assert.That(offerDesc1.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc1.sdp, Is.Not.Null.Or.Empty); + + yield return new WaitUntil(() => isGotSendOffer2); + Assert.That(isGotSendOffer1, Is.True); + Assert.That(offerDesc2.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc2.sdp, Is.Not.Null.Or.Empty); + Assert.That(peer2.peer.SignalingState, Is.EqualTo(RTCSignalingState.HaveLocalOffer)); + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc1, () => isComplete = true); + Assert.That(isComplete, Is.False); + Assert.That(peer2.peer.SignalingState, Is.EqualTo(RTCSignalingState.HaveLocalOffer)); + + track1.Dispose(); + track2.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator CalledTrackEventWhenGotSdpIncludeTrack() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer = false; + RTCSessionDescription offerDesc = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer = true; + offerDesc = description; + }; + + var isGotTrackEvent = false; + RTCTrackEvent trackEvent = null; + peer2.OnTrackEventHandler += e => + { + isGotTrackEvent = true; + trackEvent = e; + }; + + var track = new AudioStreamTrack(); + peer1.peer.AddTrack(track); + + yield return new WaitUntil(() => isGotSendOffer); + Assert.That(isGotSendOffer, Is.True); + Assert.That(offerDesc.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc.sdp, Is.Not.Null.Or.Empty); + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc, () => isComplete = true); + Assert.That(isComplete, Is.True); + + yield return new WaitUntil(() => isGotTrackEvent); + Assert.That(isGotTrackEvent, Is.True); + Assert.That(trackEvent, Is.Not.Null); + Assert.That(trackEvent.Track.Id, Is.EqualTo(track.Id)); + + track.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator CalledOnDataChannelWhenGotSdpIncludeDataChannel() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer = false; + RTCSessionDescription offerDesc = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer = true; + offerDesc = description; + }; + + var isGotSendAnswer = false; + RTCSessionDescription answerDesc = default; + peer2.SendAnswerHandler += description => + { + isGotSendAnswer = true; + answerDesc = description; + }; + + var isGotDataChannel = false; + RTCDataChannel dataChannel = null; + peer2.OnDataChannelHandler += e => + { + isGotDataChannel = true; + dataChannel = e; + }; + + var channel = peer1.peer.CreateDataChannel("test"); + Assert.That(channel, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer); + Assert.That(isGotSendOffer, Is.True); + Assert.That(offerDesc.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc.sdp, Is.Not.Null.Or.Empty); + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc, () => isComplete = true); + Assert.That(isComplete, Is.True); + + peer2.SendAnswer(); + yield return new WaitUntil(() => isGotSendAnswer); + Assert.That(isGotSendAnswer, Is.True); + Assert.That(answerDesc.type, Is.EqualTo(RTCSdpType.Answer)); + Assert.That(answerDesc.sdp, Is.Not.Null.Or.Empty); + + isComplete = false; + yield return peer1.OnGotDescription(answerDesc, () => isComplete = true); + Assert.That(isComplete, Is.True); + + yield return new WaitUntil(() => isGotDataChannel); + Assert.That(isGotDataChannel, Is.True); + Assert.That(dataChannel, Is.Not.Null); + Assert.That(dataChannel.Id, Is.EqualTo(channel.Id)); + + channel.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator CalledSendCandidateWhenAddTransceiver() + { + var peer = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer, Is.Not.Null); + + var isGotSendCandidate = false; + RTCIceCandidate candidate = null; + peer.SendCandidateHandler += e => + { + isGotSendCandidate = true; + candidate = e; + }; + + var transceiver = peer.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendCandidate); + Assert.That(isGotSendCandidate, Is.True); + Assert.That(candidate, Is.Not.Null); + + transceiver.Dispose(); + peer.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator AcceptWhenGotCandidateThatHaveRemoteDescription() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer1 = false; + RTCSessionDescription offerDesc1 = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer1 = true; + offerDesc1 = description; + }; + + var isGotSendCandidate = false; + RTCIceCandidate candidate = null; + peer1.SendCandidateHandler += e => + { + isGotSendCandidate = true; + candidate = e; + }; + + var transceiver = peer1.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer1); + Assert.That(isGotSendOffer1, Is.True); + Assert.That(offerDesc1.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc1.sdp, Is.Not.Null.Or.Empty); + + yield return new WaitUntil(() => isGotSendCandidate); + Assert.That(isGotSendCandidate, Is.True); + Assert.That(candidate, Is.Not.Null); + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc1, () => isComplete = true); + Assert.That(isComplete, Is.True); + + Assert.That(peer2.OnGotIceCandidate(candidate), Is.True); + + transceiver.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator NotAcceptWhenGotCandidateThatDontHaveRemoteDescription() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer1 = false; + RTCSessionDescription offerDesc1 = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer1 = true; + offerDesc1 = description; + }; + + var isGotSendCandidate = false; + RTCIceCandidate candidate = null; + peer1.SendCandidateHandler += e => + { + isGotSendCandidate = true; + candidate = e; + }; + + var transceiver = peer1.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer1); + Assert.That(isGotSendOffer1, Is.True); + Assert.That(offerDesc1.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc1.sdp, Is.Not.Null.Or.Empty); + + yield return new WaitUntil(() => isGotSendCandidate); + Assert.That(isGotSendCandidate, Is.True); + Assert.That(candidate, Is.Not.Null); + + Assert.That(peer2.OnGotIceCandidate(candidate), Is.False); + + transceiver.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator CalledOnConnect() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer = false; + RTCSessionDescription offerDesc = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer = true; + offerDesc = description; + }; + var isGotConnect1 = false; + peer1.OnConnectHandler += () => isGotConnect1 = true; + + var isGotSendAnswer = false; + RTCSessionDescription answerDesc = default; + peer2.SendAnswerHandler += description => + { + isGotSendAnswer = true; + answerDesc = description; + }; + var isGotConnect2 = false; + peer2.OnConnectHandler += () => isGotConnect2 = true; + + var transceiver = peer1.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer); + Assert.That(isGotSendOffer, Is.True); + Assert.That(offerDesc.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc.sdp, Is.Not.Null.Or.Empty); + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc, () => isComplete = true); + Assert.That(isComplete, Is.True); + + peer2.SendAnswer(); + yield return new WaitUntil(() => isGotSendAnswer); + Assert.That(isGotSendAnswer, Is.True); + Assert.That(answerDesc.type, Is.EqualTo(RTCSdpType.Answer)); + Assert.That(answerDesc.sdp, Is.Not.Null.Or.Empty); + + isComplete = false; + yield return peer1.OnGotDescription(answerDesc, () => isComplete = true); + Assert.That(isComplete, Is.True); + + yield return new WaitUntil(() => isGotConnect1 && isGotConnect2); + Assert.That(isGotConnect1, Is.True); + Assert.That(isGotConnect2, Is.True); + + transceiver.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator SendOfferTwiceImmediately() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + + var isGotSendOffer = false; + RTCSessionDescription offerDesc = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer = true; + offerDesc = description; + }; + + var transceiver = peer1.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver, Is.Not.Null); + + peer1.SendOffer(); + peer1.SendOffer(); + + yield return new WaitUntil(() => isGotSendOffer); + Assert.That(isGotSendOffer, Is.True); + Assert.That(offerDesc.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc.sdp, Is.Not.Null.Or.Empty); + + transceiver.Dispose(); + peer1.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator SendAnswerTwiceImmediately() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer = false; + RTCSessionDescription offerDesc = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer = true; + offerDesc = description; + }; + + var isGotSendAnswer = false; + RTCSessionDescription answerDesc = default; + peer2.SendAnswerHandler += description => + { + isGotSendAnswer = true; + answerDesc = description; + }; + + var transceiver = peer1.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer); + Assert.That(isGotSendOffer, Is.True); + Assert.That(offerDesc.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc.sdp, Is.Not.Null.Or.Empty); + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc, () => isComplete = true); + Assert.That(isComplete, Is.True); + + peer2.SendAnswer(); + peer2.SendAnswer(); + + yield return new WaitUntil(() => isGotSendAnswer); + Assert.That(isGotSendAnswer, Is.True); + Assert.That(answerDesc.type, Is.EqualTo(RTCSdpType.Answer)); + Assert.That(answerDesc.sdp, Is.Not.Null.Or.Empty); + + transceiver.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator OnGotOfferDescriptionAfterSendOfferImmediatelyInPolite() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer1 = false; + RTCSessionDescription offerDesc1 = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer1 = true; + offerDesc1 = description; + }; + + var isGotSendOffer2 = false; + RTCSessionDescription offerDesc2 = default; + peer2.SendOfferHandler += description => + { + isGotSendOffer2 = true; + offerDesc2 = description; + }; + + var isGotSendAnswer = false; + RTCSessionDescription answerDesc = default; + peer2.SendAnswerHandler += description => + { + isGotSendAnswer = true; + answerDesc = description; + }; + + var transceiver1 = peer1.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver1, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer1); + Assert.That(isGotSendOffer1, Is.True); + Assert.That(offerDesc1.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc1.sdp, Is.Not.Null.Or.Empty); + + var transceiver2 = peer2.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver2, Is.Not.Null); + + //workaround: Need to wait 1 frame for negotiationneeded to be processed + yield return 0; + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc1, () => isComplete = true); + Assert.That(isComplete, Is.True); + + yield return new WaitForSeconds(ResendOfferInterval); + Assert.That(isGotSendOffer2, Is.False, "need waiting offer cause receive remote offer"); + + peer2.SendAnswer(); + yield return new WaitUntil(() => isGotSendAnswer); + Assert.That(isGotSendAnswer, Is.True); + Assert.That(answerDesc.type, Is.EqualTo(RTCSdpType.Answer)); + Assert.That(answerDesc.sdp, Is.Not.Null.Or.Empty); + + yield return new WaitUntil(() => isGotSendOffer2); + Assert.That(isGotSendOffer2, Is.True); + Assert.That(offerDesc2.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc2.sdp, Is.Not.Null.Or.Empty); + + transceiver1.Dispose(); + transceiver2.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + public IEnumerator OnGotOfferDescriptionAfterSendOfferImmediatelyInImPolite() + { + var peer1 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(false, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer1 = false; + RTCSessionDescription offerDesc1 = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer1 = true; + offerDesc1 = description; + }; + + var isGotSendOffer2 = false; + RTCSessionDescription offerDesc2 = default; + peer2.SendOfferHandler += description => + { + isGotSendOffer2 = true; + offerDesc2 = description; + }; + + var transceiver1 = peer1.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver1, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer1); + Assert.That(isGotSendOffer1, Is.True); + Assert.That(offerDesc1.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc1.sdp, Is.Not.Null.Or.Empty); + + var transceiver2 = peer2.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver2, Is.Not.Null); + + //workaround: Need to wait 1 frame for negotiationneeded to be processed + yield return 0; + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc1, () => isComplete = true); + Assert.That(isComplete, Is.False, "need ignore offer cause peer2 is impolite"); + + yield return new WaitUntil(() => isGotSendOffer2); + Assert.That(isGotSendOffer2, Is.True); + Assert.That(offerDesc2.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc2.sdp, Is.Not.Null.Or.Empty); + + transceiver1.Dispose(); + transceiver2.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + [TestCase(true, ExpectedResult = null)] + [TestCase(false, ExpectedResult = null)] + public IEnumerator OnGotAnswerDescriptionAfterSendOfferImmediately(bool polite) + { + var peer1 = new PeerConnection(polite, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer1 = false; + RTCSessionDescription offerDesc1 = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer1 = true; + offerDesc1 = description; + }; + + var isGotSendAnswer = false; + RTCSessionDescription answerDesc = default; + peer2.SendAnswerHandler += description => + { + isGotSendAnswer = true; + answerDesc = description; + }; + + var transceiver1 = peer1.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver1, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer1); + Assert.That(isGotSendOffer1, Is.True); + Assert.That(offerDesc1.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc1.sdp, Is.Not.Null.Or.Empty); + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc1, () => isComplete = true); + Assert.That(isComplete, Is.True); + + peer2.SendAnswer(); + yield return new WaitUntil(() => isGotSendAnswer); + Assert.That(isGotSendAnswer, Is.True); + Assert.That(answerDesc.type, Is.EqualTo(RTCSdpType.Answer)); + Assert.That(answerDesc.sdp, Is.Not.Null.Or.Empty); + + yield return new WaitForSeconds(ResendOfferInterval); + peer1.SendOffer(); + + isComplete = false; + yield return peer1.OnGotDescription(answerDesc, () => isComplete = true); + Assert.That(isComplete, Is.True); + + transceiver1.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + + [UnityTest, Timeout(5000)] + [TestCase(true, ExpectedResult = null)] + [TestCase(false, ExpectedResult = null)] + public IEnumerator OnGotOfferDescriptionAfterSendAnswerImmediately(bool polite) + { + var peer1 = new PeerConnection(polite, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer1, Is.Not.Null); + var peer2 = new PeerConnection(true, config, ResendOfferInterval, + test.component.StartCoroutine, test.component.StopCoroutine); + Assert.That(peer2, Is.Not.Null); + + var isGotSendOffer1 = false; + RTCSessionDescription offerDesc1 = default; + peer1.SendOfferHandler += description => + { + isGotSendOffer1 = true; + offerDesc1 = description; + }; + + var isGotSendAnswer = false; + RTCSessionDescription answerDesc = default; + peer2.SendAnswerHandler += description => + { + isGotSendAnswer = true; + answerDesc = description; + }; + + var transceiver1 = peer1.peer.AddTransceiver(TrackKind.Video); + Assert.That(transceiver1, Is.Not.Null); + + yield return new WaitUntil(() => isGotSendOffer1); + Assert.That(isGotSendOffer1, Is.True); + Assert.That(offerDesc1.type, Is.EqualTo(RTCSdpType.Offer)); + Assert.That(offerDesc1.sdp, Is.Not.Null.Or.Empty); + + var isComplete = false; + yield return peer2.OnGotDescription(offerDesc1, () => isComplete = true); + Assert.That(isComplete, Is.True); + peer2.SendAnswer(); + + isComplete = false; + yield return peer2.OnGotDescription(offerDesc1, () => isComplete = true); + Assert.That(isComplete, Is.True); + + yield return new WaitUntil(() => isGotSendAnswer); + Assert.That(isGotSendAnswer, Is.True); + Assert.That(answerDesc.type, Is.EqualTo(RTCSdpType.Answer)); + Assert.That(answerDesc.sdp, Is.Not.Null.Or.Empty); + + transceiver1.Dispose(); + peer1.Dispose(); + peer2.Dispose(); + } + } +} diff --git a/com.unity.renderstreaming/Tests/Runtime/PeerConnectionTest.cs.meta b/com.unity.renderstreaming/Tests/Runtime/PeerConnectionTest.cs.meta new file mode 100644 index 000000000..d29b5c2aa --- /dev/null +++ b/com.unity.renderstreaming/Tests/Runtime/PeerConnectionTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ba9cd9098d364eecbabbf6d1c678b2d5 +timeCreated: 1661734624 \ No newline at end of file diff --git a/com.unity.renderstreaming/Tests/Runtime/RenderStreamingInternalTest.cs b/com.unity.renderstreaming/Tests/Runtime/RenderStreamingInternalTest.cs index badac2480..a00eb5323 100644 --- a/com.unity.renderstreaming/Tests/Runtime/RenderStreamingInternalTest.cs +++ b/com.unity.renderstreaming/Tests/Runtime/RenderStreamingInternalTest.cs @@ -62,6 +62,7 @@ private RenderStreamingDependencies CreateDependencies() iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } }, }, startCoroutine = test.component.StartCoroutine, + stopCoroutine = test.component.StopCoroutine, resentOfferInterval = ResendOfferInterval, }; } diff --git a/com.unity.renderstreaming/Tests/Runtime/SignalingHandlerTest.cs b/com.unity.renderstreaming/Tests/Runtime/SignalingHandlerTest.cs index 4ae8e6151..78669b507 100644 --- a/com.unity.renderstreaming/Tests/Runtime/SignalingHandlerTest.cs +++ b/com.unity.renderstreaming/Tests/Runtime/SignalingHandlerTest.cs @@ -101,6 +101,7 @@ private static RenderStreamingDependencies CreateDependencies(MonoBehaviour beha iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } }, }, startCoroutine = behaviour.StartCoroutine, + stopCoroutine = behaviour.StopCoroutine, resentOfferInterval = ResendOfferInterval, }; }