Merge pull request #911 from jitsi/small_videolayout_refactoring

Small videolayout refactoring
This commit is contained in:
Дамян Минков 2016-09-22 09:45:15 -05:00 committed by GitHub
commit 586ea2ae0d
6 changed files with 349 additions and 337 deletions

View File

@ -39,7 +39,7 @@ let connectionIsInterrupted = false;
*/ */
let DSExternalInstallationInProgress = 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. * Known custom conference commands.
@ -1424,6 +1424,8 @@ export default {
APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => { APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
var smallVideoId = smallVideo.getId(); 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 if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
&& !APP.conference.isLocalId(smallVideoId)) { && !APP.conference.isLocalId(smallVideoId)) {

View File

@ -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 <tt>true</tt> to enable, <tt>false</tt> 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);
}
}

View File

@ -19,7 +19,6 @@ function RemoteVideo(id, VideoLayout, emitter) {
this.setDisplayName(); this.setDisplayName();
this.flipX = false; this.flipX = false;
this.isLocal = false; this.isLocal = false;
this.isMuted = false;
} }
RemoteVideo.prototype = Object.create(SmallVideo.prototype); RemoteVideo.prototype = Object.create(SmallVideo.prototype);
@ -61,7 +60,7 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
this.popover.show = function () { this.popover.show = function () {
// update content by forcing it, to finish even if popover // update content by forcing it, to finish even if popover
// is not visible // is not visible
this.updateRemoteVideoMenu(this.isMuted, true); this.updateRemoteVideoMenu(this.isAudioMuted, true);
// call the original show, passing its actual this // call the original show, passing its actual this
origShowFunc.call(this.popover); origShowFunc.call(this.popover);
}.bind(this); }.bind(this);
@ -97,7 +96,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
muteLinkItem.id = "mutelink_" + this.id; muteLinkItem.id = "mutelink_" + this.id;
if (this.isMuted) { if (this.isAudioMuted) {
muteLinkItem.innerHTML = mutedHTML; muteLinkItem.innerHTML = mutedHTML;
muteLinkItem.className = 'mutelink disabled'; muteLinkItem.className = 'mutelink disabled';
} }
@ -109,7 +108,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
// Delegate event to the document. // Delegate event to the document.
$(document).on("click", "#mutelink_" + this.id, function(){ $(document).on("click", "#mutelink_" + this.id, function(){
if (this.isMuted) if (this.isAudioMuted)
return; return;
this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id); this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id);
@ -153,7 +152,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
*/ */
RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) { RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
this.isMuted = isMuted; this.isAudioMuted = isMuted;
// generate content, translate it and add it to document only if // generate content, translate it and add it to document only if
// popover is visible or we force to do so. // popover is visible or we force to do so.

View File

@ -7,7 +7,7 @@ import UIEvents from "../../../service/UI/UIEvents";
const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper; const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
function SmallVideo(VideoLayout) { function SmallVideo(VideoLayout) {
this.isMuted = false; this.isAudioMuted = false;
this.hasAvatar = false; this.hasAvatar = false;
this.isVideoMuted = false; this.isVideoMuted = false;
this.videoStream = null; this.videoStream = null;
@ -204,7 +204,7 @@ SmallVideo.prototype.showAudioIndicator = function(isMuted) {
else { else {
audioMutedIndicator.show(); audioMutedIndicator.show();
} }
this.isMuted = isMuted; this.isAudioMuted = isMuted;
}; };
/** /**

View File

@ -1,17 +1,16 @@
/* global $, APP, interfaceConfig */ /* global $, APP, interfaceConfig */
/* jshint -W101 */ /* jshint -W101 */
import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents";
import LargeContainer from './LargeContainer';
import FilmStrip from './FilmStrip'; import FilmStrip from './FilmStrip';
import Avatar from "../avatar/Avatar"; import LargeContainer from './LargeContainer';
import {createDeferred} from '../../util/helpers'; 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; const FADE_DURATION_MS = 300;
export const VIDEO_CONTAINER_TYPE = "camera";
/** /**
* Get stream id. * Get stream id.
* @param {JitsiTrack?} stream * @param {JitsiTrack?} stream
@ -20,7 +19,8 @@ function getStreamOwnerId(stream) {
if (!stream) { if (!stream) {
return; 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(); return APP.conference.getMyUserId();
} else { } else {
return stream.getParticipantId(); return stream.getParticipantId();
@ -154,7 +154,7 @@ function getDesktopVideoPosition(videoWidth,
/** /**
* Container for user video. * Container for user video.
*/ */
class VideoContainer extends LargeContainer { export class VideoContainer extends LargeContainer {
// FIXME: With Temasys we have to re-select everytime // FIXME: With Temasys we have to re-select everytime
get $video () { get $video () {
return $('#largeVideo'); return $('#largeVideo');
@ -383,306 +383,3 @@ class VideoContainer extends LargeContainer {
return false; 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 <tt>true</tt> to enable, <tt>false</tt> 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);
}
}

View File

@ -8,7 +8,8 @@ import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil"; import UIUtil from "../util/UIUtil";
import RemoteVideo from "./RemoteVideo"; 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 {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
import LocalVideo from "./LocalVideo"; import LocalVideo from "./LocalVideo";
@ -102,6 +103,7 @@ var VideoLayout = {
}); });
localVideoThumbnail = new LocalVideo(VideoLayout, emitter); localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
// sets default video type of local video // sets default video type of local video
// FIXME container type is totally different thing from the video type
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE); localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
// if we do not resize the thumbs here, if there is no video device // if we do not resize the thumbs here, if there is no video device
// the local video thumb maybe one pixel // the local video thumb maybe one pixel
@ -397,6 +399,7 @@ var VideoLayout = {
let videoType = VideoLayout.getRemoteVideoType(id); let videoType = VideoLayout.getRemoteVideoType(id);
if (!videoType) { if (!videoType) {
// make video type the default one (camera) // make video type the default one (camera)
// FIXME container type is not a video type
videoType = VIDEO_CONTAINER_TYPE; videoType = VIDEO_CONTAINER_TYPE;
} }
remoteVideo.setVideoType(videoType); remoteVideo.setVideoType(videoType);
@ -996,6 +999,7 @@ var VideoLayout = {
if (!isOnLarge || forceUpdate) { if (!isOnLarge || forceUpdate) {
let videoType = this.getRemoteVideoType(id); let videoType = this.getRemoteVideoType(id);
// FIXME video type is not the same thing as container type
if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) { if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id); eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
} }