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'
|
if (state === 'stop'
|
||||||
|| state === 'start'
|
|| state === 'start'
|
||||||
|| state === 'playing') {
|
|| state === 'playing') {
|
||||||
|
const localParticipant = getLocalParticipant(APP.store.getState());
|
||||||
|
|
||||||
room.removeCommand(this.commands.defaults.SHARED_VIDEO);
|
room.removeCommand(this.commands.defaults.SHARED_VIDEO);
|
||||||
room.sendCommandOnce(this.commands.defaults.SHARED_VIDEO, {
|
room.sendCommandOnce(this.commands.defaults.SHARED_VIDEO, {
|
||||||
value: url,
|
value: url,
|
||||||
|
@ -2524,7 +2526,8 @@ export default {
|
||||||
state,
|
state,
|
||||||
time,
|
time,
|
||||||
muted: isMuted,
|
muted: isMuted,
|
||||||
volume
|
volume,
|
||||||
|
from: localParticipant.id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8057,8 +8057,7 @@
|
||||||
"events": {
|
"events": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
|
||||||
"integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==",
|
"integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"eventsource": {
|
"eventsource": {
|
||||||
"version": "1.0.7",
|
"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": {
|
"react-node-resolver": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-node-resolver/-/react-node-resolver-1.0.1.tgz",
|
"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-watch-connectivity": "0.4.3",
|
||||||
"react-native-webrtc": "1.75.3",
|
"react-native-webrtc": "1.75.3",
|
||||||
"react-native-webview": "7.4.1",
|
"react-native-webview": "7.4.1",
|
||||||
|
"react-native-youtube-iframe": "1.2.3",
|
||||||
"react-redux": "7.1.0",
|
"react-redux": "7.1.0",
|
||||||
"react-textarea-autosize": "7.1.0",
|
"react-textarea-autosize": "7.1.0",
|
||||||
"react-transition-group": "2.4.0",
|
"react-transition-group": "2.4.0",
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Text, View } from 'react-native';
|
import { Text, View } from 'react-native';
|
||||||
|
|
||||||
|
import { YoutubeLargeVideo } from '../../../youtube-player';
|
||||||
import { Avatar } from '../../avatar';
|
import { Avatar } from '../../avatar';
|
||||||
import { translate } from '../../i18n';
|
import { translate } from '../../i18n';
|
||||||
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
|
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
|
||||||
|
@ -15,7 +16,7 @@ import { connect } from '../../redux';
|
||||||
import type { StyleType } from '../../styles';
|
import type { StyleType } from '../../styles';
|
||||||
import { TestHint } from '../../testing/components';
|
import { TestHint } from '../../testing/components';
|
||||||
import { getTrackByMediaTypeAndParticipant } from '../../tracks';
|
import { getTrackByMediaTypeAndParticipant } from '../../tracks';
|
||||||
import { shouldRenderParticipantVideo } from '../functions';
|
import { shouldRenderParticipantVideo, getParticipantById } from '../functions';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
|
@ -33,6 +34,13 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_connectionStatus: string,
|
_connectionStatus: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the participant which this component represents is fake.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_isFakeParticipant: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the participant which this component represents.
|
* The name of the participant which this component represents.
|
||||||
*
|
*
|
||||||
|
@ -181,8 +189,10 @@ class ParticipantView extends Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
_connectionStatus: connectionStatus,
|
_connectionStatus: connectionStatus,
|
||||||
|
_isFakeParticipant,
|
||||||
_renderVideo: renderVideo,
|
_renderVideo: renderVideo,
|
||||||
_videoTrack: videoTrack,
|
_videoTrack: videoTrack,
|
||||||
|
disableVideo,
|
||||||
onPress,
|
onPress,
|
||||||
tintStyle
|
tintStyle
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -198,9 +208,11 @@ class ParticipantView extends Component<Props> {
|
||||||
? this.props.testHintId
|
? this.props.testHintId
|
||||||
: `org.jitsi.meet.Participant#${this.props.participantId}`;
|
: `org.jitsi.meet.Participant#${this.props.participantId}`;
|
||||||
|
|
||||||
|
const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
onClick = { renderVideo ? undefined : onPress }
|
onClick = { renderVideo || renderYoutubeLargeVideo ? undefined : onPress }
|
||||||
style = {{
|
style = {{
|
||||||
...styles.participantView,
|
...styles.participantView,
|
||||||
...this.props.style
|
...this.props.style
|
||||||
|
@ -209,10 +221,12 @@ class ParticipantView extends Component<Props> {
|
||||||
|
|
||||||
<TestHint
|
<TestHint
|
||||||
id = { testHintId }
|
id = { testHintId }
|
||||||
onPress = { onPress }
|
onPress = { renderYoutubeLargeVideo ? undefined : onPress }
|
||||||
value = '' />
|
value = '' />
|
||||||
|
|
||||||
{ renderVideo
|
{ renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { this.props.participantId } /> }
|
||||||
|
|
||||||
|
{ !_isFakeParticipant && renderVideo
|
||||||
&& <VideoTrack
|
&& <VideoTrack
|
||||||
onPress = { onPress }
|
onPress = { onPress }
|
||||||
videoTrack = { videoTrack }
|
videoTrack = { videoTrack }
|
||||||
|
@ -220,7 +234,7 @@ class ParticipantView extends Component<Props> {
|
||||||
zOrder = { this.props.zOrder }
|
zOrder = { this.props.zOrder }
|
||||||
zoomEnabled = { this.props.zoomEnabled } /> }
|
zoomEnabled = { this.props.zoomEnabled } /> }
|
||||||
|
|
||||||
{ !renderVideo
|
{ !renderYoutubeLargeVideo && !renderVideo
|
||||||
&& <View style = { styles.avatarContainer }>
|
&& <View style = { styles.avatarContainer }>
|
||||||
<Avatar
|
<Avatar
|
||||||
participantId = { this.props.participantId }
|
participantId = { this.props.participantId }
|
||||||
|
@ -253,6 +267,7 @@ class ParticipantView extends Component<Props> {
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state, ownProps) {
|
function _mapStateToProps(state, ownProps) {
|
||||||
const { disableVideo, participantId } = ownProps;
|
const { disableVideo, participantId } = ownProps;
|
||||||
|
const participant = getParticipantById(state, participantId);
|
||||||
let connectionStatus;
|
let connectionStatus;
|
||||||
let participantName;
|
let participantName;
|
||||||
|
|
||||||
|
@ -260,6 +275,7 @@ function _mapStateToProps(state, ownProps) {
|
||||||
_connectionStatus:
|
_connectionStatus:
|
||||||
connectionStatus
|
connectionStatus
|
||||||
|| JitsiParticipantConnectionStatus.ACTIVE,
|
|| JitsiParticipantConnectionStatus.ACTIVE,
|
||||||
|
_isFakeParticipant: participant && participant.isFakeParticipant,
|
||||||
_participantName: participantName,
|
_participantName: participantName,
|
||||||
_renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo,
|
_renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo,
|
||||||
_videoTrack:
|
_videoTrack:
|
||||||
|
|
|
@ -241,6 +241,23 @@ function _getAllParticipants(stateful) {
|
||||||
: toState(stateful)['features/base/participants'] || []);
|
: 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.
|
* Returns true if all of the meeting participants are moderators.
|
||||||
*
|
*
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Text, View } from 'react-native';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getLocalParticipant,
|
getLocalParticipant,
|
||||||
|
getParticipantById,
|
||||||
getParticipantDisplayName,
|
getParticipantDisplayName,
|
||||||
shouldRenderParticipantVideo
|
shouldRenderParticipantVideo
|
||||||
} from '../../../base/participants';
|
} from '../../../base/participants';
|
||||||
|
@ -65,13 +66,16 @@ class DisplayNameLabel extends Component<Props> {
|
||||||
function _mapStateToProps(state: Object, ownProps: Props) {
|
function _mapStateToProps(state: Object, ownProps: Props) {
|
||||||
const { participantId } = ownProps;
|
const { participantId } = ownProps;
|
||||||
const localParticipant = getLocalParticipant(state);
|
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
|
// Currently we only render the display name if it's not the local
|
||||||
// participant and there is no video rendered for
|
// participant and there is no video rendered for
|
||||||
// them.
|
// them.
|
||||||
const _render = Boolean(participantId)
|
const _render = Boolean(participantId)
|
||||||
&& localParticipant.id !== participantId
|
&& localParticipant.id !== participantId
|
||||||
&& !shouldRenderParticipantVideo(state, participantId);
|
&& !shouldRenderParticipantVideo(state, participantId)
|
||||||
|
&& !isFakeParticipant;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_participantName:
|
_participantName:
|
||||||
|
|
|
@ -150,7 +150,7 @@ function Thumbnail(props: Props) {
|
||||||
|
|
||||||
<ParticipantView
|
<ParticipantView
|
||||||
avatarSize = { AVATAR_SIZE }
|
avatarSize = { AVATAR_SIZE }
|
||||||
disableVideo = { isScreenShare }
|
disableVideo = { isScreenShare || participant.isFakeParticipant }
|
||||||
participantId = { participantId }
|
participantId = { participantId }
|
||||||
style = { _styles.participantViewStyle }
|
style = { _styles.participantViewStyle }
|
||||||
tintEnabled = { participantInLargeVideo && !disableTint }
|
tintEnabled = { participantInLargeVideo && !disableTint }
|
||||||
|
@ -162,32 +162,32 @@ function Thumbnail(props: Props) {
|
||||||
{ renderModeratorIndicator
|
{ renderModeratorIndicator
|
||||||
&& <View style = { styles.moderatorIndicatorContainer }>
|
&& <View style = { styles.moderatorIndicatorContainer }>
|
||||||
<ModeratorIndicator />
|
<ModeratorIndicator />
|
||||||
</View> }
|
</View>}
|
||||||
|
|
||||||
<View
|
{ !participant.isFakeParticipant && <View
|
||||||
style = { [
|
style = { [
|
||||||
styles.thumbnailTopIndicatorContainer,
|
styles.thumbnailTopIndicatorContainer,
|
||||||
styles.thumbnailTopLeftIndicatorContainer
|
styles.thumbnailTopLeftIndicatorContainer
|
||||||
] }>
|
] }>
|
||||||
<RaisedHandIndicator participantId = { participant.id } />
|
<RaisedHandIndicator participantId = { participant.id } />
|
||||||
{ renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
|
{ renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
|
||||||
</View>
|
</View> }
|
||||||
|
|
||||||
<View
|
{ !participant.isFakeParticipant && <View
|
||||||
style = { [
|
style = { [
|
||||||
styles.thumbnailTopIndicatorContainer,
|
styles.thumbnailTopIndicatorContainer,
|
||||||
styles.thumbnailTopRightIndicatorContainer
|
styles.thumbnailTopRightIndicatorContainer
|
||||||
] }>
|
] }>
|
||||||
<ConnectionIndicator participantId = { participant.id } />
|
<ConnectionIndicator participantId = { participant.id } />
|
||||||
</View>
|
</View> }
|
||||||
|
|
||||||
<Container style = { styles.thumbnailIndicatorContainer }>
|
{ !participant.isFakeParticipant && <Container style = { styles.thumbnailIndicatorContainer }>
|
||||||
{ audioMuted
|
{ audioMuted
|
||||||
&& <AudioMutedIndicator /> }
|
&& <AudioMutedIndicator /> }
|
||||||
|
|
||||||
{ videoMuted
|
{ videoMuted
|
||||||
&& <VideoMutedIndicator /> }
|
&& <VideoMutedIndicator /> }
|
||||||
</Container>
|
</Container> }
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { LiveStreamButton, RecordButton } from '../../../recording';
|
||||||
import { RoomLockButton } from '../../../room-lock';
|
import { RoomLockButton } from '../../../room-lock';
|
||||||
import { ClosedCaptionButton } from '../../../subtitles';
|
import { ClosedCaptionButton } from '../../../subtitles';
|
||||||
import { TileViewButton } from '../../../video-layout';
|
import { TileViewButton } from '../../../video-layout';
|
||||||
|
import { VideoShareButton } from '../../../youtube-player';
|
||||||
import HelpButton from '../HelpButton';
|
import HelpButton from '../HelpButton';
|
||||||
|
|
||||||
import AudioOnlyButton from './AudioOnlyButton';
|
import AudioOnlyButton from './AudioOnlyButton';
|
||||||
|
@ -136,6 +137,7 @@ class OverflowMenu extends PureComponent<Props, State> {
|
||||||
<TileViewButton { ...buttonProps } />
|
<TileViewButton { ...buttonProps } />
|
||||||
<RecordButton { ...buttonProps } />
|
<RecordButton { ...buttonProps } />
|
||||||
<LiveStreamButton { ...buttonProps } />
|
<LiveStreamButton { ...buttonProps } />
|
||||||
|
<VideoShareButton { ...buttonProps } />
|
||||||
<RoomLockButton { ...buttonProps } />
|
<RoomLockButton { ...buttonProps } />
|
||||||
<ClosedCaptionButton { ...buttonProps } />
|
<ClosedCaptionButton { ...buttonProps } />
|
||||||
<SharedDocumentButton { ...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