Implement perfect negotiation
Change-Id: Ic768ffb86407a7078554488b45e44a1b2ecfbede
diff --git a/client/src/services/CallManager.tsx b/client/src/services/CallManager.tsx
index b0837b0..8522976 100644
--- a/client/src/services/CallManager.tsx
+++ b/client/src/services/CallManager.tsx
@@ -249,7 +249,7 @@
setCallStatus(CallStatus.Connecting);
console.info('Sending CallJoin', callAccept);
webSocket.send(WebSocketMessageType.sendCallJoin, callAccept);
- // TODO: move this to "onWebRtcOffer" listener so we don't add connections for non-connected members
+ // TODO: move this to "onWebRtcDescription" listener so we don't add connections for non-connected members
callMembers.forEach((member) =>
webRtcManager.addConnection(
webSocket,
diff --git a/client/src/webrtc/RtcPeerConnectionHandler.ts b/client/src/webrtc/RtcPeerConnectionHandler.ts
index d8d9d3c..1145e83 100644
--- a/client/src/webrtc/RtcPeerConnectionHandler.ts
+++ b/client/src/webrtc/RtcPeerConnectionHandler.ts
@@ -34,6 +34,10 @@
private iceConnectionState: RTCIceConnectionState = 'new';
private isReadyForIceCandidates = false;
+ private isPolite = false; // Implementing "perfect negotiation" pattern
+ private makingOffer = false;
+ private ignoreOffer = false;
+
private audioRtcRtpSenders: RTCRtpSender[] | null = null;
private videoRtcRtpSenders: RTCRtpSender[] | null = null;
@@ -65,17 +69,13 @@
screenShareLocalStream: MediaStream | undefined,
listener: Listener
) {
- console.log('constructor', callData);
+ this.isPolite = account.getUri() < contactUri;
this.listener = listener;
const iceServers = this.getIceServers(account);
this.connection = new RTCPeerConnection({ iceServers });
this.setConnectionListeners(webSocket, callData.conversationId, contactUri);
this.setWebSocketListeners(webSocket, callData.conversationId, contactUri);
this.updateLocalStreams(localStream, screenShareLocalStream);
-
- if (callData.role === 'caller') {
- this.startNegociation(webSocket, contactUri, callData.conversationId);
- }
}
updateLocalStreams(localStream: MediaStream | undefined, screenShareLocalStream: MediaStream | undefined) {
@@ -150,45 +150,21 @@
return iceServers;
}
- private startNegociation(webSocket: IWebSocketContext, contactUri: string, conversationId: string) {
- this.sendWebRtcOffer(webSocket, contactUri, conversationId);
- }
-
- private async sendWebRtcOffer(webSocket: IWebSocketContext, contactUri: string, conversationId: string) {
- const sdp = await this.connection.createOffer({
- offerToReceiveAudio: true,
- offerToReceiveVideo: true,
- });
-
- const webRtcOffer: WebSocketMessageTable['sendWebRtcOffer'] = {
- receiverId: contactUri,
- conversationId: conversationId,
- sdp,
- };
-
- await this.connection.setLocalDescription(new RTCSessionDescription(sdp));
- console.info('Sending WebRtcOffer', webRtcOffer);
- webSocket.send(WebSocketMessageType.sendWebRtcOffer, webRtcOffer);
- }
-
- private setWebSocketListeners(webSocket: IWebSocketContext, conversationId: string, contactUri: string) {
- const sendWebRtcAnswer = async () => {
- const sdp = await this.connection.createAnswer({
- offerToReceiveAudio: true,
- offerToReceiveVideo: true,
- });
-
- const webRtcAnswer: WebSocketMessageTable['sendWebRtcAnswer'] = {
+ private async sendLocalDescription(webSocket: IWebSocketContext, conversationId: string, contactUri: string) {
+ await this.connection.setLocalDescription();
+ const sdp = this.connection.localDescription;
+ if (sdp) {
+ const webRtcDescription: WebSocketMessageTable['sendWebRtcDescription'] = {
receiverId: contactUri,
conversationId: conversationId,
sdp,
};
+ webSocket.send(WebSocketMessageType.sendWebRtcDescription, webRtcDescription);
+ console.info('Sending webRtcDescription', webRtcDescription);
+ }
+ }
- await this.connection.setLocalDescription(new RTCSessionDescription(sdp));
- console.info('Sending WebRtcAnswer', webRtcAnswer);
- webSocket.send(WebSocketMessageType.sendWebRtcAnswer, webRtcAnswer);
- };
-
+ private setWebSocketListeners(webSocket: IWebSocketContext, conversationId: string, contactUri: string) {
const addQueuedIceCandidates = async () => {
console.info('WebRTC remote description has been set. Ready to receive ICE candidates');
this.isReadyForIceCandidates = true;
@@ -199,31 +175,36 @@
this.iceCandidateQueue
);
- await Promise.all(this.iceCandidateQueue.map((iceCandidate) => this.connection.addIceCandidate(iceCandidate)));
+ await Promise.all(
+ this.iceCandidateQueue.map((iceCandidate) => this.connection.addIceCandidate(iceCandidate))
+ ).catch((err) => {
+ if (!this.ignoreOffer) {
+ console.error(err);
+ }
+ });
}
};
- const webRtcOfferListener = async (data: WebRtcSdp) => {
- console.debug('receive webrtcoffer');
- console.info('Received event on WebRtcOffer', data);
- if (data.conversationId !== conversationId) {
- console.warn('Wrong incoming conversationId, ignoring action');
- return;
- }
-
- await this.connection.setRemoteDescription(new RTCSessionDescription(data.sdp));
- await sendWebRtcAnswer();
- await addQueuedIceCandidates();
- };
-
- const webRtcAnswerListener = async (data: WebRtcSdp) => {
+ const webRtcDescriptionListener = async (data: WebRtcSdp) => {
console.info('Received event on WebRtcAnswer', data);
if (data.conversationId !== conversationId) {
console.warn('Wrong incoming conversationId, ignoring action');
return;
}
- await this.connection.setRemoteDescription(new RTCSessionDescription(data.sdp));
+ const remoteDescription = data.sdp;
+ const offerCollision =
+ remoteDescription.type === 'offer' && (this.makingOffer || this.connection.signalingState !== 'stable');
+
+ this.ignoreOffer = !this.isPolite && offerCollision;
+ if (this.ignoreOffer) {
+ return;
+ }
+
+ await this.connection.setRemoteDescription(remoteDescription);
+ if (remoteDescription.type === 'offer') {
+ await this.sendLocalDescription(webSocket, conversationId, contactUri);
+ }
await addQueuedIceCandidates();
};
@@ -238,24 +219,48 @@
}
if (this.isReadyForIceCandidates) {
- await this.connection.addIceCandidate(data.candidate);
+ try {
+ await this.connection.addIceCandidate(data.candidate);
+ } catch (err) {
+ if (!this.ignoreOffer) {
+ console.error(err);
+ }
+ }
} else {
this.iceCandidateQueue.push(data.candidate);
}
};
- webSocket.bind(WebSocketMessageType.onWebRtcOffer, webRtcOfferListener);
- webSocket.bind(WebSocketMessageType.onWebRtcAnswer, webRtcAnswerListener);
+ webSocket.bind(WebSocketMessageType.onWebRtcDescription, webRtcDescriptionListener);
webSocket.bind(WebSocketMessageType.onWebRtcIceCandidate, webRtcIceCandidateListener);
this.cleaningFunctions.push(() => {
- webSocket.unbind(WebSocketMessageType.onWebRtcOffer, webRtcOfferListener);
- webSocket.unbind(WebSocketMessageType.onWebRtcAnswer, webRtcAnswerListener);
+ webSocket.unbind(WebSocketMessageType.onWebRtcDescription, webRtcDescriptionListener);
webSocket.unbind(WebSocketMessageType.onWebRtcIceCandidate, webRtcIceCandidateListener);
});
}
private setConnectionListeners(webSocket: IWebSocketContext, conversationId: string, contactUri: string) {
+ this.connection.onnegotiationneeded = async () => {
+ try {
+ this.makingOffer = true;
+ await this.sendLocalDescription(webSocket, conversationId, contactUri);
+ } catch (err) {
+ // This is not an error if there was a collision
+ console.warn(err);
+ } finally {
+ this.makingOffer = false;
+ }
+ };
+
+ this.connection.oniceconnectionstatechange = () => {
+ if (this.connection.iceConnectionState === 'failed') {
+ // This is not an error if there was a collision
+ console.warn('ICE connection failed, restarting ICE');
+ this.connection.restartIce();
+ }
+ };
+
this.connection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate) {
const webRtcIceCandidate: WebSocketMessageTable['sendWebRtcIceCandidate'] = {
diff --git a/common/src/enums/websocket-message-type.ts b/common/src/enums/websocket-message-type.ts
index b2d729c..28977fb 100644
--- a/common/src/enums/websocket-message-type.ts
+++ b/common/src/enums/websocket-message-type.ts
@@ -28,10 +28,8 @@
onCallExit = 'onCallExit',
sendCallJoin = 'sendCallJoin',
onCallJoin = 'onCallJoin',
- sendWebRtcOffer = 'sendWebRtcOffer',
- onWebRtcOffer = 'onWebRtcOffer',
- sendWebRtcAnswer = 'sendWebRtcAnswer',
- onWebRtcAnswer = 'onWebRtcAnswer',
+ sendWebRtcDescription = 'sendWebRtcDescription',
+ onWebRtcDescription = 'onWebRtcDescription',
sendWebRtcIceCandidate = 'sendWebRtcIceCandidate',
onWebRtcIceCandidate = 'onWebRtcIceCandidate',
}
diff --git a/common/src/interfaces/websocket-message.ts b/common/src/interfaces/websocket-message.ts
index f85ea5e..b1c1524 100644
--- a/common/src/interfaces/websocket-message.ts
+++ b/common/src/interfaces/websocket-message.ts
@@ -43,10 +43,8 @@
[WebSocketMessageType.onCallExit]: CallExit & WithSender;
[WebSocketMessageType.sendCallJoin]: CallJoin;
[WebSocketMessageType.onCallJoin]: CallJoin & WithSender;
- [WebSocketMessageType.sendWebRtcOffer]: WebRtcSdp & WithReceiver;
- [WebSocketMessageType.onWebRtcOffer]: WebRtcSdp & WithSender;
- [WebSocketMessageType.sendWebRtcAnswer]: WebRtcSdp & WithReceiver;
- [WebSocketMessageType.onWebRtcAnswer]: WebRtcSdp & WithSender;
+ [WebSocketMessageType.sendWebRtcDescription]: WebRtcSdp & WithReceiver;
+ [WebSocketMessageType.onWebRtcDescription]: WebRtcSdp & WithSender;
[WebSocketMessageType.sendWebRtcIceCandidate]: WebRtcIceCandidate & WithReceiver;
[WebSocketMessageType.onWebRtcIceCandidate]: WebRtcIceCandidate & WithSender;
}
diff --git a/server/src/websocket/webrtc-handler.ts b/server/src/websocket/webrtc-handler.ts
index 9d2c6e7..ce3fef5 100644
--- a/server/src/websocket/webrtc-handler.ts
+++ b/server/src/websocket/webrtc-handler.ts
@@ -46,14 +46,9 @@
});
});
- webSocketServer.bind(WebSocketMessageType.sendWebRtcOffer, (accountId, data) => {
+ webSocketServer.bind(WebSocketMessageType.sendWebRtcDescription, (accountId, data) => {
const { receiverId, ...partialData } = data;
- sendWebRtcData(WebSocketMessageType.onWebRtcOffer, partialData, accountId, receiverId);
- });
-
- webSocketServer.bind(WebSocketMessageType.sendWebRtcAnswer, (accountId, data) => {
- const { receiverId, ...partialData } = data;
- sendWebRtcData(WebSocketMessageType.onWebRtcAnswer, partialData, accountId, receiverId);
+ sendWebRtcData(WebSocketMessageType.onWebRtcDescription, partialData, accountId, receiverId);
});
webSocketServer.bind(WebSocketMessageType.sendWebRtcIceCandidate, (accountId, data) => {