浏览代码

Merge pull request #1587 from virtuacoplenny/lenny/vertical-filmstrip

vertical filmstrip and 1-on-1 mode
master
yanas 8 年前
父节点
当前提交
1ff89c5a1c

+ 23
- 0
conference.js 查看文件

@@ -1286,6 +1286,8 @@ export default {
1286 1286
 
1287 1287
             // check the roles for the new user and reflect them
1288 1288
             APP.UI.updateUserRole(user);
1289
+
1290
+            updateRemoteThumbnailsVisibility();
1289 1291
         });
1290 1292
         room.on(ConferenceEvents.USER_LEFT, (id, user) => {
1291 1293
             APP.store.dispatch(participantLeft(id, user));
@@ -1293,6 +1295,8 @@ export default {
1293 1295
             APP.API.notifyUserLeft(id);
1294 1296
             APP.UI.removeUser(id, user.getDisplayName());
1295 1297
             APP.UI.onSharedVideoStop(id);
1298
+
1299
+            updateRemoteThumbnailsVisibility();
1296 1300
         });
1297 1301
 
1298 1302
         room.on(ConferenceEvents.USER_STATUS_CHANGED, (id, status) => {
@@ -1475,6 +1479,8 @@ export default {
1475 1479
                         reportError(e);
1476 1480
                     }
1477 1481
                 }
1482
+
1483
+                updateRemoteThumbnailsVisibility();
1478 1484
             });
1479 1485
         }
1480 1486
 
@@ -1811,6 +1817,8 @@ export default {
1811 1817
                     }
1812 1818
                 });
1813 1819
             }
1820
+
1821
+            updateRemoteThumbnailsVisibility();
1814 1822
         });
1815 1823
         room.addCommandListener(
1816 1824
             this.commands.defaults.SHARED_VIDEO, ({value, attributes}, id) => {
@@ -1826,6 +1834,21 @@ export default {
1826 1834
                     APP.UI.onSharedVideoUpdate(id, value, attributes);
1827 1835
                 }
1828 1836
             });
1837
+
1838
+        function updateRemoteThumbnailsVisibility() {
1839
+            const localUserId = APP.conference.getMyUserId();
1840
+            const remoteParticipantsCount = room.getParticipantCount() - 1;
1841
+
1842
+            // Get the remote thumbnail count for cases where there are
1843
+            // non-participants displaying video, such as with video sharing.
1844
+            const remoteVideosCount = APP.UI.getRemoteVideosCount();
1845
+
1846
+            const shouldShowRemoteThumbnails = APP.UI.isPinned(localUserId)
1847
+                || remoteVideosCount > 1
1848
+                || remoteParticipantsCount !== remoteVideosCount;
1849
+
1850
+            APP.UI.setRemoteThumbnailsVisibility(shouldShowRemoteThumbnails);
1851
+        }
1829 1852
     },
1830 1853
     /**
1831 1854
     * Adds any room listener.

+ 3
- 0
config.js 查看文件

@@ -57,6 +57,9 @@ var config = { // eslint-disable-line no-unused-vars
57 57
     webrtcIceTcpDisable: false,
58 58
 
59 59
     openSctp: true, // Toggle to enable/disable SCTP channels
60
+
61
+    // Disable hiding of remote thumbnails when in a 1-on-1 conference call.
62
+    disable1On1Mode: false,
60 63
     disableStats: false,
61 64
     disableAudioLevels: false,
62 65
     channelLastN: -1, // The default value of the channel attribute last-n.

+ 17
- 0
css/_filmstrip.scss 查看文件

@@ -62,10 +62,18 @@
62 62
         videos. */
63 63
         font-size: 0pt;
64 64
 
65
+        #filmstripLocalVideo {
66
+            padding-left: 0;
67
+        }
68
+
65 69
         &.hidden {
66 70
             bottom: -196px;
67 71
         }
68 72
 
73
+        .remote-videos-container {
74
+            display: flex;
75
+        }
76
+
69 77
         .videocontainer {
70 78
             display: none;
71 79
             position: relative;
@@ -130,4 +138,13 @@
130 138
         margin-bottom: auto;
131 139
         padding-right: $defaultToolbarSize;
132 140
     }
141
+
142
+    .remote-videos-container {
143
+        transition: opacity 1s;
144
+
145
+        &.hide-videos {
146
+            opacity: 0;
147
+            pointer-events: none;
148
+        }
149
+    }
133 150
 }

+ 24
- 0
css/_jitsi_popover.scss 查看文件

@@ -44,4 +44,28 @@
44 44
         border-width: 5px;
45 45
         border-bottom-width: 0;
46 46
     }
47
+
48
+    /**
49
+     * Override default "top" styles to support popovers appearing from the
50
+     * left of the popover trigger element.
51
+     */
52
+    &.left {
53
+        margin-left: -$popoverMenuPadding;
54
+        margin-top: 0;
55
+
56
+        .arrow {
57
+            border-color: transparent transparent transparent $popoverBg;
58
+            border-width: 5px 0px 5px 5px;
59
+            margin-left: 0;
60
+            margin-top: -5px;
61
+        }
62
+
63
+        .jitsipopover {
64
+            &__menu-padding {
65
+                bottom: 0;
66
+                height: 100%;
67
+                width: $popoverMenuPadding;
68
+            }
69
+        }
70
+    }
47 71
 }

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

@@ -0,0 +1,118 @@
1
+/**
2
+ * Override other styles to support vertical filmstrip mode.
3
+ */
4
+.vertical-filmstrip {
5
+    .filmstrip {
6
+        align-items: flex-end;
7
+        box-sizing: border-box;
8
+        display: flex;
9
+        flex-direction: column-reverse;
10
+        height: 100%;
11
+
12
+        /**
13
+         * Hide videos by making them slight to the right.
14
+         */
15
+        .filmstrip__videos {
16
+            right: 0;
17
+            transition: right 2s;
18
+
19
+            &.hidden {
20
+                bottom: auto;
21
+                right: -196px;
22
+            }
23
+        }
24
+
25
+        #filmstripLocalVideo {
26
+            height: auto;
27
+            justify-content: flex-end;
28
+        }
29
+
30
+        /**
31
+         * Remove unnecssary padding that is normally used to prevent horizontal
32
+         * filmstrip from overlapping the left edge of the screen.
33
+         */
34
+        #filmstripLocalVideo,
35
+        #filmstripRemoteVideos {
36
+            padding: 0;
37
+        }
38
+
39
+        #filmstripRemoteVideos {
40
+            display: flex;
41
+            flex: 1;
42
+            flex-direction: column;
43
+            height: auto;
44
+            overflow-x: hidden !important;
45
+
46
+            .remote-videos-container {
47
+                flex-direction: column;
48
+            }
49
+        }
50
+
51
+        /**
52
+         * Rotate the hide filmstrip icon so it points towards the right edge
53
+         * of the screen.
54
+         */
55
+        &__toolbar {
56
+            transform: rotate(-90deg);
57
+        }
58
+
59
+        /**
60
+         * Move the remote video menu trigger to the bottom left of the
61
+         * video thumbnail.
62
+         */
63
+        .remotevideomenu {
64
+            bottom: 0;
65
+            left: 0;
66
+            top: auto;
67
+            right: auto;
68
+            transform: rotate(-90deg);
69
+        }
70
+
71
+        #remoteVideos {
72
+            flex-direction: column-reverse;
73
+            height: 100%;
74
+        }
75
+
76
+        .videocontainer {
77
+            /**
78
+             * Move status icons to the bottom right of the thumbnail.
79
+             */
80
+            &__toolbar {
81
+                text-align: right;
82
+
83
+                .toolbar-icon {
84
+                    float: none;
85
+                }
86
+            }
87
+        }
88
+    }
89
+
90
+    /**
91
+     * For video labels that display on the top right to adjust its position as
92
+     * the filmstrip itself or filmstrip remote videos appear and disappear.
93
+     */
94
+    .video-state-indicator {
95
+        transition: right 2s;
96
+
97
+        &.with-filmstrip {
98
+            &#recordingLabel {
99
+                right: 200px;
100
+            }
101
+
102
+            &#videoResolutionLabel {
103
+                right: 150px;
104
+            }
105
+        }
106
+    }
107
+
108
+    /**
109
+     * Move toastr closer to the bottom of the screen and move left to avoid
110
+     * overlapping of videos when they are configured at default height.
111
+     */
112
+    #toast-container {
113
+        &.notification-bottom-right {
114
+            bottom: 25px;
115
+            right: 130 + 2 * ($thumbnailVideoMargin + $thumbnailsBorder) + $thumbnailVideoBorder;
116
+        }
117
+    }
118
+}

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

@@ -73,5 +73,6 @@
73 73
 @import 'policy';
74 74
 @import 'filmstrip';
75 75
 @import 'unsupported-browser/main';
76
+@import 'vertical_filmstrip_overrides';
76 77
 
77 78
 /* Modules END */

+ 6
- 0
interface_config.js 查看文件

@@ -55,6 +55,12 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
55 55
      * Whether to only show the filmstrip (and hide the toolbar).
56 56
      */
57 57
     filmStripOnly: false,
58
+
59
+    /**
60
+     * Whether to show thumbnails in filmstrip as a column instead of as a row.
61
+     */
62
+    VERTICAL_FILMSTRIP: false,
63
+
58 64
     //A html text to be shown to guests on the close page, false disables it
59 65
     CLOSE_PAGE_GUEST_HINT: false,
60 66
     RANDOM_AVATAR_URL_PREFIX: false,

+ 34
- 0
modules/UI/UI.js 查看文件

@@ -309,6 +309,10 @@ UI.start = function () {
309 309
     SideContainerToggler.init(eventEmitter);
310 310
     Filmstrip.init(eventEmitter);
311 311
 
312
+    // By default start with remote videos hidden and rely on other logic to
313
+    // make them visible.
314
+    UI.setRemoteThumbnailsVisibility(false);
315
+
312 316
     VideoLayout.init(eventEmitter);
313 317
     if (!interfaceConfig.filmStripOnly) {
314 318
         VideoLayout.initLargeVideo();
@@ -339,6 +343,10 @@ UI.start = function () {
339 343
         JitsiPopover.enabled = false;
340 344
     }
341 345
 
346
+    if (interfaceConfig.VERTICAL_FILMSTRIP) {
347
+        $("body").addClass("vertical-filmstrip");
348
+    }
349
+
342 350
     document.title = interfaceConfig.APP_NAME;
343 351
 
344 352
     if (!interfaceConfig.filmStripOnly) {
@@ -1142,6 +1150,15 @@ UI.getLargeVideo = function () {
1142 1150
     return VideoLayout.getLargeVideo();
1143 1151
 };
1144 1152
 
1153
+/**
1154
+ * Returns whether or not the passed in user id is currently pinned to the large
1155
+ * video.
1156
+ *
1157
+ * @param {string} userId - The id of the user to check is pinned or not.
1158
+ * @returns {boolean} True if the user is currently pinned to the large video.
1159
+ */
1160
+UI.isPinned = userId => VideoLayout.getPinnedId() === userId;
1161
+
1145 1162
 /**
1146 1163
  * Shows dialog with a link to FF extension.
1147 1164
  */
@@ -1392,6 +1409,23 @@ UI.isRingOverlayVisible = () => RingOverlay.isVisible();
1392 1409
  */
1393 1410
 UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
1394 1411
 
1412
+/**
1413
+ * Returns the number of known remote videos.
1414
+ *
1415
+ * @returns {number} The number of remote videos.
1416
+ */
1417
+UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount();
1418
+
1419
+/**
1420
+ * Makes remote thumbnail videos visible or not visible.
1421
+ *
1422
+ * @param {boolean} shouldHide - True if remote thumbnails should be hidden,
1423
+ * false f they should be visible.
1424
+ * @returns {void}
1425
+ */
1426
+UI.setRemoteThumbnailsVisibility
1427
+    = shouldHide => Filmstrip.setRemoteVideoVisibility(shouldHide);
1428
+
1395 1429
 const UIListeners = new Map([
1396 1430
     [
1397 1431
         UIEvents.ETHERPAD_CLICKED,

+ 13
- 0
modules/UI/recording/Recording.js 查看文件

@@ -201,6 +201,15 @@ function _showStopRecordingPrompt(recordingType) {
201 201
  * position
202 202
  */
203 203
 function moveToCorner(selector, move) {
204
+    const {
205
+        remoteVideosCount,
206
+        remoteVideosVisible,
207
+        visible
208
+    } = APP.store.getState()['features/filmstrip'];
209
+    selector.toggleClass(
210
+        'with-filmstrip',
211
+        Boolean(remoteVideosCount && remoteVideosVisible && visible));
212
+
204 213
     let moveToCornerClass = "moveToCorner";
205 214
     let containsClass = selector.hasClass(moveToCornerClass);
206 215
 
@@ -295,6 +304,10 @@ var Recording = {
295 304
             APP.UI.messageHandler.enableNotifications(false);
296 305
             APP.UI.messageHandler.enablePopups(false);
297 306
         }
307
+
308
+        this.eventEmitter.addListener(UIEvents.UPDATED_FILMSTRIP_DISPLAY, () =>{
309
+            this._updateStatusLabel();
310
+        });
298 311
     },
299 312
 
300 313
     /**

+ 4
- 1
modules/UI/shared_video/SharedVideo.js 查看文件

@@ -456,6 +456,9 @@ export default class SharedVideoManager {
456 456
                 // revert to original behavior (prevents pausing
457 457
                 // for participants not sharing the video to pause it)
458 458
                 $("#sharedVideo").css("pointer-events","auto");
459
+
460
+                this.emitter.emit(
461
+                    UIEvents.UPDATE_SHARED_VIDEO, null, 'removed');
459 462
         });
460 463
 
461 464
         this.url = null;
@@ -656,7 +659,7 @@ SharedVideoThumb.prototype.createContainer = function (spanId) {
656 659
     avatar.src = "https://img.youtube.com/vi/" + this.url + "/0.jpg";
657 660
     container.appendChild(avatar);
658 661
 
659
-    var remotes = document.getElementById('remoteVideos');
662
+    var remotes = document.getElementById('filmstripRemoteVideosContainer');
660 663
     return remotes.appendChild(container);
661 664
 };
662 665
 

+ 84
- 18
modules/UI/util/JitsiPopover.js 查看文件

@@ -1,4 +1,74 @@
1 1
 /* global $ */
2
+
3
+const positionConfigurations = {
4
+    left: {
5
+
6
+        // Align the popover's right side to the target element.
7
+        my: 'right',
8
+
9
+        // Align the popover to the left side of the target element.
10
+        at: 'left',
11
+
12
+        // Force the popover to fit within the viewport.
13
+        collision: 'fit',
14
+
15
+        /**
16
+         * Callback invoked by jQuery UI tooltip.
17
+         *
18
+         * @param {Object} position - The top and bottom position the popover
19
+         * element should be set at.
20
+         * @param {Object} element. - Additional size and position information
21
+         * about the popover element and target.
22
+         * @param {Object} elements.element - Has position and size related data
23
+         * for the popover element itself.
24
+         * @param {Object} elements.target - Has position and size related data
25
+         * for the target element the popover displays from.
26
+         */
27
+        using: function setPositionLeft(position, elements) {
28
+            const { element, target } = elements;
29
+
30
+            $('.jitsipopover').css({
31
+                display: 'table',
32
+                left: position.left,
33
+                top: position.top
34
+            });
35
+
36
+            // Move additional padding to the right edge of the popover and
37
+            // allow css to take care of width. The padding is used to maintain
38
+            // a hover state between the target and the popover.
39
+            $('.jitsipopover > .jitsipopover__menu-padding').css({
40
+                left: element.width
41
+            });
42
+
43
+            // Find the distance from the top of the popover to the center of
44
+            // the target and use that value to position the arrow to point to
45
+            // it.
46
+            const verticalCenterOfTarget = target.height / 2;
47
+            const verticalDistanceFromTops = target.top - element.top;
48
+            const verticalPositionOfTargetCenter
49
+                = verticalDistanceFromTops + verticalCenterOfTarget;
50
+
51
+            $('.jitsipopover > .arrow').css({
52
+                left: element.width,
53
+                top: verticalPositionOfTargetCenter
54
+            });
55
+        }
56
+    },
57
+    top: {
58
+        my: "bottom",
59
+        at: "top",
60
+        collision: "fit",
61
+        using: function setPositionTop(position, elements) {
62
+            var calcLeft = elements.target.left - elements.element.left +
63
+                elements.target.width/2;
64
+            $(".jitsipopover").css(
65
+                {top: position.top, left: position.left, display: "table"});
66
+            $(".jitsipopover > .arrow").css({left: calcLeft});
67
+            $(".jitsipopover > .jitsipopover__menu-padding").css(
68
+                {left: calcLeft - 50});
69
+        }
70
+    }
71
+};
2 72
 var JitsiPopover = (function () {
3 73
     /**
4 74
      * The default options
@@ -7,7 +77,8 @@ var JitsiPopover = (function () {
7 77
         skin: 'white',
8 78
         content: '',
9 79
         hasArrow: true,
10
-        onBeforePosition: undefined
80
+        onBeforePosition: undefined,
81
+        position: 'top'
11 82
     };
12 83
 
13 84
     /**
@@ -21,7 +92,6 @@ var JitsiPopover = (function () {
21 92
     function JitsiPopover(element, options)
22 93
     {
23 94
         this.options = Object.assign({}, defaultOptions, options);
24
-
25 95
         this.elementIsHovered = false;
26 96
         this.popoverIsHovered = false;
27 97
         this.popoverShown = false;
@@ -45,12 +115,15 @@ var JitsiPopover = (function () {
45 115
      * Returns template for popover
46 116
      */
47 117
     JitsiPopover.prototype.getTemplate = function () {
118
+        const { hasArrow, position, skin } = this.options;
119
+
48 120
         let arrow = '';
49
-        if (this.options.hasArrow) {
121
+        if (hasArrow) {
50 122
             arrow = '<div class="arrow"></div>';
51 123
         }
124
+
52 125
         return  (
53
-            `<div class="jitsipopover ${this.options.skin}">
126
+            `<div class="jitsipopover ${skin} ${position}">
54 127
                 ${arrow}
55 128
                 <div class="jitsipopover__content"></div>
56 129
                 <div class="jitsipopover__menu-padding"></div>
@@ -129,21 +202,14 @@ var JitsiPopover = (function () {
129 202
      * Refreshes the position of the popover.
130 203
      */
131 204
     JitsiPopover.prototype.refreshPosition = function () {
132
-        $(".jitsipopover").position({
133
-            my: "bottom",
134
-            at: "top",
135
-            collision: "fit",
136
-            of: this.element,
137
-            using: function (position, elements) {
138
-                var calcLeft = elements.target.left - elements.element.left +
139
-                    elements.target.width/2;
140
-                $(".jitsipopover").css(
141
-                    {top: position.top, left: position.left, display: "table"});
142
-                $(".jitsipopover > .arrow").css({left: calcLeft});
143
-                $(".jitsipopover > .jitsipopover__menu-padding").css(
144
-                    {left: calcLeft - 50});
205
+        const positionOptions = Object.assign(
206
+            {},
207
+            positionConfigurations[this.options.position],
208
+            {
209
+                of: this.element
145 210
             }
146
-        });
211
+        );
212
+        $(".jitsipopover").position(positionOptions);
147 213
     };
148 214
 
149 215
     /**

+ 3
- 2
modules/UI/videolayout/ConnectionIndicator.js 查看文件

@@ -1,4 +1,4 @@
1
-/* global $, APP */
1
+/* global $, APP, interfaceConfig */
2 2
 /* jshint -W101 */
3 3
 
4 4
 import JitsiPopover from "../util/JitsiPopover";
@@ -309,7 +309,8 @@ ConnectionIndicator.prototype.create = function () {
309 309
     this.popover = new JitsiPopover($(element), {
310 310
         content: popoverContent,
311 311
         skin: "black",
312
-        onBeforePosition: el => APP.translation.translateElement(el)
312
+        onBeforePosition: el => APP.translation.translateElement(el),
313
+        position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top'
313 314
     });
314 315
 
315 316
     // override popover show method to make sure we will update the content

+ 65
- 13
modules/UI/videolayout/Filmstrip.js 查看文件

@@ -1,4 +1,9 @@
1
-/* global $, APP, JitsiMeetJS, interfaceConfig */
1
+/* global $, APP, config, JitsiMeetJS, interfaceConfig */
2
+
3
+import {
4
+    setFilmstripRemoteVideosVisibility,
5
+    setFilmstripVisibility
6
+} from '../../../react/features/filmstrip';
2 7
 
3 8
 import UIEvents from "../../../service/UI/UIEvents";
4 9
 import UIUtil from "../util/UIUtil";
@@ -14,6 +19,7 @@ const Filmstrip = {
14 19
         this.iconMenuUpClassName = 'icon-menu-up';
15 20
         this.filmstripContainerClassName = 'filmstrip';
16 21
         this.filmstrip = $('#remoteVideos');
22
+        this.filmstripRemoteVideos = $('#filmstripRemoteVideosContainer');
17 23
         this.eventEmitter = eventEmitter;
18 24
 
19 25
         // Show the toggle button and add event listeners only when out of
@@ -24,6 +30,28 @@ const Filmstrip = {
24 30
         }
25 31
     },
26 32
 
33
+    /**
34
+     * Sets a class on the remote videos container for CSS to adjust visibility
35
+     * of the remote videos. Will no-op if config.debug is truthy, as should be
36
+     * the case with torture tests.
37
+     *
38
+     * @param {boolean} shouldHide - True if remote videos should be hidden,
39
+     * false if they should be visible.
40
+     * @returns {void}
41
+     */
42
+    setRemoteVideoVisibility(shouldShow) {
43
+        // FIXME Checking config.debug is a grand hack to avoid fixing the
44
+        // torture tests after the 1-on-1 UI was implemented, which hides remote
45
+        // videos on 1-on-1 calls. If this check is to be kept, at least create
46
+        // new torture tests to verify 1-on-1 mode.
47
+        if (config.debug || config.disable1On1Mode) {
48
+            return;
49
+        }
50
+
51
+        APP.store.dispatch(setFilmstripRemoteVideosVisibility(shouldShow));
52
+        this.filmstripRemoteVideos.toggleClass('hide-videos', !shouldShow);
53
+    },
54
+
27 55
     /**
28 56
      * Initializes the filmstrip toolbar.
29 57
      */
@@ -150,11 +178,14 @@ const Filmstrip = {
150 178
 
151 179
         // Emit/fire UIEvents.TOGGLED_FILMSTRIP.
152 180
         const eventEmitter = this.eventEmitter;
181
+        const isFilmstripVisible = this.isFilmstripVisible();
182
+
153 183
         if (eventEmitter) {
154 184
             eventEmitter.emit(
155 185
                 UIEvents.TOGGLED_FILMSTRIP,
156 186
                 this.isFilmstripVisible());
157 187
         }
188
+        APP.store.dispatch(setFilmstripVisibility(isFilmstripVisible));
158 189
     },
159 190
 
160 191
     /**
@@ -177,7 +208,10 @@ const Filmstrip = {
177 208
      * @returns {number} height
178 209
      */
179 210
     getFilmstripHeight() {
180
-        if (this.isFilmstripVisible()) {
211
+        // FIXME Make it more clear the getFilmstripHeight check is used in
212
+        // horizontal film strip mode for calculating how tall large video
213
+        // display should be.
214
+        if (this.isFilmstripVisible() && !interfaceConfig.VERTICAL_FILMSTRIP) {
181 215
             return $(`.${this.filmstripContainerClassName}`).outerHeight();
182 216
         } else {
183 217
             return 0;
@@ -364,13 +398,27 @@ const Filmstrip = {
364 398
             (remoteLocalWidthRatio * numberRemoteThumbs + 1), availableHeight *
365 399
             interfaceConfig.LOCAL_THUMBNAIL_RATIO);
366 400
         const h = lW / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
401
+
402
+        const removeVideoWidth = lW * remoteLocalWidthRatio;
403
+
404
+        let localVideo;
405
+        if (interfaceConfig.VERTICAL_FILMSTRIP) {
406
+            // scale both width and height
407
+            localVideo = {
408
+                thumbWidth: removeVideoWidth,
409
+                thumbHeight: h * remoteLocalWidthRatio
410
+            };
411
+        } else {
412
+            localVideo = {
413
+                thumbWidth: lW,
414
+                thumbHeight: h
415
+            };
416
+        }
417
+
367 418
         return {
368
-                    localVideo:{
369
-                        thumbWidth: lW,
370
-                        thumbHeight: h
371
-                    },
419
+                    localVideo,
372 420
                     remoteVideo: {
373
-                        thumbWidth: lW * remoteLocalWidthRatio,
421
+                        thumbWidth: removeVideoWidth,
374 422
                         thumbHeight: h
375 423
                     }
376 424
                 };
@@ -406,10 +454,15 @@ const Filmstrip = {
406 454
                 }));
407 455
             }
408 456
             promises.push(new Promise((resolve) => {
409
-                this.filmstrip.animate({
410
-                    // adds 2 px because of small video 1px border
411
-                    height: remote.thumbHeight + 2
412
-                }, this._getAnimateOptions(animate, resolve));
457
+                // Let CSS take care of height in vertical filmstrip mode.
458
+                if (interfaceConfig.VERTICAL_FILMSTRIP) {
459
+                    resolve();
460
+                } else {
461
+                    this.filmstrip.animate({
462
+                        // adds 2 px because of small video 1px border
463
+                        height: remote.thumbHeight + 2
464
+                    }, this._getAnimateOptions(animate, resolve));
465
+                }
413 466
             }));
414 467
 
415 468
             promises.push(new Promise(() => {
@@ -456,8 +509,7 @@ const Filmstrip = {
456 509
         }
457 510
 
458 511
         let localThumb = $("#localVideoContainer");
459
-        let remoteThumbs = this.filmstrip.children(selector)
460
-            .not("#localVideoContainer");
512
+        let remoteThumbs = this.filmstripRemoteVideos.children(selector);
461 513
 
462 514
         // Exclude the local video container if it has been hidden.
463 515
         if (localThumb.hasClass("hidden")) {

+ 3
- 2
modules/UI/videolayout/RemoteVideo.js 查看文件

@@ -91,7 +91,8 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
91 91
         content: popupMenuElement.outerHTML,
92 92
         skin: "black",
93 93
         hasArrow: false,
94
-        onBeforePosition: el => APP.translation.translateElement(el)
94
+        onBeforePosition: el => APP.translation.translateElement(el),
95
+        position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top'
95 96
     };
96 97
     let element = $("#" + this.videoSpanId + " .remotevideomenu");
97 98
     this.popover = new JitsiPopover(element, options);
@@ -800,7 +801,7 @@ RemoteVideo.createContainer = function (spanId) {
800 801
     overlay.className = "videocontainer__hoverOverlay";
801 802
     container.appendChild(overlay);
802 803
 
803
-    var remotes = document.getElementById('remoteVideos');
804
+    var remotes = document.getElementById('filmstripRemoteVideosContainer');
804 805
     return remotes.appendChild(container);
805 806
 };
806 807
 

+ 16
- 0
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
+    setFilmstripRemoteVideosCount
6
+} from '../../../react/features/filmstrip';
7
+
4 8
 import Filmstrip from "./Filmstrip";
5 9
 import UIEvents from "../../../service/UI/UIEvents";
6 10
 import UIUtil from "../util/UIUtil";
@@ -550,6 +554,9 @@ var VideoLayout = {
550 554
                 if (onComplete && typeof onComplete === "function")
551 555
                     onComplete();
552 556
             });
557
+
558
+        APP.store.dispatch(
559
+            setFilmstripRemoteVideosCount(this.getRemoteVideosCount()));
553 560
         return { localVideo, remoteVideo };
554 561
     },
555 562
 
@@ -1133,6 +1140,15 @@ var VideoLayout = {
1133 1140
      */
1134 1141
     getLargeVideoWrapper() {
1135 1142
         return this.getCurrentlyOnLargeContainer().$wrapper;
1143
+    },
1144
+
1145
+    /**
1146
+     * Returns the number of remove video ids.
1147
+     *
1148
+     * @returns {number} The number of remote videos.
1149
+     */
1150
+    getRemoteVideosCount() {
1151
+        return Object.keys(remoteVideos).length;
1136 1152
     }
1137 1153
 };
1138 1154
 

+ 62
- 29
react/features/conference/components/Conference.web.js 查看文件

@@ -10,6 +10,7 @@ import { OverlayContainer } from '../../overlay';
10 10
 import { Toolbox } from '../../toolbox';
11 11
 import { HideNotificationBarStyle } from '../../unsupported-browser';
12 12
 import { VideoStatusLabel } from '../../video-status-label';
13
+import '../../filmstrip';
13 14
 
14 15
 declare var $: Function;
15 16
 declare var APP: Object;
@@ -106,35 +107,7 @@ class Conference extends Component {
106 107
                                 src = 'images/spin.svg' />
107 108
                         </span>
108 109
                     </div>
109
-                    <div className = 'filmstrip'>
110
-                        <div
111
-                            className = 'filmstrip__videos'
112
-                            id = 'remoteVideos'>
113
-                            <span
114
-                                className = 'videocontainer'
115
-                                id = 'localVideoContainer'>
116
-                                <div className = 'videocontainer__background' />
117
-                                <span id = 'localVideoWrapper' />
118
-                                <audio
119
-                                    autoPlay = { true }
120
-                                    id = 'localAudio'
121
-                                    muted = { true } />
122
-                                <div className = 'videocontainer__toolbar' />
123
-                                <div className = 'videocontainer__toptoolbar' />
124
-                                <div
125
-                                    className
126
-                                        = 'videocontainer__hoverOverlay' />
127
-                            </span>
128
-                            <audio
129
-                                id = 'userJoined'
130
-                                preload = 'auto'
131
-                                src = 'sounds/joined.wav' />
132
-                            <audio
133
-                                id = 'userLeft'
134
-                                preload = 'auto'
135
-                                src = 'sounds/left.wav' />
136
-                        </div>
137
-                    </div>
110
+                    { this._renderFilmstrip() }
138 111
                 </div>
139 112
 
140 113
                 <DialogContainer />
@@ -143,6 +116,66 @@ class Conference extends Component {
143 116
             </div>
144 117
         );
145 118
     }
119
+
120
+    /**
121
+     * Creates a React Element for displaying filmstrip videos.
122
+     *
123
+     * @private
124
+     * @returns {ReactElement}
125
+     */
126
+    _renderFilmstrip() {
127
+        return (
128
+            <div className = 'filmstrip'>
129
+                <div
130
+                    className = 'filmstrip__videos'
131
+                    id = 'remoteVideos'>
132
+                    <div
133
+                        className = 'filmstrip__videos'
134
+                        id = 'filmstripLocalVideo'>
135
+                        <span
136
+                            className = 'videocontainer'
137
+                            id = 'localVideoContainer'>
138
+                            <div className = 'videocontainer__background' />
139
+                            <span id = 'localVideoWrapper' />
140
+                            <audio
141
+                                autoPlay = { true }
142
+                                id = 'localAudio'
143
+                                muted = { true } />
144
+                            <div className = 'videocontainer__toolbar' />
145
+                            <div className = 'videocontainer__toptoolbar' />
146
+                            <div
147
+                                className
148
+                                    = 'videocontainer__hoverOverlay' />
149
+                        </span>
150
+                    </div>
151
+                    <div
152
+                        className = 'filmstrip__videos'
153
+                        id = 'filmstripRemoteVideos'>
154
+                        {
155
+
156
+                            /*
157
+                                This extra video container is needed for
158
+                                scrolling thumbnails in firefox, otherwise the
159
+                                flex thumbnails resize instead of causing
160
+                                overflow.
161
+                            */
162
+                        }
163
+                        <div
164
+                            className = 'remote-videos-container'
165
+                            id = 'filmstripRemoteVideosContainer' />
166
+                    </div>
167
+                    <audio
168
+                        id = 'userJoined'
169
+                        preload = 'auto'
170
+                        src = 'sounds/joined.wav' />
171
+                    <audio
172
+                        id = 'userLeft'
173
+                        preload = 'auto'
174
+                        src = 'sounds/left.wav' />
175
+                </div>
176
+            </div>
177
+        );
178
+    }
146 179
 }
147 180
 
148 181
 export default reactReduxConnect()(Conference);

+ 35
- 0
react/features/filmstrip/actionTypes.js 查看文件

@@ -0,0 +1,35 @@
1
+import { Symbol } from '../base/react';
2
+
3
+/**
4
+ * The type of action which signals to change the count of known remote videos
5
+ * displayed in the filmstrip.
6
+ *
7
+ * {
8
+ *     type: SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
9
+ *     remoteVideosCount: number
10
+ * }
11
+ */
12
+export const SET_FILMSTRIP_REMOTE_VIDEOS_COUNT
13
+    = Symbol('SET_FILMSTRIP_REMOTE_VIDEOS_COUNT');
14
+
15
+/**
16
+ * The type of action which signals to change the visibility of remote videos in
17
+ * the filmstrip.
18
+ *
19
+ * {
20
+ *     type: SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
21
+ *     removeVideosVisible: boolean
22
+ * }
23
+ */
24
+export const SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY
25
+    = Symbol('SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY');
26
+
27
+/**
28
+ * The type of action sets the visibility of the entire filmstrip;
29
+ *
30
+ * {
31
+ *     type: SET_FILMSTRIP_VISIBILITY,
32
+ *     visible: boolean
33
+ * }
34
+ */
35
+export const SET_FILMSTRIP_VISIBILITY = Symbol('SET_FILMSTRIP_VISIBILITY');

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

@@ -0,0 +1,54 @@
1
+import {
2
+    SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
3
+    SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
4
+    SET_FILMSTRIP_VISIBILITY
5
+} from './actionTypes';
6
+
7
+/**
8
+ * Sets the visibility of remote videos in the filmstrip.
9
+ *
10
+ * @param {boolean} remoteVideosVisible - Whether or not remote videos in the
11
+ * filmstrip should be visible.
12
+ * @returns {{
13
+ *     type: SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
14
+ *     remoteVideosVisible: boolean
15
+ * }}
16
+ */
17
+export function setFilmstripRemoteVideosVisibility(remoteVideosVisible) {
18
+    return {
19
+        type: SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
20
+        remoteVideosVisible
21
+    };
22
+}
23
+
24
+/**
25
+ * Sets how many remote videos are currently in the filmstrip.
26
+ *
27
+ * @param {number} remoteVideosCount - The number of remote videos.
28
+ * @returns {{
29
+ *     type: SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
30
+ *     remoteVideosCount: number
31
+ * }}
32
+ */
33
+export function setFilmstripRemoteVideosCount(remoteVideosCount) {
34
+    return {
35
+        type: SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
36
+        remoteVideosCount
37
+    };
38
+}
39
+
40
+/**
41
+ * Sets if the entire filmstrip should be visible.
42
+ *
43
+ * @param {boolean} visible - Whether not the filmstrip is visible.
44
+ * @returns {{
45
+ *     type: SET_FILMSTRIP_VISIBILITY,
46
+ *     visible: boolean
47
+ * }}
48
+ */
49
+export function setFilmstripVisibility(visible) {
50
+    return {
51
+        type: SET_FILMSTRIP_VISIBILITY,
52
+        visible
53
+    };
54
+}

react/features/filmstrip/components/Filmstrip.js → react/features/filmstrip/components/Filmstrip.native.js 查看文件


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


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

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

+ 30
- 0
react/features/filmstrip/middleware.js 查看文件

@@ -0,0 +1,30 @@
1
+import UIEvents from '../../../service/UI/UIEvents';
2
+
3
+import { MiddlewareRegistry } from '../base/redux';
4
+
5
+import {
6
+    SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
7
+    SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
8
+    SET_FILMSTRIP_VISIBILITY
9
+} from './actionTypes';
10
+
11
+declare var APP: Object;
12
+
13
+// eslint-disable-next-line no-unused-vars
14
+MiddlewareRegistry.register(store => next => action => {
15
+    const result = next(action);
16
+
17
+    switch (action.type) {
18
+    case SET_FILMSTRIP_REMOTE_VIDEOS_COUNT:
19
+    case SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY:
20
+    case SET_FILMSTRIP_VISIBILITY: {
21
+        if (typeof APP !== 'undefined') {
22
+            APP.UI.emitEvent(UIEvents.UPDATED_FILMSTRIP_DISPLAY);
23
+
24
+        }
25
+        break;
26
+    }
27
+    }
28
+
29
+    return result;
30
+});

+ 36
- 0
react/features/filmstrip/reducer.js 查看文件

@@ -0,0 +1,36 @@
1
+import { ReducerRegistry } from '../base/redux';
2
+import {
3
+    SET_FILMSTRIP_REMOTE_VIDEOS_COUNT,
4
+    SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY,
5
+    SET_FILMSTRIP_VISIBILITY
6
+} from './actionTypes';
7
+
8
+const DEFAULT_STATE = {
9
+    remoteVideosCount: 0,
10
+    remoteVideosVisible: true,
11
+    visible: true
12
+};
13
+
14
+ReducerRegistry.register(
15
+    'features/filmstrip',
16
+    (state = DEFAULT_STATE, action) => {
17
+        switch (action.type) {
18
+        case SET_FILMSTRIP_REMOTE_VIDEOS_COUNT:
19
+            return {
20
+                ...state,
21
+                remoteVideosCount: action.remoteVideosCount
22
+            };
23
+        case SET_FILMSTRIP_REMOTE_VIDEOS_VISIBLITY:
24
+            return {
25
+                ...state,
26
+                remoteVideosVisible: action.remoteVideosVisible
27
+            };
28
+        case SET_FILMSTRIP_VISIBILITY:
29
+            return {
30
+                ...state,
31
+                visible: action.visible
32
+            };
33
+        }
34
+
35
+        return state;
36
+    });

+ 25
- 2
react/features/video-status-label/components/VideoStatusLabel.js 查看文件

@@ -28,6 +28,11 @@ export class VideoStatusLabel extends Component {
28 28
          */
29 29
         _conferenceStarted: React.PropTypes.bool,
30 30
 
31
+        /**
32
+         * Whether or not the filmstrip is displayed with remote videos.
33
+         */
34
+        _filmstripVisible: React.PropTypes.bool,
35
+
31 36
         /**
32 37
          * Whether or not a high-definition large video is displayed.
33 38
          */
@@ -64,7 +69,13 @@ export class VideoStatusLabel extends Component {
64 69
      * @returns {ReactElement}
65 70
      */
66 71
     render() {
67
-        const { _audioOnly, _conferenceStarted, _largeVideoHD, t } = this.props;
72
+        const {
73
+            _audioOnly,
74
+            _conferenceStarted,
75
+            _filmstripVisible,
76
+            _largeVideoHD,
77
+            t
78
+        } = this.props;
68 79
 
69 80
         // FIXME The _conferenceStarted check is used to be defensive against
70 81
         // toggling audio only mode while there is no conference and hides the
@@ -82,9 +93,14 @@ export class VideoStatusLabel extends Component {
82 93
                 ? t('videoStatus.hd') : t('videoStatus.sd');
83 94
         }
84 95
 
96
+        const filmstripClassName
97
+            = _filmstripVisible ? 'with-filmstrip' : 'without-filmstrip';
98
+        const classNames
99
+            = `video-state-indicator moveToCorner ${filmstripClassName}`;
100
+
85 101
         return (
86 102
             <div
87
-                className = 'video-state-indicator moveToCorner'
103
+                className = { classNames }
88 104
                 id = 'videoResolutionLabel' >
89 105
                 { displayedLabel }
90 106
                 { this._renderVideonMenu() }
@@ -152,10 +168,17 @@ function _mapStateToProps(state) {
152 168
         conference,
153 169
         isLargeVideoHD
154 170
     } = state['features/base/conference'];
171
+    const {
172
+        remoteVideosCount,
173
+        remoteVideosVisible,
174
+        visible
175
+    } = state['features/filmstrip'];
155 176
 
156 177
     return {
157 178
         _audioOnly: audioOnly,
158 179
         _conferenceStarted: Boolean(conference),
180
+        _filmstripVisible:
181
+            Boolean(remoteVideosCount && remoteVideosVisible && visible),
159 182
         _largeVideoHD: isLargeVideoHD
160 183
     };
161 184
 }

+ 6
- 0
service/UI/UIEvents.js 查看文件

@@ -65,6 +65,12 @@ export default {
65 65
      * @see {TOGGLE_FILMSTRIP}
66 66
      */
67 67
     TOGGLED_FILMSTRIP: "UI.toggled_filmstrip",
68
+
69
+    /**
70
+     * Notifies that the filmstrip has updated its appearance, such as by
71
+     * toggling or removing videos or adding videos.
72
+     */
73
+    UPDATED_FILMSTRIP_DISPLAY: "UI.updated_filmstrip_display",
68 74
     TOGGLE_SCREENSHARING: "UI.toggle_screensharing",
69 75
     TOGGLED_SHARED_DOCUMENT: "UI.toggled_shared_document",
70 76
     CONTACT_CLICKED: "UI.contact_clicked",

正在加载...
取消
保存