瀏覽代碼

ref(Filmstrip): Use Thumbnail component.

j8
Hristo Terezov 4 年之前
父節點
當前提交
f50872285d
共有 96 個檔案被更改,包括 1311 行新增1859 行删除
  1. 0
    1
      app.js
  2. 5
    4
      conference.js
  3. 2
    2
      css/_popup_menu.scss
  4. 3
    0
      css/_videolayout_default.scss
  5. 2
    2
      css/filmstrip/_small_video.scss
  6. 1
    0
      css/filmstrip/_tile_view_overrides.scss
  7. 4
    0
      css/filmstrip/_vertical_filmstrip_overrides.scss
  8. 1
    1
      modules/API/API.js
  9. 3
    28
      modules/UI/UI.js
  10. 0
    67
      modules/UI/shared_video/SharedVideoThumb.js
  11. 0
    123
      modules/UI/videolayout/Filmstrip.js
  12. 2
    6
      modules/UI/videolayout/LargeVideoManager.js
  13. 0
    213
      modules/UI/videolayout/LocalVideo.js
  14. 0
    242
      modules/UI/videolayout/RemoteVideo.js
  15. 0
    509
      modules/UI/videolayout/SmallVideo.js
  16. 2
    7
      modules/UI/videolayout/VideoContainer.js
  17. 19
    353
      modules/UI/videolayout/VideoLayout.js
  18. 2
    1
      modules/keyboardshortcut/keyboardshortcut.js
  19. 0
    5
      package-lock.json
  20. 0
    1
      package.json
  21. 119
    6
      react/features/base/media/components/web/Video.js
  22. 115
    5
      react/features/base/media/components/web/VideoTrack.js
  23. 1
    0
      react/features/base/settings/middleware.js
  24. 10
    0
      react/features/base/tracks/actionTypes.js
  25. 11
    3
      react/features/base/tracks/actions.js
  26. 0
    1
      react/features/base/tracks/middleware.js
  27. 15
    1
      react/features/connection-stats/components/ConnectionStatsTable.js
  28. 17
    0
      react/features/filmstrip/actions.web.js
  29. 2
    2
      react/features/filmstrip/components/native/Thumbnail.js
  30. 36
    10
      react/features/filmstrip/components/web/Filmstrip.js
  31. 474
    79
      react/features/filmstrip/components/web/Thumbnail.js
  32. 89
    0
      react/features/filmstrip/constants.js
  33. 43
    1
      react/features/filmstrip/functions.web.js
  34. 5
    0
      react/features/filmstrip/logger.js
  35. 7
    24
      react/features/filmstrip/middleware.web.js
  36. 0
    25
      react/features/filmstrip/subscriber.web.js
  37. 1
    1
      react/features/mobile/external-api/middleware.js
  38. 0
    44
      react/features/remote-video-menu/components/web/RemoteVideoMenu.js
  39. 1
    1
      react/features/toolbox/components/AudioMuteButton.js
  40. 1
    1
      react/features/toolbox/components/MuteEveryoneButton.js
  41. 1
    1
      react/features/toolbox/components/MuteEveryonesVideoButton.js
  42. 16
    23
      react/features/video-layout/middleware.web.js
  43. 0
    12
      react/features/video-menu/actions.any.js
  44. 15
    0
      react/features/video-menu/actions.native.js
  45. 2
    0
      react/features/video-menu/actions.web.js
  46. 0
    0
      react/features/video-menu/components/AbstractGrantModeratorButton.js
  47. 0
    0
      react/features/video-menu/components/AbstractGrantModeratorDialog.js
  48. 0
    0
      react/features/video-menu/components/AbstractKickButton.js
  49. 0
    0
      react/features/video-menu/components/AbstractKickRemoteParticipantDialog.js
  50. 0
    0
      react/features/video-menu/components/AbstractMuteButton.js
  51. 0
    0
      react/features/video-menu/components/AbstractMuteEveryoneDialog.js
  52. 0
    0
      react/features/video-menu/components/AbstractMuteEveryoneElseButton.js
  53. 0
    0
      react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js
  54. 0
    0
      react/features/video-menu/components/AbstractMuteEveryonesVideoDialog.js
  55. 0
    0
      react/features/video-menu/components/AbstractMuteRemoteParticipantDialog.js
  56. 0
    0
      react/features/video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js
  57. 0
    0
      react/features/video-menu/components/AbstractMuteVideoButton.js
  58. 0
    0
      react/features/video-menu/components/index.native.js
  59. 0
    0
      react/features/video-menu/components/index.web.js
  60. 0
    0
      react/features/video-menu/components/native/ConnectionStatusButton.js
  61. 0
    0
      react/features/video-menu/components/native/ConnectionStatusComponent.js
  62. 0
    0
      react/features/video-menu/components/native/GrantModeratorButton.js
  63. 0
    0
      react/features/video-menu/components/native/GrantModeratorDialog.js
  64. 0
    0
      react/features/video-menu/components/native/KickButton.js
  65. 0
    0
      react/features/video-menu/components/native/KickRemoteParticipantDialog.js
  66. 0
    0
      react/features/video-menu/components/native/MuteButton.js
  67. 0
    0
      react/features/video-menu/components/native/MuteEveryoneDialog.js
  68. 0
    0
      react/features/video-menu/components/native/MuteEveryoneElseButton.js
  69. 0
    0
      react/features/video-menu/components/native/MuteRemoteParticipantDialog.js
  70. 0
    0
      react/features/video-menu/components/native/PinButton.js
  71. 1
    1
      react/features/video-menu/components/native/RemoteVideoMenu.js
  72. 0
    0
      react/features/video-menu/components/native/index.js
  73. 0
    0
      react/features/video-menu/components/native/styles.js
  74. 103
    0
      react/features/video-menu/components/web/FlipLocalVideoButton.js
  75. 2
    2
      react/features/video-menu/components/web/GrantModeratorButton.js
  76. 0
    0
      react/features/video-menu/components/web/GrantModeratorDialog.js
  77. 2
    2
      react/features/video-menu/components/web/KickButton.js
  78. 0
    0
      react/features/video-menu/components/web/KickRemoteParticipantDialog.js
  79. 100
    0
      react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js
  80. 2
    2
      react/features/video-menu/components/web/MuteButton.js
  81. 0
    0
      react/features/video-menu/components/web/MuteEveryoneDialog.js
  82. 2
    2
      react/features/video-menu/components/web/MuteEveryoneElseButton.js
  83. 2
    2
      react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js
  84. 0
    0
      react/features/video-menu/components/web/MuteEveryonesVideoDialog.js
  85. 0
    0
      react/features/video-menu/components/web/MuteRemoteParticipantDialog.js
  86. 0
    0
      react/features/video-menu/components/web/MuteRemoteParticipantsVideoDialog.js
  87. 2
    2
      react/features/video-menu/components/web/MuteVideoButton.js
  88. 2
    2
      react/features/video-menu/components/web/PrivateMessageMenuButton.js
  89. 2
    2
      react/features/video-menu/components/web/RemoteControlButton.js
  90. 5
    15
      react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js
  91. 51
    0
      react/features/video-menu/components/web/VideoMenu.js
  92. 5
    5
      react/features/video-menu/components/web/VideoMenuButton.js
  93. 0
    0
      react/features/video-menu/components/web/VolumeSlider.js
  94. 2
    1
      react/features/video-menu/components/web/index.js
  95. 0
    0
      react/features/video-menu/index.js
  96. 1
    16
      service/UI/UIEvents.js

+ 0
- 1
app.js 查看文件

@@ -1,7 +1,6 @@
1 1
 /* application specific logic */
2 2
 
3 3
 import 'jquery';
4
-import 'jquery-contextmenu';
5 4
 import 'jQuery-Impromptu';
6 5
 
7 6
 import 'olm';

+ 5
- 4
conference.js 查看文件

@@ -1399,9 +1399,6 @@ export default {
1399 1399
                     .then(() => {
1400 1400
                         this.localVideo = newTrack;
1401 1401
                         this._setSharingScreen(newTrack);
1402
-                        if (newTrack) {
1403
-                            APP.UI.addLocalVideoStream(newTrack);
1404
-                        }
1405 1402
                         this.setVideoMuteStatus(this.isLocalVideoMuted());
1406 1403
                     })
1407 1404
                     .then(resolve)
@@ -2408,7 +2405,11 @@ export default {
2408 2405
             // There is no guarantee another event will trigger the update
2409 2406
             // immediately and in all situations, for example because a remote
2410 2407
             // participant is having connection trouble so no status changes.
2411
-            APP.UI.updateAllVideos();
2408
+            const displayedUserId = APP.UI.getLargeVideoID();
2409
+
2410
+            if (displayedUserId) {
2411
+                APP.UI.updateLargeVideo(displayedUserId, true);
2412
+            }
2412 2413
         });
2413 2414
 
2414 2415
         APP.UI.addListener(

+ 2
- 2
css/_popup_menu.scss 查看文件

@@ -3,7 +3,7 @@
3 3
 **/
4 4
 
5 5
 .popupmenu {
6
-    min-width: 75px;
6
+    min-width: 150px;
7 7
     text-align: left;
8 8
     padding: 0px;
9 9
     white-space: nowrap;
@@ -109,6 +109,6 @@ ul.popupmenu {
109 109
     margin: -16px -24px;
110 110
 }
111 111
 
112
-span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
112
+span.localvideomenu:hover ul.popupmenu, span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
113 113
     display:block !important;
114 114
 }

+ 3
- 0
css/_videolayout_default.scss 查看文件

@@ -400,7 +400,9 @@
400 400
     }
401 401
 }
402 402
 
403
+.local-video-menu-trigger,
403 404
 .remote-video-menu-trigger,
405
+.localvideomenu,
404 406
 .remotevideomenu
405 407
 {
406 408
     display: inline-block;
@@ -418,6 +420,7 @@
418 420
         cursor: hand;
419 421
     }
420 422
 }
423
+.local-video-menu-trigger,
421 424
 .remote-video-menu-trigger {
422 425
     margin-top: 7px;
423 426
 }

+ 2
- 2
css/filmstrip/_small_video.scss 查看文件

@@ -19,7 +19,7 @@
19 19
         0 0 3px $videoThumbnailSelected;
20 20
     }
21 21
 
22
-    .remotevideomenu > .icon-menu {
22
+    .remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
23 23
         display: none;
24 24
     }
25 25
 
@@ -32,7 +32,7 @@
32 32
         box-shadow: inset 0 0 3px $videoThumbnailHovered,
33 33
         0 0 3px $videoThumbnailHovered;
34 34
 
35
-        .remotevideomenu > .icon-menu {
35
+        .remotevideomenu > .icon-menu, .localvideomenu > .icon-menu {
36 36
             display: inline-block;
37 37
         }
38 38
     }

+ 1
- 0
css/filmstrip/_tile_view_overrides.scss 查看文件

@@ -43,6 +43,7 @@
43 43
      * specifically the various status icons.
44 44
      */
45 45
     .remotevideomenu,
46
+    .localvideomenu,
46 47
     .videocontainer__toptoolbar {
47 48
         z-index: auto;
48 49
     }

+ 4
- 0
css/filmstrip/_vertical_filmstrip_overrides.scss 查看文件

@@ -51,6 +51,7 @@
51 51
      * and tooltips from getting a new location context due to translate3d.
52 52
      */
53 53
     .connection-indicator,
54
+    .local-video-menu-trigger,
54 55
     .remote-video-menu-trigger,
55 56
     .indicator-icon-container {
56 57
         transform: translate3d(0, 0, 0);
@@ -68,7 +69,9 @@
68 69
      * Move the remote video menu trigger to the bottom left of the video
69 70
      * thumbnail.
70 71
      */
72
+    .localvideomenu,
71 73
     .remotevideomenu,
74
+    .local-video-menu-trigger,
72 75
     .remote-video-menu-trigger {
73 76
         bottom: 0;
74 77
         left: 0;
@@ -76,6 +79,7 @@
76 79
         right: auto;
77 80
     }
78 81
 
82
+    .local-video-menu-trigger,
79 83
     .remote-video-menu-trigger {
80 84
         margin-bottom: 7px;
81 85
         margin-left: $remoteVideoMenuIconMargin;

+ 1
- 1
modules/API/API.js 查看文件

@@ -33,8 +33,8 @@ import {
33 33
 import { toggleLobbyMode } from '../../react/features/lobby/actions.web';
34 34
 import { RECORDING_TYPES } from '../../react/features/recording/constants';
35 35
 import { getActiveSession } from '../../react/features/recording/functions';
36
-import { muteAllParticipants } from '../../react/features/remote-video-menu/actions';
37 36
 import { toggleTileView } from '../../react/features/video-layout';
37
+import { muteAllParticipants } from '../../react/features/video-menu/actions';
38 38
 import { setVideoQuality } from '../../react/features/video-quality';
39 39
 import { getJitsiMeetTransport } from '../transport';
40 40
 

+ 3
- 28
modules/UI/UI.js 查看文件

@@ -115,7 +115,6 @@ UI.start = function() {
115 115
     // Set the defaults for prompt dialogs.
116 116
     $.prompt.setDefaults({ persistent: false });
117 117
 
118
-    VideoLayout.init(eventEmitter);
119 118
     VideoLayout.initLargeVideo();
120 119
 
121 120
     // Do not animate the video area on UI start (second argument passed into
@@ -135,7 +134,6 @@ UI.start = function() {
135 134
     if (config.iAmRecorder) {
136 135
         // in case of iAmSipGateway keep local video visible
137 136
         if (!config.iAmSipGateway) {
138
-            VideoLayout.setLocalVideoVisible(false);
139 137
             APP.store.dispatch(setNotificationsEnabled(false));
140 138
         }
141 139
 
@@ -179,14 +177,6 @@ UI.unbindEvents = () => {
179 177
     $(window).off('resize');
180 178
 };
181 179
 
182
-/**
183
- * Show local video stream on UI.
184
- * @param {JitsiTrack} track stream to show
185
- */
186
-UI.addLocalVideoStream = track => {
187
-    VideoLayout.changeLocalVideo(track);
188
-};
189
-
190 180
 /**
191 181
  * Setup and show Etherpad.
192 182
  * @param {string} name etherpad id
@@ -227,14 +217,6 @@ UI.addUser = function(user) {
227 217
     }
228 218
 };
229 219
 
230
-/**
231
- * Update videotype for specified user.
232
- * @param {string} id user id
233
- * @param {string} newVideoType new videotype
234
- */
235
-UI.onPeerVideoTypeChanged
236
-    = (id, newVideoType) => VideoLayout.onVideoTypeChanged(id, newVideoType);
237
-
238 220
 /**
239 221
  * Updates the user status.
240 222
  *
@@ -289,19 +271,14 @@ UI.setAudioMuted = function(id) {
289 271
  * Sets muted video state for participant
290 272
  */
291 273
 UI.setVideoMuted = function(id) {
292
-    VideoLayout.onVideoMute(id);
274
+    VideoLayout._updateLargeVideoIfDisplayed(id, true);
275
+
293 276
     if (APP.conference.isLocalId(id)) {
294 277
         APP.conference.updateVideoIconEnabled();
295 278
     }
296 279
 };
297 280
 
298
-/**
299
- * Triggers an update of remote video and large video displays so they may pick
300
- * up any state changes that have occurred elsewhere.
301
- *
302
- * @returns {void}
303
- */
304
-UI.updateAllVideos = () => VideoLayout.updateAllVideos();
281
+UI.updateLargeVideo = (id, forceUpdate) => VideoLayout.updateLargeVideo(id, forceUpdate);
305 282
 
306 283
 /**
307 284
  * Adds a listener that would be notified on the given type of event.
@@ -340,8 +317,6 @@ UI.removeListener = function(type, listener) {
340 317
  */
341 318
 UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
342 319
 
343
-UI.clickOnVideo = videoNumber => VideoLayout.togglePin(videoNumber);
344
-
345 320
 // Used by torture.
346 321
 UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
347 322
 

+ 0
- 67
modules/UI/shared_video/SharedVideoThumb.js 查看文件

@@ -1,67 +0,0 @@
1
-/* global $, APP */
2
-
3
-/* eslint-disable no-unused-vars */
4
-import React, { Component } from 'react';
5
-import ReactDOM from 'react-dom';
6
-import { I18nextProvider } from 'react-i18next';
7
-import { Provider } from 'react-redux';
8
-
9
-import { i18next } from '../../../react/features/base/i18n';
10
-import { Thumbnail } from '../../../react/features/filmstrip';
11
-import SmallVideo from '../videolayout/SmallVideo';
12
-/* eslint-enable no-unused-vars */
13
-
14
-/**
15
- *
16
- */
17
-export default class SharedVideoThumb extends SmallVideo {
18
-    /**
19
-     *
20
-     * @param {*} participant
21
-     */
22
-    constructor(participant) {
23
-        super();
24
-        this.id = participant.id;
25
-        this.isLocal = false;
26
-        this.url = participant.id;
27
-        this.videoSpanId = 'sharedVideoContainer';
28
-        this.container = this.createContainer(this.videoSpanId);
29
-        this.$container = $(this.container);
30
-        this.renderThumbnail();
31
-        this._setThumbnailSize();
32
-        this.bindHoverHandler();
33
-        this.container.onclick = this._onContainerClick;
34
-    }
35
-
36
-    /**
37
-     *
38
-     * @param {*} spanId
39
-     */
40
-    createContainer(spanId) {
41
-        const container = document.createElement('span');
42
-
43
-        container.id = spanId;
44
-        container.className = 'videocontainer';
45
-
46
-        const remoteVideosContainer
47
-            = document.getElementById('filmstripRemoteVideosContainer');
48
-        const localVideoContainer
49
-            = document.getElementById('localVideoTileViewContainer');
50
-
51
-        remoteVideosContainer.insertBefore(container, localVideoContainer);
52
-
53
-        return container;
54
-    }
55
-
56
-    /**
57
-     * Renders the thumbnail.
58
-     */
59
-    renderThumbnail(isHovered = false) {
60
-        ReactDOM.render(
61
-            <Provider store = { APP.store }>
62
-                <I18nextProvider i18n = { i18next }>
63
-                    <Thumbnail participantID = { this.id } isHovered = { isHovered } />
64
-                </I18nextProvider>
65
-            </Provider>, this.container);
66
-    }
67
-}

+ 0
- 123
modules/UI/videolayout/Filmstrip.js 查看文件

@@ -25,129 +25,6 @@ const Filmstrip = {
25 25
      */
26 26
     getVerticalFilmstripWidth() {
27 27
         return isFilmstripVisible(APP.store) ? getVerticalFilmstripVisibleAreaWidth() : 0;
28
-    },
29
-
30
-    /**
31
-     * Resizes thumbnails for tile view.
32
-     *
33
-     * @param {number} width - The new width of the thumbnails.
34
-     * @param {number} height - The new height of the thumbnails.
35
-     * @param {boolean} forceUpdate
36
-     * @returns {void}
37
-     */
38
-    resizeThumbnailsForTileView(width, height, forceUpdate = false) {
39
-        const thumbs = this._getThumbs(!forceUpdate);
40
-
41
-        if (thumbs.localThumb) {
42
-            thumbs.localThumb.css({
43
-                'padding-top': '',
44
-                height: `${height}px`,
45
-                'min-height': `${height}px`,
46
-                'min-width': `${width}px`,
47
-                width: `${width}px`
48
-            });
49
-        }
50
-
51
-        if (thumbs.remoteThumbs) {
52
-            thumbs.remoteThumbs.css({
53
-                'padding-top': '',
54
-                height: `${height}px`,
55
-                'min-height': `${height}px`,
56
-                'min-width': `${width}px`,
57
-                width: `${width}px`
58
-            });
59
-        }
60
-    },
61
-
62
-    /**
63
-     * Resizes thumbnails for horizontal view.
64
-     *
65
-     * @param {Object} dimensions - The new dimensions of the thumbnails.
66
-     * @param {boolean} forceUpdate
67
-     * @returns {void}
68
-     */
69
-    resizeThumbnailsForHorizontalView({ local = {}, remote = {} }, forceUpdate = false) {
70
-        const thumbs = this._getThumbs(!forceUpdate);
71
-
72
-        if (thumbs.localThumb) {
73
-            const { height, width } = local;
74
-
75
-            thumbs.localThumb.css({
76
-                height: `${height}px`,
77
-                'min-height': `${height}px`,
78
-                'min-width': `${width}px`,
79
-                width: `${width}px`
80
-            });
81
-        }
82
-
83
-        if (thumbs.remoteThumbs) {
84
-            const { height, width } = remote;
85
-
86
-            thumbs.remoteThumbs.css({
87
-                height: `${height}px`,
88
-                'min-height': `${height}px`,
89
-                'min-width': `${width}px`,
90
-                width: `${width}px`
91
-            });
92
-        }
93
-    },
94
-
95
-    /**
96
-     * Resizes thumbnails for vertical view.
97
-     *
98
-     * @returns {void}
99
-     */
100
-    resizeThumbnailsForVerticalView() {
101
-        const thumbs = this._getThumbs(true);
102
-
103
-        if (thumbs.localThumb) {
104
-            const heightToWidthPercent = 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
105
-
106
-            thumbs.localThumb.css({
107
-                'padding-top': `${heightToWidthPercent}%`,
108
-                width: '',
109
-                height: '',
110
-                'min-width': '',
111
-                'min-height': ''
112
-            });
113
-        }
114
-
115
-        if (thumbs.remoteThumbs) {
116
-            const heightToWidthPercent = 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO;
117
-
118
-            thumbs.remoteThumbs.css({
119
-                'padding-top': `${heightToWidthPercent}%`,
120
-                width: '',
121
-                height: '',
122
-                'min-width': '',
123
-                'min-height': ''
124
-            });
125
-        }
126
-    },
127
-
128
-    /**
129
-     * Returns thumbnails of the filmstrip
130
-     * @param onlyVisible
131
-     * @returns {object} thumbnails
132
-     */
133
-    _getThumbs(onlyVisible = false) {
134
-        let selector = 'span';
135
-
136
-        if (onlyVisible) {
137
-            selector += ':visible';
138
-        }
139
-
140
-        const localThumb = $('#localVideoContainer');
141
-        const remoteThumbs = $('#filmstripRemoteVideosContainer').children(selector);
142
-
143
-        // Exclude the local video container if it has been hidden.
144
-        if (localThumb.hasClass('hidden')) {
145
-            return { remoteThumbs };
146
-        }
147
-
148
-        return { remoteThumbs,
149
-            localThumb };
150
-
151 28
     }
152 29
 };
153 30
 

+ 2
- 6
modules/UI/videolayout/LargeVideoManager.js 查看文件

@@ -22,7 +22,6 @@ import {
22 22
 import { PresenceLabel } from '../../../react/features/presence-status';
23 23
 import { shouldDisplayTileView } from '../../../react/features/video-layout';
24 24
 /* eslint-enable no-unused-vars */
25
-import UIEvents from '../../../service/UI/UIEvents';
26 25
 import { createDeferred } from '../../util/helpers';
27 26
 import AudioLevels from '../audio_levels/AudioLevels';
28 27
 
@@ -51,21 +50,19 @@ export default class LargeVideoManager {
51 50
     /**
52 51
      *
53 52
      */
54
-    constructor(emitter) {
53
+    constructor() {
55 54
         /**
56 55
          * The map of <tt>LargeContainer</tt>s where the key is the video
57 56
          * container type.
58 57
          * @type {Object.<string, LargeContainer>}
59 58
          */
60 59
         this.containers = {};
61
-        this.eventEmitter = emitter;
62 60
 
63 61
         this.state = VIDEO_CONTAINER_TYPE;
64 62
 
65 63
         // FIXME: We are passing resizeContainer as parameter which is calling
66 64
         // Container.resize. Probably there's better way to implement this.
67
-        this.videoContainer = new VideoContainer(
68
-            () => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
65
+        this.videoContainer = new VideoContainer(() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
69 66
         this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
70 67
 
71 68
         // use the same video container to handle desktop tracks
@@ -300,7 +297,6 @@ export default class LargeVideoManager {
300 297
             // after everything is done check again if there are any pending
301 298
             // new streams.
302 299
             this.updateInProcess = false;
303
-            this.eventEmitter.emit(UIEvents.LARGE_VIDEO_ID_CHANGED, this.id);
304 300
             this.scheduleLargeVideoUpdate();
305 301
         });
306 302
     }

+ 0
- 213
modules/UI/videolayout/LocalVideo.js 查看文件

@@ -1,213 +0,0 @@
1
-/* global $, config, APP */
2
-
3
-/* eslint-disable no-unused-vars */
4
-import React, { Component } from 'react';
5
-import ReactDOM from 'react-dom';
6
-import { I18nextProvider } from 'react-i18next';
7
-import { Provider } from 'react-redux';
8
-
9
-import { i18next } from '../../../react/features/base/i18n';
10
-import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
11
-import { VideoTrack } from '../../../react/features/base/media';
12
-import { updateSettings } from '../../../react/features/base/settings';
13
-import { getLocalVideoTrack } from '../../../react/features/base/tracks';
14
-import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
15
-import { shouldDisplayTileView } from '../../../react/features/video-layout';
16
-/* eslint-enable no-unused-vars */
17
-import UIEvents from '../../../service/UI/UIEvents';
18
-
19
-import SmallVideo from './SmallVideo';
20
-
21
-/**
22
- *
23
- */
24
-export default class LocalVideo extends SmallVideo {
25
-    /**
26
-     *
27
-     * @param {*} emitter
28
-     * @param {*} streamEndedCallback
29
-     */
30
-    constructor(emitter, streamEndedCallback) {
31
-        super();
32
-        this.videoSpanId = 'localVideoContainer';
33
-        this.streamEndedCallback = streamEndedCallback;
34
-        this.container = this.createContainer();
35
-        this.$container = $(this.container);
36
-        this.isLocal = true;
37
-        this._setThumbnailSize();
38
-        this.updateDOMLocation();
39
-        this.renderThumbnail();
40
-
41
-        this.localVideoId = null;
42
-        this.bindHoverHandler();
43
-        if (!config.disableLocalVideoFlip) {
44
-            this._buildContextMenu();
45
-        }
46
-        this.emitter = emitter;
47
-
48
-        Object.defineProperty(this, 'id', {
49
-            get() {
50
-                return APP.conference.getMyUserId();
51
-            }
52
-        });
53
-        this.initBrowserSpecificProperties();
54
-
55
-        this.container.onclick = this._onContainerClick;
56
-    }
57
-
58
-    /**
59
-     *
60
-     */
61
-    createContainer() {
62
-        const containerSpan = document.createElement('span');
63
-
64
-        containerSpan.classList.add('videocontainer');
65
-        containerSpan.id = this.videoSpanId;
66
-
67
-        return containerSpan;
68
-    }
69
-
70
-    /**
71
-     * Renders the thumbnail.
72
-     */
73
-    renderThumbnail(isHovered = false) {
74
-        ReactDOM.render(
75
-            <Provider store = { APP.store }>
76
-                <I18nextProvider i18n = { i18next }>
77
-                    <Thumbnail participantID = { this.id } isHovered = { isHovered } />
78
-                </I18nextProvider>
79
-            </Provider>, this.container);
80
-    }
81
-
82
-    /**
83
-     *
84
-     * @param {*} stream
85
-     */
86
-    changeVideo(stream) {
87
-        this.localVideoId = `localVideo_${stream.getId()}`;
88
-
89
-        // eslint-disable-next-line eqeqeq
90
-        const isVideo = stream.videoType != 'desktop';
91
-        const settings = APP.store.getState()['features/base/settings'];
92
-
93
-        this._enableDisableContextMenu(isVideo);
94
-        this.setFlipX(isVideo ? settings.localFlipX : false);
95
-
96
-        const endedHandler = () => {
97
-            this._notifyOfStreamEnded();
98
-            stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
99
-        };
100
-
101
-        stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
102
-    }
103
-
104
-    /**
105
-     * Notify any subscribers of the local video stream ending.
106
-     *
107
-     * @private
108
-     * @returns {void}
109
-     */
110
-    _notifyOfStreamEnded() {
111
-        if (this.streamEndedCallback) {
112
-            this.streamEndedCallback(this.id);
113
-        }
114
-    }
115
-
116
-    /**
117
-     * Shows or hides the local video container.
118
-     * @param {boolean} true to make the local video container visible, false
119
-     * otherwise
120
-     */
121
-    setVisible(visible) {
122
-        // We toggle the hidden class as an indication to other interested parties
123
-        // that this container has been hidden on purpose.
124
-        this.$container.toggleClass('hidden');
125
-
126
-        // We still show/hide it as we need to overwrite the style property if we
127
-        // want our action to take effect. Toggling the display property through
128
-        // the above css class didn't succeed in overwriting the style.
129
-        if (visible) {
130
-            this.$container.show();
131
-        } else {
132
-            this.$container.hide();
133
-        }
134
-    }
135
-
136
-    /**
137
-     * Sets the flipX state of the video.
138
-     * @param val {boolean} true for flipped otherwise false;
139
-     */
140
-    setFlipX(val) {
141
-        this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
142
-        if (!this.localVideoId) {
143
-            return;
144
-        }
145
-        if (val) {
146
-            this.selectVideoElement().addClass('flipVideoX');
147
-        } else {
148
-            this.selectVideoElement().removeClass('flipVideoX');
149
-        }
150
-    }
151
-
152
-    /**
153
-     * Builds the context menu for the local video.
154
-     */
155
-    _buildContextMenu() {
156
-        $.contextMenu({
157
-            selector: `#${this.videoSpanId}`,
158
-            zIndex: 10000,
159
-            items: {
160
-                flip: {
161
-                    name: 'Flip',
162
-                    callback: () => {
163
-                        const { store } = APP;
164
-                        const val = !store.getState()['features/base/settings']
165
-                        .localFlipX;
166
-
167
-                        this.setFlipX(val);
168
-                        store.dispatch(updateSettings({
169
-                            localFlipX: val
170
-                        }));
171
-                    }
172
-                }
173
-            },
174
-            events: {
175
-                show(options) {
176
-                    options.items.flip.name
177
-                        = APP.translation.generateTranslationHTML(
178
-                            'videothumbnail.flip');
179
-                }
180
-            }
181
-        });
182
-    }
183
-
184
-    /**
185
-     * Enables or disables the context menu for the local video.
186
-     * @param enable {boolean} true for enable, false for disable
187
-     */
188
-    _enableDisableContextMenu(enable) {
189
-        if (this.$container.contextMenu) {
190
-            this.$container.contextMenu(enable);
191
-        }
192
-    }
193
-
194
-    /**
195
-     * Places the {@code LocalVideo} in the DOM based on the current video layout.
196
-     *
197
-     * @returns {void}
198
-     */
199
-    updateDOMLocation() {
200
-        if (!this.container) {
201
-            return;
202
-        }
203
-        if (this.container.parentElement) {
204
-            this.container.parentElement.removeChild(this.container);
205
-        }
206
-
207
-        const appendTarget = shouldDisplayTileView(APP.store.getState())
208
-            ? document.getElementById('localVideoTileViewContainer')
209
-            : document.getElementById('filmstripLocalVideoThumbnail');
210
-
211
-        appendTarget && appendTarget.appendChild(this.container);
212
-    }
213
-}

+ 0
- 242
modules/UI/videolayout/RemoteVideo.js 查看文件

@@ -1,242 +0,0 @@
1
-/* global $, APP, config */
2
-
3
-/* eslint-disable no-unused-vars */
4
-import { AtlasKitThemeProvider } from '@atlaskit/theme';
5
-import Logger from 'jitsi-meet-logger';
6
-import React from 'react';
7
-import ReactDOM from 'react-dom';
8
-import { I18nextProvider } from 'react-i18next';
9
-import { Provider } from 'react-redux';
10
-
11
-import { i18next } from '../../../react/features/base/i18n';
12
-import {
13
-    JitsiParticipantConnectionStatus
14
-} from '../../../react/features/base/lib-jitsi-meet';
15
-import { getParticipantById } from '../../../react/features/base/participants';
16
-import { isTestModeEnabled } from '../../../react/features/base/testing';
17
-import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
18
-import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
19
-import { PresenceLabel } from '../../../react/features/presence-status';
20
-import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
21
-import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
22
-/* eslint-enable no-unused-vars */
23
-import UIUtils from '../util/UIUtil';
24
-
25
-import SmallVideo from './SmallVideo';
26
-
27
-const logger = Logger.getLogger(__filename);
28
-
29
-/**
30
- * List of container events that we are going to process, will be added as listener to the
31
- * container for every event in the list. The latest event will be stored in redux.
32
- */
33
-const containerEvents = [
34
-    'abort', 'canplay', 'canplaythrough', 'emptied', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart',
35
-    'pause', 'play', 'playing', 'ratechange', 'stalled', 'suspend', 'waiting'
36
-];
37
-
38
-/**
39
- *
40
- * @param {*} spanId
41
- */
42
-function createContainer(spanId) {
43
-    const container = document.createElement('span');
44
-
45
-    container.id = spanId;
46
-    container.className = 'videocontainer';
47
-
48
-    const remoteVideosContainer
49
-        = document.getElementById('filmstripRemoteVideosContainer');
50
-    const localVideoContainer
51
-        = document.getElementById('localVideoTileViewContainer');
52
-
53
-    remoteVideosContainer.insertBefore(container, localVideoContainer);
54
-
55
-    return container;
56
-}
57
-
58
-/**
59
- *
60
- */
61
-export default class RemoteVideo extends SmallVideo {
62
-    /**
63
-     * Creates new instance of the <tt>RemoteVideo</tt>.
64
-     * @param user {JitsiParticipant} the user for whom remote video instance will
65
-     * be created.
66
-     * @constructor
67
-     */
68
-    constructor(user) {
69
-        super();
70
-
71
-        this.user = user;
72
-        this.id = user.getId();
73
-        this.videoSpanId = `participant_${this.id}`;
74
-
75
-        this.addRemoteVideoContainer();
76
-        this.bindHoverHandler();
77
-        this.flipX = false;
78
-        this.isLocal = false;
79
-
80
-        /**
81
-         * The flag is set to <tt>true</tt> after the 'canplay' event has been
82
-         * triggered on the current video element. It goes back to <tt>false</tt>
83
-         * when the stream is removed. It is used to determine whether the video
84
-         * playback has ever started.
85
-         * @type {boolean}
86
-         */
87
-        this._canPlayEventReceived = false;
88
-
89
-        this.container.onclick = this._onContainerClick;
90
-    }
91
-
92
-    /**
93
-     *
94
-     */
95
-    addRemoteVideoContainer() {
96
-        this.container = createContainer(this.videoSpanId);
97
-        this.$container = $(this.container);
98
-        this.renderThumbnail();
99
-        this._setThumbnailSize();
100
-        this.initBrowserSpecificProperties();
101
-
102
-        return this.container;
103
-    }
104
-
105
-    /**
106
-     * Renders the thumbnail.
107
-     */
108
-    renderThumbnail(isHovered = false) {
109
-        ReactDOM.render(
110
-            <Provider store = { APP.store }>
111
-                <I18nextProvider i18n = { i18next }>
112
-                    <Thumbnail participantID = { this.id } isHovered = { isHovered } />
113
-                </I18nextProvider>
114
-            </Provider>, this.container);
115
-    }
116
-
117
-    /**
118
-     * Removes the remote stream element corresponding to the given stream and
119
-     * parent container.
120
-     *
121
-     * @param stream the MediaStream
122
-     * @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one.
123
-     */
124
-    removeRemoteStreamElement(stream) {
125
-        if (!this.container) {
126
-            return false;
127
-        }
128
-
129
-        const isVideo = stream.isVideoTrack();
130
-        const elementID = `remoteVideo_${stream.getId()}`;
131
-        const select = $(`#${elementID}`);
132
-
133
-        select.remove();
134
-        if (isVideo) {
135
-            this._canPlayEventReceived = false;
136
-        }
137
-
138
-        logger.info(`Video removed ${this.id}`, select);
139
-
140
-        this.updateView();
141
-    }
142
-
143
-    /**
144
-     * The remote video is considered "playable" once the can play event has been received.
145
-     *
146
-     * @inheritdoc
147
-     * @override
148
-     */
149
-    isVideoPlayable() {
150
-        return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived;
151
-    }
152
-
153
-    /**
154
-     * @inheritDoc
155
-     */
156
-    updateView() {
157
-        this.$container.toggleClass('audio-only', APP.conference.isAudioOnly());
158
-        super.updateView();
159
-    }
160
-
161
-    /**
162
-     * Removes RemoteVideo from the page.
163
-     */
164
-    remove() {
165
-        ReactDOM.unmountComponentAtNode(this.container);
166
-        super.remove();
167
-    }
168
-
169
-    /**
170
-     *
171
-     * @param {*} streamElement
172
-     * @param {*} stream
173
-     */
174
-    waitForPlayback(streamElement, stream) {
175
-        $(streamElement).hide();
176
-
177
-        const webRtcStream = stream.getOriginalStream();
178
-        const isVideo = stream.isVideoTrack();
179
-
180
-        if (!isVideo || webRtcStream.id === 'mixedmslabel') {
181
-            return;
182
-        }
183
-
184
-        const listener = () => {
185
-            this._canPlayEventReceived = true;
186
-
187
-            logger.info(`${this.id} video is now active`, streamElement);
188
-            if (streamElement) {
189
-                $(streamElement).show();
190
-            }
191
-
192
-            streamElement.removeEventListener('canplay', listener);
193
-
194
-            // Refresh to show the video
195
-            this.updateView();
196
-        };
197
-
198
-        streamElement.addEventListener('canplay', listener);
199
-    }
200
-
201
-    /**
202
-     *
203
-     * @param {*} stream
204
-     */
205
-    addRemoteStreamElement(stream) {
206
-        if (!this.container) {
207
-            logger.debug('Not attaching remote stream due to no container');
208
-
209
-            return;
210
-        }
211
-
212
-        const isVideo = stream.isVideoTrack();
213
-
214
-        if (!stream.getOriginalStream()) {
215
-            logger.debug('Remote video stream has no original stream');
216
-
217
-            return;
218
-        }
219
-
220
-        let streamElement = document.createElement('video');
221
-
222
-        streamElement.autoplay = !config.testing?.noAutoPlayVideo;
223
-        streamElement.id = `remoteVideo_${stream.getId()}`;
224
-        streamElement.mute = true;
225
-        streamElement.playsInline = true;
226
-
227
-        // Put new stream element always in front
228
-        streamElement = UIUtils.prependChild(this.container, streamElement);
229
-
230
-        this.waitForPlayback(streamElement, stream);
231
-        stream.attach(streamElement);
232
-
233
-        if (isVideo && isTestModeEnabled(APP.store.getState())) {
234
-
235
-            const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
236
-
237
-            containerEvents.forEach(event => {
238
-                streamElement.addEventListener(event, cb.bind(this, event));
239
-            });
240
-        }
241
-    }
242
-}

+ 0
- 509
modules/UI/videolayout/SmallVideo.js 查看文件

@@ -1,509 +0,0 @@
1
-/* global $, APP, interfaceConfig */
2
-
3
-/* eslint-disable no-unused-vars */
4
-import { AtlasKitThemeProvider } from '@atlaskit/theme';
5
-import Logger from 'jitsi-meet-logger';
6
-import React from 'react';
7
-import ReactDOM from 'react-dom';
8
-import { I18nextProvider } from 'react-i18next';
9
-import { Provider } from 'react-redux';
10
-
11
-import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics';
12
-import { AudioLevelIndicator } from '../../../react/features/audio-level-indicator';
13
-import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
14
-import { i18next } from '../../../react/features/base/i18n';
15
-import { MEDIA_TYPE } from '../../../react/features/base/media';
16
-import {
17
-    getLocalParticipant,
18
-    getParticipantById,
19
-    getParticipantCount,
20
-    getPinnedParticipant,
21
-    pinParticipant
22
-} from '../../../react/features/base/participants';
23
-import {
24
-    getLocalVideoTrack,
25
-    getTrackByMediaTypeAndParticipant,
26
-    isLocalTrackMuted,
27
-    isRemoteTrackMuted
28
-} from '../../../react/features/base/tracks';
29
-import { ConnectionIndicator } from '../../../react/features/connection-indicator';
30
-import { DisplayName } from '../../../react/features/display-name';
31
-import {
32
-    DominantSpeakerIndicator,
33
-    RaisedHandIndicator,
34
-    StatusIndicators,
35
-    isVideoPlayable
36
-} from '../../../react/features/filmstrip';
37
-import {
38
-    LAYOUTS,
39
-    getCurrentLayout,
40
-    setTileView,
41
-    shouldDisplayTileView
42
-} from '../../../react/features/video-layout';
43
-/* eslint-enable no-unused-vars */
44
-
45
-const logger = Logger.getLogger(__filename);
46
-
47
-/**
48
- * Display mode constant used when video is being displayed on the small video.
49
- * @type {number}
50
- * @constant
51
- */
52
-const DISPLAY_VIDEO = 0;
53
-
54
-/**
55
- * Display mode constant used when the user's avatar is being displayed on
56
- * the small video.
57
- * @type {number}
58
- * @constant
59
- */
60
-const DISPLAY_AVATAR = 1;
61
-
62
-/**
63
- * Display mode constant used when neither video nor avatar is being displayed
64
- * on the small video. And we just show the display name.
65
- * @type {number}
66
- * @constant
67
- */
68
-const DISPLAY_BLACKNESS_WITH_NAME = 2;
69
-
70
-/**
71
- * Display mode constant used when video is displayed and display name
72
- * at the same time.
73
- * @type {number}
74
- * @constant
75
- */
76
-const DISPLAY_VIDEO_WITH_NAME = 3;
77
-
78
-/**
79
- * Display mode constant used when neither video nor avatar is being displayed
80
- * on the small video. And we just show the display name.
81
- * @type {number}
82
- * @constant
83
- */
84
-const DISPLAY_AVATAR_WITH_NAME = 4;
85
-
86
-
87
-/**
88
- *
89
- */
90
-export default class SmallVideo {
91
-    /**
92
-     * Constructor.
93
-     */
94
-    constructor() {
95
-        this.videoIsHovered = false;
96
-        this.videoType = undefined;
97
-
98
-        // Bind event handlers so they are only bound once for every instance.
99
-        this.updateView = this.updateView.bind(this);
100
-
101
-        this._onContainerClick = this._onContainerClick.bind(this);
102
-    }
103
-
104
-    /**
105
-     * Returns the identifier of this small video.
106
-     *
107
-     * @returns the identifier of this small video
108
-     */
109
-    getId() {
110
-        return this.id;
111
-    }
112
-
113
-    /**
114
-     * Indicates if this small video is currently visible.
115
-     *
116
-     * @return <tt>true</tt> if this small video isn't currently visible and
117
-     * <tt>false</tt> - otherwise.
118
-     */
119
-    isVisible() {
120
-        return this.$container.is(':visible');
121
-    }
122
-
123
-    /**
124
-     * Configures hoverIn/hoverOut handlers. Depends on connection indicator.
125
-     */
126
-    bindHoverHandler() {
127
-        // Add hover handler
128
-        this.$container.hover(
129
-            () => {
130
-                this.videoIsHovered = true;
131
-                this.renderThumbnail(true);
132
-                this.updateView();
133
-            },
134
-            () => {
135
-                this.videoIsHovered = false;
136
-                this.renderThumbnail(false);
137
-                this.updateView();
138
-            }
139
-        );
140
-    }
141
-
142
-    /**
143
-     * Renders the thumbnail.
144
-     */
145
-    renderThumbnail() {
146
-        // Should be implemented by in subclasses.
147
-    }
148
-
149
-    /**
150
-     * This is an especially interesting function. A naive reader might think that
151
-     * it returns this SmallVideo's "video" element. But it is much more exciting.
152
-     * It first finds this video's parent element using jquery, then uses a utility
153
-     * from lib-jitsi-meet to extract the video element from it (with two more
154
-     * jquery calls), and finally uses jquery again to encapsulate the video element
155
-     * in an array. This last step allows (some might prefer "forces") users of
156
-     * this function to access the video element via the 0th element of the returned
157
-     * array (after checking its length of course!).
158
-     */
159
-    selectVideoElement() {
160
-        return $($(this.container).find('video')[0]);
161
-    }
162
-
163
-    /**
164
-     * Enables / disables the css responsible for focusing/pinning a video
165
-     * thumbnail.
166
-     *
167
-     * @param isFocused indicates if the thumbnail should be focused/pinned or not
168
-     */
169
-    focus(isFocused) {
170
-        const focusedCssClass = 'videoContainerFocused';
171
-        const isFocusClassEnabled = this.$container.hasClass(focusedCssClass);
172
-
173
-        if (!isFocused && isFocusClassEnabled) {
174
-            this.$container.removeClass(focusedCssClass);
175
-        } else if (isFocused && !isFocusClassEnabled) {
176
-            this.$container.addClass(focusedCssClass);
177
-        }
178
-    }
179
-
180
-    /**
181
-     *
182
-     */
183
-    hasVideo() {
184
-        return this.selectVideoElement().length !== 0;
185
-    }
186
-
187
-    /**
188
-     * Checks whether the user associated with this <tt>SmallVideo</tt> is currently
189
-     * being displayed on the "large video".
190
-     *
191
-     * @return {boolean} <tt>true</tt> if the user is displayed on the large video
192
-     * or <tt>false</tt> otherwise.
193
-     */
194
-    isCurrentlyOnLargeVideo() {
195
-        return APP.store.getState()['features/large-video']?.participantId === this.id;
196
-    }
197
-
198
-    /**
199
-     * Checks whether there is a playable video stream available for the user
200
-     * associated with this <tt>SmallVideo</tt>.
201
-     *
202
-     * @return {boolean} <tt>true</tt> if there is a playable video stream available
203
-     * or <tt>false</tt> otherwise.
204
-     */
205
-    isVideoPlayable() {
206
-        return isVideoPlayable(APP.store.getState(), this.id);
207
-    }
208
-
209
-    /**
210
-     * Determines what should be display on the thumbnail.
211
-     *
212
-     * @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
213
-     * or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
214
-     */
215
-    selectDisplayMode(input) {
216
-        if (!input.tileViewActive && input.isScreenSharing) {
217
-            return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
218
-        } else if (input.isCurrentlyOnLargeVideo && !input.tileViewActive) {
219
-            // Display name is always and only displayed when user is on the stage
220
-            return input.isVideoPlayable && !input.isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
221
-        } else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
222
-            // check hovering and change state to video with name
223
-            return input.isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
224
-        }
225
-
226
-        // check hovering and change state to avatar with name
227
-        return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
228
-    }
229
-
230
-    /**
231
-     * Computes information that determine the display mode.
232
-     *
233
-     * @returns {Object}
234
-     */
235
-    computeDisplayModeInput() {
236
-        let isScreenSharing = false;
237
-        let connectionStatus;
238
-        const state = APP.store.getState();
239
-        const id = this.id;
240
-        const participant = getParticipantById(state, id);
241
-        const isLocal = participant?.local ?? true;
242
-        const tracks = state['features/base/tracks'];
243
-        const videoTrack
244
-            = isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
245
-
246
-        if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
247
-            isScreenSharing = videoTrack?.videoType === 'desktop';
248
-            connectionStatus = participant.connectionStatus;
249
-        }
250
-
251
-        return {
252
-            isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
253
-            isHovered: this._isHovered(),
254
-            isAudioOnly: APP.conference.isAudioOnly(),
255
-            tileViewActive: shouldDisplayTileView(state),
256
-            isVideoPlayable: this.isVideoPlayable(),
257
-            hasVideo: Boolean(this.selectVideoElement().length),
258
-            connectionStatus,
259
-            canPlayEventReceived: this._canPlayEventReceived,
260
-            videoStream: Boolean(videoTrack),
261
-            isScreenSharing,
262
-            videoStreamMuted: videoTrack ? videoTrack.muted : 'no stream'
263
-        };
264
-    }
265
-
266
-    /**
267
-     * Checks whether current video is considered hovered. Currently it is hovered
268
-     * if the mouse is over the video, or if the connection
269
-     * indicator is shown(hovered).
270
-     * @private
271
-     */
272
-    _isHovered() {
273
-        return this.videoIsHovered;
274
-    }
275
-
276
-    /**
277
-     * Updates the css classes of the thumbnail based on the current state.
278
-     */
279
-    updateView() {
280
-        this.$container.removeClass((index, classNames) =>
281
-            classNames.split(' ').filter(name => name.startsWith('display-')));
282
-
283
-        const oldDisplayMode = this.displayMode;
284
-        let displayModeString = '';
285
-
286
-        const displayModeInput = this.computeDisplayModeInput();
287
-
288
-        // Determine whether video, avatar or blackness should be displayed
289
-        this.displayMode = this.selectDisplayMode(displayModeInput);
290
-
291
-        switch (this.displayMode) {
292
-        case DISPLAY_AVATAR_WITH_NAME:
293
-            displayModeString = 'avatar-with-name';
294
-            this.$container.addClass('display-avatar-with-name');
295
-            break;
296
-        case DISPLAY_BLACKNESS_WITH_NAME:
297
-            displayModeString = 'blackness-with-name';
298
-            this.$container.addClass('display-name-on-black');
299
-            break;
300
-        case DISPLAY_VIDEO:
301
-            displayModeString = 'video';
302
-            this.$container.addClass('display-video');
303
-            break;
304
-        case DISPLAY_VIDEO_WITH_NAME:
305
-            displayModeString = 'video-with-name';
306
-            this.$container.addClass('display-name-on-video');
307
-            break;
308
-        case DISPLAY_AVATAR:
309
-        default:
310
-            displayModeString = 'avatar';
311
-            this.$container.addClass('display-avatar-only');
312
-            break;
313
-        }
314
-
315
-        if (this.displayMode !== oldDisplayMode) {
316
-            logger.debug(`Displaying ${displayModeString} for ${this.id}, data: [${JSON.stringify(displayModeInput)}]`);
317
-        }
318
-
319
-        if (this.displayMode !== DISPLAY_VIDEO
320
-            && this.displayMode !== DISPLAY_VIDEO_WITH_NAME
321
-            && displayModeInput.tileViewActive
322
-            && displayModeInput.isScreenSharing
323
-            && !displayModeInput.isAudioOnly) {
324
-            // send the event
325
-            sendAnalytics(createScreenSharingIssueEvent({
326
-                source: 'thumbnail',
327
-                ...displayModeInput
328
-            }));
329
-        }
330
-    }
331
-
332
-    /**
333
-     * Shows or hides the dominant speaker indicator.
334
-     * @param show whether to show or hide.
335
-     */
336
-    showDominantSpeakerIndicator(show) {
337
-        // Don't create and show dominant speaker indicator if
338
-        // DISABLE_DOMINANT_SPEAKER_INDICATOR is true
339
-        if (interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR) {
340
-            return;
341
-        }
342
-
343
-        if (!this.container) {
344
-            logger.warn(`Unable to set dominant speaker indicator - ${this.videoSpanId} does not exist`);
345
-
346
-            return;
347
-        }
348
-
349
-        this.$container.toggleClass('active-speaker', show);
350
-    }
351
-
352
-    /**
353
-     * Initializes any browser specific properties. Currently sets the overflow
354
-     * property for Qt browsers on Windows to hidden, thus fixing the following
355
-     * problem:
356
-     * Some browsers don't have full support of the object-fit property for the
357
-     * video element and when we set video object-fit to "cover" the video
358
-     * actually overflows the boundaries of its container, so it's important
359
-     * to indicate that the "overflow" should be hidden.
360
-     *
361
-     * Setting this property for all browsers will result in broken audio levels,
362
-     * which makes this a temporary solution, before reworking audio levels.
363
-     */
364
-    initBrowserSpecificProperties() {
365
-        const userAgent = window.navigator.userAgent;
366
-
367
-        if (userAgent.indexOf('QtWebEngine') > -1
368
-                && (userAgent.indexOf('Windows') > -1 || userAgent.indexOf('Linux') > -1)) {
369
-            this.$container.css('overflow', 'hidden');
370
-        }
371
-    }
372
-
373
-    /**
374
-     * Cleans up components on {@code SmallVideo} and removes itself from the DOM.
375
-     *
376
-     * @returns {void}
377
-     */
378
-    remove() {
379
-        logger.log('Remove thumbnail', this.id);
380
-        this._unmountThumbnail();
381
-
382
-        // Remove whole container
383
-        if (this.container.parentNode) {
384
-            this.container.parentNode.removeChild(this.container);
385
-        }
386
-    }
387
-
388
-    /**
389
-     * Helper function for re-rendering multiple react components of the small
390
-     * video.
391
-     *
392
-     * @returns {void}
393
-     */
394
-    rerender() {
395
-        this.updateView();
396
-    }
397
-
398
-    /**
399
-     * Callback invoked when the thumbnail is clicked and potentially trigger
400
-     * pinning of the participant.
401
-     *
402
-     * @param {MouseEvent} event - The click event to intercept.
403
-     * @private
404
-     * @returns {void}
405
-     */
406
-    _onContainerClick(event) {
407
-        const triggerPin = this._shouldTriggerPin(event);
408
-
409
-        if (event.stopPropagation && triggerPin) {
410
-            event.stopPropagation();
411
-            event.preventDefault();
412
-        }
413
-        if (triggerPin) {
414
-            this.togglePin();
415
-        }
416
-
417
-        return false;
418
-    }
419
-
420
-    /**
421
-     * Returns whether or not a click event is targeted at certain elements which
422
-     * should not trigger a pin.
423
-     *
424
-     * @param {MouseEvent} event - The click event to intercept.
425
-     * @private
426
-     * @returns {boolean}
427
-     */
428
-    _shouldTriggerPin(event) {
429
-        // TODO Checking the classes is a workround to allow events to bubble into
430
-        // the DisplayName component if it was clicked. React's synthetic events
431
-        // will fire after jQuery handlers execute, so stop propagation at this
432
-        // point will prevent DisplayName from getting click events. This workaround
433
-        // should be removable once LocalVideo is a React Component because then
434
-        // the components share the same eventing system.
435
-        const $source = $(event.target || event.srcElement);
436
-
437
-        return $source.parents('.displayNameContainer').length === 0
438
-            && $source.parents('.popover').length === 0
439
-            && !event.target.classList.contains('popover');
440
-    }
441
-
442
-    /**
443
-     * Pins the participant displayed by this thumbnail or unpins if already pinned.
444
-     *
445
-     * @returns {void}
446
-     */
447
-    togglePin() {
448
-        const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
449
-        const participantIdToPin = pinnedParticipant && pinnedParticipant.id === this.id ? null : this.id;
450
-
451
-        APP.store.dispatch(pinParticipant(participantIdToPin));
452
-    }
453
-
454
-    /**
455
-     * Unmounts the thumbnail.
456
-     */
457
-    _unmountThumbnail() {
458
-        ReactDOM.unmountComponentAtNode(this.container);
459
-    }
460
-
461
-    /**
462
-     * Sets the size of the thumbnail.
463
-     */
464
-    _setThumbnailSize() {
465
-        const layout = getCurrentLayout(APP.store.getState());
466
-        const heightToWidthPercent = 100
467
-                / (this.isLocal ? interfaceConfig.LOCAL_THUMBNAIL_RATIO : interfaceConfig.REMOTE_THUMBNAIL_RATIO);
468
-
469
-        switch (layout) {
470
-        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
471
-            this.$container.css('padding-top', `${heightToWidthPercent}%`);
472
-            break;
473
-        }
474
-        case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
475
-            const state = APP.store.getState();
476
-            const { local, remote } = state['features/filmstrip'].horizontalViewDimensions;
477
-            const size = this.isLocal ? local : remote;
478
-
479
-            if (typeof size !== 'undefined') {
480
-                const { height, width } = size;
481
-
482
-                this.$container.css({
483
-                    height: `${height}px`,
484
-                    'min-height': `${height}px`,
485
-                    'min-width': `${width}px`,
486
-                    width: `${width}px`
487
-                });
488
-            }
489
-            break;
490
-        }
491
-        case LAYOUTS.TILE_VIEW: {
492
-            const state = APP.store.getState();
493
-            const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
494
-
495
-            if (typeof thumbnailSize !== 'undefined') {
496
-                const { height, width } = thumbnailSize;
497
-
498
-                this.$container.css({
499
-                    height: `${height}px`,
500
-                    'min-height': `${height}px`,
501
-                    'min-width': `${width}px`,
502
-                    width: `${width}px`
503
-                });
504
-            }
505
-            break;
506
-        }
507
-        }
508
-    }
509
-}

+ 2
- 7
modules/UI/videolayout/VideoContainer.js 查看文件

@@ -9,7 +9,6 @@ import { isTestModeEnabled } from '../../../react/features/base/testing';
9 9
 import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
10 10
 import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
11 11
 /* eslint-enable no-unused-vars */
12
-import UIEvents from '../../../service/UI/UIEvents';
13 12
 import UIUtil from '../util/UIUtil';
14 13
 
15 14
 import Filmstrip from './Filmstrip';
@@ -187,16 +186,13 @@ export class VideoContainer extends LargeContainer {
187 186
      * Creates new VideoContainer instance.
188 187
      * @param resizeContainer {Function} function that takes care of the size
189 188
      * of the video container.
190
-     * @param emitter {EventEmitter} the event emitter that will be used by
191
-     * this instance.
192 189
      */
193
-    constructor(resizeContainer, emitter) {
190
+    constructor(resizeContainer) {
194 191
         super();
195 192
         this.stream = null;
196 193
         this.userId = null;
197 194
         this.videoType = null;
198 195
         this.localFlipX = true;
199
-        this.emitter = emitter;
200 196
         this.resizeContainer = resizeContainer;
201 197
 
202 198
         /**
@@ -492,7 +488,7 @@ export class VideoContainer extends LargeContainer {
492 488
 
493 489
         stream.attach(this.$video[0]);
494 490
 
495
-        const flipX = stream.isLocal() && this.localFlipX;
491
+        const flipX = stream.isLocal() && this.localFlipX && !this.isScreenSharing();
496 492
 
497 493
         this.$video.css({
498 494
             transform: flipX ? 'scaleX(-1)' : 'none'
@@ -534,7 +530,6 @@ export class VideoContainer extends LargeContainer {
534 530
         this.$avatar.css('visibility', show ? 'visible' : 'hidden');
535 531
         this.avatarDisplayed = show;
536 532
 
537
-        this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE, show);
538 533
         APP.API.notifyLargeVideoVisibilityChanged(show);
539 534
     }
540 535
 

+ 19
- 353
modules/UI/videolayout/VideoLayout.js 查看文件

@@ -4,88 +4,29 @@ import Logger from 'jitsi-meet-logger';
4 4
 
5 5
 import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
6 6
 import {
7
-    getLocalParticipant as getLocalParticipantFromStore,
8 7
     getPinnedParticipant,
9
-    getParticipantById,
10
-    pinParticipant
8
+    getParticipantById
11 9
 } from '../../../react/features/base/participants';
12 10
 import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
13
-import UIEvents from '../../../service/UI/UIEvents';
14 11
 import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
15
-import SharedVideoThumb from '../shared_video/SharedVideoThumb';
16 12
 
17 13
 import LargeVideoManager from './LargeVideoManager';
18
-import LocalVideo from './LocalVideo';
19
-import RemoteVideo from './RemoteVideo';
20 14
 import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
21 15
 
22 16
 const logger = Logger.getLogger(__filename);
23
-
24
-const remoteVideos = {};
25
-let localVideoThumbnail = null;
26
-
27
-let eventEmitter = null;
28
-
29 17
 let largeVideo;
30 18
 
31
-/**
32
- * flipX state of the localVideo
33
- */
34
-let localFlipX = null;
35
-
36
-/**
37
- * Handler for local flip X changed event.
38
- * @param {Object} val
39
- */
40
-function onLocalFlipXChanged(val) {
41
-    localFlipX = val;
42
-    if (largeVideo) {
43
-        largeVideo.onLocalFlipXChange(val);
44
-    }
45
-}
46
-
47
-/**
48
- * Returns an array of all thumbnails in the filmstrip.
49
- *
50
- * @private
51
- * @returns {Array}
52
- */
53
-function getAllThumbnails() {
54
-    return [
55
-        ...localVideoThumbnail ? [ localVideoThumbnail ] : [],
56
-        ...Object.values(remoteVideos)
57
-    ];
58
-}
59
-
60
-/**
61
- * Private helper to get the redux representation of the local participant.
62
- *
63
- * @private
64
- * @returns {Object}
65
- */
66
-function getLocalParticipant() {
67
-    return getLocalParticipantFromStore(APP.store.getState());
68
-}
69
-
70 19
 const VideoLayout = {
71
-    init(emitter) {
72
-        eventEmitter = emitter;
73
-
74
-        localVideoThumbnail = new LocalVideo(
75
-            emitter,
76
-            this._updateLargeVideoIfDisplayed.bind(this));
77
-
78
-        this.registerListeners();
79
-    },
80
-
81 20
     /**
82
-     * Registering listeners for UI events in Video layout component.
83
-     *
84
-     * @returns {void}
21
+     * Handler for local flip X changed event.
85 22
      */
86
-    registerListeners() {
87
-        eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
88
-            onLocalFlipXChanged);
23
+    onLocalFlipXChanged() {
24
+        if (largeVideo) {
25
+            const { store } = APP;
26
+            const { localFlipX } = store.getState()['features/base/settings'];
27
+
28
+            largeVideo.onLocalFlipXChange(localFlipX);
29
+        }
89 30
     },
90 31
 
91 32
     /**
@@ -95,14 +36,17 @@ const VideoLayout = {
95 36
      */
96 37
     reset() {
97 38
         this._resetLargeVideo();
98
-        this._resetFilmstrip();
99 39
     },
100 40
 
101 41
     initLargeVideo() {
102 42
         this._resetLargeVideo();
103 43
 
104
-        largeVideo = new LargeVideoManager(eventEmitter);
105
-        if (localFlipX) {
44
+        largeVideo = new LargeVideoManager();
45
+
46
+        const { store } = APP;
47
+        const { localFlipX } = store.getState()['features/base/settings'];
48
+
49
+        if (typeof localFlipX === 'boolean') {
106 50
             largeVideo.onLocalFlipXChange(localFlipX);
107 51
         }
108 52
         largeVideo.updateContainerSize();
@@ -120,55 +64,6 @@ const VideoLayout = {
120 64
         }
121 65
     },
122 66
 
123
-    changeLocalVideo(stream) {
124
-        const localId = getLocalParticipant().id;
125
-
126
-        this.onVideoTypeChanged(localId, stream.videoType);
127
-
128
-        localVideoThumbnail.changeVideo(stream);
129
-
130
-        this._updateLargeVideoIfDisplayed(localId);
131
-    },
132
-
133
-    /**
134
-     * Shows/hides local video.
135
-     * @param {boolean} true to make the local video visible, false - otherwise
136
-     */
137
-    setLocalVideoVisible(visible) {
138
-        localVideoThumbnail.setVisible(visible);
139
-    },
140
-
141
-    onRemoteStreamAdded(stream) {
142
-        const id = stream.getParticipantId();
143
-        const remoteVideo = remoteVideos[id];
144
-
145
-        logger.debug(`Received a new ${stream.getType()} stream for ${id}`);
146
-
147
-        if (!remoteVideo) {
148
-            logger.debug('No remote video element to add stream');
149
-
150
-            return;
151
-        }
152
-
153
-        remoteVideo.addRemoteStreamElement(stream);
154
-
155
-        this.onVideoMute(id);
156
-        remoteVideo.updateView();
157
-    },
158
-
159
-    onRemoteStreamRemoved(stream) {
160
-        const id = stream.getParticipantId();
161
-        const remoteVideo = remoteVideos[id];
162
-
163
-        // Remote stream may be removed after participant left the conference.
164
-        if (remoteVideo) {
165
-            remoteVideo.removeRemoteStreamElement(stream);
166
-            remoteVideo.updateView();
167
-        }
168
-
169
-        this.updateVideoMutedForNoTracks(id);
170
-    },
171
-
172 67
     /**
173 68
      * FIXME get rid of this method once muted indicator are reactified (by
174 69
      * making sure that user with no tracks is displayed as muted )
@@ -180,7 +75,7 @@ const VideoLayout = {
180 75
         const participant = APP.conference.getParticipantById(participantId);
181 76
 
182 77
         if (participant && !participant.getTracksByMediaType('video').length) {
183
-            APP.UI.setVideoMuted(participantId);
78
+            VideoLayout._updateLargeVideoIfDisplayed(participantId, true);
184 79
         }
185 80
     },
186 81
 
@@ -202,110 +97,12 @@ const VideoLayout = {
202 97
         return videoTrack?.videoType;
203 98
     },
204 99
 
205
-    isPinned(id) {
206
-        return id === this.getPinnedId();
207
-    },
208
-
209 100
     getPinnedId() {
210 101
         const { id } = getPinnedParticipant(APP.store.getState()) || {};
211 102
 
212 103
         return id || null;
213 104
     },
214 105
 
215
-    /**
216
-     * Triggers a thumbnail to pin or unpin itself.
217
-     *
218
-     * @param {number} videoNumber - The index of the video to toggle pin on.
219
-     * @private
220
-     */
221
-    togglePin(videoNumber) {
222
-        const videos = getAllThumbnails();
223
-        const videoView = videos[videoNumber];
224
-
225
-        videoView && videoView.togglePin();
226
-    },
227
-
228
-    /**
229
-     * Callback invoked to update display when the pin participant has changed.
230
-     *
231
-     * @paramn {string|null} pinnedParticipantID - The participant ID of the
232
-     * participant that is pinned or null if no one is pinned.
233
-     * @returns {void}
234
-     */
235
-    onPinChange(pinnedParticipantID) {
236
-        getAllThumbnails().forEach(thumbnail =>
237
-            thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
238
-    },
239
-
240
-    /**
241
-     * Creates a participant container for the given id.
242
-     *
243
-     * @param {Object} participant - The redux representation of a remote
244
-     * participant.
245
-     * @returns {void}
246
-     */
247
-    addRemoteParticipantContainer(participant) {
248
-        if (!participant || participant.local) {
249
-            return;
250
-        } else if (participant.isFakeParticipant) {
251
-            const sharedVideoThumb = new SharedVideoThumb(participant);
252
-
253
-            this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
254
-
255
-            return;
256
-        }
257
-
258
-        const id = participant.id;
259
-        const jitsiParticipant = APP.conference.getParticipantById(id);
260
-        const remoteVideo = new RemoteVideo(jitsiParticipant);
261
-
262
-        this.addRemoteVideoContainer(id, remoteVideo);
263
-        this.updateVideoMutedForNoTracks(id);
264
-    },
265
-
266
-    /**
267
-     * Adds remote video container for the given id and <tt>SmallVideo</tt>.
268
-     *
269
-     * @param {string} the id of the video to add
270
-     * @param {SmallVideo} smallVideo the small video instance to add as a
271
-     * remote video
272
-     */
273
-    addRemoteVideoContainer(id, remoteVideo) {
274
-        remoteVideos[id] = remoteVideo;
275
-
276
-        // Initialize the view
277
-        remoteVideo.updateView();
278
-    },
279
-
280
-    /**
281
-     * On video muted event.
282
-     */
283
-    onVideoMute(id) {
284
-        if (APP.conference.isLocalId(id)) {
285
-            localVideoThumbnail && localVideoThumbnail.updateView();
286
-        } else {
287
-            const remoteVideo = remoteVideos[id];
288
-
289
-            if (remoteVideo) {
290
-                remoteVideo.updateView();
291
-            }
292
-        }
293
-
294
-        // large video will show avatar instead of muted stream
295
-        this._updateLargeVideoIfDisplayed(id, true);
296
-    },
297
-
298
-    /**
299
-     * On dominant speaker changed event.
300
-     *
301
-     * @param {string} id - The participant ID of the new dominant speaker.
302
-     * @returns {void}
303
-     */
304
-    onDominantSpeakerChanged(id) {
305
-        getAllThumbnails().forEach(thumbnail =>
306
-            thumbnail.showDominantSpeakerIndicator(id === thumbnail.getId()));
307
-    },
308
-
309 106
     /**
310 107
      * Shows/hides warning about a user's connectivity issues.
311 108
      *
@@ -321,12 +118,6 @@ const VideoLayout = {
321 118
         // We have to trigger full large video update to transition from
322 119
         // avatar to video on connectivity restored.
323 120
         this._updateLargeVideoIfDisplayed(id, true);
324
-
325
-        const remoteVideo = remoteVideos[id];
326
-
327
-        if (remoteVideo) {
328
-            remoteVideo.updateView();
329
-        }
330 121
     },
331 122
 
332 123
     /**
@@ -339,58 +130,14 @@ const VideoLayout = {
339 130
      */
340 131
     onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
341 132
         if (endpointsLeavingLastN) {
342
-            endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
133
+            endpointsLeavingLastN.forEach(this._updateLargeVideoIfDisplayed, this);
343 134
         }
344 135
 
345 136
         if (endpointsEnteringLastN) {
346
-            endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
137
+            endpointsEnteringLastN.forEach(this._updateLargeVideoIfDisplayed, this);
347 138
         }
348 139
     },
349 140
 
350
-    /**
351
-     * Updates remote video by id if it exists.
352
-     * @param {string} id of the remote video
353
-     * @private
354
-     */
355
-    _updateRemoteVideo(id) {
356
-        const remoteVideo = remoteVideos[id];
357
-
358
-        if (remoteVideo) {
359
-            remoteVideo.updateView();
360
-            this._updateLargeVideoIfDisplayed(id);
361
-        }
362
-    },
363
-
364
-    removeParticipantContainer(id) {
365
-        // Unlock large video
366
-        if (this.getPinnedId() === id) {
367
-            logger.info('Focused video owner has left the conference');
368
-            APP.store.dispatch(pinParticipant(null));
369
-        }
370
-
371
-        const remoteVideo = remoteVideos[id];
372
-
373
-        if (remoteVideo) {
374
-            // Remove remote video
375
-            logger.info(`Removing remote video: ${id}`);
376
-            delete remoteVideos[id];
377
-            remoteVideo.remove();
378
-        } else {
379
-            logger.warn(`No remote video for ${id}`);
380
-        }
381
-    },
382
-
383
-    onVideoTypeChanged(id, newVideoType) {
384
-        const remoteVideo = remoteVideos[id];
385
-
386
-        if (!remoteVideo) {
387
-            return;
388
-        }
389
-
390
-        logger.info('Peer video type changed: ', id, newVideoType);
391
-        remoteVideo.updateView();
392
-    },
393
-
394 141
     /**
395 142
      * Resizes the video area.
396 143
      */
@@ -401,15 +148,6 @@ const VideoLayout = {
401 148
         }
402 149
     },
403 150
 
404
-    getSmallVideo(id) {
405
-        if (APP.conference.isLocalId(id)) {
406
-            return localVideoThumbnail;
407
-        }
408
-
409
-        return remoteVideos[id];
410
-
411
-    },
412
-
413 151
     changeUserAvatar(id, avatarUrl) {
414 152
         if (this.isCurrentlyOnLarge(id)) {
415 153
             largeVideo.updateAvatar(avatarUrl);
@@ -432,24 +170,6 @@ const VideoLayout = {
432 170
         return largeVideo && largeVideo.id === id;
433 171
     },
434 172
 
435
-    /**
436
-     * Triggers an update of remote video and large video displays so they may
437
-     * pick up any state changes that have occurred elsewhere.
438
-     *
439
-     * @returns {void}
440
-     */
441
-    updateAllVideos() {
442
-        const displayedUserId = this.getLargeVideoID();
443
-
444
-        if (displayedUserId) {
445
-            this.updateLargeVideo(displayedUserId, true);
446
-        }
447
-
448
-        Object.keys(remoteVideos).forEach(video => {
449
-            remoteVideos[video].updateView();
450
-        });
451
-    },
452
-
453 173
     updateLargeVideo(id, forceUpdate) {
454 174
         if (!largeVideo) {
455 175
             return;
@@ -510,13 +230,6 @@ const VideoLayout = {
510 230
             return Promise.resolve();
511 231
         }
512 232
 
513
-        const currentId = largeVideo.id;
514
-        let oldSmallVideo;
515
-
516
-        if (currentId) {
517
-            oldSmallVideo = this.getSmallVideo(currentId);
518
-        }
519
-
520 233
         let containerTypeToShow = type;
521 234
 
522 235
         // if we are hiding a container and there is focusedVideo
@@ -533,12 +246,7 @@ const VideoLayout = {
533 246
             }
534 247
         }
535 248
 
536
-        return largeVideo.showContainer(containerTypeToShow)
537
-            .then(() => {
538
-                if (oldSmallVideo) {
539
-                    oldSmallVideo && oldSmallVideo.updateView();
540
-                }
541
-            });
249
+        return largeVideo.showContainer(containerTypeToShow);
542 250
     },
543 251
 
544 252
     isLargeContainerTypeVisible(type) {
@@ -561,14 +269,6 @@ const VideoLayout = {
561 269
         return largeVideo;
562 270
     },
563 271
 
564
-    /**
565
-     * Sets the flipX state of the local video.
566
-     * @param {boolean} true for flipped otherwise false;
567
-     */
568
-    setLocalFlipX(val) {
569
-        this.localFlipX = val;
570
-    },
571
-
572 272
     /**
573 273
      * Returns the wrapper jquery selector for the largeVideo
574 274
      * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
@@ -577,15 +277,6 @@ const VideoLayout = {
577 277
         return this.getCurrentlyOnLargeContainer().$wrapper;
578 278
     },
579 279
 
580
-    /**
581
-     * Returns the number of remove video ids.
582
-     *
583
-     * @returns {number} The number of remote videos.
584
-     */
585
-    getRemoteVideosCount() {
586
-        return Object.keys(remoteVideos).length;
587
-    },
588
-
589 280
     /**
590 281
      * Helper method to invoke when the video layout has changed and elements
591 282
      * have to be re-arranged and resized.
@@ -593,12 +284,7 @@ const VideoLayout = {
593 284
      * @returns {void}
594 285
      */
595 286
     refreshLayout() {
596
-        localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
597 287
         VideoLayout.resizeVideoArea();
598
-
599
-        // Rerender the thumbnails since they are dependent on the layout because of the tooltip positioning.
600
-        localVideoThumbnail && localVideoThumbnail.rerender();
601
-        Object.values(remoteVideos).forEach(remoteVideoThumbnail => remoteVideoThumbnail.rerender());
602 288
     },
603 289
 
604 290
     /**
@@ -615,26 +301,6 @@ const VideoLayout = {
615 301
         largeVideo = null;
616 302
     },
617 303
 
618
-    /**
619
-     * Cleans up filmstrip state. While a separate {@code Filmstrip} exists, its
620
-     * implementation is mainly for querying and manipulating the DOM while
621
-     * state mostly remains in {@code VideoLayout}.
622
-     *
623
-     * @private
624
-     * @returns {void}
625
-     */
626
-    _resetFilmstrip() {
627
-        Object.keys(remoteVideos).forEach(remoteVideoId => {
628
-            this.removeParticipantContainer(remoteVideoId);
629
-            delete remoteVideos[remoteVideoId];
630
-        });
631
-
632
-        if (localVideoThumbnail) {
633
-            localVideoThumbnail.remove();
634
-            localVideoThumbnail = null;
635
-        }
636
-    },
637
-
638 304
     /**
639 305
      * Triggers an update of large video if the passed in participant is
640 306
      * currently displayed on large video.

+ 2
- 1
modules/keyboardshortcut/keyboardshortcut.js 查看文件

@@ -9,6 +9,7 @@ import {
9 9
     sendAnalytics
10 10
 } from '../../react/features/analytics';
11 11
 import { toggleDialog } from '../../react/features/base/dialog';
12
+import { clickOnVideo } from '../../react/features/filmstrip/actions';
12 13
 import { KeyboardShortcutsDialog }
13 14
     from '../../react/features/keyboard-shortcuts';
14 15
 import { SpeakerStats } from '../../react/features/speaker-stats';
@@ -54,7 +55,7 @@ const KeyboardShortcut = {
54 55
                 if (_shortcuts.has(key)) {
55 56
                     _shortcuts.get(key).function(e);
56 57
                 } else if (!isNaN(num) && num >= 0 && num <= 9) {
57
-                    APP.UI.clickOnVideo(num);
58
+                    APP.store.dispatch(clickOnVideo(num));
58 59
                 }
59 60
 
60 61
             }

+ 0
- 5
package-lock.json 查看文件

@@ -10090,11 +10090,6 @@
10090 10090
       "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
10091 10091
       "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
10092 10092
     },
10093
-    "jquery-contextmenu": {
10094
-      "version": "2.4.5",
10095
-      "resolved": "https://registry.npmjs.org/jquery-contextmenu/-/jquery-contextmenu-2.4.5.tgz",
10096
-      "integrity": "sha1-5lrOBg2M2tTQ5d94FdDXA55RdFA="
10097
-    },
10098 10093
     "jquery-i18next": {
10099 10094
       "version": "1.2.1",
10100 10095
       "resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz",

+ 0
- 1
package.json 查看文件

@@ -51,7 +51,6 @@
51 51
     "jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
52 52
     "jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
53 53
     "jquery": "3.5.1",
54
-    "jquery-contextmenu": "2.4.5",
55 54
     "jquery-i18next": "1.2.1",
56 55
     "js-md5": "0.6.1",
57 56
     "jwt-decode": "2.2.0",

+ 119
- 6
react/features/base/media/components/web/Video.js 查看文件

@@ -38,7 +38,103 @@ type Props = {
38 38
      * Used to determine the value of the autoplay attribute of the underlying
39 39
      * video element.
40 40
      */
41
-    playsinline: boolean
41
+    playsinline: boolean,
42
+
43
+    /**
44
+     * A map of the event handlers for the video HTML element.
45
+     */
46
+    eventHandlers?: {|
47
+
48
+        /**
49
+         * onAbort event handler.
50
+         */
51
+        onAbort?: ?Function,
52
+
53
+        /**
54
+         * onCanPlay event handler.
55
+         */
56
+        onCanPlay?: ?Function,
57
+
58
+        /**
59
+         * onCanPlayThrough event handler.
60
+         */
61
+        onCanPlayThrough?: ?Function,
62
+
63
+        /**
64
+         * onEmptied event handler.
65
+         */
66
+        onEmptied?: ?Function,
67
+
68
+        /**
69
+         * onEnded event handler.
70
+         */
71
+        onEnded?: ?Function,
72
+
73
+        /**
74
+         * onError event handler.
75
+         */
76
+        onError?: ?Function,
77
+
78
+        /**
79
+         * onLoadedData event handler.
80
+         */
81
+        onLoadedData?: ?Function,
82
+
83
+        /**
84
+         * onLoadedMetadata event handler.
85
+         */
86
+        onLoadedMetadata?: ?Function,
87
+
88
+        /**
89
+         * onLoadStart event handler.
90
+         */
91
+        onLoadStart?: ?Function,
92
+
93
+        /**
94
+         * onPause event handler.
95
+         */
96
+        onPause?: ?Function,
97
+
98
+        /**
99
+         * onPlay event handler.
100
+         */
101
+        onPlay?: ?Function,
102
+
103
+        /**
104
+         * onPlaying event handler.
105
+         */
106
+        onPlaying?: ?Function,
107
+
108
+        /**
109
+         * onRateChange event handler.
110
+         */
111
+        onRateChange?: ?Function,
112
+
113
+        /**
114
+         * onStalled event handler.
115
+         */
116
+        onStalled?: ?Function,
117
+
118
+        /**
119
+         * onSuspend event handler.
120
+         */
121
+        onSuspend?: ?Function,
122
+
123
+        /**
124
+         * onWaiting event handler.
125
+         */
126
+        onWaiting?: ?Function
127
+    |},
128
+
129
+    /**
130
+     * A styles that will be applied on the video element.
131
+     */
132
+    style?: Object,
133
+
134
+    /**
135
+     * The value of the muted attribute for the underlying video element.
136
+     */
137
+    muted?: boolean
42 138
 };
43 139
 
44 140
 /**
@@ -139,6 +235,10 @@ class Video extends Component<Props> {
139 235
             this._attachTrack(nextProps.videoTrack);
140 236
         }
141 237
 
238
+        if (this.props.style !== nextProps.style || this.props.className !== nextProps.className) {
239
+            return true;
240
+        }
241
+
142 242
         return false;
143 243
     }
144 244
 
@@ -149,13 +249,26 @@ class Video extends Component<Props> {
149 249
      * @returns {ReactElement}
150 250
      */
151 251
     render() {
252
+        const {
253
+            autoPlay,
254
+            className,
255
+            id,
256
+            muted,
257
+            playsinline,
258
+            style,
259
+            eventHandlers
260
+        } = this.props;
261
+
152 262
         return (
153 263
             <video
154
-                autoPlay = { this.props.autoPlay }
155
-                className = { this.props.className }
156
-                id = { this.props.id }
157
-                playsInline = { this.props.playsinline }
158
-                ref = { this._setVideoElement } />
264
+                autoPlay = { autoPlay }
265
+                className = { className }
266
+                id = { id }
267
+                muted = { muted }
268
+                playsInline = { playsinline }
269
+                ref = { this._setVideoElement }
270
+                style = { style }
271
+                { ...eventHandlers } />
159 272
         );
160 273
     }
161 274
 

+ 115
- 5
react/features/base/media/components/web/VideoTrack.js 查看文件

@@ -29,7 +29,103 @@ type Props = AbstractVideoTrackProps & {
29 29
      * Used to determine the value of the autoplay attribute of the underlying
30 30
      * video element.
31 31
      */
32
-    _noAutoPlayVideo: boolean
32
+    _noAutoPlayVideo: boolean,
33
+
34
+    /**
35
+     * A map of the event handlers for the video HTML element.
36
+     */
37
+    eventHandlers?: {|
38
+
39
+        /**
40
+         * onAbort event handler.
41
+         */
42
+        onAbort?: ?Function,
43
+
44
+        /**
45
+         * onCanPlay event handler.
46
+         */
47
+        onCanPlay?: ?Function,
48
+
49
+        /**
50
+         * onCanPlayThrough event handler.
51
+         */
52
+        onCanPlayThrough?: ?Function,
53
+
54
+        /**
55
+         * onEmptied event handler.
56
+         */
57
+        onEmptied?: ?Function,
58
+
59
+        /**
60
+         * onEnded event handler.
61
+         */
62
+        onEnded?: ?Function,
63
+
64
+        /**
65
+         * onError event handler.
66
+         */
67
+        onError?: ?Function,
68
+
69
+        /**
70
+         * onLoadedData event handler.
71
+         */
72
+        onLoadedData?: ?Function,
73
+
74
+        /**
75
+         * onLoadedMetadata event handler.
76
+         */
77
+        onLoadedMetadata?: ?Function,
78
+
79
+        /**
80
+         * onLoadStart event handler.
81
+         */
82
+        onLoadStart?: ?Function,
83
+
84
+        /**
85
+         * onPause event handler.
86
+         */
87
+        onPause?: ?Function,
88
+
89
+        /**
90
+         * onPlay event handler.
91
+         */
92
+        onPlay?: ?Function,
93
+
94
+        /**
95
+         * onPlaying event handler.
96
+         */
97
+        onPlaying?: ?Function,
98
+
99
+        /**
100
+         * onRateChange event handler.
101
+         */
102
+        onRateChange?: ?Function,
103
+
104
+        /**
105
+         * onStalled event handler.
106
+         */
107
+        onStalled?: ?Function,
108
+
109
+        /**
110
+         * onSuspend event handler.
111
+         */
112
+        onSuspend?: ?Function,
113
+
114
+        /**
115
+         * onWaiting event handler.
116
+         */
117
+        onWaiting?: ?Function,
118
+    |},
119
+
120
+    /**
121
+     * A styles that will be applied on the video element.
122
+     */
123
+    style: Object,
124
+
125
+    /**
126
+     * The value of the muted attribute for the underlying element.
127
+     */
128
+    muted?: boolean
33 129
 };
34 130
 
35 131
 /**
@@ -57,13 +153,27 @@ class VideoTrack extends AbstractVideoTrack<Props> {
57 153
      * @returns {ReactElement}
58 154
      */
59 155
     render() {
156
+        const {
157
+            _noAutoPlayVideo,
158
+            className,
159
+            id,
160
+            muted,
161
+            videoTrack,
162
+            style,
163
+            eventHandlers
164
+        } = this.props;
165
+
60 166
         return (
167
+
61 168
             <Video
62
-                autoPlay = { !this.props._noAutoPlayVideo }
63
-                className = { this.props.className }
64
-                id = { this.props.id }
169
+                autoPlay = { !_noAutoPlayVideo }
170
+                className = { className }
171
+                eventHandlers = { eventHandlers }
172
+                id = { id }
173
+                muted = { muted }
65 174
                 onVideoPlaying = { this._onVideoPlaying }
66
-                videoTrack = { this.props.videoTrack } />
175
+                style = { style }
176
+                videoTrack = { videoTrack } />
67 177
         );
68 178
     }
69 179
 

+ 1
- 0
react/features/base/settings/middleware.js 查看文件

@@ -14,6 +14,7 @@ import { SETTINGS_UPDATED } from './actionTypes';
14 14
 import { updateSettings } from './actions';
15 15
 import { handleCallIntegrationChange, handleCrashReportingChange } from './functions';
16 16
 
17
+
17 18
 /**
18 19
  * The middleware of the feature base/settings. Distributes changes to the state
19 20
  * of base/settings to the states of other features computed from the state of

+ 10
- 0
react/features/base/tracks/actionTypes.js 查看文件

@@ -75,6 +75,16 @@ export const TRACK_NO_DATA_FROM_SOURCE = 'TRACK_NO_DATA_FROM_SOURCE';
75 75
  */
76 76
 export const TRACK_REMOVED = 'TRACK_REMOVED';
77 77
 
78
+/**
79
+ * The type of redux action dispatched when a track has stopped.
80
+ *
81
+ * {
82
+ *      type: TRACK_STOPPED,
83
+ *      track: Track
84
+ * }
85
+ */
86
+export const TRACK_STOPPED = 'TRACK_STOPPED';
87
+
78 88
 /**
79 89
  * The type of redux action dispatched when a track's properties were updated.
80 90
  *

+ 11
- 3
react/features/base/tracks/actions.js 查看文件

@@ -25,9 +25,10 @@ import {
25 25
     TRACK_CREATE_ERROR,
26 26
     TRACK_NO_DATA_FROM_SOURCE,
27 27
     TRACK_REMOVED,
28
+    TRACK_STOPPED,
28 29
     TRACK_UPDATED,
29
-    TRACK_WILL_CREATE,
30
-    TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT
30
+    TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT,
31
+    TRACK_WILL_CREATE
31 32
 } from './actionTypes';
32 33
 import {
33 34
     createLocalTracksF,
@@ -400,8 +401,15 @@ export function trackAdded(track) {
400 401
 
401 402
                     noDataFromSourceNotificationInfo = { timeout };
402 403
                 }
403
-
404 404
             }
405
+
406
+            track.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED,
407
+                () => dispatch({
408
+                    type: TRACK_STOPPED,
409
+                    track: {
410
+                        jitsiTrack: track
411
+                    }
412
+                }));
405 413
         } else {
406 414
             participantId = track.getParticipantId();
407 415
             isReceivingData = true;

+ 0
- 1
react/features/base/tracks/middleware.js 查看文件

@@ -163,7 +163,6 @@ MiddlewareRegistry.register(store => next => action => {
163 163
                 } else {
164 164
                     APP.UI.setVideoMuted(participantID);
165 165
                 }
166
-                APP.UI.onPeerVideoTypeChanged(participantID, jitsiTrack.videoType);
167 166
             } else if (jitsiTrack.isLocal()) {
168 167
                 APP.conference.setAudioMuteStatus(muted);
169 168
             } else {

+ 15
- 1
react/features/connection-stats/components/ConnectionStatsTable.js 查看文件

@@ -145,6 +145,18 @@ type Props = {
145 145
     transport: Array<Object>
146 146
 };
147 147
 
148
+/**
149
+ * Click handler.
150
+ *
151
+ * @param {SyntheticEvent} event - The click event.
152
+ * @returns {void}
153
+ */
154
+function onClick(event) {
155
+    // If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
156
+    // needs to be stopped.
157
+    event.stopPropagation();
158
+}
159
+
148 160
 /**
149 161
  * React {@code Component} for displaying connection statistics.
150 162
  *
@@ -161,7 +173,9 @@ class ConnectionStatsTable extends Component<Props> {
161 173
         const { isLocalVideo, enableSaveLogs } = this.props;
162 174
 
163 175
         return (
164
-            <div className = 'connection-info'>
176
+            <div
177
+                className = 'connection-info'
178
+                onClick = { onClick }>
165 179
                 { this._renderStatistics() }
166 180
                 <div className = 'connection-actions'>
167 181
                     { isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}

+ 17
- 0
react/features/filmstrip/actions.web.js 查看文件

@@ -1,5 +1,6 @@
1 1
 // @flow
2 2
 
3
+import { pinParticipant } from '../base/participants';
3 4
 import { toState } from '../base/redux';
4 5
 import { CHAT_SIZE } from '../chat/constants';
5 6
 
@@ -69,4 +70,20 @@ export function setHorizontalViewDimensions(clientHeight: number = 0) {
69 70
     };
70 71
 }
71 72
 
73
+/**
74
+ * Emulates a click on the n-th video.
75
+ *
76
+ * @param {number} n - Number that identifies the video.
77
+ * @returns {Function}
78
+ */
79
+export function clickOnVideo(n: number) {
80
+    return (dispatch: Function, getState: Function) => {
81
+        const participants = getState()['features/base/participants'];
82
+        const nThParticipant = participants[n];
83
+        const { id, pinned } = nThParticipant;
84
+
85
+        dispatch(pinParticipant(pinned ? null : id));
86
+    };
87
+}
88
+
72 89
 export * from './actions.native';

+ 2
- 2
react/features/filmstrip/components/native/Thumbnail.js 查看文件

@@ -20,9 +20,9 @@ import { StyleType } from '../../../base/styles';
20 20
 import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
21 21
 import { ConnectionIndicator } from '../../../connection-indicator';
22 22
 import { DisplayNameLabel } from '../../../display-name';
23
-import { RemoteVideoMenu } from '../../../remote-video-menu';
24
-import ConnectionStatusComponent from '../../../remote-video-menu/components/native/ConnectionStatusComponent';
25 23
 import { toggleToolboxVisible } from '../../../toolbox/actions.native';
24
+import { RemoteVideoMenu } from '../../../video-menu';
25
+import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent';
26 26
 
27 27
 import AudioMutedIndicator from './AudioMutedIndicator';
28 28
 import DominantSpeakerIndicator from './DominantSpeakerIndicator';

+ 36
- 10
react/features/filmstrip/components/web/Filmstrip.js 查看文件

@@ -11,12 +11,15 @@ import {
11 11
 import { getToolbarButtons } from '../../../base/config';
12 12
 import { translate } from '../../../base/i18n';
13 13
 import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
14
+import { getLocalParticipant } from '../../../base/participants';
14 15
 import { connect } from '../../../base/redux';
15 16
 import { isButtonEnabled } from '../../../toolbox/functions.web';
16 17
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
17 18
 import { setFilmstripVisible } from '../../actions';
18 19
 import { shouldRemoteVideosBeVisible } from '../../functions';
19 20
 
21
+import Thumbnail from './Thumbnail';
22
+
20 23
 declare var APP: Object;
21 24
 declare var interfaceConfig: Object;
22 25
 
@@ -60,6 +63,11 @@ type Props = {
60 63
      */
61 64
     _isFilmstripButtonEnabled: boolean,
62 65
 
66
+    /**
67
+     * The participants in the call.
68
+     */
69
+    _participants: Array<Object>,
70
+
63 71
     /**
64 72
      * The number of rows in tile view.
65 73
      */
@@ -138,18 +146,15 @@ class Filmstrip extends Component <Props> {
138 146
      * @returns {ReactElement}
139 147
      */
140 148
     render() {
141
-        // Note: Appending of {@code RemoteVideo} views is handled through
142
-        // VideoLayout. The views do not get blown away on render() because
143
-        // ReactDOMComponent is only aware of the given JSX and not new appended
144
-        // DOM. As such, when updateDOMProperties gets called, only attributes
145
-        // will get updated without replacing the DOM. If the known DOM gets
146
-        // modified, then the views will get blown away.
147
-
148 149
         const filmstripStyle = { };
149 150
         const filmstripRemoteVideosContainerStyle = {};
150 151
         let remoteVideoContainerClassName = 'remote-videos-container';
152
+        const { _currentLayout, _participants } = this.props;
153
+        const remoteParticipants = _participants.filter(p => !p.local);
154
+        const localParticipant = getLocalParticipant(_participants);
155
+        const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
151 156
 
152
-        switch (this.props._currentLayout) {
157
+        switch (_currentLayout) {
153 158
         case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
154 159
             // Adding 18px for the 2px margins, 2px borders on the left and right and 5px padding on the left and right.
155 160
             // Also adding 7px for the scrollbar.
@@ -191,7 +196,13 @@ class Filmstrip extends Component <Props> {
191 196
                     <div
192 197
                         className = 'filmstrip__videos'
193 198
                         id = 'filmstripLocalVideo'>
194
-                        <div id = 'filmstripLocalVideoThumbnail' />
199
+                        <div id = 'filmstripLocalVideoThumbnail'>
200
+                            {
201
+                                !tileViewActive && <Thumbnail
202
+                                    key = 'local'
203
+                                    participantID = { localParticipant.id } />
204
+                            }
205
+                        </div>
195 206
                     </div>
196 207
                     <div
197 208
                         className = { remoteVideosWrapperClassName }
@@ -205,7 +216,21 @@ class Filmstrip extends Component <Props> {
205 216
                             className = { remoteVideoContainerClassName }
206 217
                             id = 'filmstripRemoteVideosContainer'
207 218
                             style = { filmstripRemoteVideosContainerStyle }>
208
-                            <div id = 'localVideoTileViewContainer' />
219
+                            {
220
+                                remoteParticipants.map(
221
+                                    p => (
222
+                                        <Thumbnail
223
+                                            key = { `remote_${p.id}` }
224
+                                            participantID = { p.id } />
225
+                                    ))
226
+                            }
227
+                            <div id = 'localVideoTileViewContainer'>
228
+                                {
229
+                                    tileViewActive && <Thumbnail
230
+                                        key = 'local'
231
+                                        participantID = { localParticipant.id } />
232
+                                }
233
+                            </div>
209 234
                         </div>
210 235
                     </div>
211 236
                 </div>
@@ -314,6 +339,7 @@ function _mapStateToProps(state) {
314 339
         _hideScrollbar: Boolean(iAmSipGateway),
315 340
         _hideToolbar: Boolean(iAmSipGateway),
316 341
         _isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
342
+        _participants: state['features/base/participants'],
317 343
         _rows: gridDimensions.rows,
318 344
         _videosClassName: videosClassName,
319 345
         _visible: visible

+ 474
- 79
react/features/filmstrip/components/web/Thumbnail.js 查看文件

@@ -1,8 +1,8 @@
1 1
 // @flow
2 2
 
3
-import { AtlasKitThemeProvider } from '@atlaskit/theme';
4 3
 import React, { Component } from 'react';
5 4
 
5
+import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
6 6
 import { AudioLevelIndicator } from '../../../audio-level-indicator';
7 7
 import { Avatar } from '../../../base/avatar';
8 8
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
@@ -11,42 +11,72 @@ import AudioTrack from '../../../base/media/components/web/AudioTrack';
11 11
 import {
12 12
     getLocalParticipant,
13 13
     getParticipantById,
14
-    getParticipantCount
14
+    getParticipantCount,
15
+    pinParticipant
15 16
 } from '../../../base/participants';
16 17
 import { connect } from '../../../base/redux';
17
-import { getLocalAudioTrack, getLocalVideoTrack, getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
18
+import { isTestModeEnabled } from '../../../base/testing';
19
+import {
20
+    getLocalAudioTrack,
21
+    getLocalVideoTrack,
22
+    getTrackByMediaTypeAndParticipant,
23
+    updateLastTrackVideoMediaEvent
24
+} from '../../../base/tracks';
18 25
 import { ConnectionIndicator } from '../../../connection-indicator';
19 26
 import { DisplayName } from '../../../display-name';
20 27
 import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
21 28
 import { PresenceLabel } from '../../../presence-status';
22
-import { RemoteVideoMenuTriggerButton } from '../../../remote-video-menu';
23 29
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
30
+import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
31
+import {
32
+    DISPLAY_MODE_TO_CLASS_NAME,
33
+    DISPLAY_MODE_TO_STRING,
34
+    DISPLAY_VIDEO,
35
+    DISPLAY_VIDEO_WITH_NAME,
36
+    VIDEO_TEST_EVENTS
37
+} from '../../constants';
38
+import { isVideoPlayable, computeDisplayMode } from '../../functions';
39
+import logger from '../../logger';
24 40
 
25 41
 const JitsiTrackEvents = JitsiMeetJS.events.track;
26 42
 
27 43
 declare var interfaceConfig: Object;
28 44
 
29
-
30 45
 /**
31 46
  * The type of the React {@code Component} state of {@link Thumbnail}.
32 47
  */
33
-type State = {
48
+export type State = {|
34 49
 
35 50
     /**
36 51
      * The current audio level value for the Thumbnail.
37 52
      */
38 53
     audioLevel: number,
39 54
 
55
+    /**
56
+     * Indicates that the canplay event has been received.
57
+     */
58
+    canPlayEventReceived: boolean,
59
+
60
+    /**
61
+     * The current display mode of the thumbnail.
62
+     */
63
+    displayMode: number,
64
+
65
+    /**
66
+     * Indicates whether the thumbnail is hovered or not.
67
+     */
68
+    isHovered: boolean,
69
+
40 70
     /**
41 71
      * The current volume setting for the Thumbnail.
42 72
      */
43 73
     volume: ?number
44
-};
74
+|};
45 75
 
46 76
 /**
47 77
  * The type of the React {@code Component} props of {@link Thumbnail}.
48 78
  */
49
-type Props = {
79
+export type Props = {|
50 80
 
51 81
     /**
52 82
      * The audio track related to the participant.
@@ -73,11 +103,21 @@ type Props = {
73 103
      */
74 104
     _defaultLocalDisplayName: string,
75 105
 
106
+    /**
107
+     * Indicates whether the local video flip feature is disabled or not.
108
+     */
109
+    _disableLocalVideoFlip: boolean,
110
+
76 111
     /**
77 112
      * Indicates whether the profile functionality is disabled.
78 113
      */
79 114
     _disableProfile: boolean,
80 115
 
116
+    /**
117
+     * The display mode of the thumbnail.
118
+     */
119
+    _displayMode: number,
120
+
81 121
     /**
82 122
      * The height of the Thumbnail.
83 123
      */
@@ -88,16 +128,51 @@ type Props = {
88 128
      */
89 129
     _heightToWidthPercent: number,
90 130
 
131
+    /**
132
+     * Indicates whether the thumbnail should be hidden or not.
133
+     */
134
+    _isHidden: boolean,
135
+
136
+    /**
137
+     * Indicates whether audio only mode is enabled.
138
+     */
139
+    _isAudioOnly: boolean,
140
+
141
+    /**
142
+     * Indicates whether the participant associated with the thumbnail is displayed on the large video.
143
+     */
144
+    _isCurrentlyOnLargeVideo: boolean,
145
+
146
+    /**
147
+     * Indicates whether the participant is screen sharing.
148
+     */
149
+    _isScreenSharing: boolean,
150
+
151
+    /**
152
+     * Indicates whether the video associated with the thumbnail is playable.
153
+     */
154
+    _isVideoPlayable: boolean,
155
+
91 156
     /**
92 157
      * Disable/enable the dominant speaker indicator.
93 158
      */
94 159
     _isDominantSpeakerDisabled: boolean,
95 160
 
161
+    /**
162
+     * Indicates whether testing mode is enabled.
163
+     */
164
+    _isTestModeEnabled: boolean,
165
+
96 166
     /**
97 167
      * The size of the icon of indicators.
98 168
      */
99 169
     _indicatorIconSize: number,
100 170
 
171
+    /**
172
+     * The current local video flip setting.
173
+     */
174
+    _localFlipX: boolean,
175
+
101 176
     /**
102 177
      * An object with information about the participant related to the thumbnaul.
103 178
      */
@@ -128,16 +203,23 @@ type Props = {
128 203
      */
129 204
     dispatch: Function,
130 205
 
131
-    /**
132
-     * Indicates whether the thumbnail is hovered or not.
133
-     */
134
-    isHovered: ?boolean,
135
-
136 206
     /**
137 207
      * The ID of the participant related to the thumbnail.
138 208
      */
139 209
     participantID: ?string
140
-};
210
+|};
211
+
212
+/**
213
+ * Click handler for the display name container.
214
+ *
215
+ * @param {SyntheticEvent} event - The click event.
216
+ * @returns {void}
217
+ */
218
+function onClick(event) {
219
+    // If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
220
+    // needs to be stopped.
221
+    event.stopPropagation();
222
+}
141 223
 
142 224
 /**
143 225
  * Implements a thumbnail.
@@ -145,7 +227,6 @@ type Props = {
145 227
  * @extends Component
146 228
  */
147 229
 class Thumbnail extends Component<Props, State> {
148
-
149 230
     /**
150 231
      * Initializes a new Thumbnail instance.
151 232
      *
@@ -155,14 +236,27 @@ class Thumbnail extends Component<Props, State> {
155 236
     constructor(props: Props) {
156 237
         super(props);
157 238
 
158
-        this.state = {
239
+        const state = {
159 240
             audioLevel: 0,
160
-            volume: undefined
241
+            canPlayEventReceived: false,
242
+            isHovered: false,
243
+            volume: undefined,
244
+            displayMode: DISPLAY_VIDEO
245
+        };
246
+
247
+        this.state = {
248
+            ...state,
249
+            displayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, state))
161 250
         };
162 251
 
163 252
         this._updateAudioLevel = this._updateAudioLevel.bind(this);
253
+        this._onCanPlay = this._onCanPlay.bind(this);
254
+        this._onClick = this._onClick.bind(this);
164 255
         this._onVolumeChange = this._onVolumeChange.bind(this);
165 256
         this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
257
+        this._onMouseEnter = this._onMouseEnter.bind(this);
258
+        this._onMouseLeave = this._onMouseLeave.bind(this);
259
+        this._onTestingEvent = this._onTestingEvent.bind(this);
166 260
     }
167 261
 
168 262
     /**
@@ -173,6 +267,7 @@ class Thumbnail extends Component<Props, State> {
173 267
      */
174 268
     componentDidMount() {
175 269
         this._listenForAudioUpdates();
270
+        this._onDisplayModeChanged();
176 271
     }
177 272
 
178 273
     /**
@@ -182,12 +277,121 @@ class Thumbnail extends Component<Props, State> {
182 277
      * @inheritdoc
183 278
      * @returns {void}
184 279
      */
185
-    componentDidUpdate(prevProps: Props) {
280
+    componentDidUpdate(prevProps: Props, prevState: State) {
186 281
         if (prevProps._audioTrack !== this.props._audioTrack) {
187 282
             this._stopListeningForAudioUpdates(prevProps._audioTrack);
188 283
             this._listenForAudioUpdates();
189 284
             this._updateAudioLevel(0);
190 285
         }
286
+
287
+        if (prevState.displayMode !== this.state.displayMode) {
288
+            this._onDisplayModeChanged();
289
+        }
290
+    }
291
+
292
+    /**
293
+     * Handles display mode changes.
294
+     *
295
+     * @returns {void}
296
+     */
297
+    _onDisplayModeChanged() {
298
+        const input = Thumbnail.getDisplayModeInput(this.props, this.state);
299
+        const displayModeString = DISPLAY_MODE_TO_STRING[this.state.displayMode];
300
+        const id = this.props._participant?.id;
301
+
302
+        this._maybeSendScreenSharingIssueEvents(input);
303
+        logger.debug(`Displaying ${displayModeString} for ${id}, data: [${JSON.stringify(input)}]`);
304
+    }
305
+
306
+    /**
307
+     * Sends screen sharing issue event if an issue is detected.
308
+     *
309
+     * @param {Object} input - The input used to compute the thumbnail display mode.
310
+     * @returns {void}
311
+     */
312
+    _maybeSendScreenSharingIssueEvents(input) {
313
+        const {
314
+            _currentLayout,
315
+            _isAudioOnly,
316
+            _isScreenSharing
317
+        } = this.props;
318
+        const { displayMode } = this.state;
319
+        const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
320
+
321
+        if (![ DISPLAY_VIDEO, DISPLAY_VIDEO_WITH_NAME ].includes(displayMode)
322
+            && tileViewActive
323
+            && _isScreenSharing
324
+            && !_isAudioOnly) {
325
+            sendAnalytics(createScreenSharingIssueEvent({
326
+                source: 'thumbnail',
327
+                ...input
328
+            }));
329
+        }
330
+    }
331
+
332
+    /**
333
+     * Implements React's {@link Component#getDerivedStateFromProps()}.
334
+     *
335
+     * @inheritdoc
336
+     */
337
+    static getDerivedStateFromProps(props: Props, prevState: State) {
338
+        if (!props._videoTrack && prevState.canPlayEventReceived) {
339
+            const newState = {
340
+                ...prevState,
341
+                canPlayEventReceived: false
342
+            };
343
+
344
+            return {
345
+                ...newState,
346
+                dispayMode: computeDisplayMode(Thumbnail.getDisplayModeInput(props, newState))
347
+            };
348
+        }
349
+
350
+        const newDisplayMode = computeDisplayMode(Thumbnail.getDisplayModeInput(props, prevState));
351
+
352
+        if (newDisplayMode !== prevState.displayMode) {
353
+            return {
354
+                ...prevState,
355
+                displayMode: newDisplayMode
356
+            };
357
+        }
358
+
359
+        return null;
360
+    }
361
+
362
+    /**
363
+     * Extracts information for props and state needed to compute the display mode.
364
+     *
365
+     * @param {Props} props - The component's props.
366
+     * @param {State} state - The component's state.
367
+     * @returns {Object}
368
+     */
369
+    static getDisplayModeInput(props: Props, state: State) {
370
+        const {
371
+            _currentLayout,
372
+            _isAudioOnly,
373
+            _isCurrentlyOnLargeVideo,
374
+            _isScreenSharing,
375
+            _isVideoPlayable,
376
+            _participant,
377
+            _videoTrack
378
+        } = props;
379
+        const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
380
+        const { canPlayEventReceived, isHovered } = state;
381
+
382
+        return {
383
+            isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
384
+            isHovered,
385
+            isAudioOnly: _isAudioOnly,
386
+            tileViewActive,
387
+            isVideoPlayable: _isVideoPlayable,
388
+            connectionStatus: _participant?.connectionStatus,
389
+            canPlayEventReceived,
390
+            videoStream: Boolean(_videoTrack),
391
+            isRemoteParticipant: !_participant?.isFakeParticipant && !_participant?.local,
392
+            isScreenSharing: _isScreenSharing,
393
+            videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
394
+        };
191 395
     }
192 396
 
193 397
     /**
@@ -253,8 +457,14 @@ class Thumbnail extends Component<Props, State> {
253 457
      * @returns {Object} - The styles for the thumbnail.
254 458
      */
255 459
     _getStyles(): Object {
256
-        const { _height, _heightToWidthPercent, _currentLayout } = this.props;
257
-        let styles;
460
+        const { _height, _heightToWidthPercent, _currentLayout, _isHidden, _width } = this.props;
461
+        let styles: {
462
+            thumbnail: Object,
463
+            avatar: Object
464
+        } = {
465
+            thumbnail: {},
466
+            avatar: {}
467
+        };
258 468
 
259 469
         switch (_currentLayout) {
260 470
         case LAYOUTS.TILE_VIEW:
@@ -262,7 +472,13 @@ class Thumbnail extends Component<Props, State> {
262 472
             const avatarSize = _height / 2;
263 473
 
264 474
             styles = {
265
-                avatarContainer: {
475
+                thumbnail: {
476
+                    height: `${_height}px`,
477
+                    minHeight: `${_height}px`,
478
+                    minWidth: `${_width}px`,
479
+                    width: `${_width}px`
480
+                },
481
+                avatar: {
266 482
                     height: `${avatarSize}px`,
267 483
                     width: `${avatarSize}px`
268 484
                 }
@@ -271,7 +487,10 @@ class Thumbnail extends Component<Props, State> {
271 487
         }
272 488
         case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
273 489
             styles = {
274
-                avatarContainer: {
490
+                thumbnail: {
491
+                    paddingTop: `${_heightToWidthPercent}%`
492
+                },
493
+                avatar: {
275 494
                     height: '50%',
276 495
                     width: `${_heightToWidthPercent / 2}%`
277 496
                 }
@@ -280,9 +499,49 @@ class Thumbnail extends Component<Props, State> {
280 499
         }
281 500
         }
282 501
 
502
+        if (_isHidden) {
503
+            styles.thumbnail.display = 'none';
504
+        }
505
+
283 506
         return styles;
284 507
     }
285 508
 
509
+    _onClick: () => void;
510
+
511
+    /**
512
+     * On click handler.
513
+     *
514
+     * @returns {void}
515
+     */
516
+    _onClick() {
517
+        const { _participant, dispatch } = this.props;
518
+        const { id, pinned } = _participant;
519
+
520
+        dispatch(pinParticipant(pinned ? null : id));
521
+    }
522
+
523
+    _onMouseEnter: () => void;
524
+
525
+    /**
526
+     * Mouse enter handler.
527
+     *
528
+     * @returns {void}
529
+     */
530
+    _onMouseEnter() {
531
+        this.setState({ isHovered: true });
532
+    }
533
+
534
+    _onMouseLeave: () => void;
535
+
536
+    /**
537
+     * Mouse leave handler.
538
+     *
539
+     * @returns {void}
540
+     */
541
+    _onMouseLeave() {
542
+        this.setState({ isHovered: false });
543
+    }
544
+
286 545
     /**
287 546
      * Renders a fake participant (youtube video) thumbnail.
288 547
      *
@@ -292,9 +551,17 @@ class Thumbnail extends Component<Props, State> {
292 551
     _renderFakeParticipant() {
293 552
         const { _participant } = this.props;
294 553
         const { id } = _participant;
554
+        const styles = this._getStyles();
555
+        const containerClassName = this._getContainerClassName();
295 556
 
296 557
         return (
297
-            <>
558
+            <span
559
+                className = { containerClassName }
560
+                id = 'sharedVideoContainer'
561
+                onClick = { this._onClick }
562
+                onMouseEnter = { this._onMouseEnter }
563
+                onMouseLeave = { this._onMouseLeave }
564
+                style = { styles.thumbnail }>
298 565
                 <img
299 566
                     className = 'sharedVideoAvatar'
300 567
                     src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
@@ -303,7 +570,7 @@ class Thumbnail extends Component<Props, State> {
303 570
                         elementID = 'sharedVideoContainer_name'
304 571
                         participantID = { id } />
305 572
                 </div>
306
-            </>
573
+            </span>
307 574
         );
308 575
     }
309 576
 
@@ -320,9 +587,9 @@ class Thumbnail extends Component<Props, State> {
320 587
             _isDominantSpeakerDisabled,
321 588
             _indicatorIconSize: iconSize,
322 589
             _participant,
323
-            _participantCount,
324
-            isHovered
590
+            _participantCount
325 591
         } = this.props;
592
+        const { isHovered } = this.state;
326 593
         const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
327 594
         const { id, local = false, dominantSpeaker = false } = _participant;
328 595
         const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
@@ -344,43 +611,41 @@ class Thumbnail extends Component<Props, State> {
344 611
 
345 612
         return (
346 613
             <div>
347
-                <AtlasKitThemeProvider mode = 'dark'>
348
-                    { !_connectionIndicatorDisabled
349
-                        && <ConnectionIndicator
350
-                            alwaysVisible = { showConnectionIndicator }
351
-                            enableStatsDisplay = { true }
352
-                            iconSize = { iconSize }
353
-                            isLocalVideo = { local }
354
-                            participantId = { id }
355
-                            statsPopoverPosition = { statsPopoverPosition } />
356
-                    }
357
-                    <RaisedHandIndicator
614
+                { !_connectionIndicatorDisabled
615
+                    && <ConnectionIndicator
616
+                        alwaysVisible = { showConnectionIndicator }
617
+                        enableStatsDisplay = { true }
358 618
                         iconSize = { iconSize }
619
+                        isLocalVideo = { local }
359 620
                         participantId = { id }
621
+                        statsPopoverPosition = { statsPopoverPosition } />
622
+                }
623
+                <RaisedHandIndicator
624
+                    iconSize = { iconSize }
625
+                    participantId = { id }
626
+                    tooltipPosition = { tooltipPosition } />
627
+                { showDominantSpeaker && _participantCount > 2
628
+                    && <DominantSpeakerIndicator
629
+                        iconSize = { iconSize }
360 630
                         tooltipPosition = { tooltipPosition } />
361
-                    { showDominantSpeaker && _participantCount > 2
362
-                        && <DominantSpeakerIndicator
363
-                            iconSize = { iconSize }
364
-                            tooltipPosition = { tooltipPosition } />
365
-                    }
366
-                </AtlasKitThemeProvider>
631
+                }
367 632
             </div>);
368 633
     }
369 634
 
370 635
     /**
371 636
      * Renders the avatar.
372 637
      *
638
+     * @param {Object} styles - The styles that will be applied to the avatar.
373 639
      * @returns {ReactElement}
374 640
      */
375
-    _renderAvatar() {
641
+    _renderAvatar(styles) {
376 642
         const { _participant } = this.props;
377 643
         const { id } = _participant;
378
-        const styles = this._getStyles();
379 644
 
380 645
         return (
381 646
             <div
382 647
                 className = 'avatar-container'
383
-                style = { styles.avatarContainer }>
648
+                style = { styles }>
384 649
                 <Avatar
385 650
                     className = 'userAvatar'
386 651
                     participantId = { id } />
@@ -388,6 +653,38 @@ class Thumbnail extends Component<Props, State> {
388 653
         );
389 654
     }
390 655
 
656
+    /**
657
+     * Returns the container class name.
658
+     *
659
+     * @returns {string} - The class name that will be used for the container.
660
+     */
661
+    _getContainerClassName() {
662
+        let className = 'videocontainer';
663
+        const { displayMode } = this.state;
664
+        const { _isAudioOnly, _isDominantSpeakerDisabled, _isHidden, _participant } = this.props;
665
+        const isRemoteParticipant = !_participant?.local && !_participant?.isFakeParticipant;
666
+
667
+        className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`;
668
+
669
+        if (_participant?.pinned) {
670
+            className += ' videoContainerFocused';
671
+        }
672
+
673
+        if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
674
+            className += ' active-speaker';
675
+        }
676
+
677
+        if (_isHidden) {
678
+            className += ' hidden';
679
+        }
680
+
681
+        if (isRemoteParticipant && _isAudioOnly) {
682
+            className += ' audio-only';
683
+        }
684
+
685
+        return className;
686
+    }
687
+
391 688
     /**
392 689
      * Renders the local participant's thumbnail.
393 690
      *
@@ -396,19 +693,33 @@ class Thumbnail extends Component<Props, State> {
396 693
     _renderLocalParticipant() {
397 694
         const {
398 695
             _defaultLocalDisplayName,
696
+            _disableLocalVideoFlip,
697
+            _isScreenSharing,
698
+            _localFlipX,
399 699
             _disableProfile,
400 700
             _participant,
401 701
             _videoTrack
402 702
         } = this.props;
403 703
         const { id } = _participant || {};
404 704
         const { audioLevel } = this.state;
705
+        const styles = this._getStyles();
706
+        const containerClassName = this._getContainerClassName();
707
+        const videoTrackClassName
708
+            = !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
405 709
 
406 710
 
407 711
         return (
408
-            <>
712
+            <span
713
+                className = { containerClassName }
714
+                id = 'localVideoContainer'
715
+                onClick = { this._onClick }
716
+                onMouseEnter = { this._onMouseEnter }
717
+                onMouseLeave = { this._onMouseLeave }
718
+                style = { styles.thumbnail }>
409 719
                 <div className = 'videocontainer__background' />
410 720
                 <span id = 'localVideoWrapper'>
411 721
                     <VideoTrack
722
+                        className = { videoTrackClassName }
412 723
                         id = 'localVideo_container'
413 724
                         videoTrack = { _videoTrack } />
414 725
                 </span>
@@ -419,21 +730,64 @@ class Thumbnail extends Component<Props, State> {
419 730
                     { this._renderTopIndicators() }
420 731
                 </div>
421 732
                 <div className = 'videocontainer__hoverOverlay' />
422
-                <div className = 'displayNameContainer'>
733
+                <div
734
+                    className = 'displayNameContainer'
735
+                    onClick = { onClick }>
423 736
                     <DisplayName
424 737
                         allowEditing = { !_disableProfile }
425 738
                         displayNameSuffix = { _defaultLocalDisplayName }
426 739
                         elementID = 'localDisplayName'
427 740
                         participantID = { id } />
428 741
                 </div>
429
-                { this._renderAvatar() }
742
+                { this._renderAvatar(styles.avatar) }
743
+                <span className = 'localvideomenu'>
744
+                    <LocalVideoMenuTriggerButton />
745
+                </span>
430 746
                 <span className = 'audioindicator-container'>
431 747
                     <AudioLevelIndicator audioLevel = { audioLevel } />
432 748
                 </span>
433
-            </>
749
+            </span>
434 750
         );
435 751
     }
436 752
 
753
+    _onCanPlay: Object => void;
754
+
755
+    /**
756
+     * Canplay event listener.
757
+     *
758
+     * @param {SyntheticEvent} event - The event.
759
+     * @returns {void}
760
+     */
761
+    _onCanPlay(event) {
762
+        this.setState({ canPlayEventReceived: true });
763
+
764
+        const {
765
+            _isTestModeEnabled,
766
+            _videoTrack
767
+        } = this.props;
768
+
769
+        if (_videoTrack && _isTestModeEnabled) {
770
+            this._onTestingEvent(event);
771
+        }
772
+    }
773
+
774
+    _onTestingEvent: Object => void;
775
+
776
+    /**
777
+     * Event handler for testing events.
778
+     *
779
+     * @param {SyntheticEvent} event - The event.
780
+     * @returns {void}
781
+     */
782
+    _onTestingEvent(event) {
783
+        const {
784
+            _videoTrack,
785
+            dispatch
786
+        } = this.props;
787
+        const jitsiVideoTrack = _videoTrack?.jitsiTrack;
788
+
789
+        dispatch(updateLastTrackVideoMediaEvent(jitsiVideoTrack, event.type));
790
+    }
437 791
 
438 792
     /**
439 793
      * Renders a remote participant's 'thumbnail.
@@ -443,29 +797,59 @@ class Thumbnail extends Component<Props, State> {
443 797
     _renderRemoteParticipant() {
444 798
         const {
445 799
             _audioTrack,
800
+            _isTestModeEnabled,
446 801
             _participant,
447
-            _startSilent
802
+            _startSilent,
803
+            _videoTrack
448 804
         } = this.props;
449 805
         const { id } = _participant;
450
-        const { audioLevel, volume } = this.state;
806
+        const { audioLevel, canPlayEventReceived, volume } = this.state;
807
+        const styles = this._getStyles();
808
+        const containerClassName = this._getContainerClassName();
451 809
 
452 810
         // hide volume when in silent mode
453 811
         const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
454
-        const jitsiTrack = _audioTrack?.jitsiTrack;
455
-        const audioTrackId = jitsiTrack && jitsiTrack.getId();
812
+        const jitsiAudioTrack = _audioTrack?.jitsiTrack;
813
+        const audioTrackId = jitsiAudioTrack && jitsiAudioTrack.getId();
814
+        const jitsiVideoTrack = _videoTrack?.jitsiTrack;
815
+        const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
816
+        const videoEventListeners = {};
817
+
818
+        if (_videoTrack && _isTestModeEnabled) {
819
+            VIDEO_TEST_EVENTS.forEach(attribute => {
820
+                videoEventListeners[attribute] = this._onTestingEvent;
821
+            });
822
+        }
823
+
824
+        videoEventListeners.onCanPlay = this._onCanPlay;
825
+
826
+        const videoElementStyle = canPlayEventReceived ? null : {
827
+            display: 'none'
828
+        };
456 829
 
457 830
         return (
458
-            <>
831
+            <span
832
+                className = { containerClassName }
833
+                id = { `participant_${id}` }
834
+                onClick = { this._onClick }
835
+                onMouseEnter = { this._onMouseEnter }
836
+                onMouseLeave = { this._onMouseLeave }
837
+                style = { styles.thumbnail }>
459 838
                 {
460
-                    _audioTrack
461
-                        ? <AudioTrack
462
-                            audioTrack = { _audioTrack }
463
-                            id = { `remoteAudio_${audioTrackId || ''}` }
464
-                            muted = { _startSilent }
465
-                            onInitialVolumeSet = { this._onInitialVolumeSet }
466
-                            volume = { this.state.volume } />
467
-                        : null
468
-
839
+                    _videoTrack && <VideoTrack
840
+                        eventHandlers = { videoEventListeners }
841
+                        id = { `remoteVideo_${videoTrackId || ''}` }
842
+                        muted = { true }
843
+                        style = { videoElementStyle }
844
+                        videoTrack = { _videoTrack } />
845
+                }
846
+                {
847
+                    _audioTrack && <AudioTrack
848
+                        audioTrack = { _audioTrack }
849
+                        id = { `remoteAudio_${audioTrackId || ''}` }
850
+                        muted = { _startSilent }
851
+                        onInitialVolumeSet = { this._onInitialVolumeSet }
852
+                        volume = { volume } />
469 853
                 }
470 854
                 <div className = 'videocontainer__background' />
471 855
                 <div className = 'videocontainer__toptoolbar'>
@@ -480,24 +864,22 @@ class Thumbnail extends Component<Props, State> {
480 864
                         elementID = { `participant_${id}_name` }
481 865
                         participantID = { id } />
482 866
                 </div>
483
-                { this._renderAvatar() }
867
+                { this._renderAvatar(styles.avatar) }
484 868
                 <div className = 'presence-label-container'>
485 869
                     <PresenceLabel
486 870
                         className = 'presence-label'
487 871
                         participantID = { id } />
488 872
                 </div>
489 873
                 <span className = 'remotevideomenu'>
490
-                    <AtlasKitThemeProvider mode = 'dark'>
491
-                        <RemoteVideoMenuTriggerButton
492
-                            initialVolumeValue = { volume }
493
-                            onVolumeChange = { onVolumeChange }
494
-                            participantID = { id } />
495
-                    </AtlasKitThemeProvider>
874
+                    <RemoteVideoMenuTriggerButton
875
+                        initialVolumeValue = { volume }
876
+                        onVolumeChange = { onVolumeChange }
877
+                        participantID = { id } />
496 878
                 </span>
497 879
                 <span className = 'audioindicator-container'>
498 880
                     <AudioLevelIndicator audioLevel = { audioLevel } />
499 881
                 </span>
500
-            </>
882
+            </span>
501 883
         );
502 884
     }
503 885
 
@@ -527,7 +909,6 @@ class Thumbnail extends Component<Props, State> {
527 909
         this.setState({ volume: value });
528 910
     }
529 911
 
530
-
531 912
     /**
532 913
      * Implements React's {@link Component#render()}.
533 914
      *
@@ -568,17 +949,24 @@ function _mapStateToProps(state, ownProps): Object {
568 949
 
569 950
     // Only the local participant won't have id for the time when the conference is not yet joined.
570 951
     const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
952
+    const { id } = participant;
571 953
     const isLocal = participant?.local ?? true;
954
+    const tracks = state['features/base/tracks'];
572 955
     const _videoTrack = isLocal
573
-        ? getLocalVideoTrack(state['features/base/tracks'])
574
-        : getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
956
+        ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
575 957
     const _audioTrack = isLocal
576
-        ? getLocalAudioTrack(state['features/base/tracks'])
577
-        : getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
958
+        ? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
578 959
     const _currentLayout = getCurrentLayout(state);
579 960
     let size = {};
580
-    const { startSilent, disableProfile = false } = state['features/base/config'];
961
+    const {
962
+        startSilent,
963
+        disableLocalVideoFlip,
964
+        disableProfile,
965
+        iAmRecorder,
966
+        iAmSipGateway
967
+    } = state['features/base/config'];
581 968
     const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
969
+    const { localFlipX } = state['features/base/settings'];
582 970
 
583 971
 
584 972
     switch (_currentLayout) {
@@ -617,16 +1005,23 @@ function _mapStateToProps(state, ownProps): Object {
617 1005
     }
618 1006
     }
619 1007
 
620
-
621 1008
     return {
622 1009
         _audioTrack,
623 1010
         _connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
624 1011
         _connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
625 1012
         _currentLayout,
626 1013
         _defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
1014
+        _disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
627 1015
         _disableProfile: disableProfile,
1016
+        _isHidden: isLocal && iAmRecorder && !iAmSipGateway,
1017
+        _isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
1018
+        _isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
628 1019
         _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
1020
+        _isScreenSharing: _videoTrack?.videoType === 'desktop',
1021
+        _isTestModeEnabled: isTestModeEnabled(state),
1022
+        _isVideoPlayable: isVideoPlayable(state, id),
629 1023
         _indicatorIconSize: NORMAL,
1024
+        _localFlipX: Boolean(localFlipX),
630 1025
         _participant: participant,
631 1026
         _participantCount: getParticipantCount(state),
632 1027
         _startSilent: Boolean(startSilent),

+ 89
- 0
react/features/filmstrip/constants.js 查看文件

@@ -47,3 +47,92 @@ export const DEFAULT_MAX_COLUMNS = 5;
47 47
  * An extended number of columns for tile view.
48 48
  */
49 49
 export const ABSOLUTE_MAX_COLUMNS = 7;
50
+
51
+/**
52
+ * An array of attributes of the video element that will be used for adding a listener for every event in the list.
53
+ * The latest event will be stored in redux. This is currently used by torture only.
54
+ */
55
+export const VIDEO_TEST_EVENTS = [
56
+    'onAbort',
57
+    'onCanPlay',
58
+    'onCanPlayThrough',
59
+    'onEmptied',
60
+    'onEnded',
61
+    'onError',
62
+    'onLoadedData',
63
+    'onLoadedMetadata',
64
+    'onLoadStart',
65
+    'onPause',
66
+    'onPlay',
67
+    'onPlaying',
68
+    'onRateChange',
69
+    'onStalled',
70
+    'onSuspend',
71
+    'onWaiting'
72
+];
73
+
74
+
75
+/**
76
+ * Display mode constant used when video is being displayed on the small video.
77
+ * @type {number}
78
+ * @constant
79
+ */
80
+export const DISPLAY_VIDEO = 0;
81
+
82
+/**
83
+ * Display mode constant used when the user's avatar is being displayed on
84
+ * the small video.
85
+ * @type {number}
86
+ * @constant
87
+ */
88
+export const DISPLAY_AVATAR = 1;
89
+
90
+/**
91
+ * Display mode constant used when neither video nor avatar is being displayed
92
+ * on the small video. And we just show the display name.
93
+ * @type {number}
94
+ * @constant
95
+ */
96
+export const DISPLAY_BLACKNESS_WITH_NAME = 2;
97
+
98
+/**
99
+ * Display mode constant used when video is displayed and display name
100
+ * at the same time.
101
+ * @type {number}
102
+ * @constant
103
+ */
104
+export const DISPLAY_VIDEO_WITH_NAME = 3;
105
+
106
+/**
107
+ * Display mode constant used when neither video nor avatar is being displayed
108
+ * on the small video. And we just show the display name.
109
+ * @type {number}
110
+ * @constant
111
+ */
112
+export const DISPLAY_AVATAR_WITH_NAME = 4;
113
+
114
+/**
115
+ * Maps the display modes to class name that will be applied on the thumbnail container.
116
+ * @type {Array<string>}
117
+ * @constant
118
+ */
119
+export const DISPLAY_MODE_TO_CLASS_NAME = [
120
+    'display-video',
121
+    'display-avatar-only',
122
+    'display-name-on-black',
123
+    'display-name-on-video',
124
+    'display-avatar-with-name'
125
+];
126
+
127
+/**
128
+ * Maps the display modes to string.
129
+ * @type {Array<string>}
130
+ * @constant
131
+ */
132
+export const DISPLAY_MODE_TO_STRING = [
133
+    'video',
134
+    'avatar',
135
+    'blackness-with-name',
136
+    'video-with-name',
137
+    'avatar-with-name'
138
+];

+ 43
- 1
react/features/filmstrip/functions.web.js 查看文件

@@ -16,7 +16,16 @@ import {
16 16
     isRemoteTrackMuted
17 17
 } from '../base/tracks/functions';
18 18
 
19
-import { ASPECT_RATIO_BREAKPOINT, SQUARE_TILE_ASPECT_RATIO, TILE_ASPECT_RATIO } from './constants';
19
+import {
20
+    ASPECT_RATIO_BREAKPOINT,
21
+    DISPLAY_AVATAR,
22
+    DISPLAY_AVATAR_WITH_NAME,
23
+    DISPLAY_BLACKNESS_WITH_NAME,
24
+    DISPLAY_VIDEO,
25
+    DISPLAY_VIDEO_WITH_NAME,
26
+    SQUARE_TILE_ASPECT_RATIO,
27
+    TILE_ASPECT_RATIO
28
+} from './constants';
20 29
 
21 30
 declare var interfaceConfig: Object;
22 31
 
@@ -176,3 +185,36 @@ export function getVerticalFilmstripVisibleAreaWidth() {
176 185
 
177 186
     return Math.min(filmstripMaxWidth, window.innerWidth);
178 187
 }
188
+
189
+/**
190
+ * Computes information that determine the display mode.
191
+ *
192
+ * @param {Object} input - Obejct containing all necessary information for determining the display mode for
193
+ * the thumbnail.
194
+ * @returns {number} - One of <tt>DISPLAY_VIDEO</tt>, <tt>DISPLAY_AVATAR</tt> or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
195
+*/
196
+export function computeDisplayMode(input: Object) {
197
+    const {
198
+        isAudioOnly,
199
+        isCurrentlyOnLargeVideo,
200
+        isScreenSharing,
201
+        canPlayEventReceived,
202
+        isHovered,
203
+        isRemoteParticipant,
204
+        tileViewActive
205
+    } = input;
206
+    const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
207
+
208
+    if (!tileViewActive && isScreenSharing && isRemoteParticipant) {
209
+        return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
210
+    } else if (isCurrentlyOnLargeVideo && !tileViewActive) {
211
+        // Display name is always and only displayed when user is on the stage
212
+        return adjustedIsVideoPlayable && !isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
213
+    } else if (adjustedIsVideoPlayable && !isAudioOnly) {
214
+        // check hovering and change state to video with name
215
+        return isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
216
+    }
217
+
218
+    // check hovering and change state to avatar with name
219
+    return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
220
+}

+ 5
- 0
react/features/filmstrip/logger.js 查看文件

@@ -0,0 +1,5 @@
1
+// @flow
2
+
3
+import { getLogger } from '../base/logging/functions';
4
+
5
+export default getLogger('features/filmstrip');

+ 7
- 24
react/features/filmstrip/middleware.web.js 查看文件

@@ -1,15 +1,14 @@
1 1
 // @flow
2 2
 
3
-import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
3
+import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
4 4
 import { MiddlewareRegistry } from '../base/redux';
5 5
 import { CLIENT_RESIZED } from '../base/responsive-ui';
6
+import { SETTINGS_UPDATED } from '../base/settings';
6 7
 import {
7 8
     getCurrentLayout,
8
-    LAYOUTS,
9
-    shouldDisplayTileView
9
+    LAYOUTS
10 10
 } from '../video-layout';
11 11
 
12
-import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
13 12
 import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
14 13
 
15 14
 import './subscriber.web';
@@ -48,27 +47,11 @@ MiddlewareRegistry.register(store => next => action => {
48 47
         }
49 48
         break;
50 49
     }
51
-    case SET_TILE_VIEW_DIMENSIONS: {
52
-        const state = store.getState();
53
-
54
-        if (shouldDisplayTileView(state)) {
55
-            const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
56
-
57
-            // Once the thumbnails are reactified this should be moved there too.
58
-            Filmstrip.resizeThumbnailsForTileView(width, height, true);
59
-        }
60
-        break;
61
-    }
62
-    case SET_HORIZONTAL_VIEW_DIMENSIONS: {
63
-        const state = store.getState();
64
-
65
-        if (getCurrentLayout(state) === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
66
-            const { horizontalViewDimensions = {} } = state['features/filmstrip'];
67
-
68
-            // Once the thumbnails are reactified this should be moved there too.
69
-            Filmstrip.resizeThumbnailsForHorizontalView(horizontalViewDimensions, true);
50
+    case SETTINGS_UPDATED: {
51
+        if (typeof action.settings?.localFlipX === 'boolean') {
52
+            // TODO: This needs to be removed once the large video is Reactified.
53
+            VideoLayout.onLocalFlipXChanged();
70 54
         }
71
-
72 55
         break;
73 56
     }
74 57
     }

+ 0
- 25
react/features/filmstrip/subscriber.web.js 查看文件

@@ -1,7 +1,5 @@
1 1
 // @flow
2 2
 
3
-import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
4
-import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
5 3
 import { StateListenerRegistry, equals } from '../base/redux';
6 4
 import { setFilmstripVisible } from '../filmstrip/actions';
7 5
 import { setOverflowDrawer } from '../toolbox/actions.web';
@@ -71,32 +69,9 @@ StateListenerRegistry.register(
71 69
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
72 70
             store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
73 71
             break;
74
-        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
75
-            // Once the thumbnails are reactified this should be moved there too.
76
-            Filmstrip.resizeThumbnailsForVerticalView();
77
-            break;
78 72
         }
79 73
     });
80 74
 
81
-/**
82
- * Handles on stage participant updates.
83
- */
84
-StateListenerRegistry.register(
85
-    /* selector */ state => state['features/large-video'].participantId,
86
-    /* listener */ (participantId, store, oldParticipantId) => {
87
-        const newThumbnail = VideoLayout.getSmallVideo(participantId);
88
-        const oldThumbnail = VideoLayout.getSmallVideo(oldParticipantId);
89
-
90
-        if (newThumbnail) {
91
-            newThumbnail.updateView();
92
-        }
93
-
94
-        if (oldThumbnail) {
95
-            oldThumbnail.updateView();
96
-        }
97
-    }
98
-);
99
-
100 75
 /**
101 76
  * Listens for changes in the chat state to calculate the dimensions of the tile view grid and the tiles.
102 77
  */

+ 1
- 1
react/features/mobile/external-api/middleware.js 查看文件

@@ -34,7 +34,7 @@ import { toggleScreensharing } from '../../base/tracks';
34 34
 import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
35 35
 import { openChat } from '../../chat/actions';
36 36
 import { sendMessage, setPrivateMessageRecipient, closeChat } from '../../chat/actions.any';
37
-import { muteLocal } from '../../remote-video-menu/actions';
37
+import { muteLocal } from '../../video-menu/actions';
38 38
 import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
39 39
 
40 40
 import { setParticipantsWithScreenShare } from './actions';

+ 0
- 44
react/features/remote-video-menu/components/web/RemoteVideoMenu.js 查看文件

@@ -1,44 +0,0 @@
1
-/* @flow */
2
-
3
-import React, { Component } from 'react';
4
-
5
-/**
6
- * The type of the React {@code Component} props of {@link RemoteVideoMenu}.
7
- */
8
-type Props = {
9
-
10
-    /**
11
-     * The components to place as the body of the {@code RemoteVideoMenu}.
12
-     */
13
-    children: React$Node,
14
-
15
-    /**
16
-     * The id attribute to be added to the component's DOM for retrieval when
17
-     * querying the DOM. Not used directly by the component.
18
-     */
19
-    id: string
20
-};
21
-
22
-/**
23
- * React {@code Component} responsible for displaying other components as a menu
24
- * for manipulating remote participant state.
25
- *
26
- * @extends {Component}
27
- */
28
-export default class RemoteVideoMenu extends Component<Props> {
29
-    /**
30
-     * Implements React's {@link Component#render()}.
31
-     *
32
-     * @inheritdoc
33
-     * @returns {ReactElement}
34
-     */
35
-    render() {
36
-        return (
37
-            <ul
38
-                className = 'popupmenu'
39
-                id = { this.props.id }>
40
-                { this.props.children }
41
-            </ul>
42
-        );
43
-    }
44
-}

+ 1
- 1
react/features/toolbox/components/AudioMuteButton.js 查看文件

@@ -13,7 +13,7 @@ import { connect } from '../../base/redux';
13 13
 import { AbstractAudioMuteButton } from '../../base/toolbox/components';
14 14
 import type { AbstractButtonProps } from '../../base/toolbox/components';
15 15
 import { isLocalTrackMuted } from '../../base/tracks';
16
-import { muteLocal } from '../../remote-video-menu/actions';
16
+import { muteLocal } from '../../video-menu/actions';
17 17
 
18 18
 declare var APP: Object;
19 19
 

+ 1
- 1
react/features/toolbox/components/MuteEveryoneButton.js 查看文件

@@ -7,7 +7,7 @@ import { IconMuteEveryone } from '../../base/icons';
7 7
 import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
8 8
 import { connect } from '../../base/redux';
9 9
 import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
10
-import { MuteEveryoneDialog } from '../../remote-video-menu/components';
10
+import { MuteEveryoneDialog } from '../../video-menu/components';
11 11
 
12 12
 type Props = AbstractButtonProps & {
13 13
 

+ 1
- 1
react/features/toolbox/components/MuteEveryonesVideoButton.js 查看文件

@@ -7,7 +7,7 @@ import { IconMuteVideoEveryone } from '../../base/icons';
7 7
 import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
8 8
 import { connect } from '../../base/redux';
9 9
 import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
10
-import { MuteEveryonesVideoDialog } from '../../remote-video-menu/components';
10
+import { MuteEveryonesVideoDialog } from '../../video-menu/components';
11 11
 
12 12
 type Props = AbstractButtonProps & {
13 13
 

+ 16
- 23
react/features/video-layout/middleware.web.js 查看文件

@@ -4,15 +4,12 @@ import VideoLayout from '../../../modules/UI/videolayout/VideoLayout.js';
4 4
 import { CONFERENCE_WILL_LEAVE } from '../base/conference';
5 5
 import { MEDIA_TYPE } from '../base/media';
6 6
 import {
7
-    DOMINANT_SPEAKER_CHANGED,
7
+    getLocalParticipant,
8 8
     PARTICIPANT_JOINED,
9
-    PARTICIPANT_LEFT,
10
-    PARTICIPANT_UPDATED,
11
-    PIN_PARTICIPANT,
12
-    getParticipantById
9
+    PARTICIPANT_UPDATED
13 10
 } from '../base/participants';
14 11
 import { MiddlewareRegistry } from '../base/redux';
15
-import { TRACK_ADDED, TRACK_REMOVED } from '../base/tracks';
12
+import { TRACK_ADDED, TRACK_REMOVED, TRACK_STOPPED } from '../base/tracks';
16 13
 import { SET_FILMSTRIP_VISIBLE } from '../filmstrip';
17 14
 
18 15
 import './middleware.any';
@@ -40,15 +37,10 @@ MiddlewareRegistry.register(store => next => action => {
40 37
 
41 38
     case PARTICIPANT_JOINED:
42 39
         if (!action.participant.local) {
43
-            VideoLayout.addRemoteParticipantContainer(
44
-                getParticipantById(store.getState(), action.participant.id));
40
+            VideoLayout.updateVideoMutedForNoTracks(action.participant.id);
45 41
         }
46 42
         break;
47 43
 
48
-    case PARTICIPANT_LEFT:
49
-        VideoLayout.removeParticipantContainer(action.participant.id);
50
-        break;
51
-
52 44
     case PARTICIPANT_UPDATED: {
53 45
         // Look for actions that triggered a change to connectionStatus. This is
54 46
         // done instead of changing the connection status change action to be
@@ -61,27 +53,28 @@ MiddlewareRegistry.register(store => next => action => {
61 53
         break;
62 54
     }
63 55
 
64
-    case DOMINANT_SPEAKER_CHANGED:
65
-        VideoLayout.onDominantSpeakerChanged(action.participant.id);
66
-        break;
67
-
68
-    case PIN_PARTICIPANT:
69
-        VideoLayout.onPinChange(action.participant?.id);
70
-        break;
71
-
72 56
     case SET_FILMSTRIP_VISIBLE:
73 57
         VideoLayout.resizeVideoArea();
74 58
         break;
75 59
 
76 60
     case TRACK_ADDED:
77
-        if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
78
-            VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
61
+        if (action.track.mediaType !== MEDIA_TYPE.AUDIO) {
62
+            VideoLayout._updateLargeVideoIfDisplayed(action.track.participantId, true);
79 63
         }
80 64
 
81 65
         break;
66
+
67
+    case TRACK_STOPPED: {
68
+        if (action.track.jitsiTrack.isLocal()) {
69
+            const participant = getLocalParticipant(store.getState);
70
+
71
+            VideoLayout._updateLargeVideoIfDisplayed(participant?.id);
72
+        }
73
+        break;
74
+    }
82 75
     case TRACK_REMOVED:
83 76
         if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
84
-            VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
77
+            VideoLayout.updateVideoMutedForNoTracks(action.track.jitsiTrack.getParticipantId());
85 78
         }
86 79
 
87 80
         break;

react/features/remote-video-menu/actions.js → react/features/video-menu/actions.any.js 查看文件

@@ -10,7 +10,6 @@ import {
10 10
     sendAnalytics,
11 11
     VIDEO_MUTE
12 12
 } from '../analytics';
13
-import { hideDialog } from '../base/dialog';
14 13
 import {
15 14
     MEDIA_TYPE,
16 15
     setAudioMuted,
@@ -22,21 +21,10 @@ import {
22 21
     muteRemoteParticipant
23 22
 } from '../base/participants';
24 23
 
25
-import { RemoteVideoMenu } from './components';
26
-
27 24
 declare var APP: Object;
28 25
 
29 26
 const logger = getLogger(__filename);
30 27
 
31
-/**
32
- * Hides the remote video menu.
33
- *
34
- * @returns {Function}
35
- */
36
-export function hideRemoteVideoMenu() {
37
-    return hideDialog(RemoteVideoMenu);
38
-}
39
-
40 28
 /**
41 29
  * Mutes the local participant.
42 30
  *

+ 15
- 0
react/features/video-menu/actions.native.js 查看文件

@@ -0,0 +1,15 @@
1
+// @flow
2
+import { hideDialog } from '../base/dialog';
3
+
4
+import { RemoteVideoMenu } from './components/native';
5
+
6
+/**
7
+ * Hides the remote video menu.
8
+ *
9
+ * @returns {Function}
10
+ */
11
+export function hideRemoteVideoMenu() {
12
+    return hideDialog(RemoteVideoMenu);
13
+}
14
+
15
+export * from './actions.any';

+ 2
- 0
react/features/video-menu/actions.web.js 查看文件

@@ -0,0 +1,2 @@
1
+// @flow
2
+export * from './actions.any';

react/features/remote-video-menu/components/AbstractGrantModeratorButton.js → react/features/video-menu/components/AbstractGrantModeratorButton.js 查看文件


react/features/remote-video-menu/components/AbstractGrantModeratorDialog.js → react/features/video-menu/components/AbstractGrantModeratorDialog.js 查看文件


react/features/remote-video-menu/components/AbstractKickButton.js → react/features/video-menu/components/AbstractKickButton.js 查看文件


react/features/remote-video-menu/components/AbstractKickRemoteParticipantDialog.js → react/features/video-menu/components/AbstractKickRemoteParticipantDialog.js 查看文件


react/features/remote-video-menu/components/AbstractMuteButton.js → react/features/video-menu/components/AbstractMuteButton.js 查看文件


react/features/remote-video-menu/components/AbstractMuteEveryoneDialog.js → react/features/video-menu/components/AbstractMuteEveryoneDialog.js 查看文件


react/features/remote-video-menu/components/AbstractMuteEveryoneElseButton.js → react/features/video-menu/components/AbstractMuteEveryoneElseButton.js 查看文件


react/features/remote-video-menu/components/AbstractMuteEveryoneElsesVideoButton.js → react/features/video-menu/components/AbstractMuteEveryoneElsesVideoButton.js 查看文件


react/features/remote-video-menu/components/AbstractMuteEveryonesVideoDialog.js → react/features/video-menu/components/AbstractMuteEveryonesVideoDialog.js 查看文件


react/features/remote-video-menu/components/AbstractMuteRemoteParticipantDialog.js → react/features/video-menu/components/AbstractMuteRemoteParticipantDialog.js 查看文件


react/features/remote-video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js → react/features/video-menu/components/AbstractMuteRemoteParticipantsVideoDialog.js 查看文件


react/features/remote-video-menu/components/AbstractMuteVideoButton.js → react/features/video-menu/components/AbstractMuteVideoButton.js 查看文件


react/features/remote-video-menu/components/index.native.js → react/features/video-menu/components/index.native.js 查看文件


react/features/remote-video-menu/components/index.web.js → react/features/video-menu/components/index.web.js 查看文件


react/features/remote-video-menu/components/native/ConnectionStatusButton.js → react/features/video-menu/components/native/ConnectionStatusButton.js 查看文件


react/features/remote-video-menu/components/native/ConnectionStatusComponent.js → react/features/video-menu/components/native/ConnectionStatusComponent.js 查看文件


react/features/remote-video-menu/components/native/GrantModeratorButton.js → react/features/video-menu/components/native/GrantModeratorButton.js 查看文件


react/features/remote-video-menu/components/native/GrantModeratorDialog.js → react/features/video-menu/components/native/GrantModeratorDialog.js 查看文件


react/features/remote-video-menu/components/native/KickButton.js → react/features/video-menu/components/native/KickButton.js 查看文件


react/features/remote-video-menu/components/native/KickRemoteParticipantDialog.js → react/features/video-menu/components/native/KickRemoteParticipantDialog.js 查看文件


react/features/remote-video-menu/components/native/MuteButton.js → react/features/video-menu/components/native/MuteButton.js 查看文件


react/features/remote-video-menu/components/native/MuteEveryoneDialog.js → react/features/video-menu/components/native/MuteEveryoneDialog.js 查看文件


react/features/remote-video-menu/components/native/MuteEveryoneElseButton.js → react/features/video-menu/components/native/MuteEveryoneElseButton.js 查看文件


react/features/remote-video-menu/components/native/MuteRemoteParticipantDialog.js → react/features/video-menu/components/native/MuteRemoteParticipantDialog.js 查看文件


react/features/remote-video-menu/components/native/PinButton.js → react/features/video-menu/components/native/PinButton.js 查看文件


react/features/remote-video-menu/components/native/RemoteVideoMenu.js → react/features/video-menu/components/native/RemoteVideoMenu.js 查看文件

@@ -11,7 +11,7 @@ import { getParticipantDisplayName } from '../../../base/participants';
11 11
 import { connect } from '../../../base/redux';
12 12
 import { StyleType } from '../../../base/styles';
13 13
 import { PrivateMessageButton } from '../../../chat';
14
-import { hideRemoteVideoMenu } from '../../actions';
14
+import { hideRemoteVideoMenu } from '../../actions.native';
15 15
 
16 16
 import ConnectionStatusButton from './ConnectionStatusButton';
17 17
 import GrantModeratorButton from './GrantModeratorButton';

react/features/remote-video-menu/components/native/index.js → react/features/video-menu/components/native/index.js 查看文件


react/features/remote-video-menu/components/native/styles.js → react/features/video-menu/components/native/styles.js 查看文件


+ 103
- 0
react/features/video-menu/components/web/FlipLocalVideoButton.js 查看文件

@@ -0,0 +1,103 @@
1
+/* @flow */
2
+
3
+import React, { PureComponent } from 'react';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { connect } from '../../../base/redux';
7
+import { updateSettings } from '../../../base/settings';
8
+
9
+import VideoMenuButton from './VideoMenuButton';
10
+
11
+/**
12
+ * The type of the React {@code Component} props of {@link FlipLocalVideoButton}.
13
+ */
14
+type Props = {
15
+
16
+    /**
17
+     * The current local flip x status.
18
+     */
19
+    _localFlipX: boolean,
20
+
21
+    /**
22
+     * The redux dispatch function.
23
+     */
24
+    dispatch: Function,
25
+
26
+    /**
27
+     * Invoked to obtain translated strings.
28
+     */
29
+    t: Function
30
+};
31
+
32
+/**
33
+ * Implements a React {@link Component} which displays a button for flipping the local viedo.
34
+ *
35
+ * @extends Component
36
+ */
37
+class FlipLocalVideoButton extends PureComponent<Props> {
38
+    /**
39
+     * Initializes a new {@code FlipLocalVideoButton} instance.
40
+     *
41
+     * @param {Object} props - The read-only React Component props with which
42
+     * the new instance is to be initialized.
43
+     */
44
+    constructor(props: Props) {
45
+        super(props);
46
+
47
+        // Bind event handlers so they are only bound once for every instance.
48
+        this._onClick = this._onClick.bind(this);
49
+    }
50
+
51
+    /**
52
+     * Implements React's {@link Component#render()}.
53
+     *
54
+     * @inheritdoc
55
+     * @returns {null|ReactElement}
56
+     */
57
+    render() {
58
+        const {
59
+            t
60
+        } = this.props;
61
+
62
+        return (
63
+            <VideoMenuButton
64
+                buttonText = { t('videothumbnail.flip') }
65
+                displayClass = 'fliplink'
66
+                id = 'flipLocalVideoButton'
67
+                onClick = { this._onClick } />
68
+        );
69
+    }
70
+
71
+    _onClick: () => void;
72
+
73
+    /**
74
+     * Flips the local video.
75
+     *
76
+     * @private
77
+     * @returns {void}
78
+     */
79
+    _onClick() {
80
+        const { _localFlipX, dispatch } = this.props;
81
+
82
+        dispatch(updateSettings({
83
+            localFlipX: !_localFlipX
84
+        }));
85
+    }
86
+}
87
+
88
+/**
89
+ * Maps (parts of) the Redux state to the associated {@code FlipLocalVideoButton}'s props.
90
+ *
91
+ * @param {Object} state - The Redux state.
92
+ * @private
93
+ * @returns {Props}
94
+ */
95
+function _mapStateToProps(state) {
96
+    const { localFlipX } = state['features/base/settings'];
97
+
98
+    return {
99
+        _localFlipX: Boolean(localFlipX)
100
+    };
101
+}
102
+
103
+export default translate(connect(_mapStateToProps)(FlipLocalVideoButton));

react/features/remote-video-menu/components/web/GrantModeratorButton.js → react/features/video-menu/components/web/GrantModeratorButton.js 查看文件

@@ -10,7 +10,7 @@ import AbstractGrantModeratorButton, {
10 10
     type Props
11 11
 } from '../AbstractGrantModeratorButton';
12 12
 
13
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
13
+import VideoMenuButton from './VideoMenuButton';
14 14
 
15 15
 declare var interfaceConfig: Object;
16 16
 
@@ -44,7 +44,7 @@ class GrantModeratorButton extends AbstractGrantModeratorButton {
44 44
         }
45 45
 
46 46
         return (
47
-            <RemoteVideoMenuButton
47
+            <VideoMenuButton
48 48
                 buttonText = { t('videothumbnail.grantModerator') }
49 49
                 displayClass = 'grantmoderatorlink'
50 50
                 icon = { IconCrown }

react/features/remote-video-menu/components/web/GrantModeratorDialog.js → react/features/video-menu/components/web/GrantModeratorDialog.js 查看文件


react/features/remote-video-menu/components/web/KickButton.js → react/features/video-menu/components/web/KickButton.js 查看文件

@@ -9,7 +9,7 @@ import AbstractKickButton, {
9 9
     type Props
10 10
 } from '../AbstractKickButton';
11 11
 
12
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
12
+import VideoMenuButton from './VideoMenuButton';
13 13
 
14 14
 /**
15 15
  * Implements a React {@link Component} which displays a button for kicking out
@@ -43,7 +43,7 @@ class KickButton extends AbstractKickButton {
43 43
         const { participantID, t } = this.props;
44 44
 
45 45
         return (
46
-            <RemoteVideoMenuButton
46
+            <VideoMenuButton
47 47
                 buttonText = { t('videothumbnail.kick') }
48 48
                 displayClass = 'kicklink'
49 49
                 icon = { IconKick }

react/features/remote-video-menu/components/web/KickRemoteParticipantDialog.js → react/features/video-menu/components/web/KickRemoteParticipantDialog.js 查看文件


+ 100
- 0
react/features/video-menu/components/web/LocalVideoMenuTriggerButton.js 查看文件

@@ -0,0 +1,100 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { Icon, IconMenuThumb } from '../../../base/icons';
6
+import { Popover } from '../../../base/popover';
7
+import { connect } from '../../../base/redux';
8
+import { getLocalVideoTrack } from '../../../base/tracks';
9
+import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
10
+
11
+import FlipLocalVideoButton from './FlipLocalVideoButton';
12
+import VideoMenu from './VideoMenu';
13
+
14
+/**
15
+ * The type of the React {@code Component} props of
16
+ * {@link LocalVideoMenuTriggerButton}.
17
+ */
18
+type Props = {
19
+
20
+    /**
21
+     * The position relative to the trigger the local video menu should display
22
+     * from. Valid values are those supported by AtlasKit
23
+     * {@code InlineDialog}.
24
+     */
25
+    _menuPosition: string,
26
+
27
+    /**
28
+     * Whether to display the Popover as a drawer.
29
+     */
30
+    _overflowDrawer: boolean,
31
+
32
+    /**
33
+     * Shows/hides the local video flip button.
34
+     */
35
+    _showLocalVideoFlipButton: boolean
36
+};
37
+
38
+/**
39
+ * React Component for displaying an icon associated with opening the
40
+ * the video menu for the local participant.
41
+ *
42
+ * @param {Props} props - The props passed to the component.
43
+ * @returns {ReactElement}
44
+ */
45
+function LocalVideoMenuTriggerButton(props: Props) {
46
+    return (
47
+        props._showLocalVideoFlipButton
48
+            ? <Popover
49
+                content = {
50
+                    <VideoMenu id = 'localVideoMenu'>
51
+                        <FlipLocalVideoButton />
52
+                    </VideoMenu>
53
+                }
54
+                overflowDrawer = { props._overflowDrawer }
55
+                position = { props._menuPosition }>
56
+                <span
57
+                    className = 'popover-trigger local-video-menu-trigger'>
58
+                    <Icon
59
+                        size = '1em'
60
+                        src = { IconMenuThumb }
61
+                        title = 'Local user controls' />
62
+                </span>
63
+            </Popover>
64
+            : null
65
+    );
66
+}
67
+
68
+/**
69
+ * Maps (parts of) the Redux state to the associated {@code LocalVideoMenuTriggerButton}'s props.
70
+ *
71
+ * @param {Object} state - The Redux state.
72
+ * @private
73
+ * @returns {Props}
74
+ */
75
+function _mapStateToProps(state) {
76
+    const currentLayout = getCurrentLayout(state);
77
+    const { disableLocalVideoFlip } = state['features/base/config'];
78
+    const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
79
+    const { overflowDrawer } = state['features/toolbox'];
80
+    let _menuPosition;
81
+
82
+    switch (currentLayout) {
83
+    case LAYOUTS.TILE_VIEW:
84
+        _menuPosition = 'left-start';
85
+        break;
86
+    case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
87
+        _menuPosition = 'left-end';
88
+        break;
89
+    default:
90
+        _menuPosition = 'auto';
91
+    }
92
+
93
+    return {
94
+        _menuPosition,
95
+        _showLocalVideoFlipButton: !disableLocalVideoFlip && videoTrack?.videoType !== 'desktop',
96
+        _overflowDrawer: overflowDrawer
97
+    };
98
+}
99
+
100
+export default connect(_mapStateToProps)(LocalVideoMenuTriggerButton);

react/features/remote-video-menu/components/web/MuteButton.js → react/features/video-menu/components/web/MuteButton.js 查看文件

@@ -10,7 +10,7 @@ import AbstractMuteButton, {
10 10
     type Props
11 11
 } from '../AbstractMuteButton';
12 12
 
13
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
13
+import VideoMenuButton from './VideoMenuButton';
14 14
 
15 15
 /**
16 16
  * Implements a React {@link Component} which displays a button for audio muting
@@ -51,7 +51,7 @@ class MuteButton extends AbstractMuteButton {
51 51
         };
52 52
 
53 53
         return (
54
-            <RemoteVideoMenuButton
54
+            <VideoMenuButton
55 55
                 buttonText = { t(muteConfig.translationKey) }
56 56
                 displayClass = { muteConfig.muteClassName }
57 57
                 icon = { IconMicDisabled }

react/features/remote-video-menu/components/web/MuteEveryoneDialog.js → react/features/video-menu/components/web/MuteEveryoneDialog.js 查看文件


react/features/remote-video-menu/components/web/MuteEveryoneElseButton.js → react/features/video-menu/components/web/MuteEveryoneElseButton.js 查看文件

@@ -9,7 +9,7 @@ import AbstractMuteEveryoneElseButton, {
9 9
     type Props
10 10
 } from '../AbstractMuteEveryoneElseButton';
11 11
 
12
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
12
+import VideoMenuButton from './VideoMenuButton';
13 13
 
14 14
 /**
15 15
  * Implements a React {@link Component} which displays a button for audio muting
@@ -38,7 +38,7 @@ class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton {
38 38
         const { participantID, t } = this.props;
39 39
 
40 40
         return (
41
-            <RemoteVideoMenuButton
41
+            <VideoMenuButton
42 42
                 buttonText = { t('videothumbnail.domuteOthers') }
43 43
                 displayClass = { 'mutelink' }
44 44
                 icon = { IconMuteEveryoneElse }

react/features/remote-video-menu/components/web/MuteEveryoneElsesVideoButton.js → react/features/video-menu/components/web/MuteEveryoneElsesVideoButton.js 查看文件

@@ -9,7 +9,7 @@ import AbstractMuteEveryoneElsesVideoButton, {
9 9
     type Props
10 10
 } from '../AbstractMuteEveryoneElsesVideoButton';
11 11
 
12
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
12
+import VideoMenuButton from './VideoMenuButton';
13 13
 
14 14
 /**
15 15
  * Implements a React {@link Component} which displays a button for audio muting
@@ -38,7 +38,7 @@ class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton
38 38
         const { participantID, t } = this.props;
39 39
 
40 40
         return (
41
-            <RemoteVideoMenuButton
41
+            <VideoMenuButton
42 42
                 buttonText = { t('videothumbnail.domuteVideoOfOthers') }
43 43
                 displayClass = { 'mutelink' }
44 44
                 icon = { IconMuteVideoEveryoneElse }

react/features/remote-video-menu/components/web/MuteEveryonesVideoDialog.js → react/features/video-menu/components/web/MuteEveryonesVideoDialog.js 查看文件


react/features/remote-video-menu/components/web/MuteRemoteParticipantDialog.js → react/features/video-menu/components/web/MuteRemoteParticipantDialog.js 查看文件


react/features/remote-video-menu/components/web/MuteRemoteParticipantsVideoDialog.js → react/features/video-menu/components/web/MuteRemoteParticipantsVideoDialog.js 查看文件


react/features/remote-video-menu/components/web/MuteVideoButton.js → react/features/video-menu/components/web/MuteVideoButton.js 查看文件

@@ -10,7 +10,7 @@ import AbstractMuteVideoButton, {
10 10
     type Props
11 11
 } from '../AbstractMuteVideoButton';
12 12
 
13
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
13
+import VideoMenuButton from './VideoMenuButton';
14 14
 
15 15
 /**
16 16
  * Implements a React {@link Component} which displays a button for disabling
@@ -51,7 +51,7 @@ class MuteVideoButton extends AbstractMuteVideoButton {
51 51
         };
52 52
 
53 53
         return (
54
-            <RemoteVideoMenuButton
54
+            <VideoMenuButton
55 55
                 buttonText = { t(muteConfig.translationKey) }
56 56
                 displayClass = { muteConfig.muteClassName }
57 57
                 icon = { IconCameraDisabled }

react/features/remote-video-menu/components/web/PrivateMessageMenuButton.js → react/features/video-menu/components/web/PrivateMessageMenuButton.js 查看文件

@@ -12,7 +12,7 @@ import {
12 12
 } from '../../../chat/components/PrivateMessageButton';
13 13
 import { isButtonEnabled } from '../../../toolbox/functions.web';
14 14
 
15
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
15
+import VideoMenuButton from './VideoMenuButton';
16 16
 
17 17
 declare var interfaceConfig: Object;
18 18
 
@@ -56,7 +56,7 @@ class PrivateMessageMenuButton extends Component<Props> {
56 56
         }
57 57
 
58 58
         return (
59
-            <RemoteVideoMenuButton
59
+            <VideoMenuButton
60 60
                 buttonText = { t('toolbar.privateMessage') }
61 61
                 icon = { IconMessage }
62 62
                 id = { `privmsglink_${participantID}` }

react/features/remote-video-menu/components/web/RemoteControlButton.js → react/features/video-menu/components/web/RemoteControlButton.js 查看文件

@@ -9,7 +9,7 @@ import {
9 9
 import { translate } from '../../../base/i18n';
10 10
 import { IconRemoteControlStart, IconRemoteControlStop } from '../../../base/icons';
11 11
 
12
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
12
+import VideoMenuButton from './VideoMenuButton';
13 13
 
14 14
 // TODO: Move these enums into the store after further reactification of the
15 15
 // non-react RemoteVideo component.
@@ -102,7 +102,7 @@ class RemoteControlButton extends Component<Props> {
102 102
         }
103 103
 
104 104
         return (
105
-            <RemoteVideoMenuButton
105
+            <VideoMenuButton
106 106
                 buttonText = { t('videothumbnail.remoteControl') }
107 107
                 displayClass = { className }
108 108
                 icon = { icon }

react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js → react/features/video-menu/components/web/RemoteVideoMenuTriggerButton.js 查看文件

@@ -20,7 +20,7 @@ import {
20 20
     KickButton,
21 21
     PrivateMessageMenuButton,
22 22
     RemoteControlButton,
23
-    RemoteVideoMenu,
23
+    VideoMenu,
24 24
     VolumeSlider
25 25
 } from './';
26 26
 
@@ -91,21 +91,11 @@ type Props = {
91 91
 
92 92
 /**
93 93
  * React {@code Component} for displaying an icon associated with opening the
94
- * the {@code RemoteVideoMenu}.
94
+ * the {@code VideoMenu}.
95 95
  *
96 96
  * @extends {Component}
97 97
  */
98 98
 class RemoteVideoMenuTriggerButton extends Component<Props> {
99
-    /**
100
-     * The internal reference to topmost DOM/HTML element backing the React
101
-     * {@code Component}. Accessed directly for associating an element as
102
-     * the trigger for a popover.
103
-     *
104
-     * @private
105
-     * @type {HTMLDivElement}
106
-     */
107
-    _rootElement = null;
108
-
109 99
     /**
110 100
      * Implements React's {@link Component#render()}.
111 101
      *
@@ -136,7 +126,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
136 126
     }
137 127
 
138 128
     /**
139
-     * Creates a new {@code RemoteVideoMenu} with buttons for interacting with
129
+     * Creates a new {@code VideoMenu} with buttons for interacting with
140 130
      * the remote participant.
141 131
      *
142 132
      * @private
@@ -230,9 +220,9 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
230 220
 
231 221
         if (buttons.length > 0) {
232 222
             return (
233
-                <RemoteVideoMenu id = { participantID }>
223
+                <VideoMenu id = { participantID }>
234 224
                     { buttons }
235
-                </RemoteVideoMenu>
225
+                </VideoMenu>
236 226
             );
237 227
         }
238 228
 

+ 51
- 0
react/features/video-menu/components/web/VideoMenu.js 查看文件

@@ -0,0 +1,51 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+/**
6
+ * The type of the React {@code Component} props of {@link VideoMenu}.
7
+ */
8
+type Props = {
9
+
10
+    /**
11
+     * The components to place as the body of the {@code VideoMenu}.
12
+     */
13
+    children: React$Node,
14
+
15
+    /**
16
+     * The id attribute to be added to the component's DOM for retrieval when
17
+     * querying the DOM. Not used directly by the component.
18
+     */
19
+    id: string
20
+};
21
+
22
+/**
23
+ * Click handler.
24
+ *
25
+ * @param {SyntheticEvent} event - The click event.
26
+ * @returns {void}
27
+ */
28
+function onClick(event) {
29
+    // If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
30
+    // needs to be stopped.
31
+    event.stopPropagation();
32
+}
33
+
34
+/**
35
+ * React {@code Component} responsible for displaying other components as a menu
36
+ * for manipulating participant state.
37
+ *
38
+ * @param {Props} props - The component's props.
39
+ * @returns {Component}
40
+ */
41
+export default function VideoMenu(props: Props) {
42
+    return (
43
+        <ul
44
+            className = 'popupmenu'
45
+            id = { props.id }
46
+            onClick = { onClick }>
47
+            { props.children }
48
+        </ul>
49
+    );
50
+}
51
+

react/features/remote-video-menu/components/web/RemoteVideoMenuButton.js → react/features/video-menu/components/web/VideoMenuButton.js 查看文件

@@ -6,7 +6,7 @@ import { Icon } from '../../../base/icons';
6 6
 
7 7
 /**
8 8
  * The type of the React {@code Component} props of
9
- * {@link RemoteVideoMenuButton}.
9
+ * {@link VideoMenuButton}.
10 10
  */
11 11
 type Props = {
12 12
 
@@ -23,7 +23,7 @@ type Props = {
23 23
     /**
24 24
      * The icon that will display within the component.
25 25
      */
26
-    icon: Object,
26
+    icon?: Object,
27 27
 
28 28
     /**
29 29
      * The id attribute to be added to the component's DOM for retrieval when
@@ -38,11 +38,11 @@ type Props = {
38 38
 };
39 39
 
40 40
 /**
41
- * React {@code Component} for displaying an action in {@code RemoteVideoMenu}.
41
+ * React {@code Component} for displaying an action in {@code VideoMenuButton}.
42 42
  *
43 43
  * @extends {Component}
44 44
  */
45
-export default class RemoteVideoMenuButton extends Component<Props> {
45
+export default class VideoMenuButton extends Component<Props> {
46 46
     /**
47 47
      * Implements React's {@link Component#render()}.
48 48
      *
@@ -67,7 +67,7 @@ export default class RemoteVideoMenuButton extends Component<Props> {
67 67
                     id = { id }
68 68
                     onClick = { onClick }>
69 69
                     <span className = 'popupmenu__icon'>
70
-                        <Icon src = { icon } />
70
+                        { icon && <Icon src = { icon } /> }
71 71
                     </span>
72 72
                     <span className = 'popupmenu__text'>
73 73
                         { buttonText }

react/features/remote-video-menu/components/web/VolumeSlider.js → react/features/video-menu/components/web/VolumeSlider.js 查看文件


react/features/remote-video-menu/components/web/index.js → react/features/video-menu/components/web/index.js 查看文件

@@ -14,6 +14,7 @@ export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantD
14 14
 export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
15 15
 export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
16 16
 export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
17
-export { default as RemoteVideoMenu } from './RemoteVideoMenu';
17
+export { default as VideoMenu } from './VideoMenu';
18 18
 export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
19
+export { default as LocalVideoMenuTriggerButton } from './LocalVideoMenuTriggerButton';
19 20
 export { default as VolumeSlider } from './VolumeSlider';

react/features/remote-video-menu/index.js → react/features/video-menu/index.js 查看文件


+ 1
- 16
service/UI/UIEvents.js 查看文件

@@ -48,11 +48,6 @@ export default {
48 48
     VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
49 49
     AUDIO_DEVICE_CHANGED: 'UI.audio_device_changed',
50 50
 
51
-    /**
52
-     * Notifies that flipX property of the local video is changed.
53
-     */
54
-    LOCAL_FLIPX_CHANGED: 'UI.local_flipx_changed',
55
-
56 51
     /**
57 52
      * Notifies that the side toolbar container has been toggled. The actual
58 53
      * event must contain the identifier of the container that has been toggled
@@ -63,15 +58,5 @@ export default {
63 58
     /**
64 59
      * Notifies that the raise hand has been changed.
65 60
      */
66
-    LOCAL_RAISE_HAND_CHANGED: 'UI.local_raise_hand_changed',
67
-
68
-    /**
69
-     * Notifies that the avatar is displayed or not on the largeVideo.
70
-     */
71
-    LARGE_VIDEO_AVATAR_VISIBLE: 'UI.large_video_avatar_visible',
72
-
73
-    /**
74
-     * Notifies that the displayed particpant id on the largeVideo is changed.
75
-     */
76
-    LARGE_VIDEO_ID_CHANGED: 'UI.large_video_id_changed'
61
+    LOCAL_RAISE_HAND_CHANGED: 'UI.local_raise_hand_changed'
77 62
 };

Loading…
取消
儲存