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 41fe9eeea..de08ba656 100755 Binary files a/fonts/jitsi.eot and b/fonts/jitsi.eot differ diff --git a/fonts/jitsi.svg b/fonts/jitsi.svg index 789f6cd31..0276e562c 100755 --- a/fonts/jitsi.svg +++ b/fonts/jitsi.svg @@ -28,4 +28,6 @@ + + \ No newline at end of file diff --git a/fonts/jitsi.ttf b/fonts/jitsi.ttf index 654dddab5..1959f3756 100755 Binary files a/fonts/jitsi.ttf and b/fonts/jitsi.ttf differ diff --git a/fonts/jitsi.woff b/fonts/jitsi.woff index 3aa49fdc4..9c81e8ec1 100755 Binary files a/fonts/jitsi.woff and b/fonts/jitsi.woff differ diff --git a/images/avatar2.png b/images/avatar2.png new file mode 100644 index 000000000..f59231c87 Binary files /dev/null and b/images/avatar2.png differ 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 || {})); - -