@livedigital/client 2.26.0-stop-streams.1 → 2.27.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.
- package/dist/engine/DefaultEngineDependenciesFactory.d.ts +1 -1
- package/dist/engine/media/tracks/BaseTrack.d.ts +3 -2
- package/dist/engine/media/tracks/MediaStreamTrackManager.d.ts +14 -0
- package/dist/index.es.js +1 -1
- package/dist/index.js +2 -2
- package/dist/types/common.d.ts +1 -1
- package/dist/types/engine.d.ts +1 -2
- package/package.json +3 -2
- package/src/engine/DefaultEngineDependenciesFactory.ts +15 -14
- package/src/engine/index.ts +2 -2
- package/src/engine/media/index.ts +11 -10
- package/src/engine/media/tracks/BaseTrack.ts +14 -9
- package/src/engine/media/tracks/MediaStreamTrackManager.ts +46 -0
- package/src/types/common.ts +1 -1
- package/src/types/engine.ts +1 -2
- package/dist/engine/media/tracks/TrackMediaStreamManager.d.ts +0 -11
- package/dist/engine/wid/WebRTCIssueDetector.d.ts +0 -20
- package/dist/engine/wid/WebRTCIssueEmitter.d.ts +0 -11
- package/dist/engine/wid/detectors/AvailableOutgoingBitrateIssueDetector.d.ts +0 -6
- package/dist/engine/wid/detectors/FramesDroppedIssueDetector.d.ts +0 -7
- package/dist/engine/wid/detectors/FramesEncodedSentIssueDetector.d.ts +0 -7
- package/dist/engine/wid/detectors/InboundNetworkIssueDetector.d.ts +0 -7
- package/dist/engine/wid/detectors/NetworkMediaSyncIssueDetector.d.ts +0 -7
- package/dist/engine/wid/detectors/OutboundNetworkIssueDetector.d.ts +0 -7
- package/dist/engine/wid/detectors/QualityLimitationsIssueDetector.d.ts +0 -7
- package/dist/engine/wid/detectors/VideoCodecMismatchDetector.d.ts +0 -10
- package/dist/engine/wid/lib/NetworkScoresCalculator.d.ts +0 -9
- package/dist/engine/wid/lib/PeriodicWebRTCStatsReporter.d.ts +0 -20
- package/dist/engine/wid/lib/parser/CompositeRTCStatsParser.d.ts +0 -22
- package/dist/engine/wid/lib/parser/RTCStatsParser.d.ts +0 -20
- package/dist/engine/wid/lib/parser/utils.d.ts +0 -7
- package/dist/engine/wid/types.d.ts +0 -381
- package/src/engine/media/tracks/TrackMediaStreamManager.ts +0 -41
- package/src/engine/wid/WebRTCIssueDetector.ts +0 -136
- package/src/engine/wid/WebRTCIssueEmitter.ts +0 -16
- package/src/engine/wid/detectors/AvailableOutgoingBitrateIssueDetector.ts +0 -57
- package/src/engine/wid/detectors/FramesDroppedIssueDetector.ts +0 -65
- package/src/engine/wid/detectors/FramesEncodedSentIssueDetector.ts +0 -69
- package/src/engine/wid/detectors/InboundNetworkIssueDetector.ts +0 -120
- package/src/engine/wid/detectors/NetworkMediaSyncIssueDetector.ts +0 -60
- package/src/engine/wid/detectors/OutboundNetworkIssueDetector.ts +0 -96
- package/src/engine/wid/detectors/QualityLimitationsIssueDetector.ts +0 -63
- package/src/engine/wid/detectors/VideoCodecMismatchDetector.ts +0 -78
- package/src/engine/wid/lib/NetworkScoresCalculator.ts +0 -126
- package/src/engine/wid/lib/PeriodicWebRTCStatsReporter.ts +0 -66
- package/src/engine/wid/lib/parser/CompositeRTCStatsParser.ts +0 -82
- package/src/engine/wid/lib/parser/RTCStatsParser.ts +0 -274
- package/src/engine/wid/lib/parser/utils.ts +0 -40
- package/src/engine/wid/types.ts +0 -408
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IssueDetector,
|
|
3
|
-
IssueDetectorResult,
|
|
4
|
-
IssueReason,
|
|
5
|
-
IssueType,
|
|
6
|
-
WebRTCStatsParsed,
|
|
7
|
-
} from '../types';
|
|
8
|
-
|
|
9
|
-
class FramesEncodedSentIssueDetector implements IssueDetector {
|
|
10
|
-
#lastProcessedStats: { [connectionId: string]: WebRTCStatsParsed } = {};
|
|
11
|
-
|
|
12
|
-
#missedFramesTreshold = 0.15;
|
|
13
|
-
|
|
14
|
-
detect(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
15
|
-
const issues = this.processData(data);
|
|
16
|
-
this.#lastProcessedStats[data.connection.id] = data;
|
|
17
|
-
return issues;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
private processData(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
21
|
-
const streamsWithEncodedFrames = data.video.outbound.filter((stats) => stats.framesEncoded > 0);
|
|
22
|
-
const issues: IssueDetectorResult = [];
|
|
23
|
-
const previousOutboundRTPVideoStreamsStats = this.#lastProcessedStats[data.connection.id]?.video.outbound;
|
|
24
|
-
|
|
25
|
-
if (!previousOutboundRTPVideoStreamsStats) {
|
|
26
|
-
return issues;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
streamsWithEncodedFrames.forEach((streamStats) => {
|
|
30
|
-
const previousStreamStats = previousOutboundRTPVideoStreamsStats.find((item) => item.ssrc === streamStats.ssrc);
|
|
31
|
-
|
|
32
|
-
if (!previousStreamStats) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (streamStats.framesEncoded === previousStreamStats.framesEncoded) {
|
|
37
|
-
// stream is paused
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const deltaFramesEncoded = streamStats.framesEncoded - previousStreamStats.framesEncoded;
|
|
42
|
-
const deltaFramesSent = streamStats.framesSent - previousStreamStats.framesSent;
|
|
43
|
-
|
|
44
|
-
if (deltaFramesEncoded === 0) {
|
|
45
|
-
// stream is paused
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (deltaFramesEncoded === deltaFramesSent) {
|
|
50
|
-
// stream is ok
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const missedFrames = deltaFramesSent / deltaFramesEncoded;
|
|
55
|
-
if (missedFrames >= this.#missedFramesTreshold) {
|
|
56
|
-
issues.push({
|
|
57
|
-
type: IssueType.Network,
|
|
58
|
-
reason: IssueReason.OutboundNetworkThroughput,
|
|
59
|
-
ssrc: streamStats.ssrc,
|
|
60
|
-
debug: `missedFrames: ${Math.round(missedFrames * 100)}%`,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return issues;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export default FramesEncodedSentIssueDetector;
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IssueDetector,
|
|
3
|
-
IssueDetectorResult,
|
|
4
|
-
IssueReason,
|
|
5
|
-
IssueType,
|
|
6
|
-
WebRTCStatsParsed,
|
|
7
|
-
} from '../types';
|
|
8
|
-
|
|
9
|
-
class InboundNetworkIssueDetector implements IssueDetector {
|
|
10
|
-
#lastProcessedStats: { [connectionId: string]: WebRTCStatsParsed } = {};
|
|
11
|
-
|
|
12
|
-
detect(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
13
|
-
const issues = this.processData(data);
|
|
14
|
-
this.#lastProcessedStats[data.connection.id] = data;
|
|
15
|
-
return issues;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
private processData(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
19
|
-
const issues: IssueDetectorResult = [];
|
|
20
|
-
const inboundRTPStreamsStats = [...data.audio?.inbound, ...data.video?.inbound];
|
|
21
|
-
if (!inboundRTPStreamsStats.length) {
|
|
22
|
-
return issues;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const previousStats = this.#lastProcessedStats[data.connection.id];
|
|
26
|
-
if (!previousStats) {
|
|
27
|
-
return issues;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const previousInboundStreamStats = [...previousStats.video?.inbound, ...previousStats.audio?.inbound];
|
|
31
|
-
const { packetsReceived } = data.connection;
|
|
32
|
-
const lastPacketsReceived = previousStats.connection.packetsReceived;
|
|
33
|
-
|
|
34
|
-
const rtpNetworkStats = inboundRTPStreamsStats.reduce((stats, currentStreamStats) => {
|
|
35
|
-
const previousStreamStats = previousInboundStreamStats.find((stream) => stream.ssrc === currentStreamStats.ssrc);
|
|
36
|
-
|
|
37
|
-
const lastJitterBufferDelay = previousStreamStats?.jitterBufferDelay || 0;
|
|
38
|
-
const lastJitterBufferEmittedCount = previousStreamStats?.jitterBufferEmittedCount || 0;
|
|
39
|
-
const delay = currentStreamStats.jitterBufferDelay - lastJitterBufferDelay;
|
|
40
|
-
const emitted = currentStreamStats.jitterBufferEmittedCount - lastJitterBufferEmittedCount;
|
|
41
|
-
const jitterBufferDelayMs = delay && emitted ? (1e3 * delay) / emitted : 0;
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
sumJitter: stats.sumJitter + currentStreamStats.jitter,
|
|
45
|
-
sumJitterBufferDelayMs: stats.sumJitterBufferDelayMs + jitterBufferDelayMs,
|
|
46
|
-
packetsLost: stats.packetsLost + currentStreamStats.packetsLost,
|
|
47
|
-
lastPacketsLost: stats.lastPacketsLost + (previousStreamStats?.packetsLost || 0),
|
|
48
|
-
};
|
|
49
|
-
}, {
|
|
50
|
-
sumJitter: 0,
|
|
51
|
-
sumJitterBufferDelayMs: 0,
|
|
52
|
-
packetsLost: 0,
|
|
53
|
-
lastPacketsLost: 0,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const rtt = (1e3 * data.connection.currentRoundTripTime) || 0;
|
|
57
|
-
const { sumJitter, sumJitterBufferDelayMs } = rtpNetworkStats;
|
|
58
|
-
const avgJitter = sumJitter / inboundRTPStreamsStats.length;
|
|
59
|
-
const avgJitterBufferDelay = sumJitterBufferDelayMs / inboundRTPStreamsStats.length;
|
|
60
|
-
|
|
61
|
-
const deltaPacketReceived = packetsReceived - lastPacketsReceived;
|
|
62
|
-
const deltaPacketLost = rtpNetworkStats.packetsLost - rtpNetworkStats.lastPacketsLost;
|
|
63
|
-
|
|
64
|
-
const packetsLoss = deltaPacketReceived && deltaPacketLost
|
|
65
|
-
? Math.round((deltaPacketLost * 100) / (deltaPacketReceived + deltaPacketLost))
|
|
66
|
-
: 0;
|
|
67
|
-
|
|
68
|
-
const isHighPacketsLoss = packetsLoss > 5;
|
|
69
|
-
const isHighJitter = avgJitter >= 200;
|
|
70
|
-
const isHighRTT = rtt >= 250;
|
|
71
|
-
const isHighJitterBufferDelay = avgJitterBufferDelay > 500;
|
|
72
|
-
const isNetworkIssue = (!isHighPacketsLoss && isHighJitter) || isHighJitter || isHighPacketsLoss;
|
|
73
|
-
const isServerIssue = isHighRTT && !isHighJitter && !isHighPacketsLoss;
|
|
74
|
-
const isNetworkMediaLatencyIssue = isHighPacketsLoss && isHighJitter;
|
|
75
|
-
const isNetworkMediaSyncIssue = isHighJitter && isHighJitterBufferDelay;
|
|
76
|
-
|
|
77
|
-
const debug = `packetLoss: ${packetsLoss}%, jitter: ${avgJitter}, rtt: ${rtt},`
|
|
78
|
-
+ ` jitterBuffer: ${avgJitterBufferDelay}ms`;
|
|
79
|
-
|
|
80
|
-
if (isNetworkIssue) {
|
|
81
|
-
issues.push({
|
|
82
|
-
type: IssueType.Network,
|
|
83
|
-
reason: IssueReason.InboundNetworkQuality,
|
|
84
|
-
iceCandidate: data.connection.local.id,
|
|
85
|
-
debug,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (isServerIssue) {
|
|
90
|
-
issues.push({
|
|
91
|
-
type: IssueType.Server,
|
|
92
|
-
reason: IssueReason.ServerIssue,
|
|
93
|
-
iceCandidate: data.connection.remote.id,
|
|
94
|
-
debug,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (isNetworkMediaLatencyIssue) {
|
|
99
|
-
issues.push({
|
|
100
|
-
type: IssueType.Network,
|
|
101
|
-
reason: IssueReason.InboundNetworkMediaLatency,
|
|
102
|
-
iceCandidate: data.connection.local.id,
|
|
103
|
-
debug,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (isNetworkMediaSyncIssue) {
|
|
108
|
-
issues.push({
|
|
109
|
-
type: IssueType.Network,
|
|
110
|
-
reason: IssueReason.NetworkMediaSyncFailure,
|
|
111
|
-
iceCandidate: data.connection.local.id,
|
|
112
|
-
debug,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return issues;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export default InboundNetworkIssueDetector;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IssueDetector,
|
|
3
|
-
IssueDetectorResult,
|
|
4
|
-
IssueReason,
|
|
5
|
-
IssueType,
|
|
6
|
-
WebRTCStatsParsed,
|
|
7
|
-
} from '../types';
|
|
8
|
-
|
|
9
|
-
class NetworkMediaSyncIssueDetector implements IssueDetector {
|
|
10
|
-
#lastProcessedStats: { [connectionId: string]: WebRTCStatsParsed } = {};
|
|
11
|
-
|
|
12
|
-
detect(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
13
|
-
const issues = this.processData(data);
|
|
14
|
-
this.#lastProcessedStats[data.connection.id] = data;
|
|
15
|
-
return issues;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
private processData(data: WebRTCStatsParsed): 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;
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IssueDetector,
|
|
3
|
-
IssueDetectorResult,
|
|
4
|
-
IssueReason,
|
|
5
|
-
IssueType,
|
|
6
|
-
WebRTCStatsParsed,
|
|
7
|
-
} from '../types';
|
|
8
|
-
|
|
9
|
-
class OutboundNetworkIssueDetector implements IssueDetector {
|
|
10
|
-
#lastProcessedStats: { [connectionId: string]: WebRTCStatsParsed } = {};
|
|
11
|
-
|
|
12
|
-
detect(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
13
|
-
const issues = this.processData(data);
|
|
14
|
-
this.#lastProcessedStats[data.connection.id] = data;
|
|
15
|
-
return issues;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
private processData(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
19
|
-
const issues: IssueDetectorResult = [];
|
|
20
|
-
const remoteInboundRTPStreamsStats = [
|
|
21
|
-
...data.remote?.audio.inbound || [],
|
|
22
|
-
...data.remote?.video.inbound || [],
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
if (!remoteInboundRTPStreamsStats.length) {
|
|
26
|
-
return issues;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const previousStats = this.#lastProcessedStats[data.connection.id];
|
|
30
|
-
if (!previousStats) {
|
|
31
|
-
return issues;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const previousRemoteInboundRTPStreamsStats = [
|
|
35
|
-
...previousStats.remote?.audio.inbound || [],
|
|
36
|
-
...previousStats.remote?.video.inbound || [],
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
const { packetsSent } = data.connection;
|
|
40
|
-
const lastPacketsSent = previousStats.connection.packetsSent;
|
|
41
|
-
|
|
42
|
-
const rtpNetworkStats = remoteInboundRTPStreamsStats.reduce((stats, currentStreamStats) => {
|
|
43
|
-
const previousStreamStats = previousRemoteInboundRTPStreamsStats
|
|
44
|
-
.find((stream) => stream.ssrc === currentStreamStats.ssrc);
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
sumJitter: stats.sumJitter + currentStreamStats.jitter,
|
|
48
|
-
packetsLost: stats.packetsLost + currentStreamStats.packetsLost,
|
|
49
|
-
lastPacketsLost: stats.lastPacketsLost + (previousStreamStats?.packetsLost || 0),
|
|
50
|
-
};
|
|
51
|
-
}, {
|
|
52
|
-
sumJitter: 0,
|
|
53
|
-
packetsLost: 0,
|
|
54
|
-
lastPacketsLost: 0,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const rtt = (1e3 * data.connection.currentRoundTripTime) || 0;
|
|
58
|
-
const { sumJitter } = rtpNetworkStats;
|
|
59
|
-
const avgJitter = sumJitter / remoteInboundRTPStreamsStats.length;
|
|
60
|
-
|
|
61
|
-
const deltaPacketSent = packetsSent - lastPacketsSent;
|
|
62
|
-
const deltaPacketLost = rtpNetworkStats.packetsLost - rtpNetworkStats.lastPacketsLost;
|
|
63
|
-
|
|
64
|
-
const packetsLoss = deltaPacketSent && deltaPacketLost
|
|
65
|
-
? Math.round((deltaPacketLost * 100) / (deltaPacketSent + deltaPacketLost))
|
|
66
|
-
: 0;
|
|
67
|
-
|
|
68
|
-
const isHighPacketsLoss = packetsLoss > 5;
|
|
69
|
-
const isHighJitter = avgJitter >= 200;
|
|
70
|
-
const isNetworkMediaLatencyIssue = isHighPacketsLoss && isHighJitter;
|
|
71
|
-
const isNetworkIssue = (!isHighPacketsLoss && isHighJitter) || isHighJitter || isHighPacketsLoss;
|
|
72
|
-
const debug = `packetLoss: ${packetsLoss}%, jitter: ${avgJitter}, rtt: ${rtt}`;
|
|
73
|
-
|
|
74
|
-
if (isNetworkMediaLatencyIssue) {
|
|
75
|
-
issues.push({
|
|
76
|
-
type: IssueType.Network,
|
|
77
|
-
reason: IssueReason.OutboundNetworkMediaLatency,
|
|
78
|
-
iceCandidate: data.connection.local.id,
|
|
79
|
-
debug,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (isNetworkIssue) {
|
|
84
|
-
issues.push({
|
|
85
|
-
type: IssueType.Network,
|
|
86
|
-
reason: IssueReason.OutboundNetworkQuality,
|
|
87
|
-
iceCandidate: data.connection.local.id,
|
|
88
|
-
debug,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return issues;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export default OutboundNetworkIssueDetector;
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IssueDetector,
|
|
3
|
-
IssueDetectorResult,
|
|
4
|
-
IssueReason,
|
|
5
|
-
IssueType,
|
|
6
|
-
WebRTCStatsParsed,
|
|
7
|
-
} from '../types';
|
|
8
|
-
|
|
9
|
-
class QualityLimitationsIssueDetector implements IssueDetector {
|
|
10
|
-
#lastProcessedStats: { [connectionId: string]: WebRTCStatsParsed } = {};
|
|
11
|
-
|
|
12
|
-
detect(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
13
|
-
const issues = this.processData(data);
|
|
14
|
-
this.#lastProcessedStats[data.connection.id] = data;
|
|
15
|
-
return issues;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
private processData(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
19
|
-
const streamsWithLimitation = data.video.outbound.filter((stats) => stats.qualityLimitationReason !== 'none');
|
|
20
|
-
const issues: IssueDetectorResult = [];
|
|
21
|
-
const previousOutboundRTPVideoStreamsStats = this.#lastProcessedStats[data.connection.id]?.video.outbound;
|
|
22
|
-
|
|
23
|
-
if (!previousOutboundRTPVideoStreamsStats) {
|
|
24
|
-
return issues;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
streamsWithLimitation.forEach((streamStats) => {
|
|
28
|
-
const previousStreamStats = previousOutboundRTPVideoStreamsStats.find((item) => item.ssrc === streamStats.ssrc);
|
|
29
|
-
|
|
30
|
-
if (!previousStreamStats) {
|
|
31
|
-
// can not determine current status of the stream
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (streamStats.framesSent > previousStreamStats.framesSent) {
|
|
36
|
-
// stream is still sending
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (streamStats.qualityLimitationReason === 'cpu') {
|
|
41
|
-
issues.push({
|
|
42
|
-
type: IssueType.CPU,
|
|
43
|
-
reason: IssueReason.EncoderCPUThrottling,
|
|
44
|
-
ssrc: streamStats.ssrc,
|
|
45
|
-
debug: 'qualityLimitationReason: cpu',
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (streamStats.qualityLimitationReason === 'bandwidth') {
|
|
50
|
-
issues.push({
|
|
51
|
-
type: IssueType.Network,
|
|
52
|
-
reason: IssueReason.OutboundNetworkThroughput,
|
|
53
|
-
ssrc: streamStats.ssrc,
|
|
54
|
-
debug: 'qualityLimitationReason: bandwidth',
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
return issues;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export default QualityLimitationsIssueDetector;
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
IssueDetector,
|
|
3
|
-
IssueDetectorResult,
|
|
4
|
-
IssueReason,
|
|
5
|
-
IssueType,
|
|
6
|
-
WebRTCStatsParsed,
|
|
7
|
-
} from '../types';
|
|
8
|
-
|
|
9
|
-
class VideoCodecMismatchDetector implements IssueDetector {
|
|
10
|
-
readonly UNKNOWN_DECODER = 'unknown';
|
|
11
|
-
|
|
12
|
-
#lastProcessedStats: { [connectionId: string]: WebRTCStatsParsed | undefined } = {};
|
|
13
|
-
|
|
14
|
-
#lastDecoderWithIssue: {
|
|
15
|
-
[connectionId: string]: { [ssrc: string]: string | undefined } | undefined;
|
|
16
|
-
} = {};
|
|
17
|
-
|
|
18
|
-
detect(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
19
|
-
const issues = this.processData(data);
|
|
20
|
-
this.#lastProcessedStats[data.connection.id] = data;
|
|
21
|
-
return issues;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private processData(data: WebRTCStatsParsed): IssueDetectorResult {
|
|
25
|
-
const issues: IssueDetectorResult = [];
|
|
26
|
-
const { id: connectionId } = data.connection;
|
|
27
|
-
const previousInboundRTPVideoStreamsStats = this.#lastProcessedStats[connectionId]?.video.inbound;
|
|
28
|
-
|
|
29
|
-
data.video.inbound.forEach((streamStats) => {
|
|
30
|
-
const { decoderImplementation: currentDecoder, ssrc } = streamStats;
|
|
31
|
-
const prevStats = previousInboundRTPVideoStreamsStats?.find((item) => item.ssrc === ssrc);
|
|
32
|
-
|
|
33
|
-
// skipping the first iteration on purpose
|
|
34
|
-
if (!prevStats) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
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);
|
|
45
|
-
|
|
46
|
-
issues.push({
|
|
47
|
-
ssrc,
|
|
48
|
-
type: IssueType.Stream,
|
|
49
|
-
reason: IssueReason.VideoCodecMismatchIssue,
|
|
50
|
-
trackIdentifier: streamStats.track.trackIdentifier,
|
|
51
|
-
debug: `mimeType: ${streamStats.mimeType}, decoderImplementation: ${currentDecoder}`,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return issues;
|
|
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
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export default VideoCodecMismatchDetector;
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/* eslint-disable class-methods-use-this */
|
|
2
|
-
import {
|
|
3
|
-
NetworkScore,
|
|
4
|
-
NetworkScores,
|
|
5
|
-
NetworkScoresCalculator as INetworkScoresCalculator,
|
|
6
|
-
WebRTCStatsParsed,
|
|
7
|
-
} from '../types';
|
|
8
|
-
|
|
9
|
-
class NetworkScoresCalculator implements INetworkScoresCalculator {
|
|
10
|
-
#lastProcessedStats: { [connectionId: string]: WebRTCStatsParsed } = {};
|
|
11
|
-
|
|
12
|
-
calculate(data: WebRTCStatsParsed): NetworkScores {
|
|
13
|
-
const outbound = this.calcucateOutboundScore(data);
|
|
14
|
-
const inbound = this.calculateInboundScore(data);
|
|
15
|
-
this.#lastProcessedStats[data.connection.id] = data;
|
|
16
|
-
return { outbound, inbound };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
private calcucateOutboundScore(data: WebRTCStatsParsed): NetworkScore | undefined {
|
|
20
|
-
const remoteInboundRTPStreamsStats = [
|
|
21
|
-
...data.remote?.audio.inbound || [],
|
|
22
|
-
...data.remote?.video.inbound || [],
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
if (!remoteInboundRTPStreamsStats.length) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const previousStats = this.#lastProcessedStats[data.connection.id];
|
|
30
|
-
if (!previousStats) {
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const previousRemoteInboundRTPStreamsStats = [
|
|
35
|
-
...previousStats.remote?.audio.inbound || [],
|
|
36
|
-
...previousStats.remote?.video.inbound || [],
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
const { packetsSent } = data.connection;
|
|
40
|
-
const lastPacketsSent = previousStats.connection.packetsSent;
|
|
41
|
-
|
|
42
|
-
const rtpNetworkStats = remoteInboundRTPStreamsStats.reduce((stats, currentStreamStats) => {
|
|
43
|
-
const previousStreamStats = previousRemoteInboundRTPStreamsStats
|
|
44
|
-
.find((stream) => stream.ssrc === currentStreamStats.ssrc);
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
sumJitter: stats.sumJitter + currentStreamStats.jitter,
|
|
48
|
-
packetsLost: stats.packetsLost + currentStreamStats.packetsLost,
|
|
49
|
-
lastPacketsLost: stats.lastPacketsLost + (previousStreamStats?.packetsLost || 0),
|
|
50
|
-
};
|
|
51
|
-
}, {
|
|
52
|
-
sumJitter: 0,
|
|
53
|
-
packetsLost: 0,
|
|
54
|
-
lastPacketsLost: 0,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const rtt = (1e3 * data.connection.currentRoundTripTime) || 0;
|
|
58
|
-
const { sumJitter } = rtpNetworkStats;
|
|
59
|
-
const avgJitter = sumJitter / remoteInboundRTPStreamsStats.length;
|
|
60
|
-
|
|
61
|
-
const deltaPacketSent = packetsSent - lastPacketsSent;
|
|
62
|
-
const deltaPacketLost = rtpNetworkStats.packetsLost - rtpNetworkStats.lastPacketsLost;
|
|
63
|
-
|
|
64
|
-
const packetsLoss = deltaPacketSent && deltaPacketLost
|
|
65
|
-
? Math.round((deltaPacketLost * 100) / (deltaPacketSent + deltaPacketLost))
|
|
66
|
-
: 0;
|
|
67
|
-
|
|
68
|
-
return this.calculateMOS({ avgJitter, rtt, packetsLoss });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private calculateInboundScore(data: WebRTCStatsParsed): NetworkScore | undefined {
|
|
72
|
-
const inboundRTPStreamsStats = [...data.audio?.inbound, ...data.video?.inbound];
|
|
73
|
-
if (!inboundRTPStreamsStats.length) {
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const previousStats = this.#lastProcessedStats[data.connection.id];
|
|
78
|
-
if (!previousStats) {
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const previousInboundStreamStats = [...previousStats.video?.inbound, ...previousStats.audio?.inbound];
|
|
83
|
-
const { packetsReceived } = data.connection;
|
|
84
|
-
const lastPacketsReceived = previousStats.connection.packetsReceived;
|
|
85
|
-
|
|
86
|
-
const rtpNetworkStats = inboundRTPStreamsStats.reduce((stats, currentStreamStats) => {
|
|
87
|
-
const previousStreamStats = previousInboundStreamStats.find((stream) => stream.ssrc === currentStreamStats.ssrc);
|
|
88
|
-
return {
|
|
89
|
-
sumJitter: stats.sumJitter + currentStreamStats.jitter,
|
|
90
|
-
packetsLost: stats.packetsLost + currentStreamStats.packetsLost,
|
|
91
|
-
lastPacketsLost: stats.lastPacketsLost + (previousStreamStats?.packetsLost || 0),
|
|
92
|
-
};
|
|
93
|
-
}, {
|
|
94
|
-
sumJitter: 0,
|
|
95
|
-
packetsLost: 0,
|
|
96
|
-
lastPacketsLost: 0,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const rtt = (1e3 * data.connection.currentRoundTripTime) || 0;
|
|
100
|
-
const { sumJitter } = rtpNetworkStats;
|
|
101
|
-
const avgJitter = sumJitter / inboundRTPStreamsStats.length;
|
|
102
|
-
|
|
103
|
-
const deltaPacketReceived = packetsReceived - lastPacketsReceived;
|
|
104
|
-
const deltaPacketLost = rtpNetworkStats.packetsLost - rtpNetworkStats.lastPacketsLost;
|
|
105
|
-
|
|
106
|
-
const packetsLoss = deltaPacketReceived && deltaPacketLost
|
|
107
|
-
? Math.round((deltaPacketLost * 100) / (deltaPacketReceived + deltaPacketLost))
|
|
108
|
-
: 0;
|
|
109
|
-
|
|
110
|
-
return this.calculateMOS({ avgJitter, rtt, packetsLoss });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private calculateMOS(
|
|
114
|
-
{ avgJitter, rtt, packetsLoss }:
|
|
115
|
-
{ avgJitter: number, rtt: number, packetsLoss: number },
|
|
116
|
-
): number {
|
|
117
|
-
const effectiveLatency = rtt + (avgJitter * 2) + 10;
|
|
118
|
-
let rFactor = effectiveLatency < 160
|
|
119
|
-
? 93.2 - (effectiveLatency / 40)
|
|
120
|
-
: 93.2 - (effectiveLatency / 120) - 10;
|
|
121
|
-
rFactor -= (packetsLoss * 2.5);
|
|
122
|
-
return 1 + (0.035) * rFactor + (0.000007) * rFactor * (rFactor - 60) * (100 - rFactor);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export default NetworkScoresCalculator;
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import { CompositeStatsParser, StatsReportItem } from '../types';
|
|
3
|
-
|
|
4
|
-
interface PeriodicWebRTCStatsReporterParams {
|
|
5
|
-
compositeStatsParser: CompositeStatsParser;
|
|
6
|
-
getStatsInterval?: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
class PeriodicWebRTCStatsReporter extends EventEmitter {
|
|
10
|
-
static readonly STATS_REPORT_READY_EVENT = 'stats-report-ready';
|
|
11
|
-
|
|
12
|
-
private isStopped = false;
|
|
13
|
-
|
|
14
|
-
private reportTimer: NodeJS.Timer | undefined;
|
|
15
|
-
|
|
16
|
-
private readonly getStatsInterval: number;
|
|
17
|
-
|
|
18
|
-
private readonly compositeStatsParser: CompositeStatsParser;
|
|
19
|
-
|
|
20
|
-
constructor(params: PeriodicWebRTCStatsReporterParams) {
|
|
21
|
-
super();
|
|
22
|
-
this.compositeStatsParser = params.compositeStatsParser;
|
|
23
|
-
this.getStatsInterval = params.getStatsInterval ?? 10000;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
get isRunning(): boolean {
|
|
27
|
-
return !!this.reportTimer && !this.isStopped;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
startReporting(): void {
|
|
31
|
-
if (this.reportTimer) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const doExtract = () => setTimeout(() => {
|
|
36
|
-
if (this.isStopped) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
this.parseReports()
|
|
41
|
-
.finally(() => doExtract());
|
|
42
|
-
}, this.getStatsInterval);
|
|
43
|
-
|
|
44
|
-
this.isStopped = false;
|
|
45
|
-
this.reportTimer = doExtract();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
stopReporting(): void {
|
|
49
|
-
this.isStopped = true;
|
|
50
|
-
|
|
51
|
-
if (this.reportTimer) {
|
|
52
|
-
clearTimeout(this.reportTimer);
|
|
53
|
-
this.reportTimer = undefined;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private async parseReports() {
|
|
58
|
-
const reportItems = await this.compositeStatsParser.parse();
|
|
59
|
-
|
|
60
|
-
reportItems.forEach((item: StatsReportItem) => {
|
|
61
|
-
this.emit(PeriodicWebRTCStatsReporter.STATS_REPORT_READY_EVENT, item);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export default PeriodicWebRTCStatsReporter;
|