// @flow import React, { Component, createRef } from 'react'; import { View } from 'react-native'; import YoutubePlayer from 'react-native-youtube-iframe'; import { getLocalParticipant } from '../../../base/participants'; import { connect } from '../../../base/redux'; import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui'; import { setToolboxVisible } from '../../../toolbox/actions'; import { setSharedVideoStatus } from '../../actions.native'; import { getYoutubeId } from '../../functions'; import styles from './styles'; /** * Passed to the webviewProps in order to avoid the usage of the ios player on which we cannot hide the controls. * * @private */ const webviewUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; // eslint-disable-line max-len /** * The type of the React {@link Component} props of {@link YoutubeLargeVideo}. */ type Props = { /** * Display the youtube controls on the player. * * @private */ _enableControls: boolean, /** * Is the video shared by the local user. * * @private */ _isOwner: boolean, /** * The ID of the participant (to be) depicted by LargeVideo. * * @private */ _isPlaying: string, /** * Set to true when the status is set to stop and the view should not react to further changes. * * @private */ _isStopped: boolean, /** * True if in landscape mode. * * @private */ _isWideScreen: boolean, /** * The id of the participant sharing the video. * * @private */ _ownerId: string, /** * The height of the player. * * @private */ _playerHeight: number, /** * The width of the player. * * @private */ _playerWidth: number, /** * Seek time in seconds. * * @private */ _seek: number, /** * The status of the player. * * @private */ _status: string, /** * The Redux dispatch function. */ dispatch: Function, /** * Youtube url of the video to be played. * * @private */ youtubeUrl: string }; /** * * Implements a React {@code Component} for showing a youtube video. * * @extends Component */ class YoutubeLargeVideo extends Component<Props, *> { /** * Saves a handle to the timer for seek time updates, * so that it can be cancelled when the component unmounts. */ intervalId: ?IntervalID; /** * A React ref to the HTML element containing the {@code YoutubePlayer} instance. */ playerRef: Object; /** * Initializes a new {@code YoutubeLargeVideo} instance. * * @param {Object} props - The read-only properties with which the new * instance is to be initialized. */ constructor(props: Props) { super(props); this.playerRef = createRef(); this._onReady = this._onReady.bind(this); this._onChangeState = this._onChangeState.bind(this); this.setWideScreenMode(props._isWideScreen); } /** * Seeks to the new time if the difference between the new one and the current is larger than 5 seconds. * * @inheritdoc * @returns {void} */ componentDidUpdate(prevProps: Props) { const playerRef = this.playerRef.current; const { _isWideScreen, _seek } = this.props; if (_seek !== prevProps._seek) { playerRef && playerRef.getCurrentTime().then(time => { if (shouldSeekToPosition(_seek, time)) { playerRef && playerRef.seekTo(_seek); } }); } if (_isWideScreen !== prevProps._isWideScreen) { this.setWideScreenMode(_isWideScreen); } } /** * Sets the interval for saving the seek time to redux every 5 seconds. * * @inheritdoc * @returns {void} */ componentDidMount() { this.intervalId = setInterval(() => { this.saveRefTime(); }, 5000); } /** * Clears the interval. * * @inheritdoc * @returns {void} */ componentWillUnmount() { clearInterval(this.intervalId); this.intervalId = null; } /** * Renders the YoutubeLargeVideo element. * * @override * @returns {ReactElement} */ render() { const { _enableControls, _isPlaying, _playerHeight, _playerWidth, youtubeUrl } = this.props; return ( <View pointerEvents = { _enableControls ? 'auto' : 'none' } style = { styles.youtubeVideoContainer } > <YoutubePlayer height = { _playerHeight } initialPlayerParams = {{ controls: _enableControls, modestbranding: true, preventFullScreen: true }} /* eslint-disable react/jsx-no-bind */ onChangeState = { this._onChangeState } /* eslint-disable react/jsx-no-bind */ onReady = { this._onReady } play = { _isPlaying } playbackRate = { 1 } ref = { this.playerRef } videoId = { getYoutubeId(youtubeUrl) } volume = { 50 } webViewProps = {{ bounces: false, mediaPlaybackRequiresUserAction: false, scrollEnabled: false, userAgent: webviewUserAgent }} width = { _playerWidth } /> </View>); } _onReady: () => void; /** * Callback invoked when the player is ready to play the video. * * @private * @returns {void} */ _onReady() { if (this.props?._isOwner) { this.onVideoReady( this.props.youtubeUrl, this.playerRef.current && this.playerRef.current.getCurrentTime(), this.props._ownerId); } } _onChangeState: (status: string) => void; /** * Callback invoked when the state of the player changes. * * @param {string} status - The new status of the player. * @private * @returns {void} */ _onChangeState(status) { this.playerRef?.current && this.playerRef.current.getCurrentTime().then(time => { const { _isOwner, _isPlaying, _isStopped, _ownerId, _seek, youtubeUrl } = this.props; if (shouldSetNewStatus(_isStopped, _isOwner, status, _isPlaying, time, _seek)) { this.onVideoChangeEvent(youtubeUrl, status, time, _ownerId); } }); } /** * Calls onVideoChangeEvent with the refTime. * * @private * @returns {void} */ saveRefTime() { const { youtubeUrl, _status, _ownerId } = this.props; this.playerRef.current && this.playerRef.current.getCurrentTime().then(time => { this.onVideoChangeEvent(youtubeUrl, _status, time, _ownerId); }); } /** * Dispatches the video status, time and ownerId if the status is playing or paused. * * @param {string} videoUrl - The youtube id of the video. * @param {string} status - The status of the player. * @param {number} time - The seek time. * @param {string} ownerId - The id of the participant sharing the video. * @private * @returns {void} */ onVideoChangeEvent(videoUrl, status, time, ownerId) { if (![ 'playing', 'paused' ].includes(status)) { return; } this.props.dispatch(setSharedVideoStatus({ videoUrl, status: translateStatus(status), time, ownerId })); } /** * Dispatches the 'playing' as video status, time and ownerId. * * @param {string} videoUrl - The youtube id of the video. * @param {number} time - The seek time. * @param {string} ownerId - The id of the participant sharing the video. * @private * @returns {void} */ onVideoReady(videoUrl, time, ownerId) { time.then(t => this.props.dispatch(setSharedVideoStatus({ videoUrl, status: 'playing', time: t, ownerId }))); } /** * Dispatches action to set the visibility of the toolbox, true if not widescreen, false otherwise. * * @param {isWideScreen} isWideScreen - Whether the screen is wide. * @private * @returns {void} */ setWideScreenMode(isWideScreen) { this.props.dispatch(setToolboxVisible(!isWideScreen)); } } /* eslint-disable max-params */ /** * Return true if the user is the owner and * the status has changed or the seek time difference from the previous set is larger than 5 seconds. * * @param {boolean} isStopped - Once the status was set to stop, all the other statuses should be ignored. * @param {boolean} isOwner - Whether the local user is sharing the video. * @param {string} status - The new status. * @param {boolean} isPlaying - Whether the component is playing at the moment. * @param {number} newTime - The new seek time. * @param {number} previousTime - The old seek time. * @private * @returns {boolean} */ function shouldSetNewStatus(isStopped, isOwner, status, isPlaying, newTime, previousTime) { if (isStopped) { return false; } if (!isOwner || status === 'buffering') { return false; } if ((isPlaying && status === 'paused') || (!isPlaying && status === 'playing')) { return true; } return shouldSeekToPosition(newTime, previousTime); } /** * Return true if the diffenrece between the two timees is larger than 5. * * @param {number} newTime - The current time. * @param {number} previousTime - The previous time. * @private * @returns {boolean} */ function shouldSeekToPosition(newTime, previousTime) { return Math.abs(newTime - previousTime) > 5; } /** * Maps (parts of) the Redux state to the associated YoutubeLargeVideo's props. * * @param {Object} state - Redux state. * @private * @returns {Props} */ function _mapStateToProps(state) { const { ownerId, status, time } = state['features/shared-video']; const localParticipant = getLocalParticipant(state); const responsiveUi = state['features/base/responsive-ui']; const { aspectRatio, clientHeight: screenHeight, clientWidth: screenWidth } = responsiveUi; const isWideScreen = aspectRatio === ASPECT_RATIO_WIDE; let playerHeight, playerWidth; if (isWideScreen) { playerHeight = screenHeight; playerWidth = playerHeight * 16 / 9; } else { playerWidth = screenWidth; playerHeight = playerWidth * 9 / 16; } return { _enableControls: ownerId === localParticipant.id, _isOwner: ownerId === localParticipant.id, _isPlaying: status === 'playing', _isStopped: status === 'stop', _isWideScreen: isWideScreen, _ownerId: ownerId, _playerHeight: playerHeight, _playerWidth: playerWidth, _seek: time, _status: status }; } /** * In case the status is 'paused', it is translated to 'pause' to match the web functionality. * * @param {string} status - The status of the shared video. * @private * @returns {string} */ function translateStatus(status) { if (status === 'paused') { return 'pause'; } return status; } export default connect(_mapStateToProps)(YoutubeLargeVideo);