ShowStatsUI.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using Unity.WebRTC;
  6. using UnityEngine;
  7. using UnityEngine.UI;
  8. namespace Unity.RenderStreaming.Samples
  9. {
  10. internal class ShowStatsUI : MonoBehaviour
  11. {
  12. [SerializeField] private Button showStatsButton;
  13. [SerializeField] private Button hideStatsButton;
  14. [SerializeField] private GameObject scrollView;
  15. [SerializeField] private RectTransform displayParent;
  16. [SerializeField] private Text baseText;
  17. [SerializeField] private List<SignalingHandlerBase> signalingHandlerList;
  18. private Dictionary<string, HashSet<RTCRtpSender>> activeSenderList =
  19. new Dictionary<string, HashSet<RTCRtpSender>>();
  20. private Dictionary<StreamReceiverBase, HashSet<RTCRtpReceiver>> activeReceiverList =
  21. new Dictionary<StreamReceiverBase, HashSet<RTCRtpReceiver>>();
  22. private Dictionary<RTCRtpSender, StatsDisplay> lastSenderStats =
  23. new Dictionary<RTCRtpSender, StatsDisplay>();
  24. private Dictionary<RTCRtpReceiver, StatsDisplay> lastReceiverStats =
  25. new Dictionary<RTCRtpReceiver, StatsDisplay>();
  26. private HashSet<StreamSenderBase> alreadySetupSenderList = new HashSet<StreamSenderBase>();
  27. private void Awake()
  28. {
  29. showStatsButton.onClick.AddListener(ShowStats);
  30. hideStatsButton.onClick.AddListener(HideStats);
  31. }
  32. private void ShowStats()
  33. {
  34. scrollView.gameObject.SetActive(true);
  35. hideStatsButton.gameObject.SetActive(true);
  36. showStatsButton.gameObject.SetActive(false);
  37. }
  38. private void HideStats()
  39. {
  40. scrollView.gameObject.SetActive(false);
  41. showStatsButton.gameObject.SetActive(true);
  42. hideStatsButton.gameObject.SetActive(false);
  43. }
  44. private void Start()
  45. {
  46. StartCoroutine(CollectStats());
  47. }
  48. private void OnDestroy()
  49. {
  50. lastSenderStats.Clear();
  51. lastReceiverStats.Clear();
  52. activeSenderList.Clear();
  53. activeReceiverList.Clear();
  54. alreadySetupSenderList.Clear();
  55. }
  56. public void AddSignalingHandler(SignalingHandlerBase handlerBase)
  57. {
  58. if (signalingHandlerList.Contains(handlerBase))
  59. {
  60. return;
  61. }
  62. signalingHandlerList.Add(handlerBase);
  63. }
  64. class StatsDisplay
  65. {
  66. public Text display;
  67. public RTCStatsReport lastReport;
  68. }
  69. private IEnumerator CollectStats()
  70. {
  71. var waitSec = new WaitForSeconds(1);
  72. while (true)
  73. {
  74. yield return waitSec;
  75. foreach (var streamBase in signalingHandlerList.SelectMany(x => x.Streams))
  76. {
  77. if (streamBase is StreamSenderBase senderBase)
  78. {
  79. SetUpSenderBase(senderBase);
  80. }
  81. if (streamBase is StreamReceiverBase receiverBase)
  82. {
  83. SetUpReceiverBase(receiverBase);
  84. }
  85. }
  86. List<Coroutine> coroutines = new List<Coroutine>();
  87. foreach (var sender in activeSenderList.Values.SelectMany(x => x))
  88. {
  89. var coroutine = StartCoroutine(UpdateStats(sender));
  90. coroutines.Add(coroutine);
  91. }
  92. foreach (var receiver in activeReceiverList.Values.SelectMany(x => x))
  93. {
  94. var coroutine = StartCoroutine(UpdateStats(receiver));
  95. coroutines.Add(coroutine);
  96. }
  97. foreach(var coroutine in coroutines)
  98. {
  99. yield return coroutine;
  100. }
  101. var noStatsData = !lastSenderStats.Any() && !lastReceiverStats.Any();
  102. baseText.gameObject.SetActive(noStatsData);
  103. }
  104. }
  105. IEnumerator UpdateStats(RTCRtpReceiver receiver)
  106. {
  107. var op = receiver.GetStats();
  108. yield return new WaitUntilWithTimeout(() => op.IsDone, 3f);
  109. if (op.IsError || !op.IsDone)
  110. {
  111. yield break;
  112. }
  113. var report = op.Value;
  114. if (report == null)
  115. {
  116. yield break;
  117. }
  118. if (lastReceiverStats.TryGetValue(receiver, out var statsDisplay))
  119. {
  120. var lastReport = statsDisplay.lastReport;
  121. statsDisplay.display.text = CreateDisplayString(report, lastReport);
  122. statsDisplay.lastReport = report;
  123. lastReport.Dispose();
  124. }
  125. else
  126. {
  127. var text = Instantiate(baseText, displayParent);
  128. text.text = "";
  129. text.gameObject.SetActive(true);
  130. lastReceiverStats[receiver] = new StatsDisplay { display = text, lastReport = report };
  131. }
  132. }
  133. IEnumerator UpdateStats(RTCRtpSender sender)
  134. {
  135. var op = sender.GetStats();
  136. yield return new WaitUntilWithTimeout(() => op.IsDone, 3f);
  137. if (op.IsError || !op.IsDone)
  138. {
  139. yield break;
  140. }
  141. var report = op.Value;
  142. if (report == null)
  143. {
  144. yield break;
  145. }
  146. if (lastSenderStats.TryGetValue(sender, out var statsDisplay))
  147. {
  148. var lastReport = statsDisplay.lastReport;
  149. statsDisplay.display.text = CreateDisplayString(report, lastReport);
  150. statsDisplay.lastReport = report;
  151. lastReport.Dispose();
  152. }
  153. else
  154. {
  155. var text = Instantiate(baseText, displayParent);
  156. text.text = "";
  157. text.gameObject.SetActive(true);
  158. lastSenderStats[sender] = new StatsDisplay { display = text, lastReport = report };
  159. }
  160. }
  161. private void SetUpSenderBase(StreamSenderBase senderBase)
  162. {
  163. if (alreadySetupSenderList.Contains(senderBase))
  164. {
  165. return;
  166. }
  167. senderBase.OnStartedStream += id =>
  168. {
  169. if (!activeSenderList.ContainsKey(id))
  170. {
  171. activeSenderList[id] = new HashSet<RTCRtpSender>();
  172. }
  173. if (senderBase.Transceivers.TryGetValue(id, out var transceiver))
  174. {
  175. activeSenderList[id].Add(transceiver.Sender);
  176. }
  177. };
  178. senderBase.OnStoppedStream += id =>
  179. {
  180. if (activeSenderList.TryGetValue(id, out var hashSet))
  181. {
  182. foreach (var sender in hashSet)
  183. {
  184. if (lastSenderStats.TryGetValue(sender, out var statsDisplay))
  185. {
  186. DestroyImmediate(statsDisplay.display.gameObject);
  187. lastSenderStats.Remove(sender);
  188. }
  189. }
  190. }
  191. activeSenderList.Remove(id);
  192. };
  193. foreach (var pair in senderBase.Transceivers)
  194. {
  195. if (!activeSenderList.ContainsKey(pair.Key))
  196. {
  197. activeSenderList[pair.Key] = new HashSet<RTCRtpSender>();
  198. }
  199. activeSenderList[pair.Key].Add(pair.Value.Sender);
  200. }
  201. alreadySetupSenderList.Add(senderBase);
  202. }
  203. private void SetUpReceiverBase(StreamReceiverBase receiverBase)
  204. {
  205. if (activeReceiverList.ContainsKey(receiverBase))
  206. {
  207. return;
  208. }
  209. activeReceiverList[receiverBase] = new HashSet<RTCRtpReceiver>();
  210. receiverBase.OnStartedStream += id =>
  211. {
  212. if(activeReceiverList.TryGetValue(receiverBase, out var hashSet))
  213. {
  214. hashSet.Add(receiverBase.Transceiver.Receiver);
  215. }
  216. };
  217. receiverBase.OnStoppedStream += id =>
  218. {
  219. if (activeReceiverList.TryGetValue(receiverBase, out var hashSet))
  220. {
  221. foreach (var receiver in hashSet)
  222. {
  223. if (lastReceiverStats.TryGetValue(receiver, out var statsDisplay))
  224. {
  225. DestroyImmediate(statsDisplay.display.gameObject);
  226. lastReceiverStats.Remove(receiver);
  227. }
  228. }
  229. }
  230. activeReceiverList.Remove(receiverBase);
  231. };
  232. var transceiver = receiverBase.Transceiver;
  233. if (transceiver != null && transceiver.Receiver != null)
  234. {
  235. activeReceiverList[receiverBase].Add(transceiver.Receiver);
  236. }
  237. }
  238. private static string CreateDisplayString(RTCStatsReport report, RTCStatsReport lastReport)
  239. {
  240. var builder = new StringBuilder();
  241. foreach (var stats in report.Stats.Values)
  242. {
  243. if (stats is RTCInboundRTPStreamStats inboundStats)
  244. {
  245. builder.AppendLine($"{inboundStats.kind} receiving stream stats");
  246. if (inboundStats.codecId != null && report.Get(inboundStats.codecId) is RTCCodecStats codecStats)
  247. {
  248. builder.AppendLine($"Codec: {codecStats.mimeType}");
  249. if (!string.IsNullOrEmpty(codecStats.sdpFmtpLine))
  250. {
  251. foreach (var fmtp in codecStats.sdpFmtpLine.Split(';'))
  252. {
  253. builder.AppendLine($" - {fmtp}");
  254. }
  255. }
  256. if (codecStats.payloadType > 0)
  257. {
  258. builder.AppendLine($" - {nameof(codecStats.payloadType)}={codecStats.payloadType}");
  259. }
  260. if (codecStats.clockRate > 0)
  261. {
  262. builder.AppendLine($" - {nameof(codecStats.clockRate)}={codecStats.clockRate}");
  263. }
  264. if (codecStats.channels > 0)
  265. {
  266. builder.AppendLine($" - {nameof(codecStats.channels)}={codecStats.channels}");
  267. }
  268. }
  269. if (inboundStats.kind == "video")
  270. {
  271. builder.AppendLine($"Decoder: {inboundStats.decoderImplementation}");
  272. builder.AppendLine($"Resolution: {inboundStats.frameWidth}x{inboundStats.frameHeight}");
  273. builder.AppendLine($"Framerate: {inboundStats.framesPerSecond}");
  274. }
  275. if (lastReport.TryGetValue(inboundStats.Id, out var lastStats) &&
  276. lastStats is RTCInboundRTPStreamStats lastInboundStats)
  277. {
  278. var duration = (double)(inboundStats.Timestamp - lastInboundStats.Timestamp) / 1000000;
  279. var bitrate = (8 * (inboundStats.bytesReceived - lastInboundStats.bytesReceived) / duration) / 1000;
  280. builder.AppendLine($"Bitrate: {bitrate:F2} kbit/sec");
  281. }
  282. }
  283. else if (stats is RTCOutboundRTPStreamStats outboundStats)
  284. {
  285. builder.AppendLine($"{outboundStats.kind} sending stream stats");
  286. if (outboundStats.codecId != null && report.Get(outboundStats.codecId) is RTCCodecStats codecStats)
  287. {
  288. builder.AppendLine($"Codec: {codecStats.mimeType}");
  289. if (!string.IsNullOrEmpty(codecStats.sdpFmtpLine))
  290. {
  291. foreach (var fmtp in codecStats.sdpFmtpLine.Split(';'))
  292. {
  293. builder.AppendLine($" - {fmtp}");
  294. }
  295. }
  296. if (codecStats.payloadType > 0)
  297. {
  298. builder.AppendLine($" - {nameof(codecStats.payloadType)}={codecStats.payloadType}");
  299. }
  300. if (codecStats.clockRate > 0)
  301. {
  302. builder.AppendLine($" - {nameof(codecStats.clockRate)}={codecStats.clockRate}");
  303. }
  304. if (codecStats.channels > 0)
  305. {
  306. builder.AppendLine($" - {nameof(codecStats.channels)}={codecStats.channels}");
  307. }
  308. }
  309. if (outboundStats.kind == "video")
  310. {
  311. builder.AppendLine($"Encoder: {outboundStats.encoderImplementation}");
  312. builder.AppendLine($"Resolution: {outboundStats.frameWidth}x{outboundStats.frameHeight}");
  313. builder.AppendLine($"Framerate: {outboundStats.framesPerSecond}");
  314. }
  315. if (lastReport.TryGetValue(outboundStats.Id, out var lastStats) &&
  316. lastStats is RTCOutboundRTPStreamStats lastOutboundStats)
  317. {
  318. var duration = (double)(outboundStats.Timestamp - lastOutboundStats.Timestamp) / 1000000;
  319. var bitrate = (8 * (outboundStats.bytesSent - lastOutboundStats.bytesSent) / duration) / 1000;
  320. builder.AppendLine($"Bitrate: {bitrate:F2} kbit/sec");
  321. }
  322. }
  323. }
  324. return builder.ToString();
  325. }
  326. }
  327. internal class WaitUntilWithTimeout : CustomYieldInstruction
  328. {
  329. public bool IsCompleted { get; private set; }
  330. private readonly float timeoutTime;
  331. private readonly System.Func<bool> predicate;
  332. public override bool keepWaiting
  333. {
  334. get
  335. {
  336. IsCompleted = predicate();
  337. if (IsCompleted)
  338. {
  339. return false;
  340. }
  341. return !(Time.realtimeSinceStartup >= timeoutTime);
  342. }
  343. }
  344. public WaitUntilWithTimeout(System.Func<bool> predicate, float timeout)
  345. {
  346. this.timeoutTime = Time.realtimeSinceStartup + timeout;
  347. this.predicate = predicate;
  348. }
  349. }
  350. }