浏览代码

Merge pull request #938 from jitsi/participant_conn_status

Adds participant connection status notifications
master
hristoterezov 9 年前
父节点
当前提交
2a8700bca3

+ 52
- 1
conference.js 查看文件

691
     isConnectionInterrupted () {
691
     isConnectionInterrupted () {
692
         return connectionIsInterrupted;
692
         return connectionIsInterrupted;
693
     },
693
     },
694
+    /**
695
+     * Finds JitsiParticipant for given id.
696
+     *
697
+     * @param {string} id participant's identifier(MUC nickname).
698
+     *
699
+     * @returns {JitsiParticipant|null} participant instance for given id or
700
+     * null if not found.
701
+     */
702
+    getParticipantById (id) {
703
+        return room ? room.getParticipantById(id) : null;
704
+    },
705
+    /**
706
+     * Checks whether the user identified by given id is currently connected.
707
+     *
708
+     * @param {string} id participant's identifier(MUC nickname)
709
+     *
710
+     * @returns {boolean|null} true if participant's connection is ok or false
711
+     * if the user is having connectivity issues.
712
+     */
713
+    isParticipantConnectionActive (id) {
714
+        let participant = this.getParticipantById(id);
715
+        return participant ? participant.isConnectionActive() : null;
716
+    },
717
+    /**
718
+     * Gets the display name foe the <tt>JitsiParticipant</tt> identified by
719
+     * the given <tt>id</tt>.
720
+     *
721
+     * @param id {string} the participant's id(MUC nickname/JVB endpoint id)
722
+     *
723
+     * @return {string} the participant's display name or the default string if
724
+     * absent.
725
+     */
726
+    getParticipantDisplayName (id) {
727
+        let displayName = getDisplayName(id);
728
+        if (displayName) {
729
+            return displayName;
730
+        } else {
731
+            if (APP.conference.isLocalId(id)) {
732
+                return APP.translation.generateTranslationHTML(
733
+                    interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
734
+            } else {
735
+                return interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
736
+            }
737
+        }
738
+    },
694
     getMyUserId () {
739
     getMyUserId () {
695
         return this._room
740
         return this._room
696
             && this._room.myUserId();
741
             && this._room.myUserId();
1085
 
1130
 
1086
             console.log('USER %s connnected', id, user);
1131
             console.log('USER %s connnected', id, user);
1087
             APP.API.notifyUserJoined(id);
1132
             APP.API.notifyUserJoined(id);
1088
-            APP.UI.addUser(id, user.getDisplayName());
1133
+            APP.UI.addUser(user);
1089
 
1134
 
1090
             // check the roles for the new user and reflect them
1135
             // check the roles for the new user and reflect them
1091
             APP.UI.updateUserRole(user);
1136
             APP.UI.updateUserRole(user);
1174
             ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
1219
             ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
1175
             APP.UI.handleLastNEndpoints(ids, enteringIds);
1220
             APP.UI.handleLastNEndpoints(ids, enteringIds);
1176
         });
1221
         });
1222
+        room.on(
1223
+            ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED, (id, isActive) => {
1224
+            APP.UI.participantConnectionStatusChanged(id, isActive);
1225
+        });
1177
         room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
1226
         room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
1178
             if (this.isLocalId(id)) {
1227
             if (this.isLocalId(id)) {
1179
                 this.isDominantSpeaker = true;
1228
                 this.isDominantSpeaker = true;
1205
         room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
1254
         room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
1206
             connectionIsInterrupted = true;
1255
             connectionIsInterrupted = true;
1207
             ConnectionQuality.updateLocalConnectionQuality(0);
1256
             ConnectionQuality.updateLocalConnectionQuality(0);
1257
+            APP.UI.showLocalConnectionInterrupted(true);
1208
         });
1258
         });
1209
 
1259
 
1210
         room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
1260
         room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
1211
             connectionIsInterrupted = false;
1261
             connectionIsInterrupted = false;
1262
+            APP.UI.showLocalConnectionInterrupted(false);
1212
         });
1263
         });
1213
 
1264
 
1214
         room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
1265
         room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {

+ 39
- 1
css/_videolayout_default.scss 查看文件

233
     overflow: hidden;
233
     overflow: hidden;
234
 }
234
 }
235
 
235
 
236
+.connection.connection_lost
237
+{
238
+    color: #8B8B8B;
239
+    overflow: visible;
240
+}
241
+
236
 .connection.connection_full
242
 .connection.connection_full
237
 {
243
 {
238
     color: #FFFFFF;/*#15A1ED*/
244
     color: #FFFFFF;/*#15A1ED*/
456
     filter: grayscale(.5) opacity(0.8);
462
     filter: grayscale(.5) opacity(0.8);
457
 }
463
 }
458
 
464
 
465
+.remoteVideoProblemFilter {
466
+    -webkit-filter: grayscale(100%);
467
+    filter: grayscale(100%);
468
+}
469
+
459
 .videoProblemFilter {
470
 .videoProblemFilter {
460
     -webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
471
     -webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
461
     filter: blur(10px) grayscale(.5) opacity(0.8);
472
     filter: blur(10px) grayscale(.5) opacity(0.8);
462
 }
473
 }
463
 
474
 
464
-#videoConnectionMessage {
475
+.videoThumbnailProblemFilter {
476
+    -webkit-filter: grayscale(100%);
477
+    filter: grayscale(100%);
478
+}
479
+
480
+#remoteConnectionMessage {
481
+    display: none;
482
+    position: absolute;
483
+    width: auto;
484
+    z-index: 1011;
485
+    font-weight: 600;
486
+    font-size: 14px;
487
+    text-align: center;
488
+    color: #FFF;
489
+    opacity: .80;
490
+    text-shadow:    0px 0px 1px rgba(0,0,0,0.3),
491
+                    0px 1px 1px rgba(0,0,0,0.3),
492
+                    1px 0px 1px rgba(0,0,0,0.3),
493
+                    0px 0px 1px rgba(0,0,0,0.3);
494
+
495
+    background: rgba(0,0,0,.5);
496
+    border-radius: 5px;
497
+    padding: 5px;
498
+    padding-left: 10px;
499
+    padding-right: 10px;
500
+}
501
+
502
+#localConnectionMessage {
465
     display: none;
503
     display: none;
466
     position: absolute;
504
     position: absolute;
467
     width: 100%;
505
     width: 100%;

+ 2
- 1
index.html 查看文件

228
                     <img id="dominantSpeakerAvatar" src=""/>
228
                     <img id="dominantSpeakerAvatar" src=""/>
229
                     <canvas id="dominantSpeakerAudioLevel"></canvas>
229
                     <canvas id="dominantSpeakerAudioLevel"></canvas>
230
                 </div>
230
                 </div>
231
+                <span id="remoteConnectionMessage"></span>
231
                 <div id="largeVideoWrapper">
232
                 <div id="largeVideoWrapper">
232
                     <video id="largeVideo" muted="true" autoplay></video>
233
                     <video id="largeVideo" muted="true" autoplay></video>
233
                 </div>
234
                 </div>
234
-                <span id="videoConnectionMessage"></span>
235
+                <span id="localConnectionMessage"></span>
235
                 <span id="videoResolutionLabel">HD</span>
236
                 <span id="videoResolutionLabel">HD</span>
236
                 <span id="recordingLabel" class="centeredVideoLabel">
237
                 <span id="recordingLabel" class="centeredVideoLabel">
237
                     <span id="recordingLabelText"></span>
238
                     <span id="recordingLabelText"></span>

+ 2
- 1
lang/main.json 查看文件

324
         "ATTACHED": "Attached",
324
         "ATTACHED": "Attached",
325
         "FETCH_SESSION_ID": "Obtaining session-id...",
325
         "FETCH_SESSION_ID": "Obtaining session-id...",
326
         "GOT_SESSION_ID": "Obtaining session-id... Done",
326
         "GOT_SESSION_ID": "Obtaining session-id... Done",
327
-        "GET_SESSION_ID_ERROR": "Get session-id error: "
327
+        "GET_SESSION_ID_ERROR": "Get session-id error: ",
328
+        "USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues..."
328
     },
329
     },
329
     "recording":
330
     "recording":
330
     {
331
     {

+ 27
- 4
modules/UI/UI.js 查看文件

261
     }
261
     }
262
 };
262
 };
263
 
263
 
264
+/**
265
+ * Shows/hides the indication about local connection being interrupted.
266
+ *
267
+ * @param {boolean} isInterrupted <tt>true</tt> if local connection is
268
+ * currently in the interrupted state or <tt>false</tt> if the connection
269
+ * is fine.
270
+ */
271
+UI.showLocalConnectionInterrupted = function (isInterrupted) {
272
+    VideoLayout.showLocalConnectionInterrupted(isInterrupted);
273
+};
274
+
264
 /**
275
 /**
265
  * Sets the "raised hand" status for a participant.
276
  * Sets the "raised hand" status for a participant.
266
  */
277
  */
602
 
613
 
603
 /**
614
 /**
604
  * Show user on UI.
615
  * Show user on UI.
605
- * @param {string} id user id
606
- * @param {string} displayName user nickname
616
+ * @param {JitsiParticipant} user
607
  */
617
  */
608
-UI.addUser = function (id, displayName) {
618
+UI.addUser = function (user) {
619
+    var id = user.getId();
620
+    var displayName = user.getDisplayName();
609
     UI.hideRingOverLay();
621
     UI.hideRingOverLay();
610
     ContactList.addContact(id);
622
     ContactList.addContact(id);
611
 
623
 
618
         UIUtil.playSoundNotification('userJoined');
630
         UIUtil.playSoundNotification('userJoined');
619
 
631
 
620
     // Add Peer's container
632
     // Add Peer's container
621
-    VideoLayout.addParticipantContainer(id);
633
+    VideoLayout.addParticipantContainer(user);
622
 
634
 
623
     // Configure avatar
635
     // Configure avatar
624
     UI.setUserEmail(id);
636
     UI.setUserEmail(id);
983
     VideoLayout.onLastNEndpointsChanged(ids, enteringIds);
995
     VideoLayout.onLastNEndpointsChanged(ids, enteringIds);
984
 };
996
 };
985
 
997
 
998
+/**
999
+ * Will handle notification about participant's connectivity status change.
1000
+ *
1001
+ * @param {string} id the id of remote participant(MUC jid)
1002
+ * @param {boolean} isActive true if the connection is ok or false if the user
1003
+ * is having connectivity issues.
1004
+ */
1005
+UI.participantConnectionStatusChanged = function (id, isActive) {
1006
+    VideoLayout.onParticipantConnectionStatusChanged(id, isActive);
1007
+};
1008
+
986
 /**
1009
 /**
987
  * Update audio level visualization for specified user.
1010
  * Update audio level visualization for specified user.
988
  * @param {string} id user id
1011
  * @param {string} id user id

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

243
 
243
 
244
             let thumb = new SharedVideoThumb(self.url);
244
             let thumb = new SharedVideoThumb(self.url);
245
             thumb.setDisplayName(player.getVideoData().title);
245
             thumb.setDisplayName(player.getVideoData().title);
246
-            VideoLayout.addParticipantContainer(self.url, thumb);
246
+            VideoLayout.addRemoteVideoContainer(self.url, thumb);
247
 
247
 
248
             let iframe = player.getIframe();
248
             let iframe = player.getIframe();
249
             self.sharedVideo = new SharedVideoContainer(
249
             self.sharedVideo = new SharedVideoContainer(

+ 37
- 11
modules/UI/videolayout/ConnectionIndicator.js 查看文件

245
 };
245
 };
246
 
246
 
247
 
247
 
248
-function createIcon(classes) {
248
+function createIcon(classes, iconClass) {
249
     var icon = document.createElement("span");
249
     var icon = document.createElement("span");
250
     for(var i in classes) {
250
     for(var i in classes) {
251
         icon.classList.add(classes[i]);
251
         icon.classList.add(classes[i]);
252
     }
252
     }
253
     icon.appendChild(
253
     icon.appendChild(
254
-        document.createElement("i")).classList.add("icon-connection");
254
+        document.createElement("i")).classList.add(iconClass);
255
     return icon;
255
     return icon;
256
 }
256
 }
257
 
257
 
282
     }.bind(this);
282
     }.bind(this);
283
 
283
 
284
     this.emptyIcon = this.connectionIndicatorContainer.appendChild(
284
     this.emptyIcon = this.connectionIndicatorContainer.appendChild(
285
-        createIcon(["connection", "connection_empty"]));
285
+        createIcon(["connection", "connection_empty"], "icon-connection"));
286
     this.fullIcon = this.connectionIndicatorContainer.appendChild(
286
     this.fullIcon = this.connectionIndicatorContainer.appendChild(
287
-        createIcon(["connection", "connection_full"]));
287
+        createIcon(["connection", "connection_full"], "icon-connection"));
288
+    this.interruptedIndicator = this.connectionIndicatorContainer.appendChild(
289
+        createIcon(["connection", "connection_lost"],"icon-connection-lost"));
290
+    $(this.interruptedIndicator).hide();
288
 };
291
 };
289
 
292
 
290
 /**
293
 /**
298
     this.popover.forceHide();
301
     this.popover.forceHide();
299
 };
302
 };
300
 
303
 
304
+/**
305
+ * Updates the UI which displays warning about user's connectivity problems.
306
+ *
307
+ * @param {boolean} isActive true if the connection is working fine or false if
308
+ * the user is having connectivity issues.
309
+ */
310
+ConnectionIndicator.prototype.updateConnectionStatusIndicator
311
+= function (isActive) {
312
+    this.isConnectionActive = isActive;
313
+    if (this.isConnectionActive) {
314
+        $(this.interruptedIndicator).hide();
315
+        $(this.emptyIcon).show();
316
+        $(this.fullIcon).show();
317
+    } else {
318
+        $(this.interruptedIndicator).show();
319
+        $(this.emptyIcon).hide();
320
+        $(this.fullIcon).hide();
321
+        this.updateConnectionQuality(0 /* zero bars */);
322
+    }
323
+};
324
+
301
 /**
325
 /**
302
  * Updates the data of the indicator
326
  * Updates the data of the indicator
303
  * @param percent the percent of connection quality
327
  * @param percent the percent of connection quality
314
             this.connectionIndicatorContainer.style.display = "block";
338
             this.connectionIndicatorContainer.style.display = "block";
315
         }
339
         }
316
     }
340
     }
317
-    this.bandwidth = object.bandwidth;
318
-    this.bitrate = object.bitrate;
319
-    this.packetLoss = object.packetLoss;
320
-    this.transport = object.transport;
321
-    if (object.resolution) {
322
-        this.resolution = object.resolution;
341
+    if (object) {
342
+        this.bandwidth = object.bandwidth;
343
+        this.bitrate = object.bitrate;
344
+        this.packetLoss = object.packetLoss;
345
+        this.transport = object.transport;
346
+        if (object.resolution) {
347
+            this.resolution = object.resolution;
348
+        }
323
     }
349
     }
324
     for (var quality in ConnectionIndicator.connectionQualityValues) {
350
     for (var quality in ConnectionIndicator.connectionQualityValues) {
325
         if (percent >= quality) {
351
         if (percent >= quality) {
327
                 ConnectionIndicator.connectionQualityValues[quality];
353
                 ConnectionIndicator.connectionQualityValues[quality];
328
         }
354
         }
329
     }
355
     }
330
-    if (object.isResolutionHD) {
356
+    if (object && typeof object.isResolutionHD === 'boolean') {
331
         this.isResolutionHD = object.isResolutionHD;
357
         this.isResolutionHD = object.isResolutionHD;
332
     }
358
     }
333
     this.updateResolutionIndicator();
359
     this.updateResolutionIndicator();

+ 157
- 25
modules/UI/videolayout/LargeVideoManager.js 查看文件

5
 import {createDeferred} from '../../util/helpers';
5
 import {createDeferred} from '../../util/helpers';
6
 import UIUtil from "../util/UIUtil";
6
 import UIUtil from "../util/UIUtil";
7
 import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
7
 import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
8
+import LargeContainer from "./LargeContainer";
8
 
9
 
9
 /**
10
 /**
10
  * Manager for all Large containers.
11
  * Manager for all Large containers.
11
  */
12
  */
12
 export default class LargeVideoManager {
13
 export default class LargeVideoManager {
13
     constructor (emitter) {
14
     constructor (emitter) {
15
+        /**
16
+         * The map of <tt>LargeContainer</tt>s where the key is the video
17
+         * container type.
18
+         * @type {Object.<string, LargeContainer>}
19
+         */
14
         this.containers = {};
20
         this.containers = {};
15
 
21
 
16
         this.state = VIDEO_CONTAINER_TYPE;
22
         this.state = VIDEO_CONTAINER_TYPE;
85
      * Called when the media connection has been interrupted.
91
      * Called when the media connection has been interrupted.
86
      */
92
      */
87
     onVideoInterrupted () {
93
     onVideoInterrupted () {
88
-        this.enableVideoProblemFilter(true);
89
-        let reconnectingKey = "connection.RECONNECTING";
90
-        $('#videoConnectionMessage')
91
-            .attr("data-i18n", reconnectingKey)
92
-            .text(APP.translation.translateString(reconnectingKey));
94
+        this.enableLocalConnectionProblemFilter(true);
95
+        this._setLocalConnectionMessage("connection.RECONNECTING")
93
         // Show the message only if the video is currently being displayed
96
         // Show the message only if the video is currently being displayed
94
-        this.showVideoConnectionMessage(this.state === VIDEO_CONTAINER_TYPE);
97
+        this.showLocalConnectionMessage(this.state === VIDEO_CONTAINER_TYPE);
95
     }
98
     }
96
 
99
 
97
     /**
100
     /**
98
      * Called when the media connection has been restored.
101
      * Called when the media connection has been restored.
99
      */
102
      */
100
     onVideoRestored () {
103
     onVideoRestored () {
101
-        this.enableVideoProblemFilter(false);
102
-        this.showVideoConnectionMessage(false);
104
+        this.enableLocalConnectionProblemFilter(false);
105
+        this.showLocalConnectionMessage(false);
103
     }
106
     }
104
 
107
 
105
     get id () {
108
     get id () {
118
 
121
 
119
         // Include hide()/fadeOut only if we're switching between users
122
         // Include hide()/fadeOut only if we're switching between users
120
         let preUpdate;
123
         let preUpdate;
121
-        if (this.newStreamData.id != this.id) {
124
+        let isUserSwitch = this.newStreamData.id != this.id;
125
+        if (isUserSwitch) {
122
             preUpdate = container.hide();
126
             preUpdate = container.hide();
123
         } else {
127
         } else {
124
             preUpdate = Promise.resolve();
128
             preUpdate = Promise.resolve();
136
             // change the avatar url on large
140
             // change the avatar url on large
137
             this.updateAvatar(Avatar.getAvatarUrl(id));
141
             this.updateAvatar(Avatar.getAvatarUrl(id));
138
 
142
 
143
+            // FIXME that does not really make sense, because the videoType
144
+            // (camera or desktop) is a completely different thing than
145
+            // the video container type (Etherpad, SharedVideo, VideoContainer).
146
+            // ----------------------------------------------------------------
139
             // If we the continer is VIDEO_CONTAINER_TYPE, we need to check
147
             // If we the continer is VIDEO_CONTAINER_TYPE, we need to check
140
             // its stream whether exist and is muted to set isVideoMuted
148
             // its stream whether exist and is muted to set isVideoMuted
141
             // in rest of the cases it is false
149
             // in rest of the cases it is false
142
-            let isVideoMuted = false;
150
+            let showAvatar = false;
143
             if (videoType == VIDEO_CONTAINER_TYPE)
151
             if (videoType == VIDEO_CONTAINER_TYPE)
144
-                isVideoMuted = stream ? stream.isMuted() : true;
145
-
146
-            // show the avatar on large if needed
147
-            container.showAvatar(isVideoMuted);
152
+                showAvatar = stream ? stream.isMuted() : true;
153
+
154
+            // If the user's connection is disrupted then the avatar will be
155
+            // displayed in case we have no video image cached. That is if
156
+            // there was a user switch(image is lost on stream detach) or if
157
+            // the video was not rendered, before the connection has failed.
158
+            let isHavingConnectivityIssues
159
+                = APP.conference.isParticipantConnectionActive(id) === false;
160
+            if (isHavingConnectivityIssues
161
+                    && (isUserSwitch | !container.wasVideoRendered)) {
162
+                showAvatar = true;
163
+            }
148
 
164
 
149
             let promise;
165
             let promise;
150
 
166
 
151
             // do not show stream if video is muted
167
             // do not show stream if video is muted
152
             // but we still should show watermark
168
             // but we still should show watermark
153
-            if (isVideoMuted) {
169
+            if (showAvatar) {
154
                 this.showWatermark(true);
170
                 this.showWatermark(true);
155
-                promise = Promise.resolve();
171
+                // If the intention of this switch is to show the avatar
172
+                // we need to make sure that the video is hidden
173
+                promise = container.hide();
156
             } else {
174
             } else {
157
                 promise = container.show();
175
                 promise = container.show();
158
             }
176
             }
159
 
177
 
178
+            // show the avatar on large if needed
179
+            container.showAvatar(showAvatar);
180
+
181
+            // Make sure no notification about remote failure is shown as
182
+            // it's UI conflicts with the one for local connection interrupted.
183
+            if (APP.conference.isConnectionInterrupted()) {
184
+                this.updateParticipantConnStatusIndication(id, true);
185
+            } else {
186
+                this.updateParticipantConnStatusIndication(
187
+                    id, !isHavingConnectivityIssues);
188
+            }
189
+
160
             // resolve updateLargeVideo promise after everything is done
190
             // resolve updateLargeVideo promise after everything is done
161
             promise.then(resolve);
191
             promise.then(resolve);
162
 
192
 
169
         });
199
         });
170
     }
200
     }
171
 
201
 
202
+    /**
203
+     * Shows/hides notification about participant's connectivity issues to be
204
+     * shown on the large video area.
205
+     *
206
+     * @param {string} id the id of remote participant(MUC nickname)
207
+     * @param {boolean} isConnected true if the connection is active or false
208
+     * when the user is having connectivity issues.
209
+     *
210
+     * @private
211
+     */
212
+    updateParticipantConnStatusIndication (id, isConnected) {
213
+
214
+        // Apply grey filter on the large video
215
+        this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected);
216
+
217
+        if (isConnected) {
218
+            // Hide the message
219
+            this.showRemoteConnectionMessage(false);
220
+        } else {
221
+            // Get user's display name
222
+            let displayName
223
+                = APP.conference.getParticipantDisplayName(id);
224
+            this._setRemoteConnectionMessage(
225
+                "connection.USER_CONNECTION_INTERRUPTED",
226
+                { displayName: displayName });
227
+
228
+            // Show it now only if the VideoContainer is on top
229
+            this.showRemoteConnectionMessage(
230
+                this.state === VIDEO_CONTAINER_TYPE);
231
+        }
232
+    }
233
+
172
     /**
234
     /**
173
      * Update large video.
235
      * Update large video.
174
      * Switches to large video even if previously other container was visible.
236
      * Switches to large video even if previously other container was visible.
229
     }
291
     }
230
 
292
 
231
     /**
293
     /**
232
-     * Enables/disables the filter indicating a video problem to the user.
294
+     * Enables/disables the filter indicating a video problem to the user caused
295
+     * by the problems with local media connection.
233
      *
296
      *
234
      * @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
297
      * @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
235
      */
298
      */
236
-    enableVideoProblemFilter (enable) {
237
-        this.videoContainer.enableVideoProblemFilter(enable);
299
+    enableLocalConnectionProblemFilter (enable) {
300
+        this.videoContainer.enableLocalConnectionProblemFilter(enable);
238
     }
301
     }
239
 
302
 
240
     /**
303
     /**
253
     }
316
     }
254
 
317
 
255
     /**
318
     /**
256
-     * Shows/hides the "video connection message".
319
+     * Shows/hides the message indicating problems with local media connection.
257
      * @param {boolean|null} show(optional) tells whether the message is to be
320
      * @param {boolean|null} show(optional) tells whether the message is to be
258
      * displayed or not. If missing the condition will be based on the value
321
      * displayed or not. If missing the condition will be based on the value
259
      * obtained from {@link APP.conference.isConnectionInterrupted}.
322
      * obtained from {@link APP.conference.isConnectionInterrupted}.
260
      */
323
      */
261
-    showVideoConnectionMessage (show) {
324
+    showLocalConnectionMessage (show) {
262
         if (typeof show !== 'boolean') {
325
         if (typeof show !== 'boolean') {
263
             show = APP.conference.isConnectionInterrupted();
326
             show = APP.conference.isConnectionInterrupted();
264
         }
327
         }
265
 
328
 
266
         if (show) {
329
         if (show) {
267
-            $('#videoConnectionMessage').css({display: "block"});
330
+            $('#localConnectionMessage').css({display: "block"});
331
+            // Avatar message conflicts with 'videoConnectionMessage',
332
+            // so it must be hidden
333
+            this.showRemoteConnectionMessage(false);
268
         } else {
334
         } else {
269
-            $('#videoConnectionMessage').css({display: "none"});
335
+            $('#localConnectionMessage').css({display: "none"});
270
         }
336
         }
271
     }
337
     }
272
 
338
 
339
+    /**
340
+     * Shows hides the "avatar" message which is to be displayed either in
341
+     * the middle of the screen or below the avatar image.
342
+     *
343
+     * @param {null|boolean} show (optional) <tt>true</tt> to show the avatar
344
+     * message or <tt>false</tt> to hide it. If not provided then the connection
345
+     * status of the user currently on the large video will be obtained form
346
+     * "APP.conference" and the message will be displayed if the user's
347
+     * connection is interrupted.
348
+     */
349
+    showRemoteConnectionMessage (show) {
350
+        if (typeof show !== 'boolean') {
351
+            show = APP.conference.isParticipantConnectionActive(this.id);
352
+        }
353
+
354
+        if (show) {
355
+            $('#remoteConnectionMessage').css({display: "block"});
356
+            // 'videoConnectionMessage' message conflicts with 'avatarMessage',
357
+            // so it must be hidden
358
+            this.showLocalConnectionMessage(false);
359
+        } else {
360
+            $('#remoteConnectionMessage').hide();
361
+        }
362
+    }
363
+
364
+    /**
365
+     * Updates the text which describes that the remote user is having
366
+     * connectivity issues.
367
+     *
368
+     * @param {string} msgKey the translation key which will be used to get
369
+     * the message text.
370
+     * @param {object} msgOptions translation options object.
371
+     *
372
+     * @private
373
+     */
374
+    _setRemoteConnectionMessage (msgKey, msgOptions) {
375
+        if (msgKey) {
376
+            let text = APP.translation.translateString(msgKey, msgOptions);
377
+            $('#remoteConnectionMessage')
378
+                .attr("data-i18n", msgKey).text(text);
379
+        }
380
+
381
+        this.videoContainer.positionRemoteConnectionMessage();
382
+    }
383
+
384
+    /**
385
+     * Updated the text which is to be shown on the top of large video, when
386
+     * local media connection is interrupted.
387
+     *
388
+     * @param {string} msgKey the translation key which will be used to get
389
+     * the message text to be displayed on the large video.
390
+     * @param {object} msgOptions translation options object
391
+     *
392
+     * @private
393
+     */
394
+    _setLocalConnectionMessage (msgKey, msgOptions) {
395
+        $('#localConnectionMessage')
396
+            .attr("data-i18n", msgKey)
397
+            .text(APP.translation.translateString(msgKey, msgOptions));
398
+    }
399
+
273
     /**
400
     /**
274
      * Add container of specified type.
401
      * Add container of specified type.
275
      * @param {string} type container type
402
      * @param {string} type container type
328
         // be taking care of it by itself, but that is a bigger refactoring
455
         // be taking care of it by itself, but that is a bigger refactoring
329
         if (this.state === VIDEO_CONTAINER_TYPE) {
456
         if (this.state === VIDEO_CONTAINER_TYPE) {
330
             this.showWatermark(false);
457
             this.showWatermark(false);
331
-            this.showVideoConnectionMessage(false);
458
+            this.showLocalConnectionMessage(false);
459
+            this.showRemoteConnectionMessage(false);
332
         }
460
         }
333
         oldContainer.hide();
461
         oldContainer.hide();
334
 
462
 
342
                 // the container would be taking care of it by itself, but that
470
                 // the container would be taking care of it by itself, but that
343
                 // is a bigger refactoring
471
                 // is a bigger refactoring
344
                 this.showWatermark(true);
472
                 this.showWatermark(true);
345
-                this.showVideoConnectionMessage(/* fetch the current state */);
473
+                // "avatar" and "video connection" can not be displayed both
474
+                // at the same time, but the latter is of higher priority and it
475
+                // will hide the avatar one if will be displayed.
476
+                this.showRemoteConnectionMessage(/* fet the current state */);
477
+                this.showLocalConnectionMessage(/* fetch the current state */);
346
             }
478
             }
347
         });
479
         });
348
     }
480
     }

+ 1
- 1
modules/UI/videolayout/LocalVideo.js 查看文件

201
         localVideoContainer.removeChild(localVideo);
201
         localVideoContainer.removeChild(localVideo);
202
         // when removing only the video element and we are on stage
202
         // when removing only the video element and we are on stage
203
         // update the stage
203
         // update the stage
204
-        if(this.VideoLayout.isCurrentlyOnLarge(this.id))
204
+        if(this.isCurrentlyOnLargeVideo())
205
             this.VideoLayout.updateLargeVideo(this.id);
205
             this.VideoLayout.updateLargeVideo(this.id);
206
         stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
206
         stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
207
     };
207
     };

+ 142
- 11
modules/UI/videolayout/RemoteVideo.js 查看文件

8
 import UIEvents from '../../../service/UI/UIEvents';
8
 import UIEvents from '../../../service/UI/UIEvents';
9
 import JitsiPopover from "../util/JitsiPopover";
9
 import JitsiPopover from "../util/JitsiPopover";
10
 
10
 
11
-function RemoteVideo(id, VideoLayout, emitter) {
12
-    this.id = id;
11
+/**
12
+ * Creates new instance of the <tt>RemoteVideo</tt>.
13
+ * @param user {JitsiParticipant} the user for whom remote video instance will
14
+ * be created.
15
+ * @param {VideoLayout} VideoLayout the video layout instance.
16
+ * @param {EventEmitter} emitter the event emitter which will be used by
17
+ * the new instance to emit events.
18
+ * @constructor
19
+ */
20
+function RemoteVideo(user, VideoLayout, emitter) {
21
+    this.user = user;
22
+    this.id = user.getId();
13
     this.emitter = emitter;
23
     this.emitter = emitter;
14
-    this.videoSpanId = `participant_${id}`;
24
+    this.videoSpanId = `participant_${this.id}`;
15
     SmallVideo.call(this, VideoLayout);
25
     SmallVideo.call(this, VideoLayout);
16
     this.hasRemoteVideoMenu = false;
26
     this.hasRemoteVideoMenu = false;
17
     this.addRemoteVideoContainer();
27
     this.addRemoteVideoContainer();
18
-    this.connectionIndicator = new ConnectionIndicator(this, id);
28
+    this.connectionIndicator = new ConnectionIndicator(this, this.id);
19
     this.setDisplayName();
29
     this.setDisplayName();
20
     this.flipX = false;
30
     this.flipX = false;
21
     this.isLocal = false;
31
     this.isLocal = false;
32
+    /**
33
+     * The flag is set to <tt>true</tt> after the 'onplay' event has been
34
+     * triggered on the current video element. It goes back to <tt>false</tt>
35
+     * when the stream is removed. It is used to determine whether the video
36
+     * playback has ever started.
37
+     * @type {boolean}
38
+     */
39
+    this.wasVideoPlayed = false;
40
+    /**
41
+     * The flag is set to <tt>true</tt> if remote participant's video gets muted
42
+     * during his media connection disruption. This is to prevent black video
43
+     * being render on the thumbnail, because even though once the video has
44
+     * been played the image usually remains on the video element it seems that
45
+     * after longer period of the video element being hidden this image can be
46
+     * lost.
47
+     * @type {boolean}
48
+     */
49
+    this.mutedWhileDisconnected = false;
22
 }
50
 }
23
 
51
 
24
 RemoteVideo.prototype = Object.create(SmallVideo.prototype);
52
 RemoteVideo.prototype = Object.create(SmallVideo.prototype);
162
     }
190
     }
163
 };
191
 };
164
 
192
 
193
+/**
194
+ * @inheritDoc
195
+ */
196
+RemoteVideo.prototype.setMutedView = function(isMuted) {
197
+    SmallVideo.prototype.setMutedView.call(this, isMuted);
198
+    // Update 'mutedWhileDisconnected' flag
199
+    this._figureOutMutedWhileDisconnected(this.isConnectionActive() === false);
200
+}
201
+
202
+/**
203
+ * Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
204
+ * account remote participant's network connectivity and video muted status.
205
+ *
206
+ * @param {boolean} isDisconnected <tt>true</tt> if the remote participant is
207
+ * currently having connectivity issues or <tt>false</tt> otherwise.
208
+ *
209
+ * @private
210
+ */
211
+RemoteVideo.prototype._figureOutMutedWhileDisconnected
212
+= function(isDisconnected) {
213
+    if (isDisconnected && this.isVideoMuted) {
214
+        this.mutedWhileDisconnected = true;
215
+    } else if (!isDisconnected && !this.isVideoMuted) {
216
+        this.mutedWhileDisconnected = false;
217
+    }
218
+}
219
+
165
 /**
220
 /**
166
  * Adds the remote video menu element for the given <tt>id</tt> in the
221
  * Adds the remote video menu element for the given <tt>id</tt> in the
167
  * given <tt>parentElement</tt>.
222
  * given <tt>parentElement</tt>.
209
     var select = $('#' + elementID);
264
     var select = $('#' + elementID);
210
     select.remove();
265
     select.remove();
211
 
266
 
267
+    if (isVideo) {
268
+        this.wasVideoPlayed = false;
269
+    }
270
+
212
     console.info((isVideo ? "Video" : "Audio") +
271
     console.info((isVideo ? "Video" : "Audio") +
213
                  " removed " + this.id, select);
272
                  " removed " + this.id, select);
214
 
273
 
215
     // when removing only the video element and we are on stage
274
     // when removing only the video element and we are on stage
216
     // update the stage
275
     // update the stage
217
-    if (isVideo && this.VideoLayout.isCurrentlyOnLarge(this.id))
276
+    if (isVideo && this.isCurrentlyOnLargeVideo())
218
         this.VideoLayout.updateLargeVideo(this.id);
277
         this.VideoLayout.updateLargeVideo(this.id);
278
+    else
279
+        // Missing video stream will affect display mode
280
+        this.updateView();
281
+};
282
+
283
+/**
284
+ * Checks whether the remote user associated with this <tt>RemoteVideo</tt>
285
+ * has connectivity issues.
286
+ *
287
+ * @return {boolean} <tt>true</tt> if the user's connection is fine or
288
+ * <tt>false</tt> otherwise.
289
+ */
290
+RemoteVideo.prototype.isConnectionActive = function() {
291
+    return this.user.isConnectionActive();
292
+};
293
+
294
+/**
295
+ * The remote video is considered "playable" once the stream has started
296
+ * according to the {@link #hasVideoStarted} result.
297
+ *
298
+ * @inheritdoc
299
+ * @override
300
+ */
301
+RemoteVideo.prototype.isVideoPlayable = function () {
302
+    return SmallVideo.prototype.isVideoPlayable.call(this)
303
+        && this.hasVideoStarted() && !this.mutedWhileDisconnected;
304
+};
305
+
306
+/**
307
+ * @inheritDoc
308
+ */
309
+RemoteVideo.prototype.updateView = function () {
310
+
311
+    this.updateConnectionStatusIndicator(
312
+        null /* will obtain the status from 'conference' */);
313
+
314
+    // This must be called after 'updateConnectionStatusIndicator' because it
315
+    // affects the display mode by modifying 'mutedWhileDisconnected' flag
316
+    SmallVideo.prototype.updateView.call(this);
317
+};
318
+
319
+/**
320
+ * Updates the UI to reflect user's connectivity status.
321
+ * @param isActive {boolean|null} 'true' if user's connection is active or
322
+ * 'false' when the use is having some connectivity issues and a warning
323
+ * should be displayed. When 'null' is passed then the current value will be
324
+ * obtained from the conference instance.
325
+ */
326
+RemoteVideo.prototype.updateConnectionStatusIndicator = function (isActive) {
327
+    // Check for initial value if 'isActive' is not defined
328
+    if (typeof isActive !== "boolean") {
329
+        isActive = this.isConnectionActive();
330
+        if (isActive === null) {
331
+            // Cancel processing at this point - no update
332
+            return;
333
+        }
334
+    }
335
+
336
+    console.debug(this.id + " thumbnail is connection active ? " + isActive);
337
+
338
+    // Update 'mutedWhileDisconnected' flag
339
+    this._figureOutMutedWhileDisconnected(!isActive);
340
+
341
+    if(this.connectionIndicator)
342
+        this.connectionIndicator.updateConnectionStatusIndicator(isActive);
343
+
344
+    // Toggle thumbnail video problem filter
345
+    this.selectVideoElement().toggleClass(
346
+        "videoThumbnailProblemFilter", !isActive);
347
+    this.$avatar().toggleClass(
348
+        "videoThumbnailProblemFilter", !isActive);
219
 };
349
 };
220
 
350
 
221
 /**
351
 /**
246
     // Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
376
     // Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
247
     // when video playback starts
377
     // when video playback starts
248
     var onPlayingHandler = function () {
378
     var onPlayingHandler = function () {
379
+        self.wasVideoPlayed = true;
249
         self.VideoLayout.videoactive(streamElement, self.id);
380
         self.VideoLayout.videoactive(streamElement, self.id);
250
         streamElement.onplaying = null;
381
         streamElement.onplaying = null;
382
+        // Refresh to show the video
383
+        self.updateView();
251
     };
384
     };
252
     streamElement.onplaying = onPlayingHandler;
385
     streamElement.onplaying = onPlayingHandler;
253
 };
386
 };
254
 
387
 
255
 /**
388
 /**
256
- * Checks whether or not video stream exists and has started for this
257
- * RemoteVideo instance. This is checked by trying to select video element in
258
- * this container and checking if 'currentTime' field's value is greater than 0.
389
+ * Checks whether the video stream has started for this RemoteVideo instance.
259
  *
390
  *
260
- * @returns {*|boolean} true if this RemoteVideo has active video stream running
391
+ * @returns {boolean} true if this RemoteVideo has a video stream for which
392
+ * the playback has been started.
261
  */
393
  */
262
 RemoteVideo.prototype.hasVideoStarted = function () {
394
 RemoteVideo.prototype.hasVideoStarted = function () {
263
-    var videoSelector = this.selectVideoElement();
264
-    return videoSelector.length && videoSelector[0].currentTime > 0;
395
+    return this.wasVideoPlayed;
265
 };
396
 };
266
 
397
 
267
 RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
398
 RemoteVideo.prototype.addRemoteStreamElement = function (stream) {

+ 85
- 31
modules/UI/videolayout/SmallVideo.js 查看文件

5
 
5
 
6
 const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
6
 const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
7
 
7
 
8
+/**
9
+ * Display mode constant used when video is being displayed on the small video.
10
+ * @type {number}
11
+ * @constant
12
+ */
13
+const DISPLAY_VIDEO = 0;
14
+/**
15
+ * Display mode constant used when the user's avatar is being displayed on
16
+ * the small video.
17
+ * @type {number}
18
+ * @constant
19
+ */
20
+const DISPLAY_AVATAR = 1;
21
+/**
22
+ * Display mode constant used when neither video nor avatar is being displayed
23
+ * on the small video.
24
+ * @type {number}
25
+ * @constant
26
+ */
27
+const DISPLAY_BLACKNESS = 2;
28
+
8
 function SmallVideo(VideoLayout) {
29
 function SmallVideo(VideoLayout) {
9
     this.isAudioMuted = false;
30
     this.isAudioMuted = false;
10
     this.hasAvatar = false;
31
     this.hasAvatar = false;
337
     return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0]));
358
     return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0]));
338
 };
359
 };
339
 
360
 
361
+/**
362
+ * Selects the HTML image element which displays user's avatar.
363
+ *
364
+ * @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
365
+ * element which displays the user's avatar.
366
+ */
367
+SmallVideo.prototype.$avatar = function () {
368
+    return $('#' + this.videoSpanId + ' .userAvatar');
369
+};
370
+
340
 /**
371
 /**
341
  * Enables / disables the css responsible for focusing/pinning a video
372
  * Enables / disables the css responsible for focusing/pinning a video
342
  * thumbnail.
373
  * thumbnail.
359
     return this.selectVideoElement().length !== 0;
390
     return this.selectVideoElement().length !== 0;
360
 };
391
 };
361
 
392
 
393
+/**
394
+ * Checks whether the user associated with this <tt>SmallVideo</tt> is currently
395
+ * being displayed on the "large video".
396
+ *
397
+ * @return {boolean} <tt>true</tt> if the user is displayed on the large video
398
+ * or <tt>false</tt> otherwise.
399
+ */
400
+SmallVideo.prototype.isCurrentlyOnLargeVideo = function () {
401
+    return this.VideoLayout.isCurrentlyOnLarge(this.id);
402
+};
403
+
404
+/**
405
+ * Checks whether there is a playable video stream available for the user
406
+ * associated with this <tt>SmallVideo</tt>.
407
+ *
408
+ * @return {boolean} <tt>true</tt> if there is a playable video stream available
409
+ * or <tt>false</tt> otherwise.
410
+ */
411
+SmallVideo.prototype.isVideoPlayable = function() {
412
+    return this.videoStream // Is there anything to display ?
413
+        && !this.isVideoMuted && !this.videoStream.isMuted() // Muted ?
414
+        && (this.isLocal || this.VideoLayout.isInLastN(this.id));
415
+};
416
+
417
+/**
418
+ * Determines what should be display on the thumbnail.
419
+ *
420
+ * @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
421
+ * or <tt>DISPLAY_BLACKNESS</tt>.
422
+ */
423
+SmallVideo.prototype.selectDisplayMode = function() {
424
+    // Display name is always and only displayed when user is on the stage
425
+    if (this.isCurrentlyOnLargeVideo()) {
426
+        return DISPLAY_BLACKNESS;
427
+    } else if (this.isVideoPlayable() && this.selectVideoElement().length) {
428
+        return DISPLAY_VIDEO;
429
+    } else {
430
+        return DISPLAY_AVATAR;
431
+    }
432
+};
433
+
362
 /**
434
 /**
363
  * Hides or shows the user's avatar.
435
  * Hides or shows the user's avatar.
364
  * This update assumes that large video had been updated and we will
436
  * This update assumes that large video had been updated and we will
378
         }
450
         }
379
     }
451
     }
380
 
452
 
381
-    let video = this.selectVideoElement();
382
-
383
-    let avatar = $('#' + this.videoSpanId + ' .userAvatar');
384
-
385
-    var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id);
386
-
387
-    var showVideo = !this.isVideoMuted && !isCurrentlyOnLarge;
388
-    var showAvatar;
389
-    if ((!this.isLocal
390
-            && !this.VideoLayout.isInLastN(this.id))
391
-        || this.isVideoMuted) {
392
-        showAvatar = true;
393
-    } else {
394
-        // We want to show the avatar when the video is muted or not exists
395
-        // that is when 'true' or 'null' is returned
396
-        showAvatar = !this.videoStream || this.videoStream.isMuted();
397
-    }
398
-
399
-    showAvatar = showAvatar && !isCurrentlyOnLarge;
400
-
401
-    if (video && video.length > 0) {
402
-        setVisibility(video, showVideo);
403
-    }
404
-    setVisibility(avatar, showAvatar);
453
+    // Determine whether video, avatar or blackness should be displayed
454
+    let displayMode = this.selectDisplayMode();
455
+    // Show/hide video
456
+    setVisibility(this.selectVideoElement(), displayMode === DISPLAY_VIDEO);
457
+    // Show/hide the avatar
458
+    setVisibility(this.$avatar(), displayMode === DISPLAY_AVATAR);
405
 };
459
 };
406
 
460
 
407
 SmallVideo.prototype.avatarChanged = function (avatarUrl) {
461
 SmallVideo.prototype.avatarChanged = function (avatarUrl) {
408
     var thumbnail = $('#' + this.videoSpanId);
462
     var thumbnail = $('#' + this.videoSpanId);
409
-    var avatar = $('#' + this.videoSpanId + ' .userAvatar');
463
+    var avatarSel = this.$avatar();
410
     this.hasAvatar = true;
464
     this.hasAvatar = true;
411
 
465
 
412
     // set the avatar in the thumbnail
466
     // set the avatar in the thumbnail
413
-    if (avatar && avatar.length > 0) {
414
-        avatar[0].src = avatarUrl;
467
+    if (avatarSel && avatarSel.length > 0) {
468
+        avatarSel[0].src = avatarUrl;
415
     } else {
469
     } else {
416
         if (thumbnail && thumbnail.length > 0) {
470
         if (thumbnail && thumbnail.length > 0) {
417
-            avatar = document.createElement('img');
418
-            avatar.className = 'userAvatar';
419
-            avatar.src = avatarUrl;
420
-            thumbnail.append(avatar);
471
+            var avatarElement = document.createElement('img');
472
+            avatarElement.className = 'userAvatar';
473
+            avatarElement.src = avatarUrl;
474
+            thumbnail.append(avatarElement);
421
         }
475
         }
422
     }
476
     }
423
 };
477
 };

+ 76
- 3
modules/UI/videolayout/VideoContainer.js 查看文件

173
 
173
 
174
         this.isVisible = false;
174
         this.isVisible = false;
175
 
175
 
176
+        /**
177
+         * Flag indicates whether or not the avatar is currently displayed.
178
+         * @type {boolean}
179
+         */
180
+        this.avatarDisplayed = false;
176
         this.$avatar = $('#dominantSpeaker');
181
         this.$avatar = $('#dominantSpeaker');
182
+
183
+        /**
184
+         * A jQuery selector of the remote connection message.
185
+         * @type {jQuery|HTMLElement}
186
+         */
187
+        this.$remoteConnectionMessage = $('#remoteConnectionMessage');
188
+
189
+        /**
190
+         * Indicates whether or not the video stream attached to the video
191
+         * element has started(which means that there is any image rendered
192
+         * even if the video is stalled).
193
+         * @type {boolean}
194
+         */
195
+        this.wasVideoRendered = false;
196
+
177
         this.$wrapper = $('#largeVideoWrapper');
197
         this.$wrapper = $('#largeVideoWrapper');
178
 
198
 
179
         this.avatarHeight = $("#dominantSpeakerAvatar").height();
199
         this.avatarHeight = $("#dominantSpeakerAvatar").height();
180
 
200
 
201
+        var onPlayCallback = function (event) {
202
+            if (typeof onPlay === 'function') {
203
+                onPlay(event);
204
+            }
205
+            this.wasVideoRendered = true;
206
+        }.bind(this);
181
         // This does not work with Temasys plugin - has to be a property to be
207
         // This does not work with Temasys plugin - has to be a property to be
182
         // copied between new <object> elements
208
         // copied between new <object> elements
183
         //this.$video.on('play', onPlay);
209
         //this.$video.on('play', onPlay);
184
-        this.$video[0].onplay = onPlay;
210
+        this.$video[0].onplay = onPlayCallback;
185
     }
211
     }
186
 
212
 
187
     /**
213
     /**
188
      * Enables a filter on the video which indicates that there are some
214
      * Enables a filter on the video which indicates that there are some
189
-     * problems with the media connection.
215
+     * problems with the local media connection.
190
      *
216
      *
191
      * @param {boolean} enable <tt>true</tt> if the filter is to be enabled or
217
      * @param {boolean} enable <tt>true</tt> if the filter is to be enabled or
192
      * <tt>false</tt> otherwise.
218
      * <tt>false</tt> otherwise.
193
      */
219
      */
194
-    enableVideoProblemFilter (enable) {
220
+    enableLocalConnectionProblemFilter (enable) {
195
         this.$video.toggleClass("videoProblemFilter", enable);
221
         this.$video.toggleClass("videoProblemFilter", enable);
196
     }
222
     }
197
 
223
 
251
         }
277
         }
252
     }
278
     }
253
 
279
 
280
+    /**
281
+     * Update position of the remote connection message which describes that
282
+     * the remote user is having connectivity issues.
283
+     */
284
+    positionRemoteConnectionMessage () {
285
+
286
+        if (this.avatarDisplayed) {
287
+            let $avatarImage = $("#dominantSpeakerAvatar");
288
+            this.$remoteConnectionMessage.css(
289
+                'top',
290
+                $avatarImage.offset().top + $avatarImage.height() + 10);
291
+        } else {
292
+            let height = this.$remoteConnectionMessage.height();
293
+            let parentHeight = this.$remoteConnectionMessage.parent().height();
294
+            this.$remoteConnectionMessage.css(
295
+                'top', (parentHeight/2) - (height/2));
296
+        }
297
+
298
+        let width = this.$remoteConnectionMessage.width();
299
+        let parentWidth = this.$remoteConnectionMessage.parent().width();
300
+        this.$remoteConnectionMessage.css(
301
+            'left', ((parentWidth/2) - (width/2)));
302
+    }
303
+
254
     resize (containerWidth, containerHeight, animate = false) {
304
     resize (containerWidth, containerHeight, animate = false) {
255
         let [width, height]
305
         let [width, height]
256
             = this.getVideoSize(containerWidth, containerHeight);
306
             = this.getVideoSize(containerWidth, containerHeight);
263
 
313
 
264
         this.$avatar.css('top', top);
314
         this.$avatar.css('top', top);
265
 
315
 
316
+        this.positionRemoteConnectionMessage();
317
+
266
         this.$wrapper.animate({
318
         this.$wrapper.animate({
267
             width: width,
319
             width: width,
268
             height: height,
320
             height: height,
284
      * @param {string} videoType video type
336
      * @param {string} videoType video type
285
      */
337
      */
286
     setStream (stream, videoType) {
338
     setStream (stream, videoType) {
339
+
340
+        if (this.stream === stream) {
341
+            return;
342
+        } else {
343
+            // The stream has changed, so the image will be lost on detach
344
+            this.wasVideoRendered = false;
345
+        }
346
+
287
         // detach old stream
347
         // detach old stream
288
         if (this.stream) {
348
         if (this.stream) {
289
             this.stream.detach(this.$video[0]);
349
             this.stream.detach(this.$video[0]);
339
             (show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
399
             (show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
340
 
400
 
341
         this.$avatar.css("visibility", show ? "visible" : "hidden");
401
         this.$avatar.css("visibility", show ? "visible" : "hidden");
402
+        this.avatarDisplayed = show;
342
 
403
 
343
         this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, show);
404
         this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, show);
344
     }
405
     }
345
 
406
 
407
+    /**
408
+     * Indicates that the remote user who is currently displayed by this video
409
+     * container is having connectivity issues.
410
+     *
411
+     * @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
412
+     * the indication.
413
+     */
414
+    showRemoteConnectionProblemIndicator (show) {
415
+        this.$video.toggleClass("remoteVideoProblemFilter", show);
416
+        this.$avatar.toggleClass("remoteVideoProblemFilter", show);
417
+    }
418
+
346
     // We are doing fadeOut/fadeIn animations on parent div which wraps
419
     // We are doing fadeOut/fadeIn animations on parent div which wraps
347
     // largeVideo, because when Temasys plugin is in use it replaces
420
     // largeVideo, because when Temasys plugin is in use it replaces
348
     // <video> elements with plugin <object> tag. In Safari jQuery is
421
     // <video> elements with plugin <object> tag. In Safari jQuery is

+ 60
- 5
modules/UI/videolayout/VideoLayout.js 查看文件

384
     },
384
     },
385
 
385
 
386
     /**
386
     /**
387
-     * Creates a participant container for the given id and smallVideo.
387
+     * Creates or adds a participant container for the given id and smallVideo.
388
      *
388
      *
389
-     * @param id the id of the participant to add
389
+     * @param {JitsiParticipant} user the participant to add
390
      * @param {SmallVideo} smallVideo optional small video instance to add as a
390
      * @param {SmallVideo} smallVideo optional small video instance to add as a
391
-     * remote video, if undefined RemoteVideo will be created
391
+     * remote video, if undefined <tt>RemoteVideo</tt> will be created
392
      */
392
      */
393
-    addParticipantContainer (id, smallVideo) {
393
+    addParticipantContainer (user, smallVideo) {
394
+        let id = user.getId();
394
         let remoteVideo;
395
         let remoteVideo;
395
         if(smallVideo)
396
         if(smallVideo)
396
             remoteVideo = smallVideo;
397
             remoteVideo = smallVideo;
397
         else
398
         else
398
-            remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter);
399
+            remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
400
+        this.addRemoteVideoContainer(id, remoteVideo);
401
+    },
402
+
403
+    /**
404
+     * Adds remote video container for the given id and <tt>SmallVideo</tt>.
405
+     *
406
+     * @param {string} the id of the video to add
407
+     * @param {SmallVideo} smallVideo the small video instance to add as a
408
+     * remote video
409
+     */
410
+    addRemoteVideoContainer (id, remoteVideo) {
399
         remoteVideos[id] = remoteVideo;
411
         remoteVideos[id] = remoteVideo;
400
 
412
 
401
         let videoType = VideoLayout.getRemoteVideoType(id);
413
         let videoType = VideoLayout.getRemoteVideoType(id);
413
         } else {
425
         } else {
414
             VideoLayout.resizeThumbnails(false, true);
426
             VideoLayout.resizeThumbnails(false, true);
415
         }
427
         }
428
+        // Initialize the view
429
+        remoteVideo.updateView();
416
     },
430
     },
417
 
431
 
418
     videoactive (videoelem, resourceJid) {
432
     videoactive (videoelem, resourceJid) {
487
         localVideoThumbnail.showAudioIndicator(isMuted);
501
         localVideoThumbnail.showAudioIndicator(isMuted);
488
     },
502
     },
489
 
503
 
504
+    /**
505
+     * Shows/hides the indication about local connection being interrupted.
506
+     *
507
+     * @param {boolean} isInterrupted <tt>true</tt> if local connection is
508
+     * currently in the interrupted state or <tt>false</tt> if the connection
509
+     * is fine.
510
+     */
511
+    showLocalConnectionInterrupted (isInterrupted) {
512
+        localVideoThumbnail.connectionIndicator
513
+            .updateConnectionStatusIndicator(!isInterrupted);
514
+    },
515
+
490
     /**
516
     /**
491
      * Resizes thumbnails.
517
      * Resizes thumbnails.
492
      */
518
      */
618
         }
644
         }
619
     },
645
     },
620
 
646
 
647
+    /**
648
+     * Shows/hides warning about remote user's connectivity issues.
649
+     *
650
+     * @param {string} id the ID of the remote participant(MUC nickname)
651
+     * @param {boolean} isActive true if the connection is ok or false when
652
+     * the user is having connectivity issues.
653
+     */
654
+    onParticipantConnectionStatusChanged (id, isActive) {
655
+        // Show/hide warning on the large video
656
+        if (this.isCurrentlyOnLarge(id)) {
657
+            if (largeVideo) {
658
+                // We have to trigger full large video update to transition from
659
+                // avatar to video on connectivity restored.
660
+                this.updateLargeVideo(id, true /* force update */);
661
+            }
662
+        }
663
+        // Show/hide warning on the thumbnail
664
+        let remoteVideo = remoteVideos[id];
665
+        if (remoteVideo) {
666
+            // Updating only connection status indicator is not enough, because
667
+            // when we the connection is restored while the avatar was displayed
668
+            // (due to 'muted while disconnected' condition) we may want to show
669
+            // the video stream again and in order to do that the display mode
670
+            // must be updated.
671
+            //remoteVideo.updateConnectionStatusIndicator(isActive);
672
+            remoteVideo.updateView();
673
+        }
674
+    },
675
+
621
     /**
676
     /**
622
      * On last N change event.
677
      * On last N change event.
623
      *
678
      *

正在加载...
取消
保存