BandwidthSample.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using UnityEngine;
  6. using Unity.WebRTC;
  7. using Unity.WebRTC.Samples;
  8. using UnityEngine.UI;
  9. using Button = UnityEngine.UI.Button;
  10. using Toggle = UnityEngine.UI.Toggle;
  11. class BandwidthSample : MonoBehaviour
  12. {
  13. #pragma warning disable 0649
  14. [SerializeField] private Dropdown bandwidthSelector;
  15. [SerializeField] private Dropdown scaleResolutionDownSelector;
  16. [SerializeField] private Dropdown framerateSelector;
  17. [SerializeField] private Button callButton;
  18. [SerializeField] private Button hangUpButton;
  19. [SerializeField] private InputField statsField;
  20. [SerializeField] private Toggle autoScroll;
  21. [SerializeField] private Button copyClipboard;
  22. [SerializeField] private Camera cam;
  23. [SerializeField] private RawImage sourceImage;
  24. [SerializeField] private RawImage receiveImage;
  25. [SerializeField] private Transform rotateObject;
  26. #pragma warning restore 0649
  27. private RTCPeerConnection _pc1, _pc2;
  28. private List<RTCRtpSender> pc1Senders;
  29. private MediaStream videoStream, receiveStream;
  30. private DelegateOnIceConnectionChange pc1OnIceConnectionChange;
  31. private DelegateOnIceConnectionChange pc2OnIceConnectionChange;
  32. private DelegateOnIceCandidate pc1OnIceCandidate;
  33. private DelegateOnIceCandidate pc2OnIceCandidate;
  34. private DelegateOnTrack pc2Ontrack;
  35. private DelegateOnNegotiationNeeded pc1OnNegotiationNeeded;
  36. private bool videoUpdateStarted;
  37. private Dictionary<string, ulong?> bandwidthOptions =
  38. new Dictionary<string, ulong?>()
  39. {
  40. { "undefined", null },
  41. { "10000", 10000 },
  42. { "2000", 2000 },
  43. { "1000", 1000 },
  44. { "500", 500 },
  45. { "250", 250 },
  46. { "125", 125 },
  47. };
  48. private Dictionary<string, double> scaleResolutionDownOptions =
  49. new Dictionary<string, double>()
  50. {
  51. { "Not scaling", 1.0f },
  52. { "Down scale by 2.0", 2.0f },
  53. { "Down scale by 4.0", 4.0f },
  54. { "Down scale by 8.0", 8.0f },
  55. { "Down scale by 16.0", 16.0f }
  56. };
  57. private Dictionary<string, uint?> framerateOptions =
  58. new Dictionary<string, uint?>
  59. {
  60. { "undefined", null },
  61. { "90", 90 },
  62. { "60", 60 },
  63. { "30", 30 },
  64. { "20", 20 },
  65. { "10", 10 },
  66. { "5", 5 },
  67. { "0", 0 },
  68. };
  69. private void Awake()
  70. {
  71. WebRTC.Initialize(WebRTCSettings.LimitTextureSize);
  72. bandwidthSelector.options = bandwidthOptions
  73. .Select(pair => new Dropdown.OptionData{text = pair.Key })
  74. .ToList();
  75. bandwidthSelector.onValueChanged.AddListener(ChangeBandwitdh);
  76. scaleResolutionDownSelector.options = scaleResolutionDownOptions
  77. .Select(pair => new Dropdown.OptionData { text = pair.Key })
  78. .ToList();
  79. scaleResolutionDownSelector.onValueChanged.AddListener(ChangeScaleResolutionDown);
  80. framerateSelector.options = framerateOptions
  81. .Select(pair => new Dropdown.OptionData { text = pair.Key })
  82. .ToList();
  83. framerateSelector.onValueChanged.AddListener(ChangeFramerate);
  84. callButton.onClick.AddListener(Call);
  85. hangUpButton.onClick.AddListener(HangUp);
  86. copyClipboard.onClick.AddListener(CopyClipboard);
  87. receiveStream = new MediaStream();
  88. }
  89. private void OnDestroy()
  90. {
  91. WebRTC.Dispose();
  92. }
  93. private void Start()
  94. {
  95. pc1Senders = new List<RTCRtpSender>();
  96. callButton.interactable = true;
  97. hangUpButton.interactable = false;
  98. bandwidthSelector.interactable = false;
  99. scaleResolutionDownSelector.interactable = false;
  100. pc1OnIceConnectionChange = state => { OnIceConnectionChange(_pc1, state); };
  101. pc2OnIceConnectionChange = state => { OnIceConnectionChange(_pc2, state); };
  102. pc1OnIceCandidate = candidate => { OnIceCandidate(_pc1, candidate); };
  103. pc2OnIceCandidate = candidate => { OnIceCandidate(_pc2, candidate); };
  104. pc2Ontrack = e =>
  105. {
  106. receiveStream.AddTrack(e.Track);
  107. };
  108. pc1OnNegotiationNeeded = () => { StartCoroutine(PeerNegotiationNeeded(_pc1)); };
  109. receiveStream.OnAddTrack = e =>
  110. {
  111. if (e.Track is VideoStreamTrack track)
  112. {
  113. track.OnVideoReceived += OnVideoReceived;
  114. }
  115. };
  116. }
  117. private void OnVideoReceived(Texture tex)
  118. {
  119. receiveImage.texture = tex;
  120. receiveImage.color = Color.white;
  121. statsField.text +=
  122. $"Video resolution: {tex.width}x{tex.height}" + Environment.NewLine;
  123. if (autoScroll.isOn)
  124. statsField.MoveTextEnd(false);
  125. }
  126. private void Update()
  127. {
  128. if (rotateObject != null)
  129. {
  130. float t = Time.deltaTime;
  131. rotateObject.Rotate(100 * t, 200 * t, 300 * t);
  132. }
  133. }
  134. private static RTCConfiguration GetSelectedSdpSemantics()
  135. {
  136. RTCConfiguration config = default;
  137. config.iceServers = new[] {new RTCIceServer {urls = new[] {"stun:stun.l.google.com:19302"}}};
  138. return config;
  139. }
  140. private void OnIceConnectionChange(RTCPeerConnection pc, RTCIceConnectionState state)
  141. {
  142. switch (state)
  143. {
  144. case RTCIceConnectionState.New:
  145. Debug.Log($"{GetName(pc)} IceConnectionState: New");
  146. break;
  147. case RTCIceConnectionState.Checking:
  148. Debug.Log($"{GetName(pc)} IceConnectionState: Checking");
  149. break;
  150. case RTCIceConnectionState.Closed:
  151. Debug.Log($"{GetName(pc)} IceConnectionState: Closed");
  152. break;
  153. case RTCIceConnectionState.Completed:
  154. Debug.Log($"{GetName(pc)} IceConnectionState: Completed");
  155. break;
  156. case RTCIceConnectionState.Connected:
  157. Debug.Log($"{GetName(pc)} IceConnectionState: Connected");
  158. break;
  159. case RTCIceConnectionState.Disconnected:
  160. Debug.Log($"{GetName(pc)} IceConnectionState: Disconnected");
  161. break;
  162. case RTCIceConnectionState.Failed:
  163. Debug.Log($"{GetName(pc)} IceConnectionState: Failed");
  164. break;
  165. case RTCIceConnectionState.Max:
  166. Debug.Log($"{GetName(pc)} IceConnectionState: Max");
  167. break;
  168. default:
  169. throw new ArgumentOutOfRangeException(nameof(state), state, null);
  170. }
  171. }
  172. IEnumerator PeerNegotiationNeeded(RTCPeerConnection pc)
  173. {
  174. var op = pc.CreateOffer();
  175. yield return op;
  176. if (!op.IsError)
  177. {
  178. if (pc.SignalingState != RTCSignalingState.Stable)
  179. {
  180. Debug.LogError($"{GetName(pc)} signaling state is not stable.");
  181. yield break;
  182. }
  183. yield return StartCoroutine(OnCreateOfferSuccess(pc, op.Desc));
  184. }
  185. else
  186. {
  187. OnCreateSessionDescriptionError(op.Error);
  188. }
  189. }
  190. private void AddTracks()
  191. {
  192. foreach (var track in videoStream.GetTracks())
  193. {
  194. pc1Senders.Add(_pc1.AddTrack(track, videoStream));
  195. }
  196. if (WebRTCSettings.UseVideoCodec != null)
  197. {
  198. var codecs = new[] {WebRTCSettings.UseVideoCodec};
  199. foreach (var transceiver in _pc1.GetTransceivers())
  200. {
  201. if (pc1Senders.Contains(transceiver.Sender))
  202. {
  203. transceiver.SetCodecPreferences(codecs);
  204. }
  205. }
  206. }
  207. if (!videoUpdateStarted)
  208. {
  209. StartCoroutine(WebRTC.Update());
  210. StartCoroutine(LoopStatsCoroutine());
  211. videoUpdateStarted = true;
  212. }
  213. bandwidthSelector.interactable = false;
  214. scaleResolutionDownSelector.interactable = false;
  215. }
  216. private void RemoveTracks()
  217. {
  218. foreach (var sender in pc1Senders)
  219. {
  220. _pc1.RemoveTrack(sender);
  221. }
  222. pc1Senders.Clear();
  223. MediaStreamTrack[] tracks = receiveStream.GetTracks().ToArray();
  224. foreach (var track in tracks)
  225. {
  226. receiveStream.RemoveTrack(track);
  227. track.Dispose();
  228. }
  229. }
  230. private void Call()
  231. {
  232. callButton.interactable = false;
  233. hangUpButton.interactable = true;
  234. bandwidthSelector.interactable = true;
  235. scaleResolutionDownSelector.interactable = true;
  236. statsField.text = string.Empty;
  237. var configuration = GetSelectedSdpSemantics();
  238. _pc1 = new RTCPeerConnection(ref configuration);
  239. _pc1.OnIceCandidate = pc1OnIceCandidate;
  240. _pc1.OnIceConnectionChange = pc1OnIceConnectionChange;
  241. _pc1.OnNegotiationNeeded = pc1OnNegotiationNeeded;
  242. _pc2 = new RTCPeerConnection(ref configuration);
  243. _pc2.OnIceCandidate = pc2OnIceCandidate;
  244. _pc2.OnIceConnectionChange = pc2OnIceConnectionChange;
  245. _pc2.OnTrack = pc2Ontrack;
  246. if (videoStream == null)
  247. {
  248. videoStream = cam.CaptureStream(WebRTCSettings.StreamSize.x, WebRTCSettings.StreamSize.y);
  249. }
  250. sourceImage.texture = cam.targetTexture;
  251. sourceImage.color = Color.white;
  252. AddTracks();
  253. }
  254. private void ChangeBandwitdh(int index)
  255. {
  256. if (_pc1 == null || _pc2 == null)
  257. return;
  258. ulong? bandwidth = bandwidthOptions.Values.ElementAt(index);
  259. RTCRtpSender sender = _pc1.GetSenders().First();
  260. RTCRtpSendParameters parameters = sender.GetParameters();
  261. if (bandwidth == null)
  262. {
  263. parameters.encodings[0].maxBitrate = null;
  264. parameters.encodings[0].minBitrate = null;
  265. }
  266. else
  267. {
  268. parameters.encodings[0].maxBitrate = bandwidth * 1000;
  269. parameters.encodings[0].minBitrate = bandwidth * 1000;
  270. }
  271. RTCError error = sender.SetParameters(parameters);
  272. if (error.errorType != RTCErrorType.None)
  273. {
  274. Debug.LogErrorFormat("RTCRtpSender.SetParameters failed {0}", error.errorType);
  275. statsField.text += $"Failed change bandwidth to {bandwidth * 1000}{Environment.NewLine}";
  276. bandwidthSelector.value = 0;
  277. }
  278. }
  279. private void ChangeScaleResolutionDown(int index)
  280. {
  281. if (_pc1 == null || _pc2 == null)
  282. return;
  283. double scale = scaleResolutionDownOptions.Values.ElementAt(index);
  284. RTCRtpSender sender = _pc1.GetSenders().First();
  285. RTCRtpSendParameters parameters = sender.GetParameters();
  286. parameters.encodings[0].scaleResolutionDownBy = scale;
  287. RTCError error = sender.SetParameters(parameters);
  288. if (error.errorType != RTCErrorType.None)
  289. {
  290. Debug.LogErrorFormat("RTCRtpSender.SetParameters failed {0}", error.errorType);
  291. statsField.text +=
  292. $"Failed scale down video resolution to " +
  293. $"{(int)(WebRTCSettings.StreamSize.x / scale)}x{(int)(WebRTCSettings.StreamSize.y / scale)}{Environment.NewLine}";
  294. scaleResolutionDownSelector.value = 0;
  295. }
  296. }
  297. private void ChangeFramerate(int index)
  298. {
  299. if (_pc1 == null || _pc2 == null)
  300. return;
  301. uint? framerate = framerateOptions.Values.ElementAt(index);
  302. RTCRtpSender sender = _pc1.GetSenders().First();
  303. RTCRtpSendParameters parameters = sender.GetParameters();
  304. parameters.encodings[0].maxFramerate = framerate;
  305. RTCError error = sender.SetParameters(parameters);
  306. if (error.errorType != RTCErrorType.None)
  307. {
  308. Debug.LogErrorFormat("RTCRtpSender.SetParameters failed {0}", error.errorType);
  309. statsField.text +=
  310. $"Failed maxFramerate to " +
  311. $"{framerate}{Environment.NewLine}";
  312. framerateSelector.value = 0;
  313. }
  314. }
  315. private void HangUp()
  316. {
  317. RemoveTracks();
  318. _pc1.Close();
  319. _pc2.Close();
  320. Debug.Log("Close local/remote peer connection");
  321. _pc1.Dispose();
  322. _pc2.Dispose();
  323. _pc1 = null;
  324. _pc2 = null;
  325. callButton.interactable = true;
  326. hangUpButton.interactable = false;
  327. bandwidthSelector.interactable = false;
  328. bandwidthSelector.value = 0;
  329. scaleResolutionDownSelector.interactable = false;
  330. scaleResolutionDownSelector.value = 0;
  331. sourceImage.color = Color.black;
  332. receiveImage.color = Color.black;
  333. }
  334. private void CopyClipboard()
  335. {
  336. #if UNITY_EDITOR
  337. UnityEditor.EditorGUIUtility.systemCopyBuffer = statsField.text;
  338. #endif
  339. }
  340. private void OnIceCandidate(RTCPeerConnection pc, RTCIceCandidate candidate)
  341. {
  342. GetOtherPc(pc).AddIceCandidate(candidate);
  343. Debug.Log($"{GetName(pc)} ICE candidate:\n {candidate.Candidate}");
  344. }
  345. private string GetName(RTCPeerConnection pc)
  346. {
  347. return (pc == _pc1) ? "pc1" : "pc2";
  348. }
  349. private RTCPeerConnection GetOtherPc(RTCPeerConnection pc)
  350. {
  351. return (pc == _pc1) ? _pc2 : _pc1;
  352. }
  353. private IEnumerator LoopStatsCoroutine()
  354. {
  355. while (true)
  356. {
  357. yield return StartCoroutine(UpdateStatsCoroutine());
  358. yield return new WaitForSeconds(1f);
  359. }
  360. }
  361. private IEnumerator UpdateStatsCoroutine()
  362. {
  363. RTCRtpSender sender = pc1Senders.FirstOrDefault();
  364. if (sender == null)
  365. yield break;
  366. RTCStatsReportAsyncOperation op = sender.GetStats();
  367. yield return op;
  368. if (op.IsError)
  369. {
  370. Debug.LogErrorFormat("RTCRtpSender.GetStats() is failed {0}", op.Error.errorType);
  371. }
  372. else
  373. {
  374. UpdateStatsPacketSize(op.Value);
  375. }
  376. }
  377. private RTCStatsReport lastResult = null;
  378. private void UpdateStatsPacketSize(RTCStatsReport res)
  379. {
  380. foreach (RTCStats stats in res.Stats.Values)
  381. {
  382. if (!(stats is RTCOutboundRTPStreamStats report))
  383. {
  384. continue;
  385. }
  386. long now = report.Timestamp;
  387. ulong bytes = report.bytesSent;
  388. if (lastResult != null)
  389. {
  390. if (!lastResult.TryGetValue(report.Id, out RTCStats last))
  391. continue;
  392. var lastStats = last as RTCOutboundRTPStreamStats;
  393. var duration = (double)(now - lastStats.Timestamp) / 1000000;
  394. ulong bitrate = (ulong)(8 * (bytes - lastStats.bytesSent) / duration);
  395. statsField.text += $"Bitrate: {bitrate}" + Environment.NewLine;
  396. if (autoScroll.isOn)
  397. statsField.MoveTextEnd(false);
  398. }
  399. }
  400. lastResult = res;
  401. }
  402. private IEnumerator OnCreateOfferSuccess(RTCPeerConnection pc, RTCSessionDescription desc)
  403. {
  404. Debug.Log($"Offer from {GetName(pc)}\n{desc.sdp}");
  405. Debug.Log($"{GetName(pc)} setLocalDescription start");
  406. var op = pc.SetLocalDescription(ref desc);
  407. yield return op;
  408. if (!op.IsError)
  409. {
  410. OnSetLocalSuccess(pc);
  411. }
  412. else
  413. {
  414. var error = op.Error;
  415. OnSetSessionDescriptionError(ref error);
  416. }
  417. var otherPc = GetOtherPc(pc);
  418. Debug.Log($"{GetName(otherPc)} setRemoteDescription start");
  419. var op2 = otherPc.SetRemoteDescription(ref desc);
  420. yield return op2;
  421. if (!op2.IsError)
  422. {
  423. OnSetRemoteSuccess(otherPc);
  424. }
  425. else
  426. {
  427. var error = op2.Error;
  428. OnSetSessionDescriptionError(ref error);
  429. }
  430. Debug.Log($"{GetName(otherPc)} createAnswer start");
  431. // Since the 'remote' side has no media stream we need
  432. // to pass in the right constraints in order for it to
  433. // accept the incoming offer of audio and video.
  434. var op3 = otherPc.CreateAnswer();
  435. yield return op3;
  436. if (!op3.IsError)
  437. {
  438. yield return OnCreateAnswerSuccess(otherPc, op3.Desc);
  439. }
  440. else
  441. {
  442. OnCreateSessionDescriptionError(op3.Error);
  443. }
  444. }
  445. private void OnSetLocalSuccess(RTCPeerConnection pc)
  446. {
  447. Debug.Log($"{GetName(pc)} SetLocalDescription complete");
  448. }
  449. static void OnSetSessionDescriptionError(ref RTCError error)
  450. {
  451. Debug.LogError($"Error Detail Type: {error.message}");
  452. }
  453. private void OnSetRemoteSuccess(RTCPeerConnection pc)
  454. {
  455. Debug.Log($"{GetName(pc)} SetRemoteDescription complete");
  456. if (pc == _pc1)
  457. {
  458. bandwidthSelector.interactable = true;
  459. scaleResolutionDownSelector.interactable = true;
  460. }
  461. }
  462. IEnumerator OnCreateAnswerSuccess(RTCPeerConnection pc, RTCSessionDescription desc)
  463. {
  464. Debug.Log($"Answer from {GetName(pc)}:\n{desc.sdp}");
  465. Debug.Log($"{GetName(pc)} setLocalDescription start");
  466. var op = pc.SetLocalDescription(ref desc);
  467. yield return op;
  468. if (!op.IsError)
  469. {
  470. OnSetLocalSuccess(pc);
  471. }
  472. else
  473. {
  474. var error = op.Error;
  475. OnSetSessionDescriptionError(ref error);
  476. }
  477. var otherPc = GetOtherPc(pc);
  478. Debug.Log($"{GetName(otherPc)} setRemoteDescription start");
  479. var op2 = otherPc.SetRemoteDescription(ref desc);
  480. yield return op2;
  481. if (!op2.IsError)
  482. {
  483. OnSetRemoteSuccess(otherPc);
  484. }
  485. else
  486. {
  487. var error = op2.Error;
  488. OnSetSessionDescriptionError(ref error);
  489. }
  490. }
  491. private static void OnCreateSessionDescriptionError(RTCError error)
  492. {
  493. Debug.LogError($"Error Detail Type: {error.message}");
  494. }
  495. }