import { throttle } from 'lodash-es'; import { PureComponent } from 'react'; import { createSharedVideoEvent as createEvent } from '../../../analytics/AnalyticsEvents'; import { sendAnalytics } from '../../../analytics/functions'; import { IReduxState, IStore } from '../../../app/types'; import { getCurrentConference } from '../../../base/conference/functions'; import { IJitsiConference } from '../../../base/conference/reducer'; import { MEDIA_TYPE } from '../../../base/media/constants'; import { getLocalParticipant } from '../../../base/participants/functions'; import { isLocalTrackMuted } from '../../../base/tracks/functions'; import { showWarningNotification } from '../../../notifications/actions'; import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants'; import { dockToolbox } from '../../../toolbox/actions'; import { muteLocal } from '../../../video-menu/actions.any'; import { setSharedVideoStatus, stopSharedVideo } from '../../actions'; import { PLAYBACK_STATUSES } from '../../constants'; import logger from '../../logger'; /** * Return true if the difference between the two times is larger than 5. * * @param {number} newTime - The current time. * @param {number} previousTime - The previous time. * @private * @returns {boolean} */ function shouldSeekToPosition(newTime: number, previousTime: number) { return Math.abs(newTime - previousTime) > 5; } /** * The type of the React {@link PureComponent} props of {@link AbstractVideoManager}. */ export interface IProps { /** * The current conference. */ _conference?: IJitsiConference; /** * Warning that indicates an incorrect video url. */ _displayWarning: Function; /** * Docks the toolbox. */ _dockToolbox: Function; /** * Indicates whether the local audio is muted. */ _isLocalAudioMuted: boolean; /** * Is the video shared by the local user. * * @private */ _isOwner: boolean; /** * Mutes local audio track. */ _muteLocal: Function; /** * Store flag for muted state. */ _muted?: boolean; /** * The shared video owner id. */ _ownerId?: string; /** * Updates the shared video status. */ _setSharedVideoStatus: Function; /** * The shared video status. */ _status?: string; /** * Action to stop video sharing. */ _stopSharedVideo: Function; /** * Seek time in seconds. * */ _time?: number; /** * The video url. */ _videoUrl?: string; /** * Whether this player is a passive follower (e.g. on a second-screen * window): it follows the shared playback state but never acts as the * controlling player, even when the local participant owns the shared * video, reports no analytics or status updates, and plays no audio (the * audio stays with the main window's player). */ follower?: boolean; /** * Called when a follower player fails. A follower must not tear down the * meeting's shared video, so its host is notified instead and surfaces the * failure in place of the (black) player. Ignored for a non-follower. */ onFollowerError?: (code?: any) => void; /** * The video id. */ videoId: string; } /** * The props a manager takes from its host, as opposed to the ones it maps from * the redux state below. */ type IOwnProps = Pick; /** * Manager of shared video. */ class AbstractVideoManager extends PureComponent { throttledFireUpdateSharedVideoEvent: Function; /** * Initializes a new instance of AbstractVideoManager. * * @param {IProps} props - Component props. * @returns {void} */ constructor(props: IProps) { super(props); this.throttledFireUpdateSharedVideoEvent = throttle(this.fireUpdateSharedVideoEvent.bind(this), 5000); // selenium tests handler if (!props.follower) { window._sharedVideoPlayer = this; } } /** * Implements React Component's componentDidMount. * * @inheritdoc */ override componentDidMount() { if (this.props.follower) { this.syncFollower(); } else { this.props._dockToolbox(true); this.processUpdatedProps(); } } /** * Implements React Component's componentDidUpdate. * * @inheritdoc */ override componentDidUpdate(prevProps: IProps) { const { _videoUrl, follower } = this.props; if (!follower && prevProps._videoUrl !== _videoUrl) { sendAnalytics(createEvent('started')); } this.processUpdatedProps(); } /** * Implements React Component's componentWillUnmount. * * @inheritdoc */ override componentWillUnmount() { if (!this.props.follower) { sendAnalytics(createEvent('stopped')); this.props._dockToolbox(false); } if (this.dispose) { this.dispose(); } } /** * Processes new properties. * * @returns {void} */ processUpdatedProps() { const { _status, _time, _isOwner, _muted, follower } = this.props; if (_isOwner) { return; } const playerTime = this.getTime(); if (shouldSeekToPosition(Number(_time), Number(playerTime))) { this.seek(Number(_time)); } if (this.getPlaybackStatus() !== _status) { if (_status === PLAYBACK_STATUSES.PLAYING) { this.play(); } if (_status === PLAYBACK_STATUSES.PAUSED) { this.pause(); } } if (follower) { // A follower plays no audio (the audio stays with the main // window's player), so it is kept muted instead of following the // shared muted state. if (!this.isMuted()) { this.mute(); } } else if (this.isMuted() !== _muted) { if (_muted) { this.mute(); } else { this.unMute(); } } } /** * Starts a follower player in sync with the meeting's shared state. A * follower must not start playing a video the meeting has paused: doing so * desyncs silently, because a paused video sends no further status updates * and nothing would ever correct it. Everything else (seeking to the shared * position, staying muted) is left to processUpdatedProps. * * Called both on mount and, for players created asynchronously (the YouTube * iframe API), once the player is ready; it is safe to run twice. * * @returns {void} */ syncFollower() { // Not PLAYING vs. PAUSED: a video that was just shared is in the // "start" state, which processUpdatedProps does not act on, and a // follower opened at that moment should play. if (this.props._status !== PLAYBACK_STATUSES.PAUSED) { this.play(); } this.processUpdatedProps(); } /** * Handle video error. * * @param {Object|undefined} e - The error returned by the API or none. * @returns {void} */ onError(e?: any) { const { follower, onFollowerError, videoId } = this.props; // The YouTube API reports its error code in `data`; a