2026-03-18 20:09:32 +03:00

307 lines
11 KiB
C#

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<RTCRtpTransceiver> sendingTransceiverList = new List<RTCRtpTransceiver>();
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);
}
}
}
}