diff --git a/addons/mail/static/src/components/rtc_call_participant_card/rtc_call_participant_card.xml b/addons/mail/static/src/components/rtc_call_participant_card/rtc_call_participant_card.xml
index 530f80a6b2f..4fdddb3c7af 100644
--- a/addons/mail/static/src/components/rtc_call_participant_card/rtc_call_participant_card.xml
+++ b/addons/mail/static/src/components/rtc_call_participant_card/rtc_call_participant_card.xml
@@ -64,7 +64,7 @@
-
+
diff --git a/addons/mail/static/src/models/rtc.js b/addons/mail/static/src/models/rtc.js
index 3f1f50a1e44..ce9c0bfeb35 100644
--- a/addons/mail/static/src/models/rtc.js
+++ b/addons/mail/static/src/models/rtc.js
@@ -4,7 +4,7 @@ import { browser } from "@web/core/browser/browser";
import { registerModel } from '@mail/model/model_core';
import { attr, many, one } from '@mail/model/model_field';
-import { clear, insert, replace, unlink } from '@mail/model/model_field_command';
+import { clear, insert, insertAndReplace, replace, unlink } from '@mail/model/model_field_command';
import { monitorAudio } from '@mail/utils/media_monitoring';
/**
@@ -26,14 +26,6 @@ registerModel({
identifyingFields: ['messaging'],
lifecycleHooks: {
_created() {
- /**
- /**
- * Object { token: peerConnection }
- * Contains the RTCPeerConnection established with the other rtc sessions.
- * Exposing this field and keeping references to closed peer connections may lead
- * to difficulties reconnecting to the same peer.
- */
- this._peerConnections = {};
/**
* timeoutId for the push to talk release delay.
*/
@@ -62,14 +54,14 @@ registerModel({
* @param {RtcSession[]} currentSessions list of sessions of this call.
*/
async filterCallees(currentSessions) {
- const currentSessionsTokens = new Set(currentSessions.map(session => session.peerToken));
- for (const token of Object.keys(this._peerConnections)) {
- if (!currentSessionsTokens.has(token)) {
- this._addLogEntry(token, 'session removed from the server');
- this._removePeer(token);
+ const currentSessionsTokens = new Set(currentSessions.map(session => session.id));
+ for (const rtcSession of this.connectedRtcSessions) {
+ if (!currentSessionsTokens.has(rtcSession.id)) {
+ this._addLogEntry(rtcSession.id, 'session removed from the server');
+ this._removePeer(rtcSession.id);
}
}
- if (this.channel && this.currentRtcSession && !currentSessionsTokens.has(this.currentRtcSession.peerToken)) {
+ if (this.channel && this.currentRtcSession && !currentSessionsTokens.has(this.currentRtcSession.id)) {
// if the current RTC session is not in the channel sessions, this call is no longer valid.
this.channel.endCall();
}
@@ -81,14 +73,14 @@ registerModel({
*/
filterIncomingVideoTraffic(allowedTokens) {
const tokenSet = new Set(allowedTokens);
- for (const [token, peerConnection] of Object.entries(this._peerConnections)) {
+ for (const rtcSession of this.connectedRtcSessions) {
const fullDirection = this.videoTrack ? 'sendrecv' : 'recvonly';
const limitedDirection = this.videoTrack ? 'sendonly' : 'inactive';
- const transceiver = this._getTransceiver(peerConnection, 'video');
+ const transceiver = this._getTransceiver(rtcSession.rtcPeerConnection.peerConnection, 'video');
if (!transceiver) {
continue;
}
- if (!tokenSet.size || tokenSet.has(token)) {
+ if (!tokenSet.size || tokenSet.has(rtcSession.id)) {
transceiver.direction = fullDirection;
} else {
transceiver.direction = limitedDirection;
@@ -109,7 +101,7 @@ registerModel({
if (!this.isClientRtcCompatible) {
return;
}
- if (!this._peerConnections[sender] && (!channelId || !this.channel || channelId !== this.channel.id)) {
+ if (!rtcSession.rtcPeerConnection && (!channelId || !this.channel || channelId !== this.channel.id)) {
return;
}
switch (event) {
@@ -167,11 +159,9 @@ registerModel({
* @private
*/
reset() {
- if (this._peerConnections) {
- const peerTokens = Object.keys(this._peerConnections);
- for (const token of peerTokens) {
- this._removePeer(Number(token));
- }
+ for (const rtcSession of this.connectedRtcSessions) {
+ this._removePeer(rtcSession.id);
+ rtcSession.update({ rtcPeerConnection: clear() });
}
if (this.disconnectAudioMonitor) {
@@ -180,8 +170,6 @@ registerModel({
this.audioTrack && this.audioTrack.stop();
this.videoTrack && this.videoTrack.stop();
- this._peerConnections = {};
-
for (const rtcSession of this.messaging.models['RtcSession'].all()) {
this.messaging.browser.clearTimeout(rtcSession.connectionRecoveryTimeout);
rtcSession.update({
@@ -281,8 +269,8 @@ registerModel({
audioTrack.enabled = !this.currentRtcSession.isMute && this.currentRtcSession.isTalking;
this.update({ audioTrack });
await this.async(() => this.updateVoiceActivation());
- for (const [token, peerConnection] of Object.entries(this._peerConnections)) {
- await this._updateRemoteTrack(peerConnection, 'audio', { sessionId: Number(token) });
+ for (const rtcSession of this.connectedRtcSessions) {
+ await this._updateRemoteTrack(rtcSession.rtcPeerConnection.peerConnection, 'audio', { token: rtcSession.id });
}
}
},
@@ -369,9 +357,9 @@ registerModel({
* @param {RtcSession} rtcSession
*/
async _callPeer(rtcSession) {
- const peerConnection = this._createPeerConnection(rtcSession);
+ this._createPeerConnection(rtcSession);
for (const trackKind of TRANSCEIVER_ORDER) {
- await this._updateRemoteTrack(peerConnection, trackKind, { initTransceiver: true, sessionId: rtcSession.id });
+ await this._updateRemoteTrack(rtcSession.rtcPeerConnection.peerConnection, trackKind, { initTransceiver: true, sessionId: rtcSession.id });
}
rtcSession.update({ isCurrentUserInitiatorOfConnectionOffer: true });
},
@@ -385,7 +373,7 @@ registerModel({
return;
}
for (const rtcSession of this.channel.rtcSessions) {
- if (rtcSession.peerToken in this._peerConnections) {
+ if (rtcSession.rtcPeerConnection && rtcSession.rtcPeerConnection.peerConnection) {
continue;
}
if (rtcSession === this.currentRtcSession) {
@@ -417,6 +405,7 @@ registerModel({
*
* @private
* @param {RtcSession} rtcSession
+ * @returns {RtcSession}
*/
_createPeerConnection(rtcSession) {
const peerConnection = new window.RTCPeerConnection({ iceServers: this.iceServers });
@@ -487,12 +476,12 @@ registerModel({
this._addLogEntry(rtcSession.id, `failed to send on datachannel; dataChannelInfo: ${this._serializeRTCDataChannel(dataChannel)}`, { error: e });
}
};
- this._peerConnections[rtcSession.id] = peerConnection;
+ rtcSession.update({ rtcPeerConnection: insertAndReplace({ peerConnection }) });
this.messaging.models['RtcDataChannel'].insert({
dataChannel,
rtcSession: replace(rtcSession),
});
- return peerConnection;
+ return rtcSession;
},
/**
* @private
@@ -511,17 +500,21 @@ registerModel({
* @param {Object} param1.sdp Session Description Protocol
*/
async _handleRtcTransactionAnswer(rtcSession, { sdp }) {
- const peerConnection = this._peerConnections[rtcSession.id];
- if (!peerConnection || this.invalidIceConnectionStates.has(peerConnection.iceConnectionState) || peerConnection.signalingState === 'stable') {
+ const rtcPeerConnection = rtcSession.rtcPeerConnection;
+ if (
+ !rtcPeerConnection ||
+ this.invalidIceConnectionStates.has(rtcPeerConnection.peerConnection.iceConnectionState) ||
+ rtcPeerConnection.peerConnection.signalingState === 'stable'
+ ) {
return;
}
- if (peerConnection.signalingState === 'have-remote-offer') {
+ if (rtcPeerConnection.peerConnection.signalingState === 'have-remote-offer') {
// we already have an offer
return;
}
const rtcSessionDescription = new window.RTCSessionDescription(sdp);
try {
- await peerConnection.setRemoteDescription(rtcSessionDescription);
+ await rtcPeerConnection.peerConnection.setRemoteDescription(rtcSessionDescription);
} catch (e) {
this._addLogEntry(rtcSession.id, 'answer handling: Failed at setting remoteDescription', { error: e });
// ignored the transaction may have been resolved by another concurrent offer.
@@ -534,13 +527,13 @@ registerModel({
* @param {Object} param1.candidate RTCIceCandidateInit
*/
async _handleRtcTransactionICECandidate(rtcSession, { candidate }) {
- const peerConnection = this._peerConnections[rtcSession.id];
- if (!peerConnection || this.invalidIceConnectionStates.has(peerConnection.iceConnectionState)) {
+ const rtcPeerConnection = rtcSession.rtcPeerConnection;
+ if (!rtcPeerConnection || this.invalidIceConnectionStates.has(rtcPeerConnection.peerConnection.iceConnectionState)) {
return;
}
const rtcIceCandidate = new window.RTCIceCandidate(candidate);
try {
- await peerConnection.addIceCandidate(rtcIceCandidate);
+ await rtcPeerConnection.peerConnection.addIceCandidate(rtcIceCandidate);
} catch (error) {
this._addLogEntry(rtcSession.id, 'ICE candidate handling: failed at adding the candidate to the connection', { error });
this._recoverConnection(rtcSession, { delay: this.recoveryTimeout, reason: 'failed at adding ice candidate' });
@@ -553,34 +546,35 @@ registerModel({
* @param {Object} param1.sdp Session Description Protocol
*/
async _handleRtcTransactionOffer(rtcSession, { sdp }) {
- const peerConnection = this._peerConnections[rtcSession.id] || this._createPeerConnection(rtcSession);
-
- if (!peerConnection || this.invalidIceConnectionStates.has(peerConnection.iceConnectionState)) {
+ if (!rtcSession.rtcPeerConnection) {
+ this._createPeerConnection(rtcSession)
+ }
+ if (!rtcSession.rtcPeerConnection || this.invalidIceConnectionStates.has(rtcSession.rtcPeerConnection.peerConnection.iceConnectionState)) {
return;
}
- if (peerConnection.signalingState === 'have-remote-offer') {
+ if (rtcSession.rtcPeerConnection.peerConnection.signalingState === 'have-remote-offer') {
// we already have an offer
return;
}
const rtcSessionDescription = new window.RTCSessionDescription(sdp);
try {
- await peerConnection.setRemoteDescription(rtcSessionDescription);
+ await rtcSession.rtcPeerConnection.peerConnection.setRemoteDescription(rtcSessionDescription);
} catch (e) {
this._addLogEntry(rtcSession.id, 'offer handling: failed at setting remoteDescription', { error: e });
return;
}
- await this._updateRemoteTrack(peerConnection, 'audio', { sessionId: rtcSession.id });
- await this._updateRemoteTrack(peerConnection, 'video', { sessionId: rtcSession.id });
+ await this._updateRemoteTrack(rtcSession.rtcPeerConnection.peerConnection, 'audio', { sessionId: rtcSession.id });
+ await this._updateRemoteTrack(rtcSession.rtcPeerConnection.peerConnection, 'video', { sessionId: rtcSession.id });
let answer;
try {
- answer = await peerConnection.createAnswer();
+ answer = await rtcSession.rtcPeerConnection.peerConnection.createAnswer();
} catch (e) {
this._addLogEntry(rtcSession.id, 'offer handling: failed at creating answer', { error: e });
return;
}
try {
- await peerConnection.setLocalDescription(answer);
+ await rtcSession.rtcPeerConnection.peerConnection.setLocalDescription(answer);
} catch (e) {
this._addLogEntry(rtcSession.id, 'offer handling: failed at setting localDescription', { error: e });
return;
@@ -589,7 +583,7 @@ registerModel({
this._addLogEntry(rtcSession.id, `sending notification: answer`, { step: 'sending answer' });
await this._notifyPeers([rtcSession.id], {
event: 'answer',
- payload: { sdp: peerConnection.localDescription },
+ payload: { sdp: rtcSession.rtcPeerConnection.peerConnection.localDescription },
});
this._recoverConnection(rtcSession, { delay: this.recoveryTimeout, reason: 'standard answer timeout' });
},
@@ -673,17 +667,17 @@ registerModel({
*/
async _onRecoverConnectionTimeout(rtcSession, reason) {
rtcSession.update({ connectionRecoveryTimeout: clear() });
- const peerConnection = this._peerConnections[rtcSession.id];
- if (!peerConnection || !this.channel) {
+ const rtcPeerConnection = rtcSession.rtcPeerConnection;
+ if (!rtcPeerConnection || !this.channel) {
return;
}
if (rtcSession.isCurrentUserInitiatorOfConnectionOffer) {
return;
}
- if (peerConnection.iceConnectionState === 'connected') {
+ if (rtcPeerConnection.peerConnection.iceConnectionState === 'connected') {
return;
}
- this._addLogEntry(rtcSession.id, `calling back to recover ${peerConnection.iceConnectionState} connection, reason: ${reason}`);
+ this._addLogEntry(rtcSession.id, `calling back to recover ${rtcPeerConnection.peerConnection.iceConnectionState} connection, reason: ${reason}`);
await this._notifyPeers([rtcSession.id], {
event: 'disconnect',
});
@@ -743,12 +737,11 @@ registerModel({
rtcDataChannel.delete();
}
}
- const peerConnection = this._peerConnections[sessionId];
- if (peerConnection) {
- this._removeRemoteTracks(peerConnection);
- peerConnection.close();
+ const rtcPeerConnection = rtcSession.rtcPeerConnection;
+ if (rtcPeerConnection) {
+ this._removeRemoteTracks(rtcPeerConnection.peerConnection);
+ rtcPeerConnection.delete();
}
- delete this._peerConnections[sessionId];
this.messaging.models['RtcSession'].insert({
id: sessionId,
isCurrentUserInitiatorOfConnectionOffer: clear(),
@@ -759,7 +752,7 @@ registerModel({
* Terminates the Transceivers of the peer connection.
*
* @private
- * @param {RTCPeerConnection} peerConnection
+ * @param {web.RTCPeerConnection} peerConnection
*/
_removeRemoteTracks(peerConnection) {
const RTCRtpSenders = peerConnection.getSenders();
@@ -893,8 +886,8 @@ registerModel({
return;
}
await this._toggleLocalVideoTrack(trackOptions);
- for (const [token, peerConnection] of Object.entries(this._peerConnections)) {
- await this._updateRemoteTrack(peerConnection, 'video', { sessionId: Number(token) });
+ for (const rtcSession of this.connectedRtcSessions) {
+ await this._updateRemoteTrack(rtcSession.rtcPeerConnection.peerConnection, 'video', { sessionId: rtcSession.id });
}
if (!this.currentRtcSession) {
return;
@@ -963,7 +956,7 @@ registerModel({
return;
}
this.audioTrack.enabled = !this.currentRtcSession.isMute && this.currentRtcSession.isTalking;
- await this._notifyPeers(Object.keys(this._peerConnections), {
+ await this._notifyPeers(this.connectedRtcSessions.map(rtcSession => rtcSession.id), {
event: 'trackChange',
type: 'peerToPeer',
payload: {
@@ -1216,11 +1209,18 @@ registerModel({
channel: one('Thread', {
inverse: 'rtc',
}),
+ /**
+ * Contains the RTC Session that are connected.
+ * Connected RTC Sessions have rtcPeerConnection set.
+ */
+ connectedRtcSessions: many('RtcSession', {
+ inverse: 'rtcAsConnectedSession',
+ }),
/**
* String, peerToken of the current session used to identify him during the peer-to-peer transactions.
*/
currentRtcSession: one('RtcSession', {
- inverse: 'rtc',
+ inverse: 'rtcAsCurrentSession',
}),
/**
* callback to properly end the audio monitoring.
diff --git a/addons/mail/static/src/models/rtc_peer_connection.js b/addons/mail/static/src/models/rtc_peer_connection.js
new file mode 100644
index 00000000000..bec2e011713
--- /dev/null
+++ b/addons/mail/static/src/models/rtc_peer_connection.js
@@ -0,0 +1,26 @@
+/** @odoo-module **/
+
+import { registerModel } from '@mail/model/model_core';
+import { attr, one } from '@mail/model/model_field';
+
+registerModel({
+ name: 'RtcPeerConnection',
+ identifyingFields: ['rtcSession'],
+ lifecycleHooks: {
+ _willDelete() {
+ this.peerConnection.close();
+ }
+ },
+ fields: {
+ /**
+ * Contains the browser.RTCPeerConnection instance of this RTC Session.
+ * If unset, this RTC Session is not considered as connected
+ */
+ peerConnection: attr(),
+ rtcSession: one('RtcSession', {
+ inverse: 'rtcPeerConnection',
+ readonly: true,
+ required: true,
+ }),
+ },
+});
diff --git a/addons/mail/static/src/models/rtc_session.js b/addons/mail/static/src/models/rtc_session.js
index b9ab8acf73a..fcbe55421e4 100644
--- a/addons/mail/static/src/models/rtc_session.js
+++ b/addons/mail/static/src/models/rtc_session.js
@@ -2,7 +2,7 @@
import { registerModel } from '@mail/model/model_core';
import { attr, many, one } from '@mail/model/model_field';
-import { clear } from '@mail/model/model_field_command';
+import { clear, replace } from '@mail/model/model_field_command';
registerModel({
name: 'RtcSession',
@@ -126,13 +126,13 @@ registerModel({
* of the current partner.
*/
async toggleDeaf() {
- if (!this.rtc) {
+ if (!this.rtcAsCurrentSession) {
return;
}
- if (this.rtc.currentRtcSession.isDeaf) {
- await this.rtc.undeafen();
+ if (this.messaging.rtc.currentRtcSession.isDeaf) {
+ await this.messaging.rtc.undeafen();
} else {
- await this.rtc.deafen();
+ await this.messaging.rtc.deafen();
}
},
/**
@@ -141,7 +141,7 @@ registerModel({
* @param {Object} data
*/
updateAndBroadcast(data) {
- if (!this.rtc) {
+ if (!this.rtcAsCurrentSession) {
return;
}
this.update(data);
@@ -195,6 +195,19 @@ registerModel({
return this.guest.name;
}
},
+ /**
+ * @private
+ * @returns {FieldCommand}
+ */
+ _computeRtcAsConnectedSession() {
+ if (!this.messaging || !this.messaging.rtc) {
+ return clear();
+ }
+ if (this.rtcPeerConnection) {
+ return replace(this.messaging.rtc);
+ }
+ return clear();
+ },
/**
* @private
* @returns {string}
@@ -362,7 +375,7 @@ registerModel({
* This can be true for many sessions, as one user can have multiple
* sessions active across several tabs, browsers and devices.
* To determine if this session is the active session of this tab,
- * use this.rtc instead.
+ * use this.rtcAsCurrentSession instead.
*/
isOwnSession: attr({
compute: '_computeIsOwnSession',
@@ -408,9 +421,17 @@ registerModel({
* sessions from other channels with the same partner (sessions opened from different
* tabs or devices).
*/
- rtc: one('Rtc', {
+ rtcAsCurrentSession: one('Rtc', {
inverse: 'currentRtcSession',
}),
+ rtcAsConnectedSession: one('Rtc', {
+ compute: '_computeRtcAsConnectedSession',
+ inverse: 'connectedRtcSessions',
+ }),
+ rtcPeerConnection: one('RtcPeerConnection', {
+ inverse: 'rtcSession',
+ isCausal: true,
+ }),
/**
* Contains the RTCDataChannel of the rtc session.
*/