Browse Source

feat(share-video) Add capability for sharing any direct link video

master
Tudor-Ovidiu Avram 3 years ago
parent
commit
4e4ff0f60f
35 changed files with 1482 additions and 1167 deletions
  1. 1
    58
      conference.js
  2. 9
    0
      css/_videolayout_default.scss
  3. 1
    1
      lang/main.json
  4. 13
    0
      modules/API/API.js
  5. 2
    0
      modules/API/external/external_api.js
  6. 0
    69
      modules/UI/UI.js
  7. 0
    655
      modules/UI/shared_video/SharedVideo.js
  8. 1
    2
      modules/UI/videolayout/VideoLayout.js
  9. 37
    0
      package-lock.json
  10. 1
    0
      package.json
  11. 4
    2
      react/features/base/participants/components/ParticipantView.native.js
  12. 7
    4
      react/features/filmstrip/components/web/Thumbnail.js
  13. 8
    5
      react/features/large-video/components/LargeVideo.web.js
  14. 4
    5
      react/features/shared-video/actionTypes.js
  15. 122
    0
      react/features/shared-video/actions.any.js
  16. 1
    54
      react/features/shared-video/actions.native.js
  17. 2
    43
      react/features/shared-video/actions.web.js
  18. 4
    15
      react/features/shared-video/components/AbstractSharedVideoDialog.js
  19. 16
    6
      react/features/shared-video/components/native/YoutubeLargeVideo.js
  20. 425
    0
      react/features/shared-video/components/web/AbstractVideoManager.js
  21. 147
    0
      react/features/shared-video/components/web/SharedVideo.js
  22. 4
    10
      react/features/shared-video/components/web/SharedVideoButton.js
  23. 1
    2
      react/features/shared-video/components/web/SharedVideoDialog.js
  24. 197
    0
      react/features/shared-video/components/web/VideoManager.js
  25. 251
    0
      react/features/shared-video/components/web/YoutubeVideoManager.js
  26. 1
    1
      react/features/shared-video/components/web/index.js
  27. 9
    2
      react/features/shared-video/constants.js
  28. 9
    6
      react/features/shared-video/functions.js
  29. 177
    0
      react/features/shared-video/middleware.any.js
  30. 1
    186
      react/features/shared-video/middleware.native.js
  31. 2
    27
      react/features/shared-video/middleware.web.js
  32. 11
    5
      react/features/shared-video/reducer.native.js
  33. 12
    4
      react/features/shared-video/reducer.web.js
  34. 2
    3
      react/features/video-menu/actions.any.js
  35. 0
    2
      service/UI/UIEvents.js

+ 1
- 58
conference.js View File

@@ -129,7 +129,6 @@ import {
129 129
 import { disableReceiver, stopReceiver } from './react/features/remote-control';
130 130
 import { setScreenAudioShareState, isScreenAudioShared } from './react/features/screen-share/';
131 131
 import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
132
-import { setSharedVideoStatus } from './react/features/shared-video/actions';
133 132
 import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
134 133
 import { createPresenterEffect } from './react/features/stream-effects/presenter';
135 134
 import { endpointMessageReceived } from './react/features/subtitles';
@@ -177,8 +176,7 @@ const commands = {
177 176
     AVATAR_URL: AVATAR_URL_COMMAND,
178 177
     CUSTOM_ROLE: 'custom-role',
179 178
     EMAIL: EMAIL_COMMAND,
180
-    ETHERPAD: 'etherpad',
181
-    SHARED_VIDEO: 'shared-video'
179
+    ETHERPAD: 'etherpad'
182 180
 };
183 181
 
184 182
 /**
@@ -2017,8 +2015,6 @@ export default {
2017 2015
             }
2018 2016
 
2019 2017
             logger.log(`USER ${id} LEFT:`, user);
2020
-
2021
-            APP.UI.onSharedVideoStop(id);
2022 2018
         });
2023 2019
 
2024 2020
         room.on(JitsiConferenceEvents.USER_STATUS_CHANGED, (id, status) => {
@@ -2454,59 +2450,6 @@ export default {
2454 2450
                 this.toggleScreenSharing(undefined, { audioOnly });
2455 2451
             }
2456 2452
         );
2457
-
2458
-        /* eslint-disable max-params */
2459
-        APP.UI.addListener(
2460
-            UIEvents.UPDATE_SHARED_VIDEO,
2461
-            (url, state, time, isMuted, volume) => {
2462
-                /* eslint-enable max-params */
2463
-                // send start and stop commands once, and remove any updates
2464
-                // that had left
2465
-                if (state === 'stop'
2466
-                        || state === 'start'
2467
-                        || state === 'playing') {
2468
-                    const localParticipant = getLocalParticipant(APP.store.getState());
2469
-
2470
-                    room.removeCommand(this.commands.defaults.SHARED_VIDEO);
2471
-                    room.sendCommandOnce(this.commands.defaults.SHARED_VIDEO, {
2472
-                        value: url,
2473
-                        attributes: {
2474
-                            state,
2475
-                            time,
2476
-                            muted: isMuted,
2477
-                            volume,
2478
-                            from: localParticipant.id
2479
-                        }
2480
-                    });
2481
-                } else {
2482
-                    // in case of paused, in order to allow late users to join
2483
-                    // paused
2484
-                    room.removeCommand(this.commands.defaults.SHARED_VIDEO);
2485
-                    room.sendCommand(this.commands.defaults.SHARED_VIDEO, {
2486
-                        value: url,
2487
-                        attributes: {
2488
-                            state,
2489
-                            time,
2490
-                            muted: isMuted,
2491
-                            volume
2492
-                        }
2493
-                    });
2494
-                }
2495
-
2496
-                APP.store.dispatch(setSharedVideoStatus(state));
2497
-            });
2498
-        room.addCommandListener(
2499
-            this.commands.defaults.SHARED_VIDEO,
2500
-            ({ value, attributes }, id) => {
2501
-                if (attributes.state === 'stop') {
2502
-                    APP.UI.onSharedVideoStop(id, attributes);
2503
-                } else if (attributes.state === 'start') {
2504
-                    APP.UI.onSharedVideoStart(id, value, attributes);
2505
-                } else if (attributes.state === 'playing'
2506
-                    || attributes.state === 'pause') {
2507
-                    APP.UI.onSharedVideoUpdate(id, value, attributes);
2508
-                }
2509
-            });
2510 2453
     },
2511 2454
 
2512 2455
     /**

+ 9
- 0
css/_videolayout_default.scss View File

@@ -240,6 +240,15 @@
240 240
     object-fit: cover;
241 241
 }
242 242
 
243
+#sharedVideo video {
244
+    width: 100%;
245
+    height: 100%;
246
+}
247
+
248
+#sharedVideo.disable-pointer {
249
+    pointer-events: none;
250
+}
251
+
243 252
 #sharedVideo,
244 253
 #etherpad,
245 254
 #localVideoWrapper video,

+ 1
- 1
lang/main.json View File

@@ -828,7 +828,7 @@
828 828
         "security": "Security options",
829 829
         "Settings": "Settings",
830 830
         "shareaudio": "Share audio",
831
-        "sharedvideo": "Share a YouTube video",
831
+        "sharedvideo": "Share a video",
832 832
         "shareRoom": "Invite someone",
833 833
         "shortcuts": "View shortcuts",
834 834
         "speakerStats": "Speaker stats",

+ 13
- 0
modules/API/API.js View File

@@ -44,6 +44,7 @@ import {
44 44
 import { toggleLobbyMode } from '../../react/features/lobby/actions';
45 45
 import { RECORDING_TYPES } from '../../react/features/recording/constants';
46 46
 import { getActiveSession } from '../../react/features/recording/functions';
47
+import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
47 48
 import { toggleTileView, setTileView } from '../../react/features/video-layout';
48 49
 import { muteAllParticipants } from '../../react/features/video-menu/actions';
49 50
 import { setVideoQuality } from '../../react/features/video-quality';
@@ -263,6 +264,18 @@ function initCommands() {
263 264
             APP.store.dispatch(setVideoQuality(frameHeight));
264 265
         },
265 266
 
267
+        'start-share-video': url => {
268
+            logger.debug('Share video command received');
269
+            sendAnalytics(createApiEvent('share.video.start'));
270
+            APP.store.dispatch(playSharedVideo(url));
271
+        },
272
+
273
+        'stop-share-video': () => {
274
+            logger.debug('Share video command received');
275
+            sendAnalytics(createApiEvent('share.video.start'));
276
+            APP.store.dispatch(stopSharedVideo());
277
+        },
278
+
266 279
         /**
267 280
          * Starts a file recording or streaming session depending on the passed on params.
268 281
          * For RTMP streams, `rtmpStreamKey` must be passed on. `rtmpBroadcastID` is optional.

+ 2
- 0
modules/API/external/external_api.js View File

@@ -47,7 +47,9 @@ const commands = {
47 47
     setTileView: 'set-tile-view',
48 48
     setVideoQuality: 'set-video-quality',
49 49
     startRecording: 'start-recording',
50
+    startShareVideo: 'start-share-video',
50 51
     stopRecording: 'stop-recording',
52
+    stopShareVideo: 'stop-share-video',
51 53
     subject: 'subject',
52 54
     submitFeedback: 'submit-feedback',
53 55
     toggleAudio: 'toggle-audio',

+ 0
- 69
modules/UI/UI.js View File

@@ -19,7 +19,6 @@ import {
19 19
 import UIEvents from '../../service/UI/UIEvents';
20 20
 
21 21
 import EtherpadManager from './etherpad/Etherpad';
22
-import SharedVideoManager from './shared_video/SharedVideo';
23 22
 import messageHandler from './util/MessageHandler';
24 23
 import UIUtil from './util/UIUtil';
25 24
 import VideoLayout from './videolayout/VideoLayout';
@@ -33,15 +32,11 @@ const eventEmitter = new EventEmitter();
33 32
 UI.eventEmitter = eventEmitter;
34 33
 
35 34
 let etherpadManager;
36
-let sharedVideoManager;
37 35
 
38 36
 const UIListeners = new Map([
39 37
     [
40 38
         UIEvents.ETHERPAD_CLICKED,
41 39
         () => etherpadManager && etherpadManager.toggleEtherpad()
42
-    ], [
43
-        UIEvents.SHARED_VIDEO_CLICKED,
44
-        () => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
45 40
     ], [
46 41
         UIEvents.TOGGLE_FILMSTRIP,
47 42
         () => UI.toggleFilmstrip()
@@ -58,14 +53,6 @@ UI.isFullScreen = function() {
58 53
     return UIUtil.isFullScreen();
59 54
 };
60 55
 
61
-/**
62
- * Returns true if there is a shared video which is being shown (?).
63
- * @returns {boolean} - true if there is a shared video which is being shown.
64
- */
65
-UI.isSharedVideoShown = function() {
66
-    return Boolean(sharedVideoManager && sharedVideoManager.isSharedVideoShown);
67
-};
68
-
69 56
 /**
70 57
  * Notify user that server has shut down.
71 58
  */
@@ -112,8 +99,6 @@ UI.start = function() {
112 99
     // will be seen animating in.
113 100
     VideoLayout.resizeVideoArea();
114 101
 
115
-    sharedVideoManager = new SharedVideoManager(eventEmitter);
116
-
117 102
     if (isMobileBrowser()) {
118 103
         $('body').addClass('mobile-browser');
119 104
     } else {
@@ -411,60 +396,6 @@ UI.getLargeVideo = function() {
411 396
     return VideoLayout.getLargeVideo();
412 397
 };
413 398
 
414
-/**
415
- * Show shared video.
416
- * @param {string} id the id of the sender of the command
417
- * @param {string} url video url
418
- * @param {string} attributes
419
-*/
420
-UI.onSharedVideoStart = function(id, url, attributes) {
421
-    if (sharedVideoManager) {
422
-        sharedVideoManager.onSharedVideoStart(id, url, attributes);
423
-    }
424
-};
425
-
426
-/**
427
- * Update shared video.
428
- * @param {string} id the id of the sender of the command
429
- * @param {string} url video url
430
- * @param {string} attributes
431
- */
432
-UI.onSharedVideoUpdate = function(id, url, attributes) {
433
-    if (sharedVideoManager) {
434
-        sharedVideoManager.onSharedVideoUpdate(id, url, attributes);
435
-    }
436
-};
437
-
438
-/**
439
- * Stop showing shared video.
440
- * @param {string} id the id of the sender of the command
441
- * @param {string} attributes
442
- */
443
-UI.onSharedVideoStop = function(id, attributes) {
444
-    if (sharedVideoManager) {
445
-        sharedVideoManager.onSharedVideoStop(id, attributes);
446
-    }
447
-};
448
-
449
-/**
450
- * Show shared video.
451
- * @param {string} url video url
452
- */
453
-UI.startSharedVideoEmitter = function(url) {
454
-    if (sharedVideoManager) {
455
-        sharedVideoManager.startSharedVideoEmitter(url);
456
-    }
457
-};
458
-
459
-/**
460
- * Stop shared video.
461
- */
462
-UI.stopSharedVideoEmitter = function() {
463
-    if (sharedVideoManager) {
464
-        sharedVideoManager.stopSharedVideoEmitter();
465
-    }
466
-};
467
-
468 399
 // TODO: Export every function separately. For now there is no point of doing
469 400
 // this because we are importing everything.
470 401
 export default UI;

+ 0
- 655
modules/UI/shared_video/SharedVideo.js View File

@@ -1,655 +0,0 @@
1
-/* global $, APP, YT, interfaceConfig, onPlayerReady, onPlayerStateChange,
2
-onPlayerError */
3
-
4
-import Logger from 'jitsi-meet-logger';
5
-
6
-import {
7
-    createSharedVideoEvent as createEvent,
8
-    sendAnalytics
9
-} from '../../../react/features/analytics';
10
-import {
11
-    participantJoined,
12
-    participantLeft,
13
-    pinParticipant
14
-} from '../../../react/features/base/participants';
15
-import { VIDEO_PLAYER_PARTICIPANT_NAME } from '../../../react/features/shared-video/constants';
16
-import { dockToolbox, showToolbox } from '../../../react/features/toolbox/actions.web';
17
-import { getToolboxHeight } from '../../../react/features/toolbox/functions.web';
18
-import UIEvents from '../../../service/UI/UIEvents';
19
-import Filmstrip from '../videolayout/Filmstrip';
20
-import LargeContainer from '../videolayout/LargeContainer';
21
-import VideoLayout from '../videolayout/VideoLayout';
22
-
23
-const logger = Logger.getLogger(__filename);
24
-
25
-export const SHARED_VIDEO_CONTAINER_TYPE = 'sharedvideo';
26
-
27
-/**
28
- * Example shared video link.
29
- * @type {string}
30
- */
31
-const updateInterval = 5000; // milliseconds
32
-
33
-
34
-/**
35
- * Manager of shared video.
36
- */
37
-export default class SharedVideoManager {
38
-    /**
39
-     *
40
-     */
41
-    constructor(emitter) {
42
-        this.emitter = emitter;
43
-        this.isSharedVideoShown = false;
44
-        this.isPlayerAPILoaded = false;
45
-        this.mutedWithUserInteraction = false;
46
-    }
47
-
48
-    /**
49
-     * Indicates if the player volume is currently on. This will return true if
50
-     * we have an available player, which is currently in a PLAYING state,
51
-     * which isn't muted and has it's volume greater than 0.
52
-     *
53
-     * @returns {boolean} indicating if the volume of the shared video is
54
-     * currently on.
55
-     */
56
-    isSharedVideoVolumeOn() {
57
-        return this.player
58
-                && this.player.getPlayerState() === YT.PlayerState.PLAYING
59
-                && !this.player.isMuted()
60
-                && this.player.getVolume() > 0;
61
-    }
62
-
63
-    /**
64
-     * Indicates if the local user is the owner of the shared video.
65
-     * @returns {*|boolean}
66
-     */
67
-    isSharedVideoOwner() {
68
-        return this.from && APP.conference.isLocalId(this.from);
69
-    }
70
-
71
-    /**
72
-     * Start shared video event emitter if a video is not shown.
73
-     *
74
-     * @param url of the video
75
-     */
76
-    startSharedVideoEmitter(url) {
77
-
78
-        if (!this.isSharedVideoShown) {
79
-            if (url) {
80
-                this.emitter.emit(
81
-                    UIEvents.UPDATE_SHARED_VIDEO, url, 'start');
82
-                sendAnalytics(createEvent('started'));
83
-            }
84
-
85
-            logger.log('SHARED VIDEO CANCELED');
86
-            sendAnalytics(createEvent('canceled'));
87
-        }
88
-    }
89
-
90
-    /**
91
-     * Stop shared video event emitter done by the one who shared the video.
92
-     */
93
-    stopSharedVideoEmitter() {
94
-
95
-        if (APP.conference.isLocalId(this.from)) {
96
-            if (this.intervalId) {
97
-                clearInterval(this.intervalId);
98
-                this.intervalId = null;
99
-            }
100
-            this.emitter.emit(
101
-                UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop');
102
-            sendAnalytics(createEvent('stopped'));
103
-        }
104
-    }
105
-
106
-    /**
107
-     * Shows the player component and starts the process that will be sending
108
-     * updates, if we are the one shared the video.
109
-     *
110
-     * @param id the id of the sender of the command
111
-     * @param url the video url
112
-     * @param attributes
113
-     */
114
-    onSharedVideoStart(id, url, attributes) {
115
-        if (this.isSharedVideoShown) {
116
-            return;
117
-        }
118
-
119
-        this.isSharedVideoShown = true;
120
-
121
-        // the video url
122
-        this.url = url;
123
-
124
-        // the owner of the video
125
-        this.from = id;
126
-
127
-        this.mutedWithUserInteraction = APP.conference.isLocalAudioMuted();
128
-
129
-        // listen for local audio mute events
130
-        this.localAudioMutedListener = this.onLocalAudioMuted.bind(this);
131
-        this.emitter.on(UIEvents.AUDIO_MUTED, this.localAudioMutedListener);
132
-
133
-        // This code loads the IFrame Player API code asynchronously.
134
-        const tag = document.createElement('script');
135
-
136
-        tag.src = 'https://www.youtube.com/iframe_api';
137
-        const firstScriptTag = document.getElementsByTagName('script')[0];
138
-
139
-        firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
140
-
141
-        // sometimes we receive errors like player not defined
142
-        // or player.pauseVideo is not a function
143
-        // we need to operate with player after start playing
144
-        // self.player will be defined once it start playing
145
-        // and will process any initial attributes if any
146
-        this.initialAttributes = attributes;
147
-
148
-        const self = this;
149
-
150
-        if (self.isPlayerAPILoaded) {
151
-            window.onYouTubeIframeAPIReady();
152
-        } else {
153
-            window.onYouTubeIframeAPIReady = function() {
154
-                self.isPlayerAPILoaded = true;
155
-                const showControls
156
-                    = APP.conference.isLocalId(self.from) ? 1 : 0;
157
-                const p = new YT.Player('sharedVideoIFrame', {
158
-                    height: '100%',
159
-                    width: '100%',
160
-                    videoId: self.url,
161
-                    playerVars: {
162
-                        'origin': location.origin,
163
-                        'fs': '0',
164
-                        'autoplay': 0,
165
-                        'controls': showControls,
166
-                        'rel': 0
167
-                    },
168
-                    events: {
169
-                        'onReady': onPlayerReady,
170
-                        'onStateChange': onPlayerStateChange,
171
-                        'onError': onPlayerError
172
-                    }
173
-                });
174
-
175
-                // add listener for volume changes
176
-                p.addEventListener(
177
-                    'onVolumeChange', 'onVolumeChange');
178
-
179
-                if (APP.conference.isLocalId(self.from)) {
180
-                // adds progress listener that will be firing events
181
-                // while we are paused and we change the progress of the
182
-                // video (seeking forward or backward on the video)
183
-                    p.addEventListener(
184
-                        'onVideoProgress', 'onVideoProgress');
185
-                }
186
-            };
187
-        }
188
-
189
-        /**
190
-         * Indicates that a change in state has occurred for the shared video.
191
-         * @param event the event notifying us of the change
192
-         */
193
-        window.onPlayerStateChange = function(event) {
194
-            // eslint-disable-next-line eqeqeq
195
-            if (event.data == YT.PlayerState.PLAYING) {
196
-                self.player = event.target;
197
-
198
-                if (self.initialAttributes) {
199
-                    // If a network update has occurred already now is the
200
-                    // time to process it.
201
-                    self.processVideoUpdate(
202
-                        self.player,
203
-                        self.initialAttributes);
204
-
205
-                    self.initialAttributes = null;
206
-                }
207
-                self.smartAudioMute();
208
-                // eslint-disable-next-line eqeqeq
209
-            } else if (event.data == YT.PlayerState.PAUSED) {
210
-                self.smartAudioUnmute();
211
-                sendAnalytics(createEvent('paused'));
212
-            }
213
-            // eslint-disable-next-line eqeqeq
214
-            self.fireSharedVideoEvent(event.data == YT.PlayerState.PAUSED);
215
-        };
216
-
217
-        /**
218
-         * Track player progress while paused.
219
-         * @param event
220
-         */
221
-        window.onVideoProgress = function(event) {
222
-            const state = event.target.getPlayerState();
223
-
224
-            // eslint-disable-next-line eqeqeq
225
-            if (state == YT.PlayerState.PAUSED) {
226
-                self.fireSharedVideoEvent(true);
227
-            }
228
-        };
229
-
230
-        /**
231
-         * Gets notified for volume state changed.
232
-         * @param event
233
-         */
234
-        window.onVolumeChange = function(event) {
235
-            self.fireSharedVideoEvent();
236
-
237
-            // let's check, if player is not muted lets mute locally
238
-            if (event.data.volume > 0 && !event.data.muted) {
239
-                self.smartAudioMute();
240
-            } else if (event.data.volume <= 0 || event.data.muted) {
241
-                self.smartAudioUnmute();
242
-            }
243
-            sendAnalytics(createEvent(
244
-                'volume.changed',
245
-                {
246
-                    volume: event.data.volume,
247
-                    muted: event.data.muted
248
-                }));
249
-        };
250
-
251
-        window.onPlayerReady = function(event) {
252
-            const player = event.target;
253
-
254
-            // do not relay on autoplay as it is not sending all of the events
255
-            // in onPlayerStateChange
256
-
257
-            player.playVideo();
258
-
259
-            const iframe = player.getIframe();
260
-
261
-            // eslint-disable-next-line no-use-before-define
262
-            self.sharedVideo = new SharedVideoContainer(
263
-                { url,
264
-                    iframe,
265
-                    player });
266
-
267
-            // prevents pausing participants not sharing the video
268
-            // to pause the video
269
-            if (!APP.conference.isLocalId(self.from)) {
270
-                $('#sharedVideo').css('pointer-events', 'none');
271
-            }
272
-
273
-            VideoLayout.addLargeVideoContainer(
274
-                SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo);
275
-
276
-            APP.store.dispatch(participantJoined({
277
-
278
-                // FIXME The cat is out of the bag already or rather _room is
279
-                // not private because it is used in multiple other places
280
-                // already such as AbstractPageReloadOverlay.
281
-                conference: APP.conference._room,
282
-                id: self.url,
283
-                isFakeParticipant: true,
284
-                name: VIDEO_PLAYER_PARTICIPANT_NAME
285
-            }));
286
-
287
-            APP.store.dispatch(pinParticipant(self.url));
288
-
289
-            // If we are sending the command and we are starting the player
290
-            // we need to continuously send the player current time position
291
-            if (APP.conference.isLocalId(self.from)) {
292
-                self.intervalId = setInterval(
293
-                    self.fireSharedVideoEvent.bind(self),
294
-                    updateInterval);
295
-            }
296
-        };
297
-
298
-        window.onPlayerError = function(event) {
299
-            logger.error('Error in the player:', event.data);
300
-
301
-            // store the error player, so we can remove it
302
-            self.errorInPlayer = event.target;
303
-        };
304
-    }
305
-
306
-    /**
307
-     * Process attributes, whether player needs to be paused or seek.
308
-     * @param player the player to operate over
309
-     * @param attributes the attributes with the player state we want
310
-     */
311
-    processVideoUpdate(player, attributes) {
312
-        if (!attributes) {
313
-            return;
314
-        }
315
-
316
-        // eslint-disable-next-line eqeqeq
317
-        if (attributes.state == 'playing') {
318
-
319
-            const isPlayerPaused
320
-                = this.player.getPlayerState() === YT.PlayerState.PAUSED;
321
-
322
-            // If our player is currently paused force the seek.
323
-            this.processTime(player, attributes, isPlayerPaused);
324
-
325
-            // Process mute.
326
-            const isAttrMuted = attributes.muted === 'true';
327
-
328
-            if (player.isMuted() !== isAttrMuted) {
329
-                this.smartPlayerMute(isAttrMuted, true);
330
-            }
331
-
332
-            // Process volume
333
-            if (!isAttrMuted
334
-                && attributes.volume !== undefined
335
-                // eslint-disable-next-line eqeqeq
336
-                && player.getVolume() != attributes.volume) {
337
-
338
-                player.setVolume(attributes.volume);
339
-                logger.info(`Player change of volume:${attributes.volume}`);
340
-            }
341
-
342
-            if (isPlayerPaused) {
343
-                player.playVideo();
344
-            }
345
-            // eslint-disable-next-line eqeqeq
346
-        } else if (attributes.state == 'pause') {
347
-            // if its not paused, pause it
348
-            player.pauseVideo();
349
-
350
-            this.processTime(player, attributes, true);
351
-        }
352
-    }
353
-
354
-    /**
355
-     * Check for time in attributes and if needed seek in current player
356
-     * @param player the player to operate over
357
-     * @param attributes the attributes with the player state we want
358
-     * @param forceSeek whether seek should be forced
359
-     */
360
-    processTime(player, attributes, forceSeek) {
361
-        if (forceSeek) {
362
-            logger.info('Player seekTo:', attributes.time);
363
-            player.seekTo(attributes.time);
364
-
365
-            return;
366
-        }
367
-
368
-        // check received time and current time
369
-        const currentPosition = player.getCurrentTime();
370
-        const diff = Math.abs(attributes.time - currentPosition);
371
-
372
-        // if we drift more than the interval for checking
373
-        // sync, the interval is in milliseconds
374
-        if (diff > updateInterval / 1000) {
375
-            logger.info('Player seekTo:', attributes.time,
376
-                ' current time is:', currentPosition, ' diff:', diff);
377
-            player.seekTo(attributes.time);
378
-        }
379
-    }
380
-
381
-    /**
382
-     * Checks current state of the player and fire an event with the values.
383
-     */
384
-    fireSharedVideoEvent(sendPauseEvent) {
385
-        // ignore update checks if we are not the owner of the video
386
-        // or there is still no player defined or we are stopped
387
-        // (in a process of stopping)
388
-        if (!APP.conference.isLocalId(this.from) || !this.player
389
-            || !this.isSharedVideoShown) {
390
-            return;
391
-        }
392
-
393
-        const state = this.player.getPlayerState();
394
-
395
-        // if its paused and haven't been pause - send paused
396
-
397
-        if (state === YT.PlayerState.PAUSED && sendPauseEvent) {
398
-            this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
399
-                this.url, 'pause', this.player.getCurrentTime());
400
-        } else if (state === YT.PlayerState.PLAYING) {
401
-            // if its playing and it was paused - send update with time
402
-            // if its playing and was playing just send update with time
403
-            this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO,
404
-                this.url, 'playing',
405
-                this.player.getCurrentTime(),
406
-                this.player.isMuted(),
407
-                this.player.getVolume());
408
-        }
409
-    }
410
-
411
-    /**
412
-     * Updates video, if it's not playing and needs starting or if it's playing
413
-     * and needs to be paused.
414
-     * @param id the id of the sender of the command
415
-     * @param url the video url
416
-     * @param attributes
417
-     */
418
-    onSharedVideoUpdate(id, url, attributes) {
419
-        // if we are sending the event ignore
420
-        if (APP.conference.isLocalId(this.from)) {
421
-            return;
422
-        }
423
-
424
-        if (!this.isSharedVideoShown) {
425
-            this.onSharedVideoStart(id, url, attributes);
426
-
427
-            return;
428
-        }
429
-
430
-        // eslint-disable-next-line no-negated-condition
431
-        if (!this.player) {
432
-            this.initialAttributes = attributes;
433
-        } else {
434
-            this.processVideoUpdate(this.player, attributes);
435
-        }
436
-    }
437
-
438
-    /**
439
-     * Stop shared video if it is currently showed. If the user started the
440
-     * shared video is the one in the id (called when user
441
-     * left and we want to remove video if the user sharing it left).
442
-     * @param id the id of the sender of the command
443
-     */
444
-    onSharedVideoStop(id, attributes) {
445
-        if (!this.isSharedVideoShown) {
446
-            return;
447
-        }
448
-
449
-        if (this.from !== id) {
450
-            return;
451
-        }
452
-
453
-        if (!this.player) {
454
-            // if there is no error in the player till now,
455
-            // store the initial attributes
456
-            if (!this.errorInPlayer) {
457
-                this.initialAttributes = attributes;
458
-
459
-                return;
460
-            }
461
-        }
462
-
463
-        this.emitter.removeListener(UIEvents.AUDIO_MUTED,
464
-            this.localAudioMutedListener);
465
-        this.localAudioMutedListener = null;
466
-
467
-        APP.store.dispatch(participantLeft(this.url, APP.conference._room));
468
-
469
-        VideoLayout.showLargeVideoContainer(SHARED_VIDEO_CONTAINER_TYPE, false)
470
-            .then(() => {
471
-                VideoLayout.removeLargeVideoContainer(
472
-                    SHARED_VIDEO_CONTAINER_TYPE);
473
-
474
-                if (this.player) {
475
-                    this.player.destroy();
476
-                    this.player = null;
477
-                } else if (this.errorInPlayer) {
478
-                    // if there is an error in player, remove that instance
479
-                    this.errorInPlayer.destroy();
480
-                    this.errorInPlayer = null;
481
-                }
482
-                this.smartAudioUnmute();
483
-
484
-                // revert to original behavior (prevents pausing
485
-                // for participants not sharing the video to pause it)
486
-                $('#sharedVideo').css('pointer-events', 'auto');
487
-
488
-                this.emitter.emit(
489
-                    UIEvents.UPDATE_SHARED_VIDEO, null, 'removed');
490
-            });
491
-
492
-        this.url = null;
493
-        this.isSharedVideoShown = false;
494
-        this.initialAttributes = null;
495
-    }
496
-
497
-    /**
498
-     * Receives events for local audio mute/unmute by local user.
499
-     * @param muted boolena whether it is muted or not.
500
-     * @param {boolean} indicates if this mute was a result of user interaction,
501
-     * i.e. pressing the mute button or it was programmatically triggered
502
-     */
503
-    onLocalAudioMuted(muted, userInteraction) {
504
-        if (!this.player) {
505
-            return;
506
-        }
507
-
508
-        if (muted) {
509
-            this.mutedWithUserInteraction = userInteraction;
510
-        } else if (this.player.getPlayerState() !== YT.PlayerState.PAUSED) {
511
-            this.smartPlayerMute(true, false);
512
-
513
-            // Check if we need to update other participants
514
-            this.fireSharedVideoEvent();
515
-        }
516
-    }
517
-
518
-    /**
519
-     * Mutes / unmutes the player.
520
-     * @param mute true to mute the shared video, false - otherwise.
521
-     * @param {boolean} Indicates if this mute is a consequence of a network
522
-     * video update or is called locally.
523
-     */
524
-    smartPlayerMute(mute, isVideoUpdate) {
525
-        if (!this.player.isMuted() && mute) {
526
-            this.player.mute();
527
-
528
-            if (isVideoUpdate) {
529
-                this.smartAudioUnmute();
530
-            }
531
-        } else if (this.player.isMuted() && !mute) {
532
-            this.player.unMute();
533
-            if (isVideoUpdate) {
534
-                this.smartAudioMute();
535
-            }
536
-        }
537
-    }
538
-
539
-    /**
540
-     * Smart mike unmute. If the mike is currently muted and it wasn't muted
541
-     * by the user via the mike button and the volume of the shared video is on
542
-     * we're unmuting the mike automatically.
543
-     */
544
-    smartAudioUnmute() {
545
-        if (APP.conference.isLocalAudioMuted()
546
-            && !this.mutedWithUserInteraction
547
-            && !this.isSharedVideoVolumeOn()) {
548
-            sendAnalytics(createEvent('audio.unmuted'));
549
-            logger.log('Shared video: audio unmuted');
550
-            this.emitter.emit(UIEvents.AUDIO_MUTED, false, false);
551
-        }
552
-    }
553
-
554
-    /**
555
-     * Smart mike mute. If the mike isn't currently muted and the shared video
556
-     * volume is on we mute the mike.
557
-     */
558
-    smartAudioMute() {
559
-        if (!APP.conference.isLocalAudioMuted()
560
-            && this.isSharedVideoVolumeOn()) {
561
-            sendAnalytics(createEvent('audio.muted'));
562
-            logger.log('Shared video: audio muted');
563
-            this.emitter.emit(UIEvents.AUDIO_MUTED, true, false);
564
-        }
565
-    }
566
-}
567
-
568
-/**
569
- * Container for shared video iframe.
570
- */
571
-class SharedVideoContainer extends LargeContainer {
572
-    /**
573
-     *
574
-     */
575
-    constructor({ url, iframe, player }) {
576
-        super();
577
-
578
-        this.$iframe = $(iframe);
579
-        this.url = url;
580
-        this.player = player;
581
-    }
582
-
583
-    /**
584
-     *
585
-     */
586
-    show() {
587
-        const self = this;
588
-
589
-
590
-        return new Promise(resolve => {
591
-            this.$iframe.fadeIn(300, () => {
592
-                self.bodyBackground = document.body.style.background;
593
-                document.body.style.background = 'black';
594
-                this.$iframe.css({ opacity: 1 });
595
-                APP.store.dispatch(dockToolbox(true));
596
-                resolve();
597
-            });
598
-        });
599
-    }
600
-
601
-    /**
602
-     *
603
-     */
604
-    hide() {
605
-        const self = this;
606
-
607
-        APP.store.dispatch(dockToolbox(false));
608
-
609
-        return new Promise(resolve => {
610
-            this.$iframe.fadeOut(300, () => {
611
-                document.body.style.background = self.bodyBackground;
612
-                this.$iframe.css({ opacity: 0 });
613
-                resolve();
614
-            });
615
-        });
616
-    }
617
-
618
-    /**
619
-     *
620
-     */
621
-    onHoverIn() {
622
-        APP.store.dispatch(showToolbox());
623
-    }
624
-
625
-    /**
626
-     *
627
-     */
628
-    get id() {
629
-        return this.url;
630
-    }
631
-
632
-    /**
633
-     *
634
-     */
635
-    resize(containerWidth, containerHeight) {
636
-        let height, width;
637
-
638
-        if (interfaceConfig.VERTICAL_FILMSTRIP) {
639
-            height = containerHeight - getToolboxHeight();
640
-            width = containerWidth - Filmstrip.getVerticalFilmstripWidth();
641
-        } else {
642
-            height = containerHeight - Filmstrip.getFilmstripHeight();
643
-            width = containerWidth;
644
-        }
645
-
646
-        this.$iframe.width(width).height(height);
647
-    }
648
-
649
-    /**
650
-     * @return {boolean} do not switch on dominant speaker event if on stage.
651
-     */
652
-    stayOnStage() {
653
-        return false;
654
-    }
655
-}

+ 1
- 2
modules/UI/videolayout/VideoLayout.js View File

@@ -8,7 +8,6 @@ import {
8 8
     getParticipantById
9 9
 } from '../../../react/features/base/participants';
10 10
 import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
11
-import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
12 11
 
13 12
 import LargeVideoManager from './LargeVideoManager';
14 13
 import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
@@ -89,7 +88,7 @@ const VideoLayout = {
89 88
         const participant = getParticipantById(state, id);
90 89
 
91 90
         if (participant?.isFakeParticipant) {
92
-            return SHARED_VIDEO_CONTAINER_TYPE;
91
+            return VIDEO_TYPE.CAMERA;
93 92
         }
94 93
 
95 94
         const videoTrack = getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);

+ 37
- 0
package-lock.json View File

@@ -11145,6 +11145,11 @@
11145 11145
         "strip-bom": "^3.0.0"
11146 11146
       }
11147 11147
     },
11148
+    "load-script": {
11149
+      "version": "1.0.0",
11150
+      "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
11151
+      "integrity": "sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ="
11152
+    },
11148 11153
     "loader-runner": {
11149 11154
       "version": "2.4.0",
11150 11155
       "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@@ -15305,6 +15310,23 @@
15305 15310
         }
15306 15311
       }
15307 15312
     },
15313
+    "react-youtube": {
15314
+      "version": "7.13.1",
15315
+      "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.13.1.tgz",
15316
+      "integrity": "sha512-b++TLHmHDpd0ZBS1wcbYabbuchU+W4jtx5A2MUQX0BINNKKsaIQX29sn/aLvZ9v5luwAoceia3VGtyz9blaB9w==",
15317
+      "requires": {
15318
+        "fast-deep-equal": "3.1.3",
15319
+        "prop-types": "15.7.2",
15320
+        "youtube-player": "5.5.2"
15321
+      },
15322
+      "dependencies": {
15323
+        "fast-deep-equal": {
15324
+          "version": "3.1.3",
15325
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
15326
+          "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
15327
+        }
15328
+      }
15329
+    },
15308 15330
     "read-pkg": {
15309 15331
       "version": "2.0.0",
15310 15332
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@@ -16105,6 +16127,11 @@
16105 16127
         }
16106 16128
       }
16107 16129
     },
16130
+    "sister": {
16131
+      "version": "3.0.2",
16132
+      "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz",
16133
+      "integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA=="
16134
+    },
16108 16135
     "slash": {
16109 16136
       "version": "2.0.0",
16110 16137
       "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
@@ -19806,6 +19833,16 @@
19806 19833
         }
19807 19834
       }
19808 19835
     },
19836
+    "youtube-player": {
19837
+      "version": "5.5.2",
19838
+      "resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
19839
+      "integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==",
19840
+      "requires": {
19841
+        "debug": "^2.6.6",
19842
+        "load-script": "^1.0.0",
19843
+        "sister": "^3.0.0"
19844
+      }
19845
+    },
19809 19846
     "zxcvbn": {
19810 19847
       "version": "4.4.2",
19811 19848
       "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",

+ 1
- 0
package.json View File

@@ -92,6 +92,7 @@
92 92
     "react-redux": "7.1.0",
93 93
     "react-textarea-autosize": "7.1.0",
94 94
     "react-transition-group": "2.4.0",
95
+    "react-youtube": "7.13.1",
95 96
     "redux": "4.0.4",
96 97
     "redux-thunk": "2.2.0",
97 98
     "rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",

+ 4
- 2
react/features/base/participants/components/ParticipantView.native.js View File

@@ -4,6 +4,7 @@ import React, { Component } from 'react';
4 4
 import { Text, View } from 'react-native';
5 5
 
6 6
 import { YoutubeLargeVideo } from '../../../shared-video/components';
7
+import { getYoutubeId } from '../../../shared-video/functions';
7 8
 import { Avatar } from '../../avatar';
8 9
 import { translate } from '../../i18n';
9 10
 import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
@@ -208,7 +209,8 @@ class ParticipantView extends Component<Props> {
208 209
                 ? this.props.testHintId
209 210
                 : `org.jitsi.meet.Participant#${this.props.participantId}`;
210 211
 
211
-        const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo;
212
+        const youtubeId = getYoutubeId(this.props.participantId);
213
+        const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo && Boolean(youtubeId);
212 214
 
213 215
         return (
214 216
             <Container
@@ -224,7 +226,7 @@ class ParticipantView extends Component<Props> {
224 226
                     onPress = { renderYoutubeLargeVideo ? undefined : onPress }
225 227
                     value = '' />
226 228
 
227
-                { renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { this.props.participantId } /> }
229
+                { renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { youtubeId } /> }
228 230
 
229 231
                 { !_isFakeParticipant && renderVideo
230 232
                     && <VideoTrack

+ 7
- 4
react/features/filmstrip/components/web/Thumbnail.js View File

@@ -550,7 +550,7 @@ class Thumbnail extends Component<Props, State> {
550 550
      */
551 551
     _renderFakeParticipant() {
552 552
         const { _participant } = this.props;
553
-        const { id } = _participant;
553
+        const { id, avatarURL } = _participant;
554 554
         const styles = this._getStyles();
555 555
         const containerClassName = this._getContainerClassName();
556 556
 
@@ -562,9 +562,12 @@ class Thumbnail extends Component<Props, State> {
562 562
                 onMouseEnter = { this._onMouseEnter }
563 563
                 onMouseLeave = { this._onMouseLeave }
564 564
                 style = { styles.thumbnail }>
565
-                <img
566
-                    className = 'sharedVideoAvatar'
567
-                    src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
565
+                {avatarURL ? (
566
+                    <img
567
+                        className = 'sharedVideoAvatar'
568
+                        src = { avatarURL } />
569
+                )
570
+                    : this._renderAvatar(styles.avatar)}
568 571
                 <div className = 'displayNameContainer'>
569 572
                     <DisplayName
570 573
                         elementID = 'sharedVideoContainer_name'

+ 8
- 5
react/features/large-video/components/LargeVideo.web.js View File

@@ -6,6 +6,7 @@ import { Watermarks } from '../../base/react';
6 6
 import { connect } from '../../base/redux';
7 7
 import { setColorAlpha } from '../../base/util';
8 8
 import { fetchCustomBrandingData } from '../../dynamic-branding';
9
+import { SharedVideo } from '../../shared-video/components/web';
9 10
 import { Captions } from '../../subtitles/';
10 11
 
11 12
 declare var interfaceConfig: Object;
@@ -67,17 +68,19 @@ class LargeVideo extends Component<Props> {
67 68
      * @returns {React$Element}
68 69
      */
69 70
     render() {
71
+        const {
72
+            _isChatOpen,
73
+            _noAutoPlayVideo
74
+        } = this.props;
70 75
         const style = this._getCustomSyles();
71
-        const className = `videocontainer${this.props._isChatOpen ? ' shift-right' : ''}`;
76
+        const className = `videocontainer${_isChatOpen ? ' shift-right' : ''}`;
72 77
 
73 78
         return (
74 79
             <div
75 80
                 className = { className }
76 81
                 id = 'largeVideoContainer'
77 82
                 style = { style }>
78
-                <div id = 'sharedVideo'>
79
-                    <div id = 'sharedVideoIFrame' />
80
-                </div>
83
+                <SharedVideo />
81 84
                 <div id = 'etherpad' />
82 85
 
83 86
                 <Watermarks />
@@ -101,7 +104,7 @@ class LargeVideo extends Component<Props> {
101 104
                       */}
102 105
                     <div id = 'largeVideoWrapper'>
103 106
                         <video
104
-                            autoPlay = { !this.props._noAutoPlayVideo }
107
+                            autoPlay = { !_noAutoPlayVideo }
105 108
                             id = 'largeVideo'
106 109
                             muted = { true }
107 110
                             playsInline = { true } /* for Safari on iOS to work */ />

+ 4
- 5
react/features/shared-video/actionTypes.js View File

@@ -12,15 +12,14 @@
12 12
 export const SET_SHARED_VIDEO_STATUS = 'SET_SHARED_VIDEO_STATUS';
13 13
 
14 14
 /**
15
- * The type of the action which signals to start the flow for starting or
16
- * stopping a shared video.
15
+ * The type of the action which signals to reset the current known state of the
16
+ * shared video.
17 17
  *
18 18
  * {
19
- *     type: TOGGLE_SHARED_VIDEO
19
+ *     type: RESET_SHARED_VIDEO_STATUS,
20 20
  * }
21 21
  */
22
-export const TOGGLE_SHARED_VIDEO = 'TOGGLE_SHARED_VIDEO';
23
-
22
+export const RESET_SHARED_VIDEO_STATUS = 'RESET_SHARED_VIDEO_STATUS';
24 23
 
25 24
 /**
26 25
  * The type of the action which signals to disable or enable the shared video

+ 122
- 0
react/features/shared-video/actions.any.js View File

@@ -0,0 +1,122 @@
1
+import { getCurrentConference } from '../base/conference';
2
+import { openDialog } from '../base/dialog/actions';
3
+import { getLocalParticipant } from '../base/participants';
4
+import { SharedVideoDialog } from '../shared-video/components';
5
+
6
+import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes';
7
+
8
+/**
9
+ * Resets the status of the shared video.
10
+ *
11
+ * @returns {{
12
+ *     type: SET_SHARED_VIDEO_STATUS,
13
+ * }}
14
+ */
15
+export function resetSharedVideoStatus() {
16
+    return {
17
+        type: RESET_SHARED_VIDEO_STATUS
18
+    };
19
+}
20
+
21
+/**
22
+ * Updates the current known status of the shared video.
23
+ *
24
+ * @param {{
25
+ *     muted: boolean,
26
+ *     ownerId: string,
27
+ *     status: string,
28
+ *     time: number,
29
+ *     videoUrl: string
30
+ * }} options - The options.
31
+ *
32
+ * @returns {{
33
+ *     type: SET_SHARED_VIDEO_STATUS,
34
+ *     muted: boolean,
35
+ *     ownerId: string,
36
+ *     status: string,
37
+ *     time: number,
38
+ *     videoUrl: string,
39
+ * }}
40
+ */
41
+export function setSharedVideoStatus({ videoUrl, status, time, ownerId, muted }) {
42
+    return {
43
+        type: SET_SHARED_VIDEO_STATUS,
44
+        ownerId,
45
+        status,
46
+        time,
47
+        videoUrl,
48
+        muted
49
+    };
50
+}
51
+
52
+/**
53
+ * Displays the dialog for entering the video link.
54
+ *
55
+ * @param {Function} onPostSubmit - The function to be invoked when a valid link is entered.
56
+ * @returns {Function}
57
+ */
58
+export function showSharedVideoDialog(onPostSubmit) {
59
+    return openDialog(SharedVideoDialog, { onPostSubmit });
60
+}
61
+
62
+/**
63
+ *
64
+ * Stops playing a shared video.
65
+ *
66
+ * @returns {Function}
67
+ */
68
+export function stopSharedVideo() {
69
+    return (dispatch, getState) => {
70
+        const state = getState();
71
+        const { ownerId } = state['features/shared-video'];
72
+        const localParticipant = getLocalParticipant(state);
73
+
74
+        if (ownerId === localParticipant.id) {
75
+            dispatch(resetSharedVideoStatus());
76
+        }
77
+    };
78
+}
79
+
80
+/**
81
+ *
82
+ * Plays a shared video.
83
+ *
84
+ * @param {string} videoUrl - The video url to be played.
85
+ *
86
+ * @returns {Function}
87
+ */
88
+export function playSharedVideo(videoUrl) {
89
+    return (dispatch, getState) => {
90
+        const conference = getCurrentConference(getState());
91
+
92
+        if (conference) {
93
+            const localParticipant = getLocalParticipant(getState());
94
+
95
+            dispatch(setSharedVideoStatus({
96
+                videoUrl,
97
+                status: 'start',
98
+                time: 0,
99
+                ownerId: localParticipant.id
100
+            }));
101
+        }
102
+    };
103
+}
104
+
105
+/**
106
+ *
107
+ * Stops playing a shared video.
108
+ *
109
+ * @returns {Function}
110
+ */
111
+export function toggleSharedVideo() {
112
+    return (dispatch, getState) => {
113
+        const state = getState();
114
+        const { status } = state['features/shared-video'];
115
+
116
+        if ([ 'playing', 'start', 'pause' ].includes(status)) {
117
+            dispatch(stopSharedVideo());
118
+        } else {
119
+            dispatch(showSharedVideoDialog(id => dispatch(playSharedVideo(id))));
120
+        }
121
+    };
122
+}

+ 1
- 54
react/features/shared-video/actions.native.js View File

@@ -1,54 +1 @@
1
-// @flow
2
-
3
-import { openDialog } from '../base/dialog';
4
-
5
-import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO } from './actionTypes';
6
-import { SharedVideoDialog } from './components/native';
7
-
8
-/**
9
- * Updates the current known status of the shared video.
10
- *
11
- * @param {string} videoId - The id of the video to be shared.
12
- * @param {string} status - The current status of the video being shared.
13
- * @param {number} time - The current position of the video being shared.
14
- * @param {string} ownerId - The participantId of the user sharing the 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 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 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 showSharedVideoDialog(onPostSubmit: ?Function) {
53
-    return openDialog(SharedVideoDialog, { onPostSubmit });
54
-}
1
+export * from './actions.any';

+ 2
- 43
react/features/shared-video/actions.web.js View File

@@ -1,26 +1,8 @@
1 1
 // @flow
2 2
 
3
-import { openDialog } from '../base/dialog/actions';
4
-import { SharedVideoDialog } from '../shared-video/components';
5
-
6
-import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO, SET_DISABLE_BUTTON } from './actionTypes';
7
-
8
-/**
9
- * Updates the current known status of the shared video.
10
- *
11
- * @param {string} status - The current status of the video being shared.
12
- * @returns {{
13
- *     type: SET_SHARED_VIDEO_STATUS,
14
- *     status: string
15
- * }}
16
- */
17
-export function setSharedVideoStatus(status: string) {
18
-    return {
19
-        type: SET_SHARED_VIDEO_STATUS,
20
-        status
21
-    };
22
-}
3
+import { SET_DISABLE_BUTTON } from './actionTypes';
23 4
 
5
+export * from './actions.any';
24 6
 
25 7
 /**
26 8
  * Disabled share video button.
@@ -37,26 +19,3 @@ export function setDisableButton(disabled: boolean) {
37 19
         disabled
38 20
     };
39 21
 }
40
-
41
-/**
42
- * Starts the flow for starting or stopping a shared video.
43
- *
44
- * @returns {{
45
- *     type: TOGGLE_SHARED_VIDEO
46
- * }}
47
- */
48
-export function toggleSharedVideo() {
49
-    return {
50
-        type: TOGGLE_SHARED_VIDEO
51
-    };
52
-}
53
-
54
-/**
55
- * Displays the dialog for entering the video link.
56
- *
57
- * @param {Function} onPostSubmit - The function to be invoked when a valid link is entered.
58
- * @returns {Function}
59
- */
60
-export function showSharedVideoDialog(onPostSubmit: ?Function) {
61
-    return openDialog(SharedVideoDialog, { onPostSubmit });
62
-}

+ 4
- 15
react/features/shared-video/components/AbstractSharedVideoDialog.js View File

@@ -3,9 +3,6 @@
3 3
 import { Component } from 'react';
4 4
 import type { Dispatch } from 'redux';
5 5
 
6
-import { getYoutubeLink } from '../functions';
7
-
8
-
9 6
 /**
10 7
  * The type of the React {@code Component} props of
11 8
  * {@link AbstractSharedVideoDialog}.
@@ -20,7 +17,7 @@ export type Props = {
20 17
     /**
21 18
      * Function to be invoked after typing a valid video.
22 19
      */
23
-    onPostSubmit: ?Function,
20
+    onPostSubmit: Function,
24 21
 
25 22
     /**
26 23
      * Invoked to obtain translated strings.
@@ -60,18 +57,10 @@ export default class AbstractSharedVideoDialog<S: *> extends Component < Props,
60 57
             return false;
61 58
         }
62 59
 
63
-        const videoId = getYoutubeLink(link);
60
+        const { onPostSubmit } = this.props;
64 61
 
65
-        if (videoId) {
66
-            const { onPostSubmit } = this.props;
62
+        onPostSubmit(link);
67 63
 
68
-            onPostSubmit && onPostSubmit(videoId);
69
-
70
-            return true;
71
-        }
72
-
73
-        return false;
64
+        return true;
74 65
     }
75 66
 }
76
-
77
-

+ 16
- 6
react/features/shared-video/components/native/YoutubeLargeVideo.js View File

@@ -292,32 +292,42 @@ class YoutubeLargeVideo extends Component<Props, *> {
292 292
     /**
293 293
      * Dispatches the video status, time and ownerId if the status is playing or paused.
294 294
      *
295
-     * @param {string} videoId - The youtube id of the video.
295
+     * @param {string} videoUrl - The youtube id of the video.
296 296
      * @param {string} status - The status of the player.
297 297
      * @param {number} time - The seek time.
298 298
      * @param {string} ownerId - The id of the participant sharing the video.
299 299
      * @private
300 300
      * @returns {void}
301 301
     */
302
-    onVideoChangeEvent(videoId, status, time, ownerId) {
302
+    onVideoChangeEvent(videoUrl, status, time, ownerId) {
303 303
         if (![ 'playing', 'paused' ].includes(status)) {
304 304
             return;
305 305
         }
306 306
 
307
-        this.props.dispatch(setSharedVideoStatus(videoId, translateStatus(status), time, ownerId));
307
+        this.props.dispatch(setSharedVideoStatus({
308
+            videoUrl,
309
+            status: translateStatus(status),
310
+            time,
311
+            ownerId
312
+        }));
308 313
     }
309 314
 
310 315
     /**
311 316
      * Dispatches the 'playing' as video status, time and ownerId.
312 317
      *
313
-     * @param {string} videoId - The youtube id of the video.
318
+     * @param {string} videoUrl - The youtube id of the video.
314 319
      * @param {number} time - The seek time.
315 320
      * @param {string} ownerId - The id of the participant sharing the video.
316 321
      * @private
317 322
      * @returns {void}
318 323
     */
319
-    onVideoReady(videoId, time, ownerId) {
320
-        time.then(t => this.props.dispatch(setSharedVideoStatus(videoId, 'playing', t, ownerId)));
324
+    onVideoReady(videoUrl, time, ownerId) {
325
+        time.then(t => this.props.dispatch(setSharedVideoStatus({
326
+            videoUrl,
327
+            status: 'playing',
328
+            time: t,
329
+            ownerId
330
+        })));
321 331
     }
322 332
 
323 333
     /**

+ 425
- 0
react/features/shared-video/components/web/AbstractVideoManager.js View File

@@ -0,0 +1,425 @@
1
+/* @flow */
2
+/* eslint-disable no-invalid-this */
3
+
4
+import throttle from 'lodash/throttle';
5
+import { Component } from 'react';
6
+
7
+import { sendAnalytics, createSharedVideoEvent as createEvent } from '../../../analytics';
8
+import { getCurrentConference } from '../../../base/conference';
9
+import { MEDIA_TYPE } from '../../../base/media';
10
+import { getLocalParticipant } from '../../../base/participants';
11
+import { isLocalTrackMuted } from '../../../base/tracks';
12
+import { dockToolbox } from '../../../toolbox/actions.web';
13
+import { muteLocal } from '../../../video-menu/actions.any';
14
+import { setSharedVideoStatus } from '../../actions.any';
15
+
16
+export const PLAYBACK_STATES = {
17
+    PLAYING: 'playing',
18
+    PAUSED: 'pause',
19
+    STOPPED: 'stop'
20
+};
21
+
22
+/**
23
+ * Return true if the diffenrece between the two timees is larger than 5.
24
+ *
25
+ * @param {number} newTime - The current time.
26
+ * @param {number} previousTime - The previous time.
27
+ * @private
28
+ * @returns {boolean}
29
+*/
30
+function shouldSeekToPosition(newTime, previousTime) {
31
+    return Math.abs(newTime - previousTime) > 5;
32
+}
33
+
34
+/**
35
+ * The type of the React {@link Component} props of {@link YoutubeLargeVideo}.
36
+ */
37
+export type Props = {
38
+
39
+    /**
40
+     * The current coference
41
+     */
42
+    _conference: Object,
43
+
44
+    /**
45
+     * Docks the toolbox
46
+     */
47
+    _dockToolbox: Function,
48
+
49
+    /**
50
+     * Indicates whether the local audio is muted
51
+    */
52
+    _isLocalAudioMuted: boolean,
53
+
54
+    /**
55
+     * Is the video shared by the local user.
56
+     *
57
+     * @private
58
+     */
59
+    _isOwner: boolean,
60
+
61
+    /**
62
+     * Store flag for muted state
63
+     */
64
+    _muted: boolean,
65
+
66
+    /**
67
+     * Mutes local audio track
68
+     */
69
+    _muteLocal: Function,
70
+
71
+    /**
72
+     * The shared video owner id
73
+     */
74
+    _ownerId: string,
75
+
76
+    /**
77
+     * Updates the shared video status
78
+     */
79
+    _setSharedVideoStatus: Function,
80
+
81
+    /**
82
+     * The shared video status
83
+     */
84
+     _status: string,
85
+
86
+    /**
87
+     * Seek time in seconds.
88
+     *
89
+     */
90
+    _time: number,
91
+
92
+    /**
93
+     * The video url
94
+     */
95
+     _videoUrl: string,
96
+
97
+     /**
98
+      * The video id
99
+      */
100
+     videoId: string
101
+}
102
+
103
+/**
104
+ * Manager of shared video.
105
+ */
106
+class AbstractVideoManager extends Component<Props> {
107
+    throttledFireUpdateSharedVideoEvent: Function;
108
+
109
+    /**
110
+     * Initializes a new instance of AbstractVideoManager.
111
+     *
112
+     * @returns {void}
113
+     */
114
+    constructor() {
115
+        super();
116
+
117
+        this.throttledFireUpdateSharedVideoEvent = throttle(this.fireUpdateSharedVideoEvent.bind(this), 5000);
118
+
119
+        // selenium tests handler
120
+        window._sharedVideoPlayer = this;
121
+    }
122
+
123
+    /**
124
+     * Implements React Component's componentDidMount.
125
+     *
126
+     * @inheritdoc
127
+     */
128
+    componentDidMount() {
129
+        this.props._dockToolbox(true);
130
+        this.processUpdatedProps();
131
+    }
132
+
133
+    /**
134
+     * Implements React Component's componentDidUpdate.
135
+     *
136
+     * @inheritdoc
137
+     */
138
+    componentDidUpdate(prevProps: Props) {
139
+        const { _videoUrl } = this.props;
140
+
141
+        if (prevProps._videoUrl !== _videoUrl) {
142
+            sendAnalytics(createEvent('started'));
143
+        }
144
+
145
+        this.processUpdatedProps();
146
+    }
147
+
148
+    /**
149
+     * Implements React Component's componentWillUnmount.
150
+     *
151
+     * @inheritdoc
152
+     */
153
+    componentWillUnmount() {
154
+        sendAnalytics(createEvent('stopped'));
155
+
156
+        if (this.dispose) {
157
+            this.dispose();
158
+        }
159
+
160
+        this.props._dockToolbox(false);
161
+    }
162
+
163
+    /**
164
+     * Processes new properties.
165
+     *
166
+     * @returns {void}
167
+     */
168
+    processUpdatedProps() {
169
+        const { _status, _time, _isOwner, _muted } = this.props;
170
+
171
+        if (_isOwner) {
172
+            return;
173
+        }
174
+
175
+        const playerTime = this.getTime();
176
+
177
+        if (shouldSeekToPosition(_time, playerTime)) {
178
+            this.seek(_time);
179
+        }
180
+
181
+        if (this.getPlaybackState() !== _status) {
182
+            if (_status === PLAYBACK_STATES.PLAYING) {
183
+                this.play();
184
+            }
185
+
186
+            if (_status === PLAYBACK_STATES.PAUSED) {
187
+                this.pause();
188
+            }
189
+        }
190
+
191
+        if (this.isMuted() !== _muted) {
192
+            if (_muted) {
193
+                this.mute();
194
+            } else {
195
+                this.unMute();
196
+            }
197
+        }
198
+    }
199
+
200
+    /**
201
+     * Handle video playing.
202
+     *
203
+     * @returns {void}
204
+     */
205
+    onPlay() {
206
+        this.smartAudioMute();
207
+        sendAnalytics(createEvent('play'));
208
+        this.fireUpdateSharedVideoEvent();
209
+    }
210
+
211
+    /**
212
+     * Handle video paused.
213
+     *
214
+     * @returns {void}
215
+     */
216
+    onPause() {
217
+        sendAnalytics(createEvent('paused'));
218
+        this.fireUpdateSharedVideoEvent();
219
+    }
220
+
221
+    /**
222
+     * Handle volume changed.
223
+     *
224
+     * @returns {void}
225
+     */
226
+    onVolumeChange() {
227
+        const volume = this.getVolume();
228
+        const muted = this.isMuted();
229
+
230
+        if (volume > 0 && !muted) {
231
+            this.smartAudioMute();
232
+        }
233
+
234
+        sendAnalytics(createEvent(
235
+            'volume.changed',
236
+            {
237
+                volume,
238
+                muted
239
+            }));
240
+
241
+        this.fireUpdatePlayingVideoEvent();
242
+    }
243
+
244
+    /**
245
+     * Handle changes to the shared playing video.
246
+     *
247
+     * @returns {void}
248
+     */
249
+    fireUpdatePlayingVideoEvent() {
250
+        if (this.getPlaybackState() === PLAYBACK_STATES.PLAYING) {
251
+            this.fireUpdateSharedVideoEvent();
252
+        }
253
+    }
254
+
255
+    /**
256
+     * Dispatches an update action for the shared video.
257
+     *
258
+     * @returns {void}
259
+     */
260
+    fireUpdateSharedVideoEvent() {
261
+        const { _isOwner } = this.props;
262
+
263
+        if (!_isOwner) {
264
+            return;
265
+        }
266
+
267
+        const status = this.getPlaybackState();
268
+
269
+        if (!Object.values(PLAYBACK_STATES).includes(status)) {
270
+            return;
271
+        }
272
+
273
+        const {
274
+            _ownerId,
275
+            _setSharedVideoStatus,
276
+            _videoUrl
277
+        } = this.props;
278
+
279
+        _setSharedVideoStatus({
280
+            videoUrl: _videoUrl,
281
+            status,
282
+            time: this.getTime(),
283
+            ownerId: _ownerId,
284
+            muted: this.isMuted()
285
+        });
286
+    }
287
+
288
+    /**
289
+     * Indicates if the player volume is currently on. This will return true if
290
+     * we have an available player, which is currently in a PLAYING state,
291
+     * which isn't muted and has it's volume greater than 0.
292
+     *
293
+     * @returns {boolean} Indicating if the volume of the shared video is
294
+     * currently on.
295
+     */
296
+    isSharedVideoVolumeOn() {
297
+        return this.getPlaybackState() === PLAYBACK_STATES.PLAYING
298
+                && !this.isMuted()
299
+                && this.getVolume() > 0;
300
+    }
301
+
302
+    /**
303
+     * Smart mike mute. If the mike isn't currently muted and the shared video
304
+     * volume is on we mute the mike.
305
+     *
306
+     * @returns {void}
307
+     */
308
+    smartAudioMute() {
309
+        const { _isLocalAudioMuted, _muteLocal } = this.props;
310
+
311
+        if (!_isLocalAudioMuted
312
+            && this.isSharedVideoVolumeOn()) {
313
+            sendAnalytics(createEvent('audio.muted'));
314
+            _muteLocal(true);
315
+        }
316
+    }
317
+
318
+    /**
319
+     * Seeks video to provided time
320
+     * @param {number} time
321
+     */
322
+    seek: (time: number) => void;
323
+
324
+    /**
325
+     * Indicates the playback state of the video
326
+     */
327
+    getPlaybackState: () => boolean;
328
+
329
+    /**
330
+     * Indicates whether the video is muted
331
+     */
332
+    isMuted: () => boolean;
333
+
334
+    /**
335
+     * Retrieves current volume
336
+     */
337
+    getVolume: () => number;
338
+
339
+    /**
340
+      * Sets current volume
341
+    */
342
+    setVolume: (value: number) => void;
343
+
344
+    /**
345
+     * Plays video
346
+     */
347
+    play: () => void;
348
+
349
+    /**
350
+     * Pauses video
351
+     */
352
+    pause: () => void;
353
+
354
+    /**
355
+     * Mutes video
356
+     */
357
+    mute: () => void;
358
+
359
+    /**
360
+     * Unmutes video
361
+     */
362
+    unMute: () => void;
363
+
364
+    /**
365
+     * Retrieves current time
366
+     */
367
+    getTime: () => number;
368
+
369
+    /**
370
+     * Disposes current video player
371
+     */
372
+    dispose: () => void;
373
+}
374
+
375
+
376
+export default AbstractVideoManager;
377
+
378
+/**
379
+ * Maps part of the Redux store to the props of this component.
380
+ *
381
+ * @param {Object} state - The Redux state.
382
+ * @returns {Props}
383
+ */
384
+export function _mapStateToProps(state: Object): $Shape<Props> {
385
+    const { ownerId, status, time, videoUrl, muted } = state['features/shared-video'];
386
+    const localParticipant = getLocalParticipant(state);
387
+    const _isLocalAudioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
388
+
389
+    return {
390
+        _conference: getCurrentConference(state),
391
+        _isLocalAudioMuted,
392
+        _isOwner: ownerId === localParticipant.id,
393
+        _muted: muted,
394
+        _ownerId: ownerId,
395
+        _status: status,
396
+        _time: time,
397
+        _videoUrl: videoUrl
398
+    };
399
+}
400
+
401
+/**
402
+ * Maps part of the props of this component to Redux actions.
403
+ *
404
+ * @param {Function} dispatch - The Redux dispatch function.
405
+ * @returns {Props}
406
+ */
407
+export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
408
+    return {
409
+        _dockToolbox: value => {
410
+            dispatch(dockToolbox(value));
411
+        },
412
+        _muteLocal: value => {
413
+            dispatch(muteLocal(value, MEDIA_TYPE.AUDIO));
414
+        },
415
+        _setSharedVideoStatus: ({ videoUrl, status, time, ownerId, muted }) => {
416
+            dispatch(setSharedVideoStatus({
417
+                videoUrl,
418
+                status,
419
+                time,
420
+                ownerId,
421
+                muted
422
+            }));
423
+        }
424
+    };
425
+}

+ 147
- 0
react/features/shared-video/components/web/SharedVideo.js View File

@@ -0,0 +1,147 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+
5
+import Filmstrip from '../../../../../modules/UI/videolayout/Filmstrip';
6
+import { getLocalParticipant } from '../../../base/participants';
7
+import { connect } from '../../../base/redux';
8
+import { getToolboxHeight } from '../../../toolbox/functions.web';
9
+import { getYoutubeId } from '../../functions';
10
+
11
+import VideoManager from './VideoManager';
12
+import YoutubeVideoManager from './YoutubeVideoManager';
13
+
14
+declare var interfaceConfig: Object;
15
+
16
+type Props = {
17
+
18
+    /**
19
+     * The available client width
20
+     */
21
+    clientHeight: number,
22
+
23
+    /**
24
+     * The available client width
25
+     */
26
+    clientWidth: number,
27
+
28
+    /**
29
+     * Is the video shared by the local user.
30
+     *
31
+     * @private
32
+     */
33
+     isOwner: boolean,
34
+
35
+    /**
36
+     * The shared video id
37
+     */
38
+     sharedVideoId: string,
39
+
40
+    /**
41
+     * The shared youtube video id
42
+     */
43
+     sharedYoutubeVideoId: string,
44
+}
45
+
46
+/**
47
+ * Implements a React {@link Component} which represents the large video (a.k.a.
48
+ * the conference participant who is on the local stage) on Web/React.
49
+ *
50
+ * @extends Component
51
+ */
52
+class SharedVideo extends Component<Props> {
53
+    /**
54
+     * Computes the width and the height of the component.
55
+     *
56
+     * @returns {{
57
+     *  height: number,
58
+     *  width: number
59
+     * }}
60
+     */
61
+    getDimmensions() {
62
+        const { clientHeight, clientWidth } = this.props;
63
+
64
+        let width;
65
+        let height;
66
+
67
+        if (interfaceConfig.VERTICAL_FILMSTRIP) {
68
+            height = `${clientHeight - getToolboxHeight()}px`;
69
+            width = `${clientWidth - Filmstrip.getVerticalFilmstripWidth()}px`;
70
+        } else {
71
+            height = `${clientHeight - Filmstrip.getFilmstripHeight()}px`;
72
+            width = `${clientWidth}px`;
73
+        }
74
+
75
+        return {
76
+            width,
77
+            height
78
+        };
79
+    }
80
+
81
+    /**
82
+     * Retrieves the manager to be used for playing the shared video.
83
+     *
84
+     * @returns {Component}
85
+     */
86
+    getManager() {
87
+        const {
88
+            sharedVideoId,
89
+            sharedYoutubeVideoId
90
+        } = this.props;
91
+
92
+        if (!sharedVideoId) {
93
+            return null;
94
+        }
95
+
96
+        if (sharedYoutubeVideoId) {
97
+            return <YoutubeVideoManager videoId = { sharedYoutubeVideoId } />;
98
+        }
99
+
100
+        return <VideoManager videoId = { sharedVideoId } />;
101
+    }
102
+
103
+    /**
104
+     * Implements React's {@link Component#render()}.
105
+     *
106
+     * @inheritdoc
107
+     * @returns {React$Element}
108
+     */
109
+    render() {
110
+        const { isOwner } = this.props;
111
+        const className = isOwner ? '' : 'disable-pointer';
112
+
113
+        return (
114
+            <div
115
+                className = { className }
116
+                id = 'sharedVideo'
117
+                style = { this.getDimmensions() }>
118
+                {this.getManager()}
119
+            </div>
120
+        );
121
+    }
122
+}
123
+
124
+
125
+/**
126
+ * Maps (parts of) the Redux state to the associated LargeVideo props.
127
+ *
128
+ * @param {Object} state - The Redux state.
129
+ * @private
130
+ * @returns {Props}
131
+ */
132
+function _mapStateToProps(state) {
133
+    const { ownerId, videoUrl } = state['features/shared-video'];
134
+    const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
135
+
136
+    const localParticipant = getLocalParticipant(state);
137
+
138
+    return {
139
+        clientHeight,
140
+        clientWidth,
141
+        isOwner: ownerId === localParticipant.id,
142
+        sharedVideoId: videoUrl,
143
+        sharedYoutubeVideoId: getYoutubeId(videoUrl)
144
+    };
145
+}
146
+
147
+export default connect(_mapStateToProps)(SharedVideo);

+ 4
- 10
react/features/shared-video/components/web/SharedVideoButton.js View File

@@ -9,11 +9,9 @@ import {
9 9
     AbstractButton,
10 10
     type AbstractButtonProps
11 11
 } from '../../../base/toolbox/components';
12
-import { showSharedVideoDialog } from '../../actions.web';
12
+import { toggleSharedVideo } from '../../actions.any';
13 13
 import { isSharingStatus } from '../../functions';
14 14
 
15
-declare var APP: Object;
16
-
17 15
 type Props = AbstractButtonProps & {
18 16
 
19 17
     /**
@@ -49,7 +47,7 @@ class SharedVideoButton extends AbstractButton<Props, *> {
49 47
      * @returns {void}
50 48
      */
51 49
     _handleClick() {
52
-        this._doToggleSharedVideoDialog();
50
+        this._doToggleSharedVideo();
53 51
     }
54 52
 
55 53
     /**
@@ -80,12 +78,8 @@ class SharedVideoButton extends AbstractButton<Props, *> {
80 78
      * @private
81 79
      * @returns {void}
82 80
      */
83
-    _doToggleSharedVideoDialog() {
84
-        const { dispatch } = this.props;
85
-
86
-        return this._isToggled()
87
-            ? APP.UI.stopSharedVideoEmitter()
88
-            : dispatch(showSharedVideoDialog(id => APP.UI.startSharedVideoEmitter(id)));
81
+    _doToggleSharedVideo() {
82
+        this.props.dispatch(toggleSharedVideo());
89 83
     }
90 84
 }
91 85
 

+ 1
- 2
react/features/shared-video/components/web/SharedVideoDialog.js View File

@@ -8,7 +8,6 @@ import { translate } from '../../../base/i18n';
8 8
 import { getFieldValue } from '../../../base/react';
9 9
 import { connect } from '../../../base/redux';
10 10
 import { defaultSharedVideoLink } from '../../constants';
11
-import { getYoutubeLink } from '../../functions';
12 11
 import AbstractSharedVideoDialog from '../AbstractSharedVideoDialog';
13 12
 
14 13
 /**
@@ -48,7 +47,7 @@ class SharedVideoDialog extends AbstractSharedVideoDialog<*> {
48 47
 
49 48
         this.setState({
50 49
             value: linkValue,
51
-            okDisabled: !getYoutubeLink(linkValue)
50
+            okDisabled: !linkValue
52 51
         });
53 52
     }
54 53
 

+ 197
- 0
react/features/shared-video/components/web/VideoManager.js View File

@@ -0,0 +1,197 @@
1
+import Logger from 'jitsi-meet-logger';
2
+import React from 'react';
3
+
4
+import { connect } from '../../../base/redux';
5
+
6
+import AbstractVideoManager, {
7
+    _mapDispatchToProps,
8
+    _mapStateToProps,
9
+    PLAYBACK_STATES,
10
+    Props
11
+} from './AbstractVideoManager';
12
+
13
+const logger = Logger.getLogger(__filename);
14
+
15
+/**
16
+ * Manager of shared video.
17
+ */
18
+class VideoManager extends AbstractVideoManager<Props> {
19
+    /**
20
+     * Initializes a new VideoManager instance.
21
+     *
22
+     * @param {Object} props - This component's props.
23
+     *
24
+     * @returns {void}
25
+     */
26
+    constructor(props) {
27
+        super(props);
28
+
29
+        this.playerRef = React.createRef();
30
+    }
31
+
32
+    /**
33
+     * Retrieves the current player ref.
34
+     */
35
+    get player() {
36
+        return this.playerRef.current;
37
+    }
38
+
39
+    /**
40
+     * Indicates the playback state of the video.
41
+     *
42
+     * @returns {string}
43
+     */
44
+    getPlaybackState() {
45
+        let state;
46
+
47
+        if (!this.player) {
48
+            return;
49
+        }
50
+
51
+        if (this.player.paused) {
52
+            state = PLAYBACK_STATES.PAUSED;
53
+        } else {
54
+            state = PLAYBACK_STATES.PLAYING;
55
+        }
56
+
57
+        return state;
58
+    }
59
+
60
+    /**
61
+     * Indicates whether the video is muted.
62
+     *
63
+     * @returns {boolean}
64
+     */
65
+    isMuted() {
66
+        return this.player?.muted;
67
+    }
68
+
69
+    /**
70
+     * Retrieves current volume.
71
+     *
72
+     * @returns {number}
73
+     */
74
+    getVolume() {
75
+        return this.player?.volume;
76
+    }
77
+
78
+    /**
79
+     * Sets player volume.
80
+     *
81
+     * @param {number} value - The volume.
82
+     *
83
+     * @returns {void}
84
+     */
85
+    setVolume(value) {
86
+        if (this.player) {
87
+            this.player.volume = value;
88
+        }
89
+    }
90
+
91
+    /**
92
+     * Retrieves current time.
93
+     *
94
+     * @returns {number}
95
+     */
96
+    getTime() {
97
+        return this.player?.currentTime;
98
+    }
99
+
100
+    /**
101
+     * Seeks video to provided time.
102
+     *
103
+     * @param {number} time - The time to seek to.
104
+     *
105
+     * @returns {void}
106
+     */
107
+    seek(time) {
108
+        if (this.player) {
109
+            this.player.currentTime = time;
110
+        }
111
+    }
112
+
113
+    /**
114
+     * Plays video.
115
+     *
116
+     * @returns {void}
117
+     */
118
+    play() {
119
+        return this.player?.play();
120
+    }
121
+
122
+    /**
123
+     * Pauses video.
124
+     *
125
+     * @returns {void}
126
+     */
127
+    pause() {
128
+        return this.player?.pause();
129
+    }
130
+
131
+    /**
132
+     * Mutes video.
133
+     *
134
+     * @returns {void}
135
+     */
136
+    mute() {
137
+        if (this.player) {
138
+            this.player.muted = true;
139
+        }
140
+    }
141
+
142
+    /**
143
+     * Unmutes video.
144
+     *
145
+     * @returns {void}
146
+     */
147
+    unMute() {
148
+        if (this.player) {
149
+            this.player.muted = false;
150
+        }
151
+    }
152
+
153
+    /**
154
+     * Retrieves video tag params.
155
+     *
156
+     * @returns {void}
157
+     */
158
+    getPlayerOptions() {
159
+        const { _isOwner, videoId } = this.props;
160
+
161
+        let options = {
162
+            autoPlay: true,
163
+            src: videoId,
164
+            controls: _isOwner,
165
+            onError: event => {
166
+                logger.error('Error in the player:', event);
167
+            },
168
+            onPlay: () => this.onPlay(),
169
+            onVolumeChange: () => this.onVolumeChange()
170
+        };
171
+
172
+        if (_isOwner) {
173
+            options = {
174
+                ...options,
175
+                onPause: () => this.onPause(),
176
+                onTimeUpdate: this.throttledFireUpdateSharedVideoEvent
177
+            };
178
+
179
+        }
180
+
181
+        return options;
182
+    }
183
+
184
+    /**
185
+     * Implements React Component's render.
186
+     *
187
+     * @inheritdoc
188
+     */
189
+    render() {
190
+        return (<video
191
+            id = 'sharedVideoPlayer'
192
+            ref = { this.playerRef }
193
+            { ...this.getPlayerOptions() } />);
194
+    }
195
+}
196
+
197
+export default connect(_mapStateToProps, _mapDispatchToProps)(VideoManager);

+ 251
- 0
react/features/shared-video/components/web/YoutubeVideoManager.js View File

@@ -0,0 +1,251 @@
1
+/* eslint-disable no-invalid-this */
2
+import Logger from 'jitsi-meet-logger';
3
+import React from 'react';
4
+import YouTube from 'react-youtube';
5
+
6
+import { connect } from '../../../base/redux';
7
+
8
+import AbstractVideoManager, {
9
+    _mapDispatchToProps,
10
+    _mapStateToProps,
11
+    PLAYBACK_STATES
12
+} from './AbstractVideoManager';
13
+
14
+const logger = Logger.getLogger(__filename);
15
+
16
+/**
17
+ * Manager of shared video.
18
+ *
19
+ * @returns {void}
20
+ */
21
+class YoutubeVideoManager extends AbstractVideoManager<Props> {
22
+    /**
23
+     * Initializes a new YoutubeVideoManager instance.
24
+     *
25
+     * @param {Object} props - This component's props.
26
+     *
27
+     * @returns {void}
28
+     */
29
+    constructor(props) {
30
+        super(props);
31
+
32
+        this.isPlayerAPILoaded = false;
33
+    }
34
+
35
+    /**
36
+     * Indicates the playback state of the video.
37
+     *
38
+     * @returns {string}
39
+     */
40
+    getPlaybackState() {
41
+        let state;
42
+
43
+        if (!this.player) {
44
+            return;
45
+        }
46
+
47
+        const playerState = this.player.getPlayerState();
48
+
49
+        if (playerState === YouTube.PlayerState.PLAYING) {
50
+            state = PLAYBACK_STATES.PLAYING;
51
+        }
52
+
53
+        if (playerState === YouTube.PlayerState.PAUSED) {
54
+            state = PLAYBACK_STATES.PAUSED;
55
+        }
56
+
57
+        return state;
58
+    }
59
+
60
+    /**
61
+     * Indicates whether the video is muted.
62
+     *
63
+     * @returns {boolean}
64
+     */
65
+    isMuted() {
66
+        return this.player?.isMuted();
67
+    }
68
+
69
+    /**
70
+     * Retrieves current volume.
71
+     *
72
+     * @returns {number}
73
+     */
74
+    getVolume() {
75
+        return this.player?.getVolume();
76
+    }
77
+
78
+    /**
79
+     * Sets player volume.
80
+     *
81
+     * @param {number} value - The volume.
82
+     *
83
+     * @returns {void}
84
+     */
85
+    setVolume(value) {
86
+        return this.player?.setVolume(value);
87
+    }
88
+
89
+    /**
90
+     * Retrieves current time.
91
+     *
92
+     * @returns {number}
93
+     */
94
+    getTime() {
95
+        return this.player?.getCurrentTime();
96
+    }
97
+
98
+    /**
99
+     * Seeks video to provided time.
100
+     *
101
+     * @param {number} time - The time to seek to.
102
+     *
103
+     * @returns {void}
104
+     */
105
+    seek(time) {
106
+        return this.player?.seekTo(time);
107
+    }
108
+
109
+    /**
110
+     * Plays video.
111
+     *
112
+     * @returns {void}
113
+     */
114
+    play() {
115
+        return this.player?.playVideo();
116
+    }
117
+
118
+    /**
119
+     * Pauses video.
120
+     *
121
+     * @returns {void}
122
+     */
123
+    pause() {
124
+        return this.player?.pauseVideo();
125
+    }
126
+
127
+    /**
128
+     * Mutes video.
129
+     *
130
+     * @returns {void}
131
+     */
132
+    mute() {
133
+        return this.player?.mute();
134
+    }
135
+
136
+    /**
137
+     * Unmutes video.
138
+     *
139
+     * @returns {void}
140
+     */
141
+    unMute() {
142
+        return this.player?.unMute();
143
+    }
144
+
145
+    /**
146
+     * Disposes of the current video player.
147
+     *
148
+     * @returns {void}
149
+     */
150
+    dispose() {
151
+        if (this.player) {
152
+            this.player.destroy();
153
+            this.player = null;
154
+        }
155
+
156
+        if (this.errorInPlayer) {
157
+            this.errorInPlayer.destroy();
158
+            this.errorInPlayer = null;
159
+        }
160
+    }
161
+
162
+    /**
163
+     * Fired on play state toggle.
164
+     *
165
+     * @param {Object} event - The yt player stateChange event.
166
+     *
167
+     * @returns {void}
168
+     */
169
+    onPlayerStateChange = event => {
170
+        if (event.data === YouTube.PlayerState.PLAYING) {
171
+            this.onPlay();
172
+        } else if (event.data === YouTube.PlayerState.PAUSED) {
173
+            this.onPause();
174
+        }
175
+    }
176
+
177
+    /**
178
+     * Fired when youtube player is ready.
179
+     *
180
+     * @param {Object} event - The youtube player event.
181
+     *
182
+     * @returns {void}
183
+     */
184
+    onPlayerReady = event => {
185
+        const { _isOwner } = this.props;
186
+
187
+        this.player = event.target;
188
+
189
+        this.player.addEventListener('onVolumeChange', () => {
190
+            this.onVolumeChange();
191
+        });
192
+
193
+        if (_isOwner) {
194
+            this.player.addEventListener('onVideoProgress', this.throttledFireUpdateSharedVideoEvent);
195
+        }
196
+
197
+        this.play();
198
+    };
199
+
200
+    /**
201
+     * Fired when youtube player throws an error.
202
+     *
203
+     * @param {Object} event - Youtube player error event.
204
+     *
205
+     * @returns {void}
206
+     */
207
+    onPlayerError = event => {
208
+        logger.error('Error in the player:', event.data);
209
+
210
+        // store the error player, so we can remove it
211
+        this.errorInPlayer = event.target;
212
+    };
213
+
214
+    getPlayerOptions = () => {
215
+        const { _isOwner, videoId } = this.props;
216
+        const showControls = _isOwner ? 1 : 0;
217
+
218
+        const options = {
219
+            id: 'sharedVideoPlayer',
220
+            opts: {
221
+                height: '100%',
222
+                width: '100%',
223
+                playerVars: {
224
+                    'origin': location.origin,
225
+                    'fs': '0',
226
+                    'autoplay': 0,
227
+                    'controls': showControls,
228
+                    'rel': 0
229
+                }
230
+            },
231
+            onError: this.onPlayerError,
232
+            onReady: this.onPlayerReady,
233
+            onStateChange: this.onPlayerStateChange,
234
+            videoId
235
+        };
236
+
237
+        return options;
238
+    }
239
+
240
+    /**
241
+     * Implements React Component's render.
242
+     *
243
+     * @inheritdoc
244
+     */
245
+    render() {
246
+        return (<YouTube
247
+            { ...this.getPlayerOptions() } />);
248
+    }
249
+}
250
+
251
+export default connect(_mapStateToProps, _mapDispatchToProps)(YoutubeVideoManager);

+ 1
- 1
react/features/shared-video/components/web/index.js View File

@@ -1,5 +1,5 @@
1 1
 // @flow
2 2
 
3
+export { default as SharedVideo } from './SharedVideo';
3 4
 export { default as SharedVideoButton } from './SharedVideoButton';
4 5
 export { default as SharedVideoDialog } from './SharedVideoDialog';
5
-

+ 9
- 2
react/features/shared-video/constants.js View File

@@ -4,13 +4,20 @@
4 4
  * Example shared video link.
5 5
  * @type {string}
6 6
  */
7
-export const defaultSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
7
+export const defaultSharedVideoLink = 'Youtube link or direct video link';
8 8
 
9 9
 /**
10 10
  * Fixed name of the video player fake participant.
11 11
  * @type {string}
12 12
  */
13
-export const VIDEO_PLAYER_PARTICIPANT_NAME = 'YouTube';
13
+export const VIDEO_PLAYER_PARTICIPANT_NAME = 'Video';
14
+
15
+/**
16
+ * Fixed name of the youtube player fake participant.
17
+ * @type {string}
18
+ */
19
+export const YOUTUBE_PLAYER_PARTICIPANT_NAME = 'YouTube';
20
+
14 21
 
15 22
 /**
16 23
  * Shared video command.

+ 9
- 6
react/features/shared-video/functions.js View File

@@ -2,7 +2,7 @@
2 2
 
3 3
 import { getParticipants } from '../base/participants';
4 4
 
5
-import { VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
5
+import { VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME } from './constants';
6 6
 
7 7
 /**
8 8
  * Validates the entered video url.
@@ -10,16 +10,19 @@ import { VIDEO_PLAYER_PARTICIPANT_NAME } from './constants';
10 10
  * It returns a boolean to reflect whether the url matches the youtube regex.
11 11
  *
12 12
  * @param {string} url - The entered video link.
13
- * @returns {boolean}
13
+ * @returns {string} The youtube video id if matched.
14 14
  */
15
-export function getYoutubeLink(url: string) {
15
+export function getYoutubeId(url: string) {
16
+    if (!url) {
17
+        return null;
18
+    }
19
+
16 20
     const p = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|(?:m\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;// eslint-disable-line max-len
17 21
     const result = url.match(p);
18 22
 
19
-    return result ? result[1] : false;
23
+    return result ? result[1] : null;
20 24
 }
21 25
 
22
-
23 26
 /**
24 27
  * Checks if the status is one that is actually sharing the video - playing, pause or start.
25 28
  *
@@ -39,6 +42,6 @@ export function isSharingStatus(status: string) {
39 42
  */
40 43
 export function isVideoPlaying(stateful: Object | Function): boolean {
41 44
     return Boolean(getParticipants(stateful).find(p => p.isFakeParticipant
42
-        && p.name === VIDEO_PLAYER_PARTICIPANT_NAME)
45
+        && (p.name === VIDEO_PLAYER_PARTICIPANT_NAME || p.name === YOUTUBE_PLAYER_PARTICIPANT_NAME))
43 46
     );
44 47
 }

+ 177
- 0
react/features/shared-video/middleware.any.js View File

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

+ 1
- 186
react/features/shared-video/middleware.native.js View File

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

+ 2
- 27
react/features/shared-video/middleware.web.js View File

@@ -1,37 +1,13 @@
1 1
 // @flow
2 2
 
3
-import UIEvents from '../../../service/UI/UIEvents';
4 3
 import { getCurrentConference } from '../base/conference';
5 4
 import { getLocalParticipant } from '../base/participants';
6
-import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
5
+import { StateListenerRegistry } from '../base/redux';
7 6
 
8
-import { TOGGLE_SHARED_VIDEO } from './actionTypes';
9 7
 import { setDisableButton } from './actions.web';
10 8
 import { SHARED_VIDEO } from './constants';
11 9
 
12
-declare var APP: Object;
13
-
14
-/**
15
- * Middleware that captures actions related to video sharing and updates
16
- * components not hooked into redux.
17
- *
18
- * @param {Store} store - The redux store.
19
- * @returns {Function}
20
- */
21
-// eslint-disable-next-line no-unused-vars
22
-MiddlewareRegistry.register(store => next => action => {
23
-    if (typeof APP === 'undefined') {
24
-        return next(action);
25
-    }
26
-
27
-    switch (action.type) {
28
-    case TOGGLE_SHARED_VIDEO:
29
-        APP.UI.emitEvent(UIEvents.SHARED_VIDEO_CLICKED);
30
-        break;
31
-    }
32
-
33
-    return next(action);
34
-});
10
+import './middleware.any';
35 11
 
36 12
 /**
37 13
  * Set up state change listener to disable or enable the share video button in
@@ -43,7 +19,6 @@ StateListenerRegistry.register(
43 19
         if (conference && conference !== previousConference) {
44 20
             conference.addCommandListener(SHARED_VIDEO,
45 21
                 ({ attributes }) => {
46
-
47 22
                     const { dispatch, getState } = store;
48 23
                     const { from } = attributes;
49 24
                     const localParticipantId = getLocalParticipant(getState()).id;

+ 11
- 5
react/features/shared-video/reducer.native.js View File

@@ -2,22 +2,28 @@
2 2
 
3 3
 import { ReducerRegistry } from '../base/redux';
4 4
 
5
-import { SET_SHARED_VIDEO_STATUS } from './actionTypes';
5
+import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes';
6
+
7
+const initialState = {};
6 8
 
7 9
 /**
8 10
  * Reduces the Redux actions of the feature features/shared-video.
9 11
  */
10
-ReducerRegistry.register('features/shared-video', (state = {}, action) => {
11
-    const { videoId, status, time, ownerId } = action;
12
+ReducerRegistry.register('features/shared-video', (state = initialState, action) => {
13
+    const { videoUrl, status, time, ownerId, muted, volume } = action;
12 14
 
13 15
     switch (action.type) {
16
+    case RESET_SHARED_VIDEO_STATUS:
17
+        return initialState;
14 18
     case SET_SHARED_VIDEO_STATUS:
15 19
         return {
16 20
             ...state,
17
-            videoId,
21
+            muted,
22
+            ownerId,
18 23
             status,
19 24
             time,
20
-            ownerId
25
+            videoUrl,
26
+            volume
21 27
         };
22 28
     default:
23 29
         return state;

+ 12
- 4
react/features/shared-video/reducer.web.js View File

@@ -2,19 +2,27 @@
2 2
 
3 3
 import { ReducerRegistry } from '../base/redux';
4 4
 
5
-import { SET_SHARED_VIDEO_STATUS, SET_DISABLE_BUTTON } from './actionTypes';
5
+import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS, SET_DISABLE_BUTTON } from './actionTypes';
6
+
7
+const initialState = {};
6 8
 
7 9
 /**
8 10
  * Reduces the Redux actions of the feature features/shared-video.
9 11
  */
10
-ReducerRegistry.register('features/shared-video', (state = {}, action) => {
11
-    const { status, disabled } = action;
12
+ReducerRegistry.register('features/shared-video', (state = initialState, action) => {
13
+    const { videoUrl, status, time, ownerId, disabled, muted } = action;
12 14
 
13 15
     switch (action.type) {
16
+    case RESET_SHARED_VIDEO_STATUS:
17
+        return initialState;
14 18
     case SET_SHARED_VIDEO_STATUS:
15 19
         return {
16 20
             ...state,
17
-            status
21
+            muted,
22
+            ownerId,
23
+            status,
24
+            time,
25
+            videoUrl
18 26
         };
19 27
 
20 28
     case SET_DISABLE_BUTTON:

+ 2
- 3
react/features/video-menu/actions.any.js View File

@@ -45,10 +45,9 @@ export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) {
45 45
         dispatch(isAudio ? setAudioMuted(enable, /* ensureTrack */ true)
46 46
             : setVideoMuted(enable, mediaType, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
47 47
 
48
-        // FIXME: The old conference logic as well as the shared video feature
49
-        // still rely on this event being emitted.
48
+        // FIXME: The old conference logic still relies on this event being emitted.
50 49
         typeof APP === 'undefined'
51
-            || APP.UI.emitEvent(isAudio ? UIEvents.AUDIO_MUTED : UIEvents.VIDEO_MUTED, enable, true);
50
+            || APP.UI.emitEvent(isAudio ? UIEvents.AUDIO_MUTED : UIEvents.VIDEO_MUTED, enable);
52 51
     };
53 52
 }
54 53
 

+ 0
- 2
service/UI/UIEvents.js View File

@@ -12,14 +12,12 @@ export default {
12 12
     AUDIO_MUTED: 'UI.audio_muted',
13 13
     VIDEO_MUTED: 'UI.video_muted',
14 14
     ETHERPAD_CLICKED: 'UI.etherpad_clicked',
15
-    SHARED_VIDEO_CLICKED: 'UI.start_shared_video',
16 15
 
17 16
     /**
18 17
      * Updates shared video with params: url, state, time(optional)
19 18
      * Where url is the video link, state is stop/start/pause and time is the
20 19
      * current video playing time.
21 20
      */
22
-    UPDATE_SHARED_VIDEO: 'UI.update_shared_video',
23 21
     TOGGLE_FULLSCREEN: 'UI.toogle_fullscreen',
24 22
     FULLSCREEN_TOGGLED: 'UI.fullscreen_toggled',
25 23
     AUTH_CLICKED: 'UI.auth_clicked',

Loading…
Cancel
Save