Bläddra i källkod

ref(Thumbnail): Create React component.

master
Hristo Terezov 5 år sedan
förälder
incheckning
51e381a0b1

+ 0
- 13
conference.js Visa fil

@@ -2014,7 +2014,6 @@ export default {
2014 2014
                             formattedDisplayName
2015 2015
                                 || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
2016 2016
                 });
2017
-                APP.UI.changeDisplayName(id, formattedDisplayName);
2018 2017
             }
2019 2018
         );
2020 2019
         room.on(
@@ -2053,10 +2052,7 @@ export default {
2053 2052
             (...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
2054 2053
 
2055 2054
         room.on(JitsiConferenceEvents.KICKED, participant => {
2056
-            APP.UI.hideStats();
2057 2055
             APP.store.dispatch(kickedOut(room, participant));
2058
-
2059
-            // FIXME close
2060 2056
         });
2061 2057
 
2062 2058
         room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
@@ -2389,11 +2385,6 @@ export default {
2389 2385
         APP.keyboardshortcut.init();
2390 2386
 
2391 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,10 +2862,6 @@ export default {
2871 2862
         APP.store.dispatch(updateSettings({
2872 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 Visa fil

@@ -7,7 +7,6 @@ import EventEmitter from 'events';
7 7
 import Logger from 'jitsi-meet-logger';
8 8
 
9 9
 import { isMobileBrowser } from '../../react/features/base/environment/utils';
10
-import { getLocalParticipant } from '../../react/features/base/participants';
11 10
 import { toggleChat } from '../../react/features/chat';
12 11
 import { setDocumentUrl } from '../../react/features/etherpad';
13 12
 import { setFilmstripVisible } from '../../react/features/filmstrip';
@@ -91,29 +90,11 @@ UI.notifyReservationError = function(code, msg) {
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 94
  * Initialize conference UI.
105 95
  */
106 96
 UI.initConference = function() {
107
-    const { getState } = APP.store;
108
-    const { id, name } = getLocalParticipant(getState);
109
-
110 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,19 +219,12 @@ UI.getSharedDocumentManager = () => etherpadManager;
238 219
  * @param {JitsiParticipant} user
239 220
  */
240 221
 UI.addUser = function(user) {
241
-    const id = user.getId();
242
-    const displayName = user.getDisplayName();
243 222
     const status = user.getStatus();
244 223
 
245 224
     if (status) {
246 225
         // FIXME: move updateUserStatus in participantPresenceChanged action
247 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,14 +416,6 @@ UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
442 416
  */
443 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 419
 UI.notifyTokenAuthFailed = function() {
454 420
     messageHandler.showError({
455 421
         descriptionKey: 'dialog.tokenAuthFailed',

+ 20
- 41
modules/UI/shared_video/SharedVideoThumb.js Visa fil

@@ -1,10 +1,15 @@
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 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,28 +18,21 @@ export default class SharedVideoThumb extends SmallVideo {
13 18
     /**
14 19
      *
15 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 24
         this.id = participant.id;
22 25
         this.isLocal = false;
23 26
         this.url = participant.id;
24 27
         this.videoSpanId = 'sharedVideoContainer';
25 28
         this.container = this.createContainer(this.videoSpanId);
26 29
         this.$container = $(this.container);
30
+        this.renderThumbnail();
27 31
         this._setThumbnailSize();
28 32
         this.bindHoverHandler();
29
-        this.updateDisplayName();
30 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 38
      * @param {*} spanId
@@ -45,18 +43,6 @@ export default class SharedVideoThumb extends SmallVideo {
45 43
         container.id = spanId;
46 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 46
         const remoteVideosContainer
61 47
             = document.getElementById('filmstripRemoteVideosContainer');
62 48
         const localVideoContainer
@@ -68,21 +54,14 @@ export default class SharedVideoThumb extends SmallVideo {
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 Visa fil

@@ -37,7 +37,6 @@ const Filmstrip = {
37 37
      */
38 38
     resizeThumbnailsForTileView(width, height, forceUpdate = false) {
39 39
         const thumbs = this._getThumbs(!forceUpdate);
40
-        const avatarSize = height / 2;
41 40
 
42 41
         if (thumbs.localThumb) {
43 42
             thumbs.localThumb.css({
@@ -58,11 +57,6 @@ const Filmstrip = {
58 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,7 +71,6 @@ const Filmstrip = {
77 71
 
78 72
         if (thumbs.localThumb) {
79 73
             const { height, width } = local;
80
-            const avatarSize = height / 2;
81 74
 
82 75
             thumbs.localThumb.css({
83 76
                 height: `${height}px`,
@@ -85,15 +78,10 @@ const Filmstrip = {
85 78
                 'min-width': `${width}px`,
86 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 83
         if (thumbs.remoteThumbs) {
95 84
             const { height, width } = remote;
96
-            const avatarSize = height / 2;
97 85
 
98 86
             thumbs.remoteThumbs.css({
99 87
                 height: `${height}px`,
@@ -101,10 +89,6 @@ const Filmstrip = {
101 89
                 'min-width': `${width}px`,
102 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,10 +110,6 @@ const Filmstrip = {
126 110
                 'min-width': '',
127 111
                 'min-height': ''
128 112
             });
129
-            $('#localVideoContainer > .avatar-container').css({
130
-                height: '50%',
131
-                width: `${heightToWidthPercent / 2}%`
132
-            });
133 113
         }
134 114
 
135 115
         if (thumbs.remoteThumbs) {
@@ -142,10 +122,6 @@ const Filmstrip = {
142 122
                 'min-width': '',
143 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 Visa fil

@@ -1,35 +1,34 @@
1
-/* global $, config, interfaceConfig, APP */
1
+/* global $, config, APP */
2 2
 
3
-import Logger from 'jitsi-meet-logger';
4 3
 /* eslint-disable no-unused-vars */
5 4
 import React, { Component } from 'react';
6 5
 import ReactDOM from 'react-dom';
6
+import { I18nextProvider } from 'react-i18next';
7 7
 import { Provider } from 'react-redux';
8 8
 
9
+import { i18next } from '../../../react/features/base/i18n';
9 10
 import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
10 11
 import { VideoTrack } from '../../../react/features/base/media';
11 12
 import { updateSettings } from '../../../react/features/base/settings';
12 13
 import { getLocalVideoTrack } from '../../../react/features/base/tracks';
14
+import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
13 15
 import { shouldDisplayTileView } from '../../../react/features/video-layout';
14 16
 /* eslint-enable no-unused-vars */
15 17
 import UIEvents from '../../../service/UI/UIEvents';
16 18
 
17 19
 import SmallVideo from './SmallVideo';
18 20
 
19
-const logger = Logger.getLogger(__filename);
20
-
21 21
 /**
22 22
  *
23 23
  */
24 24
 export default class LocalVideo extends SmallVideo {
25 25
     /**
26 26
      *
27
-     * @param {*} VideoLayout
28 27
      * @param {*} emitter
29 28
      * @param {*} streamEndedCallback
30 29
      */
31
-    constructor(VideoLayout, emitter, streamEndedCallback) {
32
-        super(VideoLayout);
30
+    constructor(emitter, streamEndedCallback) {
31
+        super();
33 32
         this.videoSpanId = 'localVideoContainer';
34 33
         this.streamEndedCallback = streamEndedCallback;
35 34
         this.container = this.createContainer();
@@ -37,6 +36,7 @@ export default class LocalVideo extends SmallVideo {
37 36
         this.isLocal = true;
38 37
         this._setThumbnailSize();
39 38
         this.updateDOMLocation();
39
+        this.renderThumbnail();
40 40
 
41 41
         this.localVideoId = null;
42 42
         this.bindHoverHandler();
@@ -44,7 +44,6 @@ export default class LocalVideo extends SmallVideo {
44 44
             this._buildContextMenu();
45 45
         }
46 46
         this.emitter = emitter;
47
-        this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center';
48 47
 
49 48
         Object.defineProperty(this, 'id', {
50 49
             get() {
@@ -53,18 +52,6 @@ export default class LocalVideo extends SmallVideo {
53 52
         });
54 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 55
         this.container.onclick = this._onContainerClick;
69 56
     }
70 57
 
@@ -77,38 +64,19 @@ export default class LocalVideo extends SmallVideo {
77 64
         containerSpan.classList.add('videocontainer');
78 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 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,9 +84,7 @@ export default class LocalVideo extends SmallVideo {
116 84
      * @param {*} stream
117 85
      */
118 86
     changeVideo(stream) {
119
-        this.videoStream = stream;
120 87
         this.localVideoId = `localVideo_${stream.getId()}`;
121
-        this._updateVideoElement();
122 88
 
123 89
         // eslint-disable-next-line eqeqeq
124 90
         const isVideo = stream.videoType != 'desktop';
@@ -128,17 +94,6 @@ export default class LocalVideo extends SmallVideo {
128 94
         this.setFlipX(isVideo ? settings.localFlipX : false);
129 95
 
130 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 97
             this._notifyOfStreamEnded();
143 98
             stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
144 99
         };
@@ -254,35 +209,5 @@ export default class LocalVideo extends SmallVideo {
254 209
             : document.getElementById('filmstripLocalVideoThumbnail');
255 210
 
256 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 Visa fil

@@ -1,4 +1,4 @@
1
-/* global $, APP, interfaceConfig */
1
+/* global $, APP, config */
2 2
 
3 3
 /* eslint-disable no-unused-vars */
4 4
 import { AtlasKitThemeProvider } from '@atlaskit/theme';
@@ -15,6 +15,7 @@ import {
15 15
 import { getParticipantById } from '../../../react/features/base/participants';
16 16
 import { isTestModeEnabled } from '../../../react/features/base/testing';
17 17
 import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
18
+import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
18 19
 import { PresenceLabel } from '../../../react/features/presence-status';
19 20
 import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
20 21
 import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
@@ -44,16 +45,6 @@ function createContainer(spanId) {
44 45
     container.id = spanId;
45 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 48
     const remoteVideosContainer
58 49
         = document.getElementById('filmstripRemoteVideosContainer');
59 50
     const localVideoContainer
@@ -72,21 +63,16 @@ export default class RemoteVideo extends SmallVideo {
72 63
      * Creates new instance of the <tt>RemoteVideo</tt>.
73 64
      * @param user {JitsiParticipant} the user for whom remote video instance will
74 65
      * be created.
75
-     * @param {VideoLayout} VideoLayout the video layout instance.
76 66
      * @constructor
77 67
      */
78
-    constructor(user, VideoLayout) {
79
-        super(VideoLayout);
68
+    constructor(user) {
69
+        super();
80 70
 
81 71
         this.user = user;
82 72
         this.id = user.getId();
83 73
         this.videoSpanId = `participant_${this.id}`;
84 74
 
85
-        this._audioStreamElement = null;
86
-        this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
87 75
         this.addRemoteVideoContainer();
88
-        this.updateIndicators();
89
-        this.updateDisplayName();
90 76
         this.bindHoverHandler();
91 77
         this.flipX = false;
92 78
         this.isLocal = false;
@@ -100,11 +86,6 @@ export default class RemoteVideo extends SmallVideo {
100 86
          */
101 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 89
         this.container.onclick = this._onContainerClick;
109 90
     }
110 91
 
@@ -114,76 +95,23 @@ export default class RemoteVideo extends SmallVideo {
114 95
     addRemoteVideoContainer() {
115 96
         this.container = createContainer(this.videoSpanId);
116 97
         this.$container = $(this.container);
117
-        this.initializeAvatar();
98
+        this.renderThumbnail();
118 99
         this._setThumbnailSize();
119 100
         this.initBrowserSpecificProperties();
120
-        this.updateRemoteVideoMenu();
121
-        this.updateStatusBar();
122
-        this.addAudioLevelIndicator();
123
-        this.addPresenceLabel();
124 101
 
125 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 109
         ReactDOM.render(
149 110
             <Provider store = { APP.store }>
150 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 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,7 +127,7 @@ export default class RemoteVideo extends SmallVideo {
199 127
         }
200 128
 
201 129
         const isVideo = stream.isVideoTrack();
202
-        const elementID = SmallVideo.getStreamElementID(stream);
130
+        const elementID = `remoteVideo_${stream.getId()}`;
203 131
         const select = $(`#${elementID}`);
204 132
 
205 133
         select.remove();
@@ -207,11 +135,7 @@ export default class RemoteVideo extends SmallVideo {
207 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 140
         this.updateView();
217 141
     }
@@ -223,14 +147,7 @@ export default class RemoteVideo extends SmallVideo {
223 147
      * @override
224 148
      */
225 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,9 +162,8 @@ export default class RemoteVideo extends SmallVideo {
245 162
      * Removes RemoteVideo from the page.
246 163
      */
247 164
     remove() {
165
+        ReactDOM.unmountComponentAtNode(this.container);
248 166
         super.remove();
249
-        this.removePresenceLabel();
250
-        this.removeRemoteVideoMenu();
251 167
     }
252 168
 
253 169
     /**
@@ -295,19 +211,16 @@ export default class RemoteVideo extends SmallVideo {
295 211
 
296 212
         const isVideo = stream.isVideoTrack();
297 213
 
298
-        if (isVideo) {
299
-            this.videoStream = stream;
300
-        } else {
301
-            this.audioStream = stream;
302
-        }
303
-
304 214
         if (!stream.getOriginalStream()) {
305 215
             logger.debug('Remote video stream has no original stream');
306 216
 
307 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 225
         // Put new stream element always in front
313 226
         streamElement = UIUtils.prependChild(this.container, streamElement);
@@ -315,14 +228,7 @@ export default class RemoteVideo extends SmallVideo {
315 228
         this.waitForPlayback(streamElement, stream);
316 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 233
             const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
328 234
 
@@ -331,72 +237,4 @@ export default class RemoteVideo extends SmallVideo {
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 Visa fil

@@ -1,4 +1,4 @@
1
-/* global $, APP, config, interfaceConfig */
1
+/* global $, APP, interfaceConfig */
2 2
 
3 3
 /* eslint-disable no-unused-vars */
4 4
 import { AtlasKitThemeProvider } from '@atlaskit/theme';
@@ -21,6 +21,7 @@ import {
21 21
     pinParticipant
22 22
 } from '../../../react/features/base/participants';
23 23
 import {
24
+    getLocalVideoTrack,
24 25
     getTrackByMediaTypeAndParticipant,
25 26
     isLocalTrackMuted,
26 27
     isRemoteTrackMuted
@@ -29,6 +30,7 @@ import { ConnectionIndicator } from '../../../react/features/connection-indicato
29 30
 import { DisplayName } from '../../../react/features/display-name';
30 31
 import {
31 32
     DominantSpeakerIndicator,
33
+    isVideoPlayable,
32 34
     RaisedHandIndicator,
33 35
     StatusIndicators
34 36
 } from '../../../react/features/filmstrip';
@@ -89,37 +91,10 @@ export default class SmallVideo {
89 91
     /**
90 92
      * Constructor.
91 93
      */
92
-    constructor(VideoLayout) {
93
-        this.videoStream = null;
94
-        this.audioStream = null;
95
-        this.VideoLayout = VideoLayout;
94
+    constructor() {
96 95
         this.videoIsHovered = false;
97 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 98
         // Bind event handlers so they are only bound once for every instance.
124 99
         this.updateView = this.updateView.bind(this);
125 100
 
@@ -145,33 +120,6 @@ export default class SmallVideo {
145 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 124
      * Configures hoverIn/hoverOut handlers. Depends on connection indicator.
177 125
      */
@@ -180,103 +128,22 @@ export default class SmallVideo {
180 128
         this.$container.hover(
181 129
             () => {
182 130
                 this.videoIsHovered = true;
131
+                this.renderThumbnail(true);
183 132
                 this.updateView();
184
-                this.updateIndicators();
185 133
             },
186 134
             () => {
187 135
                 this.videoIsHovered = false;
136
+                this.renderThumbnail(false);
188 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,62 +160,6 @@ export default class SmallVideo {
293 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 164
      * Enables / disables the css responsible for focusing/pinning a video
354 165
      * thumbnail.
@@ -392,18 +203,7 @@ export default class SmallVideo {
392 203
      * or <tt>false</tt> otherwise.
393 204
      */
394 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,13 +236,15 @@ export default class SmallVideo {
436 236
         let isScreenSharing = false;
437 237
         let connectionStatus;
438 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 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 248
             connectionStatus = participant.connectionStatus;
447 249
         }
448 250
 
@@ -455,9 +257,9 @@ export default class SmallVideo {
455 257
             hasVideo: Boolean(this.selectVideoElement().length),
456 258
             connectionStatus,
457 259
             canPlayEventReceived: this._canPlayEventReceived,
458
-            videoStream: Boolean(this.videoStream),
260
+            videoStream: Boolean(videoTrack),
459 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,43 +329,6 @@ export default class SmallVideo {
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 333
      * Shows or hides the dominant speaker indicator.
569 334
      * @param show whether to show or hide.
@@ -580,30 +345,8 @@ export default class SmallVideo {
580 345
 
581 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,19 +377,7 @@ export default class SmallVideo {
634 377
      */
635 378
     remove() {
636 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 382
         // Remove whole container
652 383
         if (this.container.parentNode) {
@@ -661,76 +392,9 @@ export default class SmallVideo {
661 392
      * @returns {void}
662 393
      */
663 394
     rerender() {
664
-        this.updateIndicators();
665
-        this.updateStatusBar();
666 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 399
      * Callback invoked when the thumbnail is clicked and potentially trigger
736 400
      * pinning of the participant.
@@ -788,18 +452,10 @@ export default class SmallVideo {
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,10 +469,6 @@ export default class SmallVideo {
813 469
         switch (layout) {
814 470
         case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
815 471
             this.$container.css('padding-top', `${heightToWidthPercent}%`);
816
-            this.$avatar().css({
817
-                height: '50%',
818
-                width: `${heightToWidthPercent / 2}%`
819
-            });
820 472
             break;
821 473
         }
822 474
         case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
@@ -826,7 +478,6 @@ export default class SmallVideo {
826 478
 
827 479
             if (typeof size !== 'undefined') {
828 480
                 const { height, width } = size;
829
-                const avatarSize = height / 2;
830 481
 
831 482
                 this.$container.css({
832 483
                     height: `${height}px`,
@@ -834,10 +485,6 @@ export default class SmallVideo {
834 485
                     'min-width': `${width}px`,
835 486
                     width: `${width}px`
836 487
                 });
837
-                this.$avatar().css({
838
-                    height: `${avatarSize}px`,
839
-                    width: `${avatarSize}px`
840
-                });
841 488
             }
842 489
             break;
843 490
         }
@@ -847,7 +494,6 @@ export default class SmallVideo {
847 494
 
848 495
             if (typeof thumbnailSize !== 'undefined') {
849 496
                 const { height, width } = thumbnailSize;
850
-                const avatarSize = height / 2;
851 497
 
852 498
                 this.$container.css({
853 499
                     height: `${height}px`,
@@ -855,10 +501,6 @@ export default class SmallVideo {
855 501
                     'min-width': `${width}px`,
856 502
                     width: `${width}px`
857 503
                 });
858
-                this.$avatar().css({
859
-                    height: `${avatarSize}px`,
860
-                    width: `${avatarSize}px`
861
-                });
862 504
             }
863 505
             break;
864 506
         }

+ 9
- 84
modules/UI/videolayout/VideoLayout.js Visa fil

@@ -72,7 +72,6 @@ const VideoLayout = {
72 72
         eventEmitter = emitter;
73 73
 
74 74
         localVideoThumbnail = new LocalVideo(
75
-            VideoLayout,
76 75
             emitter,
77 76
             this._updateLargeVideoIfDisplayed.bind(this));
78 77
 
@@ -116,12 +115,6 @@ const VideoLayout = {
116 115
      * @param lvl the new audio level to update to
117 116
      */
118 117
     setAudioLevel(id, lvl) {
119
-        const smallVideo = this.getSmallVideo(id);
120
-
121
-        if (smallVideo) {
122
-            smallVideo.updateAudioLevelIndicator(lvl);
123
-        }
124
-
125 118
         if (largeVideo && id === largeVideo.id) {
126 119
             largeVideo.updateLargeVideoAudioLevel(lvl);
127 120
         }
@@ -137,19 +130,6 @@ const VideoLayout = {
137 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 134
      * Shows/hides local video.
155 135
      * @param {boolean} true to make the local video visible, false - otherwise
@@ -172,11 +152,8 @@ const VideoLayout = {
172 152
 
173 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 159
     onRemoteStreamRemoved(stream) {
@@ -184,13 +161,12 @@ const VideoLayout = {
184 161
         const remoteVideo = remoteVideos[id];
185 162
 
186 163
         // Remote stream may be removed after participant left the conference.
187
-
188 164
         if (remoteVideo) {
189 165
             remoteVideo.removeRemoteStreamElement(stream);
190 166
             remoteVideo.updateView();
191 167
         }
192 168
 
193
-        this.updateMutedForNoTracks(id, stream.getType());
169
+        this.updateVideoMutedForNoTracks(id);
194 170
     },
195 171
 
196 172
     /**
@@ -199,19 +175,12 @@ const VideoLayout = {
199 175
      *
200 176
      * If participant has no tracks will make the UI display muted status.
201 177
      * @param {string} participantId
202
-     * @param {string} mediaType 'audio' or 'video'
203 178
      */
204
-    updateMutedForNoTracks(participantId, mediaType) {
179
+    updateVideoMutedForNoTracks(participantId) {
205 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,10 +248,7 @@ const VideoLayout = {
279 248
         if (!participant || participant.local) {
280 249
             return;
281 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 253
             this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
288 254
 
@@ -291,12 +257,10 @@ const VideoLayout = {
291 257
 
292 258
         const id = participant.id;
293 259
         const jitsiParticipant = APP.conference.getParticipantById(id);
294
-        const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
260
+        const remoteVideo = new RemoteVideo(jitsiParticipant);
295 261
 
296 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,22 +295,6 @@ const VideoLayout = {
331 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 299
      * On dominant speaker changed event.
352 300
      *
@@ -413,20 +361,6 @@ const VideoLayout = {
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 364
     removeParticipantContainer(id) {
431 365
         // Unlock large video
432 366
         if (this.getPinnedId() === id) {
@@ -477,15 +411,6 @@ const VideoLayout = {
477 411
     },
478 412
 
479 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 414
         if (this.isCurrentlyOnLarge(id)) {
490 415
             largeVideo.updateAvatar(avatarUrl);
491 416
         }

+ 0
- 3
react/features/base/conference/middleware.any.js Visa fil

@@ -128,9 +128,6 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
128 128
             titleKey: 'dialog.sessTerminated'
129 129
         }));
130 130
 
131
-        if (typeof APP !== 'undefined') {
132
-            APP.UI.hideStats();
133
-        }
134 131
         break;
135 132
     }
136 133
     case JitsiConferenceErrors.CONNECTION_ERROR: {

+ 216
- 0
react/features/base/media/components/web/AudioTrack.js Visa fil

@@ -0,0 +1,216 @@
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 Visa fil

@@ -99,6 +99,13 @@ class Video extends Component<Props> {
99 99
         }
100 100
 
101 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 Visa fil

@@ -0,0 +1,639 @@
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 Visa fil

@@ -7,3 +7,4 @@ export { default as ModeratorIndicator } from './ModeratorIndicator';
7 7
 export { default as RaisedHandIndicator } from './RaisedHandIndicator';
8 8
 export { default as StatusIndicators } from './StatusIndicators';
9 9
 export { default as VideoMutedIndicator } from './VideoMutedIndicator';
10
+export { default as Thumbnail } from './Thumbnail';

+ 43
- 0
react/features/filmstrip/functions.web.js Visa fil

@@ -1,10 +1,20 @@
1 1
 // @flow
2 2
 
3
+import { JitsiParticipantConnectionStatus } from '../base/lib-jitsi-meet';
4
+import { MEDIA_TYPE } from '../base/media';
3 5
 import {
6
+    getLocalParticipant,
7
+    getParticipantById,
4 8
     getParticipantCountWithFake,
5 9
     getPinnedParticipant
6 10
 } from '../base/participants';
7 11
 import { toState } from '../base/redux';
12
+import {
13
+    getLocalVideoTrack,
14
+    getTrackByMediaTypeAndParticipant,
15
+    isLocalTrackMuted,
16
+    isRemoteTrackMuted
17
+} from '../base/tracks';
8 18
 
9 19
 import { TILE_ASPECT_RATIO } from './constants';
10 20
 
@@ -63,6 +73,39 @@ export function shouldRemoteVideosBeVisible(state: Object) {
63 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 110
  * Calculates the size for thumbnails when in horizontal view layout.
68 111
  *

+ 0
- 31
react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js Visa fil

@@ -77,11 +77,6 @@ type Props = {
77 77
      */
78 78
     initialVolumeValue: number,
79 79
 
80
-    /**
81
-     * Callback to invoke when the popover has been displayed.
82
-     */
83
-    onMenuDisplay: Function,
84
-
85 80
     /**
86 81
      * Callback to invoke when changing the level of the participant's
87 82
      * audio element.
@@ -111,19 +106,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
111 106
      */
112 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 110
      * Implements React's {@link Component#render()}.
129 111
      *
@@ -140,7 +122,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
140 122
         return (
141 123
             <Popover
142 124
                 content = { content }
143
-                onPopoverOpen = { this._onShowRemoteMenu }
144 125
                 position = { this.props._menuPosition }>
145 126
                 <span
146 127
                     className = 'popover-trigger remote-video-menu-trigger'>
@@ -153,18 +134,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
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 138
      * Creates a new {@code RemoteVideoMenu} with buttons for interacting with
170 139
      * the remote participant.

+ 4
- 7
react/features/video-layout/middleware.web.js Visa fil

@@ -1,7 +1,8 @@
1 1
 // @flow
2 2
 
3 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 6
 import {
6 7
     DOMINANT_SPEAKER_CHANGED,
7 8
     PARTICIPANT_JOINED,
@@ -33,10 +34,6 @@ MiddlewareRegistry.register(store => next => action => {
33 34
     const result = next(action);
34 35
 
35 36
     switch (action.type) {
36
-    case CONFERENCE_JOINED:
37
-        VideoLayout.mucJoined();
38
-        break;
39
-
40 37
     case CONFERENCE_WILL_LEAVE:
41 38
         VideoLayout.reset();
42 39
         break;
@@ -77,13 +74,13 @@ MiddlewareRegistry.register(store => next => action => {
77 74
         break;
78 75
 
79 76
     case TRACK_ADDED:
80
-        if (!action.track.local) {
77
+        if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
81 78
             VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
82 79
         }
83 80
 
84 81
         break;
85 82
     case TRACK_REMOVED:
86
-        if (!action.track.local) {
83
+        if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
87 84
             VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
88 85
         }
89 86
 

Laddar…
Avbryt
Spara