var VideoLayout = (function (my) { var preMuted = false; var currentDominantSpeaker = null; var lastNCount = config.channelLastN; var lastNEndpointsCache = []; var largeVideoNewSrc = ''; 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); // Set default display name. setDisplayName('localVideoContainer'); AudioLevels.updateAudioLevelCanvas(); var localVideoSelector = $('#' + localVideo.id); // Add click handler to both video and video wrapper elements in case // there's no video. localVideoSelector.click(function () { VideoLayout.handleVideoThumbClicked(localVideo.src); }); $('#localVideoContainer').click(function () { VideoLayout.handleVideoThumbClicked(localVideo.src); }); // Add hover handler $('#localVideoContainer').hover( function() { VideoLayout.showDisplayName('localVideoContainer', true); }, function() { if (!VideoLayout.isLargeVideoVisible() || localVideo.src !== $('#largeVideo').attr('src')) VideoLayout.showDisplayName('localVideoContainer', false); } ); // Add stream ended handler stream.onended = function () { localVideoContainer.removeChild(localVideo); VideoLayout.updateRemovedVideo(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.updateRemovedVideo = 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 || !pick.src) { // 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) { largeVideoNewSrc = newSrc; var isVisible = $('#largeVideo').is(':visible'); // we need this here because after the fade the videoSrc may have // changed. var isDesktop = isVideoSrcDesktop(newSrc); var userJid = getJidFromVideoSrc(newSrc); // we want the notification to trigger even if userJid is undefined, // or null. $(document).trigger("selectedendpointchanged", [userJid]); $('#largeVideo').fadeOut(300, function () { var oldSrc = $(this).attr('src'); $(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 getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize; getVideoPosition = isDesktop ? getDesktopVideoPosition : getCameraVideoPosition; if (isVisible) { // Only if the large video is currently visible. // Disable previous dominant speaker video. var oldJid = getJidFromVideoSrc(oldSrc); if (oldJid) { var oldResourceJid = Strophe.getResourceFromJid(oldJid); VideoLayout.enableDominantSpeaker(oldResourceJid, false); } // Enable new dominant speaker in the remote videos section. var userJid = getJidFromVideoSrc(newSrc); if (userJid) { var resourceJid = Strophe.getResourceFromJid(userJid); VideoLayout.enableDominantSpeaker(resourceJid, true); } $(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"); } // Unlock current focused. if (focusedVideoSrc === videoSrc) { focusedVideoSrc = null; var dominantSpeakerVideo = null; // Enable the currently set dominant speaker. if (currentDominantSpeaker) { dominantSpeakerVideo = $('#participant_' + currentDominantSpeaker + '>video') .get(0); if (dominantSpeakerVideo) { VideoLayout.updateLargeVideo(dominantSpeakerVideo.src, 1); } } return; } // Lock new video focusedVideoSrc = videoSrc; // Update focused/pinned interface. var userJid = getJidFromVideoSrc(videoSrc); if (userJid) { var container = getParticipantContainer(userJid); container.addClass("videoContainerFocused"); } // Triggers a "video.selected" event. The "false" parameter indicates // this isn't a prezi. $(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) { var largeVideoJid = getJidFromVideoSrc($('#largeVideo').attr('src')); var resourceJid = Strophe.getResourceFromJid(largeVideoJid); if (isVisible) { $('#largeVideo').css({visibility: 'visible'}); $('.watermark').css({visibility: 'visible'}); VideoLayout.enableDominantSpeaker(resourceJid, true); } else { $('#largeVideo').css({visibility: 'hidden'}); $('.watermark').css({visibility: 'hidden'}); VideoLayout.enableDominantSpeaker(resourceJid, false); } }; /** * Indicates if the large video is currently visible. * * @return true if visible, false - otherwise */ my.isLargeVideoVisible = function() { return $('#largeVideo').is(':visible'); }; /** * Checks if container for participant identified by given peerJid exists * in the document and creates it eventually. * * @param peerJid peer Jid to check. * * @return Returns true if the peer container exists, * false - otherwise */ my.ensurePeerContainerExists = function(peerJid) { ContactList.ensureAddContact(peerJid); var resourceJid = Strophe.getResourceFromJid(peerJid); var videoSpanId = 'participant_' + resourceJid; if ($('#' + videoSpanId).length > 0) { // If there's been a focus change, make sure we add focus related // interface!! if (focus && $('#remote_popupmenu_' + resourceJid).length <= 0) addRemoteVideoMenu( peerJid, document.getElementById(videoSpanId)); } else { var container = VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId); // Set default display name. setDisplayName(videoSpanId); var nickfield = document.createElement('span'); nickfield.className = "nick"; nickfield.appendChild(document.createTextNode(resourceJid)); container.appendChild(nickfield); // In case this is not currently in the last n we don't show it. if (lastNCount && lastNCount > 0 && $('#remoteVideos>span').length >= lastNCount + 2) { showPeerContainer(resourceJid, false); } else VideoLayout.resizeThumbnails(); } }; my.addRemoteVideoContainer = function(peerJid, spanId) { 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); AudioLevels.updateAudioLevelCanvas(peerJid); return container; }; /** * Creates an audio or video stream element. */ my.createStreamElement = function (sid, stream) { var isVideo = stream.getVideoTracks().length > 0; var element = isVideo ? document.createElement('video') : document.createElement('audio'); var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + sid + '_' + stream.id; element.id = id; element.autoplay = true; element.oncontextmenu = function () { return false; }; return element; }; my.addRemoteStreamElement = function (container, sid, stream, peerJid, thessrc) { var newElementId = null; var isVideo = stream.getVideoTracks().length > 0; if (container) { var streamElement = VideoLayout.createStreamElement(sid, stream); newElementId = streamElement.id; container.appendChild(streamElement); var sel = $('#' + newElementId); sel.hide(); // If the container is currently visible we attach the stream. if (!isVideo || (container.offsetParent !== null && isVideo)) { var simulcast = new Simulcast(); var videoStream = simulcast.getReceivingVideoStream(stream); RTC.attachMediaStream(sel, videoStream); if (isVideo) waitForRemoteVideo(sel, thessrc, stream); } stream.onended = function () { console.log('stream ended', this); VideoLayout.removeRemoteStreamElement(stream, container); if (peerJid) ContactList.removeContact(peerJid); }; // Add click handler. container.onclick = function (event) { /* * FIXME It turns out that videoThumb may not exist (if there is * no actual video). */ var videoThumb = $('#' + container.id + '>video').get(0); if (videoThumb) VideoLayout.handleVideoThumbClicked(videoThumb.src); event.preventDefault(); return false; }; // Add hover handler $(container).hover( function() { VideoLayout.showDisplayName(container.id, true); }, function() { var videoSrc = null; if ($('#' + container.id + '>video') && $('#' + container.id + '>video').length > 0) { videoSrc = $('#' + container.id + '>video').get(0).src; } // If the video has been "pinned" by the user we want to // keep the display name on place. if (!VideoLayout.isLargeVideoVisible() || videoSrc !== $('#largeVideo').attr('src')) VideoLayout.showDisplayName(container.id, false); } ); } return newElementId; }; /** * Removes the remote stream element corresponding to the given stream and * parent container. * * @param stream the stream * @param container */ my.removeRemoteStreamElement = function (stream, container) { if (!container) return; var select = null; var removedVideoSrc = null; if (stream.getVideoTracks().length > 0) { select = $('#' + container.id + '>video'); removedVideoSrc = select.get(0).src; } else select = $('#' + container.id + '>audio'); // Remove video source from the mapping. delete videoSrcToSsrc[removedVideoSrc]; // Mark video as removed to cancel waiting loop(if video is removed // before has started) select.removed = true; select.remove(); var audioCount = $('#' + container.id + '>audio').length; var videoCount = $('#' + container.id + '>video').length; if (!audioCount && !videoCount) { console.log("Remove whole user", container.id); // Remove whole container container.remove(); Util.playSoundNotification('userLeft'); VideoLayout.resizeThumbnails(); } if (removedVideoSrc) VideoLayout.updateRemovedVideo(removedVideoSrc); }; /** * Show/hide peer container for the given resourceJid. */ function showPeerContainer(resourceJid, isShow) { var peerContainer = $('#participant_' + resourceJid); if (!peerContainer) return; if (!peerContainer.is(':visible') && isShow) peerContainer.show(); else if (peerContainer.is(':visible') && !isShow) peerContainer.hide(); }; /** * Sets the display name for the given video span id. */ function setDisplayName(videoSpanId, displayName) { var nameSpan = $('#' + videoSpanId + '>span.displayname'); var defaultLocalDisplayName = "Me"; // 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) { if (displayName && displayName.length > 0) $('#localDisplayName').text(displayName + ' (me)'); else $('#localDisplayName').text(defaultLocalDisplayName); } else { if (displayName && displayName.length > 0) $('#' + videoSpanId + '_name').text(displayName); else $('#' + videoSpanId + '_name').text(interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME); } } else { var editButton = null; nameSpan = document.createElement('span'); nameSpan.className = 'displayname'; $('#' + videoSpanId)[0].appendChild(nameSpan); if (videoSpanId === 'localVideoContainer') { editButton = createEditDisplayNameButton(); nameSpan.innerText = defaultLocalDisplayName; } else { nameSpan.innerText = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME; } if (displayName && displayName.length > 0) { nameSpan.innerText = displayName; } if (!editButton) { nameSpan.id = videoSpanId + '_name'; } else { nameSpan.id = 'localDisplayName'; $('#' + videoSpanId)[0].appendChild(editButton); var editableText = document.createElement('input'); editableText.className = 'displayname'; editableText.type = 'text'; editableText.id = 'editDisplayName'; if (displayName && 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)"); else $('#localDisplayName') .text(defaultLocalDisplayName); $('#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); } }); }); } } }; /** * 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 the presence status message for the given video. */ my.setPresenceStatus = function (videoSpanId, statusMsg) { if (!$('#' + videoSpanId).length) { // No container return; } var statusSpan = $('#' + videoSpanId + '>span.status'); if (!statusSpan.length) { //Add status span statusSpan = document.createElement('span'); statusSpan.className = 'status'; statusSpan.id = videoSpanId + '_status'; $('#' + videoSpanId)[0].appendChild(statusSpan); statusSpan = $('#' + videoSpanId + '>span.status'); } // Display status if (statusMsg && statusMsg.length) { $('#' + videoSpanId + '_status').text(statusMsg); statusSpan.get(0).setAttribute("style", "display:inline-block;"); } else { // Hide statusSpan.get(0).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'; 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.popover('hide'); 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 videoSpaceWidth = $('#remoteVideos').width(); var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth); 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).trigger("remotevideo.resized", [width, height]); }; /** * Enables the dominant speaker UI. * * @param resourceJid the jid indicating the video element to * activate/deactivate * @param isEnable indicates if the dominant speaker should be enabled or * disabled */ my.enableDominantSpeaker = function(resourceJid, isEnable) { var displayName = resourceJid; var nameSpan = $('#participant_' + resourceJid + '>span.displayname'); if (nameSpan.length > 0) displayName = nameSpan.text(); console.log("UI enable dominant speaker", displayName, resourceJid, isEnable); var videoSpanId = null; var videoContainerId = null; if (resourceJid === Strophe.getResourceFromJid(connection.emuc.myroomjid)) { videoSpanId = 'localVideoWrapper'; videoContainerId = 'localVideoContainer'; } else { videoSpanId = 'participant_' + resourceJid; videoContainerId = videoSpanId; } videoSpan = document.getElementById(videoContainerId); if (!videoSpan) { console.error("No video element for jid", resourceJid); return; } var video = $('#' + videoSpanId + '>video'); if (video && video.length > 0) { if (isEnable) { VideoLayout.showDisplayName(videoContainerId, true); if (!videoSpan.classList.contains("dominantspeaker")) videoSpan.classList.add("dominantspeaker"); video.css({visibility: 'hidden'}); } else { VideoLayout.showDisplayName(videoContainerId, false); if (videoSpan.classList.contains("dominantspeaker")) videoSpan.classList.remove("dominantspeaker"); video.css({visibility: 'visible'}); } } }; /** * 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. * * @param videoSpaceWidth the width of the video space */ my.calculateThumbnailSize = function (videoSpaceWidth) { // 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 = 0; if (lastNCount && lastNCount > 0) numvids = lastNCount + 1; else numvids = $('#remoteVideos>span:visible').length; // Remove the 3px borders arround videos and border around the remote // videos area var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70; 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); Util.setTooltip(parentElement, "The owner of
this conference", "top"); } /** * 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'; } } }; /** * Returns the current dominant speaker resource jid. */ my.getDominantSpeakerResourceJid = function () { return currentDominantSpeaker; }; /** * Returns the corresponding resource jid to the given peer container * DOM element. * * @return the corresponding resource jid to the given peer container * DOM element */ my.getPeerContainerResourceJid = function (containerElement) { var i = containerElement.id.indexOf('participant_'); if (i >= 0) return containerElement.id.substring(i + 12); }; /** * Adds the remote video menu element for the given jid in the * given parentElement. * * @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); var paddingSpan = document.createElement('span'); paddingSpan.className = 'popupmenuPadding'; popupmenuElement.appendChild(paddingSpan); } /** * On audio muted event. */ $(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); }); /** * On video muted event. */ $(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); }); /** * Display name changed. */ $(document).bind('displaynamechanged', function (event, jid, displayName, status) { if (jid === 'localVideoContainer' || jid === connection.emuc.myroomjid) { setDisplayName('localVideoContainer', displayName); } else { VideoLayout.ensurePeerContainerExists(jid); setDisplayName( 'participant_' + Strophe.getResourceFromJid(jid), displayName, status); } }); /** * On dominant speaker changed event. */ $(document).bind('dominantspeakerchanged', function (event, resourceJid) { // We ignore local user events. if (resourceJid === Strophe.getResourceFromJid(connection.emuc.myroomjid)) return; // Update the current dominant speaker. if (resourceJid !== currentDominantSpeaker) { var oldSpeakerVideoSpanId = "participant_" + currentDominantSpeaker, newSpeakerVideoSpanId = "participant_" + resourceJid; if($("#" + oldSpeakerVideoSpanId + ">span.displayname").text() === interfaceConfig.DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME) { setDisplayName(oldSpeakerVideoSpanId, null); } if($("#" + newSpeakerVideoSpanId + ">span.displayname").text() === interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME) { setDisplayName(newSpeakerVideoSpanId, interfaceConfig.DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME); } currentDominantSpeaker = resourceJid; } else { return; } // Obtain container for new dominant speaker. var container = document.getElementById( 'participant_' + resourceJid); // Local video will not have container found, but that's ok // since we don't want to switch to local video. if (container && !focusedVideoSrc) { var video = container.getElementsByTagName("video"); // Update the large video if the video source is already available, // otherwise wait for the "videoactive.jingle" event. if (video.length && video[0].currentTime > 0) VideoLayout.updateLargeVideo(video[0].src); } }); /** * On last N change event. * * @param event the event that notified us * @param lastNEndpoints the list of last N endpoints * @param endpointsEnteringLastN the list currently entering last N * endpoints */ $(document).bind('lastnchanged', function ( event, lastNEndpoints, endpointsEnteringLastN, stream) { if (lastNCount !== lastNEndpoints.length) lastNCount = lastNEndpoints.length; lastNEndpointsCache = lastNEndpoints; $('#remoteVideos>span').each(function( index, element ) { var resourceJid = VideoLayout.getPeerContainerResourceJid(element); if (resourceJid && lastNEndpoints.indexOf(resourceJid) < 0) { console.log("Remove from last N", resourceJid); showPeerContainer(resourceJid, false); } }); if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0) endpointsEnteringLastN = lastNEndpoints; if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) { endpointsEnteringLastN.forEach(function (resourceJid) { if (!$('#participant_' + resourceJid).is(':visible')) { console.log("Add to last N", resourceJid); showPeerContainer(resourceJid, true); mediaStreams.some(function (mediaStream) { if (mediaStream.peerjid && Strophe.getResourceFromJid(mediaStream.peerjid) === resourceJid && mediaStream.type === mediaStream.VIDEO_TYPE) { var sel = $('#participant_' + resourceJid + '>video'); var simulcast = new Simulcast(); var videoStream = simulcast.getReceivingVideoStream(mediaStream.stream); RTC.attachMediaStream(sel, videoStream); waitForRemoteVideo( sel, mediaStream.ssrc, mediaStream.stream); return true; } }); } }); } }); $(document).bind('videoactive.jingle', function (event, videoelem) { if (videoelem.attr('id').indexOf('mixedmslabel') === -1) { // ignore mixedmslabela0 and v0 videoelem.show(); VideoLayout.resizeThumbnails(); var videoParent = videoelem.parent(); var parentResourceJid = null; if (videoParent) parentResourceJid = VideoLayout.getPeerContainerResourceJid(videoParent[0]); // Update the large video to the last added video only if there's no // current dominant or focused speaker or update it to the current // dominant speaker. if ((!focusedVideoSrc && !VideoLayout.getDominantSpeakerResourceJid()) || (parentResourceJid && VideoLayout.getDominantSpeakerResourceJid() === parentResourceJid)) { VideoLayout.updateLargeVideo(videoelem.attr('src'), 1); } VideoLayout.showFocusIndicator(); } }); $(document).bind('simulcastlayerstarted', function(event) { var localVideoSelector = $('#' + 'localVideo_' + connection.jingle.localVideo.id); var simulcast = new Simulcast(); var stream = simulcast.getLocalVideoStream(); // Attach WebRTC stream RTC.attachMediaStream(localVideoSelector, stream); localVideoSrc = $(localVideoSelector).attr('src'); }); $(document).bind('simulcastlayerstopped', function(event) { var localVideoSelector = $('#' + 'localVideo_' + connection.jingle.localVideo.id); var simulcast = new Simulcast(); var stream = simulcast.getLocalVideoStream(); // Attach WebRTC stream RTC.attachMediaStream(localVideoSelector, stream); localVideoSrc = $(localVideoSelector).attr('src'); }); /** * On simulcast layers changed event. */ $(document).bind('simulcastlayerschanged', function (event, endpointSimulcastLayers) { var simulcast = new Simulcast(); endpointSimulcastLayers.forEach(function (esl) { var primarySSRC = esl.simulcastLayer.primarySSRC; var msid = simulcast.getRemoteVideoStreamIdBySSRC(primarySSRC); // Get session and stream from msid. var session, electedStream; var i, j, k; if (connection.jingle) { var keys = Object.keys(connection.jingle.sessions); for (i = 0; i < keys.length; i++) { var sid = keys[i]; if (electedStream) { // stream found, stop. break; } session = connection.jingle.sessions[sid]; if (session.remoteStreams) { for (j = 0; j < session.remoteStreams.length; j++) { var remoteStream = session.remoteStreams[j]; if (electedStream) { // stream found, stop. break; } var tracks = remoteStream.getVideoTracks(); if (tracks) { for (k = 0; k < tracks.length; k++) { var track = tracks[k]; if (msid === [remoteStream.id, track.id].join(' ')) { electedStream = new webkitMediaStream([track]); // stream found, stop. break; } } } } } } } if (session && electedStream) { console.info('Switching simulcast substream.'); console.info([esl, primarySSRC, msid, session, electedStream]); var msidParts = msid.split(' '); var selRemoteVideo = $(['#', 'remoteVideo_', session.sid, '_', msidParts[0]].join('')); var updateLargeVideo = (ssrc2jid[videoSrcToSsrc[selRemoteVideo.attr('src')]] == ssrc2jid[videoSrcToSsrc[largeVideoNewSrc]]); var updateFocusedVideoSrc = (selRemoteVideo.attr('src') == focusedVideoSrc); var electedStreamUrl = webkitURL.createObjectURL(electedStream); selRemoteVideo.attr('src', electedStreamUrl); videoSrcToSsrc[selRemoteVideo.attr('src')] = primarySSRC; if (updateLargeVideo) { VideoLayout.updateLargeVideo(electedStreamUrl); } if (updateFocusedVideoSrc) { focusedVideoSrc = electedStreamUrl; } } else { console.error('Could not find a stream or a session.'); } }); }); return my; }(VideoLayout || {}));