Browse Source

Adds a last-n user interface. Some code restructuring related to last-n and stream reception/deletion. Adds a contact list user interface.

master
yanas 10 years ago
parent
commit
e89c7ea85c
21 changed files with 865 additions and 200 deletions
  1. 70
    128
      app.js
  2. 32
    0
      bottom_toolbar.js
  3. 8
    1
      chat.js
  4. 1
    1
      config.js
  5. 235
    0
      contact_list.js
  6. 35
    0
      css/contact_list.css
  7. 6
    0
      css/font.css
  8. 51
    5
      css/main.css
  9. 5
    1
      css/videolayout_default.css
  10. 8
    2
      data_channels.js
  11. BIN
      fonts/jitsi.eot
  12. 2
    0
      fonts/jitsi.svg
  13. BIN
      fonts/jitsi.ttf
  14. BIN
      fonts/jitsi.woff
  15. BIN
      images/avatar2.png
  16. 36
    12
      index.html
  17. 30
    0
      media_stream.js
  18. 8
    0
      muc.js
  19. 11
    10
      toolbar.js
  20. 3
    1
      util.js
  21. 324
    39
      videolayout.js

+ 70
- 128
app.js View File

@@ -11,6 +11,8 @@ var recordingToken ='';
11 11
 var roomUrl = null;
12 12
 var roomName = null;
13 13
 var ssrc2jid = {};
14
+var mediaStreams = [];
15
+
14 16
 /**
15 17
  * The stats collector that process stats data and triggers updates to app.js.
16 18
  * @type {StatsCollector}
@@ -231,40 +233,41 @@ function doJoin() {
231 233
     connection.emuc.doJoin(roomjid);
232 234
 }
233 235
 
234
-$(document).bind('remotestreamadded.jingle', function (event, data, sid) {
235
-    function waitForRemoteVideo(selector, sid, ssrc) {
236
-        if (selector.removed) {
237
-            console.warn("media removed before had started", selector);
238
-            return;
239
-        }
240
-        var sess = connection.jingle.sessions[sid];
241
-        if (data.stream.id === 'mixedmslabel') return;
242
-        var videoTracks = data.stream.getVideoTracks();
243
-//        console.log("waiting..", videoTracks, selector[0]);
244
-
245
-        if (videoTracks.length === 0 || selector[0].currentTime > 0) {
246
-            RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
247
-
248
-            // FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type
249
-            //        in order to get rid of too many maps
250
-            if (ssrc) {
251
-                videoSrcToSsrc[sel.attr('src')] = ssrc;
252
-            } else {
253
-                console.warn("No ssrc given for video", sel);
254
-            }
236
+function waitForRemoteVideo(selector, ssrc, stream) {
237
+    if (selector.removed || !selector.parent().is(":visible")) {
238
+        console.warn("Media removed before had started", selector);
239
+        return;
240
+    }
241
+
242
+    if (stream.id === 'mixedmslabel') return;
255 243
 
256
-            $(document).trigger('callactive.jingle', [selector, sid]);
257
-            console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState);
244
+    if (selector[0].currentTime > 0) {
245
+        RTC.attachMediaStream(selector, stream); // FIXME: why do i have to do this for FF?
246
+
247
+        // FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type
248
+        //        in order to get rid of too many maps
249
+        if (ssrc && selector.attr('src')) {
250
+            videoSrcToSsrc[selector.attr('src')] = ssrc;
258 251
         } else {
259
-            setTimeout(function () { waitForRemoteVideo(selector, sid, ssrc); }, 250);
252
+            console.warn("No ssrc given for video", selector);
260 253
         }
254
+
255
+        $(document).trigger('videoactive.jingle', [selector]);
256
+    } else {
257
+        setTimeout(function () {
258
+            waitForRemoteVideo(selector, ssrc, stream);
259
+            }, 250);
261 260
     }
261
+}
262
+
263
+$(document).bind('remotestreamadded.jingle', function (event, data, sid) {
262 264
     var sess = connection.jingle.sessions[sid];
263 265
 
264 266
     var thessrc;
265 267
     // look up an associated JID for a stream id
266 268
     if (data.stream.id.indexOf('mixedmslabel') === -1) {
267
-        var ssrclines = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc');
269
+        var ssrclines
270
+            = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc');
268 271
         ssrclines = ssrclines.filter(function (line) {
269 272
             return line.indexOf('mslabel:' + data.stream.label) !== -1;
270 273
         });
@@ -278,11 +281,14 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
278 281
         }
279 282
     }
280 283
 
284
+    mediaStreams.push(new MediaStream(data, sid, thessrc));
285
+
281 286
     var container;
282 287
     var remotes = document.getElementById('remoteVideos');
283 288
 
284 289
     if (data.peerjid) {
285 290
         VideoLayout.ensurePeerContainerExists(data.peerjid);
291
+
286 292
         container  = document.getElementById(
287 293
                 'participant_' + Strophe.getResourceFromJid(data.peerjid));
288 294
     } else {
@@ -295,91 +301,22 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
295 301
         }
296 302
         // FIXME: for the mixed ms we dont need a video -- currently
297 303
         container = document.createElement('span');
304
+        container.id = 'mixedstream';
298 305
         container.className = 'videocontainer';
299 306
         remotes.appendChild(container);
300 307
         Util.playSoundNotification('userJoined');
301 308
     }
302 309
 
303 310
     var isVideo = data.stream.getVideoTracks().length > 0;
304
-    var vid = isVideo ? document.createElement('video') : document.createElement('audio');
305
-    var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + sid + '_' + data.stream.id;
306
-
307
-    vid.id = id;
308
-    vid.autoplay = true;
309
-    vid.oncontextmenu = function () { return false; };
310
-
311
-    container.appendChild(vid);
312 311
 
313
-    // TODO: make mixedstream display:none via css?
314
-    if (id.indexOf('mixedmslabel') !== -1) {
315
-        container.id = 'mixedstream';
316
-        $(container).hide();
312
+    if (container) {
313
+        VideoLayout.addRemoteStreamElement( container,
314
+                                            sid,
315
+                                            data.stream,
316
+                                            data.peerjid,
317
+                                            thessrc);
317 318
     }
318 319
 
319
-    var sel = $('#' + id);
320
-    sel.hide();
321
-    RTC.attachMediaStream(sel, data.stream);
322
-
323
-    if (isVideo) {
324
-        waitForRemoteVideo(sel, sid, thessrc);
325
-    }
326
-
327
-    data.stream.onended = function () {
328
-        console.log('stream ended', this.id);
329
-
330
-        // Mark video as removed to cancel waiting loop(if video is removed
331
-        // before has started)
332
-        sel.removed = true;
333
-        sel.remove();
334
-
335
-        var audioCount = $('#' + container.id + '>audio').length;
336
-        var videoCount = $('#' + container.id + '>video').length;
337
-        if (!audioCount && !videoCount) {
338
-            console.log("Remove whole user", container.id);
339
-            // Remove whole container
340
-            container.remove();
341
-            Util.playSoundNotification('userLeft');
342
-            VideoLayout.resizeThumbnails();
343
-        }
344
-
345
-        VideoLayout.checkChangeLargeVideo(vid.src);
346
-    };
347
-
348
-    // Add click handler.
349
-    container.onclick = function (event) {
350
-        /*
351
-         * FIXME It turns out that videoThumb may not exist (if there is no
352
-         * actual video).
353
-         */
354
-        var videoThumb = $('#' + container.id + '>video').get(0);
355
-
356
-        if (videoThumb)
357
-            VideoLayout.handleVideoThumbClicked(videoThumb.src);
358
-
359
-        event.preventDefault();
360
-        return false;
361
-    };
362
-
363
-    // Add hover handler
364
-    $(container).hover(
365
-        function() {
366
-            VideoLayout.showDisplayName(container.id, true);
367
-        },
368
-        function() {
369
-            var videoSrc = null;
370
-            if ($('#' + container.id + '>video')
371
-                    && $('#' + container.id + '>video').length > 0) {
372
-                videoSrc = $('#' + container.id + '>video').get(0).src;
373
-            }
374
-
375
-            // If the video has been "pinned" by the user we want to keep the
376
-            // display name on place.
377
-            if (!VideoLayout.isLargeVideoVisible()
378
-                    || videoSrc !== $('#largeVideo').attr('src'))
379
-                VideoLayout.showDisplayName(container.id, false);
380
-        }
381
-    );
382
-
383 320
     // an attempt to work around https://github.com/jitsi/jitmeet/issues/32
384 321
     if (isVideo &&
385 322
         data.peerjid && sess.peerjid === data.peerjid &&
@@ -587,21 +524,6 @@ $(document).bind('conferenceCreated.jingle', function (event, focus)
587 524
     }
588 525
 });
589 526
 
590
-$(document).bind('callactive.jingle', function (event, videoelem, sid) {
591
-    if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
592
-        // ignore mixedmslabela0 and v0
593
-        videoelem.show();
594
-        VideoLayout.resizeThumbnails();
595
-
596
-        // Update the large video to the last added video only if there's no
597
-        // current active or focused speaker.
598
-        if (!focusedVideoSrc && !VideoLayout.getDominantSpeakerResourceJid())
599
-            VideoLayout.updateLargeVideo(videoelem.attr('src'), 1);
600
-
601
-        VideoLayout.showFocusIndicator();
602
-    }
603
-});
604
-
605 527
 $(document).bind('callterminated.jingle', function (event, sid, jid, reason) {
606 528
     // Leave the room if my call has been remotely terminated.
607 529
     if (connection.emuc.joined && focus == null && reason === 'kick') {
@@ -680,14 +602,20 @@ $(document).bind('joined.muc', function (event, jid, info) {
680 602
 
681 603
     VideoLayout.showFocusIndicator();
682 604
 
605
+    // Add myself to the contact list.
606
+    ContactList.addContact(jid);
607
+
683 608
     // Once we've joined the muc show the toolbar
684 609
     Toolbar.showToolbar();
685 610
 
686 611
     var displayName = '';
687 612
     if (info.displayName)
688 613
         displayName = info.displayName + ' (me)';
614
+    else
615
+        displayName = "Me";
689 616
 
690
-    VideoLayout.setDisplayName('localVideoContainer', displayName);
617
+    $(document).trigger('displaynamechanged',
618
+                        ['localVideoContainer', displayName]);
691 619
 });
692 620
 
693 621
 $(document).bind('entered.muc', function (event, jid, info, pres) {
@@ -811,21 +739,15 @@ $(document).bind('presence.muc', function (event, jid, info, pres) {
811 739
             case 'recvonly':
812 740
                 el.hide();
813 741
                 // FIXME: Check if we have to change large video
814
-                //VideoLayout.checkChangeLargeVideo(el);
742
+                //VideoLayout.updateLargeVideo(el);
815 743
                 break;
816 744
             }
817 745
         }
818 746
     });
819 747
 
820
-    if (jid === connection.emuc.myroomjid) {
821
-        VideoLayout.setDisplayName('localVideoContainer',
822
-                                    info.displayName);
823
-    } else {
824
-        VideoLayout.ensurePeerContainerExists(jid);
825
-        VideoLayout.setDisplayName(
826
-                'participant_' + Strophe.getResourceFromJid(jid),
827
-                info.displayName);
828
-    }
748
+    if (info.displayName && info.displayName.length > 0)
749
+        $(document).trigger('displaynamechanged',
750
+                            [jid, info.displayName]);
829 751
 
830 752
     if (focus !== null && info.displayName !== null) {
831 753
         focus.setEndpointDisplayName(jid, info.displayName);
@@ -1370,7 +1292,27 @@ function setView(viewName) {
1370 1292
 //    }
1371 1293
 }
1372 1294
 
1373
-
1295
+function hangUp() {
1296
+    if (connection && connection.connected) {
1297
+        // ensure signout
1298
+        $.ajax({
1299
+            type: 'POST',
1300
+            url: config.bosh,
1301
+            async: false,
1302
+            cache: false,
1303
+            contentType: 'application/xml',
1304
+            data: "<body rid='" + (connection.rid || connection._proto.rid) + "' xmlns='http://jabber.org/protocol/httpbind' sid='" + (connection.sid || connection._proto.sid) + "' type='terminate'><presence xmlns='jabber:client' type='unavailable'/></body>",
1305
+            success: function (data) {
1306
+                console.log('signed out');
1307
+                console.log(data);
1308
+            },
1309
+            error: function (XMLHttpRequest, textStatus, errorThrown) {
1310
+                console.log('signout error', textStatus + ' (' + errorThrown + ')');
1311
+            }
1312
+        });
1313
+    }
1314
+    disposeConference(true);
1315
+}
1374 1316
 
1375 1317
 $(document).bind('fatalError.jingle',
1376 1318
     function (event, session, error)

+ 32
- 0
bottom_toolbar.js View File

@@ -0,0 +1,32 @@
1
+var BottomToolbar = (function (my) {
2
+    my.toggleChat = function() {
3
+        if (ContactList.isVisible()) {
4
+            buttonClick("#contactListButton", "active");
5
+            ContactList.toggleContactList();
6
+        }
7
+
8
+        buttonClick("#chatBottomButton", "active");
9
+
10
+        Chat.toggleChat();
11
+    };
12
+
13
+    my.toggleContactList = function() {
14
+        if (Chat.isVisible()) {
15
+            buttonClick("#chatBottomButton", "active");
16
+            Chat.toggleChat();
17
+        }
18
+
19
+        buttonClick("#contactListButton", "active");
20
+
21
+        ContactList.toggleContactList();
22
+    };
23
+
24
+
25
+    $(document).bind("remotevideo.resized", function (event, width, height) {
26
+        var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18;
27
+
28
+        $('#bottomToolbar').css({bottom: bottom + 'px'});
29
+    });
30
+
31
+    return my;
32
+}(BottomToolbar || {}));

+ 8
- 1
chat.js View File

@@ -80,7 +80,7 @@ var Chat = (function (my) {
80 80
         else {
81 81
             divClassName = "remoteuser";
82 82
 
83
-            if (!$('#chatspace').is(":visible")) {
83
+            if (!Chat.isVisible()) {
84 84
                 unreadMessages++;
85 85
                 Util.playSoundNotification('chatNotification');
86 86
                 setVisualNotification(true);
@@ -301,6 +301,13 @@ var Chat = (function (my) {
301 301
         return [chatWidth, availableHeight];
302 302
     };
303 303
 
304
+    /**
305
+     * Indicates if the chat is currently visible.
306
+     */
307
+    my.isVisible = function () {
308
+        return $('#chatspace').is(":visible");
309
+    };
310
+
304 311
     /**
305 312
      * Resizes the chat conversation.
306 313
      */

+ 1
- 1
config.js View File

@@ -16,7 +16,7 @@ var config = {
16 16
     minChromeExtVersion: '0.1', // Required version of Chrome extension
17 17
     enableRtpStats: true, // Enables RTP stats processing
18 18
     openSctp: true, // Toggle to enable/disable SCTP channels
19
-//    channelLastN: -1, // The default value of the channel attribute last-n.
19
+    channelLastN: -1, // The default value of the channel attribute last-n.
20 20
 //    useRtcpMux: true,
21 21
 //    useBundle: true,
22 22
     enableRecording: false,

+ 235
- 0
contact_list.js View File

@@ -0,0 +1,235 @@
1
+/**
2
+ * Contact list.
3
+ */
4
+var ContactList = (function (my) {
5
+    /**
6
+     * Indicates if the chat is currently visible.
7
+     *
8
+     * @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> -
9
+     * otherwise
10
+     */
11
+    my.isVisible = function () {
12
+        return $('#contactlist').is(":visible");
13
+    };
14
+
15
+    /**
16
+     * Adds a contact for the given peerJid if such doesn't yet exist.
17
+     *
18
+     * @param peerJid the peerJid corresponding to the contact
19
+     */
20
+    my.ensureAddContact = function(peerJid) {
21
+        var resourceJid = Strophe.getResourceFromJid(peerJid);
22
+
23
+        var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
24
+
25
+        if (!contact || contact.length <= 0)
26
+            ContactList.addContact(peerJid);
27
+    };
28
+
29
+    /**
30
+     * Adds a contact for the given peer jid.
31
+     *
32
+     * @param peerJid the jid of the contact to add
33
+     */
34
+    my.addContact = function(peerJid) {
35
+        var resourceJid = Strophe.getResourceFromJid(peerJid);
36
+
37
+        var contactlist = $('#contactlist>ul');
38
+
39
+        var newContact = document.createElement('li');
40
+        newContact.id = resourceJid;
41
+
42
+        newContact.appendChild(createAvatar());
43
+        newContact.appendChild(createDisplayNameParagraph("Participant"));
44
+
45
+        var clElement = contactlist.get(0);
46
+
47
+        if (resourceJid === Strophe.getResourceFromJid(connection.emuc.myroomjid)
48
+            && $('#contactlist>ul .title')[0].nextSibling.nextSibling)
49
+        {
50
+            clElement.insertBefore(newContact,
51
+                    $('#contactlist>ul .title')[0].nextSibling.nextSibling);
52
+        }
53
+        else {
54
+            clElement.appendChild(newContact);
55
+        }
56
+    };
57
+
58
+    /**
59
+     * Removes a contact for the given peer jid.
60
+     *
61
+     * @param peerJid the peerJid corresponding to the contact to remove
62
+     */
63
+    my.removeContact = function(peerJid) {
64
+        var resourceJid = Strophe.getResourceFromJid(peerJid);
65
+
66
+        var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
67
+
68
+        if (contact && contact.length > 0) {
69
+            var contactlist = $('#contactlist>ul');
70
+
71
+            contactlist.get(0).removeChild(contact.get(0));
72
+        }
73
+    };
74
+
75
+    /**
76
+     * Opens / closes the contact list area.
77
+     */
78
+    my.toggleContactList = function () {
79
+        var contactlist = $('#contactlist');
80
+        var videospace = $('#videospace');
81
+
82
+        var chatSize = (ContactList.isVisible()) ? [0, 0] : Chat.getChatSize();
83
+        var videospaceWidth = window.innerWidth - chatSize[0];
84
+        var videospaceHeight = window.innerHeight;
85
+        var videoSize
86
+            = getVideoSize(null, null, videospaceWidth, videospaceHeight);
87
+        var videoWidth = videoSize[0];
88
+        var videoHeight = videoSize[1];
89
+        var videoPosition = getVideoPosition(videoWidth,
90
+                                             videoHeight,
91
+                                             videospaceWidth,
92
+                                             videospaceHeight);
93
+        var horizontalIndent = videoPosition[0];
94
+        var verticalIndent = videoPosition[1];
95
+
96
+        var thumbnailSize = VideoLayout.calculateThumbnailSize(videospaceWidth);
97
+        var thumbnailsWidth = thumbnailSize[0];
98
+        var thumbnailsHeight = thumbnailSize[1];
99
+
100
+        if (ContactList.isVisible()) {
101
+            videospace.animate({right: chatSize[0],
102
+                                width: videospaceWidth,
103
+                                height: videospaceHeight},
104
+                                {queue: false,
105
+                                duration: 500});
106
+
107
+            $('#remoteVideos').animate({height: thumbnailsHeight},
108
+                                        {queue: false,
109
+                                        duration: 500});
110
+
111
+            $('#remoteVideos>span').animate({height: thumbnailsHeight,
112
+                                            width: thumbnailsWidth},
113
+                                            {queue: false,
114
+                                            duration: 500,
115
+                                            complete: function() {
116
+                                                $(document).trigger(
117
+                                                        "remotevideo.resized",
118
+                                                        [thumbnailsWidth,
119
+                                                         thumbnailsHeight]);
120
+                                            }});
121
+
122
+            $('#largeVideoContainer').animate({ width: videospaceWidth,
123
+                                                height: videospaceHeight},
124
+                                                {queue: false,
125
+                                                 duration: 500
126
+                                                });
127
+
128
+            $('#largeVideo').animate({  width: videoWidth,
129
+                                        height: videoHeight,
130
+                                        top: verticalIndent,
131
+                                        bottom: verticalIndent,
132
+                                        left: horizontalIndent,
133
+                                        right: horizontalIndent},
134
+                                        {   queue: false,
135
+                                            duration: 500
136
+                                        });
137
+
138
+            $('#contactlist').hide("slide", { direction: "right",
139
+                                            queue: false,
140
+                                            duration: 500});
141
+        }
142
+        else {
143
+            // Undock the toolbar when the chat is shown and if we're in a 
144
+            // video mode.
145
+            if (VideoLayout.isLargeVideoVisible())
146
+                Toolbar.dockToolbar(false);
147
+
148
+            videospace.animate({right: chatSize[0],
149
+                                width: videospaceWidth,
150
+                                height: videospaceHeight},
151
+                               {queue: false,
152
+                                duration: 500,
153
+                                complete: function () {
154
+                                    contactlist.trigger('shown');
155
+                                }
156
+                               });
157
+
158
+            $('#remoteVideos').animate({height: thumbnailsHeight},
159
+                    {queue: false,
160
+                    duration: 500});
161
+
162
+            $('#remoteVideos>span').animate({height: thumbnailsHeight,
163
+                        width: thumbnailsWidth},
164
+                        {queue: false,
165
+                        duration: 500,
166
+                        complete: function() {
167
+                            $(document).trigger(
168
+                                    "remotevideo.resized",
169
+                                    [thumbnailsWidth, thumbnailsHeight]);
170
+                        }});
171
+
172
+            $('#largeVideoContainer').animate({ width: videospaceWidth,
173
+                                                height: videospaceHeight},
174
+                                                {queue: false,
175
+                                                 duration: 500
176
+                                                });
177
+
178
+            $('#largeVideo').animate({  width: videoWidth,
179
+                                        height: videoHeight,
180
+                                        top: verticalIndent,
181
+                                        bottom: verticalIndent,
182
+                                        left: horizontalIndent,
183
+                                        right: horizontalIndent},
184
+                                        {queue: false,
185
+                                         duration: 500
186
+                                        });
187
+
188
+            $('#contactlist').show("slide", { direction: "right",
189
+                                            queue: false,
190
+                                            duration: 500});
191
+        }
192
+    };
193
+
194
+    /**
195
+     * Creates the avatar element.
196
+     * 
197
+     * @return the newly created avatar element
198
+     */
199
+    function createAvatar() {
200
+        var avatar = document.createElement('i');
201
+        avatar.className = "icon-avatar avatar";
202
+
203
+        return avatar;
204
+    };
205
+
206
+    /**
207
+     * Creates the display name paragraph.
208
+     *
209
+     * @param displayName the display name to set
210
+     */
211
+    function createDisplayNameParagraph(displayName) {
212
+        var p = document.createElement('p');
213
+        p.innerHTML = displayName;
214
+
215
+        return p;
216
+    };
217
+
218
+    /**
219
+     * Indicates that the display name has changed.
220
+     */
221
+    $(document).bind(   'displaynamechanged',
222
+                        function (event, peerJid, displayName) {
223
+        if (peerJid === 'localVideoContainer')
224
+            peerJid = connection.emuc.myroomjid;
225
+
226
+        var resourceJid = Strophe.getResourceFromJid(peerJid);
227
+
228
+        var contactName = $('#contactlist #' + resourceJid + '>p');
229
+
230
+        if (contactName && displayName && displayName.length > 0)
231
+            contactName.html(displayName);
232
+    });
233
+
234
+    return my;
235
+}(ContactList || {}));

+ 35
- 0
css/contact_list.css View File

@@ -0,0 +1,35 @@
1
+#contactlist {
2
+    background-color:rgba(0,0,0,.65);
3
+}
4
+
5
+#contactlist>ul {
6
+    margin: 0px;
7
+    padding: 0px;
8
+}
9
+
10
+#contactlist>ul>li {
11
+    list-style-type: none;
12
+    text-align: left;
13
+    color: #FFF;
14
+    font-size: 10pt;
15
+    padding: 8px 10px;
16
+}
17
+
18
+#contactlist>ul>li>p {
19
+    display: inline-block;
20
+    vertical-align: middle;
21
+    margin: 0px;
22
+}
23
+
24
+#contactlist>ul>li.title {
25
+    color: #00ccff;
26
+    font-size: 11pt;
27
+    border-bottom: 1px solid #676767;
28
+}
29
+
30
+.avatar {
31
+    padding: 0px;
32
+    margin-right: 10px;
33
+    vertical-align: middle;
34
+    font-size: 22pt;
35
+}

+ 6
- 0
css/font.css View File

@@ -23,6 +23,12 @@
23 23
     -webkit-font-smoothing: antialiased;
24 24
     -moz-osx-font-smoothing: grayscale;
25 25
 }
26
+.icon-contactList:before {
27
+    content: "\e615";
28
+}
29
+.icon-avatar:before {
30
+    content: "\e616";
31
+}
26 32
 .icon-callRetro:before {
27 33
     content: "\e611";
28 34
 }

+ 51
- 5
css/main.css View File

@@ -8,7 +8,8 @@ html, body{
8 8
     overflow-x: hidden;
9 9
 }
10 10
 
11
-#chatspace {
11
+#chatspace,
12
+#contactlist {
12 13
     display:none;
13 14
     position:absolute;
14 15
     float: right;
@@ -18,12 +19,14 @@ html, body{
18 19
     width: 20%;
19 20
     max-width: 200px;
20 21
     overflow: hidden;
21
-    /* background-color:#dfebf1;*/
22
-    background-color:#FFFFFF;
23
-    border-left:1px solid #424242;
24 22
     z-index: 5;
25 23
 }
26 24
 
25
+#chatspace {
26
+    background-color:#FFF;
27
+    border-left:1px solid #424242;
28
+}
29
+
27 30
 #chatconversation {
28 31
     visibility: hidden;
29 32
     position: relative;
@@ -172,7 +175,8 @@ html, body{
172 175
     0 -1px 10px #00ccff;
173 176
 }
174 177
 
175
-a.button:hover {
178
+a.button:hover,
179
+a.bottomToolbarButton:hover {
176 180
     top: 0;
177 181
     cursor: pointer;
178 182
     background: rgba(0, 0, 0, 0.3);
@@ -408,4 +412,46 @@ form {
408 412
     font-weight: 200;
409 413
 }
410 414
 
415
+#bottomToolbar {
416
+    display:block;
417
+    position: absolute;
418
+    right: -1;
419
+    bottom: 40px;
420
+    width: 29px;
421
+    border-top-left-radius: 10px;
422
+    border-bottom-left-radius: 10px;
423
+    color: #FFF;
424
+    border: 1px solid #000;
425
+    background: rgba(50,50,50,.65);
426
+    padding-top: 5px;
427
+    padding-bottom: 5px;
428
+    z-index: 6; /*+1 from #remoteVideos*/
429
+}
430
+
431
+.bottomToolbarButton {
432
+    display: inline-block;
433
+    position: relative;
434
+    color: #FFFFFF;
435
+    top: 0;
436
+    padding-top: 3px;
437
+    width: 29px;
438
+    height: 20px;
439
+    cursor: pointer;
440
+    font-size: 10pt;
441
+    text-align: center;
442
+    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
443
+    z-index: 1;
444
+}
411 445
 
446
+.active {
447
+    color: #00ccff;
448
+}
449
+
450
+.bottomToolbar_span>span {
451
+    display: inline-block;
452
+    position: absolute;
453
+    font-size: 7pt;
454
+    color: #ffffff;
455
+    text-align:center;
456
+    cursor: pointer;
457
+}

+ 5
- 1
css/videolayout_default.css View File

@@ -15,7 +15,7 @@
15 15
     padding: 18px;
16 16
     bottom: 0;
17 17
     left: 0;
18
-    right: 0;
18
+    right: 20px;
19 19
     width:auto;
20 20
     border:1px solid transparent;
21 21
     z-index: 5;
@@ -334,3 +334,7 @@
334 334
     z-index: 0;
335 335
     border-radius:10px;
336 336
 }
337
+
338
+#mixedstream {
339
+    display:none !important;
340
+}

+ 8
- 2
data_channels.js View File

@@ -74,9 +74,15 @@ function onDataChannel(event)
74 74
                  */
75 75
                 var endpointsEnteringLastN = obj.endpointsEnteringLastN;
76 76
 
77
-                console.debug(
77
+                var stream = obj.stream;
78
+
79
+                console.log(
78 80
                     "Data channel new last-n event: ",
79
-                    lastNEndpoints);
81
+                    lastNEndpoints, endpointsEnteringLastN, obj);
82
+
83
+                $(document).trigger(
84
+                        'lastnchanged',
85
+                        [lastNEndpoints, endpointsEnteringLastN, stream]);
80 86
             }
81 87
             else
82 88
             {

BIN
fonts/jitsi.eot View File


+ 2
- 0
fonts/jitsi.svg View File

@@ -28,4 +28,6 @@
28 28
 <glyph unicode="&#xe612;" d="M155.131 15.215c0-26.065-21.103-47.215-47.2-47.215h-60.703c-26.098 0-47.229 21.15-47.229 47.215v417.835c0 26.079 21.133 47.229 47.229 47.229h60.701c26.097 0 47.2-21.15 47.2-47.229v-417.835zM538.559 480.28h-280.993c-36.459 0-66.165-30.337-66.165-67.626v-377.058c0-37.259 29.706-67.596 66.165-67.596h280.993c36.49 0 66.197 30.337 66.197 67.596v377.058c0 37.29-29.707 67.626-66.197 67.626zM264.915 413.453h266.327l0.031-71.649h-266.358v71.649zM321.627 25.814h-56.776v56.776h56.776v-56.776zM321.627 128.374h-56.776v56.777h56.776v-56.777zM321.691 231.878h-56.776v56.777h56.776v-56.777zM426.45 25.814h-56.776v56.776h56.776v-56.776zM426.45 128.374h-56.776v56.777h56.776v-56.777zM426.514 231.878h-56.778v56.777h56.778v-56.777zM531.274 25.814h-56.778v56.776h56.778v-56.776zM531.274 128.374h-56.778v56.777h56.778v-56.777zM531.335 231.878h-56.777v56.777h56.777v-56.777z" horiz-adv-x="605" />
29 29
 <glyph unicode="&#xe613;" d="M561.722 469.507c-11.797 13.24-32.066 14.495-45.37 2.697l-504.135-446.718c-13.305-11.734-14.495-32.065-2.73-45.338 6.337-7.153 15.154-10.825 24.063-10.825 7.562 0 15.186 2.7 21.272 8.098l65.023 57.61c45.371-40.922 105.284-66.082 171.237-66.050 141.408 0.031 255.457 113.985 255.644 255.486 0.063 54.967-17.341 105.683-46.75 147.36l59.044 52.313c13.241 11.763 14.499 32.065 2.702 45.368zM472.211 224.909c0.064-100.461-80.948-181.601-181.255-181.476-43.78 0.062-83.786 15.575-115.031 41.284l165.638 146.755v-36.588c0.536-30.497 16.348-46.090 47.472-46.846 30.998 0.756 46.843 16.382 47.565 46.878v20.548h-36.113v-23.75c0.125-2.321-0.282-5.303-1.255-8.974-0.625-1.63-1.724-3.010-3.262-4.046-1.599-1.286-3.923-1.914-6.934-1.914-5.272 0.155-8.566 2.134-9.913 5.961-0.534 1.756-0.974 3.452-1.254 5.082-0.127 1.443-0.188 2.766-0.188 3.893v71.755l21.238 18.817c0.108-0.216 0.226-0.425 0.315-0.651 0.974-3.233 1.381-6.399 1.255-9.538v-18.386h36.113v15.060c-0.123 15.623-4.543 27.35-13.181 35.224l20.356 18.034c17.894-28.028 28.401-61.293 28.433-97.122zM119.897 165.735c-6.306 18.512-9.913 38.279-9.913 58.957-0.095 100.118 80.792 181.005 180.973 181.067 28.426 0 55.156-6.651 79.067-18.199l58.923 52.211c-39.722 25.476-86.879 40.409-137.646 40.474-141.689 0.091-255.677-113.865-255.894-255.838-0.063-39.783 9.318-77.34 25.569-110.941l58.924 52.269zM194.288 313.010h-48.757v-124.529l36.115 32.035v0.344h0.407l58.86 52.209c0 0.282 0.061 0.47 0.061 0.755 0.376 26.949-15.184 40.034-46.687 39.185zM202.98 280.759c0.971-1.384 1.537-3.235 1.661-5.556 0.156-2.226 0.219-4.8 0.219-7.623 0.125-5.458-0.345-9.915-1.475-13.493-1.476-3.797-5.492-5.678-12.079-5.678h-9.662v37.022h7.655c3.921 0 6.933-0.341 9.036-1.095 2.198-0.787 3.734-1.976 4.645-3.577z" horiz-adv-x="570" />
30 30
 <glyph unicode="&#xe614;" d="M290.639 480.854c142.428-0.095 257.404-115.258 257.213-257.498-0.189-142.524-115.036-257.277-257.435-257.308-142.27-0.031-257.656 115.259-257.466 257.211 0.219 142.968 115.005 257.719 257.688 257.595zM290.289 405.878c-100.882-0.061-182.333-81.516-182.239-182.332 0-101.009 81.262-182.368 182.239-182.492 101.009-0.158 182.587 81.515 182.524 182.712-0.126 100.884-81.578 182.175-182.524 182.112zM143.849 312.453h49.098c31.721 0.884 47.392-12.259 47.013-39.431 0.127-9.541-1.106-17.441-3.728-23.761-3.002-6.254-9.353-10.994-19.083-14.090v-0.41c14.186-3.13 21.516-11.787 21.99-25.973v-28.844c0-5.623 0.127-11.406 0.379-17.378 0.41-6.002 1.517-10.49 3.348-13.522h-35.923c-1.863 3.032-3.064 7.519-3.57 13.522-0.506 5.971-0.727 11.755-0.569 17.378v26.161c0 4.801-1.107 8.276-3.286 10.49-2.338 2.053-6.351 3.095-12.006 3.095h-7.299v-70.645h-36.365v163.41zM180.214 247.43h9.732c6.636 0 10.679 1.897 12.166 5.688 1.138 3.602 1.611 8.152 1.484 13.585 0 2.908-0.063 5.434-0.221 7.709-0.126 2.337-0.696 4.202-1.676 5.623-0.916 1.579-2.463 2.781-4.676 3.57-2.117 0.727-5.149 1.106-9.1 1.106h-7.709v-37.282zM249.186 312.453h81.041v-31.343h-44.675v-32.794h39.051v-31.343h-39.051v-36.555h46.411v-31.375h-82.779v163.409zM341.253 268c0.158 15.891 4.708 27.771 13.712 35.703 8.72 7.645 20.093 11.468 34.091 11.468 14.123 0 25.56-3.823 34.312-11.5 8.91-7.899 13.46-19.811 13.586-35.704v-15.166h-36.365v18.516c0.127 3.096-0.285 6.319-1.264 9.604-0.632 1.579-1.738 2.969-3.286 4.107-1.611 0.757-3.949 1.202-6.982 1.202-5.308-0.158-8.625-1.928-9.983-5.309-1.106-3.286-1.611-6.508-1.454-9.604v-80.978c0-1.137 0.063-2.433 0.19-3.886 0.284-1.705 0.727-3.412 1.264-5.116 1.358-3.887 4.676-5.878 9.983-6.003 3.034 0 5.372 0.63 6.982 1.896 1.549 1.075 2.654 2.433 3.286 4.108 0.98 3.665 1.391 6.665 1.264 9.003v23.918h36.365v-20.664c-0.726-30.774-16.682-46.507-47.897-47.235-31.342 0.727-47.265 16.461-47.803 47.171v74.469z" horiz-adv-x="571" />
31
+<glyph unicode="&#xe615;" d="M508.412 2.883c-1.026 7.687-2.666 15.269-3.93 22.923-4.167 25.229-16.503 43.252-41.031 53.961-39.187 17.099-77.551 36.060-116.055 54.697-27.843 13.512-26.204 44.26-17.048 57.207 5.945 8.44 11.172 17.286 11.788 28.426 0.222 4.113 4.151 9.495 7.909 11.647 13.035 7.518 19.081 19.782 25.010 32.491 1.555 3.348 3.69 6.594 6.133 9.361 4.236 4.834 6.132 9.618 3.039 15.921-0.717 1.485 0.666 4.167 1.4 6.183 2.152 6.013 5.142 11.838 6.56 18.022 1.778 7.669 2.699 15.612 3.126 23.487 0.187 3.262-3.022 6.764-2.681 9.975 1.741 15.956-7.279 28.101-12.37 41.988-6.233 17.099-18.464 27.81-29.26 40.553-2.033 2.392-2.613 6.526-2.786 9.943-0.36 7.294-3.366 10.898-11.002 9.906-3.055-0.394-6.386-1.248-9.205-0.496-2.478 0.667-6.203 3.144-6.338 5.056-0.769 9.668-4.132 11.258-14.008 9.618-6.182-1.025-14.228 4.577-20.292 8.78-5.072 3.521-9.445 5.023-15.341 3.588-2.457-0.598-5.772-0.495-7.858 0.717-2.221 1.332-4.387 2.119-6.559 2.562v0.374c-0.478-0.016-0.991-0.102-1.469-0.154-0.477 0.051-0.956 0.137-1.434 0.154v-0.375c-2.185-0.444-4.375-1.231-6.578-2.562-2.066-1.213-5.381-1.316-7.84-0.718-5.911 1.434-10.285-0.068-15.342-3.588-6.079-4.202-14.108-9.805-20.292-8.781-9.873 1.641-13.255 0.052-14.024-9.618-0.154-1.912-3.843-4.389-6.338-5.056-2.834-0.752-6.149 0.102-9.223 0.495-7.618 0.992-10.625-2.613-10.985-9.906-0.169-3.416-0.751-7.551-2.784-9.943-10.794-12.743-23.025-23.454-29.278-40.553-5.058-13.886-14.094-26.031-12.335-41.987 0.343-3.211-2.872-6.714-2.7-9.975 0.445-7.875 1.368-15.818 3.127-23.487 1.418-6.184 4.407-12.010 6.576-18.022 0.719-2.016 2.121-4.698 1.384-6.183-3.091-6.303-1.179-11.087 3.058-15.921 2.427-2.767 4.56-6.013 6.115-9.361 5.929-12.709 11.974-24.974 25.007-32.491 3.76-2.152 7.689-7.534 7.929-11.647 0.596-11.14 5.825-19.986 11.785-28.426 9.141-12.947 10.573-43.369-17.081-57.207-38.228-19.132-76.871-37.6-116.021-54.697-24.564-10.709-36.863-28.731-41.032-53.961-1.263-7.656-2.939-15.238-3.929-22.923-1.505-11.464-3.912-34.883-3.912-34.883h512.306c-0.001 0-2.39 23.419-3.894 34.883z" horiz-adv-x="513" />
32
+<glyph unicode="&#xe616;" d="M513.087 224.534c0-141.673-114.855-256.526-256.554-256.526-141.674 0-256.534 114.851-256.534 256.526 0 141.692 114.861 256.553 256.534 256.553 141.7 0 256.554-114.861 256.554-256.553zM256.534-31.993c67.863 0 129.556 26.356 175.437 69.37-4.858 5.825-11.276 10.557-19.557 14.171-29.467 12.873-58.313 27.128-87.267 41.128-20.935 10.161-19.702 33.293-12.82 43.029 4.471 6.346 8.402 12.999 8.864 21.373 0.166 3.084 3.12 7.142 5.945 8.761 9.802 5.652 14.349 14.873 18.802 24.43 1.17 2.515 2.777 4.945 4.615 7.038 3.185 3.622 4.612 7.218 2.286 11.971-0.543 1.104 0.502 3.12 1.053 4.637 1.619 4.534 3.866 8.901 4.93 13.558 1.335 5.774 2.029 11.74 2.351 17.661 0.14 2.451-2.272 5.092-2.017 7.493 1.31 12.011-5.471 21.136-9.299 31.579-4.688 12.857-13.885 20.91-22.002 30.485-1.529 1.812-1.964 4.919-2.094 7.476-0.269 5.49-2.53 8.207-8.272 7.462-2.299-0.3-4.805-0.943-6.921-0.378-1.864 0.494-4.663 2.362-4.767 3.802-0.577 7.269-3.106 8.465-10.533 7.238-4.648-0.772-10.697 3.429-15.257 6.601-3.816 2.646-7.104 3.777-11.534 2.69-1.849-0.45-4.341-0.373-5.908 0.547-1.671 0.988-3.303 1.592-4.933 1.919v0.276c-0.36-0.007-0.745-0.065-1.104-0.108-0.361 0.044-0.72 0.103-1.078 0.108v-0.276c-1.645-0.327-3.287-0.931-4.945-1.919-1.556-0.918-4.046-0.996-5.899-0.547-4.443 1.087-7.724-0.044-11.532-2.69-4.578-3.173-10.611-7.373-15.259-6.601-7.431 1.226-9.97 0.031-10.547-7.238-0.109-1.439-2.897-3.308-4.758-3.802-2.139-0.565-4.624 0.077-6.944 0.378-5.728 0.745-7.994-1.971-8.258-7.462-0.131-2.555-0.565-5.665-2.095-7.476-8.111-9.575-17.308-17.629-22.009-30.485-3.814-10.443-10.602-19.568-9.285-31.579 0.256-2.401-2.152-5.042-2.023-7.493 0.327-5.923 1.020-11.888 2.351-17.661 1.065-4.656 3.313-9.024 4.945-13.558 0.547-1.516 1.587-3.531 1.041-4.637-2.325-4.754-0.894-8.351 2.291-11.971 1.837-2.094 3.437-4.523 4.612-7.038 4.45-9.555 8.996-18.779 18.798-24.43 2.827-1.619 5.78-5.676 5.952-8.761 0.457-8.374 4.387-15.027 8.869-21.373 6.873-9.735 7.951-32.623-12.837-43.029-28.76-14.386-57.8-28.255-87.251-41.128-8.285-3.615-14.704-8.347-19.561-14.169 45.88-43.015 107.569-69.372 175.422-69.372z" horiz-adv-x="513" />
31 33
 </font></defs></svg>

BIN
fonts/jitsi.ttf View File


BIN
fonts/jitsi.woff View File


BIN
images/avatar2.png View File


+ 36
- 12
index.html View File

@@ -23,15 +23,16 @@
23 23
     <script src="libs/rayo.js?v=1"></script>
24 24
     <script src="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
25 25
     <script src="libs/popover.js?v=1"></script><!-- bootstrap tooltip lib -->
26
-    <script src="config.js?v=3"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
27
-    <script src="muc.js?v=12"></script><!-- simple MUC library -->
26
+    <script src="config.js?v=4"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
27
+    <script src="muc.js?v=13"></script><!-- simple MUC library -->
28 28
     <script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
29 29
     <script src="desktopsharing.js?v=2"></script><!-- desktop sharing -->
30
-    <script src="data_channels.js?v=2"></script><!-- data channels -->
31
-    <script src="app.js?v=4"></script><!-- application logic -->
30
+    <script src="data_channels.js?v=3"></script><!-- data channels -->
31
+    <script src="app.js?v=5"></script><!-- application logic -->
32 32
     <script src="commands.js?v=1"></script><!-- application logic -->
33
-    <script src="chat.js?v=8"></script><!-- chat logic -->
34
-    <script src="util.js?v=5"></script><!-- utility functions -->
33
+    <script src="chat.js?v=9"></script><!-- chat logic -->
34
+    <script src="contact_list.js?v=1"></script><!-- contact list logic -->
35
+    <script src="util.js?v=6"></script><!-- utility functions -->
35 36
     <script src="etherpad.js?v=8"></script><!-- etherpad plugin -->
36 37
     <script src="prezi.js?v=4"></script><!-- prezi plugin -->
37 38
     <script src="smileys.js?v=2"></script><!-- smiley images -->
@@ -40,18 +41,21 @@
40 41
     <script src="analytics.js?v=1"></script><!-- google analytics plugin -->
41 42
     <script src="rtp_stats.js?v=1"></script><!-- RTP stats processing -->
42 43
     <script src="local_stats.js?v=1"></script><!-- Local stats processing -->
43
-    <script src="videolayout.js?v=7"></script><!-- video ui -->
44
-    <script src="toolbar.js?v=3"></script><!-- toolbar ui -->
44
+    <script src="videolayout.js?v=8"></script><!-- video ui -->
45
+    <script src="toolbar.js?v=4"></script><!-- toolbar ui -->
45 46
     <script src="canvas_util.js?v=1"></script><!-- canvas drawing utils -->
46 47
     <script src="audio_levels.js?v=1"></script><!-- audio levels plugin -->
48
+    <script src="media_stream.js?v=1"></script><!-- media stream -->
49
+    <script src="bottom_toolbar.js?v=1"></script><!-- media stream -->
47 50
     <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
48
-    <link rel="stylesheet" href="css/font.css"/>
49
-    <link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=22"/>
50
-    <link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=8" id="videolayout_default"/>
51
+    <link rel="stylesheet" href="css/font.css?v=2"/>
52
+    <link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=23"/>
53
+    <link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=9" id="videolayout_default"/>
51 54
     <link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
52 55
     <link rel="stylesheet" href="css/modaldialog.css?v=3">
53 56
     <link rel="stylesheet" href="css/popup_menu.css?v=2">
54 57
     <link rel="stylesheet" href="css/popover.css?v=1">
58
+    <link rel="stylesheet" href="css/contact_list.css?v=1">
55 59
     <!--
56 60
         Link used for inline installation of chrome desktop streaming extension,
57 61
         is updated automatically from the code with the value defined in config.js -->
@@ -158,7 +162,7 @@
158 162
                     </a>
159 163
                     <div class="header_button_separator"></div>
160 164
                     <span class="toolbar_span">
161
-                        <a class="button" data-toggle="popover" data-placement="bottom" data-content="Open / close chat" onclick='Chat.toggleChat();'>
165
+                        <a class="button" data-toggle="popover" data-placement="bottom" data-content="Open / close chat" onclick='BottomToolbar.toggleChat();'>
162 166
                             <i id="chatButton" class="icon-chat"></i>
163 167
                         </a>
164 168
                         <span id="unreadMessages"></span>
@@ -222,6 +226,20 @@
222 226
                 <audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
223 227
                 <audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
224 228
             </div>
229
+            <span id="bottomToolbar">
230
+                <span class="bottomToolbar_span">
231
+                    <a class="bottomToolbarButton" data-toggle="popover" data-placement="top" data-content="Open / close chat" onclick='BottomToolbar.toggleChat();'>
232
+                        <i id="chatBottomButton" class="icon-chat-simple"></i>
233
+                    </a>
234
+                    <span id="unreadMessages"></span>
235
+                </span>
236
+                <span class="bottomToolbar_span">
237
+                    <a class="bottomToolbarButton" data-toggle="popover" data-placement="top" data-content="Open / close contact list" onclick='BottomToolbar.toggleContactList();'>
238
+                        <i id="contactListButton" class="icon-contactList"></i>
239
+                    </a>
240
+                    <span id="unreadMessages"></span>
241
+                </span>
242
+            </span>
225 243
         </div>
226 244
         <div id="chatspace">
227 245
             <div id="nickname">
@@ -238,5 +256,11 @@
238 256
         </div>
239 257
         <a id="downloadlog" onclick='dump(event.target);' data-toggle="popover" data-placement="right" data-content="Download logs" ><i class="fa fa-cloud-download"></i></a>
240 258
     </div>
259
+    <div id="contactlist">
260
+        <ul>
261
+            <li class="title"><i class="icon-contact-list"></i> CONTACT LIST</li>
262
+        </ul>
263
+    </div>
264
+    <a id="downloadlog" onclick='dump(event.target);' data-toggle="popover" data-placement="right" data-content="Download logs" ><i class="fa fa-cloud-download"></i></a>
241 265
   </body>
242 266
 </html>

+ 30
- 0
media_stream.js View File

@@ -0,0 +1,30 @@
1
+/**
2
+ * Provides a wrapper class for the MediaStream.
3
+ * 
4
+ * TODO : Add here the src from the video element and other related properties
5
+ * and get rid of some of the mappings that we use throughout the UI.
6
+ */
7
+var MediaStream = (function() {
8
+    /**
9
+     * Creates a MediaStream object for the given data, session id and ssrc.
10
+     *
11
+     * @param data the data object from which we obtain the stream,
12
+     * the peerjid, etc.
13
+     * @param sid the session id
14
+     * @param ssrc the ssrc corresponding to this MediaStream
15
+     *
16
+     * @constructor
17
+     */
18
+    function MediaStreamProto(data, sid, ssrc) {
19
+        this.VIDEO_TYPE = "Video";
20
+        this.AUDIO_TYPE = "Audio";
21
+        this.stream = data.stream;
22
+        this.peerjid = data.peerjid;
23
+        this.ssrc = ssrc;
24
+        this.session = connection.jingle.sessions[sid];
25
+        this.type = (this.stream.getVideoTracks().length > 0)
26
+                    ? this.VIDEO_TYPE : this.AUDIO_TYPE;
27
+    }
28
+
29
+    return MediaStreamProto;
30
+})();

+ 8
- 0
muc.js View File

@@ -373,5 +373,13 @@ Strophe.addConnectionPlugin('emuc', {
373 373
     addVideoInfoToPresence: function(isMuted) {
374 374
         this.presMap['videons'] = 'http://jitsi.org/jitmeet/video';
375 375
         this.presMap['videomuted'] = isMuted.toString();
376
+    },
377
+    findJidFromResource: function(resourceJid) {
378
+        var peerJid = null;
379
+        Object.keys(this.members).some(function (jid) {
380
+            peerJid = jid;
381
+            return Strophe.getResourceFromJid(jid) === resourceJid;
382
+        });
383
+        return peerJid;
376 384
     }
377 385
 });

+ 11
- 10
toolbar.js View File

@@ -118,16 +118,17 @@ var Toolbar = (function (my) {
118 118
 
119 119
         var conferenceName = roomUrl.substring(roomUrl.lastIndexOf('/') + 1);
120 120
         var subject = "Invitation to a Jitsi Meet (" + conferenceName + ")";
121
-        var body = "Hey there, I%27d like to invite you to a Jitsi Meet"
122
-                    + " conference I%27ve just set up.%0D%0A%0D%0A"
123
-                    + "Please click on the following link in order"
124
-                    + " to join the conference.%0D%0A%0D%0A"
125
-                    + roomUrl + "%0D%0A%0D%0A"
126
-                    + sharedKeyText
127
-                    + "Note that Jitsi Meet is currently only supported by Chromium,"
128
-                    + " Google Chrome and Opera, so you need"
129
-                    + " to be using one of these browsers.%0D%0A%0D%0A"
130
-                    + "Talk to you in a sec!";
121
+        var body = "Hey there, I%27d like to invite you to a Jitsi Meet" +
122
+                    " conference I%27ve just set up.%0D%0A%0D%0A" +
123
+                    "Please click on the following link in order" +
124
+                    " to join the conference.%0D%0A%0D%0A" +
125
+                    roomUrl +
126
+                    "%0D%0A%0D%0A" +
127
+                    sharedKeyText +
128
+                    "Note that Jitsi Meet is currently only supported by Chromium," +
129
+                    " Google Chrome and Opera, so you need" +
130
+                    " to be using one of these browsers.%0D%0A%0D%0A" +
131
+                    "Talk to you in a sec!";
131 132
 
132 133
         if (window.localStorage.displayname)
133 134
             body += "%0D%0A%0D%0A" + window.localStorage.displayname;

+ 3
- 1
util.js View File

@@ -52,7 +52,9 @@ var Util = (function (my) {
52 52
      */
53 53
     my.getAvailableVideoWidth = function () {
54 54
         var chatspaceWidth
55
-            = $('#chatspace').is(":visible") ? $('#chatspace').width() : 0;
55
+            = (Chat.isVisible() || ContactList.isVisible())
56
+                ? $('#chatspace').width()
57
+                : 0;
56 58
 
57 59
         return window.innerWidth - chatspaceWidth;
58 60
     };

+ 324
- 39
videolayout.js View File

@@ -1,6 +1,8 @@
1 1
 var VideoLayout = (function (my) {
2 2
     var preMuted = false;
3 3
     var currentDominantSpeaker = null;
4
+    var lastNCount = config.channelLastN;
5
+    var lastNEndpointsCache = [];
4 6
 
5 7
     my.changeLocalAudio = function(stream) {
6 8
         connection.jingle.localAudio = stream;
@@ -52,7 +54,7 @@ var VideoLayout = (function (my) {
52 54
         // Add stream ended handler
53 55
         stream.onended = function () {
54 56
             localVideoContainer.removeChild(localVideo);
55
-            VideoLayout.checkChangeLargeVideo(localVideo.src);
57
+            VideoLayout.updateRemovedVideo(localVideo.src);
56 58
         };
57 59
         // Flip video x axis if needed
58 60
         flipXLocalVideo = flipX;
@@ -63,6 +65,7 @@ var VideoLayout = (function (my) {
63 65
         RTC.attachMediaStream(localVideoSelector, stream);
64 66
 
65 67
         localVideoSrc = localVideo.src;
68
+
66 69
         VideoLayout.updateLargeVideo(localVideoSrc, 0);
67 70
     };
68 71
 
@@ -71,7 +74,7 @@ var VideoLayout = (function (my) {
71 74
      * another one instead.
72 75
      * @param removedVideoSrc src stream identifier of the video.
73 76
      */
74
-    my.checkChangeLargeVideo = function(removedVideoSrc) {
77
+    my.updateRemovedVideo = function(removedVideoSrc) {
75 78
         if (removedVideoSrc === $('#largeVideo').attr('src')) {
76 79
             // this is currently displayed as large
77 80
             // pick the last visible video in the row
@@ -83,7 +86,8 @@ var VideoLayout = (function (my) {
83 86
             if (!pick) {
84 87
                 console.info("Last visible video no longer exists");
85 88
                 pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
86
-                if (!pick) {
89
+
90
+                if (!pick || !pick.src) {
87 91
                     // Try local video
88 92
                     console.info("Fallback to local video...");
89 93
                     pick = $('#remoteVideos>span>span>video').get(0);
@@ -182,8 +186,9 @@ var VideoLayout = (function (my) {
182 186
                     = $('#participant_' + currentDominantSpeaker + '>video')
183 187
                         .get(0);
184 188
 
185
-                if (dominantSpeakerVideo)
189
+                if (dominantSpeakerVideo) {
186 190
                     VideoLayout.updateLargeVideo(dominantSpeakerVideo.src, 1);
191
+                }
187 192
             }
188 193
 
189 194
             return;
@@ -279,28 +284,42 @@ var VideoLayout = (function (my) {
279 284
      * in the document and creates it eventually.
280 285
      * 
281 286
      * @param peerJid peer Jid to check.
287
+     * 
288
+     * @return Returns <tt>true</tt> if the peer container exists,
289
+     * <tt>false</tt> - otherwise
282 290
      */
283 291
     my.ensurePeerContainerExists = function(peerJid) {
284
-        var peerResource = Strophe.getResourceFromJid(peerJid);
285
-        var videoSpanId = 'participant_' + peerResource;
292
+        ContactList.ensureAddContact(peerJid);
293
+
294
+        var resourceJid = Strophe.getResourceFromJid(peerJid);
295
+
296
+        var videoSpanId = 'participant_' + resourceJid;
286 297
 
287 298
         if ($('#' + videoSpanId).length > 0) {
288 299
             // If there's been a focus change, make sure we add focus related
289 300
             // interface!!
290
-            if (focus && $('#remote_popupmenu_' + peerResource).length <= 0)
301
+            if (focus && $('#remote_popupmenu_' + resourceJid).length <= 0)
291 302
                 addRemoteVideoMenu( peerJid,
292 303
                                     document.getElementById(videoSpanId));
293
-            return;
294 304
         }
295
-
296
-        var container
297
-            = VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId);
298
-
299
-        var nickfield = document.createElement('span');
300
-        nickfield.className = "nick";
301
-        nickfield.appendChild(document.createTextNode(peerResource));
302
-        container.appendChild(nickfield);
303
-        VideoLayout.resizeThumbnails();
305
+        else {
306
+            var container
307
+                = VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId);
308
+
309
+            var nickfield = document.createElement('span');
310
+            nickfield.className = "nick";
311
+            nickfield.appendChild(document.createTextNode(resourceJid));
312
+            container.appendChild(nickfield);
313
+
314
+            // In case this is not currently in the last n we don't show it.
315
+            if (lastNCount
316
+                && lastNCount > 0
317
+                && $('#remoteVideos>span').length >= lastNCount + 2) {
318
+                showPeerContainer(resourceJid, false);
319
+            }
320
+            else
321
+                VideoLayout.resizeThumbnails();
322
+        }
304 323
     };
305 324
 
306 325
     my.addRemoteVideoContainer = function(peerJid, spanId) {
@@ -321,9 +340,158 @@ var VideoLayout = (function (my) {
321 340
     };
322 341
 
323 342
     /**
324
-     * Shows the display name for the given video.
343
+     * Creates an audio or video stream element.
344
+     */
345
+    my.createStreamElement = function (sid, stream) {
346
+        var isVideo = stream.getVideoTracks().length > 0;
347
+
348
+        var element = isVideo
349
+                        ? document.createElement('video')
350
+                        : document.createElement('audio');
351
+        var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_')
352
+                    + sid + '_' + stream.id;
353
+
354
+        element.id = id;
355
+        element.autoplay = true;
356
+        element.oncontextmenu = function () { return false; };
357
+
358
+        return element;
359
+    };
360
+
361
+    my.addRemoteStreamElement
362
+        = function (container, sid, stream, peerJid, thessrc) {
363
+        var newElementId = null;
364
+
365
+        var isVideo = stream.getVideoTracks().length > 0;
366
+
367
+        if (container) {
368
+            var streamElement = VideoLayout.createStreamElement(sid, stream);
369
+            newElementId = streamElement.id;
370
+
371
+            container.appendChild(streamElement);
372
+
373
+            var sel = $('#' + newElementId);
374
+            sel.hide();
375
+
376
+            // If the container is currently visible we attach the stream.
377
+            if (!isVideo
378
+                || (container.offsetParent !== null && isVideo)) {
379
+                RTC.attachMediaStream(sel, stream);
380
+
381
+                if (isVideo)
382
+                    waitForRemoteVideo(sel, thessrc, stream);
383
+            }
384
+
385
+            stream.onended = function () {
386
+                console.log('stream ended', this);
387
+
388
+                VideoLayout.removeRemoteStreamElement(stream, container);
389
+
390
+                if (peerJid)
391
+                    ContactList.removeContact(peerJid);
392
+            };
393
+
394
+            // Add click handler.
395
+            container.onclick = function (event) {
396
+                /*
397
+                 * FIXME It turns out that videoThumb may not exist (if there is
398
+                 * no actual video).
399
+                 */
400
+                var videoThumb = $('#' + container.id + '>video').get(0);
401
+
402
+                if (videoThumb)
403
+                    VideoLayout.handleVideoThumbClicked(videoThumb.src);
404
+
405
+                event.preventDefault();
406
+                return false;
407
+            };
408
+
409
+            // Add hover handler
410
+            $(container).hover(
411
+                function() {
412
+                    VideoLayout.showDisplayName(container.id, true);
413
+                },
414
+                function() {
415
+                    var videoSrc = null;
416
+                    if ($('#' + container.id + '>video')
417
+                            && $('#' + container.id + '>video').length > 0) {
418
+                        videoSrc = $('#' + container.id + '>video').get(0).src;
419
+                    }
420
+
421
+                    // If the video has been "pinned" by the user we want to
422
+                    // keep the display name on place.
423
+                    if (!VideoLayout.isLargeVideoVisible()
424
+                            || videoSrc !== $('#largeVideo').attr('src'))
425
+                        VideoLayout.showDisplayName(container.id, false);
426
+                }
427
+            );
428
+        }
429
+
430
+        return newElementId;
431
+    };
432
+
433
+    /**
434
+     * Removes the remote stream element corresponding to the given stream and
435
+     * parent container.
436
+     * 
437
+     * @param stream the stream
438
+     * @param container
439
+     */
440
+    my.removeRemoteStreamElement = function (stream, container) {
441
+        if (!container)
442
+            return;
443
+
444
+        var select = null;
445
+        var removedVideoSrc = null;
446
+        if (stream.getVideoTracks().length > 0) {
447
+            select = $('#' + container.id + '>video');
448
+            removedVideoSrc = select.get(0).src;
449
+        }
450
+        else
451
+            select = $('#' + container.id + '>audio');
452
+
453
+        // Remove video source from the mapping.
454
+        delete videoSrcToSsrc[removedVideoSrc];
455
+
456
+        // Mark video as removed to cancel waiting loop(if video is removed
457
+        // before has started)
458
+        select.removed = true;
459
+        select.remove();
460
+
461
+        var audioCount = $('#' + container.id + '>audio').length;
462
+        var videoCount = $('#' + container.id + '>video').length;
463
+
464
+        if (!audioCount && !videoCount) {
465
+            console.log("Remove whole user", container.id);
466
+            // Remove whole container
467
+            container.remove();
468
+            Util.playSoundNotification('userLeft');
469
+            VideoLayout.resizeThumbnails();
470
+        }
471
+
472
+        if (removedVideoSrc)
473
+            VideoLayout.updateRemovedVideo(removedVideoSrc);
474
+    };
475
+
476
+    /**
477
+     * Show/hide peer container for the given resourceJid.
478
+     */
479
+    function showPeerContainer(resourceJid, isShow) {
480
+        var peerContainer = $('#participant_' + resourceJid);
481
+
482
+        if (!peerContainer)
483
+            return;
484
+
485
+        if (!peerContainer.is(':visible') && isShow)
486
+            peerContainer.show();
487
+        else if (peerContainer.is(':visible') && !isShow)
488
+            peerContainer.hide();
489
+    };
490
+
491
+    /**
492
+     * Sets the display name for the given video span id.
325 493
      */
326
-    my.setDisplayName = function(videoSpanId, displayName) {
494
+    function setDisplayName(videoSpanId, displayName) {
327 495
         var nameSpan = $('#' + videoSpanId + '>span.displayname');
328 496
         var defaultLocalDisplayName = "Me";
329 497
         var defaultRemoteDisplayName = "Speaker";
@@ -334,12 +502,12 @@ var VideoLayout = (function (my) {
334 502
 
335 503
             if (nameSpanElement.id === 'localDisplayName' &&
336 504
                 $('#localDisplayName').text() !== displayName) {
337
-                if (displayName)
505
+                if (displayName && displayName.length > 0)
338 506
                     $('#localDisplayName').text(displayName + ' (me)');
339 507
                 else
340 508
                     $('#localDisplayName').text(defaultLocalDisplayName);
341 509
             } else {
342
-                if (displayName)
510
+                if (displayName && displayName.length > 0)
343 511
                     $('#' + videoSpanId + '_name').text(displayName);
344 512
                 else
345 513
                     $('#' + videoSpanId + '_name').text(defaultRemoteDisplayName);
@@ -359,7 +527,7 @@ var VideoLayout = (function (my) {
359 527
                 nameSpan.innerText = defaultRemoteDisplayName;
360 528
             }
361 529
 
362
-            if (displayName && displayName.length) {
530
+            if (displayName && displayName.length > 0) {
363 531
                 nameSpan.innerText = displayName;
364 532
             }
365 533
 
@@ -434,11 +602,7 @@ var VideoLayout = (function (my) {
434 602
      * @param isShow indicates if the display name should be shown or hidden
435 603
      */
436 604
     my.showDisplayName = function(videoSpanId, isShow) {
437
-        // FIX: need to use noConflict of jquery, because apparently we're
438
-        // using another library that uses $, which conflics with jquery and
439
-        // sometimes objects are null because of that!!!!!!!!!
440
-        // http://api.jquery.com/jQuery.noConflict/
441
-        var nameSpan = jQuery('#' + videoSpanId + '>span.displayname').get(0);
605
+        var nameSpan = $('#' + videoSpanId + '>span.displayname').get(0);
442 606
         if (isShow) {
443 607
             if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length) 
444 608
                 nameSpan.setAttribute("style", "display:inline-block;");
@@ -459,8 +623,6 @@ var VideoLayout = (function (my) {
459 623
             return;
460 624
         }
461 625
 
462
-        var nameSpan = $('#' + videoSpanId + '>span.displayname');
463
-
464 626
         var statusSpan = $('#' + videoSpanId + '>span.status');
465 627
         if (!statusSpan.length) {
466 628
             //Add status span
@@ -721,6 +883,8 @@ var VideoLayout = (function (my) {
721 883
 
722 884
     /**
723 885
      * Calculates the thumbnail size.
886
+     *
887
+     * @param videoSpaceWidth the width of the video space
724 888
      */
725 889
     my.calculateThumbnailSize = function (videoSpaceWidth) {
726 890
         // Calculate the available height, which is the inner window height minus
@@ -729,11 +893,15 @@ var VideoLayout = (function (my) {
729 893
        // container used for highlighting shadow.
730 894
        var availableHeight = 100;
731 895
 
732
-       var numvids = $('#remoteVideos>span:visible').length;
896
+       var numvids = 0;
897
+       if (lastNCount && lastNCount > 0)
898
+           numvids = lastNCount + 1;
899
+       else
900
+           numvids = $('#remoteVideos>span:visible').length;
733 901
 
734 902
        // Remove the 3px borders arround videos and border around the remote
735 903
        // videos area
736
-       var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 50;
904
+       var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70;
737 905
 
738 906
        var availableWidth = availableWinWidth / numvids;
739 907
        var aspectRatio = 16.0 / 9.0;
@@ -851,6 +1019,20 @@ var VideoLayout = (function (my) {
851 1019
         return currentDominantSpeaker;
852 1020
     };
853 1021
 
1022
+    /**
1023
+     * Returns the corresponding resource jid to the given peer container
1024
+     * DOM element.
1025
+     *
1026
+     * @return the corresponding resource jid to the given peer container
1027
+     * DOM element
1028
+     */
1029
+    my.getPeerContainerResourceJid = function (containerElement) {
1030
+        var i = containerElement.id.indexOf('participant_');
1031
+
1032
+        if (i >= 0)
1033
+            return containerElement.id.substring(i + 12); 
1034
+    };
1035
+
854 1036
     /**
855 1037
      * Adds the remote video menu element for the given <tt>jid</tt> in the
856 1038
      * given <tt>parentElement</tt>.
@@ -965,6 +1147,25 @@ var VideoLayout = (function (my) {
965 1147
             VideoLayout.showVideoIndicator(videoSpanId, isMuted);
966 1148
     });
967 1149
 
1150
+    /**
1151
+     * Display name changed.
1152
+     */
1153
+    $(document).bind('displaynamechanged',
1154
+                    function (event, jid, displayName, status) {
1155
+        if (jid === 'localVideoContainer'
1156
+            || jid === connection.emuc.myroomjid) {
1157
+            setDisplayName('localVideoContainer',
1158
+                           displayName);
1159
+        } else {
1160
+            VideoLayout.ensurePeerContainerExists(jid);
1161
+
1162
+            setDisplayName(
1163
+                'participant_' + Strophe.getResourceFromJid(jid),
1164
+                displayName,
1165
+                status);
1166
+        }
1167
+    });
1168
+
968 1169
     /**
969 1170
      * On dominant speaker changed event.
970 1171
      */
@@ -974,29 +1175,113 @@ var VideoLayout = (function (my) {
974 1175
                 === Strophe.getResourceFromJid(connection.emuc.myroomjid))
975 1176
             return;
976 1177
 
977
-        // Obtain container for new dominant speaker.
978
-        var container  = document.getElementById(
979
-                'participant_' + resourceJid);
980
-
981 1178
         // Update the current dominant speaker.
982 1179
         if (resourceJid !== currentDominantSpeaker)
983 1180
             currentDominantSpeaker = resourceJid;
984 1181
         else
985 1182
             return;
986 1183
 
1184
+        // Obtain container for new dominant speaker.
1185
+        var container  = document.getElementById(
1186
+                'participant_' + resourceJid);
1187
+
987 1188
         // Local video will not have container found, but that's ok
988 1189
         // since we don't want to switch to local video.
989 1190
         if (container && !focusedVideoSrc)
990 1191
         {
991 1192
             var video = container.getElementsByTagName("video");
992
-            if (video.length)
993
-            {
1193
+
1194
+            // Update the large video if the video source is already available,
1195
+            // otherwise wait for the "videoactive.jingle" event.
1196
+            if (video.length && video[0].currentTime > 0)
994 1197
                 VideoLayout.updateLargeVideo(video[0].src);
1198
+        }
1199
+    });
1200
+
1201
+    /**
1202
+     * On last N change event.
1203
+     *
1204
+     * @param event the event that notified us
1205
+     * @param lastNEndpoints the list of last N endpoints
1206
+     * @param endpointsEnteringLastN the list currently entering last N
1207
+     * endpoints
1208
+     */
1209
+    $(document).bind('lastnchanged', function ( event,
1210
+                                                lastNEndpoints,
1211
+                                                endpointsEnteringLastN,
1212
+                                                stream) {
1213
+        if (lastNCount !== lastNEndpoints.length)
1214
+            lastNCount = lastNEndpoints.length;
1215
+
1216
+        lastNEndpointsCache = lastNEndpoints;
1217
+
1218
+        $('#remoteVideos>span').each(function( index, element ) {
1219
+            var resourceJid = VideoLayout.getPeerContainerResourceJid(element);
1220
+
1221
+            if (resourceJid
1222
+                && lastNEndpoints.length > 0
1223
+                && lastNEndpoints.indexOf(resourceJid) < 0) {
1224
+                console.log("Remove from last N", resourceJid);
1225
+                showPeerContainer(resourceJid, false);
995 1226
             }
1227
+        });
1228
+
1229
+        if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
1230
+            endpointsEnteringLastN = lastNEndpoints;
1231
+
1232
+        if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
1233
+            endpointsEnteringLastN.forEach(function (resourceJid) {
1234
+
1235
+                if (!$('#participant_' + resourceJid).is(':visible')) {
1236
+                    console.log("Add to last N", resourceJid);
1237
+                    showPeerContainer(resourceJid, true);
1238
+
1239
+                    mediaStreams.some(function (mediaStream) {
1240
+                        if (mediaStream.peerjid
1241
+                            && Strophe.getResourceFromJid(mediaStream.peerjid)
1242
+                                === resourceJid
1243
+                            && mediaStream.type === mediaStream.VIDEO_TYPE) {
1244
+                            var sel = $('#participant_' + resourceJid + '>video');
1245
+
1246
+                            RTC.attachMediaStream(sel, mediaStream.stream);
1247
+                            waitForRemoteVideo(
1248
+                                    sel,
1249
+                                    mediaStream.ssrc,
1250
+                                    mediaStream.stream);
1251
+                            return true;
1252
+                        }
1253
+                    });
1254
+                }
1255
+            });
1256
+        }
1257
+    });
1258
+
1259
+    $(document).bind('videoactive.jingle', function (event, videoelem) {
1260
+        if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
1261
+            // ignore mixedmslabela0 and v0
1262
+
1263
+            videoelem.show();
1264
+            VideoLayout.resizeThumbnails();
1265
+
1266
+            var videoParent = videoelem.parent();
1267
+            var parentResourceJid = null;
1268
+            if (videoParent)
1269
+                parentResourceJid
1270
+                    = VideoLayout.getPeerContainerResourceJid(videoParent[0]);
1271
+
1272
+            // Update the large video to the last added video only if there's no
1273
+            // current dominant or focused speaker or update it to the current
1274
+            // dominant speaker.
1275
+            if ((!focusedVideoSrc && !VideoLayout.getDominantSpeakerResourceJid())
1276
+                || (parentResourceJid
1277
+                && VideoLayout.getDominantSpeakerResourceJid()
1278
+                    === parentResourceJid)) {
1279
+                VideoLayout.updateLargeVideo(videoelem.attr('src'), 1);
1280
+            }
1281
+
1282
+            VideoLayout.showFocusIndicator();
996 1283
         }
997 1284
     });
998 1285
 
999 1286
     return my;
1000 1287
 }(VideoLayout || {}));
1001
-
1002
-    

Loading…
Cancel
Save