diff --git a/addons/mail/static/src/components/rtc_option_list/rtc_option_list.xml b/addons/mail/static/src/components/rtc_option_list/rtc_option_list.xml
index f7915f74129..5b3ddc768f8 100644
--- a/addons/mail/static/src/components/rtc_option_list/rtc_option_list.xml
+++ b/addons/mail/static/src/components/rtc_option_list/rtc_option_list.xml
@@ -24,6 +24,12 @@
Settings
+
+
+
diff --git a/addons/mail/static/src/models/rtc/rtc.js b/addons/mail/static/src/models/rtc/rtc.js
index e001b9df4e6..146dc25e1d8 100644
--- a/addons/mail/static/src/models/rtc/rtc.js
+++ b/addons/mail/static/src/models/rtc/rtc.js
@@ -1,15 +1,5 @@
/** @odoo-module **/
-function temporaryLog(message, { trace } = {}) {
- if (trace) {
- console.groupCollapsed(message);
- console.trace(trace);
- console.groupEnd();
- return;
- }
- console.log(message);
-}
-
import { browser } from "@web/core/browser/browser";
import { registerNewModel } from '@mail/model/model_core';
@@ -118,6 +108,7 @@ function factory(dependencies) {
}
for (const token of Object.keys(this._peerConnections)) {
if (!currentSessionsTokens.has(token)) {
+ this._addLogEntry(token, 'session removed from the server');
this._removePeer(token);
}
}
@@ -164,24 +155,23 @@ function factory(dependencies) {
}
switch (event) {
case "offer":
- temporaryLog(`RECEIVED NOTIFICATION: ${event} from: ${rtcSession.name}`);
+ this._addLogEntry(sender, `received notification: ${event}`, { step: 'received offer' });
await this._handleRtcTransactionOffer(sender, payload);
break;
case "answer":
- temporaryLog(`RECEIVED NOTIFICATION: ${event} from: ${rtcSession.name}`);
+ this._addLogEntry(sender, `received notification: ${event}`, { step: 'received answer' });
await this._handleRtcTransactionAnswer(sender, payload);
break;
case "ice-candidate":
await this._handleRtcTransactionICECandidate(sender, payload);
break;
case "disconnect":
+ this._addLogEntry(sender, `received notification: ${event}`, { step: ' peer cleanly disconnected ' });
this._removePeer(sender);
break;
case 'trackChange':
this._handleTrackChange(rtcSession, payload);
break;
- case 'onOpenDataChannel':
- temporaryLog(`DATA CHANNEL: onOpenDataChannel - peer connection with ${sender} established`);
}
}
@@ -235,6 +225,7 @@ function factory(dependencies) {
this.update({
currentRtcSession: clear(),
+ logs: clear(),
sendUserVideo: clear(),
sendDisplay: clear(),
videoTrack: clear(),
@@ -368,6 +359,33 @@ function factory(dependencies) {
// Private
//----------------------------------------------------------------------
+ /**
+ * @private
+ * @param {String} token
+ * @param {String} entry
+ * @param {Object} [param2]
+ * @param {String} [param2.step] current step of the flow
+ * @param {String} [param2.state] current state of the connection
+ */
+ _addLogEntry(token, entry, { step, state } = {}) {
+ if (!this.env.isDebug()) {
+ return;
+ }
+ if (!(token in this.logs)) {
+ this.logs[token] = { step: '', state: '', logs: [] };
+ }
+ this.logs[token].logs.push({
+ event: `${window.moment().format('h:mm:ss')}: ${entry}`,
+ trace: window.Error().stack.split('\n').slice(1, 15),
+ });
+ if (step) {
+ this.logs[token].step = step;
+ }
+ if (state) {
+ this.logs[token].state = state;
+ }
+ }
+
/**
* @private
* @param {String} token
@@ -397,8 +415,9 @@ function factory(dependencies) {
continue;
}
session.update({
- connectionState: 'Disconnected: sending initial RTC offer',
+ connectionState: 'Not connected: sending initial RTC offer',
});
+ this._addLogEntry(session.peerToken, 'init call', { step: 'init call' });
this._callPeer(session.peerToken);
}
}
@@ -419,6 +438,7 @@ function factory(dependencies) {
*/
_createPeerConnection(token) {
const peerConnection = new window.RTCPeerConnection({ iceServers: this.iceServers });
+ this._addLogEntry(token, `RTCPeerConnection created`, { step: 'peer connection created' });
peerConnection.onicecandidate = async (event) => {
if (!event.candidate) {
return;
@@ -429,26 +449,26 @@ function factory(dependencies) {
});
};
peerConnection.oniceconnectionstatechange = (event) => {
- temporaryLog('ICE STATE UPDATE: ' + peerConnection.iceConnectionState);
this._onICEConnectionStateChange(peerConnection.iceConnectionState, token);
};
peerConnection.onconnectionstatechange = (event) => {
- temporaryLog('CONNECTION STATE UPDATE:' + peerConnection.connectionState);
this._onConnectionStateChange(peerConnection.connectionState, token);
};
peerConnection.onicecandidateerror = async (error) => {
- temporaryLog('=== ERROR: onIceCandidate ===', { trace: error });
+ this._addLogEntry(token, `ice candidate error`);
this._recoverConnection(token, { delay: this.recoveryTimeout, reason: 'ice candidate error' });
};
peerConnection.onnegotiationneeded = async (event) => {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
+ this._addLogEntry(token, `sending notification: offer`, { step: 'sending offer' });
await this._notifyPeers([token], {
event: 'offer',
payload: { sdp: peerConnection.localDescription },
});
};
peerConnection.ontrack = ({ transceiver, track }) => {
+ this._addLogEntry(token, `received ${track.kind} track`);
this._updateExternalSessionTrack(track, token);
};
const dataChannel = peerConnection.createDataChannel("notifications", { negotiated: true, id: 1 });
@@ -456,10 +476,6 @@ function factory(dependencies) {
this.handleNotification(token, event.data);
};
dataChannel.onopen = async () => {
- await this._notifyPeers([token], {
- event: 'onOpenDataChannel',
- type: 'peerToPeer',
- });
/**
* FIXME? it appears that the track yielded by the peerConnection's 'ontrack' event is always enabled,
* even when it is disabled on the sender-side.
@@ -508,6 +524,7 @@ function factory(dependencies) {
try {
await peerConnection.setRemoteDescription(rtcSessionDescription);
} catch (e) {
+ this._addLogEntry(fromToken, 'answer handling: Failed at setting remoteDescription');
// ignored the transaction may have been resolved by another concurrent offer.
}
}
@@ -527,7 +544,7 @@ function factory(dependencies) {
try {
await peerConnection.addIceCandidate(rtcIceCandidate);
} catch (error) {
- temporaryLog('=== ERROR: ADD ICE CANDIDATE ===', { trace: error });
+ this._addLogEntry(fromToken, 'ICE candidate handling: failed at adding the candidate to the connection');
this._recoverConnection(fromToken, { delay: this.recoveryTimeout, reason: 'failed at adding ice candidate' });
}
}
@@ -552,16 +569,16 @@ function factory(dependencies) {
try {
await peerConnection.setRemoteDescription(rtcSessionDescription);
} catch (e) {
- // ignored the transaction may have been resolved by another concurrent offer.
+ this._addLogEntry(fromToken, 'offer handling: failed at setting remoteDescription');
return;
}
- await peerConnection.setRemoteDescription(rtcSessionDescription);
await this._updateRemoteTrack(peerConnection, 'audio', { token: fromToken });
await this._updateRemoteTrack(peerConnection, 'video', { token: fromToken });
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
+ this._addLogEntry(fromToken, `sending notification: answer`, { step: 'sending answer' });
await this._notifyPeers([fromToken], {
event: 'answer',
payload: { sdp: peerConnection.localDescription },
@@ -613,7 +630,6 @@ function factory(dependencies) {
});
if (type === 'server') {
- temporaryLog(`SEND NOTIFICATION: - ${event} to: [${targetTokens}]`);
await this.env.services.rpc({
route: '/mail/rtc/session/notify_call_members',
params: {
@@ -664,8 +680,7 @@ function factory(dependencies) {
if (['connected', 'closed'].includes(peerConnection.connectionState)) {
return;
}
-
- temporaryLog(`RECOVERY: calling back ${token} to salvage the connection ${peerConnection.iceConnectionState}, reason: ${reason}`);
+ this._addLogEntry(token, `calling back to recover ${peerConnection.iceConnectionState} connection, reason: ${reason}`);
await this._notifyPeers([token], {
event: 'disconnect',
});
@@ -697,6 +712,7 @@ function factory(dependencies) {
delete this._fallBackTimeouts[token];
this._outGoingCallTokens.delete(token);
+ this._addLogEntry(token, 'peer removed', { step: 'peer removed' });
}
/**
@@ -925,6 +941,7 @@ function factory(dependencies) {
* @param {String} [param2.token]
*/
async _updateRemoteTrack(peerConnection, trackKind, { initTransceiver, token } = {}) {
+ this._addLogEntry(token, `updating ${trackKind} transceiver`);
const track = trackKind === 'audio' ? this.audioTrack : this.videoTrack;
const fullDirection = track ? 'sendrecv' : 'recvonly';
const limitedDirection = track ? 'sendonly' : 'inactive';
@@ -976,6 +993,7 @@ function factory(dependencies) {
* @param {String} token of the peer whose the connection changed
*/
async _onConnectionStateChange(state, token) {
+ this._addLogEntry(token, `connection state changed: ${state}`);
switch (state) {
case "closed":
this._removePeer(token);
@@ -993,6 +1011,7 @@ function factory(dependencies) {
* @param {String} token of the peer whose the connection changed
*/
async _onICEConnectionStateChange(connectionState, token) {
+ this._addLogEntry(token, `ICE connection state changed: ${connectionState}`, { state: connectionState });
const rtcSession = this.messaging.models['mail.rtc_session'].findFromIdentifyingData({ id: token });
if (!rtcSession) {
return;
@@ -1099,13 +1118,20 @@ function factory(dependencies) {
],
}),
/**
- * How long to wait before considering a connection as needing recovery.
+ * Contains the logs of the current session by token.
+ * { token: { name, logs } }
+ */
+ logs: attr({
+ default: {},
+ }),
+ /**
+ * How long to wait before considering a connection as needing recovery in ms.
*/
recoveryTimeout: attr({
default: 15000,
}),
/**
- * How long to wait before recovering a connection that has failed.
+ * How long to wait before recovering a connection that has failed in ms.
*/
recoveryDelay: attr({
default: 3000,
diff --git a/addons/mail/static/src/models/rtc_option_list/rtc_option_list.js b/addons/mail/static/src/models/rtc_option_list/rtc_option_list.js
index e9701b9846b..cf6469880c0 100644
--- a/addons/mail/static/src/models/rtc_option_list/rtc_option_list.js
+++ b/addons/mail/static/src/models/rtc_option_list/rtc_option_list.js
@@ -12,6 +12,7 @@ function factory(dependencies) {
*/
_created() {
super._created();
+ this.onClickDownloadLogs = this.onClickDownloadLogs.bind(this);
this.onClickActivateFullScreen = this.onClickActivateFullScreen.bind(this);
this.onClickDeactivateFullScreen = this.onClickDeactivateFullScreen.bind(this);
this.onClickLayout = this.onClickLayout.bind(this);
@@ -22,6 +23,27 @@ function factory(dependencies) {
// Public
//----------------------------------------------------------------------
+ /**
+ * Creates and download a file that contains the logs of the current RTC call.
+ *
+ * @param {MouseEvent} ev
+ */
+ async onClickDownloadLogs(ev) {
+ const channel = this.rtcController.callViewer.threadView.thread;
+ if (!channel.mailRtc) {
+ return;
+ }
+ const data = window.JSON.stringify(channel.mailRtc.logs);
+ const blob = new window.Blob([data], { type: 'application/json' });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `RtcLogs_Channel${channel.id}_Session${channel.mailRtc.currentRtcSession.id}_${window.moment().format('YYYY-MM-DD_HH-mm')}.json`;
+ a.click();
+ window.URL.revokeObjectURL(url);
+ this.component.trigger('o-popover-close');
+ }
+
/**
* @param {MouseEvent} ev
*/