E2ELatencySample.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. using Unity.WebRTC;
  6. using Unity.WebRTC.Samples;
  7. using UnityEngine.UI;
  8. using Button = UnityEngine.UI.Button;
  9. class E2ELatencySample : MonoBehaviour
  10. {
  11. #pragma warning disable 0649
  12. [SerializeField] private Button startButton;
  13. [SerializeField] private Button callButton;
  14. [SerializeField] private Button hangUpButton;
  15. [SerializeField] private Text textLocalTimestamp;
  16. [SerializeField] private Text textRemoteTimestamp;
  17. [SerializeField] private Text textLatency;
  18. [SerializeField] private Text textAverageLatency;
  19. [SerializeField] private Dropdown dropDownFramerate;
  20. [SerializeField] private BarcodeEncoder encoder;
  21. [SerializeField] private BarcodeDecoder decoder;
  22. [SerializeField] private RawImage sourceImage;
  23. [SerializeField] private RawImage receiveImage;
  24. #pragma warning restore 0649
  25. private RTCPeerConnection _pc1, _pc2;
  26. private MediaStream sendStream, receiveStream;
  27. private DelegateOnIceConnectionChange pc1OnIceConnectionChange;
  28. private DelegateOnIceConnectionChange pc2OnIceConnectionChange;
  29. private DelegateOnIceCandidate pc1OnIceCandidate;
  30. private DelegateOnIceCandidate pc2OnIceCandidate;
  31. private DelegateOnTrack pc2Ontrack;
  32. private DelegateOnNegotiationNeeded pc1OnNegotiationNeeded;
  33. private bool videoUpdateStarted;
  34. RenderTexture destTexture;
  35. int averageLantecy = 0;
  36. Queue<int> queueLatency = new Queue<int>(10);
  37. int vSyncCount;
  38. int targetFrameRate;
  39. List<int> listFramerate = new List<int>()
  40. {
  41. 15,
  42. 30,
  43. 60,
  44. 90
  45. };
  46. private void Awake()
  47. {
  48. WebRTC.Initialize(WebRTCSettings.LimitTextureSize);
  49. startButton.onClick.AddListener(OnStart);
  50. callButton.onClick.AddListener(OnCall);
  51. hangUpButton.onClick.AddListener(OnHangUp);
  52. }
  53. private void OnDestroy()
  54. {
  55. // Revert global settings
  56. QualitySettings.vSyncCount = vSyncCount;
  57. Application.targetFrameRate = targetFrameRate;
  58. WebRTC.Dispose();
  59. }
  60. private void Start()
  61. {
  62. // This sample uses Compute Shader.
  63. if (!SystemInfo.supportsComputeShaders)
  64. throw new System.NotSupportedException("Compute shader is not supported on this device");
  65. vSyncCount = QualitySettings.vSyncCount;
  66. targetFrameRate = Application.targetFrameRate;
  67. callButton.interactable = false;
  68. hangUpButton.interactable = false;
  69. dropDownFramerate.interactable = true;
  70. dropDownFramerate.options =
  71. listFramerate.Select(_ => new Dropdown.OptionData($"{_}")).ToList();
  72. dropDownFramerate.value = 1;
  73. dropDownFramerate.onValueChanged.AddListener(OnFramerateChanged);
  74. OnFramerateChanged(dropDownFramerate.value);
  75. pc1OnIceConnectionChange = state => { OnIceConnectionChange(_pc1, state); };
  76. pc2OnIceConnectionChange = state => { OnIceConnectionChange(_pc2, state); };
  77. pc1OnIceCandidate = candidate => { OnIceCandidate(_pc1, candidate); };
  78. pc2OnIceCandidate = candidate => { OnIceCandidate(_pc2, candidate); };
  79. pc2Ontrack = e =>
  80. {
  81. receiveStream.AddTrack(e.Track);
  82. };
  83. pc1OnNegotiationNeeded = () => { StartCoroutine(PeerNegotiationNeeded(_pc1)); };
  84. StartCoroutine(WebRTC.Update());
  85. }
  86. private void OnFramerateChanged(int value)
  87. {
  88. // Set "Don't Sync" for changing framerate,
  89. // but iOS ignores this setting
  90. QualitySettings.vSyncCount = 0;
  91. Application.targetFrameRate = listFramerate[value];
  92. }
  93. private void OnStart()
  94. {
  95. startButton.interactable = false;
  96. callButton.interactable = true;
  97. dropDownFramerate.interactable = false;
  98. if (sendStream == null)
  99. {
  100. int width = WebRTCSettings.StreamSize.x;
  101. int height = WebRTCSettings.StreamSize.y;
  102. var format = WebRTC.GetSupportedGraphicsFormat(SystemInfo.graphicsDeviceType);
  103. var tex = new RenderTexture(width, height, 0, format);
  104. tex.Create();
  105. destTexture = new RenderTexture(width, height, 0, format);
  106. destTexture.Create();
  107. sourceImage.texture = tex;
  108. sourceImage.color = Color.white;
  109. sendStream = new MediaStream();
  110. sendStream.AddTrack(new VideoStreamTrack(destTexture));
  111. }
  112. if (receiveStream == null)
  113. {
  114. receiveStream = new MediaStream();
  115. receiveStream.OnAddTrack = e =>
  116. {
  117. if (e.Track is VideoStreamTrack track)
  118. {
  119. track.OnVideoReceived += tex =>
  120. {
  121. receiveImage.texture = tex;
  122. receiveImage.color = Color.white;
  123. videoUpdateStarted = true;
  124. };
  125. }
  126. };
  127. }
  128. }
  129. private void Update()
  130. {
  131. if (!videoUpdateStarted)
  132. return;
  133. int localTimestamp = (int)(Time.realtimeSinceStartup * 1000);
  134. encoder.SetValue(localTimestamp);
  135. Graphics.Blit(sourceImage.texture, destTexture, encoder.Material);
  136. int remoteTimestamp = decoder.GetValue(receiveImage.texture);
  137. textLocalTimestamp.text = localTimestamp.ToString();
  138. textRemoteTimestamp.text = remoteTimestamp.ToString();
  139. int latency = localTimestamp - remoteTimestamp;
  140. UpdateLatencyInfo(latency);
  141. }
  142. void UpdateLatencyInfo(int latency)
  143. {
  144. queueLatency.Enqueue(latency);
  145. if (queueLatency.Count == 10)
  146. {
  147. averageLantecy = (int)queueLatency.Average();
  148. queueLatency.Clear();
  149. }
  150. textLatency.text = latency.ToString();
  151. textAverageLatency.text = averageLantecy.ToString();
  152. }
  153. private static RTCConfiguration GetSelectedSdpSemantics()
  154. {
  155. RTCConfiguration config = default;
  156. config.iceServers = new[] {new RTCIceServer {urls = new[] {"stun:stun.l.google.com:19302"}}};
  157. return config;
  158. }
  159. private void OnIceConnectionChange(RTCPeerConnection pc, RTCIceConnectionState state)
  160. {
  161. Debug.Log($"{GetName(pc)} IceConnectionState: {state}");
  162. if (state == RTCIceConnectionState.Connected || state == RTCIceConnectionState.Completed)
  163. {
  164. StartCoroutine(CheckStats(pc));
  165. foreach(var sender in _pc1.GetSenders())
  166. {
  167. ChangeFramerate(sender, (uint)Application.targetFrameRate);
  168. }
  169. }
  170. }
  171. // Display the video codec that is actually used.
  172. IEnumerator CheckStats(RTCPeerConnection pc)
  173. {
  174. yield return new WaitForSeconds(0.1f);
  175. if (pc == null)
  176. yield break;
  177. var op = pc.GetStats();
  178. yield return op;
  179. if (op.IsError)
  180. {
  181. Debug.LogErrorFormat("RTCPeerConnection.GetStats failed: {0}", op.Error);
  182. yield break;
  183. }
  184. RTCStatsReport report = op.Value;
  185. RTCIceCandidatePairStats activeCandidatePairStats = null;
  186. RTCIceCandidateStats remoteCandidateStats = null;
  187. foreach (var transportStatus in report.Stats.Values.OfType<RTCTransportStats>())
  188. {
  189. if (report.Stats.TryGetValue(transportStatus.selectedCandidatePairId, out var tmp))
  190. {
  191. activeCandidatePairStats = tmp as RTCIceCandidatePairStats;
  192. }
  193. }
  194. if (activeCandidatePairStats == null || string.IsNullOrEmpty(activeCandidatePairStats.remoteCandidateId))
  195. {
  196. yield break;
  197. }
  198. foreach (var iceCandidateStatus in report.Stats.Values.OfType<RTCIceCandidateStats>())
  199. {
  200. if (iceCandidateStatus.Id == activeCandidatePairStats.remoteCandidateId)
  201. {
  202. remoteCandidateStats = iceCandidateStatus;
  203. }
  204. }
  205. if (remoteCandidateStats == null || string.IsNullOrEmpty(remoteCandidateStats.Id))
  206. {
  207. yield break;
  208. }
  209. Debug.Log($"{GetName(pc)} candidate stats Id:{remoteCandidateStats.Id}, Type:{remoteCandidateStats.candidateType}");
  210. }
  211. IEnumerator PeerNegotiationNeeded(RTCPeerConnection pc)
  212. {
  213. var op = pc.CreateOffer();
  214. yield return op;
  215. if (!op.IsError)
  216. {
  217. if (pc.SignalingState != RTCSignalingState.Stable)
  218. {
  219. Debug.LogError($"{GetName(pc)} signaling state is not stable.");
  220. yield break;
  221. }
  222. yield return StartCoroutine(OnCreateOfferSuccess(pc, op.Desc));
  223. }
  224. else
  225. {
  226. OnCreateSessionDescriptionError(op.Error);
  227. }
  228. }
  229. private void AddTracks()
  230. {
  231. var pc1VideoSenders = new List<RTCRtpSender>();
  232. foreach (var track in sendStream.GetTracks())
  233. {
  234. var sender = _pc1.AddTrack(track, sendStream);
  235. if (track.Kind == TrackKind.Video)
  236. {
  237. pc1VideoSenders.Add(sender);
  238. }
  239. }
  240. if (WebRTCSettings.UseVideoCodec != null)
  241. {
  242. var codecs = new[] {WebRTCSettings.UseVideoCodec};
  243. foreach (var transceiver in _pc1.GetTransceivers())
  244. {
  245. if (pc1VideoSenders.Contains(transceiver.Sender))
  246. {
  247. transceiver.SetCodecPreferences(codecs);
  248. }
  249. }
  250. }
  251. }
  252. private void ChangeFramerate(RTCRtpSender sender, uint framerate)
  253. {
  254. RTCRtpSendParameters parameters = sender.GetParameters();
  255. parameters.encodings[0].maxFramerate = framerate;
  256. RTCError error = sender.SetParameters(parameters);
  257. if (error.errorType != RTCErrorType.None)
  258. {
  259. Debug.LogErrorFormat("RTCRtpSender.SetParameters failed {0}", error.errorType);
  260. }
  261. }
  262. private void DeleteTracks()
  263. {
  264. MediaStreamTrack[] senderTracks = sendStream.GetTracks().ToArray();
  265. foreach (var track in senderTracks)
  266. {
  267. sendStream.RemoveTrack(track);
  268. track.Dispose();
  269. }
  270. MediaStreamTrack[] receiveTracks = receiveStream.GetTracks().ToArray();
  271. foreach (var track in receiveTracks)
  272. {
  273. receiveStream.RemoveTrack(track);
  274. track.Dispose();
  275. }
  276. }
  277. private void OnCall()
  278. {
  279. callButton.interactable = false;
  280. hangUpButton.interactable = true;
  281. var configuration = GetSelectedSdpSemantics();
  282. _pc1 = new RTCPeerConnection(ref configuration);
  283. _pc1.OnIceCandidate = pc1OnIceCandidate;
  284. _pc1.OnIceConnectionChange = pc1OnIceConnectionChange;
  285. _pc1.OnNegotiationNeeded = pc1OnNegotiationNeeded;
  286. _pc2 = new RTCPeerConnection(ref configuration);
  287. _pc2.OnIceCandidate = pc2OnIceCandidate;
  288. _pc2.OnIceConnectionChange = pc2OnIceConnectionChange;
  289. _pc2.OnTrack = pc2Ontrack;
  290. AddTracks();
  291. }
  292. private void OnHangUp()
  293. {
  294. videoUpdateStarted = false;
  295. DeleteTracks();
  296. _pc1.Close();
  297. _pc2.Close();
  298. _pc1.Dispose();
  299. _pc2.Dispose();
  300. _pc1 = null;
  301. _pc2 = null;
  302. sendStream.Dispose();
  303. sendStream = null;
  304. receiveStream.Dispose();
  305. receiveStream = null;
  306. startButton.interactable = true;
  307. callButton.interactable = false;
  308. hangUpButton.interactable = false;
  309. dropDownFramerate.interactable = true;
  310. receiveImage.color = Color.black;
  311. }
  312. private void OnIceCandidate(RTCPeerConnection pc, RTCIceCandidate candidate)
  313. {
  314. GetOtherPc(pc).AddIceCandidate(candidate);
  315. }
  316. private string GetName(RTCPeerConnection pc)
  317. {
  318. return (pc == _pc1) ? "pc1" : "pc2";
  319. }
  320. private RTCPeerConnection GetOtherPc(RTCPeerConnection pc)
  321. {
  322. return (pc == _pc1) ? _pc2 : _pc1;
  323. }
  324. private IEnumerator OnCreateOfferSuccess(RTCPeerConnection pc, RTCSessionDescription desc)
  325. {
  326. Debug.Log($"Offer from {GetName(pc)}\n{desc.sdp}");
  327. Debug.Log($"{GetName(pc)} setLocalDescription start");
  328. var op = pc.SetLocalDescription(ref desc);
  329. yield return op;
  330. if (!op.IsError)
  331. {
  332. OnSetLocalSuccess(pc);
  333. }
  334. else
  335. {
  336. var error = op.Error;
  337. OnSetSessionDescriptionError(ref error);
  338. yield break;
  339. }
  340. var otherPc = GetOtherPc(pc);
  341. Debug.Log($"{GetName(otherPc)} setRemoteDescription start");
  342. var op2 = otherPc.SetRemoteDescription(ref desc);
  343. yield return op2;
  344. if (!op2.IsError)
  345. {
  346. OnSetRemoteSuccess(otherPc);
  347. }
  348. else
  349. {
  350. var error = op2.Error;
  351. OnSetSessionDescriptionError(ref error);
  352. yield break;
  353. }
  354. Debug.Log($"{GetName(otherPc)} createAnswer start");
  355. // Since the 'remote' side has no media stream we need
  356. // to pass in the right constraints in order for it to
  357. // accept the incoming offer of audio and video.
  358. var op3 = otherPc.CreateAnswer();
  359. yield return op3;
  360. if (!op3.IsError)
  361. {
  362. yield return OnCreateAnswerSuccess(otherPc, op3.Desc);
  363. }
  364. else
  365. {
  366. OnCreateSessionDescriptionError(op3.Error);
  367. }
  368. }
  369. private void OnSetLocalSuccess(RTCPeerConnection pc)
  370. {
  371. Debug.Log($"{GetName(pc)} SetLocalDescription complete");
  372. }
  373. void OnSetSessionDescriptionError(ref RTCError error)
  374. {
  375. Debug.LogError($"Error Detail Type: {error.message}");
  376. OnHangUp();
  377. }
  378. private void OnSetRemoteSuccess(RTCPeerConnection pc)
  379. {
  380. Debug.Log($"{GetName(pc)} SetRemoteDescription complete");
  381. }
  382. IEnumerator OnCreateAnswerSuccess(RTCPeerConnection pc, RTCSessionDescription desc)
  383. {
  384. Debug.Log($"Answer from {GetName(pc)}:\n{desc.sdp}");
  385. Debug.Log($"{GetName(pc)} setLocalDescription start");
  386. var op = pc.SetLocalDescription(ref desc);
  387. yield return op;
  388. if (!op.IsError)
  389. {
  390. OnSetLocalSuccess(pc);
  391. }
  392. else
  393. {
  394. var error = op.Error;
  395. OnSetSessionDescriptionError(ref error);
  396. }
  397. var otherPc = GetOtherPc(pc);
  398. Debug.Log($"{GetName(otherPc)} setRemoteDescription start");
  399. var op2 = otherPc.SetRemoteDescription(ref desc);
  400. yield return op2;
  401. if (!op2.IsError)
  402. {
  403. OnSetRemoteSuccess(otherPc);
  404. }
  405. else
  406. {
  407. var error = op2.Error;
  408. OnSetSessionDescriptionError(ref error);
  409. }
  410. }
  411. private static void OnCreateSessionDescriptionError(RTCError error)
  412. {
  413. Debug.LogError($"Error Detail Type: {error.message}");
  414. }
  415. }