1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Merge pull request #1623 from matrix-org/dbkr/ice_candidate_buffer

Don't ignore ICE candidates received before offer/answer
This commit is contained in:
David Baker
2021-02-27 15:11:26 +00:00
committed by GitHub
2 changed files with 145 additions and 48 deletions

View File

@@ -190,4 +190,62 @@ describe('Call', function() {
// Hangup to stop timers // Hangup to stop timers
call.hangup(CallErrorCode.UserHangup, true); call.hangup(CallErrorCode.UserHangup, true);
}); });
it('should add candidates received before answer if party ID is correct', async function() {
await call.placeVoiceCall();
call.peerConn.addIceCandidate = jest.fn();
call.onRemoteIceCandidatesReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'the_correct_party_id',
candidates: [
{
candidate: 'the_correct_candidate',
sdpMid: '',
},
],
};
},
});
call.onRemoteIceCandidatesReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'some_other_party_id',
candidates: [
{
candidate: 'the_wrong_candidate',
sdpMid: '',
},
],
};
},
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(0);
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'the_correct_party_id',
answer: {
sdp: DUMMY_SDP,
},
};
},
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
expect(call.peerConn.addIceCandidate).toHaveBeenCalledWith({
candidate: 'the_correct_candidate',
sdpMid: '',
});
});
}); });

View File

@@ -251,8 +251,6 @@ export class MatrixCall extends EventEmitter {
private localAVStream: MediaStream; private localAVStream: MediaStream;
private inviteOrAnswerSent: boolean; private inviteOrAnswerSent: boolean;
private waitForLocalAVStream: boolean; private waitForLocalAVStream: boolean;
// XXX: This is either the invite or answer from remote...
private msg: any;
// XXX: I don't know why this is called 'config'. // XXX: I don't know why this is called 'config'.
private config: MediaStreamConstraints; private config: MediaStreamConstraints;
private successor: MatrixCall; private successor: MatrixCall;
@@ -284,6 +282,11 @@ export class MatrixCall extends EventEmitter {
private makingOffer: boolean; private makingOffer: boolean;
private ignoreOffer: boolean; private ignoreOffer: boolean;
// If candidates arrive before we've picked an opponent (which, in particular,
// will happen if the opponent sends candidates eagerly before the user answers
// the call) we buffer them up here so we can then add the ones from the party we pick
private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>();
constructor(opts: CallOpts) { constructor(opts: CallOpts) {
super(); super();
this.roomId = opts.roomId; this.roomId = opts.roomId;
@@ -291,9 +294,6 @@ export class MatrixCall extends EventEmitter {
this.type = null; this.type = null;
this.forceTURN = opts.forceTURN; this.forceTURN = opts.forceTURN;
this.ourPartyId = this.client.deviceId; this.ourPartyId = this.client.deviceId;
// We compare this to null to checks the presence of a party ID:
// make sure it's null, not undefined
this.opponentPartyId = null;
// Array of Objects with urls, username, credential keys // Array of Objects with urls, username, credential keys
this.turnServers = opts.turnServers || []; this.turnServers = opts.turnServers || [];
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) { if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
@@ -541,11 +541,16 @@ export class MatrixCall extends EventEmitter {
* @param {MatrixEvent} event The m.call.invite event * @param {MatrixEvent} event The m.call.invite event
*/ */
async initWithInvite(event: MatrixEvent) { async initWithInvite(event: MatrixEvent) {
this.msg = event.getContent(); const invite = event.getContent();
this.direction = CallDirection.Inbound; this.direction = CallDirection.Inbound;
this.peerConn = this.createPeerConnection(); this.peerConn = this.createPeerConnection();
// we must set the party ID before await-ing on anything: the call event
// handler will start giving us more call events (eg. candidates) so if
// we haven't set the party ID, we'll ignore them.
this.chooseOpponent(event);
try { try {
await this.peerConn.setRemoteDescription(this.msg.offer); await this.peerConn.setRemoteDescription(invite.offer);
} catch (e) { } catch (e) {
logger.debug("Failed to set remote description", e); logger.debug("Failed to set remote description", e);
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false); this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
@@ -564,13 +569,6 @@ export class MatrixCall extends EventEmitter {
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice; this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
this.setState(CallState.Ringing); this.setState(CallState.Ringing);
this.opponentVersion = this.msg.version;
if (this.opponentVersion !== 0) {
// ignore party ID in v0 calls: party ID isn't a thing until v1
this.opponentPartyId = this.msg.party_id || null;
}
this.opponentCaps = this.msg.capabilities || {};
this.opponentMember = event.sender;
if (event.getLocalAge()) { if (event.getLocalAge()) {
setTimeout(() => { setTimeout(() => {
@@ -584,7 +582,7 @@ export class MatrixCall extends EventEmitter {
} }
this.emit(CallEvent.Hangup); this.emit(CallEvent.Hangup);
} }
}, this.msg.lifetime - event.getLocalAge()); }, invite.lifetime - event.getLocalAge());
} }
} }
@@ -596,7 +594,6 @@ export class MatrixCall extends EventEmitter {
// perverse as it may seem, sometimes we want to instantiate a call with a // perverse as it may seem, sometimes we want to instantiate a call with a
// hangup message (because when getting the state of the room on load, events // hangup message (because when getting the state of the room on load, events
// come in reverse order and we want to remember that a call has been hung up) // come in reverse order and we want to remember that a call has been hung up)
this.msg = event.getContent();
this.setState(CallState.Ended); this.setState(CallState.Ended);
} }
@@ -1034,37 +1031,33 @@ export class MatrixCall extends EventEmitter {
return; return;
} }
if (!this.partyIdMatches(ev.getContent())) {
logger.info(
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` +
`we have chosen party ID ${this.opponentPartyId}`,
);
return;
}
const cands = ev.getContent().candidates; const cands = ev.getContent().candidates;
if (!cands) { if (!cands) {
logger.info("Ignoring candidates event with no candidates!"); logger.info("Ignoring candidates event with no candidates!");
return; return;
} }
for (const cand of cands) { const fromPartyId = ev.getContent().version === 0 ? null : ev.getContent().party_id || null;
if (
(cand.sdpMid === null || cand.sdpMid === undefined) && if (this.opponentPartyId === undefined) {
(cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined) // we haven't picked an opponent yet so save the candidates
) { logger.info(`Bufferring ${cands.length} candidates until we pick an opponent`);
logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex"); const bufferedCands = this.remoteCandidateBuffer.get(fromPartyId) || [];
return; bufferedCands.push(...cands);
} this.remoteCandidateBuffer.set(fromPartyId, bufferedCands);
logger.debug("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate); return;
try {
this.peerConn.addIceCandidate(cand);
} catch (err) {
if (!this.ignoreOffer) {
logger.info("Failed to add remore ICE candidate", err);
}
}
} }
if (!this.partyIdMatches(ev.getContent())) {
logger.info(
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` +
`we have chosen party ID ${this.opponentPartyId}`,
);
return;
}
this.addIceCandidates(cands);
} }
/** /**
@@ -1076,7 +1069,7 @@ export class MatrixCall extends EventEmitter {
return; return;
} }
if (this.opponentPartyId !== null) { if (this.opponentPartyId !== undefined) {
logger.info( logger.info(
`Ignoring answer from party ID ${event.getContent().party_id}: ` + `Ignoring answer from party ID ${event.getContent().party_id}: ` +
`we already have an answer/reject from ${this.opponentPartyId}`, `we already have an answer/reject from ${this.opponentPartyId}`,
@@ -1084,12 +1077,7 @@ export class MatrixCall extends EventEmitter {
return; return;
} }
this.opponentVersion = event.getContent().version; this.chooseOpponent(event);
if (this.opponentVersion !== 0) {
this.opponentPartyId = event.getContent().party_id || null;
}
this.opponentCaps = event.getContent().capabilities || {};
this.opponentMember = event.sender;
this.setState(CallState.Connecting); this.setState(CallState.Connecting);
@@ -1720,9 +1708,60 @@ export class MatrixCall extends EventEmitter {
private partyIdMatches(msg): boolean { private partyIdMatches(msg): boolean {
// They must either match or both be absent (in which case opponentPartyId will be null) // They must either match or both be absent (in which case opponentPartyId will be null)
const msgPartyId = msg.party_id || null; // Also we ignore party IDs on the invite/offer if the version is 0, so we must do the same
// here and use null if the version is 0 (woe betide any opponent sending messages in the
// same call with different versions)
const msgPartyId = msg.version === 0 ? null : msg.party_id || null;
return msgPartyId === this.opponentPartyId; return msgPartyId === this.opponentPartyId;
} }
// Commits to an opponent for the call
// ev: An invite or answer event
private chooseOpponent(ev: MatrixEvent) {
// I choo-choo-choose you
const msg = ev.getContent();
this.opponentVersion = msg.version;
if (this.opponentVersion === 0) {
// set to null to indicate that we've chosen an opponent, but because
// they're v0 they have no party ID (even if they sent one, we're ignoring it)
this.opponentPartyId = null;
} else {
// set to their party ID, or if they're naughty and didn't send one despite
// not being v0, set it to null to indicate we picked an opponent with no
// party ID
this.opponentPartyId = msg.party_id || null;
}
this.opponentCaps = msg.capabilities || {};
this.opponentMember = ev.sender;
const bufferedCands = this.remoteCandidateBuffer.get(this.opponentPartyId);
if (bufferedCands) {
logger.info(`Adding ${bufferedCands.length} buffered candidates for opponent ${this.opponentPartyId}`);
this.addIceCandidates(bufferedCands);
}
this.remoteCandidateBuffer = null;
}
private addIceCandidates(cands: RTCIceCandidate[]) {
for (const cand of cands) {
if (
(cand.sdpMid === null || cand.sdpMid === undefined) &&
(cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined)
) {
logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex");
return;
}
logger.debug("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
try {
this.peerConn.addIceCandidate(cand);
} catch (err) {
if (!this.ignoreOffer) {
logger.info("Failed to add remore ICE candidate", err);
}
}
}
}
} }
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) { function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {