diff --git a/conference.js b/conference.js index 0b8307c37..b0edba985 100644 --- a/conference.js +++ b/conference.js @@ -39,7 +39,7 @@ let connectionIsInterrupted = false; */ let DSExternalInstallationInProgress = false; -import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo"; +import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer"; /** * Known custom conference commands. @@ -1424,6 +1424,8 @@ export default { APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => { var smallVideoId = smallVideo.getId(); + // FIXME why VIDEO_CONTAINER_TYPE instead of checking if + // the participant is on the large video ? if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE && !APP.conference.isLocalId(smallVideoId)) { diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js new file mode 100644 index 000000000..8c145e77e --- /dev/null +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -0,0 +1,310 @@ +/* global $, APP, interfaceConfig */ +/* jshint -W101 */ + +import Avatar from "../avatar/Avatar"; +import {createDeferred} from '../../util/helpers'; +import UIUtil from "../util/UIUtil"; +import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer"; + +/** + * Manager for all Large containers. + */ +export default class LargeVideoManager { + constructor (emitter) { + this.containers = {}; + + this.state = VIDEO_CONTAINER_TYPE; + this.videoContainer = new VideoContainer( + () => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter); + this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer); + + // use the same video container to handle and desktop tracks + this.addContainer("desktop", this.videoContainer); + + this.width = 0; + this.height = 0; + + this.$container = $('#largeVideoContainer'); + + this.$container.css({ + display: 'inline-block' + }); + + if (interfaceConfig.SHOW_JITSI_WATERMARK) { + let leftWatermarkDiv + = this.$container.find("div.watermark.leftwatermark"); + + leftWatermarkDiv.css({display: 'block'}); + + UIUtil.setLinkHref( + leftWatermarkDiv.parent(), + interfaceConfig.JITSI_WATERMARK_LINK); + } + + if (interfaceConfig.SHOW_BRAND_WATERMARK) { + let rightWatermarkDiv + = this.$container.find("div.watermark.rightwatermark"); + + rightWatermarkDiv.css({ + display: 'block', + backgroundImage: 'url(images/rightwatermark.png)' + }); + + UIUtil.setLinkHref( + rightWatermarkDiv.parent(), + interfaceConfig.BRAND_WATERMARK_LINK); + } + + if (interfaceConfig.SHOW_POWERED_BY) { + this.$container.children("a.poweredby").css({display: 'block'}); + } + + this.$container.hover( + e => this.onHoverIn(e), + e => this.onHoverOut(e) + ); + } + + onHoverIn (e) { + if (!this.state) { + return; + } + let container = this.getContainer(this.state); + container.onHoverIn(e); + } + + onHoverOut (e) { + if (!this.state) { + return; + } + let container = this.getContainer(this.state); + container.onHoverOut(e); + } + + get id () { + let container = this.getContainer(this.state); + return container.id; + } + + scheduleLargeVideoUpdate () { + if (this.updateInProcess || !this.newStreamData) { + return; + } + + this.updateInProcess = true; + + let container = this.getContainer(this.state); + + // Include hide()/fadeOut only if we're switching between users + let preUpdate; + if (this.newStreamData.id != this.id) { + preUpdate = container.hide(); + } else { + preUpdate = Promise.resolve(); + } + + preUpdate.then(() => { + let {id, stream, videoType, resolve} = this.newStreamData; + this.newStreamData = null; + + console.info("hover in %s", id); + this.state = videoType; + let container = this.getContainer(this.state); + container.setStream(stream, videoType); + + // change the avatar url on large + this.updateAvatar(Avatar.getAvatarUrl(id)); + + // If we the continer is VIDEO_CONTAINER_TYPE, we need to check + // its stream whether exist and is muted to set isVideoMuted + // in rest of the cases it is false + let isVideoMuted = false; + if (videoType == VIDEO_CONTAINER_TYPE) + isVideoMuted = stream ? stream.isMuted() : true; + + // show the avatar on large if needed + container.showAvatar(isVideoMuted); + + let promise; + + // do not show stream if video is muted + // but we still should show watermark + if (isVideoMuted) { + this.showWatermark(true); + promise = Promise.resolve(); + } else { + promise = container.show(); + } + + // resolve updateLargeVideo promise after everything is done + promise.then(resolve); + + return promise; + }).then(() => { + // after everything is done check again if there are any pending + // new streams. + this.updateInProcess = false; + this.scheduleLargeVideoUpdate(); + }); + } + + /** + * Update large video. + * Switches to large video even if previously other container was visible. + * @param userID the userID of the participant associated with the stream + * @param {JitsiTrack?} stream new stream + * @param {string?} videoType new video type + * @returns {Promise} + */ + updateLargeVideo (userID, stream, videoType) { + if (this.newStreamData) { + this.newStreamData.reject(); + } + + this.newStreamData = createDeferred(); + this.newStreamData.id = userID; + this.newStreamData.stream = stream; + this.newStreamData.videoType = videoType; + + this.scheduleLargeVideoUpdate(); + + return this.newStreamData.promise; + } + + /** + * Update container size. + */ + updateContainerSize () { + this.width = UIUtil.getAvailableVideoWidth(); + this.height = window.innerHeight; + } + + /** + * Resize Large container of specified type. + * @param {string} type type of container which should be resized. + * @param {boolean} [animate=false] if resize process should be animated. + */ + resizeContainer (type, animate = false) { + let container = this.getContainer(type); + container.resize(this.width, this.height, animate); + } + + /** + * Resize all Large containers. + * @param {boolean} animate if resize process should be animated. + */ + resize (animate) { + // resize all containers + Object.keys(this.containers) + .forEach(type => this.resizeContainer(type, animate)); + + this.$container.animate({ + width: this.width, + height: this.height + }, { + queue: false, + duration: animate ? 500 : 0 + }); + } + + /** + * Enables/disables the filter indicating a video problem to the user. + * + * @param enable true to enable, false to disable + */ + enableVideoProblemFilter (enable) { + let container = this.getContainer(this.state); + container.$video.toggleClass("videoProblemFilter", enable); + } + + /** + * Updates the src of the dominant speaker avatar + */ + updateAvatar (avatarUrl) { + $("#dominantSpeakerAvatar").attr('src', avatarUrl); + } + + /** + * Show or hide watermark. + * @param {boolean} show + */ + showWatermark (show) { + $('.watermark').css('visibility', show ? 'visible' : 'hidden'); + } + + /** + * Add container of specified type. + * @param {string} type container type + * @param {LargeContainer} container container to add. + */ + addContainer (type, container) { + if (this.containers[type]) { + throw new Error(`container of type ${type} already exist`); + } + + this.containers[type] = container; + this.resizeContainer(type); + } + + /** + * Get Large container of specified type. + * @param {string} type container type. + * @returns {LargeContainer} + */ + getContainer (type) { + let container = this.containers[type]; + + if (!container) { + throw new Error(`container of type ${type} doesn't exist`); + } + + return container; + } + + /** + * Remove Large container of specified type. + * @param {string} type container type. + */ + removeContainer (type) { + if (!this.containers[type]) { + throw new Error(`container of type ${type} doesn't exist`); + } + + delete this.containers[type]; + } + + /** + * Show Large container of specified type. + * Does nothing if such container is already visible. + * @param {string} type container type. + * @returns {Promise} + */ + showContainer (type) { + if (this.state === type) { + return Promise.resolve(); + } + + let oldContainer = this.containers[this.state]; + if (this.state === VIDEO_CONTAINER_TYPE) { + this.showWatermark(false); + } + oldContainer.hide(); + + this.state = type; + let container = this.getContainer(type); + + return container.show().then(() => { + if (type === VIDEO_CONTAINER_TYPE) { + this.showWatermark(true); + } + }); + } + + /** + * Changes the flipX state of the local video. + * @param val {boolean} true if flipped. + */ + onLocalFlipXChange(val) { + this.videoContainer.setLocalFlipX(val); + } +} diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 43e4ce872..6d577765a 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -19,7 +19,6 @@ function RemoteVideo(id, VideoLayout, emitter) { this.setDisplayName(); this.flipX = false; this.isLocal = false; - this.isMuted = false; } RemoteVideo.prototype = Object.create(SmallVideo.prototype); @@ -61,7 +60,7 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) { this.popover.show = function () { // update content by forcing it, to finish even if popover // is not visible - this.updateRemoteVideoMenu(this.isMuted, true); + this.updateRemoteVideoMenu(this.isAudioMuted, true); // call the original show, passing its actual this origShowFunc.call(this.popover); }.bind(this); @@ -97,7 +96,7 @@ RemoteVideo.prototype._generatePopupContent = function () { muteLinkItem.id = "mutelink_" + this.id; - if (this.isMuted) { + if (this.isAudioMuted) { muteLinkItem.innerHTML = mutedHTML; muteLinkItem.className = 'mutelink disabled'; } @@ -109,7 +108,7 @@ RemoteVideo.prototype._generatePopupContent = function () { // Delegate event to the document. $(document).on("click", "#mutelink_" + this.id, function(){ - if (this.isMuted) + if (this.isAudioMuted) return; this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id); @@ -153,7 +152,7 @@ RemoteVideo.prototype._generatePopupContent = function () { */ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) { - this.isMuted = isMuted; + this.isAudioMuted = isMuted; // generate content, translate it and add it to document only if // popover is visible or we force to do so. diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 911c3bc4f..bf3acfce4 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -7,7 +7,7 @@ import UIEvents from "../../../service/UI/UIEvents"; const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper; function SmallVideo(VideoLayout) { - this.isMuted = false; + this.isAudioMuted = false; this.hasAvatar = false; this.isVideoMuted = false; this.videoStream = null; @@ -204,7 +204,7 @@ SmallVideo.prototype.showAudioIndicator = function(isMuted) { else { audioMutedIndicator.show(); } - this.isMuted = isMuted; + this.isAudioMuted = isMuted; }; /** @@ -222,7 +222,7 @@ SmallVideo.prototype.getAudioMutedIndicator = function () { audioMutedSpan = document.createElement('span'); audioMutedSpan.className = 'audioMuted toolbar-icon'; - + UIUtil.setTooltip(audioMutedSpan, "videothumbnail.mute", "top"); @@ -277,11 +277,11 @@ SmallVideo.prototype.getVideoMutedIndicator = function () { var mutedIndicator = document.createElement('i'); mutedIndicator.className = 'icon-camera-disabled'; - + UIUtil.setTooltip(mutedIndicator, "videothumbnail.videomute", "top"); - + videoMutedSpan.appendChild(mutedIndicator); return $('#' + this.videoSpanId + ' .videoMuted'); diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/VideoContainer.js similarity index 53% rename from modules/UI/videolayout/LargeVideo.js rename to modules/UI/videolayout/VideoContainer.js index e5449dc02..99cef5e70 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/VideoContainer.js @@ -1,17 +1,16 @@ /* global $, APP, interfaceConfig */ /* jshint -W101 */ -import UIUtil from "../util/UIUtil"; -import UIEvents from "../../../service/UI/UIEvents"; -import LargeContainer from './LargeContainer'; import FilmStrip from './FilmStrip'; -import Avatar from "../avatar/Avatar"; -import {createDeferred} from '../../util/helpers'; +import LargeContainer from './LargeContainer'; +import UIEvents from "../../../service/UI/UIEvents"; +import UIUtil from "../util/UIUtil"; + +// FIXME should be 'video' +export const VIDEO_CONTAINER_TYPE = "camera"; const FADE_DURATION_MS = 300; -export const VIDEO_CONTAINER_TYPE = "camera"; - /** * Get stream id. * @param {JitsiTrack?} stream @@ -20,7 +19,8 @@ function getStreamOwnerId(stream) { if (!stream) { return; } - if (stream.isLocal()) { // local stream doesn't have method "getParticipantId" + // local stream doesn't have method "getParticipantId" + if (stream.isLocal()) { return APP.conference.getMyUserId(); } else { return stream.getParticipantId(); @@ -154,7 +154,7 @@ function getDesktopVideoPosition(videoWidth, /** * Container for user video. */ -class VideoContainer extends LargeContainer { +export class VideoContainer extends LargeContainer { // FIXME: With Temasys we have to re-select everytime get $video () { return $('#largeVideo'); @@ -206,14 +206,14 @@ class VideoContainer extends LargeContainer { let { width, height } = this.getStreamSize(); if (this.stream && this.isScreenSharing()) { return getDesktopVideoSize( width, - height, - containerWidth, - containerHeight); + height, + containerWidth, + containerHeight); } else { return getCameraVideoSize( width, - height, - containerWidth, - containerHeight); + height, + containerWidth, + containerHeight); } } @@ -229,14 +229,14 @@ class VideoContainer extends LargeContainer { getVideoPosition (width, height, containerWidth, containerHeight) { if (this.stream && this.isScreenSharing()) { return getDesktopVideoPosition( width, - height, - containerWidth, - containerHeight); + height, + containerWidth, + containerHeight); } else { return getCameraVideoPosition( width, - height, - containerWidth, - containerHeight); + height, + containerWidth, + containerHeight); } } @@ -245,7 +245,7 @@ class VideoContainer extends LargeContainer { = this.getVideoSize(containerWidth, containerHeight); let { horizontalIndent, verticalIndent } = this.getVideoPosition(width, height, - containerWidth, containerHeight); + containerWidth, containerHeight); // update avatar position let top = containerHeight / 2 - this.avatarHeight / 4 * 3; @@ -383,306 +383,3 @@ class VideoContainer extends LargeContainer { return false; } } - -/** - * Manager for all Large containers. - */ -export default class LargeVideoManager { - constructor (emitter) { - this.containers = {}; - - this.state = VIDEO_CONTAINER_TYPE; - this.videoContainer = new VideoContainer( - () => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter); - this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer); - - // use the same video container to handle and desktop tracks - this.addContainer("desktop", this.videoContainer); - - this.width = 0; - this.height = 0; - - this.$container = $('#largeVideoContainer'); - - this.$container.css({ - display: 'inline-block' - }); - - if (interfaceConfig.SHOW_JITSI_WATERMARK) { - let leftWatermarkDiv - = this.$container.find("div.watermark.leftwatermark"); - - leftWatermarkDiv.css({display: 'block'}); - - UIUtil.setLinkHref( - leftWatermarkDiv.parent(), - interfaceConfig.JITSI_WATERMARK_LINK); - } - - if (interfaceConfig.SHOW_BRAND_WATERMARK) { - let rightWatermarkDiv - = this.$container.find("div.watermark.rightwatermark"); - - rightWatermarkDiv.css({ - display: 'block', - backgroundImage: 'url(images/rightwatermark.png)' - }); - - UIUtil.setLinkHref( - rightWatermarkDiv.parent(), - interfaceConfig.BRAND_WATERMARK_LINK); - } - - if (interfaceConfig.SHOW_POWERED_BY) { - this.$container.children("a.poweredby").css({display: 'block'}); - } - - this.$container.hover( - e => this.onHoverIn(e), - e => this.onHoverOut(e) - ); - } - - onHoverIn (e) { - if (!this.state) { - return; - } - let container = this.getContainer(this.state); - container.onHoverIn(e); - } - - onHoverOut (e) { - if (!this.state) { - return; - } - let container = this.getContainer(this.state); - container.onHoverOut(e); - } - - get id () { - let container = this.getContainer(this.state); - return container.id; - } - - scheduleLargeVideoUpdate () { - if (this.updateInProcess || !this.newStreamData) { - return; - } - - this.updateInProcess = true; - - let container = this.getContainer(this.state); - - // Include hide()/fadeOut only if we're switching between users - let preUpdate; - if (this.newStreamData.id != this.id) { - preUpdate = container.hide(); - } else { - preUpdate = Promise.resolve(); - } - - preUpdate.then(() => { - let {id, stream, videoType, resolve} = this.newStreamData; - this.newStreamData = null; - - console.info("hover in %s", id); - this.state = videoType; - let container = this.getContainer(this.state); - container.setStream(stream, videoType); - - // change the avatar url on large - this.updateAvatar(Avatar.getAvatarUrl(id)); - - // If we the continer is VIDEO_CONTAINER_TYPE, we need to check - // its stream whether exist and is muted to set isVideoMuted - // in rest of the cases it is false - let isVideoMuted = false; - if (videoType == VIDEO_CONTAINER_TYPE) - isVideoMuted = stream ? stream.isMuted() : true; - - // show the avatar on large if needed - container.showAvatar(isVideoMuted); - - let promise; - - // do not show stream if video is muted - // but we still should show watermark - if (isVideoMuted) { - this.showWatermark(true); - promise = Promise.resolve(); - } else { - promise = container.show(); - } - - // resolve updateLargeVideo promise after everything is done - promise.then(resolve); - - return promise; - }).then(() => { - // after everything is done check again if there are any pending - // new streams. - this.updateInProcess = false; - this.scheduleLargeVideoUpdate(); - }); - } - - /** - * Update large video. - * Switches to large video even if previously other container was visible. - * @param userID the userID of the participant associated with the stream - * @param {JitsiTrack?} stream new stream - * @param {string?} videoType new video type - * @returns {Promise} - */ - updateLargeVideo (userID, stream, videoType) { - if (this.newStreamData) { - this.newStreamData.reject(); - } - - this.newStreamData = createDeferred(); - this.newStreamData.id = userID; - this.newStreamData.stream = stream; - this.newStreamData.videoType = videoType; - - this.scheduleLargeVideoUpdate(); - - return this.newStreamData.promise; - } - - /** - * Update container size. - */ - updateContainerSize () { - this.width = UIUtil.getAvailableVideoWidth(); - this.height = window.innerHeight; - } - - /** - * Resize Large container of specified type. - * @param {string} type type of container which should be resized. - * @param {boolean} [animate=false] if resize process should be animated. - */ - resizeContainer (type, animate = false) { - let container = this.getContainer(type); - container.resize(this.width, this.height, animate); - } - - /** - * Resize all Large containers. - * @param {boolean} animate if resize process should be animated. - */ - resize (animate) { - // resize all containers - Object.keys(this.containers) - .forEach(type => this.resizeContainer(type, animate)); - - this.$container.animate({ - width: this.width, - height: this.height - }, { - queue: false, - duration: animate ? 500 : 0 - }); - } - - /** - * Enables/disables the filter indicating a video problem to the user. - * - * @param enable true to enable, false to disable - */ - enableVideoProblemFilter (enable) { - let container = this.getContainer(this.state); - container.$video.toggleClass("videoProblemFilter", enable); - } - - /** - * Updates the src of the dominant speaker avatar - */ - updateAvatar (avatarUrl) { - $("#dominantSpeakerAvatar").attr('src', avatarUrl); - } - - /** - * Show or hide watermark. - * @param {boolean} show - */ - showWatermark (show) { - $('.watermark').css('visibility', show ? 'visible' : 'hidden'); - } - - /** - * Add container of specified type. - * @param {string} type container type - * @param {LargeContainer} container container to add. - */ - addContainer (type, container) { - if (this.containers[type]) { - throw new Error(`container of type ${type} already exist`); - } - - this.containers[type] = container; - this.resizeContainer(type); - } - - /** - * Get Large container of specified type. - * @param {string} type container type. - * @returns {LargeContainer} - */ - getContainer (type) { - let container = this.containers[type]; - - if (!container) { - throw new Error(`container of type ${type} doesn't exist`); - } - - return container; - } - - /** - * Remove Large container of specified type. - * @param {string} type container type. - */ - removeContainer (type) { - if (!this.containers[type]) { - throw new Error(`container of type ${type} doesn't exist`); - } - - delete this.containers[type]; - } - - /** - * Show Large container of specified type. - * Does nothing if such container is already visible. - * @param {string} type container type. - * @returns {Promise} - */ - showContainer (type) { - if (this.state === type) { - return Promise.resolve(); - } - - let oldContainer = this.containers[this.state]; - if (this.state === VIDEO_CONTAINER_TYPE) { - this.showWatermark(false); - } - oldContainer.hide(); - - this.state = type; - let container = this.getContainer(type); - - return container.show().then(() => { - if (type === VIDEO_CONTAINER_TYPE) { - this.showWatermark(true); - } - }); - } - - /** - * Changes the flipX state of the local video. - * @param val {boolean} true if flipped. - */ - onLocalFlipXChange(val) { - this.videoContainer.setLocalFlipX(val); - } -} diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 3fcc02311..78f8db9c6 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -8,7 +8,8 @@ import UIEvents from "../../../service/UI/UIEvents"; import UIUtil from "../util/UIUtil"; import RemoteVideo from "./RemoteVideo"; -import LargeVideoManager, {VIDEO_CONTAINER_TYPE} from "./LargeVideo"; +import LargeVideoManager from "./LargeVideoManager"; +import {VIDEO_CONTAINER_TYPE} from "./VideoContainer"; import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo'; import LocalVideo from "./LocalVideo"; @@ -102,6 +103,7 @@ var VideoLayout = { }); localVideoThumbnail = new LocalVideo(VideoLayout, emitter); // sets default video type of local video + // FIXME container type is totally different thing from the video type localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE); // if we do not resize the thumbs here, if there is no video device // the local video thumb maybe one pixel @@ -397,6 +399,7 @@ var VideoLayout = { let videoType = VideoLayout.getRemoteVideoType(id); if (!videoType) { // make video type the default one (camera) + // FIXME container type is not a video type videoType = VIDEO_CONTAINER_TYPE; } remoteVideo.setVideoType(videoType); @@ -996,6 +999,7 @@ var VideoLayout = { if (!isOnLarge || forceUpdate) { let videoType = this.getRemoteVideoType(id); + // FIXME video type is not the same thing as container type if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) { eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id); }