mirror of https://github.com/jitsi/jitsi-meet
parent
8758c222c6
commit
df64dd8f18
@ -0,0 +1,22 @@ |
||||
/** |
||||
* The type of the action which signals to update the current known state of the |
||||
* shared YouTube video. |
||||
* |
||||
* { |
||||
* type: SET_SHARED_VIDEO_STATUS, |
||||
* status: string, |
||||
* time: string, |
||||
* ownerId: string |
||||
* } |
||||
*/ |
||||
export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS'; |
||||
|
||||
/** |
||||
* The type of the action which signals to start the flow for starting or |
||||
* stopping a shared YouTube video. |
||||
* |
||||
* { |
||||
* type: TOGGLE_SHARED_VIDEO |
||||
* } |
||||
*/ |
||||
export const TOGGLE_SHARED_VIDEO = 'TOGGLE_SHARED_VIDEO'; |
@ -0,0 +1,54 @@ |
||||
// @flow
|
||||
|
||||
import { openDialog } from '../base/dialog'; |
||||
|
||||
import { SET_SHARED_VIDEO_STATUS } from './actionTypes'; |
||||
import { EnterVideoLinkPrompt } from './components'; |
||||
|
||||
/** |
||||
* Updates the current known status of the shared YouTube video. |
||||
* |
||||
* @param {string} videoId - The youtubeId of the video to be shared. |
||||
* @param {string} status - The current status of the YouTube video being shared. |
||||
* @param {number} time - The current position of the YouTube video being shared. |
||||
* @param {string} ownerId - The participantId of the user sharing the YouTube video. |
||||
* @returns {{ |
||||
* type: SET_SHARED_VIDEO_STATUS, |
||||
* ownerId: string, |
||||
* status: string, |
||||
* time: number, |
||||
* videoId: string |
||||
* }} |
||||
*/ |
||||
export function setSharedVideoStatus(videoId: string, status: string, time: number, ownerId: string) { |
||||
return { |
||||
type: SET_SHARED_VIDEO_STATUS, |
||||
ownerId, |
||||
status, |
||||
time, |
||||
videoId |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Starts the flow for starting or stopping a shared YouTube video. |
||||
* |
||||
* @returns {{ |
||||
* type: TOGGLE_SHARED_VIDEO |
||||
* }} |
||||
*/ |
||||
export function toggleSharedVideo() { |
||||
return { |
||||
type: 'TOGGLE_SHARED_VIDEO' |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Displays the prompt for entering the youtube video link. |
||||
* |
||||
* @param {Function} onPostSubmit - The function to be invoked when a valid link is entered. |
||||
* @returns {Function} |
||||
*/ |
||||
export function showEnterVideoLinkPrompt(onPostSubmit: ?Function) { |
||||
return openDialog(EnterVideoLinkPrompt, { onPostSubmit }); |
||||
} |
@ -0,0 +1,83 @@ |
||||
// @flow
|
||||
|
||||
import { Component } from 'react'; |
||||
import type { Dispatch } from 'redux'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of |
||||
* {@link AbstractEnterVideoLinkPrompt}. |
||||
*/ |
||||
export type Props = { |
||||
|
||||
/** |
||||
* Invoked to update the shared youtube video link. |
||||
*/ |
||||
dispatch: Dispatch<any>, |
||||
|
||||
/** |
||||
* Function to be invoked after typing a valid youtube video . |
||||
*/ |
||||
onPostSubmit: ?Function |
||||
}; |
||||
|
||||
/** |
||||
* Implements an abstract class for {@code EnterVideoLinkPrompt}. |
||||
*/ |
||||
export default class AbstractEnterVideoLinkPrompt<S: *> extends Component < Props, S > { |
||||
/** |
||||
* Instantiates a new component. |
||||
* |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this._onSetVideoLink = this._onSetVideoLink.bind(this); |
||||
} |
||||
|
||||
_onSetVideoLink: string => boolean; |
||||
|
||||
/** |
||||
* Validates the entered video link by extractibg 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) { |
||||
if (!link || !link.trim()) { |
||||
return false; |
||||
} |
||||
|
||||
const videoId = getYoutubeLink(link); |
||||
|
||||
if (videoId) { |
||||
const { onPostSubmit } = this.props; |
||||
|
||||
onPostSubmit && onPostSubmit(videoId); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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 {boolean} |
||||
*/ |
||||
function getYoutubeLink(url) { |
||||
const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|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] : false; |
||||
} |
@ -0,0 +1,122 @@ |
||||
// @flow
|
||||
|
||||
import type { Dispatch } from 'redux'; |
||||
|
||||
import { translate } from '../../base/i18n'; |
||||
import { IconShareVideo } from '../../base/icons'; |
||||
import { getLocalParticipant } from '../../base/participants'; |
||||
import { connect } from '../../base/redux'; |
||||
import { AbstractButton } from '../../base/toolbox'; |
||||
import type { AbstractButtonProps } from '../../base/toolbox'; |
||||
import { toggleSharedVideo } from '../actions'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link TileViewButton}. |
||||
*/ |
||||
type Props = AbstractButtonProps & { |
||||
|
||||
/** |
||||
* Whether or not the button is disabled. |
||||
*/ |
||||
_isDisabled: boolean, |
||||
|
||||
/** |
||||
* Whether or not the local participant is sharing a YouTube video. |
||||
*/ |
||||
_sharingVideo: boolean, |
||||
|
||||
/** |
||||
* The redux {@code dispatch} function. |
||||
*/ |
||||
dispatch: Dispatch<any> |
||||
}; |
||||
|
||||
/** |
||||
* Component that renders a toolbar button for toggling the tile layout view. |
||||
* |
||||
* @extends AbstractButton |
||||
*/ |
||||
class VideoShareButton extends AbstractButton<Props, *> { |
||||
accessibilityLabel = 'toolbar.accessibilityLabel.sharedvideo'; |
||||
icon = IconShareVideo; |
||||
label = 'toolbar.sharedvideo'; |
||||
toggledLabel = 'toolbar.stopSharedVideo'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button. |
||||
* |
||||
* @override |
||||
* @protected |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
this._doToggleSharedVideo(); |
||||
} |
||||
|
||||
/** |
||||
* Indicates whether this button is in toggled state or not. |
||||
* |
||||
* @override |
||||
* @protected |
||||
* @returns {boolean} |
||||
*/ |
||||
_isToggled() { |
||||
return this.props._sharingVideo; |
||||
} |
||||
|
||||
/** |
||||
* Indicates whether this button is disabled or not. |
||||
* |
||||
* @override |
||||
* @protected |
||||
* @returns {boolean} |
||||
*/ |
||||
_isDisabled() { |
||||
return this.props._isDisabled; |
||||
} |
||||
|
||||
/** |
||||
* Dispatches an action to toggle YouTube 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 {Props} |
||||
*/ |
||||
function _mapStateToProps(state): Object { |
||||
const { ownerId, status: sharedVideoStatus } = state['features/youtube-player']; |
||||
const localParticipantId = getLocalParticipant(state).id; |
||||
|
||||
if (ownerId !== localParticipantId) { |
||||
return { |
||||
_isDisabled: isSharingStatus(sharedVideoStatus), |
||||
_sharingVideo: false }; |
||||
} |
||||
|
||||
return { |
||||
_sharingVideo: isSharingStatus(sharedVideoStatus) |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Checks if the status is one that is actually sharing the video - playing, pause or start. |
||||
* |
||||
* @param {string} status - The shared video status. |
||||
* @private |
||||
* @returns {boolean} |
||||
*/ |
||||
function isSharingStatus(status) { |
||||
return [ 'playing', 'pause', 'start' ].includes(status); |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(VideoShareButton)); |
@ -0,0 +1 @@ |
||||
export * from './native'; |
@ -0,0 +1,3 @@ |
||||
export { default as VideoShareButton } from './VideoShareButton'; |
||||
|
||||
export * from './_'; |
@ -0,0 +1,32 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { InputDialog } from '../../../base/dialog'; |
||||
import { connect } from '../../../base/redux'; |
||||
import AbstractEnterVideoLinkPrompt from '../AbstractEnterVideoLinkPrompt'; |
||||
|
||||
/** |
||||
* Implements a component to render a display name prompt. |
||||
*/ |
||||
class EnterVideoLinkPrompt extends AbstractEnterVideoLinkPrompt<*> { |
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<InputDialog |
||||
contentKey = 'dialog.shareVideoTitle' |
||||
onSubmit = { this._onSetVideoLink } |
||||
textInputProps = {{ |
||||
placeholder: 'https://youtu.be/TB7LlM4erx8' |
||||
}} /> |
||||
); |
||||
} |
||||
|
||||
_onSetVideoLink: string => boolean; |
||||
} |
||||
|
||||
export default connect()(EnterVideoLinkPrompt); |
@ -0,0 +1,311 @@ |
||||
// @flow
|
||||
|
||||
import React, { useRef, useEffect } from 'react'; |
||||
import { View } from 'react-native'; |
||||
import YoutubePlayer from 'react-native-youtube-iframe'; |
||||
|
||||
import { getLocalParticipant } from '../../../base/participants'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants'; |
||||
import { setToolboxVisible } from '../../../toolbox/actions'; |
||||
import { setSharedVideoStatus } from '../../actions'; |
||||
|
||||
import styles from './styles'; |
||||
|
||||
/** |
||||
* Passed to the webviewProps in order to avoid the usage of the ios player on which we cannot hide the controls. |
||||
* |
||||
* @private |
||||
*/ |
||||
const webviewUserAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'; // eslint-disable-line max-len
|
||||
|
||||
/** |
||||
* The type of the React {@link Component} props of {@link YoutubeLargeVideo}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* Display the youtube controls on the player. |
||||
* |
||||
* @private |
||||
*/ |
||||
_enableControls: boolean, |
||||
|
||||
/** |
||||
* Is the video shared by the local user. |
||||
* |
||||
* @private |
||||
*/ |
||||
_isOwner: boolean, |
||||
|
||||
/** |
||||
* The ID of the participant (to be) depicted by LargeVideo. |
||||
* |
||||
* @private |
||||
*/ |
||||
_isPlaying: string, |
||||
|
||||
/** |
||||
* True if in landscape mode. |
||||
* |
||||
* @private |
||||
*/ |
||||
_isWideScreen: boolean, |
||||
|
||||
/** |
||||
* Callback to invoke when the {@code YoutLargeVideo} is ready to play. |
||||
* |
||||
* @private |
||||
*/ |
||||
_onVideoReady: Function, |
||||
|
||||
/** |
||||
* Callback to invoke when the {@code YoutubeLargeVideo} changes status. |
||||
* |
||||
* @private |
||||
*/ |
||||
_onVideoChangeEvent: Function, |
||||
|
||||
/** |
||||
* Callback to invoke when { @code isWideScreen} changes. |
||||
* |
||||
* @private |
||||
*/ |
||||
_onWideScreenChanged: Function, |
||||
|
||||
/** |
||||
* The id of the participant sharing the video. |
||||
* |
||||
* @private |
||||
*/ |
||||
_ownerId: string, |
||||
|
||||
/** |
||||
* The height of the screen. |
||||
* |
||||
* @private |
||||
*/ |
||||
_screenHeight: number, |
||||
|
||||
/** |
||||
* The width of the screen. |
||||
* |
||||
* @private |
||||
*/ |
||||
_screenWidth: number, |
||||
|
||||
/** |
||||
* Seek time in seconds. |
||||
* |
||||
* @private |
||||
*/ |
||||
_seek: number, |
||||
|
||||
/** |
||||
* Youtube id of the video to be played. |
||||
* |
||||
* @private |
||||
*/ |
||||
youtubeId: string |
||||
}; |
||||
|
||||
const YoutubeLargeVideo = (props: Props) => { |
||||
const playerRef = useRef(null); |
||||
|
||||
useEffect(() => { |
||||
playerRef.current && playerRef.current.getCurrentTime().then(time => { |
||||
const { _seek } = props; |
||||
|
||||
if (shouldSeekToPosition(_seek, time)) { |
||||
playerRef.current && playerRef.current.seekTo(_seek); |
||||
} |
||||
}); |
||||
}, [ props._seek ]); |
||||
|
||||
useEffect(() => { |
||||
props._onWideScreenChanged(props._isWideScreen); |
||||
}, [ props._isWideScreen ]); |
||||
|
||||
const onChangeState = e => |
||||
playerRef.current && playerRef.current.getCurrentTime().then(time => { |
||||
const { |
||||
_isOwner, |
||||
_isPlaying, |
||||
_seek |
||||
} = props; |
||||
|
||||
if (shouldSetNewStatus(_isOwner, e, _isPlaying, time, _seek)) { |
||||
props._onVideoChangeEvent(props.youtubeId, e, time, props._ownerId); |
||||
} |
||||
}); |
||||
const onReady = () => { |
||||
if (props._isOwner) { |
||||
props._onVideoReady( |
||||
props.youtubeId, |
||||
playerRef.current && playerRef.current.getCurrentTime(), |
||||
props._ownerId); |
||||
} |
||||
}; |
||||
|
||||
let playerHeight, playerWidth; |
||||
|
||||
if (props._isWideScreen) { |
||||
playerHeight = props._screenHeight; |
||||
playerWidth = playerHeight * 16 / 9; |
||||
} else { |
||||
playerWidth = props._screenWidth; |
||||
playerHeight = playerWidth * 9 / 16; |
||||
} |
||||
|
||||
return ( |
||||
<View |
||||
pointerEvents = { props._enableControls ? 'auto' : 'none' } |
||||
style = { styles.youtubeVideoContainer } > |
||||
<YoutubePlayer |
||||
height = { playerHeight } |
||||
initialPlayerParams = {{ |
||||
controls: props._enableControls, |
||||
modestbranding: true, |
||||
preventFullScreen: true |
||||
}} |
||||
/* eslint-disable react/jsx-no-bind */ |
||||
onChangeState = { onChangeState } |
||||
/* eslint-disable react/jsx-no-bind */ |
||||
onReady = { onReady } |
||||
play = { props._isPlaying } |
||||
playbackRate = { 1 } |
||||
ref = { playerRef } |
||||
videoId = { props.youtubeId } |
||||
volume = { 50 } |
||||
webViewProps = {{ |
||||
bounces: false, |
||||
mediaPlaybackRequiresUserAction: false, |
||||
scrollEnabled: false, |
||||
userAgent: webviewUserAgent |
||||
}} |
||||
width = { playerWidth } /> |
||||
</View>); |
||||
}; |
||||
|
||||
/* eslint-disable max-params */ |
||||
|
||||
/** |
||||
* Return true if the user is the owner and |
||||
* the status has changed or the seek time difference from the previous set is larger than 5 seconds. |
||||
* |
||||
* @param {boolean} isOwner - Whether the local user is sharing the video. |
||||
* @param {string} status - The new status. |
||||
* @param {boolean} isPlaying - Whether the component is playing at the moment. |
||||
* @param {number} newTime - The new seek time. |
||||
* @param {number} previousTime - The old seek time. |
||||
* @private |
||||
* @returns {boolean} |
||||
*/ |
||||
function shouldSetNewStatus(isOwner, status, isPlaying, newTime, previousTime) { |
||||
if (!isOwner || status === 'buffering') { |
||||
return false; |
||||
} |
||||
|
||||
if ((isPlaying && status === 'paused') || (!isPlaying && status === 'playing')) { |
||||
return true; |
||||
} |
||||
|
||||
return shouldSeekToPosition(newTime, previousTime); |
||||
} |
||||
|
||||
/** |
||||
* Return true if the diffenrece between the two timees is larger than 5. |
||||
* |
||||
* @param {number} newTime - The current time. |
||||
* @param {number} previousTime - The previous time. |
||||
* @private |
||||
* @returns {boolean} |
||||
*/ |
||||
function shouldSeekToPosition(newTime, previousTime) { |
||||
return Math.abs(newTime - previousTime) > 5; |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated YoutubeLargeVideo's props. |
||||
* |
||||
* @param {Object} state - Redux state. |
||||
* @private |
||||
* @returns {Props} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const { ownerId, status, time } = state['features/youtube-player']; |
||||
const localParticipant = getLocalParticipant(state); |
||||
const responsiveUi = state['features/base/responsive-ui']; |
||||
const screenHeight = responsiveUi.clientHeight; |
||||
const screenWidth = responsiveUi.clientWidth; |
||||
|
||||
return { |
||||
_enableControls: ownerId === localParticipant.id, |
||||
_isOwner: ownerId === localParticipant.id, |
||||
_isPlaying: status === 'playing', |
||||
_isWideScreen: responsiveUi.aspectRatio === ASPECT_RATIO_WIDE, |
||||
_ownerId: ownerId, |
||||
_screenHeight: screenHeight, |
||||
_screenWidth: screenWidth, |
||||
_seek: time |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Maps dispatching of some action to React component props. |
||||
* |
||||
* @param {Function} dispatch - Redux action dispatcher. |
||||
* @private |
||||
* @returns {{ |
||||
* onVideoChangeEvent: Function, |
||||
* onVideoReady: Function, |
||||
* onWideScreenChanged: Function |
||||
* }} |
||||
*/ |
||||
function _mapDispatchToProps(dispatch) { |
||||
return { |
||||
_onVideoChangeEvent: (videoId, status, time, ownerId) => { |
||||
if (![ 'playing', 'paused' ].includes(status)) { |
||||
return; |
||||
} |
||||
dispatch(setSharedVideoStatus(videoId, translateStatus(status), time, ownerId)); |
||||
}, |
||||
_onVideoReady: (videoId, time, ownerId) => { |
||||
time.then(t => dispatch(setSharedVideoStatus(videoId, 'playing', t, ownerId))); |
||||
}, |
||||
_onWideScreenChanged: isWideScreen => { |
||||
dispatch(setToolboxVisible(!isWideScreen)); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated YoutubeLargeVideo's props. |
||||
* |
||||
* @private |
||||
* @returns {Props} |
||||
*/ |
||||
function _mergeProps({ _isOwner, ...stateProps }, { _onVideoChangeEvent, _onVideoReady, _onWideScreenChanged }) { |
||||
return Object.assign(stateProps, { |
||||
_onVideoChangeEvent: _isOwner ? _onVideoChangeEvent : () => { /* do nothing */ }, |
||||
_onVideoReady: _isOwner ? _onVideoReady : () => { /* do nothing */ }, |
||||
_onWideScreenChanged |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* In case the status is 'paused', it is translated to 'pause' to match the web functionality. |
||||
* |
||||
* @param {string} status - The status of the shared video. |
||||
* @private |
||||
* @returns {string} |
||||
*/ |
||||
function translateStatus(status) { |
||||
if (status === 'paused') { |
||||
return 'pause'; |
||||
} |
||||
|
||||
return status; |
||||
} |
||||
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps, _mergeProps)(YoutubeLargeVideo); |
@ -0,0 +1,2 @@ |
||||
export { default as EnterVideoLinkPrompt } from './EnterVideoLinkPrompt'; |
||||
export { default as YoutubeLargeVideo } from './YoutubeLargeVideo'; |
@ -0,0 +1,13 @@ |
||||
// @flow
|
||||
|
||||
/** |
||||
* The style of toolbar buttons. |
||||
*/ |
||||
export default { |
||||
youtubeVideoContainer: { |
||||
alignItems: 'center', |
||||
flex: 1, |
||||
flexDirection: 'column', |
||||
justifyContent: 'center' |
||||
} |
||||
}; |
@ -0,0 +1,6 @@ |
||||
export * from './actions'; |
||||
export * from './actionTypes'; |
||||
export * from './components'; |
||||
|
||||
import './middleware'; |
||||
import './reducer'; |
@ -0,0 +1,183 @@ |
||||
// @flow
|
||||
|
||||
import { CONFERENCE_LEFT, getCurrentConference } from '../base/conference'; |
||||
import { |
||||
PARTICIPANT_LEFT, |
||||
getLocalParticipant, |
||||
participantJoined, |
||||
participantLeft, |
||||
pinParticipant |
||||
} from '../base/participants'; |
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux'; |
||||
|
||||
import { TOGGLE_SHARED_VIDEO, SET_SHARED_VIDEO_STATUS } from './actionTypes'; |
||||
import { setSharedVideoStatus, showEnterVideoLinkPrompt } from './actions'; |
||||
|
||||
const SHARED_VIDEO = 'shared-video'; |
||||
|
||||
/** |
||||
* Middleware that captures actions related to YouTube 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; |
||||
const state = getState(); |
||||
const conference = getCurrentConference(state); |
||||
const localParticipantId = getLocalParticipant(state)?.id; |
||||
const { videoId, status, ownerId, time } = action; |
||||
|
||||
switch (action.type) { |
||||
case TOGGLE_SHARED_VIDEO: |
||||
_toggleSharedVideo(store, next, action); |
||||
break; |
||||
case CONFERENCE_LEFT: |
||||
dispatch(setSharedVideoStatus('', 'stop', 0, '')); |
||||
break; |
||||
case PARTICIPANT_LEFT: |
||||
if (action.participant.id === action.ownerId) { |
||||
dispatch(setSharedVideoStatus('', 'stop', 0, '')); |
||||
} |
||||
break; |
||||
case SET_SHARED_VIDEO_STATUS: |
||||
if (localParticipantId === ownerId) { |
||||
sendShareVideoCommand(videoId, status, conference, localParticipantId, time); |
||||
} |
||||
break; |
||||
} |
||||
|
||||
return next(action); |
||||
}); |
||||
|
||||
/** |
||||
* Set up state change listener to perform maintenance tasks when the conference |
||||
* is left or failed, e.g. clear messages or close the chat modal if it's left |
||||
* open. |
||||
*/ |
||||
StateListenerRegistry.register( |
||||
state => getCurrentConference(state), |
||||
(conference, store, previousConference) => { |
||||
if (conference && conference !== previousConference) { |
||||
conference.addCommandListener(SHARED_VIDEO, |
||||
({ value, attributes }) => { |
||||
|
||||
const { dispatch, getState } = store; |
||||
const { from } = attributes; |
||||
const localParticipantId = getLocalParticipant(getState()).id; |
||||
const status = attributes.state; |
||||
|
||||
if ([ 'playing', 'pause', 'start' ].includes(status)) { |
||||
handleSharingVideoStatus(store, value, attributes, conference); |
||||
} else if (status === 'stop') { |
||||
dispatch(participantLeft(value, conference)); |
||||
if (localParticipantId !== from) { |
||||
dispatch(setSharedVideoStatus(value, 'stop', 0, from)); |
||||
} |
||||
} |
||||
} |
||||
); |
||||
} |
||||
}); |
||||
|
||||
/** |
||||
* 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} videoId - The YoutubeId 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, videoId, { state, time, from }, conference) { |
||||
const { dispatch, getState } = store; |
||||
const localParticipantId = getLocalParticipant(getState()).id; |
||||
const oldStatus = getState()['features/youtube-player']?.status; |
||||
|
||||
if (state === 'start' || ![ 'playing', 'pause', 'start' ].includes(oldStatus)) { |
||||
dispatch(participantJoined({ |
||||
conference, |
||||
id: videoId, |
||||
isFakeParticipant: true, |
||||
avatarURL: `https://img.youtube.com/vi/${videoId}/0.jpg`, |
||||
name: 'YouTube' |
||||
})); |
||||
|
||||
dispatch(pinParticipant(videoId)); |
||||
} |
||||
|
||||
if (localParticipantId !== from) { |
||||
dispatch(setSharedVideoStatus(videoId, state, time, from)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Dispatches shared video status. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the |
||||
* specified {@code action} in the specified {@code store}. |
||||
* @param {Action} action - The redux action which is |
||||
* being dispatched in the specified {@code store}. |
||||
* @returns {Function} |
||||
*/ |
||||
function _toggleSharedVideo(store, next, action) { |
||||
const { dispatch, getState } = store; |
||||
const state = getState(); |
||||
const { videoId, ownerId, status } = state['features/youtube-player']; |
||||
const localParticipant = getLocalParticipant(state); |
||||
|
||||
if (status === 'playing' || status === 'start' || status === 'pause') { |
||||
if (ownerId === localParticipant.id) { |
||||
dispatch(setSharedVideoStatus(videoId, 'stop', 0, localParticipant.id)); |
||||
} |
||||
} else { |
||||
dispatch(showEnterVideoLinkPrompt(id => _onVideoLinkEntered(store, id))); |
||||
} |
||||
|
||||
return next(action); |
||||
} |
||||
|
||||
/** |
||||
* Sends SHARED_VIDEO start command. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @param {string} id - The youtube id of the video to be shared. |
||||
* @returns {void} |
||||
*/ |
||||
function _onVideoLinkEntered(store, id) { |
||||
const { dispatch, getState } = store; |
||||
const conference = getCurrentConference(getState()); |
||||
|
||||
if (conference) { |
||||
const localParticipant = getLocalParticipant(getState()); |
||||
|
||||
dispatch(setSharedVideoStatus(id, 'start', 0, localParticipant.id)); |
||||
} |
||||
} |
||||
|
||||
/* eslint-disable max-params */ |
||||
|
||||
/** |
||||
* Sends SHARED_VIDEO command. |
||||
* |
||||
* @param {string} id - The youtube 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} |
||||
*/ |
||||
function sendShareVideoCommand(id, status, conference, localParticipantId, time) { |
||||
conference.sendCommandOnce(SHARED_VIDEO, { |
||||
value: id, |
||||
attributes: { |
||||
from: localParticipantId, |
||||
state: status, |
||||
time |
||||
} |
||||
}); |
||||
} |
@ -0,0 +1,24 @@ |
||||
// @flow
|
||||
import { ReducerRegistry } from '../base/redux'; |
||||
|
||||
import { SET_SHARED_VIDEO_STATUS } from './actionTypes'; |
||||
|
||||
/** |
||||
* Reduces the Redux actions of the feature features/youtube-player. |
||||
*/ |
||||
ReducerRegistry.register('features/youtube-player', (state = {}, action) => { |
||||
const { videoId, status, time, ownerId } = action; |
||||
|
||||
switch (action.type) { |
||||
case SET_SHARED_VIDEO_STATUS: |
||||
return { |
||||
...state, |
||||
videoId, |
||||
status, |
||||
time, |
||||
ownerId |
||||
}; |
||||
default: |
||||
return state; |
||||
} |
||||
}); |
Loading…
Reference in new issue