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

665 lines
21 KiB
JavaScript
Raw Normal View History

/* global $, APP */
/* eslint-disable no-unused-vars */
2020-05-20 10:57:03 +00:00
import Logger from 'jitsi-meet-logger';
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics';
2019-06-26 14:08:23 +00:00
import { Avatar } from '../../../react/features/base/avatar';
import { i18next } from '../../../react/features/base/i18n';
import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../../react/features/base/media';
import { getParticipantById } from '../../../react/features/base/participants';
import { getTrackByMediaTypeAndParticipant } from '../../../react/features/base/tracks';
import { CHAT_SIZE } from '../../../react/features/chat';
feat(quality-slider): initial implementation (#1817) * feat(quality-slider): initial implementation - Add new menu button with an Inline Dialog slider for selecting received video quality. - Place P2P status in redux store for the Inline Dialog to display a warning about not respecting video quality selection. - Respond to data channel open events by setting receive video quality. This is for lonely call cases where a setting is set before the data channel is open. - Remove dropdown menu from video status label and clean up related js and css. * first pass at addressing feedback - Move VideoStatusLabel to video-quality directory. - Rename VideoStatusLabel to VideoQualityLabel. - Open VideoQualitydialog from VideoQualityLabel. - New CSS for making VideoQualityLabel display properly. - Do not render VideoQualityLabel in filmstrip only instead of hiding with css. - Remove tooltip from VideoQualityLabel. - Show LD, SD, HD labels in VideoQualityLabel. - Remove action SET_LARGE_VIDEO_HD_STATUS from conference. - Create new action UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION in large-video. - Move VideoQualityButton into video-quality directory. - General renaming (medium -> standard, menu -> dialog). - Render P2P message between title and slider. - Add padding to slider for displacement caused by P2P message's new placement. - Fix display issue with VideoQualityButton displaying out of line in the primary toolbar. * second pass at addressing feedback - Fix p2p inline message color - Force labels to break on words - Resolve rebase issues, including only dispatching quality update on change. Before there was double calling of dispatch produced by an IE11 workaround. This breaks now when setting audio only mode to true twice. - Rename some instances of quality to definition * rename to data channel opened * do not show p2p in audio only * stop toggle audio only icon automatically * remove fixme about toolbar button * find closest resolution for label * toggle dialog on button click * redo last commit for both button and label
2017-08-09 19:40:03 +00:00
import {
updateKnownLargeVideoResolution
} from '../../../react/features/large-video/actions';
2020-05-20 10:57:03 +00:00
import { PresenceLabel } from '../../../react/features/presence-status';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
2020-05-20 10:57:03 +00:00
/* eslint-enable no-unused-vars */
import { createDeferred } from '../../util/helpers';
import AudioLevels from '../audio_levels/AudioLevels';
import { VideoContainer, VIDEO_CONTAINER_TYPE } from './VideoContainer';
2016-09-28 21:31:40 +00:00
2020-05-20 10:57:03 +00:00
const logger = Logger.getLogger(__filename);
2016-09-28 21:31:40 +00:00
const DESKTOP_CONTAINER_TYPE = 'desktop';
2016-01-15 14:59:35 +00:00
/**
* Manager for all Large containers.
*/
2015-12-25 16:55:45 +00:00
export default class LargeVideoManager {
/**
* Checks whether given container is a {@link VIDEO_CONTAINER_TYPE}.
* FIXME currently this is a workaround for the problem where video type is
* mixed up with container type.
* @param {string} containerType
* @return {boolean}
*/
static isVideoContainer(containerType) {
return containerType === VIDEO_CONTAINER_TYPE
|| containerType === DESKTOP_CONTAINER_TYPE;
}
/**
*
*/
constructor() {
/**
* The map of <tt>LargeContainer</tt>s where the key is the video
* container type.
* @type {Object.<string, LargeContainer>}
*/
2015-12-25 16:55:45 +00:00
this.containers = {};
2015-06-23 08:00:46 +00:00
this.state = VIDEO_CONTAINER_TYPE;
2017-02-08 19:57:55 +00:00
// FIXME: We are passing resizeContainer as parameter which is calling
// Container.resize. Probably there's better way to implement this.
this.videoContainer = new VideoContainer(() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
2016-11-11 17:55:18 +00:00
// use the same video container to handle desktop tracks
this.addContainer(DESKTOP_CONTAINER_TYPE, this.videoContainer);
/**
* The preferred width passed as an argument to {@link updateContainerSize}.
*
* @type {number|undefined}
*/
this.preferredWidth = undefined;
/**
* The preferred height passed as an argument to {@link updateContainerSize}.
*
* @type {number|undefined}
*/
this.preferredHeight = undefined;
/**
* The calculated width that will be used for the large video.
* @type {number}
*/
2015-12-25 16:55:45 +00:00
this.width = 0;
/**
* The calculated height that will be used for the large video.
* @type {number}
*/
2015-12-25 16:55:45 +00:00
this.height = 0;
2015-06-23 08:00:46 +00:00
2017-05-03 16:47:59 +00:00
/**
* Cache the aspect ratio of the video displayed to detect changes to
* the aspect ratio on video resize events.
*
* @type {number}
*/
this._videoAspectRatio = 0;
2015-12-25 16:55:45 +00:00
this.$container = $('#largeVideoContainer');
2015-12-14 12:26:50 +00:00
2015-12-25 16:55:45 +00:00
this.$container.css({
display: 'inline-block'
});
2015-12-25 16:55:45 +00:00
this.$container.hover(
e => this.onHoverIn(e),
e => this.onHoverOut(e)
);
// Bind event handler so it is only bound once for every instance.
2017-05-03 16:47:59 +00:00
this._onVideoResolutionUpdate
= this._onVideoResolutionUpdate.bind(this);
2017-05-03 16:47:59 +00:00
this.videoContainer.addResizeListener(this._onVideoResolutionUpdate);
this._dominantSpeakerAvatarContainer
= document.getElementById('dominantSpeakerAvatarContainer');
}
/**
* Removes any listeners registered on child components, including
* React Components.
*
* @returns {void}
*/
destroy() {
this.videoContainer.removeResizeListener(
2017-05-03 16:47:59 +00:00
this._onVideoResolutionUpdate);
this.removePresenceLabel();
ReactDOM.unmountComponentAtNode(this._dominantSpeakerAvatarContainer);
this.$container.css({ display: 'none' });
2015-12-25 16:55:45 +00:00
}
/**
*
*/
onHoverIn(e) {
2015-12-25 16:55:45 +00:00
if (!this.state) {
return;
2015-12-14 12:26:50 +00:00
}
const container = this.getCurrentContainer();
2015-12-25 16:55:45 +00:00
container.onHoverIn(e);
}
/**
*
*/
onHoverOut(e) {
2015-12-25 16:55:45 +00:00
if (!this.state) {
return;
2015-12-14 12:26:50 +00:00
}
const container = this.getCurrentContainer();
2015-12-25 16:55:45 +00:00
container.onHoverOut(e);
}
/**
*
*/
get id() {
const container = this.getCurrentContainer();
// If a user switch for large video is in progress then provide what
// will be the end result of the update.
if (this.updateInProcess
&& this.newStreamData
&& this.newStreamData.id !== container.id) {
return this.newStreamData.id;
}
return container.id;
2015-12-25 16:55:45 +00:00
}
/**
*
*/
scheduleLargeVideoUpdate() {
if (this.updateInProcess || !this.newStreamData) {
return;
}
this.updateInProcess = true;
// Include hide()/fadeOut only if we're switching between users
// eslint-disable-next-line eqeqeq
const container = this.getCurrentContainer();
const isUserSwitch = this.newStreamData.id !== container.id;
const preUpdate = isUserSwitch ? container.hide() : Promise.resolve();
preUpdate.then(() => {
const { id, stream, videoType, resolve } = this.newStreamData;
// FIXME this does not really make sense, because the videoType
// (camera or desktop) is a completely different thing than
// the video container type (Etherpad, SharedVideo, VideoContainer).
const isVideoContainer = LargeVideoManager.isVideoContainer(videoType);
this.newStreamData = null;
logger.info(`hover in ${id}`);
this.state = videoType;
// eslint-disable-next-line no-shadow
const container = this.getCurrentContainer();
container.setStream(id, stream, videoType);
// change the avatar url on large
2019-06-26 14:08:23 +00:00
this.updateAvatar();
const isVideoMuted = !stream || stream.isMuted();
const state = APP.store.getState();
const participant = getParticipantById(state, id);
const connectionStatus = participant?.connectionStatus;
const isVideoRenderable = !isVideoMuted
&& (APP.conference.isLocalId(id) || connectionStatus === JitsiParticipantConnectionStatus.ACTIVE);
const isAudioOnly = APP.conference.isAudioOnly();
const showAvatar
= isVideoContainer
&& ((isAudioOnly && videoType !== VIDEO_TYPE.DESKTOP) || !isVideoRenderable);
let promise;
// do not show stream if video is muted
// but we still should show watermark
if (showAvatar) {
this.showWatermark(true);
// If the intention of this switch is to show the avatar
// we need to make sure that the video is hidden
promise = container.hide();
if ((!shouldDisplayTileView(state) || participant?.pinned) // In theory the tile view may not be
// enabled yet when we auto pin the participant.
&& participant && !participant.local && !participant.isFakeParticipant) {
// remote participant only
const track = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, id);
const isScreenSharing = track?.videoType === 'desktop';
if (isScreenSharing) {
// send the event
sendAnalytics(createScreenSharingIssueEvent({
source: 'large-video',
connectionStatus,
isVideoMuted,
isAudioOnly,
isVideoContainer,
videoType
}));
}
}
} else {
promise = container.show();
}
// show the avatar on large if needed
container.showAvatar(showAvatar);
// Clean up audio level after previous speaker.
if (showAvatar) {
this.updateLargeVideoAudioLevel(0);
}
const messageKey
= connectionStatus === JitsiParticipantConnectionStatus.INACTIVE ? 'connection.LOW_BANDWIDTH' : null;
// Do not show connection status message in the audio only mode,
// because it's based on the video playback status.
const overrideAndHide = APP.conference.isAudioOnly();
this.updateParticipantConnStatusIndication(
id,
!overrideAndHide && messageKey);
// Change the participant id the presence label is listening to.
this.updatePresenceLabel(id);
this.videoContainer.positionRemoteStatusMessages();
// 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();
2015-12-25 16:55:45 +00:00
});
}
/**
* Shows/hides notification about participant's connectivity issues to be
* shown on the large video area.
*
* @param {string} id the id of remote participant(MUC nickname)
* @param {string|null} messageKey the i18n key of the message which will be
* displayed on the large video or <tt>null</tt> to hide it.
*
* @private
*/
updateParticipantConnStatusIndication(id, messageKey) {
if (messageKey) {
// Get user's display name
const displayName
= APP.conference.getParticipantDisplayName(id);
this._setRemoteConnectionMessage(
messageKey,
{ displayName });
// Show it now only if the VideoContainer is on top
this.showRemoteConnectionMessage(
LargeVideoManager.isVideoContainer(this.state));
} else {
// Hide the message
this.showRemoteConnectionMessage(false);
}
}
/**
* 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;
}
2016-01-15 14:59:35 +00:00
/**
* Update container size.
2016-01-15 14:59:35 +00:00
*/
updateContainerSize(width, height) {
if (typeof width === 'number') {
this.preferredWidth = width;
}
if (typeof height === 'number') {
this.preferredHeight = height;
}
let widthToUse = this.preferredWidth || window.innerWidth;
const { isOpen } = APP.store.getState()['features/chat'];
2021-01-14 16:12:08 +00:00
if (isOpen && window.innerWidth > 580) {
/**
* If chat state is open, we re-compute the container width
* by subtracting the default width of the chat.
*/
widthToUse -= CHAT_SIZE;
}
this.width = widthToUse;
this.height = this.preferredHeight || window.innerHeight;
2015-12-25 16:55:45 +00:00
}
2016-01-15 14:59:35 +00:00
/**
* 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) {
const container = this.getContainer(type);
2015-12-25 16:55:45 +00:00
container.resize(this.width, this.height, animate);
}
2016-01-15 14:59:35 +00:00
/**
* Resize all Large containers.
* @param {boolean} animate if resize process should be animated.
*/
resize(animate) {
2015-12-25 16:55:45 +00:00
// resize all containers
Object.keys(this.containers)
.forEach(type => this.resizeContainer(type, animate));
2015-12-25 16:55:45 +00:00
}
2015-08-03 15:58:22 +00:00
2015-12-25 16:55:45 +00:00
/**
* Updates the src of the dominant speaker avatar
2015-12-25 16:55:45 +00:00
*/
2019-06-26 14:08:23 +00:00
updateAvatar() {
ReactDOM.render(
<Provider store = { APP.store }>
<Avatar
id = "dominantSpeakerAvatar"
2019-06-26 14:08:23 +00:00
participantId = { this.id }
size = { 200 } />
</Provider>,
this._dominantSpeakerAvatarContainer
);
2015-12-25 16:55:45 +00:00
}
2016-09-28 21:31:40 +00:00
/**
* Updates the audio level indicator of the large video.
*
* @param lvl the new audio level to set
*/
updateLargeVideoAudioLevel(lvl) {
AudioLevels.updateLargeVideoAudioLevel('dominantSpeaker', lvl);
2016-09-28 21:31:40 +00:00
}
/**
* Displays a message of the passed in participant id's presence status. The
* message will not display if the remote connection message is displayed.
*
* @param {string} id - The participant ID whose associated user's presence
* status should be displayed.
* @returns {void}
*/
updatePresenceLabel(id) {
const isConnectionMessageVisible
= $('#remoteConnectionMessage').is(':visible');
if (isConnectionMessageVisible) {
this.removePresenceLabel();
return;
}
const presenceLabelContainer = $('#remotePresenceMessage');
if (presenceLabelContainer.length) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
2018-06-26 22:56:22 +00:00
<PresenceLabel
participantID = { id }
2018-07-09 22:18:09 +00:00
className = 'presence-label' />
</I18nextProvider>
</Provider>,
presenceLabelContainer.get(0));
}
}
/**
* Removes the messages about the displayed participant's presence status.
*
* @returns {void}
*/
removePresenceLabel() {
const presenceLabelContainer = $('#remotePresenceMessage');
if (presenceLabelContainer.length) {
ReactDOM.unmountComponentAtNode(presenceLabelContainer.get(0));
}
}
/**
* Show or hide watermark.
* @param {boolean} show
*/
showWatermark(show) {
$('.watermark').css('visibility', show ? 'visible' : 'hidden');
}
/**
* Shows hides the "avatar" message which is to be displayed either in
* the middle of the screen or below the avatar image.
*
* @param {boolean|undefined} [show=undefined] <tt>true</tt> to show
* the avatar message or <tt>false</tt> to hide it. If not provided then
* the connection status of the user currently on the large video will be
* obtained form "APP.conference" and the message will be displayed if
* the user's connection is either interrupted or inactive.
*/
showRemoteConnectionMessage(show) {
if (typeof show !== 'boolean') {
const participant = getParticipantById(APP.store.getState(), this.id);
const connStatus = participant?.connectionStatus;
// eslint-disable-next-line no-param-reassign
show = !APP.conference.isLocalId(this.id)
&& (connStatus === JitsiParticipantConnectionStatus.INTERRUPTED
|| connStatus
=== JitsiParticipantConnectionStatus.INACTIVE);
}
if (show) {
$('#remoteConnectionMessage').css({ display: 'block' });
} else {
$('#remoteConnectionMessage').hide();
}
}
/**
* Updates the text which describes that the remote user is having
* connectivity issues.
*
* @param {string} msgKey the translation key which will be used to get
* the message text.
* @param {object} msgOptions translation options object.
*
* @private
*/
_setRemoteConnectionMessage(msgKey, msgOptions) {
if (msgKey) {
$('#remoteConnectionMessage')
.attr('data-i18n', msgKey)
.attr('data-i18n-options', JSON.stringify(msgOptions));
APP.translation.translateElement(
$('#remoteConnectionMessage'), msgOptions);
}
}
2016-01-15 14:59:35 +00:00
/**
* Add container of specified type.
* @param {string} type container type
* @param {LargeContainer} container container to add.
*/
addContainer(type, container) {
2015-12-25 16:55:45 +00:00
if (this.containers[type]) {
throw new Error(`container of type ${type} already exist`);
}
this.containers[type] = container;
this.resizeContainer(type);
}
2016-01-15 14:59:35 +00:00
/**
* Get Large container of specified type.
* @param {string} type container type.
* @returns {LargeContainer}
*/
getContainer(type) {
const container = this.containers[type];
2015-12-25 16:55:45 +00:00
if (!container) {
throw new Error(`container of type ${type} doesn't exist`);
}
return container;
}
/**
* Returns {@link LargeContainer} for the current {@link state}
*
* @return {LargeContainer}
*
* @throws an <tt>Error</tt> if there is no container for the current
* {@link state}.
*/
getCurrentContainer() {
return this.getContainer(this.state);
}
/**
* Returns type of the current {@link LargeContainer}
* @return {string}
*/
getCurrentContainerType() {
return this.state;
}
2016-01-15 14:59:35 +00:00
/**
* Remove Large container of specified type.
* @param {string} type container type.
*/
removeContainer(type) {
2015-12-25 16:55:45 +00:00
if (!this.containers[type]) {
throw new Error(`container of type ${type} doesn't exist`);
}
delete this.containers[type];
}
2016-01-15 14:59:35 +00:00
/**
* Show Large container of specified type.
* Does nothing if such container is already visible.
* @param {string} type container type.
* @returns {Promise}
*/
showContainer(type) {
2015-12-25 16:55:45 +00:00
if (this.state === type) {
return Promise.resolve();
}
const oldContainer = this.containers[this.state];
// FIXME when video is being replaced with other content we need to hide
// companion icons/messages. It would be best if the container would
// be taking care of it by itself, but that is a bigger refactoring
if (LargeVideoManager.isVideoContainer(this.state)) {
this.showWatermark(false);
this.showRemoteConnectionMessage(false);
}
oldContainer.hide();
2015-12-25 16:55:45 +00:00
this.state = type;
const container = this.getContainer(type);
2015-12-25 16:55:45 +00:00
return container.show().then(() => {
if (LargeVideoManager.isVideoContainer(type)) {
// FIXME when video appears on top of other content we need to
// show companion icons/messages. It would be best if
// the container would be taking care of it by itself, but that
// is a bigger refactoring
this.showWatermark(true);
// "avatar" and "video connection" can not be displayed both
// at the same time, but the latter is of higher priority and it
// will hide the avatar one if will be displayed.
this.showRemoteConnectionMessage(/* fetch the current state */);
}
});
2015-12-25 16:55:45 +00:00
}
/**
* Changes the flipX state of the local video.
* @param val {boolean} true if flipped.
*/
onLocalFlipXChange(val) {
this.videoContainer.setLocalFlipX(val);
}
/**
2019-11-14 13:23:45 +00:00
* Dispatches an action to update the known resolution state of the large video and adjusts container sizes when the
* resolution changes.
*
* @private
* @returns {void}
*/
2017-05-03 16:47:59 +00:00
_onVideoResolutionUpdate() {
const { height, width } = this.videoContainer.getStreamSize();
feat(quality-slider): initial implementation (#1817) * feat(quality-slider): initial implementation - Add new menu button with an Inline Dialog slider for selecting received video quality. - Place P2P status in redux store for the Inline Dialog to display a warning about not respecting video quality selection. - Respond to data channel open events by setting receive video quality. This is for lonely call cases where a setting is set before the data channel is open. - Remove dropdown menu from video status label and clean up related js and css. * first pass at addressing feedback - Move VideoStatusLabel to video-quality directory. - Rename VideoStatusLabel to VideoQualityLabel. - Open VideoQualitydialog from VideoQualityLabel. - New CSS for making VideoQualityLabel display properly. - Do not render VideoQualityLabel in filmstrip only instead of hiding with css. - Remove tooltip from VideoQualityLabel. - Show LD, SD, HD labels in VideoQualityLabel. - Remove action SET_LARGE_VIDEO_HD_STATUS from conference. - Create new action UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION in large-video. - Move VideoQualityButton into video-quality directory. - General renaming (medium -> standard, menu -> dialog). - Render P2P message between title and slider. - Add padding to slider for displacement caused by P2P message's new placement. - Fix display issue with VideoQualityButton displaying out of line in the primary toolbar. * second pass at addressing feedback - Fix p2p inline message color - Force labels to break on words - Resolve rebase issues, including only dispatching quality update on change. Before there was double calling of dispatch produced by an IE11 workaround. This breaks now when setting audio only mode to true twice. - Rename some instances of quality to definition * rename to data channel opened * do not show p2p in audio only * stop toggle audio only icon automatically * remove fixme about toolbar button * find closest resolution for label * toggle dialog on button click * redo last commit for both button and label
2017-08-09 19:40:03 +00:00
const { resolution } = APP.store.getState()['features/large-video'];
feat(quality-slider): initial implementation (#1817) * feat(quality-slider): initial implementation - Add new menu button with an Inline Dialog slider for selecting received video quality. - Place P2P status in redux store for the Inline Dialog to display a warning about not respecting video quality selection. - Respond to data channel open events by setting receive video quality. This is for lonely call cases where a setting is set before the data channel is open. - Remove dropdown menu from video status label and clean up related js and css. * first pass at addressing feedback - Move VideoStatusLabel to video-quality directory. - Rename VideoStatusLabel to VideoQualityLabel. - Open VideoQualitydialog from VideoQualityLabel. - New CSS for making VideoQualityLabel display properly. - Do not render VideoQualityLabel in filmstrip only instead of hiding with css. - Remove tooltip from VideoQualityLabel. - Show LD, SD, HD labels in VideoQualityLabel. - Remove action SET_LARGE_VIDEO_HD_STATUS from conference. - Create new action UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION in large-video. - Move VideoQualityButton into video-quality directory. - General renaming (medium -> standard, menu -> dialog). - Render P2P message between title and slider. - Add padding to slider for displacement caused by P2P message's new placement. - Fix display issue with VideoQualityButton displaying out of line in the primary toolbar. * second pass at addressing feedback - Fix p2p inline message color - Force labels to break on words - Resolve rebase issues, including only dispatching quality update on change. Before there was double calling of dispatch produced by an IE11 workaround. This breaks now when setting audio only mode to true twice. - Rename some instances of quality to definition * rename to data channel opened * do not show p2p in audio only * stop toggle audio only icon automatically * remove fixme about toolbar button * find closest resolution for label * toggle dialog on button click * redo last commit for both button and label
2017-08-09 19:40:03 +00:00
if (height !== resolution) {
APP.store.dispatch(updateKnownLargeVideoResolution(height));
}
2019-11-14 13:23:45 +00:00
const currentAspectRatio = height === 0 ? 0 : width / height;
2017-05-03 16:47:59 +00:00
if (this._videoAspectRatio !== currentAspectRatio) {
this._videoAspectRatio = currentAspectRatio;
this.resize();
}
}
2015-12-25 16:55:45 +00:00
}