@stream-io/video-client 0.0.1-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/LICENSE +219 -0
  2. package/README.md +14 -0
  3. package/coverage/lcov-report/base.css +224 -0
  4. package/coverage/lcov-report/block-navigation.js +87 -0
  5. package/coverage/lcov-report/favicon.png +0 -0
  6. package/coverage/lcov-report/index.html +296 -0
  7. package/coverage/lcov-report/prettify.css +1 -0
  8. package/coverage/lcov-report/prettify.js +2 -0
  9. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  10. package/coverage/lcov-report/sorter.js +196 -0
  11. package/coverage/lcov-report/src/Batcher.ts.html +214 -0
  12. package/coverage/lcov-report/src/CallDropScheduler.ts.html +661 -0
  13. package/coverage/lcov-report/src/StreamSfuClient.ts.html +640 -0
  14. package/coverage/lcov-report/src/StreamVideoClient.ts.html +1594 -0
  15. package/coverage/lcov-report/src/config/defaultConfigs.ts.html +130 -0
  16. package/coverage/lcov-report/src/config/index.html +116 -0
  17. package/coverage/lcov-report/src/coordinator/StreamCoordinatorClient.ts.html +430 -0
  18. package/coverage/lcov-report/src/coordinator/connection/base64.ts.html +325 -0
  19. package/coverage/lcov-report/src/coordinator/connection/client.ts.html +2527 -0
  20. package/coverage/lcov-report/src/coordinator/connection/connection.ts.html +2335 -0
  21. package/coverage/lcov-report/src/coordinator/connection/connection_fallback.ts.html +802 -0
  22. package/coverage/lcov-report/src/coordinator/connection/errors.ts.html +295 -0
  23. package/coverage/lcov-report/src/coordinator/connection/index.html +251 -0
  24. package/coverage/lcov-report/src/coordinator/connection/insights.ts.html +349 -0
  25. package/coverage/lcov-report/src/coordinator/connection/signing.ts.html +397 -0
  26. package/coverage/lcov-report/src/coordinator/connection/token_manager.ts.html +565 -0
  27. package/coverage/lcov-report/src/coordinator/connection/types.ts.html +418 -0
  28. package/coverage/lcov-report/src/coordinator/connection/utils.ts.html +529 -0
  29. package/coverage/lcov-report/src/coordinator/index.html +116 -0
  30. package/coverage/lcov-report/src/events/call.ts.html +583 -0
  31. package/coverage/lcov-report/src/events/index.html +161 -0
  32. package/coverage/lcov-report/src/events/internal.ts.html +226 -0
  33. package/coverage/lcov-report/src/events/participant.ts.html +376 -0
  34. package/coverage/lcov-report/src/events/speaker.ts.html +271 -0
  35. package/coverage/lcov-report/src/gen/google/protobuf/index.html +131 -0
  36. package/coverage/lcov-report/src/gen/google/protobuf/struct.ts.html +1528 -0
  37. package/coverage/lcov-report/src/gen/google/protobuf/timestamp.ts.html +958 -0
  38. package/coverage/lcov-report/src/gen/video/sfu/event/events.ts.html +5971 -0
  39. package/coverage/lcov-report/src/gen/video/sfu/event/index.html +116 -0
  40. package/coverage/lcov-report/src/gen/video/sfu/models/index.html +116 -0
  41. package/coverage/lcov-report/src/gen/video/sfu/models/models.ts.html +3271 -0
  42. package/coverage/lcov-report/src/index.html +161 -0
  43. package/coverage/lcov-report/src/rpc/index.html +131 -0
  44. package/coverage/lcov-report/src/rpc/index.ts.html +91 -0
  45. package/coverage/lcov-report/src/rpc/latency.ts.html +214 -0
  46. package/coverage/lcov-report/src/rtc/Call.ts.html +1840 -0
  47. package/coverage/lcov-report/src/rtc/CallMetadata.ts.html +157 -0
  48. package/coverage/lcov-report/src/rtc/Dispatcher.ts.html +223 -0
  49. package/coverage/lcov-report/src/rtc/IceTrickleBuffer.ts.html +148 -0
  50. package/coverage/lcov-report/src/rtc/callEventHandlers.ts.html +196 -0
  51. package/coverage/lcov-report/src/rtc/codecs.ts.html +268 -0
  52. package/coverage/lcov-report/src/rtc/helpers/iceCandidate.ts.html +133 -0
  53. package/coverage/lcov-report/src/rtc/helpers/index.html +131 -0
  54. package/coverage/lcov-report/src/rtc/helpers/tracks.ts.html +139 -0
  55. package/coverage/lcov-report/src/rtc/index.html +251 -0
  56. package/coverage/lcov-report/src/rtc/publisher.ts.html +1000 -0
  57. package/coverage/lcov-report/src/rtc/signal.ts.html +187 -0
  58. package/coverage/lcov-report/src/rtc/subscriber.ts.html +340 -0
  59. package/coverage/lcov-report/src/rtc/videoLayers.ts.html +394 -0
  60. package/coverage/lcov-report/src/stats/index.html +116 -0
  61. package/coverage/lcov-report/src/stats/state-store-stats-reporter.ts.html +1177 -0
  62. package/coverage/lcov-report/src/store/index.html +146 -0
  63. package/coverage/lcov-report/src/store/index.ts.html +91 -0
  64. package/coverage/lcov-report/src/store/rxUtils.ts.html +211 -0
  65. package/coverage/lcov-report/src/store/stateStore.ts.html +1108 -0
  66. package/coverage/lcov.info +11463 -0
  67. package/coverage/tmp/coverage-2131-1675959950516-2.json +1 -0
  68. package/dist/index.d.ts +18 -0
  69. package/dist/index.js +8636 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/src/Batcher.d.ts +12 -0
  72. package/dist/src/CallDropScheduler.d.ts +44 -0
  73. package/dist/src/StreamSfuClient.d.ts +25 -0
  74. package/dist/src/StreamVideoClient.d.ts +145 -0
  75. package/dist/src/__tests__/StreamVideoClient.test.d.ts +1 -0
  76. package/dist/src/config/defaultConfigs.d.ts +2 -0
  77. package/dist/src/config/types.d.ts +29 -0
  78. package/dist/src/coordinator/StreamCoordinatorClient.d.ts +19 -0
  79. package/dist/src/coordinator/connection/base64.d.ts +2 -0
  80. package/dist/src/coordinator/connection/client.d.ts +174 -0
  81. package/dist/src/coordinator/connection/connection.d.ts +139 -0
  82. package/dist/src/coordinator/connection/connection_fallback.d.ts +38 -0
  83. package/dist/src/coordinator/connection/errors.d.ts +16 -0
  84. package/dist/src/coordinator/connection/events.d.ts +10 -0
  85. package/dist/src/coordinator/connection/insights.d.ts +58 -0
  86. package/dist/src/coordinator/connection/signing.d.ts +30 -0
  87. package/dist/src/coordinator/connection/token_manager.d.ts +39 -0
  88. package/dist/src/coordinator/connection/types.d.ts +87 -0
  89. package/dist/src/coordinator/connection/utils.d.ts +25 -0
  90. package/dist/src/devices.d.ts +79 -0
  91. package/dist/src/events/call.d.ts +26 -0
  92. package/dist/src/events/internal.d.ts +8 -0
  93. package/dist/src/events/participant.d.ts +21 -0
  94. package/dist/src/events/speaker.d.ts +10 -0
  95. package/dist/src/gen/coordinator/index.d.ts +1973 -0
  96. package/dist/src/gen/google/protobuf/descriptor.d.ts +1650 -0
  97. package/dist/src/gen/google/protobuf/duration.d.ts +113 -0
  98. package/dist/src/gen/google/protobuf/struct.d.ts +184 -0
  99. package/dist/src/gen/google/protobuf/timestamp.d.ts +158 -0
  100. package/dist/src/gen/video/coordinator/broadcast_v1/broadcast.d.ts +66 -0
  101. package/dist/src/gen/video/coordinator/call_v1/call.d.ts +254 -0
  102. package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.d.ts +351 -0
  103. package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.d.ts +1488 -0
  104. package/dist/src/gen/video/coordinator/client_v1_rpc/envelopes.d.ts +143 -0
  105. package/dist/src/gen/video/coordinator/client_v1_rpc/websocket.d.ts +292 -0
  106. package/dist/src/gen/video/coordinator/edge_v1/edge.d.ts +183 -0
  107. package/dist/src/gen/video/coordinator/event_v1/event.d.ts +411 -0
  108. package/dist/src/gen/video/coordinator/geofence_v1/geofence.d.ts +63 -0
  109. package/dist/src/gen/video/coordinator/member_v1/member.d.ts +59 -0
  110. package/dist/src/gen/video/coordinator/participant_v1/participant.d.ts +103 -0
  111. package/dist/src/gen/video/coordinator/push_v1/push.d.ts +240 -0
  112. package/dist/src/gen/video/coordinator/stat_v1/stat.d.ts +308 -0
  113. package/dist/src/gen/video/coordinator/user_v1/user.d.ts +112 -0
  114. package/dist/src/gen/video/coordinator/utils_v1/utils.d.ts +47 -0
  115. package/dist/src/gen/video/sfu/event/events.d.ts +736 -0
  116. package/dist/src/gen/video/sfu/models/models.d.ts +460 -0
  117. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +89 -0
  118. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +320 -0
  119. package/dist/src/helpers/browsers.d.ts +8 -0
  120. package/dist/src/helpers/sound-detector.d.ts +34 -0
  121. package/dist/src/rpc/createClient.d.ts +10 -0
  122. package/dist/src/rpc/index.d.ts +2 -0
  123. package/dist/src/rpc/latency.d.ts +9 -0
  124. package/dist/src/rtc/Call.d.ts +180 -0
  125. package/dist/src/rtc/CallMetadata.d.ts +9 -0
  126. package/dist/src/rtc/Dispatcher.d.ts +9 -0
  127. package/dist/src/rtc/IceTrickleBuffer.d.ts +11 -0
  128. package/dist/src/rtc/callEventHandlers.d.ts +5 -0
  129. package/dist/src/rtc/codecs.d.ts +2 -0
  130. package/dist/src/rtc/helpers/iceCandidate.d.ts +2 -0
  131. package/dist/src/rtc/helpers/tracks.d.ts +3 -0
  132. package/dist/src/rtc/publisher.d.ts +53 -0
  133. package/dist/src/rtc/signal.d.ts +5 -0
  134. package/dist/src/rtc/subscriber.d.ts +7 -0
  135. package/dist/src/rtc/types.d.ts +84 -0
  136. package/dist/src/rtc/videoLayers.d.ts +17 -0
  137. package/dist/src/stats/coordinator-stats-reporter.d.ts +10 -0
  138. package/dist/src/stats/state-store-stats-reporter.d.ts +57 -0
  139. package/dist/src/stats/types.d.ts +42 -0
  140. package/dist/src/store/index.d.ts +2 -0
  141. package/dist/src/store/rxUtils.d.ts +18 -0
  142. package/dist/src/store/stateStore.d.ts +182 -0
  143. package/generate-openapi.sh +32 -0
  144. package/index.ts +31 -0
  145. package/openapitools.json +7 -0
  146. package/package.json +54 -0
  147. package/rollup.config.mjs +48 -0
  148. package/src/Batcher.ts +43 -0
  149. package/src/CallDropScheduler.ts +192 -0
  150. package/src/StreamSfuClient.ts +185 -0
  151. package/src/StreamVideoClient.ts +503 -0
  152. package/src/__tests__/StreamVideoClient.test.ts +83 -0
  153. package/src/config/defaultConfigs.ts +15 -0
  154. package/src/config/types.ts +30 -0
  155. package/src/coordinator/StreamCoordinatorClient.ts +115 -0
  156. package/src/coordinator/connection/base64.ts +80 -0
  157. package/src/coordinator/connection/client.ts +814 -0
  158. package/src/coordinator/connection/connection.ts +750 -0
  159. package/src/coordinator/connection/connection_fallback.ts +239 -0
  160. package/src/coordinator/connection/errors.ts +70 -0
  161. package/src/coordinator/connection/events.ts +13 -0
  162. package/src/coordinator/connection/insights.ts +88 -0
  163. package/src/coordinator/connection/signing.ts +104 -0
  164. package/src/coordinator/connection/token_manager.ts +160 -0
  165. package/src/coordinator/connection/types.ts +111 -0
  166. package/src/coordinator/connection/utils.ts +148 -0
  167. package/src/devices.ts +266 -0
  168. package/src/events/call.ts +166 -0
  169. package/src/events/internal.ts +47 -0
  170. package/src/events/participant.ts +97 -0
  171. package/src/events/speaker.ts +62 -0
  172. package/src/gen/coordinator/index.ts +1956 -0
  173. package/src/gen/google/protobuf/descriptor.ts +3466 -0
  174. package/src/gen/google/protobuf/duration.ts +232 -0
  175. package/src/gen/google/protobuf/struct.ts +481 -0
  176. package/src/gen/google/protobuf/timestamp.ts +291 -0
  177. package/src/gen/video/coordinator/broadcast_v1/broadcast.ts +154 -0
  178. package/src/gen/video/coordinator/call_v1/call.ts +651 -0
  179. package/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.ts +463 -0
  180. package/src/gen/video/coordinator/client_v1_rpc/client_rpc.ts +3819 -0
  181. package/src/gen/video/coordinator/client_v1_rpc/envelopes.ts +424 -0
  182. package/src/gen/video/coordinator/client_v1_rpc/websocket.ts +719 -0
  183. package/src/gen/video/coordinator/edge_v1/edge.ts +532 -0
  184. package/src/gen/video/coordinator/event_v1/event.ts +1171 -0
  185. package/src/gen/video/coordinator/geofence_v1/geofence.ts +128 -0
  186. package/src/gen/video/coordinator/member_v1/member.ts +138 -0
  187. package/src/gen/video/coordinator/participant_v1/participant.ts +261 -0
  188. package/src/gen/video/coordinator/push_v1/push.ts +651 -0
  189. package/src/gen/video/coordinator/stat_v1/stat.ts +656 -0
  190. package/src/gen/video/coordinator/user_v1/user.ts +277 -0
  191. package/src/gen/video/coordinator/utils_v1/utils.ts +98 -0
  192. package/src/gen/video/sfu/event/events.ts +1962 -0
  193. package/src/gen/video/sfu/models/models.ts +1062 -0
  194. package/src/gen/video/sfu/signal_rpc/signal.client.ts +108 -0
  195. package/src/gen/video/sfu/signal_rpc/signal.ts +906 -0
  196. package/src/helpers/browsers.ts +13 -0
  197. package/src/helpers/sound-detector.ts +85 -0
  198. package/src/rpc/createClient.ts +50 -0
  199. package/src/rpc/index.ts +2 -0
  200. package/src/rpc/latency.ts +43 -0
  201. package/src/rtc/Call.ts +585 -0
  202. package/src/rtc/CallMetadata.ts +24 -0
  203. package/src/rtc/Dispatcher.ts +46 -0
  204. package/src/rtc/IceTrickleBuffer.ts +21 -0
  205. package/src/rtc/callEventHandlers.ts +37 -0
  206. package/src/rtc/codecs.ts +61 -0
  207. package/src/rtc/helpers/iceCandidate.ts +16 -0
  208. package/src/rtc/helpers/tracks.ts +18 -0
  209. package/src/rtc/publisher.ts +305 -0
  210. package/src/rtc/signal.ts +34 -0
  211. package/src/rtc/subscriber.ts +85 -0
  212. package/src/rtc/types.ts +105 -0
  213. package/src/rtc/videoLayers.ts +103 -0
  214. package/src/stats/coordinator-stats-reporter.ts +167 -0
  215. package/src/stats/state-store-stats-reporter.ts +364 -0
  216. package/src/stats/types.ts +46 -0
  217. package/src/store/index.ts +2 -0
  218. package/src/store/rxUtils.ts +42 -0
  219. package/src/store/stateStore.ts +341 -0
  220. package/tsconfig.json +25 -0
  221. package/typedoc.json +11 -0
  222. package/vite.config.ts +11 -0
@@ -0,0 +1,103 @@
1
+ export type OptimalVideoLayer = RTCRtpEncodingParameters & {
2
+ width: number;
3
+ height: number;
4
+
5
+ // defined here until we have this prop included in TypeScript
6
+ // https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1380
7
+ maxFramerate?: number;
8
+ };
9
+
10
+ export const findOptimalVideoLayers = (videoTrack: MediaStreamTrack) => {
11
+ const steps: [number, number, number][] = [
12
+ [1920, 1080, 3000000],
13
+ [1280, 720, 1250000],
14
+ [960, 540, 850000],
15
+ [640, 480, 500000],
16
+ [320, 240, 250000],
17
+ [160, 120, 125000],
18
+ ];
19
+
20
+ const optimalVideoLayers: OptimalVideoLayer[] = [];
21
+ const settings = videoTrack.getSettings();
22
+ for (let step = 0; step < steps.length; step++) {
23
+ const [w, h, maxBitrate] = steps[step];
24
+ // found ideal layer
25
+ if (w === settings.width && h === settings.height) {
26
+ let scaleFactor: number = 1;
27
+ ['f', 'h', 'q'].forEach((rid) => {
28
+ // Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
29
+ // when deciding which layer to disable when CPU or bandwidth is constrained.
30
+ // Encodings should be ordered in increasing spatial resolution order.
31
+ optimalVideoLayers.unshift({
32
+ active: true,
33
+ rid,
34
+ width: w / scaleFactor,
35
+ height: h / scaleFactor,
36
+ maxBitrate: maxBitrate / scaleFactor,
37
+ scaleResolutionDownBy: scaleFactor,
38
+ maxFramerate: {
39
+ f: 30,
40
+ h: 25,
41
+ q: 20,
42
+ }[rid],
43
+ });
44
+ scaleFactor *= 2;
45
+ });
46
+
47
+ break;
48
+ }
49
+ }
50
+ // for simplicity, we start with all layers enabled, then this function
51
+ // will clear/reassign the layers that are not needed
52
+ return withSimulcastConstraints(settings, optimalVideoLayers);
53
+ };
54
+
55
+ /**
56
+ * Browsers have different simulcast constraints for different video resolutions.
57
+ *
58
+ * This function modifies the provided list of video layers according to the
59
+ * current implementation of simulcast constraints in the Chromium based browsers.
60
+ *
61
+ * https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/media/engine/simulcast.cc#90
62
+ */
63
+ const withSimulcastConstraints = (
64
+ settings: MediaTrackSettings,
65
+ optimalVideoLayers: OptimalVideoLayer[],
66
+ ) => {
67
+ let layers;
68
+
69
+ const size = Math.max(settings.width || 0, settings.height || 0);
70
+ if (size <= 320) {
71
+ // provide only one layer 320x240 (q), the one with the highest quality
72
+ layers = optimalVideoLayers.filter((layer) => layer.rid === 'f');
73
+ } else if (size <= 640) {
74
+ // provide two layers, 160x120 (q) and 640x480 (h)
75
+ layers = optimalVideoLayers.filter((layer) => layer.rid !== 'h');
76
+ } else {
77
+ // provide three layers for sizes > 640x480
78
+ layers = optimalVideoLayers;
79
+ }
80
+
81
+ const ridMapping = ['q', 'h', 'f'];
82
+ return layers.map((layer, index) => ({
83
+ ...layer,
84
+ rid: ridMapping[index], // reassign rid
85
+ }));
86
+ };
87
+
88
+ export const findOptimalScreenSharingLayers = (
89
+ videoTrack: MediaStreamTrack,
90
+ ): OptimalVideoLayer[] => {
91
+ const settings = videoTrack.getSettings();
92
+ return [
93
+ {
94
+ active: true,
95
+ rid: 'q', // single track, start from 'q'
96
+ width: settings.width || 0,
97
+ height: settings.height || 0,
98
+ maxBitrate: 3000000,
99
+ scaleResolutionDownBy: 1,
100
+ maxFramerate: 30,
101
+ },
102
+ ];
103
+ };
@@ -0,0 +1,167 @@
1
+ import { StreamVideoReadOnlyStateStore } from '../store';
2
+ import {
3
+ ReportCallStatEventRequest,
4
+ ReportCallStatEventResponse,
5
+ ReportCallStatsResponse,
6
+ } from '../gen/video/coordinator/client_v1_rpc/client_rpc';
7
+ import {
8
+ MediaStateChange,
9
+ MediaStateChangeReason,
10
+ MediaType,
11
+ MediaDirection,
12
+ } from '../gen/video/coordinator/stat_v1/stat';
13
+ import { pairwise, throttleTime } from 'rxjs';
14
+ import { StreamVideoParticipant } from '../rtc/types';
15
+ import { TrackType } from '../gen/video/sfu/models/models';
16
+
17
+ /**
18
+ * Collects stat metrics and events from the state store and sends them to the Coordinator API
19
+ * @param readOnlyStateStore
20
+ * @param sendStatMetrics
21
+ * @param sendStatEvent
22
+ */
23
+ export const reportStats = (
24
+ readOnlyStateStore: StreamVideoReadOnlyStateStore,
25
+ sendStatMetrics: (stats: Object) => Promise<ReportCallStatsResponse | void>,
26
+ sendStatEvent: (
27
+ statEvent: ReportCallStatEventRequest['event'],
28
+ ) => Promise<ReportCallStatEventResponse | void>,
29
+ ) => {
30
+ reportStatMetrics(readOnlyStateStore, sendStatMetrics);
31
+ reportStatEvents(readOnlyStateStore, sendStatEvent);
32
+ };
33
+
34
+ const reportStatMetrics = (
35
+ readOnlyStateStore: StreamVideoReadOnlyStateStore,
36
+ sendStatMetrics: (stats: Object) => Promise<ReportCallStatsResponse | void>,
37
+ ) => {
38
+ readOnlyStateStore.callStatsReport$
39
+ .pipe(throttleTime(15000))
40
+ .subscribe((report) => {
41
+ if (report?.publisherRawStats) {
42
+ const s: Record<string, any> = {};
43
+ report.publisherRawStats.forEach((v) => {
44
+ s[v.id] = v;
45
+ });
46
+ sendStatMetrics(s);
47
+ }
48
+ if (report?.subscriberRawStats) {
49
+ const s: Record<string, any> = {};
50
+ report.subscriberRawStats.forEach((v) => {
51
+ s[v.id] = v;
52
+ });
53
+ sendStatMetrics(s);
54
+ }
55
+ });
56
+ };
57
+
58
+ export const reportStatEvents = (
59
+ store: StreamVideoReadOnlyStateStore,
60
+ sendStatEvent: (
61
+ statEvent: ReportCallStatEventRequest['event'],
62
+ ) => Promise<ReportCallStatEventResponse | void>,
63
+ ) => {
64
+ store.localParticipant$
65
+ .pipe(pairwise())
66
+ .subscribe(([prevLocalParticipant, currentLocalParticipant]) => {
67
+ if (!prevLocalParticipant && currentLocalParticipant) {
68
+ const event: ReportCallStatEventRequest['event'] = {
69
+ oneofKind: 'participantConnected',
70
+ participantConnected: {},
71
+ };
72
+ sendStatEvent(event);
73
+ }
74
+ if (prevLocalParticipant && !currentLocalParticipant) {
75
+ const event: ReportCallStatEventRequest['event'] = {
76
+ oneofKind: 'participantDisconnected',
77
+ participantDisconnected: {},
78
+ };
79
+ sendStatEvent(event);
80
+ }
81
+ if (
82
+ (!prevLocalParticipant?.audioStream &&
83
+ currentLocalParticipant?.audioStream) ||
84
+ (prevLocalParticipant?.audioStream &&
85
+ !currentLocalParticipant?.audioStream)
86
+ ) {
87
+ const event: ReportCallStatEventRequest['event'] = {
88
+ oneofKind: 'mediaStateChanged',
89
+ mediaStateChanged: {
90
+ mediaType: MediaType.AUDIO,
91
+ change: currentLocalParticipant?.audioStream
92
+ ? MediaStateChange.STARTED
93
+ : MediaStateChange.ENDED,
94
+ reason: MediaStateChangeReason.CONNECTION,
95
+ direction: MediaDirection.SEND,
96
+ },
97
+ };
98
+ sendStatEvent(event);
99
+ }
100
+ if (
101
+ (!prevLocalParticipant?.videoStream &&
102
+ currentLocalParticipant?.videoStream) ||
103
+ (prevLocalParticipant?.videoStream &&
104
+ !currentLocalParticipant?.videoStream)
105
+ ) {
106
+ const event: ReportCallStatEventRequest['event'] = {
107
+ oneofKind: 'mediaStateChanged',
108
+ mediaStateChanged: {
109
+ mediaType: MediaType.VIDEO,
110
+ change: currentLocalParticipant?.videoStream
111
+ ? MediaStateChange.STARTED
112
+ : MediaStateChange.ENDED,
113
+ reason: MediaStateChangeReason.CONNECTION,
114
+ direction: MediaDirection.SEND,
115
+ },
116
+ };
117
+ sendStatEvent(event);
118
+ }
119
+ if (prevLocalParticipant && currentLocalParticipant) {
120
+ if (
121
+ isPublishingTrackOfType(prevLocalParticipant, TrackType.AUDIO) !==
122
+ isPublishingTrackOfType(currentLocalParticipant, TrackType.AUDIO)
123
+ ) {
124
+ const event: ReportCallStatEventRequest['event'] = {
125
+ oneofKind: 'mediaStateChanged',
126
+ mediaStateChanged: {
127
+ mediaType: MediaType.AUDIO,
128
+ change: isPublishingTrackOfType(
129
+ currentLocalParticipant,
130
+ TrackType.AUDIO,
131
+ )
132
+ ? MediaStateChange.ENDED
133
+ : MediaStateChange.STARTED,
134
+ reason: MediaStateChangeReason.MUTE,
135
+ direction: MediaDirection.SEND,
136
+ },
137
+ };
138
+ sendStatEvent(event);
139
+ }
140
+ if (
141
+ isPublishingTrackOfType(prevLocalParticipant, TrackType.VIDEO) !==
142
+ isPublishingTrackOfType(currentLocalParticipant, TrackType.VIDEO)
143
+ ) {
144
+ const event: ReportCallStatEventRequest['event'] = {
145
+ oneofKind: 'mediaStateChanged',
146
+ mediaStateChanged: {
147
+ mediaType: MediaType.VIDEO,
148
+ change: isPublishingTrackOfType(
149
+ currentLocalParticipant,
150
+ TrackType.VIDEO,
151
+ )
152
+ ? MediaStateChange.ENDED
153
+ : MediaStateChange.STARTED,
154
+ reason: MediaStateChangeReason.MUTE,
155
+ direction: MediaDirection.SEND,
156
+ },
157
+ };
158
+ sendStatEvent(event);
159
+ }
160
+ }
161
+ });
162
+ };
163
+
164
+ const isPublishingTrackOfType = (
165
+ participant: StreamVideoParticipant,
166
+ type: TrackType,
167
+ ) => participant.publishedTracks.includes(type);
@@ -0,0 +1,364 @@
1
+ import type {
2
+ AggregatedStatsReport,
3
+ BaseStats,
4
+ CallStatsReport,
5
+ ParticipantsStatsReport,
6
+ StatsReport,
7
+ } from './types';
8
+ import { StreamVideoWriteableStateStore } from '../store';
9
+ import { Publisher } from '../rtc/publisher';
10
+
11
+ export type StatsReporterOpts = {
12
+ subscriber: RTCPeerConnection;
13
+ publisher: Publisher;
14
+ store: StreamVideoWriteableStateStore;
15
+ pollingIntervalInMs?: number;
16
+ edgeName?: string;
17
+ };
18
+
19
+ export type StatsReporter = {
20
+ /**
21
+ * Will turn on stats reporting for a given sessionId.
22
+ *
23
+ * @param sessionId the session id.
24
+ */
25
+ startReportingStatsFor: (sessionId: string) => void;
26
+
27
+ /**
28
+ * Will turn off stats reporting for a given sessionId.
29
+ *
30
+ * @param sessionId the session id.
31
+ */
32
+ stopReportingStatsFor: (sessionId: string) => void;
33
+
34
+ /**
35
+ * Helper method for retrieving stats for a given peer connection kind
36
+ * and media stream flowing through it.
37
+ *
38
+ * @param kind the peer connection kind (subscriber or publisher).
39
+ * @param mediaStream the media stream.
40
+ */
41
+ getStatsForStream: (
42
+ kind: 'subscriber' | 'publisher',
43
+ mediaStream: MediaStream,
44
+ ) => Promise<StatsReport[]>;
45
+
46
+ /**
47
+ * Helper method for retrieving raw stats for a given peer connection kind.
48
+ *
49
+ * @param kind the peer connection kind (subscriber or publisher).
50
+ * @param selector the track selector. If not provided, stats for all tracks will be returned.
51
+ */
52
+ getRawStatsForTrack: (
53
+ kind: 'subscriber' | 'publisher',
54
+ selector?: MediaStreamTrack,
55
+ ) => Promise<RTCStatsReport | undefined>;
56
+
57
+ /**
58
+ * Stops the stats reporter and releases all resources.
59
+ */
60
+ stop: () => void;
61
+ };
62
+
63
+ /**
64
+ * Creates a new StatsReporter instance that collects metrics about the ongoing call and reports them to the state store
65
+ */
66
+ export const createStatsReporter = ({
67
+ subscriber,
68
+ publisher,
69
+ store,
70
+ edgeName,
71
+ pollingIntervalInMs = 2000,
72
+ }: StatsReporterOpts): StatsReporter => {
73
+ const getRawStatsForTrack = async (
74
+ kind: 'subscriber' | 'publisher',
75
+ selector?: MediaStreamTrack,
76
+ ) => {
77
+ if (kind === 'subscriber' && subscriber) {
78
+ return subscriber.getStats(selector);
79
+ } else if (kind === 'publisher' && publisher) {
80
+ return publisher.getStats(selector);
81
+ } else {
82
+ console.warn(`Can't retrieve RTC stats for`, kind);
83
+ return undefined;
84
+ }
85
+ };
86
+
87
+ const getStatsForStream = async (
88
+ kind: 'subscriber' | 'publisher',
89
+ mediaStream: MediaStream,
90
+ ) => {
91
+ const pc = kind === 'subscriber' ? subscriber : publisher;
92
+ const statsForStream: StatsReport[] = [];
93
+ for (let track of mediaStream.getTracks()) {
94
+ const report = await pc.getStats(track);
95
+ const stats = transform(report, {
96
+ // @ts-ignore
97
+ trackKind: track.kind,
98
+ kind,
99
+ });
100
+ statsForStream.push(stats);
101
+ }
102
+ return statsForStream;
103
+ };
104
+
105
+ const startReportingStatsFor = (sessionId: string) => {
106
+ sessionIdsToTrack.add(sessionId);
107
+ void run();
108
+ };
109
+
110
+ const stopReportingStatsFor = (sessionId: string) => {
111
+ sessionIdsToTrack.delete(sessionId);
112
+ void run();
113
+ };
114
+
115
+ const sessionIdsToTrack = new Set<string>();
116
+
117
+ /**
118
+ * The main stats reporting loop.
119
+ */
120
+ const run = async () => {
121
+ const participants = store.getCurrentValue(store.participantsSubject);
122
+ const participantStats: ParticipantsStatsReport = {};
123
+ const sessionIds = new Set(sessionIdsToTrack);
124
+ if (sessionIds.size > 0) {
125
+ for (let participant of participants) {
126
+ if (!sessionIds.has(participant.sessionId)) continue;
127
+ const kind = participant.isLoggedInUser ? 'publisher' : 'subscriber';
128
+ try {
129
+ const mergedStream = new MediaStream([
130
+ ...(participant.videoStream?.getVideoTracks() || []),
131
+ ...(participant.audioStream?.getAudioTracks() || []),
132
+ ]);
133
+ participantStats[participant.sessionId] = await getStatsForStream(
134
+ kind,
135
+ mergedStream,
136
+ );
137
+ mergedStream.getTracks().forEach((t) => {
138
+ mergedStream.removeTrack(t);
139
+ });
140
+ } catch (e) {
141
+ console.error(`Failed to collect stats for ${kind}`, participant, e);
142
+ }
143
+ }
144
+ }
145
+
146
+ const [subscriberStats, publisherStats] = await Promise.all([
147
+ subscriber
148
+ .getStats()
149
+ .then((report) =>
150
+ transform(report, {
151
+ kind: 'subscriber',
152
+ trackKind: 'video',
153
+ }),
154
+ )
155
+ .then(aggregate),
156
+ publisher
157
+ .getStats()
158
+ .then((report) =>
159
+ transform(report, {
160
+ kind: 'publisher',
161
+ trackKind: 'video',
162
+ }),
163
+ )
164
+ .then(aggregate),
165
+ ]);
166
+
167
+ const [subscriberRawStats, publisherRawStats] = await Promise.all([
168
+ getRawStatsForTrack('subscriber'),
169
+ getRawStatsForTrack('publisher'),
170
+ ]);
171
+
172
+ const statsReport: CallStatsReport = {
173
+ datacenter: edgeName || 'N/A',
174
+ publisherStats,
175
+ subscriberStats,
176
+ subscriberRawStats,
177
+ publisherRawStats,
178
+ participants: participantStats,
179
+ timestamp: Date.now(),
180
+ };
181
+
182
+ store.setCurrentValue(store.callStatsReportSubject, statsReport);
183
+ };
184
+
185
+ let timeoutId: NodeJS.Timeout | undefined;
186
+ if (pollingIntervalInMs > 0) {
187
+ const loop = async () => {
188
+ await run().catch((e) => {
189
+ console.log('Failed to collect stats', e);
190
+ });
191
+ timeoutId = setTimeout(loop, pollingIntervalInMs);
192
+ };
193
+ void loop();
194
+ }
195
+
196
+ const stop = () => {
197
+ if (timeoutId) {
198
+ clearTimeout(timeoutId);
199
+ }
200
+ };
201
+
202
+ return {
203
+ getRawStatsForTrack,
204
+ getStatsForStream,
205
+ startReportingStatsFor,
206
+ stopReportingStatsFor,
207
+ stop,
208
+ };
209
+ };
210
+
211
+ export type StatsTransformOpts = {
212
+ /**
213
+ * The kind of track we are transforming stats for.
214
+ */
215
+ trackKind: 'audio' | 'video';
216
+
217
+ /**
218
+ * The kind of peer connection we are transforming stats for.
219
+ */
220
+ kind: 'subscriber' | 'publisher';
221
+ };
222
+
223
+ /**
224
+ * Transforms raw RTC stats into a slimmer and uniform across browsers format.
225
+ *
226
+ * @param report the report to transform.
227
+ * @param opts the transform options.
228
+ */
229
+ const transform = (
230
+ report: RTCStatsReport,
231
+ opts: StatsTransformOpts,
232
+ ): StatsReport => {
233
+ const { trackKind, kind } = opts;
234
+ const direction = kind === 'subscriber' ? 'inbound-rtp' : 'outbound-rtp';
235
+ const stats = flatten(report);
236
+ const streams = stats
237
+ .filter(
238
+ (stat) =>
239
+ stat.type === direction &&
240
+ (stat as RTCRtpStreamStats).kind === trackKind,
241
+ )
242
+ .map((stat): BaseStats => {
243
+ const rtcStreamStats = stat as RTCInboundRtpStreamStats &
244
+ RTCOutboundRtpStreamStats;
245
+
246
+ const codec = stats.find(
247
+ (s) => s.type === 'codec' && s.id === rtcStreamStats.codecId,
248
+ ) as { mimeType: string } | undefined; // FIXME OL: incorrect type!
249
+
250
+ const transport = stats.find(
251
+ (s) => s.type === 'transport' && s.id === rtcStreamStats.transportId,
252
+ ) as RTCTransportStats | undefined;
253
+
254
+ let roundTripTime: number | undefined;
255
+ if (transport && transport.dtlsState === 'connected') {
256
+ const candidatePair = stats.find(
257
+ (s) =>
258
+ s.type === 'candidate-pair' &&
259
+ s.id === transport.selectedCandidatePairId,
260
+ ) as RTCIceCandidatePairStats | undefined;
261
+ roundTripTime = candidatePair?.currentRoundTripTime;
262
+ }
263
+
264
+ return {
265
+ bytesSent: rtcStreamStats.bytesSent,
266
+ bytesReceived: rtcStreamStats.bytesReceived,
267
+ codec: codec?.mimeType,
268
+ currentRoundTripTime: roundTripTime,
269
+ frameHeight: rtcStreamStats.frameHeight,
270
+ frameWidth: rtcStreamStats.frameWidth,
271
+ framesPerSecond: rtcStreamStats.framesPerSecond,
272
+ jitter: rtcStreamStats.jitter,
273
+ kind: rtcStreamStats.kind,
274
+ // @ts-ignore: available in Chrome only, TS doesn't recognize this
275
+ qualityLimitationReason: rtcStreamStats.qualityLimitationReason,
276
+ rid: rtcStreamStats.rid,
277
+ ssrc: rtcStreamStats.ssrc,
278
+ };
279
+ });
280
+
281
+ return {
282
+ rawStats: report,
283
+ streams,
284
+ timestamp: Date.now(),
285
+ };
286
+ };
287
+
288
+ /**
289
+ * Aggregates generic stats.
290
+ *
291
+ * @param stats the stats to aggregate.
292
+ */
293
+ const aggregate = (stats: StatsReport): AggregatedStatsReport => {
294
+ const aggregatedStats: AggregatedStatsReport = {
295
+ totalBytesSent: 0,
296
+ totalBytesReceived: 0,
297
+ averageJitterInMs: 0,
298
+ averageRoundTripTimeInMs: 0,
299
+ qualityLimitationReasons: 'none',
300
+ highestFrameWidth: 0,
301
+ highestFrameHeight: 0,
302
+ highestFramesPerSecond: 0,
303
+ timestamp: Date.now(),
304
+ };
305
+
306
+ let maxArea = -1;
307
+ const area = (w: number, h: number) => w * h;
308
+
309
+ const qualityLimitationReasons = new Set<string>();
310
+ const streams = stats.streams;
311
+ const report = streams.reduce((acc, stream) => {
312
+ acc.totalBytesSent += stream.bytesSent || 0;
313
+ acc.totalBytesReceived += stream.bytesReceived || 0;
314
+ acc.averageJitterInMs += stream.jitter || 0;
315
+ acc.averageRoundTripTimeInMs += stream.currentRoundTripTime || 0;
316
+
317
+ // naive calculation of the highest resolution
318
+ const streamArea = area(stream.frameWidth || 0, stream.frameHeight || 0);
319
+ if (streamArea > maxArea) {
320
+ acc.highestFrameWidth = stream.frameWidth || 0;
321
+ acc.highestFrameHeight = stream.frameHeight || 0;
322
+ acc.highestFramesPerSecond = stream.framesPerSecond || 0;
323
+ maxArea = streamArea;
324
+ }
325
+
326
+ qualityLimitationReasons.add(stream.qualityLimitationReason || '');
327
+ return acc;
328
+ }, aggregatedStats);
329
+
330
+ if (streams.length > 0) {
331
+ report.averageJitterInMs = Math.round(
332
+ (report.averageJitterInMs / streams.length) * 1000,
333
+ );
334
+ report.averageRoundTripTimeInMs = Math.round(
335
+ (report.averageRoundTripTimeInMs / streams.length) * 1000,
336
+ );
337
+ }
338
+
339
+ const qualityLimitationReason = [
340
+ qualityLimitationReasons.has('cpu') && 'cpu',
341
+ qualityLimitationReasons.has('bandwidth') && 'bandwidth',
342
+ qualityLimitationReasons.has('other') && 'other',
343
+ ]
344
+ .filter(Boolean)
345
+ .join(', ');
346
+ if (qualityLimitationReason) {
347
+ report.qualityLimitationReasons = qualityLimitationReason;
348
+ }
349
+
350
+ return report;
351
+ };
352
+
353
+ /**
354
+ * Flatten the stats report into an array of stats objects.
355
+ *
356
+ * @param report the report to flatten.
357
+ */
358
+ const flatten = (report: RTCStatsReport) => {
359
+ const stats: RTCStats[] = [];
360
+ report.forEach((s) => {
361
+ stats.push(s);
362
+ });
363
+ return stats;
364
+ };
@@ -0,0 +1,46 @@
1
+ export type BaseStats = {
2
+ bytesSent?: number;
3
+ bytesReceived?: number;
4
+ codec?: string;
5
+ currentRoundTripTime?: number;
6
+ frameWidth?: number;
7
+ frameHeight?: number;
8
+ framesPerSecond?: number;
9
+ jitter?: number;
10
+ kind?: string;
11
+ qualityLimitationReason?: string;
12
+ rid?: string;
13
+ ssrc?: number;
14
+ };
15
+
16
+ export type StatsReport = {
17
+ rawStats?: RTCStatsReport;
18
+ streams: BaseStats[];
19
+ timestamp: number;
20
+ };
21
+
22
+ export type AggregatedStatsReport = {
23
+ totalBytesSent: number;
24
+ totalBytesReceived: number;
25
+ averageJitterInMs: number;
26
+ averageRoundTripTimeInMs: number;
27
+ qualityLimitationReasons: string;
28
+ highestFrameWidth: number;
29
+ highestFrameHeight: number;
30
+ highestFramesPerSecond: number;
31
+ timestamp: number;
32
+ };
33
+
34
+ export type ParticipantsStatsReport = {
35
+ [sessionId: string]: StatsReport[] | undefined;
36
+ };
37
+
38
+ export type CallStatsReport = {
39
+ datacenter: string;
40
+ publisherStats: AggregatedStatsReport;
41
+ publisherRawStats?: RTCStatsReport;
42
+ subscriberStats: AggregatedStatsReport;
43
+ subscriberRawStats?: RTCStatsReport;
44
+ participants: ParticipantsStatsReport;
45
+ timestamp: number;
46
+ };
@@ -0,0 +1,2 @@
1
+ export * as RxUtils from './rxUtils';
2
+ export * from './stateStore';