@livedigital/client 2.22.1 → 2.23.0

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.
@@ -236,6 +236,59 @@ export declare enum DeviceErrors {
236
236
  DeviceIsBusy = "DeviceIsBusy",
237
237
  NotAllowedError = "NotAllowedError"
238
238
  }
239
+ export declare type TrackInboundStats = {
240
+ consumerId: string;
241
+ codec?: RTCRtpCodecParameters;
242
+ currentSpatialLayerParams?: SpatialLayerParams;
243
+ availableSpatialLayers?: SpatialLayerParams[];
244
+ requestedSpatialLayer?: number;
245
+ score: number;
246
+ dtlsState?: RTCDtlsTransportState;
247
+ rtcStats?: ExtendedRTCInboundRtpStreamStats;
248
+ };
249
+ export declare type TrackOutboundStats = {
250
+ producerId?: string;
251
+ codec?: RTCRtpCodecParameters;
252
+ dtlsState?: RTCDtlsTransportState;
253
+ rtcStats?: RTCOutboundRtpStreamStats;
254
+ };
255
+ export declare type PeerTrackInfo = {
256
+ trackId: string;
257
+ readyState: MediaStreamTrackState;
258
+ isRemote: boolean;
259
+ kind: MediaKind;
260
+ label: TrackLabel;
261
+ paused: boolean;
262
+ width?: number;
263
+ height?: number;
264
+ frameRate?: number;
265
+ aspectRatio?: number;
266
+ trackInboundStats?: TrackInboundStats;
267
+ trackOutboundStats?: TrackOutboundStats;
268
+ };
269
+ export declare type BaseTrackInfo = {
270
+ trackId: string;
271
+ readyState: MediaStreamTrackState;
272
+ kind: MediaKind;
273
+ label: TrackLabel;
274
+ paused: boolean;
275
+ width?: number;
276
+ height?: number;
277
+ frameRate?: number;
278
+ aspectRatio?: number;
279
+ trackOutboundStats?: TrackOutboundStats;
280
+ };
281
+ export declare type PeerInfo = {
282
+ id: string;
283
+ appData: Record<string, unknown>;
284
+ role: Role;
285
+ channelIds: string[];
286
+ appId: string;
287
+ uid?: string;
288
+ loginDate: Date;
289
+ connectionQuality: number;
290
+ tracks: PeerTrackInfo[];
291
+ };
239
292
  export declare type UpdatePeerAppDataPayload = {
240
293
  peerId: string;
241
294
  appData: Record<string, unknown>;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@livedigital/client",
3
3
  "author": "vlprojects",
4
4
  "license": "MIT",
5
- "version": "2.22.1",
5
+ "version": "2.23.0",
6
6
  "private": false,
7
7
  "bugs": {
8
8
  "url": "https://github.com/vlprojects/livedigital-sdk/issues"
@@ -8,6 +8,7 @@ import {
8
8
  ChangePreferredLayersPayload,
9
9
  Role,
10
10
  ProducerSetMaxSpatialLayer,
11
+ PeerInfo,
11
12
  } from '../types/common';
12
13
  import EnhancedEventEmitter from '../EnhancedEventEmitter';
13
14
  import Engine from './index';
@@ -157,6 +158,25 @@ class Peer {
157
158
  }
158
159
  }
159
160
 
161
+ async getInfo(): Promise<PeerInfo> {
162
+ try {
163
+ return {
164
+ id: this.id,
165
+ appData: this.appData,
166
+ role: this.role,
167
+ channelIds: this.channelIds,
168
+ appId: this.appId,
169
+ uid: this.uid,
170
+ loginDate: this.loginDate,
171
+ connectionQuality: this.overallConnectionQuality,
172
+ tracks: await Promise.all(Array.from(this.tracks.values()).map((track) => track.getInfo())),
173
+ };
174
+ } catch (error) {
175
+ this.logger.error('getInfo()', { peer: this, error });
176
+ throw new Error('Error get info');
177
+ }
178
+ }
179
+
160
180
  private handleNewProducer(producerData: ProducerData): void {
161
181
  if (this.producers.get(producerData.id)) {
162
182
  return;
@@ -6,8 +6,10 @@ import {
6
6
  ExtendedRTCInboundRtpStreamStats,
7
7
  LogLevel,
8
8
  SpatialLayerParams,
9
+ TrackInboundStats,
9
10
  } from '../types/common';
10
11
  import Logger from './Logger';
12
+ import filterStatsCodecs from '../helpers/filterStatsCodecs';
11
13
 
12
14
  export type PeerConsumerConstructorParams = {
13
15
  consumer: MediasoupConsumer,
@@ -167,6 +169,24 @@ class PeerConsumer {
167
169
  this.currentMaxSpatialLayer = spatialLayer;
168
170
  }
169
171
 
172
+ async getStats(): Promise<TrackInboundStats | undefined> {
173
+ try {
174
+ return {
175
+ consumerId: this.consumer.id,
176
+ codec: this.consumer.rtpParameters.codecs.find((codec) => filterStatsCodecs(codec)),
177
+ score: this.score,
178
+ dtlsState: this.consumer.rtpReceiver?.transport?.state,
179
+ availableSpatialLayers: this.availableSpatialLayers,
180
+ currentSpatialLayerParams: this.currentSpatialLayerParams,
181
+ requestedSpatialLayer: this.requestedSpatialLayer,
182
+ rtcStats: await this.getInboundRTPStreamStats(),
183
+ };
184
+ } catch (error) {
185
+ this.logger.debug('getStats()', { error });
186
+ return undefined;
187
+ }
188
+ }
189
+
170
190
  async getInboundRTPStreamStats(): Promise<ExtendedRTCInboundRtpStreamStats> {
171
191
  return new Promise((resolve, reject) => {
172
192
  const getRTCStatsReport = async (attempt = 0) => {
@@ -1,10 +1,11 @@
1
1
  import { Producer } from 'mediasoup-client/lib/Producer';
2
2
  import { MediaKind } from 'mediasoup-client/lib/RtpParameters';
3
3
  import {
4
+ BaseTrackInfo,
4
5
  EncoderConfig,
5
6
  LogLevel,
6
7
  SocketResponse,
7
- TrackLabel,
8
+ TrackLabel, TrackOutboundStats,
8
9
  TrackProduceParams,
9
10
  } from '../../../types/common';
10
11
  import Logger from '../../Logger';
@@ -14,6 +15,7 @@ import { PRODUCER_CHECK_STATE_TIMEOUT } from '../../../constants/common';
14
15
  import Timeout = NodeJS.Timeout;
15
16
  import { MEDIASOUP_EVENTS, INTERNAL_CLIENT_EVENTS, CLIENT_EVENTS } from '../../../constants/events';
16
17
  import EnhancedEventEmitter from '../../../EnhancedEventEmitter';
18
+ import filterStatsCodecs from '../../../helpers/filterStatsCodecs';
17
19
 
18
20
  export type BaseTrackConstructorParams = {
19
21
  mediaStreamTrack: MediaStreamTrack,
@@ -41,7 +43,7 @@ class BaseTrack {
41
43
 
42
44
  #producerRestarted = false;
43
45
 
44
- #clientEventEmitter: EnhancedEventEmitter;
46
+ readonly #clientEventEmitter: EnhancedEventEmitter;
45
47
 
46
48
  #closed = false;
47
49
 
@@ -379,6 +381,69 @@ class BaseTrack {
379
381
  throw new Error('Can`t resume track');
380
382
  }
381
383
  }
384
+
385
+ async getInfo(): Promise<BaseTrackInfo> {
386
+ const {
387
+ width, height, frameRate, aspectRatio,
388
+ } = this.#mediaStreamTrack.getSettings();
389
+ return {
390
+ trackId: this.#mediaStreamTrack.id,
391
+ readyState: this.#mediaStreamTrack.readyState,
392
+ kind: this.kind,
393
+ label: this.label,
394
+ width,
395
+ height,
396
+ frameRate,
397
+ aspectRatio,
398
+ paused: this.isPaused,
399
+ trackOutboundStats: await this.getStats(),
400
+ };
401
+ }
402
+
403
+ async getStats(): Promise<TrackOutboundStats | undefined> {
404
+ try {
405
+ return {
406
+ producerId: this.producer?.id,
407
+ codec: this.producer?.rtpParameters.codecs.find((codec) => filterStatsCodecs(codec)),
408
+ dtlsState: this.producer?.rtpSender?.transport?.state,
409
+ rtcStats: await this.getOutboundRTPStreamStats(),
410
+ };
411
+ } catch (error) {
412
+ this.logger.debug('getStats()', { error });
413
+ return undefined;
414
+ }
415
+ }
416
+
417
+ private getOutboundRTPStreamStats(): Promise<RTCOutboundRtpStreamStats> {
418
+ return new Promise((resolve, reject) => {
419
+ const getRTCStatsReport = async (attempt = 0) => {
420
+ if (!this.producer) {
421
+ throw new Error('Producer is missed');
422
+ }
423
+
424
+ try {
425
+ const rtcStatsReport = await this.producer.getStats();
426
+ const outboundRTPStreamStats: RTCOutboundRtpStreamStats = Array.from(rtcStatsReport.values())
427
+ .find((stat) => stat.type === 'outbound-rtp');
428
+ if (!outboundRTPStreamStats && attempt < 5) {
429
+ setTimeout(() => getRTCStatsReport(attempt + 1), 150);
430
+ return;
431
+ }
432
+
433
+ if (attempt >= 5 && !outboundRTPStreamStats) {
434
+ reject(new Error('OutboundRTPStreamStat not exist'));
435
+ }
436
+
437
+ resolve(outboundRTPStreamStats);
438
+ } catch (error) {
439
+ reject(new Error('Can not get RTCStatsReport'));
440
+ this.logger.debug('getOutboundRTPStreamStats()', { error });
441
+ }
442
+ };
443
+
444
+ getRTCStatsReport();
445
+ });
446
+ }
382
447
  }
383
448
 
384
449
  export default BaseTrack;
@@ -3,7 +3,8 @@ import {
3
3
  PreferredLayersParams,
4
4
  SetConsumerPriorityParams,
5
5
  SpatialLayerParams,
6
- TrackLabel,
6
+ PeerTrackInfo,
7
+ TrackLabel, TrackOutboundStats,
7
8
  } from '../../../types/common';
8
9
  import Logger from '../../Logger';
9
10
  import { MEDIASOUP_EVENTS, PEER_EVENTS } from '../../../constants/events';
@@ -57,6 +58,10 @@ class PeerTrack {
57
58
  this.#peerEventEmitter.safeEmit(PEER_EVENTS.trackStart, this);
58
59
  }
59
60
 
61
+ get isRemote(): boolean {
62
+ return !!this.consumer;
63
+ }
64
+
60
65
  get mediaStreamTrack(): MediaStreamTrack {
61
66
  return this.#mediaStreamTrack;
62
67
  }
@@ -238,6 +243,39 @@ class PeerTrack {
238
243
  return this.consumer.availableSpatialLayers;
239
244
  }
240
245
 
246
+ async getInfo(): Promise<PeerTrackInfo> {
247
+ const {
248
+ width, height, frameRate, aspectRatio,
249
+ } = this.#mediaStreamTrack.getSettings();
250
+ return {
251
+ trackId: this.#mediaStreamTrack.id,
252
+ readyState: this.#mediaStreamTrack.readyState,
253
+ isRemote: this.isRemote,
254
+ kind: this.kind,
255
+ label: this.label,
256
+ width,
257
+ height,
258
+ frameRate,
259
+ aspectRatio,
260
+ paused: this.#paused,
261
+ trackInboundStats: await this.consumer?.getStats(),
262
+ trackOutboundStats: await this.getOutboundStats(),
263
+ };
264
+ }
265
+
266
+ private async getOutboundStats(): Promise<TrackOutboundStats | undefined> {
267
+ if (this.isRemote) {
268
+ return undefined;
269
+ }
270
+
271
+ const outboundTrack = this.#engine.media.getAllTracks().find((track) => track.getLabel() === this.label);
272
+ if (!outboundTrack) {
273
+ return undefined;
274
+ }
275
+
276
+ return outboundTrack.getStats();
277
+ }
278
+
241
279
  private async requestMaxSpatialLayer(consumerId: string, spatialLayer: number): Promise<void> {
242
280
  try {
243
281
  if (!this.consumer) {
@@ -7,8 +7,14 @@ import {
7
7
  } from '../types';
8
8
 
9
9
  class VideoCodecMismatchDetector implements IssueDetector {
10
+ readonly UNKNOWN_DECODER = 'unknown';
11
+
10
12
  #lastProcessedStats: { [connectionId: string]: WebRTCStatsEventData | undefined } = {};
11
13
 
14
+ #lastDecoderWithIssue: {
15
+ [connectionId: string]: { [ssrc: string]: string | undefined } | undefined;
16
+ } = {};
17
+
12
18
  detect(data: WebRTCStatsEventData): IssueDetectorResult {
13
19
  const issues = this.processData(data);
14
20
  this.#lastProcessedStats[data.connection.id] = data;
@@ -17,31 +23,56 @@ class VideoCodecMismatchDetector implements IssueDetector {
17
23
 
18
24
  private processData(data: WebRTCStatsEventData): IssueDetectorResult {
19
25
  const issues: IssueDetectorResult = [];
20
- const previousInboundRTPVideoStreamsStats = this.#lastProcessedStats[data.connection.id]?.video.inbound;
26
+ const { id: connectionId } = data.connection;
27
+ const previousInboundRTPVideoStreamsStats = this.#lastProcessedStats[connectionId]?.video.inbound;
21
28
 
22
29
  data.video.inbound.forEach((streamStats) => {
23
30
  const { decoderImplementation: currentDecoder, ssrc } = streamStats;
24
31
  const prevStats = previousInboundRTPVideoStreamsStats?.find((item) => item.ssrc === ssrc);
25
32
 
33
+ // skipping the first iteration on purpose
26
34
  if (!prevStats) {
27
35
  return;
28
36
  }
29
37
 
30
- const { decoderImplementation: prevDecoder } = prevStats;
38
+ if (currentDecoder !== this.UNKNOWN_DECODER) {
39
+ this.setLastDecoderWithIssue(connectionId, ssrc, undefined);
40
+ return;
41
+ }
42
+
43
+ if (!this.hadLastDecoderWithIssue(connectionId, ssrc)) {
44
+ this.setLastDecoderWithIssue(connectionId, ssrc, this.UNKNOWN_DECODER);
31
45
 
32
- if (currentDecoder === 'unknown' && prevDecoder !== 'unknown') {
33
46
  issues.push({
47
+ ssrc,
34
48
  type: IssueType.Stream,
35
49
  reason: IssueReason.VideoCodecMismatchIssue,
36
- ssrc: streamStats.ssrc,
37
50
  trackIdentifier: streamStats.track.trackIdentifier,
38
- debug: `mimeType: ${streamStats.mimeType}, decoderImplementation: ${streamStats.decoderImplementation}`,
51
+ debug: `mimeType: ${streamStats.mimeType}, decoderImplementation: ${currentDecoder}`,
39
52
  });
40
53
  }
41
54
  });
42
55
 
43
56
  return issues;
44
57
  }
58
+
59
+ private setLastDecoderWithIssue(connectionId: string, ssrc: number, decoder: string | undefined): void {
60
+ const issues = this.#lastDecoderWithIssue[connectionId] ?? {};
61
+
62
+ if (decoder === undefined) {
63
+ delete issues[ssrc];
64
+ } else {
65
+ issues[ssrc] = decoder;
66
+ }
67
+
68
+ this.#lastDecoderWithIssue[connectionId] = issues;
69
+ }
70
+
71
+ private hadLastDecoderWithIssue(connectionId: string, ssrc: number): boolean {
72
+ const issues = this.#lastDecoderWithIssue[connectionId];
73
+ const decoder = issues && issues[ssrc];
74
+ return decoder === this.UNKNOWN_DECODER;
75
+ }
45
76
  }
46
77
 
47
78
  export default VideoCodecMismatchDetector;
@@ -0,0 +1,3 @@
1
+ const filterStatsCodecs = (codec: RTCRtpCodecParameters) => codec.mimeType !== 'rtx';
2
+
3
+ export default filterStatsCodecs;
@@ -274,6 +274,64 @@ export enum DeviceErrors {
274
274
  NotAllowedError = 'NotAllowedError',
275
275
  }
276
276
 
277
+ export type TrackInboundStats = {
278
+ consumerId: string,
279
+ codec?: RTCRtpCodecParameters,
280
+ currentSpatialLayerParams?: SpatialLayerParams,
281
+ availableSpatialLayers?: SpatialLayerParams[],
282
+ requestedSpatialLayer?: number,
283
+ score: number,
284
+ dtlsState?: RTCDtlsTransportState,
285
+ rtcStats?: ExtendedRTCInboundRtpStreamStats,
286
+ };
287
+
288
+ export type TrackOutboundStats = {
289
+ producerId?: string,
290
+ codec?: RTCRtpCodecParameters,
291
+ dtlsState?: RTCDtlsTransportState,
292
+ rtcStats?: RTCOutboundRtpStreamStats,
293
+ };
294
+
295
+ export type PeerTrackInfo = {
296
+ trackId: string,
297
+ readyState: MediaStreamTrackState,
298
+ isRemote: boolean,
299
+ kind: MediaKind,
300
+ label: TrackLabel,
301
+ paused: boolean,
302
+ width?: number,
303
+ height?: number,
304
+ frameRate?: number,
305
+ aspectRatio?: number,
306
+ trackInboundStats?: TrackInboundStats,
307
+ trackOutboundStats?: TrackOutboundStats,
308
+ };
309
+
310
+ export type BaseTrackInfo = {
311
+ trackId: string,
312
+ readyState: MediaStreamTrackState,
313
+ kind: MediaKind,
314
+ label: TrackLabel,
315
+ paused: boolean,
316
+ width?: number,
317
+ height?: number,
318
+ frameRate?: number,
319
+ aspectRatio?: number,
320
+ trackOutboundStats?: TrackOutboundStats,
321
+ };
322
+
323
+ export type PeerInfo = {
324
+ id: string,
325
+ appData: Record<string, unknown>,
326
+ role: Role,
327
+ channelIds: string[],
328
+ appId: string,
329
+ uid?: string,
330
+ loginDate: Date,
331
+ connectionQuality: number,
332
+ tracks: PeerTrackInfo[],
333
+ };
334
+
277
335
  export type UpdatePeerAppDataPayload = {
278
336
  peerId: string,
279
337
  appData: Record<string, unknown>,