From 4d1e6481db120d4caa7e7fee34cebcac82c8356f Mon Sep 17 00:00:00 2001 From: Thanh Dodeur Date: Mon, 3 Apr 2023 08:11:32 +0000 Subject: [PATCH] [IMP] mail: improve handling of `HTMLMediaElement` errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit improves the overlay icon that appears on call participant cards when an error in the media players occur. The icon is now clickable, which restarts an attempt to play all the `HTMLMediaElement`s of the call as a recoverable error can occur when the browser does not allow `HTMLMediaElement`s being played outside a user interaction. other related changes: * removing the `pointer-event:none` from the overlay as it was preventing hover events, and the title tooltip from appearing. * removing the try/catch around the `srcObject` assignment, as it should only crash on old browsers without support for `srcObject` for which the support of using the alternative (`URL.createObjectUrl` with `src`) is being dropped. closes odoo/odoo#117446 Signed-off-by: Alexandre Kühn (aku) --- .../mail/static/src/rtc/call_context_menu.xml | 2 ++ .../static/src/rtc/call_participant_card.js | 13 +++++++++ .../static/src/rtc/call_participant_card.scss | 11 +++++++- .../static/src/rtc/call_participant_card.xml | 7 ++--- .../static/src/rtc/call_participant_video.js | 21 ++++++++------- addons/mail/static/src/rtc/rtc_service.js | 27 +++++++------------ .../mail/static/src/rtc/rtc_session_model.js | 15 ++++++++++- 7 files changed, 64 insertions(+), 32 deletions(-) diff --git a/addons/mail/static/src/rtc/call_context_menu.xml b/addons/mail/static/src/rtc/call_context_menu.xml index b68b46c5db6..5174419a2af 100644 --- a/addons/mail/static/src/rtc/call_context_menu.xml +++ b/addons/mail/static/src/rtc/call_context_menu.xml @@ -9,6 +9,8 @@
ICE:
DTLS:
Data channel:
+
Audio player:
+
Video player:

ICE gathering:
From peer:
diff --git a/addons/mail/static/src/rtc/call_participant_card.js b/addons/mail/static/src/rtc/call_participant_card.js index d075e539a71..722cc666859 100644 --- a/addons/mail/static/src/rtc/call_participant_card.js +++ b/addons/mail/static/src/rtc/call_participant_card.js @@ -8,6 +8,7 @@ import { useService } from "@web/core/utils/hooks"; import { isEventHandled, markEventHandled } from "@mail/utils/misc"; import { usePopover } from "@web/core/popover/popover_hook"; import { useStore } from "@mail/core/messaging_hook"; +import { useHover } from "@mail/utils/hooks"; const HIDDEN_CONNECTION_STATES = new Set(["connected", "completed"]); @@ -24,6 +25,7 @@ export class CallParticipantCard extends Component { this.rpc = useService("rpc"); this.rtc = useRtc(); this.store = useStore(); + this.rootHover = useHover("root"); this.threadService = useService("mail.thread"); onMounted(() => { if (!this.rtcSession) { @@ -74,6 +76,13 @@ export class CallParticipantCard extends Component { return this.channelMember?.persona.name; } + get hasMediaError() { + return ( + this.isOfActiveCall && + Boolean(this.rtcSession?.videoError || this.rtcSession?.audioError) + ); + } + get hasVideo() { return Boolean(this.rtcSession?.videoStream); } @@ -106,6 +115,10 @@ export class CallParticipantCard extends Component { }); } + async onClickReplay() { + this.env.bus.trigger("RTC-SERVICE:PLAY_MEDIA"); + } + /** * @param {Event} ev */ diff --git a/addons/mail/static/src/rtc/call_participant_card.scss b/addons/mail/static/src/rtc/call_participant_card.scss index ba2d45a2c39..33d4bc48836 100644 --- a/addons/mail/static/src/rtc/call_participant_card.scss +++ b/addons/mail/static/src/rtc/call_participant_card.scss @@ -35,7 +35,6 @@ } .o-mail-CallParticipantCard-overlay { - pointer-events: none; margin: Min(5%, map-get($spacers, 2)); } @@ -56,3 +55,13 @@ .o-mail-CallParticipantCard-avatar:not(.o-minimized) { background-color: var(--o-mail-CallParticipantCard-avatarBgColor, #{$o-gray-700}); } + +.o-mail-CallParticipantCard-overlay-replayButton { + background-color: $o-gray-900; + &:hover { + background-color: $o-gray-700; + } + &:active { + background-color: $o-gray-800; + } +} diff --git a/addons/mail/static/src/rtc/call_participant_card.xml b/addons/mail/static/src/rtc/call_participant_card.xml index d68a55b0c65..4181584bd7c 100644 --- a/addons/mail/static/src/rtc/call_participant_card.xml +++ b/addons/mail/static/src/rtc/call_participant_card.xml @@ -29,7 +29,7 @@ - + LIVE @@ -45,8 +45,9 @@ - - + + + diff --git a/addons/mail/static/src/rtc/call_participant_video.js b/addons/mail/static/src/rtc/call_participant_video.js index 7958a454d18..eb91faa54c6 100644 --- a/addons/mail/static/src/rtc/call_participant_video.js +++ b/addons/mail/static/src/rtc/call_participant_video.js @@ -1,6 +1,6 @@ /* @odoo-module */ -import { Component, onMounted, onPatched, useRef } from "@odoo/owl"; +import { Component, onMounted, onPatched, useExternalListener, useRef } from "@odoo/owl"; import { useRtc } from "@mail/rtc/rtc_hook"; export class CallParticipantVideo extends Component { @@ -12,6 +12,9 @@ export class CallParticipantVideo extends Component { this.root = useRef("root"); onMounted(() => this._update()); onPatched(() => this._update()); + useExternalListener(this.env.bus, "RTC-SERVICE:PLAY_MEDIA", async () => { + await this.play(); + }); } _update() { @@ -26,16 +29,16 @@ export class CallParticipantVideo extends Component { this.root.el.load(); } - async onVideoLoadedMetaData(ev) { + async play() { try { - await ev.target.play(); + await this.root.el?.play?.(); + this.props.session.videoError = undefined; } 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; + this.props.session.videoError = error.name; } } + + async onVideoLoadedMetaData() { + await this.play(); + } } diff --git a/addons/mail/static/src/rtc/rtc_service.js b/addons/mail/static/src/rtc/rtc_service.js index c76da6b4e18..bc3a13cfb95 100644 --- a/addons/mail/static/src/rtc/rtc_service.js +++ b/addons/mail/static/src/rtc/rtc_service.js @@ -194,7 +194,11 @@ export class Rtc { } } ); - + this.env.bus.addEventListener("RTC-SERVICE:PLAY_MEDIA", () => { + for (const session of Object.values(this.state.channel.rtcSessions)) { + session.playAudio(); + } + }); browser.addEventListener("keydown", (ev) => { if ( !this.state.channel || @@ -926,7 +930,8 @@ export class Rtc { delete session.dtlsState; delete session.iceState; delete session.logStep; - session.isAudioInError = false; + delete session.audioError; + delete session.videoError; session.isRaisingHand = false; session.isTalking = false; this.removeVideoFromSession(session); @@ -1417,11 +1422,7 @@ export class Rtc { stream.addTrack(track); if (track.kind === "audio") { const audioElement = session.audioElement || new window.Audio(); - try { - audioElement.srcObject = stream; - } catch { - session.isAudioInError = true; - } + audioElement.srcObject = stream; audioElement.load(); audioElement.muted = mute; audioElement.volume = this.userSettingsService.getVolume(session); @@ -1432,17 +1433,7 @@ export class Rtc { session.audioStream = stream; session.isSelfMuted = false; session.isTalking = false; - try { - await audioElement.play(); - session.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; - } - session.isAudioInError = true; - } + await session.playAudio(); } if (track.kind === "video") { session.videoStream = stream; diff --git a/addons/mail/static/src/rtc/rtc_session_model.js b/addons/mail/static/src/rtc/rtc_session_model.js index 47e12eda5c8..ed4e237483d 100644 --- a/addons/mail/static/src/rtc/rtc_session_model.js +++ b/addons/mail/static/src/rtc/rtc_session_model.js @@ -18,7 +18,8 @@ export class RtcSession { audioStream; /** @type {RTCDataChannel} */ dataChannel; - isAudioInError; + audioError; + videoError; isRaisingHand; isTalking; localVolume; @@ -84,6 +85,18 @@ export class RtcSession { this.localVolume = value; } + async playAudio() { + if (!this.audioElement) { + return; + } + try { + await this.audioElement.play(); + this.audioError = undefined; + } catch (error) { + this.audioError = error.name; + } + } + async updateStats() { delete this.localCandidateType; delete this.remoteCandidateType;