2017-10-10 23:31:40 +00:00
|
|
|
/* global $, config, interfaceConfig, APP */
|
2017-07-14 19:22:27 +00:00
|
|
|
|
2020-05-20 10:57:03 +00:00
|
|
|
import Logger from 'jitsi-meet-logger';
|
2017-07-14 19:22:27 +00:00
|
|
|
/* eslint-disable no-unused-vars */
|
|
|
|
import React, { Component } from 'react';
|
|
|
|
import ReactDOM from 'react-dom';
|
|
|
|
import { Provider } from 'react-redux';
|
|
|
|
|
2017-10-10 23:31:40 +00:00
|
|
|
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
|
2017-07-14 19:22:27 +00:00
|
|
|
import { VideoTrack } from '../../../react/features/base/media';
|
2018-04-12 19:58:20 +00:00
|
|
|
import { updateSettings } from '../../../react/features/base/settings';
|
fix(tile-view): prevent local participant being selected on pin exit
On tile view enter/exit, local video is moved in the DOM (an effect
of not being reactified and moving being easier) and play is called
on its video element. The race condition setup is such: in tile
view with other participants and local video is on large (not
visible in the UI but visible in the app state and pip popout).
The race is such: pin a remote video, large video update is queued,
tile view is exited, local video is moved, play is called,,
onVideoPlaying callback executed, middleware fires mute update,
which checks if local is on large (it is), previous large video
update is cleared, and local is placed on large.
The fix is ensuring the redux representation of local video is
passed in, which holds the boolean videoStarted, which prevents
the onVideoPlaying callback from firing on subsequent plays.
2018-11-27 22:13:47 +00:00
|
|
|
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
|
2018-08-08 18:48:23 +00:00
|
|
|
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
2017-07-14 19:22:27 +00:00
|
|
|
/* eslint-enable no-unused-vars */
|
2017-10-12 23:02:29 +00:00
|
|
|
import UIEvents from '../../../service/UI/UIEvents';
|
2020-05-20 10:57:03 +00:00
|
|
|
|
2017-10-12 23:02:29 +00:00
|
|
|
import SmallVideo from './SmallVideo';
|
2015-12-14 12:26:50 +00:00
|
|
|
|
2020-05-20 10:57:03 +00:00
|
|
|
const logger = Logger.getLogger(__filename);
|
|
|
|
|
2017-10-12 23:02:29 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
2019-12-16 14:15:02 +00:00
|
|
|
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);
|
2020-01-24 16:28:47 +00:00
|
|
|
this.isLocal = true;
|
|
|
|
this._setThumbnailSize();
|
2019-12-16 14:15:02 +00:00
|
|
|
this.updateDOMLocation();
|
|
|
|
|
|
|
|
this.localVideoId = null;
|
|
|
|
this.bindHoverHandler();
|
|
|
|
if (!config.disableLocalVideoFlip) {
|
|
|
|
this._buildContextMenu();
|
2016-02-09 10:19:43 +00:00
|
|
|
}
|
2019-12-16 14:15:02 +00:00
|
|
|
this.emitter = emitter;
|
2020-01-24 16:28:47 +00:00
|
|
|
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center';
|
2019-12-16 14:15:02 +00:00
|
|
|
|
|
|
|
Object.defineProperty(this, 'id', {
|
|
|
|
get() {
|
|
|
|
return APP.conference.getMyUserId();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.initBrowserSpecificProperties();
|
2017-06-30 17:40:55 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
// Set default display name.
|
|
|
|
this.updateDisplayName();
|
2017-06-30 17:40:55 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
// 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();
|
2017-06-30 17:40:55 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
this.addAudioLevelIndicator();
|
|
|
|
this.updateIndicators();
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
this.container.onclick = this._onContainerClick;
|
2015-06-23 08:00:46 +00:00
|
|
|
}
|
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
createContainer() {
|
|
|
|
const containerSpan = document.createElement('span');
|
|
|
|
|
|
|
|
containerSpan.classList.add('videocontainer');
|
|
|
|
containerSpan.id = this.videoSpanId;
|
|
|
|
|
|
|
|
containerSpan.innerHTML = `
|
|
|
|
<div class = 'videocontainer__background'></div>
|
|
|
|
<span id = 'localVideoWrapper'></span>
|
|
|
|
<div class = 'videocontainer__toolbar'></div>
|
|
|
|
<div class = 'videocontainer__toptoolbar'></div>
|
|
|
|
<div class = 'videocontainer__hoverOverlay'></div>
|
|
|
|
<div class = 'displayNameContainer'></div>
|
|
|
|
<div class = 'avatar-container'></div>`;
|
|
|
|
|
|
|
|
return containerSpan;
|
|
|
|
}
|
2015-06-23 08:00:46 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
2015-06-23 08:00:46 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
this._renderDisplayName({
|
|
|
|
allowEditing: APP.store.getState()['features/base/jwt'].isGuest,
|
|
|
|
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
|
|
|
|
elementID: 'localDisplayName',
|
|
|
|
participantID: this.id
|
|
|
|
});
|
|
|
|
}
|
2015-06-23 08:00:46 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {*} stream
|
|
|
|
*/
|
|
|
|
changeVideo(stream) {
|
|
|
|
this.videoStream = stream;
|
|
|
|
this.localVideoId = `localVideo_${stream.getId()}`;
|
|
|
|
this._updateVideoElement();
|
|
|
|
|
|
|
|
// eslint-disable-next-line eqeqeq
|
|
|
|
const isVideo = stream.videoType != 'desktop';
|
|
|
|
const settings = APP.store.getState()['features/base/settings'];
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
this._notifyOfStreamEnded();
|
|
|
|
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
|
|
|
};
|
2017-10-12 23:02:29 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
|
|
|
}
|
2015-06-23 08:00:46 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
/**
|
|
|
|
* Notify any subscribers of the local video stream ending.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_notifyOfStreamEnded() {
|
|
|
|
if (this.streamEndedCallback) {
|
|
|
|
this.streamEndedCallback(this.id);
|
2017-07-14 19:22:27 +00:00
|
|
|
}
|
2018-05-21 22:10:43 +00:00
|
|
|
}
|
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
/**
|
|
|
|
* 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();
|
|
|
|
}
|
2016-05-01 18:35:18 +00:00
|
|
|
}
|
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
/**
|
|
|
|
* 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');
|
|
|
|
}
|
2016-05-07 01:50:37 +00:00
|
|
|
}
|
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
/**
|
|
|
|
* 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');
|
2016-05-07 01:50:37 +00:00
|
|
|
}
|
|
|
|
}
|
2019-12-16 14:15:02 +00:00
|
|
|
});
|
2017-10-12 23:02:29 +00:00
|
|
|
}
|
2016-05-07 01:50:37 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
2018-08-08 18:48:23 +00:00
|
|
|
}
|
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
2018-08-08 18:48:23 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
const appendTarget = shouldDisplayTileView(APP.store.getState())
|
|
|
|
? document.getElementById('localVideoTileViewContainer')
|
|
|
|
: document.getElementById('filmstripLocalVideoThumbnail');
|
2018-08-08 18:48:23 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
appendTarget && appendTarget.appendChild(this.container);
|
|
|
|
this._updateVideoElement();
|
|
|
|
}
|
2018-08-08 18:48:23 +00:00
|
|
|
|
2019-12-16 14:15:02 +00:00
|
|
|
/**
|
|
|
|
* 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']);
|
|
|
|
|
|
|
|
ReactDOM.render(
|
|
|
|
<Provider store = { APP.store }>
|
|
|
|
<VideoTrack
|
|
|
|
id = 'localVideo_container'
|
|
|
|
videoTrack = { videoTrack } />
|
|
|
|
</Provider>,
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|