Compare commits

...

2 Commits

Author SHA1 Message Date
Hristo Terezov ffeb88677a fix(thumbnails): es6 support & cleanup. 2019-12-13 14:20:25 +00:00
Hristo Terezov b2ddf55d44 feat(load-test): Initial implementation. 2019-11-28 13:20:49 +00:00
13 changed files with 9107 additions and 1799 deletions

View File

@ -699,10 +699,6 @@ UI.showExtensionInlineInstallationDialog = function(callback) {
});
};
UI.updateDevicesAvailability = function(id, devices) {
VideoLayout.setDeviceAvailabilityIcons(id, devices);
};
/**
* Show shared video.
* @param {string} id the id of the sender of the command

View File

@ -7,7 +7,15 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
*
*/
export default function SharedVideoThumb(participant, videoType, VideoLayout) {
export default class SharedVideoThumb extends SmallVideo {
/**
*
* @param {*} participant
* @param {*} videoType
* @param {*} VideoLayout
*/
constructor(participant, videoType, VideoLayout) {
super(VideoLayout);
this.id = participant.id;
this.url = participant.id;
@ -17,25 +25,22 @@ export default function SharedVideoThumb(participant, videoType, VideoLayout) {
this.$container = $(this.container);
this.bindHoverHandler();
SmallVideo.call(this, VideoLayout);
this.isVideoMuted = true;
this.updateDisplayName();
this.container.onclick = this._onContainerClick;
}
SharedVideoThumb.prototype = Object.create(SmallVideo.prototype);
SharedVideoThumb.prototype.constructor = SharedVideoThumb;
}
/**
* hide display name
/**
*
*/
// eslint-disable-next-line no-empty-function
SharedVideoThumb.prototype.setDeviceAvailabilityIcons = function() {};
initializeAvatar() {} // eslint-disable-line no-empty-function
// eslint-disable-next-line no-empty-function
SharedVideoThumb.prototype.initializeAvatar = function() {};
SharedVideoThumb.prototype.createContainer = function(spanId) {
/**
*
* @param {*} spanId
*/
createContainer(spanId) {
const container = document.createElement('span');
container.id = spanId;
@ -61,14 +66,14 @@ SharedVideoThumb.prototype.createContainer = function(spanId) {
remoteVideosContainer.insertBefore(container, localVideoContainer);
return container;
};
}
/**
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
SharedVideoThumb.prototype.updateDisplayName = function() {
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId
} does not exist`);
@ -80,4 +85,5 @@ SharedVideoThumb.prototype.updateDisplayName = function() {
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
};
}
}

View File

@ -64,11 +64,9 @@ const Filmstrip = {
return this._calculateThumbnailSizeForTileView();
}
const availableSizes = this.calculateAvailableSize();
const width = availableSizes.availableWidth;
const height = availableSizes.availableHeight;
const { availableWidth, availableHeight } = this.calculateAvailableSize();
return this.calculateThumbnailSizeFromAvailable(width, height);
return this.calculateThumbnailSizeFromAvailable(availableWidth, availableHeight);
},
/**
@ -80,8 +78,7 @@ const Filmstrip = {
calculateAvailableSize() {
const state = APP.store.getState();
const currentLayout = getCurrentLayout(state);
const isHorizontalFilmstripView
= currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
const isHorizontalFilmstripView = currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
/**
* If the videoAreaAvailableWidth is set we use this one to calculate
@ -100,7 +97,6 @@ const Filmstrip = {
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
let availableWidth = videoAreaAvailableWidth;
const thumbs = this.getThumbs(true);
// If local thumb is not hidden
@ -149,15 +145,11 @@ const Filmstrip = {
);
}
const maxHeight
// If the MAX_HEIGHT property hasn't been specified
// we have the static value.
= Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
availableHeight);
const maxHeight = Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120, availableHeight);
availableHeight
= Math.min(maxHeight, window.innerHeight - 18);
availableHeight = Math.min(maxHeight, window.innerHeight - 18);
return {
availableHeight,
@ -239,13 +231,10 @@ const Filmstrip = {
* availableHeight/h > availableWidth/totalWidth otherwise 2) is true
*/
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 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;
@ -333,18 +322,12 @@ const Filmstrip = {
if (shouldDisplayTileView(state)) {
// The size of the side margins for each tile as set in CSS.
const sideMargins = 10 * 2;
const {
columns,
rows
} = getTileViewGridDimensions(state, getMaxColumnCount());
const { columns, rows } = getTileViewGridDimensions(state, getMaxColumnCount());
const hasOverflow = rows > columns;
// Width is set so that the flex layout can automatically wrap
// tiles onto new rows.
this.filmstripRemoteVideos.css({
width: (local.thumbWidth * columns) + (columns * sideMargins)
});
this.filmstripRemoteVideos.css({ width: (local.thumbWidth * columns) + (columns * sideMargins) });
this.filmstripRemoteVideos.toggleClass('has-overflow', hasOverflow);
} else {
this.filmstripRemoteVideos.css('width', '');

View File

@ -20,7 +20,15 @@ import SmallVideo from './SmallVideo';
/**
*
*/
function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
export default class LocalVideo extends SmallVideo {
/**
*
* @param {*} VideoLayout
* @param {*} emitter
* @param {*} streamEndedCallback
*/
constructor(VideoLayout, emitter, streamEndedCallback) {
super(VideoLayout);
this.videoSpanId = 'localVideoContainer';
this.streamEndedCallback = streamEndedCallback;
this.container = this.createContainer();
@ -44,8 +52,6 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
});
this.initBrowserSpecificProperties();
SmallVideo.call(this, VideoLayout);
// Set default display name.
this.updateDisplayName();
@ -58,12 +64,12 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
this.updateIndicators();
this.container.onclick = this._onContainerClick;
}
}
LocalVideo.prototype = Object.create(SmallVideo.prototype);
LocalVideo.prototype.constructor = LocalVideo;
LocalVideo.prototype.createContainer = function() {
/**
*
*/
createContainer() {
const containerSpan = document.createElement('span');
containerSpan.classList.add('videocontainer');
@ -79,14 +85,14 @@ LocalVideo.prototype.createContainer = function() {
<div class = 'avatar-container'></div>`;
return containerSpan;
};
}
/**
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
LocalVideo.prototype.updateDisplayName = function() {
updateDisplayName() {
if (!this.container) {
logger.warn(
`Unable to set displayName - ${this.videoSpanId
@ -101,13 +107,15 @@ LocalVideo.prototype.updateDisplayName = function() {
elementID: 'localDisplayName',
participantID: this.id
});
};
}
LocalVideo.prototype.changeVideo = function(stream) {
/**
*
* @param {*} stream
*/
changeVideo(stream) {
this.videoStream = stream;
this.localVideoId = `localVideo_${stream.getId()}`;
this._updateVideoElement();
// eslint-disable-next-line eqeqeq
@ -134,27 +142,26 @@ LocalVideo.prototype.changeVideo = function(stream) {
};
stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};
}
/**
/**
* Notify any subscribers of the local video stream ending.
*
* @private
* @returns {void}
*/
LocalVideo.prototype._notifyOfStreamEnded = function() {
_notifyOfStreamEnded() {
if (this.streamEndedCallback) {
this.streamEndedCallback(this.id);
}
};
}
/**
/**
* Shows or hides the local video container.
* @param {boolean} true to make the local video container visible, false
* otherwise
*/
LocalVideo.prototype.setVisible = function(visible) {
setVisible(visible) {
// We toggle the hidden class as an indication to other interested parties
// that this container has been hidden on purpose.
this.$container.toggleClass('hidden');
@ -167,13 +174,13 @@ LocalVideo.prototype.setVisible = function(visible) {
} else {
this.$container.hide();
}
};
};
/**
/**
* Sets the flipX state of the video.
* @param val {boolean} true for flipped otherwise false;
*/
LocalVideo.prototype.setFlipX = function(val) {
setFlipX(val) {
this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
if (!this.localVideoId) {
return;
@ -183,12 +190,12 @@ LocalVideo.prototype.setFlipX = function(val) {
} else {
this.selectVideoElement().removeClass('flipVideoX');
}
};
}
/**
/**
* Builds the context menu for the local video.
*/
LocalVideo.prototype._buildContextMenu = function() {
_buildContextMenu() {
$.contextMenu({
selector: `#${this.videoSpanId}`,
zIndex: 10000,
@ -215,28 +222,27 @@ LocalVideo.prototype._buildContextMenu = function() {
}
}
});
};
}
/**
/**
* Enables or disables the context menu for the local video.
* @param enable {boolean} true for enable, false for disable
*/
LocalVideo.prototype._enableDisableContextMenu = function(enable) {
_enableDisableContextMenu(enable) {
if (this.$container.contextMenu) {
this.$container.contextMenu(enable);
}
};
}
/**
/**
* Places the {@code LocalVideo} in the DOM based on the current video layout.
*
* @returns {void}
*/
LocalVideo.prototype.updateDOMLocation = function() {
updateDOMLocation() {
if (!this.container) {
return;
}
if (this.container.parentElement) {
this.container.parentElement.removeChild(this.container);
}
@ -246,15 +252,14 @@ LocalVideo.prototype.updateDOMLocation = function() {
: document.getElementById('filmstripLocalVideoThumbnail');
appendTarget && appendTarget.appendChild(this.container);
this._updateVideoElement();
};
}
/**
/**
* Renders the React Element for displaying video in {@code LocalVideo}.
*
*/
LocalVideo.prototype._updateVideoElement = function() {
_updateVideoElement() {
const localVideoContainer = document.getElementById('localVideoWrapper');
const videoTrack
= getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
@ -274,6 +279,5 @@ LocalVideo.prototype._updateVideoElement = function() {
const video = this.container.querySelector('video');
video && !config.testing?.noAutoPlayVideo && video.play();
};
export default LocalVideo;
}
}

View File

@ -7,7 +7,6 @@ import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next';
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import { createThumbnailOffsetParentIsNullEvent, sendAnalytics } from '../../../react/features/analytics';
import { i18next } from '../../../react/features/base/i18n';
import {
JitsiParticipantConnectionStatus
@ -34,24 +33,56 @@ import SmallVideo from './SmallVideo';
import UIUtils from '../util/UIUtil';
/**
*
* @param {*} spanId
*/
function createContainer(spanId) {
const container = document.createElement('span');
container.id = spanId;
container.className = 'videocontainer';
container.innerHTML = `
<div class = 'videocontainer__background'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>
<div class ='presence-label-container'></div>
<span class = 'remotevideomenu'></span>`;
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
= document.getElementById('localVideoTileViewContainer');
remoteVideosContainer.insertBefore(container, localVideoContainer);
return container;
}
/**
*
*/
export default class RemoteVideo extends SmallVideo {
/**
* Creates new instance of the <tt>RemoteVideo</tt>.
* @param user {JitsiParticipant} the user for whom remote video instance will
* be created.
* @param {VideoLayout} VideoLayout the video layout instance.
* @param {EventEmitter} emitter the event emitter which will be used by
* the new instance to emit events.
* @constructor
*/
function RemoteVideo(user, VideoLayout, emitter) {
constructor(user, VideoLayout) {
super(VideoLayout);
this.user = user;
this.id = user.getId();
this.emitter = emitter;
this.videoSpanId = `participant_${this.id}`;
SmallVideo.call(this, VideoLayout);
this._audioStreamElement = null;
this._supportsRemoteControl = false;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
? 'left bottom' : 'top center';
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
this.addRemoteVideoContainer();
this.updateIndicators();
this.updateDisplayName();
@ -90,50 +121,41 @@ function RemoteVideo(user, VideoLayout, emitter) {
this._stopRemoteControl = this._stopRemoteControl.bind(this);
this.container.onclick = this._onContainerClick;
}
}
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
RemoteVideo.prototype.constructor = RemoteVideo;
RemoteVideo.prototype.addRemoteVideoContainer = function() {
this.container = RemoteVideo.createContainer(this.videoSpanId);
/**
*
*/
addRemoteVideoContainer() {
this.container = createContainer(this.videoSpanId);
this.$container = $(this.container);
this.initBrowserSpecificProperties();
this.updateRemoteVideoMenu();
this.VideoLayout.resizeThumbnails(true);
this.addAudioLevelIndicator();
this.addPresenceLabel();
return this.container;
};
}
/**
/**
* Checks whether current video is considered hovered. Currently it is hovered
* if the mouse is over the video, or if the connection indicator or the popup
* menu is shown(hovered).
* @private
* NOTE: extends SmallVideo's method
*/
RemoteVideo.prototype._isHovered = function() {
const isHovered = SmallVideo.prototype._isHovered.call(this)
|| this.popupMenuIsHovered;
_isHovered() {
return super._isHovered() || this.popupMenuIsHovered;
}
return isHovered;
};
/**
/**
* Generates the popup menu content.
*
* @returns {Element|*} the constructed element, containing popup menu items
* @private
*/
RemoteVideo.prototype._generatePopupContent = function() {
_generatePopupContent() {
if (interfaceConfig.filmStripOnly) {
return;
}
@ -163,15 +185,13 @@ RemoteVideo.prototype._generatePopupContent = function() {
}
}
const initialVolumeValue
= this._audioStreamElement && this._audioStreamElement.volume;
const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
// hide volume when in silent mode
const onVolumeChange = APP.store.getState()['features/base/config'].startSilent
? undefined : this._setAudioVolume;
const onVolumeChange
= APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
const { isModerator } = APP.conference;
const participantID = this.id;
const currentLayout = getCurrentLayout(APP.store.getState());
let remoteMenuPosition;
@ -202,58 +222,58 @@ RemoteVideo.prototype._generatePopupContent = function() {
</I18nextProvider>
</Provider>,
remoteVideoMenuContainer);
};
}
RemoteVideo.prototype._onRemoteVideoMenuDisplay = function() {
/**
*
*/
_onRemoteVideoMenuDisplay() {
this.updateRemoteVideoMenu();
};
}
/**
/**
* Sets the remote control active status for the remote video.
*
* @param {boolean} isActive - The new remote control active status.
* @returns {void}
*/
RemoteVideo.prototype.setRemoteControlActiveStatus = function(isActive) {
setRemoteControlActiveStatus(isActive) {
this._isRemoteControlSessionActive = isActive;
this.updateRemoteVideoMenu();
};
}
/**
/**
* Sets the remote control supported value and initializes or updates the menu
* depending on the remote control is supported or not.
* @param {boolean} isSupported
*/
RemoteVideo.prototype.setRemoteControlSupport = function(isSupported = false) {
setRemoteControlSupport(isSupported = false) {
if (this._supportsRemoteControl === isSupported) {
return;
}
this._supportsRemoteControl = isSupported;
this.updateRemoteVideoMenu();
};
}
/**
/**
* Requests permissions for remote control session.
*/
RemoteVideo.prototype._requestRemoteControlPermissions = function() {
APP.remoteControl.controller.requestPermissions(
this.id, this.VideoLayout.getLargeVideoWrapper()).then(result => {
_requestRemoteControlPermissions() {
APP.remoteControl.controller.requestPermissions(this.id, this.VideoLayout.getLargeVideoWrapper())
.then(result => {
if (result === null) {
return;
}
this.updateRemoteVideoMenu();
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
result === false ? 'dialog.remoteControlDeniedMessage'
: 'dialog.remoteControlAllowedMessage',
{ user: this.user.getDisplayName()
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
result === false ? 'dialog.remoteControlDeniedMessage' : 'dialog.remoteControlAllowedMessage',
{ user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
);
if (result === true) {
// the remote control permissions has been granted
// pin the controlled participant
const pinnedParticipant
= getPinnedParticipant(APP.store.getState()) || {};
const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
const pinnedId = pinnedParticipant.id;
if (pinnedId !== this.id) {
@ -266,65 +286,62 @@ RemoteVideo.prototype._requestRemoteControlPermissions = function() {
APP.UI.messageHandler.notify(
'dialog.remoteControlTitle',
'dialog.remoteControlErrorMessage',
{ user: this.user.getDisplayName()
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
{ user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
);
});
this.updateRemoteVideoMenu();
};
}
/**
/**
* Stops remote control session.
*/
RemoteVideo.prototype._stopRemoteControl = function() {
_stopRemoteControl() {
// send message about stopping
APP.remoteControl.controller.stop();
this.updateRemoteVideoMenu();
};
}
/**
/**
* Change the remote participant's volume level.
*
* @param {int} newVal - The value to set the slider to.
*/
RemoteVideo.prototype._setAudioVolume = function(newVal) {
_setAudioVolume(newVal) {
if (this._audioStreamElement) {
this._audioStreamElement.volume = newVal;
}
};
}
/**
/**
* Updates the remote video menu.
*
* @param isMuted the new muted state to update to
*/
RemoteVideo.prototype.updateRemoteVideoMenu = function(isMuted) {
updateRemoteVideoMenu(isMuted) {
if (typeof isMuted !== 'undefined') {
this.isAudioMuted = isMuted;
}
this._generatePopupContent();
};
}
/**
/**
* @inheritDoc
* @override
*/
RemoteVideo.prototype.setVideoMutedView = function(isMuted) {
SmallVideo.prototype.setVideoMutedView.call(this, isMuted);
setVideoMutedView(isMuted) {
super.setVideoMutedView(isMuted);
// Update 'mutedWhileDisconnected' flag
this._figureOutMutedWhileDisconnected();
};
}
/**
/**
* Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
* account remote participant's network connectivity and video muted status.
*
* @private
*/
RemoteVideo.prototype._figureOutMutedWhileDisconnected = function() {
_figureOutMutedWhileDisconnected() {
const isActive = this.isConnectionActive();
if (!isActive && this.isVideoMuted) {
@ -332,55 +349,50 @@ RemoteVideo.prototype._figureOutMutedWhileDisconnected = function() {
} else if (isActive && !this.isVideoMuted) {
this.mutedWhileDisconnected = false;
}
};
}
/**
/**
* Removes the remote stream element corresponding to the given stream and
* parent container.
*
* @param stream the MediaStream
* @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one.
*/
RemoteVideo.prototype.removeRemoteStreamElement = function(stream) {
removeRemoteStreamElement(stream) {
if (!this.container) {
return false;
}
const isVideo = stream.isVideoTrack();
const elementID = SmallVideo.getStreamElementID(stream);
const select = $(`#${elementID}`);
select.remove();
if (isVideo) {
this.wasVideoPlayed = false;
}
logger.info(`${isVideo ? 'Video' : 'Audio'
} removed ${this.id}`, select);
logger.info(`${isVideo ? 'Video' : 'Audio'} removed ${this.id}`, select);
if (stream === this.videoStream) {
this.videoStream = null;
}
this.updateView();
};
}
/**
/**
* Checks whether the remote user associated with this <tt>RemoteVideo</tt>
* has connectivity issues.
*
* @return {boolean} <tt>true</tt> if the user's connection is fine or
* <tt>false</tt> otherwise.
*/
RemoteVideo.prototype.isConnectionActive = function() {
return this.user.getConnectionStatus()
=== JitsiParticipantConnectionStatus.ACTIVE;
};
isConnectionActive() {
return this.user.getConnectionStatus() === JitsiParticipantConnectionStatus.ACTIVE;
}
/**
/**
* The remote video is considered "playable" once the stream has started
* according to the {@link #hasVideoStarted} result.
* It will be allowed to display video also in
@ -393,34 +405,31 @@ RemoteVideo.prototype.isConnectionActive = function() {
* @inheritdoc
* @override
*/
RemoteVideo.prototype.isVideoPlayable = function() {
const connectionState
= APP.conference.getParticipantConnectionStatus(this.id);
isVideoPlayable() {
const connectionState = APP.conference.getParticipantConnectionStatus(this.id);
return SmallVideo.prototype.isVideoPlayable.call(this)
return super.isVideoPlayable()
&& this.hasVideoStarted()
&& (connectionState === JitsiParticipantConnectionStatus.ACTIVE
|| (connectionState === JitsiParticipantConnectionStatus.INTERRUPTED
&& !this.mutedWhileDisconnected));
};
|| (connectionState === JitsiParticipantConnectionStatus.INTERRUPTED && !this.mutedWhileDisconnected));
}
/**
/**
* @inheritDoc
*/
RemoteVideo.prototype.updateView = function() {
updateView() {
this.$container.toggleClass('audio-only', APP.conference.isAudioOnly());
this.updateConnectionStatusIndicator();
// This must be called after 'updateConnectionStatusIndicator' because it
// affects the display mode by modifying 'mutedWhileDisconnected' flag
SmallVideo.prototype.updateView.call(this);
};
super.updateView();
}
/**
/**
* Updates the UI to reflect user's connectivity status.
*/
RemoteVideo.prototype.updateConnectionStatusIndicator = function() {
updateConnectionStatusIndicator() {
const connectionStatus = this.user.getConnectionStatus();
logger.debug(`${this.id} thumbnail connection status: ${connectionStatus}`);
@ -430,29 +439,29 @@ RemoteVideo.prototype.updateConnectionStatusIndicator = function() {
this._figureOutMutedWhileDisconnected();
this.updateConnectionStatus(connectionStatus);
const isInterrupted
= connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED;
const isInterrupted = connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED;
// Toggle thumbnail video problem filter
this.selectVideoElement().toggleClass(
'videoThumbnailProblemFilter', isInterrupted);
this.$avatar().toggleClass(
'videoThumbnailProblemFilter', isInterrupted);
};
this.selectVideoElement().toggleClass('videoThumbnailProblemFilter', isInterrupted);
this.$avatar().toggleClass('videoThumbnailProblemFilter', isInterrupted);
}
/**
/**
* Removes RemoteVideo from the page.
*/
RemoteVideo.prototype.remove = function() {
SmallVideo.prototype.remove.call(this);
remove() {
super.remove();
this.removePresenceLabel();
this.removeRemoteVideoMenu();
};
RemoteVideo.prototype.waitForPlayback = function(streamElement, stream) {
}
/**
*
* @param {*} streamElement
* @param {*} stream
*/
waitForPlayback(streamElement, stream) {
const webRtcStream = stream.getOriginalStream();
const isVideo = stream.isVideoTrack();
@ -460,32 +469,31 @@ RemoteVideo.prototype.waitForPlayback = function(streamElement, stream) {
return;
}
const self = this;
// Triggers when video playback starts
const onPlayingHandler = function() {
self.wasVideoPlayed = true;
self.VideoLayout.remoteVideoActive(streamElement, self.id);
streamElement.onplaying = () => {
this.wasVideoPlayed = true;
this.VideoLayout.remoteVideoActive(streamElement, this.id);
streamElement.onplaying = null;
// Refresh to show the video
self.updateView();
this.updateView();
};
}
streamElement.onplaying = onPlayingHandler;
};
/**
/**
* Checks whether the video stream has started for this RemoteVideo instance.
*
* @returns {boolean} true if this RemoteVideo has a video stream for which
* the playback has been started.
*/
RemoteVideo.prototype.hasVideoStarted = function() {
hasVideoStarted() {
return this.wasVideoPlayed;
};
}
RemoteVideo.prototype.addRemoteStreamElement = function(stream) {
/**
*
* @param {*} stream
*/
addRemoteStreamElement(stream) {
if (!this.container) {
logger.debug('Not attaching remote stream due to no container');
@ -516,30 +524,6 @@ RemoteVideo.prototype.addRemoteStreamElement = function(stream) {
this.waitForPlayback(streamElement, stream);
stream.attach(streamElement);
// TODO: Remove once we verify that this.container.offsetParent === null was the reason for not attached video
// streams to the thumbnail.
if (isVideo && this.container.offsetParent === null) {
sendAnalytics(createThumbnailOffsetParentIsNullEvent(this.id));
const parentNodesDisplayProps = [
'#filmstripRemoteVideosContainer',
'#filmstripRemoteVideos',
'#remoteVideos',
'.filmstrip',
'#videospace',
'#videoconference_page',
'#react'
].map(selector => `${selector} - ${$(selector).css('display')}`);
const videoConferencePageParent = $('#videoconference_page').parent();
const reactDiv = document.getElementById('react');
parentNodesDisplayProps.push(
`${videoConferencePageParent.attr('class')} - ${videoConferencePageParent.css('display')}`);
parentNodesDisplayProps.push(`this.container - ${this.$container.css('display')}`);
logger.debug(`this.container.offsetParent is null [user: ${this.id}, ${
parentNodesDisplayProps.join(', ')}, #react.offsetParent - ${
reactDiv && reactDiv.offsetParent !== null ? 'not null' : 'null'}]`);
}
if (!isVideo) {
this._audioStreamElement = streamElement;
@ -548,17 +532,16 @@ RemoteVideo.prototype.addRemoteStreamElement = function(stream) {
// slider.
this.updateRemoteVideoMenu();
}
};
}
/**
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
RemoteVideo.prototype.updateDisplayName = function() {
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId
} does not exist`);
logger.warn(`Unable to set displayName - ${this.videoSpanId} does not exist`);
return;
}
@ -567,32 +550,31 @@ RemoteVideo.prototype.updateDisplayName = function() {
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
};
}
/**
/**
* Removes remote video menu element from video element identified by
* given <tt>videoElementId</tt>.
*
* @param videoElementId the id of local or remote video element.
*/
RemoteVideo.prototype.removeRemoteVideoMenu = function() {
removeRemoteVideoMenu() {
const menuSpan = this.$container.find('.remotevideomenu');
if (menuSpan.length) {
ReactDOM.unmountComponentAtNode(menuSpan.get(0));
menuSpan.remove();
}
};
}
/**
/**
* Mounts the {@code PresenceLabel} for displaying the participant's current
* presence status.
*
* @return {void}
*/
RemoteVideo.prototype.addPresenceLabel = function() {
const presenceLabelContainer
= this.container.querySelector('.presence-label-container');
addPresenceLabel() {
const presenceLabelContainer = this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.render(
@ -605,46 +587,18 @@ RemoteVideo.prototype.addPresenceLabel = function() {
</Provider>,
presenceLabelContainer);
}
};
}
/**
/**
* Unmounts the {@code PresenceLabel} component.
*
* @return {void}
*/
RemoteVideo.prototype.removePresenceLabel = function() {
const presenceLabelContainer
= this.container.querySelector('.presence-label-container');
removePresenceLabel() {
const presenceLabelContainer = this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
}
};
RemoteVideo.createContainer = function(spanId) {
const container = document.createElement('span');
container.id = spanId;
container.className = 'videocontainer';
container.innerHTML = `
<div class = 'videocontainer__background'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>
<div class ='presence-label-container'></div>
<span class = 'remotevideomenu'></span>`;
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
= document.getElementById('localVideoTileViewContainer');
remoteVideosContainer.insertBefore(container, localVideoContainer);
return container;
};
export default RemoteVideo;
}
}

View File

@ -8,17 +8,14 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { AudioLevelIndicator }
from '../../../react/features/audio-level-indicator';
import { AudioLevelIndicator } from '../../../react/features/audio-level-indicator';
import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
import {
getParticipantCount,
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
import {
ConnectionIndicator
} from '../../../react/features/connection-indicator';
import { ConnectionIndicator } from '../../../react/features/connection-indicator';
import { DisplayName } from '../../../react/features/display-name';
import {
AudioMutedIndicator,
@ -79,10 +76,15 @@ const DISPLAY_VIDEO_WITH_NAME = 3;
*/
const DISPLAY_AVATAR_WITH_NAME = 4;
/**
*
*/
export default class SmallVideo {
/**
* Constructor.
*/
function SmallVideo(VideoLayout) {
constructor(VideoLayout) {
this._isModerator = false;
this.isAudioMuted = false;
this.hasAvatar = false;
@ -117,8 +119,7 @@ function SmallVideo(VideoLayout) {
* @private
* @type {boolean}
*/
this._showConnectionIndicator
= !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
this._showConnectionIndicator = !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
/**
* Whether or not the dominant speaker indicator should be displayed.
@ -141,57 +142,55 @@ function SmallVideo(VideoLayout) {
this.updateView = this.updateView.bind(this);
this._onContainerClick = this._onContainerClick.bind(this);
}
}
/**
/**
* Returns the identifier of this small video.
*
* @returns the identifier of this small video
*/
SmallVideo.prototype.getId = function() {
getId() {
return this.id;
};
}
/* Indicates if this small video is currently visible.
/**
* Indicates if this small video is currently visible.
*
* @return <tt>true</tt> if this small video isn't currently visible and
* <tt>false</tt> - otherwise.
*/
SmallVideo.prototype.isVisible = function() {
isVisible() {
return this.$container.is(':visible');
};
}
/**
/**
* Sets the type of the video displayed by this instance.
* Note that this is a string without clearly defined or checked values, and
* it is NOT one of the strings defined in service/RTC/VideoType in
* lib-jitsi-meet.
* @param videoType 'camera' or 'desktop', or 'sharedvideo'.
*/
SmallVideo.prototype.setVideoType = function(videoType) {
setVideoType(videoType) {
this.videoType = videoType;
};
}
/**
/**
* Returns the type of the video displayed by this instance.
* Note that this is a string without clearly defined or checked values, and
* it is NOT one of the strings defined in service/RTC/VideoType in
* lib-jitsi-meet.
* @returns {String} 'camera', 'screen', 'sharedvideo', or undefined.
*/
SmallVideo.prototype.getVideoType = function() {
getVideoType() {
return this.videoType;
};
}
/**
/**
* Creates an audio or video element for a particular MediaStream.
*/
SmallVideo.createStreamElement = function(stream) {
static createStreamElement(stream) {
const isVideo = stream.isVideoTrack();
const element = isVideo
? document.createElement('video')
: document.createElement('audio');
const element = isVideo ? document.createElement('video') : document.createElement('audio');
if (isVideo) {
element.setAttribute('muted', 'true');
@ -203,21 +202,19 @@ SmallVideo.createStreamElement = function(stream) {
element.id = SmallVideo.getStreamElementID(stream);
return element;
};
}
/**
/**
* Returns the element id for a particular MediaStream.
*/
SmallVideo.getStreamElementID = function(stream) {
const isVideo = stream.isVideoTrack();
static getStreamElementID(stream) {
return (stream.isVideoTrack() ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
}
return (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
};
/**
/**
* Configures hoverIn/hoverOut handlers. Depends on connection indicator.
*/
SmallVideo.prototype.bindHoverHandler = function() {
bindHoverHandler() {
// Add hover handler
this.$container.hover(
() => {
@ -231,63 +228,60 @@ SmallVideo.prototype.bindHoverHandler = function() {
this.updateIndicators();
}
);
};
}
/**
/**
* Unmounts the ConnectionIndicator component.
* @returns {void}
*/
SmallVideo.prototype.removeConnectionIndicator = function() {
removeConnectionIndicator() {
this._showConnectionIndicator = false;
this.updateIndicators();
};
}
/**
/**
* Updates the connectionStatus stat which displays in the ConnectionIndicator.
* @returns {void}
*/
SmallVideo.prototype.updateConnectionStatus = function(connectionStatus) {
updateConnectionStatus(connectionStatus) {
this._connectionStatus = connectionStatus;
this.updateIndicators();
};
}
/**
/**
* Shows / hides the audio muted indicator over small videos.
*
* @param {boolean} isMuted indicates if the muted element should be shown
* or hidden
*/
SmallVideo.prototype.showAudioIndicator = function(isMuted) {
showAudioIndicator(isMuted) {
this.isAudioMuted = isMuted;
this.updateStatusBar();
};
}
/**
/**
* Shows video muted indicator over small videos and disables/enables avatar
* if video muted.
*
* @param {boolean} isMuted indicates if we should set the view to muted view
* or not
*/
SmallVideo.prototype.setVideoMutedView = function(isMuted) {
setVideoMutedView(isMuted) {
this.isVideoMuted = isMuted;
this.updateView();
this.updateStatusBar();
};
}
/**
/**
* Create or updates the ReactElement for displaying status indicators about
* audio mute, video mute, and moderator status.
*
* @returns {void}
*/
SmallVideo.prototype.updateStatusBar = function() {
const statusBarContainer
= this.container.querySelector('.videocontainer__toolbar');
updateStatusBar() {
const statusBarContainer = this.container.querySelector('.videocontainer__toolbar');
if (!statusBarContainer) {
return;
@ -322,22 +316,22 @@ SmallVideo.prototype.updateStatusBar = function() {
</div>
</I18nextProvider>,
statusBarContainer);
};
}
/**
/**
* Adds the element indicating the moderator(owner) of the conference.
*/
SmallVideo.prototype.addModeratorIndicator = function() {
addModeratorIndicator() {
this._isModerator = true;
this.updateStatusBar();
};
}
/**
/**
* Adds the element indicating the audio level of the participant.
*
* @returns {void}
*/
SmallVideo.prototype.addAudioLevelIndicator = function() {
addAudioLevelIndicator() {
let audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
@ -347,59 +341,55 @@ SmallVideo.prototype.addAudioLevelIndicator = function() {
audioLevelContainer = document.createElement('span');
audioLevelContainer.className = 'audioindicator-container';
this.container.appendChild(audioLevelContainer);
this.updateAudioLevelIndicator();
};
}
/**
/**
* Removes the element indicating the audio level of the participant.
*
* @returns {void}
*/
SmallVideo.prototype.removeAudioLevelIndicator = function() {
removeAudioLevelIndicator() {
const audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
ReactDOM.unmountComponentAtNode(audioLevelContainer);
}
};
}
/**
/**
* Updates the audio level for this small video.
*
* @param lvl the new audio level to set
* @returns {void}
*/
SmallVideo.prototype.updateAudioLevelIndicator = function(lvl = 0) {
updateAudioLevelIndicator(lvl = 0) {
const audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
ReactDOM.render(
<AudioLevelIndicator
audioLevel = { lvl }/>,
audioLevelContainer);
ReactDOM.render(<AudioLevelIndicator audioLevel = { lvl }/>, audioLevelContainer);
}
}
};
/**
/**
* Queries the component's DOM for the element that should be the parent to the
* AudioLevelIndicator.
*
* @returns {HTMLElement} The DOM element that holds the AudioLevelIndicator.
*/
SmallVideo.prototype._getAudioLevelContainer = function() {
_getAudioLevelContainer() {
return this.container.querySelector('.audioindicator-container');
};
}
/**
/**
* Removes the element indicating the moderator(owner) of the conference.
*/
SmallVideo.prototype.removeModeratorIndicator = function() {
removeModeratorIndicator() {
this._isModerator = false;
this.updateStatusBar();
};
}
/**
/**
* This is an especially interesting function. A naive reader might think that
* it returns this SmallVideo's "video" element. But it is much more exciting.
* It first finds this video's parent element using jquery, then uses a utility
@ -409,31 +399,31 @@ SmallVideo.prototype.removeModeratorIndicator = function() {
* this function to access the video element via the 0th element of the returned
* array (after checking its length of course!).
*/
SmallVideo.prototype.selectVideoElement = function() {
selectVideoElement() {
return $($(this.container).find('video')[0]);
};
}
/**
/**
* Selects the HTML image element which displays user's avatar.
*
* @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
* element which displays the user's avatar.
*/
SmallVideo.prototype.$avatar = function() {
$avatar() {
return this.$container.find('.avatar-container');
};
}
/**
/**
* Returns the display name element, which appears on the video thumbnail.
*
* @return {jQuery} a jQuery selector pointing to the display name element of
* the video thumbnail
*/
SmallVideo.prototype.$displayName = function() {
$displayName() {
return this.$container.find('.displayNameContainer');
};
}
/**
/**
* Creates or updates the participant's display name that is shown over the
* video preview.
*
@ -441,9 +431,8 @@ SmallVideo.prototype.$displayName = function() {
* {@code DisplayName} component.
* @returns {void}
*/
SmallVideo.prototype._renderDisplayName = function(props) {
const displayNameContainer
= this.container.querySelector('.displayNameContainer');
_renderDisplayName(props) {
const displayNameContainer = this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.render(
@ -454,30 +443,29 @@ SmallVideo.prototype._renderDisplayName = function(props) {
</Provider>,
displayNameContainer);
}
};
}
/**
/**
* Removes the component responsible for showing the participant's display name,
* if its container is present.
*
* @returns {void}
*/
SmallVideo.prototype.removeDisplayName = function() {
const displayNameContainer
= this.container.querySelector('.displayNameContainer');
removeDisplayName() {
const displayNameContainer = this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.unmountComponentAtNode(displayNameContainer);
}
};
}
/**
/**
* Enables / disables the css responsible for focusing/pinning a video
* thumbnail.
*
* @param isFocused indicates if the thumbnail should be focused/pinned or not
*/
SmallVideo.prototype.focus = function(isFocused) {
focus(isFocused) {
const focusedCssClass = 'videoContainerFocused';
const isFocusClassEnabled = this.$container.hasClass(focusedCssClass);
@ -486,42 +474,44 @@ SmallVideo.prototype.focus = function(isFocused) {
} else if (isFocused && !isFocusClassEnabled) {
this.$container.addClass(focusedCssClass);
}
};
}
SmallVideo.prototype.hasVideo = function() {
/**
*
*/
hasVideo() {
return this.selectVideoElement().length !== 0;
};
}
/**
/**
* Checks whether the user associated with this <tt>SmallVideo</tt> is currently
* being displayed on the "large video".
*
* @return {boolean} <tt>true</tt> if the user is displayed on the large video
* or <tt>false</tt> otherwise.
*/
SmallVideo.prototype.isCurrentlyOnLargeVideo = function() {
isCurrentlyOnLargeVideo() {
return this.VideoLayout.isCurrentlyOnLarge(this.id);
};
}
/**
/**
* Checks whether there is a playable video stream available for the user
* associated with this <tt>SmallVideo</tt>.
*
* @return {boolean} <tt>true</tt> if there is a playable video stream available
* or <tt>false</tt> otherwise.
*/
SmallVideo.prototype.isVideoPlayable = function() {
isVideoPlayable() {
return this.videoStream && !this.isVideoMuted && !APP.conference.isAudioOnly();
};
}
/**
/**
* Determines what should be display on the thumbnail.
*
* @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
* or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>.
*/
SmallVideo.prototype.selectDisplayMode = function(input) {
selectDisplayMode(input) {
// Display name is always and only displayed when user is on the stage
if (input.isCurrentlyOnLargeVideo && !input.tileViewEnabled) {
return input.isVideoPlayable && !input.isAudioOnly ? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
@ -532,14 +522,14 @@ SmallVideo.prototype.selectDisplayMode = function(input) {
// check hovering and change state to avatar with name
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
};
}
/**
/**
* Computes information that determine the display mode.
*
* @returns {Object}
*/
SmallVideo.prototype.computeDisplayModeInput = function() {
computeDisplayModeInput() {
return {
isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
isHovered: this._isHovered(),
@ -554,24 +544,24 @@ SmallVideo.prototype.computeDisplayModeInput = function() {
isVideoMuted: this.isVideoMuted,
videoStreamMuted: this.videoStream ? this.videoStream.isMuted() : 'no stream'
};
};
}
/**
/**
* Checks whether current video is considered hovered. Currently it is hovered
* if the mouse is over the video, or if the connection
* indicator is shown(hovered).
* @private
*/
SmallVideo.prototype._isHovered = function() {
_isHovered() {
return this.videoIsHovered || this._popoverIsHovered;
};
}
/**
/**
* Hides or shows the user's avatar.
* This update assumes that large video had been updated and we will
* reflect it on this small video.
*/
SmallVideo.prototype.updateView = function() {
updateView() {
if (this.id) {
// Init / refresh avatar
this.initializeAvatar();
@ -619,15 +609,15 @@ SmallVideo.prototype.updateView = function() {
if (this.displayMode !== oldDisplayMode) {
logger.debug(`Displaying ${displayModeString} for ${this.id}, data: [${JSON.stringify(displayModeInput)}]`);
}
};
}
/**
/**
* Updates the react component displaying the avatar with the passed in avatar
* url.
*
* @returns {void}
*/
SmallVideo.prototype.initializeAvatar = function() {
initializeAvatar() {
const thumbnail = this.$avatar().get(0);
this.hasAvatar = true;
@ -645,27 +635,27 @@ SmallVideo.prototype.initializeAvatar = function() {
thumbnail
);
}
};
}
/**
/**
* Unmounts any attached react components (particular the avatar image) from
* the avatar container.
*
* @returns {void}
*/
SmallVideo.prototype.removeAvatar = function() {
removeAvatar() {
const thumbnail = this.$avatar().get(0);
if (thumbnail) {
ReactDOM.unmountComponentAtNode(thumbnail);
}
};
}
/**
/**
* Shows or hides the dominant speaker indicator.
* @param show whether to show or hide.
*/
SmallVideo.prototype.showDominantSpeakerIndicator = function(show) {
showDominantSpeakerIndicator(show) {
// Don't create and show dominant speaker indicator if
// DISABLE_DOMINANT_SPEAKER_INDICATOR is true
if (interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR) {
@ -673,29 +663,25 @@ SmallVideo.prototype.showDominantSpeakerIndicator = function(show) {
}
if (!this.container) {
logger.warn(`Unable to set dominant speaker indicator - ${
this.videoSpanId} does not exist`);
logger.warn(`Unable to set dominant speaker indicator - ${this.videoSpanId} does not exist`);
return;
}
if (this._showDominantSpeaker === show) {
return;
}
this._showDominantSpeaker = show;
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
this.updateIndicators();
this.updateView();
};
}
/**
/**
* Shows or hides the raised hand indicator.
* @param show whether to show or hide.
*/
SmallVideo.prototype.showRaisedHandIndicator = function(show) {
showRaisedHandIndicator(show) {
if (!this.container) {
logger.warn(`Unable to raised hand indication - ${
this.videoSpanId} does not exist`);
@ -704,16 +690,15 @@ SmallVideo.prototype.showRaisedHandIndicator = function(show) {
}
this._showRaisedHand = show;
this.updateIndicators();
};
}
/**
/**
* Adds a listener for onresize events for this video, which will monitor for
* resolution changes, will calculate the delay since the moment the listened
* is added, and will fire a RESOLUTION_CHANGED event.
*/
SmallVideo.prototype.waitForResolutionChange = function() {
waitForResolutionChange() {
const beforeChange = window.performance.now();
const videos = this.selectVideoElement();
@ -734,18 +719,14 @@ SmallVideo.prototype.waitForResolutionChange = function() {
const emitter = this.VideoLayout.getEventEmitter();
if (emitter) {
emitter.emit(
UIEvents.RESOLUTION_CHANGED,
this.getId(),
`${oldWidth}x${oldHeight}`,
`${video.videoWidth}x${video.videoHeight}`,
delay);
emitter.emit(UIEvents.RESOLUTION_CHANGED, this.getId(), `${oldWidth}x${oldHeight}`,
`${video.videoWidth}x${video.videoHeight}`, delay);
}
}
};
};
}
/**
/**
* Initalizes any browser specific properties. Currently sets the overflow
* property for Qt browsers on Windows to hidden, thus fixing the following
* problem:
@ -757,25 +738,22 @@ SmallVideo.prototype.waitForResolutionChange = function() {
* Setting this property for all browsers will result in broken audio levels,
* which makes this a temporary solution, before reworking audio levels.
*/
SmallVideo.prototype.initBrowserSpecificProperties = function() {
initBrowserSpecificProperties() {
const userAgent = window.navigator.userAgent;
if (userAgent.indexOf('QtWebEngine') > -1
&& (userAgent.indexOf('Windows') > -1
|| userAgent.indexOf('Linux') > -1)) {
&& (userAgent.indexOf('Windows') > -1 || userAgent.indexOf('Linux') > -1)) {
this.$container.css('overflow', 'hidden');
}
};
}
/**
/**
* Cleans up components on {@code SmallVideo} and removes itself from the DOM.
*
* @returns {void}
*/
SmallVideo.prototype.remove = function() {
remove() {
logger.log('Remove thumbnail', this.id);
this.removeAudioLevelIndicator();
const toolbarContainer
@ -786,32 +764,29 @@ SmallVideo.prototype.remove = function() {
}
this.removeConnectionIndicator();
this.removeDisplayName();
this.removeAvatar();
this._unmountIndicators();
// Remove whole container
if (this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
};
}
/**
/**
* Helper function for re-rendering multiple react components of the small
* video.
*
* @returns {void}
*/
SmallVideo.prototype.rerender = function() {
rerender() {
this.updateIndicators();
this.updateStatusBar();
this.updateView();
};
}
/**
/**
* Updates the React element responsible for showing connection status, dominant
* speaker, and raised hand icons. Uses instance variables to get the necessary
* state to display. Will create the React element if not already created.
@ -819,17 +794,15 @@ SmallVideo.prototype.rerender = function() {
* @private
* @returns {void}
*/
SmallVideo.prototype.updateIndicators = function() {
const indicatorToolbar
= this.container.querySelector('.videocontainer__toptoolbar');
updateIndicators() {
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
if (!indicatorToolbar) {
return;
}
const iconSize = UIUtil.getIndicatorFontSize();
const showConnectionIndicator = this.videoIsHovered
|| !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
const showConnectionIndicator = this.videoIsHovered || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
const state = APP.store.getState();
const currentLayout = getCurrentLayout(state);
const participantCount = getParticipantCount(state);
@ -878,9 +851,9 @@ SmallVideo.prototype.updateIndicators = function() {
</Provider>,
indicatorToolbar
);
};
}
/**
/**
* Callback invoked when the thumbnail is clicked and potentially trigger
* pinning of the participant.
*
@ -888,22 +861,21 @@ SmallVideo.prototype.updateIndicators = function() {
* @private
* @returns {void}
*/
SmallVideo.prototype._onContainerClick = function(event) {
_onContainerClick(event) {
const triggerPin = this._shouldTriggerPin(event);
if (event.stopPropagation && triggerPin) {
event.stopPropagation();
event.preventDefault();
}
if (triggerPin) {
this.togglePin();
}
return false;
};
}
/**
/**
* Returns whether or not a click event is targeted at certain elements which
* should not trigger a pin.
*
@ -911,7 +883,7 @@ SmallVideo.prototype._onContainerClick = function(event) {
* @private
* @returns {boolean}
*/
SmallVideo.prototype._shouldTriggerPin = function(event) {
_shouldTriggerPin(event) {
// TODO Checking the classes is a workround to allow events to bubble into
// the DisplayName component if it was clicked. React's synthetic events
// will fire after jQuery handlers execute, so stop propogation at this
@ -923,40 +895,36 @@ SmallVideo.prototype._shouldTriggerPin = function(event) {
return $source.parents('.displayNameContainer').length === 0
&& $source.parents('.popover').length === 0
&& !event.target.classList.contains('popover');
};
}
/**
/**
* Pins the participant displayed by this thumbnail or unpins if already pinned.
*
* @returns {void}
*/
SmallVideo.prototype.togglePin = function() {
const pinnedParticipant
= getPinnedParticipant(APP.store.getState()) || {};
const participantIdToPin
= pinnedParticipant && pinnedParticipant.id === this.id
? null : this.id;
togglePin() {
const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
const participantIdToPin = pinnedParticipant && pinnedParticipant.id === this.id ? null : this.id;
APP.store.dispatch(pinParticipant(participantIdToPin));
};
}
/**
/**
* Removes the React element responsible for showing connection status, dominant
* speaker, and raised hand icons.
*
* @private
* @returns {void}
*/
SmallVideo.prototype._unmountIndicators = function() {
const indicatorToolbar
= this.container.querySelector('.videocontainer__toptoolbar');
_unmountIndicators() {
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
if (indicatorToolbar) {
ReactDOM.unmountComponentAtNode(indicatorToolbar);
}
};
}
/**
/**
* Updates the current state of the connection indicator popover being hovered.
* If hovered, display the small video as if it is hovered.
*
@ -964,10 +932,8 @@ SmallVideo.prototype._unmountIndicators = function() {
* currently over the connection indicator popover.
* @returns {void}
*/
SmallVideo.prototype._onPopoverHover = function(popoverIsHovered) {
_onPopoverHover(popoverIsHovered) {
this._popoverIsHovered = popoverIsHovered;
this.updateView();
};
export default SmallVideo;
}
}

View File

@ -166,27 +166,6 @@ const VideoLayout = {
localVideoThumbnail.updateIndicators();
},
/**
* Adds or removes icons for not available camera and microphone.
* @param resourceJid the jid of user
* @param devices available devices
*/
setDeviceAvailabilityIcons(id, devices) {
if (APP.conference.isLocalId(id)) {
localVideoThumbnail.setDeviceAvailabilityIcons(devices);
return;
}
const video = remoteVideos[id];
if (!video) {
return;
}
video.setDeviceAvailabilityIcons(devices);
},
/**
* Shows/hides local video.
* @param {boolean} true to make the local video visible, false - otherwise
@ -411,27 +390,20 @@ const VideoLayout = {
/**
* Resizes thumbnails.
*/
resizeThumbnails(
forceUpdate = false,
onComplete = null) {
const { localVideo, remoteVideo }
= Filmstrip.calculateThumbnailSize();
resizeThumbnails(forceUpdate = false, onComplete = null) {
const { localVideo, remoteVideo } = Filmstrip.calculateThumbnailSize();
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
if (shouldDisplayTileView(APP.store.getState())) {
const height
= (localVideo && localVideo.thumbHeight)
|| (remoteVideo && remoteVideo.thumbnHeight)
|| 0;
const height = (localVideo && localVideo.thumbHeight) || (remoteVideo && remoteVideo.thumbnHeight) || 0;
const qualityLevel = getNearestReceiverVideoQualityLevel(height);
APP.store.dispatch(setMaxReceiverVideoQuality(qualityLevel));
}
localVideoThumbnail && localVideoThumbnail.rerender();
Object.values(remoteVideos).forEach(
remoteVideoThumbnail => remoteVideoThumbnail.rerender());
Object.values(remoteVideos).forEach(remoteVideoThumbnail => remoteVideoThumbnail.rerender());
if (onComplete && typeof onComplete === 'function') {
onComplete();

View File

@ -692,21 +692,6 @@ export function createSyncTrackStateEvent(mediaType, muted) {
};
}
/**
* Creates an event that indicates the thumbnail offset parent is null.
*
* @param {string} id - The id of the user related to the thumbnail.
* @returns {Object} The event in a format suitable for sending via sendAnalytics.
*/
export function createThumbnailOffsetParentIsNullEvent(id) {
return {
action: 'OffsetParentIsNull',
attributes: {
id
}
};
}
/**
* Creates an event associated with a toolbar button being clicked/pressed. By
* convention, where appropriate an attribute named 'enable' should be used to

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="../../libs/lib-jitsi-meet.min.js?v=139"></script>
<script src="libs/load-test-participant.min.js" ></script>
</head>
<body>
<div>Number of participants: <span id="participants">1</span></div>
</body>
</html>

View File

@ -0,0 +1,261 @@
/* global $, JitsiMeetJS */
import 'jquery';
import parseURLParams from '../../react/features/base/config/parseURLParams';
const params = parseURLParams(window.location, false, 'hash');
const { isHuman = false } = params;
const {
roomName = 'loadtest0',
localAudio = isHuman,
localVideo = isHuman,
remoteVideo = isHuman,
remoteAudio = isHuman
} = params;
const options = {
hosts: {
domain: 'george-perf.jitsi.net',
muc: 'conference.george-perf.jitsi.net'
},
bosh: '//george-perf.jitsi.net/http-bind',
// The name of client node advertised in XEP-0115 'c' stanza
clientNode: 'http://jitsi.org/jitsimeet'
};
const confOptions = {
openBridgeChannel: 'websocket',
testing: {
testMode: true,
noAutoPlayVideo: true
},
disableNS: true,
disableAEC: true,
gatherStats: true,
callStatsID: false
};
let connection = null;
let isJoined = false;
let room = null;
let numParticipants = 1;
let localTracks = [];
const remoteTracks = {};
window.APP = {
get room() {
return room;
},
get connection() {
return connection;
},
get numParticipants() {
return numParticipants;
},
get localTracks() {
return localTracks;
},
get remoteTracks() {
return remoteTracks;
},
get params() {
return {
roomName,
localAudio,
localVideo,
remoteVideo,
remoteAudio
};
}
};
/**
*
*/
function setNumberOfParticipants() {
$('#participants').text(numParticipants);
}
/**
* Handles local tracks.
* @param tracks Array with JitsiTrack objects
*/
function onLocalTracks(tracks = []) {
localTracks = tracks;
for (let i = 0; i < localTracks.length; i++) {
if (localTracks[i].getType() === 'video') {
$('body').append(`<video autoplay='1' id='localVideo${i}' />`);
localTracks[i].attach($(`#localVideo${i}`)[0]);
} else {
$('body').append(
`<audio autoplay='1' muted='true' id='localAudio${i}' />`);
localTracks[i].attach($(`#localAudio${i}`)[0]);
}
if (isJoined) {
room.addTrack(localTracks[i]);
}
}
}
/**
* Handles remote tracks
* @param track JitsiTrack object
*/
function onRemoteTrack(track) {
if (track.isLocal()
|| (track.getType() === 'video' && !remoteVideo) || (track.getType() === 'audio' && !remoteAudio)) {
return;
}
const participant = track.getParticipantId();
if (!remoteTracks[participant]) {
remoteTracks[participant] = [];
}
const idx = remoteTracks[participant].push(track);
const id = participant + track.getType() + idx;
if (track.getType() === 'video') {
$('body').append(`<video autoplay='1' id='${id}' />`);
} else {
$('body').append(`<audio autoplay='1' id='${id}' />`);
}
track.attach($(`#${id}`)[0]);
}
/**
* That function is executed when the conference is joined
*/
function onConferenceJoined() {
isJoined = true;
for (let i = 0; i < localTracks.length; i++) {
room.addTrack(localTracks[i]);
}
}
/**
*
* @param id
*/
function onUserLeft(id) {
numParticipants--;
setNumberOfParticipants();
if (!remoteTracks[id]) {
return;
}
const tracks = remoteTracks[id];
for (let i = 0; i < tracks.length; i++) {
const container = $(`#${id}${tracks[i].getType()}${i + 1}`)[0];
if (container) {
tracks[i].detach(container);
container.parentElement.removeChild(container);
}
}
}
/**
* That function is called when connection is established successfully
*/
function onConnectionSuccess() {
room = connection.initJitsiConference(roomName, confOptions);
room.on(JitsiMeetJS.events.conference.TRACK_ADDED, onRemoteTrack);
room.on(JitsiMeetJS.events.conference.CONFERENCE_JOINED, onConferenceJoined);
room.on(JitsiMeetJS.events.conference.USER_JOINED, id => {
numParticipants++;
setNumberOfParticipants();
remoteTracks[id] = [];
});
room.on(JitsiMeetJS.events.conference.USER_LEFT, onUserLeft);
room.join();
}
/**
* This function is called when the connection fail.
*/
function onConnectionFailed() {
console.error('Connection Failed!');
}
/**
* This function is called when we disconnect.
*/
function disconnect() {
console.log('disconnect!');
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED,
onConnectionSuccess);
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_FAILED,
onConnectionFailed);
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED,
disconnect);
}
/**
*
*/
function unload() {
for (let i = 0; i < localTracks.length; i++) {
localTracks[i].dispose();
}
room.leave();
connection.disconnect();
}
$(window).bind('beforeunload', unload);
$(window).bind('unload', unload);
JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
const initOptions = {
disableAudioLevels: true,
// The ID of the jidesha extension for Chrome.
desktopSharingChromeExtId: 'mbocklcggfhnbahlnepmldehdhpjfcjp',
// Whether desktop sharing should be disabled on Chrome.
desktopSharingChromeDisabled: true,
// The media sources to use when using screen sharing with the Chrome
// extension.
desktopSharingChromeSources: [ 'screen', 'window' ],
// Required version of Chrome extension
desktopSharingChromeMinExtVersion: '0.1',
// Whether desktop sharing should be disabled on Firefox.
desktopSharingFirefoxDisabled: true
};
JitsiMeetJS.init(initOptions);
connection = new JitsiMeetJS.JitsiConnection(null, null, options);
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED, onConnectionSuccess);
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_FAILED, onConnectionFailed);
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED, disconnect);
connection.connect();
const devices = [];
if (localVideo) {
devices.push('video');
}
if (localAudio) {
devices.push('audio');
}
if (devices.length > 0) {
JitsiMeetJS.createLocalTracks({ devices })
.then(onLocalTracks)
.catch(error => {
throw error;
});
}

6983
static/load-test/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
{
"name": "jitsi-meet-load-test",
"version": "0.0.0",
"description": "A load test participant",
"repository": {
"type": "git",
"url": "git://github.com/jitsi/jitsi-meet"
},
"keywords": [
"jingle",
"webrtc",
"xmpp",
"browser"
],
"author": "",
"readmeFilename": "../README.md",
"dependencies": {
"jquery": "3.4.0"
},
"devDependencies": {
"@babel/core": "7.5.5",
"@babel/plugin-proposal-class-properties": "7.1.0",
"@babel/plugin-proposal-export-default-from": "7.0.0",
"@babel/plugin-proposal-export-namespace-from": "7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
"@babel/plugin-proposal-optional-chaining": "7.2.0",
"@babel/plugin-transform-flow-strip-types": "7.0.0",
"@babel/preset-env": "7.1.0",
"@babel/preset-flow": "7.0.0",
"@babel/runtime": "7.5.5",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.4",
"eslint": "5.6.1",
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#1.0.1",
"eslint-plugin-flowtype": "2.50.3",
"eslint-plugin-import": "2.14.0",
"eslint-plugin-jsdoc": "3.8.0",
"expose-loader": "0.7.5",
"flow-bin": "0.104.0",
"imports-loader": "0.7.1",
"string-replace-loader": "2.1.1",
"style-loader": "0.19.0",
"webpack": "4.27.1",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.1.2"
},
"engines": {
"node": ">=8.0.0",
"npm": ">=6.0.0"
},
"license": "Apache-2.0",
"scripts": {
"build": "webpack -p"
}
}

View File

@ -0,0 +1,131 @@
/* global __dirname */
const process = require('process');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const analyzeBundle = process.argv.indexOf('--analyze-bundle') !== -1;
const minimize
= process.argv.indexOf('-p') !== -1
|| process.argv.indexOf('--optimize-minimize') !== -1;
/**
* Build a Performance configuration object for the given size.
* See: https://webpack.js.org/configuration/performance/
*/
function getPerformanceHints(size) {
return {
hints: minimize ? 'error' : false,
maxAssetSize: size,
maxEntrypointSize: size
};
}
// The base Webpack configuration to bundle the JavaScript artifacts of
// jitsi-meet such as app.bundle.js and external_api.js.
const config = {
devtool: 'source-map',
mode: minimize ? 'production' : 'development',
module: {
rules: [ {
// Transpile ES2015 (aka ES6) to ES5. Accept the JSX syntax by React
// as well.
exclude: [
new RegExp(`${__dirname}/node_modules/(?!js-utils)`)
],
loader: 'babel-loader',
options: {
// XXX The require.resolve bellow solves failures to locate the
// presets when lib-jitsi-meet, for example, is npm linked in
// jitsi-meet.
plugins: [
require.resolve('@babel/plugin-transform-flow-strip-types'),
require.resolve('@babel/plugin-proposal-class-properties'),
require.resolve('@babel/plugin-proposal-export-default-from'),
require.resolve('@babel/plugin-proposal-export-namespace-from'),
require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'),
require.resolve('@babel/plugin-proposal-optional-chaining')
],
presets: [
[
require.resolve('@babel/preset-env'),
// Tell babel to avoid compiling imports into CommonJS
// so that webpack may do tree shaking.
{
modules: false,
// Specify our target browsers so no transpiling is
// done unnecessarily. For browsers not specified
// here, the ES2015+ profile will be used.
targets: {
chrome: 58,
electron: 2,
firefox: 54,
safari: 11
}
}
],
require.resolve('@babel/preset-flow'),
require.resolve('@babel/preset-react')
]
},
test: /\.jsx?$/
}, {
// Expose jquery as the globals $ and jQuery because it is expected
// to be available in such a form by multiple jitsi-meet
// dependencies including lib-jitsi-meet.
loader: 'expose-loader?$!expose-loader?jQuery',
test: /\/node_modules\/jquery\/.*\.js$/
}]
},
node: {
// Allow the use of the real filename of the module being executed. By
// default Webpack does not leak path-related information and provides a
// value that is a mock (/index.js).
__filename: true
},
optimization: {
concatenateModules: minimize,
minimize
},
output: {
filename: `[name]${minimize ? '.min' : ''}.js`,
path: `${__dirname}/libs`,
publicPath: 'load-test/libs/',
sourceMapFilename: `[name].${minimize ? 'min' : 'js'}.map`
},
plugins: [
analyzeBundle
&& new BundleAnalyzerPlugin({
analyzerMode: 'disabled',
generateStatsFile: true
})
].filter(Boolean),
resolve: {
alias: {
jquery: `jquery/dist/jquery${minimize ? '.min' : ''}.js`
},
aliasFields: [
'browser'
],
extensions: [
'.web.js',
// Webpack defaults:
'.js',
'.json'
]
}
};
module.exports = [
Object.assign({}, config, {
entry: {
'load-test-participant': './load-test-participant.js'
},
performance: getPerformanceHints(3 * 1024 * 1024)
})
];