瀏覽代碼

feat(Filmstrip): Pagination.

master
Hristo Terezov 4 年之前
父節點
當前提交
16cfda3c7a

+ 15
- 53
css/filmstrip/_horizontal_filmstrip.scss 查看文件

33
     }
33
     }
34
 
34
 
35
     &__videos {
35
     &__videos {
36
-        @extend %align-right;
37
         position:relative;
36
         position:relative;
38
         padding: 0;
37
         padding: 0;
39
         /* The filmstrip should not be covered by the left toolbar. */
38
         /* The filmstrip should not be covered by the left toolbar. */
40
         bottom: 0;
39
         bottom: 0;
41
         width:auto;
40
         width:auto;
42
-        overflow: visible !important;
43
 
41
 
44
         &#remoteVideos {
42
         &#remoteVideos {
45
             border: $thumbnailsBorder solid transparent;
43
             border: $thumbnailsBorder solid transparent;
46
             transition: bottom 2s;
44
             transition: bottom 2s;
47
             flex-grow: 1;
45
             flex-grow: 1;
46
+            display: flex;
47
+            flex-direction: row-reverse;
48
             @include minHWAutoFix()
48
             @include minHWAutoFix()
49
         }
49
         }
50
 
50
 
60
         &.hidden {
60
         &.hidden {
61
             bottom: calc(-196px - #{$newToolbarSizeWithPadding});
61
             bottom: calc(-196px - #{$newToolbarSizeWithPadding});
62
         }
62
         }
63
-
64
-        .remote-videos-container {
65
-            display: flex;
66
-        }
67
     }
63
     }
68
 
64
 
69
-    .remote-videos-container {
70
-        transition: opacity 1s;
71
-    }
65
+    .remote-videos {
66
+        & > div {
67
+            transition: opacity 1s;
68
+            position: absolute;
69
+        }
72
 
70
 
73
-    &.hide-videos {
74
-        .remote-videos-container {
75
-            opacity: 0;
76
-            pointer-events: none;
71
+        &.is-not-overflowing > div {
72
+            right: 2px;
77
         }
73
         }
78
     }
74
     }
79
 
75
 
80
-    #filmstripRemoteVideos {
81
-        @include minHWAutoFix();
82
-
83
-        display: flex;
84
-        flex: 1;
85
-        width: auto;
86
-        justify-content: flex-end;
87
-        flex-direction: row;
88
-
89
-        #filmstripRemoteVideosContainer {
90
-            flex-direction: row-reverse;
91
-            /**
92
-             * Add padding as a hack for Firefox not to show scrollbars when
93
-             * unnecessary.
94
-             */
95
-            padding: 1px 0;
96
-            overflow-y: hidden;
97
-            overflow-x: scroll;
76
+    &.hide-videos {
77
+        .remote-videos  {
78
+            & > div {
79
+                opacity: 0;
80
+                pointer-events: none;
81
+            }
98
         }
82
         }
99
     }
83
     }
100
 
84
 
103
     }
87
     }
104
 }
88
 }
105
 
89
 
106
-
107
-/**
108
- * Workarounds for Edge and Firefox not handling scrolling properly with
109
- * flex-direction: row-reverse.
110
- */
111
- @mixin undoRowReverseVideos() {
112
-    .horizontal-filmstrip {
113
-        #remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
114
-            flex-direction: row;
115
-        }
116
-    }
117
-}
118
-
119
-/** Firefox detection hack **/
120
-@-moz-document url-prefix() {
121
-    @include undoRowReverseVideos();
122
-}
123
-
124
-/** Edge detection hack **/
125
-@supports (-ms-ime-align:auto) {
126
-    @include undoRowReverseVideos();
127
-}

+ 39
- 48
css/filmstrip/_tile_view.scss 查看文件

10
         box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px $videoThumbnailSelected;
10
         box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px $videoThumbnailSelected;
11
     }
11
     }
12
 
12
 
13
-    #filmstripRemoteVideos {
13
+    .remote-videos {
14
         align-items: center;
14
         align-items: center;
15
         box-sizing: border-box;
15
         box-sizing: border-box;
16
         display: flex;
16
         display: flex;
17
         flex-direction: column;
17
         flex-direction: column;
18
-        height: 100%;
19
-        width: 100%;
20
     }
18
     }
21
 
19
 
22
     .filmstrip__videos .videocontainer {
20
     .filmstrip__videos .videocontainer {
34
          */
32
          */
35
         height: 100% !important;
33
         height: 100% !important;
36
         width: 100%;
34
         width: 100%;
35
+        display: flex;
36
+        justify-content: center;
37
+        align-items: center;
37
     }
38
     }
38
 
39
 
39
     .filmstrip {
40
     .filmstrip {
50
             &.shift-right {
51
             &.shift-right {
51
                 margin-left: $sidebarWidth;
52
                 margin-left: $sidebarWidth;
52
                 width: calc(100% - #{$sidebarWidth});
53
                 width: calc(100% - #{$sidebarWidth});
54
+
55
+                .remote-videos{
56
+                    width: calc(100vw - #{$sidebarWidth});
57
+                }
53
             }
58
             }
54
         }
59
         }
55
     }
60
     }
62
         display: block;
67
         display: block;
63
     }
68
     }
64
 
69
 
65
-    #filmstripRemoteVideos {
70
+    .remote-videos {
66
         box-sizing: border-box;
71
         box-sizing: border-box;
67
 
72
 
68
-        /**
69
-         * Allow vertical scrolling of the thumbnails.
70
-         */
71
-        overflow-x: hidden;
72
-        overflow-y: auto;
73
-    }
74
 
73
 
75
-    /**
76
-     * The size of the thumbnails should be set with javascript, based on
77
-     * desired column count and window width. The rows are created using flex
78
-     * and allowing the thumbnails to wrap.
79
-     */
80
-    #filmstripRemoteVideosContainer {
81
-        align-content: center;
82
-        align-items: center;
83
-        box-sizing: border-box;
84
-        display: flex;
85
-        flex-wrap: wrap;
86
-        flex-shrink: 0;
87
-        margin-top: auto;
88
-        margin-bottom: auto;
89
-        justify-content: center;
90
-
91
-        .videocontainer {
92
-            border: 0;
74
+        /**
75
+        * The size of the thumbnails should be set with javascript, based on
76
+        * desired column count and window width. The rows are created using flex
77
+        * and allowing the thumbnails to wrap.
78
+        */
79
+        & > div {
80
+            align-content: center;
81
+            align-items: center;
93
             box-sizing: border-box;
82
             box-sizing: border-box;
94
-            display: block;
95
-            margin: 2px;
96
-        }
83
+            display: flex;
84
+            margin-top: auto;
85
+            margin-bottom: auto;
86
+            justify-content: center;
87
+            position: absolute;
97
 
88
 
98
-        video {
99
-            object-fit: contain;
100
-        }
89
+            .videocontainer {
90
+                border: 0;
91
+                box-sizing: border-box;
92
+                display: block;
93
+                margin: 2px;
94
+            }
101
 
95
 
102
-        /**
103
-         * Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
104
-         */
105
-        @media only screen and (max-width: 500px) {
106
             video {
96
             video {
107
-                object-fit: cover;
97
+                object-fit: contain;
108
             }
98
             }
109
-        }
110
-    }
111
 
99
 
112
-    .has-overflow#filmstripRemoteVideosContainer {
113
-        align-content: baseline;
114
-    }
115
-
116
-    .has-overflow .videocontainer {
117
-        align-self: baseline;
100
+            /**
101
+            * Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
102
+            */
103
+            @media only screen and (max-width: 500px) {
104
+                video {
105
+                    object-fit: cover;
106
+                }
107
+            }
108
+        }
118
     }
109
     }
119
 }
110
 }
120
 
111
 
121
-.shift-right #filmstripRemoteVideosContainer {
112
+.shift-right .remote-videos > div {
122
     /**
113
     /**
123
      * Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants,
114
      * Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants,
124
      * from which we subtract the chat size.
115
      * from which we subtract the chat size.

+ 16
- 76
css/filmstrip/_vertical_filmstrip.scss 查看文件

1
 .vertical-filmstrip .filmstrip {
1
 .vertical-filmstrip .filmstrip {
2
     &.hide-videos {
2
     &.hide-videos {
3
-        .remote-videos-container {
4
-            opacity: 0;
5
-            pointer-events: none;
3
+        .remote-videos {
4
+            & > div {
5
+                opacity: 0;
6
+                pointer-events: none;
7
+            }
6
         }
8
         }
7
     }
9
     }
8
 
10
 
39
     right: 0;
41
     right: 0;
40
     z-index: $filmstripVideosZ;
42
     z-index: $filmstripVideosZ;
41
 
43
 
42
-    &.reduce-height {
43
-        height: calc(100% - #{$newToolbarSizeWithPadding});
44
-    }
45
-
46
     /**
44
     /**
47
      * Hide videos by making them slight to the right.
45
      * Hide videos by making them slight to the right.
48
      */
46
      */
98
      * filmstrip from overlapping the left edge of the screen.
96
      * filmstrip from overlapping the left edge of the screen.
99
      */
97
      */
100
     #filmstripLocalVideo,
98
     #filmstripLocalVideo,
101
-    #filmstripRemoteVideos {
99
+    .remote-videos {
102
         padding: 0;
100
         padding: 0;
103
     }
101
     }
104
 
102
 
105
-    #filmstripRemoteVideos {
106
-        @include minHWAutoFix();
107
-
108
-        display: flex;
109
-        flex: 1;
110
-        flex-direction: column-reverse;
111
-        height: auto;
112
-        overflow-x: hidden;
113
-        overflow-y: scroll;
114
-
115
-        #filmstripRemoteVideosContainer {
116
-            @include minHWAutoFix();
117
-            flex-direction: column-reverse;
118
-            overflow: visible;
119
-            width: calc(100% - 8px); // 8px for margin + border of the thumbnails
120
-
121
-            .videocontainer {
122
-                height: 0px;
123
-                width: 100%;
124
-            }
125
-        }
126
-    }
127
-
128
     #remoteVideos {
103
     #remoteVideos {
129
         @include minHWAutoFix();
104
         @include minHWAutoFix();
130
 
105
 
132
         flex-grow: 1;
107
         flex-grow: 1;
133
     }
108
     }
134
 
109
 
135
-    .remote-videos-container {
136
-        display: flex;
137
-        transition: opacity 1s;
110
+    &.reduce-height {
111
+        height: calc(100% - calc(#{$newToolbarSizeWithPadding} + #{$scrollHeight}));
138
     }
112
     }
139
 
113
 
140
-    .hide-scrollbar#filmstripRemoteVideos {
141
-        margin-right: 7px; // Scrollbar size
142
-        &::-webkit-scrollbar {
143
-            display: none;
144
-        }
145
-    }
146
-}
114
+    .remote-videos {
115
+        display: flex;
116
+        transition: height .3s ease-in;
147
 
117
 
148
-/**
149
- * Workarounds for Edge and Firefox not handling scrolling properly with
150
- * flex-direction: column-reverse. The remove videos in filmstrip should
151
- * start scrolling from the bottom of the filmstrip, but in those browsers the
152
- * scrolling won't happen. Per W3C spec, scrolling should happen from the
153
- * bottom. As such, use css hacks to get around the css issue, with the intent
154
- * being to remove the hacks as the spec is supported.
155
- */
156
-@mixin undoColumnReverseVideos() {
157
-    .vertical-filmstrip {
158
-        #remoteVideos #filmstripRemoteVideos #filmstripRemoteVideosContainer {
159
-            flex-direction: column;
118
+        & > div {
119
+            position: absolute;
120
+            transition: opacity 1s;
160
         }
121
         }
161
-    }
162
-}
163
 
122
 
164
-/**
165
- * FF does not include the scroll width when calculating the size of the content. That's why we need to include
166
- * ourselves the width of the scroll so that the remote videos are aligned with the local one.
167
- */
168
-@mixin filmstripSizeWithoutScroll {
169
-    .vertical-filmstrip {
170
-        #remoteVideos #filmstripRemoteVideos {
171
-            #filmstripRemoteVideosContainer {
172
-                width: calc(100% - 15px) // 8 px - margins + border of the thumbnails; 7px - for the scroll
173
-            }
123
+        &.is-not-overflowing > div {
124
+            bottom: 0px;
174
         }
125
         }
175
     }
126
     }
176
 }
127
 }
177
-
178
-/** Firefox detection hack **/
179
-@-moz-document url-prefix() {
180
-    @include undoColumnReverseVideos();
181
-    @include filmstripSizeWithoutScroll();
182
-}
183
-
184
-/** Edge detection hack **/
185
-@supports (-ms-ime-align:auto) {
186
-    @include undoColumnReverseVideos();
187
-}

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

11390
         }
11390
         }
11391
       }
11391
       }
11392
     },
11392
     },
11393
+    "memoize-one": {
11394
+      "version": "5.1.1",
11395
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
11396
+      "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
11397
+    },
11393
     "memory-fs": {
11398
     "memory-fs": {
11394
       "version": "0.4.1",
11399
       "version": "0.4.1",
11395
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
11400
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
15318
         }
15323
         }
15319
       }
15324
       }
15320
     },
15325
     },
15326
+    "react-window": {
15327
+      "version": "1.8.6",
15328
+      "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
15329
+      "integrity": "sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==",
15330
+      "requires": {
15331
+        "@babel/runtime": "^7.0.0",
15332
+        "memoize-one": ">=3.1.1 <6"
15333
+      }
15334
+    },
15321
     "react-youtube": {
15335
     "react-youtube": {
15322
       "version": "7.13.1",
15336
       "version": "7.13.1",
15323
       "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.13.1.tgz",
15337
       "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-7.13.1.tgz",

+ 1
- 0
package.json 查看文件

94
     "react-textarea-autosize": "8.3.0",
94
     "react-textarea-autosize": "8.3.0",
95
     "react-transition-group": "2.4.0",
95
     "react-transition-group": "2.4.0",
96
     "react-youtube": "7.13.1",
96
     "react-youtube": "7.13.1",
97
+    "react-window": "1.8.6",
97
     "redux": "4.0.4",
98
     "redux": "4.0.4",
98
     "redux-thunk": "2.2.0",
99
     "redux-thunk": "2.2.0",
99
     "rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
100
     "rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",

+ 40
- 26
react/features/base/media/components/web/AudioTrack.js 查看文件

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
 import { createAudioPlayErrorEvent, createAudioPlaySuccessEvent, sendAnalytics } from '../../../../analytics';
5
 import { createAudioPlayErrorEvent, createAudioPlaySuccessEvent, sendAnalytics } from '../../../../analytics';
6
+import { connect } from '../../../redux';
6
 import logger from '../../logger';
7
 import logger from '../../logger';
7
 
8
 
8
 /**
9
 /**
10
  */
11
  */
11
 type Props = {
12
 type Props = {
12
 
13
 
14
+    /**
15
+     * Represents muted property of the underlying audio element.
16
+     */
17
+    _muted: ?Boolean,
18
+
19
+    /**
20
+     * Represents volume property of the underlying audio element.
21
+     */
22
+    _volume: ?number,
23
+
13
     /**
24
     /**
14
      * The value of the id attribute of the audio element.
25
      * The value of the id attribute of the audio element.
15
      */
26
      */
28
     autoPlay: boolean,
39
     autoPlay: boolean,
29
 
40
 
30
     /**
41
     /**
31
-     * Represents muted property of the underlying audio element.
32
-     */
33
-    muted: ?Boolean,
34
-
35
-    /**
36
-     * Represents volume property of the underlying audio element.
42
+     * The ID of the participant associated with the audio element.
37
      */
43
      */
38
-    volume: ?number,
39
-
40
-    /**
41
-     * A function that will be executed when the reference to the underlying audio element changes in order to report
42
-     * the initial volume value.
43
-     */
44
-    onInitialVolumeSet: Function
44
+    participantId: string
45
 };
45
 };
46
 
46
 
47
 /**
47
 /**
48
  * The React/Web {@link Component} which is similar to and wraps around {@code HTMLAudioElement}.
48
  * The React/Web {@link Component} which is similar to and wraps around {@code HTMLAudioElement}.
49
  */
49
  */
50
-export default class AudioTrack extends Component<Props> {
50
+class AudioTrack extends Component<Props> {
51
     /**
51
     /**
52
      * Reference to the HTML audio element, stored until the file is ready.
52
      * Reference to the HTML audio element, stored until the file is ready.
53
      */
53
      */
94
         this._attachTrack(this.props.audioTrack);
94
         this._attachTrack(this.props.audioTrack);
95
 
95
 
96
         if (this._ref) {
96
         if (this._ref) {
97
-            const { muted, volume } = this.props;
97
+            const { _muted, _volume } = this.props;
98
 
98
 
99
-            if (typeof volume === 'number') {
100
-                this._ref.volume = volume;
99
+            if (typeof _volume === 'number') {
100
+                this._ref.volume = _volume;
101
             }
101
             }
102
 
102
 
103
-            if (typeof muted === 'boolean') {
104
-                this._ref.muted = muted;
103
+            if (typeof _muted === 'boolean') {
104
+                this._ref.muted = _muted;
105
             }
105
             }
106
         }
106
         }
107
     }
107
     }
136
 
136
 
137
         if (this._ref) {
137
         if (this._ref) {
138
             const currentVolume = this._ref.volume;
138
             const currentVolume = this._ref.volume;
139
-            const nextVolume = nextProps.volume;
139
+            const nextVolume = nextProps._volume;
140
 
140
 
141
             if (typeof nextVolume === 'number' && !isNaN(nextVolume) && currentVolume !== nextVolume) {
141
             if (typeof nextVolume === 'number' && !isNaN(nextVolume) && currentVolume !== nextVolume) {
142
                 this._ref.volume = nextVolume;
142
                 this._ref.volume = nextVolume;
143
             }
143
             }
144
 
144
 
145
             const currentMuted = this._ref.muted;
145
             const currentMuted = this._ref.muted;
146
-            const nextMuted = nextProps.muted;
146
+            const nextMuted = nextProps._muted;
147
 
147
 
148
             if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
148
             if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
149
                 this._ref.muted = nextMuted;
149
                 this._ref.muted = nextMuted;
258
      */
258
      */
259
     _setRef(audioElement: ?HTMLAudioElement) {
259
     _setRef(audioElement: ?HTMLAudioElement) {
260
         this._ref = audioElement;
260
         this._ref = audioElement;
261
-        const { onInitialVolumeSet } = this.props;
262
-
263
-        if (this._ref && onInitialVolumeSet) {
264
-            onInitialVolumeSet(this._ref.volume);
265
-        }
266
     }
261
     }
267
 }
262
 }
263
+
264
+/**
265
+ * Maps (parts of) the Redux state to the associated {@code AudioTrack}'s props.
266
+ *
267
+ * @param {Object} state - The Redux state.
268
+ * @param {Object} ownProps - The props passed to the component.
269
+ * @private
270
+ * @returns {Props}
271
+ */
272
+function _mapStateToProps(state, ownProps) {
273
+    const { participantsVolume } = state['features/filmstrip'];
274
+
275
+    return {
276
+        _muted: state['features/base/config'].startSilent,
277
+        _volume: participantsVolume[ownProps.participantId]
278
+    };
279
+}
280
+
281
+export default connect(_mapStateToProps)(AudioTrack);

+ 1
- 0
react/features/base/media/components/web/index.js 查看文件

1
 export { default as Audio } from './Audio';
1
 export { default as Audio } from './Audio';
2
+export { default as AudioTrack } from './AudioTrack';
2
 export { default as Video } from './Video';
3
 export { default as Video } from './Video';
3
 export { default as VideoTrack } from './VideoTrack';
4
 export { default as VideoTrack } from './VideoTrack';

+ 22
- 1
react/features/filmstrip/actionTypes.js 查看文件

27
  *         gridDimensions: {
27
  *         gridDimensions: {
28
  *             columns: number,
28
  *             columns: number,
29
  *             height: number,
29
  *             height: number,
30
- *             visibleRows: number,
30
+ *             minVisibleRows: number,
31
  *             width: number
31
  *             width: number
32
  *         },
32
  *         },
33
  *         thumbnailSize: {
33
  *         thumbnailSize: {
49
  * }
49
  * }
50
  */
50
  */
51
 export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS';
51
 export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS';
52
+
53
+/**
54
+ * The type of (redux) action which sets the dimensions of the thumbnails in vertical view.
55
+ *
56
+ * {
57
+ *     type: SET_VERTICAL_VIEW_DIMENSIONS,
58
+ *     dimensions: Object
59
+ * }
60
+ */
61
+export const SET_VERTICAL_VIEW_DIMENSIONS = 'SET_VERTICAL_VIEW_DIMENSIONS';
62
+
63
+/**
64
+ * The type of (redux) action which sets the volume for a thumnail's audio.
65
+ *
66
+ * {
67
+ *     type: SET_VOLUME,
68
+ *     participantId: string,
69
+ *     volume: number
70
+ * }
71
+ */
72
+export const SET_VOLUME = 'SET_VOLUME';

+ 115
- 40
react/features/filmstrip/actions.web.js 查看文件

1
 // @flow
1
 // @flow
2
+import type { Dispatch } from 'redux';
2
 
3
 
3
 import { pinParticipant } from '../base/participants';
4
 import { pinParticipant } from '../base/participants';
4
-import { toState } from '../base/redux';
5
 
5
 
6
-import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
7
-import { calculateThumbnailSizeForHorizontalView, calculateThumbnailSizeForTileView } from './functions';
8
-
9
-/**
10
- * The size of the side margins for the entire tile view area.
11
- */
12
-const TILE_VIEW_SIDE_MARGINS = 20;
6
+import {
7
+    SET_HORIZONTAL_VIEW_DIMENSIONS,
8
+    SET_TILE_VIEW_DIMENSIONS,
9
+    SET_VERTICAL_VIEW_DIMENSIONS,
10
+    SET_VOLUME
11
+} from './actionTypes';
12
+import {
13
+    HORIZONTAL_FILMSTRIP_MARGIN,
14
+    SCROLL_SIZE,
15
+    STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
16
+    STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER,
17
+    TILE_HORIZONTAL_MARGIN,
18
+    TILE_VERTICAL_MARGIN,
19
+    VERTICAL_FILMSTRIP_VERTICAL_MARGIN
20
+} from './constants';
21
+import {
22
+    calculateThumbnailSizeForHorizontalView,
23
+    calculateThumbnailSizeForTileView,
24
+    calculateThumbnailSizeForVerticalView
25
+} from './functions';
13
 
26
 
14
 /**
27
 /**
15
  * Sets the dimensions of the tile view grid.
28
  * Sets the dimensions of the tile view grid.
16
  *
29
  *
17
  * @param {Object} dimensions - Whether the filmstrip is visible.
30
  * @param {Object} dimensions - Whether the filmstrip is visible.
18
- * @param {Object} windowSize - The size of the window.
19
  * @param {Object | Function} stateful - An object or function that can be
31
  * @param {Object | Function} stateful - An object or function that can be
20
  * resolved to Redux state using the {@code toState} function.
32
  * resolved to Redux state using the {@code toState} function.
21
- * @returns {{
22
- *     type: SET_TILE_VIEW_DIMENSIONS,
23
- *     dimensions: Object
24
- * }}
33
+ * @returns {Function}
25
  */
34
  */
26
-export function setTileViewDimensions(dimensions: Object, windowSize: Object, stateful: Object | Function) {
27
-    const state = toState(stateful);
28
-    const { clientWidth, clientHeight } = windowSize;
29
-    const { disableResponsiveTiles } = state['features/base/config'];
35
+export function setTileViewDimensions(dimensions: Object) {
36
+    return (dispatch: Dispatch<any>, getState: Function) => {
37
+        const state = getState();
38
+        const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
39
+        const { disableResponsiveTiles } = state['features/base/config'];
40
+        const {
41
+            height,
42
+            width
43
+        } = calculateThumbnailSizeForTileView({
44
+            ...dimensions,
45
+            clientWidth,
46
+            clientHeight,
47
+            disableResponsiveTiles
48
+        });
49
+        const { columns, rows } = dimensions;
50
+        const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
51
+        const hasScroll = clientHeight < thumbnailsTotalHeight;
52
+        const filmstripWidth = (columns * (TILE_HORIZONTAL_MARGIN + width)) + (hasScroll ? SCROLL_SIZE : 0);
53
+        const filmstripHeight = Math.min(clientHeight, thumbnailsTotalHeight);
30
 
54
 
31
-    const thumbnailSize = calculateThumbnailSizeForTileView({
32
-        ...dimensions,
33
-        clientWidth,
34
-        clientHeight,
35
-        disableResponsiveTiles
36
-    });
37
-    const filmstripWidth = dimensions.columns * (TILE_VIEW_SIDE_MARGINS + thumbnailSize.width);
55
+        dispatch({
56
+            type: SET_TILE_VIEW_DIMENSIONS,
57
+            dimensions: {
58
+                gridDimensions: dimensions,
59
+                thumbnailSize: {
60
+                    height,
61
+                    width
62
+                },
63
+                filmstripHeight,
64
+                filmstripWidth
65
+            }
66
+        });
67
+    };
68
+}
38
 
69
 
39
-    return {
40
-        type: SET_TILE_VIEW_DIMENSIONS,
41
-        dimensions: {
42
-            gridDimensions: dimensions,
43
-            thumbnailSize,
44
-            filmstripWidth
45
-        }
70
+/**
71
+ * Sets the dimensions of the thumbnails in vertical view.
72
+ *
73
+ * @returns {Function}
74
+ */
75
+export function setVerticalViewDimensions() {
76
+    return (dispatch: Dispatch<any>, getState: Function) => {
77
+        const state = getState();
78
+        const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
79
+        const thumbnails = calculateThumbnailSizeForVerticalView(clientWidth);
80
+
81
+        dispatch({
82
+            type: SET_VERTICAL_VIEW_DIMENSIONS,
83
+            dimensions: {
84
+                ...thumbnails,
85
+                remoteVideosContainer: {
86
+                    width: thumbnails?.local?.width
87
+                        + TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER + SCROLL_SIZE,
88
+                    height: clientHeight - thumbnails?.local?.height - VERTICAL_FILMSTRIP_VERTICAL_MARGIN
89
+                }
90
+            }
91
+
92
+        });
46
     };
93
     };
47
 }
94
 }
48
 
95
 
49
 /**
96
 /**
50
  * Sets the dimensions of the thumbnails in horizontal view.
97
  * Sets the dimensions of the thumbnails in horizontal view.
51
  *
98
  *
52
- * @param {number} clientHeight - The height of the window.
53
- * @returns {{
54
- *     type: SET_HORIZONTAL_VIEW_DIMENSIONS,
55
- *     dimensions: Object
56
- * }}
99
+ * @returns {Function}
57
  */
100
  */
58
-export function setHorizontalViewDimensions(clientHeight: number = 0) {
59
-    return {
60
-        type: SET_HORIZONTAL_VIEW_DIMENSIONS,
61
-        dimensions: calculateThumbnailSizeForHorizontalView(clientHeight)
101
+export function setHorizontalViewDimensions() {
102
+    return (dispatch: Dispatch<any>, getState: Function) => {
103
+        const state = getState();
104
+        const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
105
+        const thumbnails = calculateThumbnailSizeForHorizontalView(clientHeight);
106
+
107
+        dispatch({
108
+            type: SET_HORIZONTAL_VIEW_DIMENSIONS,
109
+            dimensions: {
110
+                ...thumbnails,
111
+                remoteVideosContainer: {
112
+                    width: clientWidth - thumbnails?.local?.width - HORIZONTAL_FILMSTRIP_MARGIN,
113
+                    height: thumbnails?.local?.height
114
+                        + TILE_VERTICAL_MARGIN + STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER + SCROLL_SIZE
115
+                }
116
+            }
117
+        });
62
     };
118
     };
63
 }
119
 }
64
 
120
 
78
     };
134
     };
79
 }
135
 }
80
 
136
 
137
+/**
138
+ * Sets the volume for a thumnail's audio.
139
+ *
140
+ * @param {string} participantId - The participant ID asociated with the audio.
141
+ * @param {string} volume - The volume level.
142
+ * @returns {{
143
+ *     type: SET_VOLUME,
144
+ *     participantId: string,
145
+ *     volume: number
146
+ * }}
147
+ */
148
+export function setVolume(participantId: string, volume: number) {
149
+    return {
150
+        type: SET_VOLUME,
151
+        participantId,
152
+        volume
153
+    };
154
+}
155
+
81
 export * from './actions.native';
156
 export * from './actions.native';

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

1
+/* @flow */
2
+import React from 'react';
3
+
4
+import { AudioTrack, MEDIA_TYPE } from '../../../base/media';
5
+import { connect } from '../../../base/redux';
6
+
7
+/**
8
+ * The type of the React {@code Component} props of {@link AudioTracksContainer}.
9
+ */
10
+type Props = {
11
+
12
+    /**
13
+     * All media tracks stored in redux.
14
+     */
15
+    _tracks: Array<Object>
16
+};
17
+
18
+/**
19
+ * A container for the remote tracks audio elements.
20
+ *
21
+ * @param {Props} props - The props of the component.
22
+ * @returns {Array<ReactElement>}
23
+ */
24
+function AudioTracksContainer(props: Props) {
25
+    const { _tracks } = props;
26
+    const remoteAudioTracks = _tracks.filter(t => !t.local && t.mediaType === MEDIA_TYPE.AUDIO);
27
+
28
+    return (
29
+        <div>
30
+            {
31
+                remoteAudioTracks.map(t => {
32
+                    const { jitsiTrack, participantId } = t;
33
+                    const audioTrackId = jitsiTrack && jitsiTrack.getId();
34
+                    const id = `remoteAudio_${audioTrackId || ''}`;
35
+
36
+                    return (
37
+                        <AudioTrack
38
+                            audioTrack = { t }
39
+                            id = { id }
40
+                            key = { id }
41
+                            participantId = { participantId } />);
42
+                })
43
+            }
44
+        </div>);
45
+}
46
+
47
+/**
48
+ * Maps (parts of) the Redux state to the associated {@code AudioTracksContainer}'s props.
49
+ *
50
+ * @param {Object} state - The Redux state.
51
+ * @private
52
+ * @returns {Props}
53
+ */
54
+function _mapStateToProps(state) {
55
+    // NOTE: The disadvantage of this approach is that the component will re-render on any track change.
56
+    // One way to solve the problem would be to pass only the participant ID to the AudioTrack component and
57
+    // find the corresponding track inside the AudioTrack's mapStateToProps. But currently this will be very
58
+    // inefficient because features/base/tracks is an array and in order to find a track by participant ID
59
+    // we need to go trough the array. Introducing a map participantID -> track could be beneficial in this case.
60
+    return {
61
+        _tracks: state['features/base/tracks']
62
+    };
63
+}
64
+
65
+export default connect(_mapStateToProps)(AudioTracksContainer);

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

1
 /* @flow */
1
 /* @flow */
2
 
2
 
3
-import React, { Component } from 'react';
3
+import React, { PureComponent } from 'react';
4
+import { FixedSizeList, FixedSizeGrid } from 'react-window';
4
 import type { Dispatch } from 'redux';
5
 import type { Dispatch } from 'redux';
5
 
6
 
6
 import {
7
 import {
11
 import { getToolbarButtons } from '../../../base/config';
12
 import { getToolbarButtons } from '../../../base/config';
12
 import { translate } from '../../../base/i18n';
13
 import { translate } from '../../../base/i18n';
13
 import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
14
 import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
14
-import { getLocalParticipant } from '../../../base/participants';
15
 import { connect } from '../../../base/redux';
15
 import { connect } from '../../../base/redux';
16
 import { showToolbox } from '../../../toolbox/actions.web';
16
 import { showToolbox } from '../../../toolbox/actions.web';
17
 import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
17
 import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
18
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
18
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
19
 import { setFilmstripVisible } from '../../actions';
19
 import { setFilmstripVisible } from '../../actions';
20
+import { TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN, TOOLBAR_HEIGHT } from '../../constants';
20
 import { shouldRemoteVideosBeVisible } from '../../functions';
21
 import { shouldRemoteVideosBeVisible } from '../../functions';
21
 
22
 
23
+import AudioTracksContainer from './AudioTracksContainer';
22
 import Thumbnail from './Thumbnail';
24
 import Thumbnail from './Thumbnail';
25
+import ThumbnailWrapper from './ThumbnailWrapper';
23
 
26
 
24
 declare var APP: Object;
27
 declare var APP: Object;
25
 declare var interfaceConfig: Object;
28
 declare var interfaceConfig: Object;
50
     _filmstripWidth: number,
53
     _filmstripWidth: number,
51
 
54
 
52
     /**
55
     /**
53
-     * Whether the filmstrip scrollbar should be hidden or not.
56
+     * The height of the filmstrip.
54
      */
57
      */
55
-    _hideScrollbar: boolean,
56
-
57
-    /**
58
-     * Whether the filmstrip toolbar should be hidden or not.
59
-     */
60
-    _hideToolbar: boolean,
58
+    _filmstripHeight: number,
61
 
59
 
62
     /**
60
     /**
63
      * Whether the filmstrip button is enabled.
61
      * Whether the filmstrip button is enabled.
67
     /**
65
     /**
68
      * The participants in the call.
66
      * The participants in the call.
69
      */
67
      */
70
-    _participants: Array<Object>,
68
+    _remoteParticipants: Array<Object>,
69
+
70
+
71
+    /**
72
+     * The length of the remote participants array.
73
+     */
74
+    _remoteParticipantsLength: number,
71
 
75
 
72
     /**
76
     /**
73
      * The number of rows in tile view.
77
      * The number of rows in tile view.
74
      */
78
      */
75
     _rows: number,
79
     _rows: number,
76
 
80
 
81
+    /**
82
+     * The height of the thumbnail.
83
+     */
84
+    _thumbnailHeight: number,
85
+
86
+    /**
87
+     * The width of the thumbnail.
88
+     */
89
+    _thumbnailWidth: number,
90
+
77
     /**
91
     /**
78
      * Additional CSS class names to add to the container of all the thumbnails.
92
      * Additional CSS class names to add to the container of all the thumbnails.
79
      */
93
      */
106
  *
120
  *
107
  * @extends Component
121
  * @extends Component
108
  */
122
  */
109
-class Filmstrip extends Component <Props> {
123
+class Filmstrip extends PureComponent <Props> {
110
 
124
 
111
     /**
125
     /**
112
      * Initializes a new {@code Filmstrip} instance.
126
      * Initializes a new {@code Filmstrip} instance.
121
         this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
135
         this._onShortcutToggleFilmstrip = this._onShortcutToggleFilmstrip.bind(this);
122
         this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
136
         this._onToolbarToggleFilmstrip = this._onToolbarToggleFilmstrip.bind(this);
123
         this._onTabIn = this._onTabIn.bind(this);
137
         this._onTabIn = this._onTabIn.bind(this);
138
+        this._gridItemKey = this._gridItemKey.bind(this);
139
+        this._listItemKey = this._listItemKey.bind(this);
124
     }
140
     }
125
 
141
 
126
     /**
142
     /**
154
      */
170
      */
155
     render() {
171
     render() {
156
         const filmstripStyle = { };
172
         const filmstripStyle = { };
157
-        const filmstripRemoteVideosContainerStyle = {};
158
-        let remoteVideoContainerClassName = 'remote-videos-container';
159
-        const { _currentLayout, _participants } = this.props;
160
-        const remoteParticipants = _participants.filter(p => !p.local);
161
-        const localParticipant = getLocalParticipant(_participants);
173
+        const { _currentLayout } = this.props;
162
         const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
174
         const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
163
 
175
 
164
         switch (_currentLayout) {
176
         switch (_currentLayout) {
167
             // Also adding 7px for the scrollbar.
179
             // Also adding 7px for the scrollbar.
168
             filmstripStyle.maxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120) + 25;
180
             filmstripStyle.maxWidth = (interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120) + 25;
169
             break;
181
             break;
170
-        case LAYOUTS.TILE_VIEW: {
171
-            // The size of the side margins for each tile as set in CSS.
172
-            const { _columns, _rows, _filmstripWidth } = this.props;
173
-
174
-            if (_rows > _columns) {
175
-                remoteVideoContainerClassName += ' has-overflow';
176
-            }
177
-
178
-            filmstripRemoteVideosContainerStyle.width = _filmstripWidth;
179
-            break;
180
-        }
181
-        }
182
-
183
-        let remoteVideosWrapperClassName = 'filmstrip__videos';
184
-
185
-        if (this.props._hideScrollbar) {
186
-            remoteVideosWrapperClassName += ' hide-scrollbar';
187
         }
182
         }
188
 
183
 
189
         let toolbar = null;
184
         let toolbar = null;
190
 
185
 
191
-        if (!this.props._hideToolbar && this.props._isFilmstripButtonEnabled) {
186
+        if (this.props._isFilmstripButtonEnabled) {
192
             toolbar = this._renderToggleButton();
187
             toolbar = this._renderToggleButton();
193
         }
188
         }
194
 
189
 
206
                         <div id = 'filmstripLocalVideoThumbnail'>
201
                         <div id = 'filmstripLocalVideoThumbnail'>
207
                             {
202
                             {
208
                                 !tileViewActive && <Thumbnail
203
                                 !tileViewActive && <Thumbnail
209
-                                    key = 'local'
210
-                                    participantID = { localParticipant.id } />
204
+                                    key = 'local' />
211
                             }
205
                             }
212
                         </div>
206
                         </div>
213
                     </div>
207
                     </div>
214
-                    <div
215
-                        className = { remoteVideosWrapperClassName }
216
-                        id = 'filmstripRemoteVideos'>
217
-                        {/*
218
-                          * XXX This extra video container is needed for
219
-                          * scrolling thumbnails in Firefox; otherwise, the flex
220
-                          * thumbnails resize instead of causing overflow.
221
-                          */}
222
-                        <div
223
-                            className = { remoteVideoContainerClassName }
224
-                            id = 'filmstripRemoteVideosContainer'
225
-                            style = { filmstripRemoteVideosContainerStyle }>
226
-                            {
227
-                                remoteParticipants.map(
228
-                                    p => (
229
-                                        <Thumbnail
230
-                                            key = { `remote_${p.id}` }
231
-                                            participantID = { p.id } />
232
-                                    ))
233
-                            }
234
-                            <div id = 'localVideoTileViewContainer'>
235
-                                {
236
-                                    tileViewActive && <Thumbnail
237
-                                        key = 'local'
238
-                                        participantID = { localParticipant.id } />
239
-                                }
240
-                            </div>
241
-                        </div>
242
-                    </div>
208
+                    {
209
+                        this._renderRemoteParticipants()
210
+                    }
243
                 </div>
211
                 </div>
212
+                <AudioTracksContainer />
244
             </div>
213
             </div>
245
         );
214
         );
246
     }
215
     }
258
         }
227
         }
259
     }
228
     }
260
 
229
 
230
+    _listItemKey: number => string;
231
+
232
+    /**
233
+     * The key to be used for every ThumbnailWrapper element in stage view.
234
+     *
235
+     * @param {number} index - The index of the ThumbnailWrapper instance.
236
+     * @returns {string} - The key.
237
+     */
238
+    _listItemKey(index) {
239
+        const { _remoteParticipants, _remoteParticipantsLength } = this.props;
240
+
241
+        if (typeof index !== 'number' || _remoteParticipantsLength <= index) {
242
+            return `empty-${index}`;
243
+        }
244
+
245
+        return _remoteParticipants[_remoteParticipantsLength - index - 1];
246
+    }
247
+
248
+    _gridItemKey: Object => string;
249
+
250
+    /**
251
+     * The key to be used for every ThumbnailWrapper element in tile views.
252
+     *
253
+     * @param {Object} data - An object with the indexes identifying the ThumbnailWrapper instance.
254
+     * @returns {string} - The key.
255
+     */
256
+    _gridItemKey({ columnIndex, rowIndex }) {
257
+        const { _columns, _remoteParticipants, _remoteParticipantsLength } = this.props;
258
+        const index = (rowIndex * _columns) + columnIndex;
259
+
260
+        if (index > _remoteParticipantsLength) {
261
+            return `empty-${index}`;
262
+        }
263
+
264
+        if (index === _remoteParticipantsLength) {
265
+            return 'local';
266
+        }
267
+
268
+        return _remoteParticipants[index];
269
+    }
270
+
271
+    /**
272
+     * Renders the thumbnails for remote participants.
273
+     *
274
+     * @returns {ReactElement}
275
+     */
276
+    _renderRemoteParticipants() {
277
+        const {
278
+            _columns,
279
+            _currentLayout,
280
+            _filmstripHeight,
281
+            _filmstripWidth,
282
+            _remoteParticipantsLength,
283
+            _rows,
284
+            _thumbnailHeight,
285
+            _thumbnailWidth
286
+        } = this.props;
287
+
288
+        if (!_thumbnailWidth || isNaN(_thumbnailWidth) || !_thumbnailHeight
289
+            || isNaN(_thumbnailHeight) || !_filmstripHeight || isNaN(_filmstripHeight) || !_filmstripWidth
290
+            || isNaN(_filmstripWidth)) {
291
+            return null;
292
+        }
293
+
294
+        if (_currentLayout === LAYOUTS.TILE_VIEW) {
295
+            return (
296
+                <FixedSizeGrid
297
+                    className = 'filmstrip__videos remote-videos'
298
+                    columnCount = { _columns }
299
+                    columnWidth = { _thumbnailWidth + TILE_HORIZONTAL_MARGIN }
300
+                    height = { _filmstripHeight }
301
+                    initialScrollLeft = { 0 }
302
+                    initialScrollTop = { 0 }
303
+                    itemKey = { this._gridItemKey }
304
+                    rowCount = { _rows }
305
+                    rowHeight = { _thumbnailHeight + TILE_VERTICAL_MARGIN }
306
+                    width = { _filmstripWidth }>
307
+                    {
308
+                        ThumbnailWrapper
309
+                    }
310
+                </FixedSizeGrid>
311
+            );
312
+        }
313
+
314
+
315
+        const props = {
316
+            itemCount: _remoteParticipantsLength,
317
+            className: 'filmstrip__videos remote-videos',
318
+            height: _filmstripHeight,
319
+            itemKey: this._listItemKey,
320
+            itemSize: 0,
321
+            width: _filmstripWidth,
322
+            style: {
323
+                willChange: 'auto'
324
+            }
325
+        };
326
+
327
+        if (_currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
328
+            const itemSize = _thumbnailWidth + TILE_HORIZONTAL_MARGIN;
329
+            const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripWidth;
330
+
331
+            props.itemSize = itemSize;
332
+
333
+            // $FlowFixMe
334
+            props.layout = 'horizontal';
335
+            if (isNotOverflowing) {
336
+                props.className += ' is-not-overflowing';
337
+            }
338
+
339
+        } else if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
340
+            const itemSize = _thumbnailHeight + TILE_VERTICAL_MARGIN;
341
+            const isNotOverflowing = (_remoteParticipantsLength * itemSize) <= _filmstripHeight;
342
+
343
+            if (isNotOverflowing) {
344
+                props.className += ' is-not-overflowing';
345
+            }
346
+
347
+            props.itemSize = itemSize;
348
+        }
349
+
350
+        return (
351
+            <FixedSizeList { ...props }>
352
+                {
353
+                    ThumbnailWrapper
354
+                }
355
+            </FixedSizeList>
356
+        );
357
+    }
358
+
261
     /**
359
     /**
262
      * Dispatches an action to change the visibility of the filmstrip.
360
      * Dispatches an action to change the visibility of the filmstrip.
263
      *
361
      *
344
  * @returns {Props}
442
  * @returns {Props}
345
  */
443
  */
346
 function _mapStateToProps(state) {
444
 function _mapStateToProps(state) {
347
-    const { iAmSipGateway } = state['features/base/config'];
348
     const toolbarButtons = getToolbarButtons(state);
445
     const toolbarButtons = getToolbarButtons(state);
349
-    const { visible } = state['features/filmstrip'];
350
-    const reduceHeight
351
-        = state['features/toolbox'].visible && toolbarButtons.length;
446
+    const { visible, remoteParticipants } = state['features/filmstrip'];
447
+    const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
352
     const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
448
     const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
353
     const { isOpen: shiftRight } = state['features/chat'];
449
     const { isOpen: shiftRight } = state['features/chat'];
354
     const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
450
     const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
355
         reduceHeight ? 'reduce-height' : ''
451
         reduceHeight ? 'reduce-height' : ''
356
     } ${shiftRight ? 'shift-right' : ''}`.trim();
452
     } ${shiftRight ? 'shift-right' : ''}`.trim();
357
     const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
453
     const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
358
-    const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
454
+    const {
455
+        gridDimensions = {},
456
+        filmstripHeight,
457
+        filmstripWidth,
458
+        thumbnailSize: tileViewThumbnailSize
459
+    } = state['features/filmstrip'].tileViewDimensions;
460
+    const _currentLayout = getCurrentLayout(state);
461
+    let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
462
+
463
+    switch (_currentLayout) {
464
+    case LAYOUTS.TILE_VIEW:
465
+        _thumbnailSize = tileViewThumbnailSize;
466
+        remoteFilmstripHeight = filmstripHeight;
467
+        remoteFilmstripWidth = filmstripWidth;
468
+        break;
469
+    case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
470
+        const { remote, remoteVideosContainer } = state['features/filmstrip'].verticalViewDimensions;
471
+
472
+        _thumbnailSize = remote;
473
+        remoteFilmstripHeight = remoteVideosContainer?.height - (reduceHeight ? TOOLBAR_HEIGHT : 0);
474
+        remoteFilmstripWidth = remoteVideosContainer?.width;
475
+        break;
476
+    }
477
+    case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
478
+        const { remote, remoteVideosContainer } = state['features/filmstrip'].horizontalViewDimensions;
479
+
480
+        _thumbnailSize = remote;
481
+        remoteFilmstripHeight = remoteVideosContainer?.height;
482
+        remoteFilmstripWidth = remoteVideosContainer?.width;
483
+        break;
484
+    }
485
+    }
359
 
486
 
360
     return {
487
     return {
361
         _className: className,
488
         _className: className,
362
         _columns: gridDimensions.columns,
489
         _columns: gridDimensions.columns,
363
-        _currentLayout: getCurrentLayout(state),
364
-        _filmstripWidth: filmstripWidth,
365
-        _hideScrollbar: Boolean(iAmSipGateway),
366
-        _hideToolbar: Boolean(iAmSipGateway),
490
+        _currentLayout,
491
+        _filmstripHeight: remoteFilmstripHeight,
492
+        _filmstripWidth: remoteFilmstripWidth,
367
         _isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
493
         _isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
368
-        _participants: state['features/base/participants'],
494
+        _remoteParticipantsLength: remoteParticipants.length,
495
+        _remoteParticipants: remoteParticipants,
369
         _rows: gridDimensions.rows,
496
         _rows: gridDimensions.rows,
497
+        _thumbnailWidth: _thumbnailSize?.width,
498
+        _thumbnailHeight: _thumbnailSize?.height,
370
         _videosClassName: videosClassName,
499
         _videosClassName: videosClassName,
371
         _visible: visible,
500
         _visible: visible,
372
         _isToolboxVisible: isToolboxVisible(state)
501
         _isToolboxVisible: isToolboxVisible(state)

+ 62
- 84
react/features/filmstrip/components/web/Thumbnail.js 查看文件

7
 import { Avatar } from '../../../base/avatar';
7
 import { Avatar } from '../../../base/avatar';
8
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
8
 import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
9
 import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
9
 import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
10
-import AudioTrack from '../../../base/media/components/web/AudioTrack';
11
 import {
10
 import {
12
     getLocalParticipant,
11
     getLocalParticipant,
13
     getParticipantById,
12
     getParticipantById,
28
 import { PresenceLabel } from '../../../presence-status';
27
 import { PresenceLabel } from '../../../presence-status';
29
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
28
 import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
30
 import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
29
 import { LocalVideoMenuTriggerButton, RemoteVideoMenuTriggerButton } from '../../../video-menu';
30
+import { setVolume } from '../../actions.web';
31
 import {
31
 import {
32
     DISPLAY_MODE_TO_CLASS_NAME,
32
     DISPLAY_MODE_TO_CLASS_NAME,
33
     DISPLAY_MODE_TO_STRING,
33
     DISPLAY_MODE_TO_STRING,
65
     /**
65
     /**
66
      * Indicates whether the thumbnail is hovered or not.
66
      * Indicates whether the thumbnail is hovered or not.
67
      */
67
      */
68
-    isHovered: boolean,
69
-
70
-    /**
71
-     * The current volume setting for the Thumbnail.
72
-     */
73
-    volume: ?number
68
+    isHovered: boolean
74
 |};
69
 |};
75
 
70
 
76
 /**
71
 /**
179
     _participant: Object,
174
     _participant: Object,
180
 
175
 
181
     /**
176
     /**
182
-     * The number of participants in the call.
177
+     * True if there are more than 2 participants in the call.
183
      */
178
      */
184
-    _participantCount: number,
179
+     _participantCountMoreThan2: boolean,
185
 
180
 
186
     /**
181
     /**
187
      * Indicates whether the "start silent" mode is enabled.
182
      * Indicates whether the "start silent" mode is enabled.
193
      */
188
      */
194
     _videoTrack: ?Object,
189
     _videoTrack: ?Object,
195
 
190
 
191
+    /**
192
+     * The volume level for the thumbnail.
193
+     */
194
+    _volume?: ?number,
195
+
196
     /**
196
     /**
197
      * The width of the thumbnail.
197
      * The width of the thumbnail.
198
      */
198
      */
203
      */
203
      */
204
     dispatch: Function,
204
     dispatch: Function,
205
 
205
 
206
+    /**
207
+     * The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
208
+     */
209
+    horizontalOffset: number,
210
+
206
     /**
211
     /**
207
      * The ID of the participant related to the thumbnail.
212
      * The ID of the participant related to the thumbnail.
208
      */
213
      */
209
-    participantID: ?string
214
+    participantID: ?string,
215
+
216
+    /**
217
+     * Styles that will be set to the Thumbnail's main span element.
218
+     */
219
+    style?: ?Object
210
 |};
220
 |};
211
 
221
 
212
 /**
222
 /**
240
             audioLevel: 0,
250
             audioLevel: 0,
241
             canPlayEventReceived: false,
251
             canPlayEventReceived: false,
242
             isHovered: false,
252
             isHovered: false,
243
-            volume: undefined,
244
             displayMode: DISPLAY_VIDEO
253
             displayMode: DISPLAY_VIDEO
245
         };
254
         };
246
 
255
 
253
         this._onCanPlay = this._onCanPlay.bind(this);
262
         this._onCanPlay = this._onCanPlay.bind(this);
254
         this._onClick = this._onClick.bind(this);
263
         this._onClick = this._onClick.bind(this);
255
         this._onVolumeChange = this._onVolumeChange.bind(this);
264
         this._onVolumeChange = this._onVolumeChange.bind(this);
256
-        this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
257
         this._onMouseEnter = this._onMouseEnter.bind(this);
265
         this._onMouseEnter = this._onMouseEnter.bind(this);
258
         this._onMouseLeave = this._onMouseLeave.bind(this);
266
         this._onMouseLeave = this._onMouseLeave.bind(this);
259
         this._onTestingEvent = this._onTestingEvent.bind(this);
267
         this._onTestingEvent = this._onTestingEvent.bind(this);
457
      * @returns {Object} - The styles for the thumbnail.
465
      * @returns {Object} - The styles for the thumbnail.
458
      */
466
      */
459
     _getStyles(): Object {
467
     _getStyles(): Object {
460
-        const { _height, _heightToWidthPercent, _currentLayout, _isHidden, _width } = this.props;
468
+        const { _height, _isHidden, _width, style, horizontalOffset } = this.props;
461
         let styles: {
469
         let styles: {
462
             thumbnail: Object,
470
             thumbnail: Object,
463
             avatar: Object
471
             avatar: Object
466
             avatar: {}
474
             avatar: {}
467
         };
475
         };
468
 
476
 
469
-        switch (_currentLayout) {
470
-        case LAYOUTS.TILE_VIEW:
471
-        case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
472
-            const avatarSize = _height / 2;
473
-
474
-            styles = {
475
-                thumbnail: {
476
-                    height: `${_height}px`,
477
-                    minHeight: `${_height}px`,
478
-                    minWidth: `${_width}px`,
479
-                    width: `${_width}px`
480
-                },
481
-                avatar: {
482
-                    height: `${avatarSize}px`,
483
-                    width: `${avatarSize}px`
484
-                }
485
-            };
486
-            break;
487
-        }
488
-        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
489
-            styles = {
490
-                thumbnail: {
491
-                    paddingTop: `${_heightToWidthPercent}%`
492
-                },
493
-                avatar: {
494
-                    height: '50%',
495
-                    width: `${_heightToWidthPercent / 2}%`
496
-                }
497
-            };
498
-            break;
499
-        }
477
+        const avatarSize = _height / 2;
478
+        let { left } = style || {};
479
+
480
+        if (typeof left === 'number' && horizontalOffset) {
481
+            left += horizontalOffset;
500
         }
482
         }
501
 
483
 
484
+        styles = {
485
+            thumbnail: {
486
+                ...style,
487
+                left,
488
+                height: `${_height}px`,
489
+                minHeight: `${_height}px`,
490
+                minWidth: `${_width}px`,
491
+                width: `${_width}px`
492
+            },
493
+            avatar: {
494
+                height: `${avatarSize}px`,
495
+                width: `${avatarSize}px`
496
+            }
497
+        };
498
+
502
         if (_isHidden) {
499
         if (_isHidden) {
503
             styles.thumbnail.display = 'none';
500
             styles.thumbnail.display = 'none';
504
         }
501
         }
584
             _isDominantSpeakerDisabled,
581
             _isDominantSpeakerDisabled,
585
             _indicatorIconSize: iconSize,
582
             _indicatorIconSize: iconSize,
586
             _participant,
583
             _participant,
587
-            _participantCount
584
+            _participantCountMoreThan2
588
         } = this.props;
585
         } = this.props;
589
         const { isHovered } = this.state;
586
         const { isHovered } = this.state;
590
         const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
587
         const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
621
                     iconSize = { iconSize }
618
                     iconSize = { iconSize }
622
                     participantId = { id }
619
                     participantId = { id }
623
                     tooltipPosition = { tooltipPosition } />
620
                     tooltipPosition = { tooltipPosition } />
624
-                { showDominantSpeaker && _participantCount > 2
621
+                { showDominantSpeaker && _participantCountMoreThan2
625
                     && <DominantSpeakerIndicator
622
                     && <DominantSpeakerIndicator
626
                         iconSize = { iconSize }
623
                         iconSize = { iconSize }
627
                         tooltipPosition = { tooltipPosition } />
624
                         tooltipPosition = { tooltipPosition } />
793
      */
790
      */
794
     _renderRemoteParticipant() {
791
     _renderRemoteParticipant() {
795
         const {
792
         const {
796
-            _audioTrack,
797
             _isTestModeEnabled,
793
             _isTestModeEnabled,
798
             _participant,
794
             _participant,
799
             _startSilent,
795
             _startSilent,
800
-            _videoTrack
796
+            _videoTrack,
797
+            _volume = 1
801
         } = this.props;
798
         } = this.props;
802
         const { id } = _participant;
799
         const { id } = _participant;
803
-        const { audioLevel, canPlayEventReceived, volume } = this.state;
800
+        const { audioLevel, canPlayEventReceived } = this.state;
804
         const styles = this._getStyles();
801
         const styles = this._getStyles();
805
         const containerClassName = this._getContainerClassName();
802
         const containerClassName = this._getContainerClassName();
806
 
803
 
807
         // hide volume when in silent mode
804
         // hide volume when in silent mode
808
         const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
805
         const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
809
-        const jitsiAudioTrack = _audioTrack?.jitsiTrack;
810
-        const audioTrackId = jitsiAudioTrack && jitsiAudioTrack.getId();
811
         const jitsiVideoTrack = _videoTrack?.jitsiTrack;
806
         const jitsiVideoTrack = _videoTrack?.jitsiTrack;
812
         const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
807
         const videoTrackId = jitsiVideoTrack && jitsiVideoTrack.getId();
813
         const videoEventListeners = {};
808
         const videoEventListeners = {};
840
                         style = { videoElementStyle }
835
                         style = { videoElementStyle }
841
                         videoTrack = { _videoTrack } />
836
                         videoTrack = { _videoTrack } />
842
                 }
837
                 }
843
-                {
844
-                    _audioTrack && <AudioTrack
845
-                        audioTrack = { _audioTrack }
846
-                        id = { `remoteAudio_${audioTrackId || ''}` }
847
-                        muted = { _startSilent }
848
-                        onInitialVolumeSet = { this._onInitialVolumeSet }
849
-                        volume = { volume } />
850
-                }
851
                 <div className = 'videocontainer__background' />
838
                 <div className = 'videocontainer__background' />
852
                 <div className = 'videocontainer__toptoolbar'>
839
                 <div className = 'videocontainer__toptoolbar'>
853
                     { this._renderTopIndicators() }
840
                     { this._renderTopIndicators() }
872
                 </span>
859
                 </span>
873
                 <span className = 'remotevideomenu'>
860
                 <span className = 'remotevideomenu'>
874
                     <RemoteVideoMenuTriggerButton
861
                     <RemoteVideoMenuTriggerButton
875
-                        initialVolumeValue = { volume }
862
+                        initialVolumeValue = { _volume }
876
                         onVolumeChange = { onVolumeChange }
863
                         onVolumeChange = { onVolumeChange }
877
                         participantID = { id } />
864
                         participantID = { id } />
878
                 </span>
865
                 </span>
880
         );
867
         );
881
     }
868
     }
882
 
869
 
883
-    _onInitialVolumeSet: Object => void;
884
-
885
-    /**
886
-     * A handler for the initial volume value of the audio element.
887
-     *
888
-     * @param {number} volume - Properties of the audio element.
889
-     * @returns {void}
890
-     */
891
-    _onInitialVolumeSet(volume) {
892
-        if (this.state.volume !== volume) {
893
-            this.setState({ volume });
894
-        }
895
-    }
896
-
897
     _onVolumeChange: number => void;
870
     _onVolumeChange: number => void;
898
 
871
 
899
     /**
872
     /**
903
      * @returns {void}
876
      * @returns {void}
904
      */
877
      */
905
     _onVolumeChange(value) {
878
     _onVolumeChange(value) {
906
-        this.setState({ volume: value });
879
+        const { _participant, dispatch } = this.props;
880
+        const { id } = _participant;
881
+
882
+        dispatch(setVolume(id, value));
907
     }
883
     }
908
 
884
 
909
     /**
885
     /**
949
     const { id } = participant;
925
     const { id } = participant;
950
     const isLocal = participant?.local ?? true;
926
     const isLocal = participant?.local ?? true;
951
     const tracks = state['features/base/tracks'];
927
     const tracks = state['features/base/tracks'];
928
+    const { participantsVolume } = state['features/filmstrip'];
952
     const _videoTrack = isLocal
929
     const _videoTrack = isLocal
953
         ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
930
         ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
954
     const _audioTrack = isLocal
931
     const _audioTrack = isLocal
967
 
944
 
968
 
945
 
969
     switch (_currentLayout) {
946
     switch (_currentLayout) {
947
+    case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
970
     case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
948
     case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
971
         const {
949
         const {
972
             horizontalViewDimensions = {
950
             horizontalViewDimensions = {
973
                 local: {},
951
                 local: {},
974
                 remote: {}
952
                 remote: {}
953
+            },
954
+            verticalViewDimensions = {
955
+                local: {},
956
+                remote: {}
975
             }
957
             }
976
         } = state['features/filmstrip'];
958
         } = state['features/filmstrip'];
977
-        const { local, remote } = horizontalViewDimensions;
959
+        const { local, remote }
960
+            = _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
961
+                ? verticalViewDimensions : horizontalViewDimensions;
978
         const { width, height } = isLocal ? local : remote;
962
         const { width, height } = isLocal ? local : remote;
979
 
963
 
980
         size = {
964
         size = {
984
 
968
 
985
         break;
969
         break;
986
     }
970
     }
987
-    case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
988
-        size = {
989
-            _heightToWidthPercent: isLocal
990
-                ? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
991
-                : 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
992
-        };
993
-        break;
994
     case LAYOUTS.TILE_VIEW: {
971
     case LAYOUTS.TILE_VIEW: {
995
         const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
972
         const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
996
 
973
 
1020
         _indicatorIconSize: NORMAL,
997
         _indicatorIconSize: NORMAL,
1021
         _localFlipX: Boolean(localFlipX),
998
         _localFlipX: Boolean(localFlipX),
1022
         _participant: participant,
999
         _participant: participant,
1023
-        _participantCount: getParticipantCount(state),
1000
+        _participantCountMoreThan2: getParticipantCount(state) > 2,
1024
         _startSilent: Boolean(startSilent),
1001
         _startSilent: Boolean(startSilent),
1025
         _videoTrack,
1002
         _videoTrack,
1003
+        _volume: isLocal ? undefined : participantsVolume[id],
1026
         ...size
1004
         ...size
1027
     };
1005
     };
1028
 }
1006
 }

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

1
+/* @flow */
2
+import React, { Component } from 'react';
3
+import { shouldComponentUpdate } from 'react-window';
4
+
5
+import { connect } from '../../../base/redux';
6
+import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
7
+
8
+import Thumbnail from './Thumbnail';
9
+
10
+/**
11
+ * The type of the React {@code Component} props of {@link ThumbnailWrapper}.
12
+ */
13
+type Props = {
14
+
15
+    /**
16
+     * The ID of the participant associated with the Thumbnail.
17
+     */
18
+    _participantID: ?string,
19
+
20
+    /**
21
+     * The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
22
+     */
23
+    _horizontalOffset: number,
24
+
25
+    /**
26
+     * The index of the column in tile view.
27
+     */
28
+    columnIndex?: number,
29
+
30
+    /**
31
+     * The index of the ThumbnailWrapper in stage view.
32
+     */
33
+    index?: number,
34
+
35
+    /**
36
+     * The index of the row in tile view.
37
+     */
38
+    rowIndex?: number,
39
+
40
+    /**
41
+     * The styles comming from react-window.
42
+     */
43
+    style: Object
44
+};
45
+
46
+/**
47
+ * A wrapper Component for the Thumbnail that translates the react-window specific props
48
+ * to the Thumbnail Component's props.
49
+ */
50
+class ThumbnailWrapper extends Component<Props> {
51
+
52
+    /**
53
+     * Creates new ThumbnailWrapper instance.
54
+     *
55
+     * @param {Props} props - The props of the component.
56
+     */
57
+    constructor(props: Props) {
58
+        super(props);
59
+
60
+        this.shouldComponentUpdate = shouldComponentUpdate.bind(this);
61
+    }
62
+
63
+    shouldComponentUpdate: Props => boolean;
64
+
65
+    /**
66
+     * Implements React's {@link Component#render()}.
67
+     *
68
+     * @inheritdoc
69
+     * @returns {ReactElement}
70
+     */
71
+    render() {
72
+        const { _participantID, style, _horizontalOffset = 0 } = this.props;
73
+
74
+        if (typeof _participantID !== 'string') {
75
+            return null;
76
+        }
77
+
78
+        if (_participantID === 'local') {
79
+            return (
80
+                <Thumbnail
81
+                    horizontalOffset = { _horizontalOffset }
82
+                    key = 'local'
83
+                    style = { style } />);
84
+        }
85
+
86
+        return (
87
+            <Thumbnail
88
+                horizontalOffset = { _horizontalOffset }
89
+                key = { `remote_${_participantID}` }
90
+                participantID = { _participantID }
91
+                style = { style } />);
92
+    }
93
+}
94
+
95
+/**
96
+ * Maps (parts of) the Redux state to the associated {@code ThumbnailWrapper}'s props.
97
+ *
98
+ * @param {Object} state - The Redux state.
99
+ * @param {Object} ownProps - The props passed to the component.
100
+ * @private
101
+ * @returns {Props}
102
+ */
103
+function _mapStateToProps(state, ownProps) {
104
+    const _currentLayout = getCurrentLayout(state);
105
+    const { remoteParticipants } = state['features/filmstrip'];
106
+    const remoteParticipantsLength = remoteParticipants.length;
107
+
108
+    if (_currentLayout === LAYOUTS.TILE_VIEW) {
109
+        const { columnIndex, rowIndex } = ownProps;
110
+        const { gridDimensions = {}, thumbnailSize } = state['features/filmstrip'].tileViewDimensions;
111
+        const { columns, rows } = gridDimensions;
112
+        const index = (rowIndex * columns) + columnIndex;
113
+        let horizontalOffset;
114
+
115
+        if (rowIndex === rows - 1) { // center the last row
116
+            const { width: thumbnailWidth } = thumbnailSize;
117
+            const participantsInTheLastRow = (remoteParticipantsLength + 1) % columns;
118
+
119
+            if (participantsInTheLastRow > 0) {
120
+                horizontalOffset = Math.floor((columns - participantsInTheLastRow) * (thumbnailWidth + 4) / 2);
121
+            }
122
+
123
+        }
124
+
125
+        if (index > remoteParticipantsLength) {
126
+            return {};
127
+        }
128
+
129
+        if (index === remoteParticipantsLength) {
130
+            return {
131
+                _participantID: 'local',
132
+                _horizontalOffset: horizontalOffset
133
+            };
134
+        }
135
+
136
+
137
+        return {
138
+            _participantID: remoteParticipants[index],
139
+            _horizontalOffset: horizontalOffset
140
+        };
141
+
142
+    }
143
+
144
+    const { index } = ownProps;
145
+
146
+    if (typeof index !== 'number' || remoteParticipantsLength <= index) {
147
+        return {};
148
+    }
149
+
150
+    return {
151
+        _participantID: remoteParticipants[index]
152
+    };
153
+}
154
+
155
+export default connect(_mapStateToProps)(ThumbnailWrapper);

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

143
     'video-with-name',
143
     'video-with-name',
144
     'avatar-with-name'
144
     'avatar-with-name'
145
 ];
145
 ];
146
+
147
+/**
148
+ * The vertical margin of a tile.
149
+ *
150
+ * @type {number}
151
+ */
152
+export const TILE_VERTICAL_MARGIN = 4;
153
+
154
+/**
155
+ * The horizontal margin of a tile.
156
+ *
157
+ * @type {number}
158
+ */
159
+export const TILE_HORIZONTAL_MARGIN = 4;
160
+
161
+/**
162
+ * The height of the whole toolbar.
163
+ */
164
+export const TOOLBAR_HEIGHT = 72;
165
+
166
+/**
167
+ * The size of the horizontal border of a thumbnail.
168
+ *
169
+ * @type {number}
170
+ */
171
+export const STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER = 4;
172
+
173
+/**
174
+ * The size of the vertical border of a thumbnail.
175
+ *
176
+ * @type {number}
177
+ */
178
+export const STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER = 4;
179
+
180
+/**
181
+ * The size of the scroll.
182
+ *
183
+ * @type {number}
184
+ */
185
+export const SCROLL_SIZE = 7;
186
+
187
+/**
188
+ * The total vertical space between the thumbnails container and the edges of the window.
189
+ *
190
+ * NOTE: This will include margins, paddings and the space for the 'hide filmstrip' icon.
191
+ *
192
+ * @type {number}
193
+ */
194
+export const VERTICAL_FILMSTRIP_VERTICAL_MARGIN = 60;
195
+
196
+/**
197
+ * The min horizontal space between the thumbnails container and the edges of the window.
198
+ *
199
+ * @type {number}
200
+ */
201
+export const VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN = 10;
202
+
203
+/**
204
+ * The total horizontal space between the thumbnails container and the edges of the window.
205
+ *
206
+ * NOTE: This will include margins, paddings and the space for the 'hide filmstrip' icon.
207
+ *
208
+ * @type {number}
209
+ */
210
+export const HORIZONTAL_FILMSTRIP_MARGIN = 39;

+ 56
- 11
react/features/filmstrip/functions.web.js 查看文件

23
     DISPLAY_BLACKNESS_WITH_NAME,
23
     DISPLAY_BLACKNESS_WITH_NAME,
24
     DISPLAY_VIDEO,
24
     DISPLAY_VIDEO,
25
     DISPLAY_VIDEO_WITH_NAME,
25
     DISPLAY_VIDEO_WITH_NAME,
26
+    SCROLL_SIZE,
26
     SQUARE_TILE_ASPECT_RATIO,
27
     SQUARE_TILE_ASPECT_RATIO,
27
-    TILE_ASPECT_RATIO
28
+    STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER,
29
+    TILE_ASPECT_RATIO,
30
+    TILE_HORIZONTAL_MARGIN,
31
+    TILE_VERTICAL_MARGIN,
32
+    VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
28
 } from './constants';
33
 } from './constants';
29
 
34
 
30
 declare var interfaceConfig: Object;
35
 declare var interfaceConfig: Object;
31
 
36
 
32
-// Minimum space to keep between the sides of the tiles and the sides
33
-// of the window.
34
-const TILE_VIEW_SIDE_MARGINS = 20;
35
-
36
 /**
37
 /**
37
  * Returns true if the filmstrip on mobile is visible, false otherwise.
38
  * Returns true if the filmstrip on mobile is visible, false otherwise.
38
  *
39
  *
139
     };
140
     };
140
 }
141
 }
141
 
142
 
143
+/**
144
+ * Calculates the size for thumbnails when in vertical view layout.
145
+ *
146
+ * @param {number} clientWidth - The height of the app window.
147
+ * @returns {{local: {height, width}, remote: {height, width}}}
148
+ */
149
+export function calculateThumbnailSizeForVerticalView(clientWidth: number = 0) {
150
+    const horizontalMargin
151
+        = VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN + SCROLL_SIZE
152
+            + TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER;
153
+    const availableWidth = Math.min(
154
+        Math.max(clientWidth - horizontalMargin, 0),
155
+        interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120);
156
+
157
+    return {
158
+        local: {
159
+            height: Math.floor(availableWidth / interfaceConfig.LOCAL_THUMBNAIL_RATIO),
160
+            width: availableWidth
161
+        },
162
+        remote: {
163
+            height: Math.floor(availableWidth / interfaceConfig.REMOTE_THUMBNAIL_RATIO),
164
+            width: availableWidth
165
+        }
166
+    };
167
+}
168
+
142
 /**
169
 /**
143
  * Calculates the size for thumbnails when in tile view layout.
170
  * Calculates the size for thumbnails when in tile view layout.
144
  *
171
  *
145
  * @param {Object} dimensions - The desired dimensions of the tile view grid.
172
  * @param {Object} dimensions - The desired dimensions of the tile view grid.
146
- * @returns {{height, width}}
173
+ * @returns {{hasScroll, height, width}}
147
  */
174
  */
148
 export function calculateThumbnailSizeForTileView({
175
 export function calculateThumbnailSizeForTileView({
149
     columns,
176
     columns,
150
-    visibleRows,
177
+    minVisibleRows,
178
+    rows,
151
     clientWidth,
179
     clientWidth,
152
     clientHeight,
180
     clientHeight,
153
     disableResponsiveTiles
181
     disableResponsiveTiles
158
         aspectRatio = SQUARE_TILE_ASPECT_RATIO;
186
         aspectRatio = SQUARE_TILE_ASPECT_RATIO;
159
     }
187
     }
160
 
188
 
161
-    const viewWidth = clientWidth - TILE_VIEW_SIDE_MARGINS;
162
-    const viewHeight = clientHeight - TILE_VIEW_SIDE_MARGINS;
189
+    const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN);
190
+    const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN);
163
     const initialWidth = viewWidth / columns;
191
     const initialWidth = viewWidth / columns;
192
+    const initialHeight = viewHeight / minVisibleRows;
164
     const aspectRatioHeight = initialWidth / aspectRatio;
193
     const aspectRatioHeight = initialWidth / aspectRatio;
165
-    const height = Math.floor(Math.min(aspectRatioHeight, viewHeight / visibleRows));
166
-    const width = Math.floor(aspectRatio * height);
194
+    const noScrollHeight = (clientHeight / rows) - TILE_VERTICAL_MARGIN;
195
+    const scrollInitialWidth = (viewWidth - SCROLL_SIZE) / columns;
196
+    let height = Math.floor(Math.min(aspectRatioHeight, initialHeight));
197
+    let width = Math.floor(aspectRatio * height);
198
+
199
+    if (height > noScrollHeight && width > scrollInitialWidth) { // we will have scroll and we need more space for it.
200
+        const scrollAspectRatioHeight = scrollInitialWidth / aspectRatio;
201
+
202
+        // Recalculating width/height to fit the available space when a scroll is displayed.
203
+        // NOTE: Math.min(scrollAspectRatioHeight, initialHeight) would be enough to recalculate but since the new
204
+        // height value can theoretically be dramatically smaller and the scroll may not be neccessary anymore we need
205
+        // to compare it with noScrollHeight( the optimal height to fit all thumbnails without scroll) and get the
206
+        // bigger one. This way we ensure that we always strech the thumbnails as close as we can to the edges of the
207
+        // window.
208
+        height = Math.floor(Math.max(Math.min(scrollAspectRatioHeight, initialHeight), noScrollHeight));
209
+        width = Math.floor(aspectRatio * height);
210
+    }
211
+
167
 
212
 
168
     return {
213
     return {
169
         height,
214
         height,

+ 8
- 14
react/features/filmstrip/middleware.web.js 查看文件

9
     LAYOUTS
9
     LAYOUTS
10
 } from '../video-layout';
10
 } from '../video-layout';
11
 
11
 
12
-import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
12
+import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
13
 
13
 
14
 import './subscriber.web';
14
 import './subscriber.web';
15
 
15
 
27
         switch (layout) {
27
         switch (layout) {
28
         case LAYOUTS.TILE_VIEW: {
28
         case LAYOUTS.TILE_VIEW: {
29
             const { gridDimensions } = state['features/filmstrip'].tileViewDimensions;
29
             const { gridDimensions } = state['features/filmstrip'].tileViewDimensions;
30
-            const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
31
-
32
-            store.dispatch(
33
-                setTileViewDimensions(
34
-                    gridDimensions,
35
-                    {
36
-                        clientHeight,
37
-                        clientWidth
38
-                    },
39
-                    store
40
-                )
41
-            );
30
+
31
+            store.dispatch(setTileViewDimensions(gridDimensions));
42
             break;
32
             break;
43
         }
33
         }
44
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
34
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
45
-            store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
35
+            store.dispatch(setHorizontalViewDimensions());
36
+            break;
37
+
38
+        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
39
+            store.dispatch(setVerticalViewDimensions());
46
             break;
40
             break;
47
         }
41
         }
48
         break;
42
         break;

+ 65
- 1
react/features/filmstrip/reducer.js 查看文件

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
3
 import { ReducerRegistry } from '../base/redux';
4
 import { ReducerRegistry } from '../base/redux';
4
 
5
 
5
 import {
6
 import {
6
     SET_FILMSTRIP_ENABLED,
7
     SET_FILMSTRIP_ENABLED,
7
     SET_FILMSTRIP_VISIBLE,
8
     SET_FILMSTRIP_VISIBLE,
8
     SET_HORIZONTAL_VIEW_DIMENSIONS,
9
     SET_HORIZONTAL_VIEW_DIMENSIONS,
9
-    SET_TILE_VIEW_DIMENSIONS
10
+    SET_TILE_VIEW_DIMENSIONS,
11
+    SET_VERTICAL_VIEW_DIMENSIONS,
12
+    SET_VOLUME
10
 } from './actionTypes';
13
 } from './actionTypes';
11
 
14
 
12
 const DEFAULT_STATE = {
15
 const DEFAULT_STATE = {
26
      */
29
      */
27
     horizontalViewDimensions: {},
30
     horizontalViewDimensions: {},
28
 
31
 
32
+    /**
33
+     * The custom audio volume levels per perticipant.
34
+     *
35
+     * @type {Object}
36
+     */
37
+    participantsVolume: {},
38
+
39
+    /**
40
+     * The ordered IDs of the remote participants displayed in the filmstrip.
41
+     *
42
+     * NOTE: Currently the order will match the one from the base/participants array. But this is good initial step for
43
+     * reordering the remote participants.
44
+     */
45
+    remoteParticipants: [],
46
+
29
     /**
47
     /**
30
      * The tile view dimensions.
48
      * The tile view dimensions.
31
      *
49
      *
34
      */
52
      */
35
     tileViewDimensions: {},
53
     tileViewDimensions: {},
36
 
54
 
55
+    /**
56
+     * The vertical view dimensions.
57
+     *
58
+     * @public
59
+     * @type {Object}
60
+     */
61
+    verticalViewDimensions: {},
62
+
37
     /**
63
     /**
38
      * The indicator which determines whether the {@link Filmstrip} is visible.
64
      * The indicator which determines whether the {@link Filmstrip} is visible.
39
      *
65
      *
69
                 ...state,
95
                 ...state,
70
                 tileViewDimensions: action.dimensions
96
                 tileViewDimensions: action.dimensions
71
             };
97
             };
98
+        case SET_VERTICAL_VIEW_DIMENSIONS:
99
+            return {
100
+                ...state,
101
+                verticalViewDimensions: action.dimensions
102
+            };
103
+        case SET_VOLUME:
104
+            return {
105
+                ...state,
106
+                participantsVolume: {
107
+                    ...state.participantsVolume,
108
+
109
+                    // NOTE: This would fit better in the features/base/participants. But currently we store
110
+                    // the participants as an array which will make it expensive to search for the volume for
111
+                    // every participant separately.
112
+                    [action.participantId]: action.volume
113
+                }
114
+            };
115
+        case PARTICIPANT_JOINED: {
116
+            const { id, local } = action.participant;
117
+
118
+            if (!local) {
119
+                state.remoteParticipants = [ ...state.remoteParticipants, id ];
120
+            }
121
+
122
+            return state;
123
+        }
124
+        case PARTICIPANT_LEFT: {
125
+            const { id, local } = action.participant;
126
+
127
+            if (local) {
128
+                return state;
129
+            }
130
+
131
+            state.remoteParticipants = state.remoteParticipants.filter(participantId => participantId !== id);
132
+            delete state.participantsVolume[id];
133
+
134
+            return state;
135
+        }
72
         }
136
         }
73
 
137
 
74
         return state;
138
         return state;

+ 10
- 40
react/features/filmstrip/subscriber.web.js 查看文件

7
 import { setOverflowDrawer } from '../toolbox/actions.web';
7
 import { setOverflowDrawer } from '../toolbox/actions.web';
8
 import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
8
 import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
9
 
9
 
10
-import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
10
+import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
11
 import {
11
 import {
12
     ASPECT_RATIO_BREAKPOINT,
12
     ASPECT_RATIO_BREAKPOINT,
13
     DISPLAY_DRAWER_THRESHOLD,
13
     DISPLAY_DRAWER_THRESHOLD,
28
             const oldGridDimensions = state['features/filmstrip'].tileViewDimensions.gridDimensions;
28
             const oldGridDimensions = state['features/filmstrip'].tileViewDimensions.gridDimensions;
29
 
29
 
30
             if (!equals(gridDimensions, oldGridDimensions)) {
30
             if (!equals(gridDimensions, oldGridDimensions)) {
31
-                const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
32
-
33
-                store.dispatch(
34
-                    setTileViewDimensions(
35
-                        gridDimensions,
36
-                        {
37
-                            clientHeight,
38
-                            clientWidth
39
-                        },
40
-                        store
41
-                    )
42
-                );
31
+                store.dispatch(setTileViewDimensions(gridDimensions));
43
             }
32
             }
44
         }
33
         }
45
     });
34
     });
53
         const state = store.getState();
42
         const state = store.getState();
54
 
43
 
55
         switch (layout) {
44
         switch (layout) {
56
-        case LAYOUTS.TILE_VIEW: {
57
-            const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
58
-
59
-            store.dispatch(
60
-                setTileViewDimensions(
61
-                    getTileViewGridDimensions(state),
62
-                    {
63
-                        clientHeight,
64
-                        clientWidth
65
-                    },
66
-                    store
67
-                )
68
-            );
45
+        case LAYOUTS.TILE_VIEW:
46
+            store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
69
             break;
47
             break;
70
-        }
71
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
48
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
72
-            store.dispatch(setHorizontalViewDimensions(state['features/base/responsive-ui'].clientHeight));
49
+            store.dispatch(setHorizontalViewDimensions());
50
+            break;
51
+        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
52
+            store.dispatch(setVerticalViewDimensions());
73
             break;
53
             break;
74
         }
54
         }
75
     });
55
     });
168
 
148
 
169
         if (shouldDisplayTileView(state)) {
149
         if (shouldDisplayTileView(state)) {
170
             const gridDimensions = getTileViewGridDimensions(state);
150
             const gridDimensions = getTileViewGridDimensions(state);
171
-            const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
172
-
173
-            store.dispatch(
174
-                setTileViewDimensions(
175
-                    gridDimensions,
176
-                    {
177
-                        clientHeight,
178
-                        clientWidth
179
-                    },
180
-                    store
181
-                )
182
-            );
151
+
152
+            store.dispatch(setTileViewDimensions(gridDimensions));
183
         }
153
         }
184
     });
154
     });

+ 3
- 2
react/features/video-layout/functions.js 查看文件

106
     const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
106
     const columnsToMaintainASquare = Math.ceil(Math.sqrt(numberOfParticipants));
107
     const columns = Math.min(columnsToMaintainASquare, maxColumns);
107
     const columns = Math.min(columnsToMaintainASquare, maxColumns);
108
     const rows = Math.ceil(numberOfParticipants / columns);
108
     const rows = Math.ceil(numberOfParticipants / columns);
109
-    const visibleRows = Math.min(maxColumns, rows);
109
+    const minVisibleRows = Math.min(maxColumns, rows);
110
 
110
 
111
     return {
111
     return {
112
         columns,
112
         columns,
113
-        visibleRows
113
+        minVisibleRows,
114
+        rows
114
     };
115
     };
115
 }
116
 }
116
 
117
 

Loading…
取消
儲存