bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/timer/indicator/component.jsx
Arthur B. Grossi dbbef569a9 fix(timer): autoplay
When user enters meeting with music already playing, an event listener is set
to play the music only after user interaction.
Also, to keep timer more cohesive and reduce complexity of the condition for
playing music, timer automatically stops after 90% of users notify that the
timer has expired.
2023-05-18 15:29:34 -03:00

296 lines
6.8 KiB
JavaScript

import React, { Component } from 'react';
import Icon from '/imports/ui/components/common/icon/component';
import TimerService from '/imports/ui/components/timer/service';
import Styled from './styles';
const CDN = Meteor.settings.public.app.cdn;
const BASENAME = Meteor.settings.public.app.basename;
const HOST = CDN + BASENAME;
const trackName = Meteor.settings.public.timer.music.track;
class Indicator extends Component {
constructor(props) {
super(props);
this.timeRef = React.createRef();
this.interval = null;
this.alarm = null;
this.music = null;
// We need to avoid trigger on mount
this.triggered = true;
this.updateTime = this.updateTime.bind(this);
}
componentDidMount() {
const { timer } = this.props;
const { running } = timer;
this.alarm = new Audio(`${HOST}/resources/sounds/alarm.mp3`);
this.setUpMusic();
this.triggered = this.initTriggered();
const { current } = this.timeRef;
if (current && running) {
this.interval = setInterval(this.updateTime, TimerService.getInterval());
}
}
componentDidUpdate(prevProps) {
const { timer } = this.props;
const { timer: prevTimer } = prevProps;
if (this.shouldPlayMusic()) {
this.playMusic();
}
this.updateInterval(prevTimer, timer);
this.updateAlarmTrigger(prevTimer, timer);
}
componentWillUnmount() {
clearInterval(this.interval);
}
updateInterval(prevTimer, timer) {
const { running } = timer;
const { running: prevRunning } = prevTimer;
if (!prevRunning && running) {
this.interval = setInterval(this.updateTime, TimerService.getInterval());
}
if (prevRunning && !running) {
clearInterval(this.interval);
}
}
setUpMusic() {
this.music = new Audio(`${HOST}/resources/sounds/${trackName}.mp3`);
this.music.volume = TimerService.getMusicVolume();
this.music.addEventListener('ended', () => {
this.music.currentTime = 0;
this.music.play();
}, false);
}
updateAlarmTrigger(prevTimer, timer) {
const {
accumulated,
timestamp,
} = timer;
const { timestamp: prevTimestamp } = prevTimer;
const reseted = timestamp !== prevTimestamp && accumulated === 0;
if (reseted) {
this.triggered = false;
}
}
initTriggered() {
const {
timer,
timeOffset,
} = this.props;
const {
stopwatch,
running,
} = timer;
if (stopwatch || !running) return false;
const {
time,
accumulated,
timestamp,
} = timer;
const elapsedTime = TimerService.getElapsedTime(running, timestamp, timeOffset, accumulated);
const updatedTime = Math.max(time - elapsedTime, 0);
if (updatedTime === 0) return true;
return false;
}
play() {
if (this.alarm && !this.triggered) {
TimerService.timerEnded();
this.triggered = true;
this.alarm.play();
}
}
soundAlarm(time) {
const { timer } = this.props;
const {
running,
stopwatch,
} = timer;
const enabled = TimerService.isAlarmEnabled();
const zero = time === 0;
return enabled && running && zero && !stopwatch;
}
playMusic() {
const handleUserInteraction = () => {
this.music.play();
window.removeEventListener('click', handleUserInteraction);
window.removeEventListener('auxclick', handleUserInteraction);
window.removeEventListener('keydown', handleUserInteraction);
window.removeEventListener('touchstart', handleUserInteraction);
};
const playMusicAfterUserInteraction = () => {
window.addEventListener('click', handleUserInteraction);
window.addEventListener('auxclick', handleUserInteraction);
window.addEventListener('keydown', handleUserInteraction);
window.addEventListener('touchstart', handleUserInteraction);
};
this.handleUserInteraction = handleUserInteraction;
if (this.music !== null) {
this.music.play()
.catch((error) => {
if (error.name === 'NotAllowedError') {
playMusicAfterUserInteraction();
}
});
}
}
stopMusic() {
if (this.music !== null) {
this.music.pause();
this.music.currentTime = 0;
window.removeEventListener('click', this.handleUserInteraction);
window.removeEventListener('auxclick', this.handleUserInteraction);
window.removeEventListener('keydown', this.handleUserInteraction);
window.removeEventListener('touchstart', this.handleUserInteraction);
}
}
shouldPlayMusic() {
const {
timer,
isMusicActive,
} = this.props;
const {
running,
stopwatch,
} = timer;
return isMusicActive && running && !stopwatch;
}
shouldStopMusic(time) {
const { timer } = this.props;
const {
running,
stopwatch,
} = timer;
const isActive = TimerService.isMusicActive();
const zero = time === 0;
const validMusic = this.music != null;
const reachedZeroOrStopped = (running && zero) || (!running);
return !isActive || !validMusic || stopwatch || reachedZeroOrStopped;
}
getTime() {
const {
timer,
timeOffset,
} = this.props;
const {
stopwatch,
running,
time,
accumulated,
timestamp,
} = timer;
const elapsedTime = TimerService.getElapsedTime(running, timestamp, timeOffset, accumulated);
let updatedTime;
if (stopwatch) {
updatedTime = elapsedTime;
} else {
updatedTime = Math.max(time - elapsedTime, 0);
}
if (this.shouldStopMusic(updatedTime)) {
this.stopMusic();
}
if (this.soundAlarm(updatedTime)) {
this.play();
}
return TimerService.getTimeAsString(updatedTime, stopwatch);
}
updateTime() {
const { current } = this.timeRef;
if (current) {
current.textContent = this.getTime();
}
}
render() {
const { isTimerActive } = this.props;
if (!isTimerActive) return null;
const {
isModerator,
hidden,
timer,
} = this.props;
const { running } = timer;
const onClick = running ? TimerService.stopTimer : TimerService.startTimer;
return (
<Styled.TimerWrapper>
<Styled.Timer>
<Styled.TimerIndicator
role="button"
tabIndex={0}
running={running}
disabled={!isModerator}
hide={hidden}
onClick={isModerator ? onClick : null}
>
<Styled.TimerContent>
<Styled.TimerIcon>
<Icon iconName="time" />
</Styled.TimerIcon>
<Styled.TimerTime
aria-hidden
ref={this.timeRef}
>
{this.getTime()}
</Styled.TimerTime>
</Styled.TimerContent>
</Styled.TimerIndicator>
</Styled.Timer>
</Styled.TimerWrapper>
);
}
}
export default Indicator;