WebRTCStats.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using Unity.EditorCoroutines.Editor;
  7. using UnityEditor;
  8. using UnityEditor.UIElements;
  9. using UnityEngine;
  10. using UnityEngine.UIElements;
  11. namespace Unity.WebRTC.Editor
  12. {
  13. internal delegate void OnPeerListHandler(IEnumerable<WeakReference<RTCPeerConnection>> peerList);
  14. internal delegate void OnStatsReportHandler(RTCPeerConnection peer, RTCStatsReport statsReport);
  15. internal class WebRTCStats : EditorWindow
  16. {
  17. [MenuItem("Window/Analysis/WebRTC Stats")]
  18. public static void Init()
  19. {
  20. WebRTCStats wnd = GetWindow<WebRTCStats>();
  21. wnd.titleContent = new GUIContent("WebRTC Stats");
  22. }
  23. private const int UpdateStatsInterval = 1;
  24. private static readonly Color BackgroundColorInProSkin = new Color(45 / 255f, 45 / 255f, 45 / 255f);
  25. public event OnPeerListHandler OnPeerList;
  26. public event OnStatsReportHandler OnStats;
  27. private EditorCoroutine m_editorCoroutine;
  28. private Dictionary<int, PeerConnectionRecord> m_peerConnenctionDataStore =
  29. new Dictionary<int, PeerConnectionRecord>();
  30. private void OnEnable()
  31. {
  32. var root = this.rootVisualElement;
  33. root.style.backgroundColor = EditorGUIUtility.isProSkin ? BackgroundColorInProSkin : Color.white;
  34. var toolbar = new Toolbar {style = {alignItems = Align.FlexEnd}};
  35. root.Add(toolbar);
  36. toolbar.Add(new ToolbarSpacer {flex = true});
  37. var buttonContainer = new VisualElement
  38. {
  39. tooltip = "Save current webrtc stats information to a json file",
  40. };
  41. toolbar.Add(buttonContainer);
  42. var dumpButton = new ToolbarButton(() =>
  43. {
  44. if (!m_peerConnenctionDataStore.Any())
  45. {
  46. return;
  47. }
  48. var filePath = EditorUtility.SaveFilePanel("Save", "Assets", "dump", "json");
  49. if (string.IsNullOrEmpty(filePath))
  50. {
  51. return;
  52. }
  53. var peerRecord = string.Join(",",
  54. m_peerConnenctionDataStore.Select(record => $"\"{record.Key}\":{{{record.Value.ToJson()}}}"));
  55. var json =
  56. $"{{\"getUserMedia\":[], \"PeerConnections\":{{{peerRecord}}}, \"UserAgent\":\"UnityEditor\"}}";
  57. File.WriteAllText(filePath, json);
  58. })
  59. { text = "Save"};
  60. buttonContainer.Add(dumpButton);
  61. root.Add(CreateStatsView());
  62. EditorApplication.update += () =>
  63. {
  64. dumpButton.SetEnabled(m_peerConnenctionDataStore.Any());
  65. };
  66. EditorApplication.playModeStateChanged += change =>
  67. {
  68. switch (change)
  69. {
  70. case PlayModeStateChange.EnteredPlayMode:
  71. m_peerConnenctionDataStore.Clear();
  72. m_editorCoroutine = EditorCoroutineUtility.StartCoroutineOwnerless(GetStatsPolling());
  73. break;
  74. case PlayModeStateChange.ExitingPlayMode:
  75. EditorCoroutineUtility.StopCoroutine(m_editorCoroutine);
  76. break;
  77. }
  78. };
  79. if (EditorApplication.isPlaying && m_editorCoroutine == null)
  80. {
  81. m_editorCoroutine = EditorCoroutineUtility.StartCoroutineOwnerless(GetStatsPolling());
  82. }
  83. }
  84. private void OnDisable()
  85. {
  86. if (m_editorCoroutine != null)
  87. {
  88. EditorCoroutineUtility.StopCoroutine(m_editorCoroutine);
  89. }
  90. m_peerConnenctionDataStore.Clear();
  91. }
  92. IEnumerator GetStatsPolling()
  93. {
  94. while (true)
  95. {
  96. var peerList = WebRTC.PeerList;
  97. if (peerList != null)
  98. {
  99. OnPeerList?.Invoke(peerList);
  100. foreach (var weakReference in peerList)
  101. {
  102. if (!weakReference.TryGetTarget(out var peer))
  103. {
  104. continue;
  105. }
  106. var op = peer.GetStats();
  107. yield return op;
  108. if (!op.IsError)
  109. {
  110. OnStats?.Invoke(peer, op.Value);
  111. var peerId = peer.GetHashCode();
  112. if (!m_peerConnenctionDataStore.ContainsKey(peerId))
  113. {
  114. m_peerConnenctionDataStore[peerId] = new PeerConnectionRecord(peer.GetConfiguration());
  115. }
  116. m_peerConnenctionDataStore[peerId].Update(op.Value);
  117. }
  118. }
  119. }
  120. yield return new EditorWaitForSeconds(UpdateStatsInterval);
  121. }
  122. }
  123. private VisualElement CreateStatsView()
  124. {
  125. var container = new VisualElement {style = {flexDirection = FlexDirection.Row, flexGrow = 1,}};
  126. var sideView = new VisualElement
  127. {
  128. style = {borderRightColor = Color.gray, borderRightWidth = 1, width = 250,}
  129. };
  130. var mainView = new VisualElement {style = {flexGrow = 1}};
  131. container.Add(sideView);
  132. container.Add(mainView);
  133. // peer connection list view
  134. var peerListView = new PeerListView(this);
  135. sideView.Add(peerListView.Create());
  136. peerListView.OnChangePeer += newPeer =>
  137. {
  138. mainView.Clear();
  139. // main stats view
  140. var statsView = new PeerStatsView(newPeer, this);
  141. mainView.Add(statsView.Create());
  142. };
  143. mainView.Add(new Label("Statistics are displayed when in play mode"));
  144. return container;
  145. }
  146. }
  147. internal class PeerConnectionRecord
  148. {
  149. private readonly RTCConfiguration m_config;
  150. private readonly Dictionary<string, StatsRecord> m_statsRecordMap;
  151. public PeerConnectionRecord(RTCConfiguration config)
  152. {
  153. m_config = config;
  154. m_statsRecordMap = new Dictionary<string, StatsRecord>();
  155. }
  156. public void Update(RTCStatsReport report)
  157. {
  158. foreach (var element in report.Stats)
  159. {
  160. if (!m_statsRecordMap.ContainsKey(element.Key))
  161. {
  162. m_statsRecordMap[element.Key] = new StatsRecord(element.Value.Id);
  163. }
  164. m_statsRecordMap[element.Key].Update(element.Value.Timestamp, element.Value.Dict);
  165. }
  166. }
  167. public string ToJson()
  168. {
  169. var constraintsJson = "\"constraints\": \"\"";
  170. var configJson = $"\"rtcConfiguration\":{JsonUtility.ToJson(m_config)}";
  171. var statsJson = $"\"stats\":{{{string.Join(",", m_statsRecordMap.Select(x => x.Value.ToJson()))}}}";
  172. var url = "\"url\":\"\"";
  173. var updateLog = "\"updateLog\":[]";
  174. return string.Join(",", constraintsJson, configJson, statsJson, url, updateLog);
  175. }
  176. }
  177. internal class StatsRecord
  178. {
  179. private const int MAX_BUFFER_SIZE = 1000;
  180. private readonly Dictionary<string, List<(long timeStamp, object value)>> m_memberRecord;
  181. private readonly string m_id;
  182. public StatsRecord(string id)
  183. {
  184. m_id = id;
  185. m_memberRecord = new Dictionary<string, List<(long, object)>>();
  186. }
  187. public void Update(long timeStamp, IDictionary<string, object> record)
  188. {
  189. foreach (var pair in record)
  190. {
  191. if (!m_memberRecord.ContainsKey((pair.Key)))
  192. {
  193. m_memberRecord[pair.Key] = new List<(long, object)>();
  194. }
  195. var target = m_memberRecord[pair.Key];
  196. if (target.Count > MAX_BUFFER_SIZE)
  197. {
  198. target.RemoveAt(0);
  199. }
  200. target.Add((timeStamp, pair.Value));
  201. }
  202. }
  203. public string ToJson()
  204. {
  205. return string.Join(",", m_memberRecord.Select(x =>
  206. {
  207. var start = DateTimeOffset.FromUnixTimeMilliseconds(x.Value.Min(y => y.timeStamp)/1000).DateTime.ToUniversalTime().ToString("O");
  208. var end = DateTimeOffset.FromUnixTimeMilliseconds(x.Value.Max(y => y.timeStamp)/1000).DateTime.ToUniversalTime().ToString("O");
  209. var values = string.Join(",", x.Value.Select(y =>
  210. {
  211. if (y.value is string z && !string.IsNullOrEmpty(z))
  212. {
  213. return $"\\\"{z}\\\"";
  214. }
  215. if (y.value is bool b)
  216. {
  217. return b.ToString().ToLower();
  218. }
  219. return y.value;
  220. }).Where(y =>
  221. {
  222. if (y is string z)
  223. {
  224. return !string.IsNullOrEmpty(z);
  225. }
  226. return y != null;
  227. }));
  228. return string.IsNullOrEmpty(values) ? "" : $"\"{m_id}-{x.Key}\":{{\"startTime\":\"{start}\", \"endTime\":\"{end}\", \"values\":\"[{values}]\"}}";
  229. }).Where(x => !string.IsNullOrEmpty(x)));
  230. }
  231. }
  232. }