@livedigital/client 2.21.0 → 2.22.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.
@@ -7,11 +7,13 @@ import ChannelEventHandler from '../engine/handlers/ChannelEventHandler';
7
7
  import MediaSoupEventHandler from '../engine/handlers/MediaSoupEventHandler';
8
8
  import WebRTCIssueDetector from '../engine/wid/WebRTCIssueDetector';
9
9
  import { LoadBalancerApiClientParams } from '../engine/network/LoadBalancerClient';
10
- import { IssueDetectorResult } from '../engine/wid/types';
10
+ import { IssueDetectorResult, NetworkScores } from '../engine/wid/types';
11
11
  import { LogLevel } from './common';
12
12
  export declare type IssuesHandler = (issues: IssueDetectorResult) => void;
13
+ export declare type NetworkScoresUpdatedHandler = (networks: NetworkScores) => void;
13
14
  export interface CreateIssueDetectorParams {
14
15
  onIssues?: IssuesHandler;
16
+ onNetworkScoresUpdated?: NetworkScoresUpdatedHandler;
15
17
  ignoreSSRCList?: number[];
16
18
  }
17
19
  export interface CreateMediaParams {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@livedigital/client",
3
3
  "author": "vlprojects",
4
4
  "license": "MIT",
5
- "version": "2.21.0",
5
+ "version": "2.22.0",
6
6
  "private": false,
7
7
  "bugs": {
8
8
  "url": "https://github.com/vlprojects/livedigital-sdk/issues"
@@ -50,6 +50,7 @@ class DefaultEngineDependenciesFactory implements EngineDependenciesFactory {
50
50
  createIssueDetector(params: CreateIssueDetectorParams): WebRTCIssueDetector {
51
51
  return new WebRTCIssueDetector({
52
52
  onIssues: params.onIssues,
53
+ onNetworkScoresUpdated: params.onNetworkScoresUpdated,
53
54
  ignoreSSRCList: params.ignoreSSRCList,
54
55
  });
55
56
  }
@@ -28,7 +28,7 @@ import VideoTrack from './media/tracks/VideoTrack';
28
28
  import PeerTrack from './media/tracks/PeerTrack';
29
29
  import WebRTCIssueDetector from './wid/WebRTCIssueDetector';
30
30
  import { retryAsync } from '../helpers/retry';
31
- import { EngineDependenciesFactory, IssuesHandler } from '../types/engine';
31
+ import { EngineDependenciesFactory, IssuesHandler, NetworkScoresUpdatedHandler } from '../types/engine';
32
32
  import { LogLevels } from '../constants/common';
33
33
  import { LoadBalancerApiClientParams } from './network/LoadBalancerClient';
34
34
  import validateAppData from '../helpers/appDataValidator';
@@ -42,6 +42,7 @@ type EngineParams = {
42
42
  logLevel?: LogLevel;
43
43
  onLogMessage?: LogMessageHandler;
44
44
  onIssues?: IssuesHandler;
45
+ onNetworkScoresUpdated?: NetworkScoresUpdatedHandler;
45
46
  };
46
47
 
47
48
  class Engine {
@@ -108,6 +109,7 @@ class Engine {
108
109
 
109
110
  this.webRtcIssueDetector = dependenciesFactory.createIssueDetector({
110
111
  onIssues: params.onIssues,
112
+ onNetworkScoresUpdated: params.onNetworkScoresUpdated,
111
113
  ignoreSSRCList: [1234], // mediasoup-client probator
112
114
  });
113
115
 
@@ -1,4 +1,4 @@
1
- import { WebRTCStats } from '@peermetrics/webrtc-stats';
1
+ import { WebRTCStats, WebRTCStatsEventData } from '@peermetrics/webrtc-stats';
2
2
  import { WebRTCIssueEmitter } from './WebRTCIssueEmitter';
3
3
  import {
4
4
  WebRTCIssueDetectorConstructorParams,
@@ -16,6 +16,7 @@ import OutboundNetworkIssueDetector from './detectors/OutboundNetworkIssueDetect
16
16
  import NetworkMediaSyncIssueDetector from './detectors/NetworkMediaSyncIssueDetector';
17
17
  import AvailableOutgoingBitrateIssueDetector from './detectors/AvailableOutgoingBitrateIssueDetector';
18
18
  import VideoCodecMismatchDetector from './detectors/VideoCodecMismatchDetector';
19
+ import NetworkScoresCalculator from './lib/NetworkScoresCalculator';
19
20
 
20
21
  class WebRTCIssueDetector {
21
22
  private readonly webrtcStats: WebRTCStats;
@@ -26,6 +27,8 @@ class WebRTCIssueDetector {
26
27
 
27
28
  private readonly detectors: IssueDetector[] = [];
28
29
 
30
+ private readonly networkScoresCalculator: NetworkScoresCalculator;
31
+
29
32
  readonly eventEmitter = new WebRTCIssueEmitter();
30
33
 
31
34
  #running = false;
@@ -35,6 +38,10 @@ class WebRTCIssueDetector {
35
38
  this.eventEmitter.on(EventType.Issue, params.onIssues);
36
39
  }
37
40
 
41
+ if (params.onNetworkScoresUpdated) {
42
+ this.eventEmitter.on(EventType.NetworkScoresUpdated, params.onNetworkScoresUpdated);
43
+ }
44
+
38
45
  // Move instantiation from the constructor
39
46
  this.webrtcStats = new WebRTCStats({
40
47
  getStatsInterval: this.getStatsInterval,
@@ -55,11 +62,15 @@ class WebRTCIssueDetector {
55
62
  new VideoCodecMismatchDetector(),
56
63
  ];
57
64
 
65
+ this.networkScoresCalculator = new NetworkScoresCalculator();
66
+
58
67
  this.webrtcStats.on('stats', (event) => {
59
68
  this.detectIssues({
60
69
  data: event.data,
61
70
  ignoreSSRCList: params.ignoreSSRCList,
62
71
  });
72
+
73
+ this.calculateNetworkScores(event.data);
63
74
  });
64
75
  }
65
76
 
@@ -115,6 +126,11 @@ class WebRTCIssueDetector {
115
126
  }
116
127
  }
117
128
 
129
+ private calculateNetworkScores(data: WebRTCStatsEventData): void {
130
+ const networkScores = this.networkScoresCalculator.calculate(data);
131
+ this.eventEmitter.emit(EventType.NetworkScoresUpdated, networkScores);
132
+ }
133
+
118
134
  private wrapRTCPeerConnection(): void {
119
135
  if (!window.RTCPeerConnection) {
120
136
  return;
@@ -1,9 +1,16 @@
1
1
  import { EventEmitter } from 'events';
2
- import { EventType, EventPayload, IssueDetectorResult } from './types';
2
+ import {
3
+ EventType,
4
+ EventPayload,
5
+ IssueDetectorResult,
6
+ NetworkScores,
7
+ } from './types';
3
8
 
4
9
  export declare interface WebRTCIssueEmitter {
5
- on(event: EventType, listener: (issues: IssueDetectorResult) => void): this;
6
- emit(event: EventType, payload: EventPayload): boolean;
10
+ on(event: EventType.Issue, listener: (payload: IssueDetectorResult) => void): this;
11
+ on(event: EventType.NetworkScoresUpdated, listener: (payload: NetworkScores) => void): this;
12
+ emit(event: EventType.Issue, payload: EventPayload): boolean;
13
+ emit(event: EventType.NetworkScoresUpdated, payload: NetworkScores): boolean;
7
14
  }
8
15
 
9
16
  export class WebRTCIssueEmitter extends EventEmitter {}
@@ -65,36 +65,17 @@ class InboundNetworkIssueDetector implements IssueDetector {
65
65
  ? Math.round((deltaPacketLost * 100) / (deltaPacketReceived + deltaPacketLost))
66
66
  : 0;
67
67
 
68
- const effectiveLatency = rtt + (avgJitter * 2) + 10;
69
- let rFactor = effectiveLatency < 160
70
- ? 93.2 - (effectiveLatency / 40)
71
- : 93.2 - (effectiveLatency / 120) - 10;
72
- rFactor -= (packetsLoss * 2.5);
73
- const mos = 1 + (0.035) * rFactor + (0.000007) * rFactor * (rFactor - 60) * (100 - rFactor);
74
-
75
68
  const isHighPacketsLoss = packetsLoss > 5;
76
69
  const isHighJitter = avgJitter >= 200;
77
70
  const isHighRTT = rtt >= 250;
78
71
  const isHighJitterBufferDelay = avgJitterBufferDelay > 500;
79
- const isPoorConnectionQuality = mos < 3.1;
80
-
81
72
  const isNetworkIssue = (!isHighPacketsLoss && isHighJitter) || isHighJitter || isHighPacketsLoss;
82
73
  const isServerIssue = isHighRTT && !isHighJitter && !isHighPacketsLoss;
83
74
  const isNetworkMediaLatencyIssue = isHighPacketsLoss && isHighJitter;
84
75
  const isNetworkMediaSyncIssue = isHighJitter && isHighJitterBufferDelay;
85
76
 
86
77
  const debug = `packetLoss: ${packetsLoss}%, jitter: ${avgJitter}, rtt: ${rtt},`
87
- + ` jitterBuffer: ${avgJitterBufferDelay}ms, MOS: ${mos}`;
88
-
89
- if (isPoorConnectionQuality) {
90
- issues.push({
91
- type: IssueType.Network,
92
- reason: IssueReason.LowInboundMOS,
93
- iceCandidate: data.connection.local.id,
94
- debug,
95
- data: mos,
96
- });
97
- }
78
+ + ` jitterBuffer: ${avgJitterBufferDelay}ms`;
98
79
 
99
80
  if (isNetworkIssue) {
100
81
  issues.push({
@@ -65,20 +65,11 @@ class OutboundNetworkIssueDetector implements IssueDetector {
65
65
  ? Math.round((deltaPacketLost * 100) / (deltaPacketSent + deltaPacketLost))
66
66
  : 0;
67
67
 
68
- const effectiveLatency = rtt + (avgJitter * 2) + 10;
69
- let rFactor = effectiveLatency < 160
70
- ? 93.2 - (effectiveLatency / 40)
71
- : 93.2 - (effectiveLatency / 120) - 10;
72
- rFactor -= (packetsLoss * 2.5);
73
- const mos = 1 + (0.035) * rFactor + (0.000007) * rFactor * (rFactor - 60) * (100 - rFactor);
74
-
75
68
  const isHighPacketsLoss = packetsLoss > 5;
76
69
  const isHighJitter = avgJitter >= 200;
77
- const isPoorConnectionQuality = mos < 3.5;
78
70
  const isNetworkMediaLatencyIssue = isHighPacketsLoss && isHighJitter;
79
71
  const isNetworkIssue = (!isHighPacketsLoss && isHighJitter) || isHighJitter || isHighPacketsLoss;
80
- const debug = `packetLoss: ${packetsLoss}%, jitter: ${avgJitter}, rtt: ${rtt},`
81
- + ` MOS: ${mos}`;
72
+ const debug = `packetLoss: ${packetsLoss}%, jitter: ${avgJitter}, rtt: ${rtt}`;
82
73
 
83
74
  if (isNetworkMediaLatencyIssue) {
84
75
  issues.push({
@@ -98,16 +89,6 @@ class OutboundNetworkIssueDetector implements IssueDetector {
98
89
  });
99
90
  }
100
91
 
101
- if (isPoorConnectionQuality) {
102
- issues.push({
103
- type: IssueType.Network,
104
- reason: IssueReason.LowOutboundMOS,
105
- iceCandidate: data.connection.local.id,
106
- debug,
107
- data: mos,
108
- });
109
- }
110
-
111
92
  return issues;
112
93
  }
113
94
  }
@@ -0,0 +1,122 @@
1
+ /* eslint-disable class-methods-use-this */
2
+ import { WebRTCStatsEventData } from '@peermetrics/webrtc-stats';
3
+ import { NetworkScore, NetworkScores } from '../types';
4
+
5
+ class NetworkScoresCalculator {
6
+ #lastProcessedStats: { [connectionId: string]: WebRTCStatsEventData } = {};
7
+
8
+ calculate(data: WebRTCStatsEventData): NetworkScores {
9
+ const outbound = this.calcucateOutboundScore(data);
10
+ const inbound = this.calculateInboundScore(data);
11
+ this.#lastProcessedStats[data.connection.id] = data;
12
+ return { outbound, inbound };
13
+ }
14
+
15
+ private calcucateOutboundScore(data: WebRTCStatsEventData): NetworkScore | undefined {
16
+ const remoteInboundRTPStreamsStats = [
17
+ ...data.remote?.audio.inbound || [],
18
+ ...data.remote?.video.inbound || [],
19
+ ];
20
+
21
+ if (!remoteInboundRTPStreamsStats.length) {
22
+ return undefined;
23
+ }
24
+
25
+ const previousStats = this.#lastProcessedStats[data.connection.id];
26
+ if (!previousStats) {
27
+ return undefined;
28
+ }
29
+
30
+ const previousRemoteInboundRTPStreamsStats = [
31
+ ...previousStats.remote?.audio.inbound || [],
32
+ ...previousStats.remote?.video.inbound || [],
33
+ ];
34
+
35
+ const { packetsSent } = data.connection;
36
+ const lastPacketsSent = previousStats.connection.packetsSent;
37
+
38
+ const rtpNetworkStats = remoteInboundRTPStreamsStats.reduce((stats, currentStreamStats) => {
39
+ const previousStreamStats = previousRemoteInboundRTPStreamsStats
40
+ .find((stream) => stream.ssrc === currentStreamStats.ssrc);
41
+
42
+ return {
43
+ sumJitter: stats.sumJitter + currentStreamStats.jitter,
44
+ packetsLost: stats.packetsLost + currentStreamStats.packetsLost,
45
+ lastPacketsLost: stats.lastPacketsLost + (previousStreamStats?.packetsLost || 0),
46
+ };
47
+ }, {
48
+ sumJitter: 0,
49
+ packetsLost: 0,
50
+ lastPacketsLost: 0,
51
+ });
52
+
53
+ const rtt = (1e3 * data.connection.currentRoundTripTime) || 0;
54
+ const { sumJitter } = rtpNetworkStats;
55
+ const avgJitter = sumJitter / remoteInboundRTPStreamsStats.length;
56
+
57
+ const deltaPacketSent = packetsSent - lastPacketsSent;
58
+ const deltaPacketLost = rtpNetworkStats.packetsLost - rtpNetworkStats.lastPacketsLost;
59
+
60
+ const packetsLoss = deltaPacketSent && deltaPacketLost
61
+ ? Math.round((deltaPacketLost * 100) / (deltaPacketSent + deltaPacketLost))
62
+ : 0;
63
+
64
+ return this.calculateMOS({ avgJitter, rtt, packetsLoss });
65
+ }
66
+
67
+ private calculateInboundScore(data: WebRTCStatsEventData): NetworkScore | undefined {
68
+ const inboundRTPStreamsStats = [...data.audio?.inbound, ...data.video?.inbound];
69
+ if (!inboundRTPStreamsStats.length) {
70
+ return undefined;
71
+ }
72
+
73
+ const previousStats = this.#lastProcessedStats[data.connection.id];
74
+ if (!previousStats) {
75
+ return undefined;
76
+ }
77
+
78
+ const previousInboundStreamStats = [...previousStats.video?.inbound, ...previousStats.audio?.inbound];
79
+ const { packetsReceived } = data.connection;
80
+ const lastPacketsReceived = previousStats.connection.packetsReceived;
81
+
82
+ const rtpNetworkStats = inboundRTPStreamsStats.reduce((stats, currentStreamStats) => {
83
+ const previousStreamStats = previousInboundStreamStats.find((stream) => stream.ssrc === currentStreamStats.ssrc);
84
+ return {
85
+ sumJitter: stats.sumJitter + currentStreamStats.jitter,
86
+ packetsLost: stats.packetsLost + currentStreamStats.packetsLost,
87
+ lastPacketsLost: stats.lastPacketsLost + (previousStreamStats?.packetsLost || 0),
88
+ };
89
+ }, {
90
+ sumJitter: 0,
91
+ packetsLost: 0,
92
+ lastPacketsLost: 0,
93
+ });
94
+
95
+ const rtt = (1e3 * data.connection.currentRoundTripTime) || 0;
96
+ const { sumJitter } = rtpNetworkStats;
97
+ const avgJitter = sumJitter / inboundRTPStreamsStats.length;
98
+
99
+ const deltaPacketReceived = packetsReceived - lastPacketsReceived;
100
+ const deltaPacketLost = rtpNetworkStats.packetsLost - rtpNetworkStats.lastPacketsLost;
101
+
102
+ const packetsLoss = deltaPacketReceived && deltaPacketLost
103
+ ? Math.round((deltaPacketLost * 100) / (deltaPacketReceived + deltaPacketLost))
104
+ : 0;
105
+
106
+ return this.calculateMOS({ avgJitter, rtt, packetsLoss });
107
+ }
108
+
109
+ private calculateMOS(
110
+ { avgJitter, rtt, packetsLoss }:
111
+ { avgJitter: number, rtt: number, packetsLoss: number },
112
+ ): number {
113
+ const effectiveLatency = rtt + (avgJitter * 2) + 10;
114
+ let rFactor = effectiveLatency < 160
115
+ ? 93.2 - (effectiveLatency / 40)
116
+ : 93.2 - (effectiveLatency / 120) - 10;
117
+ rFactor -= (packetsLoss * 2.5);
118
+ return 1 + (0.035) * rFactor + (0.000007) * rFactor * (rFactor - 60) * (100 - rFactor);
119
+ }
120
+ }
121
+
122
+ export default NetworkScoresCalculator;
@@ -14,12 +14,14 @@ export interface IssueDetector {
14
14
 
15
15
  export enum EventType {
16
16
  Issue = 'issue',
17
+ NetworkScoresUpdated = 'network-scores-updated',
17
18
  }
18
19
 
19
20
  export type EventPayload = IssueDetectorResult;
20
21
 
21
22
  export type WebRTCIssueDetectorConstructorParams = {
22
23
  onIssues?: (payload: IssueDetectorResult) => void,
24
+ onNetworkScoresUpdated?: (payload: NetworkScores) => void,
23
25
  ignoreSSRCList?: number[],
24
26
  };
25
27
 
@@ -60,3 +62,10 @@ export type DetectIssuesPayload = {
60
62
  data: WebRTCStatsEventData,
61
63
  ignoreSSRCList?: number[],
62
64
  };
65
+
66
+ export type NetworkScore = number;
67
+
68
+ export type NetworkScores = {
69
+ outbound?: NetworkScore,
70
+ inbound?: NetworkScore,
71
+ };
package/src/index.ts CHANGED
@@ -14,7 +14,7 @@ import Engine from './engine';
14
14
  import Peer from './engine/Peer';
15
15
  import { LoadBalancerApiClientParams } from './engine/network/LoadBalancerClient';
16
16
  import DefaultEngineDependenciesFactory from './engine/DefaultEngineDependenciesFactory';
17
- import { IssuesHandler } from './types/engine';
17
+ import { IssuesHandler, NetworkScoresUpdatedHandler } from './types/engine';
18
18
 
19
19
  type ClientParams = {
20
20
  observer?: EnhancedEventEmitter;
@@ -24,6 +24,7 @@ type ClientParams = {
24
24
  logLevel?: LogLevel,
25
25
  onLogMessage?: LogMessageHandler;
26
26
  onIssues?: IssuesHandler,
27
+ onNetworkScoresUpdated?: NetworkScoresUpdatedHandler,
27
28
  };
28
29
 
29
30
  class Client {
@@ -37,6 +38,7 @@ class Client {
37
38
  network,
38
39
  onLogMessage,
39
40
  onIssues,
41
+ onNetworkScoresUpdated,
40
42
  logLevel,
41
43
  } = params;
42
44
 
@@ -54,6 +56,7 @@ class Client {
54
56
  logLevel,
55
57
  onLogMessage,
56
58
  onIssues,
59
+ onNetworkScoresUpdated,
57
60
  });
58
61
  }
59
62
 
@@ -7,13 +7,16 @@ import ChannelEventHandler from '../engine/handlers/ChannelEventHandler';
7
7
  import MediaSoupEventHandler from '../engine/handlers/MediaSoupEventHandler';
8
8
  import WebRTCIssueDetector from '../engine/wid/WebRTCIssueDetector';
9
9
  import { LoadBalancerApiClientParams } from '../engine/network/LoadBalancerClient';
10
- import { IssueDetectorResult } from '../engine/wid/types';
10
+ import { IssueDetectorResult, NetworkScores } from '../engine/wid/types';
11
11
  import { LogLevel } from './common';
12
12
 
13
13
  export type IssuesHandler = (issues: IssueDetectorResult) => void;
14
14
 
15
+ export type NetworkScoresUpdatedHandler = (networks: NetworkScores) => void;
16
+
15
17
  export interface CreateIssueDetectorParams {
16
18
  onIssues?: IssuesHandler;
19
+ onNetworkScoresUpdated?: NetworkScoresUpdatedHandler;
17
20
  ignoreSSRCList?: number[];
18
21
  }
19
22