Skip to content

Commit f4efadd

Browse files
committed
fix: added ipversion to metrics
1 parent 3b91ada commit f4efadd

File tree

6 files changed

+246
-3
lines changed

6 files changed

+246
-3
lines changed

packages/@webex/plugin-meetings/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"@webex/media-helpers": "workspace:*",
7676
"@webex/plugin-people": "workspace:*",
7777
"@webex/plugin-rooms": "workspace:*",
78+
"@webex/ts-sdp": "^1.8.1",
7879
"@webex/web-capabilities": "^1.4.0",
7980
"@webex/webex-core": "workspace:*",
8081
"ampersand-collection": "^2.0.2",

packages/@webex/plugin-meetings/src/media/properties.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
RemoteStream,
88
} from '@webex/media-helpers';
99

10+
import {parse} from '@webex/ts-sdp';
11+
import {ClientEvent} from '@webex/internal-plugin-metrics';
1012
import {MEETINGS, QUALITY_LEVELS} from '../constants';
1113
import LoggerProxy from '../common/logs/logger-proxy';
1214
import MediaConnectionAwaiter from './MediaConnectionAwaiter';
@@ -20,6 +22,8 @@ export type MediaDirection = {
2022
receiveShare: boolean;
2123
};
2224

25+
export type IPVersion = ClientEvent['payload']['ipVersion'];
26+
2327
/**
2428
* @class MediaProperties
2529
*/
@@ -212,6 +216,91 @@ export default class MediaProperties {
212216
};
213217
}
214218

219+
/**
220+
* Checks if the given IP address is IPv6
221+
* @param {string} ip address to check
222+
* @returns {boolean} true if the address is IPv6, false otherwise
223+
*/
224+
private isIPv6(ip: string): boolean {
225+
return ip.includes(':');
226+
}
227+
228+
/** Finds out if we connected using IPv4 or IPv6
229+
* @param {RTCPeerConnection} webrtcMediaConnection
230+
* @param {Array<any>} allStatsReports array of RTC stats reports
231+
* @returns {string} IPVersion
232+
*/
233+
private getConnectionIpVersion(
234+
webrtcMediaConnection: RTCPeerConnection,
235+
allStatsReports: any[]
236+
): IPVersion | undefined {
237+
const transports = allStatsReports.filter((report) => report.type === 'transport');
238+
239+
let selectedCandidatePair;
240+
241+
if (transports.length > 0 && transports[0].selectedCandidatePairId) {
242+
selectedCandidatePair = allStatsReports.find(
243+
(report) =>
244+
report.type === 'candidate-pair' && report.id === transports[0].selectedCandidatePairId
245+
);
246+
} else {
247+
// Firefox doesn't have selectedCandidatePairId, but has selected property on the candidate pair
248+
selectedCandidatePair = allStatsReports.find(
249+
(report) => report.type === 'candidate-pair' && report.selected
250+
);
251+
}
252+
253+
if (selectedCandidatePair) {
254+
const localCandidate = allStatsReports.find(
255+
(report) =>
256+
report.type === 'local-candidate' && report.id === selectedCandidatePair.localCandidateId
257+
);
258+
259+
if (localCandidate) {
260+
if (localCandidate.address) {
261+
return this.isIPv6(localCandidate.address) ? 'IPv6' : 'IPv4';
262+
}
263+
264+
try {
265+
// safari doesn't have address field on the candidate, so we have to use the port to look up the candidate in the SDP
266+
const localSdp = webrtcMediaConnection.localDescription.sdp;
267+
268+
const parsedSdp = parse(localSdp);
269+
270+
for (const mediaLine of parsedSdp.avMedia) {
271+
const matchingCandidate = mediaLine.iceInfo.candidates.find(
272+
(candidate) => candidate.port === localCandidate.port
273+
);
274+
if (matchingCandidate) {
275+
return this.isIPv6(matchingCandidate.connectionAddress) ? 'IPv6' : 'IPv4';
276+
}
277+
}
278+
279+
LoggerProxy.logger.warn(
280+
`Media:properties#getConnectionIpVersion --> failed to find local candidate in the SDP for port ${localCandidate.port}`
281+
);
282+
} catch (error) {
283+
LoggerProxy.logger.warn(
284+
`Media:properties#getConnectionIpVersion --> error while trying to find candidate in local SDP:`,
285+
error
286+
);
287+
288+
return undefined;
289+
}
290+
} else {
291+
LoggerProxy.logger.warn(
292+
`Media:properties#getConnectionIpVersion --> failed to find local candidate "${selectedCandidatePair.localCandidateId}" in getStats() results`
293+
);
294+
}
295+
} else {
296+
LoggerProxy.logger.warn(
297+
`Media:properties#getConnectionIpVersion --> failed to find selected candidate pair in getStats() results (transports.length=${transports.length}, selectedCandidatePairId=${transports[0]?.selectedCandidatePairId})`
298+
);
299+
}
300+
301+
return undefined;
302+
}
303+
215304
/**
216305
* Returns the type of a connection that has been established
217306
* It should be 'UDP' | 'TCP' | 'TURN-TLS' | 'TURN-TCP' | 'TURN-UDP' | 'unknown'
@@ -284,6 +373,7 @@ export default class MediaProperties {
284373
*/
285374
async getCurrentConnectionInfo(): Promise<{
286375
connectionType: string;
376+
ipVersion?: IPVersion;
287377
selectedCandidatePairChanges: number;
288378
numTransports: number;
289379
}> {
@@ -309,10 +399,15 @@ export default class MediaProperties {
309399
});
310400

311401
const connectionType = this.getConnectionType(allStatsReports);
402+
const rtcPeerconnection =
403+
this.webrtcMediaConnection.multistreamConnection?.pc.pc ||
404+
this.webrtcMediaConnection.mediaConnection?.pc;
405+
const ipVersion = this.getConnectionIpVersion(rtcPeerconnection, allStatsReports);
312406
const {selectedCandidatePairChanges, numTransports} = this.getTransportInfo(allStatsReports);
313407

314408
return {
315409
connectionType,
410+
ipVersion,
316411
selectedCandidatePairChanges,
317412
numTransports,
318413
};
@@ -323,6 +418,7 @@ export default class MediaProperties {
323418

324419
return {
325420
connectionType: 'unknown',
421+
ipVersion: undefined,
326422
selectedCandidatePairChanges: -1,
327423
numTransports: 0,
328424
};

packages/@webex/plugin-meetings/src/meeting/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7771,7 +7771,7 @@ export default class Meeting extends StatelessWebexPlugin {
77717771
await this.enqueueScreenShareFloorRequest();
77727772
}
77737773

7774-
const {connectionType, selectedCandidatePairChanges, numTransports} =
7774+
const {connectionType, ipVersion, selectedCandidatePairChanges, numTransports} =
77757775
await this.mediaProperties.getCurrentConnectionInfo();
77767776

77777777
const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
@@ -7782,6 +7782,7 @@ export default class Meeting extends StatelessWebexPlugin {
77827782
correlation_id: this.correlationId,
77837783
locus_id: this.locusUrl.split('/').pop(),
77847784
connectionType,
7785+
ipVersion,
77857786
selectedCandidatePairChanges,
77867787
numTransports,
77877788
isMultistream: this.isMultistream,
@@ -7794,6 +7795,9 @@ export default class Meeting extends StatelessWebexPlugin {
77947795
// @ts-ignore
77957796
this.webex.internal.newMetrics.submitClientEvent({
77967797
name: 'client.media-engine.ready',
7798+
payload: {
7799+
ipVersion,
7800+
},
77977801
options: {
77987802
meetingId: this.id,
77997803
},

packages/@webex/plugin-meetings/test/unit/spec/media/properties.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'jsdom-global/register';
22
import {assert} from '@webex/test-helper-chai';
33
import sinon from 'sinon';
44
import {ConnectionState} from '@webex/internal-media-core';
5+
import * as tsSdpModule from '@webex/ts-sdp';
56
import MediaProperties from '@webex/plugin-meetings/src/media/properties';
67
import {Defer} from '@webex/common';
78
import MediaConnectionAwaiter from '../../../../src/media/MediaConnectionAwaiter';
@@ -10,15 +11,21 @@ describe('MediaProperties', () => {
1011
let mediaProperties;
1112
let mockMC;
1213
let clock;
14+
let rtcPeerConnection;
1315

1416
beforeEach(() => {
1517
clock = sinon.useFakeTimers();
1618

19+
rtcPeerConnection = {
20+
localDescription: {sdp: ''},
21+
};
22+
1723
mockMC = {
1824
getStats: sinon.stub().resolves([]),
1925
on: sinon.stub(),
2026
off: sinon.stub(),
2127
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
28+
multistreamConnection: {pc: {pc: rtcPeerConnection}},
2229
};
2330

2431
mediaProperties = new MediaProperties();
@@ -81,6 +88,129 @@ describe('MediaProperties', () => {
8188
assert.equal(numTransports, 0);
8289
});
8390

91+
describe('ipVersion', () => {
92+
it('returns ipVersion=undefined if getStats() returns no candidate pairs', async () => {
93+
mockMC.getStats.resolves([{type: 'something', id: '1234'}]);
94+
const info = await mediaProperties.getCurrentConnectionInfo();
95+
assert.equal(info.ipVersion, undefined);
96+
});
97+
98+
it('returns ipVersion=undefined if getStats() returns no selected candidate pair', async () => {
99+
mockMC.getStats.resolves([{type: 'candidate-pair', id: '1234', selected: false}]);
100+
const info = await mediaProperties.getCurrentConnectionInfo();
101+
assert.equal(info.ipVersion, undefined);
102+
});
103+
104+
it('returns ipVersion="IPv4" if transport has selectedCandidatePairId and local candidate has IPv4 address', async () => {
105+
mockMC.getStats.resolves([
106+
{type: 'transport', id: 't1', selectedCandidatePairId: 'cp1'},
107+
{type: 'candidate-pair', id: 'cp1', localCandidateId: 'lc1'},
108+
{type: 'local-candidate', id: 'lc1', address: '192.168.1.1'},
109+
]);
110+
const info = await mediaProperties.getCurrentConnectionInfo();
111+
assert.equal(info.ipVersion, 'IPv4');
112+
});
113+
114+
it('returns ipVersion="IPv6" if transport has selectedCandidatePairId and local candidate has IPv6 address', async () => {
115+
mockMC.getStats.resolves([
116+
{type: 'transport', id: 't1', selectedCandidatePairId: 'cp1'},
117+
{type: 'candidate-pair', id: 'cp1', localCandidateId: 'lc1'},
118+
{type: 'local-candidate', id: 'lc1', address: 'fd8f:12e6:5e53:784f:a0ba:f8d5:b906:1acc'},
119+
]);
120+
const info = await mediaProperties.getCurrentConnectionInfo();
121+
assert.equal(info.ipVersion, 'IPv6');
122+
});
123+
124+
it('returns ipVersion="IPv4" if transport has no selectedCandidatePairId but finds selected candidate pair and local candidate has IPv4 address', async () => {
125+
mockMC.getStats.resolves([
126+
{type: 'transport', id: 't1'},
127+
{type: 'candidate-pair', id: 'cp2', localCandidateId: 'lc2', selected: true},
128+
{type: 'local-candidate', id: 'lc2', address: '10.0.0.1'},
129+
]);
130+
const info = await mediaProperties.getCurrentConnectionInfo();
131+
assert.equal(info.ipVersion, 'IPv4');
132+
});
133+
134+
it('returns ipVersion="IPv6" if transport has no selectedCandidatePairId but finds selected candidate pair and local candidate has IPv6 address', async () => {
135+
mockMC.getStats.resolves([
136+
{type: 'transport', id: 't1'},
137+
{type: 'candidate-pair', id: 'cp2', localCandidateId: 'lc2', selected: true},
138+
{type: 'local-candidate', id: 'lc2', address: 'fe80::1ff:fe23:4567:890a'},
139+
]);
140+
const info = await mediaProperties.getCurrentConnectionInfo();
141+
assert.equal(info.ipVersion, 'IPv6');
142+
});
143+
144+
describe('local candidate without address', () => {
145+
it('return="IPv4" if candidate from SDP with matching port number has IPv4 address', async () => {
146+
sinon.stub(tsSdpModule, 'parse').returns({
147+
avMedia: [
148+
{
149+
iceInfo: {
150+
candidates: [
151+
{
152+
port: 1234,
153+
connectionAddress: '192.168.0.1',
154+
},
155+
],
156+
},
157+
},
158+
],
159+
});
160+
161+
mockMC.getStats.resolves([
162+
{type: 'transport', id: 't1'},
163+
{type: 'candidate-pair', id: 'cp2', localCandidateId: 'lc2', selected: true},
164+
{type: 'local-candidate', id: 'lc2', port: 1234},
165+
]);
166+
const info = await mediaProperties.getCurrentConnectionInfo();
167+
assert.equal(info.ipVersion, 'IPv4');
168+
169+
assert.calledWith(tsSdpModule.parse, rtcPeerConnection.localDescription.sdp);
170+
});
171+
172+
it('returns ipVersion="IPv6" if candidate from SDP with matching port number has IPv6 address', async () => {
173+
sinon.stub(tsSdpModule, 'parse').returns({
174+
avMedia: [
175+
{
176+
iceInfo: {
177+
candidates: [
178+
{
179+
port: 5000,
180+
connectionAddress: 'fe80::1ff:fe23:4567:890a',
181+
},
182+
],
183+
},
184+
},
185+
],
186+
});
187+
188+
mockMC.getStats.resolves([
189+
{type: 'transport', id: 't1'},
190+
{type: 'candidate-pair', id: 'cp2', localCandidateId: 'lc2', selected: true},
191+
{type: 'local-candidate', id: 'lc2', port: 5000},
192+
]);
193+
const info = await mediaProperties.getCurrentConnectionInfo();
194+
assert.equal(info.ipVersion, 'IPv6');
195+
196+
assert.calledWith(tsSdpModule.parse, rtcPeerConnection.localDescription.sdp);
197+
});
198+
199+
it('returns ipVersion=undefined if parsing of the SDP fails', async () => {
200+
sinon.stub(tsSdpModule, 'parse').throws(new Error('fake error'));
201+
202+
mockMC.getStats.resolves([
203+
{type: 'candidate-pair', id: 'cp2', localCandidateId: 'lc2', selected: true},
204+
{type: 'local-candidate', id: 'lc2', port: 5000},
205+
]);
206+
const info = await mediaProperties.getCurrentConnectionInfo();
207+
assert.equal(info.ipVersion, undefined);
208+
209+
assert.calledWith(tsSdpModule.parse, rtcPeerConnection.localDescription.sdp);
210+
});
211+
});
212+
});
213+
84214
describe('selectedCandidatePairChanges and numTransports', () => {
85215
it('returns correct values when getStats() returns no transport stats at all', async () => {
86216
mockMC.getStats.resolves([{type: 'something', id: '1234'}]);

packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2047,7 +2047,12 @@ describe('plugin-meetings', () => {
20472047
meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
20482048
meeting.mediaProperties.getCurrentConnectionInfo = sinon
20492049
.stub()
2050-
.resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1});
2050+
.resolves({
2051+
connectionType: 'udp',
2052+
selectedCandidatePairChanges: 2,
2053+
numTransports: 1,
2054+
ipVersion: 'IPv6',
2055+
});
20512056
meeting.audio = muteStateStub;
20522057
meeting.video = muteStateStub;
20532058
sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
@@ -3059,6 +3064,9 @@ describe('plugin-meetings', () => {
30593064
});
30603065
assert.calledWith(webex.internal.newMetrics.submitClientEvent.thirdCall, {
30613066
name: 'client.media-engine.ready',
3067+
payload: {
3068+
ipVersion: 'IPv6',
3069+
},
30623070
options: {
30633071
meetingId: meeting.id,
30643072
},
@@ -3115,6 +3123,7 @@ describe('plugin-meetings', () => {
31153123
locus_id: meeting.locusUrl.split('/').pop(),
31163124
connectionType: 'udp',
31173125
selectedCandidatePairChanges: 2,
3126+
ipVersion: 'IPv6',
31183127
numTransports: 1,
31193128
isMultistream: false,
31203129
retriedWithTurnServer: true,
@@ -3268,6 +3277,7 @@ describe('plugin-meetings', () => {
32683277
locus_id: meeting.locusUrl.split('/').pop(),
32693278
connectionType: 'udp',
32703279
selectedCandidatePairChanges: 2,
3280+
ipVersion: 'IPv6',
32713281
numTransports: 1,
32723282
isMultistream: false,
32733283
retriedWithTurnServer: false,
@@ -3443,6 +3453,7 @@ describe('plugin-meetings', () => {
34433453
correlation_id: meeting.correlationId,
34443454
locus_id: meeting.locusUrl.split('/').pop(),
34453455
connectionType: 'udp',
3456+
ipVersion: 'IPv6',
34463457
selectedCandidatePairChanges: 2,
34473458
numTransports: 1,
34483459
isMultistream: false,

0 commit comments

Comments
 (0)