Bläddra i källkod

Merge pull request #938 from jitsi/participant_conn_status

Adds participant connection status notifications
master
hristoterezov 8 år sedan
förälder
incheckning
2a8700bca3

+ 52
- 1
conference.js Visa fil

@@ -691,6 +691,51 @@ export default {
691 691
     isConnectionInterrupted () {
692 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 739
     getMyUserId () {
695 740
         return this._room
696 741
             && this._room.myUserId();
@@ -1085,7 +1130,7 @@ export default {
1085 1130
 
1086 1131
             console.log('USER %s connnected', id, user);
1087 1132
             APP.API.notifyUserJoined(id);
1088
-            APP.UI.addUser(id, user.getDisplayName());
1133
+            APP.UI.addUser(user);
1089 1134
 
1090 1135
             // check the roles for the new user and reflect them
1091 1136
             APP.UI.updateUserRole(user);
@@ -1174,6 +1219,10 @@ export default {
1174 1219
             ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
1175 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 1226
         room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
1178 1227
             if (this.isLocalId(id)) {
1179 1228
                 this.isDominantSpeaker = true;
@@ -1205,10 +1254,12 @@ export default {
1205 1254
         room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
1206 1255
             connectionIsInterrupted = true;
1207 1256
             ConnectionQuality.updateLocalConnectionQuality(0);
1257
+            APP.UI.showLocalConnectionInterrupted(true);
1208 1258
         });
1209 1259
 
1210 1260
         room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
1211 1261
             connectionIsInterrupted = false;
1262
+            APP.UI.showLocalConnectionInterrupted(false);
1212 1263
         });
1213 1264
 
1214 1265
         room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {

+ 39
- 1
css/_videolayout_default.scss Visa fil

@@ -233,6 +233,12 @@
233 233
     overflow: hidden;
234 234
 }
235 235
 
236
+.connection.connection_lost
237
+{
238
+    color: #8B8B8B;
239
+    overflow: visible;
240
+}
241
+
236 242
 .connection.connection_full
237 243
 {
238 244
     color: #FFFFFF;/*#15A1ED*/
@@ -456,12 +462,44 @@
456 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 470
 .videoProblemFilter {
460 471
     -webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
461 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 503
     display: none;
466 504
     position: absolute;
467 505
     width: 100%;

+ 2
- 1
index.html Visa fil

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

+ 2
- 1
lang/main.json Visa fil

@@ -324,7 +324,8 @@
324 324
         "ATTACHED": "Attached",
325 325
         "FETCH_SESSION_ID": "Obtaining session-id...",
326 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 330
     "recording":
330 331
     {

+ 27
- 4
modules/UI/UI.js Visa fil

@@ -261,6 +261,17 @@ UI.changeDisplayName = function (id, displayName) {
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 276
  * Sets the "raised hand" status for a participant.
266 277
  */
@@ -602,10 +613,11 @@ UI.getSharedDocumentManager = function () {
602 613
 
603 614
 /**
604 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 621
     UI.hideRingOverLay();
610 622
     ContactList.addContact(id);
611 623
 
@@ -618,7 +630,7 @@ UI.addUser = function (id, displayName) {
618 630
         UIUtil.playSoundNotification('userJoined');
619 631
 
620 632
     // Add Peer's container
621
-    VideoLayout.addParticipantContainer(id);
633
+    VideoLayout.addParticipantContainer(user);
622 634
 
623 635
     // Configure avatar
624 636
     UI.setUserEmail(id);
@@ -983,6 +995,17 @@ UI.handleLastNEndpoints = function (ids, enteringIds) {
983 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 1010
  * Update audio level visualization for specified user.
988 1011
  * @param {string} id user id

+ 1
- 1
modules/UI/shared_video/SharedVideo.js Visa fil

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

+ 37
- 11
modules/UI/videolayout/ConnectionIndicator.js Visa fil

@@ -245,13 +245,13 @@ ConnectionIndicator.prototype.showMore = function () {
245 245
 };
246 246
 
247 247
 
248
-function createIcon(classes) {
248
+function createIcon(classes, iconClass) {
249 249
     var icon = document.createElement("span");
250 250
     for(var i in classes) {
251 251
         icon.classList.add(classes[i]);
252 252
     }
253 253
     icon.appendChild(
254
-        document.createElement("i")).classList.add("icon-connection");
254
+        document.createElement("i")).classList.add(iconClass);
255 255
     return icon;
256 256
 }
257 257
 
@@ -282,9 +282,12 @@ ConnectionIndicator.prototype.create = function () {
282 282
     }.bind(this);
283 283
 
284 284
     this.emptyIcon = this.connectionIndicatorContainer.appendChild(
285
-        createIcon(["connection", "connection_empty"]));
285
+        createIcon(["connection", "connection_empty"], "icon-connection"));
286 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,6 +301,27 @@ ConnectionIndicator.prototype.remove = function() {
298 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 326
  * Updates the data of the indicator
303 327
  * @param percent the percent of connection quality
@@ -314,12 +338,14 @@ ConnectionIndicator.prototype.updateConnectionQuality =
314 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 350
     for (var quality in ConnectionIndicator.connectionQualityValues) {
325 351
         if (percent >= quality) {
@@ -327,7 +353,7 @@ ConnectionIndicator.prototype.updateConnectionQuality =
327 353
                 ConnectionIndicator.connectionQualityValues[quality];
328 354
         }
329 355
     }
330
-    if (object.isResolutionHD) {
356
+    if (object && typeof object.isResolutionHD === 'boolean') {
331 357
         this.isResolutionHD = object.isResolutionHD;
332 358
     }
333 359
     this.updateResolutionIndicator();

+ 157
- 25
modules/UI/videolayout/LargeVideoManager.js Visa fil

@@ -5,12 +5,18 @@ import Avatar from "../avatar/Avatar";
5 5
 import {createDeferred} from '../../util/helpers';
6 6
 import UIUtil from "../util/UIUtil";
7 7
 import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
8
+import LargeContainer from "./LargeContainer";
8 9
 
9 10
 /**
10 11
  * Manager for all Large containers.
11 12
  */
12 13
 export default class LargeVideoManager {
13 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 20
         this.containers = {};
15 21
 
16 22
         this.state = VIDEO_CONTAINER_TYPE;
@@ -85,21 +91,18 @@ export default class LargeVideoManager {
85 91
      * Called when the media connection has been interrupted.
86 92
      */
87 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 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 101
      * Called when the media connection has been restored.
99 102
      */
100 103
     onVideoRestored () {
101
-        this.enableVideoProblemFilter(false);
102
-        this.showVideoConnectionMessage(false);
104
+        this.enableLocalConnectionProblemFilter(false);
105
+        this.showLocalConnectionMessage(false);
103 106
     }
104 107
 
105 108
     get id () {
@@ -118,7 +121,8 @@ export default class LargeVideoManager {
118 121
 
119 122
         // Include hide()/fadeOut only if we're switching between users
120 123
         let preUpdate;
121
-        if (this.newStreamData.id != this.id) {
124
+        let isUserSwitch = this.newStreamData.id != this.id;
125
+        if (isUserSwitch) {
122 126
             preUpdate = container.hide();
123 127
         } else {
124 128
             preUpdate = Promise.resolve();
@@ -136,27 +140,53 @@ export default class LargeVideoManager {
136 140
             // change the avatar url on large
137 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 147
             // If we the continer is VIDEO_CONTAINER_TYPE, we need to check
140 148
             // its stream whether exist and is muted to set isVideoMuted
141 149
             // in rest of the cases it is false
142
-            let isVideoMuted = false;
150
+            let showAvatar = false;
143 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 165
             let promise;
150 166
 
151 167
             // do not show stream if video is muted
152 168
             // but we still should show watermark
153
-            if (isVideoMuted) {
169
+            if (showAvatar) {
154 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 174
             } else {
157 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 190
             // resolve updateLargeVideo promise after everything is done
161 191
             promise.then(resolve);
162 192
 
@@ -169,6 +199,38 @@ export default class LargeVideoManager {
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 235
      * Update large video.
174 236
      * Switches to large video even if previously other container was visible.
@@ -229,12 +291,13 @@ export default class LargeVideoManager {
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 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,23 +316,87 @@ export default class LargeVideoManager {
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 320
      * @param {boolean|null} show(optional) tells whether the message is to be
258 321
      * displayed or not. If missing the condition will be based on the value
259 322
      * obtained from {@link APP.conference.isConnectionInterrupted}.
260 323
      */
261
-    showVideoConnectionMessage (show) {
324
+    showLocalConnectionMessage (show) {
262 325
         if (typeof show !== 'boolean') {
263 326
             show = APP.conference.isConnectionInterrupted();
264 327
         }
265 328
 
266 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 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 401
      * Add container of specified type.
275 402
      * @param {string} type container type
@@ -328,7 +455,8 @@ export default class LargeVideoManager {
328 455
         // be taking care of it by itself, but that is a bigger refactoring
329 456
         if (this.state === VIDEO_CONTAINER_TYPE) {
330 457
             this.showWatermark(false);
331
-            this.showVideoConnectionMessage(false);
458
+            this.showLocalConnectionMessage(false);
459
+            this.showRemoteConnectionMessage(false);
332 460
         }
333 461
         oldContainer.hide();
334 462
 
@@ -342,7 +470,11 @@ export default class LargeVideoManager {
342 470
                 // the container would be taking care of it by itself, but that
343 471
                 // is a bigger refactoring
344 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 Visa fil

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

+ 142
- 11
modules/UI/videolayout/RemoteVideo.js Visa fil

@@ -8,17 +8,45 @@ import UIUtils from "../util/UIUtil";
8 8
 import UIEvents from '../../../service/UI/UIEvents';
9 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 23
     this.emitter = emitter;
14
-    this.videoSpanId = `participant_${id}`;
24
+    this.videoSpanId = `participant_${this.id}`;
15 25
     SmallVideo.call(this, VideoLayout);
16 26
     this.hasRemoteVideoMenu = false;
17 27
     this.addRemoteVideoContainer();
18
-    this.connectionIndicator = new ConnectionIndicator(this, id);
28
+    this.connectionIndicator = new ConnectionIndicator(this, this.id);
19 29
     this.setDisplayName();
20 30
     this.flipX = false;
21 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 52
 RemoteVideo.prototype = Object.create(SmallVideo.prototype);
@@ -162,6 +190,33 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
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 221
  * Adds the remote video menu element for the given <tt>id</tt> in the
167 222
  * given <tt>parentElement</tt>.
@@ -209,13 +264,88 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) {
209 264
     var select = $('#' + elementID);
210 265
     select.remove();
211 266
 
267
+    if (isVideo) {
268
+        this.wasVideoPlayed = false;
269
+    }
270
+
212 271
     console.info((isVideo ? "Video" : "Audio") +
213 272
                  " removed " + this.id, select);
214 273
 
215 274
     // when removing only the video element and we are on stage
216 275
     // update the stage
217
-    if (isVideo && this.VideoLayout.isCurrentlyOnLarge(this.id))
276
+    if (isVideo && this.isCurrentlyOnLargeVideo())
218 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,22 +376,23 @@ RemoteVideo.prototype.waitForPlayback = function (streamElement, stream) {
246 376
     // Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
247 377
     // when video playback starts
248 378
     var onPlayingHandler = function () {
379
+        self.wasVideoPlayed = true;
249 380
         self.VideoLayout.videoactive(streamElement, self.id);
250 381
         streamElement.onplaying = null;
382
+        // Refresh to show the video
383
+        self.updateView();
251 384
     };
252 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 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 398
 RemoteVideo.prototype.addRemoteStreamElement = function (stream) {

+ 85
- 31
modules/UI/videolayout/SmallVideo.js Visa fil

@@ -5,6 +5,27 @@ import UIEvents from "../../../service/UI/UIEvents";
5 5
 
6 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 29
 function SmallVideo(VideoLayout) {
9 30
     this.isAudioMuted = false;
10 31
     this.hasAvatar = false;
@@ -337,6 +358,16 @@ SmallVideo.prototype.selectVideoElement = function () {
337 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 372
  * Enables / disables the css responsible for focusing/pinning a video
342 373
  * thumbnail.
@@ -359,6 +390,47 @@ SmallVideo.prototype.hasVideo = function () {
359 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 435
  * Hides or shows the user's avatar.
364 436
  * This update assumes that large video had been updated and we will
@@ -378,46 +450,28 @@ SmallVideo.prototype.updateView = function () {
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 461
 SmallVideo.prototype.avatarChanged = function (avatarUrl) {
408 462
     var thumbnail = $('#' + this.videoSpanId);
409
-    var avatar = $('#' + this.videoSpanId + ' .userAvatar');
463
+    var avatarSel = this.$avatar();
410 464
     this.hasAvatar = true;
411 465
 
412 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 469
     } else {
416 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 Visa fil

@@ -173,25 +173,51 @@ export class VideoContainer extends LargeContainer {
173 173
 
174 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 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 197
         this.$wrapper = $('#largeVideoWrapper');
178 198
 
179 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 207
         // This does not work with Temasys plugin - has to be a property to be
182 208
         // copied between new <object> elements
183 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 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 217
      * @param {boolean} enable <tt>true</tt> if the filter is to be enabled or
192 218
      * <tt>false</tt> otherwise.
193 219
      */
194
-    enableVideoProblemFilter (enable) {
220
+    enableLocalConnectionProblemFilter (enable) {
195 221
         this.$video.toggleClass("videoProblemFilter", enable);
196 222
     }
197 223
 
@@ -251,6 +277,30 @@ export class VideoContainer extends LargeContainer {
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 304
     resize (containerWidth, containerHeight, animate = false) {
255 305
         let [width, height]
256 306
             = this.getVideoSize(containerWidth, containerHeight);
@@ -263,6 +313,8 @@ export class VideoContainer extends LargeContainer {
263 313
 
264 314
         this.$avatar.css('top', top);
265 315
 
316
+        this.positionRemoteConnectionMessage();
317
+
266 318
         this.$wrapper.animate({
267 319
             width: width,
268 320
             height: height,
@@ -284,6 +336,14 @@ export class VideoContainer extends LargeContainer {
284 336
      * @param {string} videoType video type
285 337
      */
286 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 347
         // detach old stream
288 348
         if (this.stream) {
289 349
             this.stream.detach(this.$video[0]);
@@ -339,10 +399,23 @@ export class VideoContainer extends LargeContainer {
339 399
             (show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
340 400
 
341 401
         this.$avatar.css("visibility", show ? "visible" : "hidden");
402
+        this.avatarDisplayed = show;
342 403
 
343 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 419
     // We are doing fadeOut/fadeIn animations on parent div which wraps
347 420
     // largeVideo, because when Temasys plugin is in use it replaces
348 421
     // <video> elements with plugin <object> tag. In Safari jQuery is

+ 60
- 5
modules/UI/videolayout/VideoLayout.js Visa fil

@@ -384,18 +384,30 @@ var VideoLayout = {
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 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 395
         let remoteVideo;
395 396
         if(smallVideo)
396 397
             remoteVideo = smallVideo;
397 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 411
         remoteVideos[id] = remoteVideo;
400 412
 
401 413
         let videoType = VideoLayout.getRemoteVideoType(id);
@@ -413,6 +425,8 @@ var VideoLayout = {
413 425
         } else {
414 426
             VideoLayout.resizeThumbnails(false, true);
415 427
         }
428
+        // Initialize the view
429
+        remoteVideo.updateView();
416 430
     },
417 431
 
418 432
     videoactive (videoelem, resourceJid) {
@@ -487,6 +501,18 @@ var VideoLayout = {
487 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 517
      * Resizes thumbnails.
492 518
      */
@@ -618,6 +644,35 @@ var VideoLayout = {
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 677
      * On last N change event.
623 678
      *

Laddar…
Avbryt
Spara