浏览代码

ref(Filmstrip): Use Thumbnail component.

master
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
 /* application specific logic */
1
 /* application specific logic */
2
 
2
 
3
 import 'jquery';
3
 import 'jquery';
4
-import 'jquery-contextmenu';
5
 import 'jQuery-Impromptu';
4
 import 'jQuery-Impromptu';
6
 
5
 
7
 import 'olm';
6
 import 'olm';

+ 5
- 4
conference.js 查看文件

1399
                     .then(() => {
1399
                     .then(() => {
1400
                         this.localVideo = newTrack;
1400
                         this.localVideo = newTrack;
1401
                         this._setSharingScreen(newTrack);
1401
                         this._setSharingScreen(newTrack);
1402
-                        if (newTrack) {
1403
-                            APP.UI.addLocalVideoStream(newTrack);
1404
-                        }
1405
                         this.setVideoMuteStatus(this.isLocalVideoMuted());
1402
                         this.setVideoMuteStatus(this.isLocalVideoMuted());
1406
                     })
1403
                     })
1407
                     .then(resolve)
1404
                     .then(resolve)
2408
             // There is no guarantee another event will trigger the update
2405
             // There is no guarantee another event will trigger the update
2409
             // immediately and in all situations, for example because a remote
2406
             // immediately and in all situations, for example because a remote
2410
             // participant is having connection trouble so no status changes.
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
         APP.UI.addListener(
2415
         APP.UI.addListener(

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

3
 **/
3
 **/
4
 
4
 
5
 .popupmenu {
5
 .popupmenu {
6
-    min-width: 75px;
6
+    min-width: 150px;
7
     text-align: left;
7
     text-align: left;
8
     padding: 0px;
8
     padding: 0px;
9
     white-space: nowrap;
9
     white-space: nowrap;
109
     margin: -16px -24px;
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
     display:block !important;
113
     display:block !important;
114
 }
114
 }

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

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

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

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

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

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

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

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

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

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

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

115
     // Set the defaults for prompt dialogs.
115
     // Set the defaults for prompt dialogs.
116
     $.prompt.setDefaults({ persistent: false });
116
     $.prompt.setDefaults({ persistent: false });
117
 
117
 
118
-    VideoLayout.init(eventEmitter);
119
     VideoLayout.initLargeVideo();
118
     VideoLayout.initLargeVideo();
120
 
119
 
121
     // Do not animate the video area on UI start (second argument passed into
120
     // Do not animate the video area on UI start (second argument passed into
135
     if (config.iAmRecorder) {
134
     if (config.iAmRecorder) {
136
         // in case of iAmSipGateway keep local video visible
135
         // in case of iAmSipGateway keep local video visible
137
         if (!config.iAmSipGateway) {
136
         if (!config.iAmSipGateway) {
138
-            VideoLayout.setLocalVideoVisible(false);
139
             APP.store.dispatch(setNotificationsEnabled(false));
137
             APP.store.dispatch(setNotificationsEnabled(false));
140
         }
138
         }
141
 
139
 
179
     $(window).off('resize');
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
  * Setup and show Etherpad.
181
  * Setup and show Etherpad.
192
  * @param {string} name etherpad id
182
  * @param {string} name etherpad id
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
  * Updates the user status.
221
  * Updates the user status.
240
  *
222
  *
289
  * Sets muted video state for participant
271
  * Sets muted video state for participant
290
  */
272
  */
291
 UI.setVideoMuted = function(id) {
273
 UI.setVideoMuted = function(id) {
292
-    VideoLayout.onVideoMute(id);
274
+    VideoLayout._updateLargeVideoIfDisplayed(id, true);
275
+
293
     if (APP.conference.isLocalId(id)) {
276
     if (APP.conference.isLocalId(id)) {
294
         APP.conference.updateVideoIconEnabled();
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
  * Adds a listener that would be notified on the given type of event.
284
  * Adds a listener that would be notified on the given type of event.
340
  */
317
  */
341
 UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
318
 UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
342
 
319
 
343
-UI.clickOnVideo = videoNumber => VideoLayout.togglePin(videoNumber);
344
-
345
 // Used by torture.
320
 // Used by torture.
346
 UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
321
 UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
347
 
322
 

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

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

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

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
-/* 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
-/* 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
 import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
9
 import { ORIENTATION, LargeVideoBackground, updateLastLargeVideoMediaEvent } from '../../../react/features/large-video';
10
 import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
10
 import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
11
 /* eslint-enable no-unused-vars */
11
 /* eslint-enable no-unused-vars */
12
-import UIEvents from '../../../service/UI/UIEvents';
13
 import UIUtil from '../util/UIUtil';
12
 import UIUtil from '../util/UIUtil';
14
 
13
 
15
 import Filmstrip from './Filmstrip';
14
 import Filmstrip from './Filmstrip';
187
      * Creates new VideoContainer instance.
186
      * Creates new VideoContainer instance.
188
      * @param resizeContainer {Function} function that takes care of the size
187
      * @param resizeContainer {Function} function that takes care of the size
189
      * of the video container.
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
         super();
191
         super();
195
         this.stream = null;
192
         this.stream = null;
196
         this.userId = null;
193
         this.userId = null;
197
         this.videoType = null;
194
         this.videoType = null;
198
         this.localFlipX = true;
195
         this.localFlipX = true;
199
-        this.emitter = emitter;
200
         this.resizeContainer = resizeContainer;
196
         this.resizeContainer = resizeContainer;
201
 
197
 
202
         /**
198
         /**
492
 
488
 
493
         stream.attach(this.$video[0]);
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
         this.$video.css({
493
         this.$video.css({
498
             transform: flipX ? 'scaleX(-1)' : 'none'
494
             transform: flipX ? 'scaleX(-1)' : 'none'
534
         this.$avatar.css('visibility', show ? 'visible' : 'hidden');
530
         this.$avatar.css('visibility', show ? 'visible' : 'hidden');
535
         this.avatarDisplayed = show;
531
         this.avatarDisplayed = show;
536
 
532
 
537
-        this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE, show);
538
         APP.API.notifyLargeVideoVisibilityChanged(show);
533
         APP.API.notifyLargeVideoVisibilityChanged(show);
539
     }
534
     }
540
 
535
 

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

4
 
4
 
5
 import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
5
 import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
6
 import {
6
 import {
7
-    getLocalParticipant as getLocalParticipantFromStore,
8
     getPinnedParticipant,
7
     getPinnedParticipant,
9
-    getParticipantById,
10
-    pinParticipant
8
+    getParticipantById
11
 } from '../../../react/features/base/participants';
9
 } from '../../../react/features/base/participants';
12
 import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
10
 import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
13
-import UIEvents from '../../../service/UI/UIEvents';
14
 import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
11
 import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
15
-import SharedVideoThumb from '../shared_video/SharedVideoThumb';
16
 
12
 
17
 import LargeVideoManager from './LargeVideoManager';
13
 import LargeVideoManager from './LargeVideoManager';
18
-import LocalVideo from './LocalVideo';
19
-import RemoteVideo from './RemoteVideo';
20
 import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
14
 import { VIDEO_CONTAINER_TYPE } from './VideoContainer';
21
 
15
 
22
 const logger = Logger.getLogger(__filename);
16
 const logger = Logger.getLogger(__filename);
23
-
24
-const remoteVideos = {};
25
-let localVideoThumbnail = null;
26
-
27
-let eventEmitter = null;
28
-
29
 let largeVideo;
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
 const VideoLayout = {
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
      */
36
      */
96
     reset() {
37
     reset() {
97
         this._resetLargeVideo();
38
         this._resetLargeVideo();
98
-        this._resetFilmstrip();
99
     },
39
     },
100
 
40
 
101
     initLargeVideo() {
41
     initLargeVideo() {
102
         this._resetLargeVideo();
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
             largeVideo.onLocalFlipXChange(localFlipX);
50
             largeVideo.onLocalFlipXChange(localFlipX);
107
         }
51
         }
108
         largeVideo.updateContainerSize();
52
         largeVideo.updateContainerSize();
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
      * FIXME get rid of this method once muted indicator are reactified (by
68
      * FIXME get rid of this method once muted indicator are reactified (by
174
      * making sure that user with no tracks is displayed as muted )
69
      * making sure that user with no tracks is displayed as muted )
180
         const participant = APP.conference.getParticipantById(participantId);
75
         const participant = APP.conference.getParticipantById(participantId);
181
 
76
 
182
         if (participant && !participant.getTracksByMediaType('video').length) {
77
         if (participant && !participant.getTracksByMediaType('video').length) {
183
-            APP.UI.setVideoMuted(participantId);
78
+            VideoLayout._updateLargeVideoIfDisplayed(participantId, true);
184
         }
79
         }
185
     },
80
     },
186
 
81
 
202
         return videoTrack?.videoType;
97
         return videoTrack?.videoType;
203
     },
98
     },
204
 
99
 
205
-    isPinned(id) {
206
-        return id === this.getPinnedId();
207
-    },
208
-
209
     getPinnedId() {
100
     getPinnedId() {
210
         const { id } = getPinnedParticipant(APP.store.getState()) || {};
101
         const { id } = getPinnedParticipant(APP.store.getState()) || {};
211
 
102
 
212
         return id || null;
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
      * Shows/hides warning about a user's connectivity issues.
107
      * Shows/hides warning about a user's connectivity issues.
311
      *
108
      *
321
         // We have to trigger full large video update to transition from
118
         // We have to trigger full large video update to transition from
322
         // avatar to video on connectivity restored.
119
         // avatar to video on connectivity restored.
323
         this._updateLargeVideoIfDisplayed(id, true);
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
      */
130
      */
340
     onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
131
     onLastNEndpointsChanged(endpointsLeavingLastN, endpointsEnteringLastN) {
341
         if (endpointsLeavingLastN) {
132
         if (endpointsLeavingLastN) {
342
-            endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
133
+            endpointsLeavingLastN.forEach(this._updateLargeVideoIfDisplayed, this);
343
         }
134
         }
344
 
135
 
345
         if (endpointsEnteringLastN) {
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
      * Resizes the video area.
142
      * Resizes the video area.
396
      */
143
      */
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
     changeUserAvatar(id, avatarUrl) {
151
     changeUserAvatar(id, avatarUrl) {
414
         if (this.isCurrentlyOnLarge(id)) {
152
         if (this.isCurrentlyOnLarge(id)) {
415
             largeVideo.updateAvatar(avatarUrl);
153
             largeVideo.updateAvatar(avatarUrl);
432
         return largeVideo && largeVideo.id === id;
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
     updateLargeVideo(id, forceUpdate) {
173
     updateLargeVideo(id, forceUpdate) {
454
         if (!largeVideo) {
174
         if (!largeVideo) {
455
             return;
175
             return;
510
             return Promise.resolve();
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
         let containerTypeToShow = type;
233
         let containerTypeToShow = type;
521
 
234
 
522
         // if we are hiding a container and there is focusedVideo
235
         // if we are hiding a container and there is focusedVideo
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
     isLargeContainerTypeVisible(type) {
252
     isLargeContainerTypeVisible(type) {
561
         return largeVideo;
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
      * Returns the wrapper jquery selector for the largeVideo
273
      * Returns the wrapper jquery selector for the largeVideo
574
      * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
274
      * @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
577
         return this.getCurrentlyOnLargeContainer().$wrapper;
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
      * Helper method to invoke when the video layout has changed and elements
281
      * Helper method to invoke when the video layout has changed and elements
591
      * have to be re-arranged and resized.
282
      * have to be re-arranged and resized.
593
      * @returns {void}
284
      * @returns {void}
594
      */
285
      */
595
     refreshLayout() {
286
     refreshLayout() {
596
-        localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
597
         VideoLayout.resizeVideoArea();
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
         largeVideo = null;
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
      * Triggers an update of large video if the passed in participant is
305
      * Triggers an update of large video if the passed in participant is
640
      * currently displayed on large video.
306
      * currently displayed on large video.

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

9
     sendAnalytics
9
     sendAnalytics
10
 } from '../../react/features/analytics';
10
 } from '../../react/features/analytics';
11
 import { toggleDialog } from '../../react/features/base/dialog';
11
 import { toggleDialog } from '../../react/features/base/dialog';
12
+import { clickOnVideo } from '../../react/features/filmstrip/actions';
12
 import { KeyboardShortcutsDialog }
13
 import { KeyboardShortcutsDialog }
13
     from '../../react/features/keyboard-shortcuts';
14
     from '../../react/features/keyboard-shortcuts';
14
 import { SpeakerStats } from '../../react/features/speaker-stats';
15
 import { SpeakerStats } from '../../react/features/speaker-stats';
54
                 if (_shortcuts.has(key)) {
55
                 if (_shortcuts.has(key)) {
55
                     _shortcuts.get(key).function(e);
56
                     _shortcuts.get(key).function(e);
56
                 } else if (!isNaN(num) && num >= 0 && num <= 9) {
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
       "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
10090
       "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
10091
       "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
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
     "jquery-i18next": {
10093
     "jquery-i18next": {
10099
       "version": "1.2.1",
10094
       "version": "1.2.1",
10100
       "resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz",
10095
       "resolved": "https://registry.npmjs.org/jquery-i18next/-/jquery-i18next-1.2.1.tgz",

+ 0
- 1
package.json 查看文件

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

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

38
      * Used to determine the value of the autoplay attribute of the underlying
38
      * Used to determine the value of the autoplay attribute of the underlying
39
      * video element.
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
             this._attachTrack(nextProps.videoTrack);
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
         return false;
242
         return false;
143
     }
243
     }
144
 
244
 
149
      * @returns {ReactElement}
249
      * @returns {ReactElement}
150
      */
250
      */
151
     render() {
251
     render() {
252
+        const {
253
+            autoPlay,
254
+            className,
255
+            id,
256
+            muted,
257
+            playsinline,
258
+            style,
259
+            eventHandlers
260
+        } = this.props;
261
+
152
         return (
262
         return (
153
             <video
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
      * Used to determine the value of the autoplay attribute of the underlying
29
      * Used to determine the value of the autoplay attribute of the underlying
30
      * video element.
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
      * @returns {ReactElement}
153
      * @returns {ReactElement}
58
      */
154
      */
59
     render() {
155
     render() {
156
+        const {
157
+            _noAutoPlayVideo,
158
+            className,
159
+            id,
160
+            muted,
161
+            videoTrack,
162
+            style,
163
+            eventHandlers
164
+        } = this.props;
165
+
60
         return (
166
         return (
167
+
61
             <Video
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
                 onVideoPlaying = { this._onVideoPlaying }
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
 import { updateSettings } from './actions';
14
 import { updateSettings } from './actions';
15
 import { handleCallIntegrationChange, handleCrashReportingChange } from './functions';
15
 import { handleCallIntegrationChange, handleCrashReportingChange } from './functions';
16
 
16
 
17
+
17
 /**
18
 /**
18
  * The middleware of the feature base/settings. Distributes changes to the state
19
  * The middleware of the feature base/settings. Distributes changes to the state
19
  * of base/settings to the states of other features computed from the state of
20
  * of base/settings to the states of other features computed from the state of

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

75
  */
75
  */
76
 export const TRACK_REMOVED = 'TRACK_REMOVED';
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
  * The type of redux action dispatched when a track's properties were updated.
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
     TRACK_CREATE_ERROR,
25
     TRACK_CREATE_ERROR,
26
     TRACK_NO_DATA_FROM_SOURCE,
26
     TRACK_NO_DATA_FROM_SOURCE,
27
     TRACK_REMOVED,
27
     TRACK_REMOVED,
28
+    TRACK_STOPPED,
28
     TRACK_UPDATED,
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
 } from './actionTypes';
32
 } from './actionTypes';
32
 import {
33
 import {
33
     createLocalTracksF,
34
     createLocalTracksF,
400
 
401
 
401
                     noDataFromSourceNotificationInfo = { timeout };
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
         } else {
413
         } else {
406
             participantId = track.getParticipantId();
414
             participantId = track.getParticipantId();
407
             isReceivingData = true;
415
             isReceivingData = true;

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

163
                 } else {
163
                 } else {
164
                     APP.UI.setVideoMuted(participantID);
164
                     APP.UI.setVideoMuted(participantID);
165
                 }
165
                 }
166
-                APP.UI.onPeerVideoTypeChanged(participantID, jitsiTrack.videoType);
167
             } else if (jitsiTrack.isLocal()) {
166
             } else if (jitsiTrack.isLocal()) {
168
                 APP.conference.setAudioMuteStatus(muted);
167
                 APP.conference.setAudioMuteStatus(muted);
169
             } else {
168
             } else {

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

145
     transport: Array<Object>
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
  * React {@code Component} for displaying connection statistics.
161
  * React {@code Component} for displaying connection statistics.
150
  *
162
  *
161
         const { isLocalVideo, enableSaveLogs } = this.props;
173
         const { isLocalVideo, enableSaveLogs } = this.props;
162
 
174
 
163
         return (
175
         return (
164
-            <div className = 'connection-info'>
176
+            <div
177
+                className = 'connection-info'
178
+                onClick = { onClick }>
165
                 { this._renderStatistics() }
179
                 { this._renderStatistics() }
166
                 <div className = 'connection-actions'>
180
                 <div className = 'connection-actions'>
167
                     { isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}
181
                     { isLocalVideo && enableSaveLogs ? this._renderSaveLogs() : null}

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

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { pinParticipant } from '../base/participants';
3
 import { toState } from '../base/redux';
4
 import { toState } from '../base/redux';
4
 import { CHAT_SIZE } from '../chat/constants';
5
 import { CHAT_SIZE } from '../chat/constants';
5
 
6
 
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
 export * from './actions.native';
89
 export * from './actions.native';

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

20
 import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
20
 import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
21
 import { ConnectionIndicator } from '../../../connection-indicator';
21
 import { ConnectionIndicator } from '../../../connection-indicator';
22
 import { DisplayNameLabel } from '../../../display-name';
22
 import { DisplayNameLabel } from '../../../display-name';
23
-import { RemoteVideoMenu } from '../../../remote-video-menu';
24
-import ConnectionStatusComponent from '../../../remote-video-menu/components/native/ConnectionStatusComponent';
25
 import { toggleToolboxVisible } from '../../../toolbox/actions.native';
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
 import AudioMutedIndicator from './AudioMutedIndicator';
27
 import AudioMutedIndicator from './AudioMutedIndicator';
28
 import DominantSpeakerIndicator from './DominantSpeakerIndicator';
28
 import DominantSpeakerIndicator from './DominantSpeakerIndicator';

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

11
 import { getToolbarButtons } from '../../../base/config';
11
 import { getToolbarButtons } from '../../../base/config';
12
 import { translate } from '../../../base/i18n';
12
 import { translate } from '../../../base/i18n';
13
 import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
13
 import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
14
+import { getLocalParticipant } from '../../../base/participants';
14
 import { connect } from '../../../base/redux';
15
 import { connect } from '../../../base/redux';
15
 import { isButtonEnabled } from '../../../toolbox/functions.web';
16
 import { isButtonEnabled } from '../../../toolbox/functions.web';
16
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
17
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
17
 import { setFilmstripVisible } from '../../actions';
18
 import { setFilmstripVisible } from '../../actions';
18
 import { shouldRemoteVideosBeVisible } from '../../functions';
19
 import { shouldRemoteVideosBeVisible } from '../../functions';
19
 
20
 
21
+import Thumbnail from './Thumbnail';
22
+
20
 declare var APP: Object;
23
 declare var APP: Object;
21
 declare var interfaceConfig: Object;
24
 declare var interfaceConfig: Object;
22
 
25
 
60
      */
63
      */
61
     _isFilmstripButtonEnabled: boolean,
64
     _isFilmstripButtonEnabled: boolean,
62
 
65
 
66
+    /**
67
+     * The participants in the call.
68
+     */
69
+    _participants: Array<Object>,
70
+
63
     /**
71
     /**
64
      * The number of rows in tile view.
72
      * The number of rows in tile view.
65
      */
73
      */
138
      * @returns {ReactElement}
146
      * @returns {ReactElement}
139
      */
147
      */
140
     render() {
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
         const filmstripStyle = { };
149
         const filmstripStyle = { };
149
         const filmstripRemoteVideosContainerStyle = {};
150
         const filmstripRemoteVideosContainerStyle = {};
150
         let remoteVideoContainerClassName = 'remote-videos-container';
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
         case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
158
         case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
154
             // Adding 18px for the 2px margins, 2px borders on the left and right and 5px padding on the left and right.
159
             // Adding 18px for the 2px margins, 2px borders on the left and right and 5px padding on the left and right.
155
             // Also adding 7px for the scrollbar.
160
             // Also adding 7px for the scrollbar.
191
                     <div
196
                     <div
192
                         className = 'filmstrip__videos'
197
                         className = 'filmstrip__videos'
193
                         id = 'filmstripLocalVideo'>
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
                     </div>
206
                     </div>
196
                     <div
207
                     <div
197
                         className = { remoteVideosWrapperClassName }
208
                         className = { remoteVideosWrapperClassName }
205
                             className = { remoteVideoContainerClassName }
216
                             className = { remoteVideoContainerClassName }
206
                             id = 'filmstripRemoteVideosContainer'
217
                             id = 'filmstripRemoteVideosContainer'
207
                             style = { filmstripRemoteVideosContainerStyle }>
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
                         </div>
234
                         </div>
210
                     </div>
235
                     </div>
211
                 </div>
236
                 </div>
314
         _hideScrollbar: Boolean(iAmSipGateway),
339
         _hideScrollbar: Boolean(iAmSipGateway),
315
         _hideToolbar: Boolean(iAmSipGateway),
340
         _hideToolbar: Boolean(iAmSipGateway),
316
         _isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
341
         _isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
342
+        _participants: state['features/base/participants'],
317
         _rows: gridDimensions.rows,
343
         _rows: gridDimensions.rows,
318
         _videosClassName: videosClassName,
344
         _videosClassName: videosClassName,
319
         _visible: visible
345
         _visible: visible

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

1
 // @flow
1
 // @flow
2
 
2
 
3
-import { AtlasKitThemeProvider } from '@atlaskit/theme';
4
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
5
 
4
 
5
+import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
6
 import { AudioLevelIndicator } from '../../../audio-level-indicator';
6
 import { AudioLevelIndicator } from '../../../audio-level-indicator';
7
 import { Avatar } from '../../../base/avatar';
7
 import { Avatar } from '../../../base/avatar';
8
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
8
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
11
 import {
11
 import {
12
     getLocalParticipant,
12
     getLocalParticipant,
13
     getParticipantById,
13
     getParticipantById,
14
-    getParticipantCount
14
+    getParticipantCount,
15
+    pinParticipant
15
 } from '../../../base/participants';
16
 } from '../../../base/participants';
16
 import { connect } from '../../../base/redux';
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
 import { ConnectionIndicator } from '../../../connection-indicator';
25
 import { ConnectionIndicator } from '../../../connection-indicator';
19
 import { DisplayName } from '../../../display-name';
26
 import { DisplayName } from '../../../display-name';
20
 import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
27
 import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
21
 import { PresenceLabel } from '../../../presence-status';
28
 import { PresenceLabel } from '../../../presence-status';
22
-import { RemoteVideoMenuTriggerButton } from '../../../remote-video-menu';
23
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
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
 const JitsiTrackEvents = JitsiMeetJS.events.track;
41
 const JitsiTrackEvents = JitsiMeetJS.events.track;
26
 
42
 
27
 declare var interfaceConfig: Object;
43
 declare var interfaceConfig: Object;
28
 
44
 
29
-
30
 /**
45
 /**
31
  * The type of the React {@code Component} state of {@link Thumbnail}.
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
      * The current audio level value for the Thumbnail.
51
      * The current audio level value for the Thumbnail.
37
      */
52
      */
38
     audioLevel: number,
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
      * The current volume setting for the Thumbnail.
71
      * The current volume setting for the Thumbnail.
42
      */
72
      */
43
     volume: ?number
73
     volume: ?number
44
-};
74
+|};
45
 
75
 
46
 /**
76
 /**
47
  * The type of the React {@code Component} props of {@link Thumbnail}.
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
      * The audio track related to the participant.
82
      * The audio track related to the participant.
73
      */
103
      */
74
     _defaultLocalDisplayName: string,
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
      * Indicates whether the profile functionality is disabled.
112
      * Indicates whether the profile functionality is disabled.
78
      */
113
      */
79
     _disableProfile: boolean,
114
     _disableProfile: boolean,
80
 
115
 
116
+    /**
117
+     * The display mode of the thumbnail.
118
+     */
119
+    _displayMode: number,
120
+
81
     /**
121
     /**
82
      * The height of the Thumbnail.
122
      * The height of the Thumbnail.
83
      */
123
      */
88
      */
128
      */
89
     _heightToWidthPercent: number,
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
      * Disable/enable the dominant speaker indicator.
157
      * Disable/enable the dominant speaker indicator.
93
      */
158
      */
94
     _isDominantSpeakerDisabled: boolean,
159
     _isDominantSpeakerDisabled: boolean,
95
 
160
 
161
+    /**
162
+     * Indicates whether testing mode is enabled.
163
+     */
164
+    _isTestModeEnabled: boolean,
165
+
96
     /**
166
     /**
97
      * The size of the icon of indicators.
167
      * The size of the icon of indicators.
98
      */
168
      */
99
     _indicatorIconSize: number,
169
     _indicatorIconSize: number,
100
 
170
 
171
+    /**
172
+     * The current local video flip setting.
173
+     */
174
+    _localFlipX: boolean,
175
+
101
     /**
176
     /**
102
      * An object with information about the participant related to the thumbnaul.
177
      * An object with information about the participant related to the thumbnaul.
103
      */
178
      */
128
      */
203
      */
129
     dispatch: Function,
204
     dispatch: Function,
130
 
205
 
131
-    /**
132
-     * Indicates whether the thumbnail is hovered or not.
133
-     */
134
-    isHovered: ?boolean,
135
-
136
     /**
206
     /**
137
      * The ID of the participant related to the thumbnail.
207
      * The ID of the participant related to the thumbnail.
138
      */
208
      */
139
     participantID: ?string
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
  * Implements a thumbnail.
225
  * Implements a thumbnail.
145
  * @extends Component
227
  * @extends Component
146
  */
228
  */
147
 class Thumbnail extends Component<Props, State> {
229
 class Thumbnail extends Component<Props, State> {
148
-
149
     /**
230
     /**
150
      * Initializes a new Thumbnail instance.
231
      * Initializes a new Thumbnail instance.
151
      *
232
      *
155
     constructor(props: Props) {
236
     constructor(props: Props) {
156
         super(props);
237
         super(props);
157
 
238
 
158
-        this.state = {
239
+        const state = {
159
             audioLevel: 0,
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
         this._updateAudioLevel = this._updateAudioLevel.bind(this);
252
         this._updateAudioLevel = this._updateAudioLevel.bind(this);
253
+        this._onCanPlay = this._onCanPlay.bind(this);
254
+        this._onClick = this._onClick.bind(this);
164
         this._onVolumeChange = this._onVolumeChange.bind(this);
255
         this._onVolumeChange = this._onVolumeChange.bind(this);
165
         this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
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
      */
267
      */
174
     componentDidMount() {
268
     componentDidMount() {
175
         this._listenForAudioUpdates();
269
         this._listenForAudioUpdates();
270
+        this._onDisplayModeChanged();
176
     }
271
     }
177
 
272
 
178
     /**
273
     /**
182
      * @inheritdoc
277
      * @inheritdoc
183
      * @returns {void}
278
      * @returns {void}
184
      */
279
      */
185
-    componentDidUpdate(prevProps: Props) {
280
+    componentDidUpdate(prevProps: Props, prevState: State) {
186
         if (prevProps._audioTrack !== this.props._audioTrack) {
281
         if (prevProps._audioTrack !== this.props._audioTrack) {
187
             this._stopListeningForAudioUpdates(prevProps._audioTrack);
282
             this._stopListeningForAudioUpdates(prevProps._audioTrack);
188
             this._listenForAudioUpdates();
283
             this._listenForAudioUpdates();
189
             this._updateAudioLevel(0);
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
      * @returns {Object} - The styles for the thumbnail.
457
      * @returns {Object} - The styles for the thumbnail.
254
      */
458
      */
255
     _getStyles(): Object {
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
         switch (_currentLayout) {
469
         switch (_currentLayout) {
260
         case LAYOUTS.TILE_VIEW:
470
         case LAYOUTS.TILE_VIEW:
262
             const avatarSize = _height / 2;
472
             const avatarSize = _height / 2;
263
 
473
 
264
             styles = {
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
                     height: `${avatarSize}px`,
482
                     height: `${avatarSize}px`,
267
                     width: `${avatarSize}px`
483
                     width: `${avatarSize}px`
268
                 }
484
                 }
271
         }
487
         }
272
         case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
488
         case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
273
             styles = {
489
             styles = {
274
-                avatarContainer: {
490
+                thumbnail: {
491
+                    paddingTop: `${_heightToWidthPercent}%`
492
+                },
493
+                avatar: {
275
                     height: '50%',
494
                     height: '50%',
276
                     width: `${_heightToWidthPercent / 2}%`
495
                     width: `${_heightToWidthPercent / 2}%`
277
                 }
496
                 }
280
         }
499
         }
281
         }
500
         }
282
 
501
 
502
+        if (_isHidden) {
503
+            styles.thumbnail.display = 'none';
504
+        }
505
+
283
         return styles;
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
      * Renders a fake participant (youtube video) thumbnail.
546
      * Renders a fake participant (youtube video) thumbnail.
288
      *
547
      *
292
     _renderFakeParticipant() {
551
     _renderFakeParticipant() {
293
         const { _participant } = this.props;
552
         const { _participant } = this.props;
294
         const { id } = _participant;
553
         const { id } = _participant;
554
+        const styles = this._getStyles();
555
+        const containerClassName = this._getContainerClassName();
295
 
556
 
296
         return (
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
                 <img
565
                 <img
299
                     className = 'sharedVideoAvatar'
566
                     className = 'sharedVideoAvatar'
300
                     src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
567
                     src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
303
                         elementID = 'sharedVideoContainer_name'
570
                         elementID = 'sharedVideoContainer_name'
304
                         participantID = { id } />
571
                         participantID = { id } />
305
                 </div>
572
                 </div>
306
-            </>
573
+            </span>
307
         );
574
         );
308
     }
575
     }
309
 
576
 
320
             _isDominantSpeakerDisabled,
587
             _isDominantSpeakerDisabled,
321
             _indicatorIconSize: iconSize,
588
             _indicatorIconSize: iconSize,
322
             _participant,
589
             _participant,
323
-            _participantCount,
324
-            isHovered
590
+            _participantCount
325
         } = this.props;
591
         } = this.props;
592
+        const { isHovered } = this.state;
326
         const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
593
         const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
327
         const { id, local = false, dominantSpeaker = false } = _participant;
594
         const { id, local = false, dominantSpeaker = false } = _participant;
328
         const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
595
         const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
344
 
611
 
345
         return (
612
         return (
346
             <div>
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
                         iconSize = { iconSize }
618
                         iconSize = { iconSize }
619
+                        isLocalVideo = { local }
359
                         participantId = { id }
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
                         tooltipPosition = { tooltipPosition } />
630
                         tooltipPosition = { tooltipPosition } />
361
-                    { showDominantSpeaker && _participantCount > 2
362
-                        && <DominantSpeakerIndicator
363
-                            iconSize = { iconSize }
364
-                            tooltipPosition = { tooltipPosition } />
365
-                    }
366
-                </AtlasKitThemeProvider>
631
+                }
367
             </div>);
632
             </div>);
368
     }
633
     }
369
 
634
 
370
     /**
635
     /**
371
      * Renders the avatar.
636
      * Renders the avatar.
372
      *
637
      *
638
+     * @param {Object} styles - The styles that will be applied to the avatar.
373
      * @returns {ReactElement}
639
      * @returns {ReactElement}
374
      */
640
      */
375
-    _renderAvatar() {
641
+    _renderAvatar(styles) {
376
         const { _participant } = this.props;
642
         const { _participant } = this.props;
377
         const { id } = _participant;
643
         const { id } = _participant;
378
-        const styles = this._getStyles();
379
 
644
 
380
         return (
645
         return (
381
             <div
646
             <div
382
                 className = 'avatar-container'
647
                 className = 'avatar-container'
383
-                style = { styles.avatarContainer }>
648
+                style = { styles }>
384
                 <Avatar
649
                 <Avatar
385
                     className = 'userAvatar'
650
                     className = 'userAvatar'
386
                     participantId = { id } />
651
                     participantId = { id } />
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
      * Renders the local participant's thumbnail.
689
      * Renders the local participant's thumbnail.
393
      *
690
      *
396
     _renderLocalParticipant() {
693
     _renderLocalParticipant() {
397
         const {
694
         const {
398
             _defaultLocalDisplayName,
695
             _defaultLocalDisplayName,
696
+            _disableLocalVideoFlip,
697
+            _isScreenSharing,
698
+            _localFlipX,
399
             _disableProfile,
699
             _disableProfile,
400
             _participant,
700
             _participant,
401
             _videoTrack
701
             _videoTrack
402
         } = this.props;
702
         } = this.props;
403
         const { id } = _participant || {};
703
         const { id } = _participant || {};
404
         const { audioLevel } = this.state;
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
         return (
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
                 <div className = 'videocontainer__background' />
719
                 <div className = 'videocontainer__background' />
410
                 <span id = 'localVideoWrapper'>
720
                 <span id = 'localVideoWrapper'>
411
                     <VideoTrack
721
                     <VideoTrack
722
+                        className = { videoTrackClassName }
412
                         id = 'localVideo_container'
723
                         id = 'localVideo_container'
413
                         videoTrack = { _videoTrack } />
724
                         videoTrack = { _videoTrack } />
414
                 </span>
725
                 </span>
419
                     { this._renderTopIndicators() }
730
                     { this._renderTopIndicators() }
420
                 </div>
731
                 </div>
421
                 <div className = 'videocontainer__hoverOverlay' />
732
                 <div className = 'videocontainer__hoverOverlay' />
422
-                <div className = 'displayNameContainer'>
733
+                <div
734
+                    className = 'displayNameContainer'
735
+                    onClick = { onClick }>
423
                     <DisplayName
736
                     <DisplayName
424
                         allowEditing = { !_disableProfile }
737
                         allowEditing = { !_disableProfile }
425
                         displayNameSuffix = { _defaultLocalDisplayName }
738
                         displayNameSuffix = { _defaultLocalDisplayName }
426
                         elementID = 'localDisplayName'
739
                         elementID = 'localDisplayName'
427
                         participantID = { id } />
740
                         participantID = { id } />
428
                 </div>
741
                 </div>
429
-                { this._renderAvatar() }
742
+                { this._renderAvatar(styles.avatar) }
743
+                <span className = 'localvideomenu'>
744
+                    <LocalVideoMenuTriggerButton />
745
+                </span>
430
                 <span className = 'audioindicator-container'>
746
                 <span className = 'audioindicator-container'>
431
                     <AudioLevelIndicator audioLevel = { audioLevel } />
747
                     <AudioLevelIndicator audioLevel = { audioLevel } />
432
                 </span>
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
      * Renders a remote participant's 'thumbnail.
793
      * Renders a remote participant's 'thumbnail.
443
     _renderRemoteParticipant() {
797
     _renderRemoteParticipant() {
444
         const {
798
         const {
445
             _audioTrack,
799
             _audioTrack,
800
+            _isTestModeEnabled,
446
             _participant,
801
             _participant,
447
-            _startSilent
802
+            _startSilent,
803
+            _videoTrack
448
         } = this.props;
804
         } = this.props;
449
         const { id } = _participant;
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
         // hide volume when in silent mode
810
         // hide volume when in silent mode
453
         const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
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
         return (
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
                 <div className = 'videocontainer__background' />
854
                 <div className = 'videocontainer__background' />
471
                 <div className = 'videocontainer__toptoolbar'>
855
                 <div className = 'videocontainer__toptoolbar'>
480
                         elementID = { `participant_${id}_name` }
864
                         elementID = { `participant_${id}_name` }
481
                         participantID = { id } />
865
                         participantID = { id } />
482
                 </div>
866
                 </div>
483
-                { this._renderAvatar() }
867
+                { this._renderAvatar(styles.avatar) }
484
                 <div className = 'presence-label-container'>
868
                 <div className = 'presence-label-container'>
485
                     <PresenceLabel
869
                     <PresenceLabel
486
                         className = 'presence-label'
870
                         className = 'presence-label'
487
                         participantID = { id } />
871
                         participantID = { id } />
488
                 </div>
872
                 </div>
489
                 <span className = 'remotevideomenu'>
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
                 </span>
878
                 </span>
497
                 <span className = 'audioindicator-container'>
879
                 <span className = 'audioindicator-container'>
498
                     <AudioLevelIndicator audioLevel = { audioLevel } />
880
                     <AudioLevelIndicator audioLevel = { audioLevel } />
499
                 </span>
881
                 </span>
500
-            </>
882
+            </span>
501
         );
883
         );
502
     }
884
     }
503
 
885
 
527
         this.setState({ volume: value });
909
         this.setState({ volume: value });
528
     }
910
     }
529
 
911
 
530
-
531
     /**
912
     /**
532
      * Implements React's {@link Component#render()}.
913
      * Implements React's {@link Component#render()}.
533
      *
914
      *
568
 
949
 
569
     // Only the local participant won't have id for the time when the conference is not yet joined.
950
     // Only the local participant won't have id for the time when the conference is not yet joined.
570
     const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
951
     const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
952
+    const { id } = participant;
571
     const isLocal = participant?.local ?? true;
953
     const isLocal = participant?.local ?? true;
954
+    const tracks = state['features/base/tracks'];
572
     const _videoTrack = isLocal
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
     const _audioTrack = isLocal
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
     const _currentLayout = getCurrentLayout(state);
959
     const _currentLayout = getCurrentLayout(state);
579
     let size = {};
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
     const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
968
     const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
969
+    const { localFlipX } = state['features/base/settings'];
582
 
970
 
583
 
971
 
584
     switch (_currentLayout) {
972
     switch (_currentLayout) {
617
     }
1005
     }
618
     }
1006
     }
619
 
1007
 
620
-
621
     return {
1008
     return {
622
         _audioTrack,
1009
         _audioTrack,
623
         _connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
1010
         _connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
624
         _connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
1011
         _connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
625
         _currentLayout,
1012
         _currentLayout,
626
         _defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
1013
         _defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
1014
+        _disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
627
         _disableProfile: disableProfile,
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
         _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
1019
         _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
1020
+        _isScreenSharing: _videoTrack?.videoType === 'desktop',
1021
+        _isTestModeEnabled: isTestModeEnabled(state),
1022
+        _isVideoPlayable: isVideoPlayable(state, id),
629
         _indicatorIconSize: NORMAL,
1023
         _indicatorIconSize: NORMAL,
1024
+        _localFlipX: Boolean(localFlipX),
630
         _participant: participant,
1025
         _participant: participant,
631
         _participantCount: getParticipantCount(state),
1026
         _participantCount: getParticipantCount(state),
632
         _startSilent: Boolean(startSilent),
1027
         _startSilent: Boolean(startSilent),

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

47
  * An extended number of columns for tile view.
47
  * An extended number of columns for tile view.
48
  */
48
  */
49
 export const ABSOLUTE_MAX_COLUMNS = 7;
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
     isRemoteTrackMuted
16
     isRemoteTrackMuted
17
 } from '../base/tracks/functions';
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
 declare var interfaceConfig: Object;
30
 declare var interfaceConfig: Object;
22
 
31
 
176
 
185
 
177
     return Math.min(filmstripMaxWidth, window.innerWidth);
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 查看文件

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
 // @flow
1
 // @flow
2
 
2
 
3
-import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
3
+import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
4
 import { MiddlewareRegistry } from '../base/redux';
4
 import { MiddlewareRegistry } from '../base/redux';
5
 import { CLIENT_RESIZED } from '../base/responsive-ui';
5
 import { CLIENT_RESIZED } from '../base/responsive-ui';
6
+import { SETTINGS_UPDATED } from '../base/settings';
6
 import {
7
 import {
7
     getCurrentLayout,
8
     getCurrentLayout,
8
-    LAYOUTS,
9
-    shouldDisplayTileView
9
+    LAYOUTS
10
 } from '../video-layout';
10
 } from '../video-layout';
11
 
11
 
12
-import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
13
 import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
12
 import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
14
 
13
 
15
 import './subscriber.web';
14
 import './subscriber.web';
48
         }
47
         }
49
         break;
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
         break;
55
         break;
73
     }
56
     }
74
     }
57
     }

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

1
 // @flow
1
 // @flow
2
 
2
 
3
-import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
4
-import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
5
 import { StateListenerRegistry, equals } from '../base/redux';
3
 import { StateListenerRegistry, equals } from '../base/redux';
6
 import { setFilmstripVisible } from '../filmstrip/actions';
4
 import { setFilmstripVisible } from '../filmstrip/actions';
7
 import { setOverflowDrawer } from '../toolbox/actions.web';
5
 import { setOverflowDrawer } from '../toolbox/actions.web';
71
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
69
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
72
             store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
70
             store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
73
             break;
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
  * Listens for changes in the chat state to calculate the dimensions of the tile view grid and the tiles.
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
 import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
34
 import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
35
 import { openChat } from '../../chat/actions';
35
 import { openChat } from '../../chat/actions';
36
 import { sendMessage, setPrivateMessageRecipient, closeChat } from '../../chat/actions.any';
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
 import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
38
 import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
39
 
39
 
40
 import { setParticipantsWithScreenShare } from './actions';
40
 import { setParticipantsWithScreenShare } from './actions';

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

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
 import { AbstractAudioMuteButton } from '../../base/toolbox/components';
13
 import { AbstractAudioMuteButton } from '../../base/toolbox/components';
14
 import type { AbstractButtonProps } from '../../base/toolbox/components';
14
 import type { AbstractButtonProps } from '../../base/toolbox/components';
15
 import { isLocalTrackMuted } from '../../base/tracks';
15
 import { isLocalTrackMuted } from '../../base/tracks';
16
-import { muteLocal } from '../../remote-video-menu/actions';
16
+import { muteLocal } from '../../video-menu/actions';
17
 
17
 
18
 declare var APP: Object;
18
 declare var APP: Object;
19
 
19
 

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

7
 import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
7
 import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
8
 import { connect } from '../../base/redux';
8
 import { connect } from '../../base/redux';
9
 import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
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
 type Props = AbstractButtonProps & {
12
 type Props = AbstractButtonProps & {
13
 
13
 

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

7
 import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
7
 import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
8
 import { connect } from '../../base/redux';
8
 import { connect } from '../../base/redux';
9
 import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
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
 type Props = AbstractButtonProps & {
12
 type Props = AbstractButtonProps & {
13
 
13
 

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

4
 import { CONFERENCE_WILL_LEAVE } from '../base/conference';
4
 import { CONFERENCE_WILL_LEAVE } from '../base/conference';
5
 import { MEDIA_TYPE } from '../base/media';
5
 import { MEDIA_TYPE } from '../base/media';
6
 import {
6
 import {
7
-    DOMINANT_SPEAKER_CHANGED,
7
+    getLocalParticipant,
8
     PARTICIPANT_JOINED,
8
     PARTICIPANT_JOINED,
9
-    PARTICIPANT_LEFT,
10
-    PARTICIPANT_UPDATED,
11
-    PIN_PARTICIPANT,
12
-    getParticipantById
9
+    PARTICIPANT_UPDATED
13
 } from '../base/participants';
10
 } from '../base/participants';
14
 import { MiddlewareRegistry } from '../base/redux';
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
 import { SET_FILMSTRIP_VISIBLE } from '../filmstrip';
13
 import { SET_FILMSTRIP_VISIBLE } from '../filmstrip';
17
 
14
 
18
 import './middleware.any';
15
 import './middleware.any';
40
 
37
 
41
     case PARTICIPANT_JOINED:
38
     case PARTICIPANT_JOINED:
42
         if (!action.participant.local) {
39
         if (!action.participant.local) {
43
-            VideoLayout.addRemoteParticipantContainer(
44
-                getParticipantById(store.getState(), action.participant.id));
40
+            VideoLayout.updateVideoMutedForNoTracks(action.participant.id);
45
         }
41
         }
46
         break;
42
         break;
47
 
43
 
48
-    case PARTICIPANT_LEFT:
49
-        VideoLayout.removeParticipantContainer(action.participant.id);
50
-        break;
51
-
52
     case PARTICIPANT_UPDATED: {
44
     case PARTICIPANT_UPDATED: {
53
         // Look for actions that triggered a change to connectionStatus. This is
45
         // Look for actions that triggered a change to connectionStatus. This is
54
         // done instead of changing the connection status change action to be
46
         // done instead of changing the connection status change action to be
61
         break;
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
     case SET_FILMSTRIP_VISIBLE:
56
     case SET_FILMSTRIP_VISIBLE:
73
         VideoLayout.resizeVideoArea();
57
         VideoLayout.resizeVideoArea();
74
         break;
58
         break;
75
 
59
 
76
     case TRACK_ADDED:
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
         break;
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
     case TRACK_REMOVED:
75
     case TRACK_REMOVED:
83
         if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
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
         break;
80
         break;

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

10
     sendAnalytics,
10
     sendAnalytics,
11
     VIDEO_MUTE
11
     VIDEO_MUTE
12
 } from '../analytics';
12
 } from '../analytics';
13
-import { hideDialog } from '../base/dialog';
14
 import {
13
 import {
15
     MEDIA_TYPE,
14
     MEDIA_TYPE,
16
     setAudioMuted,
15
     setAudioMuted,
22
     muteRemoteParticipant
21
     muteRemoteParticipant
23
 } from '../base/participants';
22
 } from '../base/participants';
24
 
23
 
25
-import { RemoteVideoMenu } from './components';
26
-
27
 declare var APP: Object;
24
 declare var APP: Object;
28
 
25
 
29
 const logger = getLogger(__filename);
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
  * Mutes the local participant.
29
  * Mutes the local participant.
42
  *
30
  *

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

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 查看文件

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
 import { connect } from '../../../base/redux';
11
 import { connect } from '../../../base/redux';
12
 import { StyleType } from '../../../base/styles';
12
 import { StyleType } from '../../../base/styles';
13
 import { PrivateMessageButton } from '../../../chat';
13
 import { PrivateMessageButton } from '../../../chat';
14
-import { hideRemoteVideoMenu } from '../../actions';
14
+import { hideRemoteVideoMenu } from '../../actions.native';
15
 
15
 
16
 import ConnectionStatusButton from './ConnectionStatusButton';
16
 import ConnectionStatusButton from './ConnectionStatusButton';
17
 import GrantModeratorButton from './GrantModeratorButton';
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 查看文件

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
     type Props
10
     type Props
11
 } from '../AbstractGrantModeratorButton';
11
 } from '../AbstractGrantModeratorButton';
12
 
12
 
13
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
13
+import VideoMenuButton from './VideoMenuButton';
14
 
14
 
15
 declare var interfaceConfig: Object;
15
 declare var interfaceConfig: Object;
16
 
16
 
44
         }
44
         }
45
 
45
 
46
         return (
46
         return (
47
-            <RemoteVideoMenuButton
47
+            <VideoMenuButton
48
                 buttonText = { t('videothumbnail.grantModerator') }
48
                 buttonText = { t('videothumbnail.grantModerator') }
49
                 displayClass = 'grantmoderatorlink'
49
                 displayClass = 'grantmoderatorlink'
50
                 icon = { IconCrown }
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
     type Props
9
     type Props
10
 } from '../AbstractKickButton';
10
 } from '../AbstractKickButton';
11
 
11
 
12
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
12
+import VideoMenuButton from './VideoMenuButton';
13
 
13
 
14
 /**
14
 /**
15
  * Implements a React {@link Component} which displays a button for kicking out
15
  * Implements a React {@link Component} which displays a button for kicking out
43
         const { participantID, t } = this.props;
43
         const { participantID, t } = this.props;
44
 
44
 
45
         return (
45
         return (
46
-            <RemoteVideoMenuButton
46
+            <VideoMenuButton
47
                 buttonText = { t('videothumbnail.kick') }
47
                 buttonText = { t('videothumbnail.kick') }
48
                 displayClass = 'kicklink'
48
                 displayClass = 'kicklink'
49
                 icon = { IconKick }
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 查看文件

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
     type Props
10
     type Props
11
 } from '../AbstractMuteButton';
11
 } from '../AbstractMuteButton';
12
 
12
 
13
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
13
+import VideoMenuButton from './VideoMenuButton';
14
 
14
 
15
 /**
15
 /**
16
  * Implements a React {@link Component} which displays a button for audio muting
16
  * Implements a React {@link Component} which displays a button for audio muting
51
         };
51
         };
52
 
52
 
53
         return (
53
         return (
54
-            <RemoteVideoMenuButton
54
+            <VideoMenuButton
55
                 buttonText = { t(muteConfig.translationKey) }
55
                 buttonText = { t(muteConfig.translationKey) }
56
                 displayClass = { muteConfig.muteClassName }
56
                 displayClass = { muteConfig.muteClassName }
57
                 icon = { IconMicDisabled }
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
     type Props
9
     type Props
10
 } from '../AbstractMuteEveryoneElseButton';
10
 } from '../AbstractMuteEveryoneElseButton';
11
 
11
 
12
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
12
+import VideoMenuButton from './VideoMenuButton';
13
 
13
 
14
 /**
14
 /**
15
  * Implements a React {@link Component} which displays a button for audio muting
15
  * Implements a React {@link Component} which displays a button for audio muting
38
         const { participantID, t } = this.props;
38
         const { participantID, t } = this.props;
39
 
39
 
40
         return (
40
         return (
41
-            <RemoteVideoMenuButton
41
+            <VideoMenuButton
42
                 buttonText = { t('videothumbnail.domuteOthers') }
42
                 buttonText = { t('videothumbnail.domuteOthers') }
43
                 displayClass = { 'mutelink' }
43
                 displayClass = { 'mutelink' }
44
                 icon = { IconMuteEveryoneElse }
44
                 icon = { IconMuteEveryoneElse }

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

9
     type Props
9
     type Props
10
 } from '../AbstractMuteEveryoneElsesVideoButton';
10
 } from '../AbstractMuteEveryoneElsesVideoButton';
11
 
11
 
12
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
12
+import VideoMenuButton from './VideoMenuButton';
13
 
13
 
14
 /**
14
 /**
15
  * Implements a React {@link Component} which displays a button for audio muting
15
  * Implements a React {@link Component} which displays a button for audio muting
38
         const { participantID, t } = this.props;
38
         const { participantID, t } = this.props;
39
 
39
 
40
         return (
40
         return (
41
-            <RemoteVideoMenuButton
41
+            <VideoMenuButton
42
                 buttonText = { t('videothumbnail.domuteVideoOfOthers') }
42
                 buttonText = { t('videothumbnail.domuteVideoOfOthers') }
43
                 displayClass = { 'mutelink' }
43
                 displayClass = { 'mutelink' }
44
                 icon = { IconMuteVideoEveryoneElse }
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
     type Props
10
     type Props
11
 } from '../AbstractMuteVideoButton';
11
 } from '../AbstractMuteVideoButton';
12
 
12
 
13
-import RemoteVideoMenuButton from './RemoteVideoMenuButton';
13
+import VideoMenuButton from './VideoMenuButton';
14
 
14
 
15
 /**
15
 /**
16
  * Implements a React {@link Component} which displays a button for disabling
16
  * Implements a React {@link Component} which displays a button for disabling
51
         };
51
         };
52
 
52
 
53
         return (
53
         return (
54
-            <RemoteVideoMenuButton
54
+            <VideoMenuButton
55
                 buttonText = { t(muteConfig.translationKey) }
55
                 buttonText = { t(muteConfig.translationKey) }
56
                 displayClass = { muteConfig.muteClassName }
56
                 displayClass = { muteConfig.muteClassName }
57
                 icon = { IconCameraDisabled }
57
                 icon = { IconCameraDisabled }

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

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

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

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

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

20
     KickButton,
20
     KickButton,
21
     PrivateMessageMenuButton,
21
     PrivateMessageMenuButton,
22
     RemoteControlButton,
22
     RemoteControlButton,
23
-    RemoteVideoMenu,
23
+    VideoMenu,
24
     VolumeSlider
24
     VolumeSlider
25
 } from './';
25
 } from './';
26
 
26
 
91
 
91
 
92
 /**
92
 /**
93
  * React {@code Component} for displaying an icon associated with opening the
93
  * React {@code Component} for displaying an icon associated with opening the
94
- * the {@code RemoteVideoMenu}.
94
+ * the {@code VideoMenu}.
95
  *
95
  *
96
  * @extends {Component}
96
  * @extends {Component}
97
  */
97
  */
98
 class RemoteVideoMenuTriggerButton extends Component<Props> {
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
      * Implements React's {@link Component#render()}.
100
      * Implements React's {@link Component#render()}.
111
      *
101
      *
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
      * the remote participant.
130
      * the remote participant.
141
      *
131
      *
142
      * @private
132
      * @private
230
 
220
 
231
         if (buttons.length > 0) {
221
         if (buttons.length > 0) {
232
             return (
222
             return (
233
-                <RemoteVideoMenu id = { participantID }>
223
+                <VideoMenu id = { participantID }>
234
                     { buttons }
224
                     { buttons }
235
-                </RemoteVideoMenu>
225
+                </VideoMenu>
236
             );
226
             );
237
         }
227
         }
238
 
228
 

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

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
 
6
 
7
 /**
7
 /**
8
  * The type of the React {@code Component} props of
8
  * The type of the React {@code Component} props of
9
- * {@link RemoteVideoMenuButton}.
9
+ * {@link VideoMenuButton}.
10
  */
10
  */
11
 type Props = {
11
 type Props = {
12
 
12
 
23
     /**
23
     /**
24
      * The icon that will display within the component.
24
      * The icon that will display within the component.
25
      */
25
      */
26
-    icon: Object,
26
+    icon?: Object,
27
 
27
 
28
     /**
28
     /**
29
      * The id attribute to be added to the component's DOM for retrieval when
29
      * The id attribute to be added to the component's DOM for retrieval when
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
  * @extends {Component}
43
  * @extends {Component}
44
  */
44
  */
45
-export default class RemoteVideoMenuButton extends Component<Props> {
45
+export default class VideoMenuButton extends Component<Props> {
46
     /**
46
     /**
47
      * Implements React's {@link Component#render()}.
47
      * Implements React's {@link Component#render()}.
48
      *
48
      *
67
                     id = { id }
67
                     id = { id }
68
                     onClick = { onClick }>
68
                     onClick = { onClick }>
69
                     <span className = 'popupmenu__icon'>
69
                     <span className = 'popupmenu__icon'>
70
-                        <Icon src = { icon } />
70
+                        { icon && <Icon src = { icon } /> }
71
                     </span>
71
                     </span>
72
                     <span className = 'popupmenu__text'>
72
                     <span className = 'popupmenu__text'>
73
                         { buttonText }
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
 export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
14
 export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
15
 export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
15
 export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
16
 export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
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
 export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
18
 export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
19
+export { default as LocalVideoMenuTriggerButton } from './LocalVideoMenuTriggerButton';
19
 export { default as VolumeSlider } from './VolumeSlider';
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
     VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
48
     VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
49
     AUDIO_DEVICE_CHANGED: 'UI.audio_device_changed',
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
      * Notifies that the side toolbar container has been toggled. The actual
52
      * Notifies that the side toolbar container has been toggled. The actual
58
      * event must contain the identifier of the container that has been toggled
53
      * event must contain the identifier of the container that has been toggled
63
     /**
58
     /**
64
      * Notifies that the raise hand has been changed.
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
 };

正在加载...
取消
保存