Skip to content

Commit bfad21f

Browse files
authored
Merge pull request #1623 from matrix-org/dbkr/ice_candidate_buffer
Don't ignore ICE candidates received before offer/answer
2 parents 81e68ab + ea39b69 commit bfad21f

File tree

2 files changed

+143
-46
lines changed

2 files changed

+143
-46
lines changed

spec/unit/webrtc/call.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,62 @@ describe('Call', function() {
190190
// Hangup to stop timers
191191
call.hangup(CallErrorCode.UserHangup, true);
192192
});
193+
194+
it('should add candidates received before answer if party ID is correct', async function() {
195+
await call.placeVoiceCall();
196+
call.peerConn.addIceCandidate = jest.fn();
197+
198+
call.onRemoteIceCandidatesReceived({
199+
getContent: () => {
200+
return {
201+
version: 1,
202+
call_id: call.callId,
203+
party_id: 'the_correct_party_id',
204+
candidates: [
205+
{
206+
candidate: 'the_correct_candidate',
207+
sdpMid: '',
208+
},
209+
],
210+
};
211+
},
212+
});
213+
214+
call.onRemoteIceCandidatesReceived({
215+
getContent: () => {
216+
return {
217+
version: 1,
218+
call_id: call.callId,
219+
party_id: 'some_other_party_id',
220+
candidates: [
221+
{
222+
candidate: 'the_wrong_candidate',
223+
sdpMid: '',
224+
},
225+
],
226+
};
227+
},
228+
});
229+
230+
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(0);
231+
232+
await call.onAnswerReceived({
233+
getContent: () => {
234+
return {
235+
version: 1,
236+
call_id: call.callId,
237+
party_id: 'the_correct_party_id',
238+
answer: {
239+
sdp: DUMMY_SDP,
240+
},
241+
};
242+
},
243+
});
244+
245+
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
246+
expect(call.peerConn.addIceCandidate).toHaveBeenCalledWith({
247+
candidate: 'the_correct_candidate',
248+
sdpMid: '',
249+
});
250+
});
193251
});

src/webrtc/call.ts

Lines changed: 85 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,6 @@ export class MatrixCall extends EventEmitter {
251251
private localAVStream: MediaStream;
252252
private inviteOrAnswerSent: boolean;
253253
private waitForLocalAVStream: boolean;
254-
// XXX: This is either the invite or answer from remote...
255-
private msg: any;
256254
// XXX: I don't know why this is called 'config'.
257255
private config: MediaStreamConstraints;
258256
private successor: MatrixCall;
@@ -284,16 +282,18 @@ export class MatrixCall extends EventEmitter {
284282
private makingOffer: boolean;
285283
private ignoreOffer: boolean;
286284

285+
// If candidates arrive before we've picked an opponent (which, in particular,
286+
// will happen if the opponent sends candidates eagerly before the user answers
287+
// the call) we buffer them up here so we can then add the ones from the party we pick
288+
private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>();
289+
287290
constructor(opts: CallOpts) {
288291
super();
289292
this.roomId = opts.roomId;
290293
this.client = opts.client;
291294
this.type = null;
292295
this.forceTURN = opts.forceTURN;
293296
this.ourPartyId = this.client.deviceId;
294-
// We compare this to null to checks the presence of a party ID:
295-
// make sure it's null, not undefined
296-
this.opponentPartyId = null;
297297
// Array of Objects with urls, username, credential keys
298298
this.turnServers = opts.turnServers || [];
299299
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
@@ -541,11 +541,16 @@ export class MatrixCall extends EventEmitter {
541541
* @param {MatrixEvent} event The m.call.invite event
542542
*/
543543
async initWithInvite(event: MatrixEvent) {
544-
this.msg = event.getContent();
544+
const invite = event.getContent();
545545
this.direction = CallDirection.Inbound;
546+
546547
this.peerConn = this.createPeerConnection();
548+
// we must set the party ID before await-ing on anything: the call event
549+
// handler will start giving us more call events (eg. candidates) so if
550+
// we haven't set the party ID, we'll ignore them.
551+
this.chooseOpponent(event);
547552
try {
548-
await this.peerConn.setRemoteDescription(this.msg.offer);
553+
await this.peerConn.setRemoteDescription(invite.offer);
549554
} catch (e) {
550555
logger.debug("Failed to set remote description", e);
551556
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
@@ -564,13 +569,6 @@ export class MatrixCall extends EventEmitter {
564569
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
565570

566571
this.setState(CallState.Ringing);
567-
this.opponentVersion = this.msg.version;
568-
if (this.opponentVersion !== 0) {
569-
// ignore party ID in v0 calls: party ID isn't a thing until v1
570-
this.opponentPartyId = this.msg.party_id || null;
571-
}
572-
this.opponentCaps = this.msg.capabilities || {};
573-
this.opponentMember = event.sender;
574572

575573
if (event.getLocalAge()) {
576574
setTimeout(() => {
@@ -584,7 +582,7 @@ export class MatrixCall extends EventEmitter {
584582
}
585583
this.emit(CallEvent.Hangup);
586584
}
587-
}, this.msg.lifetime - event.getLocalAge());
585+
}, invite.lifetime - event.getLocalAge());
588586
}
589587
}
590588

@@ -596,7 +594,6 @@ export class MatrixCall extends EventEmitter {
596594
// perverse as it may seem, sometimes we want to instantiate a call with a
597595
// hangup message (because when getting the state of the room on load, events
598596
// come in reverse order and we want to remember that a call has been hung up)
599-
this.msg = event.getContent();
600597
this.setState(CallState.Ended);
601598
}
602599

@@ -1034,37 +1031,33 @@ export class MatrixCall extends EventEmitter {
10341031
return;
10351032
}
10361033

1034+
const cands = ev.getContent().candidates;
1035+
if (!cands) {
1036+
logger.info("Ignoring candidates event with no candidates!");
1037+
return;
1038+
}
1039+
1040+
const fromPartyId = ev.getContent().version === 0 ? null : ev.getContent().party_id || null;
1041+
1042+
if (this.opponentPartyId === undefined) {
1043+
// we haven't picked an opponent yet so save the candidates
1044+
logger.info(`Bufferring ${cands.length} candidates until we pick an opponent`);
1045+
const bufferedCands = this.remoteCandidateBuffer.get(fromPartyId) || [];
1046+
bufferedCands.push(...cands);
1047+
this.remoteCandidateBuffer.set(fromPartyId, bufferedCands);
1048+
return;
1049+
}
1050+
10371051
if (!this.partyIdMatches(ev.getContent())) {
10381052
logger.info(
10391053
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` +
10401054
`we have chosen party ID ${this.opponentPartyId}`,
10411055
);
1042-
return;
1043-
}
10441056

1045-
const cands = ev.getContent().candidates;
1046-
if (!cands) {
1047-
logger.info("Ignoring candidates event with no candidates!");
10481057
return;
10491058
}
10501059

1051-
for (const cand of cands) {
1052-
if (
1053-
(cand.sdpMid === null || cand.sdpMid === undefined) &&
1054-
(cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined)
1055-
) {
1056-
logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex");
1057-
return;
1058-
}
1059-
logger.debug("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
1060-
try {
1061-
this.peerConn.addIceCandidate(cand);
1062-
} catch (err) {
1063-
if (!this.ignoreOffer) {
1064-
logger.info("Failed to add remore ICE candidate", err);
1065-
}
1066-
}
1067-
}
1060+
this.addIceCandidates(cands);
10681061
}
10691062

10701063
/**
@@ -1076,20 +1069,15 @@ export class MatrixCall extends EventEmitter {
10761069
return;
10771070
}
10781071

1079-
if (this.opponentPartyId !== null) {
1072+
if (this.opponentPartyId !== undefined) {
10801073
logger.info(
10811074
`Ignoring answer from party ID ${event.getContent().party_id}: ` +
10821075
`we already have an answer/reject from ${this.opponentPartyId}`,
10831076
);
10841077
return;
10851078
}
10861079

1087-
this.opponentVersion = event.getContent().version;
1088-
if (this.opponentVersion !== 0) {
1089-
this.opponentPartyId = event.getContent().party_id || null;
1090-
}
1091-
this.opponentCaps = event.getContent().capabilities || {};
1092-
this.opponentMember = event.sender;
1080+
this.chooseOpponent(event);
10931081

10941082
this.setState(CallState.Connecting);
10951083

@@ -1720,9 +1708,60 @@ export class MatrixCall extends EventEmitter {
17201708

17211709
private partyIdMatches(msg): boolean {
17221710
// They must either match or both be absent (in which case opponentPartyId will be null)
1723-
const msgPartyId = msg.party_id || null;
1711+
// Also we ignore party IDs on the invite/offer if the version is 0, so we must do the same
1712+
// here and use null if the version is 0 (woe betide any opponent sending messages in the
1713+
// same call with different versions)
1714+
const msgPartyId = msg.version === 0 ? null : msg.party_id || null;
17241715
return msgPartyId === this.opponentPartyId;
17251716
}
1717+
1718+
// Commits to an opponent for the call
1719+
// ev: An invite or answer event
1720+
private chooseOpponent(ev: MatrixEvent) {
1721+
// I choo-choo-choose you
1722+
const msg = ev.getContent();
1723+
1724+
this.opponentVersion = msg.version;
1725+
if (this.opponentVersion === 0) {
1726+
// set to null to indicate that we've chosen an opponent, but because
1727+
// they're v0 they have no party ID (even if they sent one, we're ignoring it)
1728+
this.opponentPartyId = null;
1729+
} else {
1730+
// set to their party ID, or if they're naughty and didn't send one despite
1731+
// not being v0, set it to null to indicate we picked an opponent with no
1732+
// party ID
1733+
this.opponentPartyId = msg.party_id || null;
1734+
}
1735+
this.opponentCaps = msg.capabilities || {};
1736+
this.opponentMember = ev.sender;
1737+
1738+
const bufferedCands = this.remoteCandidateBuffer.get(this.opponentPartyId);
1739+
if (bufferedCands) {
1740+
logger.info(`Adding ${bufferedCands.length} buffered candidates for opponent ${this.opponentPartyId}`);
1741+
this.addIceCandidates(bufferedCands);
1742+
}
1743+
this.remoteCandidateBuffer = null;
1744+
}
1745+
1746+
private addIceCandidates(cands: RTCIceCandidate[]) {
1747+
for (const cand of cands) {
1748+
if (
1749+
(cand.sdpMid === null || cand.sdpMid === undefined) &&
1750+
(cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined)
1751+
) {
1752+
logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex");
1753+
return;
1754+
}
1755+
logger.debug("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
1756+
try {
1757+
this.peerConn.addIceCandidate(cand);
1758+
} catch (err) {
1759+
if (!this.ignoreOffer) {
1760+
logger.info("Failed to add remore ICE candidate", err);
1761+
}
1762+
}
1763+
}
1764+
}
17261765
}
17271766

17281767
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {

0 commit comments

Comments
 (0)