ReplaceTrackSample.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. using System;
  2. using System.Linq;
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. using UnityEngine;
  6. using Unity.WebRTC;
  7. using Unity.WebRTC.Samples;
  8. using UnityEngine.Assertions;
  9. using UnityEngine.UI;
  10. class ReplaceTrackSample : MonoBehaviour
  11. {
  12. #pragma warning disable 0649
  13. [SerializeField] private Button startButton;
  14. [SerializeField] private Button stopButton;
  15. [SerializeField] private Button switchButton;
  16. [SerializeField] private Camera camera1;
  17. [SerializeField] private Camera camera2;
  18. [SerializeField] private RawImage sourceImage1;
  19. [SerializeField] private RawImage sourceImage2;
  20. [SerializeField] private RawImage receiveImage;
  21. [SerializeField] private Dropdown dropdown1;
  22. [SerializeField] private Dropdown dropdown2;
  23. [SerializeField] private Text textSourceImage1;
  24. [SerializeField] private Text textSourceImage2;
  25. [SerializeField] private Text textReceiveImage;
  26. [SerializeField] private Transform[] rotateObjects;
  27. #pragma warning restore 0649
  28. List<Vector2Int> streamSizeList = new List<Vector2Int>()
  29. {
  30. new Vector2Int(640, 360),
  31. new Vector2Int(1280, 720),
  32. new Vector2Int(1920, 1080),
  33. new Vector2Int(2560, 1440),
  34. new Vector2Int(3840, 2160),
  35. };
  36. private Peer peer1, peer2;
  37. private Vector2Int size1, size2;
  38. private void Awake()
  39. {
  40. WebRTC.Initialize(WebRTCSettings.LimitTextureSize);
  41. }
  42. private void OnDestroy()
  43. {
  44. peer1?.Dispose();
  45. peer2?.Dispose();
  46. WebRTC.Dispose();
  47. }
  48. private void Start()
  49. {
  50. size1 = streamSizeList[dropdown1.value];
  51. size2 = streamSizeList[dropdown2.value];
  52. dropdown1.options = streamSizeList.Select(size => new Dropdown.OptionData($" {size.x} x {size.y} ")).ToList();
  53. dropdown2.options = streamSizeList.Select(size => new Dropdown.OptionData($" {size.x} x {size.y} ")).ToList();
  54. dropdown1.onValueChanged.AddListener(value => size1 = streamSizeList[value]);
  55. dropdown2.onValueChanged.AddListener(value => size2 = streamSizeList[value]);
  56. dropdown1.value = 0;
  57. dropdown2.value = 1;
  58. startButton.onClick.AddListener(OnStart);
  59. startButton.gameObject.SetActive(true);
  60. stopButton.onClick.AddListener(OnStop);
  61. stopButton.gameObject.SetActive(false);
  62. switchButton.onClick.AddListener(OnSwitchTrack);
  63. switchButton.interactable = false;
  64. StartCoroutine(WebRTC.Update());
  65. }
  66. private void OnStart()
  67. {
  68. peer1 = new Peer(this, true, camera1, size1, camera2, size2);
  69. peer2 = new Peer(this, false, receiveImage);
  70. sourceImage1.texture = camera1.targetTexture;
  71. sourceImage2.texture = camera2.targetTexture;
  72. sourceImage1.color = Color.white;
  73. sourceImage2.color = Color.white;
  74. receiveImage.color = Color.white;
  75. dropdown1.interactable = false;
  76. dropdown2.interactable = false;
  77. startButton.gameObject.SetActive(false);
  78. stopButton.gameObject.SetActive(true);
  79. switchButton.interactable = true;
  80. }
  81. private void OnStop()
  82. {
  83. peer1.Dispose();
  84. peer2.Dispose();
  85. sourceImage1.color = Color.black;
  86. sourceImage2.color = Color.black;
  87. receiveImage.color = Color.black;
  88. dropdown1.interactable = true;
  89. dropdown2.interactable = true;
  90. startButton.gameObject.SetActive(true);
  91. stopButton.gameObject.SetActive(false);
  92. switchButton.interactable = false;
  93. }
  94. private void OnSwitchTrack()
  95. {
  96. peer1.SwitchTrack();
  97. }
  98. private void Update()
  99. {
  100. if (rotateObjects != null)
  101. {
  102. foreach (var rotateObject in rotateObjects)
  103. {
  104. float t = Time.deltaTime;
  105. rotateObject.Rotate(100 * t, 200 * t, 300 * t);
  106. }
  107. }
  108. string TextureResolutionText(RawImage image)
  109. {
  110. if (image != null && image.texture != null)
  111. return string.Format($"{image.texture.width}x{image.texture.height}");
  112. return string.Empty;
  113. }
  114. textSourceImage1.text = TextureResolutionText(sourceImage1);
  115. textSourceImage2.text = TextureResolutionText(sourceImage2);
  116. textReceiveImage.text = TextureResolutionText(receiveImage);
  117. }
  118. void PostMessage(Peer from, Message message)
  119. {
  120. var other = from == peer1 ? peer2 : peer1;
  121. other.OnMessage(message);
  122. }
  123. class Message
  124. {
  125. public RTCSessionDescription description;
  126. public RTCIceCandidate candidate;
  127. }
  128. class Peer : IDisposable
  129. {
  130. private readonly ReplaceTrackSample parent;
  131. private readonly bool polite;
  132. private readonly RTCPeerConnection pc;
  133. private readonly VideoStreamTrack sourceVideoTrack1;
  134. private readonly VideoStreamTrack sourceVideoTrack2;
  135. RTCRtpTransceiver sendingTransceiver = null;
  136. private bool makingOffer;
  137. private bool ignoreOffer;
  138. private bool srdAnswerPending;
  139. private bool sldGetBackStable;
  140. Peer(ReplaceTrackSample parent, bool polite)
  141. {
  142. this.parent = parent;
  143. this.polite = polite;
  144. var config = GetSelectedSdpSemantics();
  145. pc = new RTCPeerConnection(ref config);
  146. pc.OnIceCandidate = candidate =>
  147. {
  148. var message = new Message { candidate = candidate };
  149. this.parent.PostMessage(this, message);
  150. };
  151. pc.OnNegotiationNeeded = () =>
  152. {
  153. this.parent.StartCoroutine(NegotiationProcess());
  154. };
  155. }
  156. public Peer(ReplaceTrackSample parent, bool polite, RawImage receive)
  157. : this(parent, polite)
  158. {
  159. pc.OnTrack = e =>
  160. {
  161. if (e.Track is VideoStreamTrack video)
  162. {
  163. video.OnVideoReceived += tex =>
  164. {
  165. receive.texture = tex;
  166. };
  167. }
  168. };
  169. }
  170. public Peer(
  171. ReplaceTrackSample parent,
  172. bool polite,
  173. Camera source1,
  174. Vector2Int size1,
  175. Camera source2,
  176. Vector2Int size2)
  177. : this(parent, polite)
  178. {
  179. sourceVideoTrack1 = source1?.CaptureStreamTrack(size1.x, size1.y);
  180. sourceVideoTrack2 = source2?.CaptureStreamTrack(size2.x, size2.y);
  181. }
  182. private IEnumerator NegotiationProcess()
  183. {
  184. Debug.Log($"{this} SLD due to negotiationneeded");
  185. yield return new WaitWhile(() => sldGetBackStable);
  186. Assert.AreEqual(pc.SignalingState, RTCSignalingState.Stable,
  187. $"{this} negotiationneeded always fires in stable state");
  188. Assert.AreEqual(makingOffer, false, $"{this} negotiationneeded not already in progress");
  189. makingOffer = true;
  190. var op = pc.SetLocalDescription();
  191. yield return op;
  192. if (op.IsError)
  193. {
  194. Debug.LogError($"{this} {op.Error.message}");
  195. makingOffer = false;
  196. yield break;
  197. }
  198. Assert.AreEqual(pc.SignalingState, RTCSignalingState.HaveLocalOffer,
  199. $"{this} negotiationneeded always fires in stable state");
  200. Assert.AreEqual(pc.LocalDescription.type, RTCSdpType.Offer, $"{this} negotiationneeded SLD worked");
  201. makingOffer = false;
  202. var offer = new Message { description = pc.LocalDescription };
  203. parent.PostMessage(this, offer);
  204. }
  205. public void Dispose()
  206. {
  207. sourceVideoTrack1?.Dispose();
  208. sourceVideoTrack2?.Dispose();
  209. pc.Dispose();
  210. }
  211. public override string ToString()
  212. {
  213. var str = polite ? "polite" : "impolite";
  214. return $"[{str}-{base.ToString()}]";
  215. }
  216. private static RTCConfiguration GetSelectedSdpSemantics()
  217. {
  218. RTCConfiguration config = default;
  219. config.iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } };
  220. return config;
  221. }
  222. public void OnMessage(Message message)
  223. {
  224. if (message.candidate != null)
  225. {
  226. if (!pc.AddIceCandidate(message.candidate) && !ignoreOffer)
  227. {
  228. Debug.LogError($"{this} this candidate can't accept current signaling state {pc.SignalingState}, ignoreOffer {ignoreOffer}.");
  229. }
  230. return;
  231. }
  232. parent.StartCoroutine(OfferAnswerProcess(message.description));
  233. }
  234. private IEnumerator OfferAnswerProcess(RTCSessionDescription description)
  235. {
  236. var isStable =
  237. pc.SignalingState == RTCSignalingState.Stable ||
  238. (pc.SignalingState == RTCSignalingState.HaveLocalOffer && srdAnswerPending);
  239. ignoreOffer =
  240. description.type == RTCSdpType.Offer && !polite && (makingOffer || !isStable);
  241. if (ignoreOffer)
  242. {
  243. Debug.Log($"{this} glare - ignoring offer");
  244. yield break;
  245. }
  246. yield return new WaitWhile(() => makingOffer);
  247. srdAnswerPending = description.type == RTCSdpType.Answer;
  248. Debug.Log($"{this} SRD {description.type} SignalingState {pc.SignalingState}");
  249. var op1 = pc.SetRemoteDescription(ref description);
  250. yield return op1;
  251. Assert.IsFalse(op1.IsError, $"{this} {op1.Error.message}");
  252. srdAnswerPending = false;
  253. if (description.type == RTCSdpType.Offer)
  254. {
  255. Assert.AreEqual(pc.RemoteDescription.type, RTCSdpType.Offer, $"{this} SRD worked");
  256. Assert.AreEqual(pc.SignalingState, RTCSignalingState.HaveRemoteOffer, $"{this} Remote offer");
  257. Debug.Log($"{this} SLD to get back to stable");
  258. sldGetBackStable = true;
  259. var op2 = pc.SetLocalDescription();
  260. yield return op2;
  261. Assert.IsFalse(op2.IsError, $"{this} {op2.Error.message}");
  262. Assert.AreEqual(pc.LocalDescription.type, RTCSdpType.Answer, $"{this} onmessage SLD worked");
  263. Assert.AreEqual(pc.SignalingState, RTCSignalingState.Stable,
  264. $"{this} onmessage not racing with negotiationneeded");
  265. sldGetBackStable = false;
  266. var answer = new Message { description = pc.LocalDescription };
  267. parent.PostMessage(this, answer);
  268. }
  269. else
  270. {
  271. Assert.AreEqual(pc.RemoteDescription.type, RTCSdpType.Answer, $"{this} Answer was set");
  272. Assert.AreEqual(pc.SignalingState, RTCSignalingState.Stable, $"{this} answered");
  273. }
  274. }
  275. public void SwitchTrack()
  276. {
  277. if (sendingTransceiver == null)
  278. {
  279. var transceiver = pc.AddTransceiver(sourceVideoTrack1);
  280. transceiver.Direction = RTCRtpTransceiverDirection.SendRecv;
  281. if (WebRTCSettings.UseVideoCodec != null)
  282. {
  283. var codecs = new[] { WebRTCSettings.UseVideoCodec };
  284. transceiver.SetCodecPreferences(codecs);
  285. }
  286. sendingTransceiver = transceiver;
  287. return;
  288. }
  289. var nextTrack = sendingTransceiver.Sender.Track == sourceVideoTrack1 ? sourceVideoTrack2 : sourceVideoTrack1;
  290. sendingTransceiver.Sender.ReplaceTrack(nextTrack);
  291. }
  292. }
  293. }