import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createAudioPlayErrorEvent, createAudioPlaySuccessEvent } from '../../../../analytics/AnalyticsEvents'; import { sendAnalytics } from '../../../../analytics/functions'; import { IReduxState } from '../../../../app/types'; import { DEFAULT_ORIGINAL_VOLUME } from '../../../../audio-translation/constants'; import { getDuckedVolumeForParticipant, shouldDuckOriginalAudio } from '../../../../audio-translation/functions'; import { browser } from '../../../lib-jitsi-meet'; import { ITrack } from '../../../tracks/types'; import logger from '../../logger'; // iOS (WebKit) ignores programmatic HTMLMediaElement.volume — it is under the user's hardware control, so // assigning it is a no-op. Ducking therefore can't lower the volume there; we fall back to muting the // original entirely while its translation plays. `element.muted` IS honoured on iOS. const IS_IOS_BROWSER = browser.isIosBrowser(); /** * The type of the React {@code Component} props of {@link AudioTrack}. */ interface IProps { /** * Whether this track's original audio is currently ducked because its translated counterpart is playing. * On iOS, where element volume cannot be lowered, this causes the element to be muted instead. */ _ducked?: boolean; /** * Represents muted property of the underlying audio element. */ _muted?: boolean; /** * Represents volume property of the underlying audio element. */ _volume?: number | boolean; /** * The audio track. */ audioTrack?: ITrack; /** * Used to determine the value of the autoplay attribute of the underlying * audio element. */ autoPlay: boolean; /** * The value of the id attribute of the audio element. */ id: string; /** * 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 { /** * Reference to the HTML audio element, stored until the file is ready. */ _ref: React.RefObject; /** * The current timeout ID for attach or play retries. Shared so that * {@link _detachTrack} can cancel whichever retry is pending. */ _retryTimeout: number | undefined; /** * Tracks how many full re-attach cycles (attach retries + play retries exhausted) have occurred * to prevent infinite recovery loops. */ _reattachCount = 0; /** * Default values for {@code AudioTrack} component's properties. * * @static */ static defaultProps = { autoPlay: true, id: '' }; /** * Creates new Audio element instance with given props. * * @param {Object} props - The read-only properties with which the new * instance is to be initialized. */ constructor(props: IProps) { super(props); // Bind event handlers so they are only bound once for every instance. this._errorHandler = this._errorHandler.bind(this); this._ref = React.createRef(); this._play = this._play.bind(this); } /** * Attaches the audio track to the audio element and plays it. * * @inheritdoc * @returns {void} */ override componentDidMount() { this._attachTrack(this.props.audioTrack); if (this._ref?.current) { const audio = this._ref?.current; const { _volume } = this.props; if (typeof _volume === 'number') { audio.volume = _volume; } audio.muted = this._isMuted(this.props); // @ts-ignore audio.addEventListener('error', this._errorHandler); } else { // This should never happen logger.error(`The react reference is null for AudioTrack ${this.props?.id}`); } } /** * Remove any existing associations between the current audio track and the * component's audio element. * * @inheritdoc * @returns {void} */ override componentWillUnmount() { this._detachTrack(this.props.audioTrack); // @ts-ignore this._ref?.current?.removeEventListener('error', this._errorHandler); } /** * 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. */ override shouldComponentUpdate(nextProps: IProps) { 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?.current) { const audio = this._ref?.current; const currentVolume = audio.volume; const nextVolume = nextProps._volume; if (typeof nextVolume === 'number' && !isNaN(nextVolume) && currentVolume !== nextVolume) { if (nextVolume === 0) { logger.debug(`Setting audio element ${nextProps?.id} volume to 0`); } audio.volume = nextVolume; } const currentMuted = audio.muted; const nextMuted = this._isMuted(nextProps); if (currentMuted !== nextMuted) { logger.debug(`Setting audio element ${nextProps?.id} muted to ${nextMuted}`); audio.muted = nextMuted; } } return false; } /** * Computes the effective muted state of the audio element: muted when the conference is joined silently * ({@code _muted}), or — on iOS, where the volume cannot be lowered — while the track is ducked because * its translation is playing. * * @param {IProps} props - The props to evaluate. * @returns {boolean} */ _isMuted(props: IProps) { return Boolean(props._muted) || (IS_IOS_BROWSER && Boolean(props._ducked)); } /** * Implements React's {@link Component#render()}. * * @inheritdoc * @returns {ReactElement} */ override render() { const { autoPlay, id } = this.props; return (