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'; Util.setTooltip(indicatorSpan, "The owner of
this conference", "top"); 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'; Util.setTooltip(mutedIndicator, "Participant has
stopped the camera.", "top"); 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'; Util.setTooltip(audioMutedSpan, "Participant is muted", "top"); if (videoMutedSpan) { audioMutedSpan.right = '30px'; } $('#' + videoSpanId)[0].appendChild(audioMutedSpan); var mutedIndicator = document.createElement('i'); mutedIndicator.className = 'icon-mic-disabled'; 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) { var oldSpeaker = currentActiveSpeaker; setTimeout(function () { VideoLayout.enableActiveSpeaker(oldSpeaker, false); }, 200); } currentActiveSpeaker = resourceJid; } else if (resourceJid === currentActiveSpeaker) currentActiveSpeaker = null; var video = $('#' + videoSpanId + '>video'); if (video && video.length > 0) { var videoElement = video.get(0); if (isEnable) { if (!videoElement.classList.contains("activespeaker")) videoElement.classList.add("activespeaker"); VideoLayout.showDisplayName(videoSpanId, true); } else { VideoLayout.showDisplayName(videoSpanId, false); if (videoElement.classList.contains("activespeaker")) videoElement.classList.remove("activespeaker"); } } }; /** * 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'; Util.setTooltip(editButton, 'Click to edit your
display name', "top"); 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'; 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 || {}));