using System; using System.Collections; using System.Collections.Generic; using Unity.WebRTC; using Unity.WebRTC.Samples; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.UI; class PerfectNegotiationSample : MonoBehaviour { #pragma warning disable 0649 [SerializeField] private Button politeSwapButton; [SerializeField] private Button impoliteSwapButton; [SerializeField] private Button swapPoliteFirstButton; [SerializeField] private Button swapImpoliteFirstButton; [SerializeField] private Camera politeSourceCamera1; [SerializeField] private Camera politeSourceCamera2; [SerializeField] private Camera impoliteSourceCamera1; [SerializeField] private Camera impoliteSourceCamera2; [SerializeField] private RawImage politeSourceImage1; [SerializeField] private RawImage politeSourceImage2; [SerializeField] private RawImage impoliteSourceImage1; [SerializeField] private RawImage impoliteSourceImage2; [SerializeField] private RawImage politeReceiveImage; [SerializeField] private RawImage impoliteReceiveImage; #pragma warning restore 0649 private Peer politePeer, impolitePeer; private readonly Color red = Color.red; private readonly Color magenta = Color.magenta; private readonly Color blue = Color.blue; private readonly Color cyan = Color.cyan; private readonly Color green = Color.green; private readonly Color yellow = Color.yellow; private int count = 0; private const int MAX = 120; private float lerp = 0.0f; private void OnDestroy() { politePeer?.Dispose(); impolitePeer?.Dispose(); } private void Start() { politePeer = new Peer(this, true, politeSourceCamera1, politeSourceCamera2, politeReceiveImage); impolitePeer = new Peer(this, false, impoliteSourceCamera1, impoliteSourceCamera2, impoliteReceiveImage); politeSourceImage1.texture = politeSourceCamera1.targetTexture; politeSourceImage2.texture = politeSourceCamera2.targetTexture; impoliteSourceImage1.texture = impoliteSourceCamera1.targetTexture; impoliteSourceImage2.texture = impoliteSourceCamera2.targetTexture; politeSwapButton.onClick.AddListener(politePeer.SwapTransceivers); impoliteSwapButton.onClick.AddListener(impolitePeer.SwapTransceivers); swapPoliteFirstButton.onClick.AddListener(() => { politePeer.SwapTransceivers(); impolitePeer.SwapTransceivers(); }); swapImpoliteFirstButton.onClick.AddListener(() => { impolitePeer.SwapTransceivers(); politePeer.SwapTransceivers(); }); StartCoroutine(WebRTC.Update()); } private void Update() { count++; count %= MAX; lerp = (float)count / MAX; politeSourceCamera1.backgroundColor = Color.LerpUnclamped(red, magenta, lerp); politeSourceCamera2.backgroundColor = Color.LerpUnclamped(magenta, yellow, lerp); impoliteSourceCamera1.backgroundColor = Color.LerpUnclamped(blue, cyan, lerp); impoliteSourceCamera2.backgroundColor = Color.LerpUnclamped(cyan, green, lerp); } void PostMessage(Peer from, Message message) { var other = from == politePeer ? impolitePeer : politePeer; other.OnMessage(message); } class Message { public RTCSessionDescription description; public RTCIceCandidate candidate; } class Peer : IDisposable { private readonly PerfectNegotiationSample parent; private readonly bool polite; private readonly RTCPeerConnection pc; private readonly VideoStreamTrack sourceVideoTrack1; private readonly VideoStreamTrack sourceVideoTrack2; private readonly List sendingTransceiverList = new List(); private bool makingOffer; private bool ignoreOffer; private bool srdAnswerPending; private bool sldGetBackStable; private const int width = 256; private const int height = 256; public Peer( PerfectNegotiationSample parent, bool polite, Camera source1, Camera source2, RawImage receive) { this.parent = parent; this.polite = polite; var config = GetSelectedSdpSemantics(); pc = new RTCPeerConnection(ref config); pc.OnTrack = e => { Debug.Log($"{this} OnTrack"); if (e.Track is VideoStreamTrack video) { video.OnVideoReceived += tex => { receive.texture = tex; }; } }; pc.OnIceCandidate = candidate => { var message = new Message { candidate = candidate }; this.parent.PostMessage(this, message); }; pc.OnNegotiationNeeded = () => { this.parent.StartCoroutine(NegotiationProcess()); }; sourceVideoTrack1 = source1.CaptureStreamTrack(width, height); sourceVideoTrack2 = source2.CaptureStreamTrack(width, height); } private IEnumerator NegotiationProcess() { Debug.Log($"{this} SLD due to negotiationneeded"); yield return new WaitWhile(() => sldGetBackStable); Assert.AreEqual(pc.SignalingState, RTCSignalingState.Stable, $"{this} negotiationneeded always fires in stable state"); Assert.AreEqual(makingOffer, false, $"{this} negotiationneeded not already in progress"); makingOffer = true; var op = pc.SetLocalDescription(); yield return op; if (op.IsError) { Debug.LogError($"{this} {op.Error.message}"); makingOffer = false; yield break; } Assert.AreEqual(pc.SignalingState, RTCSignalingState.HaveLocalOffer, $"{this} negotiationneeded always fires in stable state"); Assert.AreEqual(pc.LocalDescription.type, RTCSdpType.Offer, $"{this} negotiationneeded SLD worked"); makingOffer = false; var offer = new Message { description = pc.LocalDescription }; parent.PostMessage(this, offer); } public void Dispose() { sendingTransceiverList.Clear(); sourceVideoTrack1.Dispose(); sourceVideoTrack2.Dispose(); pc.Dispose(); } public override string ToString() { var str = polite ? "polite" : "impolite"; return $"[{str}-{base.ToString()}]"; } private static RTCConfiguration GetSelectedSdpSemantics() { RTCConfiguration config = default; config.iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } }; return config; } public void OnMessage(Message message) { if (message.candidate != null) { if (!pc.AddIceCandidate(message.candidate) && !ignoreOffer) { Debug.LogError($"{this} this candidate can't accept current signaling state {pc.SignalingState}."); } return; } parent.StartCoroutine(OfferAnswerProcess(message.description)); } private IEnumerator OfferAnswerProcess(RTCSessionDescription description) { var isStable = pc.SignalingState == RTCSignalingState.Stable || (pc.SignalingState == RTCSignalingState.HaveLocalOffer && srdAnswerPending); ignoreOffer = description.type == RTCSdpType.Offer && !polite && (makingOffer || !isStable); if (ignoreOffer) { Debug.Log($"{this} glare - ignoring offer"); yield break; } yield return new WaitWhile(() => makingOffer); srdAnswerPending = description.type == RTCSdpType.Answer; Debug.Log($"{this} SRD {description.type} SignalingState {pc.SignalingState}"); var op1 = pc.SetRemoteDescription(ref description); yield return op1; Assert.IsFalse(op1.IsError, $"{this} {op1.Error.message}"); srdAnswerPending = false; if (description.type == RTCSdpType.Offer) { Assert.AreEqual(pc.RemoteDescription.type, RTCSdpType.Offer, $"{this} SRD worked"); Assert.AreEqual(pc.SignalingState, RTCSignalingState.HaveRemoteOffer, $"{this} Remote offer"); Debug.Log($"{this} SLD to get back to stable"); sldGetBackStable = true; var op2 = pc.SetLocalDescription(); yield return op2; Assert.IsFalse(op2.IsError, $"{this} {op2.Error.message}"); Assert.AreEqual(pc.LocalDescription.type, RTCSdpType.Answer, $"{this} onmessage SLD worked"); Assert.AreEqual(pc.SignalingState, RTCSignalingState.Stable, $"{this} onmessage not racing with negotiationneeded"); sldGetBackStable = false; var answer = new Message { description = pc.LocalDescription }; parent.PostMessage(this, answer); } else { Assert.AreEqual(pc.RemoteDescription.type, RTCSdpType.Answer, $"{this} Answer was set"); Assert.AreEqual(pc.SignalingState, RTCSignalingState.Stable, $"{this} answered"); } } public void SwapTransceivers() { Debug.Log($"{this} swapTransceivers"); if (sendingTransceiverList.Count == 0) { var transceiver1 = pc.AddTransceiver(sourceVideoTrack1); transceiver1.Direction = RTCRtpTransceiverDirection.SendOnly; var transceiver2 = pc.AddTransceiver(sourceVideoTrack2); transceiver2.Direction = RTCRtpTransceiverDirection.Inactive; if (WebRTCSettings.UseVideoCodec != null) { var codecs = new[] { WebRTCSettings.UseVideoCodec }; transceiver1.SetCodecPreferences(codecs); transceiver2.SetCodecPreferences(codecs); } sendingTransceiverList.Add(transceiver1); sendingTransceiverList.Add(transceiver2); return; } if (sendingTransceiverList[0].CurrentDirection == RTCRtpTransceiverDirection.SendOnly) { sendingTransceiverList[0].Direction = RTCRtpTransceiverDirection.Inactive; sendingTransceiverList[0].Sender.ReplaceTrack(null); sendingTransceiverList[1].Direction = RTCRtpTransceiverDirection.SendOnly; sendingTransceiverList[1].Sender.ReplaceTrack(sourceVideoTrack2); } else { sendingTransceiverList[1].Direction = RTCRtpTransceiverDirection.Inactive; sendingTransceiverList[1].Sender.ReplaceTrack(null); sendingTransceiverList[0].Direction = RTCRtpTransceiverDirection.SendOnly; sendingTransceiverList[0].Sender.ReplaceTrack(sourceVideoTrack1); } } } }