init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
import React from 'react';
import AbstractAudio, { IProps } from '../AbstractAudio';
/**
* The React/Web {@link Component} which is similar to and wraps around
* {@code HTMLAudioElement} in order to facilitate cross-platform source code.
*/
export default class Audio extends AbstractAudio {
/**
* Set to <code>true</code> when the whole file is loaded.
*/
_audioFileLoaded: boolean;
/**
* Reference to the HTML audio element, stored until the file is ready.
*/
_ref?: HTMLAudioElement | null;
/**
* 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: IProps) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onCanPlayThrough = this._onCanPlayThrough.bind(this);
this._setRef = this._setRef?.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
return (
<audio
loop = { Boolean(this.props.loop) }
onCanPlayThrough = { this._onCanPlayThrough }
preload = 'auto'
ref = { this._setRef }
src = { this.props.src } />
);
}
/**
* Stops the audio HTML element.
*
* @returns {void}
*/
override stop() {
if (this._ref) {
this._ref.pause();
this._ref.currentTime = 0;
}
}
/**
* If audio element reference has been set and the file has been
* loaded then {@link setAudioElementImpl} will be called to eventually add
* the audio to the Redux store.
*
* @private
* @returns {void}
*/
_maybeSetAudioElementImpl() {
if (this._ref && this._audioFileLoaded) {
this.setAudioElementImpl(this._ref);
}
}
/**
* Called when 'canplaythrough' event is triggered on the audio element,
* which means that the whole file has been loaded.
*
* @private
* @returns {void}
*/
_onCanPlayThrough() {
this._audioFileLoaded = true;
this._maybeSetAudioElementImpl();
}
/**
* Sets the reference to the HTML audio element.
*
* @param {HTMLAudioElement} audioElement - The HTML audio element instance.
* @private
* @returns {void}
*/
_setRef(audioElement?: HTMLAudioElement | null) {
this._ref = audioElement;
if (audioElement) {
this._maybeSetAudioElementImpl();
} else {
// AbstractAudioElement is supposed to trigger "removeAudio" only if
// it was previously added, so it's safe to just call it.
this.setAudioElementImpl(null);
// Reset the loaded flag, as the audio element is being removed from
// the DOM tree.
this._audioFileLoaded = false;
}
}
}

View File

@@ -0,0 +1,314 @@
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 { ITrack } from '../../../tracks/types';
import logger from '../../logger';
/**
* The type of the React {@code Component} props of {@link AudioTrack}.
*/
interface IProps {
/**
* 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<IProps> {
/**
* Reference to the HTML audio element, stored until the file is ready.
*/
_ref: React.RefObject<HTMLAudioElement>;
/**
* The current timeout ID for play() retries.
*/
_playTimeout: number | undefined;
/**
* 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: 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 { _muted, _volume } = this.props;
if (typeof _volume === 'number') {
audio.volume = _volume;
}
if (typeof _muted === 'boolean') {
audio.muted = _muted;
}
// @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 = nextProps._muted;
if (typeof nextMuted === 'boolean' && currentMuted !== nextMuted) {
logger.debug(`Setting audio element ${nextProps?.id} muted to true`);
audio.muted = nextMuted;
}
}
return false;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { autoPlay, id } = this.props;
return (
<audio
autoPlay = { autoPlay }
id = { id }
ref = { this._ref } />
);
}
/**
* 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?: ITrack) {
const { id } = this.props;
if (!track?.jitsiTrack) {
logger.warn(`Attach is called on audio element ${id} without tracks passed!`);
return;
}
if (!this._ref?.current) {
logger.warn(`Attempting to attach track ${track?.jitsiTrack} on AudioTrack ${id} without reference!`);
return;
}
track.jitsiTrack.attach(this._ref.current)
.catch((error: Error) => {
logger.error(
`Attaching the remote track ${track.jitsiTrack} to video with id ${id} has failed with `,
error);
})
.finally(() => {
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?: ITrack) {
if (this._ref?.current && track?.jitsiTrack) {
clearTimeout(this._playTimeout);
this._playTimeout = undefined;
track.jitsiTrack.detach(this._ref.current);
}
}
/**
* Reattaches the audio track to the underlying HTMLAudioElement when an 'error' event is fired.
*
* @param {Error} error - The error event fired on the HTMLAudioElement.
* @returns {void}
*/
_errorHandler(error: Error) {
logger.error(`Error ${error?.message} called on audio track ${this.props.audioTrack?.jitsiTrack}. `
+ 'Attempting to reattach the audio track to the element and execute play on it');
this._detachTrack(this.props.audioTrack);
this._attachTrack(this.props.audioTrack);
}
/**
* Plays the underlying HTMLAudioElement.
*
* @param {number} retries - The number of previously failed retries.
* @returns {void}
*/
_play(retries = 0) {
const { autoPlay, id } = this.props;
if (!this._ref?.current) {
// nothing to play.
logger.warn(`Attempting to call play on AudioTrack ${id} without reference!`);
return;
}
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.current.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 on audio element ${id}! retry: ${retries} ; Error:`, e);
if (retries < 3) {
this._playTimeout = window.setTimeout(() => this._play(retries + 1), 1000);
if (retries === 0) {
// send only 1 error event.
sendAnalytics(createAudioPlayErrorEvent(id));
}
} else {
this._playTimeout = undefined;
}
});
}
}
}
/**
* 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 {IProps}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const { participantsVolume } = state['features/filmstrip'];
return {
_muted: state['features/base/config'].startSilent,
_volume: participantsVolume[ownProps.participantId]
};
}
export default connect(_mapStateToProps)(AudioTrack);

View File

@@ -0,0 +1,391 @@
import React, { Component, ReactEventHandler } from 'react';
import { ITrack } from '../../../tracks/types';
import logger from '../../logger';
/**
* The type of the React {@code Component} props of {@link Video}.
*/
interface IProps {
/**
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
autoPlay: boolean;
/**
* CSS classes to add to the video element.
*/
className: string;
/**
* A map of the event handlers for the video HTML element.
*/
eventHandlers?: {
/**
* OnAbort event handler.
*/
onAbort?: ReactEventHandler<HTMLVideoElement>;
/**
* OnCanPlay event handler.
*/
onCanPlay?: ReactEventHandler<HTMLVideoElement>;
/**
* OnCanPlayThrough event handler.
*/
onCanPlayThrough?: ReactEventHandler<HTMLVideoElement>;
/**
* OnEmptied event handler.
*/
onEmptied?: ReactEventHandler<HTMLVideoElement>;
/**
* OnEnded event handler.
*/
onEnded?: ReactEventHandler<HTMLVideoElement>;
/**
* OnError event handler.
*/
onError?: ReactEventHandler<HTMLVideoElement>;
/**
* OnLoadStart event handler.
*/
onLoadStart?: ReactEventHandler<HTMLVideoElement>;
/**
* OnLoadedData event handler.
*/
onLoadedData?: ReactEventHandler<HTMLVideoElement>;
/**
* OnLoadedMetadata event handler.
*/
onLoadedMetadata?: ReactEventHandler<HTMLVideoElement>;
/**
* OnPause event handler.
*/
onPause?: ReactEventHandler<HTMLVideoElement>;
/**
* OnPlay event handler.
*/
onPlay?: ReactEventHandler<HTMLVideoElement>;
/**
* OnPlaying event handler.
*/
onPlaying?: ReactEventHandler<HTMLVideoElement>;
/**
* OnRateChange event handler.
*/
onRateChange?: ReactEventHandler<HTMLVideoElement>;
/**
* OnStalled event handler.
*/
onStalled?: ReactEventHandler<HTMLVideoElement>;
/**
* OnSuspend event handler.
*/
onSuspend?: ReactEventHandler<HTMLVideoElement>;
/**
* OnWaiting event handler.
*/
onWaiting?: ReactEventHandler<HTMLVideoElement>;
};
/**
* The value of the id attribute of the video. Used by the torture tests to
* locate video elements.
*/
id: string;
/**
* Used on native.
*/
mirror?: boolean;
/**
* The value of the muted attribute for the underlying video element.
*/
muted?: boolean;
/**
* Used on native.
*/
onPlaying?: Function;
/**
* Used on native.
*/
onPress?: Function;
/**
* Optional callback to invoke once the video starts playing.
*/
onVideoPlaying?: Function;
/**
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
playsinline: boolean;
/**
* Used on native.
*/
stream?: any;
/**
* A styles that will be applied on the video element.
*/
style?: Object;
/**
* The JitsiLocalTrack to display.
*/
videoTrack?: Partial<ITrack>;
/**
* Used on native.
*/
zOrder?: number;
/**
* Used on native.
*/
zoomEnabled?: boolean;
}
/**
* Component that renders a video element for a passed in video track.
*
* @augments Component
*/
class Video extends Component<IProps> {
_videoElement?: HTMLVideoElement | null;
_mounted: boolean;
/**
* Default values for {@code Video} component's properties.
*
* @static
*/
static defaultProps = {
className: '',
autoPlay: true,
id: '',
playsinline: true
};
/**
* Initializes a new {@code Video} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
/**
* The internal reference to the DOM/HTML element intended for
* displaying a video.
*
* @private
* @type {HTMLVideoElement}
*/
this._videoElement = null;
// Bind event handlers so they are only bound once for every instance.
this._onVideoPlaying = this._onVideoPlaying.bind(this);
this._setVideoElement = this._setVideoElement.bind(this);
}
/**
* Invokes the library for rendering the video on initial display. Sets the
* volume level to zero to ensure no sound plays.
*
* @inheritdoc
* @returns {void}
*/
override componentDidMount() {
this._mounted = true;
if (this._videoElement) {
this._videoElement.volume = 0;
this._videoElement.onplaying = this._onVideoPlaying;
}
this._attachTrack(this.props.videoTrack).finally(() => {
if (this._videoElement && this.props.autoPlay) {
// Ensure the video 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 video does not autoplay.
this._videoElement.play()
.catch(error => {
// Prevent uncaught "DOMException: The play() request was interrupted by a new load request"
// when video playback takes long to start and it starts after the component was unmounted.
if (this._mounted) {
logger.error(`Error while trying to play video with id ${
this.props.id} and video track ${this.props.videoTrack?.jitsiTrack}: ${error}`);
}
});
}
});
}
/**
* Remove any existing associations between the current video track and the
* component's video element.
*
* @inheritdoc
* @returns {void}
*/
override componentWillUnmount() {
this._mounted = false;
this._detachTrack(this.props.videoTrack);
}
/**
* Updates the video display only if a new track is added. This component's
* updating is blackboxed from React to prevent re-rendering of video
* element, as the lib uses {@code track.attach(videoElement)} instead.
*
* @inheritdoc
* @returns {boolean} - False is always returned to blackbox this component
* from React.
*/
override shouldComponentUpdate(nextProps: IProps) {
const currentJitsiTrack = this.props.videoTrack?.jitsiTrack;
const nextJitsiTrack = nextProps.videoTrack?.jitsiTrack;
if (currentJitsiTrack !== nextJitsiTrack) {
this._detachTrack(this.props.videoTrack);
this._attachTrack(nextProps.videoTrack).catch((_error: Error) => {
// Ignore the error. We are already logging it.
});
// NOTE: We may want to consider calling .play() explicitly in this case if any issues araise in future.
// For now it seems we are good with the autoplay attribute of the video element.
}
if (this.props.style !== nextProps.style || this.props.className !== nextProps.className) {
return true;
}
return false;
}
/**
* Renders the video element.
*
* @override
* @returns {ReactElement}
*/
override render() {
const {
autoPlay,
className,
id,
muted,
playsinline,
style,
eventHandlers
} = this.props;
return (
<video
autoPlay = { autoPlay }
className = { className }
id = { id }
muted = { muted }
playsInline = { playsinline }
ref = { this._setVideoElement }
style = { style }
{ ...eventHandlers } />
);
}
/**
* Calls into the passed in track to associate the track with the
* component's video element and render video.
*
* @param {Object} videoTrack - The redux representation of the
* {@code JitsiLocalTrack}.
* @private
* @returns {void}
*/
_attachTrack(videoTrack?: Partial<ITrack>) {
const { id } = this.props;
if (!videoTrack?.jitsiTrack) {
logger.warn(`Attach is called on video element ${id} without tracks passed!`);
// returning Promise.resolve just keep the previous logic.
// TODO: Check if it make sense to call play on this element or we can just return promise.reject().
return Promise.resolve();
}
return videoTrack.jitsiTrack.attach(this._videoElement)
.catch((error: Error) => {
logger.error(
`Attaching the remote track ${videoTrack.jitsiTrack} to video with id ${id} has failed with `,
error);
});
}
/**
* Removes the association to the component's video element from the passed
* in redux representation of jitsi video track to stop the track from
* rendering.
*
* @param {Object} videoTrack - The redux representation of the
* {@code JitsiLocalTrack}.
* @private
* @returns {void}
*/
_detachTrack(videoTrack?: Partial<ITrack>) {
if (this._videoElement && videoTrack?.jitsiTrack) {
videoTrack.jitsiTrack.detach(this._videoElement);
}
}
/**
* Invokes the onvideoplaying callback if defined.
*
* @private
* @returns {void}
*/
_onVideoPlaying() {
if (this.props.onVideoPlaying) {
this.props.onVideoPlaying();
}
}
/**
* Sets an instance variable for the component's video element so it can be
* referenced later for attaching and detaching a JitsiLocalTrack.
*
* @param {Object} element - DOM element for the component's video display.
* @private
* @returns {void}
*/
_setVideoElement(element: HTMLVideoElement | null) {
this._videoElement = element;
}
}
export default Video;

View File

@@ -0,0 +1,197 @@
import React, { ReactEventHandler } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../../app/types';
import AbstractVideoTrack, { IProps as AbstractVideoTrackProps } from '../AbstractVideoTrack';
import Video from './Video';
/**
* The type of the React {@code Component} props of {@link VideoTrack}.
*/
interface IProps extends AbstractVideoTrackProps {
/**
*
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
_noAutoPlayVideo: boolean;
/**
* CSS classes to add to the video element.
*/
className: string;
/**
* A map of the event handlers for the video HTML element.
*/
eventHandlers?: {
/**
* OnAbort event handler.
*/
onAbort?: ReactEventHandler<HTMLVideoElement>;
/**
* OnCanPlay event handler.
*/
onCanPlay?: ReactEventHandler<HTMLVideoElement>;
/**
* OnCanPlayThrough event handler.
*/
onCanPlayThrough?: ReactEventHandler<HTMLVideoElement>;
/**
* OnEmptied event handler.
*/
onEmptied?: ReactEventHandler<HTMLVideoElement>;
/**
* OnEnded event handler.
*/
onEnded?: ReactEventHandler<HTMLVideoElement>;
/**
* OnError event handler.
*/
onError?: ReactEventHandler<HTMLVideoElement>;
/**
* OnLoadStart event handler.
*/
onLoadStart?: ReactEventHandler<HTMLVideoElement>;
/**
* OnLoadedData event handler.
*/
onLoadedData?: ReactEventHandler<HTMLVideoElement>;
/**
* OnLoadedMetadata event handler.
*/
onLoadedMetadata?: ReactEventHandler<HTMLVideoElement>;
/**
* OnPause event handler.
*/
onPause?: ReactEventHandler<HTMLVideoElement>;
/**
* OnPlay event handler.
*/
onPlay?: ReactEventHandler<HTMLVideoElement>;
/**
* OnPlaying event handler.
*/
onPlaying?: ReactEventHandler<HTMLVideoElement>;
/**
* OnRateChange event handler.
*/
onRateChange?: ReactEventHandler<HTMLVideoElement>;
/**
* OnStalled event handler.
*/
onStalled?: ReactEventHandler<HTMLVideoElement>;
/**
* OnSuspend event handler.
*/
onSuspend?: ReactEventHandler<HTMLVideoElement>;
/**
* OnWaiting event handler.
*/
onWaiting?: ReactEventHandler<HTMLVideoElement>;
};
/**
* The value of the id attribute of the video. Used by the torture tests
* to locate video elements.
*/
id: string;
/**
* The value of the muted attribute for the underlying element.
*/
muted?: boolean;
/**
* A styles that will be applied on the video element.
*/
style: Object;
}
/**
* Component that renders a video element for a passed in video track and
* notifies the store when the video has started playing.
*
* @augments AbstractVideoTrack
*/
class VideoTrack extends AbstractVideoTrack<IProps> {
/**
* Default values for {@code VideoTrack} component's properties.
*
* @static
*/
static defaultProps = {
className: '',
id: ''
};
/**
* Renders the video element.
*
* @override
* @returns {ReactElement}
*/
override render() {
const {
_noAutoPlayVideo,
className,
id,
muted,
videoTrack,
style,
eventHandlers
} = this.props;
return (
<Video
autoPlay = { !_noAutoPlayVideo }
className = { className }
eventHandlers = { eventHandlers }
id = { id }
muted = { muted }
onVideoPlaying = { this._onVideoPlaying }
style = { style }
videoTrack = { videoTrack } />
);
}
}
/**
* Maps (parts of) the Redux state to the associated VideoTracks props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _noAutoPlayVideo: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const testingConfig = state['features/base/config'].testing;
return {
_noAutoPlayVideo: Boolean(testingConfig?.noAutoPlayVideo)
};
}
export default connect(_mapStateToProps)(VideoTrack);