rn: add youtube player for mobile app

This commit is contained in:
tmoldovan8x8 2020-06-12 13:15:16 +03:00 committed by GitHub
parent 8758c222c6
commit df64dd8f18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 923 additions and 17 deletions

View File

@ -2517,6 +2517,8 @@ export default {
if (state === 'stop'
|| state === 'start'
|| state === 'playing') {
const localParticipant = getLocalParticipant(APP.store.getState());
room.removeCommand(this.commands.defaults.SHARED_VIDEO);
room.sendCommandOnce(this.commands.defaults.SHARED_VIDEO, {
value: url,
@ -2524,7 +2526,8 @@ export default {
state,
time,
muted: isMuted,
volume
volume,
from: localParticipant.id
}
});
} else {

11
package-lock.json generated
View File

@ -8057,8 +8057,7 @@
"events": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
"integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==",
"dev": true
"integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg=="
},
"eventsource": {
"version": "1.0.7",
@ -15104,6 +15103,14 @@
}
}
},
"react-native-youtube-iframe": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-1.2.3.tgz",
"integrity": "sha512-3O8OFJyohGNlYX4D97aWfLLlhEHhlLHDCLgXM+SsQBwP9r1oLnKgXWoy1gce+Vr8qgrqeQgmx1ki+10AAd4KWQ==",
"requires": {
"events": "^3.0.0"
}
},
"react-node-resolver": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/react-node-resolver/-/react-node-resolver-1.0.1.tgz",

View File

@ -83,6 +83,7 @@
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "1.75.3",
"react-native-webview": "7.4.1",
"react-native-youtube-iframe": "1.2.3",
"react-redux": "7.1.0",
"react-textarea-autosize": "7.1.0",
"react-transition-group": "2.4.0",

View File

@ -3,6 +3,7 @@
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { YoutubeLargeVideo } from '../../../youtube-player';
import { Avatar } from '../../avatar';
import { translate } from '../../i18n';
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
@ -15,7 +16,7 @@ import { connect } from '../../redux';
import type { StyleType } from '../../styles';
import { TestHint } from '../../testing/components';
import { getTrackByMediaTypeAndParticipant } from '../../tracks';
import { shouldRenderParticipantVideo } from '../functions';
import { shouldRenderParticipantVideo, getParticipantById } from '../functions';
import styles from './styles';
@ -33,6 +34,13 @@ type Props = {
*/
_connectionStatus: string,
/**
* True if the participant which this component represents is fake.
*
* @private
*/
_isFakeParticipant: boolean,
/**
* The name of the participant which this component represents.
*
@ -181,8 +189,10 @@ class ParticipantView extends Component<Props> {
render() {
const {
_connectionStatus: connectionStatus,
_isFakeParticipant,
_renderVideo: renderVideo,
_videoTrack: videoTrack,
disableVideo,
onPress,
tintStyle
} = this.props;
@ -198,9 +208,11 @@ class ParticipantView extends Component<Props> {
? this.props.testHintId
: `org.jitsi.meet.Participant#${this.props.participantId}`;
const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo;
return (
<Container
onClick = { renderVideo ? undefined : onPress }
onClick = { renderVideo || renderYoutubeLargeVideo ? undefined : onPress }
style = {{
...styles.participantView,
...this.props.style
@ -209,10 +221,12 @@ class ParticipantView extends Component<Props> {
<TestHint
id = { testHintId }
onPress = { onPress }
onPress = { renderYoutubeLargeVideo ? undefined : onPress }
value = '' />
{ renderVideo
{ renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { this.props.participantId } /> }
{ !_isFakeParticipant && renderVideo
&& <VideoTrack
onPress = { onPress }
videoTrack = { videoTrack }
@ -220,7 +234,7 @@ class ParticipantView extends Component<Props> {
zOrder = { this.props.zOrder }
zoomEnabled = { this.props.zoomEnabled } /> }
{ !renderVideo
{ !renderYoutubeLargeVideo && !renderVideo
&& <View style = { styles.avatarContainer }>
<Avatar
participantId = { this.props.participantId }
@ -253,6 +267,7 @@ class ParticipantView extends Component<Props> {
*/
function _mapStateToProps(state, ownProps) {
const { disableVideo, participantId } = ownProps;
const participant = getParticipantById(state, participantId);
let connectionStatus;
let participantName;
@ -260,6 +275,7 @@ function _mapStateToProps(state, ownProps) {
_connectionStatus:
connectionStatus
|| JitsiParticipantConnectionStatus.ACTIVE,
_isFakeParticipant: participant && participant.isFakeParticipant,
_participantName: participantName,
_renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo,
_videoTrack:

View File

@ -241,6 +241,23 @@ function _getAllParticipants(stateful) {
: toState(stateful)['features/base/participants'] || []);
}
/**
* Returns the youtube fake participant.
* At the moment it is considered the youtube participant the only fake participant in the list.
*
* @param {(Function|Object|Participant[])} stateful - The redux state
* features/base/participants, the (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @private
* @returns {Participant}
*/
export function getYoutubeParticipant(stateful: Object | Function) {
const participants = _getAllParticipants(stateful);
return participants.filter(p => p.isFakeParticipant)[0];
}
/**
* Returns true if all of the meeting participants are moderators.
*

View File

@ -5,6 +5,7 @@ import { Text, View } from 'react-native';
import {
getLocalParticipant,
getParticipantById,
getParticipantDisplayName,
shouldRenderParticipantVideo
} from '../../../base/participants';
@ -65,13 +66,16 @@ class DisplayNameLabel extends Component<Props> {
function _mapStateToProps(state: Object, ownProps: Props) {
const { participantId } = ownProps;
const localParticipant = getLocalParticipant(state);
const participant = getParticipantById(state, participantId);
const isFakeParticipant = participant && participant.isFakeParticipant;
// Currently we only render the display name if it's not the local
// participant and there is no video rendered for
// them.
const _render = Boolean(participantId)
&& localParticipant.id !== participantId
&& !shouldRenderParticipantVideo(state, participantId);
&& !shouldRenderParticipantVideo(state, participantId)
&& !isFakeParticipant;
return {
_participantName:

View File

@ -150,7 +150,7 @@ function Thumbnail(props: Props) {
<ParticipantView
avatarSize = { AVATAR_SIZE }
disableVideo = { isScreenShare }
disableVideo = { isScreenShare || participant.isFakeParticipant }
participantId = { participantId }
style = { _styles.participantViewStyle }
tintEnabled = { participantInLargeVideo && !disableTint }
@ -162,32 +162,32 @@ function Thumbnail(props: Props) {
{ renderModeratorIndicator
&& <View style = { styles.moderatorIndicatorContainer }>
<ModeratorIndicator />
</View> }
</View>}
<View
{ !participant.isFakeParticipant && <View
style = { [
styles.thumbnailTopIndicatorContainer,
styles.thumbnailTopLeftIndicatorContainer
] }>
<RaisedHandIndicator participantId = { participant.id } />
{ renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
</View>
</View> }
<View
{ !participant.isFakeParticipant && <View
style = { [
styles.thumbnailTopIndicatorContainer,
styles.thumbnailTopRightIndicatorContainer
] }>
<ConnectionIndicator participantId = { participant.id } />
</View>
</View> }
<Container style = { styles.thumbnailIndicatorContainer }>
{ !participant.isFakeParticipant && <Container style = { styles.thumbnailIndicatorContainer }>
{ audioMuted
&& <AudioMutedIndicator /> }
{ videoMuted
&& <VideoMutedIndicator /> }
</Container>
</Container> }
</Container>
);

View File

@ -17,6 +17,7 @@ import { LiveStreamButton, RecordButton } from '../../../recording';
import { RoomLockButton } from '../../../room-lock';
import { ClosedCaptionButton } from '../../../subtitles';
import { TileViewButton } from '../../../video-layout';
import { VideoShareButton } from '../../../youtube-player';
import HelpButton from '../HelpButton';
import AudioOnlyButton from './AudioOnlyButton';
@ -136,6 +137,7 @@ class OverflowMenu extends PureComponent<Props, State> {
<TileViewButton { ...buttonProps } />
<RecordButton { ...buttonProps } />
<LiveStreamButton { ...buttonProps } />
<VideoShareButton { ...buttonProps } />
<RoomLockButton { ...buttonProps } />
<ClosedCaptionButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />

View File

@ -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';

View File

@ -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 });
}

View File

@ -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;
}

View File

@ -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));

View File

@ -0,0 +1 @@
export * from './native';

View File

@ -0,0 +1,3 @@
export { default as VideoShareButton } from './VideoShareButton';
export * from './_';

View File

@ -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);

View File

@ -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);

View File

@ -0,0 +1,2 @@
export { default as EnterVideoLinkPrompt } from './EnterVideoLinkPrompt';
export { default as YoutubeLargeVideo } from './YoutubeLargeVideo';

View File

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

View File

@ -0,0 +1,6 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
import './middleware';
import './reducer';

View File

@ -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
}
});
}

View File

@ -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;
}
});