This commit is contained in:
111
react/features/base/media/components/web/Audio.tsx
Normal file
111
react/features/base/media/components/web/Audio.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
314
react/features/base/media/components/web/AudioTrack.tsx
Normal file
314
react/features/base/media/components/web/AudioTrack.tsx
Normal 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);
|
||||
391
react/features/base/media/components/web/Video.tsx
Normal file
391
react/features/base/media/components/web/Video.tsx
Normal 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;
|
||||
197
react/features/base/media/components/web/VideoTrack.tsx
Normal file
197
react/features/base/media/components/web/VideoTrack.tsx
Normal 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);
|
||||
Reference in New Issue
Block a user