mirror of https://github.com/jitsi/jitsi-meet
parent
e937e99284
commit
f50872285d
@ -1,67 +0,0 @@ |
||||
/* global $, APP */ |
||||
|
||||
/* eslint-disable no-unused-vars */ |
||||
import React, { Component } from 'react'; |
||||
import ReactDOM from 'react-dom'; |
||||
import { I18nextProvider } from 'react-i18next'; |
||||
import { Provider } from 'react-redux'; |
||||
|
||||
import { i18next } from '../../../react/features/base/i18n'; |
||||
import { Thumbnail } from '../../../react/features/filmstrip'; |
||||
import SmallVideo from '../videolayout/SmallVideo'; |
||||
/* eslint-enable no-unused-vars */ |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
export default class SharedVideoThumb extends SmallVideo { |
||||
/** |
||||
* |
||||
* @param {*} participant |
||||
*/ |
||||
constructor(participant) { |
||||
super(); |
||||
this.id = participant.id; |
||||
this.isLocal = false; |
||||
this.url = participant.id; |
||||
this.videoSpanId = 'sharedVideoContainer'; |
||||
this.container = this.createContainer(this.videoSpanId); |
||||
this.$container = $(this.container); |
||||
this.renderThumbnail(); |
||||
this._setThumbnailSize(); |
||||
this.bindHoverHandler(); |
||||
this.container.onclick = this._onContainerClick; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param {*} spanId |
||||
*/ |
||||
createContainer(spanId) { |
||||
const container = document.createElement('span'); |
||||
|
||||
container.id = spanId; |
||||
container.className = 'videocontainer'; |
||||
|
||||
const remoteVideosContainer |
||||
= document.getElementById('filmstripRemoteVideosContainer'); |
||||
const localVideoContainer |
||||
= document.getElementById('localVideoTileViewContainer'); |
||||
|
||||
remoteVideosContainer.insertBefore(container, localVideoContainer); |
||||
|
||||
return container; |
||||
} |
||||
|
||||
/** |
||||
* Renders the thumbnail. |
||||
*/ |
||||
renderThumbnail(isHovered = false) { |
||||
ReactDOM.render( |
||||
<Provider store = { APP.store }> |
||||
<I18nextProvider i18n = { i18next }> |
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } /> |
||||
</I18nextProvider> |
||||
</Provider>, this.container); |
||||
} |
||||
} |
||||
@ -1,213 +0,0 @@ |
||||
/* global $, config, APP */ |
||||
|
||||
/* eslint-disable no-unused-vars */ |
||||
import React, { Component } from 'react'; |
||||
import ReactDOM from 'react-dom'; |
||||
import { I18nextProvider } from 'react-i18next'; |
||||
import { Provider } from 'react-redux'; |
||||
|
||||
import { i18next } from '../../../react/features/base/i18n'; |
||||
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet'; |
||||
import { VideoTrack } from '../../../react/features/base/media'; |
||||
import { updateSettings } from '../../../react/features/base/settings'; |
||||
import { getLocalVideoTrack } from '../../../react/features/base/tracks'; |
||||
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail'; |
||||
import { shouldDisplayTileView } from '../../../react/features/video-layout'; |
||||
/* eslint-enable no-unused-vars */ |
||||
import UIEvents from '../../../service/UI/UIEvents'; |
||||
|
||||
import SmallVideo from './SmallVideo'; |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
export default class LocalVideo extends SmallVideo { |
||||
/** |
||||
* |
||||
* @param {*} emitter |
||||
* @param {*} streamEndedCallback |
||||
*/ |
||||
constructor(emitter, streamEndedCallback) { |
||||
super(); |
||||
this.videoSpanId = 'localVideoContainer'; |
||||
this.streamEndedCallback = streamEndedCallback; |
||||
this.container = this.createContainer(); |
||||
this.$container = $(this.container); |
||||
this.isLocal = true; |
||||
this._setThumbnailSize(); |
||||
this.updateDOMLocation(); |
||||
this.renderThumbnail(); |
||||
|
||||
this.localVideoId = null; |
||||
this.bindHoverHandler(); |
||||
if (!config.disableLocalVideoFlip) { |
||||
this._buildContextMenu(); |
||||
} |
||||
this.emitter = emitter; |
||||
|
||||
Object.defineProperty(this, 'id', { |
||||
get() { |
||||
return APP.conference.getMyUserId(); |
||||
} |
||||
}); |
||||
this.initBrowserSpecificProperties(); |
||||
|
||||
this.container.onclick = this._onContainerClick; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
createContainer() { |
||||
const containerSpan = document.createElement('span'); |
||||
|
||||
containerSpan.classList.add('videocontainer'); |
||||
containerSpan.id = this.videoSpanId; |
||||
|
||||
return containerSpan; |
||||
} |
||||
|
||||
/** |
||||
* Renders the thumbnail. |
||||
*/ |
||||
renderThumbnail(isHovered = false) { |
||||
ReactDOM.render( |
||||
<Provider store = { APP.store }> |
||||
<I18nextProvider i18n = { i18next }> |
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } /> |
||||
</I18nextProvider> |
||||
</Provider>, this.container); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param {*} stream |
||||
*/ |
||||
changeVideo(stream) { |
||||
this.localVideoId = `localVideo_${stream.getId()}`; |
||||
|
||||
// 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 = () => { |
||||
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} |
||||
*/ |
||||
_notifyOfStreamEnded() { |
||||
if (this.streamEndedCallback) { |
||||
this.streamEndedCallback(this.id); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Shows or hides the local video container. |
||||
* @param {boolean} true to make the local video container visible, false |
||||
* otherwise |
||||
*/ |
||||
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(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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'); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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'); |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 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); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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); |
||||
} |
||||
} |
||||
@ -1,242 +0,0 @@ |
||||
/* global $, APP, config */ |
||||
|
||||
/* eslint-disable no-unused-vars */ |
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme'; |
||||
import Logger from 'jitsi-meet-logger'; |
||||
import React from 'react'; |
||||
import ReactDOM from 'react-dom'; |
||||
import { I18nextProvider } from 'react-i18next'; |
||||
import { Provider } from 'react-redux'; |
||||
|
||||
import { i18next } from '../../../react/features/base/i18n'; |
||||
import { |
||||
JitsiParticipantConnectionStatus |
||||
} from '../../../react/features/base/lib-jitsi-meet'; |
||||
import { getParticipantById } from '../../../react/features/base/participants'; |
||||
import { isTestModeEnabled } from '../../../react/features/base/testing'; |
||||
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks'; |
||||
import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip'; |
||||
import { PresenceLabel } from '../../../react/features/presence-status'; |
||||
import { stopController, requestRemoteControl } from '../../../react/features/remote-control'; |
||||
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu'; |
||||
/* eslint-enable no-unused-vars */ |
||||
import UIUtils from '../util/UIUtil'; |
||||
|
||||
import SmallVideo from './SmallVideo'; |
||||
|
||||
const logger = Logger.getLogger(__filename); |
||||
|
||||
/** |
||||
* List of container events that we are going to process, will be added as listener to the |
||||
* container for every event in the list. The latest event will be stored in redux. |
||||
*/ |
||||
const containerEvents = [ |
||||
'abort', 'canplay', 'canplaythrough', 'emptied', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart', |
||||
'pause', 'play', 'playing', 'ratechange', 'stalled', 'suspend', 'waiting' |
||||
]; |
||||
|
||||
/** |
||||
* |
||||
* @param {*} spanId |
||||
*/ |
||||
function createContainer(spanId) { |
||||
const container = document.createElement('span'); |
||||
|
||||
container.id = spanId; |
||||
container.className = 'videocontainer'; |
||||
|
||||
const remoteVideosContainer |
||||
= document.getElementById('filmstripRemoteVideosContainer'); |
||||
const localVideoContainer |
||||
= document.getElementById('localVideoTileViewContainer'); |
||||
|
||||
remoteVideosContainer.insertBefore(container, localVideoContainer); |
||||
|
||||
return container; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
export default class RemoteVideo extends SmallVideo { |
||||
/** |
||||
* Creates new instance of the <tt>RemoteVideo</tt>. |
||||
* @param user {JitsiParticipant} the user for whom remote video instance will |
||||
* be created. |
||||
* @constructor |
||||
*/ |
||||
constructor(user) { |
||||
super(); |
||||
|
||||
this.user = user; |
||||
this.id = user.getId(); |
||||
this.videoSpanId = `participant_${this.id}`; |
||||
|
||||
this.addRemoteVideoContainer(); |
||||
this.bindHoverHandler(); |
||||
this.flipX = false; |
||||
this.isLocal = false; |
||||
|
||||
/** |
||||
* The flag is set to <tt>true</tt> after the 'canplay' event has been |
||||
* triggered on the current video element. It goes back to <tt>false</tt> |
||||
* when the stream is removed. It is used to determine whether the video |
||||
* playback has ever started. |
||||
* @type {boolean} |
||||
*/ |
||||
this._canPlayEventReceived = false; |
||||
|
||||
this.container.onclick = this._onContainerClick; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
addRemoteVideoContainer() { |
||||
this.container = createContainer(this.videoSpanId); |
||||
this.$container = $(this.container); |
||||
this.renderThumbnail(); |
||||
this._setThumbnailSize(); |
||||
this.initBrowserSpecificProperties(); |
||||
|
||||
return this.container; |
||||
} |
||||
|
||||
/** |
||||
* Renders the thumbnail. |
||||
*/ |
||||
renderThumbnail(isHovered = false) { |
||||
ReactDOM.render( |
||||
<Provider store = { APP.store }> |
||||
<I18nextProvider i18n = { i18next }> |
||||
<Thumbnail participantID = { this.id } isHovered = { isHovered } /> |
||||
</I18nextProvider> |
||||
</Provider>, this.container); |
||||
} |
||||
|
||||
/** |
||||
* Removes the remote stream element corresponding to the given stream and |
||||
* parent container. |
||||
* |
||||
* @param stream the MediaStream |
||||
* @param isVideo <tt>true</tt> if given <tt>stream</tt> is a video one. |
||||
*/ |
||||
removeRemoteStreamElement(stream) { |
||||
if (!this.container) { |
||||
return false; |
||||
} |
||||
|
||||
const isVideo = stream.isVideoTrack(); |
||||
const elementID = `remoteVideo_${stream.getId()}`; |
||||
const select = $(`#${elementID}`); |
||||
|
||||
select.remove(); |
||||
if (isVideo) { |
||||
this._canPlayEventReceived = false; |
||||
} |
||||
|
||||
logger.info(`Video removed ${this.id}`, select); |
||||
|
||||
this.updateView(); |
||||
} |
||||
|
||||
/** |
||||
* The remote video is considered "playable" once the can play event has been received. |
||||
* |
||||
* @inheritdoc |
||||
* @override |
||||
*/ |
||||
isVideoPlayable() { |
||||
return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived; |
||||
} |
||||
|
||||
/** |
||||
* @inheritDoc |
||||
*/ |
||||
updateView() { |
||||
this.$container.toggleClass('audio-only', APP.conference.isAudioOnly()); |
||||
super.updateView(); |
||||
} |
||||
|
||||
/** |
||||
* Removes RemoteVideo from the page. |
||||
*/ |
||||
remove() { |
||||
ReactDOM.unmountComponentAtNode(this.container); |
||||
super.remove(); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param {*} streamElement |
||||
* @param {*} stream |
||||
*/ |
||||
waitForPlayback(streamElement, stream) { |
||||
$(streamElement).hide(); |
||||
|
||||
const webRtcStream = stream.getOriginalStream(); |
||||
const isVideo = stream.isVideoTrack(); |
||||
|
||||
if (!isVideo || webRtcStream.id === 'mixedmslabel') { |
||||
return; |
||||
} |
||||
|
||||
const listener = () => { |
||||
this._canPlayEventReceived = true; |
||||
|
||||
logger.info(`${this.id} video is now active`, streamElement); |
||||
if (streamElement) { |
||||
$(streamElement).show(); |
||||
} |
||||
|
||||
streamElement.removeEventListener('canplay', listener); |
||||
|
||||
// Refresh to show the video
|
||||
this.updateView(); |
||||
}; |
||||
|
||||
streamElement.addEventListener('canplay', listener); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param {*} stream |
||||
*/ |
||||
addRemoteStreamElement(stream) { |
||||
if (!this.container) { |
||||
logger.debug('Not attaching remote stream due to no container'); |
||||
|
||||
return; |
||||
} |
||||
|
||||
const isVideo = stream.isVideoTrack(); |
||||
|
||||
if (!stream.getOriginalStream()) { |
||||
logger.debug('Remote video stream has no original stream'); |
||||
|
||||
return; |
||||
} |
||||
|
||||
let streamElement = document.createElement('video'); |
||||
|
||||
streamElement.autoplay = !config.testing?.noAutoPlayVideo; |
||||
streamElement.id = `remoteVideo_${stream.getId()}`; |
||||
streamElement.mute = true; |
||||
streamElement.playsInline = true; |
||||
|
||||
// Put new stream element always in front
|
||||
streamElement = UIUtils.prependChild(this.container, streamElement); |
||||
|
||||
this.waitForPlayback(streamElement, stream); |
||||
stream.attach(streamElement); |
||||
|
||||
if (isVideo && isTestModeEnabled(APP.store.getState())) { |
||||
|
||||
const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name)); |
||||
|
||||
containerEvents.forEach(event => { |
||||
streamElement.addEventListener(event, cb.bind(this, event)); |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
@ -1,509 +0,0 @@ |
||||
/* global $, APP, interfaceConfig */ |
||||
|
||||
/* eslint-disable no-unused-vars */ |
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme'; |
||||
import Logger from 'jitsi-meet-logger'; |
||||
import React from 'react'; |
||||
import ReactDOM from 'react-dom'; |
||||
import { I18nextProvider } from 'react-i18next'; |
||||
import { Provider } from 'react-redux'; |
||||
|
||||
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../react/features/analytics'; |
||||
import { AudioLevelIndicator } from '../../../react/features/audio-level-indicator'; |
||||
import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar'; |
||||
import { i18next } from '../../../react/features/base/i18n'; |
||||
import { MEDIA_TYPE } from '../../../react/features/base/media'; |
||||
import { |
||||
getLocalParticipant, |
||||
getParticipantById, |
||||
getParticipantCount, |
||||
getPinnedParticipant, |
||||
pinParticipant |
||||
} from '../../../react/features/base/participants'; |
||||
import { |
||||
getLocalVideoTrack, |
||||
getTrackByMediaTypeAndParticipant, |
||||
isLocalTrackMuted, |
||||
isRemoteTrackMuted |
||||
} from '../../../react/features/base/tracks'; |
||||
import { ConnectionIndicator } from '../../../react/features/connection-indicator'; |
||||
import { DisplayName } from '../../../react/features/display-name'; |
||||
import { |
||||
DominantSpeakerIndicator, |
||||
RaisedHandIndicator, |
||||
StatusIndicators, |
||||
isVideoPlayable |
||||
} from '../../../react/features/filmstrip'; |
||||
import { |
||||
LAYOUTS, |
||||
getCurrentLayout, |
||||
setTileView, |
||||
shouldDisplayTileView |
||||
} from '../../../react/features/video-layout'; |
||||
/* eslint-enable no-unused-vars */ |
||||
|
||||
const logger = Logger.getLogger(__filename); |
||||
|
||||
/** |
||||
* Display mode constant used when video is being displayed on the small video. |
||||
* @type {number} |
||||
* @constant |
||||
*/ |
||||
const DISPLAY_VIDEO = 0; |
||||
|
||||
/** |
||||
* Display mode constant used when the user's avatar is being displayed on |
||||
* the small video. |
||||
* @type {number} |
||||
* @constant |
||||
*/ |
||||
const DISPLAY_AVATAR = 1; |
||||
|
||||
/** |
||||
* Display mode constant used when neither video nor avatar is being displayed |
||||
* on the small video. And we just show the display name. |
||||
* @type {number} |
||||
* @constant |
||||
*/ |
||||
const DISPLAY_BLACKNESS_WITH_NAME = 2; |
||||
|
||||
/** |
||||
* Display mode constant used when video is displayed and display name |
||||
* at the same time. |
||||
* @type {number} |
||||
* @constant |
||||
*/ |
||||
const DISPLAY_VIDEO_WITH_NAME = 3; |
||||
|
||||
/** |
||||
* Display mode constant used when neither video nor avatar is being displayed |
||||
* on the small video. And we just show the display name. |
||||
* @type {number} |
||||
* @constant |
||||
*/ |
||||
const DISPLAY_AVATAR_WITH_NAME = 4; |
||||
|
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
export default class SmallVideo { |
||||
/** |
||||
* Constructor. |
||||
*/ |
||||
constructor() { |
||||
this.videoIsHovered = false; |
||||
this.videoType = undefined; |
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
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 |
||||
*/ |
||||
getId() { |
||||
return this.id; |
||||
} |
||||
|
||||
/** |
||||
* Indicates if this small video is currently visible. |
||||
* |
||||
* @return <tt>true</tt> if this small video isn't currently visible and |
||||
* <tt>false</tt> - otherwise. |
||||
*/ |
||||
isVisible() { |
||||
return this.$container.is(':visible'); |
||||
} |
||||
|
||||
/** |
||||
* Configures hoverIn/hoverOut handlers. Depends on connection indicator. |
||||
*/ |
||||
bindHoverHandler() { |
||||
// Add hover handler
|
||||
this.$container.hover( |
||||
() => { |
||||
this.videoIsHovered = true; |
||||
this.renderThumbnail(true); |
||||
this.updateView(); |
||||
}, |
||||
() => { |
||||
this.videoIsHovered = false; |
||||
this.renderThumbnail(false); |
||||
this.updateView(); |
||||
} |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders the thumbnail. |
||||
*/ |
||||
renderThumbnail() { |
||||
// Should be implemented by in subclasses.
|
||||
} |
||||
|
||||
/** |
||||
* 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]); |
||||
} |
||||
|
||||
/** |
||||
* 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 <tt>SmallVideo</tt> is currently |
||||
* being displayed on the "large video". |
||||
* |
||||
* @return {boolean} <tt>true</tt> if the user is displayed on the large video |
||||
* or <tt>false</tt> otherwise. |
||||
*/ |
||||
isCurrentlyOnLargeVideo() { |
||||
return APP.store.getState()['features/large-video']?.participantId === this.id; |
||||
} |
||||
|
||||
/** |
||||
* Checks whether there is a playable video stream available for the user |
||||
* associated with this <tt>SmallVideo</tt>. |
||||
* |
||||
* @return {boolean} <tt>true</tt> if there is a playable video stream available |
||||
* or <tt>false</tt> otherwise. |
||||
*/ |
||||
isVideoPlayable() { |
||||
return isVideoPlayable(APP.store.getState(), this.id); |
||||
} |
||||
|
||||
/** |
||||
* Determines what should be display on the thumbnail. |
||||
* |
||||
* @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt> |
||||
* or <tt>DISPLAY_BLACKNESS_WITH_NAME</tt>. |
||||
*/ |
||||
selectDisplayMode(input) { |
||||
if (!input.tileViewActive && input.isScreenSharing) { |
||||
return input.isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR; |
||||
} else if (input.isCurrentlyOnLargeVideo && !input.tileViewActive) { |
||||
// Display name is always and only displayed when user is on the stage
|
||||
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() { |
||||
let isScreenSharing = false; |
||||
let connectionStatus; |
||||
const state = APP.store.getState(); |
||||
const id = this.id; |
||||
const participant = getParticipantById(state, id); |
||||
const isLocal = participant?.local ?? true; |
||||
const tracks = state['features/base/tracks']; |
||||
const videoTrack |
||||
= isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id); |
||||
|
||||
if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) { |
||||
isScreenSharing = videoTrack?.videoType === 'desktop'; |
||||
connectionStatus = participant.connectionStatus; |
||||
} |
||||
|
||||
return { |
||||
isCurrentlyOnLargeVideo: this.isCurrentlyOnLargeVideo(), |
||||
isHovered: this._isHovered(), |
||||
isAudioOnly: APP.conference.isAudioOnly(), |
||||
tileViewActive: shouldDisplayTileView(state), |
||||
isVideoPlayable: this.isVideoPlayable(), |
||||
hasVideo: Boolean(this.selectVideoElement().length), |
||||
connectionStatus, |
||||
canPlayEventReceived: this._canPlayEventReceived, |
||||
videoStream: Boolean(videoTrack), |
||||
isScreenSharing, |
||||
videoStreamMuted: videoTrack ? videoTrack.muted : '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; |
||||
} |
||||
|
||||
/** |
||||
* Updates the css classes of the thumbnail based on the current state. |
||||
*/ |
||||
updateView() { |
||||
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)}]`); |
||||
} |
||||
|
||||
if (this.displayMode !== DISPLAY_VIDEO |
||||
&& this.displayMode !== DISPLAY_VIDEO_WITH_NAME |
||||
&& displayModeInput.tileViewActive |
||||
&& displayModeInput.isScreenSharing |
||||
&& !displayModeInput.isAudioOnly) { |
||||
// send the event
|
||||
sendAnalytics(createScreenSharingIssueEvent({ |
||||
source: 'thumbnail', |
||||
...displayModeInput |
||||
})); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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; |
||||
} |
||||
|
||||
this.$container.toggleClass('active-speaker', show); |
||||
} |
||||
|
||||
/** |
||||
* Initializes 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._unmountThumbnail(); |
||||
|
||||
// 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.updateView(); |
||||
} |
||||
|
||||
/** |
||||
* 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 (event.stopPropagation && triggerPin) { |
||||
event.stopPropagation(); |
||||
event.preventDefault(); |
||||
} |
||||
if (triggerPin) { |
||||
this.togglePin(); |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Returns whether or not a click event is targeted at certain elements which |
||||
* should not trigger a pin. |
||||
* |
||||
* @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 propagation at this
|
||||
// point will prevent DisplayName from getting click events. This workaround
|
||||
// should be removable 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} |
||||
*/ |
||||
togglePin() { |
||||
const pinnedParticipant = getPinnedParticipant(APP.store.getState()) || {}; |
||||
const participantIdToPin = pinnedParticipant && pinnedParticipant.id === this.id ? null : this.id; |
||||
|
||||
APP.store.dispatch(pinParticipant(participantIdToPin)); |
||||
} |
||||
|
||||
/** |
||||
* Unmounts the thumbnail. |
||||
*/ |
||||
_unmountThumbnail() { |
||||
ReactDOM.unmountComponentAtNode(this.container); |
||||
} |
||||
|
||||
/** |
||||
* Sets the size of the thumbnail. |
||||
*/ |
||||
_setThumbnailSize() { |
||||
const layout = getCurrentLayout(APP.store.getState()); |
||||
const heightToWidthPercent = 100 |
||||
/ (this.isLocal ? interfaceConfig.LOCAL_THUMBNAIL_RATIO : interfaceConfig.REMOTE_THUMBNAIL_RATIO); |
||||
|
||||
switch (layout) { |
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: { |
||||
this.$container.css('padding-top', `${heightToWidthPercent}%`); |
||||
break; |
||||
} |
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: { |
||||
const state = APP.store.getState(); |
||||
const { local, remote } = state['features/filmstrip'].horizontalViewDimensions; |
||||
const size = this.isLocal ? local : remote; |
||||
|
||||
if (typeof size !== 'undefined') { |
||||
const { height, width } = size; |
||||
|
||||
this.$container.css({ |
||||
height: `${height}px`, |
||||
'min-height': `${height}px`, |
||||
'min-width': `${width}px`, |
||||
width: `${width}px` |
||||
}); |
||||
} |
||||
break; |
||||
} |
||||
case LAYOUTS.TILE_VIEW: { |
||||
const state = APP.store.getState(); |
||||
const { thumbnailSize } = state['features/filmstrip'].tileViewDimensions; |
||||
|
||||
if (typeof thumbnailSize !== 'undefined') { |
||||
const { height, width } = thumbnailSize; |
||||
|
||||
this.$container.css({ |
||||
height: `${height}px`, |
||||
'min-height': `${height}px`, |
||||
'min-width': `${width}px`, |
||||
width: `${width}px` |
||||
}); |
||||
} |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,5 @@ |
||||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions'; |
||||
|
||||
export default getLogger('features/filmstrip'); |
||||
@ -1,44 +0,0 @@ |
||||
/* @flow */ |
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link RemoteVideoMenu}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The components to place as the body of the {@code RemoteVideoMenu}. |
||||
*/ |
||||
children: React$Node, |
||||
|
||||
/** |
||||
* The id attribute to be added to the component's DOM for retrieval when |
||||
* querying the DOM. Not used directly by the component. |
||||
*/ |
||||
id: string |
||||
}; |
||||
|
||||
/** |
||||
* React {@code Component} responsible for displaying other components as a menu |
||||
* for manipulating remote participant state. |
||||
* |
||||
* @extends {Component} |
||||
*/ |
||||
export default class RemoteVideoMenu extends Component<Props> { |
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<ul |
||||
className = 'popupmenu' |
||||
id = { this.props.id }> |
||||
{ this.props.children } |
||||
</ul> |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,15 @@ |
||||
// @flow
|
||||
import { hideDialog } from '../base/dialog'; |
||||
|
||||
import { RemoteVideoMenu } from './components/native'; |
||||
|
||||
/** |
||||
* Hides the remote video menu. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export function hideRemoteVideoMenu() { |
||||
return hideDialog(RemoteVideoMenu); |
||||
} |
||||
|
||||
export * from './actions.any'; |
||||
@ -0,0 +1,2 @@ |
||||
// @flow
|
||||
export * from './actions.any'; |
||||
@ -0,0 +1,103 @@ |
||||
/* @flow */ |
||||
|
||||
import React, { PureComponent } from 'react'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { updateSettings } from '../../../base/settings'; |
||||
|
||||
import VideoMenuButton from './VideoMenuButton'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link FlipLocalVideoButton}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The current local flip x status. |
||||
*/ |
||||
_localFlipX: boolean, |
||||
|
||||
/** |
||||
* The redux dispatch function. |
||||
*/ |
||||
dispatch: Function, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
}; |
||||
|
||||
/** |
||||
* Implements a React {@link Component} which displays a button for flipping the local viedo. |
||||
* |
||||
* @extends Component |
||||
*/ |
||||
class FlipLocalVideoButton extends PureComponent<Props> { |
||||
/** |
||||
* Initializes a new {@code FlipLocalVideoButton} instance. |
||||
* |
||||
* @param {Object} props - The read-only React Component props with which |
||||
* the new instance is to be initialized. |
||||
*/ |
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onClick = this._onClick.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {null|ReactElement} |
||||
*/ |
||||
render() { |
||||
const { |
||||
t |
||||
} = this.props; |
||||
|
||||
return ( |
||||
<VideoMenuButton |
||||
buttonText = { t('videothumbnail.flip') } |
||||
displayClass = 'fliplink' |
||||
id = 'flipLocalVideoButton' |
||||
onClick = { this._onClick } /> |
||||
); |
||||
} |
||||
|
||||
_onClick: () => void; |
||||
|
||||
/** |
||||
* Flips the local video. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onClick() { |
||||
const { _localFlipX, dispatch } = this.props; |
||||
|
||||
dispatch(updateSettings({ |
||||
localFlipX: !_localFlipX |
||||
})); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated {@code FlipLocalVideoButton}'s props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {Props} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const { localFlipX } = state['features/base/settings']; |
||||
|
||||
return { |
||||
_localFlipX: Boolean(localFlipX) |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(FlipLocalVideoButton)); |
||||
@ -0,0 +1,100 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { Icon, IconMenuThumb } from '../../../base/icons'; |
||||
import { Popover } from '../../../base/popover'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { getLocalVideoTrack } from '../../../base/tracks'; |
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout'; |
||||
|
||||
import FlipLocalVideoButton from './FlipLocalVideoButton'; |
||||
import VideoMenu from './VideoMenu'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of |
||||
* {@link LocalVideoMenuTriggerButton}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The position relative to the trigger the local video menu should display |
||||
* from. Valid values are those supported by AtlasKit |
||||
* {@code InlineDialog}. |
||||
*/ |
||||
_menuPosition: string, |
||||
|
||||
/** |
||||
* Whether to display the Popover as a drawer. |
||||
*/ |
||||
_overflowDrawer: boolean, |
||||
|
||||
/** |
||||
* Shows/hides the local video flip button. |
||||
*/ |
||||
_showLocalVideoFlipButton: boolean |
||||
}; |
||||
|
||||
/** |
||||
* React Component for displaying an icon associated with opening the |
||||
* the video menu for the local participant. |
||||
* |
||||
* @param {Props} props - The props passed to the component. |
||||
* @returns {ReactElement} |
||||
*/ |
||||
function LocalVideoMenuTriggerButton(props: Props) { |
||||
return ( |
||||
props._showLocalVideoFlipButton |
||||
? <Popover |
||||
content = { |
||||
<VideoMenu id = 'localVideoMenu'> |
||||
<FlipLocalVideoButton /> |
||||
</VideoMenu> |
||||
} |
||||
overflowDrawer = { props._overflowDrawer } |
||||
position = { props._menuPosition }> |
||||
<span |
||||
className = 'popover-trigger local-video-menu-trigger'> |
||||
<Icon |
||||
size = '1em' |
||||
src = { IconMenuThumb } |
||||
title = 'Local user controls' /> |
||||
</span> |
||||
</Popover> |
||||
: null |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the Redux state to the associated {@code LocalVideoMenuTriggerButton}'s props. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @private |
||||
* @returns {Props} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const currentLayout = getCurrentLayout(state); |
||||
const { disableLocalVideoFlip } = state['features/base/config']; |
||||
const videoTrack = getLocalVideoTrack(state['features/base/tracks']); |
||||
const { overflowDrawer } = state['features/toolbox']; |
||||
let _menuPosition; |
||||
|
||||
switch (currentLayout) { |
||||
case LAYOUTS.TILE_VIEW: |
||||
_menuPosition = 'left-start'; |
||||
break; |
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: |
||||
_menuPosition = 'left-end'; |
||||
break; |
||||
default: |
||||
_menuPosition = 'auto'; |
||||
} |
||||
|
||||
return { |
||||
_menuPosition, |
||||
_showLocalVideoFlipButton: !disableLocalVideoFlip && videoTrack?.videoType !== 'desktop', |
||||
_overflowDrawer: overflowDrawer |
||||
}; |
||||
} |
||||
|
||||
export default connect(_mapStateToProps)(LocalVideoMenuTriggerButton); |
||||
@ -0,0 +1,51 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link VideoMenu}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The components to place as the body of the {@code VideoMenu}. |
||||
*/ |
||||
children: React$Node, |
||||
|
||||
/** |
||||
* The id attribute to be added to the component's DOM for retrieval when |
||||
* querying the DOM. Not used directly by the component. |
||||
*/ |
||||
id: string |
||||
}; |
||||
|
||||
/** |
||||
* Click handler. |
||||
* |
||||
* @param {SyntheticEvent} event - The click event. |
||||
* @returns {void} |
||||
*/ |
||||
function onClick(event) { |
||||
// If the event is propagated to the thumbnail container the participant will be pinned. That's why the propagation
|
||||
// needs to be stopped.
|
||||
event.stopPropagation(); |
||||
} |
||||
|
||||
/** |
||||
* React {@code Component} responsible for displaying other components as a menu |
||||
* for manipulating participant state. |
||||
* |
||||
* @param {Props} props - The component's props. |
||||
* @returns {Component} |
||||
*/ |
||||
export default function VideoMenu(props: Props) { |
||||
return ( |
||||
<ul |
||||
className = 'popupmenu' |
||||
id = { props.id } |
||||
onClick = { onClick }> |
||||
{ props.children } |
||||
</ul> |
||||
); |
||||
} |
||||
|
||||
Loading…
Reference in new issue