rn: add youtube player for mobile app
This commit is contained in:
parent
8758c222c6
commit
df64dd8f18
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 } />
|
||||
|
|
|
@ -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