@livedigital/client 2.6.0 → 2.7.1-wid-bug-fixes.1

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 (33) hide show
  1. package/dist/engine/Logger.d.ts +6 -5
  2. package/dist/engine/Peer.d.ts +1 -0
  3. package/dist/engine/index.d.ts +3 -1
  4. package/dist/engine/wid/WIDRTCPeerConnection.d.ts +4 -0
  5. package/dist/engine/wid/WebRTCIssueDetector.d.ts +18 -0
  6. package/dist/engine/wid/WebRTCIssueEmitter.d.ts +9 -0
  7. package/dist/engine/wid/detectors/AvailableOutgoingBitrateIssueDetector.d.ts +7 -0
  8. package/dist/engine/wid/detectors/FramesDroppedIssueDetector.d.ts +8 -0
  9. package/dist/engine/wid/detectors/FramesEncodedSentIssueDetector.d.ts +8 -0
  10. package/dist/engine/wid/detectors/NetworkIssueDetector.d.ts +8 -0
  11. package/dist/engine/wid/detectors/NetworkMediaSyncIssueDetector.d.ts +8 -0
  12. package/dist/engine/wid/detectors/QualityLimitationsIssueDetector.d.ts +8 -0
  13. package/dist/engine/wid/types.d.ts +39 -0
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.es.js +1 -1
  16. package/dist/index.js +2 -2
  17. package/dist/types/common.d.ts +1 -0
  18. package/package.json +2 -1
  19. package/src/engine/Logger.ts +19 -7
  20. package/src/engine/Peer.ts +7 -0
  21. package/src/engine/index.ts +23 -4
  22. package/src/engine/wid/WIDRTCPeerConnection.ts +13 -0
  23. package/src/engine/wid/WebRTCIssueDetector.ts +110 -0
  24. package/src/engine/wid/WebRTCIssueEmitter.ts +9 -0
  25. package/src/engine/wid/detectors/AvailableOutgoingBitrateIssueDetector.ts +32 -0
  26. package/src/engine/wid/detectors/FramesDroppedIssueDetector.ts +68 -0
  27. package/src/engine/wid/detectors/FramesEncodedSentIssueDetector.ts +73 -0
  28. package/src/engine/wid/detectors/NetworkIssueDetector.ts +95 -0
  29. package/src/engine/wid/detectors/NetworkMediaSyncIssueDetector.ts +60 -0
  30. package/src/engine/wid/detectors/QualityLimitationsIssueDetector.ts +67 -0
  31. package/src/engine/wid/types.ts +48 -0
  32. package/src/index.ts +4 -1
  33. package/src/types/common.ts +2 -0
@@ -209,3 +209,4 @@ export declare type TransformParams = {
209
209
  width?: number;
210
210
  height?: number;
211
211
  };
212
+ export declare type LogMessageHandler = (msg: any, ...meta: any) => void;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@livedigital/client",
3
3
  "author": "vlprojects",
4
4
  "license": "MIT",
5
- "version": "2.6.0",
5
+ "version": "2.7.1-wid-bug-fixes.1",
6
6
  "private": false,
7
7
  "bugs": {
8
8
  "url": "https://github.com/vlprojects/livedigital-sdk/issues"
@@ -40,6 +40,7 @@
40
40
  ]
41
41
  },
42
42
  "dependencies": {
43
+ "@peermetrics/webrtc-stats": "^5.4.0",
43
44
  "axios": "^0.21.4",
44
45
  "debug": "^4.3.1",
45
46
  "mediasoup-client": "^3.6.50",
@@ -1,4 +1,5 @@
1
1
  import debug from 'debug';
2
+ import { LogMessageHandler } from '../types/common';
2
3
 
3
4
  const APP_NAME = 'LiveDigital';
4
5
 
@@ -9,7 +10,9 @@ class Logger {
9
10
 
10
11
  private readonly _error: debug.Debugger;
11
12
 
12
- constructor(prefix?: string) {
13
+ private readonly onLogMessage: LogMessageHandler;
14
+
15
+ constructor(prefix?: string, onLogMessage?: LogMessageHandler) {
13
16
  if (prefix) {
14
17
  this._debug = debug(`${APP_NAME}:${prefix}`);
15
18
  this._warn = debug(`${APP_NAME}:WARN:${prefix}`);
@@ -25,18 +28,27 @@ class Logger {
25
28
  this._warn.log = console.warn.bind(console);
26
29
  this._error.log = console.error.bind(console);
27
30
  /* eslint-enable no-console */
31
+
32
+ this.onLogMessage = (msg: any, ...meta: any[]) => {
33
+ if (typeof onLogMessage === 'function') {
34
+ onLogMessage(msg, ...meta);
35
+ }
36
+ };
28
37
  }
29
38
 
30
- get debug(): debug.Debugger {
31
- return this._debug;
39
+ debug(msg: any, ...meta: any[]): void {
40
+ this._debug(msg, ...meta);
41
+ this.onLogMessage(msg, ...meta);
32
42
  }
33
43
 
34
- get warn(): debug.Debugger {
35
- return this._warn;
44
+ warn(msg: any, ...meta: any[]): void {
45
+ this._warn(msg, ...meta);
46
+ this.onLogMessage(msg, ...meta);
36
47
  }
37
48
 
38
- get error(): debug.Debugger {
39
- return this._error;
49
+ error(msg: any, ...meta: any[]): void {
50
+ this._error(msg, ...meta);
51
+ this.onLogMessage(msg, ...meta);
40
52
  }
41
53
  }
42
54
 
@@ -68,6 +68,8 @@ class Peer {
68
68
 
69
69
  private outgoingConnectionQuality: ConnectionQuality = ConnectionQuality.GOOD;
70
70
 
71
+ private overallConnectionQuality: ConnectionQuality = ConnectionQuality.GOOD;
72
+
71
73
  constructor({
72
74
  id,
73
75
  channelIds,
@@ -332,10 +334,15 @@ class Peer {
332
334
  setTimeout(() => {
333
335
  // pick connection (incoming or outgoing) with lower quality
334
336
  const connectionQuality = Math.min(this.outgoingConnectionQuality, this.incomingConnectionQuality);
337
+ if (connectionQuality === this.overallConnectionQuality) {
338
+ return;
339
+ }
340
+
335
341
  this.observer.safeEmit(PEER_EVENTS.connectionQualityChanged, {
336
342
  connectionQuality,
337
343
  });
338
344
 
345
+ this.overallConnectionQuality = connectionQuality;
339
346
  this.logger.debug('emitConnectionQuality()', { peer: this, connectionQuality });
340
347
  }, 1000);
341
348
  }
@@ -10,6 +10,7 @@ import {
10
10
  SocketResponse,
11
11
  Track,
12
12
  TrackLabel,
13
+ LogMessageHandler,
13
14
  } from '../types/common';
14
15
  import EnhancedEventEmitter from '../EnhancedEventEmitter';
15
16
  import System from './system';
@@ -26,11 +27,13 @@ import { GetNodeRequest } from '../types/network';
26
27
  import VideoTrack from './media/tracks/VideoTrack';
27
28
  import AudioTrack from './media/tracks/AudioTrack';
28
29
  import PeerTrack from './media/tracks/PeerTrack';
30
+ import WebRTCIssueDetector from './wid/WebRTCIssueDetector';
29
31
  import { retryAsync } from '../helpers/retry';
30
32
 
31
33
  type EngineParams = {
32
34
  clientEventEmitter: EnhancedEventEmitter,
33
35
  network: NetworkParams,
36
+ onLogMessage?: LogMessageHandler;
34
37
  };
35
38
 
36
39
  class Engine {
@@ -60,8 +63,13 @@ class Engine {
60
63
 
61
64
  private logger: Logger;
62
65
 
66
+ private webRtcIssueDetector: WebRTCIssueDetector;
67
+
63
68
  constructor(params: EngineParams) {
64
- const { clientEventEmitter, network } = params;
69
+ const {
70
+ clientEventEmitter,
71
+ network,
72
+ } = params;
65
73
  this.system = new System({ clientEventEmitter });
66
74
  this.media = new Media();
67
75
  this.network = new Network(network);
@@ -69,7 +77,15 @@ class Engine {
69
77
  this.clientEventEmitter = clientEventEmitter;
70
78
  this.channelEventsHandler = new ChannelEventHandler(this);
71
79
  this.mediaSoupEventsHandler = new MediaSoupEventHandler(this);
72
- this.logger = new Logger('Engine');
80
+ this.logger = new Logger('Engine', params.onLogMessage);
81
+ this.webRtcIssueDetector = new WebRTCIssueDetector({
82
+ onIssue: (issues: any[]) => {
83
+ issues.forEach((issue) => {
84
+ this.logger.warn('webRtcIssueDetector', issue);
85
+ });
86
+ },
87
+ });
88
+
73
89
  this.watchSocketState();
74
90
  }
75
91
 
@@ -78,6 +94,7 @@ class Engine {
78
94
  const { rtpCapabilities } = await this.network.socket
79
95
  .request('router.getRtpCapabilities') as { rtpCapabilities: RtpCapabilities };
80
96
  await this.media.loadDevice(rtpCapabilities);
97
+ this.webRtcIssueDetector.watchNewPeerConnections();
81
98
  await this.network.createSendTransport(this.media.mediasoupDevice);
82
99
  await this.network.createRecvTransport(this.media.mediasoupDevice);
83
100
  this.initialized = true;
@@ -94,6 +111,8 @@ class Engine {
94
111
  this.isJoined = false;
95
112
  this.app = undefined;
96
113
  this.channel = undefined;
114
+ this.webRtcIssueDetector.stopWatchingNewPeerConnections();
115
+ this.webRtcIssueDetector.resetWatchedPeerConnectionsList();
97
116
  this.network.socket.disconnect();
98
117
  this.peersRepository.clear();
99
118
  await this.unpublish();
@@ -488,11 +507,11 @@ class Engine {
488
507
  private async performJoin(params: JoinChannelParams): Promise<void> {
489
508
  try {
490
509
  await this.sendJoinChannelRequestWithRetry(params);
510
+ this.app = params.appId;
511
+ this.channel = params.channelId;
491
512
  await this.initialize();
492
513
  this.channelEventsHandler.subscribeToEvents();
493
514
  this.mediaSoupEventsHandler.subscribeToEvents();
494
- this.app = params.appId;
495
- this.channel = params.channelId;
496
515
  this.isJoined = true;
497
516
  this.logger.debug(`Successfully joined to ${params.channelId}. Peers in channel:`, this.peersRepository);
498
517
  } catch (error) {
@@ -0,0 +1,13 @@
1
+ import { WIDWindow } from './types';
2
+
3
+ class WIDRTCPeerConnection extends RTCPeerConnection {
4
+ constructor(configuration?: RTCConfiguration) {
5
+ super(configuration);
6
+ const { wid: webRTCIssueDetector } = (window as unknown as WIDWindow);
7
+ if (webRTCIssueDetector) {
8
+ webRTCIssueDetector.handleNewPeerConnection(this);
9
+ }
10
+ }
11
+ }
12
+
13
+ export default WIDRTCPeerConnection;
@@ -0,0 +1,110 @@
1
+ import { WebRTCStats, WebRTCStatsEventData } from '@peermetrics/webrtc-stats';
2
+ import WIDRTCPeerConnection from './WIDRTCPeerConnection';
3
+ import { WebRTCIssueEmitter } from './WebRTCIssueEmitter';
4
+ import {
5
+ WebRTCIssueDetectorConstructorParams,
6
+ EventType,
7
+ IssuePayload,
8
+ WIDWindow,
9
+ IssueDetector,
10
+ } from './types';
11
+ import QualityLimitationsIssueDetector from './detectors/QualityLimitationsIssueDetector';
12
+ import FramesDroppedIssueDetector from './detectors/FramesDroppedIssueDetector';
13
+ import FramesEncodedSentIssueDetector from './detectors/FramesEncodedSentIssueDetector';
14
+ import NetworkIssueDetector from './detectors/NetworkIssueDetector';
15
+ import NetworkMediaSyncIssueDetector from './detectors/NetworkMediaSyncIssueDetector';
16
+ import AvailableOutgoingBitrateIssueDetector from './detectors/AvailableOutgoingBitrateIssueDetector';
17
+
18
+ class WebRTCIssueDetector {
19
+ private readonly webrtcStats: WebRTCStats;
20
+
21
+ private readonly webrtcStatsPeerId = 'peerId';
22
+
23
+ private readonly getStatsInterval = 5000;
24
+
25
+ private readonly detectors: IssueDetector[] = [];
26
+
27
+ readonly eventEmitter = new WebRTCIssueEmitter();
28
+
29
+ #running = false;
30
+
31
+ constructor(params: WebRTCIssueDetectorConstructorParams) {
32
+ if (params.onIssue) {
33
+ this.eventEmitter.on(EventType.Issue, params.onIssue);
34
+ }
35
+
36
+ this.webrtcStats = new WebRTCStats({
37
+ getStatsInterval: this.getStatsInterval,
38
+ });
39
+
40
+ (window as unknown as WIDWindow).wid = this;
41
+ if (window.RTCPeerConnection) {
42
+ window.RTCPeerConnection = WIDRTCPeerConnection;
43
+ }
44
+
45
+ this.detectors = [
46
+ new QualityLimitationsIssueDetector(),
47
+ new FramesDroppedIssueDetector(),
48
+ new FramesEncodedSentIssueDetector(),
49
+ new NetworkIssueDetector(),
50
+ new NetworkMediaSyncIssueDetector(),
51
+ new AvailableOutgoingBitrateIssueDetector(),
52
+ ];
53
+
54
+ this.webrtcStats.on('stats', (event) => {
55
+ this.detectIssues(event.data);
56
+ });
57
+ }
58
+
59
+ public watchNewPeerConnections(): void {
60
+ if (this.#running) {
61
+ throw new Error('WebRTCIssueDetector is already started');
62
+ }
63
+
64
+ this.#running = true;
65
+ }
66
+
67
+ public stopWatchingNewPeerConnections(): void {
68
+ if (!this.#running) {
69
+ throw new Error('WebRTCIssueDetector is already stopped');
70
+ }
71
+
72
+ this.#running = false;
73
+ }
74
+
75
+ public resetWatchedPeerConnectionsList(): void {
76
+ this.webrtcStats.removePeer(this.webrtcStatsPeerId);
77
+ }
78
+
79
+ public handleNewPeerConnection(pc: RTCPeerConnection): void {
80
+ if (!this.#running) {
81
+ return;
82
+ }
83
+
84
+ if (!(pc instanceof RTCPeerConnection)) {
85
+ return;
86
+ }
87
+
88
+ this.webrtcStats.addConnection({
89
+ pc,
90
+ peerId: this.webrtcStatsPeerId,
91
+ });
92
+ }
93
+
94
+ private emitIssues(issues: IssuePayload[]): void {
95
+ this.eventEmitter.emit(EventType.Issue, issues);
96
+ }
97
+
98
+ private detectIssues(data: WebRTCStatsEventData): void {
99
+ const issues = this.detectors.reduce((acc, detector) => [
100
+ ...acc,
101
+ ...detector.detect(data),
102
+ ], [] as IssuePayload[]);
103
+
104
+ if (issues.length > 0) {
105
+ this.emitIssues(issues);
106
+ }
107
+ }
108
+ }
109
+
110
+ export default WebRTCIssueDetector;
@@ -0,0 +1,9 @@
1
+ import { EventEmitter } from 'events';
2
+ import { EventType, EventPayload, IssueDetectorResult } from './types';
3
+
4
+ export declare interface WebRTCIssueEmitter {
5
+ on(event: EventType, listener: (issues: IssueDetectorResult) => void): this;
6
+ emit(event: EventType, payload: EventPayload): boolean;
7
+ }
8
+
9
+ export class WebRTCIssueEmitter extends EventEmitter {}
@@ -0,0 +1,32 @@
1
+ import { WebRTCStatsEventData } from '@peermetrics/webrtc-stats';
2
+ import {
3
+ IssueDetector,
4
+ IssueDetectorResult,
5
+ IssueReason,
6
+ IssueType,
7
+ } from '../types';
8
+
9
+ class AvailableOutgoingBitrateIssueDetector implements IssueDetector {
10
+ #availableOutgoingBitrateTreshhold = 300000; // 300 kbit/s
11
+
12
+ detect(data: WebRTCStatsEventData): IssueDetectorResult {
13
+ const issues: IssueDetectorResult = [];
14
+ const { availableOutgoingBitrate } = data.connection;
15
+ if (availableOutgoingBitrate === undefined) {
16
+ // availableOutgoingBitrate is not measured yet
17
+ return issues;
18
+ }
19
+
20
+ if (availableOutgoingBitrate < this.#availableOutgoingBitrateTreshhold) {
21
+ issues.push({
22
+ type: IssueType.Network,
23
+ reason: IssueReason.OutboundNetworkThroughput,
24
+ debug: `availableOutgoingBitrate: ${availableOutgoingBitrate}`,
25
+ });
26
+ }
27
+
28
+ return issues;
29
+ }
30
+ }
31
+
32
+ export default AvailableOutgoingBitrateIssueDetector;
@@ -0,0 +1,68 @@
1
+ import { WebRTCStatsEventData } from '@peermetrics/webrtc-stats';
2
+ import {
3
+ IssueDetector,
4
+ IssueDetectorResult,
5
+ IssueReason,
6
+ IssueType,
7
+ } from '../types';
8
+
9
+ class FramesDroppedIssueDetector implements IssueDetector {
10
+ #lastProcessedStats: { [connectionId: string]: WebRTCStatsEventData } = {};
11
+
12
+ #framesDroppedTreshold = 0.5;
13
+
14
+ detect(data: WebRTCStatsEventData): IssueDetectorResult {
15
+ const issues = this.processData(data);
16
+ this.#lastProcessedStats[data.connection.id] = data;
17
+ return issues;
18
+ }
19
+
20
+ private processData(data: WebRTCStatsEventData): IssueDetectorResult {
21
+ if (!data.video?.inbound) {
22
+ return [];
23
+ }
24
+
25
+ const streamsWithDroppedFrames = data.video.inbound.filter((stats) => stats.framesDropped > 0);
26
+ const issues: IssueDetectorResult = [];
27
+ const previousInboundRTPVideoStreamsStats = this.#lastProcessedStats[data.connection.id]?.video.inbound;
28
+
29
+ if (!previousInboundRTPVideoStreamsStats) {
30
+ return issues;
31
+ }
32
+
33
+ streamsWithDroppedFrames.forEach((streamStats) => {
34
+ const previousStreamStats = previousInboundRTPVideoStreamsStats.find((item) => item.ssrc === streamStats.ssrc);
35
+ if (!previousStreamStats) {
36
+ return;
37
+ }
38
+
39
+ if (streamStats.framesDropped === previousStreamStats.framesDropped) {
40
+ // stream is decoded correctly
41
+ return;
42
+ }
43
+
44
+ const deltaFramesReceived = streamStats.framesReceived - previousStreamStats.framesReceived;
45
+ const deltaFramesDecoded = streamStats.framesDecoded - previousStreamStats.framesDecoded;
46
+ const deltaFramesDropped = streamStats.framesDropped - previousStreamStats.framesDropped;
47
+ if (deltaFramesReceived === 0 && deltaFramesDecoded === 0) {
48
+ // looks like stream is stopped, skip checking framesDropped
49
+ return;
50
+ }
51
+
52
+ const framesDropped = deltaFramesDropped / deltaFramesReceived;
53
+ if (framesDropped >= this.#framesDroppedTreshold) {
54
+ // more than half of the received frames were dropped
55
+ issues.push({
56
+ type: IssueType.CPU,
57
+ reason: IssueReason.DecoderCPUThrottling,
58
+ ssrc: streamStats.ssrc,
59
+ debug: `framesDropped: ${Math.round(framesDropped * 100)}`,
60
+ });
61
+ }
62
+ });
63
+
64
+ return issues;
65
+ }
66
+ }
67
+
68
+ export default FramesDroppedIssueDetector;
@@ -0,0 +1,73 @@
1
+ import { WebRTCStatsEventData } from '@peermetrics/webrtc-stats';
2
+ import {
3
+ IssueDetector,
4
+ IssueDetectorResult,
5
+ IssueReason,
6
+ IssueType,
7
+ } from '../types';
8
+
9
+ class FramesEncodedSentIssueDetector implements IssueDetector {
10
+ #lastProcessedStats: { [connectionId: string]: WebRTCStatsEventData } = {};
11
+
12
+ #missedFramesTreshold = 0.15;
13
+
14
+ detect(data: WebRTCStatsEventData): IssueDetectorResult {
15
+ const issues = this.processData(data);
16
+ this.#lastProcessedStats[data.connection.id] = data;
17
+ return issues;
18
+ }
19
+
20
+ private processData(data: WebRTCStatsEventData): IssueDetectorResult {
21
+ if (!data.video?.outbound) {
22
+ return [];
23
+ }
24
+
25
+ const streamsWithEncodedFrames = data.video.outbound.filter((stats) => stats.framesEncoded > 0);
26
+ const issues: IssueDetectorResult = [];
27
+ const previousOutboundRTPVideoStreamsStats = this.#lastProcessedStats[data.connection.id]?.video.outbound;
28
+
29
+ if (!previousOutboundRTPVideoStreamsStats) {
30
+ return issues;
31
+ }
32
+
33
+ streamsWithEncodedFrames.forEach((streamStats) => {
34
+ const previousStreamStats = previousOutboundRTPVideoStreamsStats.find((item) => item.ssrc === streamStats.ssrc);
35
+
36
+ if (!previousStreamStats) {
37
+ return;
38
+ }
39
+
40
+ if (streamStats.framesEncoded === previousStreamStats.framesEncoded) {
41
+ // stream is paused
42
+ return;
43
+ }
44
+
45
+ const deltaFramesEncoded = streamStats.framesEncoded - previousStreamStats.framesEncoded;
46
+ const deltaFramesSent = streamStats.framesSent - previousStreamStats.framesSent;
47
+
48
+ if (deltaFramesEncoded === 0) {
49
+ // stream is paused
50
+ return;
51
+ }
52
+
53
+ if (deltaFramesEncoded === deltaFramesSent) {
54
+ // stream is ok
55
+ return;
56
+ }
57
+
58
+ const missedFrames = deltaFramesSent / deltaFramesEncoded;
59
+ if (missedFrames >= this.#missedFramesTreshold) {
60
+ issues.push({
61
+ type: IssueType.Network,
62
+ reason: IssueReason.OutboundNetworkThroughput,
63
+ ssrc: streamStats.ssrc,
64
+ debug: `missedFrames: ${Math.round(missedFrames * 100)}%`,
65
+ });
66
+ }
67
+ });
68
+
69
+ return issues;
70
+ }
71
+ }
72
+
73
+ export default FramesEncodedSentIssueDetector;
@@ -0,0 +1,95 @@
1
+ import { WebRTCStatsEventData } from '@peermetrics/webrtc-stats';
2
+ import {
3
+ IssueDetector,
4
+ IssueDetectorResult,
5
+ IssueReason,
6
+ IssueType,
7
+ } from '../types';
8
+
9
+ class NetworkIssueDetector implements IssueDetector {
10
+ #lastProcessedStats: { [connectionId: string]: WebRTCStatsEventData } = {};
11
+
12
+ detect(data: WebRTCStatsEventData): IssueDetectorResult {
13
+ const issues = this.processData(data);
14
+ this.#lastProcessedStats[data.connection.id] = data;
15
+ return issues;
16
+ }
17
+
18
+ private processData(data: WebRTCStatsEventData): IssueDetectorResult {
19
+ const issues: IssueDetectorResult = [];
20
+ const inboundRTPStreamsStats = [...data.audio?.inbound, ...data.video?.inbound];
21
+ const previousStats = this.#lastProcessedStats[data.connection.id];
22
+ const previousInboundStreamStats = [...previousStats.video?.inbound, ...previousStats.audio?.inbound];
23
+
24
+ if (!inboundRTPStreamsStats.length || !previousStats) {
25
+ return issues;
26
+ }
27
+
28
+ const { packetsReceived } = data.connection;
29
+ const lastPacketsReceived = previousStats.connection.packetsReceived;
30
+
31
+ const rtpNetworkStats = inboundRTPStreamsStats.reduce((stats, currentStreamStats) => {
32
+ const previousStreamStats = previousInboundStreamStats.find((stream) => stream.ssrc === currentStreamStats.ssrc);
33
+ const lastJitterBufferDelay = previousStreamStats?.jitterBufferDelay || 0;
34
+ const lastJitterBufferEmittedCount = previousStreamStats?.jitterBufferEmittedCount || 0;
35
+ const delay = currentStreamStats.jitterBufferDelay - lastJitterBufferDelay;
36
+ const emitted = currentStreamStats.jitterBufferEmittedCount - lastJitterBufferEmittedCount;
37
+ const latency = delay && emitted ? (1e3 * delay) / emitted : 0;
38
+
39
+ return {
40
+ maxLatency: latency > stats.maxLatency ? latency : stats.maxLatency,
41
+ packetsLost: stats.packetsLost + currentStreamStats.packetsLost,
42
+ lastPacketsLost: stats.lastPacketsLost + (previousStreamStats?.packetsLost || 0),
43
+ };
44
+ }, { maxLatency: 0, packetsLost: 0, lastPacketsLost: 0 });
45
+
46
+ const rtt = (1e3 * data.connection.currentRoundTripTime) || 0;
47
+ const jitter = Math.round(rtt / 2 + rtpNetworkStats.maxLatency);
48
+
49
+ const deltaPacketReceived = packetsReceived - lastPacketsReceived;
50
+ const deltaPacketLost = rtpNetworkStats.packetsLost - rtpNetworkStats.lastPacketsLost;
51
+
52
+ const packetsLoss = deltaPacketReceived && deltaPacketLost
53
+ ? Math.round((deltaPacketLost * 100) / (deltaPacketReceived + deltaPacketLost))
54
+ : 0;
55
+
56
+ const isHighPacketsLoss = packetsLoss > 5;
57
+ const isHighJitter = jitter >= 200;
58
+ const isHighRTT = rtt >= 200;
59
+
60
+ const isNetworkIssue = (!isHighPacketsLoss && isHighJitter) || isHighJitter || isHighPacketsLoss;
61
+ const isServerIssue = isHighRTT && !isHighJitter && !isHighPacketsLoss;
62
+ const isNetworkMediaLatencyIssue = isHighPacketsLoss && isHighJitter;
63
+
64
+ if (isNetworkIssue) {
65
+ issues.push({
66
+ type: IssueType.Network,
67
+ reason: IssueReason.NetworkQuality,
68
+ iceCandidate: data.connection.local.id,
69
+ debug: `packetLoss: ${packetsLoss}%, jitter: ${jitter}`,
70
+ });
71
+ }
72
+
73
+ if (isServerIssue) {
74
+ issues.push({
75
+ type: IssueType.Server,
76
+ reason: IssueReason.ServerIssue,
77
+ iceCandidate: data.connection.remote.id,
78
+ debug: `packetLoss: ${packetsLoss}%,jitter: ${jitter}, rtt: ${rtt},`,
79
+ });
80
+ }
81
+
82
+ if (isNetworkMediaLatencyIssue) {
83
+ issues.push({
84
+ type: IssueType.Network,
85
+ reason: IssueReason.NetworkMediaLatency,
86
+ iceCandidate: data.connection.local.id,
87
+ debug: `packetLoss: ${packetsLoss}%, jitter: ${jitter}`,
88
+ });
89
+ }
90
+
91
+ return issues;
92
+ }
93
+ }
94
+
95
+ export default NetworkIssueDetector;
@@ -0,0 +1,60 @@
1
+ import { WebRTCStatsEventData } from '@peermetrics/webrtc-stats';
2
+ import {
3
+ IssueDetector,
4
+ IssueDetectorResult,
5
+ IssueReason,
6
+ IssueType,
7
+ } from '../types';
8
+
9
+ class NetworkMediaSyncIssueDetector implements IssueDetector {
10
+ #lastProcessedStats: { [connectionId: string]: WebRTCStatsEventData } = {};
11
+
12
+ detect(data: WebRTCStatsEventData): IssueDetectorResult {
13
+ const issues = this.processData(data);
14
+ this.#lastProcessedStats[data.connection.id] = data;
15
+ return issues;
16
+ }
17
+
18
+ private processData(data: WebRTCStatsEventData): IssueDetectorResult {
19
+ const inboundRTPAudioStreamsStats = data.audio.inbound;
20
+ const issues: IssueDetectorResult = [];
21
+ const previousInboundRTPAudioStreamsStats = this.#lastProcessedStats[data.connection.id]?.audio.inbound;
22
+
23
+ if (!previousInboundRTPAudioStreamsStats) {
24
+ return issues;
25
+ }
26
+
27
+ inboundRTPAudioStreamsStats.forEach((stats) => {
28
+ const previousStreamStats = previousInboundRTPAudioStreamsStats.find((item) => item.ssrc === stats.ssrc);
29
+ if (!previousStreamStats) {
30
+ return;
31
+ }
32
+
33
+ const nowCorrectedSamples = stats.track.insertedSamplesForDeceleration
34
+ + stats.track.removedSamplesForAcceleration;
35
+ const lastCorrectedSamples = previousStreamStats.track.insertedSamplesForDeceleration
36
+ + previousStreamStats.track.removedSamplesForAcceleration;
37
+
38
+ if (nowCorrectedSamples === lastCorrectedSamples) {
39
+ return;
40
+ }
41
+
42
+ const deltaSamplesReceived = stats.track.totalSamplesReceived - previousStreamStats.track.totalSamplesReceived;
43
+ const deltaCorrectedSamples = nowCorrectedSamples - lastCorrectedSamples;
44
+ const correctedSamplesPercentage = Math.round((deltaCorrectedSamples * 100) / deltaSamplesReceived);
45
+
46
+ if (correctedSamplesPercentage > 5) {
47
+ issues.push({
48
+ type: IssueType.Network,
49
+ reason: IssueReason.NetworkMediaSyncFailure,
50
+ ssrc: stats.ssrc,
51
+ debug: `correctedSamplesPercentage: ${correctedSamplesPercentage}%`,
52
+ });
53
+ }
54
+ });
55
+
56
+ return issues;
57
+ }
58
+ }
59
+
60
+ export default NetworkMediaSyncIssueDetector;