[IMP] mail: improve handling of HTMLMediaElement errors

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) <aku@odoo.com>
This commit is contained in:
Thanh Dodeur
2023-04-06 16:12:10 +02:00
parent d270fbda24
commit 4d1e6481db
7 changed files with 64 additions and 32 deletions
@@ -9,6 +9,8 @@
<div><span class="fw-bolder">ICE: </span><t t-esc="props.rtcSession.iceState"/></div>
<div><span class="fw-bolder">DTLS: </span><t t-esc="props.rtcSession.dtlsState"/></div>
<div><span class="fw-bolder">Data channel: </span><t t-esc="props.rtcSession.dataChannelState"/></div>
<div t-if="props.rtcSession.audioError"><span class="fw-bolder">Audio player: </span><t t-esc="props.rtcSession.audioError"/></div>
<div t-if="props.rtcSession.videoError"><span class="fw-bolder">Video player: </span><t t-esc="props.rtcSession.videoError"/></div>
<hr class="w-100 border-top"/>
<div><span class="fw-bolder">ICE gathering: </span><t t-esc="props.rtcSession.iceGatheringState"/></div>
<div><span class="fw-bolder">From peer: </span><t t-esc="inboundConnectionTypeText"/></div>
@@ -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
*/
@@ -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;
}
}
@@ -29,7 +29,7 @@
</div>
<t t-if="rtcSession">
<!-- overlay -->
<span class="o-mail-CallParticipantCard-overlay o-mail-CallParticipantCard-overlayBottom position-absolute bottom-0 start-0 d-flex overflow-hidden">
<span class="o-mail-CallParticipantCard-overlay o-mail-CallParticipantCard-overlayBottom z-index-1 position-absolute bottom-0 start-0 d-flex overflow-hidden">
<span t-if="!props.minimized" class="p-1 rounded-1 bg-black-75 text-truncate" t-esc="name"/>
<small t-if="rtcSession.isScreenSharingOn and props.minimized and !isOfActiveCall" class="user-select-none o-minimized rounded-pill text-bg-danger d-flex align-items-center fw-bolder" title="live" aria-label="live">
LIVE
@@ -45,8 +45,9 @@
<span t-if="rtcSession.isDeaf" class="d-flex flex-column justify-content-center me-1 rounded-circle bg-900" t-att-class="{'o-minimized p-1': props.minimized, 'p-2': !props.minimized }" title="deaf" aria-label="deaf">
<i class="fa fa-deaf"/>
</span>
<span t-if="isOfActiveCall and rtcSession.isAudioInError" class="d-flex flex-column justify-content-center me-1 p-2 rounded-circle bg-900 text-danger" title="Issue with audio">
<i class="fa fa-exclamation-triangle"/>
<span t-if="hasMediaError" class="o-mail-CallParticipantCard-overlay-replayButton d-flex flex-column justify-content-center me-1 p-2 rounded-circle" title="media player Error" t-on-click.stop="onClickReplay">
<i t-if="rootHover.isHover" class="fa fa-repeat text-danger"/>
<i t-else="" class="fa fa-exclamation-triangle text-danger"/>
</span>
<span t-if="showConnectionState" class="d-flex flex-column justify-content-center me-1 p-2 rounded-circle bg-900" t-att-title="rtcSession.connectionState">
<i class="fa fa-exclamation-triangle text-warning"/>
@@ -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();
}
}
+9 -18
View File
@@ -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;
@@ -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;