Browse Source

rn: add youtube player for mobile app

j8
tmoldovan8x8 4 years ago
parent
commit
df64dd8f18
No account linked to committer's email address

+ 4
- 1
conference.js View File

@@ -2517,6 +2517,8 @@ export default {
2517 2517
                 if (state === 'stop'
2518 2518
                         || state === 'start'
2519 2519
                         || state === 'playing') {
2520
+                    const localParticipant = getLocalParticipant(APP.store.getState());
2521
+
2520 2522
                     room.removeCommand(this.commands.defaults.SHARED_VIDEO);
2521 2523
                     room.sendCommandOnce(this.commands.defaults.SHARED_VIDEO, {
2522 2524
                         value: url,
@@ -2524,7 +2526,8 @@ export default {
2524 2526
                             state,
2525 2527
                             time,
2526 2528
                             muted: isMuted,
2527
-                            volume
2529
+                            volume,
2530
+                            from: localParticipant.id
2528 2531
                         }
2529 2532
                     });
2530 2533
                 } else {

+ 9
- 2
package-lock.json View File

@@ -8057,8 +8057,7 @@
8057 8057
     "events": {
8058 8058
       "version": "3.1.0",
8059 8059
       "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
8060
-      "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==",
8061
-      "dev": true
8060
+      "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg=="
8062 8061
     },
8063 8062
     "eventsource": {
8064 8063
       "version": "1.0.7",
@@ -15104,6 +15103,14 @@
15104 15103
         }
15105 15104
       }
15106 15105
     },
15106
+    "react-native-youtube-iframe": {
15107
+      "version": "1.2.3",
15108
+      "resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-1.2.3.tgz",
15109
+      "integrity": "sha512-3O8OFJyohGNlYX4D97aWfLLlhEHhlLHDCLgXM+SsQBwP9r1oLnKgXWoy1gce+Vr8qgrqeQgmx1ki+10AAd4KWQ==",
15110
+      "requires": {
15111
+        "events": "^3.0.0"
15112
+      }
15113
+    },
15107 15114
     "react-node-resolver": {
15108 15115
       "version": "1.0.1",
15109 15116
       "resolved": "https://registry.npmjs.org/react-node-resolver/-/react-node-resolver-1.0.1.tgz",

+ 1
- 0
package.json View File

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

+ 21
- 5
react/features/base/participants/components/ParticipantView.native.js View File

@@ -3,6 +3,7 @@
3 3
 import React, { Component } from 'react';
4 4
 import { Text, View } from 'react-native';
5 5
 
6
+import { YoutubeLargeVideo } from '../../../youtube-player';
6 7
 import { Avatar } from '../../avatar';
7 8
 import { translate } from '../../i18n';
8 9
 import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
@@ -15,7 +16,7 @@ import { connect } from '../../redux';
15 16
 import type { StyleType } from '../../styles';
16 17
 import { TestHint } from '../../testing/components';
17 18
 import { getTrackByMediaTypeAndParticipant } from '../../tracks';
18
-import { shouldRenderParticipantVideo } from '../functions';
19
+import { shouldRenderParticipantVideo, getParticipantById } from '../functions';
19 20
 
20 21
 import styles from './styles';
21 22
 
@@ -33,6 +34,13 @@ type Props = {
33 34
      */
34 35
     _connectionStatus: string,
35 36
 
37
+    /**
38
+     * True if the participant which this component represents is fake.
39
+     *
40
+     * @private
41
+     */
42
+    _isFakeParticipant: boolean,
43
+
36 44
     /**
37 45
      * The name of the participant which this component represents.
38 46
      *
@@ -181,8 +189,10 @@ class ParticipantView extends Component<Props> {
181 189
     render() {
182 190
         const {
183 191
             _connectionStatus: connectionStatus,
192
+            _isFakeParticipant,
184 193
             _renderVideo: renderVideo,
185 194
             _videoTrack: videoTrack,
195
+            disableVideo,
186 196
             onPress,
187 197
             tintStyle
188 198
         } = this.props;
@@ -198,9 +208,11 @@ class ParticipantView extends Component<Props> {
198 208
                 ? this.props.testHintId
199 209
                 : `org.jitsi.meet.Participant#${this.props.participantId}`;
200 210
 
211
+        const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo;
212
+
201 213
         return (
202 214
             <Container
203
-                onClick = { renderVideo ? undefined : onPress }
215
+                onClick = { renderVideo || renderYoutubeLargeVideo ? undefined : onPress }
204 216
                 style = {{
205 217
                     ...styles.participantView,
206 218
                     ...this.props.style
@@ -209,10 +221,12 @@ class ParticipantView extends Component<Props> {
209 221
 
210 222
                 <TestHint
211 223
                     id = { testHintId }
212
-                    onPress = { onPress }
224
+                    onPress = { renderYoutubeLargeVideo ? undefined : onPress }
213 225
                     value = '' />
214 226
 
215
-                { renderVideo
227
+                { renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { this.props.participantId } /> }
228
+
229
+                { !_isFakeParticipant && renderVideo
216 230
                     && <VideoTrack
217 231
                         onPress = { onPress }
218 232
                         videoTrack = { videoTrack }
@@ -220,7 +234,7 @@ class ParticipantView extends Component<Props> {
220 234
                         zOrder = { this.props.zOrder }
221 235
                         zoomEnabled = { this.props.zoomEnabled } /> }
222 236
 
223
-                { !renderVideo
237
+                { !renderYoutubeLargeVideo && !renderVideo
224 238
                     && <View style = { styles.avatarContainer }>
225 239
                         <Avatar
226 240
                             participantId = { this.props.participantId }
@@ -253,6 +267,7 @@ class ParticipantView extends Component<Props> {
253 267
  */
254 268
 function _mapStateToProps(state, ownProps) {
255 269
     const { disableVideo, participantId } = ownProps;
270
+    const participant = getParticipantById(state, participantId);
256 271
     let connectionStatus;
257 272
     let participantName;
258 273
 
@@ -260,6 +275,7 @@ function _mapStateToProps(state, ownProps) {
260 275
         _connectionStatus:
261 276
             connectionStatus
262 277
                 || JitsiParticipantConnectionStatus.ACTIVE,
278
+        _isFakeParticipant: participant && participant.isFakeParticipant,
263 279
         _participantName: participantName,
264 280
         _renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo,
265 281
         _videoTrack:

+ 17
- 0
react/features/base/participants/functions.js View File

@@ -241,6 +241,23 @@ function _getAllParticipants(stateful) {
241 241
             : toState(stateful)['features/base/participants'] || []);
242 242
 }
243 243
 
244
+/**
245
+ * Returns the youtube fake participant.
246
+ * At the moment it is considered the youtube participant the only fake participant in the list.
247
+ *
248
+ * @param {(Function|Object|Participant[])} stateful - The redux state
249
+ * features/base/participants, the (whole) redux state, or redux's
250
+ * {@code getState} function to be used to retrieve the state
251
+ * features/base/participants.
252
+ * @private
253
+ * @returns {Participant}
254
+ */
255
+export function getYoutubeParticipant(stateful: Object | Function) {
256
+    const participants = _getAllParticipants(stateful);
257
+
258
+    return participants.filter(p => p.isFakeParticipant)[0];
259
+}
260
+
244 261
 /**
245 262
  * Returns true if all of the meeting participants are moderators.
246 263
  *

+ 5
- 1
react/features/display-name/components/native/DisplayNameLabel.js View File

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

+ 8
- 8
react/features/filmstrip/components/native/Thumbnail.js View File

@@ -150,7 +150,7 @@ function Thumbnail(props: Props) {
150 150
 
151 151
             <ParticipantView
152 152
                 avatarSize = { AVATAR_SIZE }
153
-                disableVideo = { isScreenShare }
153
+                disableVideo = { isScreenShare || participant.isFakeParticipant }
154 154
                 participantId = { participantId }
155 155
                 style = { _styles.participantViewStyle }
156 156
                 tintEnabled = { participantInLargeVideo && !disableTint }
@@ -162,32 +162,32 @@ function Thumbnail(props: Props) {
162 162
             { renderModeratorIndicator
163 163
                 && <View style = { styles.moderatorIndicatorContainer }>
164 164
                     <ModeratorIndicator />
165
-                </View> }
165
+                </View>}
166 166
 
167
-            <View
167
+            { !participant.isFakeParticipant && <View
168 168
                 style = { [
169 169
                     styles.thumbnailTopIndicatorContainer,
170 170
                     styles.thumbnailTopLeftIndicatorContainer
171 171
                 ] }>
172 172
                 <RaisedHandIndicator participantId = { participant.id } />
173 173
                 { renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
174
-            </View>
174
+            </View> }
175 175
 
176
-            <View
176
+            { !participant.isFakeParticipant && <View
177 177
                 style = { [
178 178
                     styles.thumbnailTopIndicatorContainer,
179 179
                     styles.thumbnailTopRightIndicatorContainer
180 180
                 ] }>
181 181
                 <ConnectionIndicator participantId = { participant.id } />
182
-            </View>
182
+            </View> }
183 183
 
184
-            <Container style = { styles.thumbnailIndicatorContainer }>
184
+            { !participant.isFakeParticipant && <Container style = { styles.thumbnailIndicatorContainer }>
185 185
                 { audioMuted
186 186
                     && <AudioMutedIndicator /> }
187 187
 
188 188
                 { videoMuted
189 189
                     && <VideoMutedIndicator /> }
190
-            </Container>
190
+            </Container> }
191 191
 
192 192
         </Container>
193 193
     );

+ 2
- 0
react/features/toolbox/components/native/OverflowMenu.js View File

@@ -17,6 +17,7 @@ import { LiveStreamButton, RecordButton } from '../../../recording';
17 17
 import { RoomLockButton } from '../../../room-lock';
18 18
 import { ClosedCaptionButton } from '../../../subtitles';
19 19
 import { TileViewButton } from '../../../video-layout';
20
+import { VideoShareButton } from '../../../youtube-player';
20 21
 import HelpButton from '../HelpButton';
21 22
 
22 23
 import AudioOnlyButton from './AudioOnlyButton';
@@ -136,6 +137,7 @@ class OverflowMenu extends PureComponent<Props, State> {
136 137
                     <TileViewButton { ...buttonProps } />
137 138
                     <RecordButton { ...buttonProps } />
138 139
                     <LiveStreamButton { ...buttonProps } />
140
+                    <VideoShareButton { ...buttonProps } />
139 141
                     <RoomLockButton { ...buttonProps } />
140 142
                     <ClosedCaptionButton { ...buttonProps } />
141 143
                     <SharedDocumentButton { ...buttonProps } />

+ 22
- 0
react/features/youtube-player/actionTypes.js View File

@@ -0,0 +1,22 @@
1
+/**
2
+ * The type of the action which signals to update the current known state of the
3
+ * shared YouTube video.
4
+ *
5
+ * {
6
+ *     type: SET_SHARED_VIDEO_STATUS,
7
+ *     status: string,
8
+ *     time: string,
9
+ *     ownerId: string
10
+ * }
11
+ */
12
+export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS';
13
+
14
+/**
15
+ * The type of the action which signals to start the flow for starting or
16
+ * stopping a shared YouTube video.
17
+ *
18
+ * {
19
+ *     type: TOGGLE_SHARED_VIDEO
20
+ * }
21
+ */
22
+export const TOGGLE_SHARED_VIDEO = 'TOGGLE_SHARED_VIDEO';

+ 54
- 0
react/features/youtube-player/actions.js View File

@@ -0,0 +1,54 @@
1
+// @flow
2
+
3
+import { openDialog } from '../base/dialog';
4
+
5
+import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
6
+import { EnterVideoLinkPrompt } from './components';
7
+
8
+/**
9
+ * Updates the current known status of the shared YouTube video.
10
+ *
11
+ * @param {string} videoId - The youtubeId of the video to be shared.
12
+ * @param {string} status - The current status of the YouTube video being shared.
13
+ * @param {number} time - The current position of the YouTube video being shared.
14
+ * @param {string} ownerId - The participantId of the user sharing the YouTube video.
15
+ * @returns {{
16
+ *     type: SET_SHARED_VIDEO_STATUS,
17
+ *     ownerId: string,
18
+ *     status: string,
19
+ *     time: number,
20
+ *     videoId: string
21
+ * }}
22
+ */
23
+export function setSharedVideoStatus(videoId: string, status: string, time: number, ownerId: string) {
24
+    return {
25
+        type: SET_SHARED_VIDEO_STATUS,
26
+        ownerId,
27
+        status,
28
+        time,
29
+        videoId
30
+    };
31
+}
32
+
33
+/**
34
+ * Starts the flow for starting or stopping a shared YouTube video.
35
+ *
36
+ * @returns {{
37
+ *     type: TOGGLE_SHARED_VIDEO
38
+ * }}
39
+ */
40
+export function toggleSharedVideo() {
41
+    return {
42
+        type: 'TOGGLE_SHARED_VIDEO'
43
+    };
44
+}
45
+
46
+/**
47
+ * Displays the prompt for entering the youtube video link.
48
+ *
49
+ * @param {Function} onPostSubmit - The function to be invoked when a valid link is entered.
50
+ * @returns {Function}
51
+ */
52
+export function showEnterVideoLinkPrompt(onPostSubmit: ?Function) {
53
+    return openDialog(EnterVideoLinkPrompt, { onPostSubmit });
54
+}

+ 83
- 0
react/features/youtube-player/components/AbstractEnterVideoLinkPrompt.js View File

@@ -0,0 +1,83 @@
1
+// @flow
2
+
3
+import { Component } from 'react';
4
+import type { Dispatch } from 'redux';
5
+
6
+/**
7
+ * The type of the React {@code Component} props of
8
+ * {@link AbstractEnterVideoLinkPrompt}.
9
+ */
10
+export type Props = {
11
+
12
+    /**
13
+     * Invoked to update the shared youtube video link.
14
+     */
15
+    dispatch: Dispatch<any>,
16
+
17
+    /**
18
+     * Function to be invoked after typing a valid youtube video .
19
+     */
20
+    onPostSubmit: ?Function
21
+};
22
+
23
+/**
24
+ * Implements an abstract class for {@code EnterVideoLinkPrompt}.
25
+ */
26
+export default class AbstractEnterVideoLinkPrompt<S: *> extends Component < Props, S > {
27
+    /**
28
+     * Instantiates a new component.
29
+     *
30
+     *
31
+     * @inheritdoc
32
+     */
33
+    constructor(props: Props) {
34
+        super(props);
35
+
36
+        this._onSetVideoLink = this._onSetVideoLink.bind(this);
37
+    }
38
+
39
+    _onSetVideoLink: string => boolean;
40
+
41
+    /**
42
+     * Validates the entered video link by extractibg the id and dispatches it.
43
+     *
44
+     * It returns a boolean to comply the Dialog behaviour:
45
+     *     {@code true} - the dialog should be closed.
46
+     *     {@code false} - the dialog should be left open.
47
+     *
48
+     * @param {string} link - The entered video link.
49
+     * @returns {boolean}
50
+     */
51
+    _onSetVideoLink(link) {
52
+        if (!link || !link.trim()) {
53
+            return false;
54
+        }
55
+
56
+        const videoId = getYoutubeLink(link);
57
+
58
+        if (videoId) {
59
+            const { onPostSubmit } = this.props;
60
+
61
+            onPostSubmit && onPostSubmit(videoId);
62
+
63
+            return true;
64
+        }
65
+
66
+        return false;
67
+    }
68
+}
69
+
70
+/**
71
+ * Validates the entered video url.
72
+ *
73
+ * It returns a boolean to reflect whether the url matches the youtube regex.
74
+ *
75
+ * @param {string} url - The entered video link.
76
+ * @returns {boolean}
77
+ */
78
+function getYoutubeLink(url) {
79
+    const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
80
+    const result = url.match(p);
81
+
82
+    return result ? result[1] : false;
83
+}

+ 122
- 0
react/features/youtube-player/components/VideoShareButton.js View File

@@ -0,0 +1,122 @@
1
+// @flow
2
+
3
+import type { Dispatch } from 'redux';
4
+
5
+import { translate } from '../../base/i18n';
6
+import { IconShareVideo } from '../../base/icons';
7
+import { getLocalParticipant } from '../../base/participants';
8
+import { connect } from '../../base/redux';
9
+import { AbstractButton } from '../../base/toolbox';
10
+import type { AbstractButtonProps } from '../../base/toolbox';
11
+import { toggleSharedVideo } from '../actions';
12
+
13
+/**
14
+ * The type of the React {@code Component} props of {@link TileViewButton}.
15
+ */
16
+type Props = AbstractButtonProps & {
17
+
18
+    /**
19
+     * Whether or not the button is disabled.
20
+     */
21
+    _isDisabled: boolean,
22
+
23
+    /**
24
+     * Whether or not the local participant is sharing a YouTube video.
25
+     */
26
+    _sharingVideo: boolean,
27
+
28
+    /**
29
+     * The redux {@code dispatch} function.
30
+     */
31
+    dispatch: Dispatch<any>
32
+};
33
+
34
+/**
35
+ * Component that renders a toolbar button for toggling the tile layout view.
36
+ *
37
+ * @extends AbstractButton
38
+ */
39
+class VideoShareButton extends AbstractButton<Props, *> {
40
+    accessibilityLabel = 'toolbar.accessibilityLabel.sharedvideo';
41
+    icon = IconShareVideo;
42
+    label = 'toolbar.sharedvideo';
43
+    toggledLabel = 'toolbar.stopSharedVideo';
44
+
45
+    /**
46
+     * Handles clicking / pressing the button.
47
+     *
48
+     * @override
49
+     * @protected
50
+     * @returns {void}
51
+     */
52
+    _handleClick() {
53
+        this._doToggleSharedVideo();
54
+    }
55
+
56
+    /**
57
+     * Indicates whether this button is in toggled state or not.
58
+     *
59
+     * @override
60
+     * @protected
61
+     * @returns {boolean}
62
+     */
63
+    _isToggled() {
64
+        return this.props._sharingVideo;
65
+    }
66
+
67
+    /**
68
+     * Indicates whether this button is disabled or not.
69
+     *
70
+     * @override
71
+     * @protected
72
+     * @returns {boolean}
73
+     */
74
+    _isDisabled() {
75
+        return this.props._isDisabled;
76
+    }
77
+
78
+    /**
79
+     * Dispatches an action to toggle YouTube video sharing.
80
+     *
81
+     * @private
82
+     * @returns {void}
83
+     */
84
+    _doToggleSharedVideo() {
85
+        this.props.dispatch(toggleSharedVideo());
86
+    }
87
+}
88
+
89
+/**
90
+ * Maps part of the Redux state to the props of this component.
91
+ *
92
+ * @param {Object} state - The Redux state.
93
+ * @private
94
+ * @returns {Props}
95
+ */
96
+function _mapStateToProps(state): Object {
97
+    const { ownerId, status: sharedVideoStatus } = state['features/youtube-player'];
98
+    const localParticipantId = getLocalParticipant(state).id;
99
+
100
+    if (ownerId !== localParticipantId) {
101
+        return {
102
+            _isDisabled: isSharingStatus(sharedVideoStatus),
103
+            _sharingVideo: false };
104
+    }
105
+
106
+    return {
107
+        _sharingVideo: isSharingStatus(sharedVideoStatus)
108
+    };
109
+}
110
+
111
+/**
112
+ * Checks if the status is one that is actually sharing the video - playing, pause or start.
113
+ *
114
+ * @param {string} status - The shared video status.
115
+ * @private
116
+ * @returns {boolean}
117
+ */
118
+function isSharingStatus(status) {
119
+    return [ 'playing', 'pause', 'start' ].includes(status);
120
+}
121
+
122
+export default translate(connect(_mapStateToProps)(VideoShareButton));

+ 1
- 0
react/features/youtube-player/components/_.native.js View File

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

+ 3
- 0
react/features/youtube-player/components/index.js View File

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

+ 32
- 0
react/features/youtube-player/components/native/EnterVideoLinkPrompt.js View File

@@ -0,0 +1,32 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { InputDialog } from '../../../base/dialog';
6
+import { connect } from '../../../base/redux';
7
+import AbstractEnterVideoLinkPrompt from '../AbstractEnterVideoLinkPrompt';
8
+
9
+/**
10
+ * Implements a component to render a display name prompt.
11
+ */
12
+class EnterVideoLinkPrompt extends AbstractEnterVideoLinkPrompt<*> {
13
+    /**
14
+     * Implements React's {@link Component#render()}.
15
+     *
16
+     * @inheritdoc
17
+     */
18
+    render() {
19
+        return (
20
+            <InputDialog
21
+                contentKey = 'dialog.shareVideoTitle'
22
+                onSubmit = { this._onSetVideoLink }
23
+                textInputProps = {{
24
+                    placeholder: 'https://youtu.be/TB7LlM4erx8'
25
+                }} />
26
+        );
27
+    }
28
+
29
+    _onSetVideoLink: string => boolean;
30
+}
31
+
32
+export default connect()(EnterVideoLinkPrompt);

+ 311
- 0
react/features/youtube-player/components/native/YoutubeLargeVideo.js View File

@@ -0,0 +1,311 @@
1
+// @flow
2
+
3
+import React, { useRef, useEffect } from 'react';
4
+import { View } from 'react-native';
5
+import YoutubePlayer from 'react-native-youtube-iframe';
6
+
7
+import { getLocalParticipant } from '../../../base/participants';
8
+import { connect } from '../../../base/redux';
9
+import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
10
+import { setToolboxVisible } from '../../../toolbox/actions';
11
+import { setSharedVideoStatus } from '../../actions';
12
+
13
+import styles from './styles';
14
+
15
+/**
16
+ * Passed to the webviewProps in order to avoid the usage of the ios player on which we cannot hide the controls.
17
+ *
18
+ * @private
19
+ */
20
+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
21
+
22
+/**
23
+ * The type of the React {@link Component} props of {@link YoutubeLargeVideo}.
24
+ */
25
+type Props = {
26
+
27
+    /**
28
+     * Display the youtube controls on the player.
29
+     *
30
+     * @private
31
+     */
32
+    _enableControls: boolean,
33
+
34
+    /**
35
+     * Is the video shared by the local user.
36
+     *
37
+     * @private
38
+     */
39
+    _isOwner: boolean,
40
+
41
+    /**
42
+     * The ID of the participant (to be) depicted by LargeVideo.
43
+     *
44
+     * @private
45
+     */
46
+    _isPlaying: string,
47
+
48
+    /**
49
+     * True if in landscape mode.
50
+     *
51
+     * @private
52
+     */
53
+    _isWideScreen: boolean,
54
+
55
+    /**
56
+     * Callback to invoke when the {@code YoutLargeVideo} is ready to play.
57
+     *
58
+     * @private
59
+     */
60
+    _onVideoReady: Function,
61
+
62
+    /**
63
+     * Callback to invoke when the {@code YoutubeLargeVideo} changes status.
64
+     *
65
+     * @private
66
+     */
67
+    _onVideoChangeEvent: Function,
68
+
69
+    /**
70
+     * Callback to invoke when { @code isWideScreen} changes.
71
+     *
72
+     * @private
73
+     */
74
+    _onWideScreenChanged: Function,
75
+
76
+    /**
77
+     * The id of the participant sharing the video.
78
+     *
79
+     * @private
80
+     */
81
+    _ownerId: string,
82
+
83
+    /**
84
+     * The height of the screen.
85
+     *
86
+     * @private
87
+     */
88
+    _screenHeight: number,
89
+
90
+    /**
91
+     * The width of the screen.
92
+     *
93
+     * @private
94
+     */
95
+    _screenWidth: number,
96
+
97
+    /**
98
+     * Seek time in seconds.
99
+     *
100
+     * @private
101
+     */
102
+    _seek: number,
103
+
104
+    /**
105
+     * Youtube id of the video to be played.
106
+     *
107
+     * @private
108
+     */
109
+    youtubeId: string
110
+};
111
+
112
+const YoutubeLargeVideo = (props: Props) => {
113
+    const playerRef = useRef(null);
114
+
115
+    useEffect(() => {
116
+        playerRef.current && playerRef.current.getCurrentTime().then(time => {
117
+            const { _seek } = props;
118
+
119
+            if (shouldSeekToPosition(_seek, time)) {
120
+                playerRef.current && playerRef.current.seekTo(_seek);
121
+            }
122
+        });
123
+    }, [ props._seek ]);
124
+
125
+    useEffect(() => {
126
+        props._onWideScreenChanged(props._isWideScreen);
127
+    }, [ props._isWideScreen ]);
128
+
129
+    const onChangeState = e =>
130
+        playerRef.current && playerRef.current.getCurrentTime().then(time => {
131
+            const {
132
+                _isOwner,
133
+                _isPlaying,
134
+                _seek
135
+            } = props;
136
+
137
+            if (shouldSetNewStatus(_isOwner, e, _isPlaying, time, _seek)) {
138
+                props._onVideoChangeEvent(props.youtubeId, e, time, props._ownerId);
139
+            }
140
+        });
141
+    const onReady = () => {
142
+        if (props._isOwner) {
143
+            props._onVideoReady(
144
+                props.youtubeId,
145
+                playerRef.current && playerRef.current.getCurrentTime(),
146
+                props._ownerId);
147
+        }
148
+    };
149
+
150
+    let playerHeight, playerWidth;
151
+
152
+    if (props._isWideScreen) {
153
+        playerHeight = props._screenHeight;
154
+        playerWidth = playerHeight * 16 / 9;
155
+    } else {
156
+        playerWidth = props._screenWidth;
157
+        playerHeight = playerWidth * 9 / 16;
158
+    }
159
+
160
+    return (
161
+        <View
162
+            pointerEvents = { props._enableControls ? 'auto' : 'none' }
163
+            style = { styles.youtubeVideoContainer } >
164
+            <YoutubePlayer
165
+                height = { playerHeight }
166
+                initialPlayerParams = {{
167
+                    controls: props._enableControls,
168
+                    modestbranding: true,
169
+                    preventFullScreen: true
170
+                }}
171
+                /* eslint-disable react/jsx-no-bind */
172
+                onChangeState = { onChangeState }
173
+                /* eslint-disable react/jsx-no-bind */
174
+                onReady = { onReady }
175
+                play = { props._isPlaying }
176
+                playbackRate = { 1 }
177
+                ref = { playerRef }
178
+                videoId = { props.youtubeId }
179
+                volume = { 50 }
180
+                webViewProps = {{
181
+                    bounces: false,
182
+                    mediaPlaybackRequiresUserAction: false,
183
+                    scrollEnabled: false,
184
+                    userAgent: webviewUserAgent
185
+                }}
186
+                width = { playerWidth } />
187
+        </View>);
188
+};
189
+
190
+/* eslint-disable max-params */
191
+
192
+/**
193
+ * Return true if the user is the owner and
194
+ * the status has changed or the seek time difference from the previous set is larger than 5 seconds.
195
+ *
196
+ * @param {boolean} isOwner - Whether the local user is sharing the video.
197
+ * @param {string} status - The new status.
198
+ * @param {boolean} isPlaying - Whether the component is playing at the moment.
199
+ * @param {number} newTime - The new seek time.
200
+ * @param {number} previousTime - The old seek time.
201
+ * @private
202
+ * @returns {boolean}
203
+*/
204
+function shouldSetNewStatus(isOwner, status, isPlaying, newTime, previousTime) {
205
+    if (!isOwner || status === 'buffering') {
206
+        return false;
207
+    }
208
+
209
+    if ((isPlaying && status === 'paused') || (!isPlaying && status === 'playing')) {
210
+        return true;
211
+    }
212
+
213
+    return shouldSeekToPosition(newTime, previousTime);
214
+}
215
+
216
+/**
217
+ * Return true if the diffenrece between the two timees is larger than 5.
218
+ *
219
+ * @param {number} newTime - The current time.
220
+ * @param {number} previousTime - The previous time.
221
+ * @private
222
+ * @returns {boolean}
223
+*/
224
+function shouldSeekToPosition(newTime, previousTime) {
225
+    return Math.abs(newTime - previousTime) > 5;
226
+}
227
+
228
+/**
229
+ * Maps (parts of) the Redux state to the associated YoutubeLargeVideo's props.
230
+ *
231
+ * @param {Object} state - Redux state.
232
+ * @private
233
+ * @returns {Props}
234
+ */
235
+function _mapStateToProps(state) {
236
+    const { ownerId, status, time } = state['features/youtube-player'];
237
+    const localParticipant = getLocalParticipant(state);
238
+    const responsiveUi = state['features/base/responsive-ui'];
239
+    const screenHeight = responsiveUi.clientHeight;
240
+    const screenWidth = responsiveUi.clientWidth;
241
+
242
+    return {
243
+        _enableControls: ownerId === localParticipant.id,
244
+        _isOwner: ownerId === localParticipant.id,
245
+        _isPlaying: status === 'playing',
246
+        _isWideScreen: responsiveUi.aspectRatio === ASPECT_RATIO_WIDE,
247
+        _ownerId: ownerId,
248
+        _screenHeight: screenHeight,
249
+        _screenWidth: screenWidth,
250
+        _seek: time
251
+    };
252
+}
253
+
254
+/**
255
+ * Maps dispatching of some action to React component props.
256
+ *
257
+ * @param {Function} dispatch - Redux action dispatcher.
258
+ * @private
259
+ * @returns {{
260
+ *     onVideoChangeEvent: Function,
261
+ *     onVideoReady: Function,
262
+ *     onWideScreenChanged: Function
263
+ * }}
264
+ */
265
+function _mapDispatchToProps(dispatch) {
266
+    return {
267
+        _onVideoChangeEvent: (videoId, status, time, ownerId) => {
268
+            if (![ 'playing', 'paused' ].includes(status)) {
269
+                return;
270
+            }
271
+            dispatch(setSharedVideoStatus(videoId, translateStatus(status), time, ownerId));
272
+        },
273
+        _onVideoReady: (videoId, time, ownerId) => {
274
+            time.then(t => dispatch(setSharedVideoStatus(videoId, 'playing', t, ownerId)));
275
+        },
276
+        _onWideScreenChanged: isWideScreen => {
277
+            dispatch(setToolboxVisible(!isWideScreen));
278
+        }
279
+    };
280
+}
281
+
282
+/**
283
+ * Maps (parts of) the Redux state to the associated YoutubeLargeVideo's props.
284
+ *
285
+ * @private
286
+ * @returns {Props}
287
+ */
288
+function _mergeProps({ _isOwner, ...stateProps }, { _onVideoChangeEvent, _onVideoReady, _onWideScreenChanged }) {
289
+    return Object.assign(stateProps, {
290
+        _onVideoChangeEvent: _isOwner ? _onVideoChangeEvent : () => { /* do nothing */ },
291
+        _onVideoReady: _isOwner ? _onVideoReady : () => { /* do nothing */ },
292
+        _onWideScreenChanged
293
+    });
294
+}
295
+
296
+/**
297
+ * In case the status is 'paused', it is translated to 'pause' to match the web functionality.
298
+ *
299
+ * @param {string} status - The status of the shared video.
300
+ * @private
301
+ * @returns {string}
302
+ */
303
+function translateStatus(status) {
304
+    if (status === 'paused') {
305
+        return 'pause';
306
+    }
307
+
308
+    return status;
309
+}
310
+
311
+export default connect(_mapStateToProps, _mapDispatchToProps, _mergeProps)(YoutubeLargeVideo);

+ 2
- 0
react/features/youtube-player/components/native/index.js View File

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

+ 13
- 0
react/features/youtube-player/components/native/styles.js View File

@@ -0,0 +1,13 @@
1
+// @flow
2
+
3
+/**
4
+ * The style of toolbar buttons.
5
+ */
6
+export default {
7
+    youtubeVideoContainer: {
8
+        alignItems: 'center',
9
+        flex: 1,
10
+        flexDirection: 'column',
11
+        justifyContent: 'center'
12
+    }
13
+};

+ 6
- 0
react/features/youtube-player/index.js View File

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

+ 183
- 0
react/features/youtube-player/middleware.js View File

@@ -0,0 +1,183 @@
1
+// @flow
2
+
3
+import { CONFERENCE_LEFT, getCurrentConference } from '../base/conference';
4
+import {
5
+    PARTICIPANT_LEFT,
6
+    getLocalParticipant,
7
+    participantJoined,
8
+    participantLeft,
9
+    pinParticipant
10
+} from '../base/participants';
11
+import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
12
+
13
+import { TOGGLE_SHARED_VIDEO, SET_SHARED_VIDEO_STATUS } from './actionTypes';
14
+import { setSharedVideoStatus, showEnterVideoLinkPrompt } from './actions';
15
+
16
+const SHARED_VIDEO = 'shared-video';
17
+
18
+/**
19
+ * Middleware that captures actions related to YouTube video sharing and updates
20
+ * components not hooked into redux.
21
+ *
22
+ * @param {Store} store - The redux store.
23
+ * @returns {Function}
24
+ */
25
+MiddlewareRegistry.register(store => next => action => {
26
+    const { dispatch, getState } = store;
27
+    const state = getState();
28
+    const conference = getCurrentConference(state);
29
+    const localParticipantId = getLocalParticipant(state)?.id;
30
+    const { videoId, status, ownerId, time } = action;
31
+
32
+    switch (action.type) {
33
+    case TOGGLE_SHARED_VIDEO:
34
+        _toggleSharedVideo(store, next, action);
35
+        break;
36
+    case CONFERENCE_LEFT:
37
+        dispatch(setSharedVideoStatus('', 'stop', 0, ''));
38
+        break;
39
+    case PARTICIPANT_LEFT:
40
+        if (action.participant.id === action.ownerId) {
41
+            dispatch(setSharedVideoStatus('', 'stop', 0, ''));
42
+        }
43
+        break;
44
+    case SET_SHARED_VIDEO_STATUS:
45
+        if (localParticipantId === ownerId) {
46
+            sendShareVideoCommand(videoId, status, conference, localParticipantId, time);
47
+        }
48
+        break;
49
+    }
50
+
51
+    return next(action);
52
+});
53
+
54
+/**
55
+ * Set up state change listener to perform maintenance tasks when the conference
56
+ * is left or failed, e.g. clear messages or close the chat modal if it's left
57
+ * open.
58
+ */
59
+StateListenerRegistry.register(
60
+    state => getCurrentConference(state),
61
+    (conference, store, previousConference) => {
62
+        if (conference && conference !== previousConference) {
63
+            conference.addCommandListener(SHARED_VIDEO,
64
+                ({ value, attributes }) => {
65
+
66
+                    const { dispatch, getState } = store;
67
+                    const { from } = attributes;
68
+                    const localParticipantId = getLocalParticipant(getState()).id;
69
+                    const status = attributes.state;
70
+
71
+                    if ([ 'playing', 'pause', 'start' ].includes(status)) {
72
+                        handleSharingVideoStatus(store, value, attributes, conference);
73
+                    } else if (status === 'stop') {
74
+                        dispatch(participantLeft(value, conference));
75
+                        if (localParticipantId !== from) {
76
+                            dispatch(setSharedVideoStatus(value, 'stop', 0, from));
77
+                        }
78
+                    }
79
+                }
80
+            );
81
+        }
82
+    });
83
+
84
+/**
85
+ * Handles the playing, pause and start statuses for the shared video.
86
+ * Dispatches participantJoined event and, if necessary, pins it.
87
+ * Sets the SharedVideoStatus if the event was triggered by the local user.
88
+ *
89
+ * @param {Store} store - The redux store.
90
+ * @param {string} videoId - The YoutubeId of the video to the shared.
91
+ * @param {Object} attributes - The attributes received from the share video command.
92
+ * @param {JitsiConference} conference - The current conference.
93
+ * @returns {void}
94
+ */
95
+function handleSharingVideoStatus(store, videoId, { state, time, from }, conference) {
96
+    const { dispatch, getState } = store;
97
+    const localParticipantId = getLocalParticipant(getState()).id;
98
+    const oldStatus = getState()['features/youtube-player']?.status;
99
+
100
+    if (state === 'start' || ![ 'playing', 'pause', 'start' ].includes(oldStatus)) {
101
+        dispatch(participantJoined({
102
+            conference,
103
+            id: videoId,
104
+            isFakeParticipant: true,
105
+            avatarURL: `https://img.youtube.com/vi/${videoId}/0.jpg`,
106
+            name: 'YouTube'
107
+        }));
108
+
109
+        dispatch(pinParticipant(videoId));
110
+    }
111
+
112
+    if (localParticipantId !== from) {
113
+        dispatch(setSharedVideoStatus(videoId, state, time, from));
114
+    }
115
+}
116
+
117
+/**
118
+ * Dispatches shared video status.
119
+ *
120
+ * @param {Store} store - The redux store.
121
+ * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
122
+ * specified {@code action} in the specified {@code store}.
123
+ * @param {Action} action - The redux action which is
124
+ * being dispatched in the specified {@code store}.
125
+ * @returns {Function}
126
+ */
127
+function _toggleSharedVideo(store, next, action) {
128
+    const { dispatch, getState } = store;
129
+    const state = getState();
130
+    const { videoId, ownerId, status } = state['features/youtube-player'];
131
+    const localParticipant = getLocalParticipant(state);
132
+
133
+    if (status === 'playing' || status === 'start' || status === 'pause') {
134
+        if (ownerId === localParticipant.id) {
135
+            dispatch(setSharedVideoStatus(videoId, 'stop', 0, localParticipant.id));
136
+        }
137
+    } else {
138
+        dispatch(showEnterVideoLinkPrompt(id => _onVideoLinkEntered(store, id)));
139
+    }
140
+
141
+    return next(action);
142
+}
143
+
144
+/**
145
+ * Sends SHARED_VIDEO start command.
146
+ *
147
+ * @param {Store} store - The redux store.
148
+ * @param {string} id - The youtube id of the video to be shared.
149
+ * @returns {void}
150
+ */
151
+function _onVideoLinkEntered(store, id) {
152
+    const { dispatch, getState } = store;
153
+    const conference = getCurrentConference(getState());
154
+
155
+    if (conference) {
156
+        const localParticipant = getLocalParticipant(getState());
157
+
158
+        dispatch(setSharedVideoStatus(id, 'start', 0, localParticipant.id));
159
+    }
160
+}
161
+
162
+/* eslint-disable max-params */
163
+
164
+/**
165
+ * Sends SHARED_VIDEO command.
166
+ *
167
+ * @param {string} id - The youtube id of the video.
168
+ * @param {string} status - The status of the shared video.
169
+ * @param {JitsiConference} conference - The current conference.
170
+ * @param {string} localParticipantId - The id of the local participant.
171
+ * @param {string} time - The seek position of the video.
172
+ * @returns {void}
173
+ */
174
+function sendShareVideoCommand(id, status, conference, localParticipantId, time) {
175
+    conference.sendCommandOnce(SHARED_VIDEO, {
176
+        value: id,
177
+        attributes: {
178
+            from: localParticipantId,
179
+            state: status,
180
+            time
181
+        }
182
+    });
183
+}

+ 24
- 0
react/features/youtube-player/reducer.js View File

@@ -0,0 +1,24 @@
1
+// @flow
2
+import { ReducerRegistry } from '../base/redux';
3
+
4
+import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
5
+
6
+/**
7
+ * Reduces the Redux actions of the feature features/youtube-player.
8
+ */
9
+ReducerRegistry.register('features/youtube-player', (state = {}, action) => {
10
+    const { videoId, status, time, ownerId } = action;
11
+
12
+    switch (action.type) {
13
+    case SET_SHARED_VIDEO_STATUS:
14
+        return {
15
+            ...state,
16
+            videoId,
17
+            status,
18
+            time,
19
+            ownerId
20
+        };
21
+    default:
22
+        return state;
23
+    }
24
+});

Loading…
Cancel
Save