@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.
- package/dist/engine/Logger.d.ts +6 -5
- package/dist/engine/Peer.d.ts +1 -0
- package/dist/engine/index.d.ts +3 -1
- package/dist/engine/wid/WIDRTCPeerConnection.d.ts +4 -0
- package/dist/engine/wid/WebRTCIssueDetector.d.ts +18 -0
- package/dist/engine/wid/WebRTCIssueEmitter.d.ts +9 -0
- package/dist/engine/wid/detectors/AvailableOutgoingBitrateIssueDetector.d.ts +7 -0
- package/dist/engine/wid/detectors/FramesDroppedIssueDetector.d.ts +8 -0
- package/dist/engine/wid/detectors/FramesEncodedSentIssueDetector.d.ts +8 -0
- package/dist/engine/wid/detectors/NetworkIssueDetector.d.ts +8 -0
- package/dist/engine/wid/detectors/NetworkMediaSyncIssueDetector.d.ts +8 -0
- package/dist/engine/wid/detectors/QualityLimitationsIssueDetector.d.ts +8 -0
- package/dist/engine/wid/types.d.ts +39 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.es.js +1 -1
- package/dist/index.js +2 -2
- package/dist/types/common.d.ts +1 -0
- package/package.json +2 -1
- package/src/engine/Logger.ts +19 -7
- package/src/engine/Peer.ts +7 -0
- package/src/engine/index.ts +23 -4
- package/src/engine/wid/WIDRTCPeerConnection.ts +13 -0
- package/src/engine/wid/WebRTCIssueDetector.ts +110 -0
- package/src/engine/wid/WebRTCIssueEmitter.ts +9 -0
- package/src/engine/wid/detectors/AvailableOutgoingBitrateIssueDetector.ts +32 -0
- package/src/engine/wid/detectors/FramesDroppedIssueDetector.ts +68 -0
- package/src/engine/wid/detectors/FramesEncodedSentIssueDetector.ts +73 -0
- package/src/engine/wid/detectors/NetworkIssueDetector.ts +95 -0
- package/src/engine/wid/detectors/NetworkMediaSyncIssueDetector.ts +60 -0
- package/src/engine/wid/detectors/QualityLimitationsIssueDetector.ts +67 -0
- package/src/engine/wid/types.ts +48 -0
- package/src/index.ts +4 -1
- package/src/types/common.ts +2 -0
package/dist/types/common.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@livedigital/client",
|
|
3
3
|
"author": "vlprojects",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "2.
|
|
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",
|
package/src/engine/Logger.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
39
|
+
debug(msg: any, ...meta: any[]): void {
|
|
40
|
+
this._debug(msg, ...meta);
|
|
41
|
+
this.onLogMessage(msg, ...meta);
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
warn(msg: any, ...meta: any[]): void {
|
|
45
|
+
this._warn(msg, ...meta);
|
|
46
|
+
this.onLogMessage(msg, ...meta);
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
error(msg: any, ...meta: any[]): void {
|
|
50
|
+
this._error(msg, ...meta);
|
|
51
|
+
this.onLogMessage(msg, ...meta);
|
|
40
52
|
}
|
|
41
53
|
}
|
|
42
54
|
|
package/src/engine/Peer.ts
CHANGED
|
@@ -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
|
}
|
package/src/engine/index.ts
CHANGED
|
@@ -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 {
|
|
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;
|