From e89c7ea85c4913cd9f8b59e69a2e82e660476c08 Mon Sep 17 00:00:00 2001 From: yanas Date: Fri, 22 Aug 2014 17:37:11 +0200 Subject: [PATCH] Adds a last-n user interface. Some code restructuring related to last-n and stream reception/deletion. Adds a contact list user interface. --- app.js | 204 ++++++++------------ bottom_toolbar.js | 32 ++++ chat.js | 9 +- config.js | 2 +- contact_list.js | 235 +++++++++++++++++++++++ css/contact_list.css | 35 ++++ css/font.css | 6 + css/main.css | 56 +++++- css/videolayout_default.css | 6 +- data_channels.js | 10 +- fonts/jitsi.eot | Bin 7264 -> 9028 bytes fonts/jitsi.svg | 2 + fonts/jitsi.ttf | Bin 7108 -> 8872 bytes fonts/jitsi.woff | Bin 5140 -> 5880 bytes images/avatar2.png | Bin 0 -> 1607 bytes index.html | 48 +++-- media_stream.js | 30 +++ muc.js | 8 + toolbar.js | 21 ++- util.js | 4 +- videolayout.js | 359 ++++++++++++++++++++++++++++++++---- 21 files changed, 866 insertions(+), 201 deletions(-) create mode 100644 bottom_toolbar.js create mode 100644 contact_list.js create mode 100644 css/contact_list.css create mode 100644 images/avatar2.png create mode 100644 media_stream.js diff --git a/app.js b/app.js index 964aecde1..afd786b38 100644 --- a/app.js +++ b/app.js @@ -11,6 +11,8 @@ var recordingToken =''; var roomUrl = null; var roomName = null; var ssrc2jid = {}; +var mediaStreams = []; + /** * The stats collector that process stats data and triggers updates to app.js. * @type {StatsCollector} @@ -231,40 +233,41 @@ function doJoin() { connection.emuc.doJoin(roomjid); } -$(document).bind('remotestreamadded.jingle', function (event, data, sid) { - function waitForRemoteVideo(selector, sid, ssrc) { - if (selector.removed) { - console.warn("media removed before had started", selector); - return; - } - var sess = connection.jingle.sessions[sid]; - if (data.stream.id === 'mixedmslabel') return; - var videoTracks = data.stream.getVideoTracks(); -// console.log("waiting..", videoTracks, selector[0]); - - if (videoTracks.length === 0 || selector[0].currentTime > 0) { - RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF? - - // FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type - // in order to get rid of too many maps - if (ssrc) { - videoSrcToSsrc[sel.attr('src')] = ssrc; - } else { - console.warn("No ssrc given for video", sel); - } - - $(document).trigger('callactive.jingle', [selector, sid]); - console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState); - } else { - setTimeout(function () { waitForRemoteVideo(selector, sid, ssrc); }, 250); - } +function waitForRemoteVideo(selector, ssrc, stream) { + if (selector.removed || !selector.parent().is(":visible")) { + console.warn("Media removed before had started", selector); + return; } + + if (stream.id === 'mixedmslabel') return; + + if (selector[0].currentTime > 0) { + RTC.attachMediaStream(selector, stream); // FIXME: why do i have to do this for FF? + + // FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type + // in order to get rid of too many maps + if (ssrc && selector.attr('src')) { + videoSrcToSsrc[selector.attr('src')] = ssrc; + } else { + console.warn("No ssrc given for video", selector); + } + + $(document).trigger('videoactive.jingle', [selector]); + } else { + setTimeout(function () { + waitForRemoteVideo(selector, ssrc, stream); + }, 250); + } +} + +$(document).bind('remotestreamadded.jingle', function (event, data, sid) { var sess = connection.jingle.sessions[sid]; var thessrc; // look up an associated JID for a stream id if (data.stream.id.indexOf('mixedmslabel') === -1) { - var ssrclines = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc'); + var ssrclines + = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc'); ssrclines = ssrclines.filter(function (line) { return line.indexOf('mslabel:' + data.stream.label) !== -1; }); @@ -278,11 +281,14 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { } } + mediaStreams.push(new MediaStream(data, sid, thessrc)); + var container; var remotes = document.getElementById('remoteVideos'); if (data.peerjid) { VideoLayout.ensurePeerContainerExists(data.peerjid); + container = document.getElementById( 'participant_' + Strophe.getResourceFromJid(data.peerjid)); } else { @@ -295,91 +301,22 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { } // FIXME: for the mixed ms we dont need a video -- currently container = document.createElement('span'); + container.id = 'mixedstream'; container.className = 'videocontainer'; remotes.appendChild(container); Util.playSoundNotification('userJoined'); } var isVideo = data.stream.getVideoTracks().length > 0; - var vid = isVideo ? document.createElement('video') : document.createElement('audio'); - var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + sid + '_' + data.stream.id; - vid.id = id; - vid.autoplay = true; - vid.oncontextmenu = function () { return false; }; - - container.appendChild(vid); - - // TODO: make mixedstream display:none via css? - if (id.indexOf('mixedmslabel') !== -1) { - container.id = 'mixedstream'; - $(container).hide(); + if (container) { + VideoLayout.addRemoteStreamElement( container, + sid, + data.stream, + data.peerjid, + thessrc); } - var sel = $('#' + id); - sel.hide(); - RTC.attachMediaStream(sel, data.stream); - - if (isVideo) { - waitForRemoteVideo(sel, sid, thessrc); - } - - data.stream.onended = function () { - console.log('stream ended', this.id); - - // Mark video as removed to cancel waiting loop(if video is removed - // before has started) - sel.removed = true; - sel.remove(); - - var audioCount = $('#' + container.id + '>audio').length; - var videoCount = $('#' + container.id + '>video').length; - if (!audioCount && !videoCount) { - console.log("Remove whole user", container.id); - // Remove whole container - container.remove(); - Util.playSoundNotification('userLeft'); - VideoLayout.resizeThumbnails(); - } - - VideoLayout.checkChangeLargeVideo(vid.src); - }; - - // Add click handler. - container.onclick = function (event) { - /* - * FIXME It turns out that videoThumb may not exist (if there is no - * actual video). - */ - var videoThumb = $('#' + container.id + '>video').get(0); - - if (videoThumb) - VideoLayout.handleVideoThumbClicked(videoThumb.src); - - event.preventDefault(); - return false; - }; - - // Add hover handler - $(container).hover( - function() { - VideoLayout.showDisplayName(container.id, true); - }, - function() { - var videoSrc = null; - if ($('#' + container.id + '>video') - && $('#' + container.id + '>video').length > 0) { - videoSrc = $('#' + container.id + '>video').get(0).src; - } - - // If the video has been "pinned" by the user we want to keep the - // display name on place. - if (!VideoLayout.isLargeVideoVisible() - || videoSrc !== $('#largeVideo').attr('src')) - VideoLayout.showDisplayName(container.id, false); - } - ); - // an attempt to work around https://github.com/jitsi/jitmeet/issues/32 if (isVideo && data.peerjid && sess.peerjid === data.peerjid && @@ -587,21 +524,6 @@ $(document).bind('conferenceCreated.jingle', function (event, focus) } }); -$(document).bind('callactive.jingle', function (event, videoelem, sid) { - if (videoelem.attr('id').indexOf('mixedmslabel') === -1) { - // ignore mixedmslabela0 and v0 - videoelem.show(); - VideoLayout.resizeThumbnails(); - - // Update the large video to the last added video only if there's no - // current active or focused speaker. - if (!focusedVideoSrc && !VideoLayout.getDominantSpeakerResourceJid()) - VideoLayout.updateLargeVideo(videoelem.attr('src'), 1); - - VideoLayout.showFocusIndicator(); - } -}); - $(document).bind('callterminated.jingle', function (event, sid, jid, reason) { // Leave the room if my call has been remotely terminated. if (connection.emuc.joined && focus == null && reason === 'kick') { @@ -680,14 +602,20 @@ $(document).bind('joined.muc', function (event, jid, info) { VideoLayout.showFocusIndicator(); + // Add myself to the contact list. + ContactList.addContact(jid); + // Once we've joined the muc show the toolbar Toolbar.showToolbar(); var displayName = ''; if (info.displayName) displayName = info.displayName + ' (me)'; + else + displayName = "Me"; - VideoLayout.setDisplayName('localVideoContainer', displayName); + $(document).trigger('displaynamechanged', + ['localVideoContainer', displayName]); }); $(document).bind('entered.muc', function (event, jid, info, pres) { @@ -811,21 +739,15 @@ $(document).bind('presence.muc', function (event, jid, info, pres) { case 'recvonly': el.hide(); // FIXME: Check if we have to change large video - //VideoLayout.checkChangeLargeVideo(el); + //VideoLayout.updateLargeVideo(el); break; } } }); - if (jid === connection.emuc.myroomjid) { - VideoLayout.setDisplayName('localVideoContainer', - info.displayName); - } else { - VideoLayout.ensurePeerContainerExists(jid); - VideoLayout.setDisplayName( - 'participant_' + Strophe.getResourceFromJid(jid), - info.displayName); - } + if (info.displayName && info.displayName.length > 0) + $(document).trigger('displaynamechanged', + [jid, info.displayName]); if (focus !== null && info.displayName !== null) { focus.setEndpointDisplayName(jid, info.displayName); @@ -1370,7 +1292,27 @@ function setView(viewName) { // } } - +function hangUp() { + if (connection && connection.connected) { + // ensure signout + $.ajax({ + type: 'POST', + url: config.bosh, + async: false, + cache: false, + contentType: 'application/xml', + data: "", + success: function (data) { + console.log('signed out'); + console.log(data); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + console.log('signout error', textStatus + ' (' + errorThrown + ')'); + } + }); + } + disposeConference(true); +} $(document).bind('fatalError.jingle', function (event, session, error) diff --git a/bottom_toolbar.js b/bottom_toolbar.js new file mode 100644 index 000000000..f8bfd4642 --- /dev/null +++ b/bottom_toolbar.js @@ -0,0 +1,32 @@ +var BottomToolbar = (function (my) { + my.toggleChat = function() { + if (ContactList.isVisible()) { + buttonClick("#contactListButton", "active"); + ContactList.toggleContactList(); + } + + buttonClick("#chatBottomButton", "active"); + + Chat.toggleChat(); + }; + + my.toggleContactList = function() { + if (Chat.isVisible()) { + buttonClick("#chatBottomButton", "active"); + Chat.toggleChat(); + } + + buttonClick("#contactListButton", "active"); + + ContactList.toggleContactList(); + }; + + + $(document).bind("remotevideo.resized", function (event, width, height) { + var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18; + + $('#bottomToolbar').css({bottom: bottom + 'px'}); + }); + + return my; +}(BottomToolbar || {})); diff --git a/chat.js b/chat.js index 8db249b7e..505c8a47f 100644 --- a/chat.js +++ b/chat.js @@ -80,7 +80,7 @@ var Chat = (function (my) { else { divClassName = "remoteuser"; - if (!$('#chatspace').is(":visible")) { + if (!Chat.isVisible()) { unreadMessages++; Util.playSoundNotification('chatNotification'); setVisualNotification(true); @@ -301,6 +301,13 @@ var Chat = (function (my) { return [chatWidth, availableHeight]; }; + /** + * Indicates if the chat is currently visible. + */ + my.isVisible = function () { + return $('#chatspace').is(":visible"); + }; + /** * Resizes the chat conversation. */ diff --git a/config.js b/config.js index 9d967488f..920e056aa 100644 --- a/config.js +++ b/config.js @@ -16,7 +16,7 @@ var config = { minChromeExtVersion: '0.1', // Required version of Chrome extension enableRtpStats: true, // Enables RTP stats processing openSctp: true, // Toggle to enable/disable SCTP channels -// channelLastN: -1, // The default value of the channel attribute last-n. + channelLastN: -1, // The default value of the channel attribute last-n. // useRtcpMux: true, // useBundle: true, enableRecording: false, diff --git a/contact_list.js b/contact_list.js new file mode 100644 index 000000000..cdbdc3e47 --- /dev/null +++ b/contact_list.js @@ -0,0 +1,235 @@ +/** + * Contact list. + */ +var ContactList = (function (my) { + /** + * Indicates if the chat is currently visible. + * + * @return true if the chat is currently visible, false - + * otherwise + */ + my.isVisible = function () { + return $('#contactlist').is(":visible"); + }; + + /** + * Adds a contact for the given peerJid if such doesn't yet exist. + * + * @param peerJid the peerJid corresponding to the contact + */ + my.ensureAddContact = function(peerJid) { + var resourceJid = Strophe.getResourceFromJid(peerJid); + + var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]'); + + if (!contact || contact.length <= 0) + ContactList.addContact(peerJid); + }; + + /** + * Adds a contact for the given peer jid. + * + * @param peerJid the jid of the contact to add + */ + my.addContact = function(peerJid) { + var resourceJid = Strophe.getResourceFromJid(peerJid); + + var contactlist = $('#contactlist>ul'); + + var newContact = document.createElement('li'); + newContact.id = resourceJid; + + newContact.appendChild(createAvatar()); + newContact.appendChild(createDisplayNameParagraph("Participant")); + + var clElement = contactlist.get(0); + + if (resourceJid === Strophe.getResourceFromJid(connection.emuc.myroomjid) + && $('#contactlist>ul .title')[0].nextSibling.nextSibling) + { + clElement.insertBefore(newContact, + $('#contactlist>ul .title')[0].nextSibling.nextSibling); + } + else { + clElement.appendChild(newContact); + } + }; + + /** + * Removes a contact for the given peer jid. + * + * @param peerJid the peerJid corresponding to the contact to remove + */ + my.removeContact = function(peerJid) { + var resourceJid = Strophe.getResourceFromJid(peerJid); + + var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]'); + + if (contact && contact.length > 0) { + var contactlist = $('#contactlist>ul'); + + contactlist.get(0).removeChild(contact.get(0)); + } + }; + + /** + * Opens / closes the contact list area. + */ + my.toggleContactList = function () { + var contactlist = $('#contactlist'); + var videospace = $('#videospace'); + + var chatSize = (ContactList.isVisible()) ? [0, 0] : Chat.getChatSize(); + var videospaceWidth = window.innerWidth - chatSize[0]; + var videospaceHeight = window.innerHeight; + var videoSize + = getVideoSize(null, null, videospaceWidth, videospaceHeight); + var videoWidth = videoSize[0]; + var videoHeight = videoSize[1]; + var videoPosition = getVideoPosition(videoWidth, + videoHeight, + videospaceWidth, + videospaceHeight); + var horizontalIndent = videoPosition[0]; + var verticalIndent = videoPosition[1]; + + var thumbnailSize = VideoLayout.calculateThumbnailSize(videospaceWidth); + var thumbnailsWidth = thumbnailSize[0]; + var thumbnailsHeight = thumbnailSize[1]; + + if (ContactList.isVisible()) { + videospace.animate({right: chatSize[0], + width: videospaceWidth, + height: videospaceHeight}, + {queue: false, + duration: 500}); + + $('#remoteVideos').animate({height: thumbnailsHeight}, + {queue: false, + duration: 500}); + + $('#remoteVideos>span').animate({height: thumbnailsHeight, + width: thumbnailsWidth}, + {queue: false, + duration: 500, + complete: function() { + $(document).trigger( + "remotevideo.resized", + [thumbnailsWidth, + thumbnailsHeight]); + }}); + + $('#largeVideoContainer').animate({ width: videospaceWidth, + height: videospaceHeight}, + {queue: false, + duration: 500 + }); + + $('#largeVideo').animate({ width: videoWidth, + height: videoHeight, + top: verticalIndent, + bottom: verticalIndent, + left: horizontalIndent, + right: horizontalIndent}, + { queue: false, + duration: 500 + }); + + $('#contactlist').hide("slide", { direction: "right", + queue: false, + duration: 500}); + } + else { + // Undock the toolbar when the chat is shown and if we're in a + // video mode. + if (VideoLayout.isLargeVideoVisible()) + Toolbar.dockToolbar(false); + + videospace.animate({right: chatSize[0], + width: videospaceWidth, + height: videospaceHeight}, + {queue: false, + duration: 500, + complete: function () { + contactlist.trigger('shown'); + } + }); + + $('#remoteVideos').animate({height: thumbnailsHeight}, + {queue: false, + duration: 500}); + + $('#remoteVideos>span').animate({height: thumbnailsHeight, + width: thumbnailsWidth}, + {queue: false, + duration: 500, + complete: function() { + $(document).trigger( + "remotevideo.resized", + [thumbnailsWidth, thumbnailsHeight]); + }}); + + $('#largeVideoContainer').animate({ width: videospaceWidth, + height: videospaceHeight}, + {queue: false, + duration: 500 + }); + + $('#largeVideo').animate({ width: videoWidth, + height: videoHeight, + top: verticalIndent, + bottom: verticalIndent, + left: horizontalIndent, + right: horizontalIndent}, + {queue: false, + duration: 500 + }); + + $('#contactlist').show("slide", { direction: "right", + queue: false, + duration: 500}); + } + }; + + /** + * Creates the avatar element. + * + * @return the newly created avatar element + */ + function createAvatar() { + var avatar = document.createElement('i'); + avatar.className = "icon-avatar avatar"; + + return avatar; + }; + + /** + * Creates the display name paragraph. + * + * @param displayName the display name to set + */ + function createDisplayNameParagraph(displayName) { + var p = document.createElement('p'); + p.innerHTML = displayName; + + return p; + }; + + /** + * Indicates that the display name has changed. + */ + $(document).bind( 'displaynamechanged', + function (event, peerJid, displayName) { + if (peerJid === 'localVideoContainer') + peerJid = connection.emuc.myroomjid; + + var resourceJid = Strophe.getResourceFromJid(peerJid); + + var contactName = $('#contactlist #' + resourceJid + '>p'); + + if (contactName && displayName && displayName.length > 0) + contactName.html(displayName); + }); + + return my; +}(ContactList || {})); diff --git a/css/contact_list.css b/css/contact_list.css new file mode 100644 index 000000000..bb7823ee1 --- /dev/null +++ b/css/contact_list.css @@ -0,0 +1,35 @@ +#contactlist { + background-color:rgba(0,0,0,.65); +} + +#contactlist>ul { + margin: 0px; + padding: 0px; +} + +#contactlist>ul>li { + list-style-type: none; + text-align: left; + color: #FFF; + font-size: 10pt; + padding: 8px 10px; +} + +#contactlist>ul>li>p { + display: inline-block; + vertical-align: middle; + margin: 0px; +} + +#contactlist>ul>li.title { + color: #00ccff; + font-size: 11pt; + border-bottom: 1px solid #676767; +} + +.avatar { + padding: 0px; + margin-right: 10px; + vertical-align: middle; + font-size: 22pt; +} \ No newline at end of file diff --git a/css/font.css b/css/font.css index b9ed14b00..89587c18f 100755 --- a/css/font.css +++ b/css/font.css @@ -23,6 +23,12 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +.icon-contactList:before { + content: "\e615"; +} +.icon-avatar:before { + content: "\e616"; +} .icon-callRetro:before { content: "\e611"; } diff --git a/css/main.css b/css/main.css index 3c9400ecc..d70f989e4 100644 --- a/css/main.css +++ b/css/main.css @@ -8,7 +8,8 @@ html, body{ overflow-x: hidden; } -#chatspace { +#chatspace, +#contactlist { display:none; position:absolute; float: right; @@ -18,12 +19,14 @@ html, body{ width: 20%; max-width: 200px; overflow: hidden; - /* background-color:#dfebf1;*/ - background-color:#FFFFFF; - border-left:1px solid #424242; z-index: 5; } +#chatspace { + background-color:#FFF; + border-left:1px solid #424242; +} + #chatconversation { visibility: hidden; position: relative; @@ -172,7 +175,8 @@ html, body{ 0 -1px 10px #00ccff; } -a.button:hover { +a.button:hover, +a.bottomToolbarButton:hover { top: 0; cursor: pointer; background: rgba(0, 0, 0, 0.3); @@ -408,4 +412,46 @@ form { font-weight: 200; } +#bottomToolbar { + display:block; + position: absolute; + right: -1; + bottom: 40px; + width: 29px; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + color: #FFF; + border: 1px solid #000; + background: rgba(50,50,50,.65); + padding-top: 5px; + padding-bottom: 5px; + z-index: 6; /*+1 from #remoteVideos*/ +} +.bottomToolbarButton { + display: inline-block; + position: relative; + color: #FFFFFF; + top: 0; + padding-top: 3px; + width: 29px; + height: 20px; + cursor: pointer; + font-size: 10pt; + text-align: center; + text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7); + z-index: 1; +} + +.active { + color: #00ccff; +} + +.bottomToolbar_span>span { + display: inline-block; + position: absolute; + font-size: 7pt; + color: #ffffff; + text-align:center; + cursor: pointer; +} diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 229ef6910..9067c0ef2 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -15,7 +15,7 @@ padding: 18px; bottom: 0; left: 0; - right: 0; + right: 20px; width:auto; border:1px solid transparent; z-index: 5; @@ -334,3 +334,7 @@ z-index: 0; border-radius:10px; } + +#mixedstream { + display:none !important; +} diff --git a/data_channels.js b/data_channels.js index 9470f6153..17cc334d4 100644 --- a/data_channels.js +++ b/data_channels.js @@ -74,9 +74,15 @@ function onDataChannel(event) */ var endpointsEnteringLastN = obj.endpointsEnteringLastN; - console.debug( + var stream = obj.stream; + + console.log( "Data channel new last-n event: ", - lastNEndpoints); + lastNEndpoints, endpointsEnteringLastN, obj); + + $(document).trigger( + 'lastnchanged', + [lastNEndpoints, endpointsEnteringLastN, stream]); } else { diff --git a/fonts/jitsi.eot b/fonts/jitsi.eot index 41fe9eeea3aed231312d4312f4b328100fa87392..de08ba6566f62284206324118c1301d06a6176c9 100755 GIT binary patch literal 9028 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 S^KnA=-K6*6s~7OX^8W+6Q3pN% literal 7264 zcmai3dyHJwc|YH|k8|(Ly?0)BW_D(GXLn|HXV&|e-Pzrlv3JelM{QH{E)ZU}YsU|a zAD945ph+Y*N}E=dT3TF%0x>k8L`q7VG;N70`bd>ON>zbEnp70(*rtX4QK1HvB$Z&c zzwg|c_1d8H?%sRe-}%n>{?6tPo}Bm?dLGc@r^J0?R5V3H%wgp6mFVD&;^MS0 z#N3Uy%+5QN_x}++FXG*Q@Z^E}i?{vLaZJJq#C7)^c<_FdNwklmbnZFv$dPnn_uXit zKmW6%hYuX0PygLN3sKT`I5|+WSAeKfK(-r2?`7q7}dGTT&2k?W~pKH z7x3&6Dq$*5t6oiYl{b2GI<*LBZ;rI}lzInKzHGUUEs3OQ$Zty1v<%|Mbd>HTDKUuW zth7u^O3St};hPv%#(f4)HZ1vwVJpWp9Y@JimLf}i%CubDrh|7LzWYVPRZ8A3Ey;6H zJR%+2H7w=W=1FBqW&NsD=#WSD9K7=o`Ym8$NK@Gs{S_HlBu(3p2Ko$}HwQXjVO%iB zpdHvAcqqL+_IbF6l5wA%NG9xu4b0hm=&nO|b>Ek$Uz%y?A-&p8BzPvyauv^nY{=D% za+EHJK~WVGVg_t4gZkN2Sdz<>?JUf%$X05xA*+UJRa50uwO-DaTUq|lx$co*ARUXP z2ZHqTulK8Oef&l`^X6%4bYIo)g;-`F=)M~i((z-*;^{&_K`@Ysb^mN-g`DoszQ1wf zfldbkWed4$cHYth^;*Ol3z&0(Qq^QgzO$8IztB8j%RnDw|r$|#B}3|ZgyfZljzpRH+_-l zJR{ltF(a(eO>nLI4RX6beN@F=%fgDpTIK7HZ+x5;3|dXAKawRnr}3|dx|m*1O?`qM zDa#$Z&C-ksjED&==Mi)2nlV+)BRU8omzXPFF6l;0@gt^qXHp|M|EtUzYUg+gb0tS! zI`6;Sr_0NlA;*{*In5T$gKLB>pQ68o2U`|5iBrHwX@u4+Ku=Wix%m}hq{vnTrR$4L zHiQ<|*6% z6N6zq9u6iFgBj1u3?}TDGGSSzG$g5b(nWjCpdFL_}RfO{JhLvO-#T$>>iK5i0b0VTHuJfj2K^d$WYD76P+3sY({)fQWw z6FS+m3R8;f5eZohmmswHPDJ{ShAj`>MdOx5re(%` z&+0msZJN@ym0?)6CFe}njahWn8VzF!(D0>rHcUGkFy>kb917&2X;Ya7AOEx_ssj&l zqWday;Rw+^cbtUlk#f}HO-vixb+=k^&$HN}FbSsEU$CBi?0V(+wx*}k{lHK$H3nxz z%otNC+o-E#GUYP!P8Vk@A>7w~ZTsKf17~YO_M!I<^sNaSxmnyMj*8QuWP>W2d^r9k z^5p|VEi?>R_s)t@@1WLMp~YIALjuSZNGIo+)b*<=UvEcBr1JKBWxfheUt6rTc@e8t zUX_e|M>b8*^Uba@6&xWc%K=KD%4y{#d?(1J6um}HI-Bs)VbXO`!Zm;{#&LLL>p99) z>NzU_Q3kQhGLs`&pTMwD^0On!)No!;$BJj^ui0zEt;FJv?K|vY$TH0q{lEgCD=7Pb z>41?c92ia}hl?2nl6uM=s7w~Ong{^oKFx=JFHH>vWSFu5SYED_N(@dsl^V|Zer`B* zmL5%f+q9K3wUi0(2nR*C{GURDSycaf4UD*S8!HG z#}5LwH53~VZ{8vfK~sN2d@(xrWi~2xrEN-SqZK(pZN)w`qh8G}l#CH-K?GGtWm#DB zEe=&xr=IO-wQqr@)qKN%`-0eBO|2@4*4~{H6Fc{|`sK_G%azLV4Kp)0a(Uy-Obx`QD2 ztwnkRh5=Uh9sO0kve&zexsHLkz{oWT#SrfwjqOpGvi-E zxrSFio?M3)Ekzi0f6%Nkf=nx9Z}2tXotERBpcDduY-IPe=Vh6@Oq%o6=0axzxvvt=JxUaDu(hGdiK)thM9mNpfa6^Vw{WecG#S|#J7YPhwa9H4LA=O z{6A?_@y};Kg98U-fA}9Vno&Dnq7{PrL5`Odjx>>>en2*J12CamaPpZDLpz>uzk-{yO9t=A^(TB zEn+v;73|^{3Xm6W@}8+`72!XJGm<2R3O@D)Zk(J(o5Q=C=S=tqaFKY|^7d`pDzTLB z7}#D~mL;9IGRO!=>N#&2iiJ!{)S>ZvrBDdO^$6+v0t2q~`$E!Z>D8 zGIN;f8?yw{jH0&}q_>j=B&%IDRe*9S)RqlErjo zvAI~~PbFPUIv^Gzq-+k8JkLgKp&dWqIWEHjOvhz#*d^MHB?Kl^zMLJK8yuP&%c2|_ zMEM;Y3`x|bgFfRRdqU8H&?OGObFce6g7_*VW{6L8itDIkBH`e861NIg zJPb1q!82e9Xy7`S&-&ndHl%OB%Z?5YXS_@*P=4C8EHCY=Acckq?+W1Hq_o3>Zm+osO#w}>5Lx42QK68D6zgo77iRDzt}anAm~O zg}E@xsbG7)y@0H*-Q-=aq6JQt6=yAguy5zYh8_FbTu$uVr=uDj&D6GYRI|PI`%Nz$ zx0w$}mJvGh_3b5cX)`u97UTO@?vEZ8KWE3y)Af4e7No3*u^lI7Sus}`d)EZmH9`@vK9aF$%7=n z_7x55b^IFz0>Kwx^;hxjLE=^VDfBB`@*us3*`q?rvMx#9fW9YCFALnpqkK|agV&Lp z^m~kjUC-tDT;GouyUD$HmxL>q1xE11pa&)K4lUx|_h03tyia~b{+IDNvu2*NZnZ1+ zG5gzUK%I9h?$=^JjK8muDToi)U*YMWNH=<)K>SJXJOtJiMS9>I+25zMG=W9@8jZ3OeuPi`Om z + + \ No newline at end of file diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf index 654dddab54da071b0c56a7081dc3f98e818b12d9..1959f3756f0d35b695deaa8a9c76cc8a320c490e 100755 GIT binary patch literal 8872 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 F{s%6m)sX-I delta 408 zcmeyNJ4HjJ+~3X3KP1GTfk8xsfr|kMgwITVC?-Bpo4Z~hnt?$edeMX3fBl2?jTjgN z1Ax*AAk3lk=TvfTVgXQW1CZ|n!cw7UveOfbL1KS^d=4lU$Vg30VPFtq0rJg2n6di6 zl?#`Sxg%;a!V?JVhKP#P(4G6p7AAy54njIKm&!200r2N#>JvoW7ij#qXiLsMGoI!H(dq&gE8ccfpoIj?B{Foxz&BHZW zUc`k_WO9m#Gmu;%Vj}bXc|>bGzs*+$ZYH253=HQlJ8;A3$v;HQ86_thh?=u92c2w(u3zeqHaQDgES(O@7MAQr;tK6$;EIuipkDA*@|5Mu`by3Aj^ diff --git a/images/avatar2.png b/images/avatar2.png new file mode 100644 index 0000000000000000000000000000000000000000..f59231c872de648d6bfbd688a58dd62039318af5 GIT binary patch literal 1607 zcmV-N2Dtf&P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2i^!3 z5jZ;tC#MSl000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}000G|NklvY`nTe*$<9;12-51IR0-KAZAa82}Om z@I8R<09+Pm@(qAz0De_U{f1@}$uP+T$pZDN;Pw-41}I}BGi;19B*#1l#c`Z4$uzy8 zwBtBouZ)%%dQ&rvgfdDpOK&Sna?~`K7)giCA%|ql6qp2?QwiN*me|}{(sBqSnFeqZ z&97gSQa@DlhgP*C<{JS605_{0K~^xNxCf*CHfmlu|!dn{E(SGMTJ-v{bT(4yx15(w|DDP%4#-1xuw;38|Dwq!V;`{5xtMNQVcZP{`P*IkZqH#Qpt! ziLdbBOE_zy=kaXTIG97rW<{c&H2|>OE85GFL;U5-mkrBB2=#rL>2=?s< zNn1K>C=?1OrapClwd=Lu_EgV!-i7FTjA{vbvA2rvnUzeO#omQn(sRSU?x^(H1 zWOJ<+4r=hk<@5QB!x8SYtYyt1$<578`u%=m>y6*<=jP_7XgUfc-|4!7SzKH+24`_` zQQG}?TJNWJcXt^K2DO6;27}z)-4(yjvT#u6t3~bY?MNn*+Si(7GKu!~cIjr{8>#7= zpP#2?S>hwcvMkQe&+DrxBzeO`)zHeyis->mDaDnQ6`d~M0LcMUWmS<#L<~+O64CAM zfaSg=cw^!mGBPsa$+nKs8>Q6e!|UK@Cg8+kaz&l-cwDEe&%VqdM@>zw@$vDxr_}iP zxNbM2TJNnYX=7ufu7z1C#f^;(oi2WTZ{_NViK!I`1TZ)_SbKVIXb6EoK&OjOj)U{F zsQ~f(`SUe@wyUd4w|mEPpn>N=dwYBA>guX_TJ`kwuv{){tH}G3$I;@YpLG9QGMP+W z*C)1ZTe8UtLZGKr>QM5{U%+`uen`EBgBSm`Eh*g><%uYxU%LOh6HYls)sx zR7(YWu~_87!UFsI`x{neaR_N=XGdQJy9+2*O%L1K+q{4OzR{6#nss+~b9#FEw7hbC z!QM%=f}cKp;?&d>J3Bi)1LdflJ9m!v?%g}?vQOsa9*?}xTVG#iJRWCTTbpO09JRK# zHpXHxuCK43`Ndu}fYQZck$3LgVM|L((?B_DEiEl+Eyu^<;o+rbh2q*0G%L}hU=lE- z7yze9pg69-W=mlJWIYZh>-yO+&3F{d3_PMRfV4-Tq+NeJr!fGT20_WV{d-|B3b;>ZPx_|8rfU#eD`v;9p6Ysf;cp3lz002ovPDHLk FV1j*K_I3aO literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 3cab3be24..14334e6ba 100644 --- a/index.html +++ b/index.html @@ -23,15 +23,16 @@ - - + + - - + + - - + + + @@ -40,18 +41,21 @@ - - + + + + - - - + + + + @@ -158,7 +162,7 @@
- + @@ -222,6 +226,20 @@ + + + + + + + + + + + + + +
@@ -238,5 +256,11 @@
+
+
    +
  • CONTACT LIST
  • +
+
+ diff --git a/media_stream.js b/media_stream.js new file mode 100644 index 000000000..7a71f6aba --- /dev/null +++ b/media_stream.js @@ -0,0 +1,30 @@ +/** + * Provides a wrapper class for the MediaStream. + * + * TODO : Add here the src from the video element and other related properties + * and get rid of some of the mappings that we use throughout the UI. + */ +var MediaStream = (function() { + /** + * Creates a MediaStream object for the given data, session id and ssrc. + * + * @param data the data object from which we obtain the stream, + * the peerjid, etc. + * @param sid the session id + * @param ssrc the ssrc corresponding to this MediaStream + * + * @constructor + */ + function MediaStreamProto(data, sid, ssrc) { + this.VIDEO_TYPE = "Video"; + this.AUDIO_TYPE = "Audio"; + this.stream = data.stream; + this.peerjid = data.peerjid; + this.ssrc = ssrc; + this.session = connection.jingle.sessions[sid]; + this.type = (this.stream.getVideoTracks().length > 0) + ? this.VIDEO_TYPE : this.AUDIO_TYPE; + } + + return MediaStreamProto; +})(); \ No newline at end of file diff --git a/muc.js b/muc.js index ae45b0ee0..154e24614 100644 --- a/muc.js +++ b/muc.js @@ -373,5 +373,13 @@ Strophe.addConnectionPlugin('emuc', { addVideoInfoToPresence: function(isMuted) { this.presMap['videons'] = 'http://jitsi.org/jitmeet/video'; this.presMap['videomuted'] = isMuted.toString(); + }, + findJidFromResource: function(resourceJid) { + var peerJid = null; + Object.keys(this.members).some(function (jid) { + peerJid = jid; + return Strophe.getResourceFromJid(jid) === resourceJid; + }); + return peerJid; } }); diff --git a/toolbar.js b/toolbar.js index 88127fb5a..6f7f8aa4b 100644 --- a/toolbar.js +++ b/toolbar.js @@ -118,16 +118,17 @@ var Toolbar = (function (my) { var conferenceName = roomUrl.substring(roomUrl.lastIndexOf('/') + 1); var subject = "Invitation to a Jitsi Meet (" + conferenceName + ")"; - var body = "Hey there, I%27d like to invite you to a Jitsi Meet" - + " conference I%27ve just set up.%0D%0A%0D%0A" - + "Please click on the following link in order" - + " to join the conference.%0D%0A%0D%0A" - + roomUrl + "%0D%0A%0D%0A" - + sharedKeyText - + "Note that Jitsi Meet is currently only supported by Chromium," - + " Google Chrome and Opera, so you need" - + " to be using one of these browsers.%0D%0A%0D%0A" - + "Talk to you in a sec!"; + var body = "Hey there, I%27d like to invite you to a Jitsi Meet" + + " conference I%27ve just set up.%0D%0A%0D%0A" + + "Please click on the following link in order" + + " to join the conference.%0D%0A%0D%0A" + + roomUrl + + "%0D%0A%0D%0A" + + sharedKeyText + + "Note that Jitsi Meet is currently only supported by Chromium," + + " Google Chrome and Opera, so you need" + + " to be using one of these browsers.%0D%0A%0D%0A" + + "Talk to you in a sec!"; if (window.localStorage.displayname) body += "%0D%0A%0D%0A" + window.localStorage.displayname; diff --git a/util.js b/util.js index cd199309f..682b2f6a6 100644 --- a/util.js +++ b/util.js @@ -52,7 +52,9 @@ var Util = (function (my) { */ my.getAvailableVideoWidth = function () { var chatspaceWidth - = $('#chatspace').is(":visible") ? $('#chatspace').width() : 0; + = (Chat.isVisible() || ContactList.isVisible()) + ? $('#chatspace').width() + : 0; return window.innerWidth - chatspaceWidth; }; diff --git a/videolayout.js b/videolayout.js index d20110e9e..069565cc1 100644 --- a/videolayout.js +++ b/videolayout.js @@ -1,6 +1,8 @@ var VideoLayout = (function (my) { var preMuted = false; var currentDominantSpeaker = null; + var lastNCount = config.channelLastN; + var lastNEndpointsCache = []; my.changeLocalAudio = function(stream) { connection.jingle.localAudio = stream; @@ -52,7 +54,7 @@ var VideoLayout = (function (my) { // Add stream ended handler stream.onended = function () { localVideoContainer.removeChild(localVideo); - VideoLayout.checkChangeLargeVideo(localVideo.src); + VideoLayout.updateRemovedVideo(localVideo.src); }; // Flip video x axis if needed flipXLocalVideo = flipX; @@ -63,6 +65,7 @@ var VideoLayout = (function (my) { RTC.attachMediaStream(localVideoSelector, stream); localVideoSrc = localVideo.src; + VideoLayout.updateLargeVideo(localVideoSrc, 0); }; @@ -71,7 +74,7 @@ var VideoLayout = (function (my) { * another one instead. * @param removedVideoSrc src stream identifier of the video. */ - my.checkChangeLargeVideo = function(removedVideoSrc) { + my.updateRemovedVideo = function(removedVideoSrc) { if (removedVideoSrc === $('#largeVideo').attr('src')) { // this is currently displayed as large // pick the last visible video in the row @@ -83,7 +86,8 @@ var VideoLayout = (function (my) { if (!pick) { console.info("Last visible video no longer exists"); pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0); - if (!pick) { + + if (!pick || !pick.src) { // Try local video console.info("Fallback to local video..."); pick = $('#remoteVideos>span>span>video').get(0); @@ -182,8 +186,9 @@ var VideoLayout = (function (my) { = $('#participant_' + currentDominantSpeaker + '>video') .get(0); - if (dominantSpeakerVideo) + if (dominantSpeakerVideo) { VideoLayout.updateLargeVideo(dominantSpeakerVideo.src, 1); + } } return; @@ -279,28 +284,42 @@ var VideoLayout = (function (my) { * in the document and creates it eventually. * * @param peerJid peer Jid to check. + * + * @return Returns true if the peer container exists, + * false - otherwise */ my.ensurePeerContainerExists = function(peerJid) { - var peerResource = Strophe.getResourceFromJid(peerJid); - var videoSpanId = 'participant_' + peerResource; + ContactList.ensureAddContact(peerJid); + + var resourceJid = Strophe.getResourceFromJid(peerJid); + + var videoSpanId = 'participant_' + resourceJid; if ($('#' + videoSpanId).length > 0) { // If there's been a focus change, make sure we add focus related // interface!! - if (focus && $('#remote_popupmenu_' + peerResource).length <= 0) + if (focus && $('#remote_popupmenu_' + resourceJid).length <= 0) addRemoteVideoMenu( peerJid, document.getElementById(videoSpanId)); - return; } + else { + var container + = VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId); - var container - = VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId); + var nickfield = document.createElement('span'); + nickfield.className = "nick"; + nickfield.appendChild(document.createTextNode(resourceJid)); + container.appendChild(nickfield); - var nickfield = document.createElement('span'); - nickfield.className = "nick"; - nickfield.appendChild(document.createTextNode(peerResource)); - container.appendChild(nickfield); - VideoLayout.resizeThumbnails(); + // In case this is not currently in the last n we don't show it. + if (lastNCount + && lastNCount > 0 + && $('#remoteVideos>span').length >= lastNCount + 2) { + showPeerContainer(resourceJid, false); + } + else + VideoLayout.resizeThumbnails(); + } }; my.addRemoteVideoContainer = function(peerJid, spanId) { @@ -321,9 +340,158 @@ var VideoLayout = (function (my) { }; /** - * Shows the display name for the given video. + * Creates an audio or video stream element. */ - my.setDisplayName = function(videoSpanId, displayName) { + my.createStreamElement = function (sid, stream) { + var isVideo = stream.getVideoTracks().length > 0; + + var element = isVideo + ? document.createElement('video') + : document.createElement('audio'); + var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + + sid + '_' + stream.id; + + element.id = id; + element.autoplay = true; + element.oncontextmenu = function () { return false; }; + + return element; + }; + + my.addRemoteStreamElement + = function (container, sid, stream, peerJid, thessrc) { + var newElementId = null; + + var isVideo = stream.getVideoTracks().length > 0; + + if (container) { + var streamElement = VideoLayout.createStreamElement(sid, stream); + newElementId = streamElement.id; + + container.appendChild(streamElement); + + var sel = $('#' + newElementId); + sel.hide(); + + // If the container is currently visible we attach the stream. + if (!isVideo + || (container.offsetParent !== null && isVideo)) { + RTC.attachMediaStream(sel, stream); + + if (isVideo) + waitForRemoteVideo(sel, thessrc, stream); + } + + stream.onended = function () { + console.log('stream ended', this); + + VideoLayout.removeRemoteStreamElement(stream, container); + + if (peerJid) + ContactList.removeContact(peerJid); + }; + + // Add click handler. + container.onclick = function (event) { + /* + * FIXME It turns out that videoThumb may not exist (if there is + * no actual video). + */ + var videoThumb = $('#' + container.id + '>video').get(0); + + if (videoThumb) + VideoLayout.handleVideoThumbClicked(videoThumb.src); + + event.preventDefault(); + return false; + }; + + // Add hover handler + $(container).hover( + function() { + VideoLayout.showDisplayName(container.id, true); + }, + function() { + var videoSrc = null; + if ($('#' + container.id + '>video') + && $('#' + container.id + '>video').length > 0) { + videoSrc = $('#' + container.id + '>video').get(0).src; + } + + // If the video has been "pinned" by the user we want to + // keep the display name on place. + if (!VideoLayout.isLargeVideoVisible() + || videoSrc !== $('#largeVideo').attr('src')) + VideoLayout.showDisplayName(container.id, false); + } + ); + } + + return newElementId; + }; + + /** + * Removes the remote stream element corresponding to the given stream and + * parent container. + * + * @param stream the stream + * @param container + */ + my.removeRemoteStreamElement = function (stream, container) { + if (!container) + return; + + var select = null; + var removedVideoSrc = null; + if (stream.getVideoTracks().length > 0) { + select = $('#' + container.id + '>video'); + removedVideoSrc = select.get(0).src; + } + else + select = $('#' + container.id + '>audio'); + + // Remove video source from the mapping. + delete videoSrcToSsrc[removedVideoSrc]; + + // Mark video as removed to cancel waiting loop(if video is removed + // before has started) + select.removed = true; + select.remove(); + + var audioCount = $('#' + container.id + '>audio').length; + var videoCount = $('#' + container.id + '>video').length; + + if (!audioCount && !videoCount) { + console.log("Remove whole user", container.id); + // Remove whole container + container.remove(); + Util.playSoundNotification('userLeft'); + VideoLayout.resizeThumbnails(); + } + + if (removedVideoSrc) + VideoLayout.updateRemovedVideo(removedVideoSrc); + }; + + /** + * Show/hide peer container for the given resourceJid. + */ + function showPeerContainer(resourceJid, isShow) { + var peerContainer = $('#participant_' + resourceJid); + + if (!peerContainer) + return; + + if (!peerContainer.is(':visible') && isShow) + peerContainer.show(); + else if (peerContainer.is(':visible') && !isShow) + peerContainer.hide(); + }; + + /** + * Sets the display name for the given video span id. + */ + function setDisplayName(videoSpanId, displayName) { var nameSpan = $('#' + videoSpanId + '>span.displayname'); var defaultLocalDisplayName = "Me"; var defaultRemoteDisplayName = "Speaker"; @@ -334,12 +502,12 @@ var VideoLayout = (function (my) { if (nameSpanElement.id === 'localDisplayName' && $('#localDisplayName').text() !== displayName) { - if (displayName) + if (displayName && displayName.length > 0) $('#localDisplayName').text(displayName + ' (me)'); else $('#localDisplayName').text(defaultLocalDisplayName); } else { - if (displayName) + if (displayName && displayName.length > 0) $('#' + videoSpanId + '_name').text(displayName); else $('#' + videoSpanId + '_name').text(defaultRemoteDisplayName); @@ -359,7 +527,7 @@ var VideoLayout = (function (my) { nameSpan.innerText = defaultRemoteDisplayName; } - if (displayName && displayName.length) { + if (displayName && displayName.length > 0) { nameSpan.innerText = displayName; } @@ -434,11 +602,7 @@ var VideoLayout = (function (my) { * @param isShow indicates if the display name should be shown or hidden */ my.showDisplayName = function(videoSpanId, isShow) { - // FIX: need to use noConflict of jquery, because apparently we're - // using another library that uses $, which conflics with jquery and - // sometimes objects are null because of that!!!!!!!!! - // http://api.jquery.com/jQuery.noConflict/ - var nameSpan = jQuery('#' + videoSpanId + '>span.displayname').get(0); + var nameSpan = $('#' + videoSpanId + '>span.displayname').get(0); if (isShow) { if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length) nameSpan.setAttribute("style", "display:inline-block;"); @@ -459,8 +623,6 @@ var VideoLayout = (function (my) { return; } - var nameSpan = $('#' + videoSpanId + '>span.displayname'); - var statusSpan = $('#' + videoSpanId + '>span.status'); if (!statusSpan.length) { //Add status span @@ -721,6 +883,8 @@ var VideoLayout = (function (my) { /** * Calculates the thumbnail size. + * + * @param videoSpaceWidth the width of the video space */ my.calculateThumbnailSize = function (videoSpaceWidth) { // Calculate the available height, which is the inner window height minus @@ -729,11 +893,15 @@ var VideoLayout = (function (my) { // container used for highlighting shadow. var availableHeight = 100; - var numvids = $('#remoteVideos>span:visible').length; + var numvids = 0; + if (lastNCount && lastNCount > 0) + numvids = lastNCount + 1; + else + numvids = $('#remoteVideos>span:visible').length; // Remove the 3px borders arround videos and border around the remote // videos area - var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 50; + var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70; var availableWidth = availableWinWidth / numvids; var aspectRatio = 16.0 / 9.0; @@ -851,6 +1019,20 @@ var VideoLayout = (function (my) { return currentDominantSpeaker; }; + /** + * Returns the corresponding resource jid to the given peer container + * DOM element. + * + * @return the corresponding resource jid to the given peer container + * DOM element + */ + my.getPeerContainerResourceJid = function (containerElement) { + var i = containerElement.id.indexOf('participant_'); + + if (i >= 0) + return containerElement.id.substring(i + 12); + }; + /** * Adds the remote video menu element for the given jid in the * given parentElement. @@ -965,6 +1147,25 @@ var VideoLayout = (function (my) { VideoLayout.showVideoIndicator(videoSpanId, isMuted); }); + /** + * Display name changed. + */ + $(document).bind('displaynamechanged', + function (event, jid, displayName, status) { + if (jid === 'localVideoContainer' + || jid === connection.emuc.myroomjid) { + setDisplayName('localVideoContainer', + displayName); + } else { + VideoLayout.ensurePeerContainerExists(jid); + + setDisplayName( + 'participant_' + Strophe.getResourceFromJid(jid), + displayName, + status); + } + }); + /** * On dominant speaker changed event. */ @@ -974,29 +1175,113 @@ var VideoLayout = (function (my) { === Strophe.getResourceFromJid(connection.emuc.myroomjid)) return; - // Obtain container for new dominant speaker. - var container = document.getElementById( - 'participant_' + resourceJid); - // Update the current dominant speaker. if (resourceJid !== currentDominantSpeaker) currentDominantSpeaker = resourceJid; else return; + // Obtain container for new dominant speaker. + var container = document.getElementById( + 'participant_' + resourceJid); + // Local video will not have container found, but that's ok // since we don't want to switch to local video. if (container && !focusedVideoSrc) { var video = container.getElementsByTagName("video"); - if (video.length) - { + + // Update the large video if the video source is already available, + // otherwise wait for the "videoactive.jingle" event. + if (video.length && video[0].currentTime > 0) VideoLayout.updateLargeVideo(video[0].src); + } + }); + + /** + * On last N change event. + * + * @param event the event that notified us + * @param lastNEndpoints the list of last N endpoints + * @param endpointsEnteringLastN the list currently entering last N + * endpoints + */ + $(document).bind('lastnchanged', function ( event, + lastNEndpoints, + endpointsEnteringLastN, + stream) { + if (lastNCount !== lastNEndpoints.length) + lastNCount = lastNEndpoints.length; + + lastNEndpointsCache = lastNEndpoints; + + $('#remoteVideos>span').each(function( index, element ) { + var resourceJid = VideoLayout.getPeerContainerResourceJid(element); + + if (resourceJid + && lastNEndpoints.length > 0 + && lastNEndpoints.indexOf(resourceJid) < 0) { + console.log("Remove from last N", resourceJid); + showPeerContainer(resourceJid, false); } + }); + + if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0) + endpointsEnteringLastN = lastNEndpoints; + + if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) { + endpointsEnteringLastN.forEach(function (resourceJid) { + + if (!$('#participant_' + resourceJid).is(':visible')) { + console.log("Add to last N", resourceJid); + showPeerContainer(resourceJid, true); + + mediaStreams.some(function (mediaStream) { + if (mediaStream.peerjid + && Strophe.getResourceFromJid(mediaStream.peerjid) + === resourceJid + && mediaStream.type === mediaStream.VIDEO_TYPE) { + var sel = $('#participant_' + resourceJid + '>video'); + + RTC.attachMediaStream(sel, mediaStream.stream); + waitForRemoteVideo( + sel, + mediaStream.ssrc, + mediaStream.stream); + return true; + } + }); + } + }); + } + }); + + $(document).bind('videoactive.jingle', function (event, videoelem) { + if (videoelem.attr('id').indexOf('mixedmslabel') === -1) { + // ignore mixedmslabela0 and v0 + + videoelem.show(); + VideoLayout.resizeThumbnails(); + + var videoParent = videoelem.parent(); + var parentResourceJid = null; + if (videoParent) + parentResourceJid + = VideoLayout.getPeerContainerResourceJid(videoParent[0]); + + // Update the large video to the last added video only if there's no + // current dominant or focused speaker or update it to the current + // dominant speaker. + if ((!focusedVideoSrc && !VideoLayout.getDominantSpeakerResourceJid()) + || (parentResourceJid + && VideoLayout.getDominantSpeakerResourceJid() + === parentResourceJid)) { + VideoLayout.updateLargeVideo(videoelem.attr('src'), 1); + } + + VideoLayout.showFocusIndicator(); } }); return my; }(VideoLayout || {})); - -