[IMP] mail: speed up time of RTC peers connection

Before this PR, the RPC to notify peers was called too much time too quickly.
Reducing request overload significantly improves the overall connection time.

closes odoo/odoo#76625

X-original-commit: f4c1d974b5d55868f81cd3259c45e83d934686f5
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Sébastien Theys
2021-09-16 07:18:31 +00:00
parent 3524b3c0db
commit 0a49d2cfa8
8 changed files with 191 additions and 66 deletions
+15 -15
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from collections import defaultdict
from datetime import datetime, timedelta
from odoo import http
@@ -374,25 +375,24 @@ class DiscussController(http.Controller):
# --------------------------------------------------------------------------
@http.route('/mail/rtc/session/notify_call_members', methods=['POST'], type="json", auth="public")
def session_call_notify(self, sender_session_id, target_session_ids=None, content=None):
def session_call_notify(self, peer_notifications):
""" Sends content to other session of the same channel, only works if the user is the user of that session.
This is used to send peer to peer information between sessions.
:param int sender_session_id: id of the session from which the content is sent
:param list target_session_ids: list of the ids of the sessions that should receive the content
:param content: the content to send to the other sessions
:param peer_notifications: list of tuple with the following elements:
- int sender_session_id: id of the session from which the content is sent
- list target_session_ids: list of the ids of the sessions that should receive the content
- string content: the content to send to the other sessions
"""
if request.env.user._is_public():
guest = request.env['mail.guest']._get_guest_from_request(request)
if guest:
session = guest.env['mail.channel.rtc.session'].sudo().browse(int(sender_session_id))
if session.exists() and session.guest_id == guest:
session._notify_peers(target_session_ids, content)
return
raise NotFound()
session = request.env['mail.channel.rtc.session'].sudo().browse(int(sender_session_id))
if session.exists() and session.partner_id == request.env.user.partner_id:
session._notify_peers(target_session_ids, content)
guest = request.env['mail.guest']._get_guest_from_request(request)
notifications_by_session = defaultdict(list)
for sender_session_id, target_session_ids, content in peer_notifications:
session_sudo = guest.env['mail.channel.rtc.session'].sudo().browse(int(sender_session_id)).exists()
if not session_sudo or (session_sudo.guest_id and session_sudo.guest_id != guest) or (session_sudo.partner_id and session_sudo.partner_id != request.env.user.partner_id):
continue
notifications_by_session[session_sudo].append(([int(sid) for sid in target_session_ids], content))
for session_sudo, notifications in notifications_by_session.items():
session_sudo._notify_peers(notifications)
@http.route('/mail/rtc/session/update_and_broadcast', methods=['POST'], type="json", auth="public")
def session_update_and_broadcast(self, session_id, values):
+15 -18
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
@@ -99,28 +100,24 @@ class MailRtcSession(models.Model):
self.unlink()
self.env['bus.bus'].sendmany(notifications)
def _notify_peers(self, target_session_ids, content):
def _notify_peers(self, notifications):
""" Used for peer-to-peer communication,
guarantees that the sender is the current guest or partner.
:param target_session_ids: a list of mail.channel.rtc.session ids
:param content: a dict with the content to be sent to the targets
:param notifications: list of tuple with the following elements:
- target_session_ids: a list of mail.channel.rtc.session ids
- content: a string with the content to be sent to the targets
"""
notifications = []
target_sessions = self.search([('id', 'in', [int(target) for target in target_session_ids]), ('channel_id', '=', self.channel_id.id)])
for session in target_sessions:
model, record_id = ('mail.guest', session.guest_id.id) if session.guest_id else ('res.partner', session.partner_id.id)
notifications.append([
(self._cr.dbname, model, record_id),
{
'type': 'rtc_peer_notification',
'payload': {
'sender': self.id,
'content': content,
},
},
])
return self.env['bus.bus'].sendmany(notifications)
self.ensure_one()
payload_by_target = defaultdict(lambda: {'sender': self.id, 'notifications': []})
for target_session_ids, content in notifications:
for target_session in self.env['mail.channel.rtc.session'].browse(target_session_ids).exists():
model, record_id = ('mail.guest', target_session.guest_id.id) if target_session.guest_id else ('res.partner', target_session.partner_id.id)
payload_by_target[(self._cr.dbname, model, record_id)]['notifications'].append(content)
return self.env['bus.bus'].sendmany([(target, {
'type': 'rtc_peer_notification',
'payload': payload,
}) for target, payload in payload_by_target.items()])
def _mail_rtc_session_format(self):
self.ensure_one()
@@ -60,6 +60,11 @@
<i class="fa fa-deaf"/>
</span>
</t>
<t t-if="callParticipantCard.rtcSession.channel.mailRtc and callParticipantCard.rtcSession.isAudioInError">
<span class="o_RtcCallParticipantCard_overlayTop_element text-danger" title="Issue with audio">
<i class="fa fa-exclamation-triangle"/>
</span>
</t>
<t t-if="callParticipantCard.rtcSession.channel.mailRtc and !callParticipantCard.rtcSession.mailRtc and !['connected', 'completed'].includes(callParticipantCard.rtcSession.connectionState)">
<span class="o_RtcCallParticipantCard_overlayTop_element" t-att-title="callParticipantCard.rtcSession.connectionState">
<i class="fa fa-lg fa-signal o_RtcCallParticipantCard_connectionState"/>
@@ -69,7 +69,7 @@ export class RtcVideo extends Component {
async _onVideoLoadedMetaData(ev) {
try {
await ev.target.play();
} catch (error) {
} catch (error) {
if (typeof error === 'object' && error.name === 'NotAllowedError') {
// Ignored as some browsers may reject play() calls that do not
// originate from a user input.
@@ -546,11 +546,13 @@ function factory(dependencies) {
/**
* @private
* @param {Object} data
* @param {string} [data.sender]
* @param {string} [data.content]
* @param {string} data.sender
* @param {string[]} data.notifications
*/
_handleNotificationRtcPeerToPeer({ sender, content }) {
this.messaging.mailRtc.handleNotification(sender, content);
_handleNotificationRtcPeerToPeer({ sender, notifications }) {
for (const content of notifications) {
this.messaging.mailRtc.handleNotification(sender, content);
}
}
/**
+84 -24
View File
@@ -3,8 +3,8 @@
import { browser } from "@web/core/browser/browser";
import { registerNewModel } from '@mail/model/model_core';
import { attr, one2one } from '@mail/model/model_field';
import { clear, insert } from '@mail/model/model_field_command';
import { attr, one2many, one2one } from '@mail/model/model_field';
import { clear, create, insert, unlink } from '@mail/model/model_field_command';
import { monitorAudio } from '@mail/utils/media_monitoring/media_monitoring';
@@ -622,21 +622,17 @@ function factory(dependencies) {
if (!targetTokens.length || !this.channel || !this.currentRtcSession) {
return;
}
const content = JSON.stringify({
event,
channelId: this.channel.id,
payload,
});
if (type === 'server') {
await this.env.services.rpc({
route: '/mail/rtc/session/notify_call_members',
params: {
sender_session_id: this.currentRtcSession.id,
target_session_ids: targetTokens,
content,
},
}, { shadow: true });
this.update({
peerNotificationsToSend: create({
channelId: this.channel.id,
event,
payload,
senderId: this.currentRtcSession.id,
targetTokens
}),
});
await this._sendPeerNotifications();
}
if (type === 'peerToPeer') {
@@ -645,7 +641,11 @@ function factory(dependencies) {
if (!dataChannel || dataChannel.readyState !== 'open') {
continue;
}
dataChannel.send(content);
dataChannel.send(JSON.stringify({
event,
channelId: this.channel.id,
payload,
}));
}
}
}
@@ -738,6 +738,50 @@ function factory(dependencies) {
}
}
/**
* Sends this peer notifications to send as soon as the last pending
* sending finishes.
*
* @private
*/
async _sendPeerNotifications() {
if (this.isNotifyPeersRPCInProgress) {
return;
}
this.update({ isNotifyPeersRPCInProgress: true });
await new Promise(resolve => setTimeout(resolve, this.peerNotificationWaitDelay));
const peerNotifications = this.peerNotificationsToSend;
try {
await this.env.services.rpc({
route: '/mail/rtc/session/notify_call_members',
params: {
'peer_notifications': peerNotifications.map(peerNotification =>
[
peerNotification.senderId,
peerNotification.targetTokens,
JSON.stringify({
event: peerNotification.event,
channelId: peerNotification.channelId,
payload: peerNotification.payload,
}),
],
),
},
}, { shadow: true });
if (!this.exists()) {
return;
}
this.update({ peerNotificationsToSend: unlink(peerNotifications) });
} finally {
if (this.exists()) {
this.update({ isNotifyPeersRPCInProgress: false });
if (this.peerNotificationsToSend.length > 0) {
this._sendPeerNotifications();
}
}
}
}
/**
* Updates the "isTalking" state of the current user and sets the
* enabled state of its audio track accordingly.
@@ -1095,13 +1139,6 @@ function factory(dependencies) {
currentRtcSession: one2one('mail.rtc_session', {
inverse: 'mailRtc',
}),
/**
* true if the browser supports webRTC
*/
isClientRtcCompatible: attr({
compute: '_computeIsClientRtcCompatible',
default: true,
}),
/**
* ICE servers used by RTCPeerConnection to retrieve the public IP address (STUN)
* or to relay packets when necessary (TURN).
@@ -1116,6 +1153,16 @@ function factory(dependencies) {
},
],
}),
/**
* true if the browser supports webRTC
*/
isClientRtcCompatible: attr({
compute: '_computeIsClientRtcCompatible',
default: true,
}),
isNotifyPeersRPCInProgress: attr({
default: false,
}),
/**
* Contains the logs of the current session by token.
* { token: { name<String>, logs<Array> } }
@@ -1123,6 +1170,19 @@ function factory(dependencies) {
logs: attr({
default: {},
}),
peerNotificationsToSend: one2many('mail.rtc_peer_notification', {
isCausal: true,
}),
/**
* Determines the delay to wait (in ms) before sending peer
* notifications to the server. Sending many notifications at once
* significantly increase the connection time because the server can't
* handle too many requests at once, but handles much faster one bigger
* request, even with a delay. The delay should however not be too high.
*/
peerNotificationWaitDelay: attr({
default: 50,
}),
/**
* How long to wait before considering a connection as needing recovery in ms.
*/
@@ -0,0 +1,38 @@
/** @odoo-module **/
import { registerNewModel } from '@mail/model/model_core';
import { attr } from '@mail/model/model_field';
function factory(dependencies) {
class RTCPeerNotification extends dependencies['mail.model'] {
}
RTCPeerNotification.fields = {
channelId: attr({
readonly: true,
required: true,
}),
event: attr({
readonly: true,
required: true,
}),
payload: attr({
readonly: true,
}),
senderId: attr({
readonly: true,
required: true,
}),
targetTokens: attr({
readonly: true,
required: true,
}),
};
RTCPeerNotification.modelName = 'mail.rtc_peer_notification';
return RTCPeerNotification;
}
registerNewModel('mail.rtc_peer_notification', factory);
@@ -71,7 +71,12 @@ function factory(dependencies) {
async setAudio({ audioStream, isMuted, isTalking }) {
this._removeAudio();
const audioElement = this.audioElement || new window.Audio();
audioElement.srcObject = audioStream;
try {
audioElement.srcObject = audioStream;
} catch (error) {
this.update({ isAudioInError: true });
console.error(error);
}
audioElement.load();
audioElement.volume = this.partner && this.partner.volumeSetting ? this.partner.volumeSetting.volume : this.volume;
audioElement.muted = this.messaging.mailRtc.currentRtcSession.isDeaf;
@@ -86,13 +91,20 @@ function factory(dependencies) {
});
try {
await audioElement.play();
} catch (error) {
if (!this.exists()) {
return;
}
this.update({ isAudioInError: false });
} catch (error) {
if (typeof error === 'object' && error.name === 'NotAllowedError') {
// Ignored as some browsers may reject play() calls that do not
// originate from a user input.
return;
}
throw error;
if (this.exists()) {
this.update({ isAudioInError: true });
}
console.error(error);
}
}
@@ -284,10 +296,15 @@ function factory(dependencies) {
}
if (this.audioElement) {
this.audioElement.pause();
this.audioElement.srcObject = undefined;
try {
this.audioElement.srcObject = undefined;
} catch (error) {
// ignore error during remove, the value will be overwritten at next usage anyway
}
}
this.update({
audioStream: clear(),
isAudioInError: false,
});
}
@@ -345,6 +362,12 @@ function factory(dependencies) {
calledChannels: one2many('mail.thread', {
inverse: 'rtcInvitingSession',
}),
/**
* States whether there is currently an error with the audio element.
*/
isAudioInError: attr({
default: false,
}),
/**
* Determines if the user is broadcasting a video from a user device (camera).
*/