jiti-meet/modules/UI/videolayout/Filmstrip.js

538 lines
18 KiB
JavaScript
Raw Normal View History

/* global $, APP, interfaceConfig */
import { setFilmstripVisibility } from '../../../react/features/filmstrip';
import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../util/UIUtil';
import {
TOOLBAR_FILMSTRIP_TOGGLED,
sendAnalyticsEvent
} from '../../../react/features/analytics';
const Filmstrip = {
/**
*
* @param eventEmitter the {EventEmitter} through which {Filmstrip} is to
* emit/fire {UIEvents} (such as {UIEvents.TOGGLED_FILMSTRIP}).
*/
init(eventEmitter) {
2016-11-03 11:44:17 +00:00
this.iconMenuDownClassName = 'icon-menu-down';
this.iconMenuUpClassName = 'icon-menu-up';
this.filmstripContainerClassName = 'filmstrip';
this.filmstrip = $('#remoteVideos');
this.filmstripRemoteVideos = $('#filmstripRemoteVideosContainer');
this.eventEmitter = eventEmitter;
// Show the toggle button and add event listeners only when out of
// filmstrip only mode.
if (!interfaceConfig.filmStripOnly) {
this._initFilmstripToolbar();
this.registerListeners();
}
},
/**
* Initializes the filmstrip toolbar.
*/
_initFilmstripToolbar() {
const toolbarContainerHTML = this._generateToolbarHTML();
const className = this.filmstripContainerClassName;
const container = document.querySelector(`.${className}`);
UIUtil.prependChild(container, toolbarContainerHTML);
const iconSelector = '#toggleFilmstripButton i';
this.toggleFilmstripIcon = document.querySelector(iconSelector);
},
/**
* Generates HTML layout for filmstrip toggle button and wrapping container.
2016-11-03 11:44:17 +00:00
* @returns {HTMLElement}
* @private
*/
_generateToolbarHTML() {
const container = document.createElement('div');
const isVisible = this.isFilmstripVisible();
2016-11-03 15:07:48 +00:00
container.className = 'filmstrip__toolbar';
container.innerHTML = `
<button id="toggleFilmstripButton">
<i class="icon-menu-${isVisible ? 'down' : 'up'}">
</i>
</button>
`;
2016-11-03 11:44:17 +00:00
return container;
},
/**
* Attach 'click' listener to "hide filmstrip" button
*/
registerListeners() {
// Important:
// Firing the event instead of executing toggleFilmstrip method because
// it's important to hide the filmstrip by UI.toggleFilmstrip in order
// to correctly resize the video area.
$('#toggleFilmstripButton').on('click',
() => this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP));
this._registerToggleFilmstripShortcut();
},
/**
* Registering toggle filmstrip shortcut
* @private
*/
_registerToggleFilmstripShortcut() {
const shortcut = 'F';
const shortcutAttr = 'filmstripPopover';
const description = 'keyboardShortcuts.toggleFilmstrip';
// Important:
// Firing the event instead of executing toggleFilmstrip method because
// it's important to hide the filmstrip by UI.toggleFilmstrip in order
// to correctly resize the video area.
const handler = () => this.eventEmitter.emit(UIEvents.TOGGLE_FILMSTRIP);
APP.keyboardshortcut.registerShortcut(
shortcut,
shortcutAttr,
handler,
description
);
2016-11-03 11:44:17 +00:00
},
/**
* Changes classes of icon for showing down state
*/
showMenuDownIcon() {
const icon = this.toggleFilmstripIcon;
if (icon) {
icon.classList.add(this.iconMenuDownClassName);
icon.classList.remove(this.iconMenuUpClassName);
}
2016-11-03 11:44:17 +00:00
},
/**
* Changes classes of icon for showing up state
*/
showMenuUpIcon() {
const icon = this.toggleFilmstripIcon;
if (icon) {
icon.classList.add(this.iconMenuUpClassName);
icon.classList.remove(this.iconMenuDownClassName);
}
},
/**
* Toggles the visibility of the filmstrip.
*
* @param visible optional {Boolean} which specifies the desired visibility
* of the filmstrip. If not specified, the visibility will be flipped
* (i.e. toggled); otherwise, the visibility will be set to the specified
* value.
2016-12-04 19:28:48 +00:00
* @param {Boolean} sendAnalytics - True to send an analytics event. The
* default value is true.
*
* Note:
* This method shouldn't be executed directly to hide the filmstrip.
* It's important to hide the filmstrip with UI.toggleFilmstrip in order
* to correctly resize the video area.
*/
toggleFilmstrip(visible, sendAnalytics = true) {
2016-12-04 19:28:48 +00:00
const isVisibleDefined = typeof visible === 'boolean';
2016-11-03 13:02:03 +00:00
if (!isVisibleDefined) {
// eslint-disable-next-line no-param-reassign
visible = this.isFilmstripVisible();
} else if (this.isFilmstripVisible() === visible) {
return;
}
2016-12-04 19:28:48 +00:00
if (sendAnalytics) {
sendAnalyticsEvent(TOOLBAR_FILMSTRIP_TOGGLED);
}
this.filmstrip.toggleClass('hidden');
2016-12-04 19:28:48 +00:00
if (visible) {
2016-11-03 13:02:03 +00:00
this.showMenuUpIcon();
2016-12-04 19:28:48 +00:00
} else {
this.showMenuDownIcon();
2016-11-03 13:02:03 +00:00
}
// Emit/fire UIEvents.TOGGLED_FILMSTRIP.
2016-12-04 19:28:48 +00:00
const eventEmitter = this.eventEmitter;
const isFilmstripVisible = this.isFilmstripVisible();
if (eventEmitter) {
eventEmitter.emit(
UIEvents.TOGGLED_FILMSTRIP,
this.isFilmstripVisible());
}
APP.store.dispatch(setFilmstripVisibility(isFilmstripVisible));
},
2016-11-03 13:12:45 +00:00
/**
* Shows if filmstrip is visible
* @returns {boolean}
*/
isFilmstripVisible() {
return !this.filmstrip.hasClass('hidden');
},
/**
* Adjusts styles for filmstrip-only mode.
*/
setFilmstripOnly() {
this.filmstrip.addClass('filmstrip__videos-filmstripOnly');
},
2016-11-03 13:12:45 +00:00
/**
* Returns the height of filmstrip
* @returns {number} height
*/
getFilmstripHeight() {
// FIXME Make it more clear the getFilmstripHeight check is used in
// horizontal film strip mode for calculating how tall large video
// display should be.
if (this.isFilmstripVisible() && !interfaceConfig.VERTICAL_FILMSTRIP) {
return $(`.${this.filmstripContainerClassName}`).outerHeight();
}
return 0;
},
/**
* Returns the width of filmstip
* @returns {number} width
*/
getFilmstripWidth() {
return this.isFilmstripVisible()
? this.filmstrip.outerWidth()
- parseInt(this.filmstrip.css('paddingLeft'), 10)
- parseInt(this.filmstrip.css('paddingRight'), 10)
: 0;
},
2016-11-03 13:12:45 +00:00
/**
* Calculates the size for thumbnails: local and remote one
* @returns {*|{localVideo, remoteVideo}}
*/
2016-09-15 02:20:54 +00:00
calculateThumbnailSize() {
const availableSizes = this.calculateAvailableSize();
const width = availableSizes.availableWidth;
const height = availableSizes.availableHeight;
2016-09-15 02:20:54 +00:00
return this.calculateThumbnailSizeFromAvailable(width, height);
},
2016-11-07 23:00:50 +00:00
/**
* Calculates available size for one thumbnail according to
* the current window size.
*
* @returns {{availableWidth: number, availableHeight: number}}
*/
2016-09-15 02:20:54 +00:00
calculateAvailableSize() {
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
const thumbs = this.getThumbs(true);
const numvids = thumbs.remoteThumbs.length;
const localVideoContainer = $('#localVideoContainer');
/**
* If the videoAreaAvailableWidth is set we use this one to calculate
* the filmstrip width, because we're probably in a state where the
* filmstrip size hasn't been updated yet, but it will be.
*/
const videoAreaAvailableWidth
= UIUtil.getAvailableVideoWidth()
- this._getFilmstripExtraPanelsWidth()
- UIUtil.parseCssInt(this.filmstrip.css('right'), 10)
- UIUtil.parseCssInt(this.filmstrip.css('paddingLeft'), 10)
- UIUtil.parseCssInt(this.filmstrip.css('paddingRight'), 10)
- UIUtil.parseCssInt(this.filmstrip.css('borderLeftWidth'), 10)
- UIUtil.parseCssInt(this.filmstrip.css('borderRightWidth'), 10)
2016-05-01 18:35:18 +00:00
- 5;
let availableWidth = videoAreaAvailableWidth;
2016-09-15 02:20:54 +00:00
// If local thumb is not hidden
if (thumbs.localThumb) {
2016-05-01 18:35:18 +00:00
availableWidth = Math.floor(
videoAreaAvailableWidth - (
UIUtil.parseCssInt(
localVideoContainer.css('borderLeftWidth'), 10)
+ UIUtil.parseCssInt(
localVideoContainer.css('borderRightWidth'), 10)
+ UIUtil.parseCssInt(
localVideoContainer.css('paddingLeft'), 10)
+ UIUtil.parseCssInt(
localVideoContainer.css('paddingRight'), 10)
+ UIUtil.parseCssInt(
localVideoContainer.css('marginLeft'), 10)
+ UIUtil.parseCssInt(
localVideoContainer.css('marginRight'), 10))
2016-09-15 02:20:54 +00:00
);
}
// If the number of videos is 0 or undefined or we're in vertical
// filmstrip mode we don't need to calculate further any adjustments
// to width based on the number of videos present.
if (numvids && !interfaceConfig.VERTICAL_FILMSTRIP) {
const remoteVideoContainer = thumbs.remoteThumbs.eq(0);
2016-09-15 02:20:54 +00:00
availableWidth = Math.floor(
videoAreaAvailableWidth - (numvids * (
UIUtil.parseCssInt(
remoteVideoContainer.css('borderLeftWidth'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('borderRightWidth'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('paddingLeft'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('paddingRight'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('marginLeft'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('marginRight'), 10)))
2016-09-15 02:20:54 +00:00
);
}
const maxHeight
// If the MAX_HEIGHT property hasn't been specified
// we have the static value.
2016-09-15 02:20:54 +00:00
= Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
availableHeight);
availableHeight
2016-09-15 02:20:54 +00:00
= Math.min(maxHeight, window.innerHeight - 18);
return { availableWidth,
availableHeight };
2016-09-15 02:20:54 +00:00
},
2016-11-03 13:12:45 +00:00
/**
2016-11-08 11:10:50 +00:00
* Traverse all elements inside the filmstrip
* and calculates the sum of all of them except
* remote videos element. Used for calculation of
* available width for video thumbnails.
*
2016-11-08 11:10:50 +00:00
* @returns {number} calculated width
* @private
2016-11-04 16:13:57 +00:00
*/
_getFilmstripExtraPanelsWidth() {
const className = this.filmstripContainerClassName;
2016-11-08 11:10:50 +00:00
let width = 0;
2016-11-04 16:13:57 +00:00
$(`.${className}`)
.children()
.each(function() {
/* eslint-disable no-invalid-this */
2016-11-04 16:13:57 +00:00
if (this.id !== 'remoteVideos') {
2016-11-08 11:10:50 +00:00
width += $(this).outerWidth();
2016-11-04 16:13:57 +00:00
}
/* eslint-enable no-invalid-this */
2016-11-04 16:13:57 +00:00
});
2016-11-08 11:10:50 +00:00
return width;
2016-11-04 16:13:57 +00:00
},
/**
Calculate the thumbnail size in order to fit all the thumnails in passed
* dimensions.
* NOTE: Here we assume that the remote and local thumbnails are with the
* same height.
* @param {int} availableWidth the maximum width for all thumbnails
* @param {int} availableHeight the maximum height for all thumbnails
2016-11-04 16:13:57 +00:00
* @returns {{localVideo, remoteVideo}}
2016-11-03 13:12:45 +00:00
*/
2016-09-15 02:20:54 +00:00
calculateThumbnailSizeFromAvailable(availableWidth, availableHeight) {
/**
* Let:
* lW - width of the local thumbnail
* rW - width of the remote thumbnail
* h - the height of the thumbnails
* remoteRatio - width:height for the remote thumbnail
* localRatio - width:height for the local thumbnail
* remoteThumbsInRow - number of remote thumbnails in a row (we have
* only one local thumbnail) next to the local thumbnail. In vertical
* filmstrip mode, this will always be 0.
*
* Since the height for local thumbnail = height for remote thumbnail
* and we know the ratio (width:height) for the local and for the
* remote thumbnail we can find rW/lW:
* rW / remoteRatio = lW / localRatio then -
* remoteLocalWidthRatio = rW / lW = remoteRatio / localRatio
* and rW = lW * remoteRatio / localRatio = lW * remoteLocalWidthRatio
* And the total width for the thumbnails is:
* totalWidth = rW * remoteThumbsInRow + lW
* = lW * remoteLocalWidthRatio * remoteThumbsInRow + lW =
* lW * (remoteLocalWidthRatio * remoteThumbsInRow + 1)
* and the h = lW/localRatio
*
* In order to fit all the thumbails in the area defined by
* availableWidth * availableHeight we should check one of the
* following options:
* 1) if availableHeight == h - totalWidth should be less than
* availableWidth
* 2) if availableWidth == totalWidth - h should be less than
* availableHeight
*
* 1) or 2) will be true and we are going to use it to calculate all
* sizes.
*
* if 1) is true that means that
* availableHeight/h > availableWidth/totalWidth otherwise 2) is true
*/
2016-09-15 02:20:54 +00:00
const remoteThumbsInRow = interfaceConfig.VERTICAL_FILMSTRIP
? 0 : this.getThumbs(true).remoteThumbs.length;
const remoteLocalWidthRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO
/ interfaceConfig.LOCAL_THUMBNAIL_RATIO;
const lW = Math.min(availableWidth
/ ((remoteLocalWidthRatio * remoteThumbsInRow) + 1), availableHeight
* interfaceConfig.LOCAL_THUMBNAIL_RATIO);
const h = lW / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
const remoteVideoWidth = lW * remoteLocalWidthRatio;
let localVideo;
if (interfaceConfig.VERTICAL_FILMSTRIP) {
localVideo = {
thumbWidth: remoteVideoWidth,
thumbHeight: h * remoteLocalWidthRatio
};
} else {
localVideo = {
thumbWidth: lW,
thumbHeight: h
};
}
2016-11-04 16:13:57 +00:00
return {
localVideo,
remoteVideo: {
thumbWidth: remoteVideoWidth,
thumbHeight: h
}
};
},
2016-11-03 13:12:45 +00:00
/**
* Resizes thumbnails
* @param local
* @param remote
* @param animate
* @param forceUpdate
* @returns {Promise}
*/
// eslint-disable-next-line max-params
2016-12-04 19:28:48 +00:00
resizeThumbnails(local, remote, animate = false, forceUpdate = false) {
return new Promise(resolve => {
const thumbs = this.getThumbs(!forceUpdate);
const promises = [];
if (thumbs.localThumb) {
// eslint-disable-next-line no-shadow
promises.push(new Promise(resolve => {
thumbs.localThumb.animate({
height: local.thumbHeight,
'min-height': local.thumbHeight,
'min-width': local.thumbWidth,
width: local.thumbWidth
}, this._getAnimateOptions(animate, resolve));
}));
}
if (thumbs.remoteThumbs) {
// eslint-disable-next-line no-shadow
promises.push(new Promise(resolve => {
thumbs.remoteThumbs.animate({
height: remote.thumbHeight,
'min-height': remote.thumbHeight,
'min-width': remote.thumbWidth,
width: remote.thumbWidth
}, this._getAnimateOptions(animate, resolve));
}));
}
// eslint-disable-next-line no-shadow
promises.push(new Promise(resolve => {
// Let CSS take care of height in vertical filmstrip mode.
if (interfaceConfig.VERTICAL_FILMSTRIP) {
WiP(invite-ui): Initial move of invite UI to invite button (#1950) * WiP(invite-ui): Initial move of invite UI to invite button * Adjusts styling to fit both horizontal and vertical filmstrip * Removes comment and functions not needed * [squash] Addressing various review comments * [squash] Move invite options to a separate config * [squash] Adjust invite button styles until we fix the whole UI theme * [squash] Fix the remote videos scroll * [squash]:Do not show popup menu when 1 option is available * [squash]: Disable the invite button in filmstrip mode * feat(connection-indicator): implement automatic hiding on good connection (#2009) * ref(connection-stats): use PropTypes package * feat(connection-stats): display a summary of the connection quality * feat(connection-indicator): show empty bars for interrupted connection * feat(connection-indicator): change background color based on status * feat(connection-indicator): implement automatic hiding on good connection * fix(connection-indicator): explicitly set font size Currently non-react code will set an icon size on ConnectionIndicator. This doesn't work on initial call join in vertical filmstrip after some changes to support hiding the indicator. The chosen fix is passing in the icon size to mirror what would happe with full filmstrip reactification. * ref(connection-stats): rename statuses * feat(connection-indicator): make hiding behavior configurable The original implementation made the auto hiding of the indicator configured in interfaceConfig. * fix(connection-indicator): readd class expected by torture tests * fix(connection-indicator): change connection quality display styling Bold the connection summary in the stats popover so it stands out. Change the summaries so there are only three--strong, nonoptimal, poor. * fix(connection-indicator): gray background on lost connection * feat(icons): add new gsm bars icon * feat(connection-indicator): use new 3-bar icon * ref(icons): remove icon-connection and icon-connection-lost Both have been replaced by icon-gsm-bars so they are not being referenced anymore. Mobile looks to have connect-lost as a separate icon in font-icons/jitsi.json. * fix(defaultToolbarButtons): Fixes unresolved InfoDialogButton component problem * [squash]: Makes invite button fit the container * [squash]:Addressing invite truncate, remote menu position and comment * [squash]:Fix z-index in horizontal mode, z-index in lonely call * [squash]: Fix filmstripOnly property, remove important from css
2017-10-03 16:30:42 +00:00
$('#filmstripLocalVideo').animate({
// adds 4 px because of small video 2px border
width: local.thumbWidth + 4
}, this._getAnimateOptions(animate, resolve));
} else {
this.filmstrip.animate({
WiP(invite-ui): Initial move of invite UI to invite button (#1950) * WiP(invite-ui): Initial move of invite UI to invite button * Adjusts styling to fit both horizontal and vertical filmstrip * Removes comment and functions not needed * [squash] Addressing various review comments * [squash] Move invite options to a separate config * [squash] Adjust invite button styles until we fix the whole UI theme * [squash] Fix the remote videos scroll * [squash]:Do not show popup menu when 1 option is available * [squash]: Disable the invite button in filmstrip mode * feat(connection-indicator): implement automatic hiding on good connection (#2009) * ref(connection-stats): use PropTypes package * feat(connection-stats): display a summary of the connection quality * feat(connection-indicator): show empty bars for interrupted connection * feat(connection-indicator): change background color based on status * feat(connection-indicator): implement automatic hiding on good connection * fix(connection-indicator): explicitly set font size Currently non-react code will set an icon size on ConnectionIndicator. This doesn't work on initial call join in vertical filmstrip after some changes to support hiding the indicator. The chosen fix is passing in the icon size to mirror what would happe with full filmstrip reactification. * ref(connection-stats): rename statuses * feat(connection-indicator): make hiding behavior configurable The original implementation made the auto hiding of the indicator configured in interfaceConfig. * fix(connection-indicator): readd class expected by torture tests * fix(connection-indicator): change connection quality display styling Bold the connection summary in the stats popover so it stands out. Change the summaries so there are only three--strong, nonoptimal, poor. * fix(connection-indicator): gray background on lost connection * feat(icons): add new gsm bars icon * feat(connection-indicator): use new 3-bar icon * ref(icons): remove icon-connection and icon-connection-lost Both have been replaced by icon-gsm-bars so they are not being referenced anymore. Mobile looks to have connect-lost as a separate icon in font-icons/jitsi.json. * fix(defaultToolbarButtons): Fixes unresolved InfoDialogButton component problem * [squash]: Makes invite button fit the container * [squash]:Addressing invite truncate, remote menu position and comment * [squash]:Fix z-index in horizontal mode, z-index in lonely call * [squash]: Fix filmstripOnly property, remove important from css
2017-10-03 16:30:42 +00:00
// adds 4 px because of small video 2px border
height: remote.thumbHeight + 4
}, this._getAnimateOptions(animate, resolve));
}
}));
2016-11-11 15:09:07 +00:00
promises.push(new Promise(() => {
const { localThumb } = this.getThumbs();
const height = localThumb ? localThumb.height() : 0;
const fontSize = UIUtil.getIndicatorFontSize(height);
this.filmstrip.find('.indicator').animate({
fontSize
}, this._getAnimateOptions(animate, resolve));
}));
if (!animate) {
resolve();
}
Promise.all(promises).then(resolve);
});
},
/**
* Helper method. Returns options for jQuery animation
* @param animate {Boolean} - animation flag
* @param cb {Function} - complete callback
* @returns {Object} - animation options object
* @private
*/
_getAnimateOptions(animate, cb = $.noop) {
return {
queue: false,
duration: animate ? 500 : 0,
complete: cb
};
},
2016-11-03 13:12:45 +00:00
/**
* Returns thumbnails of the filmstrip
* @param onlyVisible
2016-11-03 13:12:45 +00:00
* @returns {object} thumbnails
*/
getThumbs(onlyVisible = false) {
let selector = 'span';
if (onlyVisible) {
selector += ':visible';
}
const localThumb = $('#localVideoContainer');
const remoteThumbs = this.filmstripRemoteVideos.children(selector);
2016-09-15 02:20:54 +00:00
2016-05-01 18:35:18 +00:00
// Exclude the local video container if it has been hidden.
if (localThumb.hasClass('hidden')) {
2016-09-15 02:20:54 +00:00
return { remoteThumbs };
}
return { remoteThumbs,
localThumb };
2016-09-28 21:31:40 +00:00
}
};
export default Filmstrip;