This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
4
react/features/shared-video/components/index.native.ts
Normal file
4
react/features/shared-video/components/index.native.ts
Normal 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';
|
||||
3
react/features/shared-video/components/index.web.ts
Normal file
3
react/features/shared-video/components/index.web.ts
Normal 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';
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}) } />
|
||||
);
|
||||
}
|
||||
167
react/features/shared-video/components/native/SharedVideo.tsx
Normal file
167
react/features/shared-video/components/native/SharedVideo.tsx
Normal 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);
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
196
react/features/shared-video/components/native/VideoManager.tsx
Normal file
196
react/features/shared-video/components/native/VideoManager.tsx
Normal 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);
|
||||
@@ -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);
|
||||
11
react/features/shared-video/components/native/styles.ts
Normal file
11
react/features/shared-video/components/native/styles.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* The style of toolbar buttons.
|
||||
*/
|
||||
export default {
|
||||
videoContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
185
react/features/shared-video/components/web/SharedVideo.tsx
Normal file
185
react/features/shared-video/components/web/SharedVideo.tsx
Normal 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);
|
||||
@@ -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));
|
||||
118
react/features/shared-video/components/web/SharedVideoDialog.tsx
Normal file
118
react/features/shared-video/components/web/SharedVideoDialog.tsx
Normal 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));
|
||||
184
react/features/shared-video/components/web/VideoManager.tsx
Normal file
184
react/features/shared-video/components/web/VideoManager.tsx
Normal 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);
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user