Newer
Older
import Interop from '@jitsi/sdp-interop';
import transform from 'sdp-transform';
import logger from '/imports/startup/client/logger';
// sdp-interop library for unified-plan <-> plan-b translation
const interop = new Interop.Interop();
// Some heuristics to determine if the input SDP is Unified Plan
const isUnifiedPlan = (sdp) => {
const parsedSDP = transform.parse(sdp);
if (parsedSDP.media.length <= 3 && parsedSDP.media.every(m => ['video', 'audio', 'data'].indexOf(m.mid) !== -1)) {
logger.info({ logCode: 'sdp_utils_not_unified_plan' }, 'SDP does not look like Unified Plan');
return false;
}
logger.info({ logCode: 'sdp_utils_is_unified_plan' }, 'SDP looks like Unified Plan');
// Some heuristics to determine if the input SDP is Plan B
const isPlanB = (sdp) => {
const parsedSDP = transform.parse(sdp);
if (parsedSDP.media.length > 3 || !parsedSDP.media.every(m => ['video', 'audio', 'data'].indexOf(m.mid) !== -1)) {
logger.info({ logCode: 'sdp_utils_not_plan_b' }, 'SDP does not look like Plan B');
return false;
}
logger.info({ logCode: 'sdp_utils_is_plan_b' }, 'SDP looks like Plan B');
return true;
// Specific method for translating FS SDPs from Plan B to Unified Plan (vice-versa)
const toPlanB = (unifiedPlanSDP) => {
const planBSDP = interop.toPlanB(unifiedPlanSDP);
logger.info({ logCode: 'sdp_utils_unified_plan_to_plan_b' }, `Converted Unified Plan to Plan B ${JSON.stringify(planBSDP)}`);
return planBSDP;
const toUnifiedPlan = (planBSDP) => {
const unifiedPlanSDP = interop.toUnifiedPlan(planBSDP);
logger.info({ logCode: 'sdp_utils_plan_b_to_unified_plan' }, `Converted Plan B to Unified Plan ${JSON.stringify(unifiedPlanSDP)}`);
return unifiedPlanSDP;
const stripMDnsCandidates = (sdp) => {
let strippedCandidates = 0;
parsedSDP.media.forEach((media) => {
if (media.candidates) {
media.candidates = media.candidates.filter((candidate) => {
if (candidate.ip && candidate.ip.indexOf('.local') === -1) {
return true;
}
strippedCandidates += 1;
return false;
});
}
});
if (strippedCandidates > 0) {
logger.info({ logCode: 'sdp_utils_mdns_candidate_strip' }, `Stripped ${strippedCandidates} mDNS candidates`);
}
return { sdp: transform.write(parsedSDP), type: sdp.type };
const filterValidIceCandidates = (validIceCandidates, sdp) => {
if (!validIceCandidates || !validIceCandidates.length) return sdp;
const matchCandidatesIp = (candidate, mediaCandidate) => (
(candidate.address && candidate.address.includes(mediaCandidate.ip))
|| (candidate.relatedAddress
&& candidate.relatedAddress.includes(mediaCandidate.ip))
);
const parsedSDP = transform.parse(sdp.sdp);
let strippedCandidates = 0;
parsedSDP.media.forEach((media) => {
if (media.candidates) {
media.candidates = media.candidates.filter((candidate) => {
if (candidate.ip
&& candidate.type
&& candidate.transport
&& validIceCandidates.find((c) => (c.protocol === candidate.transport)
&& matchCandidatesIp(c, candidate))
) {
return true;
}
strippedCandidates += 1;
return false;
});
}
});
if (strippedCandidates > 0) {
logger.info({ logCode: 'sdp_utils_mdns_candidate_strip' },
`Filtered ${strippedCandidates} invalid candidates from trickle SDP`);
}
return { sdp: transform.write(parsedSDP), type: sdp.type };
};
const isPublicIpv4 = (ip) => {
const ipParts = ip.split('.');
switch (ipParts[0]) {
case 10:
case 127:
return false;
case 172:
return ipParts[1] <= 16 || ipParts[1] > 32;
case 192:
return ipParts[1] !== 168;
default:
return true;
}
};
const parseIP = (ip) => {
if (ip && typeof ip === 'string') {
if (ip.indexOf(':') !== -1) return { type: 'v6', public: true };
if (ip.indexOf('.local') !== -1) return { type: 'mdns', public: false };
if (ip.indexOf('.')) return { type: 'v4', public: isPublicIpv4(ip) };
}
return { type: 'unknown', public: false };
};
const analyzeSdp = (sdp, sendLogs = true) => {
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// For now we just need to parse and log the different pieces. In the future we're going to want
// to be tracking whether there were TURN candidates and IPv4 candidates to make informed
// decisions about what to do on fallbacks/reconnects.
const parsedSDP = transform.parse(sdp);
const v4Info = {
found: false,
public: false,
};
const v6Info = {
found: false,
public: false,
};
const srflxInfo = {
found: false,
type: 'not found',
public: false,
};
const prflxInfo = {
found: false,
type: 'not found',
public: false,
};
const relayInfo = {
found: false,
type: 'not found',
public: false,
};
// Things to parse:
// Are there any IPv4/IPv6
// Is there a server reflexive candidate? (srflx) is a public or private IP
// Is there a relay (TURN) candidate
parsedSDP.media.forEach((media) => {
if (media.candidates) {
// console.log("**** Found candidates ****")
media.candidates.forEach((candidate) => {
// console.log(candidate)
const ipInfo = parseIP(candidate.ip);
switch (ipInfo.type) {
case 'v4':
v4Info.found = true;
v4Info.public = v4Info.public || ipInfo.public;
break;
case 'v6':
v6Info.found = true;
v6Info.public = v6Info.public || ipInfo.public;
break;
}
switch (candidate.type) {
case 'srflx':
srflxInfo.found = true;
if (srflxInfo.type === 'not found') {
srflxInfo.type = ipInfo.type;
} else if (srflxInfo.type !== ipInfo.type) {
srflxInfo.type = 'both';
}
srflxInfo.public = srflxInfo.public || ipInfo.public;
break;
case 'prflx':
prflxInfo.found = true;
if (prflxInfo.type === 'not found') {
prflxInfo.type = ipInfo.type;
} else if (prflxInfo.type !== ipInfo.type) {
prflxInfo.type = 'both';
}
prflxInfo.public = prflxInfo.public || ipInfo.public;
break;
case 'relay':
relayInfo.found = true;
if (relayInfo.type === 'not found') {
relayInfo.type = ipInfo.type;
} else if (relayInfo.type !== ipInfo.type) {
relayInfo.type = 'both';
}
relayInfo.public = relayInfo.public || ipInfo.public;
break;
}
});
// console.log("**** End of candidates ****")
}
});
// candidate types
foundV4Candidate: v4Info.found,
foundV4PublicCandidate: v4Info.public,
foundV6Candidate: v6Info.found,
}, `Found candidates ${v4Info.found ? 'with' : 'without'} type v4 (public? ${v4Info.public}) and ${v6Info.found ? 'with' : 'without'} type v6`);
// server reflexive
if (srflxInfo.found) {
logger.info({
logCode: 'sdp_utils_server_reflexive_found',
extraInfo: {
candidateType: srflxInfo.type,
candidatePublic: srflxInfo.public,
},
}, 'Found a server reflexive candidate');
} else {
logger.info({
logCode: 'sdp_utils_no_server_reflexive',
}, 'No server reflexive candidate found');
}
// peer reflexive
if (prflxInfo.found) {
logger.info({
logCode: 'sdp_utils_peer_reflexive_found',
extraInfo: {
candidateType: prflxInfo.type,
candidatePublic: prflxInfo.public,
},
}, 'Found a peer reflexive candidate');
} else {
logger.info({
logCode: 'sdp_utils_no_peer_reflexive',
}, 'No peer reflexive candidate found');
}
// relay
if (relayInfo.found) {
logger.info({
logCode: 'sdp_utils_relay_found',
extraInfo: {
candidateType: relayInfo.type,
candidatePublic: relayInfo.public,
},
}, 'Found a relay candidate');
} else {
logger.info({
logCode: 'sdp_utils_no_relay',
}, 'No relay candidate found');
}
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
return {
v4Info,
v6Info,
srflxInfo,
prflxInfo,
relayInfo,
};
};
// We grab the protocol type from the answer SDP because Safari stats don't contain the
// candidate IP addresses
const logSelectedCandidate = async (peer, isIpv6) => {
peer.getStats().then((report) => {
let localCandidate;
const values = Array.from(report.values());
const candidatePair = values.find(item => item.type === 'candidate-pair' && (item.selected || item.state === 'succeeded'));
if (candidatePair) {
localCandidate = values.find(item => item.id === candidatePair.localCandidateId);
}
const ipType = isIpv6 ? 'v6' : 'v4';
if (candidatePair) {
if (localCandidate) {
// console.log(localCandidate);
// Safari doesn't include the IP address in the candidate info so we can't rely on this
// const candidateIp = localCandidate.ip || localCandidate.address;
// const ipInfo = parseIP(candidateIp);
logger.info({
logCode: 'sip_js_candidate_selected',
extraInfo: {
candidateType: localCandidate.candidateType,
candidateProtocol: localCandidate.protocol,
ipType,
networkType: localCandidate.networkType,
},
}, `ICE Candidate selected - type: ${localCandidate.candidateType}, protocol: ${localCandidate.protocol}, ipProtocol: ${ipType}`);
} else {
logger.info({
logCode: 'sip_js_lcandidate_not_found',
extraInfo: {
ipType,
},
}, `ICE Candidate selected, but could not find local candidate - ipProtocol: ${ipType}`);
}
} else {
logger.info({
logCode: 'sip_js_candidate_pair_not_found',
extraInfo: {
ipType,
},
}, `ICE Candidate selected, but could not find pair - ipProtocol: ${ipType}`);
}
// TODO return the data back
});
interop,
isUnifiedPlan,
toPlanB,
toUnifiedPlan,
stripMDnsCandidates,
filterValidIceCandidates,