Browse Source

ref(Thumbnail): Create React component.

master
Hristo Terezov 5 years ago
parent
commit
51e381a0b1

+ 0
- 13
conference.js View File

2014
                             formattedDisplayName
2014
                             formattedDisplayName
2015
                                 || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
2015
                                 || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
2016
                 });
2016
                 });
2017
-                APP.UI.changeDisplayName(id, formattedDisplayName);
2018
             }
2017
             }
2019
         );
2018
         );
2020
         room.on(
2019
         room.on(
2053
             (...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
2052
             (...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
2054
 
2053
 
2055
         room.on(JitsiConferenceEvents.KICKED, participant => {
2054
         room.on(JitsiConferenceEvents.KICKED, participant => {
2056
-            APP.UI.hideStats();
2057
             APP.store.dispatch(kickedOut(room, participant));
2055
             APP.store.dispatch(kickedOut(room, participant));
2058
-
2059
-            // FIXME close
2060
         });
2056
         });
2061
 
2057
 
2062
         room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
2058
         room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
2389
         APP.keyboardshortcut.init();
2385
         APP.keyboardshortcut.init();
2390
 
2386
 
2391
         APP.store.dispatch(conferenceJoined(room));
2387
         APP.store.dispatch(conferenceJoined(room));
2392
-
2393
-        const displayName
2394
-            = APP.store.getState()['features/base/settings'].displayName;
2395
-
2396
-        APP.UI.changeDisplayName('localVideoContainer', displayName);
2397
     },
2388
     },
2398
 
2389
 
2399
     /**
2390
     /**
2871
         APP.store.dispatch(updateSettings({
2862
         APP.store.dispatch(updateSettings({
2872
             displayName: formattedNickname
2863
             displayName: formattedNickname
2873
         }));
2864
         }));
2874
-
2875
-        if (room) {
2876
-            APP.UI.changeDisplayName(id, formattedNickname);
2877
-        }
2878
     },
2865
     },
2879
 
2866
 
2880
     /**
2867
     /**

+ 0
- 34
modules/UI/UI.js View File

7
 import Logger from 'jitsi-meet-logger';
7
 import Logger from 'jitsi-meet-logger';
8
 
8
 
9
 import { isMobileBrowser } from '../../react/features/base/environment/utils';
9
 import { isMobileBrowser } from '../../react/features/base/environment/utils';
10
-import { getLocalParticipant } from '../../react/features/base/participants';
11
 import { toggleChat } from '../../react/features/chat';
10
 import { toggleChat } from '../../react/features/chat';
12
 import { setDocumentUrl } from '../../react/features/etherpad';
11
 import { setDocumentUrl } from '../../react/features/etherpad';
13
 import { setFilmstripVisible } from '../../react/features/filmstrip';
12
 import { setFilmstripVisible } from '../../react/features/filmstrip';
91
     });
90
     });
92
 };
91
 };
93
 
92
 
94
-/**
95
- * Change nickname for the user.
96
- * @param {string} id user id
97
- * @param {string} displayName new nickname
98
- */
99
-UI.changeDisplayName = function(id, displayName) {
100
-    VideoLayout.onDisplayNameChanged(id, displayName);
101
-};
102
-
103
 /**
93
 /**
104
  * Initialize conference UI.
94
  * Initialize conference UI.
105
  */
95
  */
106
 UI.initConference = function() {
96
 UI.initConference = function() {
107
-    const { getState } = APP.store;
108
-    const { id, name } = getLocalParticipant(getState);
109
-
110
     UI.showToolbar();
97
     UI.showToolbar();
111
-
112
-    const displayName = config.displayJids ? id : name;
113
-
114
-    if (displayName) {
115
-        UI.changeDisplayName('localVideoContainer', displayName);
116
-    }
117
 };
98
 };
118
 
99
 
119
 /**
100
 /**
238
  * @param {JitsiParticipant} user
219
  * @param {JitsiParticipant} user
239
  */
220
  */
240
 UI.addUser = function(user) {
221
 UI.addUser = function(user) {
241
-    const id = user.getId();
242
-    const displayName = user.getDisplayName();
243
     const status = user.getStatus();
222
     const status = user.getStatus();
244
 
223
 
245
     if (status) {
224
     if (status) {
246
         // FIXME: move updateUserStatus in participantPresenceChanged action
225
         // FIXME: move updateUserStatus in participantPresenceChanged action
247
         UI.updateUserStatus(user, status);
226
         UI.updateUserStatus(user, status);
248
     }
227
     }
249
-
250
-    // set initial display name
251
-    if (displayName) {
252
-        UI.changeDisplayName(id, displayName);
253
-    }
254
 };
228
 };
255
 
229
 
256
 /**
230
 /**
442
  */
416
  */
443
 UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
417
 UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
444
 
418
 
445
-/**
446
- * Hide connection quality statistics from UI.
447
- */
448
-UI.hideStats = function() {
449
-    VideoLayout.hideStats();
450
-};
451
-
452
-
453
 UI.notifyTokenAuthFailed = function() {
419
 UI.notifyTokenAuthFailed = function() {
454
     messageHandler.showError({
420
     messageHandler.showError({
455
         descriptionKey: 'dialog.tokenAuthFailed',
421
         descriptionKey: 'dialog.tokenAuthFailed',

+ 20
- 41
modules/UI/shared_video/SharedVideoThumb.js View File

1
-/* global $ */
1
+/* global $, APP */
2
 
2
 
3
-import Logger from 'jitsi-meet-logger';
3
+/* eslint-disable no-unused-vars */
4
+import React, { Component } from 'react';
5
+import ReactDOM from 'react-dom';
6
+import { I18nextProvider } from 'react-i18next';
7
+import { Provider } from 'react-redux';
4
 
8
 
9
+import { i18next } from '../../../react/features/base/i18n';
10
+import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
5
 import SmallVideo from '../videolayout/SmallVideo';
11
 import SmallVideo from '../videolayout/SmallVideo';
6
-
7
-const logger = Logger.getLogger(__filename);
12
+/* eslint-enable no-unused-vars */
8
 
13
 
9
 /**
14
 /**
10
  *
15
  *
13
     /**
18
     /**
14
      *
19
      *
15
      * @param {*} participant
20
      * @param {*} participant
16
-     * @param {*} videoType
17
-     * @param {*} VideoLayout
18
      */
21
      */
19
-    constructor(participant, videoType, VideoLayout) {
20
-        super(VideoLayout);
22
+    constructor(participant) {
23
+        super();
21
         this.id = participant.id;
24
         this.id = participant.id;
22
         this.isLocal = false;
25
         this.isLocal = false;
23
         this.url = participant.id;
26
         this.url = participant.id;
24
         this.videoSpanId = 'sharedVideoContainer';
27
         this.videoSpanId = 'sharedVideoContainer';
25
         this.container = this.createContainer(this.videoSpanId);
28
         this.container = this.createContainer(this.videoSpanId);
26
         this.$container = $(this.container);
29
         this.$container = $(this.container);
30
+        this.renderThumbnail();
27
         this._setThumbnailSize();
31
         this._setThumbnailSize();
28
         this.bindHoverHandler();
32
         this.bindHoverHandler();
29
-        this.updateDisplayName();
30
         this.container.onclick = this._onContainerClick;
33
         this.container.onclick = this._onContainerClick;
31
     }
34
     }
32
 
35
 
33
-    /**
34
-     *
35
-     */
36
-    initializeAvatar() {} // eslint-disable-line no-empty-function
37
-
38
     /**
36
     /**
39
      *
37
      *
40
      * @param {*} spanId
38
      * @param {*} spanId
45
         container.id = spanId;
43
         container.id = spanId;
46
         container.className = 'videocontainer';
44
         container.className = 'videocontainer';
47
 
45
 
48
-        // add the avatar
49
-        const avatar = document.createElement('img');
50
-
51
-        avatar.className = 'sharedVideoAvatar';
52
-        avatar.src = `https://img.youtube.com/vi/${this.url}/0.jpg`;
53
-        container.appendChild(avatar);
54
-
55
-        const displayNameContainer = document.createElement('div');
56
-
57
-        displayNameContainer.className = 'displayNameContainer';
58
-        container.appendChild(displayNameContainer);
59
-
60
         const remoteVideosContainer
46
         const remoteVideosContainer
61
             = document.getElementById('filmstripRemoteVideosContainer');
47
             = document.getElementById('filmstripRemoteVideosContainer');
62
         const localVideoContainer
48
         const localVideoContainer
68
     }
54
     }
69
 
55
 
70
     /**
56
     /**
71
-     * Triggers re-rendering of the display name using current instance state.
72
-     *
73
-     * @returns {void}
57
+     * Renders the thumbnail.
74
      */
58
      */
75
-    updateDisplayName() {
76
-        if (!this.container) {
77
-            logger.warn(`Unable to set displayName - ${this.videoSpanId
78
-            } does not exist`);
79
-
80
-            return;
81
-        }
82
-
83
-        this._renderDisplayName({
84
-            elementID: `${this.videoSpanId}_name`,
85
-            participantID: this.id
86
-        });
59
+    renderThumbnail(isHovered = false) {
60
+        ReactDOM.render(
61
+            <Provider store = { APP.store }>
62
+                <I18nextProvider i18n = { i18next }>
63
+                    <Thumbnail participantID = { this.id } isHovered = { isHovered } />
64
+                </I18nextProvider>
65
+            </Provider>, this.container);
87
     }
66
     }
88
 }
67
 }

+ 0
- 24
modules/UI/videolayout/Filmstrip.js View File

37
      */
37
      */
38
     resizeThumbnailsForTileView(width, height, forceUpdate = false) {
38
     resizeThumbnailsForTileView(width, height, forceUpdate = false) {
39
         const thumbs = this._getThumbs(!forceUpdate);
39
         const thumbs = this._getThumbs(!forceUpdate);
40
-        const avatarSize = height / 2;
41
 
40
 
42
         if (thumbs.localThumb) {
41
         if (thumbs.localThumb) {
43
             thumbs.localThumb.css({
42
             thumbs.localThumb.css({
58
                 width: `${width}px`
57
                 width: `${width}px`
59
             });
58
             });
60
         }
59
         }
61
-
62
-        $('.avatar-container').css({
63
-            height: `${avatarSize}px`,
64
-            width: `${avatarSize}px`
65
-        });
66
     },
60
     },
67
 
61
 
68
     /**
62
     /**
77
 
71
 
78
         if (thumbs.localThumb) {
72
         if (thumbs.localThumb) {
79
             const { height, width } = local;
73
             const { height, width } = local;
80
-            const avatarSize = height / 2;
81
 
74
 
82
             thumbs.localThumb.css({
75
             thumbs.localThumb.css({
83
                 height: `${height}px`,
76
                 height: `${height}px`,
85
                 'min-width': `${width}px`,
78
                 'min-width': `${width}px`,
86
                 width: `${width}px`
79
                 width: `${width}px`
87
             });
80
             });
88
-            $('#localVideoContainer > .avatar-container').css({
89
-                height: `${avatarSize}px`,
90
-                width: `${avatarSize}px`
91
-            });
92
         }
81
         }
93
 
82
 
94
         if (thumbs.remoteThumbs) {
83
         if (thumbs.remoteThumbs) {
95
             const { height, width } = remote;
84
             const { height, width } = remote;
96
-            const avatarSize = height / 2;
97
 
85
 
98
             thumbs.remoteThumbs.css({
86
             thumbs.remoteThumbs.css({
99
                 height: `${height}px`,
87
                 height: `${height}px`,
101
                 'min-width': `${width}px`,
89
                 'min-width': `${width}px`,
102
                 width: `${width}px`
90
                 width: `${width}px`
103
             });
91
             });
104
-            $('#filmstripRemoteVideosContainer > span > .avatar-container').css({
105
-                height: `${avatarSize}px`,
106
-                width: `${avatarSize}px`
107
-            });
108
         }
92
         }
109
     },
93
     },
110
 
94
 
126
                 'min-width': '',
110
                 'min-width': '',
127
                 'min-height': ''
111
                 'min-height': ''
128
             });
112
             });
129
-            $('#localVideoContainer > .avatar-container').css({
130
-                height: '50%',
131
-                width: `${heightToWidthPercent / 2}%`
132
-            });
133
         }
113
         }
134
 
114
 
135
         if (thumbs.remoteThumbs) {
115
         if (thumbs.remoteThumbs) {
142
                 'min-width': '',
122
                 'min-width': '',
143
                 'min-height': ''
123
                 'min-height': ''
144
             });
124
             });
145
-            $('#filmstripRemoteVideosContainer > span > .avatar-container').css({
146
-                height: '50%',
147
-                width: `${heightToWidthPercent / 2}%`
148
-            });
149
         }
125
         }
150
     },
126
     },
151
 
127
 

+ 15
- 90
modules/UI/videolayout/LocalVideo.js View File

1
-/* global $, config, interfaceConfig, APP */
1
+/* global $, config, APP */
2
 
2
 
3
-import Logger from 'jitsi-meet-logger';
4
 /* eslint-disable no-unused-vars */
3
 /* eslint-disable no-unused-vars */
5
 import React, { Component } from 'react';
4
 import React, { Component } from 'react';
6
 import ReactDOM from 'react-dom';
5
 import ReactDOM from 'react-dom';
6
+import { I18nextProvider } from 'react-i18next';
7
 import { Provider } from 'react-redux';
7
 import { Provider } from 'react-redux';
8
 
8
 
9
+import { i18next } from '../../../react/features/base/i18n';
9
 import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
10
 import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
10
 import { VideoTrack } from '../../../react/features/base/media';
11
 import { VideoTrack } from '../../../react/features/base/media';
11
 import { updateSettings } from '../../../react/features/base/settings';
12
 import { updateSettings } from '../../../react/features/base/settings';
12
 import { getLocalVideoTrack } from '../../../react/features/base/tracks';
13
 import { getLocalVideoTrack } from '../../../react/features/base/tracks';
14
+import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
13
 import { shouldDisplayTileView } from '../../../react/features/video-layout';
15
 import { shouldDisplayTileView } from '../../../react/features/video-layout';
14
 /* eslint-enable no-unused-vars */
16
 /* eslint-enable no-unused-vars */
15
 import UIEvents from '../../../service/UI/UIEvents';
17
 import UIEvents from '../../../service/UI/UIEvents';
16
 
18
 
17
 import SmallVideo from './SmallVideo';
19
 import SmallVideo from './SmallVideo';
18
 
20
 
19
-const logger = Logger.getLogger(__filename);
20
-
21
 /**
21
 /**
22
  *
22
  *
23
  */
23
  */
24
 export default class LocalVideo extends SmallVideo {
24
 export default class LocalVideo extends SmallVideo {
25
     /**
25
     /**
26
      *
26
      *
27
-     * @param {*} VideoLayout
28
      * @param {*} emitter
27
      * @param {*} emitter
29
      * @param {*} streamEndedCallback
28
      * @param {*} streamEndedCallback
30
      */
29
      */
31
-    constructor(VideoLayout, emitter, streamEndedCallback) {
32
-        super(VideoLayout);
30
+    constructor(emitter, streamEndedCallback) {
31
+        super();
33
         this.videoSpanId = 'localVideoContainer';
32
         this.videoSpanId = 'localVideoContainer';
34
         this.streamEndedCallback = streamEndedCallback;
33
         this.streamEndedCallback = streamEndedCallback;
35
         this.container = this.createContainer();
34
         this.container = this.createContainer();
37
         this.isLocal = true;
36
         this.isLocal = true;
38
         this._setThumbnailSize();
37
         this._setThumbnailSize();
39
         this.updateDOMLocation();
38
         this.updateDOMLocation();
39
+        this.renderThumbnail();
40
 
40
 
41
         this.localVideoId = null;
41
         this.localVideoId = null;
42
         this.bindHoverHandler();
42
         this.bindHoverHandler();
44
             this._buildContextMenu();
44
             this._buildContextMenu();
45
         }
45
         }
46
         this.emitter = emitter;
46
         this.emitter = emitter;
47
-        this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center';
48
 
47
 
49
         Object.defineProperty(this, 'id', {
48
         Object.defineProperty(this, 'id', {
50
             get() {
49
             get() {
53
         });
52
         });
54
         this.initBrowserSpecificProperties();
53
         this.initBrowserSpecificProperties();
55
 
54
 
56
-        // Set default display name.
57
-        this.updateDisplayName();
58
-
59
-        // Initialize the avatar display with an avatar url selected from the redux
60
-        // state. Redux stores the local user with a hardcoded participant id of
61
-        // 'local' if no id has been assigned yet.
62
-        this.initializeAvatar();
63
-
64
-        this.addAudioLevelIndicator();
65
-        this.updateIndicators();
66
-        this.updateStatusBar();
67
-
68
         this.container.onclick = this._onContainerClick;
55
         this.container.onclick = this._onContainerClick;
69
     }
56
     }
70
 
57
 
77
         containerSpan.classList.add('videocontainer');
64
         containerSpan.classList.add('videocontainer');
78
         containerSpan.id = this.videoSpanId;
65
         containerSpan.id = this.videoSpanId;
79
 
66
 
80
-        containerSpan.innerHTML = `
81
-            <div class = 'videocontainer__background'></div>
82
-            <span id = 'localVideoWrapper'></span>
83
-            <div class = 'videocontainer__toolbar'></div>
84
-            <div class = 'videocontainer__toptoolbar'></div>
85
-            <div class = 'videocontainer__hoverOverlay'></div>
86
-            <div class = 'displayNameContainer'></div>
87
-            <div class = 'avatar-container'></div>`;
88
-
89
         return containerSpan;
67
         return containerSpan;
90
     }
68
     }
91
 
69
 
92
     /**
70
     /**
93
-     * Triggers re-rendering of the display name using current instance state.
94
-     *
95
-     * @returns {void}
71
+     * Renders the thumbnail.
96
      */
72
      */
97
-    updateDisplayName() {
98
-        if (!this.container) {
99
-            logger.warn(
100
-                    `Unable to set displayName - ${this.videoSpanId
101
-                    } does not exist`);
102
-
103
-            return;
104
-        }
105
-
106
-        this._renderDisplayName({
107
-            allowEditing: !config.disableProfile,
108
-            displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
109
-            elementID: 'localDisplayName',
110
-            participantID: this.id
111
-        });
73
+    renderThumbnail(isHovered = false) {
74
+        ReactDOM.render(
75
+            <Provider store = { APP.store }>
76
+                <I18nextProvider i18n = { i18next }>
77
+                    <Thumbnail participantID = { this.id } isHovered = { isHovered } />
78
+                </I18nextProvider>
79
+            </Provider>, this.container);
112
     }
80
     }
113
 
81
 
114
     /**
82
     /**
116
      * @param {*} stream
84
      * @param {*} stream
117
      */
85
      */
118
     changeVideo(stream) {
86
     changeVideo(stream) {
119
-        this.videoStream = stream;
120
         this.localVideoId = `localVideo_${stream.getId()}`;
87
         this.localVideoId = `localVideo_${stream.getId()}`;
121
-        this._updateVideoElement();
122
 
88
 
123
         // eslint-disable-next-line eqeqeq
89
         // eslint-disable-next-line eqeqeq
124
         const isVideo = stream.videoType != 'desktop';
90
         const isVideo = stream.videoType != 'desktop';
128
         this.setFlipX(isVideo ? settings.localFlipX : false);
94
         this.setFlipX(isVideo ? settings.localFlipX : false);
129
 
95
 
130
         const endedHandler = () => {
96
         const endedHandler = () => {
131
-            const localVideoContainer
132
-                = document.getElementById('localVideoWrapper');
133
-
134
-            // Only remove if there is no video and not a transition state.
135
-            // Previous non-react logic created a new video element with each track
136
-            // removal whereas react reuses the video component so it could be the
137
-            // stream ended but a new one is being used.
138
-            if (localVideoContainer && this.videoStream.isEnded()) {
139
-                ReactDOM.unmountComponentAtNode(localVideoContainer);
140
-            }
141
-
142
             this._notifyOfStreamEnded();
97
             this._notifyOfStreamEnded();
143
             stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
98
             stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
144
         };
99
         };
254
             : document.getElementById('filmstripLocalVideoThumbnail');
209
             : document.getElementById('filmstripLocalVideoThumbnail');
255
 
210
 
256
         appendTarget && appendTarget.appendChild(this.container);
211
         appendTarget && appendTarget.appendChild(this.container);
257
-        this._updateVideoElement();
258
-    }
259
-
260
-    /**
261
-     * Renders the React Element for displaying video in {@code LocalVideo}.
262
-     *
263
-     */
264
-    _updateVideoElement() {
265
-        const localVideoContainer = document.getElementById('localVideoWrapper');
266
-        const videoTrack
267
-            = getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
268
-
269
-        ReactDOM.render(
270
-            <Provider store = { APP.store }>
271
-                <VideoTrack
272
-                    id = 'localVideo_container'
273
-                    videoTrack = { videoTrack } />
274
-            </Provider>,
275
-            localVideoContainer
276
-        );
277
-
278
-        // Ensure the video gets play() called on it. This may be necessary in the
279
-        // case where the local video container was moved and re-attached, in which
280
-        // case video does not autoplay. Also, set the playsinline attribute on the
281
-        // video element so that local video doesn't open in full screen by default
282
-        // in Safari browser on iOS.
283
-        const video = this.container.querySelector('video');
284
-
285
-        video && video.setAttribute('playsinline', 'true');
286
-        video && !config.testing?.noAutoPlayVideo && video.play();
287
     }
212
     }
288
 }
213
 }

+ 18
- 180
modules/UI/videolayout/RemoteVideo.js View File

1
-/* global $, APP, interfaceConfig */
1
+/* global $, APP, config */
2
 
2
 
3
 /* eslint-disable no-unused-vars */
3
 /* eslint-disable no-unused-vars */
4
 import { AtlasKitThemeProvider } from '@atlaskit/theme';
4
 import { AtlasKitThemeProvider } from '@atlaskit/theme';
15
 import { getParticipantById } from '../../../react/features/base/participants';
15
 import { getParticipantById } from '../../../react/features/base/participants';
16
 import { isTestModeEnabled } from '../../../react/features/base/testing';
16
 import { isTestModeEnabled } from '../../../react/features/base/testing';
17
 import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
17
 import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
18
+import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
18
 import { PresenceLabel } from '../../../react/features/presence-status';
19
 import { PresenceLabel } from '../../../react/features/presence-status';
19
 import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
20
 import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
20
 import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
21
 import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
44
     container.id = spanId;
45
     container.id = spanId;
45
     container.className = 'videocontainer';
46
     container.className = 'videocontainer';
46
 
47
 
47
-    container.innerHTML = `
48
-        <div class = 'videocontainer__background'></div>
49
-        <div class = 'videocontainer__toptoolbar'></div>
50
-        <div class = 'videocontainer__toolbar'></div>
51
-        <div class = 'videocontainer__hoverOverlay'></div>
52
-        <div class = 'displayNameContainer'></div>
53
-        <div class = 'avatar-container'></div>
54
-        <div class ='presence-label-container'></div>
55
-        <span class = 'remotevideomenu'></span>`;
56
-
57
     const remoteVideosContainer
48
     const remoteVideosContainer
58
         = document.getElementById('filmstripRemoteVideosContainer');
49
         = document.getElementById('filmstripRemoteVideosContainer');
59
     const localVideoContainer
50
     const localVideoContainer
72
      * Creates new instance of the <tt>RemoteVideo</tt>.
63
      * Creates new instance of the <tt>RemoteVideo</tt>.
73
      * @param user {JitsiParticipant} the user for whom remote video instance will
64
      * @param user {JitsiParticipant} the user for whom remote video instance will
74
      * be created.
65
      * be created.
75
-     * @param {VideoLayout} VideoLayout the video layout instance.
76
      * @constructor
66
      * @constructor
77
      */
67
      */
78
-    constructor(user, VideoLayout) {
79
-        super(VideoLayout);
68
+    constructor(user) {
69
+        super();
80
 
70
 
81
         this.user = user;
71
         this.user = user;
82
         this.id = user.getId();
72
         this.id = user.getId();
83
         this.videoSpanId = `participant_${this.id}`;
73
         this.videoSpanId = `participant_${this.id}`;
84
 
74
 
85
-        this._audioStreamElement = null;
86
-        this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
87
         this.addRemoteVideoContainer();
75
         this.addRemoteVideoContainer();
88
-        this.updateIndicators();
89
-        this.updateDisplayName();
90
         this.bindHoverHandler();
76
         this.bindHoverHandler();
91
         this.flipX = false;
77
         this.flipX = false;
92
         this.isLocal = false;
78
         this.isLocal = false;
100
          */
86
          */
101
         this._canPlayEventReceived = false;
87
         this._canPlayEventReceived = false;
102
 
88
 
103
-        // Bind event handlers so they are only bound once for every instance.
104
-        // TODO The event handlers should be turned into actions so changes can be
105
-        // handled through reducers and middleware.
106
-        this._setAudioVolume = this._setAudioVolume.bind(this);
107
-
108
         this.container.onclick = this._onContainerClick;
89
         this.container.onclick = this._onContainerClick;
109
     }
90
     }
110
 
91
 
114
     addRemoteVideoContainer() {
95
     addRemoteVideoContainer() {
115
         this.container = createContainer(this.videoSpanId);
96
         this.container = createContainer(this.videoSpanId);
116
         this.$container = $(this.container);
97
         this.$container = $(this.container);
117
-        this.initializeAvatar();
98
+        this.renderThumbnail();
118
         this._setThumbnailSize();
99
         this._setThumbnailSize();
119
         this.initBrowserSpecificProperties();
100
         this.initBrowserSpecificProperties();
120
-        this.updateRemoteVideoMenu();
121
-        this.updateStatusBar();
122
-        this.addAudioLevelIndicator();
123
-        this.addPresenceLabel();
124
 
101
 
125
         return this.container;
102
         return this.container;
126
     }
103
     }
127
 
104
 
128
     /**
105
     /**
129
-     * Generates the popup menu content.
130
-     *
131
-     * @returns {Element|*} the constructed element, containing popup menu items
132
-     * @private
106
+     * Renders the thumbnail.
133
      */
107
      */
134
-    _generatePopupContent() {
135
-        const remoteVideoMenuContainer
136
-            = this.container.querySelector('.remotevideomenu');
137
-
138
-        if (!remoteVideoMenuContainer) {
139
-            return;
140
-        }
141
-
142
-        const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
143
-
144
-        // hide volume when in silent mode
145
-        const onVolumeChange
146
-            = APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
147
-
108
+    renderThumbnail(isHovered = false) {
148
         ReactDOM.render(
109
         ReactDOM.render(
149
             <Provider store = { APP.store }>
110
             <Provider store = { APP.store }>
150
                 <I18nextProvider i18n = { i18next }>
111
                 <I18nextProvider i18n = { i18next }>
151
-                    <AtlasKitThemeProvider mode = 'dark'>
152
-                        <RemoteVideoMenuTriggerButton
153
-                            initialVolumeValue = { initialVolumeValue }
154
-                            onMenuDisplay
155
-                                = {this._onRemoteVideoMenuDisplay.bind(this)}
156
-                            onVolumeChange = { onVolumeChange }
157
-                            participantID = { this.id } />
158
-                    </AtlasKitThemeProvider>
112
+                    <Thumbnail participantID = { this.id } isHovered = { isHovered } />
159
                 </I18nextProvider>
113
                 </I18nextProvider>
160
-            </Provider>,
161
-            remoteVideoMenuContainer);
162
-    }
163
-
164
-    /**
165
-     *
166
-     */
167
-    _onRemoteVideoMenuDisplay() {
168
-        this.updateRemoteVideoMenu();
169
-    }
170
-
171
-    /**
172
-     * Change the remote participant's volume level.
173
-     *
174
-     * @param {int} newVal - The value to set the slider to.
175
-     */
176
-    _setAudioVolume(newVal) {
177
-        if (this._audioStreamElement) {
178
-            this._audioStreamElement.volume = newVal;
179
-        }
180
-    }
181
-
182
-    /**
183
-     * Updates the remote video menu.
184
-     */
185
-    updateRemoteVideoMenu() {
186
-        this._generatePopupContent();
114
+            </Provider>, this.container);
187
     }
115
     }
188
 
116
 
189
     /**
117
     /**
199
         }
127
         }
200
 
128
 
201
         const isVideo = stream.isVideoTrack();
129
         const isVideo = stream.isVideoTrack();
202
-        const elementID = SmallVideo.getStreamElementID(stream);
130
+        const elementID = `remoteVideo_${stream.getId()}`;
203
         const select = $(`#${elementID}`);
131
         const select = $(`#${elementID}`);
204
 
132
 
205
         select.remove();
133
         select.remove();
207
             this._canPlayEventReceived = false;
135
             this._canPlayEventReceived = false;
208
         }
136
         }
209
 
137
 
210
-        logger.info(`${isVideo ? 'Video' : 'Audio'} removed ${this.id}`, select);
211
-
212
-        if (stream === this.videoStream) {
213
-            this.videoStream = null;
214
-        }
138
+        logger.info(`Video removed ${this.id}`, select);
215
 
139
 
216
         this.updateView();
140
         this.updateView();
217
     }
141
     }
223
      * @override
147
      * @override
224
      */
148
      */
225
     isVideoPlayable() {
149
     isVideoPlayable() {
226
-        const participant = getParticipantById(APP.store.getState(), this.id);
227
-        const { connectionStatus } = participant || {};
228
-
229
-        return (
230
-            super.isVideoPlayable()
231
-                && this._canPlayEventReceived
232
-                && connectionStatus === JitsiParticipantConnectionStatus.ACTIVE
233
-        );
150
+        return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived;
234
     }
151
     }
235
 
152
 
236
     /**
153
     /**
245
      * Removes RemoteVideo from the page.
162
      * Removes RemoteVideo from the page.
246
      */
163
      */
247
     remove() {
164
     remove() {
165
+        ReactDOM.unmountComponentAtNode(this.container);
248
         super.remove();
166
         super.remove();
249
-        this.removePresenceLabel();
250
-        this.removeRemoteVideoMenu();
251
     }
167
     }
252
 
168
 
253
     /**
169
     /**
295
 
211
 
296
         const isVideo = stream.isVideoTrack();
212
         const isVideo = stream.isVideoTrack();
297
 
213
 
298
-        if (isVideo) {
299
-            this.videoStream = stream;
300
-        } else {
301
-            this.audioStream = stream;
302
-        }
303
-
304
         if (!stream.getOriginalStream()) {
214
         if (!stream.getOriginalStream()) {
305
             logger.debug('Remote video stream has no original stream');
215
             logger.debug('Remote video stream has no original stream');
306
 
216
 
307
             return;
217
             return;
308
         }
218
         }
309
 
219
 
310
-        let streamElement = SmallVideo.createStreamElement(stream);
220
+        let streamElement = document.createElement('video');
221
+
222
+        streamElement.autoplay = !config.testing?.noAutoPlayVideo;
223
+        streamElement.id = `remoteVideo_${stream.getId()}`;
311
 
224
 
312
         // Put new stream element always in front
225
         // Put new stream element always in front
313
         streamElement = UIUtils.prependChild(this.container, streamElement);
226
         streamElement = UIUtils.prependChild(this.container, streamElement);
315
         this.waitForPlayback(streamElement, stream);
228
         this.waitForPlayback(streamElement, stream);
316
         stream.attach(streamElement);
229
         stream.attach(streamElement);
317
 
230
 
318
-        if (!isVideo) {
319
-            this._audioStreamElement = streamElement;
320
-
321
-            // If the remote video menu was created before the audio stream was
322
-            // attached we need to update the menu in order to show the volume
323
-            // slider.
324
-            this.updateRemoteVideoMenu();
325
-        } else if (isTestModeEnabled(APP.store.getState())) {
231
+        if (isVideo && isTestModeEnabled(APP.store.getState())) {
326
 
232
 
327
             const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
233
             const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
328
 
234
 
331
             });
237
             });
332
         }
238
         }
333
     }
239
     }
334
-
335
-    /**
336
-     * Triggers re-rendering of the display name using current instance state.
337
-     *
338
-     * @returns {void}
339
-     */
340
-    updateDisplayName() {
341
-        if (!this.container) {
342
-            logger.warn(`Unable to set displayName - ${this.videoSpanId} does not exist`);
343
-
344
-            return;
345
-        }
346
-
347
-        this._renderDisplayName({
348
-            elementID: `${this.videoSpanId}_name`,
349
-            participantID: this.id
350
-        });
351
-    }
352
-
353
-    /**
354
-     * Removes remote video menu element from video element identified by
355
-     * given <tt>videoElementId</tt>.
356
-     *
357
-     * @param videoElementId the id of local or remote video element.
358
-     */
359
-    removeRemoteVideoMenu() {
360
-        const menuSpan = this.$container.find('.remotevideomenu');
361
-
362
-        if (menuSpan.length) {
363
-            ReactDOM.unmountComponentAtNode(menuSpan.get(0));
364
-            menuSpan.remove();
365
-        }
366
-    }
367
-
368
-    /**
369
-     * Mounts the {@code PresenceLabel} for displaying the participant's current
370
-     * presence status.
371
-     *
372
-     * @return {void}
373
-     */
374
-    addPresenceLabel() {
375
-        const presenceLabelContainer = this.container.querySelector('.presence-label-container');
376
-
377
-        if (presenceLabelContainer) {
378
-            ReactDOM.render(
379
-                <Provider store = { APP.store }>
380
-                    <I18nextProvider i18n = { i18next }>
381
-                        <PresenceLabel
382
-                            participantID = { this.id }
383
-                            className = 'presence-label' />
384
-                    </I18nextProvider>
385
-                </Provider>,
386
-                presenceLabelContainer);
387
-        }
388
-    }
389
-
390
-    /**
391
-     * Unmounts the {@code PresenceLabel} component.
392
-     *
393
-     * @return {void}
394
-     */
395
-    removePresenceLabel() {
396
-        const presenceLabelContainer = this.container.querySelector('.presence-label-container');
397
-
398
-        if (presenceLabelContainer) {
399
-            ReactDOM.unmountComponentAtNode(presenceLabelContainer);
400
-        }
401
-    }
402
 }
240
 }

+ 24
- 382
modules/UI/videolayout/SmallVideo.js View File

1
-/* global $, APP, config, interfaceConfig */
1
+/* global $, APP, interfaceConfig */
2
 
2
 
3
 /* eslint-disable no-unused-vars */
3
 /* eslint-disable no-unused-vars */
4
 import { AtlasKitThemeProvider } from '@atlaskit/theme';
4
 import { AtlasKitThemeProvider } from '@atlaskit/theme';
21
     pinParticipant
21
     pinParticipant
22
 } from '../../../react/features/base/participants';
22
 } from '../../../react/features/base/participants';
23
 import {
23
 import {
24
+    getLocalVideoTrack,
24
     getTrackByMediaTypeAndParticipant,
25
     getTrackByMediaTypeAndParticipant,
25
     isLocalTrackMuted,
26
     isLocalTrackMuted,
26
     isRemoteTrackMuted
27
     isRemoteTrackMuted
29
 import { DisplayName } from '../../../react/features/display-name';
30
 import { DisplayName } from '../../../react/features/display-name';
30
 import {
31
 import {
31
     DominantSpeakerIndicator,
32
     DominantSpeakerIndicator,
33
+    isVideoPlayable,
32
     RaisedHandIndicator,
34
     RaisedHandIndicator,
33
     StatusIndicators
35
     StatusIndicators
34
 } from '../../../react/features/filmstrip';
36
 } from '../../../react/features/filmstrip';
89
     /**
91
     /**
90
      * Constructor.
92
      * Constructor.
91
      */
93
      */
92
-    constructor(VideoLayout) {
93
-        this.videoStream = null;
94
-        this.audioStream = null;
95
-        this.VideoLayout = VideoLayout;
94
+    constructor() {
96
         this.videoIsHovered = false;
95
         this.videoIsHovered = false;
97
         this.videoType = undefined;
96
         this.videoType = undefined;
98
 
97
 
99
-        /**
100
-         * Whether or not the connection indicator should be displayed.
101
-         *
102
-         * @private
103
-         * @type {boolean}
104
-         */
105
-        this._showConnectionIndicator = !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
106
-
107
-        /**
108
-         * Whether or not the dominant speaker indicator should be displayed.
109
-         *
110
-         * @private
111
-         * @type {boolean}
112
-         */
113
-        this._showDominantSpeaker = false;
114
-
115
-        /**
116
-         * Whether or not the raised hand indicator should be displayed.
117
-         *
118
-         * @private
119
-         * @type {boolean}
120
-         */
121
-        this._showRaisedHand = false;
122
-
123
         // Bind event handlers so they are only bound once for every instance.
98
         // Bind event handlers so they are only bound once for every instance.
124
         this.updateView = this.updateView.bind(this);
99
         this.updateView = this.updateView.bind(this);
125
 
100
 
145
         return this.$container.is(':visible');
120
         return this.$container.is(':visible');
146
     }
121
     }
147
 
122
 
148
-    /**
149
-     * Creates an audio or video element for a particular MediaStream.
150
-     */
151
-    static createStreamElement(stream) {
152
-        const isVideo = stream.isVideoTrack();
153
-        const element = isVideo ? document.createElement('video') : document.createElement('audio');
154
-
155
-        if (isVideo) {
156
-            element.setAttribute('muted', 'true');
157
-            element.setAttribute('playsInline', 'true'); /* for Safari on iOS to work */
158
-        } else if (config.startSilent) {
159
-            element.muted = true;
160
-        }
161
-
162
-        element.autoplay = !config.testing?.noAutoPlayVideo;
163
-        element.id = SmallVideo.getStreamElementID(stream);
164
-
165
-        return element;
166
-    }
167
-
168
-    /**
169
-     * Returns the element id for a particular MediaStream.
170
-     */
171
-    static getStreamElementID(stream) {
172
-        return (stream.isVideoTrack() ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
173
-    }
174
-
175
     /**
123
     /**
176
      * Configures hoverIn/hoverOut handlers. Depends on connection indicator.
124
      * Configures hoverIn/hoverOut handlers. Depends on connection indicator.
177
      */
125
      */
180
         this.$container.hover(
128
         this.$container.hover(
181
             () => {
129
             () => {
182
                 this.videoIsHovered = true;
130
                 this.videoIsHovered = true;
131
+                this.renderThumbnail(true);
183
                 this.updateView();
132
                 this.updateView();
184
-                this.updateIndicators();
185
             },
133
             },
186
             () => {
134
             () => {
187
                 this.videoIsHovered = false;
135
                 this.videoIsHovered = false;
136
+                this.renderThumbnail(false);
188
                 this.updateView();
137
                 this.updateView();
189
-                this.updateIndicators();
190
             }
138
             }
191
         );
139
         );
192
     }
140
     }
193
 
141
 
194
     /**
142
     /**
195
-     * Unmounts the ConnectionIndicator component.
196
-
197
-    * @returns {void}
198
-    */
199
-    removeConnectionIndicator() {
200
-        this._showConnectionIndicator = false;
201
-        this.updateIndicators();
202
-    }
203
-
204
-    /**
205
-     * Create or updates the ReactElement for displaying status indicators about
206
-     * audio mute, video mute, and moderator status.
207
-     *
208
-     * @returns {void}
209
-     */
210
-    updateStatusBar() {
211
-        const statusBarContainer = this.container.querySelector('.videocontainer__toolbar');
212
-
213
-        if (!statusBarContainer) {
214
-            return;
215
-        }
216
-
217
-        ReactDOM.render(
218
-            <Provider store = { APP.store }>
219
-                <I18nextProvider i18n = { i18next }>
220
-                    <StatusIndicators
221
-                        participantID = { this.id } />
222
-                </I18nextProvider>
223
-            </Provider>,
224
-            statusBarContainer);
225
-    }
226
-
227
-    /**
228
-     * Adds the element indicating the audio level of the participant.
229
-     *
230
-     * @returns {void}
231
-     */
232
-    addAudioLevelIndicator() {
233
-        let audioLevelContainer = this._getAudioLevelContainer();
234
-
235
-        if (audioLevelContainer) {
236
-            return;
237
-        }
238
-
239
-        audioLevelContainer = document.createElement('span');
240
-        audioLevelContainer.className = 'audioindicator-container';
241
-        this.container.appendChild(audioLevelContainer);
242
-        this.updateAudioLevelIndicator();
243
-    }
244
-
245
-    /**
246
-     * Removes the element indicating the audio level of the participant.
247
-     *
248
-     * @returns {void}
249
-     */
250
-    removeAudioLevelIndicator() {
251
-        const audioLevelContainer = this._getAudioLevelContainer();
252
-
253
-        if (audioLevelContainer) {
254
-            ReactDOM.unmountComponentAtNode(audioLevelContainer);
255
-        }
256
-    }
257
-
258
-    /**
259
-     * Updates the audio level for this small video.
260
-     *
261
-     * @param lvl the new audio level to set
262
-     * @returns {void}
263
-     */
264
-    updateAudioLevelIndicator(lvl = 0) {
265
-        const audioLevelContainer = this._getAudioLevelContainer();
266
-
267
-        if (audioLevelContainer) {
268
-            ReactDOM.render(<AudioLevelIndicator audioLevel = { lvl }/>, audioLevelContainer);
269
-        }
270
-    }
271
-
272
-    /**
273
-     * Queries the component's DOM for the element that should be the parent to the
274
-     * AudioLevelIndicator.
275
-     *
276
-     * @returns {HTMLElement} The DOM element that holds the AudioLevelIndicator.
143
+     * Renders the thumbnail.
277
      */
144
      */
278
-    _getAudioLevelContainer() {
279
-        return this.container.querySelector('.audioindicator-container');
145
+    renderThumbnail() {
146
+        // Should be implemented by in subclasses.
280
     }
147
     }
281
 
148
 
282
     /**
149
     /**
293
         return $($(this.container).find('video')[0]);
160
         return $($(this.container).find('video')[0]);
294
     }
161
     }
295
 
162
 
296
-    /**
297
-     * Selects the HTML image element which displays user's avatar.
298
-     *
299
-     * @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
300
-     * element which displays the user's avatar.
301
-     */
302
-    $avatar() {
303
-        return this.$container.find('.avatar-container');
304
-    }
305
-
306
-    /**
307
-     * Returns the display name element, which appears on the video thumbnail.
308
-     *
309
-     * @return {jQuery} a jQuery selector pointing to the display name element of
310
-     * the video thumbnail
311
-     */
312
-    $displayName() {
313
-        return this.$container.find('.displayNameContainer');
314
-    }
315
-
316
-    /**
317
-     * Creates or updates the participant's display name that is shown over the
318
-     * video preview.
319
-     *
320
-     * @param {Object} props - The React {@code Component} props to pass into the
321
-     * {@code DisplayName} component.
322
-     * @returns {void}
323
-     */
324
-    _renderDisplayName(props) {
325
-        const displayNameContainer = this.container.querySelector('.displayNameContainer');
326
-
327
-        if (displayNameContainer) {
328
-            ReactDOM.render(
329
-                <Provider store = { APP.store }>
330
-                    <I18nextProvider i18n = { i18next }>
331
-                        <DisplayName { ...props } />
332
-                    </I18nextProvider>
333
-                </Provider>,
334
-                displayNameContainer);
335
-        }
336
-    }
337
-
338
-    /**
339
-     * Removes the component responsible for showing the participant's display name,
340
-     * if its container is present.
341
-     *
342
-     * @returns {void}
343
-     */
344
-    removeDisplayName() {
345
-        const displayNameContainer = this.container.querySelector('.displayNameContainer');
346
-
347
-        if (displayNameContainer) {
348
-            ReactDOM.unmountComponentAtNode(displayNameContainer);
349
-        }
350
-    }
351
-
352
     /**
163
     /**
353
      * Enables / disables the css responsible for focusing/pinning a video
164
      * Enables / disables the css responsible for focusing/pinning a video
354
      * thumbnail.
165
      * thumbnail.
392
      * or <tt>false</tt> otherwise.
203
      * or <tt>false</tt> otherwise.
393
      */
204
      */
394
     isVideoPlayable() {
205
     isVideoPlayable() {
395
-        const state = APP.store.getState();
396
-        const tracks = state['features/base/tracks'];
397
-        const participant = this.id ? getParticipantById(state, this.id) : getLocalParticipant(state);
398
-        let isVideoMuted = true;
399
-
400
-        if (participant?.local) {
401
-            isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
402
-        } else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
403
-            isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, this.id);
404
-        }
405
-
406
-        return this.videoStream && !isVideoMuted && !APP.conference.isAudioOnly();
206
+        return isVideoPlayable(APP.store.getState(), this.id);
407
     }
207
     }
408
 
208
 
409
     /**
209
     /**
436
         let isScreenSharing = false;
236
         let isScreenSharing = false;
437
         let connectionStatus;
237
         let connectionStatus;
438
         const state = APP.store.getState();
238
         const state = APP.store.getState();
439
-        const participant = getParticipantById(state, this.id);
239
+        const id = this.id;
240
+        const participant = getParticipantById(state, id);
241
+        const isLocal = participant?.local ?? true;
242
+        const tracks = state['features/base/tracks'];
243
+        const videoTrack
244
+            = isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
440
 
245
 
441
         if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
246
         if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
442
-            const tracks = state['features/base/tracks'];
443
-            const track = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, this.id);
444
-
445
-            isScreenSharing = typeof track !== 'undefined' && track.videoType === 'desktop';
247
+            isScreenSharing = typeof track !== 'undefined' && videoTrack?.videoType === 'desktop';
446
             connectionStatus = participant.connectionStatus;
248
             connectionStatus = participant.connectionStatus;
447
         }
249
         }
448
 
250
 
455
             hasVideo: Boolean(this.selectVideoElement().length),
257
             hasVideo: Boolean(this.selectVideoElement().length),
456
             connectionStatus,
258
             connectionStatus,
457
             canPlayEventReceived: this._canPlayEventReceived,
259
             canPlayEventReceived: this._canPlayEventReceived,
458
-            videoStream: Boolean(this.videoStream),
260
+            videoStream: Boolean(videoTrack),
459
             isScreenSharing,
261
             isScreenSharing,
460
-            videoStreamMuted: this.videoStream ? this.videoStream.isMuted() : 'no stream'
262
+            videoStreamMuted: videoTrack ? videoTrack.muted : 'no stream'
461
         };
263
         };
462
     }
264
     }
463
 
265
 
527
         }
329
         }
528
     }
330
     }
529
 
331
 
530
-    /**
531
-     * Updates the react component displaying the avatar with the passed in avatar
532
-     * url.
533
-     *
534
-     * @returns {void}
535
-     */
536
-    initializeAvatar() {
537
-        const thumbnail = this.$avatar().get(0);
538
-
539
-        if (thumbnail) {
540
-            // Maybe add a special case for local participant, as on init of
541
-            // LocalVideo.js the id is set to "local" but will get updated later.
542
-            ReactDOM.render(
543
-                <Provider store = { APP.store }>
544
-                    <AvatarDisplay
545
-                        className = 'userAvatar'
546
-                        participantId = { this.id } />
547
-                </Provider>,
548
-                thumbnail
549
-            );
550
-        }
551
-    }
552
-
553
-    /**
554
-     * Unmounts any attached react components (particular the avatar image) from
555
-     * the avatar container.
556
-     *
557
-     * @returns {void}
558
-     */
559
-    removeAvatar() {
560
-        const thumbnail = this.$avatar().get(0);
561
-
562
-        if (thumbnail) {
563
-            ReactDOM.unmountComponentAtNode(thumbnail);
564
-        }
565
-    }
566
-
567
     /**
332
     /**
568
      * Shows or hides the dominant speaker indicator.
333
      * Shows or hides the dominant speaker indicator.
569
      * @param show whether to show or hide.
334
      * @param show whether to show or hide.
580
 
345
 
581
             return;
346
             return;
582
         }
347
         }
583
-        if (this._showDominantSpeaker === show) {
584
-            return;
585
-        }
586
 
348
 
587
-        this._showDominantSpeaker = show;
588
-        this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
589
-        this.updateIndicators();
590
-        this.updateView();
591
-    }
592
-
593
-    /**
594
-     * Shows or hides the raised hand indicator.
595
-     * @param show whether to show or hide.
596
-     */
597
-    showRaisedHandIndicator(show) {
598
-        if (!this.container) {
599
-            logger.warn(`Unable to raised hand indication - ${
600
-                this.videoSpanId} does not exist`);
601
-
602
-            return;
603
-        }
604
-
605
-        this._showRaisedHand = show;
606
-        this.updateIndicators();
349
+        this.$container.toggleClass('active-speaker', show);
607
     }
350
     }
608
 
351
 
609
     /**
352
     /**
634
      */
377
      */
635
     remove() {
378
     remove() {
636
         logger.log('Remove thumbnail', this.id);
379
         logger.log('Remove thumbnail', this.id);
637
-        this.removeAudioLevelIndicator();
638
-
639
-        const toolbarContainer
640
-            = this.container.querySelector('.videocontainer__toolbar');
641
-
642
-        if (toolbarContainer) {
643
-            ReactDOM.unmountComponentAtNode(toolbarContainer);
644
-        }
645
-
646
-        this.removeConnectionIndicator();
647
-        this.removeDisplayName();
648
-        this.removeAvatar();
649
-        this._unmountIndicators();
380
+        this._unmountThumbnail();
650
 
381
 
651
         // Remove whole container
382
         // Remove whole container
652
         if (this.container.parentNode) {
383
         if (this.container.parentNode) {
661
      * @returns {void}
392
      * @returns {void}
662
      */
393
      */
663
     rerender() {
394
     rerender() {
664
-        this.updateIndicators();
665
-        this.updateStatusBar();
666
         this.updateView();
395
         this.updateView();
667
     }
396
     }
668
 
397
 
669
-    /**
670
-     * Updates the React element responsible for showing connection status, dominant
671
-     * speaker, and raised hand icons. Uses instance variables to get the necessary
672
-     * state to display. Will create the React element if not already created.
673
-     *
674
-     * @private
675
-     * @returns {void}
676
-     */
677
-    updateIndicators() {
678
-        const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
679
-
680
-        if (!indicatorToolbar) {
681
-            return;
682
-        }
683
-
684
-        const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
685
-        const iconSize = NORMAL;
686
-        const showConnectionIndicator = this.videoIsHovered || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
687
-        const state = APP.store.getState();
688
-        const currentLayout = getCurrentLayout(state);
689
-        const participantCount = getParticipantCount(state);
690
-        let statsPopoverPosition, tooltipPosition;
691
-
692
-        if (currentLayout === LAYOUTS.TILE_VIEW) {
693
-            statsPopoverPosition = 'right top';
694
-            tooltipPosition = 'right';
695
-        } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
696
-            statsPopoverPosition = this.statsPopoverLocation;
697
-            tooltipPosition = 'left';
698
-        } else {
699
-            statsPopoverPosition = this.statsPopoverLocation;
700
-            tooltipPosition = 'top';
701
-        }
702
-
703
-        ReactDOM.render(
704
-            <Provider store = { APP.store }>
705
-                <I18nextProvider i18n = { i18next }>
706
-                    <div>
707
-                        <AtlasKitThemeProvider mode = 'dark'>
708
-                            { this._showConnectionIndicator
709
-                                ? <ConnectionIndicator
710
-                                    alwaysVisible = { showConnectionIndicator }
711
-                                    iconSize = { iconSize }
712
-                                    isLocalVideo = { this.isLocal }
713
-                                    enableStatsDisplay = { true }
714
-                                    participantId = { this.id }
715
-                                    statsPopoverPosition = { statsPopoverPosition } />
716
-                                : null }
717
-                            <RaisedHandIndicator
718
-                                iconSize = { iconSize }
719
-                                participantId = { this.id }
720
-                                tooltipPosition = { tooltipPosition } />
721
-                            { this._showDominantSpeaker && participantCount > 2
722
-                                ? <DominantSpeakerIndicator
723
-                                    iconSize = { iconSize }
724
-                                    tooltipPosition = { tooltipPosition } />
725
-                                : null }
726
-                        </AtlasKitThemeProvider>
727
-                    </div>
728
-                </I18nextProvider>
729
-            </Provider>,
730
-            indicatorToolbar
731
-        );
732
-    }
733
-
734
     /**
398
     /**
735
      * Callback invoked when the thumbnail is clicked and potentially trigger
399
      * Callback invoked when the thumbnail is clicked and potentially trigger
736
      * pinning of the participant.
400
      * pinning of the participant.
788
     }
452
     }
789
 
453
 
790
     /**
454
     /**
791
-     * Removes the React element responsible for showing connection status, dominant
792
-     * speaker, and raised hand icons.
793
-     *
794
-     * @private
795
-     * @returns {void}
455
+     * Unmounts the thumbnail.
796
      */
456
      */
797
-    _unmountIndicators() {
798
-        const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
799
-
800
-        if (indicatorToolbar) {
801
-            ReactDOM.unmountComponentAtNode(indicatorToolbar);
802
-        }
457
+    _unmountThumbnail() {
458
+        ReactDOM.unmountComponentAtNode(this.container);
803
     }
459
     }
804
 
460
 
805
     /**
461
     /**
813
         switch (layout) {
469
         switch (layout) {
814
         case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
470
         case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
815
             this.$container.css('padding-top', `${heightToWidthPercent}%`);
471
             this.$container.css('padding-top', `${heightToWidthPercent}%`);
816
-            this.$avatar().css({
817
-                height: '50%',
818
-                width: `${heightToWidthPercent / 2}%`
819
-            });
820
             break;
472
             break;
821
         }
473
         }
822
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
474
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
826
 
478
 
827
             if (typeof size !== 'undefined') {
479
             if (typeof size !== 'undefined') {
828
                 const { height, width } = size;
480
                 const { height, width } = size;
829
-                const avatarSize = height / 2;
830
 
481
 
831
                 this.$container.css({
482
                 this.$container.css({
832
                     height: `${height}px`,
483
                     height: `${height}px`,
834
                     'min-width': `${width}px`,
485
                     'min-width': `${width}px`,
835
                     width: `${width}px`
486
                     width: `${width}px`
836
                 });
487
                 });
837
-                this.$avatar().css({
838
-                    height: `${avatarSize}px`,
839
-                    width: `${avatarSize}px`
840
-                });
841
             }
488
             }
842
             break;
489
             break;
843
         }
490
         }
847
 
494
 
848
             if (typeof thumbnailSize !== 'undefined') {
495
             if (typeof thumbnailSize !== 'undefined') {
849
                 const { height, width } = thumbnailSize;
496
                 const { height, width } = thumbnailSize;
850
-                const avatarSize = height / 2;
851
 
497
 
852
                 this.$container.css({
498
                 this.$container.css({
853
                     height: `${height}px`,
499
                     height: `${height}px`,
855
                     'min-width': `${width}px`,
501
                     'min-width': `${width}px`,
856
                     width: `${width}px`
502
                     width: `${width}px`
857
                 });
503
                 });
858
-                this.$avatar().css({
859
-                    height: `${avatarSize}px`,
860
-                    width: `${avatarSize}px`
861
-                });
862
             }
504
             }
863
             break;
505
             break;
864
         }
506
         }

+ 9
- 84
modules/UI/videolayout/VideoLayout.js View File

72
         eventEmitter = emitter;
72
         eventEmitter = emitter;
73
 
73
 
74
         localVideoThumbnail = new LocalVideo(
74
         localVideoThumbnail = new LocalVideo(
75
-            VideoLayout,
76
             emitter,
75
             emitter,
77
             this._updateLargeVideoIfDisplayed.bind(this));
76
             this._updateLargeVideoIfDisplayed.bind(this));
78
 
77
 
116
      * @param lvl the new audio level to update to
115
      * @param lvl the new audio level to update to
117
      */
116
      */
118
     setAudioLevel(id, lvl) {
117
     setAudioLevel(id, lvl) {
119
-        const smallVideo = this.getSmallVideo(id);
120
-
121
-        if (smallVideo) {
122
-            smallVideo.updateAudioLevelIndicator(lvl);
123
-        }
124
-
125
         if (largeVideo && id === largeVideo.id) {
118
         if (largeVideo && id === largeVideo.id) {
126
             largeVideo.updateLargeVideoAudioLevel(lvl);
119
             largeVideo.updateLargeVideoAudioLevel(lvl);
127
         }
120
         }
137
         this._updateLargeVideoIfDisplayed(localId);
130
         this._updateLargeVideoIfDisplayed(localId);
138
     },
131
     },
139
 
132
 
140
-    /**
141
-     * Get's the localID of the conference and set it to the local video
142
-     * (small one). This needs to be called as early as possible, when muc is
143
-     * actually joined. Otherwise events can come with information like email
144
-     * and setting them assume the id is already set.
145
-     */
146
-    mucJoined() {
147
-        // FIXME: replace this call with a generic update call once SmallVideo
148
-        // only contains a ReactElement. Then remove this call once the
149
-        // Filmstrip is fully in React.
150
-        localVideoThumbnail.updateIndicators();
151
-    },
152
-
153
     /**
133
     /**
154
      * Shows/hides local video.
134
      * Shows/hides local video.
155
      * @param {boolean} true to make the local video visible, false - otherwise
135
      * @param {boolean} true to make the local video visible, false - otherwise
172
 
152
 
173
         remoteVideo.addRemoteStreamElement(stream);
153
         remoteVideo.addRemoteStreamElement(stream);
174
 
154
 
175
-        // Make sure track's muted state is reflected
176
-        if (stream.getType() !== 'audio') {
177
-            this.onVideoMute(id);
178
-            remoteVideo.updateView();
179
-        }
155
+        this.onVideoMute(id);
156
+        remoteVideo.updateView();
180
     },
157
     },
181
 
158
 
182
     onRemoteStreamRemoved(stream) {
159
     onRemoteStreamRemoved(stream) {
184
         const remoteVideo = remoteVideos[id];
161
         const remoteVideo = remoteVideos[id];
185
 
162
 
186
         // Remote stream may be removed after participant left the conference.
163
         // Remote stream may be removed after participant left the conference.
187
-
188
         if (remoteVideo) {
164
         if (remoteVideo) {
189
             remoteVideo.removeRemoteStreamElement(stream);
165
             remoteVideo.removeRemoteStreamElement(stream);
190
             remoteVideo.updateView();
166
             remoteVideo.updateView();
191
         }
167
         }
192
 
168
 
193
-        this.updateMutedForNoTracks(id, stream.getType());
169
+        this.updateVideoMutedForNoTracks(id);
194
     },
170
     },
195
 
171
 
196
     /**
172
     /**
199
      *
175
      *
200
      * If participant has no tracks will make the UI display muted status.
176
      * If participant has no tracks will make the UI display muted status.
201
      * @param {string} participantId
177
      * @param {string} participantId
202
-     * @param {string} mediaType 'audio' or 'video'
203
      */
178
      */
204
-    updateMutedForNoTracks(participantId, mediaType) {
179
+    updateVideoMutedForNoTracks(participantId) {
205
         const participant = APP.conference.getParticipantById(participantId);
180
         const participant = APP.conference.getParticipantById(participantId);
206
 
181
 
207
-        if (participant && !participant.getTracksByMediaType(mediaType).length) {
208
-            if (mediaType === 'audio') {
209
-                APP.UI.setAudioMuted(participantId, true);
210
-            } else if (mediaType === 'video') {
211
-                APP.UI.setVideoMuted(participantId);
212
-            } else {
213
-                logger.error(`Unsupported media type: ${mediaType}`);
214
-            }
182
+        if (participant && !participant.getTracksByMediaType('video').length) {
183
+            APP.UI.setVideoMuted(participantId);
215
         }
184
         }
216
     },
185
     },
217
 
186
 
279
         if (!participant || participant.local) {
248
         if (!participant || participant.local) {
280
             return;
249
             return;
281
         } else if (participant.isFakeParticipant) {
250
         } else if (participant.isFakeParticipant) {
282
-            const sharedVideoThumb = new SharedVideoThumb(
283
-                participant,
284
-                SHARED_VIDEO_CONTAINER_TYPE,
285
-                VideoLayout);
251
+            const sharedVideoThumb = new SharedVideoThumb(participant);
286
 
252
 
287
             this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
253
             this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
288
 
254
 
291
 
257
 
292
         const id = participant.id;
258
         const id = participant.id;
293
         const jitsiParticipant = APP.conference.getParticipantById(id);
259
         const jitsiParticipant = APP.conference.getParticipantById(id);
294
-        const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
260
+        const remoteVideo = new RemoteVideo(jitsiParticipant);
295
 
261
 
296
         this.addRemoteVideoContainer(id, remoteVideo);
262
         this.addRemoteVideoContainer(id, remoteVideo);
297
-
298
-        this.updateMutedForNoTracks(id, 'audio');
299
-        this.updateMutedForNoTracks(id, 'video');
263
+        this.updateVideoMutedForNoTracks(id);
300
     },
264
     },
301
 
265
 
302
     /**
266
     /**
331
         this._updateLargeVideoIfDisplayed(id, true);
295
         this._updateLargeVideoIfDisplayed(id, true);
332
     },
296
     },
333
 
297
 
334
-    /**
335
-     * Display name changed.
336
-     */
337
-    onDisplayNameChanged(id) {
338
-        if (id === 'localVideoContainer'
339
-            || APP.conference.isLocalId(id)) {
340
-            localVideoThumbnail.updateDisplayName();
341
-        } else {
342
-            const remoteVideo = remoteVideos[id];
343
-
344
-            if (remoteVideo) {
345
-                remoteVideo.updateDisplayName();
346
-            }
347
-        }
348
-    },
349
-
350
     /**
298
     /**
351
      * On dominant speaker changed event.
299
      * On dominant speaker changed event.
352
      *
300
      *
413
         }
361
         }
414
     },
362
     },
415
 
363
 
416
-    /**
417
-     * Hides all the indicators
418
-     */
419
-    hideStats() {
420
-        for (const video in remoteVideos) { // eslint-disable-line guard-for-in
421
-            const remoteVideo = remoteVideos[video];
422
-
423
-            if (remoteVideo) {
424
-                remoteVideo.removeConnectionIndicator();
425
-            }
426
-        }
427
-        localVideoThumbnail.removeConnectionIndicator();
428
-    },
429
-
430
     removeParticipantContainer(id) {
364
     removeParticipantContainer(id) {
431
         // Unlock large video
365
         // Unlock large video
432
         if (this.getPinnedId() === id) {
366
         if (this.getPinnedId() === id) {
477
     },
411
     },
478
 
412
 
479
     changeUserAvatar(id, avatarUrl) {
413
     changeUserAvatar(id, avatarUrl) {
480
-        const smallVideo = VideoLayout.getSmallVideo(id);
481
-
482
-        if (smallVideo) {
483
-            smallVideo.initializeAvatar();
484
-        } else {
485
-            logger.warn(
486
-                `Missed avatar update - no small video yet for ${id}`
487
-            );
488
-        }
489
         if (this.isCurrentlyOnLarge(id)) {
414
         if (this.isCurrentlyOnLarge(id)) {
490
             largeVideo.updateAvatar(avatarUrl);
415
             largeVideo.updateAvatar(avatarUrl);
491
         }
416
         }

+ 0
- 3
react/features/base/conference/middleware.any.js View File

128
             titleKey: 'dialog.sessTerminated'
128
             titleKey: 'dialog.sessTerminated'
129
         }));
129
         }));
130
 
130
 
131
-        if (typeof APP !== 'undefined') {
132
-            APP.UI.hideStats();
133
-        }
134
         break;
131
         break;
135
     }
132
     }
136
     case JitsiConferenceErrors.CONNECTION_ERROR: {
133
     case JitsiConferenceErrors.CONNECTION_ERROR: {

+ 216
- 0
react/features/base/media/components/web/AudioTrack.js View File

1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+
5
+/**
6
+ * The type of the React {@code Component} props of {@link AudioTrack}.
7
+ */
8
+type Props = {
9
+
10
+    /**
11
+     * The value of the id attribute of the audio element.
12
+     */
13
+    id: string,
14
+
15
+
16
+    /**
17
+     * The audio track.
18
+     */
19
+    audioTrack: ?Object,
20
+
21
+    /**
22
+     * Used to determine the value of the autoplay attribute of the underlying
23
+     * audio element.
24
+     */
25
+    autoPlay: boolean,
26
+
27
+    /**
28
+     * Represents muted property of the underlying audio element.
29
+     */
30
+    muted: ?Boolean,
31
+
32
+    /**
33
+     * Represents volume property of the underlying audio element.
34
+     */
35
+    volume: ?number,
36
+
37
+    /**
38
+     * A function that will be executed when the reference to the underlying audio element changes.
39
+     */
40
+    onAudioElementReferenceChanged: Function
41
+};
42
+
43
+/**
44
+ * The React/Web {@link Component} which is similar to and wraps around {@code HTMLAudioElement}.
45
+ */
46
+export default class AudioTrack extends Component<Props> {
47
+    /**
48
+     * Reference to the HTML audio element, stored until the file is ready.
49
+     */
50
+    _ref: ?HTMLAudioElement;
51
+
52
+    /**
53
+     * Default values for {@code AudioTrack} component's properties.
54
+     *
55
+     * @static
56
+     */
57
+    static defaultProps = {
58
+        autoPlay: true,
59
+        id: ''
60
+    };
61
+
62
+
63
+    /**
64
+     * Creates new <code>Audio</code> element instance with given props.
65
+     *
66
+     * @param {Object} props - The read-only properties with which the new
67
+     * instance is to be initialized.
68
+     */
69
+    constructor(props: Props) {
70
+        super(props);
71
+
72
+        // Bind event handlers so they are only bound once for every instance.
73
+        this._setRef = this._setRef.bind(this);
74
+    }
75
+
76
+
77
+    /**
78
+     * Attaches the audio track to the audio element and plays it.
79
+     *
80
+     * @inheritdoc
81
+     * @returns {void}
82
+     */
83
+    componentDidMount() {
84
+        this._attachTrack(this.props.audioTrack);
85
+
86
+        if (this._ref) {
87
+            const { autoPlay, muted, volume } = this.props;
88
+
89
+            if (autoPlay) {
90
+                // Ensure the audio gets play() called on it. This may be necessary in the
91
+                // case where the local video container was moved and re-attached, in which
92
+                // case the audio may not autoplay.
93
+                this._ref.play();
94
+            }
95
+
96
+            if (typeof volume === 'number') {
97
+                this._ref.volume = volume;
98
+            }
99
+
100
+            if (typeof muted === 'boolean') {
101
+                this._ref.muted = muted;
102
+            }
103
+        }
104
+    }
105
+
106
+    /**
107
+     * Remove any existing associations between the current audio track and the
108
+     * component's audio element.
109
+     *
110
+     * @inheritdoc
111
+     * @returns {void}
112
+     */
113
+    componentWillUnmount() {
114
+        this._detachTrack(this.props.audioTrack);
115
+    }
116
+
117
+    /**
118
+     * This component's updating is blackboxed from React to prevent re-rendering of the audio
119
+     * element, as we set all the properties manually.
120
+     *
121
+     * @inheritdoc
122
+     * @returns {boolean} - False is always returned to blackbox this component
123
+     * from React.
124
+     */
125
+    shouldComponentUpdate(nextProps: Props) {
126
+        const currentJitsiTrack = this.props.audioTrack && this.props.audioTrack.jitsiTrack;
127
+        const nextJitsiTrack = nextProps.audioTrack && nextProps.audioTrack.jitsiTrack;
128
+
129
+        if (currentJitsiTrack !== nextJitsiTrack) {
130
+            this._detachTrack(this.props.audioTrack);
131
+            this._attachTrack(nextProps.audioTrack);
132
+        }
133
+
134
+        if (this._ref) {
135
+            const currentVolume = this._ref.volume;
136
+            const nextVolume = nextProps.volume;
137
+
138
+            if (typeof nextVolume === 'number' && currentVolume !== nextVolume) {
139
+                this._ref.volume = nextVolume;
140
+            }
141
+
142
+            const currentMuted = this._ref.muted;
143
+            const nextMuted = nextProps.muted;
144
+
145
+            if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
146
+                this._ref.muted = nextMuted;
147
+            }
148
+        }
149
+
150
+        return false;
151
+    }
152
+
153
+    /**
154
+     * Implements React's {@link Component#render()}.
155
+     *
156
+     * @inheritdoc
157
+     * @returns {ReactElement}
158
+     */
159
+    render() {
160
+        const { autoPlay, id } = this.props;
161
+
162
+        return (
163
+            <audio
164
+                autoPlay = { autoPlay }
165
+                id = { id }
166
+                ref = { this._setRef } />
167
+        );
168
+    }
169
+
170
+    /**
171
+     * Calls into the passed in track to associate the track with the component's audio element.
172
+     *
173
+     * @param {Object} track - The redux representation of the {@code JitsiLocalTrack}.
174
+     * @private
175
+     * @returns {void}
176
+     */
177
+    _attachTrack(track) {
178
+        if (!track || !track.jitsiTrack) {
179
+            return;
180
+        }
181
+
182
+        track.jitsiTrack.attach(this._ref);
183
+    }
184
+
185
+    /**
186
+     * Removes the association to the component's audio element from the passed
187
+     * in redux representation of jitsi audio track.
188
+     *
189
+     * @param {Object} track -  The redux representation of the {@code JitsiLocalTrack}.
190
+     * @private
191
+     * @returns {void}
192
+     */
193
+    _detachTrack(track) {
194
+        if (this._ref && track && track.jitsiTrack) {
195
+            track.jitsiTrack.detach(this._ref);
196
+        }
197
+    }
198
+
199
+    _setRef: (?HTMLAudioElement) => void;
200
+
201
+    /**
202
+     * Sets the reference to the HTML audio element.
203
+     *
204
+     * @param {HTMLAudioElement} audioElement - The HTML audio element instance.
205
+     * @private
206
+     * @returns {void}
207
+     */
208
+    _setRef(audioElement: ?HTMLAudioElement) {
209
+        this._ref = audioElement;
210
+        const { onAudioElementReferenceChanged } = this.props;
211
+
212
+        if (this._ref && onAudioElementReferenceChanged) {
213
+            onAudioElementReferenceChanged({ volume: this._ref.volume });
214
+        }
215
+    }
216
+}

+ 7
- 0
react/features/base/media/components/web/Video.js View File

99
         }
99
         }
100
 
100
 
101
         this._attachTrack(this.props.videoTrack);
101
         this._attachTrack(this.props.videoTrack);
102
+
103
+        if (this._videoElement && this.props.autoPlay) {
104
+            // Ensure the video gets play() called on it. This may be necessary in the
105
+            // case where the local video container was moved and re-attached, in which
106
+            // case video does not autoplay.
107
+            this._videoElement.play();
108
+        }
102
     }
109
     }
103
 
110
 
104
     /**
111
     /**

+ 639
- 0
react/features/filmstrip/components/web/Thumbnail.js View File

1
+// @flow
2
+
3
+import { AtlasKitThemeProvider } from '@atlaskit/theme';
4
+import React, { Component } from 'react';
5
+
6
+import { AudioLevelIndicator } from '../../../audio-level-indicator';
7
+import { Avatar } from '../../../base/avatar';
8
+import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
9
+import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
10
+import AudioTrack from '../../../base/media/components/web/AudioTrack';
11
+import {
12
+    getLocalParticipant,
13
+    getParticipantById,
14
+    getParticipantCount
15
+} from '../../../base/participants';
16
+import { connect } from '../../../base/redux';
17
+import { getLocalAudioTrack, getLocalVideoTrack, getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
18
+import { ConnectionIndicator } from '../../../connection-indicator';
19
+import { DisplayName } from '../../../display-name';
20
+import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
21
+import { PresenceLabel } from '../../../presence-status';
22
+import { RemoteVideoMenuTriggerButton } from '../../../remote-video-menu';
23
+import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
24
+
25
+const JitsiTrackEvents = JitsiMeetJS.events.track;
26
+
27
+declare var interfaceConfig: Object;
28
+
29
+
30
+/**
31
+ * The type of the React {@code Component} state of {@link Thumbnail}.
32
+ */
33
+type State = {
34
+
35
+    /**
36
+     * The current audio level value for the Thumbnail.
37
+     */
38
+    audioLevel: number,
39
+
40
+    /**
41
+     * The current volume setting for the Thumbnail.
42
+     */
43
+    volume: ?number
44
+};
45
+
46
+/**
47
+ * The type of the React {@code Component} props of {@link Thumbnail}.
48
+ */
49
+type Props = {
50
+
51
+    /**
52
+     * The audio track related to the participant.
53
+     */
54
+    _audioTrack: ?Object,
55
+
56
+    /**
57
+     * Disable/enable the auto hide functionality for the connection indicator.
58
+     */
59
+    _connectionIndicatorAutoHideEnabled: boolean,
60
+
61
+    /**
62
+     * Disable/enable the connection indicator.
63
+     */
64
+    _connectionIndicatorDisabled: boolean,
65
+
66
+    /**
67
+     * The current layout of the filmstrip.
68
+     */
69
+    _currentLayout: string,
70
+
71
+    /**
72
+     * The default display name for the local participant.
73
+     */
74
+    _defaultLocalDisplayName: string,
75
+
76
+    /**
77
+     * Indicates whether the profile functionality is disabled.
78
+     */
79
+    _disableProfile: boolean,
80
+
81
+    /**
82
+     * The height of the Thumbnail.
83
+     */
84
+    _height: number,
85
+
86
+    /**
87
+     * The aspect ratio of the Thumbnail in percents.
88
+     */
89
+    _heightToWidthPercent: number,
90
+
91
+    /**
92
+     * Disable/enable the dominant speaker indicator.
93
+     */
94
+    _isDominantSpeakerDisabled: boolean,
95
+
96
+    /**
97
+     * The size of the icon of indicators.
98
+     */
99
+    _indicatorIconSize: number,
100
+
101
+    /**
102
+     * An object with information about the participant related to the thumbnaul.
103
+     */
104
+    _participant: Object,
105
+
106
+    /**
107
+     * The number of participants in the call.
108
+     */
109
+    _participantCount: number,
110
+
111
+    /**
112
+     * Indicates whether the "start silent" mode is enabled.
113
+     */
114
+    _startSilent: Boolean,
115
+
116
+     /**
117
+     * The video track that will be displayed in the thumbnail.
118
+     */
119
+    _videoTrack: ?Object,
120
+
121
+    /**
122
+     * The width of the thumbnail.
123
+     */
124
+    _width: number,
125
+
126
+    /**
127
+     * The redux dispatch function.
128
+     */
129
+    dispatch: Function,
130
+
131
+    /**
132
+     * Indicates whether the thumbnail is hovered or not.
133
+     */
134
+    isHovered: ?boolean,
135
+
136
+    /**
137
+     * The ID of the participant related to the thumbnail.
138
+     */
139
+    participantID: ?string
140
+};
141
+
142
+/**
143
+ * Implements a thumbnail.
144
+ *
145
+ * @extends Component
146
+ */
147
+class Thumbnail extends Component<Props, State> {
148
+
149
+    /**
150
+     * Initializes a new Thumbnail instance.
151
+     *
152
+     * @param {Object} props - The read-only React Component props with which
153
+     * the new instance is to be initialized.
154
+     */
155
+    constructor(props: Props) {
156
+        super(props);
157
+
158
+        this.state = {
159
+            audioLevel: 0,
160
+            volume: undefined
161
+        };
162
+
163
+        this._updateAudioLevel = this._updateAudioLevel.bind(this);
164
+        this._onVolumeChange = this._onVolumeChange.bind(this);
165
+        this._onAudioElementReferenceChanged = this._onAudioElementReferenceChanged.bind(this);
166
+    }
167
+
168
+    /**
169
+     * Starts listening for audio level updates after the initial render.
170
+     *
171
+     * @inheritdoc
172
+     * @returns {void}
173
+     */
174
+    componentDidMount() {
175
+        this._listenForAudioUpdates();
176
+    }
177
+
178
+    /**
179
+     * Stops listening for audio level updates on the old track and starts
180
+     * listening instead on the new track.
181
+     *
182
+     * @inheritdoc
183
+     * @returns {void}
184
+     */
185
+    componentDidUpdate(prevProps: Props) {
186
+        if (prevProps._audioTrack !== this.props._audioTrack) {
187
+            this._stopListeningForAudioUpdates(prevProps._audioTrack);
188
+            this._listenForAudioUpdates();
189
+            this._updateAudioLevel(0);
190
+        }
191
+    }
192
+
193
+    /**
194
+     * Unsubscribe from audio level updates.
195
+     *
196
+     * @inheritdoc
197
+     * @returns {void}
198
+     */
199
+    componentWillUnmount() {
200
+        this._stopListeningForAudioUpdates(this.props._audioTrack);
201
+    }
202
+
203
+    /**
204
+     * Starts listening for audio level updates from the library.
205
+     *
206
+     * @private
207
+     * @returns {void}
208
+     */
209
+    _listenForAudioUpdates() {
210
+        const { _audioTrack } = this.props;
211
+
212
+        if (_audioTrack) {
213
+            const { jitsiTrack } = _audioTrack;
214
+
215
+            jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
216
+        }
217
+    }
218
+
219
+    /**
220
+     * Stops listening to further updates from the passed track.
221
+     *
222
+     * @param {Object} audioTrack - The track.
223
+     * @private
224
+     * @returns {void}
225
+     */
226
+    _stopListeningForAudioUpdates(audioTrack) {
227
+        if (audioTrack) {
228
+            const { jitsiTrack } = audioTrack;
229
+
230
+            jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
231
+        }
232
+    }
233
+
234
+    _updateAudioLevel: (number) => void;
235
+
236
+    /**
237
+     * Updates the internal state of the last know audio level. The level should
238
+     * be between 0 and 1, as the level will be used as a percentage out of 1.
239
+     *
240
+     * @param {number} audioLevel - The new audio level for the track.
241
+     * @private
242
+     * @returns {void}
243
+     */
244
+    _updateAudioLevel(audioLevel) {
245
+        this.setState({
246
+            audioLevel
247
+        });
248
+    }
249
+
250
+    /**
251
+     * Returns an object with the styles for thumbnail.
252
+     *
253
+     * @returns {Object} - The styles for the thumbnail.
254
+     */
255
+    _getStyles(): Object {
256
+        const { _height, _heightToWidthPercent, _currentLayout } = this.props;
257
+        let styles;
258
+
259
+        switch (_currentLayout) {
260
+        case LAYOUTS.TILE_VIEW:
261
+        case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
262
+            const avatarSize = _height / 2;
263
+
264
+            styles = {
265
+                avatarContainer: {
266
+                    height: `${avatarSize}px`,
267
+                    width: `${avatarSize}px`
268
+                }
269
+            };
270
+            break;
271
+        }
272
+        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
273
+            styles = {
274
+                avatarContainer: {
275
+                    height: '50%',
276
+                    width: `${_heightToWidthPercent / 2}%`
277
+                }
278
+            };
279
+            break;
280
+        }
281
+        }
282
+
283
+        return styles;
284
+    }
285
+
286
+    /**
287
+     * Renders a fake participant (youtube video) thumbnail.
288
+     *
289
+     * @param {string} id - The id of the participant.
290
+     * @returns {ReactElement}
291
+     */
292
+    _renderFakeParticipant() {
293
+        const { _participant } = this.props;
294
+        const { id } = _participant;
295
+
296
+        return (
297
+            <>
298
+                <img
299
+                    className = 'sharedVideoAvatar'
300
+                    src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
301
+                <div className = 'displayNameContainer'>
302
+                    <DisplayName
303
+                        elementID = 'sharedVideoContainer_name'
304
+                        participantID = { id } />
305
+                </div>
306
+            </>
307
+        );
308
+    }
309
+
310
+    /**
311
+     * Renders the top toolbar of the thumbnail.
312
+     *
313
+     * @returns {Component}
314
+     */
315
+    _renderTopToolbar() {
316
+        const {
317
+            _connectionIndicatorAutoHideEnabled,
318
+            _connectionIndicatorDisabled,
319
+            _currentLayout,
320
+            _isDominantSpeakerDisabled,
321
+            _indicatorIconSize: iconSize,
322
+            _participant,
323
+            _participantCount,
324
+            isHovered
325
+        } = this.props;
326
+        const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
327
+        const { id, local = false, dominantSpeaker = false } = _participant;
328
+        const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
329
+        let statsPopoverPosition, tooltipPosition;
330
+
331
+        switch (_currentLayout) {
332
+        case LAYOUTS.TILE_VIEW:
333
+            statsPopoverPosition = 'right top';
334
+            tooltipPosition = 'right';
335
+            break;
336
+        case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
337
+            statsPopoverPosition = 'left top';
338
+            tooltipPosition = 'left';
339
+            break;
340
+        default:
341
+            statsPopoverPosition = 'top center';
342
+            tooltipPosition = 'top';
343
+        }
344
+
345
+        return (
346
+            <div>
347
+                <AtlasKitThemeProvider mode = 'dark'>
348
+                    { _connectionIndicatorDisabled
349
+                        ? null
350
+                        : <ConnectionIndicator
351
+                            alwaysVisible = { showConnectionIndicator }
352
+                            enableStatsDisplay = { true }
353
+                            iconSize = { iconSize }
354
+                            isLocalVideo = { local }
355
+                            participantId = { id }
356
+                            statsPopoverPosition = { statsPopoverPosition } />
357
+                    }
358
+                    <RaisedHandIndicator
359
+                        iconSize = { iconSize }
360
+                        participantId = { id }
361
+                        tooltipPosition = { tooltipPosition } />
362
+                    { showDominantSpeaker && _participantCount > 2
363
+                        ? <DominantSpeakerIndicator
364
+                            iconSize = { iconSize }
365
+                            tooltipPosition = { tooltipPosition } />
366
+                        : null }
367
+                </AtlasKitThemeProvider>
368
+            </div>);
369
+    }
370
+
371
+    /**
372
+     * Renders the avatar.
373
+     *
374
+     * @returns {ReactElement}
375
+     */
376
+    _renderAvatar() {
377
+        const { _participant } = this.props;
378
+        const { id } = _participant;
379
+        const styles = this._getStyles();
380
+
381
+        return (
382
+            <div
383
+                className = 'avatar-container'
384
+                style = { styles.avatarContainer }>
385
+                <Avatar
386
+                    className = 'userAvatar'
387
+                    participantId = { id } />
388
+            </div>
389
+        );
390
+    }
391
+
392
+    /**
393
+     * Renders the local participant's thumbnail.
394
+     *
395
+     * @returns {ReactElement}
396
+     */
397
+    _renderLocalParticipant() {
398
+        const {
399
+            _defaultLocalDisplayName,
400
+            _disableProfile,
401
+            _participant,
402
+            _videoTrack
403
+        } = this.props;
404
+        const { id } = _participant || {};
405
+        const { audioLevel = 0 } = this.state;
406
+
407
+
408
+        return (
409
+            <>
410
+                <div className = 'videocontainer__background' />
411
+                <span id = 'localVideoWrapper'>
412
+                    <VideoTrack
413
+                        id = 'localVideo_container'
414
+                        videoTrack = { _videoTrack } />
415
+                </span>
416
+                <div className = 'videocontainer__toolbar'>
417
+                    <StatusIndicators participantID = { id } />
418
+                </div>
419
+                <div className = 'videocontainer__toptoolbar'>
420
+                    { this._renderTopToolbar() }
421
+                </div>
422
+                <div className = 'videocontainer__hoverOverlay' />
423
+                <div className = 'displayNameContainer'>
424
+                    <DisplayName
425
+                        allowEditing = { !_disableProfile }
426
+                        displayNameSuffix = { _defaultLocalDisplayName }
427
+                        elementID = 'localDisplayName'
428
+                        participantID = { id } />
429
+                </div>
430
+                { this._renderAvatar() }
431
+                <span className = 'audioindicator-container'>
432
+                    <AudioLevelIndicator audioLevel = { audioLevel } />
433
+                </span>
434
+            </>
435
+        );
436
+    }
437
+
438
+
439
+    /**
440
+     * Renders a remote participant's 'thumbnail.
441
+     *
442
+     * @returns {ReactElement}
443
+     */
444
+    _renderRemoteParticipant() {
445
+        const {
446
+            _audioTrack,
447
+            _participant,
448
+            _startSilent
449
+        } = this.props;
450
+        const { id } = _participant;
451
+        const { audioLevel = 0, volume = 1 } = this.state;
452
+
453
+        // hide volume when in silent mode
454
+        const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
455
+        const { jitsiTrack } = _audioTrack ?? {};
456
+        const audioTrackId = jitsiTrack && jitsiTrack.getId();
457
+
458
+        return (
459
+            <>
460
+                {
461
+                    _audioTrack
462
+                        ? <AudioTrack
463
+                            audioTrack = { _audioTrack }
464
+                            id = { `remoteAudio_${audioTrackId}` }
465
+                            muted = { _startSilent }
466
+                            onAudioElementReferenceChanged = { this._onAudioElementReferenceChanged }
467
+                            volume = { this.state.volume } />
468
+                        : null
469
+
470
+                }
471
+                <div className = 'videocontainer__background' />
472
+                <div className = 'videocontainer__toptoolbar'>
473
+                    { this._renderTopToolbar() }
474
+                </div>
475
+                <div className = 'videocontainer__toolbar'>
476
+                    <StatusIndicators participantID = { id } />
477
+                </div>
478
+                <div className = 'videocontainer__hoverOverlay' />
479
+                <div className = 'displayNameContainer'>
480
+                    <DisplayName
481
+                        elementID = { `participant_${id}_name` }
482
+                        participantID = { id } />
483
+                </div>
484
+                { this._renderAvatar() }
485
+                <div className = 'presence-label-container'>
486
+                    <PresenceLabel
487
+                        className = 'presence-label'
488
+                        participantID = { id } />
489
+                </div>
490
+                <span className = 'remotevideomenu'>
491
+                    <AtlasKitThemeProvider mode = 'dark'>
492
+                        <RemoteVideoMenuTriggerButton
493
+                            initialVolumeValue = { volume }
494
+                            onVolumeChange = { onVolumeChange }
495
+                            participantID = { id } />
496
+                    </AtlasKitThemeProvider>
497
+                </span>
498
+                <span className = 'audioindicator-container'>
499
+                    <AudioLevelIndicator audioLevel = { audioLevel } />
500
+                </span>
501
+            </>
502
+        );
503
+    }
504
+
505
+    _onAudioElementReferenceChanged: Object => void;
506
+
507
+    /**
508
+     * Handles audio element references changes by receiving some properties from the audio element.
509
+     *
510
+     * @param {Obejct} audioElementProps - Properties of the audio element.
511
+     * @returns {void}
512
+     */
513
+    _onAudioElementReferenceChanged({ volume }) {
514
+        if (this.state.volume !== volume) {
515
+            this.setState({ volume });
516
+        }
517
+    }
518
+
519
+    _onVolumeChange: number => void;
520
+
521
+    /**
522
+     * Handles volume changes.
523
+     *
524
+     * @param {number} value - The new value for the volume.
525
+     * @returns {void}
526
+     */
527
+    _onVolumeChange(value) {
528
+        this.setState({ volume: value });
529
+    }
530
+
531
+
532
+    /**
533
+     * Implements React's {@link Component#render()}.
534
+     *
535
+     * @inheritdoc
536
+     * @returns {ReactElement}
537
+     */
538
+    render() {
539
+        const { _participant } = this.props;
540
+
541
+        if (!_participant) {
542
+            return null;
543
+        }
544
+
545
+        const { isFakeParticipant, local = false } = _participant;
546
+
547
+        if (local) {
548
+            return this._renderLocalParticipant();
549
+        }
550
+
551
+        if (isFakeParticipant) {
552
+            return this._renderFakeParticipant();
553
+        }
554
+
555
+        return this._renderRemoteParticipant();
556
+    }
557
+}
558
+
559
+/**
560
+ * Maps (parts of) the redux state to the associated props for this component.
561
+ *
562
+ * @param {Object} state - The Redux state.
563
+ * @param {Object} ownProps - The own props of the component.
564
+ * @private
565
+ * @returns {Props}
566
+ */
567
+function _mapStateToProps(state, ownProps): Object {
568
+    const { participantID } = ownProps;
569
+
570
+    // Only the local participant won't have id for the time when the conference is not yet joined.
571
+    const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
572
+    const isLocal = participant?.local ?? true;
573
+    const _videoTrack = isLocal
574
+        ? getLocalVideoTrack(state['features/base/tracks'])
575
+        : getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
576
+    const _audioTrack = isLocal
577
+        ? getLocalAudioTrack(state['features/base/tracks'])
578
+        : getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
579
+    const _currentLayout = getCurrentLayout(state);
580
+    let size = {};
581
+    const { startSilent, disableProfile = false } = state['features/base/config'];
582
+    const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
583
+
584
+
585
+    switch (_currentLayout) {
586
+    case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
587
+        const {
588
+            horizontalViewDimensions = {
589
+                local: {},
590
+                remote: {}
591
+            }
592
+        } = state['features/filmstrip'];
593
+        const { local, remote } = horizontalViewDimensions;
594
+        const { width, height } = isLocal ? local : remote;
595
+
596
+        size = {
597
+            _width: width,
598
+            _height: height
599
+        };
600
+
601
+        break;
602
+    }
603
+    case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
604
+        size = {
605
+            _heightToWidthPercent: isLocal
606
+                ? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
607
+                : 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
608
+        };
609
+        break;
610
+    case LAYOUTS.TILE_VIEW: {
611
+        const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
612
+
613
+        size = {
614
+            _width: width,
615
+            _height: height
616
+        };
617
+        break;
618
+    }
619
+    }
620
+
621
+
622
+    return {
623
+        _audioTrack,
624
+        _connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
625
+        _connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
626
+        _currentLayout,
627
+        _defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
628
+        _disableProfile: disableProfile,
629
+        _isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
630
+        _indicatorIconSize: NORMAL,
631
+        _participant: participant,
632
+        _participantCount: getParticipantCount(state),
633
+        _startSilent: Boolean(startSilent),
634
+        _videoTrack,
635
+        ...size
636
+    };
637
+}
638
+
639
+export default connect(_mapStateToProps)(Thumbnail);

+ 1
- 0
react/features/filmstrip/components/web/index.js View File

7
 export { default as RaisedHandIndicator } from './RaisedHandIndicator';
7
 export { default as RaisedHandIndicator } from './RaisedHandIndicator';
8
 export { default as StatusIndicators } from './StatusIndicators';
8
 export { default as StatusIndicators } from './StatusIndicators';
9
 export { default as VideoMutedIndicator } from './VideoMutedIndicator';
9
 export { default as VideoMutedIndicator } from './VideoMutedIndicator';
10
+export { default as Thumbnail } from './Thumbnail';

+ 43
- 0
react/features/filmstrip/functions.web.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { JitsiParticipantConnectionStatus } from '../base/lib-jitsi-meet';
4
+import { MEDIA_TYPE } from '../base/media';
3
 import {
5
 import {
6
+    getLocalParticipant,
7
+    getParticipantById,
4
     getParticipantCountWithFake,
8
     getParticipantCountWithFake,
5
     getPinnedParticipant
9
     getPinnedParticipant
6
 } from '../base/participants';
10
 } from '../base/participants';
7
 import { toState } from '../base/redux';
11
 import { toState } from '../base/redux';
12
+import {
13
+    getLocalVideoTrack,
14
+    getTrackByMediaTypeAndParticipant,
15
+    isLocalTrackMuted,
16
+    isRemoteTrackMuted
17
+} from '../base/tracks';
8
 
18
 
9
 import { TILE_ASPECT_RATIO } from './constants';
19
 import { TILE_ASPECT_RATIO } from './constants';
10
 
20
 
63
             || state['features/base/config'].disable1On1Mode);
73
             || state['features/base/config'].disable1On1Mode);
64
 }
74
 }
65
 
75
 
76
+/**
77
+ * Checks whether there is a playable video stream available for the user associated with the passed ID.
78
+ *
79
+ * @param {Object | Function} stateful - The Object or Function that can be
80
+ * resolved to a Redux state object with the toState function.
81
+ * @param {string} id - The id of the participant.
82
+ * @returns {boolean} <tt>true</tt> if there is a playable video stream available
83
+ * or <tt>false</tt> otherwise.
84
+ */
85
+export function isVideoPlayable(stateful: Object | Function, id: String) {
86
+    const state = toState(stateful);
87
+    const tracks = state['features/base/tracks'];
88
+    const participant = id ? getParticipantById(state, id) : getLocalParticipant(state);
89
+    let isVideoMuted = true;
90
+    const isLocal = participant?.local ?? true;
91
+    const { connectionStatus } = participant || {};
92
+    const videoTrack
93
+        = isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
94
+    const isAudioOnly = Boolean(state['features/base/audio-only'].enabled);
95
+    let isPlayable = false;
96
+
97
+    if (participant?.local) {
98
+        isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
99
+        isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly;
100
+    } else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
101
+        isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, id);
102
+        isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly
103
+            && connectionStatus === JitsiParticipantConnectionStatus.ACTIVE;
104
+    }
105
+
106
+    return isPlayable;
107
+}
108
+
66
 /**
109
 /**
67
  * Calculates the size for thumbnails when in horizontal view layout.
110
  * Calculates the size for thumbnails when in horizontal view layout.
68
  *
111
  *

+ 0
- 31
react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js View File

77
      */
77
      */
78
     initialVolumeValue: number,
78
     initialVolumeValue: number,
79
 
79
 
80
-    /**
81
-     * Callback to invoke when the popover has been displayed.
82
-     */
83
-    onMenuDisplay: Function,
84
-
85
     /**
80
     /**
86
      * Callback to invoke when changing the level of the participant's
81
      * Callback to invoke when changing the level of the participant's
87
      * audio element.
82
      * audio element.
111
      */
106
      */
112
     _rootElement = null;
107
     _rootElement = null;
113
 
108
 
114
-    /**
115
-     * Initializes a new {#@code RemoteVideoMenuTriggerButton} instance.
116
-     *
117
-     * @param {Object} props - The read-only properties with which the new
118
-     * instance is to be initialized.
119
-     */
120
-    constructor(props: Object) {
121
-        super(props);
122
-
123
-        // Bind event handler so it is only bound once for every instance.
124
-        this._onShowRemoteMenu = this._onShowRemoteMenu.bind(this);
125
-    }
126
-
127
     /**
109
     /**
128
      * Implements React's {@link Component#render()}.
110
      * Implements React's {@link Component#render()}.
129
      *
111
      *
140
         return (
122
         return (
141
             <Popover
123
             <Popover
142
                 content = { content }
124
                 content = { content }
143
-                onPopoverOpen = { this._onShowRemoteMenu }
144
                 position = { this.props._menuPosition }>
125
                 position = { this.props._menuPosition }>
145
                 <span
126
                 <span
146
                     className = 'popover-trigger remote-video-menu-trigger'>
127
                     className = 'popover-trigger remote-video-menu-trigger'>
153
         );
134
         );
154
     }
135
     }
155
 
136
 
156
-    _onShowRemoteMenu: () => void;
157
-
158
-    /**
159
-     * Opens the {@code RemoteVideoMenu}.
160
-     *
161
-     * @private
162
-     * @returns {void}
163
-     */
164
-    _onShowRemoteMenu() {
165
-        this.props.onMenuDisplay();
166
-    }
167
-
168
     /**
137
     /**
169
      * Creates a new {@code RemoteVideoMenu} with buttons for interacting with
138
      * Creates a new {@code RemoteVideoMenu} with buttons for interacting with
170
      * the remote participant.
139
      * the remote participant.

+ 4
- 7
react/features/video-layout/middleware.web.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
 import VideoLayout from '../../../modules/UI/videolayout/VideoLayout.js';
3
 import VideoLayout from '../../../modules/UI/videolayout/VideoLayout.js';
4
-import { CONFERENCE_JOINED, CONFERENCE_WILL_LEAVE } from '../base/conference';
4
+import { CONFERENCE_WILL_LEAVE } from '../base/conference';
5
+import { MEDIA_TYPE } from '../base/media/index.js';
5
 import {
6
 import {
6
     DOMINANT_SPEAKER_CHANGED,
7
     DOMINANT_SPEAKER_CHANGED,
7
     PARTICIPANT_JOINED,
8
     PARTICIPANT_JOINED,
33
     const result = next(action);
34
     const result = next(action);
34
 
35
 
35
     switch (action.type) {
36
     switch (action.type) {
36
-    case CONFERENCE_JOINED:
37
-        VideoLayout.mucJoined();
38
-        break;
39
-
40
     case CONFERENCE_WILL_LEAVE:
37
     case CONFERENCE_WILL_LEAVE:
41
         VideoLayout.reset();
38
         VideoLayout.reset();
42
         break;
39
         break;
77
         break;
74
         break;
78
 
75
 
79
     case TRACK_ADDED:
76
     case TRACK_ADDED:
80
-        if (!action.track.local) {
77
+        if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
81
             VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
78
             VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
82
         }
79
         }
83
 
80
 
84
         break;
81
         break;
85
     case TRACK_REMOVED:
82
     case TRACK_REMOVED:
86
-        if (!action.track.local) {
83
+        if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
87
             VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
84
             VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
88
         }
85
         }
89
 
86
 

Loading…
Cancel
Save