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

314 lines
9.6 KiB
JavaScript
Raw Normal View History

/* global $, config, interfaceConfig, APP */
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
import { VideoTrack } from '../../../react/features/base/media';
import {
getAvatarURLByParticipantId
} from '../../../react/features/base/participants';
import { updateSettings } from '../../../react/features/base/settings';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename);
2016-11-11 15:00:54 +00:00
import UIEvents from '../../../service/UI/UIEvents';
import SmallVideo from './SmallVideo';
2015-12-14 12:26:50 +00:00
/**
*
*/
function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
this.videoSpanId = 'localVideoContainer';
this.streamEndedCallback = streamEndedCallback;
this.container = this.createContainer();
this.$container = $(this.container);
this.updateDOMLocation();
this.localVideoId = null;
this.bindHoverHandler();
if (config.enableLocalVideoFlip) {
this._buildContextMenu();
}
this.isLocal = true;
2015-12-01 12:53:01 +00:00
this.emitter = emitter;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP
WiP(invite-ui): Initial move of invite UI to invite button (#1950) * WiP(invite-ui): Initial move of invite UI to invite button * Adjusts styling to fit both horizontal and vertical filmstrip * Removes comment and functions not needed * [squash] Addressing various review comments * [squash] Move invite options to a separate config * [squash] Adjust invite button styles until we fix the whole UI theme * [squash] Fix the remote videos scroll * [squash]:Do not show popup menu when 1 option is available * [squash]: Disable the invite button in filmstrip mode * feat(connection-indicator): implement automatic hiding on good connection (#2009) * ref(connection-stats): use PropTypes package * feat(connection-stats): display a summary of the connection quality * feat(connection-indicator): show empty bars for interrupted connection * feat(connection-indicator): change background color based on status * feat(connection-indicator): implement automatic hiding on good connection * fix(connection-indicator): explicitly set font size Currently non-react code will set an icon size on ConnectionIndicator. This doesn't work on initial call join in vertical filmstrip after some changes to support hiding the indicator. The chosen fix is passing in the icon size to mirror what would happe with full filmstrip reactification. * ref(connection-stats): rename statuses * feat(connection-indicator): make hiding behavior configurable The original implementation made the auto hiding of the indicator configured in interfaceConfig. * fix(connection-indicator): readd class expected by torture tests * fix(connection-indicator): change connection quality display styling Bold the connection summary in the stats popover so it stands out. Change the summaries so there are only three--strong, nonoptimal, poor. * fix(connection-indicator): gray background on lost connection * feat(icons): add new gsm bars icon * feat(connection-indicator): use new 3-bar icon * ref(icons): remove icon-connection and icon-connection-lost Both have been replaced by icon-gsm-bars so they are not being referenced anymore. Mobile looks to have connect-lost as a separate icon in font-icons/jitsi.json. * fix(defaultToolbarButtons): Fixes unresolved InfoDialogButton component problem * [squash]: Makes invite button fit the container * [squash]:Addressing invite truncate, remote menu position and comment * [squash]:Fix z-index in horizontal mode, z-index in lonely call * [squash]: Fix filmstripOnly property, remove important from css
2017-10-03 16:30:42 +00:00
? 'left top' : 'top center';
Object.defineProperty(this, 'id', {
get() {
2016-07-08 01:44:04 +00:00
return APP.conference.getMyUserId();
}
});
this.initBrowserSpecificProperties();
SmallVideo.call(this, VideoLayout);
// Set default display name.
this.setDisplayName();
// 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.avatarChanged(
getAvatarURLByParticipantId(APP.store.getState(), this.id));
2016-09-28 21:31:40 +00:00
this.addAudioLevelIndicator();
this.updateIndicators();
this.container.onclick = this._onContainerClick.bind(this);
2015-06-23 08:00:46 +00:00
}
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 = `
<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
/**
* Sets the display name for the given video span id.
*/
LocalVideo.prototype.setDisplayName = function(displayName) {
2015-06-23 08:00:46 +00:00
if (!this.container) {
2016-11-11 15:00:54 +00:00
logger.warn(
`Unable to set displayName - ${this.videoSpanId
} does not exist`);
2015-06-23 08:00:46 +00:00
return;
}
this.updateDisplayName({
allowEditing: APP.store.getState()['features/base/jwt'].isGuest,
displayName,
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
elementID: 'localDisplayName',
participantID: this.id
});
};
2015-06-23 08:00:46 +00:00
LocalVideo.prototype.changeVideo = function(stream) {
this.videoStream = stream;
2015-06-23 08:00:46 +00:00
this.localVideoId = `localVideo_${stream.getId()}`;
2015-06-23 08:00:46 +00:00
this._updateVideoElement();
2015-06-23 08:00:46 +00:00
// 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');
2015-06-23 08:00:46 +00:00
// 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);
2016-01-06 22:39:13 +00:00
};
stream.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};
2015-06-23 08:00:46 +00:00
/**
* Notify any subscribers of the local video stream ending.
*
* @private
* @returns {void}
*/
LocalVideo.prototype._notifyOfStreamEnded = function() {
if (this.streamEndedCallback) {
this.streamEndedCallback(this.id);
}
};
2016-05-01 18:35:18 +00:00
/**
* Shows or hides the local video container.
* @param {boolean} true to make the local video container visible, false
* otherwise
*/
LocalVideo.prototype.setVisible = function(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');
2016-05-01 18:35:18 +00:00
// 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
}
};
/**
* 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;
}
if (val) {
this.selectVideoElement().addClass('flipVideoX');
} else {
this.selectVideoElement().removeClass('flipVideoX');
}
};
/**
* 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;
this.setFlipX(val);
store.dispatch(updateSettings({
localFlipX: val
}));
}
}
},
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
*/
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);
}
const appendTarget = shouldDisplayTileView(APP.store.getState())
? document.getElementById('localVideoTileViewContainer')
: document.getElementById('filmstripLocalVideoThumbnail');
appendTarget && appendTarget.appendChild(this.container);
this._updateVideoElement();
};
/**
* Callback invoked when the thumbnail is clicked. Will directly call
* VideoLayout to handle thumbnail click if certain elements have not been
* clicked.
*
* @param {MouseEvent} event - The click event to intercept.
* @private
* @returns {void}
*/
LocalVideo.prototype._onContainerClick = 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);
const { classList } = event.target;
const clickedOnDisplayName
= $source.parents('.displayNameContainer').length > 0;
const clickedOnPopover = $source.parents('.popover').length > 0
|| classList.contains('popover');
const ignoreClick = clickedOnDisplayName || clickedOnPopover;
if (event.stopPropagation && !ignoreClick) {
event.stopPropagation();
}
if (!ignoreClick) {
this._togglePin();
}
};
/**
* Renders the React Element for displaying video in {@code LocalVideo}.
*
*/
LocalVideo.prototype._updateVideoElement = function() {
const localVideoContainer = document.getElementById('localVideoWrapper');
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = 'localVideo_container'
videoTrack = {{ jitsiTrack: this.videoStream }} />
</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 && video.play();
};
2015-12-14 12:26:50 +00:00
export default LocalVideo;