@webex/plugin-meetings 2.60.1-next.13 → 2.60.1-next.14

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 (57) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/interpretation/index.js +1 -1
  4. package/dist/interpretation/siLanguage.js +1 -1
  5. package/dist/mediaQualityMetrics/config.d.ts +103 -99
  6. package/dist/mediaQualityMetrics/config.js +133 -129
  7. package/dist/mediaQualityMetrics/config.js.map +1 -1
  8. package/dist/meeting/index.js +4 -2
  9. package/dist/meeting/index.js.map +1 -1
  10. package/dist/meeting/request.d.ts +2 -0
  11. package/dist/meeting/request.js +4 -0
  12. package/dist/meeting/request.js.map +1 -1
  13. package/dist/meetings/index.js +19 -0
  14. package/dist/meetings/index.js.map +1 -1
  15. package/dist/meetings/util.js +1 -1
  16. package/dist/meetings/util.js.map +1 -1
  17. package/dist/metrics/constants.d.ts +2 -0
  18. package/dist/metrics/constants.js +3 -1
  19. package/dist/metrics/constants.js.map +1 -1
  20. package/dist/reachability/index.js +14 -20
  21. package/dist/reachability/index.js.map +1 -1
  22. package/dist/reconnection-manager/index.js +63 -43
  23. package/dist/reconnection-manager/index.js.map +1 -1
  24. package/dist/roap/turnDiscovery.d.ts +18 -2
  25. package/dist/roap/turnDiscovery.js +163 -69
  26. package/dist/roap/turnDiscovery.js.map +1 -1
  27. package/dist/rtcMetrics/index.d.ts +7 -0
  28. package/dist/rtcMetrics/index.js +38 -1
  29. package/dist/rtcMetrics/index.js.map +1 -1
  30. package/dist/statsAnalyzer/index.js +135 -23
  31. package/dist/statsAnalyzer/index.js.map +1 -1
  32. package/dist/statsAnalyzer/mqaUtil.d.ts +28 -4
  33. package/dist/statsAnalyzer/mqaUtil.js +278 -148
  34. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  35. package/dist/webinar/index.js +1 -1
  36. package/package.json +21 -21
  37. package/src/mediaQualityMetrics/config.ts +107 -107
  38. package/src/meeting/index.ts +2 -0
  39. package/src/meeting/request.ts +6 -0
  40. package/src/meetings/index.ts +22 -0
  41. package/src/meetings/util.ts +1 -1
  42. package/src/metrics/constants.ts +2 -0
  43. package/src/reachability/index.ts +0 -6
  44. package/src/reconnection-manager/index.ts +18 -7
  45. package/src/roap/turnDiscovery.ts +100 -24
  46. package/src/rtcMetrics/index.ts +43 -1
  47. package/src/statsAnalyzer/index.ts +158 -24
  48. package/src/statsAnalyzer/mqaUtil.ts +302 -154
  49. package/test/unit/spec/meeting/index.js +46 -0
  50. package/test/unit/spec/meeting/request.js +2 -0
  51. package/test/unit/spec/meetings/utils.js +35 -8
  52. package/test/unit/spec/reachability/index.ts +74 -0
  53. package/test/unit/spec/reconnection-manager/index.js +36 -1
  54. package/test/unit/spec/roap/turnDiscovery.ts +326 -76
  55. package/test/unit/spec/rtcMetrics/index.ts +32 -3
  56. package/test/unit/spec/stats-analyzer/index.js +439 -1
  57. package/test/utils/webex-test-users.js +12 -4
@@ -95,12 +95,6 @@ export default class Reachability {
95
95
  * @memberof Reachability
96
96
  */
97
97
  public async gatherReachability(): Promise<ReachabilityResults> {
98
- // Remove stored reachability results to ensure no stale data
99
- // @ts-ignore
100
- await this.webex.boundedStorage.del(this.namespace, REACHABILITY.localStorageResult);
101
- // @ts-ignore
102
- await this.webex.boundedStorage.del(this.namespace, REACHABILITY.localStorageJoinCookie);
103
-
104
98
  // Fetch clusters and measure latency
105
99
  try {
106
100
  const {clusters, joinCookie} = await this.reachabilityRequest.getClusters(
@@ -349,7 +349,20 @@ export default class ReconnectionManager {
349
349
  });
350
350
  }
351
351
 
352
- return this.executeReconnection({networkDisconnect}).catch((reconnectError) => {
352
+ try {
353
+ await this.webex.meetings.startReachability();
354
+ } catch (err) {
355
+ LoggerProxy.logger.info(
356
+ 'ReconnectionManager:index#reconnect --> Reachability failed, continuing with reconnection attempt, err: ',
357
+ err
358
+ );
359
+ }
360
+
361
+ try {
362
+ const media = await this.executeReconnection({networkDisconnect});
363
+
364
+ return media;
365
+ } catch (reconnectError) {
353
366
  if (reconnectError instanceof NeedsRetryError) {
354
367
  LoggerProxy.logger.info(
355
368
  'ReconnectionManager:index#reconnect --> Reconnection not successful, retrying.'
@@ -370,6 +383,7 @@ export default class ReconnectionManager {
370
383
  'ReconnectionManager:index#reconnect --> Sending reconnect abort metric.'
371
384
  );
372
385
 
386
+ // send call aborted event with catogery as expected as we are trying to rejoin
373
387
  // @ts-ignore
374
388
  this.webex.internal.newMetrics.submitClientEvent({
375
389
  name: 'client.call.aborted',
@@ -388,16 +402,13 @@ export default class ReconnectionManager {
388
402
  meetingId: this.meeting.id,
389
403
  },
390
404
  });
391
- if (reconnectError instanceof NeedsRejoinError) {
392
- // send call aborded event with catogery as expected as we are trying to rejoin
393
405
 
394
- if (this.autoRejoinEnabled) {
395
- return this.rejoinMeeting(reconnectError.wasSharing);
396
- }
406
+ if (reconnectError instanceof NeedsRejoinError && this.autoRejoinEnabled) {
407
+ return this.rejoinMeeting(reconnectError.wasSharing);
397
408
  }
398
409
 
399
410
  throw reconnectError;
400
- });
411
+ }
401
412
  }
402
413
 
403
414
  /**
@@ -56,7 +56,7 @@ export default class TurnDiscovery {
56
56
  * @private
57
57
  * @memberof Roap
58
58
  */
59
- private waitForTurnDiscoveryResponse() {
59
+ private waitForTurnDiscoveryResponse(): Promise<{isOkRequired: boolean}> {
60
60
  if (!this.defer) {
61
61
  LoggerProxy.logger.warn(
62
62
  'Roap:turnDiscovery#waitForTurnDiscoveryResponse --> TURN discovery is not in progress'
@@ -88,22 +88,32 @@ export default class TurnDiscovery {
88
88
  * handles TURN_DISCOVERY_RESPONSE roap message
89
89
  *
90
90
  * @param {Object} roapMessage
91
+ * @param {string} from string to indicate how we got the response (used just for logging)
91
92
  * @returns {void}
92
93
  * @public
93
94
  * @memberof Roap
94
95
  */
95
- public handleTurnDiscoveryResponse(roapMessage: object) {
96
- // @ts-ignore - Fix missing type
96
+ public handleTurnDiscoveryResponse(roapMessage: any, from: string) {
97
97
  const {headers} = roapMessage;
98
98
 
99
99
  if (!this.defer) {
100
100
  LoggerProxy.logger.warn(
101
- 'Roap:turnDiscovery#handleTurnDiscoveryResponse --> unexpected TURN discovery response'
101
+ `Roap:turnDiscovery#handleTurnDiscoveryResponse --> unexpected TURN discovery response ${from}`
102
102
  );
103
103
 
104
104
  return;
105
105
  }
106
106
 
107
+ if (roapMessage.messageType !== ROAP.ROAP_TYPES.TURN_DISCOVERY_RESPONSE) {
108
+ this.defer.reject(
109
+ new Error(
110
+ `TURN_DISCOVERY_RESPONSE ${from} has unexpected messageType: ${JSON.stringify(
111
+ roapMessage
112
+ )}`
113
+ )
114
+ );
115
+ }
116
+
107
117
  const expectedHeaders = [
108
118
  {headerName: 'x-cisco-turn-url', field: 'url'},
109
119
  {headerName: 'x-cisco-turn-username', field: 'username'},
@@ -129,21 +139,39 @@ export default class TurnDiscovery {
129
139
 
130
140
  if (foundHeaders !== expectedHeaders.length) {
131
141
  LoggerProxy.logger.warn(
132
- `Roap:turnDiscovery#handleTurnDiscoveryResponse --> missing some headers, received: ${JSON.stringify(
142
+ `Roap:turnDiscovery#handleTurnDiscoveryResponse --> missing some headers, received ${from}: ${JSON.stringify(
133
143
  headers
134
144
  )}`
135
145
  );
136
146
  this.defer.reject(
137
- new Error(`TURN_DISCOVERY_RESPONSE missing some headers: ${JSON.stringify(headers)}`)
147
+ new Error(
148
+ `TURN_DISCOVERY_RESPONSE ${from} missing some headers: ${JSON.stringify(headers)}`
149
+ )
138
150
  );
139
151
  } else {
140
152
  LoggerProxy.logger.info(
141
- `Roap:turnDiscovery#handleTurnDiscoveryResponse --> received a valid response, url=${this.turnInfo.url}`
153
+ `Roap:turnDiscovery#handleTurnDiscoveryResponse --> received a valid response ${from}, url=${this.turnInfo.url}`
142
154
  );
143
- this.defer.resolve();
155
+
156
+ this.defer.resolve({isOkRequired: !headers?.includes('noOkInTransaction')});
144
157
  }
145
158
  }
146
159
 
160
+ /**
161
+ * handles TURN_DISCOVERY_RESPONSE roap message that came in http response
162
+ *
163
+ * @param {Object} roapMessage
164
+ * @returns {Promise}
165
+ * @memberof Roap
166
+ */
167
+ private async handleTurnDiscoveryResponseInHttpResponse(
168
+ roapMessage: object
169
+ ): Promise<{isOkRequired: boolean}> {
170
+ this.handleTurnDiscoveryResponse(roapMessage, 'in http response');
171
+
172
+ return this.defer.promise;
173
+ }
174
+
147
175
  /**
148
176
  * sends the TURN_DISCOVERY_REQUEST roap request
149
177
  *
@@ -168,6 +196,7 @@ export default class TurnDiscovery {
168
196
  messageType: ROAP.ROAP_TYPES.TURN_DISCOVERY_REQUEST,
169
197
  version: ROAP.ROAP_VERSION,
170
198
  seq: TURN_DISCOVERY_SEQ,
199
+ headers: ['includeAnswerInHttpResponse', 'noOkInTransaction'],
171
200
  };
172
201
 
173
202
  LoggerProxy.logger.info(
@@ -186,10 +215,41 @@ export default class TurnDiscovery {
186
215
  // @ts-ignore - because of meeting.webex
187
216
  ipVersion: MeetingUtil.getIpVersion(meeting.webex),
188
217
  })
189
- .then(({mediaConnections}) => {
218
+ .then((response) => {
219
+ const {mediaConnections} = response;
220
+
221
+ let turnDiscoveryResponse;
222
+
190
223
  if (mediaConnections) {
191
224
  meeting.updateMediaConnections(mediaConnections);
225
+
226
+ if (mediaConnections[0]?.remoteSdp) {
227
+ const remoteSdp = JSON.parse(mediaConnections[0].remoteSdp);
228
+
229
+ if (remoteSdp.roapMessage) {
230
+ // yes, it's misleading that remoteSdp actually contains a TURN discovery response, but that's how the backend works...
231
+ const {seq, messageType, errorType, errorCause, headers} = remoteSdp.roapMessage;
232
+
233
+ turnDiscoveryResponse = {
234
+ seq,
235
+ messageType,
236
+ errorType,
237
+ errorCause,
238
+ headers,
239
+ };
240
+ }
241
+ }
242
+ }
243
+
244
+ if (!turnDiscoveryResponse) {
245
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_HTTP_RESPONSE_MISSING, {
246
+ correlationId: meeting.correlationId,
247
+ messageType: 'TURN_DISCOVERY_RESPONSE',
248
+ isMultistream: meeting.isMultistream,
249
+ });
192
250
  }
251
+
252
+ return turnDiscoveryResponse;
193
253
  });
194
254
  }
195
255
 
@@ -284,30 +344,46 @@ export default class TurnDiscovery {
284
344
  };
285
345
  }
286
346
 
287
- return this.sendRoapTurnDiscoveryRequest(meeting, isReconnecting)
288
- .then(() => this.waitForTurnDiscoveryResponse())
289
- .then(() => this.sendRoapOK(meeting))
290
- .then(() => {
291
- this.defer = undefined;
347
+ try {
348
+ const httpResponse = await this.sendRoapTurnDiscoveryRequest(meeting, isReconnecting);
292
349
 
293
- LoggerProxy.logger.info('Roap:turnDiscovery#doTurnDiscovery --> TURN discovery completed');
350
+ // if we haven't got the response over http, we need to wait for it to come over the websocket via Mercury
351
+ const {isOkRequired} = httpResponse
352
+ ? await this.handleTurnDiscoveryResponseInHttpResponse(httpResponse)
353
+ : await this.waitForTurnDiscoveryResponse();
354
+
355
+ if (isOkRequired) {
356
+ await this.sendRoapOK(meeting);
294
357
 
295
- return {turnServerInfo: this.turnInfo, turnDiscoverySkippedReason: undefined};
296
- })
297
- .catch((e) => {
298
- // we catch any errors and resolve with no turn information so that the normal call join flow can continue without TURN
299
358
  LoggerProxy.logger.info(
300
- `Roap:turnDiscovery#doTurnDiscovery --> TURN discovery failed, continuing without TURN: ${e}`
359
+ 'Roap:turnDiscovery#doTurnDiscovery --> TURN discovery response requires OK'
301
360
  );
302
361
 
303
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_FAILURE, {
362
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_REQUIRES_OK, {
304
363
  correlation_id: meeting.correlationId,
305
364
  locus_id: meeting.locusUrl.split('/').pop(),
306
- reason: e.message,
307
- stack: e.stack,
308
365
  });
366
+ }
309
367
 
310
- return {turnServerInfo: undefined, turnDiscoverySkippedReason: undefined};
368
+ this.defer = undefined;
369
+
370
+ LoggerProxy.logger.info('Roap:turnDiscovery#doTurnDiscovery --> TURN discovery completed');
371
+
372
+ return {turnServerInfo: this.turnInfo, turnDiscoverySkippedReason: undefined};
373
+ } catch (e) {
374
+ // we catch any errors and resolve with no turn information so that the normal call join flow can continue without TURN
375
+ LoggerProxy.logger.info(
376
+ `Roap:turnDiscovery#doTurnDiscovery --> TURN discovery failed, continuing without TURN: ${e}`
377
+ );
378
+
379
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_FAILURE, {
380
+ correlation_id: meeting.correlationId,
381
+ locus_id: meeting.locusUrl.split('/').pop(),
382
+ reason: e.message,
383
+ stack: e.stack,
311
384
  });
385
+
386
+ return {turnServerInfo: undefined, turnDiscoverySkippedReason: undefined};
387
+ }
312
388
  }
313
389
  }
@@ -1,7 +1,20 @@
1
1
  /* eslint-disable class-methods-use-this */
2
2
  import {CallDiagnosticUtils} from '@webex/internal-plugin-metrics';
3
+ import uuid from 'uuid';
3
4
  import RTC_METRICS from './constants';
4
5
 
6
+ const parseJsonPayload = (payload: any[]): any | null => {
7
+ try {
8
+ if (payload && payload[0]) {
9
+ return JSON.parse(payload[0]);
10
+ }
11
+
12
+ return null;
13
+ } catch (_) {
14
+ return null;
15
+ }
16
+ };
17
+
5
18
  /**
6
19
  * Rtc Metrics
7
20
  */
@@ -19,6 +32,8 @@ export default class RtcMetrics {
19
32
 
20
33
  correlationId: string;
21
34
 
35
+ connectionId: string;
36
+
22
37
  /**
23
38
  * Initialize the interval.
24
39
  *
@@ -32,6 +47,7 @@ export default class RtcMetrics {
32
47
  this.meetingId = meetingId;
33
48
  this.webex = webex;
34
49
  this.correlationId = correlationId;
50
+ this.setNewConnectionId();
35
51
  // Send the first set of metrics at 5 seconds in the case of a user leaving the call shortly after joining.
36
52
  setTimeout(this.sendMetricsInQueue.bind(this), 5 * 1000);
37
53
  }
@@ -60,7 +76,23 @@ export default class RtcMetrics {
60
76
  if (data.name === 'stats-report') {
61
77
  data.payload = data.payload.map(this.anonymizeIp);
62
78
  }
79
+
63
80
  this.metricsQueue.push(data);
81
+
82
+ try {
83
+ // If a connection fails, send the rest of the metrics in queue and get a new connection id.
84
+ const parsedPayload = parseJsonPayload(data.payload);
85
+ if (
86
+ data.name === 'onconnectionstatechange' &&
87
+ parsedPayload &&
88
+ parsedPayload.value === 'failed'
89
+ ) {
90
+ this.sendMetricsInQueue();
91
+ this.setNewConnectionId();
92
+ }
93
+ } catch (e) {
94
+ console.error(e);
95
+ }
64
96
  }
65
97
  }
66
98
 
@@ -93,6 +125,15 @@ export default class RtcMetrics {
93
125
  return JSON.stringify(data);
94
126
  }
95
127
 
128
+ /**
129
+ * Set a new connection id.
130
+ *
131
+ * @returns {void}
132
+ */
133
+ private setNewConnectionId() {
134
+ this.connectionId = uuid.v4();
135
+ }
136
+
96
137
  /**
97
138
  * Send metrics to the metrics service.
98
139
  *
@@ -111,10 +152,11 @@ export default class RtcMetrics {
111
152
  metrics: [
112
153
  {
113
154
  type: 'webrtc',
114
- version: '1.0.1',
155
+ version: '1.1.0',
115
156
  userId: this.webex.internal.device.userId,
116
157
  meetingId: this.meetingId,
117
158
  correlationId: this.correlationId,
159
+ connectionId: this.connectionId,
118
160
  data: this.metricsQueue,
119
161
  },
120
162
  ],
@@ -18,6 +18,10 @@ import {
18
18
  emptyMqaInterval,
19
19
  emptyVideoReceive,
20
20
  emptyVideoTransmit,
21
+ emptyAudioReceiveStream,
22
+ emptyAudioTransmitStream,
23
+ emptyVideoReceiveStream,
24
+ emptyVideoTransmitStream,
21
25
  } from '../mediaQualityMetrics/config';
22
26
  import LoggerProxy from '../common/logs/logger-proxy';
23
27
 
@@ -27,6 +31,10 @@ import {
27
31
  getAudioReceiverMqa,
28
32
  getVideoSenderMqa,
29
33
  getVideoReceiverMqa,
34
+ getAudioSenderStreamMqa,
35
+ getAudioReceiverStreamMqa,
36
+ getVideoSenderStreamMqa,
37
+ getVideoReceiverStreamMqa,
30
38
  } from './mqaUtil';
31
39
  import {ReceiveSlot} from '../multistream/receiveSlot';
32
40
 
@@ -153,6 +161,7 @@ export class StatsAnalyzer extends EventsScope {
153
161
  sendMqaData() {
154
162
  const newMqa = cloneDeep(emptyMqaInterval);
155
163
 
164
+ // Fill in empty stats items for lastMqaDataSent
156
165
  Object.keys(this.statsResults).forEach((mediaType) => {
157
166
  if (!this.lastMqaDataSent[mediaType]) {
158
167
  this.lastMqaDataSent[mediaType] = {};
@@ -165,53 +174,178 @@ export class StatsAnalyzer extends EventsScope {
165
174
  if (!this.lastMqaDataSent[mediaType].recv && mediaType.includes('-recv')) {
166
175
  this.lastMqaDataSent[mediaType].recv = {};
167
176
  }
177
+ });
178
+
179
+ // Create stats the first level, totals for senders and receivers
180
+ const audioSender = cloneDeep(emptyAudioTransmit);
181
+ const audioShareSender = cloneDeep(emptyAudioTransmit);
182
+ const audioReceiver = cloneDeep(emptyAudioReceive);
183
+ const audioShareReceiver = cloneDeep(emptyAudioReceive);
184
+ const videoSender = cloneDeep(emptyVideoTransmit);
185
+ const videoShareSender = cloneDeep(emptyVideoTransmit);
186
+ const videoReceiver = cloneDeep(emptyVideoReceive);
187
+ const videoShareReceiver = cloneDeep(emptyVideoReceive);
188
+
189
+ getAudioSenderMqa({
190
+ audioSender,
191
+ statsResults: this.statsResults,
192
+ lastMqaDataSent: this.lastMqaDataSent,
193
+ baseMediaType: 'audio-send',
194
+ });
195
+ newMqa.audioTransmit.push(audioSender);
196
+
197
+ getAudioSenderMqa({
198
+ audioSender: audioShareSender,
199
+ statsResults: this.statsResults,
200
+ lastMqaDataSent: this.lastMqaDataSent,
201
+ baseMediaType: 'audio-share-send',
202
+ });
203
+ newMqa.audioTransmit.push(audioShareSender);
204
+
205
+ getAudioReceiverMqa({
206
+ audioReceiver,
207
+ statsResults: this.statsResults,
208
+ lastMqaDataSent: this.lastMqaDataSent,
209
+ baseMediaType: 'audio-recv',
210
+ });
211
+ newMqa.audioReceive.push(audioReceiver);
212
+
213
+ getAudioReceiverMqa({
214
+ audioReceiver: audioShareReceiver,
215
+ statsResults: this.statsResults,
216
+ lastMqaDataSent: this.lastMqaDataSent,
217
+ baseMediaType: 'audio-share-recv',
218
+ });
219
+ newMqa.audioReceive.push(audioShareReceiver);
220
+
221
+ getVideoSenderMqa({
222
+ videoSender,
223
+ statsResults: this.statsResults,
224
+ lastMqaDataSent: this.lastMqaDataSent,
225
+ baseMediaType: 'video-send',
226
+ });
227
+ newMqa.videoTransmit.push(videoSender);
228
+
229
+ getVideoSenderMqa({
230
+ videoSender: videoShareSender,
231
+ statsResults: this.statsResults,
232
+ lastMqaDataSent: this.lastMqaDataSent,
233
+ baseMediaType: 'video-share-send',
234
+ });
235
+ newMqa.videoTransmit.push(videoShareSender);
236
+
237
+ getVideoReceiverMqa({
238
+ videoReceiver,
239
+ statsResults: this.statsResults,
240
+ lastMqaDataSent: this.lastMqaDataSent,
241
+ baseMediaType: 'video-recv',
242
+ });
243
+ newMqa.videoReceive.push(videoReceiver);
244
+
245
+ getVideoReceiverMqa({
246
+ videoReceiver: videoShareReceiver,
247
+ statsResults: this.statsResults,
248
+ lastMqaDataSent: this.lastMqaDataSent,
249
+ baseMediaType: 'video-share-recv',
250
+ });
251
+ newMqa.videoReceive.push(videoShareReceiver);
252
+
253
+ // Add stats for individual streams
254
+ Object.keys(this.statsResults).forEach((mediaType) => {
255
+ if (mediaType.includes('audio-send')) {
256
+ const audioSenderStream = cloneDeep(emptyAudioTransmitStream);
257
+
258
+ getAudioSenderStreamMqa({
259
+ audioSenderStream,
260
+ statsResults: this.statsResults,
261
+ lastMqaDataSent: this.lastMqaDataSent,
262
+ mediaType,
263
+ });
264
+ newMqa.audioTransmit[0].streams.push(audioSenderStream);
168
265
 
169
- if (mediaType.includes('audio-send') || mediaType.includes('audio-share-send')) {
170
- const audioSender = cloneDeep(emptyAudioTransmit);
266
+ this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
267
+ } else if (mediaType.includes('audio-share-send')) {
268
+ const audioSenderStream = cloneDeep(emptyAudioTransmitStream);
171
269
 
172
- getAudioSenderMqa({
173
- audioSender,
270
+ getAudioSenderStreamMqa({
271
+ audioSenderStream,
174
272
  statsResults: this.statsResults,
175
273
  lastMqaDataSent: this.lastMqaDataSent,
176
274
  mediaType,
177
275
  });
178
- newMqa.audioTransmit.push(audioSender);
276
+ newMqa.audioTransmit[1].streams.push(audioSenderStream);
179
277
 
180
278
  this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
181
- } else if (mediaType.includes('audio-recv') || mediaType.includes('audio-share-recv')) {
182
- const audioReceiver = cloneDeep(emptyAudioReceive);
279
+ } else if (mediaType.includes('audio-recv')) {
280
+ const audioReceiverStream = cloneDeep(emptyAudioReceiveStream);
281
+
282
+ getAudioReceiverStreamMqa({
283
+ audioReceiverStream,
284
+ statsResults: this.statsResults,
285
+ lastMqaDataSent: this.lastMqaDataSent,
286
+ mediaType,
287
+ });
288
+ newMqa.audioReceive[0].streams.push(audioReceiverStream);
183
289
 
184
- getAudioReceiverMqa({
185
- audioReceiver,
290
+ this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
291
+ } else if (mediaType.includes('audio-share-recv')) {
292
+ const audioReceiverStream = cloneDeep(emptyAudioReceiveStream);
293
+
294
+ getAudioReceiverStreamMqa({
295
+ audioReceiverStream,
186
296
  statsResults: this.statsResults,
187
297
  lastMqaDataSent: this.lastMqaDataSent,
188
298
  mediaType,
189
299
  });
190
- newMqa.audioReceive.push(audioReceiver);
300
+ newMqa.audioReceive[1].streams.push(audioReceiverStream);
191
301
 
192
302
  this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
193
- } else if (mediaType.includes('video-send') || mediaType.includes('video-share-send')) {
194
- const videoSender = cloneDeep(emptyVideoTransmit);
303
+ } else if (mediaType.includes('video-send')) {
304
+ const videoSenderStream = cloneDeep(emptyVideoTransmitStream);
305
+
306
+ getVideoSenderStreamMqa({
307
+ videoSenderStream,
308
+ statsResults: this.statsResults,
309
+ lastMqaDataSent: this.lastMqaDataSent,
310
+ mediaType,
311
+ });
312
+ newMqa.videoTransmit[0].streams.push(videoSenderStream);
313
+
314
+ this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
315
+ } else if (mediaType.includes('video-share-send')) {
316
+ const videoSenderStream = cloneDeep(emptyVideoTransmitStream);
195
317
 
196
- getVideoSenderMqa({
197
- videoSender,
318
+ getVideoSenderStreamMqa({
319
+ videoSenderStream,
198
320
  statsResults: this.statsResults,
199
321
  lastMqaDataSent: this.lastMqaDataSent,
200
322
  mediaType,
201
323
  });
202
- newMqa.videoTransmit.push(videoSender);
324
+ newMqa.videoTransmit[1].streams.push(videoSenderStream);
203
325
 
204
326
  this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
205
- } else if (mediaType.includes('video-recv') || mediaType.includes('video-share-recv')) {
206
- const videoReceiver = cloneDeep(emptyVideoReceive);
327
+ } else if (mediaType.includes('video-recv')) {
328
+ const videoReceiverStream = cloneDeep(emptyVideoReceiveStream);
329
+
330
+ getVideoReceiverStreamMqa({
331
+ videoReceiverStream,
332
+ statsResults: this.statsResults,
333
+ lastMqaDataSent: this.lastMqaDataSent,
334
+ mediaType,
335
+ });
336
+ newMqa.videoReceive[0].streams.push(videoReceiverStream);
337
+
338
+ this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
339
+ } else if (mediaType.includes('video-share-recv')) {
340
+ const videoReceiverStream = cloneDeep(emptyVideoReceiveStream);
207
341
 
208
- getVideoReceiverMqa({
209
- videoReceiver,
342
+ getVideoReceiverStreamMqa({
343
+ videoReceiverStream,
210
344
  statsResults: this.statsResults,
211
345
  lastMqaDataSent: this.lastMqaDataSent,
212
346
  mediaType,
213
347
  });
214
- newMqa.videoReceive.push(videoReceiver);
348
+ newMqa.videoReceive[1].streams.push(videoReceiverStream);
215
349
 
216
350
  this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
217
351
  }
@@ -381,7 +515,6 @@ export class StatsAnalyzer extends EventsScope {
381
515
  this.parseCandidate(getStatsResult, type, isSender, false);
382
516
  break;
383
517
  case 'media-source':
384
- // @ts-ignore
385
518
  this.parseAudioSource(getStatsResult, type);
386
519
  break;
387
520
  default:
@@ -911,6 +1044,7 @@ export class StatsAnalyzer extends EventsScope {
911
1044
  if (result.bytesReceived) {
912
1045
  let kilobytes = 0;
913
1046
  const receiveSlot = this.receiveSlotCallback(result.ssrc);
1047
+ const sourceState = receiveSlot?.sourceState;
914
1048
  const idAndCsi = receiveSlot
915
1049
  ? `id: "${receiveSlot.id || ''}"${receiveSlot.csi ? ` and csi: ${receiveSlot.csi}` : ''}`
916
1050
  : '';
@@ -938,10 +1072,10 @@ export class StatsAnalyzer extends EventsScope {
938
1072
  this.statsResults[mediaType][sendrecvType].totalPacketsReceived = result.packetsReceived;
939
1073
 
940
1074
  if (currentPacketsReceived === 0) {
941
- if (receiveSlot) {
1075
+ if (receiveSlot && sourceState === 'live') {
942
1076
  LoggerProxy.logger.info(
943
- `StatsAnalyzer:index#processInboundRTPResult --> No packets received for receive slot ${idAndCsi}`,
944
- currentPacketsReceived
1077
+ `StatsAnalyzer:index#processInboundRTPResult --> No packets received for receive slot ${idAndCsi}. Total packets received on slot: `,
1078
+ result.packetsReceived
945
1079
  );
946
1080
  }
947
1081
  }