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,68 @@
import { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { IStore } from '../../app/types';
import { extractYoutubeIdOrURL } from '../functions';
/**
* The type of the React {@code Component} props of
* {@link AbstractSharedVideoDialog}.
*/
export interface IProps extends WithTranslation {
/**
* The allowed URL domains for shared video.
*/
_allowedUrlDomains: Array<string>;
/**
* Invoked to update the shared video link.
*/
dispatch: IStore['dispatch'];
/**
* Function to be invoked after typing a valid video.
*/
onPostSubmit: Function;
}
/**
* Implements an abstract class for {@code SharedVideoDialog}.
*/
export default class AbstractSharedVideoDialog<S> extends Component < IProps, S > {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onSetVideoLink = this._onSetVideoLink.bind(this);
}
/**
* Validates the entered video link by extracting the id and dispatches it.
*
* It returns a boolean to comply the Dialog behaviour:
* {@code true} - the dialog should be closed.
* {@code false} - the dialog should be left open.
*
* @param {string} link - The entered video link.
* @returns {boolean}
*/
_onSetVideoLink(link: string) {
const { onPostSubmit } = this.props;
const id = extractYoutubeIdOrURL(link);
if (!id) {
return false;
}
onPostSubmit(id);
return true;
}
}

View File

@@ -0,0 +1,4 @@
// @ts-ignore
export { default as SharedVideoDialog } from './native/SharedVideoDialog';
export { default as SharedVideoButton } from './native/SharedVideoButton';
export { default as ShareVideoConfirmDialog } from './native/ShareVideoConfirmDialog';

View File

@@ -0,0 +1,3 @@
export { default as SharedVideoDialog } from './web/SharedVideoDialog';
export { default as SharedVideoButton } from './web/SharedVideoButton';
export { default as ShareVideoConfirmDialog } from './web/ShareVideoConfirmDialog';

View File

@@ -0,0 +1,266 @@
import { throttle } from 'lodash-es';
import { PureComponent } from 'react';
import { IReduxState, IStore } from '../../../app/types';
import { getCurrentConference } from '../../../base/conference/functions';
import { IJitsiConference } from '../../../base/conference/reducer';
import { getLocalParticipant } from '../../../base/participants/functions';
import { setSharedVideoStatus } from '../../actions';
import { PLAYBACK_STATUSES } from '../../constants';
/**
* 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 Component} props of {@link AbstractVideoManager}.
*/
export interface IProps {
/**
* The current conference.
*/
_conference?: IJitsiConference;
/**
* Is the video shared by the local user.
*
* @private
*/
_isOwner: boolean;
/**
* The shared video owner id.
*/
_ownerId?: string;
/**
* The shared video status.
*/
_status?: string;
/**
* Seek time in seconds.
*
*/
_time: number;
/**
* The video url.
*/
_videoUrl?: string;
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* The player's height.
*/
height: number;
/**
* The video id.
*/
videoId: string;
/**
* The player's width.
*/
width: number;
}
/**
* Manager of shared video.
*/
abstract class AbstractVideoManager<S=void> extends PureComponent<IProps, S> {
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);
}
/**
* Implements React Component's componentDidMount.
*
* @inheritdoc
*/
override componentDidMount() {
this.processUpdatedProps();
}
/**
* Implements React Component's componentDidUpdate.
*
* @inheritdoc
*/
override componentDidUpdate() {
this.processUpdatedProps();
}
/**
* Implements React Component's componentWillUnmount.
*
* @inheritdoc
*/
override componentWillUnmount() {
if (this.dispose) {
this.dispose();
}
}
/**
* Processes new properties.
*
* @returns {void}
*/
async processUpdatedProps() {
const { _status, _time, _isOwner } = this.props;
if (_isOwner) {
return;
}
const playerTime = await this.getTime();
if (shouldSeekToPosition(_time, playerTime)) {
this.seek(_time);
}
if (this.getPlaybackStatus() !== _status) {
if (_status === PLAYBACK_STATUSES.PLAYING) {
this.play();
} else if (_status === PLAYBACK_STATUSES.PAUSED) {
this.pause();
}
}
}
/**
* Handle video playing.
*
* @returns {void}
*/
onPlay() {
this.fireUpdateSharedVideoEvent();
}
/**
* Handle video paused.
*
* @returns {void}
*/
onPause() {
this.fireUpdateSharedVideoEvent();
}
/**
* Dispatches an update action for the shared video.
*
* @returns {void}
*/
async fireUpdateSharedVideoEvent() {
const { _isOwner } = this.props;
if (!_isOwner) {
return;
}
const status = this.getPlaybackStatus();
if (!Object.values(PLAYBACK_STATUSES).includes(status)) {
return;
}
const time = await this.getTime();
const {
_ownerId,
_videoUrl,
dispatch
} = this.props;
dispatch(setSharedVideoStatus({
videoUrl: _videoUrl ?? '',
status,
time,
ownerId: _ownerId
}));
}
/**
* Seeks video to provided time.
*/
abstract seek(time: number): void;
/**
* Indicates the playback state of the video.
*/
abstract getPlaybackStatus(): string;
/**
* Plays video.
*/
abstract play(): void;
/**
* Pauses video.
*
* @returns {void}
*/
abstract pause(): void;
/**
* Retrieves current time.
*/
abstract getTime(): number;
/**
* Disposes current video player.
*
* @returns {void}
*/
dispose() {
// optional abstract method to be implemented by sub-class
}
}
export default AbstractVideoManager;
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {IProps}
*/
export function _mapStateToProps(state: IReduxState) {
const { ownerId, status, time, videoUrl } = state['features/shared-video'];
const localParticipant = getLocalParticipant(state);
return {
_conference: getCurrentConference(state),
_isOwner: ownerId === localParticipant?.id,
_ownerId: ownerId,
_status: status,
_time: Number(time),
_videoUrl: videoUrl
};
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import { DialogProps } from '../../../base/dialog/constants';
interface IProps extends DialogProps {
/**
* The name of the remote participant that shared the video.
*/
actorName: string;
/**
* The function to execute when confirmed.
*/
onSubmit: () => void;
}
/**
* Dialog to confirm playing a video shared from a remote participant.
*
* @returns {JSX.Element}
*/
export default function ShareVideoConfirmDialog({ actorName, onSubmit }: IProps): JSX.Element {
const { t } = useTranslation();
return (
<ConfirmDialog
cancelLabel = 'dialog.Cancel'
confirmLabel = 'dialog.Ok'
descriptionKey = 'dialog.shareVideoConfirmPlay'
onSubmit = { onSubmit }
title = { t('dialog.shareVideoConfirmPlayTitle', {
name: actorName
}) } />
);
}

View File

@@ -0,0 +1,167 @@
import React, { Component } from 'react';
import { View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { getLocalParticipant } from '../../../base/participants/functions';
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
import { setToolboxVisible } from '../../../toolbox/actions';
import VideoManager from './VideoManager';
import YoutubeVideoManager from './YoutubeVideoManager';
import styles from './styles';
interface IProps {
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* Is the video shared by the local user.
*
* @private
*/
isOwner: boolean;
/**
* True if in landscape mode.
*
* @private
*/
isWideScreen: boolean;
/**
* The available player width.
*/
playerHeight: number;
/**
* The available player width.
*/
playerWidth: number;
/**
* The shared video url.
*/
videoUrl?: string;
}
/** .
* Implements a React {@link Component} which represents the large video (a.k.a.
* The conference participant who is on the local stage) on Web/React.
*
* @augments Component
*/
class SharedVideo extends Component<IProps> {
/**
* Initializes a new {@code SharedVideo} instance.
*
* @param {Object} props - The properties.
*/
constructor(props: IProps) {
super(props);
this.setWideScreenMode(props.isWideScreen);
}
/**
* Implements React's {@link Component#componentDidUpdate()}.
*
* @inheritdoc
* @returns {void}
*/
override componentDidUpdate(prevProps: IProps) {
const { isWideScreen } = this.props;
if (isWideScreen !== prevProps.isWideScreen) {
this.setWideScreenMode(isWideScreen);
}
}
/**
* 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: boolean) {
this.props.dispatch(setToolboxVisible(!isWideScreen));
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {React$Element}
*/
override render() {
const {
isOwner,
playerHeight,
playerWidth,
videoUrl
} = this.props;
if (!videoUrl) {
return null;
}
return (
<View
pointerEvents = { isOwner ? 'auto' : 'none' }
style = { styles.videoContainer as ViewStyle } >
{videoUrl.match(/http/)
? (
<VideoManager
height = { playerHeight }
videoId = { videoUrl }
width = { playerWidth } />
) : (
<YoutubeVideoManager
height = { playerHeight }
videoId = { videoUrl }
width = { playerWidth } />
)
}
</View>
);
}
}
/**
* Maps (parts of) the Redux state to the associated LargeVideo props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { ownerId, videoUrl } = state['features/shared-video'];
const { aspectRatio, clientHeight, clientWidth } = state['features/base/responsive-ui'];
const isWideScreen = aspectRatio === ASPECT_RATIO_WIDE;
const localParticipant = getLocalParticipant(state);
let playerHeight, playerWidth;
if (isWideScreen) {
playerHeight = clientHeight;
playerWidth = playerHeight * 16 / 9;
} else {
playerWidth = clientWidth;
playerHeight = playerWidth * 9 / 16;
}
return {
isOwner: ownerId === localParticipant?.id,
isWideScreen,
playerHeight,
playerWidth,
videoUrl
};
}
export default connect(_mapStateToProps)(SharedVideo);

View File

@@ -0,0 +1,113 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { VIDEO_SHARE_BUTTON_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconPlay } from '../../../base/icons/svg';
import { getLocalParticipant } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleSharedVideo } from '../../actions';
import { isSharingStatus } from '../../functions';
/**
* The type of the React {@code Component} props of {@link TileViewButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether or not the button is disabled.
*/
_isDisabled: boolean;
/**
* Whether or not the local participant is sharing a video.
*/
_sharingVideo: boolean;
}
/**
* Component that renders a toolbar button for toggling the tile layout view.
*
* @augments AbstractButton
*/
class VideoShareButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.sharedvideo';
override icon = IconPlay;
override label = 'toolbar.sharedvideo';
override toggledLabel = 'toolbar.stopSharedVideo';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
this._doToggleSharedVideo();
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._sharingVideo;
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isDisabled() {
return this.props._isDisabled;
}
/**
* Dispatches an action to toggle video sharing.
*
* @private
* @returns {void}
*/
_doToggleSharedVideo() {
this.props.dispatch(toggleSharedVideo());
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const { ownerId, status: sharedVideoStatus } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state)?.id;
const enabled = getFeatureFlag(state, VIDEO_SHARE_BUTTON_ENABLED, true);
const { visible = enabled } = ownProps;
if (ownerId !== localParticipantId) {
return {
_isDisabled: isSharingStatus(sharedVideoStatus ?? ''),
_sharingVideo: false,
visible
};
}
return {
_isDisabled: false,
_sharingVideo: isSharingStatus(sharedVideoStatus ?? ''),
visible
};
}
export default translate(connect(_mapStateToProps)(VideoShareButton));

View File

@@ -0,0 +1,86 @@
import React from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import InputDialog from '../../../base/dialog/components/native/InputDialog';
import { translate } from '../../../base/i18n/functions';
import AbstractSharedVideoDialog, { IProps } from '../AbstractSharedVideoDialog';
interface IState {
error: boolean;
}
/**
* Implements a component to render a display name prompt.
*/
class SharedVideoDialog extends AbstractSharedVideoDialog<IState> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this.state = {
error: false
};
this._onSubmitValue = this._onSubmitValue.bind(this);
}
/**
* Callback to be invoked when the value of the link input is submitted.
*
* @param {string} value - The entered video link.
* @returns {boolean}
*/
_onSubmitValue(value: string) {
const result = super._onSetVideoLink(value);
if (!result) {
this.setState({ error: true });
}
return result;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
override render() {
const { t } = this.props;
const { error } = this.state;
return (
<InputDialog
messageKey = { error ? 'dialog.sharedVideoDialogError' : undefined }
onSubmit = { this._onSubmitValue }
textInputProps = {{
autoCapitalize: 'none',
autoCorrect: false,
placeholder: t('dialog.sharedVideoLinkPlaceholder')
}}
titleKey = 'dialog.shareVideoTitle' />
);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState) {
const { allowedUrlDomains } = state['features/shared-video'];
return {
_allowedUrlDomains: allowedUrlDomains
};
}
export default translate(connect(mapStateToProps)(SharedVideoDialog));

View File

@@ -0,0 +1,196 @@
import React, { RefObject } from 'react';
import Video from 'react-native-video';
import { connect } from 'react-redux';
import { PLAYBACK_STATUSES } from '../../constants';
import logger from '../../logger';
import AbstractVideoManager, {
IProps,
_mapStateToProps
} from './AbstractVideoManager';
interface IState {
currentTime: number;
paused: boolean;
}
/**
* Manager of shared video.
*/
class VideoManager extends AbstractVideoManager<IState> {
playerRef: RefObject<typeof Video>;
/**
* Initializes a new VideoManager instance.
*
* @param {Object} props - This component's props.
*
* @returns {void}
*/
constructor(props: IProps) {
super(props);
this.state = {
currentTime: 0,
paused: false
};
this.playerRef = React.createRef();
this.onPlaybackRateChange = this.onPlaybackRateChange.bind(this);
this.onProgress = this.onProgress.bind(this);
}
/**
* Retrieves the current player ref.
*/
get player() {
return this.playerRef.current;
}
/**
* Indicates the playback state of the video.
*
* @returns {string}
*/
getPlaybackStatus() {
let status;
if (this.state.paused) {
status = PLAYBACK_STATUSES.PAUSED;
} else {
status = PLAYBACK_STATUSES.PLAYING;
}
return status;
}
/**
* Retrieves current time.
*
* @returns {number}
*/
getTime() {
return this.state.currentTime;
}
/**
* Seeks video to provided time.
*
* @param {number} time - The time to seek to.
*
* @returns {void}
*/
seek(time: number) {
if (this.player) {
// @ts-ignore
this.player.seek(time);
}
}
/**
* Plays video.
*
* @returns {void}
*/
play() {
this.setState({
paused: false
});
}
/**
* Pauses video.
*
* @returns {void}
*/
pause() {
this.setState({
paused: true
});
}
/**
* Handles playback rate changed event.
*
* @param {Object} options.playbackRate - Playback rate: 1 - playing, 0 - paused, other - slowed down / sped up.
* @returns {void}
*/
onPlaybackRateChange({ playbackRate }: { playbackRate: number; }) {
if (playbackRate === 0) {
this.setState({
paused: true
}, () => {
this.onPause();
});
}
if (playbackRate === 1) {
this.setState({
paused: false
}, () => {
this.onPlay();
});
}
}
/**
* Handles progress update event.
*
* @param {Object} options - Progress event options.
* @returns {void}
*/
onProgress(options: { currentTime: number; }) {
this.setState({ currentTime: options.currentTime });
this.throttledFireUpdateSharedVideoEvent();
}
/**
* Retrieves video tag params.
*
* @returns {void}
*/
getPlayerOptions() {
const { _isOwner, videoId, width, height } = this.props;
const { paused } = this.state;
const options: any = {
paused,
progressUpdateInterval: 5000,
resizeMode: 'cover' as const,
style: {
height,
width
},
source: { uri: videoId },
controls: _isOwner,
pictureInPicture: false,
onProgress: this.onProgress,
onError: (event: Error) => {
logger.error('Error in the player:', event);
}
};
if (_isOwner) {
options.onPlaybackRateChange = this.onPlaybackRateChange;
}
return options;
}
/**
* Implements React Component's render.
*
* @inheritdoc
*/
override render() {
return (
<Video
ref = { this.playerRef }
{ ...this.getPlayerOptions() } />
);
}
}
export default connect(_mapStateToProps)(VideoManager);

View File

@@ -0,0 +1,203 @@
import React, { RefObject } from 'react';
import Video from 'react-native-youtube-iframe';
import { connect } from 'react-redux';
import { PLAYBACK_STATUSES } from '../../constants';
import AbstractVideoManager, {
IProps,
_mapStateToProps
} from './AbstractVideoManager';
/**
* 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
interface IState {
paused: boolean;
}
/**
* Manager of youtube shared video.
*/
class YoutubeVideoManager extends AbstractVideoManager<IState> {
playerRef: RefObject<typeof Video>;
/**
* Initializes a new VideoManager instance.
*
* @param {Object} props - This component's props.
*
* @returns {void}
*/
constructor(props: IProps) {
super(props);
this.state = {
paused: false
};
this.playerRef = React.createRef();
this._onReady = this._onReady.bind(this);
this._onChangeState = this._onChangeState.bind(this);
}
/**
* Retrieves the current player ref.
*/
get player() {
return this.playerRef.current;
}
/**
* Indicates the playback state of the video.
*
* @returns {string}
*/
getPlaybackStatus() {
let status;
if (this.state.paused) {
status = PLAYBACK_STATUSES.PAUSED;
} else {
status = PLAYBACK_STATUSES.PLAYING;
}
return status;
}
/**
* Retrieves current time.
*
* @returns {number}
*/
getTime() {
// @ts-ignore
return this.player?.getCurrentTime();
}
/**
* Seeks video to provided time.
*
* @param {number} time - The time to seek to.
*
* @returns {void}
*/
seek(time: number) {
if (this.player) {
// @ts-ignore
this.player.seekTo(time);
}
}
/**
* Plays video.
*
* @returns {void}
*/
play() {
this.setState({
paused: false
});
}
/**
* Pauses video.
*
* @returns {void}
*/
pause() {
this.setState({
paused: true
});
}
/**
* Handles state change event.
*
* @param {string} event - State event.
* @returns {void}
*/
_onChangeState(event: string) {
if (event === 'paused') {
this.setState({
paused: true
}, () => {
this.onPause();
});
}
if (event === PLAYBACK_STATUSES.PLAYING) {
this.setState({
paused: false
}, () => {
this.onPlay();
});
}
}
/**
* Handles onReady event.
*
* @returns {void}
*/
_onReady() {
this.setState({
paused: false
});
}
/**
* Retrieves video tag params.
*
* @returns {void}
*/
getPlayerOptions() {
const { _isOwner, videoId, width, height } = this.props;
const options: any = {
height,
initialPlayerParams: {
controls: _isOwner,
modestbranding: true,
preventFullScreen: true
},
play: !this.state.paused,
ref: this.playerRef,
videoId,
volume: 50,
webViewProps: {
bounces: false,
mediaPlaybackRequiresUserAction: false,
scrollEnabled: false,
userAgent: webviewUserAgent
},
width
};
if (_isOwner) {
options.onChangeState = this._onChangeState;
options.onReady = this._onReady;
}
return options;
}
/**
* Implements React Component's render.
*
* @inheritdoc
*/
override render() {
return (
<Video
ref = { this.playerRef }
{ ...this.getPlayerOptions() } />
);
}
}
export default connect(_mapStateToProps)(YoutubeVideoManager);

View File

@@ -0,0 +1,11 @@
/**
* The style of toolbar buttons.
*/
export default {
videoContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
}
};

View File

@@ -0,0 +1,493 @@
// @ts-expect-error
import Logger from '@jitsi/logger';
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';
const logger = Logger.getLogger(__filename);
/**
* 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;
/**
* The video id.
*/
videoId: string;
}
/**
* Manager of shared video.
*/
class AbstractVideoManager extends PureComponent<IProps> {
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
window._sharedVideoPlayer = this;
}
/**
* Implements React Component's componentDidMount.
*
* @inheritdoc
*/
override componentDidMount() {
this.props._dockToolbox(true);
this.processUpdatedProps();
}
/**
* Implements React Component's componentDidUpdate.
*
* @inheritdoc
*/
override componentDidUpdate(prevProps: IProps) {
const { _videoUrl } = this.props;
if (prevProps._videoUrl !== _videoUrl) {
sendAnalytics(createEvent('started'));
}
this.processUpdatedProps();
}
/**
* Implements React Component's componentWillUnmount.
*
* @inheritdoc
*/
override componentWillUnmount() {
sendAnalytics(createEvent('stopped'));
if (this.dispose) {
this.dispose();
}
this.props._dockToolbox(false);
}
/**
* Processes new properties.
*
* @returns {void}
*/
processUpdatedProps() {
const { _status, _time, _isOwner, _muted } = 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 (this.isMuted() !== _muted) {
if (_muted) {
this.mute();
} else {
this.unMute();
}
}
}
/**
* Handle video error.
*
* @param {Object|undefined} e - The error returned by the API or none.
* @returns {void}
*/
onError(e?: any) {
logger.error('Error in the video player', e?.data,
e?.data ? 'Check error code at https://developers.google.com/youtube/iframe_api_reference#onError' : '');
this.props._stopSharedVideo();
this.props._displayWarning();
}
/**
* Handle video playing.
*
* @returns {void}
*/
onPlay() {
this.smartAudioMute();
sendAnalytics(createEvent('play'));
this.fireUpdateSharedVideoEvent();
}
/**
* Handle video paused.
*
* @returns {void}
*/
onPause() {
sendAnalytics(createEvent('paused'));
this.fireUpdateSharedVideoEvent();
}
/**
* Handle volume changed.
*
* @returns {void}
*/
onVolumeChange() {
const volume = this.getVolume();
const muted = this.isMuted();
if (Number(volume) > 0 && !muted) {
this.smartAudioMute();
}
sendAnalytics(createEvent(
'volume.changed',
{
volume,
muted
}));
this.fireUpdatePlayingVideoEvent();
}
/**
* Handle changes to the shared playing video.
*
* @returns {void}
*/
fireUpdatePlayingVideoEvent() {
if (this.getPlaybackStatus() === PLAYBACK_STATUSES.PLAYING) {
this.fireUpdateSharedVideoEvent();
}
}
/**
* Dispatches an update action for the shared video.
*
* @returns {void}
*/
fireUpdateSharedVideoEvent() {
const { _isOwner } = this.props;
if (!_isOwner) {
return;
}
const status = this.getPlaybackStatus();
if (!Object.values(PLAYBACK_STATUSES).includes(status ?? '')) {
return;
}
const {
_ownerId,
_setSharedVideoStatus,
_videoUrl
} = this.props;
_setSharedVideoStatus({
videoUrl: _videoUrl,
status,
time: this.getTime(),
ownerId: _ownerId,
muted: this.isMuted()
});
}
/**
* Indicates if the player volume is currently on. This will return true if
* we have an available player, which is currently in a PLAYING state,
* which isn't muted and has it's volume greater than 0.
*
* @returns {boolean} Indicating if the volume of the shared video is
* currently on.
*/
isSharedVideoVolumeOn() {
return this.getPlaybackStatus() === PLAYBACK_STATUSES.PLAYING
&& !this.isMuted()
&& Number(this.getVolume()) > 0;
}
/**
* Smart mike mute. If the mike isn't currently muted and the shared video
* volume is on we mute the mike.
*
* @returns {void}
*/
smartAudioMute() {
const { _isLocalAudioMuted, _muteLocal } = this.props;
if (!_isLocalAudioMuted
&& this.isSharedVideoVolumeOn()) {
sendAnalytics(createEvent('audio.muted'));
_muteLocal(true);
}
}
/**
* Seeks video to provided time.
*
* @param {number} _time - Time to seek to.
* @returns {void}
*/
seek(_time: number) {
// to be implemented by subclass
}
/**
* Indicates the playback state of the video.
*
* @returns {string}
*/
getPlaybackStatus(): string | undefined {
return;
}
/**
* Indicates whether the video is muted.
*
* @returns {boolean}
*/
isMuted(): boolean | undefined {
return;
}
/**
* Retrieves current volume.
*
* @returns {number}
*/
getVolume() {
return 1;
}
/**
* Plays video.
*
* @returns {void}
*/
play() {
// to be implemented by subclass
}
/**
* Pauses video.
*
* @returns {void}
*/
pause() {
// to be implemented by subclass
}
/**
* Mutes video.
*
* @returns {void}
*/
mute() {
// to be implemented by subclass
}
/**
* Unmutes video.
*
* @returns {void}
*/
unMute() {
// to be implemented by subclass
}
/**
* Retrieves current time.
*
* @returns {number}
*/
getTime() {
return 0;
}
/**
* Disposes current video player.
*
* @returns {void}
*/
dispose() {
// to be implemented by subclass
}
}
export default AbstractVideoManager;
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {IProps}
*/
export function _mapStateToProps(state: IReduxState) {
const { ownerId, status, time, videoUrl, muted } = state['features/shared-video'];
const localParticipant = getLocalParticipant(state);
const _isLocalAudioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
return {
_conference: getCurrentConference(state),
_isLocalAudioMuted,
_isOwner: ownerId === localParticipant?.id,
_muted: muted,
_ownerId: ownerId,
_status: status,
_time: time,
_videoUrl: videoUrl
};
}
/**
* Maps part of the props of this component to Redux actions.
*
* @param {Function} dispatch - The Redux dispatch function.
* @returns {IProps}
*/
export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
return {
_displayWarning: () => {
dispatch(showWarningNotification({
titleKey: 'dialog.shareVideoLinkError'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
},
_dockToolbox: (value: boolean) => {
dispatch(dockToolbox(value));
},
_stopSharedVideo: () => {
dispatch(stopSharedVideo());
},
_muteLocal: (value: boolean) => {
dispatch(muteLocal(value, MEDIA_TYPE.AUDIO));
},
_setSharedVideoStatus: ({ videoUrl, status, time, ownerId, muted }: any) => {
dispatch(setSharedVideoStatus({
videoUrl,
status,
time,
ownerId,
muted
}));
}
};
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { DialogProps } from '../../../base/dialog/constants';
import Dialog from '../../../base/ui/components/web/Dialog';
interface IProps extends DialogProps {
/**
* The name of the remote participant that shared the video.
*/
actorName: string;
/**
* The function to execute when confirmed.
*/
onSubmit: () => void;
}
/**
* Dialog to confirm playing a video shared from a remote participant.
*
* @returns {JSX.Element}
*/
export default function ShareVideoConfirmDialog({ actorName, onSubmit }: IProps): JSX.Element {
const { t } = useTranslation();
return (
<Dialog
onSubmit = { onSubmit }
title = { t('dialog.shareVideoConfirmPlayTitle', {
name: actorName
}) }>
<div>
{ t('dialog.shareVideoConfirmPlay') }
</div>
</Dialog>
);
}

View File

@@ -0,0 +1,185 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
// @ts-expect-error
import Filmstrip from '../../../../../modules/UI/videolayout/Filmstrip';
import { IReduxState } from '../../../app/types';
import { FakeParticipant } from '../../../base/participants/types';
import { getVerticalViewMaxWidth } from '../../../filmstrip/functions.web';
import { getLargeVideoParticipant } from '../../../large-video/functions';
import { getToolboxHeight } from '../../../toolbox/functions.web';
import { isSharedVideoEnabled, isVideoPlaying } from '../../functions';
import VideoManager from './VideoManager';
import YoutubeVideoManager from './YoutubeVideoManager';
interface IProps {
/**
* The available client width.
*/
clientHeight: number;
/**
* The available client width.
*/
clientWidth: number;
/**
* Whether the (vertical) filmstrip is visible or not.
*/
filmstripVisible: boolean;
/**
* The width of the vertical filmstrip.
*/
filmstripWidth: number;
/**
* Whether the shared video is enabled or not.
*/
isEnabled: boolean;
/**
* Whether the user is actively resizing the filmstrip.
*/
isResizing: boolean;
/**
* Whether the shared video is currently playing.
*/
isVideoShared: boolean;
/**
* Whether the shared video should be shown on stage.
*/
onStage: boolean;
/**
* The shared video url.
*/
videoUrl?: string;
}
/** .
* Implements a React {@link Component} which represents the large video (a.k.a.
* The conference participant who is on the local stage) on Web/React.
*
* @augments Component
*/
class SharedVideo extends Component<IProps> {
/**
* Computes the width and the height of the component.
*
* @returns {{
* height: number,
* width: number
* }}
*/
getDimensions() {
const { clientHeight, clientWidth, filmstripVisible, filmstripWidth } = this.props;
let width;
let height;
if (interfaceConfig.VERTICAL_FILMSTRIP) {
if (filmstripVisible) {
width = `${clientWidth - filmstripWidth}px`;
} else {
width = `${clientWidth}px`;
}
height = `${clientHeight - getToolboxHeight()}px`;
} else {
if (filmstripVisible) {
height = `${clientHeight - Filmstrip.getFilmstripHeight()}px`;
} else {
height = `${clientHeight}px`;
}
width = `${clientWidth}px`;
}
return {
width,
height
};
}
/**
* Retrieves the manager to be used for playing the shared video.
*
* @returns {Component}
*/
getManager() {
const { videoUrl } = this.props;
if (!videoUrl) {
return null;
}
if (videoUrl.match(/http/)) {
return <VideoManager videoId = { videoUrl } />;
}
return <YoutubeVideoManager videoId = { videoUrl } />;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {React$Element}
*/
override render() {
const { isEnabled, isResizing, isVideoShared, onStage } = this.props;
if (!isEnabled || !isVideoShared) {
return null;
}
const style: any = this.getDimensions();
if (!onStage) {
style.display = 'none';
}
return (
<div
className = { (isResizing && 'disable-pointer') || '' }
id = 'sharedVideo'
style = { style }>
{this.getManager()}
</div>
);
}
}
/**
* Maps (parts of) the Redux state to the associated LargeVideo props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { videoUrl } = state['features/shared-video'];
const { clientHeight, videoSpaceWidth } = state['features/base/responsive-ui'];
const { visible, isResizing } = state['features/filmstrip'];
const { isResizing: isChatResizing } = state['features/chat'];
const onStage = getLargeVideoParticipant(state)?.fakeParticipant === FakeParticipant.SharedVideo;
const isVideoShared = isVideoPlaying(state);
return {
clientHeight,
clientWidth: videoSpaceWidth,
filmstripVisible: visible,
filmstripWidth: getVerticalViewMaxWidth(state),
isEnabled: isSharedVideoEnabled(state),
isResizing: isResizing || isChatResizing,
isVideoShared,
onStage,
videoUrl
};
}
export default connect(_mapStateToProps)(SharedVideo);

View File

@@ -0,0 +1,97 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconPlay } from '../../../base/icons/svg';
import { getLocalParticipant } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleSharedVideo } from '../../actions';
import { isSharingStatus } from '../../functions';
interface IProps extends AbstractButtonProps {
/**
* Whether or not the button is disabled.
*/
_isDisabled: boolean;
/**
* Whether or not the local participant is sharing a video.
*/
_sharingVideo: boolean;
}
/**
* Implements an {@link AbstractButton} to open the user documentation in a new window.
*/
class SharedVideoButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.sharedvideo';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.stopSharedVideo';
override icon = IconPlay;
override label = 'toolbar.sharedvideo';
override toggledLabel = 'toolbar.stopSharedVideo';
override tooltip = 'toolbar.sharedvideo';
override toggledTooltip = 'toolbar.stopSharedVideo';
/**
* Handles clicking / pressing the button, and opens a new dialog.
*
* @private
* @returns {void}
*/
override _handleClick() {
this._doToggleSharedVideo();
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._sharingVideo;
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isDisabled() {
return this.props._isDisabled;
}
/**
* Dispatches an action to toggle video sharing.
*
* @private
* @returns {void}
*/
_doToggleSharedVideo() {
this.props.dispatch(toggleSharedVideo());
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { ownerId, status: sharedVideoStatus } = state['features/shared-video'];
const localParticipantId = getLocalParticipant(state)?.id;
const isSharing = isSharingStatus(sharedVideoStatus ?? '');
return {
_isDisabled: isSharing && ownerId !== localParticipantId,
_sharingVideo: isSharing
};
}
export default translate(connect(_mapStateToProps)(SharedVideoButton));

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { hideDialog } from '../../../base/dialog/actions';
import { translate } from '../../../base/i18n/functions';
import Dialog from '../../../base/ui/components/web/Dialog';
import Input from '../../../base/ui/components/web/Input';
import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
/**
* Component that renders the video share dialog.
*
* @returns {React$Element<any>}
*/
class SharedVideoDialog extends AbstractSharedVideoDialog<any> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: any) {
super(props);
this.state = {
value: '',
okDisabled: true,
error: false
};
this._onChange = this._onChange.bind(this);
this._onSubmitValue = this._onSubmitValue.bind(this);
}
/**
* Callback for the onChange event of the field.
*
* @param {string} value - The static event.
* @returns {void}
*/
_onChange(value: string) {
this.setState({
value,
okDisabled: !value
});
}
/**
* Callback to be invoked when the value of the link input is submitted.
*
* @returns {boolean}
*/
_onSubmitValue() {
const result = super._onSetVideoLink(this.state.value);
if (result) {
this.props.dispatch(hideDialog());
} else {
this.setState({
error: true
});
}
return result;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
override render() {
const { t } = this.props;
const { error } = this.state;
return (
<Dialog
disableAutoHideOnSubmit = { true }
ok = {{
disabled: this.state.okDisabled,
translationKey: 'dialog.Share'
}}
onSubmit = { this._onSubmitValue }
titleKey = 'dialog.shareVideoTitle'>
<Input
autoFocus = { true }
bottomLabel = { error && t('dialog.sharedVideoDialogError') }
className = 'dialog-bottom-margin'
error = { error }
id = 'shared-video-url-input'
label = { t('dialog.videoLink') }
name = 'sharedVideoUrl'
onChange = { this._onChange }
placeholder = { t('dialog.sharedVideoLinkPlaceholder') }
type = 'text'
value = { this.state.value } />
</Dialog>
);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState) {
const { allowedUrlDomains } = state['features/shared-video'];
return {
_allowedUrlDomains: allowedUrlDomains
};
}
export default translate(connect(mapStateToProps)(SharedVideoDialog));

View File

@@ -0,0 +1,184 @@
import React from 'react';
import { connect } from 'react-redux';
import { PLAYBACK_STATUSES } from '../../constants';
import AbstractVideoManager, {
IProps,
_mapDispatchToProps,
_mapStateToProps
} from './AbstractVideoManager';
/**
* Manager of shared video.
*/
class VideoManager extends AbstractVideoManager {
playerRef: React.RefObject<HTMLVideoElement>;
/**
* Initializes a new VideoManager instance.
*
* @param {Object} props - This component's props.
*
* @returns {void}
*/
constructor(props: IProps) {
super(props);
this.playerRef = React.createRef();
}
/**
* Retrieves the current player ref.
*/
get player() {
return this.playerRef.current;
}
/**
* Indicates the playback state of the video.
*
* @returns {string}
*/
override getPlaybackStatus() {
let status;
if (!this.player) {
return;
}
if (this.player.paused) {
status = PLAYBACK_STATUSES.PAUSED;
} else {
status = PLAYBACK_STATUSES.PLAYING;
}
return status;
}
/**
* Indicates whether the video is muted.
*
* @returns {boolean}
*/
override isMuted() {
return this.player?.muted;
}
/**
* Retrieves current volume.
*
* @returns {number}
*/
override getVolume() {
return Number(this.player?.volume);
}
/**
* Retrieves current time.
*
* @returns {number}
*/
override getTime() {
return Number(this.player?.currentTime);
}
/**
* Seeks video to provided time.
*
* @param {number} time - The time to seek to.
*
* @returns {void}
*/
override seek(time: number) {
if (this.player) {
this.player.currentTime = time;
}
}
/**
* Plays video.
*
* @returns {void}
*/
override play() {
return this.player?.play();
}
/**
* Pauses video.
*
* @returns {void}
*/
override pause() {
return this.player?.pause();
}
/**
* Mutes video.
*
* @returns {void}
*/
override mute() {
if (this.player) {
this.player.muted = true;
}
}
/**
* Unmutes video.
*
* @returns {void}
*/
override unMute() {
if (this.player) {
this.player.muted = false;
}
}
/**
* Retrieves video tag params.
*
* @returns {void}
*/
getPlayerOptions() {
const { _isOwner, videoId } = this.props;
let options: any = {
autoPlay: true,
src: videoId,
controls: _isOwner,
onError: () => this.onError(),
onPlay: () => this.onPlay(),
onVolumeChange: () => this.onVolumeChange()
};
if (_isOwner) {
options = {
...options,
onPause: () => this.onPause(),
onTimeUpdate: this.throttledFireUpdateSharedVideoEvent
};
}
return options;
}
/**
* Implements React Component's render.
*
* @inheritdoc
*/
override render() {
return (
<video
id = 'sharedVideoPlayer'
ref = { this.playerRef }
{ ...this.getPlayerOptions() } />
);
}
}
export default connect(_mapStateToProps, _mapDispatchToProps)(VideoManager);

View File

@@ -0,0 +1,230 @@
/* eslint-disable no-invalid-this */
import React from 'react';
import { connect } from 'react-redux';
import YouTube from 'react-youtube';
import { PLAYBACK_STATUSES } from '../../constants';
import AbstractVideoManager, {
IProps,
_mapDispatchToProps,
_mapStateToProps
} from './AbstractVideoManager';
/**
* Manager of shared video.
*
* @returns {void}
*/
class YoutubeVideoManager extends AbstractVideoManager {
isPlayerAPILoaded: boolean;
player?: any;
/**
* Initializes a new YoutubeVideoManager instance.
*
* @param {Object} props - This component's props.
*
* @returns {void}
*/
constructor(props: IProps) {
super(props);
this.isPlayerAPILoaded = false;
}
/**
* Indicates the playback state of the video.
*
* @returns {string}
*/
override getPlaybackStatus() {
let status;
if (!this.player) {
return;
}
const playerState = this.player.getPlayerState();
if (playerState === YouTube.PlayerState.PLAYING) {
status = PLAYBACK_STATUSES.PLAYING;
}
if (playerState === YouTube.PlayerState.PAUSED) {
status = PLAYBACK_STATUSES.PAUSED;
}
return status;
}
/**
* Indicates whether the video is muted.
*
* @returns {boolean}
*/
override isMuted() {
return this.player?.isMuted();
}
/**
* Retrieves current volume.
*
* @returns {number}
*/
override getVolume() {
return this.player?.getVolume();
}
/**
* Retrieves current time.
*
* @returns {number}
*/
override getTime() {
return this.player?.getCurrentTime();
}
/**
* Seeks video to provided time.
*
* @param {number} time - The time to seek to.
*
* @returns {void}
*/
override seek(time: number) {
return this.player?.seekTo(time);
}
/**
* Plays video.
*
* @returns {void}
*/
override play() {
return this.player?.playVideo();
}
/**
* Pauses video.
*
* @returns {void}
*/
override pause() {
return this.player?.pauseVideo();
}
/**
* Mutes video.
*
* @returns {void}
*/
override mute() {
return this.player?.mute();
}
/**
* Unmutes video.
*
* @returns {void}
*/
override unMute() {
return this.player?.unMute();
}
/**
* Disposes of the current video player.
*
* @returns {void}
*/
override dispose() {
if (this.player) {
this.player.destroy();
this.player = null;
}
}
/**
* Fired on play state toggle.
*
* @param {Object} event - The yt player stateChange event.
*
* @returns {void}
*/
onPlayerStateChange = (event: any) => {
if (event.data === YouTube.PlayerState.PLAYING) {
this.onPlay();
} else if (event.data === YouTube.PlayerState.PAUSED) {
this.onPause();
}
};
/**
* Fired when youtube player is ready.
*
* @param {Object} event - The youtube player event.
*
* @returns {void}
*/
onPlayerReady = (event: any) => {
const { _isOwner } = this.props;
this.player = event.target;
this.player.addEventListener('onVolumeChange', () => {
this.onVolumeChange();
});
if (_isOwner) {
this.player.addEventListener('onVideoProgress', this.throttledFireUpdateSharedVideoEvent);
}
this.play();
// sometimes youtube can get muted state from previous videos played in the browser
// and as we are disabling controls we want to unmute it
if (this.isMuted()) {
this.unMute();
}
};
getPlayerOptions = () => {
const { _isOwner, videoId } = this.props;
const showControls = _isOwner ? 1 : 0;
const options = {
id: 'sharedVideoPlayer',
opts: {
height: '100%',
width: '100%',
playerVars: {
'origin': location.origin,
'fs': '0',
'autoplay': 0,
'controls': showControls,
'rel': 0
}
},
onError: (e: any) => this.onError(e),
onReady: this.onPlayerReady,
onStateChange: this.onPlayerStateChange,
videoId
};
return options;
};
/**
* Implements React Component's render.
*
* @inheritdoc
*/
override render() {
return (
<YouTube
{ ...this.getPlayerOptions() } />
);
}
}
export default connect(_mapStateToProps, _mapDispatchToProps)(YoutubeVideoManager);