@livedigital/client 2.15.0 → 2.17.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/constants/events.d.ts +2 -0
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/media/tracks/AudioTrack.d.ts +2 -1
- package/dist/engine/wid/detectors/InboundNetworkIssueDetector.d.ts +8 -0
- package/dist/engine/wid/detectors/OutboundNetworkIssueDetector.d.ts +8 -0
- package/dist/engine/wid/detectors/{NetworkIssueDetector.d.ts → VideoCodecMismatchDetector.d.ts} +2 -2
- package/dist/engine/wid/types.d.ts +11 -4
- package/dist/index.es.js +1 -1
- package/dist/index.js +2 -2
- package/dist/types/common.d.ts +8 -0
- package/package.json +1 -1
- package/src/constants/events.ts +2 -0
- package/src/engine/handlers/MediaSoupEventHandler.ts +28 -2
- package/src/engine/index.ts +5 -5
- package/src/engine/media/index.ts +4 -9
- package/src/engine/media/tracks/AudioTrack.ts +10 -1
- package/src/engine/media/tracks/VideoTrack.ts +11 -2
- package/src/engine/network/Socket.ts +5 -1
- package/src/engine/network/index.ts +2 -4
- package/src/engine/wid/WebRTCIssueDetector.ts +7 -2
- package/src/engine/wid/detectors/{NetworkIssueDetector.ts → InboundNetworkIssueDetector.ts} +6 -5
- package/src/engine/wid/detectors/OutboundNetworkIssueDetector.ts +115 -0
- package/src/engine/wid/detectors/VideoCodecMismatchDetector.ts +39 -0
- package/src/engine/wid/types.ts +10 -3
- package/src/types/common.ts +10 -0
package/dist/types/common.d.ts
CHANGED
|
@@ -113,12 +113,14 @@ export declare enum ConnectionQuality {
|
|
|
113
113
|
}
|
|
114
114
|
export declare type EncoderConfig = {};
|
|
115
115
|
export declare type VideoCodec = 'h264' | 'vp8';
|
|
116
|
+
export declare type AudioCodec = 'opus';
|
|
116
117
|
export declare type VideoEncoderConfig = EncoderConfig & {
|
|
117
118
|
preferredCodec?: VideoCodec;
|
|
118
119
|
encodings?: RtpEncodingParameters[];
|
|
119
120
|
videoGoogleStartBitrate?: number;
|
|
120
121
|
};
|
|
121
122
|
export declare type AudioEncoderConfig = EncoderConfig & {
|
|
123
|
+
preferredCodec?: AudioCodec;
|
|
122
124
|
enableFec?: boolean;
|
|
123
125
|
};
|
|
124
126
|
export declare type CreateTrackOptions = {
|
|
@@ -204,3 +206,9 @@ export declare type RemoteConsumerOptions = ConsumerOptions & {
|
|
|
204
206
|
};
|
|
205
207
|
export declare type LogMessageHandler = (msg: any, ...meta: any) => void;
|
|
206
208
|
export declare type LogLevel = 3 | 4 | 6 | 7;
|
|
209
|
+
export declare type TransportConnectionTimeoutPayload = {
|
|
210
|
+
reason: 'ice' | 'dtls';
|
|
211
|
+
transportId: string;
|
|
212
|
+
direction: 'receive' | 'send';
|
|
213
|
+
timeout: number;
|
|
214
|
+
};
|
package/package.json
CHANGED
package/src/constants/events.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const CLIENT_EVENTS = {
|
|
|
11
11
|
peerLeft: 'peer-left',
|
|
12
12
|
channelRejoinRequired: 'channel-rejoin-required',
|
|
13
13
|
devicesListUpdated: 'devices-list-updated',
|
|
14
|
+
transportConnectionTimeout: 'transport-connection-timeout',
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export const PEER_EVENTS = {
|
|
@@ -51,6 +52,7 @@ export const MEDIASOUP_EVENTS = {
|
|
|
51
52
|
transportClose: 'transport.close',
|
|
52
53
|
transportConnect: 'transport.connect',
|
|
53
54
|
transportProduce: 'transport.produce',
|
|
55
|
+
transportConnectionTimeout: 'transport.connectionTimeout',
|
|
54
56
|
transportStateChange: 'connectionstatechange',
|
|
55
57
|
transportGetIceParameters: 'getIceParameters',
|
|
56
58
|
};
|
|
@@ -4,10 +4,12 @@ import {
|
|
|
4
4
|
ConsumerScoreChangedPayload,
|
|
5
5
|
ProducerData,
|
|
6
6
|
ProducerRequestMaxSpatialLayer,
|
|
7
|
-
ProducerScoreChangedPayload,
|
|
7
|
+
ProducerScoreChangedPayload,
|
|
8
|
+
ProducerSetMaxSpatialLayer,
|
|
9
|
+
TransportConnectionTimeoutPayload,
|
|
8
10
|
} from '../../types/common';
|
|
9
11
|
import Engine from '../index';
|
|
10
|
-
import { MEDIASOUP_EVENTS } from '../../constants/events';
|
|
12
|
+
import { CLIENT_EVENTS, MEDIASOUP_EVENTS } from '../../constants/events';
|
|
11
13
|
import Logger from '../Logger';
|
|
12
14
|
import VideoTrack from '../media/tracks/VideoTrack';
|
|
13
15
|
|
|
@@ -138,6 +140,30 @@ class MediaSoupEventHandler {
|
|
|
138
140
|
|
|
139
141
|
peer.observer.safeEmit(MEDIASOUP_EVENTS.producerSetMaxSpatialLayer, { peerId, producerId, spatialLayer });
|
|
140
142
|
});
|
|
143
|
+
|
|
144
|
+
connection.on(MEDIASOUP_EVENTS.transportConnectionTimeout, async ({
|
|
145
|
+
reason,
|
|
146
|
+
transportId,
|
|
147
|
+
direction,
|
|
148
|
+
timeout,
|
|
149
|
+
}: TransportConnectionTimeoutPayload) => {
|
|
150
|
+
const transport = [
|
|
151
|
+
this.engine.network.receiveTransport,
|
|
152
|
+
this.engine.network.sendTransport,
|
|
153
|
+
].find((item) => item?.id === transportId);
|
|
154
|
+
|
|
155
|
+
if (!transport || transport.connectionState === 'connected') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.logger.warn(MEDIASOUP_EVENTS.transportConnectionTimeout, {
|
|
160
|
+
reason,
|
|
161
|
+
transportId,
|
|
162
|
+
direction,
|
|
163
|
+
timeout,
|
|
164
|
+
});
|
|
165
|
+
this.engine.clientEventEmitter.emit(CLIENT_EVENTS.transportConnectionTimeout, { direction, reason });
|
|
166
|
+
});
|
|
141
167
|
}
|
|
142
168
|
|
|
143
169
|
private async handleProducerSetMaxSpatialLayer({
|
package/src/engine/index.ts
CHANGED
|
@@ -69,7 +69,7 @@ class Engine {
|
|
|
69
69
|
|
|
70
70
|
private channel?: string;
|
|
71
71
|
|
|
72
|
-
private
|
|
72
|
+
private isChannelJoining = false;
|
|
73
73
|
|
|
74
74
|
private readonly logger: Logger;
|
|
75
75
|
|
|
@@ -187,14 +187,14 @@ class Engine {
|
|
|
187
187
|
public async join(params: JoinChannelParams): Promise<void> {
|
|
188
188
|
try {
|
|
189
189
|
this.logger.debug('join()', { params });
|
|
190
|
-
this.
|
|
190
|
+
this.isChannelJoining = true;
|
|
191
191
|
await this.connectToSocketServerWithRetry(params);
|
|
192
192
|
await this.performJoin(params);
|
|
193
193
|
} catch (error) {
|
|
194
194
|
this.logger.error('join()', { error });
|
|
195
195
|
throw error;
|
|
196
196
|
} finally {
|
|
197
|
-
this.
|
|
197
|
+
this.isChannelJoining = false;
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -512,13 +512,13 @@ class Engine {
|
|
|
512
512
|
const stopListening = () => this.network.socket.observer.removeListener('state', onSocketStateChange);
|
|
513
513
|
const isStateNotExpected = [SocketIOEvents.Disconnected, SocketIOEvents.Reconnecting].includes(state);
|
|
514
514
|
|
|
515
|
-
if (error || (this.
|
|
515
|
+
if (error || (this.isChannelJoining && isStateNotExpected)) {
|
|
516
516
|
stopListening();
|
|
517
517
|
reject(error || 'Not expected socket state for new connection');
|
|
518
518
|
return;
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
-
if (this.
|
|
521
|
+
if (this.isChannelJoining && [SocketIOEvents.Connected, SocketIOEvents.Reconnected].includes(state)) {
|
|
522
522
|
stopListening();
|
|
523
523
|
resolve();
|
|
524
524
|
}
|
|
@@ -45,18 +45,13 @@ class Media {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
getTrackCodec(track: Track): RtpCodecCapability | undefined {
|
|
48
|
-
|
|
48
|
+
const { rtpCapabilities, rtpCapabilities: { codecs } } = this.mediasoupDevice;
|
|
49
|
+
if (!codecs) {
|
|
50
|
+
this.#logger.error('getTrackCodec()', { track, rtpCapabilities });
|
|
49
51
|
return undefined;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return this.mediasoupDevice
|
|
57
|
-
.rtpCapabilities
|
|
58
|
-
.codecs
|
|
59
|
-
.find((c) => c.mimeType.toLowerCase() === `video/${track.getPreferredCodec()}`);
|
|
54
|
+
return codecs.find((c) => c.mimeType.toLowerCase() === `${track.kind}/${track.getPreferredCodec()}`);
|
|
60
55
|
}
|
|
61
56
|
|
|
62
57
|
private createTracks(stream: MediaStream): Track[] {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProducerCodecOptions } from 'mediasoup-client/lib/Producer';
|
|
2
|
-
import { AudioEncoderConfig } from '../../../types/common';
|
|
2
|
+
import { AudioCodec, AudioEncoderConfig } from '../../../types/common';
|
|
3
3
|
import BaseTrack from './BaseTrack';
|
|
4
4
|
import TrackWithCodecOptions from './TrackWithCodecOptions';
|
|
5
5
|
|
|
@@ -13,6 +13,15 @@ class AudioTrack extends BaseTrack implements TrackWithCodecOptions {
|
|
|
13
13
|
opusFec: this.getEncoderConfig().enableFec || true,
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
getPreferredCodec(): AudioCodec {
|
|
18
|
+
const { preferredCodec } = this.getEncoderConfig();
|
|
19
|
+
if (preferredCodec) {
|
|
20
|
+
return preferredCodec;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return 'opus';
|
|
24
|
+
}
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export default AudioTrack;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ProducerCodecOptions } from 'mediasoup-client/lib/Producer';
|
|
2
2
|
import { RtpEncodingParameters } from 'mediasoup-client/lib/RtpParameters';
|
|
3
3
|
import { WEBCAM_SIMULCAST_ENCODINGS } from '../../../constants/simulcastEncodings';
|
|
4
|
-
import { VideoCodec, VideoEncoderConfig } from '../../../types/common';
|
|
4
|
+
import { TrackLabel, VideoCodec, VideoEncoderConfig } from '../../../types/common';
|
|
5
5
|
import BaseTrack from './BaseTrack';
|
|
6
6
|
import TrackWithCodecOptions from './TrackWithCodecOptions';
|
|
7
7
|
import TrackWithEncodings from './TrackWithEncodings';
|
|
@@ -20,7 +20,16 @@ class VideoTrack extends BaseTrack implements TrackWithCodecOptions, TrackWithEn
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
getPreferredCodec(): VideoCodec {
|
|
23
|
-
|
|
23
|
+
const { preferredCodec } = this.getEncoderConfig();
|
|
24
|
+
if (preferredCodec) {
|
|
25
|
+
return preferredCodec;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (this.getLabel() === TrackLabel.ScreenVideo) {
|
|
29
|
+
return 'vp8';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return 'h264';
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
getEncodings(): RtpEncodingParameters[] {
|
|
@@ -49,7 +49,11 @@ class SocketIO {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
connect(serverUrl: string): void {
|
|
52
|
-
const connection = io(serverUrl
|
|
52
|
+
const connection = io(serverUrl, {
|
|
53
|
+
transports: ['polling', 'websocket'],
|
|
54
|
+
upgrade: true,
|
|
55
|
+
rememberUpgrade: false,
|
|
56
|
+
});
|
|
53
57
|
|
|
54
58
|
this.connection = connection;
|
|
55
59
|
this.serverUrl = serverUrl;
|
|
@@ -92,7 +92,7 @@ class Network {
|
|
|
92
92
|
|
|
93
93
|
const sendTransportOptions = await this.socket.request(MEDIASOUP_EVENTS.transportCreate, {
|
|
94
94
|
sctpCapabilities: mediasoupDevice.sctpCapabilities,
|
|
95
|
-
localDirection: '
|
|
95
|
+
localDirection: 'send',
|
|
96
96
|
}) as TransportOptions;
|
|
97
97
|
|
|
98
98
|
this.sendTransport = mediasoupDevice.createSendTransport(sendTransportOptions);
|
|
@@ -118,7 +118,6 @@ class Network {
|
|
|
118
118
|
}
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
122
121
|
this.sendTransport.on(MEDIASOUP_TRANSPORT_EVENTS.produce, async (
|
|
123
122
|
parameters: ProduceParams,
|
|
124
123
|
callback: (a: unknown) => void,
|
|
@@ -158,8 +157,7 @@ class Network {
|
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
const recvTransportOptions = await this.socket.request(MEDIASOUP_EVENTS.transportCreate, {
|
|
161
|
-
|
|
162
|
-
localDirection: 'in',
|
|
160
|
+
localDirection: 'receive',
|
|
163
161
|
}) as TransportOptions;
|
|
164
162
|
this.receiveTransport = mediasoupDevice.createRecvTransport(recvTransportOptions);
|
|
165
163
|
this.logger.debug('createRecvTransport()', { transport: this.receiveTransport });
|
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
import QualityLimitationsIssueDetector from './detectors/QualityLimitationsIssueDetector';
|
|
11
11
|
import FramesDroppedIssueDetector from './detectors/FramesDroppedIssueDetector';
|
|
12
12
|
import FramesEncodedSentIssueDetector from './detectors/FramesEncodedSentIssueDetector';
|
|
13
|
-
import
|
|
13
|
+
import InboundNetworkIssueDetector from './detectors/InboundNetworkIssueDetector';
|
|
14
|
+
import OutboundNetworkIssueDetector from './detectors/OutboundNetworkIssueDetector';
|
|
14
15
|
import NetworkMediaSyncIssueDetector from './detectors/NetworkMediaSyncIssueDetector';
|
|
15
16
|
import AvailableOutgoingBitrateIssueDetector from './detectors/AvailableOutgoingBitrateIssueDetector';
|
|
17
|
+
import VideoCodecMismatchDetector from './detectors/VideoCodecMismatchDetector';
|
|
16
18
|
|
|
17
19
|
class WebRTCIssueDetector {
|
|
18
20
|
private readonly webrtcStats: WebRTCStats;
|
|
@@ -35,6 +37,7 @@ class WebRTCIssueDetector {
|
|
|
35
37
|
// Move instantiation from the constructor
|
|
36
38
|
this.webrtcStats = new WebRTCStats({
|
|
37
39
|
getStatsInterval: this.getStatsInterval,
|
|
40
|
+
remote: true,
|
|
38
41
|
});
|
|
39
42
|
|
|
40
43
|
(window as unknown as WIDWindow).wid = this;
|
|
@@ -44,9 +47,11 @@ class WebRTCIssueDetector {
|
|
|
44
47
|
new QualityLimitationsIssueDetector(),
|
|
45
48
|
new FramesDroppedIssueDetector(),
|
|
46
49
|
new FramesEncodedSentIssueDetector(),
|
|
47
|
-
new
|
|
50
|
+
new InboundNetworkIssueDetector(),
|
|
51
|
+
new OutboundNetworkIssueDetector(),
|
|
48
52
|
new NetworkMediaSyncIssueDetector(),
|
|
49
53
|
new AvailableOutgoingBitrateIssueDetector(),
|
|
54
|
+
new VideoCodecMismatchDetector(),
|
|
50
55
|
];
|
|
51
56
|
|
|
52
57
|
this.webrtcStats.on('stats', (event) => {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
IssueType,
|
|
7
7
|
} from '../types';
|
|
8
8
|
|
|
9
|
-
class
|
|
9
|
+
class InboundNetworkIssueDetector implements IssueDetector {
|
|
10
10
|
#lastProcessedStats: { [connectionId: string]: WebRTCStatsEventData } = {};
|
|
11
11
|
|
|
12
12
|
detect(data: WebRTCStatsEventData): IssueDetectorResult {
|
|
@@ -89,16 +89,17 @@ class NetworkIssueDetector implements IssueDetector {
|
|
|
89
89
|
if (isPoorConnectionQuality) {
|
|
90
90
|
issues.push({
|
|
91
91
|
type: IssueType.Network,
|
|
92
|
-
reason: IssueReason.
|
|
92
|
+
reason: IssueReason.LowInboundMOS,
|
|
93
93
|
iceCandidate: data.connection.local.id,
|
|
94
94
|
debug,
|
|
95
|
+
data: mos,
|
|
95
96
|
});
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
if (isNetworkIssue) {
|
|
99
100
|
issues.push({
|
|
100
101
|
type: IssueType.Network,
|
|
101
|
-
reason: IssueReason.
|
|
102
|
+
reason: IssueReason.InboundNetworkQuality,
|
|
102
103
|
iceCandidate: data.connection.local.id,
|
|
103
104
|
debug,
|
|
104
105
|
});
|
|
@@ -116,7 +117,7 @@ class NetworkIssueDetector implements IssueDetector {
|
|
|
116
117
|
if (isNetworkMediaLatencyIssue) {
|
|
117
118
|
issues.push({
|
|
118
119
|
type: IssueType.Network,
|
|
119
|
-
reason: IssueReason.
|
|
120
|
+
reason: IssueReason.InboundNetworkMediaLatency,
|
|
120
121
|
iceCandidate: data.connection.local.id,
|
|
121
122
|
debug,
|
|
122
123
|
});
|
|
@@ -135,4 +136,4 @@ class NetworkIssueDetector implements IssueDetector {
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
export default
|
|
139
|
+
export default InboundNetworkIssueDetector;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { WebRTCStatsEventData } from '@peermetrics/webrtc-stats';
|
|
2
|
+
import {
|
|
3
|
+
IssueDetector,
|
|
4
|
+
IssueDetectorResult,
|
|
5
|
+
IssueReason,
|
|
6
|
+
IssueType,
|
|
7
|
+
} from '../types';
|
|
8
|
+
|
|
9
|
+
class OutboundNetworkIssueDetector 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 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 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
|
+
const isHighPacketsLoss = packetsLoss > 5;
|
|
76
|
+
const isHighJitter = avgJitter >= 200;
|
|
77
|
+
const isPoorConnectionQuality = mos < 3.5;
|
|
78
|
+
const isNetworkMediaLatencyIssue = isHighPacketsLoss && isHighJitter;
|
|
79
|
+
const isNetworkIssue = (!isHighPacketsLoss && isHighJitter) || isHighJitter || isHighPacketsLoss;
|
|
80
|
+
const debug = `packetLoss: ${packetsLoss}%, jitter: ${avgJitter}, rtt: ${rtt},`
|
|
81
|
+
+ ` MOS: ${mos}`;
|
|
82
|
+
|
|
83
|
+
if (isNetworkMediaLatencyIssue) {
|
|
84
|
+
issues.push({
|
|
85
|
+
type: IssueType.Network,
|
|
86
|
+
reason: IssueReason.OutboundNetworkMediaLatency,
|
|
87
|
+
iceCandidate: data.connection.local.id,
|
|
88
|
+
debug,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (isNetworkIssue) {
|
|
93
|
+
issues.push({
|
|
94
|
+
type: IssueType.Network,
|
|
95
|
+
reason: IssueReason.OutboundNetworkQuality,
|
|
96
|
+
iceCandidate: data.connection.local.id,
|
|
97
|
+
debug,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
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
|
+
return issues;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default OutboundNetworkIssueDetector;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { WebRTCStatsEventData } from '@peermetrics/webrtc-stats';
|
|
2
|
+
import {
|
|
3
|
+
IssueDetector,
|
|
4
|
+
IssueDetectorResult,
|
|
5
|
+
IssueReason,
|
|
6
|
+
IssueType,
|
|
7
|
+
} from '../types';
|
|
8
|
+
|
|
9
|
+
class VideoCodecMismatchDetector 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 previousInboundRTPVideoStreamsStats = this.#lastProcessedStats[data.connection.id]?.video.inbound;
|
|
21
|
+
|
|
22
|
+
data.video.inbound.forEach((streamStats) => {
|
|
23
|
+
const previousStreamStats = previousInboundRTPVideoStreamsStats?.find((item) => item.ssrc === streamStats.ssrc);
|
|
24
|
+
if (streamStats.decoderImplementation === 'unknown' && previousStreamStats?.decoderImplementation !== 'unknown') {
|
|
25
|
+
issues.push({
|
|
26
|
+
type: IssueType.Stream,
|
|
27
|
+
reason: IssueReason.VideoCodecMismatchIssue,
|
|
28
|
+
ssrc: streamStats.ssrc,
|
|
29
|
+
trackIdentifier: streamStats.track.trackIdentifier,
|
|
30
|
+
debug: `mimeType: ${streamStats.mimeType}, decoderImplementation: ${streamStats.decoderImplementation}`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return issues;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default VideoCodecMismatchDetector;
|
package/src/engine/wid/types.ts
CHANGED
|
@@ -26,18 +26,23 @@ export enum IssueType {
|
|
|
26
26
|
Network = 'network',
|
|
27
27
|
CPU = 'cpu',
|
|
28
28
|
Server = 'server',
|
|
29
|
+
Stream = 'stream',
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export enum IssueReason {
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
OutboundNetworkQuality = 'outbound-network-quality',
|
|
34
|
+
InboundNetworkQuality = 'inbound-network-quality',
|
|
35
|
+
OutboundNetworkMediaLatency = 'outbound-network-media-latency',
|
|
36
|
+
InboundNetworkMediaLatency = 'inbound-network-media-latency',
|
|
34
37
|
NetworkMediaSyncFailure = 'network-media-sync-failure',
|
|
35
38
|
OutboundNetworkThroughput = 'outbound-network-throughput',
|
|
36
39
|
InboundNetworkThroughput = 'inbound-network-throughput',
|
|
37
40
|
EncoderCPUThrottling = 'encoder-cpu-throttling',
|
|
38
41
|
DecoderCPUThrottling = 'decoder-cpu-throttling',
|
|
39
42
|
ServerIssue = 'server-issue',
|
|
40
|
-
|
|
43
|
+
VideoCodecMismatchIssue = 'codec-mismatch',
|
|
44
|
+
LowInboundMOS = 'low-inbound-mean-opinion-score',
|
|
45
|
+
LowOutboundMOS = 'low-outbound-mean-opinion-score',
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
export type IssuePayload = {
|
|
@@ -45,5 +50,7 @@ export type IssuePayload = {
|
|
|
45
50
|
reason: IssueReason,
|
|
46
51
|
ssrc?: number,
|
|
47
52
|
iceCandidate?: string,
|
|
53
|
+
data?: number;
|
|
48
54
|
debug?: string,
|
|
55
|
+
trackIdentifier?: string,
|
|
49
56
|
};
|
package/src/types/common.ts
CHANGED
|
@@ -135,6 +135,8 @@ export type EncoderConfig = {};
|
|
|
135
135
|
|
|
136
136
|
export type VideoCodec = 'h264' | 'vp8';
|
|
137
137
|
|
|
138
|
+
export type AudioCodec = 'opus';
|
|
139
|
+
|
|
138
140
|
export type VideoEncoderConfig = EncoderConfig & {
|
|
139
141
|
preferredCodec?: VideoCodec,
|
|
140
142
|
encodings?: RtpEncodingParameters[],
|
|
@@ -142,6 +144,7 @@ export type VideoEncoderConfig = EncoderConfig & {
|
|
|
142
144
|
};
|
|
143
145
|
|
|
144
146
|
export type AudioEncoderConfig = EncoderConfig & {
|
|
147
|
+
preferredCodec?: AudioCodec,
|
|
145
148
|
enableFec?: boolean;
|
|
146
149
|
};
|
|
147
150
|
|
|
@@ -235,3 +238,10 @@ export type RemoteConsumerOptions = ConsumerOptions & {
|
|
|
235
238
|
export type LogMessageHandler = (msg: any, ...meta: any) => void;
|
|
236
239
|
|
|
237
240
|
export type LogLevel = 3 | 4 | 6 | 7;
|
|
241
|
+
|
|
242
|
+
export type TransportConnectionTimeoutPayload = {
|
|
243
|
+
reason: 'ice' | 'dtls',
|
|
244
|
+
transportId: string,
|
|
245
|
+
direction: 'receive' | 'send',
|
|
246
|
+
timeout: number,
|
|
247
|
+
};
|