jiti-meet/react/features/base/media/components/web/AudioTrack.js

282 lines
7.5 KiB
JavaScript

// @flow
import React, { Component } from 'react';
import { createAudioPlayErrorEvent, createAudioPlaySuccessEvent, sendAnalytics } from '../../../../analytics';
import { connect } from '../../../redux';
import logger from '../../logger';
/**
* The type of the React {@code Component} props of {@link AudioTrack}.
*/
type Props = {
/**
* Represents muted property of the underlying audio element.
*/
_muted: ?Boolean,
/**
* Represents volume property of the underlying audio element.
*/
_volume: ?number,
/**
* The value of the id attribute of the audio element.
*/
id: string,
/**
* The audio track.
*/
audioTrack: ?Object,
/**
* Used to determine the value of the autoplay attribute of the underlying
* audio element.
*/
autoPlay: boolean,
/**
* The ID of the participant associated with the audio element.
*/
participantId: string
};
/**
* The React/Web {@link Component} which is similar to and wraps around {@code HTMLAudioElement}.
*/
class AudioTrack extends Component<Props> {
/**
* Reference to the HTML audio element, stored until the file is ready.
*/
_ref: ?HTMLAudioElement;
/**
* The current timeout ID for play() retries.
*/
_playTimeout: ?TimeoutID;
/**
* Default values for {@code AudioTrack} component's properties.
*
* @static
*/
static defaultProps = {
autoPlay: true,
id: ''
};
/**
* Creates new <code>Audio</code> element instance with given props.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._setRef = this._setRef.bind(this);
this._play = this._play.bind(this);
}
/**
* Attaches the audio track to the audio element and plays it.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._attachTrack(this.props.audioTrack);
if (this._ref) {
const { _muted, _volume } = this.props;
if (typeof _volume === 'number') {
this._ref.volume = _volume;
}
if (typeof _muted === 'boolean') {
this._ref.muted = _muted;
}
}
}
/**
* Remove any existing associations between the current audio track and the
* component's audio element.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._detachTrack(this.props.audioTrack);
}
/**
* This component's updating is blackboxed from React to prevent re-rendering of the audio
* element, as we set all the properties manually.
*
* @inheritdoc
* @returns {boolean} - False is always returned to blackbox this component
* from React.
*/
shouldComponentUpdate(nextProps: Props) {
const currentJitsiTrack = this.props.audioTrack?.jitsiTrack;
const nextJitsiTrack = nextProps.audioTrack?.jitsiTrack;
if (currentJitsiTrack !== nextJitsiTrack) {
this._detachTrack(this.props.audioTrack);
this._attachTrack(nextProps.audioTrack);
}
if (this._ref) {
const currentVolume = this._ref.volume;
const nextVolume = nextProps._volume;
if (typeof nextVolume === 'number' && !isNaN(nextVolume) && currentVolume !== nextVolume) {
this._ref.volume = nextVolume;
}
const currentMuted = this._ref.muted;
const nextMuted = nextProps._muted;
if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
this._ref.muted = nextMuted;
}
}
return false;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { autoPlay, id } = this.props;
return (
<audio
autoPlay = { autoPlay }
id = { id }
ref = { this._setRef } />
);
}
/**
* Calls into the passed in track to associate the track with the component's audio element.
*
* @param {Object} track - The redux representation of the {@code JitsiLocalTrack}.
* @private
* @returns {void}
*/
_attachTrack(track) {
if (!track || !track.jitsiTrack) {
return;
}
track.jitsiTrack.attach(this._ref);
this._play();
}
/**
* Removes the association to the component's audio element from the passed
* in redux representation of jitsi audio track.
*
* @param {Object} track - The redux representation of the {@code JitsiLocalTrack}.
* @private
* @returns {void}
*/
_detachTrack(track) {
if (this._ref && track && track.jitsiTrack) {
clearTimeout(this._playTimeout);
this._playTimeout = undefined;
track.jitsiTrack.detach(this._ref);
}
}
_play: ?number => void;
/**
* Plays the uderlying HTMLAudioElement.
*
* @param {number} retries - The number of previously failed retries.
* @returns {void}
*/
_play(retries = 0) {
if (!this._ref) {
// nothing to play.
return;
}
const { autoPlay, id } = this.props;
if (autoPlay) {
// Ensure the audio gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case the audio may not autoplay.
this._ref.play()
.then(() => {
if (retries !== 0) {
// success after some failures
this._playTimeout = undefined;
sendAnalytics(createAudioPlaySuccessEvent(id));
logger.info(`Successfully played audio track! retries: ${retries}`);
}
}, e => {
logger.error(`Failed to play audio track! retry: ${retries} ; Error: ${e}`);
if (retries < 3) {
this._playTimeout = setTimeout(() => this._play(retries + 1), 1000);
if (retries === 0) {
// send only 1 error event.
sendAnalytics(createAudioPlayErrorEvent(id));
}
} else {
this._playTimeout = undefined;
}
});
}
}
_setRef: (?HTMLAudioElement) => void;
/**
* Sets the reference to the HTML audio element.
*
* @param {HTMLAudioElement} audioElement - The HTML audio element instance.
* @private
* @returns {void}
*/
_setRef(audioElement: ?HTMLAudioElement) {
this._ref = audioElement;
}
}
/**
* Maps (parts of) the Redux state to the associated {@code AudioTrack}'s props.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The props passed to the component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { participantsVolume } = state['features/filmstrip'];
return {
_muted: state['features/base/config'].startSilent,
_volume: participantsVolume[ownProps.participantId]
};
}
export default connect(_mapStateToProps)(AudioTrack);