diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index 893969b4a..eb884ca98 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -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
diff --git a/modules/UI/shared_video/SharedVideoThumb.js b/modules/UI/shared_video/SharedVideoThumb.js
index 2c9aa53b0..8f8bedfb1 100644
--- a/modules/UI/shared_video/SharedVideoThumb.js
+++ b/modules/UI/shared_video/SharedVideoThumb.js
@@ -7,77 +7,83 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
*
*/
-export default function SharedVideoThumb(participant, videoType, VideoLayout) {
- this.id = participant.id;
+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;
- this.setVideoType(videoType);
- this.videoSpanId = 'sharedVideoContainer';
- this.container = this.createContainer(this.videoSpanId);
- this.$container = $(this.container);
+ this.url = participant.id;
+ this.setVideoType(videoType);
+ this.videoSpanId = 'sharedVideoContainer';
+ this.container = this.createContainer(this.videoSpanId);
+ this.$container = $(this.container);
- this.bindHoverHandler();
- SmallVideo.call(this, VideoLayout);
- this.isVideoMuted = true;
- this.updateDisplayName();
+ this.bindHoverHandler();
+ 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() {};
-
-// eslint-disable-next-line no-empty-function
-SharedVideoThumb.prototype.initializeAvatar = function() {};
-
-SharedVideoThumb.prototype.createContainer = function(spanId) {
- const container = document.createElement('span');
-
- container.id = spanId;
- container.className = 'videocontainer';
-
- // add the avatar
- const avatar = document.createElement('img');
-
- avatar.className = 'sharedVideoAvatar';
- avatar.src = `https://img.youtube.com/vi/${this.url}/0.jpg`;
- container.appendChild(avatar);
-
- const displayNameContainer = document.createElement('div');
-
- displayNameContainer.className = 'displayNameContainer';
- container.appendChild(displayNameContainer);
-
- const remoteVideosContainer
- = document.getElementById('filmstripRemoteVideosContainer');
- const localVideoContainer
- = document.getElementById('localVideoTileViewContainer');
-
- remoteVideosContainer.insertBefore(container, localVideoContainer);
-
- return container;
-};
-
-/**
- * Triggers re-rendering of the display name using current instance state.
- *
- * @returns {void}
- */
-SharedVideoThumb.prototype.updateDisplayName = function() {
- if (!this.container) {
- logger.warn(`Unable to set displayName - ${this.videoSpanId
- } does not exist`);
-
- return;
+ this.container.onclick = this._onContainerClick;
}
- this._renderDisplayName({
- elementID: `${this.videoSpanId}_name`,
- participantID: this.id
- });
-};
+ /**
+ *
+ */
+ initializeAvatar() {} // eslint-disable-line no-empty-function
+
+ /**
+ *
+ * @param {*} spanId
+ */
+ createContainer(spanId) {
+ const container = document.createElement('span');
+
+ container.id = spanId;
+ container.className = 'videocontainer';
+
+ // add the avatar
+ const avatar = document.createElement('img');
+
+ avatar.className = 'sharedVideoAvatar';
+ avatar.src = `https://img.youtube.com/vi/${this.url}/0.jpg`;
+ container.appendChild(avatar);
+
+ const displayNameContainer = document.createElement('div');
+
+ displayNameContainer.className = 'displayNameContainer';
+ container.appendChild(displayNameContainer);
+
+ const remoteVideosContainer
+ = document.getElementById('filmstripRemoteVideosContainer');
+ const localVideoContainer
+ = document.getElementById('localVideoTileViewContainer');
+
+ remoteVideosContainer.insertBefore(container, localVideoContainer);
+
+ return container;
+ }
+
+ /**
+ * Triggers re-rendering of the display name using current instance state.
+ *
+ * @returns {void}
+ */
+ updateDisplayName() {
+ if (!this.container) {
+ logger.warn(`Unable to set displayName - ${this.videoSpanId
+ } does not exist`);
+
+ return;
+ }
+
+ this._renderDisplayName({
+ elementID: `${this.videoSpanId}_name`,
+ participantID: this.id
+ });
+ }
+}
diff --git a/modules/UI/videolayout/Filmstrip.js b/modules/UI/videolayout/Filmstrip.js
index 264295862..71ff2c735 100644
--- a/modules/UI/videolayout/Filmstrip.js
+++ b/modules/UI/videolayout/Filmstrip.js
@@ -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.
+ const maxHeight = Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120, availableHeight);
- // If the MAX_HEIGHT property hasn't been specified
- // we have the static value.
- = 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', '');
diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js
index 0234b8a02..43dc26e6a 100644
--- a/modules/UI/videolayout/LocalVideo.js
+++ b/modules/UI/videolayout/LocalVideo.js
@@ -20,260 +20,264 @@ import SmallVideo from './SmallVideo';
/**
*
*/
-function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
- this.videoSpanId = 'localVideoContainer';
- this.streamEndedCallback = streamEndedCallback;
- this.container = this.createContainer();
- this.$container = $(this.container);
- this.updateDOMLocation();
+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();
+ this.$container = $(this.container);
+ this.updateDOMLocation();
- this.localVideoId = null;
- this.bindHoverHandler();
- if (!config.disableLocalVideoFlip) {
- this._buildContextMenu();
- }
- this.isLocal = true;
- this.emitter = emitter;
- this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
- ? 'left top' : 'top center';
-
- Object.defineProperty(this, 'id', {
- get() {
- return APP.conference.getMyUserId();
+ this.localVideoId = null;
+ this.bindHoverHandler();
+ if (!config.disableLocalVideoFlip) {
+ this._buildContextMenu();
}
- });
- this.initBrowserSpecificProperties();
+ this.isLocal = true;
+ this.emitter = emitter;
+ this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
+ ? 'left top' : 'top center';
- SmallVideo.call(this, VideoLayout);
+ Object.defineProperty(this, 'id', {
+ get() {
+ return APP.conference.getMyUserId();
+ }
+ });
+ this.initBrowserSpecificProperties();
- // Set default display name.
- this.updateDisplayName();
+ // Set default display name.
+ this.updateDisplayName();
- // Initialize the avatar display with an avatar url selected from the redux
- // state. Redux stores the local user with a hardcoded participant id of
- // 'local' if no id has been assigned yet.
- this.initializeAvatar();
+ // Initialize the avatar display with an avatar url selected from the redux
+ // state. Redux stores the local user with a hardcoded participant id of
+ // 'local' if no id has been assigned yet.
+ this.initializeAvatar();
- this.addAudioLevelIndicator();
- this.updateIndicators();
+ this.addAudioLevelIndicator();
+ this.updateIndicators();
- this.container.onclick = this._onContainerClick;
-}
-
-LocalVideo.prototype = Object.create(SmallVideo.prototype);
-LocalVideo.prototype.constructor = LocalVideo;
-
-LocalVideo.prototype.createContainer = function() {
- const containerSpan = document.createElement('span');
-
- containerSpan.classList.add('videocontainer');
- containerSpan.id = this.videoSpanId;
-
- containerSpan.innerHTML = `
-
-
-
-
-
-
- `;
-
- return containerSpan;
-};
-
-/**
- * Triggers re-rendering of the display name using current instance state.
- *
- * @returns {void}
- */
-LocalVideo.prototype.updateDisplayName = function() {
- if (!this.container) {
- logger.warn(
- `Unable to set displayName - ${this.videoSpanId
- } does not exist`);
-
- return;
+ this.container.onclick = this._onContainerClick;
}
- this._renderDisplayName({
- allowEditing: APP.store.getState()['features/base/jwt'].isGuest,
- displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
- elementID: 'localDisplayName',
- participantID: this.id
- });
-};
+ /**
+ *
+ */
+ createContainer() {
+ const containerSpan = document.createElement('span');
-LocalVideo.prototype.changeVideo = function(stream) {
- this.videoStream = stream;
+ containerSpan.classList.add('videocontainer');
+ containerSpan.id = this.videoSpanId;
- this.localVideoId = `localVideo_${stream.getId()}`;
+ containerSpan.innerHTML = `
+
+
+
+
+
+
+ `;
- this._updateVideoElement();
+ return containerSpan;
+ }
- // eslint-disable-next-line eqeqeq
- const isVideo = stream.videoType != 'desktop';
- const settings = APP.store.getState()['features/base/settings'];
+ /**
+ * Triggers re-rendering of the display name using current instance state.
+ *
+ * @returns {void}
+ */
+ updateDisplayName() {
+ if (!this.container) {
+ logger.warn(
+ `Unable to set displayName - ${this.videoSpanId
+ } does not exist`);
- this._enableDisableContextMenu(isVideo);
- this.setFlipX(isVideo ? settings.localFlipX : false);
-
- const endedHandler = () => {
- const localVideoContainer
- = document.getElementById('localVideoWrapper');
-
- // Only remove if there is no video and not a transition state.
- // Previous non-react logic created a new video element with each track
- // removal whereas react reuses the video component so it could be the
- // stream ended but a new one is being used.
- if (localVideoContainer && this.videoStream.isEnded()) {
- ReactDOM.unmountComponentAtNode(localVideoContainer);
+ return;
}
- this._notifyOfStreamEnded();
- stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
- };
-
- stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
-};
-
-/**
- * Notify any subscribers of the local video stream ending.
- *
- * @private
- * @returns {void}
- */
-LocalVideo.prototype._notifyOfStreamEnded = function() {
- if (this.streamEndedCallback) {
- this.streamEndedCallback(this.id);
+ this._renderDisplayName({
+ allowEditing: APP.store.getState()['features/base/jwt'].isGuest,
+ displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
+ elementID: 'localDisplayName',
+ participantID: 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) {
+ /**
+ *
+ * @param {*} stream
+ */
+ changeVideo(stream) {
+ this.videoStream = stream;
+ this.localVideoId = `localVideo_${stream.getId()}`;
+ this._updateVideoElement();
- // We toggle the hidden class as an indication to other interested parties
- // that this container has been hidden on purpose.
- this.$container.toggleClass('hidden');
+ // eslint-disable-next-line eqeqeq
+ const isVideo = stream.videoType != 'desktop';
+ const settings = APP.store.getState()['features/base/settings'];
- // We still show/hide it as we need to overwrite the style property if we
- // want our action to take effect. Toggling the display property through
- // the above css class didn't succeed in overwriting the style.
- if (visible) {
- this.$container.show();
- } else {
- this.$container.hide();
+ this._enableDisableContextMenu(isVideo);
+ this.setFlipX(isVideo ? settings.localFlipX : false);
+
+ const endedHandler = () => {
+ const localVideoContainer
+ = document.getElementById('localVideoWrapper');
+
+ // Only remove if there is no video and not a transition state.
+ // Previous non-react logic created a new video element with each track
+ // removal whereas react reuses the video component so it could be the
+ // stream ended but a new one is being used.
+ if (localVideoContainer && this.videoStream.isEnded()) {
+ ReactDOM.unmountComponentAtNode(localVideoContainer);
+ }
+
+ this._notifyOfStreamEnded();
+ stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
+ };
+
+ stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
}
-};
-/**
- * Sets the flipX state of the video.
- * @param val {boolean} true for flipped otherwise false;
- */
-LocalVideo.prototype.setFlipX = function(val) {
- this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
- if (!this.localVideoId) {
- return;
+ /**
+ * Notify any subscribers of the local video stream ending.
+ *
+ * @private
+ * @returns {void}
+ */
+ _notifyOfStreamEnded() {
+ if (this.streamEndedCallback) {
+ this.streamEndedCallback(this.id);
+ }
}
- if (val) {
- this.selectVideoElement().addClass('flipVideoX');
- } else {
- this.selectVideoElement().removeClass('flipVideoX');
+
+ /**
+ * Shows or hides the local video container.
+ * @param {boolean} true to make the local video container visible, false
+ * otherwise
+ */
+ 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');
+
+ // We still show/hide it as we need to overwrite the style property if we
+ // want our action to take effect. Toggling the display property through
+ // the above css class didn't succeed in overwriting the style.
+ if (visible) {
+ this.$container.show();
+ } else {
+ this.$container.hide();
+ }
}
-};
-/**
- * Builds the context menu for the local video.
- */
-LocalVideo.prototype._buildContextMenu = function() {
- $.contextMenu({
- selector: `#${this.videoSpanId}`,
- zIndex: 10000,
- items: {
- flip: {
- name: 'Flip',
- callback: () => {
- const { store } = APP;
- const val = !store.getState()['features/base/settings']
- .localFlipX;
+ /**
+ * Sets the flipX state of the video.
+ * @param val {boolean} true for flipped otherwise false;
+ */
+ setFlipX(val) {
+ this.emitter.emit(UIEvents.LOCAL_FLIPX_CHANGED, val);
+ if (!this.localVideoId) {
+ return;
+ }
+ if (val) {
+ this.selectVideoElement().addClass('flipVideoX');
+ } else {
+ this.selectVideoElement().removeClass('flipVideoX');
+ }
+ }
- this.setFlipX(val);
- store.dispatch(updateSettings({
- localFlipX: val
- }));
+ /**
+ * Builds the context menu for the local video.
+ */
+ _buildContextMenu() {
+ $.contextMenu({
+ selector: `#${this.videoSpanId}`,
+ zIndex: 10000,
+ items: {
+ flip: {
+ name: 'Flip',
+ callback: () => {
+ const { store } = APP;
+ const val = !store.getState()['features/base/settings']
+ .localFlipX;
+
+ this.setFlipX(val);
+ store.dispatch(updateSettings({
+ localFlipX: val
+ }));
+ }
+ }
+ },
+ events: {
+ show(options) {
+ options.items.flip.name
+ = APP.translation.generateTranslationHTML(
+ 'videothumbnail.flip');
}
}
- },
- events: {
- show(options) {
- options.items.flip.name
- = APP.translation.generateTranslationHTML(
- 'videothumbnail.flip');
- }
+ });
+ }
+
+ /**
+ * Enables or disables the context menu for the local video.
+ * @param enable {boolean} true for enable, false for disable
+ */
+ _enableDisableContextMenu(enable) {
+ if (this.$container.contextMenu) {
+ this.$container.contextMenu(enable);
}
- });
-};
-
-/**
- * Enables or disables the context menu for the local video.
- * @param enable {boolean} true for enable, false for disable
- */
-LocalVideo.prototype._enableDisableContextMenu = function(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() {
- if (!this.container) {
- return;
}
- if (this.container.parentElement) {
- this.container.parentElement.removeChild(this.container);
+ /**
+ * Places the {@code LocalVideo} in the DOM based on the current video layout.
+ *
+ * @returns {void}
+ */
+ updateDOMLocation() {
+ if (!this.container) {
+ return;
+ }
+ if (this.container.parentElement) {
+ this.container.parentElement.removeChild(this.container);
+ }
+
+ const appendTarget = shouldDisplayTileView(APP.store.getState())
+ ? document.getElementById('localVideoTileViewContainer')
+ : document.getElementById('filmstripLocalVideoThumbnail');
+
+ appendTarget && appendTarget.appendChild(this.container);
+ this._updateVideoElement();
}
- const appendTarget = shouldDisplayTileView(APP.store.getState())
- ? document.getElementById('localVideoTileViewContainer')
- : document.getElementById('filmstripLocalVideoThumbnail');
+ /**
+ * Renders the React Element for displaying video in {@code LocalVideo}.
+ *
+ */
+ _updateVideoElement() {
+ const localVideoContainer = document.getElementById('localVideoWrapper');
+ const videoTrack
+ = getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
- appendTarget && appendTarget.appendChild(this.container);
+ ReactDOM.render(
+
+
+ ,
+ localVideoContainer
+ );
- this._updateVideoElement();
-};
+ // Ensure the video gets play() called on it. This may be necessary in the
+ // case where the local video container was moved and re-attached, in which
+ // case video does not autoplay.
+ const video = this.container.querySelector('video');
-/**
- * Renders the React Element for displaying video in {@code LocalVideo}.
- *
- */
-LocalVideo.prototype._updateVideoElement = function() {
- const localVideoContainer = document.getElementById('localVideoWrapper');
- const videoTrack
- = getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
-
- ReactDOM.render(
-
-
- ,
- localVideoContainer
- );
-
- // Ensure the video gets play() called on it. This may be necessary in the
- // case where the local video container was moved and re-attached, in which
- // case video does not autoplay.
- const video = this.container.querySelector('video');
-
- video && !config.testing?.noAutoPlayVideo && video.play();
-};
-
-export default LocalVideo;
+ video && !config.testing?.noAutoPlayVideo && video.play();
+ }
+}
diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js
index 3f4a197e7..6e28a8294 100644
--- a/modules/UI/videolayout/RemoteVideo.js
+++ b/modules/UI/videolayout/RemoteVideo.js
@@ -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,594 +33,10 @@ import SmallVideo from './SmallVideo';
import UIUtils from '../util/UIUtil';
/**
- * Creates new instance of the RemoteVideo.
- * @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) {
- 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.addRemoteVideoContainer();
- this.updateIndicators();
- this.updateDisplayName();
- this.bindHoverHandler();
- this.flipX = false;
- this.isLocal = false;
- this.popupMenuIsHovered = false;
- this._isRemoteControlSessionActive = false;
-
- /**
- * The flag is set to true after the 'onplay' event has been
- * triggered on the current video element. It goes back to false
- * when the stream is removed. It is used to determine whether the video
- * playback has ever started.
- * @type {boolean}
- */
- this.wasVideoPlayed = false;
-
- /**
- * The flag is set to true if remote participant's video gets muted
- * during his media connection disruption. This is to prevent black video
- * being render on the thumbnail, because even though once the video has
- * been played the image usually remains on the video element it seems that
- * after longer period of the video element being hidden this image can be
- * lost.
- * @type {boolean}
- */
- this.mutedWhileDisconnected = false;
-
- // Bind event handlers so they are only bound once for every instance.
- // TODO The event handlers should be turned into actions so changes can be
- // handled through reducers and middleware.
- this._requestRemoteControlPermissions
- = this._requestRemoteControlPermissions.bind(this);
- this._setAudioVolume = this._setAudioVolume.bind(this);
- 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);
- 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;
-
-
- return isHovered;
-};
-
-/**
- * Generates the popup menu content.
*
- * @returns {Element|*} the constructed element, containing popup menu items
- * @private
+ * @param {*} spanId
*/
-RemoteVideo.prototype._generatePopupContent = function() {
- if (interfaceConfig.filmStripOnly) {
- return;
- }
-
- const remoteVideoMenuContainer
- = this.container.querySelector('.remotevideomenu');
-
- if (!remoteVideoMenuContainer) {
- return;
- }
-
- const { controller } = APP.remoteControl;
- let remoteControlState = null;
- let onRemoteControlToggle;
-
- if (this._supportsRemoteControl
- && ((!APP.remoteControl.active && !this._isRemoteControlSessionActive)
- || APP.remoteControl.controller.activeParticipant === this.id)) {
- if (controller.getRequestedParticipant() === this.id) {
- remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
- } else if (controller.isStarted()) {
- onRemoteControlToggle = this._stopRemoteControl;
- remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
- } else {
- onRemoteControlToggle = this._requestRemoteControlPermissions;
- remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
- }
- }
-
- 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 { isModerator } = APP.conference;
- const participantID = this.id;
-
- const currentLayout = getCurrentLayout(APP.store.getState());
- let remoteMenuPosition;
-
- if (currentLayout === LAYOUTS.TILE_VIEW) {
- remoteMenuPosition = 'left top';
- } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
- remoteMenuPosition = 'left bottom';
- } else {
- remoteMenuPosition = 'top center';
- }
-
- ReactDOM.render(
-
-
-
-
-
-
- ,
- remoteVideoMenuContainer);
-};
-
-RemoteVideo.prototype._onRemoteVideoMenuDisplay = function() {
- 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) {
- 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) {
- 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 => {
- 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 }
- );
- if (result === true) {
- // the remote control permissions has been granted
- // pin the controlled participant
- const pinnedParticipant
- = getPinnedParticipant(APP.store.getState()) || {};
- const pinnedId = pinnedParticipant.id;
-
- if (pinnedId !== this.id) {
- APP.store.dispatch(pinParticipant(this.id));
- }
- }
- }, error => {
- logger.error(error);
- this.updateRemoteVideoMenu();
- APP.UI.messageHandler.notify(
- 'dialog.remoteControlTitle',
- 'dialog.remoteControlErrorMessage',
- { user: this.user.getDisplayName()
- || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
- );
- });
- this.updateRemoteVideoMenu();
-};
-
-/**
- * Stops remote control session.
- */
-RemoteVideo.prototype._stopRemoteControl = function() {
- // 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) {
- 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) {
-
- if (typeof isMuted !== 'undefined') {
- this.isAudioMuted = isMuted;
- }
-
- this._generatePopupContent();
-};
-
-/**
- * @inheritDoc
- * @override
- */
-RemoteVideo.prototype.setVideoMutedView = function(isMuted) {
- SmallVideo.prototype.setVideoMutedView.call(this, 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() {
- const isActive = this.isConnectionActive();
-
- if (!isActive && this.isVideoMuted) {
- this.mutedWhileDisconnected = true;
- } 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 true if given stream is a video one.
- */
-RemoteVideo.prototype.removeRemoteStreamElement = function(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);
-
-
- if (stream === this.videoStream) {
- this.videoStream = null;
- }
-
- this.updateView();
-};
-
-/**
- * Checks whether the remote user associated with this RemoteVideo
- * has connectivity issues.
- *
- * @return {boolean} true if the user's connection is fine or
- * false otherwise.
- */
-RemoteVideo.prototype.isConnectionActive = function() {
- 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
- * {@link JitsiParticipantConnectionStatus.INTERRUPTED} if the video was ever
- * played and was not muted while not in ACTIVE state. This basically means
- * that there is stalled video image cached that could be displayed. It's used
- * to show "grey video image" in user's thumbnail when there are connectivity
- * issues.
- *
- * @inheritdoc
- * @override
- */
-RemoteVideo.prototype.isVideoPlayable = function() {
- const connectionState
- = APP.conference.getParticipantConnectionStatus(this.id);
-
- return SmallVideo.prototype.isVideoPlayable.call(this)
- && this.hasVideoStarted()
- && (connectionState === JitsiParticipantConnectionStatus.ACTIVE
- || (connectionState === JitsiParticipantConnectionStatus.INTERRUPTED
- && !this.mutedWhileDisconnected));
-};
-
-/**
- * @inheritDoc
- */
-RemoteVideo.prototype.updateView = function() {
- 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);
-};
-
-/**
- * Updates the UI to reflect user's connectivity status.
- */
-RemoteVideo.prototype.updateConnectionStatusIndicator = function() {
- const connectionStatus = this.user.getConnectionStatus();
-
- logger.debug(`${this.id} thumbnail connection status: ${connectionStatus}`);
-
- // FIXME rename 'mutedWhileDisconnected' to 'mutedWhileNotRendering'
- // Update 'mutedWhileDisconnected' flag
- this._figureOutMutedWhileDisconnected();
- this.updateConnectionStatus(connectionStatus);
-
- const isInterrupted
- = connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED;
-
- // Toggle thumbnail video problem filter
-
- this.selectVideoElement().toggleClass(
- 'videoThumbnailProblemFilter', isInterrupted);
- this.$avatar().toggleClass(
- 'videoThumbnailProblemFilter', isInterrupted);
-};
-
-/**
- * Removes RemoteVideo from the page.
- */
-RemoteVideo.prototype.remove = function() {
- SmallVideo.prototype.remove.call(this);
-
- this.removePresenceLabel();
- this.removeRemoteVideoMenu();
-};
-
-RemoteVideo.prototype.waitForPlayback = function(streamElement, stream) {
-
- const webRtcStream = stream.getOriginalStream();
- const isVideo = stream.isVideoTrack();
-
- if (!isVideo || webRtcStream.id === 'mixedmslabel') {
- return;
- }
-
- const self = this;
-
- // Triggers when video playback starts
- const onPlayingHandler = function() {
- self.wasVideoPlayed = true;
- self.VideoLayout.remoteVideoActive(streamElement, self.id);
- streamElement.onplaying = null;
-
- // Refresh to show the video
- self.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() {
- return this.wasVideoPlayed;
-};
-
-RemoteVideo.prototype.addRemoteStreamElement = function(stream) {
- if (!this.container) {
- logger.debug('Not attaching remote stream due to no container');
-
- return;
- }
-
- const isVideo = stream.isVideoTrack();
-
- isVideo ? this.videoStream = stream : this.audioStream = stream;
-
- if (isVideo) {
- this.setVideoType(stream.videoType);
- }
-
- if (!stream.getOriginalStream()) {
- logger.debug('Remote video stream has no original stream');
-
- return;
- }
-
- const streamElement = SmallVideo.createStreamElement(stream);
-
- // Put new stream element always in front
- UIUtils.prependChild(this.container, streamElement);
-
- $(streamElement).hide();
-
- 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;
-
- // If the remote video menu was created before the audio stream was
- // attached we need to update the menu in order to show the volume
- // slider.
- this.updateRemoteVideoMenu();
- }
-};
-
-/**
- * Triggers re-rendering of the display name using current instance state.
- *
- * @returns {void}
- */
-RemoteVideo.prototype.updateDisplayName = function() {
- if (!this.container) {
- logger.warn(`Unable to set displayName - ${this.videoSpanId
- } does not exist`);
-
- return;
- }
-
- this._renderDisplayName({
- elementID: `${this.videoSpanId}_name`,
- participantID: this.id
- });
-};
-
-/**
- * Removes remote video menu element from video element identified by
- * given videoElementId.
- *
- * @param videoElementId the id of local or remote video element.
- */
-RemoteVideo.prototype.removeRemoteVideoMenu = function() {
- 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');
-
- if (presenceLabelContainer) {
- ReactDOM.render(
-
-
-
-
- ,
- presenceLabelContainer);
- }
-};
-
-/**
- * Unmounts the {@code PresenceLabel} component.
- *
- * @return {void}
- */
-RemoteVideo.prototype.removePresenceLabel = function() {
- const presenceLabelContainer
- = this.container.querySelector('.presence-label-container');
-
- if (presenceLabelContainer) {
- ReactDOM.unmountComponentAtNode(presenceLabelContainer);
- }
-};
-
-RemoteVideo.createContainer = function(spanId) {
+function createContainer(spanId) {
const container = document.createElement('span');
container.id = spanId;
@@ -645,6 +60,544 @@ RemoteVideo.createContainer = function(spanId) {
remoteVideosContainer.insertBefore(container, localVideoContainer);
return container;
-};
+}
-export default RemoteVideo;
+/**
+ *
+ */
+export default class RemoteVideo extends SmallVideo {
+ /**
+ * Creates new instance of the RemoteVideo.
+ * @param user {JitsiParticipant} the user for whom remote video instance will
+ * be created.
+ * @param {VideoLayout} VideoLayout the video layout instance.
+ * @constructor
+ */
+ constructor(user, VideoLayout) {
+ super(VideoLayout);
+
+ this.user = user;
+ this.id = user.getId();
+ this.videoSpanId = `participant_${this.id}`;
+
+ this._audioStreamElement = null;
+ this._supportsRemoteControl = false;
+ this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
+ this.addRemoteVideoContainer();
+ this.updateIndicators();
+ this.updateDisplayName();
+ this.bindHoverHandler();
+ this.flipX = false;
+ this.isLocal = false;
+ this.popupMenuIsHovered = false;
+ this._isRemoteControlSessionActive = false;
+
+ /**
+ * The flag is set to true after the 'onplay' event has been
+ * triggered on the current video element. It goes back to false
+ * when the stream is removed. It is used to determine whether the video
+ * playback has ever started.
+ * @type {boolean}
+ */
+ this.wasVideoPlayed = false;
+
+ /**
+ * The flag is set to true if remote participant's video gets muted
+ * during his media connection disruption. This is to prevent black video
+ * being render on the thumbnail, because even though once the video has
+ * been played the image usually remains on the video element it seems that
+ * after longer period of the video element being hidden this image can be
+ * lost.
+ * @type {boolean}
+ */
+ this.mutedWhileDisconnected = false;
+
+ // Bind event handlers so they are only bound once for every instance.
+ // TODO The event handlers should be turned into actions so changes can be
+ // handled through reducers and middleware.
+ this._requestRemoteControlPermissions
+ = this._requestRemoteControlPermissions.bind(this);
+ this._setAudioVolume = this._setAudioVolume.bind(this);
+ this._stopRemoteControl = this._stopRemoteControl.bind(this);
+
+ this.container.onclick = this._onContainerClick;
+ }
+
+ /**
+ *
+ */
+ addRemoteVideoContainer() {
+ this.container = createContainer(this.videoSpanId);
+ this.$container = $(this.container);
+ this.initBrowserSpecificProperties();
+ this.updateRemoteVideoMenu();
+ 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
+ */
+ _isHovered() {
+ return super._isHovered() || this.popupMenuIsHovered;
+ }
+
+ /**
+ * Generates the popup menu content.
+ *
+ * @returns {Element|*} the constructed element, containing popup menu items
+ * @private
+ */
+ _generatePopupContent() {
+ if (interfaceConfig.filmStripOnly) {
+ return;
+ }
+
+ const remoteVideoMenuContainer
+ = this.container.querySelector('.remotevideomenu');
+
+ if (!remoteVideoMenuContainer) {
+ return;
+ }
+
+ const { controller } = APP.remoteControl;
+ let remoteControlState = null;
+ let onRemoteControlToggle;
+
+ if (this._supportsRemoteControl
+ && ((!APP.remoteControl.active && !this._isRemoteControlSessionActive)
+ || APP.remoteControl.controller.activeParticipant === this.id)) {
+ if (controller.getRequestedParticipant() === this.id) {
+ remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
+ } else if (controller.isStarted()) {
+ onRemoteControlToggle = this._stopRemoteControl;
+ remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
+ } else {
+ onRemoteControlToggle = this._requestRemoteControlPermissions;
+ remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
+ }
+ }
+
+ 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 { isModerator } = APP.conference;
+ const participantID = this.id;
+ const currentLayout = getCurrentLayout(APP.store.getState());
+ let remoteMenuPosition;
+
+ if (currentLayout === LAYOUTS.TILE_VIEW) {
+ remoteMenuPosition = 'left top';
+ } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
+ remoteMenuPosition = 'left bottom';
+ } else {
+ remoteMenuPosition = 'top center';
+ }
+
+ ReactDOM.render(
+
+
+
+
+
+
+ ,
+ remoteVideoMenuContainer);
+ }
+
+ /**
+ *
+ */
+ _onRemoteVideoMenuDisplay() {
+ this.updateRemoteVideoMenu();
+ }
+
+ /**
+ * Sets the remote control active status for the remote video.
+ *
+ * @param {boolean} isActive - The new remote control active status.
+ * @returns {void}
+ */
+ 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
+ */
+ setRemoteControlSupport(isSupported = false) {
+ if (this._supportsRemoteControl === isSupported) {
+ return;
+ }
+ this._supportsRemoteControl = isSupported;
+ this.updateRemoteVideoMenu();
+ }
+
+ /**
+ * Requests permissions for remote control session.
+ */
+ _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 }
+ );
+ if (result === true) {
+ // the remote control permissions has been granted
+ // pin the controlled participant
+ const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
+ const pinnedId = pinnedParticipant.id;
+
+ if (pinnedId !== this.id) {
+ APP.store.dispatch(pinParticipant(this.id));
+ }
+ }
+ }, error => {
+ logger.error(error);
+ this.updateRemoteVideoMenu();
+ APP.UI.messageHandler.notify(
+ 'dialog.remoteControlTitle',
+ 'dialog.remoteControlErrorMessage',
+ { user: this.user.getDisplayName() || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME }
+ );
+ });
+ this.updateRemoteVideoMenu();
+ }
+
+ /**
+ * Stops remote control session.
+ */
+ _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.
+ */
+ _setAudioVolume(newVal) {
+ if (this._audioStreamElement) {
+ this._audioStreamElement.volume = newVal;
+ }
+ }
+
+ /**
+ * Updates the remote video menu.
+ *
+ * @param isMuted the new muted state to update to
+ */
+ updateRemoteVideoMenu(isMuted) {
+ if (typeof isMuted !== 'undefined') {
+ this.isAudioMuted = isMuted;
+ }
+ this._generatePopupContent();
+ }
+
+ /**
+ * @inheritDoc
+ * @override
+ */
+ 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
+ */
+ _figureOutMutedWhileDisconnected() {
+ const isActive = this.isConnectionActive();
+
+ if (!isActive && this.isVideoMuted) {
+ this.mutedWhileDisconnected = true;
+ } 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 true if given stream is a video one.
+ */
+ 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);
+
+ if (stream === this.videoStream) {
+ this.videoStream = null;
+ }
+
+ this.updateView();
+ }
+
+ /**
+ * Checks whether the remote user associated with this RemoteVideo
+ * has connectivity issues.
+ *
+ * @return {boolean} true if the user's connection is fine or
+ * false otherwise.
+ */
+ 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
+ * {@link JitsiParticipantConnectionStatus.INTERRUPTED} if the video was ever
+ * played and was not muted while not in ACTIVE state. This basically means
+ * that there is stalled video image cached that could be displayed. It's used
+ * to show "grey video image" in user's thumbnail when there are connectivity
+ * issues.
+ *
+ * @inheritdoc
+ * @override
+ */
+ isVideoPlayable() {
+ const connectionState = APP.conference.getParticipantConnectionStatus(this.id);
+
+ return super.isVideoPlayable()
+ && this.hasVideoStarted()
+ && (connectionState === JitsiParticipantConnectionStatus.ACTIVE
+ || (connectionState === JitsiParticipantConnectionStatus.INTERRUPTED && !this.mutedWhileDisconnected));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ 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
+ super.updateView();
+ }
+
+ /**
+ * Updates the UI to reflect user's connectivity status.
+ */
+ updateConnectionStatusIndicator() {
+ const connectionStatus = this.user.getConnectionStatus();
+
+ logger.debug(`${this.id} thumbnail connection status: ${connectionStatus}`);
+
+ // FIXME rename 'mutedWhileDisconnected' to 'mutedWhileNotRendering'
+ // Update 'mutedWhileDisconnected' flag
+ this._figureOutMutedWhileDisconnected();
+ this.updateConnectionStatus(connectionStatus);
+
+ const isInterrupted = connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED;
+
+ // Toggle thumbnail video problem filter
+
+ this.selectVideoElement().toggleClass('videoThumbnailProblemFilter', isInterrupted);
+ this.$avatar().toggleClass('videoThumbnailProblemFilter', isInterrupted);
+ }
+
+ /**
+ * Removes RemoteVideo from the page.
+ */
+ remove() {
+ super.remove();
+ this.removePresenceLabel();
+ this.removeRemoteVideoMenu();
+ }
+
+ /**
+ *
+ * @param {*} streamElement
+ * @param {*} stream
+ */
+ waitForPlayback(streamElement, stream) {
+ const webRtcStream = stream.getOriginalStream();
+ const isVideo = stream.isVideoTrack();
+
+ if (!isVideo || webRtcStream.id === 'mixedmslabel') {
+ return;
+ }
+
+ streamElement.onplaying = () => {
+ this.wasVideoPlayed = true;
+ this.VideoLayout.remoteVideoActive(streamElement, this.id);
+ streamElement.onplaying = null;
+
+ // Refresh to show the video
+ this.updateView();
+ };
+ }
+
+ /**
+ * 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.
+ */
+ hasVideoStarted() {
+ return this.wasVideoPlayed;
+ }
+
+ /**
+ *
+ * @param {*} stream
+ */
+ addRemoteStreamElement(stream) {
+ if (!this.container) {
+ logger.debug('Not attaching remote stream due to no container');
+
+ return;
+ }
+
+ const isVideo = stream.isVideoTrack();
+
+ isVideo ? this.videoStream = stream : this.audioStream = stream;
+
+ if (isVideo) {
+ this.setVideoType(stream.videoType);
+ }
+
+ if (!stream.getOriginalStream()) {
+ logger.debug('Remote video stream has no original stream');
+
+ return;
+ }
+
+ const streamElement = SmallVideo.createStreamElement(stream);
+
+ // Put new stream element always in front
+ UIUtils.prependChild(this.container, streamElement);
+
+ $(streamElement).hide();
+
+ this.waitForPlayback(streamElement, stream);
+ stream.attach(streamElement);
+
+ if (!isVideo) {
+ this._audioStreamElement = streamElement;
+
+ // If the remote video menu was created before the audio stream was
+ // attached we need to update the menu in order to show the volume
+ // slider.
+ this.updateRemoteVideoMenu();
+ }
+ }
+
+ /**
+ * Triggers re-rendering of the display name using current instance state.
+ *
+ * @returns {void}
+ */
+ updateDisplayName() {
+ if (!this.container) {
+ logger.warn(`Unable to set displayName - ${this.videoSpanId} does not exist`);
+
+ return;
+ }
+
+ this._renderDisplayName({
+ elementID: `${this.videoSpanId}_name`,
+ participantID: this.id
+ });
+ }
+
+ /**
+ * Removes remote video menu element from video element identified by
+ * given videoElementId.
+ *
+ * @param videoElementId the id of local or remote video element.
+ */
+ 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}
+ */
+ addPresenceLabel() {
+ const presenceLabelContainer = this.container.querySelector('.presence-label-container');
+
+ if (presenceLabelContainer) {
+ ReactDOM.render(
+
+
+
+
+ ,
+ presenceLabelContainer);
+ }
+ }
+
+ /**
+ * Unmounts the {@code PresenceLabel} component.
+ *
+ * @return {void}
+ */
+ removePresenceLabel() {
+ const presenceLabelContainer = this.container.querySelector('.presence-label-container');
+
+ if (presenceLabelContainer) {
+ ReactDOM.unmountComponentAtNode(presenceLabelContainer);
+ }
+ }
+}
diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js
index 8db3eb0a5..c97ef8998 100644
--- a/modules/UI/videolayout/SmallVideo.js
+++ b/modules/UI/videolayout/SmallVideo.js
@@ -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,895 +76,864 @@ const DISPLAY_VIDEO_WITH_NAME = 3;
*/
const DISPLAY_AVATAR_WITH_NAME = 4;
-/**
- * Constructor.
- */
-function SmallVideo(VideoLayout) {
- this._isModerator = false;
- this.isAudioMuted = false;
- this.hasAvatar = false;
- this.isVideoMuted = false;
- this.videoStream = null;
- this.audioStream = null;
- this.VideoLayout = VideoLayout;
- this.videoIsHovered = false;
-
- /**
- * The current state of the user's bridge connection. The value should be
- * a string as enumerated in the library's participantConnectionStatus
- * constants.
- *
- * @private
- * @type {string|null}
- */
- this._connectionStatus = null;
-
- /**
- * Whether or not the ConnectionIndicator's popover is hovered. Modifies
- * how the video overlays display based on hover state.
- *
- * @private
- * @type {boolean}
- */
- this._popoverIsHovered = false;
-
- /**
- * Whether or not the connection indicator should be displayed.
- *
- * @private
- * @type {boolean}
- */
- this._showConnectionIndicator
- = !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
-
- /**
- * Whether or not the dominant speaker indicator should be displayed.
- *
- * @private
- * @type {boolean}
- */
- this._showDominantSpeaker = false;
-
- /**
- * Whether or not the raised hand indicator should be displayed.
- *
- * @private
- * @type {boolean}
- */
- this._showRaisedHand = false;
-
- // Bind event handlers so they are only bound once for every instance.
- this._onPopoverHover = this._onPopoverHover.bind(this);
- 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() {
- return this.id;
-};
+export default class SmallVideo {
+ /**
+ * Constructor.
+ */
+ constructor(VideoLayout) {
+ this._isModerator = false;
+ this.isAudioMuted = false;
+ this.hasAvatar = false;
+ this.isVideoMuted = false;
+ this.videoStream = null;
+ this.audioStream = null;
+ this.VideoLayout = VideoLayout;
+ this.videoIsHovered = false;
-/* Indicates if this small video is currently visible.
- *
- * @return true if this small video isn't currently visible and
- * false - otherwise.
- */
-SmallVideo.prototype.isVisible = function() {
- return this.$container.is(':visible');
-};
+ /**
+ * The current state of the user's bridge connection. The value should be
+ * a string as enumerated in the library's participantConnectionStatus
+ * constants.
+ *
+ * @private
+ * @type {string|null}
+ */
+ this._connectionStatus = null;
-/**
- * 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) {
- this.videoType = videoType;
-};
+ /**
+ * Whether or not the ConnectionIndicator's popover is hovered. Modifies
+ * how the video overlays display based on hover state.
+ *
+ * @private
+ * @type {boolean}
+ */
+ this._popoverIsHovered = false;
-/**
- * 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() {
- return this.videoType;
-};
+ /**
+ * Whether or not the connection indicator should be displayed.
+ *
+ * @private
+ * @type {boolean}
+ */
+ this._showConnectionIndicator = !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
-/**
- * Creates an audio or video element for a particular MediaStream.
- */
-SmallVideo.createStreamElement = function(stream) {
- const isVideo = stream.isVideoTrack();
+ /**
+ * Whether or not the dominant speaker indicator should be displayed.
+ *
+ * @private
+ * @type {boolean}
+ */
+ this._showDominantSpeaker = false;
- const element = isVideo
- ? document.createElement('video')
- : document.createElement('audio');
+ /**
+ * Whether or not the raised hand indicator should be displayed.
+ *
+ * @private
+ * @type {boolean}
+ */
+ this._showRaisedHand = false;
- if (isVideo) {
- element.setAttribute('muted', 'true');
- } else if (config.startSilent) {
- element.muted = true;
+ // Bind event handlers so they are only bound once for every instance.
+ this._onPopoverHover = this._onPopoverHover.bind(this);
+ this.updateView = this.updateView.bind(this);
+
+ this._onContainerClick = this._onContainerClick.bind(this);
}
- element.autoplay = !config.testing?.noAutoPlayVideo;
- element.id = SmallVideo.getStreamElementID(stream);
+ /**
+ * Returns the identifier of this small video.
+ *
+ * @returns the identifier of this small video
+ */
+ getId() {
+ return this.id;
+ }
- return element;
-};
+ /**
+ * Indicates if this small video is currently visible.
+ *
+ * @return true if this small video isn't currently visible and
+ * false - otherwise.
+ */
+ isVisible() {
+ return this.$container.is(':visible');
+ }
-/**
- * Returns the element id for a particular MediaStream.
- */
-SmallVideo.getStreamElementID = function(stream) {
- const isVideo = stream.isVideoTrack();
+ /**
+ * 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'.
+ */
+ setVideoType(videoType) {
+ this.videoType = videoType;
+ }
- return (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
-};
+ /**
+ * 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.
+ */
+ getVideoType() {
+ return this.videoType;
+ }
-/**
- * Configures hoverIn/hoverOut handlers. Depends on connection indicator.
- */
-SmallVideo.prototype.bindHoverHandler = function() {
- // Add hover handler
- this.$container.hover(
- () => {
- this.videoIsHovered = true;
- this.updateView();
- this.updateIndicators();
- },
- () => {
- this.videoIsHovered = false;
- this.updateView();
- this.updateIndicators();
+ /**
+ * Creates an audio or video element for a particular MediaStream.
+ */
+ static createStreamElement(stream) {
+ const isVideo = stream.isVideoTrack();
+ const element = isVideo ? document.createElement('video') : document.createElement('audio');
+
+ if (isVideo) {
+ element.setAttribute('muted', 'true');
+ } else if (config.startSilent) {
+ element.muted = true;
}
- );
-};
-/**
- * Unmounts the ConnectionIndicator component.
+ element.autoplay = !config.testing?.noAutoPlayVideo;
+ element.id = SmallVideo.getStreamElementID(stream);
- * @returns {void}
- */
-SmallVideo.prototype.removeConnectionIndicator = function() {
- this._showConnectionIndicator = false;
-
- this.updateIndicators();
-};
-
-/**
- * Updates the connectionStatus stat which displays in the ConnectionIndicator.
-
- * @returns {void}
- */
-SmallVideo.prototype.updateConnectionStatus = function(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) {
- 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) {
- 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');
-
- if (!statusBarContainer) {
- return;
+ return element;
}
- const currentLayout = getCurrentLayout(APP.store.getState());
- let tooltipPosition;
-
- if (currentLayout === LAYOUTS.TILE_VIEW) {
- tooltipPosition = 'right';
- } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
- tooltipPosition = 'left';
- } else {
- tooltipPosition = 'top';
+ /**
+ * Returns the element id for a particular MediaStream.
+ */
+ static getStreamElementID(stream) {
+ return (stream.isVideoTrack() ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
}
- ReactDOM.render(
-
-
- { this.isAudioMuted
- ?
- : null }
- { this.isVideoMuted
- ?
- : null }
- { this._isModerator && !interfaceConfig.DISABLE_FOCUS_INDICATOR
- ?
- : null }
-
- ,
- statusBarContainer);
-};
-
-/**
- * Adds the element indicating the moderator(owner) of the conference.
- */
-SmallVideo.prototype.addModeratorIndicator = function() {
- this._isModerator = true;
- this.updateStatusBar();
-};
-
-/**
- * Adds the element indicating the audio level of the participant.
- *
- * @returns {void}
- */
-SmallVideo.prototype.addAudioLevelIndicator = function() {
- let audioLevelContainer = this._getAudioLevelContainer();
-
- if (audioLevelContainer) {
- return;
+ /**
+ * Configures hoverIn/hoverOut handlers. Depends on connection indicator.
+ */
+ bindHoverHandler() {
+ // Add hover handler
+ this.$container.hover(
+ () => {
+ this.videoIsHovered = true;
+ this.updateView();
+ this.updateIndicators();
+ },
+ () => {
+ this.videoIsHovered = false;
+ this.updateView();
+ this.updateIndicators();
+ }
+ );
}
- audioLevelContainer = document.createElement('span');
- audioLevelContainer.className = 'audioindicator-container';
- this.container.appendChild(audioLevelContainer);
+ /**
+ * Unmounts the ConnectionIndicator component.
- this.updateAudioLevelIndicator();
-};
-
-/**
- * Removes the element indicating the audio level of the participant.
- *
- * @returns {void}
- */
-SmallVideo.prototype.removeAudioLevelIndicator = function() {
- const audioLevelContainer = this._getAudioLevelContainer();
-
- if (audioLevelContainer) {
- ReactDOM.unmountComponentAtNode(audioLevelContainer);
+ * @returns {void}
+ */
+ removeConnectionIndicator() {
+ this._showConnectionIndicator = false;
+ this.updateIndicators();
}
-};
-/**
- * Updates the audio level for this small video.
- *
- * @param lvl the new audio level to set
- * @returns {void}
- */
-SmallVideo.prototype.updateAudioLevelIndicator = function(lvl = 0) {
- const audioLevelContainer = this._getAudioLevelContainer();
+ /**
+ * Updates the connectionStatus stat which displays in the ConnectionIndicator.
+
+ * @returns {void}
+ */
+ 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
+ */
+ 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
+ */
+ 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}
+ */
+ updateStatusBar() {
+ const statusBarContainer = this.container.querySelector('.videocontainer__toolbar');
+
+ if (!statusBarContainer) {
+ return;
+ }
+
+ const currentLayout = getCurrentLayout(APP.store.getState());
+ let tooltipPosition;
+
+ if (currentLayout === LAYOUTS.TILE_VIEW) {
+ tooltipPosition = 'right';
+ } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
+ tooltipPosition = 'left';
+ } else {
+ tooltipPosition = 'top';
+ }
- if (audioLevelContainer) {
ReactDOM.render(
- ,
- audioLevelContainer);
+
+
+ { this.isAudioMuted
+ ?
+ : null }
+ { this.isVideoMuted
+ ?
+ : null }
+ { this._isModerator && !interfaceConfig.DISABLE_FOCUS_INDICATOR
+ ?
+ : null }
+
+ ,
+ statusBarContainer);
}
-};
-/**
- * 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() {
- return this.container.querySelector('.audioindicator-container');
-};
+ /**
+ * Adds the element indicating the moderator(owner) of the conference.
+ */
+ addModeratorIndicator() {
+ this._isModerator = true;
+ this.updateStatusBar();
+ }
-/**
- * Removes the element indicating the moderator(owner) of the conference.
- */
-SmallVideo.prototype.removeModeratorIndicator = function() {
- this._isModerator = false;
- this.updateStatusBar();
-};
+ /**
+ * Adds the element indicating the audio level of the participant.
+ *
+ * @returns {void}
+ */
+ addAudioLevelIndicator() {
+ let audioLevelContainer = this._getAudioLevelContainer();
-/**
- * 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
- * from lib-jitsi-meet to extract the video element from it (with two more
- * jquery calls), and finally uses jquery again to encapsulate the video element
- * in an array. This last step allows (some might prefer "forces") users of
- * 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() {
- return $($(this.container).find('video')[0]);
-};
+ if (audioLevelContainer) {
+ return;
+ }
-/**
- * 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() {
- return this.$container.find('.avatar-container');
-};
+ audioLevelContainer = document.createElement('span');
+ audioLevelContainer.className = 'audioindicator-container';
+ this.container.appendChild(audioLevelContainer);
+ this.updateAudioLevelIndicator();
+ }
-/**
- * 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() {
- return this.$container.find('.displayNameContainer');
-};
+ /**
+ * Removes the element indicating the audio level of the participant.
+ *
+ * @returns {void}
+ */
+ removeAudioLevelIndicator() {
+ const audioLevelContainer = this._getAudioLevelContainer();
-/**
- * Creates or updates the participant's display name that is shown over the
- * video preview.
- *
- * @param {Object} props - The React {@code Component} props to pass into the
- * {@code DisplayName} component.
- * @returns {void}
- */
-SmallVideo.prototype._renderDisplayName = function(props) {
- const displayNameContainer
- = this.container.querySelector('.displayNameContainer');
+ if (audioLevelContainer) {
+ ReactDOM.unmountComponentAtNode(audioLevelContainer);
+ }
+ }
+
+ /**
+ * Updates the audio level for this small video.
+ *
+ * @param lvl the new audio level to set
+ * @returns {void}
+ */
+ updateAudioLevelIndicator(lvl = 0) {
+ const audioLevelContainer = this._getAudioLevelContainer();
+
+ if (audioLevelContainer) {
+ ReactDOM.render(, 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.
+ */
+ _getAudioLevelContainer() {
+ return this.container.querySelector('.audioindicator-container');
+ }
+
+ /**
+ * Removes the element indicating the moderator(owner) of the conference.
+ */
+ 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
+ * from lib-jitsi-meet to extract the video element from it (with two more
+ * jquery calls), and finally uses jquery again to encapsulate the video element
+ * in an array. This last step allows (some might prefer "forces") users of
+ * this function to access the video element via the 0th element of the returned
+ * array (after checking its length of course!).
+ */
+ 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.
+ */
+ $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
+ */
+ $displayName() {
+ return this.$container.find('.displayNameContainer');
+ }
+
+ /**
+ * Creates or updates the participant's display name that is shown over the
+ * video preview.
+ *
+ * @param {Object} props - The React {@code Component} props to pass into the
+ * {@code DisplayName} component.
+ * @returns {void}
+ */
+ _renderDisplayName(props) {
+ const displayNameContainer = this.container.querySelector('.displayNameContainer');
+
+ if (displayNameContainer) {
+ ReactDOM.render(
+
+
+
+
+ ,
+ displayNameContainer);
+ }
+ }
+
+ /**
+ * Removes the component responsible for showing the participant's display name,
+ * if its container is present.
+ *
+ * @returns {void}
+ */
+ 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
+ */
+ focus(isFocused) {
+ const focusedCssClass = 'videoContainerFocused';
+ const isFocusClassEnabled = this.$container.hasClass(focusedCssClass);
+
+ if (!isFocused && isFocusClassEnabled) {
+ this.$container.removeClass(focusedCssClass);
+ } else if (isFocused && !isFocusClassEnabled) {
+ this.$container.addClass(focusedCssClass);
+ }
+ }
+
+ /**
+ *
+ */
+ hasVideo() {
+ return this.selectVideoElement().length !== 0;
+ }
+
+ /**
+ * Checks whether the user associated with this SmallVideo is currently
+ * being displayed on the "large video".
+ *
+ * @return {boolean} true if the user is displayed on the large video
+ * or false otherwise.
+ */
+ isCurrentlyOnLargeVideo() {
+ return this.VideoLayout.isCurrentlyOnLarge(this.id);
+ }
+
+ /**
+ * Checks whether there is a playable video stream available for the user
+ * associated with this SmallVideo.
+ *
+ * @return {boolean} true if there is a playable video stream available
+ * or false otherwise.
+ */
+ isVideoPlayable() {
+ return this.videoStream && !this.isVideoMuted && !APP.conference.isAudioOnly();
+ }
+
+ /**
+ * Determines what should be display on the thumbnail.
+ *
+ * @return {number} one of DISPLAY_VIDEO,DISPLAY_AVATAR
+ * or DISPLAY_BLACKNESS_WITH_NAME.
+ */
+ 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;
+ } else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
+ // check hovering and change state to video with name
+ return input.isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
+ }
+
+ // 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}
+ */
+ computeDisplayModeInput() {
+ return {
+ isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
+ isHovered: this._isHovered(),
+ isAudioOnly: APP.conference.isAudioOnly(),
+ tileViewEnabled: shouldDisplayTileView(APP.store.getState()),
+ isVideoPlayable: this.isVideoPlayable(),
+ hasVideo: Boolean(this.selectVideoElement().length),
+ connectionStatus: APP.conference.getParticipantConnectionStatus(this.id),
+ mutedWhileDisconnected: this.mutedWhileDisconnected,
+ wasVideoPlayed: this.wasVideoPlayed,
+ videoStream: Boolean(this.videoStream),
+ 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
+ */
+ _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.
+ */
+ updateView() {
+ if (this.id) {
+ // Init / refresh avatar
+ this.initializeAvatar();
+ } else {
+ logger.error('Unable to init avatar - no id', this);
+
+ return;
+ }
+
+ this.$container.removeClass((index, classNames) =>
+ classNames.split(' ').filter(name => name.startsWith('display-')));
+
+ const oldDisplayMode = this.displayMode;
+ let displayModeString = '';
+
+ const displayModeInput = this.computeDisplayModeInput();
+
+ // Determine whether video, avatar or blackness should be displayed
+ this.displayMode = this.selectDisplayMode(displayModeInput);
+
+ switch (this.displayMode) {
+ case DISPLAY_AVATAR_WITH_NAME:
+ displayModeString = 'avatar-with-name';
+ this.$container.addClass('display-avatar-with-name');
+ break;
+ case DISPLAY_BLACKNESS_WITH_NAME:
+ displayModeString = 'blackness-with-name';
+ this.$container.addClass('display-name-on-black');
+ break;
+ case DISPLAY_VIDEO:
+ displayModeString = 'video';
+ this.$container.addClass('display-video');
+ break;
+ case DISPLAY_VIDEO_WITH_NAME:
+ displayModeString = 'video-with-name';
+ this.$container.addClass('display-name-on-video');
+ break;
+ case DISPLAY_AVATAR:
+ default:
+ displayModeString = 'avatar';
+ this.$container.addClass('display-avatar-only');
+ break;
+ }
+
+ 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}
+ */
+ initializeAvatar() {
+ const thumbnail = this.$avatar().get(0);
+
+ this.hasAvatar = true;
+
+ if (thumbnail) {
+ // Maybe add a special case for local participant, as on init of
+ // LocalVideo.js the id is set to "local" but will get updated later.
+ ReactDOM.render(
+
+
+ ,
+ thumbnail
+ );
+ }
+ }
+
+ /**
+ * Unmounts any attached react components (particular the avatar image) from
+ * the avatar container.
+ *
+ * @returns {void}
+ */
+ 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.
+ */
+ showDominantSpeakerIndicator(show) {
+ // Don't create and show dominant speaker indicator if
+ // DISABLE_DOMINANT_SPEAKER_INDICATOR is true
+ if (interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR) {
+ return;
+ }
+
+ if (!this.container) {
+ 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.
+ */
+ showRaisedHandIndicator(show) {
+ if (!this.container) {
+ logger.warn(`Unable to raised hand indication - ${
+ this.videoSpanId} does not exist`);
+
+ return;
+ }
+
+ 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.
+ */
+ waitForResolutionChange() {
+ const beforeChange = window.performance.now();
+ const videos = this.selectVideoElement();
+
+ if (!videos || !videos.length || videos.length <= 0) {
+ return;
+ }
+ const video = videos[0];
+ const oldWidth = video.videoWidth;
+ const oldHeight = video.videoHeight;
+
+ video.onresize = () => {
+ // eslint-disable-next-line eqeqeq
+ if (video.videoWidth != oldWidth || video.videoHeight != oldHeight) {
+ // Only run once.
+ video.onresize = null;
+
+ const delay = window.performance.now() - beforeChange;
+ const emitter = this.VideoLayout.getEventEmitter();
+
+ if (emitter) {
+ 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:
+ * Some browsers don't have full support of the object-fit property for the
+ * video element and when we set video object-fit to "cover" the video
+ * actually overflows the boundaries of its container, so it's important
+ * to indicate that the "overflow" should be hidden.
+ *
+ * Setting this property for all browsers will result in broken audio levels,
+ * which makes this a temporary solution, before reworking audio levels.
+ */
+ initBrowserSpecificProperties() {
+ const userAgent = window.navigator.userAgent;
+
+ if (userAgent.indexOf('QtWebEngine') > -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}
+ */
+ remove() {
+ logger.log('Remove thumbnail', this.id);
+ this.removeAudioLevelIndicator();
+
+ const toolbarContainer
+ = this.container.querySelector('.videocontainer__toolbar');
+
+ if (toolbarContainer) {
+ ReactDOM.unmountComponentAtNode(toolbarContainer);
+ }
+
+ 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}
+ */
+ 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.
+ *
+ * @private
+ * @returns {void}
+ */
+ 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 state = APP.store.getState();
+ const currentLayout = getCurrentLayout(state);
+ const participantCount = getParticipantCount(state);
+ let statsPopoverPosition, tooltipPosition;
+
+ if (currentLayout === LAYOUTS.TILE_VIEW) {
+ statsPopoverPosition = 'right top';
+ tooltipPosition = 'right';
+ } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
+ statsPopoverPosition = this.statsPopoverLocation;
+ tooltipPosition = 'left';
+ } else {
+ statsPopoverPosition = this.statsPopoverLocation;
+ tooltipPosition = 'top';
+ }
- if (displayNameContainer) {
ReactDOM.render(
-
+
+
+ { this._showConnectionIndicator
+ ?
+ : null }
+
+ { this._showDominantSpeaker && participantCount > 2
+ ?
+ : null }
+
+
,
- 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');
-
- 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) {
- const focusedCssClass = 'videoContainerFocused';
- const isFocusClassEnabled = this.$container.hasClass(focusedCssClass);
-
- if (!isFocused && isFocusClassEnabled) {
- this.$container.removeClass(focusedCssClass);
- } else if (isFocused && !isFocusClassEnabled) {
- this.$container.addClass(focusedCssClass);
- }
-};
-
-SmallVideo.prototype.hasVideo = function() {
- return this.selectVideoElement().length !== 0;
-};
-
-/**
- * Checks whether the user associated with this SmallVideo is currently
- * being displayed on the "large video".
- *
- * @return {boolean} true if the user is displayed on the large video
- * or false otherwise.
- */
-SmallVideo.prototype.isCurrentlyOnLargeVideo = function() {
- return this.VideoLayout.isCurrentlyOnLarge(this.id);
-};
-
-/**
- * Checks whether there is a playable video stream available for the user
- * associated with this SmallVideo.
- *
- * @return {boolean} true if there is a playable video stream available
- * or false otherwise.
- */
-SmallVideo.prototype.isVideoPlayable = function() {
- return this.videoStream && !this.isVideoMuted && !APP.conference.isAudioOnly();
-};
-
-/**
- * Determines what should be display on the thumbnail.
- *
- * @return {number} one of DISPLAY_VIDEO,DISPLAY_AVATAR
- * or DISPLAY_BLACKNESS_WITH_NAME.
- */
-SmallVideo.prototype.selectDisplayMode = function(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;
- } else if (input.isVideoPlayable && input.hasVideo && !input.isAudioOnly) {
- // check hovering and change state to video with name
- return input.isHovered ? DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
- }
-
- // 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() {
- return {
- isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(),
- isHovered: this._isHovered(),
- isAudioOnly: APP.conference.isAudioOnly(),
- tileViewEnabled: shouldDisplayTileView(APP.store.getState()),
- isVideoPlayable: this.isVideoPlayable(),
- hasVideo: Boolean(this.selectVideoElement().length),
- connectionStatus: APP.conference.getParticipantConnectionStatus(this.id),
- mutedWhileDisconnected: this.mutedWhileDisconnected,
- wasVideoPlayed: this.wasVideoPlayed,
- videoStream: Boolean(this.videoStream),
- 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() {
- 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() {
- if (this.id) {
- // Init / refresh avatar
- this.initializeAvatar();
- } else {
- logger.error('Unable to init avatar - no id', this);
-
- return;
- }
-
- this.$container.removeClass((index, classNames) =>
- classNames.split(' ').filter(name => name.startsWith('display-')));
-
- const oldDisplayMode = this.displayMode;
- let displayModeString = '';
-
- const displayModeInput = this.computeDisplayModeInput();
-
- // Determine whether video, avatar or blackness should be displayed
- this.displayMode = this.selectDisplayMode(displayModeInput);
-
- switch (this.displayMode) {
- case DISPLAY_AVATAR_WITH_NAME:
- displayModeString = 'avatar-with-name';
- this.$container.addClass('display-avatar-with-name');
- break;
- case DISPLAY_BLACKNESS_WITH_NAME:
- displayModeString = 'blackness-with-name';
- this.$container.addClass('display-name-on-black');
- break;
- case DISPLAY_VIDEO:
- displayModeString = 'video';
- this.$container.addClass('display-video');
- break;
- case DISPLAY_VIDEO_WITH_NAME:
- displayModeString = 'video-with-name';
- this.$container.addClass('display-name-on-video');
- break;
- case DISPLAY_AVATAR:
- default:
- displayModeString = 'avatar';
- this.$container.addClass('display-avatar-only');
- break;
- }
-
- 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() {
- const thumbnail = this.$avatar().get(0);
-
- this.hasAvatar = true;
-
- if (thumbnail) {
- // Maybe add a special case for local participant, as on init of
- // LocalVideo.js the id is set to "local" but will get updated later.
- ReactDOM.render(
-
-
- ,
- thumbnail
+ indicatorToolbar
);
}
-};
-/**
- * Unmounts any attached react components (particular the avatar image) from
- * the avatar container.
- *
- * @returns {void}
- */
-SmallVideo.prototype.removeAvatar = function() {
- const thumbnail = this.$avatar().get(0);
+ /**
+ * Callback invoked when the thumbnail is clicked and potentially trigger
+ * pinning of the participant.
+ *
+ * @param {MouseEvent} event - The click event to intercept.
+ * @private
+ * @returns {void}
+ */
+ _onContainerClick(event) {
+ const triggerPin = this._shouldTriggerPin(event);
- if (thumbnail) {
- ReactDOM.unmountComponentAtNode(thumbnail);
- }
-};
-
-/**
- * Shows or hides the dominant speaker indicator.
- * @param show whether to show or hide.
- */
-SmallVideo.prototype.showDominantSpeakerIndicator = function(show) {
- // Don't create and show dominant speaker indicator if
- // DISABLE_DOMINANT_SPEAKER_INDICATOR is true
- if (interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR) {
- return;
- }
-
- if (!this.container) {
- 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) {
- if (!this.container) {
- logger.warn(`Unable to raised hand indication - ${
- this.videoSpanId} does not exist`);
-
- return;
- }
-
- 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() {
- const beforeChange = window.performance.now();
- const videos = this.selectVideoElement();
-
- if (!videos || !videos.length || videos.length <= 0) {
- return;
- }
- const video = videos[0];
- const oldWidth = video.videoWidth;
- const oldHeight = video.videoHeight;
-
- video.onresize = () => {
- // eslint-disable-next-line eqeqeq
- if (video.videoWidth != oldWidth || video.videoHeight != oldHeight) {
- // Only run once.
- video.onresize = null;
-
- const delay = window.performance.now() - beforeChange;
- const emitter = this.VideoLayout.getEventEmitter();
-
- if (emitter) {
- emitter.emit(
- UIEvents.RESOLUTION_CHANGED,
- this.getId(),
- `${oldWidth}x${oldHeight}`,
- `${video.videoWidth}x${video.videoHeight}`,
- delay);
- }
+ if (event.stopPropagation && triggerPin) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ if (triggerPin) {
+ this.togglePin();
}
- };
-};
-/**
- * Initalizes any browser specific properties. Currently sets the overflow
- * property for Qt browsers on Windows to hidden, thus fixing the following
- * problem:
- * Some browsers don't have full support of the object-fit property for the
- * video element and when we set video object-fit to "cover" the video
- * actually overflows the boundaries of its container, so it's important
- * to indicate that the "overflow" should be hidden.
- *
- * 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() {
-
- const userAgent = window.navigator.userAgent;
-
- if (userAgent.indexOf('QtWebEngine') > -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() {
- logger.log('Remove thumbnail', this.id);
-
- this.removeAudioLevelIndicator();
-
- const toolbarContainer
- = this.container.querySelector('.videocontainer__toolbar');
-
- if (toolbarContainer) {
- ReactDOM.unmountComponentAtNode(toolbarContainer);
+ return false;
}
- this.removeConnectionIndicator();
+ /**
+ * Returns whether or not a click event is targeted at certain elements which
+ * should not trigger a pin.
+ *
+ * @param {MouseEvent} event - The click event to intercept.
+ * @private
+ * @returns {boolean}
+ */
+ _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
+ // point will prevent DisplayName from getting click events. This workaround
+ // should be removeable once LocalVideo is a React Component because then
+ // the components share the same eventing system.
+ const $source = $(event.target || event.srcElement);
- 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() {
- 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.
- *
- * @private
- * @returns {void}
- */
-SmallVideo.prototype.updateIndicators = function() {
- const indicatorToolbar
- = this.container.querySelector('.videocontainer__toptoolbar');
-
- if (!indicatorToolbar) {
- return;
+ return $source.parents('.displayNameContainer').length === 0
+ && $source.parents('.popover').length === 0
+ && !event.target.classList.contains('popover');
}
- const iconSize = UIUtil.getIndicatorFontSize();
- const showConnectionIndicator = this.videoIsHovered
- || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
- const state = APP.store.getState();
- const currentLayout = getCurrentLayout(state);
- const participantCount = getParticipantCount(state);
- let statsPopoverPosition, tooltipPosition;
+ /**
+ * Pins the participant displayed by this thumbnail or unpins if already pinned.
+ *
+ * @returns {void}
+ */
+ togglePin() {
+ const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {};
+ const participantIdToPin = pinnedParticipant && pinnedParticipant.id === this.id ? null : this.id;
- if (currentLayout === LAYOUTS.TILE_VIEW) {
- statsPopoverPosition = 'right top';
- tooltipPosition = 'right';
- } else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
- statsPopoverPosition = this.statsPopoverLocation;
- tooltipPosition = 'left';
- } else {
- statsPopoverPosition = this.statsPopoverLocation;
- tooltipPosition = 'top';
+ APP.store.dispatch(pinParticipant(participantIdToPin));
}
- ReactDOM.render(
-
-
-
-
- { this._showConnectionIndicator
- ?
- : null }
-
- { this._showDominantSpeaker && participantCount > 2
- ?
- : null }
-
-
-
- ,
- indicatorToolbar
- );
-};
+ /**
+ * Removes the React element responsible for showing connection status, dominant
+ * speaker, and raised hand icons.
+ *
+ * @private
+ * @returns {void}
+ */
+ _unmountIndicators() {
+ const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
-/**
- * Callback invoked when the thumbnail is clicked and potentially trigger
- * pinning of the participant.
- *
- * @param {MouseEvent} event - The click event to intercept.
- * @private
- * @returns {void}
- */
-SmallVideo.prototype._onContainerClick = function(event) {
- const triggerPin = this._shouldTriggerPin(event);
-
- if (event.stopPropagation && triggerPin) {
- event.stopPropagation();
- event.preventDefault();
+ if (indicatorToolbar) {
+ ReactDOM.unmountComponentAtNode(indicatorToolbar);
+ }
}
- if (triggerPin) {
- this.togglePin();
+ /**
+ * Updates the current state of the connection indicator popover being hovered.
+ * If hovered, display the small video as if it is hovered.
+ *
+ * @param {boolean} popoverIsHovered - Whether or not the mouse cursor is
+ * currently over the connection indicator popover.
+ * @returns {void}
+ */
+ _onPopoverHover(popoverIsHovered) {
+ this._popoverIsHovered = popoverIsHovered;
+ this.updateView();
}
-
- return false;
-};
-
-/**
- * Returns whether or not a click event is targeted at certain elements which
- * should not trigger a pin.
- *
- * @param {MouseEvent} event - The click event to intercept.
- * @private
- * @returns {boolean}
- */
-SmallVideo.prototype._shouldTriggerPin = function(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
- // point will prevent DisplayName from getting click events. This workaround
- // should be removeable once LocalVideo is a React Component because then
- // the components share the same eventing system.
- const $source = $(event.target || event.srcElement);
-
- 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;
-
- 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');
-
- 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.
- *
- * @param {boolean} popoverIsHovered - Whether or not the mouse cursor is
- * currently over the connection indicator popover.
- * @returns {void}
- */
-SmallVideo.prototype._onPopoverHover = function(popoverIsHovered) {
- this._popoverIsHovered = popoverIsHovered;
- this.updateView();
-};
-
-
-export default SmallVideo;
+}
diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js
index 4ba485eca..f0e6fb729 100644
--- a/modules/UI/videolayout/VideoLayout.js
+++ b/modules/UI/videolayout/VideoLayout.js
@@ -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
@@ -327,8 +306,7 @@ const VideoLayout = {
const id = participant.id;
const jitsiParticipant = APP.conference.getParticipantById(id);
- const remoteVideo
- = new RemoteVideo(jitsiParticipant, VideoLayout, eventEmitter);
+ const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
this._setRemoteControlProperties(jitsiParticipant, remoteVideo);
this.addRemoteVideoContainer(id, remoteVideo);
@@ -411,27 +389,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();
diff --git a/react/features/analytics/AnalyticsEvents.js b/react/features/analytics/AnalyticsEvents.js
index 5e56d2c0e..ed8b82144 100644
--- a/react/features/analytics/AnalyticsEvents.js
+++ b/react/features/analytics/AnalyticsEvents.js
@@ -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