mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-28 07:27:46 +00:00
Compare commits
7 Commits
2987
...
base_sessi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2191e3a28 | ||
|
|
72e3e8593d | ||
|
|
67a8b4915d | ||
|
|
468d4a7150 | ||
|
|
2a01e29fec | ||
|
|
90a64d30dc | ||
|
|
31905d4f63 |
12
config.js
12
config.js
@@ -347,18 +347,6 @@ var config = {
|
||||
// userRegion: "asia"
|
||||
}
|
||||
|
||||
// Options related to end-to-end (participant to participant) ping.
|
||||
// e2eping: {
|
||||
// // The interval in milliseconds at which pings will be sent.
|
||||
// // Defaults to 10000, set to <= 0 to disable.
|
||||
// pingInterval: 10000,
|
||||
//
|
||||
// // The interval in milliseconds at which analytics events
|
||||
// // with the measured RTT will be sent. Defaults to 60000, set
|
||||
// // to <= 0 to disable.
|
||||
// analyticsInterval: 60000,
|
||||
// }
|
||||
|
||||
// List of undocumented settings used in jitsi-meet
|
||||
/**
|
||||
_immediateReloadThreshold
|
||||
|
||||
@@ -14,9 +14,14 @@
|
||||
* Focused video thumbnail.
|
||||
*/
|
||||
&.videoContainerFocused {
|
||||
border: $thumbnailVideoBorder solid $videoThumbnailSelected;
|
||||
transition-duration: 0.5s;
|
||||
-webkit-transition-duration: 0.5s;
|
||||
-webkit-animation-name: greyPulse;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
border: $thumbnailVideoBorder solid $videoThumbnailSelected !important;
|
||||
box-shadow: inset 0 0 3px $videoThumbnailSelected,
|
||||
0 0 3px $videoThumbnailSelected;
|
||||
0 0 3px $videoThumbnailSelected !important;
|
||||
}
|
||||
|
||||
.remotevideomenu > .icon-menu {
|
||||
@@ -26,7 +31,7 @@
|
||||
/**
|
||||
* Hovered video thumbnail.
|
||||
*/
|
||||
&:hover:not(.videoContainerFocused):not(.active-speaker) {
|
||||
&:hover {
|
||||
cursor: hand;
|
||||
border: $thumbnailVideoBorder solid $videoThumbnailHovered;
|
||||
box-shadow: inset 0 0 3px $videoThumbnailHovered,
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* CSS styles that are specific to the filmstrip that shows the thumbnail tiles.
|
||||
*/
|
||||
.tile-view {
|
||||
/**
|
||||
* Add a border around the active speaker to make the thumbnail easier to
|
||||
* see.
|
||||
*/
|
||||
.active-speaker {
|
||||
border: $thumbnailVideoBorder solid $videoThumbnailSelected;
|
||||
box-shadow: inset 0 0 3px $videoThumbnailSelected,
|
||||
0 0 3px $videoThumbnailSelected;
|
||||
}
|
||||
|
||||
#filmstripRemoteVideos {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.filmstrip__videos .videocontainer {
|
||||
&:not(.active-speaker),
|
||||
&:hover:not(.active-speaker) {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
#remoteVideos {
|
||||
/**
|
||||
* Height is modified with an inline style in horizontal filmstrip mode
|
||||
* so !important is used to override that.
|
||||
*/
|
||||
height: 100% !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: $filmstripVideosZ
|
||||
}
|
||||
|
||||
/**
|
||||
* Regardless of the user setting, do not let the filmstrip be in a hidden
|
||||
* state.
|
||||
*/
|
||||
.filmstrip__videos.hidden {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#filmstripRemoteVideos {
|
||||
box-sizing: border-box;
|
||||
|
||||
/**
|
||||
* Allow scrolling of the thumbnails.
|
||||
*/
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of the thumbnails should be set with javascript, based on
|
||||
* desired column count and window width. The rows are created using flex
|
||||
* and allowing the thumbnails to wrap.
|
||||
*/
|
||||
#filmstripRemoteVideosContainer {
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
padding: 100px 0;
|
||||
|
||||
.videocontainer {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
video {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.has-overflow#filmstripRemoteVideosContainer {
|
||||
align-content: baseline;
|
||||
}
|
||||
|
||||
.has-overflow .videocontainer {
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Firefox flex acts a little differently. To make sure the bottom row of
|
||||
* thumbnails is not overlapped by the horizontal toolbar, margin is added
|
||||
* to the local thumbnail to keep it from the bottom of the screen. It is
|
||||
* assumed the local thumbnail will always be on the bottom row.
|
||||
*/
|
||||
.has-overflow #localVideoContainer {
|
||||
margin-bottom: 100px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Various overrides outside of the filmstrip to style the app to support a
|
||||
* tiled thumbnail experience.
|
||||
*/
|
||||
.tile-view {
|
||||
/**
|
||||
* Let the avatar grow with the tile.
|
||||
*/
|
||||
.userAvatar {
|
||||
max-height: initial;
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide various features that should not be displayed while in tile view.
|
||||
*/
|
||||
#dominantSpeaker,
|
||||
#filmstripLocalVideoThumbnail,
|
||||
#largeVideoElementsContainer,
|
||||
#sharedVideo,
|
||||
.filmstrip__toolbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#localConnectionMessage,
|
||||
#remoteConnectionMessage,
|
||||
.watermark {
|
||||
z-index: $filmstripVideosZ + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The follow styling uses !important to override inline styles set with
|
||||
* javascript.
|
||||
*
|
||||
* TODO: These overrides should be more easy to remove and should be removed
|
||||
* when the components are in react so their rendering done declaratively,
|
||||
* making conditional styling easier to apply.
|
||||
*/
|
||||
#largeVideoElementsContainer,
|
||||
#remoteConnectionMessage,
|
||||
#remotePresenceMessage {
|
||||
display: none !important;
|
||||
}
|
||||
#largeVideoContainer {
|
||||
background-color: $defaultBackground !important;
|
||||
}
|
||||
}
|
||||
@@ -72,8 +72,6 @@
|
||||
@import 'filmstrip/filmstrip_toolbar';
|
||||
@import 'filmstrip/horizontal_filmstrip';
|
||||
@import 'filmstrip/small_video';
|
||||
@import 'filmstrip/tile_view';
|
||||
@import 'filmstrip/tile_view_overrides';
|
||||
@import 'filmstrip/vertical_filmstrip';
|
||||
@import 'filmstrip/vertical_filmstrip_overrides';
|
||||
@import 'unsupported-browser/main';
|
||||
|
||||
@@ -48,8 +48,7 @@ var interfaceConfig = {
|
||||
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
|
||||
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
|
||||
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
'tileview'
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts'
|
||||
],
|
||||
|
||||
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
|
||||
@@ -173,12 +172,6 @@ var interfaceConfig = {
|
||||
*/
|
||||
RECENT_LIST_ENABLED: true
|
||||
|
||||
/**
|
||||
* How many columns the tile view can expand to. The respected range is
|
||||
* between 1 and 5.
|
||||
*/
|
||||
// TILE_VIEW_MAX_COLUMNS: 5,
|
||||
|
||||
/**
|
||||
* Specify custom URL for downloading android mobile app.
|
||||
*/
|
||||
|
||||
@@ -102,7 +102,6 @@
|
||||
"shortcuts": "Toggle shortcuts",
|
||||
"speakerStats": "Toggle speaker statistics",
|
||||
"toggleCamera": "Toggle camera",
|
||||
"tileView": "Toggle tile view",
|
||||
"videomute": "Toggle mute video"
|
||||
},
|
||||
"addPeople": "Add people to your call",
|
||||
@@ -145,7 +144,6 @@
|
||||
"raiseHand": "Raise / Lower your hand",
|
||||
"shortcuts": "View shortcuts",
|
||||
"speakerStats": "Speaker stats",
|
||||
"tileViewToggle": "Toggle tile view",
|
||||
"invite": "Invite people"
|
||||
},
|
||||
"chat":{
|
||||
@@ -200,7 +198,6 @@
|
||||
"packetloss": "Packet loss:",
|
||||
"resolution": "Resolution:",
|
||||
"framerate": "Frame rate:",
|
||||
"e2e_rtt": "E2E RTT:",
|
||||
"less": "Show less",
|
||||
"more": "Show more",
|
||||
"address": "Address:",
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
getPinnedParticipant,
|
||||
pinParticipant
|
||||
} from '../react/features/base/participants';
|
||||
import { setTileView } from '../react/features/video-layout';
|
||||
import UIEvents from '../service/UI/UIEvents';
|
||||
import VideoLayout from './UI/videolayout/VideoLayout';
|
||||
|
||||
@@ -118,31 +117,6 @@ class State {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A getter for this object instance to know the state of tile view.
|
||||
*
|
||||
* @returns {boolean} True if tile view is enabled.
|
||||
*/
|
||||
get tileViewEnabled() {
|
||||
return this._tileViewEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* A setter for {@link tileViewEnabled}. Fires a property change event for
|
||||
* other participants to follow.
|
||||
*
|
||||
* @param {boolean} b - Whether or not tile view is enabled.
|
||||
* @returns {void}
|
||||
*/
|
||||
set tileViewEnabled(b) {
|
||||
const oldValue = this._tileViewEnabled;
|
||||
|
||||
if (oldValue !== b) {
|
||||
this._tileViewEnabled = b;
|
||||
this._firePropertyChange('tileViewEnabled', oldValue, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {_propertyChangeCallback} to notify it that {property} had its
|
||||
* value changed from {oldValue} to {newValue}.
|
||||
@@ -215,10 +189,6 @@ class FollowMe {
|
||||
this._sharedDocumentToggled
|
||||
.bind(this, this._UI.getSharedDocumentManager().isVisible());
|
||||
}
|
||||
|
||||
this._tileViewToggled.bind(
|
||||
this,
|
||||
APP.store.getState()['features/video-layout'].tileViewEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,10 +214,6 @@ class FollowMe {
|
||||
this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
|
||||
this._UI.addListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
|
||||
this.sharedDocEventHandler);
|
||||
|
||||
this.tileViewEventHandler = this._tileViewToggled.bind(this);
|
||||
this._UI.addListener(UIEvents.TOGGLED_TILE_VIEW,
|
||||
this.tileViewEventHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,8 +227,6 @@ class FollowMe {
|
||||
this.sharedDocEventHandler);
|
||||
this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
|
||||
this.pinnedEndpointEventHandler);
|
||||
this._UI.removeListener(UIEvents.TOGGLED_TILE_VIEW,
|
||||
this.tileViewEventHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,18 +266,6 @@ class FollowMe {
|
||||
this._local.sharedDocumentVisible = sharedDocumentVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this instance that the tile view mode has been enabled or
|
||||
* disabled.
|
||||
*
|
||||
* @param {boolean} enabled - True if tile view has been enabled, false
|
||||
* if has been disabled.
|
||||
* @returns {void}
|
||||
*/
|
||||
_tileViewToggled(enabled) {
|
||||
this._local.tileViewEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the nextOnStage property value.
|
||||
*
|
||||
@@ -364,8 +316,7 @@ class FollowMe {
|
||||
attributes: {
|
||||
filmstripVisible: local.filmstripVisible,
|
||||
nextOnStage: local.nextOnStage,
|
||||
sharedDocumentVisible: local.sharedDocumentVisible,
|
||||
tileViewEnabled: local.tileViewEnabled
|
||||
sharedDocumentVisible: local.sharedDocumentVisible
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -404,7 +355,6 @@ class FollowMe {
|
||||
this._onFilmstripVisible(attributes.filmstripVisible);
|
||||
this._onNextOnStage(attributes.nextOnStage);
|
||||
this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
|
||||
this._onTileViewEnabled(attributes.tileViewEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -484,21 +434,6 @@ class FollowMe {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a tile view enabled / disabled event received from FOLLOW-ME.
|
||||
*
|
||||
* @param {boolean} enabled - Whether or not tile view should be shown.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTileViewEnabled(enabled) {
|
||||
if (typeof enabled === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.store.dispatch(setTileView(enabled === 'true'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pins / unpins the video thumbnail given by clickId.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* global $, APP */
|
||||
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||
|
||||
/* global $ */
|
||||
import SmallVideo from '../videolayout/SmallVideo';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
@@ -66,9 +64,7 @@ SharedVideoThumb.prototype.createContainer = function(spanId) {
|
||||
* The thumb click handler.
|
||||
*/
|
||||
SharedVideoThumb.prototype.videoClick = function() {
|
||||
if (!shouldDisplayTileView(APP.store.getState())) {
|
||||
this._togglePin();
|
||||
}
|
||||
this._togglePin();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
/* global $, APP, interfaceConfig */
|
||||
|
||||
import { setFilmstripVisible } from '../../../react/features/filmstrip';
|
||||
import {
|
||||
LAYOUTS,
|
||||
getCurrentLayout,
|
||||
getMaxColumnCount,
|
||||
getTileViewGridDimensions,
|
||||
shouldDisplayTileView
|
||||
} from '../../../react/features/video-layout';
|
||||
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import UIUtil from '../util/UIUtil';
|
||||
@@ -240,10 +233,6 @@ const Filmstrip = {
|
||||
* @returns {*|{localVideo, remoteVideo}}
|
||||
*/
|
||||
calculateThumbnailSize() {
|
||||
if (shouldDisplayTileView(APP.store.getState())) {
|
||||
return this._calculateThumbnailSizeForTileView();
|
||||
}
|
||||
|
||||
const availableSizes = this.calculateAvailableSize();
|
||||
const width = availableSizes.availableWidth;
|
||||
const height = availableSizes.availableHeight;
|
||||
@@ -258,10 +247,11 @@ const Filmstrip = {
|
||||
* @returns {{availableWidth: number, availableHeight: number}}
|
||||
*/
|
||||
calculateAvailableSize() {
|
||||
const state = APP.store.getState();
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
const isHorizontalFilmstripView
|
||||
= currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
|
||||
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
|
||||
const thumbs = this.getThumbs(true);
|
||||
const numvids = thumbs.remoteThumbs.length;
|
||||
|
||||
const localVideoContainer = $('#localVideoContainer');
|
||||
|
||||
/**
|
||||
* If the videoAreaAvailableWidth is set we use this one to calculate
|
||||
@@ -278,15 +268,10 @@ const Filmstrip = {
|
||||
- UIUtil.parseCssInt(this.filmstrip.css('borderRightWidth'), 10)
|
||||
- 5;
|
||||
|
||||
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
|
||||
let availableWidth = videoAreaAvailableWidth;
|
||||
|
||||
const thumbs = this.getThumbs(true);
|
||||
|
||||
// If local thumb is not hidden
|
||||
if (thumbs.localThumb) {
|
||||
const localVideoContainer = $('#localVideoContainer');
|
||||
|
||||
availableWidth = Math.floor(
|
||||
videoAreaAvailableWidth - (
|
||||
UIUtil.parseCssInt(
|
||||
@@ -304,12 +289,10 @@ const Filmstrip = {
|
||||
);
|
||||
}
|
||||
|
||||
// If the number of videos is 0 or undefined or we're not in horizontal
|
||||
// If the number of videos is 0 or undefined or we're in vertical
|
||||
// filmstrip mode we don't need to calculate further any adjustments
|
||||
// to width based on the number of videos present.
|
||||
const numvids = thumbs.remoteThumbs.length;
|
||||
|
||||
if (numvids && isHorizontalFilmstripView) {
|
||||
if (numvids && !interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
const remoteVideoContainer = thumbs.remoteThumbs.eq(0);
|
||||
|
||||
availableWidth = Math.floor(
|
||||
@@ -339,10 +322,8 @@ const Filmstrip = {
|
||||
availableHeight
|
||||
= Math.min(maxHeight, window.innerHeight - 18);
|
||||
|
||||
return {
|
||||
availableHeight,
|
||||
availableWidth
|
||||
};
|
||||
return { availableWidth,
|
||||
availableHeight };
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -453,51 +434,6 @@ const Filmstrip = {
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates the size for thumbnails when in tile view layout.
|
||||
*
|
||||
* @returns {{localVideo, remoteVideo}}
|
||||
*/
|
||||
_calculateThumbnailSizeForTileView() {
|
||||
const tileAspectRatio = 16 / 9;
|
||||
|
||||
// The distance from the top and bottom of the screen, as set by CSS, to
|
||||
// avoid overlapping UI elements.
|
||||
const topBottomPadding = 200;
|
||||
|
||||
// Minimum space to keep between the sides of the tiles and the sides
|
||||
// of the window.
|
||||
const sideMargins = 30 * 2;
|
||||
|
||||
const state = APP.store.getState();
|
||||
|
||||
const viewWidth = document.body.clientWidth - sideMargins;
|
||||
const viewHeight = document.body.clientHeight - topBottomPadding;
|
||||
|
||||
const {
|
||||
columns,
|
||||
visibleRows
|
||||
} = getTileViewGridDimensions(state, getMaxColumnCount());
|
||||
const initialWidth = viewWidth / columns;
|
||||
const aspectRatioHeight = initialWidth / tileAspectRatio;
|
||||
|
||||
const heightOfEach = Math.min(
|
||||
aspectRatioHeight,
|
||||
viewHeight / visibleRows);
|
||||
const widthOfEach = tileAspectRatio * heightOfEach;
|
||||
|
||||
return {
|
||||
localVideo: {
|
||||
thumbWidth: widthOfEach,
|
||||
thumbHeight: heightOfEach
|
||||
},
|
||||
remoteVideo: {
|
||||
thumbWidth: widthOfEach,
|
||||
thumbHeight: heightOfEach
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes thumbnails
|
||||
* @param local
|
||||
@@ -507,28 +443,6 @@ const Filmstrip = {
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
resizeThumbnails(local, remote, forceUpdate = false) {
|
||||
const state = APP.store.getState();
|
||||
|
||||
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 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.toggleClass('has-overflow', hasOverflow);
|
||||
} else {
|
||||
this.filmstripRemoteVideos.css('width', '');
|
||||
}
|
||||
|
||||
const thumbs = this.getThumbs(!forceUpdate);
|
||||
|
||||
if (thumbs.localThumb) {
|
||||
@@ -552,15 +466,13 @@ const Filmstrip = {
|
||||
});
|
||||
}
|
||||
|
||||
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||
|
||||
// Let CSS take care of height in vertical filmstrip mode.
|
||||
if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
$('#filmstripLocalVideo').css({
|
||||
// adds 4 px because of small video 2px border
|
||||
width: `${local.thumbWidth + 4}px`
|
||||
});
|
||||
} else if (currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
|
||||
} else {
|
||||
this.filmstrip.css({
|
||||
// adds 4 px because of small video 2px border
|
||||
height: `${remote.thumbHeight + 4}px`
|
||||
|
||||
@@ -11,7 +11,6 @@ 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);
|
||||
@@ -27,7 +26,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
|
||||
this.streamEndedCallback = streamEndedCallback;
|
||||
this.container = this.createContainer();
|
||||
this.$container = $(this.container);
|
||||
this.updateDOMLocation();
|
||||
$('#filmstripLocalVideoThumbnail').append(this.container);
|
||||
|
||||
this.localVideoId = null;
|
||||
this.bindHoverHandler();
|
||||
@@ -110,7 +109,16 @@ LocalVideo.prototype.changeVideo = function(stream) {
|
||||
|
||||
this.localVideoId = `localVideo_${stream.getId()}`;
|
||||
|
||||
this._updateVideoElement();
|
||||
const localVideoContainer = document.getElementById('localVideoWrapper');
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<VideoTrack
|
||||
id = { this.localVideoId }
|
||||
videoTrack = {{ jitsiTrack: stream }} />
|
||||
</Provider>,
|
||||
localVideoContainer
|
||||
);
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const isVideo = stream.videoType != 'desktop';
|
||||
@@ -120,14 +128,12 @@ LocalVideo.prototype.changeVideo = function(stream) {
|
||||
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()) {
|
||||
if (this.videoStream.isEnded()) {
|
||||
ReactDOM.unmountComponentAtNode(localVideoContainer);
|
||||
}
|
||||
|
||||
@@ -229,29 +235,6 @@ LocalVideo.prototype._enableDisableContextMenu = function(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
|
||||
@@ -275,9 +258,7 @@ LocalVideo.prototype._onContainerClick = function(event) {
|
||||
= $source.parents('.displayNameContainer').length > 0;
|
||||
const clickedOnPopover = $source.parents('.popover').length > 0
|
||||
|| classList.contains('popover');
|
||||
const ignoreClick = clickedOnDisplayName
|
||||
|| clickedOnPopover
|
||||
|| shouldDisplayTileView(APP.store.getState());
|
||||
const ignoreClick = clickedOnDisplayName || clickedOnPopover;
|
||||
|
||||
if (event.stopPropagation && !ignoreClick) {
|
||||
event.stopPropagation();
|
||||
@@ -288,28 +269,4 @@ LocalVideo.prototype._onContainerClick = function(event) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
};
|
||||
|
||||
export default LocalVideo;
|
||||
|
||||
@@ -20,11 +20,6 @@ import {
|
||||
REMOTE_CONTROL_MENU_STATES,
|
||||
RemoteVideoMenuTriggerButton
|
||||
} from '../../../react/features/remote-video-menu';
|
||||
import {
|
||||
LAYOUTS,
|
||||
getCurrentLayout,
|
||||
shouldDisplayTileView
|
||||
} from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
@@ -168,17 +163,8 @@ RemoteVideo.prototype._generatePopupContent = function() {
|
||||
const onVolumeChange = 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';
|
||||
}
|
||||
const menuPosition = interfaceConfig.VERTICAL_FILMSTRIP
|
||||
? 'left bottom' : 'top center';
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
@@ -188,7 +174,7 @@ RemoteVideo.prototype._generatePopupContent = function() {
|
||||
initialVolumeValue = { initialVolumeValue }
|
||||
isAudioMuted = { this.isAudioMuted }
|
||||
isModerator = { isModerator }
|
||||
menuPosition = { remoteMenuPosition }
|
||||
menuPosition = { menuPosition }
|
||||
onMenuDisplay
|
||||
= {this._onRemoteVideoMenuDisplay.bind(this)}
|
||||
onRemoteControlToggle = { onRemoteControlToggle }
|
||||
@@ -627,8 +613,7 @@ RemoteVideo.prototype._onContainerClick = function(event) {
|
||||
const { classList } = event.target;
|
||||
|
||||
const ignoreClick = $source.parents('.popover').length > 0
|
||||
|| classList.contains('popover')
|
||||
|| shouldDisplayTileView(APP.store.getState());
|
||||
|| classList.contains('popover');
|
||||
|
||||
if (!ignoreClick) {
|
||||
this._togglePin();
|
||||
|
||||
@@ -27,11 +27,6 @@ import {
|
||||
RaisedHandIndicator,
|
||||
VideoMutedIndicator
|
||||
} from '../../../react/features/filmstrip';
|
||||
import {
|
||||
LAYOUTS,
|
||||
getCurrentLayout,
|
||||
shouldDisplayTileView
|
||||
} from '../../../react/features/video-layout';
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
@@ -333,21 +328,7 @@ SmallVideo.prototype.setVideoMutedView = function(isMuted) {
|
||||
SmallVideo.prototype.updateStatusBar = function() {
|
||||
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';
|
||||
}
|
||||
const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
@@ -566,8 +547,7 @@ SmallVideo.prototype.isVideoPlayable = function() {
|
||||
*/
|
||||
SmallVideo.prototype.selectDisplayMode = function() {
|
||||
// Display name is always and only displayed when user is on the stage
|
||||
if (this.isCurrentlyOnLargeVideo()
|
||||
&& !shouldDisplayTileView(APP.store.getState())) {
|
||||
if (this.isCurrentlyOnLargeVideo()) {
|
||||
return this.isVideoPlayable() && !APP.conference.isAudioOnly()
|
||||
? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
|
||||
} else if (this.isVideoPlayable()
|
||||
@@ -705,10 +685,7 @@ SmallVideo.prototype.showDominantSpeakerIndicator = function(show) {
|
||||
|
||||
this._showDominantSpeaker = show;
|
||||
|
||||
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
|
||||
|
||||
this.updateIndicators();
|
||||
this.updateView();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -788,18 +765,6 @@ SmallVideo.prototype.initBrowserSpecificProperties = function() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -819,19 +784,7 @@ SmallVideo.prototype.updateIndicators = function() {
|
||||
const iconSize = UIUtil.getIndicatorFontSize();
|
||||
const showConnectionIndicator = this.videoIsHovered
|
||||
|| !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
|
||||
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||
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';
|
||||
}
|
||||
const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
@@ -846,7 +799,7 @@ SmallVideo.prototype.updateIndicators = function() {
|
||||
enableStatsDisplay
|
||||
= { !interfaceConfig.filmStripOnly }
|
||||
statsPopoverPosition
|
||||
= { statsPopoverPosition }
|
||||
= { this.statsPopoverLocation }
|
||||
userID = { this.id } />
|
||||
: null }
|
||||
{ this._showRaisedHand
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
/* global APP, $, interfaceConfig */
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
import {
|
||||
getNearestReceiverVideoQualityLevel,
|
||||
setMaxReceiverVideoQuality
|
||||
} from '../../../react/features/base/conference';
|
||||
import {
|
||||
JitsiParticipantConnectionStatus
|
||||
} from '../../../react/features/base/lib-jitsi-meet';
|
||||
@@ -13,9 +9,6 @@ import {
|
||||
getPinnedParticipant,
|
||||
pinParticipant
|
||||
} from '../../../react/features/base/participants';
|
||||
import {
|
||||
shouldDisplayTileView
|
||||
} from '../../../react/features/video-layout';
|
||||
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
|
||||
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
|
||||
|
||||
@@ -601,19 +594,12 @@ const VideoLayout = {
|
||||
|
||||
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
|
||||
|
||||
if (shouldDisplayTileView(APP.store.getState())) {
|
||||
const height
|
||||
= (localVideo && localVideo.thumbHeight)
|
||||
|| (remoteVideo && remoteVideo.thumbnHeight)
|
||||
|| 0;
|
||||
const qualityLevel = getNearestReceiverVideoQualityLevel(height);
|
||||
|
||||
APP.store.dispatch(setMaxReceiverVideoQuality(qualityLevel));
|
||||
}
|
||||
|
||||
if (onComplete && typeof onComplete === 'function') {
|
||||
onComplete();
|
||||
}
|
||||
|
||||
return { localVideo,
|
||||
remoteVideo };
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1156,22 +1142,6 @@ const VideoLayout = {
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to invoke when the video layout has changed and elements
|
||||
* have to be re-arranged and resized.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
refreshLayout() {
|
||||
localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
|
||||
VideoLayout.resizeVideoArea();
|
||||
|
||||
localVideoThumbnail && localVideoThumbnail.rerender();
|
||||
Object.values(remoteVideos).forEach(
|
||||
remoteVideo => remoteVideo.rerender()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers an update of large video if the passed in participant is
|
||||
* currently displayed on large video.
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -9719,8 +9719,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#4a28a196160411d657518022de8bded7c02ad679",
|
||||
"from": "github:jitsi/lib-jitsi-meet#4a28a196160411d657518022de8bded7c02ad679",
|
||||
"version": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
|
||||
"from": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
|
||||
"requires": {
|
||||
"@jitsi/sdp-interop": "0.1.13",
|
||||
"@jitsi/sdp-simulcast": "0.2.1",
|
||||
@@ -14298,9 +14298,9 @@
|
||||
}
|
||||
},
|
||||
"sdp": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.8.0.tgz",
|
||||
"integrity": "sha512-wRSES07rAwKWAR7aev9UuClT7kdf9ZTdeUK5gTgHue9vlhs19Fbm3ccNEGJO4y2IitH4/JzS4sdzyPl6H2KQLw=="
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.7.4.tgz",
|
||||
"integrity": "sha512-0+wTfgvUUEGcvvFoHIC0aiGbx6gzwAUm8FkKt5Oqqkjf9mEEDLgwnoDKX7MYTGXrNNwzikVbutJ+OVNAGmJBQw=="
|
||||
},
|
||||
"sdp-transform": {
|
||||
"version": "2.3.0",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"jsc-android": "224109.1.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#4a28a196160411d657518022de8bded7c02ad679",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
|
||||
"lodash": "4.17.4",
|
||||
"moment": "2.19.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
AVATAR_ID_COMMAND,
|
||||
AVATAR_URL_COMMAND,
|
||||
EMAIL_COMMAND,
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
VIDEO_QUALITY_LEVELS
|
||||
JITSI_CONFERENCE_URL_KEY
|
||||
} from './constants';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
@@ -103,38 +102,6 @@ export function getCurrentConference(stateful: Function | Object) {
|
||||
: joining);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the nearest match for the passed in {@link availableHeight} to am
|
||||
* enumerated value in {@code VIDEO_QUALITY_LEVELS}.
|
||||
*
|
||||
* @param {number} availableHeight - The height to which a matching video
|
||||
* quality level should be found.
|
||||
* @returns {number} The closest matching value from
|
||||
* {@code VIDEO_QUALITY_LEVELS}.
|
||||
*/
|
||||
export function getNearestReceiverVideoQualityLevel(availableHeight: number) {
|
||||
const qualityLevels = [
|
||||
VIDEO_QUALITY_LEVELS.HIGH,
|
||||
VIDEO_QUALITY_LEVELS.STANDARD,
|
||||
VIDEO_QUALITY_LEVELS.LOW
|
||||
];
|
||||
|
||||
let selectedLevel = qualityLevels[0];
|
||||
|
||||
for (let i = 1; i < qualityLevels.length; i++) {
|
||||
const previousValue = qualityLevels[i - 1];
|
||||
const currentValue = qualityLevels[i];
|
||||
const diffWithCurrent = Math.abs(availableHeight - currentValue);
|
||||
const diffWithPrevious = Math.abs(availableHeight - previousValue);
|
||||
|
||||
if (diffWithCurrent < diffWithPrevious) {
|
||||
selectedLevel = currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
return selectedLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
|
||||
* manipulating a conference participant (e.g. pin or select participant).
|
||||
|
||||
@@ -96,7 +96,6 @@ const WHITELISTED_KEYS = [
|
||||
'disableRtx',
|
||||
'disableSuspendVideo',
|
||||
'displayJids',
|
||||
'e2eping',
|
||||
'enableDisplayNameInStats',
|
||||
'enableLayerSuspension',
|
||||
'enableLipSync',
|
||||
|
||||
@@ -37,6 +37,9 @@ const INITIAL_RN_STATE = {
|
||||
// fastest to merely disable them.
|
||||
disableAudioLevels: true,
|
||||
|
||||
// FIXME flow complains about missing 'locationURL' missing in _setConfig
|
||||
locationURL: undefined,
|
||||
|
||||
p2p: {
|
||||
disableH264: false,
|
||||
preferH264: true
|
||||
@@ -126,8 +129,10 @@ function _setConfig(state, { config }) {
|
||||
|
||||
const newState = _.merge(
|
||||
{},
|
||||
config,
|
||||
{ error: undefined },
|
||||
config, {
|
||||
error: undefined,
|
||||
locationURL: state.locationURL
|
||||
},
|
||||
|
||||
// The config of _getInitialState() is meant to override the config
|
||||
// downloaded from the Jitsi Meet deployment because the former contains
|
||||
|
||||
@@ -14,7 +14,6 @@ export const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
export const JitsiConnectionEvents = JitsiMeetJS.events.connection;
|
||||
export const JitsiConnectionQualityEvents
|
||||
= JitsiMeetJS.events.connectionQuality;
|
||||
export const JitsiE2ePingEvents = JitsiMeetJS.events.e2eping;
|
||||
export const JitsiMediaDevicesEvents = JitsiMeetJS.events.mediaDevices;
|
||||
export const JitsiParticipantConnectionStatus
|
||||
= JitsiMeetJS.constants.participantConnectionStatus;
|
||||
|
||||
14
react/features/base/session/actionTypes.js
Normal file
14
react/features/base/session/actionTypes.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* {
|
||||
* type: SET_SESSION,
|
||||
* session: {
|
||||
* url: {string},
|
||||
* state: {string},
|
||||
* ...data
|
||||
* }
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_SESSION = Symbol('SET_SESSION');
|
||||
16
react/features/base/session/actions.js
Normal file
16
react/features/base/session/actions.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { SET_SESSION } from './actionTypes';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {string} session - FIXME.
|
||||
* @returns {{
|
||||
* type: SET_SESSION
|
||||
* }}
|
||||
*/
|
||||
export function setSession(session) {
|
||||
return {
|
||||
type: SET_SESSION,
|
||||
session
|
||||
};
|
||||
}
|
||||
12
react/features/base/session/constants.js
Normal file
12
react/features/base/session/constants.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
export const SESSION_CONFIGURED = Symbol('SESSION_CONFIGURED');
|
||||
|
||||
export const SESSION_ENDED = Symbol('SESSION_ENDED');
|
||||
|
||||
export const SESSION_FAILED = Symbol('SESSION_FAILED');
|
||||
|
||||
export const SESSION_STARTED = Symbol('SESSION_STARTED');
|
||||
|
||||
export const SESSION_WILL_END = Symbol('SESSION_WILL_END');
|
||||
|
||||
export const SESSION_WILL_START = Symbol('SESSION_WILL_START');
|
||||
36
react/features/base/session/functions.js
Normal file
36
react/features/base/session/functions.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// @flow
|
||||
|
||||
import { toState } from '../redux';
|
||||
import { toURLString } from '../util';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Function|Object} stateful - FIXME.
|
||||
* @param {string} url - FIXME.
|
||||
* @returns {*}
|
||||
*/
|
||||
export function getSession(stateful: Function | Object, url: string): ?Object {
|
||||
const state = toState(stateful);
|
||||
|
||||
const session = state['features/base/session'].get(url);
|
||||
|
||||
if (!session) {
|
||||
console.info(`SESSION NOT FOUND FOR URL: ${url}`);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Function | Object} stateful - FIXME.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getCurrentSession(stateful: Function | Object): ?Object {
|
||||
const state = toState(stateful);
|
||||
const { locationURL } = state['features/base/config'];
|
||||
|
||||
return getSession(state, toURLString(locationURL));
|
||||
}
|
||||
7
react/features/base/session/index.js
Normal file
7
react/features/base/session/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './constants';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
349
react/features/base/session/middleware.js
Normal file
349
react/features/base/session/middleware.js
Normal file
@@ -0,0 +1,349 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
isRoomValid
|
||||
} from '../../base/conference';
|
||||
import {
|
||||
CONNECTION_DISCONNECTED,
|
||||
CONNECTION_FAILED,
|
||||
CONNECTION_WILL_CONNECT
|
||||
} from '../../base/connection';
|
||||
import {
|
||||
MiddlewareRegistry,
|
||||
toState
|
||||
} from '../../base/redux';
|
||||
import { parseURIString, toURLString } from '../../base/util';
|
||||
|
||||
import {
|
||||
SESSION_CONFIGURED,
|
||||
SESSION_ENDED,
|
||||
SESSION_FAILED,
|
||||
SESSION_STARTED,
|
||||
SESSION_WILL_END,
|
||||
SESSION_WILL_START
|
||||
} from './constants';
|
||||
import { setSession } from './actions';
|
||||
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from '../config';
|
||||
import { getCurrentSession, getSession } from './functions';
|
||||
|
||||
/**
|
||||
* Middleware that maintains conference sessions. The features spans across
|
||||
* three features strictly related to the conference lifecycle.
|
||||
* The first one is the base/config which configures the session. It's
|
||||
* 'locationURL' state is used to tell what's the current conference URL the app
|
||||
* is working with. The session starts as soon as {@link CONFIG_WILL_LOAD} event
|
||||
* arrives. The {@code locationURL} instance is stored in the session to
|
||||
* associate the load config request with the session and be able to distinguish
|
||||
* between the current and outdated load config request failures. After the
|
||||
* config is loaded the lifecycle moves to the base/connection feature which
|
||||
* creates a {@code JitsiConnection} and tries to connect to the server. On
|
||||
* {@code CONNECTION_WILL_CONNECT} the connection instance is stored in the
|
||||
* session and used later to filter the events similar to what's done for
|
||||
* the load config requests. The base/conference feature adds the last part to
|
||||
* the session's lifecycle. A {@code JitsiConference} instance is stored in the
|
||||
* session on the {@code CONFERENCE_WILL_JOIN} action. A session is considered
|
||||
* alive as long as either connection or conference is available and
|
||||
* operational.
|
||||
*
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
const { type } = action;
|
||||
|
||||
switch (type) {
|
||||
case CONFERENCE_WILL_JOIN: {
|
||||
const { conference } = action;
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
const url = toURLString(locationURL);
|
||||
const session = getSession(store, url);
|
||||
|
||||
if (session) {
|
||||
store.dispatch(setSession({
|
||||
url: session.url,
|
||||
conference
|
||||
}));
|
||||
} else {
|
||||
console.info(`IGNORED WILL_JOIN FOR: ${url}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED: {
|
||||
const { conference } = action;
|
||||
const session = findSessionForConference(store, conference);
|
||||
const state = session && session.state;
|
||||
|
||||
if (state === SESSION_CONFIGURED) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
|
||||
// Flow complains that the session can be undefined, but it
|
||||
// can't if the state is defined.
|
||||
// $FlowExpectedError
|
||||
url: session.url,
|
||||
state: SESSION_STARTED
|
||||
}));
|
||||
} else {
|
||||
// eslint-disable-next-line max-len
|
||||
console.info(`IGNORED CONF JOINED FOR: ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_FAILED: {
|
||||
const { conference, error } = action;
|
||||
const session = findSessionForConference(store, conference);
|
||||
|
||||
// FIXME update comments
|
||||
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
|
||||
// prevented the user from joining a specific conference but the app may
|
||||
// be able to eventually join the conference. For example, the app will
|
||||
// ask the user for a password upon
|
||||
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
|
||||
// conference afterwards. Such errors are to not reach the native
|
||||
// counterpart of the External API (or at least not in the
|
||||
// fatality/finality semantics attributed to
|
||||
// conferenceFailed:/onConferenceFailed).
|
||||
if (session) {
|
||||
if (!error || isGameOver(store, session, error)) {
|
||||
if (session.connection) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
conference: undefined
|
||||
}));
|
||||
} else {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
state: error ? SESSION_FAILED : SESSION_ENDED,
|
||||
error
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line max-len
|
||||
console.info(`IGNORED FAILED/LEFT for ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`, error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// NOTE WILL_JOIN is fired on SET_ROOM
|
||||
// case CONFERENCE_WILL_JOIN:
|
||||
case CONFERENCE_WILL_LEAVE: {
|
||||
const { conference } = action;
|
||||
const url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
|
||||
const session = findSessionForConference(store, conference);
|
||||
const state = session && session.state;
|
||||
|
||||
if (state && state !== SESSION_WILL_END) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
|
||||
// Flow complains that the session can be undefined, but it
|
||||
// can't if the state is defined.
|
||||
// $FlowExpectedError
|
||||
url: session.url,
|
||||
state: SESSION_WILL_END
|
||||
}));
|
||||
} else {
|
||||
console.info(`IGNORED WILL LEAVE FOR ${url}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONNECTION_WILL_CONNECT: {
|
||||
const { connection } = action;
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
const url = toURLString(locationURL);
|
||||
const session = getSession(store, url);
|
||||
|
||||
if (session) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
connection,
|
||||
conference: undefined // Detach from the old conference
|
||||
}));
|
||||
} else {
|
||||
console.info(`IGNORED CONNECTION_WILL_CONNECT FOR: ${url}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONNECTION_DISCONNECTED:
|
||||
case CONNECTION_FAILED: {
|
||||
const { connection, error } = action;
|
||||
const session = findSessionForConnection(store, connection);
|
||||
|
||||
if (session) {
|
||||
// Remove connection from the session, but wait for
|
||||
// the conference to be removed as well.
|
||||
if (!error || isGameOver(store, session, error)) {
|
||||
if (session.conference) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
connection: undefined
|
||||
}));
|
||||
} else {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
state: error ? SESSION_FAILED : SESSION_ENDED,
|
||||
error
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.info('Ignored DISCONNECTED/FAILED for connection');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_CONFIG: {
|
||||
// XXX SET_CONFIG IS ALWAYS RELEVANT
|
||||
const { locationURL } = store.getState()['features/base/config'];
|
||||
const url = toURLString(locationURL);
|
||||
const session = getSession(store, url);
|
||||
const state = session && session.state;
|
||||
|
||||
if (state === SESSION_WILL_START) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url,
|
||||
state: SESSION_CONFIGURED
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFIG_WILL_LOAD: {
|
||||
const { locationURL } = action;
|
||||
const url = toURLString(locationURL);
|
||||
const session = getSession(store, url);
|
||||
|
||||
// The back and forth to string conversion is here, because there's no
|
||||
// guarantee that the locationURL will be the exact custom structure
|
||||
// which contains the room property.
|
||||
let { room } = parseURIString(url);
|
||||
|
||||
// Validate the room
|
||||
room = isRoomValid(room) ? room : undefined;
|
||||
|
||||
if (room && !session) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url,
|
||||
state: SESSION_WILL_START,
|
||||
locationURL,
|
||||
room
|
||||
}));
|
||||
} else if (room && session) {
|
||||
// Update to the new locationURL instance
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url,
|
||||
locationURL
|
||||
}));
|
||||
} else {
|
||||
console.info(`IGNORED CFG WILL LOAD FOR ${url}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case LOAD_CONFIG_ERROR: {
|
||||
const { error, locationURL } = action;
|
||||
const url = toURLString(locationURL);
|
||||
const session = getSession(store, url);
|
||||
|
||||
if (session && session.locationURL === locationURL) {
|
||||
if (isGameOver(store, session, error)) {
|
||||
store.dispatch(
|
||||
setSession({
|
||||
url,
|
||||
state: SESSION_FAILED,
|
||||
error
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
console.info(`IGNORED LOAD_CONFIG_ERROR FOR: ${url}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* FIXME A session is to be terminated either when the recoverable flag is
|
||||
* explicitly set to {@code false} or if the error arrives for a session which
|
||||
* is no longer current (the app has started working with another session).
|
||||
* This can happen when a conference which is being disconnected fails in which
|
||||
* case the session needs to be ended even if the flag is not {@code false}
|
||||
* because we know that there's no fatal error handling. This is kind of
|
||||
* a contract between the fatal error feature and the session which probably
|
||||
* indicates that the fatal error detection and handling should be incorporated
|
||||
* into the session feature.
|
||||
*
|
||||
* @param {Object | Function} stateful - FIXME.
|
||||
* @param {Object} session - FIXME.
|
||||
* @param {Object} error - FIXME.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isGameOver(stateful, session, error) {
|
||||
return getCurrentSession(stateful) !== session
|
||||
|| error.recoverable === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object | Function} stateful - FIXME.
|
||||
* @param {JitsiConnection} connection - FIXME.
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
function findSessionForConnection(stateful, connection) {
|
||||
const state = toState(stateful);
|
||||
|
||||
for (const session of state['features/base/session'].values()) {
|
||||
if (session.connection === connection) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
console.info('Session not found for a connection');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object | Function} stateful - FIXME.
|
||||
* @param {JitsiConference} conference - FIXME.
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
function findSessionForConference(stateful, conference) {
|
||||
const state = toState(stateful);
|
||||
|
||||
for (const session of state['features/base/session'].values()) {
|
||||
if (session.conference === conference) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
console.info('Session not found for a conference');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
71
react/features/base/session/reducer.js
Normal file
71
react/features/base/session/reducer.js
Normal file
@@ -0,0 +1,71 @@
|
||||
// @flow
|
||||
|
||||
import { assign, ReducerRegistry } from '../../base/redux';
|
||||
import { getSymbolDescription } from '../util';
|
||||
|
||||
import { SET_SESSION } from './actionTypes';
|
||||
import {
|
||||
SESSION_FAILED,
|
||||
SESSION_ENDED,
|
||||
SESSION_WILL_START
|
||||
} from './constants';
|
||||
|
||||
ReducerRegistry.register('features/base/session',
|
||||
(state = new Map(), action) => {
|
||||
switch (action.type) {
|
||||
case SET_SESSION:
|
||||
return _setSession(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object} featureState - FIXME.
|
||||
* @param {Object} action - FIXME.
|
||||
* @returns {Map<any, any>} - FIXME.
|
||||
* @private
|
||||
*/
|
||||
function _setSession(featureState, action) {
|
||||
const { url, state, ...data } = action.session;
|
||||
const session = featureState.get(url);
|
||||
const nextState = new Map(featureState);
|
||||
|
||||
// Drop the whole action if the url is not defined
|
||||
if (!url) {
|
||||
console.error('SET SESSION - NO URL');
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
if (session) {
|
||||
if (state === SESSION_ENDED || state === SESSION_FAILED) {
|
||||
nextState.delete(url);
|
||||
} else {
|
||||
nextState.set(
|
||||
url,
|
||||
assign(session, {
|
||||
url,
|
||||
state: state ? state : session.state,
|
||||
...data
|
||||
}));
|
||||
}
|
||||
} else if (state === SESSION_WILL_START) {
|
||||
nextState.set(
|
||||
url, {
|
||||
url,
|
||||
state,
|
||||
...data
|
||||
});
|
||||
}
|
||||
console.info(
|
||||
'SESSION STATE REDUCED: ',
|
||||
new Map(nextState),
|
||||
url,
|
||||
state && getSymbolDescription(state),
|
||||
action.session.error);
|
||||
|
||||
return nextState;
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { connect as reactReduxConnect } from 'react-redux';
|
||||
|
||||
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
|
||||
|
||||
import { obtainConfig } from '../../base/config';
|
||||
import { connect, disconnect } from '../../base/connection';
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
@@ -15,12 +13,6 @@ import { CalleeInfoContainer } from '../../invite';
|
||||
import { LargeVideo } from '../../large-video';
|
||||
import { NotificationsContainer } from '../../notifications';
|
||||
import { SidePanel } from '../../side-panel';
|
||||
import {
|
||||
LAYOUTS,
|
||||
getCurrentLayout,
|
||||
shouldDisplayTileView
|
||||
} from '../../video-layout';
|
||||
|
||||
import { default as Notice } from './Notice';
|
||||
import {
|
||||
Toolbox,
|
||||
@@ -57,10 +49,9 @@ const FULL_SCREEN_EVENTS = [
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
const LAYOUT_CLASSNAMES = {
|
||||
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
|
||||
[LAYOUTS.TILE_VIEW]: 'tile-view',
|
||||
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip'
|
||||
const LAYOUT_CLASSES = {
|
||||
HORIZONTAL_FILMSTRIP: 'horizontal-filmstrip',
|
||||
VERTICAL_FILMSTRIP: 'vertical-filmstrip'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -77,18 +68,13 @@ type Props = {
|
||||
* The CSS class to apply to the root of {@link Conference} to modify the
|
||||
* application layout.
|
||||
*/
|
||||
_layoutClassName: string,
|
||||
_layoutModeClassName: string,
|
||||
|
||||
/**
|
||||
* Conference room name.
|
||||
*/
|
||||
_room: string,
|
||||
|
||||
/**
|
||||
* Whether or not the current UI layout should be in tile view.
|
||||
*/
|
||||
_shouldDisplayTileView: boolean,
|
||||
|
||||
dispatch: Function,
|
||||
t: Function
|
||||
}
|
||||
@@ -157,25 +143,6 @@ class Conference extends Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls into legacy UI to update the application layout, if necessary.
|
||||
*
|
||||
* @inheritdoc
|
||||
* returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props._shouldDisplayTileView
|
||||
=== prevProps._shouldDisplayTileView) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: For now VideoLayout is being called as LargeVideo and Filmstrip
|
||||
// sizing logic is still handled outside of React. Once all components
|
||||
// are in react they should calculate size on their own as much as
|
||||
// possible and pass down sizings.
|
||||
VideoLayout.refreshLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the conference when component will be
|
||||
* unmounted.
|
||||
@@ -213,7 +180,7 @@ class Conference extends Component<Props> {
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { this.props._layoutClassName }
|
||||
className = { this.props._layoutModeClassName }
|
||||
id = 'videoconference_page'
|
||||
onMouseMove = { this._onShowToolbar }>
|
||||
<Notice />
|
||||
@@ -290,19 +257,29 @@ class Conference extends Component<Props> {
|
||||
* @private
|
||||
* @returns {{
|
||||
* _iAmRecorder: boolean,
|
||||
* _layoutClassName: string,
|
||||
* _room: ?string,
|
||||
* _shouldDisplayTileView: boolean
|
||||
* _room: ?string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
const { room } = state['features/base/conference'];
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_iAmRecorder: state['features/base/config'].iAmRecorder,
|
||||
_layoutClassName: LAYOUT_CLASSNAMES[currentLayout],
|
||||
_room: state['features/base/conference'].room,
|
||||
_shouldDisplayTileView: shouldDisplayTileView(state)
|
||||
/**
|
||||
* Whether the local participant is recording the conference.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_iAmRecorder: iAmRecorder,
|
||||
|
||||
_layoutModeClassName: interfaceConfig.VERTICAL_FILMSTRIP
|
||||
? LAYOUT_CLASSES.VERTICAL_FILMSTRIP
|
||||
: LAYOUT_CLASSES.HORIZONTAL_FILMSTRIP,
|
||||
|
||||
/**
|
||||
* Conference room name.
|
||||
*/
|
||||
_room: room
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -324,7 +324,6 @@ class ConnectionIndicator extends Component {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onStatsUpdated(stats = {}) {
|
||||
// Rely on React to batch setState actions.
|
||||
const { connectionQuality } = stats;
|
||||
const newPercentageState = typeof connectionQuality === 'undefined'
|
||||
? {} : { percent: connectionQuality };
|
||||
@@ -338,6 +337,7 @@ class ConnectionIndicator extends Component {
|
||||
stats: newStats
|
||||
});
|
||||
|
||||
// Rely on React to batch setState actions.
|
||||
this._updateIndicatorAutoHide(newStats.percent);
|
||||
}
|
||||
|
||||
@@ -410,10 +410,8 @@ class ConnectionIndicator extends Component {
|
||||
const {
|
||||
bandwidth,
|
||||
bitrate,
|
||||
e2eRtt,
|
||||
framerate,
|
||||
packetLoss,
|
||||
region,
|
||||
resolution,
|
||||
transport
|
||||
} = this.state.stats;
|
||||
@@ -423,12 +421,10 @@ class ConnectionIndicator extends Component {
|
||||
bandwidth = { bandwidth }
|
||||
bitrate = { bitrate }
|
||||
connectionSummary = { this._getConnectionStatusTip() }
|
||||
e2eRtt = { e2eRtt }
|
||||
framerate = { framerate }
|
||||
isLocalVideo = { this.props.isLocalVideo }
|
||||
onShowMore = { this._onToggleShowMore }
|
||||
packetLoss = { packetLoss }
|
||||
region = { region }
|
||||
resolution = { resolution }
|
||||
shouldShowMore = { this.state.showMoreStats }
|
||||
transport = { transport } />
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
JitsiConnectionQualityEvents,
|
||||
JitsiE2ePingEvents
|
||||
} from '../base/lib-jitsi-meet';
|
||||
import { JitsiConnectionQualityEvents } from '../base/lib-jitsi-meet';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -36,17 +33,6 @@ const statsEmitter = {
|
||||
|
||||
conference.on(JitsiConnectionQualityEvents.REMOTE_STATS_UPDATED,
|
||||
(id, stats) => this._emitStatsUpdate(id, stats));
|
||||
|
||||
conference.on(
|
||||
JitsiE2ePingEvents.E2E_RTT_CHANGED,
|
||||
(participant, e2eRtt) => {
|
||||
const stats = {
|
||||
e2eRtt,
|
||||
region: participant.getProperty('region')
|
||||
};
|
||||
|
||||
this._emitStatsUpdate(participant.getId(), stats);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,12 +39,7 @@ class ConnectionStatsTable extends Component {
|
||||
connectionSummary: PropTypes.string,
|
||||
|
||||
/**
|
||||
* The end-to-end round-trip-time.
|
||||
*/
|
||||
e2eRtt: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Statistics related to frame rates for each ssrc.
|
||||
* Statistics related to framerates for each ssrc.
|
||||
* {{
|
||||
* [ ssrc ]: Number
|
||||
* }}
|
||||
@@ -52,7 +47,7 @@ class ConnectionStatsTable extends Component {
|
||||
framerate: PropTypes.object,
|
||||
|
||||
/**
|
||||
* Whether or not the statistics are for local video.
|
||||
* Whether or not the statitics are for local video.
|
||||
*/
|
||||
isLocalVideo: PropTypes.bool,
|
||||
|
||||
@@ -70,11 +65,6 @@ class ConnectionStatsTable extends Component {
|
||||
*/
|
||||
packetLoss: PropTypes.object,
|
||||
|
||||
/**
|
||||
* The region.
|
||||
*/
|
||||
region: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Statistics related to display resolutions for each ssrc.
|
||||
* {{
|
||||
@@ -218,31 +208,6 @@ class ConnectionStatsTable extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table row as a ReactElement for displaying end-to-end RTT and
|
||||
* the region.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderE2eRtt() {
|
||||
const { e2eRtt, region, t } = this.props;
|
||||
let str = e2eRtt ? `${e2eRtt.toFixed(0)}ms` : 'N/A';
|
||||
|
||||
if (region) {
|
||||
str += ` (${region})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<span>{ t('connectionindicator.e2e_rtt') }</span>
|
||||
</td>
|
||||
<td>{ str }</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table row as a ReactElement for displaying frame rate related
|
||||
* statistics.
|
||||
@@ -365,15 +330,12 @@ class ConnectionStatsTable extends Component {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderStatistics() {
|
||||
const isRemoteVideo = !this.props.isLocalVideo;
|
||||
|
||||
return (
|
||||
<table className = 'connection-info__container'>
|
||||
<tbody>
|
||||
{ this._renderConnectionSummary() }
|
||||
{ this._renderBitrate() }
|
||||
{ this._renderPacketLoss() }
|
||||
{ isRemoteVideo ? this._renderE2eRtt() : null }
|
||||
{ this._renderResolution() }
|
||||
{ this._renderFrameRate() }
|
||||
</tbody>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { dockToolbox } from '../../../toolbox';
|
||||
|
||||
import { setFilmstripHovered } from '../../actions';
|
||||
import { shouldRemoteVideosBeVisible } from '../../functions';
|
||||
|
||||
import Toolbar from './Toolbar';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
@@ -186,8 +185,9 @@ function _mapStateToProps(state) {
|
||||
&& state['features/toolbox'].visible
|
||||
&& interfaceConfig.TOOLBAR_BUTTONS.length;
|
||||
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
|
||||
|
||||
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
|
||||
reduceHeight ? 'reduce-height' : ''}`.trim();
|
||||
reduceHeight ? 'reduce-height' : ''}`;
|
||||
|
||||
return {
|
||||
_className: className,
|
||||
|
||||
@@ -6,9 +6,7 @@ import {
|
||||
} from '../analytics';
|
||||
import { _handleParticipantError } from '../base/conference';
|
||||
import { MEDIA_TYPE } from '../base/media';
|
||||
import { getParticipants } from '../base/participants';
|
||||
import { reportError } from '../base/util';
|
||||
import { shouldDisplayTileView } from '../video-layout';
|
||||
|
||||
import {
|
||||
SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||
@@ -28,19 +26,17 @@ export function selectParticipant() {
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (conference) {
|
||||
const ids = shouldDisplayTileView(state)
|
||||
? getParticipants(state).map(participant => participant.id)
|
||||
: [ state['features/large-video'].participantId ];
|
||||
const largeVideo = state['features/large-video'];
|
||||
const id = largeVideo.participantId;
|
||||
|
||||
try {
|
||||
conference.selectParticipants(ids);
|
||||
conference.selectParticipant(id);
|
||||
} catch (err) {
|
||||
_handleParticipantError(err);
|
||||
|
||||
sendAnalytics(createSelectParticipantFailedEvent(err));
|
||||
|
||||
reportError(
|
||||
err, `Failed to select participants ${ids.toString()}`);
|
||||
reportError(err, `Failed to select participant ${id}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import React, { Component } from 'react';
|
||||
|
||||
import { isFilmstripVisible } from '../../filmstrip';
|
||||
import { RecordingLabel } from '../../recording';
|
||||
import { shouldDisplayTileView } from '../../video-layout';
|
||||
import { VideoQualityLabel } from '../../video-quality';
|
||||
import { TranscribingLabel } from '../../transcribing/';
|
||||
|
||||
@@ -18,11 +17,6 @@ export type Props = {
|
||||
* determine display classes to set.
|
||||
*/
|
||||
_filmstripVisible: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the video quality label should be displayed.
|
||||
*/
|
||||
_showVideoQualityLabel: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,13 +72,11 @@ export default class AbstractLabels<P: Props, S> extends Component<P, S> {
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _filmstripVisible: boolean,
|
||||
* _showVideoQualityLabel: boolean
|
||||
* _filmstripVisible: boolean
|
||||
* }}
|
||||
*/
|
||||
export function _abstractMapStateToProps(state: Object) {
|
||||
return {
|
||||
_filmstripVisible: isFilmstripVisible(state),
|
||||
_showVideoQualityLabel: !shouldDisplayTileView(state)
|
||||
_filmstripVisible: isFilmstripVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,8 +89,7 @@ class Labels extends AbstractLabels<Props, State> {
|
||||
this._renderTranscribingLabel()
|
||||
}
|
||||
{
|
||||
this.props._showVideoQualityLabel
|
||||
&& this._renderVideoQualityLabel()
|
||||
this._renderVideoQualityLabel()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class LargeVideo extends Component<*> {
|
||||
</div>
|
||||
<div id = 'remotePresenceMessage' />
|
||||
<span id = 'remoteConnectionMessage' />
|
||||
<div id = 'largeVideoElementsContainer'>
|
||||
<div>
|
||||
<div id = 'largeVideoBackgroundContainer' />
|
||||
{
|
||||
|
||||
|
||||
@@ -5,14 +5,7 @@ import uuid from 'uuid';
|
||||
import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
|
||||
import { appNavigate, getName } from '../../app';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN,
|
||||
CONFERENCE_JOINED,
|
||||
SET_AUDIO_ONLY,
|
||||
getCurrentConference
|
||||
} from '../../base/conference';
|
||||
import { SET_AUDIO_ONLY } from '../../base/conference';
|
||||
import { getInviteURL } from '../../base/connection';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
@@ -20,6 +13,16 @@ import {
|
||||
setAudioMuted
|
||||
} from '../../base/media';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
import {
|
||||
SESSION_CONFIGURED,
|
||||
SESSION_ENDED,
|
||||
SESSION_FAILED,
|
||||
SESSION_STARTED,
|
||||
SET_SESSION,
|
||||
getCurrentSession,
|
||||
getSession,
|
||||
setSession
|
||||
} from '../../base/session';
|
||||
import {
|
||||
TRACK_ADDED,
|
||||
TRACK_REMOVED,
|
||||
@@ -51,21 +54,12 @@ CallKit && MiddlewareRegistry.register(store => next => action => {
|
||||
});
|
||||
break;
|
||||
|
||||
case CONFERENCE_FAILED:
|
||||
return _conferenceFailed(store, next, action);
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
return _conferenceJoined(store, next, action);
|
||||
|
||||
case CONFERENCE_LEFT:
|
||||
return _conferenceLeft(store, next, action);
|
||||
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
return _conferenceWillJoin(store, next, action);
|
||||
|
||||
case SET_AUDIO_ONLY:
|
||||
return _setAudioOnly(store, next, action);
|
||||
|
||||
case SET_SESSION:
|
||||
return _setSession(store, next, action);
|
||||
|
||||
case TRACK_ADDED:
|
||||
case TRACK_REMOVED:
|
||||
case TRACK_UPDATED:
|
||||
@@ -127,6 +121,35 @@ function _appWillMount({ dispatch, getState }, next, action) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Store}store - FIXME.
|
||||
* @param {Dispatch} next - FIXME.
|
||||
* @param {Action} action - FIXME.
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
* @private
|
||||
*/
|
||||
function _setSession(store, next, action) {
|
||||
const { state } = action.session;
|
||||
|
||||
switch (state) {
|
||||
case SESSION_CONFIGURED:
|
||||
return _sessionConfigured(store, next, action);
|
||||
|
||||
case SESSION_ENDED:
|
||||
return _sessionEnded(store, next, action);
|
||||
|
||||
case SESSION_FAILED:
|
||||
return _sessionFailed(store, next, action);
|
||||
|
||||
case SESSION_STARTED:
|
||||
return _sessionJoined(store, next, action);
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature callkit that the action {@link CONFERENCE_FAILED} is
|
||||
* being dispatched within a specific redux {@code store}.
|
||||
@@ -140,21 +163,14 @@ function _appWillMount({ dispatch, getState }, next, action) {
|
||||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceFailed(store, next, action) {
|
||||
const result = next(action);
|
||||
function _sessionFailed(store, next, action) {
|
||||
const callUUID = _getCallUUIDForSessionAction(store, action);
|
||||
|
||||
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
|
||||
// prevented the user from joining a specific conference but the app may be
|
||||
// able to eventually join the conference.
|
||||
if (!action.error.recoverable) {
|
||||
const { callUUID } = action.conference;
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.reportCallFailed(callUUID);
|
||||
}
|
||||
if (callUUID) {
|
||||
CallKit.reportCallFailed(callUUID);
|
||||
}
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,16 +186,14 @@ function _conferenceFailed(store, next, action) {
|
||||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceJoined(store, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const { callUUID } = action.conference;
|
||||
function _sessionJoined(store, next, action) {
|
||||
const callUUID = _getCallUUIDForSessionAction(store, action);
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.reportConnectedOutgoingCall(callUUID);
|
||||
}
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,16 +209,34 @@ function _conferenceJoined(store, next, action) {
|
||||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceLeft(store, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const { callUUID } = action.conference;
|
||||
function _sessionEnded(store, next, action) {
|
||||
const callUUID = _getCallUUIDForSessionAction(store, action);
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.endCall(callUUID);
|
||||
}
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Store} store - FIXME.
|
||||
* @param {Object} action - FIXME.
|
||||
* @returns {string|undefined}
|
||||
* @private
|
||||
*/
|
||||
function _getCallUUIDForSessionAction(store, action) {
|
||||
const url = action.session.url;
|
||||
const session = getSession(store, url);
|
||||
const callUUID = session && session.callkit && session.callkit.callUUID;
|
||||
|
||||
if (!callUUID) {
|
||||
console.info(`CALLKIT SESSION NOT FOUND FOR URL: ${url}`);
|
||||
}
|
||||
|
||||
return callUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,27 +252,32 @@ function _conferenceLeft(store, next, action) {
|
||||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceWillJoin({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const { conference } = action;
|
||||
function _sessionConfigured({ getState }, next, action) {
|
||||
const state = getState();
|
||||
const { callHandle, callUUID } = state['features/base/config'];
|
||||
const { callHandle, callUUID: _callUUID } = state['features/base/config'];
|
||||
const url = getInviteURL(state);
|
||||
const handle = callHandle || url.toString();
|
||||
const hasVideo = !isVideoMutedByAudioOnly(state);
|
||||
|
||||
// When assigning the call UUID, do so in upper case, since iOS will return
|
||||
// it upper cased.
|
||||
conference.callUUID = (callUUID || uuid.v4()).toUpperCase();
|
||||
const callUUID = (_callUUID || uuid.v4()).toUpperCase();
|
||||
|
||||
CallKit.startCall(conference.callUUID, handle, hasVideo)
|
||||
// Store the callUUID in the session
|
||||
action.session.callkit = {
|
||||
callUUID
|
||||
};
|
||||
|
||||
CallKit.startCall(callUUID, handle, hasVideo)
|
||||
.then(() => {
|
||||
const session = getSession(getState(), action.session.url);
|
||||
const { callee } = state['features/base/jwt'];
|
||||
const displayName
|
||||
= state['features/base/config'].callDisplayName
|
||||
|| (callee && callee.name)
|
||||
|| state['features/base/conference'].room;
|
||||
|| (session && session.room);
|
||||
|
||||
console.info(`CALLKIT WILL USE NAME: ${displayName}`);
|
||||
|
||||
const muted
|
||||
= isLocalTrackMuted(
|
||||
@@ -248,11 +285,11 @@ function _conferenceWillJoin({ getState }, next, action) {
|
||||
MEDIA_TYPE.AUDIO);
|
||||
|
||||
// eslint-disable-next-line object-property-newline
|
||||
CallKit.updateCall(conference.callUUID, { displayName, hasVideo });
|
||||
CallKit.setMuted(conference.callUUID, muted);
|
||||
CallKit.updateCall(callUUID, { displayName, hasVideo });
|
||||
CallKit.setMuted(callUUID, muted);
|
||||
});
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,18 +300,47 @@ function _conferenceWillJoin({ getState }, next, action) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onPerformEndCallAction({ callUUID }) {
|
||||
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
|
||||
const conference = getCurrentConference(getState);
|
||||
const { dispatch } = this; // eslint-disable-line no-invalid-this
|
||||
// eslint-disable-next-line max-len
|
||||
const session = _findSessionForCallUUID(this, callUUID); // eslint-disable-line no-invalid-this
|
||||
|
||||
if (conference && conference.callUUID === callUUID) {
|
||||
if (session) {
|
||||
// We arrive here when a call is ended by the system, for example, when
|
||||
// another incoming call is received and the user selects "End &
|
||||
// Accept".
|
||||
delete conference.callUUID;
|
||||
dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
callkit: undefined
|
||||
}));
|
||||
dispatch(appNavigate(undefined));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Store} getState - FIXME.
|
||||
* @param {string} callUUID - FIXME.
|
||||
* @returns {Object|null}
|
||||
* @private
|
||||
*/
|
||||
function _findSessionForCallUUID({ getState }, callUUID) {
|
||||
const sessions = getState()['features/base/session'];
|
||||
|
||||
for (const session of sessions.values()) {
|
||||
const _callUUID = session.callkit && session.callkit.callUUID;
|
||||
|
||||
if (callUUID === _callUUID) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
console.info(`SESSION NOT FOUND FOR CALL ID: ${callUUID}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles CallKit's event {@code performSetMutedCallAction}.
|
||||
*
|
||||
@@ -283,10 +349,11 @@ function _onPerformEndCallAction({ callUUID }) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onPerformSetMutedCallAction({ callUUID, muted }) {
|
||||
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
|
||||
const conference = getCurrentConference(getState);
|
||||
const { dispatch } = this; // eslint-disable-line no-invalid-this
|
||||
// eslint-disable-next-line max-len
|
||||
const session = _findSessionForCallUUID(this, callUUID); // eslint-disable-line no-invalid-this
|
||||
|
||||
if (conference && conference.callUUID === callUUID) {
|
||||
if (session) {
|
||||
muted = Boolean(muted); // eslint-disable-line no-param-reassign
|
||||
sendAnalytics(createTrackMutedEvent('audio', 'callkit', muted));
|
||||
dispatch(setAudioMuted(muted, /* ensureTrack */ true));
|
||||
@@ -316,11 +383,11 @@ function _onPerformSetMutedCallAction({ callUUID, muted }) {
|
||||
function _setAudioOnly({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const session = getCurrentSession(state);
|
||||
|
||||
if (conference && conference.callUUID) {
|
||||
if (session && session.callUUID) {
|
||||
CallKit.updateCall(
|
||||
conference.callUUID,
|
||||
session.callUUID,
|
||||
{ hasVideo: !action.audioOnly });
|
||||
}
|
||||
|
||||
@@ -369,20 +436,25 @@ function _syncTrackState({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
const { jitsiTrack } = action.track;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (jitsiTrack.isLocal() && conference && conference.callUUID) {
|
||||
// It could go over all sessions here, but even if we'd support simultaneous
|
||||
// sessions / putting on hold, probably only the active session would be
|
||||
// holding the tracks.
|
||||
const session = getCurrentSession(state);
|
||||
const callUUID = session && session.callkit && session.callkit.callUUID;
|
||||
|
||||
if (jitsiTrack.isLocal() && callUUID) {
|
||||
switch (jitsiTrack.getType()) {
|
||||
case 'audio': {
|
||||
const tracks = state['features/base/tracks'];
|
||||
const muted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
|
||||
|
||||
CallKit.setMuted(conference.callUUID, muted);
|
||||
CallKit.setMuted(callUUID, muted);
|
||||
break;
|
||||
}
|
||||
case 'video': {
|
||||
CallKit.updateCall(
|
||||
conference.callUUID,
|
||||
callUUID,
|
||||
{ hasVideo: !isVideoMutedByAudioOnly(state) });
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,56 @@
|
||||
// @flow
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import { getAppProp } from '../../app';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
SET_ROOM,
|
||||
forEachConference,
|
||||
isRoomValid
|
||||
CONFERENCE_WILL_LEAVE
|
||||
} from '../../base/conference';
|
||||
import { LOAD_CONFIG_ERROR } from '../../base/config';
|
||||
import { CONNECTION_FAILED } from '../../base/connection';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
import { getSymbolDescription, toURLString } from '../../base/util';
|
||||
import {
|
||||
SESSION_ENDED,
|
||||
SESSION_FAILED,
|
||||
SESSION_STARTED,
|
||||
SESSION_WILL_END,
|
||||
SESSION_WILL_START,
|
||||
SET_SESSION
|
||||
} from '../../base/session';
|
||||
import { getSymbolDescription } from '../../base/util';
|
||||
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Symbol} state - FIXME.
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
function _stateToApiEventName(state) {
|
||||
switch (state) {
|
||||
case SESSION_WILL_START:
|
||||
return getSymbolDescription(CONFERENCE_WILL_JOIN);
|
||||
|
||||
case SESSION_STARTED:
|
||||
return getSymbolDescription(CONFERENCE_JOINED);
|
||||
|
||||
case SESSION_WILL_END:
|
||||
return getSymbolDescription(CONFERENCE_WILL_LEAVE);
|
||||
|
||||
case SESSION_ENDED:
|
||||
return getSymbolDescription(CONFERENCE_LEFT);
|
||||
|
||||
case SESSION_FAILED:
|
||||
return getSymbolDescription(CONFERENCE_FAILED);
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
import { sendEvent } from './functions';
|
||||
|
||||
/**
|
||||
@@ -27,63 +61,19 @@ import { sendEvent } from './functions';
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
action.type && console.info(`ACTION ${getSymbolDescription(action.type)}`);
|
||||
|
||||
const result = next(action);
|
||||
const { type } = action;
|
||||
|
||||
switch (type) {
|
||||
case CONFERENCE_FAILED: {
|
||||
const { error, ...data } = action;
|
||||
|
||||
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
|
||||
// prevented the user from joining a specific conference but the app may
|
||||
// be able to eventually join the conference. For example, the app will
|
||||
// ask the user for a password upon
|
||||
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
|
||||
// conference afterwards. Such errors are to not reach the native
|
||||
// counterpart of the External API (or at least not in the
|
||||
// fatality/finality semantics attributed to
|
||||
// conferenceFailed:/onConferenceFailed).
|
||||
if (!error.recoverable) {
|
||||
_sendConferenceEvent(store, /* action */ {
|
||||
error: _toErrorString(error),
|
||||
...data
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
_sendConferenceEvent(store, action);
|
||||
break;
|
||||
|
||||
case CONNECTION_FAILED:
|
||||
!action.error.recoverable
|
||||
&& _sendConferenceFailedOnConnectionError(store, action);
|
||||
case SET_SESSION:
|
||||
_setSession(store, action);
|
||||
break;
|
||||
|
||||
case ENTER_PICTURE_IN_PICTURE:
|
||||
sendEvent(store, getSymbolDescription(type), /* data */ {});
|
||||
break;
|
||||
|
||||
case LOAD_CONFIG_ERROR: {
|
||||
const { error, locationURL } = action;
|
||||
|
||||
sendEvent(
|
||||
store,
|
||||
getSymbolDescription(type),
|
||||
/* data */ {
|
||||
error: _toErrorString(error),
|
||||
url: toURLString(locationURL)
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_ROOM:
|
||||
_maybeTriggerEarlyConferenceWillJoin(store, action);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -110,145 +100,47 @@ function _toErrorString(
|
||||
}
|
||||
|
||||
/**
|
||||
* If {@link SET_ROOM} action happens for a valid conference room this method
|
||||
* will emit an early {@link CONFERENCE_WILL_JOIN} event to let the external API
|
||||
* know that a conference is being joined. Before that happens a connection must
|
||||
* be created and only then base/conference feature would emit
|
||||
* {@link CONFERENCE_WILL_JOIN}. That is fine for the Jitsi Meet app, because
|
||||
* that's the a conference instance gets created, but it's too late for
|
||||
* the external API to learn that. The latter {@link CONFERENCE_WILL_JOIN} is
|
||||
* swallowed in {@link _swallowEvent}.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Action} action - The redux action.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeTriggerEarlyConferenceWillJoin(store, action) {
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
const { room } = action;
|
||||
|
||||
isRoomValid(room) && locationURL && sendEvent(
|
||||
store,
|
||||
getSymbolDescription(CONFERENCE_WILL_JOIN),
|
||||
/* data */ {
|
||||
url: toURLString(locationURL)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an event to the native counterpart of the External API for a specific
|
||||
* conference-related redux action.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Action} action - The redux action.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _sendConferenceEvent(
|
||||
store: Object,
|
||||
action: {
|
||||
conference: Object,
|
||||
type: Symbol,
|
||||
url: ?string
|
||||
}) {
|
||||
const { conference, type, ...data } = action;
|
||||
|
||||
// For these (redux) actions, conference identifies a JitsiConference
|
||||
// instance. The external API cannot transport such an object so we have to
|
||||
// transport an "equivalent".
|
||||
if (conference) {
|
||||
data.url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
|
||||
}
|
||||
|
||||
_swallowEvent(store, action, data)
|
||||
|| sendEvent(store, getSymbolDescription(type), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends {@link CONFERENCE_FAILED} event when the {@link CONNECTION_FAILED}
|
||||
* occurs. It should be done only if the connection fails before the conference
|
||||
* instance is created. Otherwise the eventual failure event is supposed to be
|
||||
* emitted by the base/conference feature.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Action} action - The redux action.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _sendConferenceFailedOnConnectionError(store, action) {
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
const { connection } = action;
|
||||
|
||||
locationURL
|
||||
&& forEachConference(
|
||||
store,
|
||||
|
||||
// If there's any conference in the base/conference state then the
|
||||
// base/conference feature is supposed to emit a failure.
|
||||
conference => conference.getConnection() !== connection)
|
||||
&& sendEvent(
|
||||
store,
|
||||
getSymbolDescription(CONFERENCE_FAILED),
|
||||
/* data */ {
|
||||
url: toURLString(locationURL),
|
||||
error: action.error.name
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to not send a {@code CONFERENCE_LEFT} event to the native
|
||||
* counterpart of the External API.
|
||||
* Sends a specific event to the native counterpart of the External API. Native
|
||||
* apps may listen to such events via the mechanisms provided by the (native)
|
||||
* mobile Jitsi Meet SDK.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @param {Action} action - The redux action which is causing the sending of the
|
||||
* event.
|
||||
* @param {string} name - The name of the event to send.
|
||||
* @param {Object} data - The details/specifics of the event to send determined
|
||||
* by/associated with the specified {@code action}.
|
||||
* @returns {boolean} If the specified event is to not be sent, {@code true};
|
||||
* otherwise, {@code false}.
|
||||
* by/associated with the specified {@code name}.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _swallowConferenceLeft({ getState }, action, { url }) {
|
||||
// XXX Internally, we work with JitsiConference instances. Externally
|
||||
// though, we deal with URL strings. The relation between the two is many to
|
||||
// one so it's technically and practically possible (by externally loading
|
||||
// the same URL string multiple times) to try to send CONFERENCE_LEFT
|
||||
// externally for a URL string which identifies a JitsiConference that the
|
||||
// app is internally legitimately working with.
|
||||
let swallowConferenceLeft = false;
|
||||
function _sendEvent(store: Object, name: string, data: Object) {
|
||||
// The JavaScript App needs to provide uniquely identifying information to
|
||||
// the native ExternalAPI module so that the latter may match the former to
|
||||
// the native JitsiMeetView which hosts it.
|
||||
const externalAPIScope = getAppProp(store, 'externalAPIScope');
|
||||
|
||||
url
|
||||
&& forEachConference(getState, (conference, conferenceURL) => {
|
||||
if (conferenceURL && conferenceURL.toString() === url) {
|
||||
swallowConferenceLeft = true;
|
||||
}
|
||||
console.info(
|
||||
`EXT EVENT ${name} URL: ${data.url} DATA: ${JSON.stringify(data)}`);
|
||||
|
||||
return !swallowConferenceLeft;
|
||||
});
|
||||
|
||||
return swallowConferenceLeft;
|
||||
externalAPIScope
|
||||
&& NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether to not send a specific event to the native counterpart of
|
||||
* the External API.
|
||||
* FIXME.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @param {Action} action - The redux action which is causing the sending of the
|
||||
* event.
|
||||
* @param {Object} data - The details/specifics of the event to send determined
|
||||
* by/associated with the specified {@code action}.
|
||||
* @returns {boolean} If the specified event is to not be sent, {@code true};
|
||||
* otherwise, {@code false}.
|
||||
* @param {Store} store - FIXME.
|
||||
* @param {Action} action - FIXME.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function _swallowEvent(store, action, data) {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_LEFT:
|
||||
return _swallowConferenceLeft(store, action, data);
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
// CONFERENCE_WILL_JOIN is dispatched to the external API on SET_ROOM,
|
||||
// before the connection is created, so we need to swallow the original
|
||||
// one emitted by base/conference.
|
||||
return true;
|
||||
function _setSession(store, action) {
|
||||
const { error, state, url } = action.session;
|
||||
const apiEventName = _stateToApiEventName(state);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
apiEventName && _sendEvent(
|
||||
store,
|
||||
apiEventName,
|
||||
/* data */ {
|
||||
url,
|
||||
error: error && _toErrorString(error)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
SESSION_FAILED,
|
||||
getCurrentSession,
|
||||
setSession
|
||||
} from '../base/session';
|
||||
|
||||
import {
|
||||
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
|
||||
SET_FATAL_ERROR,
|
||||
@@ -57,3 +63,37 @@ export function setFatalError(fatalError) {
|
||||
fatalError
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME naming is not quite accurate - came from the previous method which was
|
||||
* reemitting the action. I feel that this part needs more discussion. Changing
|
||||
* it back to emitting the original action which caused the fatal error will
|
||||
* also require changes to how it's being detected (currently through the state
|
||||
* listener, but we'd have to go back to the middleware way).
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function reemitFatalError() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { fatalError } = state['features/overlay'];
|
||||
|
||||
if (fatalError) {
|
||||
const session = getCurrentSession(state);
|
||||
|
||||
if (session) {
|
||||
dispatch(
|
||||
setSession({
|
||||
url: session.url,
|
||||
state: SESSION_FAILED,
|
||||
error: fatalError
|
||||
}));
|
||||
} else {
|
||||
console.info('No current session!');
|
||||
}
|
||||
dispatch(setFatalError(undefined));
|
||||
} else {
|
||||
console.info('NO FATAL ERROR');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { LoadingIndicator } from '../../base/react';
|
||||
|
||||
import AbstractPageReloadOverlay, { abstractMapStateToProps }
|
||||
from './AbstractPageReloadOverlay';
|
||||
import { setFatalError } from '../actions';
|
||||
import { reemitFatalError } from '../actions';
|
||||
import OverlayFrame from './OverlayFrame';
|
||||
import { pageReloadOverlay as styles } from './styles';
|
||||
|
||||
@@ -41,7 +41,7 @@ class PageReloadOverlay extends AbstractPageReloadOverlay {
|
||||
*/
|
||||
_onCancel() {
|
||||
clearInterval(this._interval);
|
||||
this.props.dispatch(setFatalError(undefined));
|
||||
this.props.dispatch(reemitFatalError());
|
||||
this.props.dispatch(appNavigate(undefined));
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
import { toggleSharedVideo } from '../../../shared-video';
|
||||
import { toggleChat } from '../../../side-panel';
|
||||
import { SpeakerStats } from '../../../speaker-stats';
|
||||
import { TileViewButton } from '../../../video-layout';
|
||||
import {
|
||||
OverflowMenuVideoQualityItem,
|
||||
VideoQualityDialog
|
||||
@@ -370,8 +369,6 @@ class Toolbox extends Component<Props> {
|
||||
visible = { this._shouldShowButton('camera') } />
|
||||
</div>
|
||||
<div className = 'button-group-right'>
|
||||
{ this._shouldShowButton('tileview')
|
||||
&& <TileViewButton /> }
|
||||
{ this._shouldShowButton('invite')
|
||||
&& !_hideInviteButton
|
||||
&& <ToolbarButton
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* The type of the action which enables or disables the feature for showing
|
||||
* video thumbnails in a two-axis tile view.
|
||||
*
|
||||
* @returns {{
|
||||
* type: SET_TILE_VIEW,
|
||||
* enabled: boolean
|
||||
* }}
|
||||
*/
|
||||
export const SET_TILE_VIEW = Symbol('SET_TILE_VIEW');
|
||||
@@ -1,20 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { SET_TILE_VIEW } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Creates a (redux) action which signals to set the UI layout to be tiled view
|
||||
* or not.
|
||||
*
|
||||
* @param {boolean} enabled - Whether or not tile view should be shown.
|
||||
* @returns {{
|
||||
* type: SET_TILE_VIEW,
|
||||
* enabled: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setTileView(enabled: boolean) {
|
||||
return {
|
||||
type: SET_TILE_VIEW,
|
||||
enabled
|
||||
};
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
createToolbarEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { translate } from '../../base/i18n';
|
||||
import {
|
||||
AbstractButton,
|
||||
type AbstractButtonProps
|
||||
} from '../../base/toolbox';
|
||||
|
||||
import { setTileView } from '../actions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link TileViewButton}.
|
||||
*/
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* Whether or not tile view layout has been enabled as the user preference.
|
||||
*/
|
||||
_tileViewEnabled: boolean,
|
||||
|
||||
/**
|
||||
* Used to dispatch actions from the buttons.
|
||||
*/
|
||||
dispatch: Dispatch<*>
|
||||
};
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for toggling the tile layout view.
|
||||
*
|
||||
* @extends AbstractButton
|
||||
*/
|
||||
class TileViewButton<P: Props> extends AbstractButton<P, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.tileView';
|
||||
iconName = 'icon-tiles-many';
|
||||
toggledIconName = 'icon-tiles-many toggled';
|
||||
tooltip = 'toolbar.tileViewToggle';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { _tileViewEnabled, dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent(
|
||||
'tileview.button',
|
||||
{
|
||||
'is_enabled': _tileViewEnabled
|
||||
}));
|
||||
|
||||
dispatch(setTileView(!_tileViewEnabled));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is in toggled state or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isToggled() {
|
||||
return this.props._tileViewEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for the
|
||||
* {@code TileViewButton} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {{
|
||||
* _tileViewEnabled: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_tileViewEnabled: state['features/video-layout'].tileViewEnabled
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(TileViewButton));
|
||||
@@ -1 +0,0 @@
|
||||
export { default as TileViewButton } from './TileViewButton';
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* An enumeration of the different display layouts supported by the application.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
export const LAYOUTS = {
|
||||
HORIZONTAL_FILMSTRIP_VIEW: 'horizontal-filmstrip-view',
|
||||
TILE_VIEW: 'tile-view',
|
||||
VERTICAL_FILMSTRIP_VIEW: 'vertical-filmstrip-view'
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { LAYOUTS } from './constants';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Returns the {@code LAYOUTS} constant associated with the layout
|
||||
* the application should currently be in.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getCurrentLayout(state: Object) {
|
||||
if (shouldDisplayTileView(state)) {
|
||||
return LAYOUTS.TILE_VIEW;
|
||||
} else if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
return LAYOUTS.VERTICAL_FILMSTRIP_VIEW;
|
||||
}
|
||||
|
||||
return LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many columns should be displayed in tile view. The number
|
||||
* returned will be between 1 and 5, inclusive.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getMaxColumnCount() {
|
||||
const configuredMax = interfaceConfig.TILE_VIEW_MAX_COLUMNS || 5;
|
||||
|
||||
return Math.max(Math.min(configuredMax, 1), 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cell count dimensions for tile view. Tile view tries to uphold
|
||||
* equal count of tiles for height and width, until maxColumn is reached in
|
||||
* which rows will be added but no more columns.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {number} maxColumns - The maximum number of columns that can be
|
||||
* displayed.
|
||||
* @returns {Object} An object is return with the desired number of columns,
|
||||
* rows, and visible rows (the rest should overflow) for the tile view layout.
|
||||
*/
|
||||
export function getTileViewGridDimensions(state: Object, maxColumns: number) {
|
||||
// Purposefully include all participants, which includes fake participants
|
||||
// that should show a thumbnail.
|
||||
const potentialThumbnails = state['features/base/participants'].length;
|
||||
|
||||
const columnsToMaintainASquare = Math.ceil(Math.sqrt(potentialThumbnails));
|
||||
const columns = Math.min(columnsToMaintainASquare, maxColumns);
|
||||
const rows = Math.ceil(potentialThumbnails / columns);
|
||||
const visibleRows = Math.min(maxColumns, rows);
|
||||
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
visibleRows
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the UI layout should be in tile view. Tile view
|
||||
* is determined by more than just having the tile view setting enabled, as
|
||||
* one-on-one calls should not be in tile view, as well as etherpad editing.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean} True if tile view should be displayed.
|
||||
*/
|
||||
export function shouldDisplayTileView(state: Object = {}) {
|
||||
return Boolean(
|
||||
state['features/video-layout']
|
||||
&& state['features/video-layout'].tileViewEnabled
|
||||
&& !state['features/etherpad'].editing
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
export * from './constants';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
import './subscriber';
|
||||
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { TRACK_ADDED } from '../base/tracks';
|
||||
|
||||
import { SET_TILE_VIEW } from './actionTypes';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
@@ -73,10 +71,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
Boolean(action.participant.id));
|
||||
break;
|
||||
|
||||
case SET_TILE_VIEW:
|
||||
APP.UI.emitEvent(UIEvents.TOGGLED_TILE_VIEW, action.enabled);
|
||||
break;
|
||||
|
||||
case TRACK_ADDED:
|
||||
if (!action.track.local) {
|
||||
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_TILE_VIEW } from './actionTypes';
|
||||
|
||||
ReducerRegistry.register('features/video-layout', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case SET_TILE_VIEW:
|
||||
return {
|
||||
...state,
|
||||
tileViewEnabled: action.enabled
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
VIDEO_QUALITY_LEVELS,
|
||||
setMaxReceiverVideoQuality
|
||||
} from '../base/conference';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
import { selectParticipant } from '../large-video';
|
||||
import { shouldDisplayTileView } from './functions';
|
||||
|
||||
/**
|
||||
* StateListenerRegistry provides a reliable way of detecting changes to
|
||||
* preferred layout state and dispatching additional actions.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => shouldDisplayTileView(state),
|
||||
/* listener */ (displayTileView, { dispatch }) => {
|
||||
dispatch(selectParticipant());
|
||||
|
||||
if (!displayTileView) {
|
||||
dispatch(setMaxReceiverVideoQuality(VIDEO_QUALITY_LEVELS.HIGH));
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -59,7 +59,6 @@ export default {
|
||||
TOGGLED_FILMSTRIP: 'UI.toggled_filmstrip',
|
||||
TOGGLE_SCREENSHARING: 'UI.toggle_screensharing',
|
||||
TOGGLED_SHARED_DOCUMENT: 'UI.toggled_shared_document',
|
||||
TOGGLED_TILE_VIEW: 'UI.toggled_tile_view',
|
||||
HANGUP: 'UI.hangup',
|
||||
LOGOUT: 'UI.logout',
|
||||
VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',
|
||||
|
||||
Reference in New Issue
Block a user