This commit is contained in:
38
react/features/shared-video/actionTypes.ts
Normal file
38
react/features/shared-video/actionTypes.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* The type of the action which signals to update the current known state of the
|
||||
* shared video.
|
||||
*
|
||||
* {
|
||||
* type: SET_SHARED_VIDEO_STATUS,
|
||||
* status: string
|
||||
* }
|
||||
*/
|
||||
export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to reset the current known state of the
|
||||
* shared video.
|
||||
*
|
||||
* {
|
||||
* type: RESET_SHARED_VIDEO_STATUS,
|
||||
* }
|
||||
*/
|
||||
export const RESET_SHARED_VIDEO_STATUS = 'RESET_SHARED_VIDEO_STATUS';
|
||||
|
||||
/**
|
||||
* The type of the action which marks that the user had confirmed to play video.
|
||||
*
|
||||
* {
|
||||
* type: SET_CONFIRM_SHOW_VIDEO
|
||||
* }
|
||||
*/
|
||||
export const SET_CONFIRM_SHOW_VIDEO = 'SET_CONFIRM_SHOW_VIDEO';
|
||||
|
||||
/**
|
||||
* The type of the action which sets an array of whitelisted urls.
|
||||
*
|
||||
* {
|
||||
* type: SET_ALLOWED_URL_DOMAINS
|
||||
* }
|
||||
*/
|
||||
export const SET_ALLOWED_URL_DOMAINS = 'SET_ALLOWED_URL_DOMAINS';
|
||||
203
react/features/shared-video/actions.ts
Normal file
203
react/features/shared-video/actions.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { hideDialog, openDialog } from '../base/dialog/actions';
|
||||
import { getLocalParticipant } from '../base/participants/functions';
|
||||
|
||||
import {
|
||||
RESET_SHARED_VIDEO_STATUS,
|
||||
SET_ALLOWED_URL_DOMAINS,
|
||||
SET_CONFIRM_SHOW_VIDEO,
|
||||
SET_SHARED_VIDEO_STATUS
|
||||
} from './actionTypes';
|
||||
import { ShareVideoConfirmDialog, SharedVideoDialog } from './components';
|
||||
import { PLAYBACK_START, PLAYBACK_STATUSES } from './constants';
|
||||
import { isSharedVideoEnabled, sendShareVideoCommand } from './functions';
|
||||
|
||||
|
||||
/**
|
||||
* Marks that user confirmed or not to play video.
|
||||
*
|
||||
* @param {boolean} value - The value to set.
|
||||
* @returns {{
|
||||
* type: SET_CONFIRM_SHOW_VIDEO,
|
||||
* }}
|
||||
*/
|
||||
export function setConfirmShowVideo(value: boolean) {
|
||||
return {
|
||||
type: SET_CONFIRM_SHOW_VIDEO,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the status of the shared video.
|
||||
*
|
||||
* @returns {{
|
||||
* type: SET_SHARED_VIDEO_STATUS,
|
||||
* }}
|
||||
*/
|
||||
export function resetSharedVideoStatus() {
|
||||
return {
|
||||
type: RESET_SHARED_VIDEO_STATUS
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current known status of the shared video.
|
||||
*
|
||||
* @param {Object} options - The options.
|
||||
* @param {boolean} options.muted - Is video muted.
|
||||
* @param {boolean} options.ownerId - Participant ID of the owner.
|
||||
* @param {boolean} options.status - Sharing status.
|
||||
* @param {boolean} options.time - Playback timestamp.
|
||||
* @param {boolean} options.videoUrl - URL of the shared video.
|
||||
*
|
||||
* @returns {{
|
||||
* type: SET_SHARED_VIDEO_STATUS,
|
||||
* muted: boolean,
|
||||
* ownerId: string,
|
||||
* status: string,
|
||||
* time: number,
|
||||
* videoUrl: string,
|
||||
* }}
|
||||
*/
|
||||
export function setSharedVideoStatus({ videoUrl, status, time, ownerId, muted }: {
|
||||
muted?: boolean; ownerId?: string; status: string; time: number; videoUrl: string;
|
||||
}) {
|
||||
return {
|
||||
type: SET_SHARED_VIDEO_STATUS,
|
||||
ownerId,
|
||||
status,
|
||||
time,
|
||||
videoUrl,
|
||||
muted
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the dialog for entering the video link.
|
||||
*
|
||||
* @param {Function} onPostSubmit - The function to be invoked when a valid link is entered.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showSharedVideoDialog(onPostSubmit: Function) {
|
||||
return openDialog(SharedVideoDialog, { onPostSubmit });
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Stops playing a shared video.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function stopSharedVideo() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const { ownerId } = state['features/shared-video'];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
if (ownerId === localParticipant?.id) {
|
||||
dispatch(resetSharedVideoStatus());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Plays a shared video.
|
||||
*
|
||||
* @param {string} videoUrl - The video url to be played.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function playSharedVideo(videoUrl: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
if (!isSharedVideoEnabled(getState())) {
|
||||
return;
|
||||
}
|
||||
const conference = getCurrentConference(getState());
|
||||
|
||||
if (conference) {
|
||||
const localParticipant = getLocalParticipant(getState());
|
||||
|
||||
// we will send the command and will create local video fake participant
|
||||
// and start playing once we receive ourselves the command
|
||||
sendShareVideoCommand({
|
||||
conference,
|
||||
id: videoUrl,
|
||||
localParticipantId: localParticipant?.id,
|
||||
status: PLAYBACK_START,
|
||||
time: 0
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Stops playing a shared video.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleSharedVideo() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const { status = '' } = state['features/shared-video'];
|
||||
|
||||
if ([ PLAYBACK_STATUSES.PLAYING, PLAYBACK_START, PLAYBACK_STATUSES.PAUSED ].includes(status)) {
|
||||
dispatch(stopSharedVideo());
|
||||
} else {
|
||||
dispatch(showSharedVideoDialog((id: string) => dispatch(playSharedVideo(id))));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the allowed URL domains of the shared video.
|
||||
*
|
||||
* @param {Array<string>} allowedUrlDomains - The new whitelist to be set.
|
||||
* @returns {{
|
||||
* type: SET_ALLOWED_URL_DOMAINS,
|
||||
* allowedUrlDomains: Array<string>
|
||||
* }}
|
||||
*/
|
||||
export function setAllowedUrlDomians(allowedUrlDomains: Array<string>) {
|
||||
return {
|
||||
type: SET_ALLOWED_URL_DOMAINS,
|
||||
allowedUrlDomains
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog whether to play the external video link.
|
||||
*
|
||||
* @param {string} actor - The actor's name.
|
||||
* @param {Function} onSubmit - The function to execute when confirmed.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showConfirmPlayingDialog(actor: String, onSubmit: Function) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
// shows only one dialog at a time
|
||||
dispatch(setConfirmShowVideo(false));
|
||||
|
||||
dispatch(openDialog(ShareVideoConfirmDialog, {
|
||||
actorName: actor,
|
||||
onSubmit: () => {
|
||||
dispatch(setConfirmShowVideo(true));
|
||||
onSubmit();
|
||||
}
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the video play confirmation dialog.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hideConfirmPlayingDialog() {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
dispatch(hideDialog(ShareVideoConfirmDialog));
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
50
react/features/shared-video/constants.ts
Normal file
50
react/features/shared-video/constants.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Fixed name of the video player fake participant.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const VIDEO_PLAYER_PARTICIPANT_NAME = 'Video';
|
||||
|
||||
/**
|
||||
* Fixed name of the youtube player fake participant.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const YOUTUBE_PLAYER_PARTICIPANT_NAME = 'YouTube';
|
||||
|
||||
|
||||
/**
|
||||
* Shared video command.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const SHARED_VIDEO = 'shared-video';
|
||||
|
||||
/**
|
||||
* Available playback statuses.
|
||||
*/
|
||||
export const PLAYBACK_STATUSES = {
|
||||
PLAYING: 'playing',
|
||||
PAUSED: 'pause',
|
||||
STOPPED: 'stop'
|
||||
};
|
||||
|
||||
/**
|
||||
* Playback start state.
|
||||
*/
|
||||
export const PLAYBACK_START = 'start';
|
||||
|
||||
/**
|
||||
* The domain for youtube URLs.
|
||||
*/
|
||||
export const YOUTUBE_URL_DOMAIN = 'youtube.com';
|
||||
|
||||
/**
|
||||
* The constant to allow URL domains.
|
||||
*/
|
||||
export const ALLOW_ALL_URL_DOMAINS = '*';
|
||||
|
||||
/**
|
||||
* The default white listed domains for shared video.
|
||||
*/
|
||||
export const DEFAULT_ALLOWED_URL_DOMAINS = [ YOUTUBE_URL_DOMAIN ];
|
||||
166
react/features/shared-video/functions.ts
Normal file
166
react/features/shared-video/functions.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { IJitsiConference } from '../base/conference/reducer';
|
||||
import { getFakeParticipants } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
import {
|
||||
ALLOW_ALL_URL_DOMAINS,
|
||||
PLAYBACK_START,
|
||||
PLAYBACK_STATUSES,
|
||||
SHARED_VIDEO,
|
||||
VIDEO_PLAYER_PARTICIPANT_NAME,
|
||||
YOUTUBE_PLAYER_PARTICIPANT_NAME,
|
||||
YOUTUBE_URL_DOMAIN
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* Validates the entered video url.
|
||||
*
|
||||
* It returns a boolean to reflect whether the url matches the youtube regex.
|
||||
*
|
||||
* @param {string} url - The entered video link.
|
||||
* @returns {string} The youtube video id if matched.
|
||||
*/
|
||||
function getYoutubeId(url: string) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|(?:m\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
|
||||
const result = url.match(p);
|
||||
|
||||
return result ? result[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the status is one that is actually sharing the video - playing, pause or start.
|
||||
*
|
||||
* @param {string} status - The shared video status.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSharingStatus(status: string) {
|
||||
return [ PLAYBACK_STATUSES.PLAYING, PLAYBACK_STATUSES.PAUSED, PLAYBACK_START ].includes(status);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if there is a video being shared in the meeting.
|
||||
*
|
||||
* @param {Object | Function} stateful - The Redux state or a function that gets resolved to the Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVideoPlaying(stateful: IStateful): boolean {
|
||||
let videoPlaying = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [ id, p ] of getFakeParticipants(stateful)) {
|
||||
if (p.name === VIDEO_PLAYER_PARTICIPANT_NAME || p.name === YOUTUBE_PLAYER_PARTICIPANT_NAME) {
|
||||
videoPlaying = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return videoPlaying;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a Youtube id or URL from the user input.
|
||||
*
|
||||
* @param {string} input - The user input.
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function extractYoutubeIdOrURL(input: string) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedLink = input.trim();
|
||||
|
||||
if (!trimmedLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const youtubeId = getYoutubeId(trimmedLink);
|
||||
|
||||
if (youtubeId) {
|
||||
return youtubeId;
|
||||
}
|
||||
|
||||
// Check if the URL is valid, native may crash otherwise.
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(trimmedLink);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
|
||||
return trimmedLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if shared video functionality is enabled and false otherwise.
|
||||
*
|
||||
* @param {IStateful} stateful - - The redux store or {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSharedVideoEnabled(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
const { disableThirdPartyRequests = false } = state['features/base/config'];
|
||||
|
||||
return !disableThirdPartyRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the passed url is allowed to be used for shared video or not.
|
||||
*
|
||||
* @param {string} url - The URL.
|
||||
* @param {Array<string>} allowedUrlDomains - The allowed url domains.
|
||||
* @param {boolean} considerNonURLsAllowedForYoututbe - If true, the invalid URLs will be considered youtube IDs
|
||||
* and if youtube is allowed the function will return true.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isURLAllowedForSharedVideo(url: string,
|
||||
allowedUrlDomains: Array<string> = [], considerNonURLsAllowedForYoututbe = false) {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObject = new URL(url);
|
||||
|
||||
if ([ 'http:', 'https:' ].includes(urlObject?.protocol?.toLowerCase())) {
|
||||
return allowedUrlDomains.includes(ALLOW_ALL_URL_DOMAINS) || allowedUrlDomains.includes(urlObject?.hostname);
|
||||
}
|
||||
} catch (_e) { // it should be YouTube id.
|
||||
return considerNonURLsAllowedForYoututbe && allowedUrlDomains.includes(YOUTUBE_URL_DOMAIN);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends SHARED_VIDEO command.
|
||||
*
|
||||
* @param {string} id - The id of the video.
|
||||
* @param {string} status - The status of the shared video.
|
||||
* @param {JitsiConference} conference - The current conference.
|
||||
* @param {string} localParticipantId - The id of the local participant.
|
||||
* @param {string} time - The seek position of the video.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function sendShareVideoCommand({ id, status, conference, localParticipantId = '', time, muted, volume }: {
|
||||
conference?: IJitsiConference; id: string; localParticipantId?: string; muted?: boolean;
|
||||
status: string; time: number; volume?: number;
|
||||
}) {
|
||||
conference?.sendCommandOnce(SHARED_VIDEO, {
|
||||
value: id,
|
||||
attributes: {
|
||||
from: localParticipantId,
|
||||
muted,
|
||||
state: status,
|
||||
time,
|
||||
volume
|
||||
}
|
||||
});
|
||||
}
|
||||
24
react/features/shared-video/hooks.ts
Normal file
24
react/features/shared-video/hooks.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { SharedVideoButton } from './components';
|
||||
import { isSharedVideoEnabled } from './functions';
|
||||
|
||||
const shareVideo = {
|
||||
key: 'sharedvideo',
|
||||
Content: SharedVideoButton,
|
||||
group: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the shared video button if it is enabled and undefined otherwise.
|
||||
*
|
||||
* @returns {Object | undefined}
|
||||
*/
|
||||
export function useSharedVideoButton() {
|
||||
const sharedVideoEnabled = useSelector(isSharedVideoEnabled);
|
||||
|
||||
if (sharedVideoEnabled) {
|
||||
return shareVideo;
|
||||
}
|
||||
}
|
||||
|
||||
3
react/features/shared-video/logger.ts
Normal file
3
react/features/shared-video/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/share-video');
|
||||
290
react/features/shared-video/middleware.ts
Normal file
290
react/features/shared-video/middleware.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { CONFERENCE_JOIN_IN_PROGRESS, CONFERENCE_LEFT } from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { IJitsiConference } from '../base/conference/reducer';
|
||||
import { SET_CONFIG } from '../base/config/actionTypes';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import { PARTICIPANT_LEFT } from '../base/participants/actionTypes';
|
||||
import { participantJoined, participantLeft, pinParticipant } from '../base/participants/actions';
|
||||
import { getLocalParticipant, getParticipantById, getParticipantDisplayName } from '../base/participants/functions';
|
||||
import { FakeParticipant } from '../base/participants/types';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { SET_DYNAMIC_BRANDING_DATA } from '../dynamic-branding/actionTypes';
|
||||
import { showWarningNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
|
||||
import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes';
|
||||
import {
|
||||
hideConfirmPlayingDialog,
|
||||
resetSharedVideoStatus,
|
||||
setAllowedUrlDomians,
|
||||
setSharedVideoStatus,
|
||||
showConfirmPlayingDialog
|
||||
} from './actions';
|
||||
import {
|
||||
DEFAULT_ALLOWED_URL_DOMAINS,
|
||||
PLAYBACK_START,
|
||||
PLAYBACK_STATUSES,
|
||||
SHARED_VIDEO,
|
||||
VIDEO_PLAYER_PARTICIPANT_NAME
|
||||
} from './constants';
|
||||
import { isSharedVideoEnabled, isSharingStatus, isURLAllowedForSharedVideo, sendShareVideoCommand } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
/**
|
||||
* Middleware that captures actions related to video sharing and updates
|
||||
* components not hooked into redux.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
if (!isSharedVideoEnabled(getState())) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOIN_IN_PROGRESS: {
|
||||
const { conference } = action;
|
||||
const localParticipantId = getLocalParticipant(getState())?.id;
|
||||
|
||||
conference.addCommandListener(SHARED_VIDEO,
|
||||
({ value, attributes }: { attributes: {
|
||||
muted: string; state: string; time: string; }; value: string; },
|
||||
from: string) => {
|
||||
const state = getState();
|
||||
const sharedVideoStatus = attributes.state;
|
||||
|
||||
const { ownerId } = state['features/shared-video'];
|
||||
|
||||
if (ownerId && ownerId !== from) {
|
||||
logger.warn(
|
||||
`User with id: ${from} sent shared video command: ${sharedVideoStatus} while we are playing.`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSharingStatus(sharedVideoStatus)) {
|
||||
// confirmShowVideo is undefined the first time we receive
|
||||
// when confirmShowVideo is false we ignore everything except stop that resets it
|
||||
if (state['features/shared-video'].confirmShowVideo === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isURLAllowedForSharedVideo(value, state['features/shared-video'].allowedUrlDomains, true)
|
||||
|| localParticipantId === from
|
||||
|| state['features/shared-video'].confirmShowVideo) { // if confirmed skip asking again
|
||||
handleSharingVideoStatus(store, value, {
|
||||
...attributes,
|
||||
from
|
||||
}, conference);
|
||||
} else {
|
||||
dispatch(showConfirmPlayingDialog(getParticipantDisplayName(state, from), () => {
|
||||
|
||||
handleSharingVideoStatus(store, value, {
|
||||
...attributes,
|
||||
from
|
||||
}, conference);
|
||||
|
||||
return true; // on mobile this is used to close the dialog
|
||||
}));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (sharedVideoStatus === 'stop') {
|
||||
const videoParticipant = getParticipantById(state, value);
|
||||
|
||||
if (state['features/shared-video'].confirmShowVideo === false) {
|
||||
dispatch(showWarningNotification({
|
||||
titleKey: 'dialog.shareVideoLinkStopped',
|
||||
titleArguments: {
|
||||
name: getParticipantDisplayName(state, from)
|
||||
}
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
}
|
||||
|
||||
dispatch(hideConfirmPlayingDialog());
|
||||
|
||||
dispatch(participantLeft(value, conference, {
|
||||
fakeParticipant: videoParticipant?.fakeParticipant
|
||||
}));
|
||||
|
||||
if (localParticipantId !== from) {
|
||||
dispatch(resetSharedVideoStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_LEFT:
|
||||
dispatch(setAllowedUrlDomians(DEFAULT_ALLOWED_URL_DOMAINS));
|
||||
dispatch(resetSharedVideoStatus());
|
||||
break;
|
||||
case PARTICIPANT_LEFT: {
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const { ownerId: stateOwnerId, videoUrl: statevideoUrl } = state['features/shared-video'];
|
||||
|
||||
if (action.participant.id === stateOwnerId) {
|
||||
batch(() => {
|
||||
dispatch(resetSharedVideoStatus());
|
||||
dispatch(participantLeft(statevideoUrl ?? '', conference));
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SET_CONFIG:
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
const { sharedVideoAllowedURLDomains: allowedURLDomainsFromConfig = [] } = state['features/base/config'];
|
||||
const { sharedVideoAllowedURLDomains: allowedURLDomainsFromBranding = [] } = state['features/dynamic-branding'];
|
||||
|
||||
dispatch(setAllowedUrlDomians([
|
||||
...DEFAULT_ALLOWED_URL_DOMAINS,
|
||||
...allowedURLDomainsFromBranding,
|
||||
...allowedURLDomainsFromConfig
|
||||
]));
|
||||
|
||||
return result;
|
||||
}
|
||||
case SET_SHARED_VIDEO_STATUS: {
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const localParticipantId = getLocalParticipant(state)?.id;
|
||||
const { videoUrl, status, ownerId, time, muted, volume } = action;
|
||||
const operator = status === PLAYBACK_STATUSES.PLAYING ? 'is' : '';
|
||||
|
||||
logger.debug(`User with id: ${ownerId} ${operator} ${status} video sharing.`);
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyAudioOrVideoSharingToggled(MEDIA_TYPE.VIDEO, status, ownerId);
|
||||
}
|
||||
|
||||
// when setting status we need to send the command for that, but not do it for the start command
|
||||
// as we are sending the command in playSharedVideo and setting the start status once
|
||||
// we receive the response, this way we will start the video at the same time when remote participants
|
||||
// start it, on receiving the command
|
||||
if (status === 'start') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (localParticipantId === ownerId) {
|
||||
sendShareVideoCommand({
|
||||
conference,
|
||||
localParticipantId,
|
||||
muted,
|
||||
status,
|
||||
time,
|
||||
id: videoUrl,
|
||||
volume
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RESET_SHARED_VIDEO_STATUS: {
|
||||
const state = getState();
|
||||
const localParticipantId = getLocalParticipant(state)?.id;
|
||||
const { ownerId: stateOwnerId, videoUrl: statevideoUrl } = state['features/shared-video'];
|
||||
|
||||
if (!stateOwnerId) {
|
||||
break;
|
||||
}
|
||||
|
||||
logger.debug(`User with id: ${stateOwnerId} stop video sharing.`);
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyAudioOrVideoSharingToggled(MEDIA_TYPE.VIDEO, 'stop', stateOwnerId);
|
||||
}
|
||||
|
||||
if (localParticipantId === stateOwnerId) {
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
sendShareVideoCommand({
|
||||
conference,
|
||||
id: statevideoUrl ?? '',
|
||||
localParticipantId,
|
||||
muted: true,
|
||||
status: 'stop',
|
||||
time: 0,
|
||||
volume: 0
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the playing, pause and start statuses for the shared video.
|
||||
* Dispatches participantJoined event and, if necessary, pins it.
|
||||
* Sets the SharedVideoStatus if the event was triggered by the local user.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {string} videoUrl - The id of the video to the shared.
|
||||
* @param {Object} attributes - The attributes received from the share video command.
|
||||
* @param {JitsiConference} conference - The current conference.
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleSharingVideoStatus(store: IStore, videoUrl: string,
|
||||
{ state, time, from, muted }: { from: string; muted: string; state: string; time: string; },
|
||||
conference: IJitsiConference) {
|
||||
const { dispatch, getState } = store;
|
||||
const localParticipantId = getLocalParticipant(getState())?.id;
|
||||
const oldStatus = getState()['features/shared-video']?.status ?? '';
|
||||
const oldVideoUrl = getState()['features/shared-video'].videoUrl;
|
||||
|
||||
if (oldVideoUrl && oldVideoUrl !== videoUrl) {
|
||||
logger.warn(
|
||||
`User with id: ${from} sent videoUrl: ${videoUrl} while we are playing: ${oldVideoUrl}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the video was not started (no participant) we want to create the participant
|
||||
// this can be triggered by start, but also by paused or playing
|
||||
// commands (joining late) and getting the current state
|
||||
if (state === PLAYBACK_START || !isSharingStatus(oldStatus)) {
|
||||
const youtubeId = videoUrl.match(/http/) ? false : videoUrl;
|
||||
const avatarURL = youtubeId ? `https://img.youtube.com/vi/${youtubeId}/0.jpg` : '';
|
||||
|
||||
dispatch(participantJoined({
|
||||
conference,
|
||||
fakeParticipant: FakeParticipant.SharedVideo,
|
||||
id: videoUrl,
|
||||
avatarURL,
|
||||
name: VIDEO_PLAYER_PARTICIPANT_NAME
|
||||
}));
|
||||
|
||||
dispatch(pinParticipant(videoUrl));
|
||||
|
||||
if (localParticipantId === from) {
|
||||
dispatch(setSharedVideoStatus({
|
||||
videoUrl,
|
||||
status: state,
|
||||
time: Number(time),
|
||||
ownerId: localParticipantId
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (localParticipantId !== from) {
|
||||
dispatch(setSharedVideoStatus({
|
||||
muted: muted === 'true',
|
||||
ownerId: from,
|
||||
status: state,
|
||||
time: Number(time),
|
||||
videoUrl
|
||||
}));
|
||||
}
|
||||
}
|
||||
66
react/features/shared-video/reducer.ts
Normal file
66
react/features/shared-video/reducer.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
RESET_SHARED_VIDEO_STATUS,
|
||||
SET_ALLOWED_URL_DOMAINS,
|
||||
SET_CONFIRM_SHOW_VIDEO,
|
||||
SET_SHARED_VIDEO_STATUS
|
||||
} from './actionTypes';
|
||||
import { DEFAULT_ALLOWED_URL_DOMAINS } from './constants';
|
||||
|
||||
const initialState = {
|
||||
allowedUrlDomains: DEFAULT_ALLOWED_URL_DOMAINS
|
||||
};
|
||||
|
||||
export interface ISharedVideoState {
|
||||
allowedUrlDomains: Array<string>;
|
||||
confirmShowVideo?: boolean;
|
||||
muted?: boolean;
|
||||
ownerId?: string;
|
||||
status?: string;
|
||||
time?: number;
|
||||
videoUrl?: string;
|
||||
volume?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the Redux actions of the feature features/shared-video.
|
||||
*/
|
||||
ReducerRegistry.register<ISharedVideoState>('features/shared-video',
|
||||
(state = initialState, action): ISharedVideoState => {
|
||||
const { videoUrl, status, time, ownerId, muted, volume } = action;
|
||||
|
||||
switch (action.type) {
|
||||
case RESET_SHARED_VIDEO_STATUS:
|
||||
return {
|
||||
...initialState,
|
||||
allowedUrlDomains: state.allowedUrlDomains
|
||||
};
|
||||
case SET_CONFIRM_SHOW_VIDEO: {
|
||||
return {
|
||||
...state,
|
||||
confirmShowVideo: action.value
|
||||
};
|
||||
}
|
||||
case SET_SHARED_VIDEO_STATUS:
|
||||
return {
|
||||
...state,
|
||||
muted,
|
||||
ownerId,
|
||||
status,
|
||||
time,
|
||||
videoUrl,
|
||||
volume
|
||||
};
|
||||
|
||||
case SET_ALLOWED_URL_DOMAINS: {
|
||||
return {
|
||||
...state,
|
||||
allowedUrlDomains: action.allowedUrlDomains
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user