diff --git a/README.md b/README.md index f21db5cfb..64fe4e7b0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ You can find information on how to deploy Jitsi Meet in the [installation instru You may also find it helpful to have a look at our sample [config files](https://github.com/jitsi/jitsi-meet/tree/master/doc/example-config-files/) +## Discuss +Please use the [Jitsi dev mailing list](http://lists.jitsi.org/pipermail/dev/) to discuss feature requests before opening an issue on github. + ## Acknowledgements Jitsi Meet started out as a sample conferencing application using Jitsi Videobridge. It was originally developed by Philipp Hancke who then contributed it to the community where development continues with joint forces! diff --git a/app.js b/app.js index db471bd9b..e671642fc 100644 --- a/app.js +++ b/app.js @@ -8,6 +8,17 @@ var nickname = null; var sharedKey = ''; var roomUrl = null; var ssrc2jid = {}; +/** + * The stats collector that process stats data and triggers updates to app.js. + * @type {StatsCollector} + */ +var statsCollector = null; + +/** + * The stats collector for the local stream. + * @type {LocalStatsCollector} + */ +var localStatsCollector = null; /** * Indicates whether ssrc is camera video or desktop stream. @@ -15,6 +26,11 @@ var ssrc2jid = {}; */ var ssrc2videoType = {}; var videoSrcToSsrc = {}; +/** + * Currently focused video "src"(displayed in large video). + * @type {String} + */ +var focusedVideoSrc = null; var mutedAudios = {}; var localVideoSrc = null; @@ -36,8 +52,6 @@ var getVideoPosition; /* window.onbeforeunload = closePageWarning; */ -var preMuted = false; - var sessionTerminated = false; function init() { @@ -113,24 +127,27 @@ function obtainAudioAndVideoPermissions(callback) { function audioStreamReady(stream) { - change_local_audio(stream); + VideoLayout.changeLocalAudio(stream); + + startLocalRtpStatsCollector(stream); if (RTC.browser !== 'firefox') { - getUserMediaWithConstraints(['video'], videoStreamReady, videoStreamFailed, config.resolution || '360'); + getUserMediaWithConstraints(['video'], + videoStreamReady, + videoStreamFailed, + config.resolution || '360'); } else { doJoin(); } } function videoStreamReady(stream) { - - change_local_video(stream, true); + VideoLayout.changeLocalVideo(stream, true); doJoin(); } function videoStreamFailed(error) { - console.warn("Failed to obtain video stream - continue anyway", error); doJoin(); @@ -179,52 +196,6 @@ function doJoin() { connection.emuc.doJoin(roomjid); } -function change_local_audio(stream) { - connection.jingle.localAudio = stream; - RTC.attachMediaStream($('#localAudio'), stream); - document.getElementById('localAudio').autoplay = true; - document.getElementById('localAudio').volume = 0; - if (preMuted) { - toggleAudio(); - preMuted = false; - } -} - -function change_local_video(stream, flipX) { - - connection.jingle.localVideo = stream; - - var localVideo = document.createElement('video'); - localVideo.id = 'localVideo_' + stream.id; - localVideo.autoplay = true; - localVideo.volume = 0; // is it required if audio is separated ? - localVideo.oncontextmenu = function () { return false; }; - - var localVideoContainer = document.getElementById('localVideoWrapper'); - localVideoContainer.appendChild(localVideo); - - var localVideoSelector = $('#' + localVideo.id); - // Add click handler - localVideoSelector.click(function () { - handleVideoThumbClicked(localVideo.src); - }); - // Add stream ended handler - stream.onended = function () { - localVideoContainer.removeChild(localVideo); - checkChangeLargeVideo(localVideo.src); - }; - // Flip video x axis if needed - flipXLocalVideo = flipX; - if (flipX) { - localVideoSelector.addClass("flipVideoX"); - } - // Attach WebRTC stream - RTC.attachMediaStream(localVideoSelector, stream); - - localVideoSrc = localVideo.src; - updateLargeVideo(localVideoSrc, 0); -} - $(document).bind('remotestreamadded.jingle', function (event, data, sid) { function waitForRemoteVideo(selector, sid, ssrc) { if (selector.removed) { @@ -276,12 +247,14 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { var remotes = document.getElementById('remoteVideos'); if (data.peerjid) { - ensurePeerContainerExists(data.peerjid); + VideoLayout.ensurePeerContainerExists(data.peerjid); container = document.getElementById( 'participant_' + Strophe.getResourceFromJid(data.peerjid)); } else { if (data.stream.id !== 'mixedmslabel') { - console.error('can not associate stream', data.stream.id, 'with a participant'); + console.error( 'can not associate stream', + data.stream.id, + 'with a participant'); // We don't want to add it here since it will cause troubles return; } @@ -330,16 +303,37 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { // Remove whole container container.remove(); Util.playSoundNotification('userLeft'); - resizeThumbnails(); + VideoLayout.resizeThumbnails(); } - checkChangeLargeVideo(vid.src); + VideoLayout.checkChangeLargeVideo(vid.src); }; // Add click handler sel.click(function () { - handleVideoThumbClicked(vid.src); + VideoLayout.handleVideoThumbClicked(vid.src); }); + // 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 (focusedVideoSrc && focusedVideoSrc !== videoSrc + || (!focusedVideoSrc + && container.id + !== VideoLayout.getActiveSpeakerContainerId())) + VideoLayout.showDisplayName(container.id, false); + } + ); // an attempt to work around https://github.com/jitsi/jitmeet/issues/32 if (isVideo && @@ -353,48 +347,23 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) { } }); -function handleVideoThumbClicked(videoSrc) { - - $(document).trigger("video.selected", [false]); - - updateLargeVideo(videoSrc, 1); - - $('audio').each(function (idx, el) { - if (el.id.indexOf('mixedmslabel') !== -1) { - el.volume = 0; - el.volume = 1; - } - }); -} - /** - * Checks if removed video is currently displayed and tries to display another one instead. - * @param removedVideoSrc src stream identifier of the video. + * Returns the JID of the user to whom given videoSrc belongs. + * @param videoSrc the video "src" identifier. + * @returns {null | String} the JID of the user to whom given videoSrc + * belongs. */ -function checkChangeLargeVideo(removedVideoSrc) { - if (removedVideoSrc === $('#largeVideo').attr('src')) { - // this is currently displayed as large - // pick the last visible video in the row - // if nobody else is left, this picks the local video - var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video').get(0); +function getJidFromVideoSrc(videoSrc) +{ + if (videoSrc === localVideoSrc) + return connection.emuc.myroomjid; - if (!pick) { - console.info("Last visible video no longer exists"); - pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0); - if (!pick) { - // Try local video - console.info("Fallback to local video..."); - pick = $('#remoteVideos>span>span>video').get(0); - } - } - - // mute if localvideo - if (pick) { - updateLargeVideo(pick.src, pick.volume); - } else { - console.warn("Failed to elect large video"); - } + var ssrc = videoSrcToSsrc[videoSrc]; + if (!ssrc) + { + return null; } + return ssrc2jid[ssrc]; } // an attempt to work around https://github.com/jitsi/jitmeet/issues/32 @@ -427,7 +396,7 @@ function sendKeyframe(pc) { ); } -// really mute video, i.e. dont even send black frames +// Really mute video, i.e. dont even send black frames function muteVideo(pc, unmute) { // FIXME: this probably needs another of those lovely state safeguards... // which checks for iceconn == connected and sigstate == stable @@ -464,12 +433,90 @@ function muteVideo(pc, unmute) { ); } +/** + * Callback called by {@link StatsCollector} in intervals supplied to it's + * constructor. + * @param statsCollector {@link StatsCollector} source of the event. + */ +function statsUpdated(statsCollector) +{ + Object.keys(statsCollector.jid2stats).forEach(function (jid) + { + var peerStats = statsCollector.jid2stats[jid]; + Object.keys(peerStats.ssrc2AudioLevel).forEach(function (ssrc) + { + console.info(jid + " audio level: " + + peerStats.ssrc2AudioLevel[ssrc] + " of ssrc: " + ssrc); +// VideoLayout.updateAudioLevel( Strophe.getResourceFromJid(jid), +// peerStats.ssrc2AudioLevel[ssrc]); + }); + }); +} + +/** + * Callback called by {@link LocalStatsCollector} in intervals supplied to it's + * constructor. + * @param statsCollector {@link LocalStatsCollector} source of the event. + */ +function localStatsUpdated(statsCollector) +{ + console.info("Local audio level: " + statsCollector.audioLevel); +} + +/** + * Starts the {@link StatsCollector} if the feature is enabled in config.js. + */ +function startRtpStatsCollector() +{ + if (config.enableRtpStats && !statsCollector) + { + statsCollector = new StatsCollector( + getConferenceHandler().peerconnection, 200, statsUpdated); + + stopLocalRtpStatsCollector(); + + statsCollector.start(); + } +} +/** + * Starts the {@link LocalStatsCollector} if the feature is enabled in config.js + * @param stream the stream that will be used for collecting statistics. + */ +function startLocalRtpStatsCollector(stream) +{ + if(config.enableRtpStats) + { + localStatsCollector = new LocalStatsCollector(stream, 200, localStatsUpdated); + localStatsCollector.start(); + } +} + +/** + * Stops the {@link LocalStatsCollector}. + */ +function stopLocalRtpStatsCollector() +{ + if(localStatsCollector) + { + localStatsCollector.stop(); + localStatsCollector = null; + } +} + $(document).bind('callincoming.jingle', function (event, sid) { var sess = connection.jingle.sessions[sid]; // TODO: do we check activecall == null? activecall = sess; + startRtpStatsCollector(); + + // Bind data channel listener in case we're a regular participant + if (config.openSctp) + { + bindDataChannelListener(sess.peerconnection); + } + // TODO: check affiliation and/or role console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]); sess.usedrip = true; // not-so-naive trickle ice @@ -478,15 +525,30 @@ $(document).bind('callincoming.jingle', function (event, sid) { }); +$(document).bind('conferenceCreated.jingle', function (event, focus) +{ + startRtpStatsCollector(); +}); + +$(document).bind('conferenceCreated.jingle', function (event, focus) +{ + // Bind data channel listener in case we're the focus + if (config.openSctp) + { + bindDataChannelListener(focus.peerconnection); + } +}); + $(document).bind('callactive.jingle', function (event, videoelem, sid) { if (videoelem.attr('id').indexOf('mixedmslabel') === -1) { // ignore mixedmslabela0 and v0 videoelem.show(); - resizeThumbnails(); + VideoLayout.resizeThumbnails(); - updateLargeVideo(videoelem.attr('src'), 1); + if (!focusedVideoSrc) + VideoLayout.updateLargeVideo(videoelem.attr('src'), 1); - showFocusIndicator(); + VideoLayout.showFocusIndicator(); } }); @@ -507,7 +569,7 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) { var directions = {}; var localSDP = new SDP(sess.peerconnection.localDescription.sdp); localSDP.media.forEach(function (media) { - var type = SDPUtil.parse_mline(media.split('\r\n')[0]).media; + var type = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:')); if (SDPUtil.find_line(media, 'a=ssrc:')) { // assumes a single local ssrc @@ -517,8 +579,8 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) { directions[type] = ( SDPUtil.find_line(media, 'a=sendrecv') || SDPUtil.find_line(media, 'a=recvonly') || - SDPUtil.find_line('a=sendonly') || - SDPUtil.find_line('a=inactive') || + SDPUtil.find_line(media, 'a=sendonly') || + SDPUtil.find_line(media, 'a=inactive') || 'a=sendrecv').substr(2); } }); @@ -555,16 +617,16 @@ $(document).bind('joined.muc', function (event, jid, info) { Etherpad.init(); } - showFocusIndicator(); + VideoLayout.showFocusIndicator(); // Once we've joined the muc show the toolbar - showToolbar(); + Toolbar.showToolbar(); var displayName = ''; if (info.displayName) displayName = info.displayName + ' (me)'; - showDisplayName('localVideoContainer', displayName); + VideoLayout.setDisplayName('localVideoContainer', displayName); }); $(document).bind('entered.muc', function (event, jid, info, pres) { @@ -572,7 +634,7 @@ $(document).bind('entered.muc', function (event, jid, info, pres) { console.log('is focus?' + focus ? 'true' : 'false'); // Add Peer's container - ensurePeerContainerExists(jid); + VideoLayout.ensurePeerContainerExists(jid); if (focus !== null) { // FIXME: this should prepare the video @@ -585,7 +647,7 @@ $(document).bind('entered.muc', function (event, jid, info, pres) { } } else if (sharedKey) { - updateLockButton(); + Toolbar.updateLockButton(); } }); @@ -599,10 +661,20 @@ $(document).bind('left.muc', function (event, jid) { if (container) { // hide here, wait for video to close before removing $(container).hide(); - resizeThumbnails(); + VideoLayout.resizeThumbnails(); } }, 10); + // Unlock large video + if (focusedVideoSrc) + { + if (getJidFromVideoSrc(focusedVideoSrc) === jid) + { + console.info("Focused video owner has left the conference"); + focusedVideoSrc = null; + } + } + connection.jingle.terminateByJid(jid); if (focus == null @@ -662,7 +734,7 @@ $(document).bind('presence.muc', function (event, jid, info, pres) { case 'recvonly': el.hide(); // FIXME: Check if we have to change large video - //checkChangeLargeVideo(el); + //VideoLayout.checkChangeLargeVideo(el); break; } } @@ -670,11 +742,13 @@ $(document).bind('presence.muc', function (event, jid, info, pres) { if (info.displayName) { if (jid === connection.emuc.myroomjid) { - showDisplayName('localVideoContainer', info.displayName + ' (me)'); + VideoLayout.setDisplayName('localVideoContainer', + info.displayName + ' (me)'); } else { - ensurePeerContainerExists(jid); - showDisplayName('participant_' + Strophe.getResourceFromJid(jid), - info.displayName); + VideoLayout.ensurePeerContainerExists(jid); + VideoLayout.setDisplayName( + 'participant_' + Strophe.getResourceFromJid(jid), + info.displayName); } } }); @@ -703,75 +777,10 @@ $(document).bind('passwordrequired.muc', function (event, jid) { }); }); -$(document).bind('audiomuted.muc', function (event, jid, isMuted) { - var videoSpanId = null; - if (jid === connection.emuc.myroomjid) { - videoSpanId = 'localVideoContainer'; - } else { - ensurePeerContainerExists(jid); - videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid); - } - - if (focus) { - mutedAudios[jid] = isMuted; - updateRemoteVideoMenu(jid, isMuted); - } - - if (videoSpanId) - showAudioIndicator(videoSpanId, isMuted); -}); - -$(document).bind('videomuted.muc', function (event, jid, isMuted) { - var videoSpanId = null; - if (jid === connection.emuc.myroomjid) { - videoSpanId = 'localVideoContainer'; - } else { - ensurePeerContainerExists(jid); - videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid); - } - - if (videoSpanId) - showVideoIndicator(videoSpanId, isMuted); -}); - -/** - * Updates the large video with the given new video source. - */ -function updateLargeVideo(newSrc, vol) { - console.log('hover in', newSrc); - - if ($('#largeVideo').attr('src') != newSrc) { - - var isVisible = $('#largeVideo').is(':visible'); - - $('#largeVideo').fadeOut(300, function () { - $(this).attr('src', newSrc); - - // Screen stream is already rotated - var flipX = (newSrc === localVideoSrc) && flipXLocalVideo; - - var videoTransform = document.getElementById('largeVideo').style.webkitTransform; - if (flipX && videoTransform !== 'scaleX(-1)') { - document.getElementById('largeVideo').style.webkitTransform = "scaleX(-1)"; - } - else if (!flipX && videoTransform === 'scaleX(-1)') { - document.getElementById('largeVideo').style.webkitTransform = "none"; - } - - // Change the way we'll be measuring and positioning large video - var isDesktop = isVideoSrcDesktop(newSrc); - getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize; - getVideoPosition = isDesktop ? getDesktopVideoPosition : getCameraVideoPosition; - - if (isVisible) - $(this).fadeIn(300); - }); - } -} - /** * Checks if video identified by given src is desktop stream. - * @param videoSrc eg. blob:https%3A//pawel.jitsi.net/9a46e0bd-131e-4d18-9c14-a9264e8db395 + * @param videoSrc eg. + * blob:https%3A//pawel.jitsi.net/9a46e0bd-131e-4d18-9c14-a9264e8db395 * @returns {boolean} */ function isVideoSrcDesktop(videoSrc) { @@ -799,20 +808,6 @@ function isVideoSrcDesktop(videoSrc) { return isDesktop; } -/** - * Shows/hides the large video. - */ -function setLargeVideoVisible(isVisible) { - if (isVisible) { - $('#largeVideo').css({visibility: 'visible'}); - $('.watermark').css({visibility: 'visible'}); - } - else { - $('#largeVideo').css({visibility: 'hidden'}); - $('.watermark').css({visibility: 'hidden'}); - } -} - function getConferenceHandler() { return focus ? focus : activecall; } @@ -872,38 +867,6 @@ function toggleAudio() { buttonClick("#mute", "icon-microphone icon-mic-disabled"); } -/** - * Positions the large video. - * - * @param videoWidth the stream video width - * @param videoHeight the stream video height - */ -var positionLarge = function (videoWidth, videoHeight) { - var videoSpaceWidth = $('#videospace').width(); - var videoSpaceHeight = window.innerHeight; - - var videoSize = getVideoSize(videoWidth, - videoHeight, - videoSpaceWidth, - videoSpaceHeight); - - var largeVideoWidth = videoSize[0]; - var largeVideoHeight = videoSize[1]; - - var videoPosition = getVideoPosition(largeVideoWidth, - largeVideoHeight, - videoSpaceWidth, - videoSpaceHeight); - - var horizontalIndent = videoPosition[0]; - var verticalIndent = videoPosition[1]; - - positionVideo($('#largeVideo'), - largeVideoWidth, - largeVideoHeight, - horizontalIndent, verticalIndent); -}; - /** * Returns an array of the video horizontal and vertical indents, * so that if fits its parent. @@ -982,116 +945,12 @@ function getCameraVideoSize(videoWidth, return [availableWidth, availableHeight]; } -/** - * Returns an array of the video dimensions, so that it keeps it's aspect ratio and fits available area with it's - * larger dimension. This method ensures that whole video will be visible and can leave empty areas. - * - * @return an array with 2 elements, the video width and the video height - */ -function getDesktopVideoSize(videoWidth, - videoHeight, - videoSpaceWidth, - videoSpaceHeight) { - if (!videoWidth) - videoWidth = currentVideoWidth; - if (!videoHeight) - videoHeight = currentVideoHeight; - - var aspectRatio = videoWidth / videoHeight; - - var availableWidth = Math.max(videoWidth, videoSpaceWidth); - var availableHeight = Math.max(videoHeight, videoSpaceHeight); - - videoSpaceHeight -= $('#remoteVideos').outerHeight(); - - if (availableWidth / aspectRatio >= videoSpaceHeight) - { - availableHeight = videoSpaceHeight; - availableWidth = availableHeight * aspectRatio; - } - - if (availableHeight * aspectRatio >= videoSpaceWidth) - { - availableWidth = videoSpaceWidth; - availableHeight = availableWidth / aspectRatio; - } - - return [availableWidth, availableHeight]; -} - -/** - * Sets the size and position of the given video element. - * - * @param video the video element to position - * @param width the desired video width - * @param height the desired video height - * @param horizontalIndent the left and right indent - * @param verticalIndent the top and bottom indent - */ -function positionVideo(video, - width, - height, - horizontalIndent, - verticalIndent) { - video.width(width); - video.height(height); - video.css({ top: verticalIndent + 'px', - bottom: verticalIndent + 'px', - left: horizontalIndent + 'px', - right: horizontalIndent + 'px'}); -} - -var resizeLargeVideoContainer = function () { - Chat.resizeChat(); - var availableHeight = window.innerHeight; - var availableWidth = Util.getAvailableVideoWidth(); - - if (availableWidth < 0 || availableHeight < 0) return; - - $('#videospace').width(availableWidth); - $('#videospace').height(availableHeight); - $('#largeVideoContainer').width(availableWidth); - $('#largeVideoContainer').height(availableHeight); - - resizeThumbnails(); -}; - -var calculateThumbnailSize = function () { - // Calculate the available height, which is the inner window height minus - // 39px for the header minus 2px for the delimiter lines on the top and - // bottom of the large video, minus the 36px space inside the remoteVideos - // container used for highlighting shadow. - var availableHeight = 100; - - var numvids = $('#remoteVideos>span:visible').length; - - // Remove the 1px borders arround videos and the chat width. - var availableWinWidth = $('#remoteVideos').width() - 2 * numvids - 50; - var availableWidth = availableWinWidth / numvids; - var aspectRatio = 16.0 / 9.0; - var maxHeight = Math.min(160, availableHeight); - availableHeight = Math.min(maxHeight, availableWidth / aspectRatio); - if (availableHeight < availableWidth / aspectRatio) { - availableWidth = Math.floor(availableHeight * aspectRatio); - } - - return [availableWidth, availableHeight]; -}; - -function resizeThumbnails() { - var thumbnailSize = calculateThumbnailSize(); - var width = thumbnailSize[0]; - var height = thumbnailSize[1]; - - // size videos so that while keeping AR and max height, we have a nice fit - $('#remoteVideos').height(height); - $('#remoteVideos>span').width(width); - $('#remoteVideos>span').height(height); -} - $(document).ready(function () { Chat.init(); + $('body').popover({ selector: '[data-toggle=popover]', + trigger: 'click hover'}); + // Set the defaults for prompt dialogs. jQuery.prompt.setDefaults({persistent: false}); @@ -1106,17 +965,17 @@ $(document).ready(function () { getVideoSize = getCameraVideoSize; getVideoPosition = getCameraVideoPosition; - resizeLargeVideoContainer(); + VideoLayout.resizeLargeVideoContainer(); $(window).resize(function () { - resizeLargeVideoContainer(); - positionLarge(); + VideoLayout.resizeLargeVideoContainer(); + VideoLayout.positionLarge(); }); // Listen for large video size updates document.getElementById('largeVideo') .addEventListener('loadedmetadata', function (e) { currentVideoWidth = this.videoWidth; currentVideoHeight = this.videoHeight; - positionLarge(currentVideoWidth, currentVideoHeight); + VideoLayout.positionLarge(currentVideoWidth, currentVideoHeight); }); if (!$('#settings').is(':visible')) { @@ -1150,10 +1009,10 @@ $(window).bind('beforeunload', function () { } }); } - disposeConference(); + disposeConference(true); }); -function disposeConference() { +function disposeConference(onUnload) { var handler = getConferenceHandler(); if (handler && handler.peerconnection) { // FIXME: probably removing streams is not required and close() should be enough @@ -1165,6 +1024,18 @@ function disposeConference() { } handler.peerconnection.close(); } + if (statsCollector) + { + statsCollector.stop(); + statsCollector = null; + } + if(!onUnload) { + startLocalRtpStatsCollector(connection.jingle.localAudio); + } + else + { + stopLocalRtpStatsCollector(); + } focus = null; activecall = null; } @@ -1221,167 +1092,6 @@ function openMessageDialog(titleString, messageString) { ); } -/** - * Opens the lock room dialog. - */ -function openLockDialog() { - // Only the focus is able to set a shared key. - if (focus === null) { - if (sharedKey) - $.prompt("This conversation is currently protected by a shared secret key.", - { - title: "Secrect key", - persistent: false - } - ); - else - $.prompt("This conversation isn't currently protected by a secret key. Only the owner of the conference could set a shared key.", - { - title: "Secrect key", - persistent: false - } - ); - } else { - if (sharedKey) { - $.prompt("Are you sure you would like to remove your secret key?", - { - title: "Remove secrect key", - persistent: false, - buttons: { "Remove": true, "Cancel": false}, - defaultButton: 1, - submit: function (e, v, m, f) { - if (v) { - setSharedKey(''); - lockRoom(false); - } - } - } - ); - } else { - $.prompt('

Set a secrect key to lock your room

' + - '', - { - persistent: false, - buttons: { "Save": true, "Cancel": false}, - defaultButton: 1, - loaded: function (event) { - document.getElementById('lockKey').focus(); - }, - submit: function (e, v, m, f) { - if (v) { - var lockKey = document.getElementById('lockKey'); - - if (lockKey.value) { - setSharedKey(Util.escapeHtml(lockKey.value)); - lockRoom(true); - } - } - } - } - ); - } - } -} - -/** - * Opens the invite link dialog. - */ -function openLinkDialog() { - var inviteLink; - if (roomUrl == null) - inviteLink = "Your conference is currently being created..."; - else - inviteLink = encodeURI(roomUrl); - - $.prompt('', - { - title: "Share this link with everyone you want to invite", - persistent: false, - buttons: { "Invite": true, "Cancel": false}, - defaultButton: 1, - loaded: function (event) { - if (roomUrl) - document.getElementById('inviteLinkRef').select(); - else - document.getElementById('jqi_state0_buttonInvite') - .disabled = true; - }, - submit: function (e, v, m, f) { - if (v) { - if (roomUrl) { - inviteParticipants(); - } - } - } - } - ); -} - -/** - * Invite participants to conference. - */ -function inviteParticipants() { - if (roomUrl == null) - return; - - 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" - + "Note that Jitsi Meet is currently only supported by Chromim," - + " 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; - - window.open("mailto:?subject=" + subject + "&body=" + body, '_blank'); -} - -/** - * Opens the settings dialog. - */ -function openSettingsDialog() { - $.prompt('

Configure your conference

' + - ' Participants join muted
' + - ' Require nicknames

' + - 'Set a secrect key to lock your room: ', - { - persistent: false, - buttons: { "Save": true, "Cancel": false}, - defaultButton: 1, - loaded: function (event) { - document.getElementById('lockKey').focus(); - }, - submit: function (e, v, m, f) { - if (v) { - if ($('#initMuted').is(":checked")) { - // it is checked - } - - if ($('#requireNicknames').is(":checked")) { - // it is checked - } - /* - var lockKey = document.getElementById('lockKey'); - - if (lockKey.value) - { - setSharedKey(lockKey.value); - lockRoom(true); - } - */ - } - } - } - ); -} - /** * Locks / unlocks the room. */ @@ -1391,7 +1101,7 @@ function lockRoom(lock) { else connection.emuc.lockRoom(''); - updateLockButton(); + Toolbar.updateLockButton(); } /** @@ -1401,86 +1111,6 @@ function setSharedKey(sKey) { sharedKey = sKey; } -/** - * Updates the lock button state. - */ -function updateLockButton() { - buttonClick("#lockIcon", "icon-security icon-security-locked"); -} - -/** - * Hides the toolbar. - */ -var hideToolbar = function () { - - var isToolbarHover = false; - $('#header').find('*').each(function () { - var id = $(this).attr('id'); - if ($("#" + id + ":hover").length > 0) { - isToolbarHover = true; - } - }); - - clearTimeout(toolbarTimeout); - toolbarTimeout = null; - - if (!isToolbarHover) { - $('#header').hide("slide", { direction: "up", duration: 300}); - } - else { - toolbarTimeout = setTimeout(hideToolbar, 2000); - } -}; - -/** - * Shows the call main toolbar. - */ -function showToolbar() { - if (!$('#header').is(':visible')) { - $('#header').show("slide", { direction: "up", duration: 300}); - - if (toolbarTimeout) { - clearTimeout(toolbarTimeout); - toolbarTimeout = null; - } - toolbarTimeout = setTimeout(hideToolbar, 2000); - } - - if (focus != null) - { -// TODO: Enable settings functionality. Need to uncomment the settings button in index.html. -// $('#settingsButton').css({visibility:"visible"}); - } - - // Show/hide desktop sharing button - showDesktopSharingButton(); -} - -/** - * Docks/undocks the toolbar. - * - * @param isDock indicates what operation to perform - */ -function dockToolbar(isDock) { - if (isDock) { - // First make sure the toolbar is shown. - if (!$('#header').is(':visible')) { - showToolbar(); - } - // Then clear the time out, to dock the toolbar. - clearTimeout(toolbarTimeout); - toolbarTimeout = null; - } - else { - if (!$('#header').is(':visible')) { - showToolbar(); - } - else { - toolbarTimeout = setTimeout(hideToolbar, 2000); - } - } -} - /** * Updates the room invite url. */ @@ -1501,378 +1131,19 @@ function updateRoomUrl(newRoomUrl) { */ function closePageWarning() { if (focus !== null) - return "You are the owner of this conference call and you are about to end it."; + return "You are the owner of this conference call and" + + " you are about to end it."; else return "You are about to leave this conversation."; } -/** - * Shows a visual indicator for the focus of the conference. - * Currently if we're not the owner of the conference we obtain the focus - * from the connection.jingle.sessions. - */ -function showFocusIndicator() { - if (focus !== null) { - var indicatorSpan = $('#localVideoContainer .focusindicator'); - - if (indicatorSpan.children().length === 0) - { - createFocusIndicatorElement(indicatorSpan[0]); - } - } - else if (Object.keys(connection.jingle.sessions).length > 0) { - // If we're only a participant the focus will be the only session we have. - var session - = connection.jingle.sessions - [Object.keys(connection.jingle.sessions)[0]]; - var focusId - = 'participant_' + Strophe.getResourceFromJid(session.peerjid); - var focusContainer = document.getElementById(focusId); - if (!focusContainer) { - console.error("No focus container!"); - return; - } - var indicatorSpan = $('#' + focusId + ' .focusindicator'); - - if (!indicatorSpan || indicatorSpan.length === 0) { - indicatorSpan = document.createElement('span'); - indicatorSpan.className = 'focusindicator'; - focusContainer.appendChild(indicatorSpan); - - createFocusIndicatorElement(indicatorSpan); - } - } -} - -/** - * Checks if container for participant identified by given peerJid exists in the document and creates it eventually. - * @param peerJid peer Jid to check. - */ -function ensurePeerContainerExists(peerJid) { - var peerResource = Strophe.getResourceFromJid(peerJid); - var videoSpanId = 'participant_' + peerResource; - - 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) - addRemoteVideoMenu( peerJid, - document.getElementById(videoSpanId)); - return; - } - - var container = addRemoteVideoContainer(peerJid, videoSpanId); - - var nickfield = document.createElement('span'); - nickfield.className = "nick"; - nickfield.appendChild(document.createTextNode(peerResource)); - container.appendChild(nickfield); - resizeThumbnails(); -} - -function addRemoteVideoContainer(peerJid, spanId) { - var container = document.createElement('span'); - container.id = spanId; - container.className = 'videocontainer'; - var remotes = document.getElementById('remoteVideos'); - - // If the peerJid is null then this video span couldn't be directly - // associated with a participant (this could happen in the case of prezi). - if (focus && peerJid != null) - addRemoteVideoMenu(peerJid, container); - - remotes.appendChild(container); - return container; -} - -/** - * Creates the element indicating the focus of the conference. - */ -function createFocusIndicatorElement(parentElement) { - var focusIndicator = document.createElement('i'); - focusIndicator.className = 'fa fa-star'; - focusIndicator.title = "The owner of this conference"; - parentElement.appendChild(focusIndicator); -} - -function addRemoteVideoMenu(jid, parentElement) { - var spanElement = document.createElement('span'); - spanElement.className = 'remotevideomenu'; - - parentElement.appendChild(spanElement); - - var menuElement = document.createElement('i'); - menuElement.className = 'fa fa-angle-down'; - menuElement.title = 'Remote user controls'; - spanElement.appendChild(menuElement); - -// - var popupmenuElement = document.createElement('ul'); - popupmenuElement.className = 'popupmenu'; - popupmenuElement.id = 'remote_popupmenu_' + Strophe.getResourceFromJid(jid); - spanElement.appendChild(popupmenuElement); - - var muteMenuItem = document.createElement('li'); - var muteLinkItem = document.createElement('a'); - - var mutedIndicator = ""; - - if (!mutedAudios[jid]) { - muteLinkItem.innerHTML = mutedIndicator + 'Mute'; - muteLinkItem.className = 'mutelink'; - } - else { - muteLinkItem.innerHTML = mutedIndicator + 'Muted'; - muteLinkItem.className = 'mutelink disabled'; - } - - muteLinkItem.onclick = function(){ - if ($(this).attr('disabled') != undefined) { - event.preventDefault(); - } - var isMute = !mutedAudios[jid]; - connection.moderate.setMute(jid, isMute); - popupmenuElement.setAttribute('style', 'display:none;'); - - if (isMute) { - this.innerHTML = mutedIndicator + 'Muted'; - this.className = 'mutelink disabled'; - } - else { - this.innerHTML = mutedIndicator + 'Mute'; - this.className = 'mutelink'; - } - }; - - muteMenuItem.appendChild(muteLinkItem); - popupmenuElement.appendChild(muteMenuItem); - - var ejectIndicator = ""; - - var ejectMenuItem = document.createElement('li'); - var ejectLinkItem = document.createElement('a'); - ejectLinkItem.innerHTML = ejectIndicator + 'Kick out'; - ejectLinkItem.onclick = function(){ - connection.moderate.eject(jid); - popupmenuElement.setAttribute('style', 'display:none;'); - }; - - ejectMenuItem.appendChild(ejectLinkItem); - popupmenuElement.appendChild(ejectMenuItem); -} - -function updateRemoteVideoMenu(jid, isMuted) { - var muteMenuItem - = $('#remote_popupmenu_' - + Strophe.getResourceFromJid(jid) - + '>li>a.mutelink'); - - var mutedIndicator = ""; - - if (muteMenuItem.length) { - var muteLink = muteMenuItem.get(0); - - if (isMuted === 'true') { - muteLink.innerHTML = mutedIndicator + 'Muted'; - muteLink.className = 'mutelink disabled'; - } - else { - muteLink.innerHTML = mutedIndicator + 'Mute'; - muteLink.className = 'mutelink'; - } - } -} - -/** - * Toggles the application in and out of full screen mode - * (a.k.a. presentation mode in Chrome). - */ -function toggleFullScreen() { - var fsElement = document.documentElement; - - if (!document.mozFullScreen && !document.webkitIsFullScreen) { - //Enter Full Screen - if (fsElement.mozRequestFullScreen) { - fsElement.mozRequestFullScreen(); - } - else { - fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); - } - } else { - //Exit Full Screen - if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else { - document.webkitCancelFullScreen(); - } - } -} - -/** - * Shows the display name for the given video. - */ -function showDisplayName(videoSpanId, displayName) { - var nameSpan = $('#' + videoSpanId + '>span.displayname'); - - // If we already have a display name for this video. - if (nameSpan.length > 0) { - var nameSpanElement = nameSpan.get(0); - - if (nameSpanElement.id === 'localDisplayName' && - $('#localDisplayName').text() !== displayName) { - $('#localDisplayName').text(displayName); - } else { - $('#' + videoSpanId + '_name').text(displayName); - } - } else { - var editButton = null; - - if (videoSpanId === 'localVideoContainer') { - editButton = createEditDisplayNameButton(); - } - if (displayName.length) { - nameSpan = document.createElement('span'); - nameSpan.className = 'displayname'; - nameSpan.innerText = displayName; - $('#' + videoSpanId)[0].appendChild(nameSpan); - } - - if (!editButton) { - nameSpan.id = videoSpanId + '_name'; - } else { - nameSpan.id = 'localDisplayName'; - $('#' + videoSpanId)[0].appendChild(editButton); - - var editableText = document.createElement('input'); - editableText.className = 'displayname'; - editableText.id = 'editDisplayName'; - - if (displayName.length) { - editableText.value = displayName.substring(0, displayName.indexOf(' (me)')); - } - - editableText.setAttribute('style', 'display:none;'); - editableText.setAttribute('placeholder', 'ex. Jane Pink'); - $('#' + videoSpanId)[0].appendChild(editableText); - - $('#localVideoContainer .displayname').bind("click", function (e) { - e.preventDefault(); - $('#localDisplayName').hide(); - $('#editDisplayName').show(); - $('#editDisplayName').focus(); - $('#editDisplayName').select(); - - var inputDisplayNameHandler = function (name) { - if (nickname !== name) { - nickname = name; - window.localStorage.displayname = nickname; - connection.emuc.addDisplayNameToPresence(nickname); - connection.emuc.sendPresence(); - - Chat.setChatConversationMode(true); - } - - if (!$('#localDisplayName').is(":visible")) { - $('#localDisplayName').text(nickname + " (me)"); - $('#localDisplayName').show(); - $('#editDisplayName').hide(); - } - }; - - $('#editDisplayName').one("focusout", function (e) { - inputDisplayNameHandler(this.value); - }); - - $('#editDisplayName').on('keydown', function (e) { - if (e.keyCode === 13) { - e.preventDefault(); - inputDisplayNameHandler(this.value); - } - }); - }); - } - } -} - -/** - * Creates the edit display name button. - * - * @returns the edit button - */ -function createEditDisplayNameButton() { - var editButton = document.createElement('a'); - editButton.className = 'displayname'; - editButton.innerHTML = ''; - - return editButton; -} - -/** - * Shows audio muted indicator over small videos. - */ -function showAudioIndicator(videoSpanId, isMuted) { - var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted'); - - if (isMuted === 'false') { - if (audioMutedSpan.length > 0) { - audioMutedSpan.remove(); - } - } - else { - var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted'); - - audioMutedSpan = document.createElement('span'); - audioMutedSpan.className = 'audioMuted'; - if (videoMutedSpan) { - audioMutedSpan.right = '30px'; - } - $('#' + videoSpanId)[0].appendChild(audioMutedSpan); - - var mutedIndicator = document.createElement('i'); - mutedIndicator.className = 'icon-mic-disabled'; - mutedIndicator.title = "Participant is muted"; - audioMutedSpan.appendChild(mutedIndicator); - } -} - -/** - * Shows video muted indicator over small videos. - */ -function showVideoIndicator(videoSpanId, isMuted) { - var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted'); - - if (isMuted === 'false') { - if (videoMutedSpan.length > 0) { - videoMutedSpan.remove(); - } - } - else { - var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted'); - - videoMutedSpan = document.createElement('span'); - videoMutedSpan.className = 'videoMuted'; - if (audioMutedSpan) { - videoMutedSpan.right = '30px'; - } - $('#' + videoSpanId)[0].appendChild(videoMutedSpan); - - var mutedIndicator = document.createElement('i'); - mutedIndicator.className = 'icon-camera-disabled'; - mutedIndicator.title = "Participant has stopped the camera."; - videoMutedSpan.appendChild(mutedIndicator); - } -} - /** * Resizes and repositions videos in full screen mode. */ $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange', function () { - resizeLargeVideoContainer(); - positionLarge(); + VideoLayout.resizeLargeVideoContainer(); + VideoLayout.positionLarge(); isFullScreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen; @@ -1899,3 +1170,13 @@ function setView(viewName) { // document.getElementById('videolayout_fullscreen').disabled = true; // } } + +$(document).bind('fatalError.jingle', + function (event, session, error) + { + sessionTerminated = true; + connection.emuc.doLeave(); + openMessageDialog( "Sorry", + "Your browser version is too old. Please update and try again..."); + } +); diff --git a/chat.js b/chat.js index ad7e50ba5..b7255a346 100644 --- a/chat.js +++ b/chat.js @@ -39,10 +39,19 @@ var Chat = (function (my) { $('#usermsg').keydown(function (event) { if (event.keyCode === 13) { event.preventDefault(); - var message = Util.escapeHtml(this.value); + var value = this.value; $('#usermsg').val('').trigger('autosize.resize'); this.focus(); - connection.emuc.sendMessage(message, nickname); + var command = new CommandsProcessor(value); + if(command.isCommand()) + { + command.processCommand(); + } + else + { + var message = Util.escapeHtml(value); + connection.emuc.sendMessage(message, nickname); + } } }); @@ -90,6 +99,45 @@ var Chat = (function (my) { { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000); }; + /** + * Appends error message to the conversation + * @param errorMessage the received error message. + * @param originalText the original message. + */ + my.chatAddError = function(errorMessage, originalText) + { + errorMessage = Util.escapeHtml(errorMessage); + originalText = Util.escapeHtml(originalText); + + $('#chatconversation').append('
Error: ' + + 'Your message' + (originalText? (' \"'+ originalText + '\"') : "") + + ' was not sent.' + (errorMessage? (' Reason: ' + errorMessage) : '') + + '
'); + $('#chatconversation').animate( + { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000); + + } + + /** + * Sets the subject to the UI + * @param subject the subject + */ + my.chatSetSubject = function(subject) + { + if(subject) + subject = subject.trim(); + $('#subject').html(linkify(Util.escapeHtml(subject))); + if(subject == "") + { + $("#subject").css({display: "none"}); + } + else + { + $("#subject").css({display: "block"}); + } + } + + /** * Opens / closes the chat area. */ @@ -242,7 +290,7 @@ var Chat = (function (my) { if (unreadMessages) { unreadMsgElement.innerHTML = unreadMessages.toString(); - showToolbar(); + Toolbar.showToolbar(); var chatButtonElement = document.getElementById('chatButton').parentNode; diff --git a/commands.js b/commands.js new file mode 100644 index 000000000..110e8cf1c --- /dev/null +++ b/commands.js @@ -0,0 +1,98 @@ +/** + * Handles commands received via chat messages. + */ +var CommandsProcessor = (function() +{ + /** + * Constructs new CommandProccessor instance from a message. + * @param message the message + * @constructor + */ + function CommandsPrototype(message) + { + /** + * Extracts the command from the message. + * @param message the received message + * @returns {string} the command + */ + function getCommand(message) + { + if(message) + { + for(var command in commands) + { + if(message.indexOf("/" + command) == 0) + return command; + } + } + return ""; + }; + + var command = getCommand(message); + + /** + * Returns the name of the command. + * @returns {String} the command + */ + this.getCommand = function() + { + return command; + } + + + var messageArgument = message.substr(command.length + 2); + + /** + * Returns the arguments of the command. + * @returns {string} + */ + this.getArgument = function() + { + return messageArgument; + } + } + + /** + * Checks whether this instance is valid command or not. + * @returns {boolean} + */ + CommandsPrototype.prototype.isCommand = function() + { + if(this.getCommand()) + return true; + return false; + } + + /** + * Processes the command. + */ + CommandsPrototype.prototype.processCommand = function() + { + if(!this.isCommand()) + return; + + commands[this.getCommand()](this.getArgument()); + + } + + /** + * Processes the data for topic command. + * @param commandArguments the arguments of the topic command. + */ + var processTopic = function(commandArguments) + { + var topic = Util.escapeHtml(commandArguments); + connection.emuc.setSubject(topic); + } + + /** + * List with supported commands. The keys are the names of the commands and + * the value is the function that processes the message. + * @type {{String: function}} + */ + var commands = { + "topic" : processTopic + }; + + return CommandsPrototype; +})(); diff --git a/config.js b/config.js index be326bf83..22d93589a 100644 --- a/config.js +++ b/config.js @@ -11,5 +11,7 @@ var config = { bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable. chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension - minChromeExtVersion: '0.1' // Required version of Chrome extension + minChromeExtVersion: '0.1', // Required version of Chrome extension + enableRtpStats: false, // Enables RTP stats processing + openSctp: true //Toggle to enable/disable SCTP channels }; \ No newline at end of file diff --git a/css/main.css b/css/main.css index afbd5b8cc..eb9cc650d 100644 --- a/css/main.css +++ b/css/main.css @@ -43,6 +43,10 @@ html, body{ color: #087dba; } +.errorMessage { + color: red; +} + .remoteuser { color: #424242; } diff --git a/css/popover.css b/css/popover.css new file mode 100644 index 000000000..f32c6034a --- /dev/null +++ b/css/popover.css @@ -0,0 +1,124 @@ +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 300px; + min-width: 100px; + padding: 1px; + text-align: left; + color: #428bca; + background-color: #ffffff; + background-clip: padding-box; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4); + white-space: normal; +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover-title { + margin: 0; + padding: 8px 14px; + font-size: 11pt; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; + font-size: 10pt; + white-space:pre-wrap; + text-align: center; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + border-width: 10px; + content: ""; +} +.popover.top > .arrow { + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + border-top-color: #999999; + border-top-color: rgba(0, 0, 0, 0.25); + bottom: -11px; +} +.popover.top > .arrow:after { + content: " "; + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + border-top-color: #ffffff; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-left-width: 0; + border-right-color: #999999; + border-right-color: rgba(0, 0, 0, 0.25); +} +.popover.right > .arrow:after { + content: " "; + left: 1px; + bottom: -10px; + border-left-width: 0; + border-right-color: #ffffff; +} +.popover.bottom > .arrow { + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999999; + border-bottom-color: rgba(0, 0, 0, 0.25); + top: -11px; +} +.popover.bottom > .arrow:after { + content: " "; + top: 1px; + margin-left: -10px; + border-top-width: 0; + border-bottom-color: #ffffff; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999999; + border-left-color: rgba(0, 0, 0, 0.25); +} +.popover.left > .arrow:after { + content: " "; + right: 1px; + border-right-width: 0; + border-left-color: #ffffff; + bottom: -10px; +} \ No newline at end of file diff --git a/css/popup_menu.css b/css/popup_menu.css index 74d99551d..f4ba0008b 100644 --- a/css/popup_menu.css +++ b/css/popup_menu.css @@ -9,7 +9,7 @@ ul.popupmenu { padding-bottom: 5px; padding-top: 5px; right: 10px; - left: 0px; + left: -5px; width: 100px; background-color: rgba(0,0,0,1); -webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000; @@ -21,7 +21,7 @@ ul.popupmenu:after { display: block; position: absolute; bottom: -9px; - left: 13px; + left: 11px; } ul.popupmenu li { diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 7c7823100..5c04ef009 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -32,9 +32,11 @@ background-size: contain; border-radius:8px; border: 2px solid #212425; + margin-right: 3px; } -#remoteVideos .videocontainer:hover { +#remoteVideos .videocontainer:hover, +#remoteVideos .videocontainer.videoContainerFocused { width: 100%; height: 100%; content:""; @@ -49,7 +51,6 @@ -webkit-animation-iteration-count: 1; -webkit-box-shadow: 0 0 18px #388396; border: 2px solid #388396; - z-index: 3; } #localVideoWrapper { @@ -93,6 +94,11 @@ height: 100%; } +.activespeaker { + -webkit-filter: grayscale(1); + filter: grayscale(1); +} + #etherpad, #presentation { text-align: center; @@ -119,6 +125,7 @@ text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7); border: 0px; z-index: 2; + text-align: center; } #remoteVideos .nick { @@ -133,25 +140,22 @@ .videocontainer>span.displayname, .videocontainer>input.displayname { - display: inline-block; + display: none; position: absolute; - background: -webkit-linear-gradient(left, rgba(0,0,0,.7), rgba(0,0,0,0)); color: #FFFFFF; - bottom: 0; - left: 0; - padding: 3px 5px; - width: 100%; - height: auto; - max-height: 18px; - font-size: 9pt; - text-align: left; + background: rgba(0,0,0,.7); + text-align: center; text-overflow: ellipsis; + width: 70%; + height: 20%; + left: 15%; + top: 40%; + padding: 5px; + font-size: 11pt; overflow: hidden; white-space: nowrap; z-index: 2; - box-sizing: border-box; - border-bottom-left-radius:4px; - border-bottom-right-radius:4px; + border-radius:20px; } #localVideoContainer>span.displayname:hover { @@ -162,6 +166,10 @@ pointer-events: none; } +.videocontainer>input.displayname { + height: auto; +} + #localDisplayName { pointer-events: auto !important; } @@ -190,6 +198,7 @@ text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7); border: 0px; z-index: 3; + text-align: center; } .videocontainer>span.videoMuted { @@ -226,7 +235,6 @@ #header{ display:none; position:absolute; - height: 0px; text-align:center; top:0; left:0; @@ -241,13 +249,27 @@ margin-right:auto; height:39px; width:auto; - overflow: hidden; background: linear-gradient(to bottom, rgba(103,103,103,.65) , rgba(0,0,0,.65)); -webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; } +#subject { + position: relative; + z-index: 3; + width: auto; + padding: 5px; + margin-left: 40%; + margin-right: 40%; + text-align: center; + background: linear-gradient(to bottom, rgba(255,255,255,.85) , rgba(255,255,255,.35)); + -webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + display: none; +} + .watermark { display: block; position: absolute; diff --git a/data_channels.js b/data_channels.js new file mode 100644 index 000000000..7a1ae59e3 --- /dev/null +++ b/data_channels.js @@ -0,0 +1,80 @@ +/* global connection, Strophe, updateLargeVideo, focusedVideoSrc*/ +/** + * Callback triggered by PeerConnection when new data channel is opened + * on the bridge. + * @param event the event info object. + */ +function onDataChannel(event) +{ + var dataChannel = event.channel; + + dataChannel.onopen = function () + { + console.info("Data channel opened by the bridge !!!", dataChannel); + + // Code sample for sending string and/or binary data + // Sends String message to the bridge + //dataChannel.send("Hello bridge!"); + // Sends 12 bytes binary message to the bridge + //dataChannel.send(new ArrayBuffer(12)); + }; + + dataChannel.onerror = function (error) + { + console.error("Data Channel Error:", error, dataChannel); + }; + + dataChannel.onmessage = function (event) + { + var msgData = event.data; + console.info("Got Data Channel Message:", msgData, dataChannel); + + // Active speaker event + if (msgData.indexOf('activeSpeaker') === 0) + { + // Endpoint ID from the bridge + var resourceJid = msgData.split(":")[1]; + + console.info( + "Data channel new active speaker event: " + resourceJid); + $(document).trigger('activespeakerchanged', [resourceJid]); + } + }; + + dataChannel.onclose = function () + { + console.info("The Data Channel closed", dataChannel); + }; +} + +/** + * Binds "ondatachannel" event listener to given PeerConnection instance. + * @param peerConnection WebRTC peer connection instance. + */ +function bindDataChannelListener(peerConnection) +{ + peerConnection.ondatachannel = onDataChannel; + + // Sample code for opening new data channel from Jitsi Meet to the bridge. + // Although it's not a requirement to open separate channels from both bridge + // and peer as single channel can be used for sending and receiving data. + // So either channel opened by the bridge or the one opened here is enough + // for communication with the bridge. + /*var dataChannelOptions = + { + reliable: true + }; + var dataChannel + = peerConnection.createDataChannel("myChannel", dataChannelOptions); + + // Can be used only when is in open state + dataChannel.onopen = function () + { + dataChannel.send("My channel !!!"); + }; + dataChannel.onmessage = function (event) + { + var msgData = event.data; + console.info("Got My Data Channel Message:", msgData, dataChannel); + };*/ +} \ No newline at end of file diff --git a/desktopsharing.js b/desktopsharing.js index dc8194f54..2e5e41895 100644 --- a/desktopsharing.js +++ b/desktopsharing.js @@ -1,4 +1,4 @@ -/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, change_local_video, getConferenceHandler */ +/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, changeLocalVideo, getConferenceHandler */ /** * Indicates that desktop stream is currently in use(for toggle purpose). * @type {boolean} @@ -251,7 +251,9 @@ function newStreamCreated(stream) { var oldStream = connection.jingle.localVideo; - change_local_video(stream, !isUsingScreenStream); + connection.jingle.localVideo = stream; + + VideoLayout.changeLocalVideo(stream, !isUsingScreenStream); var conferenceHandler = getConferenceHandler(); if (conferenceHandler) { diff --git a/etherpad.js b/etherpad.js index 6bb63e2e1..78111fd0c 100644 --- a/etherpad.js +++ b/etherpad.js @@ -45,8 +45,8 @@ var Etherpad = (function (my) { if (Prezi.isPresentationVisible()) { largeVideo.css({opacity: '0'}); } else { - setLargeVideoVisible(false); - dockToolbar(true); + VideoLayout.setLargeVideoVisible(false); + Toolbar.dockToolbar(true); } $('#etherpad>iframe').fadeIn(300, function () { @@ -63,8 +63,8 @@ var Etherpad = (function (my) { document.body.style.background = 'black'; if (!isPresentation) { $('#largeVideo').fadeIn(300, function () { - setLargeVideoVisible(true); - dockToolbar(false); + VideoLayout.setLargeVideoVisible(true); + Toolbar.dockToolbar(false); }); } }); diff --git a/index.html b/index.html index d323dd1de..a99546eb9 100644 --- a/index.html +++ b/index.html @@ -20,26 +20,35 @@ - - + + + + - - - - - - - - - + + + + + + + + + + + + + + + - - + + - + + @@ -49,35 +58,40 @@ -