ソースを参照

feat(tile-view): initial implementation for tile view (#3317)

* feat(tile-view): initial implementation for tile view

- Modify the classname on the app root so layout can adjust
  depending on the desired layout mode--vertical filmstrip,
  horizontal filmstrip, and tile view.
- Create a button for toggling tile view.
- Add a StateListenerRegistry to automatically update the
  selected participant and max receiver frame height on tile
  view toggle.
- Rezise thumbnails when switching in and out of tile view.
- Move the local video when switching in and out of tile view.
- Update reactified pieces of thumbnails when switching in and
  out of tile view.
- Cap the max receiver video quality in tile view based on tile
  size.
- Use CSS to hide UI components that should not display in tile
  view.
- Signal follow me changes.

* change local video id for tests

* change approach: leverage more css

* squash: fix some formatting

* squash: prevent pinning, hide pin border in tile view

* squash: change logic for maxReceiverQuality due to sidestepping resizing logic

* squash: fix typo, columns configurable, remove unused constants

* squash: resize with js again

* squash: use yana's math for calculating tile size
efficient_tiling
virtuacoplenny 6年前
コミット
c353e9377f
32個のファイルの変更876行の追加81行の削除
  1. 3
    8
      css/filmstrip/_small_video.scss
  2. 113
    0
      css/filmstrip/_tile_view.scss
  3. 47
    0
      css/filmstrip/_tile_view_overrides.scss
  4. 2
    0
      css/main.scss
  5. 8
    1
      interface_config.js
  6. 2
    0
      lang/main.json
  7. 66
    1
      modules/FollowMe.js
  8. 6
    2
      modules/UI/shared_video/SharedVideoThumb.js
  9. 99
    11
      modules/UI/videolayout/Filmstrip.js
  10. 56
    13
      modules/UI/videolayout/LocalVideo.js
  11. 19
    4
      modules/UI/videolayout/RemoteVideo.js
  12. 51
    4
      modules/UI/videolayout/SmallVideo.js
  13. 33
    3
      modules/UI/videolayout/VideoLayout.js
  14. 34
    1
      react/features/base/conference/functions.js
  15. 46
    23
      react/features/conference/components/Conference.web.js
  16. 2
    2
      react/features/filmstrip/components/web/Filmstrip.js
  17. 8
    4
      react/features/large-video/actions.js
  18. 10
    2
      react/features/large-video/components/AbstractLabels.js
  19. 2
    1
      react/features/large-video/components/Labels.web.js
  20. 1
    1
      react/features/large-video/components/LargeVideo.web.js
  21. 3
    0
      react/features/toolbox/components/web/Toolbox.js
  22. 10
    0
      react/features/video-layout/actionTypes.js
  23. 20
    0
      react/features/video-layout/actions.js
  24. 90
    0
      react/features/video-layout/components/TileViewButton.js
  25. 1
    0
      react/features/video-layout/components/index.js
  26. 10
    0
      react/features/video-layout/constants.js
  27. 78
    0
      react/features/video-layout/functions.js
  28. 8
    0
      react/features/video-layout/index.js
  29. 6
    0
      react/features/video-layout/middleware.web.js
  30. 17
    0
      react/features/video-layout/reducer.js
  31. 24
    0
      react/features/video-layout/subscriber.js
  32. 1
    0
      service/UI/UIEvents.js

+ 3
- 8
css/filmstrip/_small_video.scss ファイルの表示

@@ -14,14 +14,9 @@
14 14
      * Focused video thumbnail.
15 15
      */
16 16
     &.videoContainerFocused {
17
-        transition-duration: 0.5s;
18
-        -webkit-transition-duration: 0.5s;
19
-        -webkit-animation-name: greyPulse;
20
-        -webkit-animation-duration: 2s;
21
-        -webkit-animation-iteration-count: 1;
22
-        border: $thumbnailVideoBorder solid $videoThumbnailSelected !important;
17
+        border: $thumbnailVideoBorder solid $videoThumbnailSelected;
23 18
         box-shadow: inset 0 0 3px $videoThumbnailSelected,
24
-        0 0 3px $videoThumbnailSelected !important;
19
+        0 0 3px $videoThumbnailSelected;
25 20
     }
26 21
 
27 22
     .remotevideomenu > .icon-menu {
@@ -31,7 +26,7 @@
31 26
     /**
32 27
      * Hovered video thumbnail.
33 28
      */
34
-    &:hover {
29
+    &:hover:not(.videoContainerFocused):not(.active-speaker) {
35 30
         cursor: hand;
36 31
         border: $thumbnailVideoBorder solid $videoThumbnailHovered;
37 32
         box-shadow: inset 0 0 3px $videoThumbnailHovered,

+ 113
- 0
css/filmstrip/_tile_view.scss ファイルの表示

@@ -0,0 +1,113 @@
1
+/**
2
+ * CSS styles that are specific to the filmstrip that shows the thumbnail tiles.
3
+ */
4
+.tile-view {
5
+    /**
6
+     * Add a border around the active speaker to make the thumbnail easier to
7
+     * see.
8
+     */
9
+    .active-speaker {
10
+        border: $thumbnailVideoBorder solid $videoThumbnailSelected;
11
+        box-shadow: inset 0 0 3px $videoThumbnailSelected,
12
+        0 0 3px $videoThumbnailSelected;
13
+    }
14
+
15
+    #filmstripRemoteVideos {
16
+        align-items: center;
17
+        box-sizing: border-box;
18
+        display: flex;
19
+        flex-direction: column;
20
+        height: 100vh;
21
+        width: 100vw;
22
+    }
23
+
24
+    .filmstrip__videos .videocontainer {
25
+        &:not(.active-speaker),
26
+        &:hover:not(.active-speaker) {
27
+            border: none;
28
+            box-shadow: none;
29
+        }
30
+    }
31
+
32
+    #remoteVideos {
33
+        /**
34
+         * Height is modified with an inline style in horizontal filmstrip mode
35
+         * so !important is used to override that.
36
+         */
37
+        height: 100% !important;
38
+        width: 100%;
39
+    }
40
+
41
+    .filmstrip {
42
+        align-items: center;
43
+        display: flex;
44
+        height: 100%;
45
+        justify-content: center;
46
+        left: 0;
47
+        position: fixed;
48
+        top: 0;
49
+        width: 100%;
50
+        z-index: $filmstripVideosZ
51
+    }
52
+
53
+    /**
54
+     * Regardless of the user setting, do not let the filmstrip be in a hidden
55
+     * state.
56
+     */
57
+    .filmstrip__videos.hidden {
58
+        display: block;
59
+    }
60
+
61
+    #filmstripRemoteVideos {
62
+        box-sizing: border-box;
63
+
64
+        /**
65
+         * Allow scrolling of the thumbnails.
66
+         */
67
+        overflow: auto;
68
+    }
69
+
70
+    /**
71
+     * The size of the thumbnails should be set with javascript, based on
72
+     * desired column count and window width. The rows are created using flex
73
+     * and allowing the thumbnails to wrap.
74
+     */
75
+    #filmstripRemoteVideosContainer {
76
+        align-content: center;
77
+        align-items: center;
78
+        box-sizing: border-box;
79
+        display: flex;
80
+        flex-wrap: wrap;
81
+        height: 100vh;
82
+        justify-content: center;
83
+        padding: 100px 0;
84
+
85
+        .videocontainer {
86
+            box-sizing: border-box;
87
+            display: block;
88
+            margin: 5px;
89
+        }
90
+
91
+        video {
92
+            object-fit: contain;
93
+        }
94
+    }
95
+
96
+    .has-overflow#filmstripRemoteVideosContainer {
97
+        align-content: baseline;
98
+    }
99
+
100
+    .has-overflow .videocontainer {
101
+        align-self: baseline;
102
+    }
103
+
104
+    /**
105
+     * Firefox flex acts a little differently. To make sure the bottom row of
106
+     * thumbnails is not overlapped by the horizontal toolbar, margin is added
107
+     * to the local thumbnail to keep it from the bottom of the screen. It is
108
+     * assumed the local thumbnail will always be on the bottom row.
109
+     */
110
+    .has-overflow #localVideoContainer {
111
+        margin-bottom: 100px !important;
112
+    }
113
+}

+ 47
- 0
css/filmstrip/_tile_view_overrides.scss ファイルの表示

@@ -0,0 +1,47 @@
1
+/**
2
+ * Various overrides outside of the filmstrip to style the app to support a
3
+ * tiled thumbnail experience.
4
+ */
5
+.tile-view {
6
+    /**
7
+     * Let the avatar grow with the tile.
8
+     */
9
+    .userAvatar {
10
+        max-height: initial;
11
+        max-width: initial;
12
+    }
13
+
14
+    /**
15
+     * Hide various features that should not be displayed while in tile view.
16
+     */
17
+    #dominantSpeaker,
18
+    #filmstripLocalVideoThumbnail,
19
+    #largeVideoElementsContainer,
20
+    #sharedVideo,
21
+    .filmstrip__toolbar {
22
+        display: none;
23
+    }
24
+
25
+    #localConnectionMessage,
26
+    #remoteConnectionMessage,
27
+    .watermark {
28
+        z-index: $filmstripVideosZ + 1;
29
+    }
30
+
31
+    /**
32
+     * The follow styling uses !important to override inline styles set with
33
+     * javascript.
34
+     *
35
+     * TODO: These overrides should be more easy to remove and should be removed
36
+     * when the components are in react so their rendering done declaratively,
37
+     * making conditional styling easier to apply.
38
+     */
39
+    #largeVideoElementsContainer,
40
+    #remoteConnectionMessage,
41
+    #remotePresenceMessage {
42
+        display: none !important;
43
+    }
44
+    #largeVideoContainer {
45
+        background-color: $defaultBackground !important;
46
+    }
47
+}

+ 2
- 0
css/main.scss ファイルの表示

@@ -72,6 +72,8 @@
72 72
 @import 'filmstrip/filmstrip_toolbar';
73 73
 @import 'filmstrip/horizontal_filmstrip';
74 74
 @import 'filmstrip/small_video';
75
+@import 'filmstrip/tile_view';
76
+@import 'filmstrip/tile_view_overrides';
75 77
 @import 'filmstrip/vertical_filmstrip';
76 78
 @import 'filmstrip/vertical_filmstrip_overrides';
77 79
 @import 'unsupported-browser/main';

+ 8
- 1
interface_config.js ファイルの表示

@@ -48,7 +48,8 @@ var interfaceConfig = {
48 48
         'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
49 49
         'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
50 50
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
51
-        'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts'
51
+        'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
52
+        'tileview'
52 53
     ],
53 54
 
54 55
     SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
@@ -172,6 +173,12 @@ var interfaceConfig = {
172 173
      */
173 174
     RECENT_LIST_ENABLED: true
174 175
 
176
+    /**
177
+     * How many columns the tile view can expand to. The respected range is
178
+     * between 1 and 5.
179
+     */
180
+    // TILE_VIEW_MAX_COLUMNS: 5,
181
+
175 182
     /**
176 183
      * Specify custom URL for downloading android mobile app.
177 184
      */

+ 2
- 0
lang/main.json ファイルの表示

@@ -102,6 +102,7 @@
102 102
             "shortcuts": "Toggle shortcuts",
103 103
             "speakerStats": "Toggle speaker statistics",
104 104
             "toggleCamera": "Toggle camera",
105
+            "tileView": "Toggle tile view",
105 106
             "videomute": "Toggle mute video"
106 107
         },
107 108
         "addPeople": "Add people to your call",
@@ -144,6 +145,7 @@
144 145
         "raiseHand": "Raise / Lower your hand",
145 146
         "shortcuts": "View shortcuts",
146 147
         "speakerStats": "Speaker stats",
148
+        "tileViewToggle": "Toggle tile view",
147 149
         "invite": "Invite people"
148 150
     },
149 151
     "chat":{

+ 66
- 1
modules/FollowMe.js ファイルの表示

@@ -21,6 +21,7 @@ import {
21 21
     getPinnedParticipant,
22 22
     pinParticipant
23 23
 } from '../react/features/base/participants';
24
+import { setTileView } from '../react/features/video-layout';
24 25
 import UIEvents from '../service/UI/UIEvents';
25 26
 import VideoLayout from './UI/videolayout/VideoLayout';
26 27
 
@@ -117,6 +118,31 @@ class State {
117 118
         }
118 119
     }
119 120
 
121
+    /**
122
+     * A getter for this object instance to know the state of tile view.
123
+     *
124
+     * @returns {boolean} True if tile view is enabled.
125
+     */
126
+    get tileViewEnabled() {
127
+        return this._tileViewEnabled;
128
+    }
129
+
130
+    /**
131
+     * A setter for {@link tileViewEnabled}. Fires a property change event for
132
+     * other participants to follow.
133
+     *
134
+     * @param {boolean} b - Whether or not tile view is enabled.
135
+     * @returns {void}
136
+     */
137
+    set tileViewEnabled(b) {
138
+        const oldValue = this._tileViewEnabled;
139
+
140
+        if (oldValue !== b) {
141
+            this._tileViewEnabled = b;
142
+            this._firePropertyChange('tileViewEnabled', oldValue, b);
143
+        }
144
+    }
145
+
120 146
     /**
121 147
      * Invokes {_propertyChangeCallback} to notify it that {property} had its
122 148
      * value changed from {oldValue} to {newValue}.
@@ -189,6 +215,10 @@ class FollowMe {
189 215
             this._sharedDocumentToggled
190 216
                 .bind(this, this._UI.getSharedDocumentManager().isVisible());
191 217
         }
218
+
219
+        this._tileViewToggled.bind(
220
+            this,
221
+            APP.store.getState()['features/video-layout'].tileViewEnabled);
192 222
     }
193 223
 
194 224
     /**
@@ -214,6 +244,10 @@ class FollowMe {
214 244
         this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
215 245
         this._UI.addListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
216 246
                             this.sharedDocEventHandler);
247
+
248
+        this.tileViewEventHandler = this._tileViewToggled.bind(this);
249
+        this._UI.addListener(UIEvents.TOGGLED_TILE_VIEW,
250
+              this.tileViewEventHandler);
217 251
     }
218 252
 
219 253
     /**
@@ -227,6 +261,8 @@ class FollowMe {
227 261
                                 this.sharedDocEventHandler);
228 262
         this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
229 263
                                 this.pinnedEndpointEventHandler);
264
+        this._UI.removeListener(UIEvents.TOGGLED_TILE_VIEW,
265
+                                this.tileViewEventHandler);
230 266
     }
231 267
 
232 268
     /**
@@ -266,6 +302,18 @@ class FollowMe {
266 302
         this._local.sharedDocumentVisible = sharedDocumentVisible;
267 303
     }
268 304
 
305
+    /**
306
+     * Notifies this instance that the tile view mode has been enabled or
307
+     * disabled.
308
+     *
309
+     * @param {boolean} enabled - True if tile view has been enabled, false
310
+     * if has been disabled.
311
+     * @returns {void}
312
+     */
313
+    _tileViewToggled(enabled) {
314
+        this._local.tileViewEnabled = enabled;
315
+    }
316
+
269 317
     /**
270 318
      * Changes the nextOnStage property value.
271 319
      *
@@ -316,7 +364,8 @@ class FollowMe {
316 364
                     attributes: {
317 365
                         filmstripVisible: local.filmstripVisible,
318 366
                         nextOnStage: local.nextOnStage,
319
-                        sharedDocumentVisible: local.sharedDocumentVisible
367
+                        sharedDocumentVisible: local.sharedDocumentVisible,
368
+                        tileViewEnabled: local.tileViewEnabled
320 369
                     }
321 370
                 });
322 371
     }
@@ -355,6 +404,7 @@ class FollowMe {
355 404
         this._onFilmstripVisible(attributes.filmstripVisible);
356 405
         this._onNextOnStage(attributes.nextOnStage);
357 406
         this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
407
+        this._onTileViewEnabled(attributes.tileViewEnabled);
358 408
     }
359 409
 
360 410
     /**
@@ -434,6 +484,21 @@ class FollowMe {
434 484
         }
435 485
     }
436 486
 
487
+    /**
488
+     * Process a tile view enabled / disabled event received from FOLLOW-ME.
489
+     *
490
+     * @param {boolean} enabled - Whether or not tile view should be shown.
491
+     * @private
492
+     * @returns {void}
493
+     */
494
+    _onTileViewEnabled(enabled) {
495
+        if (typeof enabled === 'undefined') {
496
+            return;
497
+        }
498
+
499
+        APP.store.dispatch(setTileView(enabled === 'true'));
500
+    }
501
+
437 502
     /**
438 503
      * Pins / unpins the video thumbnail given by clickId.
439 504
      *

+ 6
- 2
modules/UI/shared_video/SharedVideoThumb.js ファイルの表示

@@ -1,4 +1,6 @@
1
-/* global $ */
1
+/* global $, APP */
2
+import { shouldDisplayTileView } from '../../../react/features/video-layout';
3
+
2 4
 import SmallVideo from '../videolayout/SmallVideo';
3 5
 
4 6
 const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -64,7 +66,9 @@ SharedVideoThumb.prototype.createContainer = function(spanId) {
64 66
  * The thumb click handler.
65 67
  */
66 68
 SharedVideoThumb.prototype.videoClick = function() {
67
-    this._togglePin();
69
+    if (!shouldDisplayTileView(APP.store.getState())) {
70
+        this._togglePin();
71
+    }
68 72
 };
69 73
 
70 74
 /**

+ 99
- 11
modules/UI/videolayout/Filmstrip.js ファイルの表示

@@ -1,6 +1,13 @@
1 1
 /* global $, APP, interfaceConfig */
2 2
 
3 3
 import { setFilmstripVisible } from '../../../react/features/filmstrip';
4
+import {
5
+    LAYOUTS,
6
+    getCurrentLayout,
7
+    getMaxColumnCount,
8
+    getTileViewGridDimensions,
9
+    shouldDisplayTileView
10
+} from '../../../react/features/video-layout';
4 11
 
5 12
 import UIEvents from '../../../service/UI/UIEvents';
6 13
 import UIUtil from '../util/UIUtil';
@@ -233,6 +240,10 @@ const Filmstrip = {
233 240
      * @returns {*|{localVideo, remoteVideo}}
234 241
      */
235 242
     calculateThumbnailSize() {
243
+        if (shouldDisplayTileView(APP.store.getState())) {
244
+            return this._calculateThumbnailSizeForTileView();
245
+        }
246
+
236 247
         const availableSizes = this.calculateAvailableSize();
237 248
         const width = availableSizes.availableWidth;
238 249
         const height = availableSizes.availableHeight;
@@ -247,11 +258,10 @@ const Filmstrip = {
247 258
      * @returns {{availableWidth: number, availableHeight: number}}
248 259
      */
249 260
     calculateAvailableSize() {
250
-        let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
251
-        const thumbs = this.getThumbs(true);
252
-        const numvids = thumbs.remoteThumbs.length;
253
-
254
-        const localVideoContainer = $('#localVideoContainer');
261
+        const state = APP.store.getState();
262
+        const currentLayout = getCurrentLayout(state);
263
+        const isHorizontalFilmstripView
264
+            = currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
255 265
 
256 266
         /**
257 267
          * If the videoAreaAvailableWidth is set we use this one to calculate
@@ -268,10 +278,15 @@ const Filmstrip = {
268 278
             - UIUtil.parseCssInt(this.filmstrip.css('borderRightWidth'), 10)
269 279
             - 5;
270 280
 
281
+        let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
271 282
         let availableWidth = videoAreaAvailableWidth;
272 283
 
284
+        const thumbs = this.getThumbs(true);
285
+
273 286
         // If local thumb is not hidden
274 287
         if (thumbs.localThumb) {
288
+            const localVideoContainer = $('#localVideoContainer');
289
+
275 290
             availableWidth = Math.floor(
276 291
                 videoAreaAvailableWidth - (
277 292
                     UIUtil.parseCssInt(
@@ -289,10 +304,12 @@ const Filmstrip = {
289 304
             );
290 305
         }
291 306
 
292
-        // If the number of videos is 0 or undefined or we're in vertical
307
+        // If the number of videos is 0 or undefined or we're not in horizontal
293 308
         // filmstrip mode we don't need to calculate further any adjustments
294 309
         // to width based on the number of videos present.
295
-        if (numvids && !interfaceConfig.VERTICAL_FILMSTRIP) {
310
+        const numvids = thumbs.remoteThumbs.length;
311
+
312
+        if (numvids && isHorizontalFilmstripView) {
296 313
             const remoteVideoContainer = thumbs.remoteThumbs.eq(0);
297 314
 
298 315
             availableWidth = Math.floor(
@@ -322,8 +339,10 @@ const Filmstrip = {
322 339
         availableHeight
323 340
             = Math.min(maxHeight, window.innerHeight - 18);
324 341
 
325
-        return { availableWidth,
326
-            availableHeight };
342
+        return {
343
+            availableHeight,
344
+            availableWidth
345
+        };
327 346
     },
328 347
 
329 348
     /**
@@ -434,6 +453,51 @@ const Filmstrip = {
434 453
         };
435 454
     },
436 455
 
456
+    /**
457
+     * Calculates the size for thumbnails when in tile view layout.
458
+     *
459
+     * @returns {{localVideo, remoteVideo}}
460
+     */
461
+    _calculateThumbnailSizeForTileView() {
462
+        const tileAspectRatio = 16 / 9;
463
+
464
+        // The distance from the top and bottom of the screen, as set by CSS, to
465
+        // avoid overlapping UI elements.
466
+        const topBottomPadding = 200;
467
+
468
+        // Minimum space to keep between the sides of the tiles and the sides
469
+        // of the window.
470
+        const sideMargins = 30 * 2;
471
+
472
+        const state = APP.store.getState();
473
+
474
+        const viewWidth = document.body.clientWidth - sideMargins;
475
+        const viewHeight = document.body.clientHeight - topBottomPadding;
476
+
477
+        const {
478
+            columns,
479
+            visibleRows
480
+        } = getTileViewGridDimensions(state, getMaxColumnCount());
481
+        const initialWidth = viewWidth / columns;
482
+        const aspectRatioHeight = initialWidth / tileAspectRatio;
483
+
484
+        const heightOfEach = Math.min(
485
+            aspectRatioHeight,
486
+            viewHeight / visibleRows);
487
+        const widthOfEach = tileAspectRatio * heightOfEach;
488
+
489
+        return {
490
+            localVideo: {
491
+                thumbWidth: widthOfEach,
492
+                thumbHeight: heightOfEach
493
+            },
494
+            remoteVideo: {
495
+                thumbWidth: widthOfEach,
496
+                thumbHeight: heightOfEach
497
+            }
498
+        };
499
+    },
500
+
437 501
     /**
438 502
      * Resizes thumbnails
439 503
      * @param local
@@ -443,6 +507,28 @@ const Filmstrip = {
443 507
      */
444 508
     // eslint-disable-next-line max-params
445 509
     resizeThumbnails(local, remote, forceUpdate = false) {
510
+        const state = APP.store.getState();
511
+
512
+        if (shouldDisplayTileView(state)) {
513
+            // The size of the side margins for each tile as set in CSS.
514
+            const sideMargins = 10 * 2;
515
+            const {
516
+                columns,
517
+                rows
518
+            } = getTileViewGridDimensions(state, getMaxColumnCount());
519
+            const hasOverflow = rows > columns;
520
+
521
+            // Width is set so that the flex layout can automatically wrap
522
+            // tiles onto new rows.
523
+            this.filmstripRemoteVideos.css({
524
+                width: (local.thumbWidth * columns) + (columns * sideMargins)
525
+            });
526
+
527
+            this.filmstripRemoteVideos.toggleClass('has-overflow', hasOverflow);
528
+        } else {
529
+            this.filmstripRemoteVideos.css('width', '');
530
+        }
531
+
446 532
         const thumbs = this.getThumbs(!forceUpdate);
447 533
 
448 534
         if (thumbs.localThumb) {
@@ -466,13 +552,15 @@ const Filmstrip = {
466 552
             });
467 553
         }
468 554
 
555
+        const currentLayout = getCurrentLayout(APP.store.getState());
556
+
469 557
         // Let CSS take care of height in vertical filmstrip mode.
470
-        if (interfaceConfig.VERTICAL_FILMSTRIP) {
558
+        if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
471 559
             $('#filmstripLocalVideo').css({
472 560
                 // adds 4 px because of small video 2px border
473 561
                 width: `${local.thumbWidth + 4}px`
474 562
             });
475
-        } else {
563
+        } else if (currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
476 564
             this.filmstrip.css({
477 565
                 // adds 4 px because of small video 2px border
478 566
                 height: `${remote.thumbHeight + 4}px`

+ 56
- 13
modules/UI/videolayout/LocalVideo.js ファイルの表示

@@ -11,6 +11,7 @@ import {
11 11
     getAvatarURLByParticipantId
12 12
 } from '../../../react/features/base/participants';
13 13
 import { updateSettings } from '../../../react/features/base/settings';
14
+import { shouldDisplayTileView } from '../../../react/features/video-layout';
14 15
 /* eslint-enable no-unused-vars */
15 16
 
16 17
 const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -26,7 +27,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
26 27
     this.streamEndedCallback = streamEndedCallback;
27 28
     this.container = this.createContainer();
28 29
     this.$container = $(this.container);
29
-    $('#filmstripLocalVideoThumbnail').append(this.container);
30
+    this.updateDOMLocation();
30 31
 
31 32
     this.localVideoId = null;
32 33
     this.bindHoverHandler();
@@ -109,16 +110,7 @@ LocalVideo.prototype.changeVideo = function(stream) {
109 110
 
110 111
     this.localVideoId = `localVideo_${stream.getId()}`;
111 112
 
112
-    const localVideoContainer = document.getElementById('localVideoWrapper');
113
-
114
-    ReactDOM.render(
115
-        <Provider store = { APP.store }>
116
-            <VideoTrack
117
-                id = { this.localVideoId }
118
-                videoTrack = {{ jitsiTrack: stream }} />
119
-        </Provider>,
120
-        localVideoContainer
121
-    );
113
+    this._updateVideoElement();
122 114
 
123 115
     // eslint-disable-next-line eqeqeq
124 116
     const isVideo = stream.videoType != 'desktop';
@@ -128,12 +120,14 @@ LocalVideo.prototype.changeVideo = function(stream) {
128 120
     this.setFlipX(isVideo ? settings.localFlipX : false);
129 121
 
130 122
     const endedHandler = () => {
123
+        const localVideoContainer
124
+            = document.getElementById('localVideoWrapper');
131 125
 
132 126
         // Only remove if there is no video and not a transition state.
133 127
         // Previous non-react logic created a new video element with each track
134 128
         // removal whereas react reuses the video component so it could be the
135 129
         // stream ended but a new one is being used.
136
-        if (this.videoStream.isEnded()) {
130
+        if (localVideoContainer && this.videoStream.isEnded()) {
137 131
             ReactDOM.unmountComponentAtNode(localVideoContainer);
138 132
         }
139 133
 
@@ -235,6 +229,29 @@ LocalVideo.prototype._enableDisableContextMenu = function(enable) {
235 229
     }
236 230
 };
237 231
 
232
+/**
233
+ * Places the {@code LocalVideo} in the DOM based on the current video layout.
234
+ *
235
+ * @returns {void}
236
+ */
237
+LocalVideo.prototype.updateDOMLocation = function() {
238
+    if (!this.container) {
239
+        return;
240
+    }
241
+
242
+    if (this.container.parentElement) {
243
+        this.container.parentElement.removeChild(this.container);
244
+    }
245
+
246
+    const appendTarget = shouldDisplayTileView(APP.store.getState())
247
+        ? document.getElementById('localVideoTileViewContainer')
248
+        : document.getElementById('filmstripLocalVideoThumbnail');
249
+
250
+    appendTarget && appendTarget.appendChild(this.container);
251
+
252
+    this._updateVideoElement();
253
+};
254
+
238 255
 /**
239 256
  * Callback invoked when the thumbnail is clicked. Will directly call
240 257
  * VideoLayout to handle thumbnail click if certain elements have not been
@@ -258,7 +275,9 @@ LocalVideo.prototype._onContainerClick = function(event) {
258 275
         = $source.parents('.displayNameContainer').length > 0;
259 276
     const clickedOnPopover = $source.parents('.popover').length > 0
260 277
             || classList.contains('popover');
261
-    const ignoreClick = clickedOnDisplayName || clickedOnPopover;
278
+    const ignoreClick = clickedOnDisplayName
279
+        || clickedOnPopover
280
+        || shouldDisplayTileView(APP.store.getState());
262 281
 
263 282
     if (event.stopPropagation && !ignoreClick) {
264 283
         event.stopPropagation();
@@ -269,4 +288,28 @@ LocalVideo.prototype._onContainerClick = function(event) {
269 288
     }
270 289
 };
271 290
 
291
+/**
292
+ * Renders the React Element for displaying video in {@code LocalVideo}.
293
+ *
294
+ */
295
+LocalVideo.prototype._updateVideoElement = function() {
296
+    const localVideoContainer = document.getElementById('localVideoWrapper');
297
+
298
+    ReactDOM.render(
299
+        <Provider store = { APP.store }>
300
+            <VideoTrack
301
+                id = 'localVideo_container'
302
+                videoTrack = {{ jitsiTrack: this.videoStream }} />
303
+        </Provider>,
304
+        localVideoContainer
305
+    );
306
+
307
+    // Ensure the video gets play() called on it. This may be necessary in the
308
+    // case where the local video container was moved and re-attached, in which
309
+    // case video does not autoplay.
310
+    const video = this.container.querySelector('video');
311
+
312
+    video && video.play();
313
+};
314
+
272 315
 export default LocalVideo;

+ 19
- 4
modules/UI/videolayout/RemoteVideo.js ファイルの表示

@@ -20,6 +20,11 @@ import {
20 20
     REMOTE_CONTROL_MENU_STATES,
21 21
     RemoteVideoMenuTriggerButton
22 22
 } from '../../../react/features/remote-video-menu';
23
+import {
24
+    LAYOUTS,
25
+    getCurrentLayout,
26
+    shouldDisplayTileView
27
+} from '../../../react/features/video-layout';
23 28
 /* eslint-enable no-unused-vars */
24 29
 
25 30
 const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -163,8 +168,17 @@ RemoteVideo.prototype._generatePopupContent = function() {
163 168
     const onVolumeChange = this._setAudioVolume;
164 169
     const { isModerator } = APP.conference;
165 170
     const participantID = this.id;
166
-    const menuPosition = interfaceConfig.VERTICAL_FILMSTRIP
167
-        ? 'left bottom' : 'top center';
171
+
172
+    const currentLayout = getCurrentLayout(APP.store.getState());
173
+    let remoteMenuPosition;
174
+
175
+    if (currentLayout === LAYOUTS.TILE_VIEW) {
176
+        remoteMenuPosition = 'left top';
177
+    } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
178
+        remoteMenuPosition = 'left bottom';
179
+    } else {
180
+        remoteMenuPosition = 'top center';
181
+    }
168 182
 
169 183
     ReactDOM.render(
170 184
         <Provider store = { APP.store }>
@@ -174,7 +188,7 @@ RemoteVideo.prototype._generatePopupContent = function() {
174 188
                         initialVolumeValue = { initialVolumeValue }
175 189
                         isAudioMuted = { this.isAudioMuted }
176 190
                         isModerator = { isModerator }
177
-                        menuPosition = { menuPosition }
191
+                        menuPosition = { remoteMenuPosition }
178 192
                         onMenuDisplay
179 193
                             = {this._onRemoteVideoMenuDisplay.bind(this)}
180 194
                         onRemoteControlToggle = { onRemoteControlToggle }
@@ -613,7 +627,8 @@ RemoteVideo.prototype._onContainerClick = function(event) {
613 627
     const { classList } = event.target;
614 628
 
615 629
     const ignoreClick = $source.parents('.popover').length > 0
616
-            || classList.contains('popover');
630
+            || classList.contains('popover')
631
+            || shouldDisplayTileView(APP.store.getState());
617 632
 
618 633
     if (!ignoreClick) {
619 634
         this._togglePin();

+ 51
- 4
modules/UI/videolayout/SmallVideo.js ファイルの表示

@@ -27,6 +27,11 @@ import {
27 27
     RaisedHandIndicator,
28 28
     VideoMutedIndicator
29 29
 } from '../../../react/features/filmstrip';
30
+import {
31
+    LAYOUTS,
32
+    getCurrentLayout,
33
+    shouldDisplayTileView
34
+} from '../../../react/features/video-layout';
30 35
 /* eslint-enable no-unused-vars */
31 36
 
32 37
 const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -328,7 +333,21 @@ SmallVideo.prototype.setVideoMutedView = function(isMuted) {
328 333
 SmallVideo.prototype.updateStatusBar = function() {
329 334
     const statusBarContainer
330 335
         = this.container.querySelector('.videocontainer__toolbar');
331
-    const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
336
+
337
+    if (!statusBarContainer) {
338
+        return;
339
+    }
340
+
341
+    const currentLayout = getCurrentLayout(APP.store.getState());
342
+    let tooltipPosition;
343
+
344
+    if (currentLayout === LAYOUTS.TILE_VIEW) {
345
+        tooltipPosition = 'right';
346
+    } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
347
+        tooltipPosition = 'left';
348
+    } else {
349
+        tooltipPosition = 'top';
350
+    }
332 351
 
333 352
     ReactDOM.render(
334 353
         <I18nextProvider i18n = { i18next }>
@@ -547,7 +566,8 @@ SmallVideo.prototype.isVideoPlayable = function() {
547 566
  */
548 567
 SmallVideo.prototype.selectDisplayMode = function() {
549 568
     // Display name is always and only displayed when user is on the stage
550
-    if (this.isCurrentlyOnLargeVideo()) {
569
+    if (this.isCurrentlyOnLargeVideo()
570
+        && !shouldDisplayTileView(APP.store.getState())) {
551 571
         return this.isVideoPlayable() && !APP.conference.isAudioOnly()
552 572
             ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
553 573
     } else if (this.isVideoPlayable()
@@ -685,7 +705,10 @@ SmallVideo.prototype.showDominantSpeakerIndicator = function(show) {
685 705
 
686 706
     this._showDominantSpeaker = show;
687 707
 
708
+    this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
709
+
688 710
     this.updateIndicators();
711
+    this.updateView();
689 712
 };
690 713
 
691 714
 /**
@@ -765,6 +788,18 @@ SmallVideo.prototype.initBrowserSpecificProperties = function() {
765 788
     }
766 789
 };
767 790
 
791
+/**
792
+ * Helper function for re-rendering multiple react components of the small
793
+ * video.
794
+ *
795
+ * @returns {void}
796
+ */
797
+SmallVideo.prototype.rerender = function() {
798
+    this.updateIndicators();
799
+    this.updateStatusBar();
800
+    this.updateView();
801
+};
802
+
768 803
 /**
769 804
  * Updates the React element responsible for showing connection status, dominant
770 805
  * speaker, and raised hand icons. Uses instance variables to get the necessary
@@ -784,7 +819,19 @@ SmallVideo.prototype.updateIndicators = function() {
784 819
     const iconSize = UIUtil.getIndicatorFontSize();
785 820
     const showConnectionIndicator = this.videoIsHovered
786 821
         || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
787
-    const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
822
+    const currentLayout = getCurrentLayout(APP.store.getState());
823
+    let statsPopoverPosition, tooltipPosition;
824
+
825
+    if (currentLayout === LAYOUTS.TILE_VIEW) {
826
+        statsPopoverPosition = 'right top';
827
+        tooltipPosition = 'right';
828
+    } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
829
+        statsPopoverPosition = this.statsPopoverLocation;
830
+        tooltipPosition = 'left';
831
+    } else {
832
+        statsPopoverPosition = this.statsPopoverLocation;
833
+        tooltipPosition = 'top';
834
+    }
788 835
 
789 836
     ReactDOM.render(
790 837
             <I18nextProvider i18n = { i18next }>
@@ -799,7 +846,7 @@ SmallVideo.prototype.updateIndicators = function() {
799 846
                                 enableStatsDisplay
800 847
                                     = { !interfaceConfig.filmStripOnly }
801 848
                                 statsPopoverPosition
802
-                                    = { this.statsPopoverLocation }
849
+                                    = { statsPopoverPosition }
803 850
                                 userID = { this.id } />
804 851
                             : null }
805 852
                         { this._showRaisedHand

+ 33
- 3
modules/UI/videolayout/VideoLayout.js ファイルの表示

@@ -1,6 +1,10 @@
1 1
 /* global APP, $, interfaceConfig  */
2 2
 const logger = require('jitsi-meet-logger').getLogger(__filename);
3 3
 
4
+import {
5
+    getNearestReceiverVideoQualityLevel,
6
+    setMaxReceiverVideoQuality
7
+} from '../../../react/features/base/conference';
4 8
 import {
5 9
     JitsiParticipantConnectionStatus
6 10
 } from '../../../react/features/base/lib-jitsi-meet';
@@ -9,6 +13,9 @@ import {
9 13
     getPinnedParticipant,
10 14
     pinParticipant
11 15
 } from '../../../react/features/base/participants';
16
+import {
17
+    shouldDisplayTileView
18
+} from '../../../react/features/video-layout';
12 19
 import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
13 20
 import SharedVideoThumb from '../shared_video/SharedVideoThumb';
14 21
 
@@ -594,12 +601,19 @@ const VideoLayout = {
594 601
 
595 602
         Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
596 603
 
604
+        if (shouldDisplayTileView(APP.store.getState())) {
605
+            const height
606
+                = (localVideo && localVideo.thumbHeight)
607
+                || (remoteVideo && remoteVideo.thumbnHeight)
608
+                || 0;
609
+            const qualityLevel = getNearestReceiverVideoQualityLevel(height);
610
+
611
+            APP.store.dispatch(setMaxReceiverVideoQuality(qualityLevel));
612
+        }
613
+
597 614
         if (onComplete && typeof onComplete === 'function') {
598 615
             onComplete();
599 616
         }
600
-
601
-        return { localVideo,
602
-            remoteVideo };
603 617
     },
604 618
 
605 619
     /**
@@ -1142,6 +1156,22 @@ const VideoLayout = {
1142 1156
         );
1143 1157
     },
1144 1158
 
1159
+    /**
1160
+     * Helper method to invoke when the video layout has changed and elements
1161
+     * have to be re-arranged and resized.
1162
+     *
1163
+     * @returns {void}
1164
+     */
1165
+    refreshLayout() {
1166
+        localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
1167
+        VideoLayout.resizeVideoArea();
1168
+
1169
+        localVideoThumbnail && localVideoThumbnail.rerender();
1170
+        Object.values(remoteVideos).forEach(
1171
+            remoteVideo => remoteVideo.rerender()
1172
+        );
1173
+    },
1174
+
1145 1175
     /**
1146 1176
      * Triggers an update of large video if the passed in participant is
1147 1177
      * currently displayed on large video.

+ 34
- 1
react/features/base/conference/functions.js ファイルの表示

@@ -8,7 +8,8 @@ import {
8 8
     AVATAR_ID_COMMAND,
9 9
     AVATAR_URL_COMMAND,
10 10
     EMAIL_COMMAND,
11
-    JITSI_CONFERENCE_URL_KEY
11
+    JITSI_CONFERENCE_URL_KEY,
12
+    VIDEO_QUALITY_LEVELS
12 13
 } from './constants';
13 14
 
14 15
 const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -102,6 +103,38 @@ export function getCurrentConference(stateful: Function | Object) {
102 103
             : joining);
103 104
 }
104 105
 
106
+/**
107
+ * Finds the nearest match for the passed in {@link availableHeight} to am
108
+ * enumerated value in {@code VIDEO_QUALITY_LEVELS}.
109
+ *
110
+ * @param {number} availableHeight - The height to which a matching video
111
+ * quality level should be found.
112
+ * @returns {number} The closest matching value from
113
+ * {@code VIDEO_QUALITY_LEVELS}.
114
+ */
115
+export function getNearestReceiverVideoQualityLevel(availableHeight: number) {
116
+    const qualityLevels = [
117
+        VIDEO_QUALITY_LEVELS.HIGH,
118
+        VIDEO_QUALITY_LEVELS.STANDARD,
119
+        VIDEO_QUALITY_LEVELS.LOW
120
+    ];
121
+
122
+    let selectedLevel = qualityLevels[0];
123
+
124
+    for (let i = 1; i < qualityLevels.length; i++) {
125
+        const previousValue = qualityLevels[i - 1];
126
+        const currentValue = qualityLevels[i];
127
+        const diffWithCurrent = Math.abs(availableHeight - currentValue);
128
+        const diffWithPrevious = Math.abs(availableHeight - previousValue);
129
+
130
+        if (diffWithCurrent < diffWithPrevious) {
131
+            selectedLevel = currentValue;
132
+        }
133
+    }
134
+
135
+    return selectedLevel;
136
+}
137
+
105 138
 /**
106 139
  * Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
107 140
  * manipulating a conference participant (e.g. pin or select participant).

+ 46
- 23
react/features/conference/components/Conference.web.js ファイルの表示

@@ -4,6 +4,8 @@ import _ from 'lodash';
4 4
 import React, { Component } from 'react';
5 5
 import { connect as reactReduxConnect } from 'react-redux';
6 6
 
7
+import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
8
+
7 9
 import { obtainConfig } from '../../base/config';
8 10
 import { connect, disconnect } from '../../base/connection';
9 11
 import { DialogContainer } from '../../base/dialog';
@@ -13,6 +15,12 @@ import { CalleeInfoContainer } from '../../invite';
13 15
 import { LargeVideo } from '../../large-video';
14 16
 import { NotificationsContainer } from '../../notifications';
15 17
 import { SidePanel } from '../../side-panel';
18
+import {
19
+    LAYOUTS,
20
+    getCurrentLayout,
21
+    shouldDisplayTileView
22
+} from '../../video-layout';
23
+
16 24
 import { default as Notice } from './Notice';
17 25
 import {
18 26
     Toolbox,
@@ -49,9 +57,10 @@ const FULL_SCREEN_EVENTS = [
49 57
  * @private
50 58
  * @type {Object}
51 59
  */
52
-const LAYOUT_CLASSES = {
53
-    HORIZONTAL_FILMSTRIP: 'horizontal-filmstrip',
54
-    VERTICAL_FILMSTRIP: 'vertical-filmstrip'
60
+const LAYOUT_CLASSNAMES = {
61
+    [LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
62
+    [LAYOUTS.TILE_VIEW]: 'tile-view',
63
+    [LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip'
55 64
 };
56 65
 
57 66
 /**
@@ -68,13 +77,18 @@ type Props = {
68 77
      * The CSS class to apply to the root of {@link Conference} to modify the
69 78
      * application layout.
70 79
      */
71
-    _layoutModeClassName: string,
80
+    _layoutClassName: string,
72 81
 
73 82
     /**
74 83
      * Conference room name.
75 84
      */
76 85
     _room: string,
77 86
 
87
+    /**
88
+     * Whether or not the current UI layout should be in tile view.
89
+     */
90
+    _shouldDisplayTileView: boolean,
91
+
78 92
     dispatch: Function,
79 93
     t: Function
80 94
 }
@@ -143,6 +157,25 @@ class Conference extends Component<Props> {
143 157
         }
144 158
     }
145 159
 
160
+    /**
161
+     * Calls into legacy UI to update the application layout, if necessary.
162
+     *
163
+     * @inheritdoc
164
+     * returns {void}
165
+     */
166
+    componentDidUpdate(prevProps) {
167
+        if (this.props._shouldDisplayTileView
168
+            === prevProps._shouldDisplayTileView) {
169
+            return;
170
+        }
171
+
172
+        // TODO: For now VideoLayout is being called as LargeVideo and Filmstrip
173
+        // sizing logic is still handled outside of React. Once all components
174
+        // are in react they should calculate size on their own as much as
175
+        // possible and pass down sizings.
176
+        VideoLayout.refreshLayout();
177
+    }
178
+
146 179
     /**
147 180
      * Disconnect from the conference when component will be
148 181
      * unmounted.
@@ -180,7 +213,7 @@ class Conference extends Component<Props> {
180 213
 
181 214
         return (
182 215
             <div
183
-                className = { this.props._layoutModeClassName }
216
+                className = { this.props._layoutClassName }
184 217
                 id = 'videoconference_page'
185 218
                 onMouseMove = { this._onShowToolbar }>
186 219
                 <Notice />
@@ -257,29 +290,19 @@ class Conference extends Component<Props> {
257 290
  * @private
258 291
  * @returns {{
259 292
  *     _iAmRecorder: boolean,
260
- *     _room: ?string
293
+ *     _layoutClassName: string,
294
+ *     _room: ?string,
295
+ *     _shouldDisplayTileView: boolean
261 296
  * }}
262 297
  */
263 298
 function _mapStateToProps(state) {
264
-    const { room } = state['features/base/conference'];
265
-    const { iAmRecorder } = state['features/base/config'];
299
+    const currentLayout = getCurrentLayout(state);
266 300
 
267 301
     return {
268
-        /**
269
-         * Whether the local participant is recording the conference.
270
-         *
271
-         * @private
272
-         */
273
-        _iAmRecorder: iAmRecorder,
274
-
275
-        _layoutModeClassName: interfaceConfig.VERTICAL_FILMSTRIP
276
-            ? LAYOUT_CLASSES.VERTICAL_FILMSTRIP
277
-            : LAYOUT_CLASSES.HORIZONTAL_FILMSTRIP,
278
-
279
-        /**
280
-         * Conference room name.
281
-         */
282
-        _room: room
302
+        _iAmRecorder: state['features/base/config'].iAmRecorder,
303
+        _layoutClassName: LAYOUT_CLASSNAMES[currentLayout],
304
+        _room: state['features/base/conference'].room,
305
+        _shouldDisplayTileView: shouldDisplayTileView(state)
283 306
     };
284 307
 }
285 308
 

+ 2
- 2
react/features/filmstrip/components/web/Filmstrip.js ファイルの表示

@@ -8,6 +8,7 @@ import { dockToolbox } from '../../../toolbox';
8 8
 
9 9
 import { setFilmstripHovered } from '../../actions';
10 10
 import { shouldRemoteVideosBeVisible } from '../../functions';
11
+
11 12
 import Toolbar from './Toolbar';
12 13
 
13 14
 declare var interfaceConfig: Object;
@@ -185,9 +186,8 @@ function _mapStateToProps(state) {
185 186
         && state['features/toolbox'].visible
186 187
         && interfaceConfig.TOOLBAR_BUTTONS.length;
187 188
     const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
188
-
189 189
     const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
190
-        reduceHeight ? 'reduce-height' : ''}`;
190
+        reduceHeight ? 'reduce-height' : ''}`.trim();
191 191
 
192 192
     return {
193 193
         _className: className,

+ 8
- 4
react/features/large-video/actions.js ファイルの表示

@@ -6,7 +6,9 @@ import {
6 6
 } from '../analytics';
7 7
 import { _handleParticipantError } from '../base/conference';
8 8
 import { MEDIA_TYPE } from '../base/media';
9
+import { getParticipants } from '../base/participants';
9 10
 import { reportError } from '../base/util';
11
+import { shouldDisplayTileView } from '../video-layout';
10 12
 
11 13
 import {
12 14
     SELECT_LARGE_VIDEO_PARTICIPANT,
@@ -26,17 +28,19 @@ export function selectParticipant() {
26 28
         const { conference } = state['features/base/conference'];
27 29
 
28 30
         if (conference) {
29
-            const largeVideo = state['features/large-video'];
30
-            const id = largeVideo.participantId;
31
+            const ids = shouldDisplayTileView(state)
32
+                ? getParticipants(state).map(participant => participant.id)
33
+                : [ state['features/large-video'].participantId ];
31 34
 
32 35
             try {
33
-                conference.selectParticipant(id);
36
+                conference.selectParticipants(ids);
34 37
             } catch (err) {
35 38
                 _handleParticipantError(err);
36 39
 
37 40
                 sendAnalytics(createSelectParticipantFailedEvent(err));
38 41
 
39
-                reportError(err, `Failed to select participant ${id}`);
42
+                reportError(
43
+                    err, `Failed to select participants ${ids.toString()}`);
40 44
             }
41 45
         }
42 46
     };

+ 10
- 2
react/features/large-video/components/AbstractLabels.js ファイルの表示

@@ -4,6 +4,7 @@ import React, { Component } from 'react';
4 4
 
5 5
 import { isFilmstripVisible } from '../../filmstrip';
6 6
 import { RecordingLabel } from '../../recording';
7
+import { shouldDisplayTileView } from '../../video-layout';
7 8
 import { VideoQualityLabel } from '../../video-quality';
8 9
 import { TranscribingLabel } from '../../transcribing/';
9 10
 
@@ -17,6 +18,11 @@ export type Props = {
17 18
     * determine display classes to set.
18 19
     */
19 20
     _filmstripVisible: boolean,
21
+
22
+    /**
23
+     * Whether or not the video quality label should be displayed.
24
+     */
25
+    _showVideoQualityLabel: boolean
20 26
 };
21 27
 
22 28
 /**
@@ -72,11 +78,13 @@ export default class AbstractLabels<P: Props, S> extends Component<P, S> {
72 78
  * @param {Object} state - The Redux state.
73 79
  * @private
74 80
  * @returns {{
75
- *     _filmstripVisible: boolean
81
+ *     _filmstripVisible: boolean,
82
+ *     _showVideoQualityLabel: boolean
76 83
  * }}
77 84
  */
78 85
 export function _abstractMapStateToProps(state: Object) {
79 86
     return {
80
-        _filmstripVisible: isFilmstripVisible(state)
87
+        _filmstripVisible: isFilmstripVisible(state),
88
+        _showVideoQualityLabel: !shouldDisplayTileView(state)
81 89
     };
82 90
 }

+ 2
- 1
react/features/large-video/components/Labels.web.js ファイルの表示

@@ -89,7 +89,8 @@ class Labels extends AbstractLabels<Props, State> {
89 89
                     this._renderTranscribingLabel()
90 90
                 }
91 91
                 {
92
-                    this._renderVideoQualityLabel()
92
+                    this.props._showVideoQualityLabel
93
+                        && this._renderVideoQualityLabel()
93 94
                 }
94 95
             </div>
95 96
         );

+ 1
- 1
react/features/large-video/components/LargeVideo.web.js ファイルの表示

@@ -50,7 +50,7 @@ export default class LargeVideo extends Component<*> {
50 50
                 </div>
51 51
                 <div id = 'remotePresenceMessage' />
52 52
                 <span id = 'remoteConnectionMessage' />
53
-                <div>
53
+                <div id = 'largeVideoElementsContainer'>
54 54
                     <div id = 'largeVideoBackgroundContainer' />
55 55
                     {
56 56
 

+ 3
- 0
react/features/toolbox/components/web/Toolbox.js ファイルの表示

@@ -40,6 +40,7 @@ import {
40 40
 import { toggleSharedVideo } from '../../../shared-video';
41 41
 import { toggleChat } from '../../../side-panel';
42 42
 import { SpeakerStats } from '../../../speaker-stats';
43
+import { TileViewButton } from '../../../video-layout';
43 44
 import {
44 45
     OverflowMenuVideoQualityItem,
45 46
     VideoQualityDialog
@@ -369,6 +370,8 @@ class Toolbox extends Component<Props> {
369 370
                         visible = { this._shouldShowButton('camera') } />
370 371
                 </div>
371 372
                 <div className = 'button-group-right'>
373
+                    { this._shouldShowButton('tileview')
374
+                        && <TileViewButton /> }
372 375
                     { this._shouldShowButton('invite')
373 376
                         && !_hideInviteButton
374 377
                         && <ToolbarButton

+ 10
- 0
react/features/video-layout/actionTypes.js ファイルの表示

@@ -0,0 +1,10 @@
1
+/**
2
+ * The type of the action which enables or disables the feature for showing
3
+ * video thumbnails in a two-axis tile view.
4
+ *
5
+ * @returns {{
6
+ *     type: SET_TILE_VIEW,
7
+ *     enabled: boolean
8
+ * }}
9
+ */
10
+export const SET_TILE_VIEW = Symbol('SET_TILE_VIEW');

+ 20
- 0
react/features/video-layout/actions.js ファイルの表示

@@ -0,0 +1,20 @@
1
+// @flow
2
+
3
+import { SET_TILE_VIEW } from './actionTypes';
4
+
5
+/**
6
+ * Creates a (redux) action which signals to set the UI layout to be tiled view
7
+ * or not.
8
+ *
9
+ * @param {boolean} enabled - Whether or not tile view should be shown.
10
+ * @returns {{
11
+ *     type: SET_TILE_VIEW,
12
+ *     enabled: boolean
13
+ * }}
14
+ */
15
+export function setTileView(enabled: boolean) {
16
+    return {
17
+        type: SET_TILE_VIEW,
18
+        enabled
19
+    };
20
+}

+ 90
- 0
react/features/video-layout/components/TileViewButton.js ファイルの表示

@@ -0,0 +1,90 @@
1
+// @flow
2
+
3
+import { connect } from 'react-redux';
4
+
5
+import {
6
+    createToolbarEvent,
7
+    sendAnalytics
8
+} from '../../analytics';
9
+import { translate } from '../../base/i18n';
10
+import {
11
+    AbstractButton,
12
+    type AbstractButtonProps
13
+} from '../../base/toolbox';
14
+
15
+import { setTileView } from '../actions';
16
+
17
+/**
18
+ * The type of the React {@code Component} props of {@link TileViewButton}.
19
+ */
20
+type Props = AbstractButtonProps & {
21
+
22
+    /**
23
+     * Whether or not tile view layout has been enabled as the user preference.
24
+     */
25
+    _tileViewEnabled: boolean,
26
+
27
+    /**
28
+     * Used to dispatch actions from the buttons.
29
+     */
30
+    dispatch: Dispatch<*>
31
+};
32
+
33
+/**
34
+ * Component that renders a toolbar button for toggling the tile layout view.
35
+ *
36
+ * @extends AbstractButton
37
+ */
38
+class TileViewButton<P: Props> extends AbstractButton<P, *> {
39
+    accessibilityLabel = 'toolbar.accessibilityLabel.tileView';
40
+    iconName = 'icon-tiles-many';
41
+    toggledIconName = 'icon-tiles-many toggled';
42
+    tooltip = 'toolbar.tileViewToggle';
43
+
44
+    /**
45
+     * Handles clicking / pressing the button.
46
+     *
47
+     * @override
48
+     * @protected
49
+     * @returns {void}
50
+     */
51
+    _handleClick() {
52
+        const { _tileViewEnabled, dispatch } = this.props;
53
+
54
+        sendAnalytics(createToolbarEvent(
55
+            'tileview.button',
56
+            {
57
+                'is_enabled': _tileViewEnabled
58
+            }));
59
+
60
+        dispatch(setTileView(!_tileViewEnabled));
61
+    }
62
+
63
+    /**
64
+     * Indicates whether this button is in toggled state or not.
65
+     *
66
+     * @override
67
+     * @protected
68
+     * @returns {boolean}
69
+     */
70
+    _isToggled() {
71
+        return this.props._tileViewEnabled;
72
+    }
73
+}
74
+
75
+/**
76
+ * Maps (parts of) the redux state to the associated props for the
77
+ * {@code TileViewButton} component.
78
+ *
79
+ * @param {Object} state - The Redux state.
80
+ * @returns {{
81
+ *     _tileViewEnabled: boolean
82
+ * }}
83
+ */
84
+function _mapStateToProps(state) {
85
+    return {
86
+        _tileViewEnabled: state['features/video-layout'].tileViewEnabled
87
+    };
88
+}
89
+
90
+export default translate(connect(_mapStateToProps)(TileViewButton));

+ 1
- 0
react/features/video-layout/components/index.js ファイルの表示

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

+ 10
- 0
react/features/video-layout/constants.js ファイルの表示

@@ -0,0 +1,10 @@
1
+/**
2
+ * An enumeration of the different display layouts supported by the application.
3
+ *
4
+ * @type {Object}
5
+ */
6
+export const LAYOUTS = {
7
+    HORIZONTAL_FILMSTRIP_VIEW: 'horizontal-filmstrip-view',
8
+    TILE_VIEW: 'tile-view',
9
+    VERTICAL_FILMSTRIP_VIEW: 'vertical-filmstrip-view'
10
+};

+ 78
- 0
react/features/video-layout/functions.js ファイルの表示

@@ -0,0 +1,78 @@
1
+// @flow
2
+
3
+import { LAYOUTS } from './constants';
4
+
5
+declare var interfaceConfig: Object;
6
+
7
+/**
8
+ * Returns the {@code LAYOUTS} constant associated with the layout
9
+ * the application should currently be in.
10
+ *
11
+ * @param {Object} state - The redux state.
12
+ * @returns {string}
13
+ */
14
+export function getCurrentLayout(state: Object) {
15
+    if (shouldDisplayTileView(state)) {
16
+        return LAYOUTS.TILE_VIEW;
17
+    } else if (interfaceConfig.VERTICAL_FILMSTRIP) {
18
+        return LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
19
+    }
20
+
21
+    return LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
22
+}
23
+
24
+/**
25
+ * Returns how many columns should be displayed in tile view. The number
26
+ * returned will be between 1 and 5, inclusive.
27
+ *
28
+ * @returns {number}
29
+ */
30
+export function getMaxColumnCount() {
31
+    const configuredMax = interfaceConfig.TILE_VIEW_MAX_COLUMNS || 5;
32
+
33
+    return Math.max(Math.min(configuredMax, 1), 5);
34
+}
35
+
36
+/**
37
+ * Returns the cell count dimensions for tile view. Tile view tries to uphold
38
+ * equal count of tiles for height and width, until maxColumn is reached in
39
+ * which rows will be added but no more columns.
40
+ *
41
+ * @param {Object} state - The redux state.
42
+ * @param {number} maxColumns - The maximum number of columns that can be
43
+ * displayed.
44
+ * @returns {Object} An object is return with the desired number of columns,
45
+ * rows, and visible rows (the rest should overflow) for the tile view layout.
46
+ */
47
+export function getTileViewGridDimensions(state: Object, maxColumns: number) {
48
+    // Purposefully include all participants, which includes fake participants
49
+    // that should show a thumbnail.
50
+    const potentialThumbnails = state['features/base/participants'].length;
51
+
52
+    const columnsToMaintainASquare = Math.ceil(Math.sqrt(potentialThumbnails));
53
+    const columns = Math.min(columnsToMaintainASquare, maxColumns);
54
+    const rows = Math.ceil(potentialThumbnails / columns);
55
+    const visibleRows = Math.min(maxColumns, rows);
56
+
57
+    return {
58
+        columns,
59
+        rows,
60
+        visibleRows
61
+    };
62
+}
63
+
64
+/**
65
+ * Selector for determining if the UI layout should be in tile view. Tile view
66
+ * is determined by more than just having the tile view setting enabled, as
67
+ * one-on-one calls should not be in tile view, as well as etherpad editing.
68
+ *
69
+ * @param {Object} state - The redux state.
70
+ * @returns {boolean} True if tile view should be displayed.
71
+ */
72
+export function shouldDisplayTileView(state: Object = {}) {
73
+    return Boolean(
74
+        state['features/video-layout']
75
+            && state['features/video-layout'].tileViewEnabled
76
+            && !state['features/etherpad'].editing
77
+    );
78
+}

+ 8
- 0
react/features/video-layout/index.js ファイルの表示

@@ -1 +1,9 @@
1
+export * from './actions';
2
+export * from './actionTypes';
3
+export * from './components';
4
+export * from './constants';
5
+export * from './functions';
6
+
1 7
 import './middleware';
8
+import './reducer';
9
+import './subscriber';

+ 6
- 0
react/features/video-layout/middleware.web.js ファイルの表示

@@ -15,6 +15,8 @@ import {
15 15
 import { MiddlewareRegistry } from '../base/redux';
16 16
 import { TRACK_ADDED } from '../base/tracks';
17 17
 
18
+import { SET_TILE_VIEW } from './actionTypes';
19
+
18 20
 declare var APP: Object;
19 21
 
20 22
 /**
@@ -71,6 +73,10 @@ MiddlewareRegistry.register(store => next => action => {
71 73
             Boolean(action.participant.id));
72 74
         break;
73 75
 
76
+    case SET_TILE_VIEW:
77
+        APP.UI.emitEvent(UIEvents.TOGGLED_TILE_VIEW, action.enabled);
78
+        break;
79
+
74 80
     case TRACK_ADDED:
75 81
         if (!action.track.local) {
76 82
             VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);

+ 17
- 0
react/features/video-layout/reducer.js ファイルの表示

@@ -0,0 +1,17 @@
1
+// @flow
2
+
3
+import { ReducerRegistry } from '../base/redux';
4
+
5
+import { SET_TILE_VIEW } from './actionTypes';
6
+
7
+ReducerRegistry.register('features/video-layout', (state = {}, action) => {
8
+    switch (action.type) {
9
+    case SET_TILE_VIEW:
10
+        return {
11
+            ...state,
12
+            tileViewEnabled: action.enabled
13
+        };
14
+    }
15
+
16
+    return state;
17
+});

+ 24
- 0
react/features/video-layout/subscriber.js ファイルの表示

@@ -0,0 +1,24 @@
1
+// @flow
2
+
3
+import {
4
+    VIDEO_QUALITY_LEVELS,
5
+    setMaxReceiverVideoQuality
6
+} from '../base/conference';
7
+import { StateListenerRegistry } from '../base/redux';
8
+import { selectParticipant } from '../large-video';
9
+import { shouldDisplayTileView } from './functions';
10
+
11
+/**
12
+ * StateListenerRegistry provides a reliable way of detecting changes to
13
+ * preferred layout state and dispatching additional actions.
14
+ */
15
+StateListenerRegistry.register(
16
+    /* selector */ state => shouldDisplayTileView(state),
17
+    /* listener */ (displayTileView, { dispatch }) => {
18
+        dispatch(selectParticipant());
19
+
20
+        if (!displayTileView) {
21
+            dispatch(setMaxReceiverVideoQuality(VIDEO_QUALITY_LEVELS.HIGH));
22
+        }
23
+    }
24
+);

+ 1
- 0
service/UI/UIEvents.js ファイルの表示

@@ -59,6 +59,7 @@ export default {
59 59
     TOGGLED_FILMSTRIP: 'UI.toggled_filmstrip',
60 60
     TOGGLE_SCREENSHARING: 'UI.toggle_screensharing',
61 61
     TOGGLED_SHARED_DOCUMENT: 'UI.toggled_shared_document',
62
+    TOGGLED_TILE_VIEW: 'UI.toggled_tile_view',
62 63
     HANGUP: 'UI.hangup',
63 64
     LOGOUT: 'UI.logout',
64 65
     VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',

読み込み中…
キャンセル
保存