From a5e15e80fc985449cbf29db6262d835e84fb692c Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 12 Jun 2014 21:33:57 +0300 Subject: [PATCH] Part 2 of previous commit. Makes the toolbar more visible. Moves toolbar and videolayout related code in separate classes. --- toolbar.js | 232 +++++++++++++ videolayout.js | 859 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1091 insertions(+) create mode 100644 toolbar.js create mode 100644 videolayout.js diff --git a/toolbar.js b/toolbar.js new file mode 100644 index 000000000..b932286f5 --- /dev/null +++ b/toolbar.js @@ -0,0 +1,232 @@ +var Toolbar = (function (my) { + var INITIAL_TOOLBAR_TIMEOUT = 20000; + var TOOLBAR_TIMEOUT = INITIAL_TOOLBAR_TIMEOUT; + + /** + * Opens the lock room dialog. + */ + my.openLockDialog = function() { + // 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. + */ + my.openLinkDialog = function() { + $.prompt('', + { + title: "Share this link with everyone you want to invite", + persistent: false, + buttons: { "Cancel": false}, + loaded: function (event) { + document.getElementById('inviteLinkRef').select(); + } + } + ); + }; + + /** + * Opens the settings dialog. + */ + my.openSettingsDialog = function() { + $.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); + } + */ + } + } + } + ); + }; + + /** + * Toggles the application in and out of full screen mode + * (a.k.a. presentation mode in Chrome). + */ + my.toggleFullScreen = function() { + 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 main toolbar. + */ + my.showToolbar = function() { + if (!$('#header').is(':visible')) { + $('#header').show("slide", { direction: "up", duration: 300}); + + if (toolbarTimeout) { + clearTimeout(toolbarTimeout); + toolbarTimeout = null; + } + toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT); + TOOLBAR_TIMEOUT = 4000; + } + + 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 + */ + my.dockToolbar = function(isDock) { + if (isDock) { + // First make sure the toolbar is shown. + if (!$('#header').is(':visible')) { + Toolbar.showToolbar(); + } + // Then clear the time out, to dock the toolbar. + clearTimeout(toolbarTimeout); + toolbarTimeout = null; + } + else { + if (!$('#header').is(':visible')) { + Toolbar.showToolbar(); + } + else { + toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT); + } + } + }; + + /** + * Updates the lock button state. + */ + my.updateLockButton = function() { + 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, TOOLBAR_TIMEOUT); + } + }; + + return my; +}(Toolbar || {})); \ No newline at end of file diff --git a/videolayout.js b/videolayout.js new file mode 100644 index 000000000..5024acfe9 --- /dev/null +++ b/videolayout.js @@ -0,0 +1,859 @@ +var VideoLayout = (function (my) { + var preMuted = false; + var currentActiveSpeaker = null; + + my.changeLocalAudio = function(stream) { + connection.jingle.localAudio = stream; + + RTC.attachMediaStream($('#localAudio'), stream); + document.getElementById('localAudio').autoplay = true; + document.getElementById('localAudio').volume = 0; + if (preMuted) { + toggleAudio(); + preMuted = false; + } + }; + + my.changeLocalVideo = function(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 () { + VideoLayout.handleVideoThumbClicked(localVideo.src); + }); + // Add hover handler + $('#localVideoContainer').hover( + function() { + VideoLayout.showDisplayName('localVideoContainer', true); + }, + function() { + if (focusedVideoSrc !== localVideo.src) + VideoLayout.showDisplayName('localVideoContainer', false); + } + ); + // Add stream ended handler + stream.onended = function () { + localVideoContainer.removeChild(localVideo); + VideoLayout.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; + VideoLayout.updateLargeVideo(localVideoSrc, 0); + }; + + /** + * Checks if removed video is currently displayed and tries to display another one instead. + * @param removedVideoSrc src stream identifier of the video. + */ + my.checkChangeLargeVideo = function(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); + + 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) { + VideoLayout.updateLargeVideo(pick.src, pick.volume); + } else { + console.warn("Failed to elect large video"); + } + } + }; + + + /** + * Updates the large video with the given new video source. + */ + my.updateLargeVideo = function(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); + }); + } + }; + + my.handleVideoThumbClicked = function(videoSrc) { + // Restore style for previously focused video + var focusJid = getJidFromVideoSrc(focusedVideoSrc); + var oldContainer = + getParticipantContainer(focusJid); + + if (oldContainer) { + oldContainer.removeClass("videoContainerFocused"); + VideoLayout.enableActiveSpeaker( + Strophe.getResourceFromJid(focusJid), false); + } + + // Unlock + if (focusedVideoSrc === videoSrc) + { + focusedVideoSrc = null; + return; + } + + // Lock new video + focusedVideoSrc = videoSrc; + + var userJid = getJidFromVideoSrc(videoSrc); + if (userJid) + { + var container = getParticipantContainer(userJid); + container.addClass("videoContainerFocused"); + + var resourceJid = Strophe.getResourceFromJid(userJid); + VideoLayout.enableActiveSpeaker(resourceJid, true); + } + + $(document).trigger("video.selected", [false]); + + VideoLayout.updateLargeVideo(videoSrc, 1); + + $('audio').each(function (idx, el) { + if (el.id.indexOf('mixedmslabel') !== -1) { + el.volume = 0; + el.volume = 1; + } + }); + }; + + /** + * Positions the large video. + * + * @param videoWidth the stream video width + * @param videoHeight the stream video height + */ + my.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); + }; + + /** + * Shows/hides the large video. + */ + my.setLargeVideoVisible = function(isVisible) { + if (isVisible) { + $('#largeVideo').css({visibility: 'visible'}); + $('.watermark').css({visibility: 'visible'}); + } + else { + $('#largeVideo').css({visibility: 'hidden'}); + $('.watermark').css({visibility: 'hidden'}); + } + }; + + + /** + * Checks if container for participant identified by given peerJid exists + * in the document and creates it eventually. + * + * @param peerJid peer Jid to check. + */ + my.ensurePeerContainerExists = function(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 + = VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId); + + var nickfield = document.createElement('span'); + nickfield.className = "nick"; + nickfield.appendChild(document.createTextNode(peerResource)); + container.appendChild(nickfield); + VideoLayout.resizeThumbnails(); + }; + + my.addRemoteVideoContainer = function(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; + }; + + /** + * Shows the display name for the given video. + */ + my.setDisplayName = function(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")) { + if (nickname) { + $('#localDisplayName').text(nickname + " (me)"); + $('#localDisplayName').show(); + } + else { + $('#localDisplayName').text(nickname); + } + + $('#editDisplayName').hide(); + } + }; + + $('#editDisplayName').one("focusout", function (e) { + inputDisplayNameHandler(this.value); + }); + + $('#editDisplayName').on('keydown', function (e) { + if (e.keyCode === 13) { + e.preventDefault(); + inputDisplayNameHandler(this.value); + } + }); + }); + } + } + }; + + /** + * Shows/hides the display name on the remote video. + * @param videoSpanId the identifier of the video span element + * @param isShow indicates if the display name should be shown or hidden + */ + my.showDisplayName = function(videoSpanId, isShow) { + var nameSpan = $('#' + videoSpanId + '>span.displayname').get(0); + + if (isShow) { + if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length) + nameSpan.setAttribute("style", "display:inline-block;"); + } + else { + if (nameSpan) + nameSpan.setAttribute("style", "display:none;"); + } + }; + + /** + * 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. + */ + my.showFocusIndicator = function() { + 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); + } + } + }; + + /** + * Shows video muted indicator over small videos. + */ + my.showVideoIndicator = function(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); + } + }; + + /** + * Shows audio muted indicator over small videos. + */ + my.showAudioIndicator = function(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); + } + }; + + /** + * Resizes the large video container. + */ + my.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); + + VideoLayout.resizeThumbnails(); + }; + + /** + * Resizes thumbnails. + */ + my.resizeThumbnails = function() { + 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); + }; + + /** + * Enables the active speaker UI. + * + * @param resourceJid the jid indicating the video element to + * activate/deactivate + * @param isEnable indicates if the active speaker should be enabled or + * disabled + */ + my.enableActiveSpeaker = function(resourceJid, isEnable) { + var videoSpanId = null; + if (resourceJid + === Strophe.getResourceFromJid(connection.emuc.myroomjid)) + videoSpanId = 'localVideoWrapper'; + else + videoSpanId = 'participant_' + resourceJid; + + videoSpan = document.getElementById(videoSpanId); + + if (!videoSpan) { + console.error("No video element for jid", resourceJid); + return; + } + + // If there's an active speaker (automatically) selected we have to + // disable this state and update the current active speaker. + if (isEnable) { + if (currentActiveSpeaker) + VideoLayout.enableActiveSpeaker(currentActiveSpeaker, false); + else + currentActiveSpeaker = resourceJid; + } + else if (resourceJid === currentActiveSpeaker) + currentActiveSpeaker = null; + + var activeSpeakerCanvas = $('#' + videoSpanId + '>canvas'); + var videoElement = $('#' + videoSpanId + '>video'); + var canvasSize = calculateThumbnailSize(); + + if (isEnable && (!activeSpeakerCanvas + || activeSpeakerCanvas.length === 0)) { + + activeSpeakerCanvas = document.createElement('canvas'); + activeSpeakerCanvas.width = canvasSize[0]; + activeSpeakerCanvas.height = canvasSize[1]; + + // We flip the canvas image if this is the local video. + if (videoSpanId === 'localVideoWrapper') + activeSpeakerCanvas.className += " flipVideoX"; + + videoSpan.appendChild(activeSpeakerCanvas); + activeSpeakerCanvas.addEventListener( + 'click', + function() { + VideoLayout.handleVideoThumbClicked( + videoElement.get(0).src); + }, false); + } + else { + activeSpeakerCanvas = activeSpeakerCanvas.get(0); + } + + if (videoElement && videoElement.length > 0) { + var video = document.getElementById(videoElement.get(0).id); + if (isEnable) { + var context = activeSpeakerCanvas.getContext('2d'); + + context.fillRect(0, 0, canvasSize[0], canvasSize[1]); + context.drawImage(video, 0, 0, canvasSize[0], canvasSize[1]); + Util.imageToGrayScale(activeSpeakerCanvas); + + VideoLayout + .showDisplayName(videoSpanId, true); + activeSpeakerCanvas + .setAttribute('style', 'display:block !important;'); + video.setAttribute('style', 'display:none !important;'); + } + else { + VideoLayout + .showDisplayName(videoSpanId, false); + video.setAttribute('style', 'display:block !important;'); + activeSpeakerCanvas + .setAttribute('style', 'display:none !important;'); + } + } + }; + + /** + * Gets the selector of video thumbnail container for the user identified by + * given userJid + * @param userJid user's Jid for whom we want to get the video container. + */ + function getParticipantContainer(userJid) + { + if (!userJid) + return null; + + if (userJid === connection.emuc.myroomjid) + return $("#localVideoContainer"); + else + return $("#participant_" + Strophe.getResourceFromJid(userJid)); + } + + /** + * 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'}); + } + + /** + * Calculates the thumbnail size. + */ + 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]; + }; + + /** + * 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]; + } + + /** + * Creates the edit display name button. + * + * @returns the edit button + */ + function createEditDisplayNameButton() { + var editButton = document.createElement('a'); + editButton.className = 'displayname'; + editButton.innerHTML = ''; + + return editButton; + } + + /** + * Creates the element indicating the focus of the conference. + * + * @param parentElement the parent element where the focus indicator will + * be added + */ + function createFocusIndicatorElement(parentElement) { + var focusIndicator = document.createElement('i'); + focusIndicator.className = 'fa fa-star'; + focusIndicator.title = "The owner of this conference"; + parentElement.appendChild(focusIndicator); + } + + /** + * Updates the remote video menu. + * + * @param jid the jid indicating the video for which we're adding a menu. + * @param isMuted indicates the current mute state + */ + my.updateRemoteVideoMenu = function(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'; + } + } + }; + + /** + * Adds the remote video menu element for the given jid in the + * given parentElement. + * + * @param jid the jid indicating the video for which we're adding a menu. + * @param parentElement the parent element where this menu will be added + */ + 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); + } + + $(document).bind('audiomuted.muc', function (event, jid, isMuted) { + var videoSpanId = null; + if (jid === connection.emuc.myroomjid) { + videoSpanId = 'localVideoContainer'; + } else { + VideoLayout.ensurePeerContainerExists(jid); + videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid); + } + + if (focus) { + mutedAudios[jid] = isMuted; + VideoLayout.updateRemoteVideoMenu(jid, isMuted); + } + + if (videoSpanId) + VideoLayout.showAudioIndicator(videoSpanId, isMuted); + }); + + $(document).bind('videomuted.muc', function (event, jid, isMuted) { + var videoSpanId = null; + if (jid === connection.emuc.myroomjid) { + videoSpanId = 'localVideoContainer'; + } else { + VideoLayout.ensurePeerContainerExists(jid); + videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid); + } + + if (videoSpanId) + VideoLayout.showVideoIndicator(videoSpanId, isMuted); + }); + + return my; +}(VideoLayout || {})); \ No newline at end of file