Compare commits

...

18 Commits

Author SHA1 Message Date
Jaya Allamsetty
68a75380a4 fix(UI): Re-compute large-video width only once if the chat window is open. (#7758)
Deduct the chat window width from large-video width only once if we keep toggling between stage view and grid view while the chat window is open.
2020-09-22 17:10:27 -04:00
Дамян Минков
8dcf04897a feat: Throttle out call attempts to the max number per minute (#7742)
* feat: Make possible to reload config for filter rayo iq.

* feat: Throttle out call attempts to the max number per minute

* squash: Updates comment about config
2020-09-22 10:53:43 -05:00
Saúl Ibarra Corretgé
69b7301b9d fix(build) reduce bundle size by about 700K
app.bundle.js before: 3851549 after: 4506493.

The culprit for the bloat was Olm. It feature-detects the environment in order
to pick a suitable random byte generator, and alas Webpack includes the None
crypto pollyfill. This is due to the existence of the "node" block in our
Webpack configuration file.

The solution is to provide empty modules to make bundling work, as we did
already for the fs module, since we know they are not used at runtime.
2020-09-22 10:59:55 +02:00
Jaya Allamsetty
794713b930 fix(iFrame): Make resizeLargeVideo only available on web 2020-09-21 19:54:45 -04:00
Jaya Allamsetty
89cd6e8e3e feat(stats): Add the ability to enable callStats for certain % of confs 2020-09-21 13:20:01 -04:00
Saúl Ibarra Corretgé
7a7937c072 fix(ios,version) versions must match 2020-09-18 21:02:51 +02:00
Saúl Ibarra Corretgé
4765ab9d63 chore(rn,version) bump app and sdk versions 2020-09-18 17:55:41 +02:00
Jaya Allamsetty
1d5decc14f feat(iFrame): Add a method for capturing screenshot of the large video (#7717) 2020-09-18 11:53:27 -04:00
Saúl Ibarra Corretgé
119b79fd84 fix(SmallVideo) screen-sharing indicator
The stream is attached before the video type change event is fired, so comparing
them is too late. Unconditionally update the screen-sharing indicator, and
perform the check for a change right there, to avoid re-renders.
2020-09-18 17:17:30 +02:00
Saúl Ibarra Corretgé
188771751d fix(config) remove unused options (#7723)
* fix(config) remove unused options

* Update configWhitelist.js
2020-09-18 08:55:09 -05:00
Vlad Piersec
d2ec0ea6f3 fix(branding): Fix dynamic logo display
* Display of the logo has been reworked (simplified).
* The logo will not be displayed if the call to `branding` endpoint fails.
* Add more docs.
2020-09-18 16:38:44 +03:00
Saúl Ibarra Corretgé
ed6e75b241 fix(ios) add local network usage description for iOS 14 2020-09-18 12:54:11 +02:00
Saúl Ibarra Corretgé
dedd3f4ef0 fix(config) remove no longer valid option 2020-09-18 12:31:55 +02:00
Jaya Allamsetty
bbb4fbd5f8 feat(iFrame): Add a method for resizing large video container from iFrame 2020-09-17 23:41:19 -04:00
Saúl Ibarra Corretgé
92235ae535 fix(android,calendar) avoid crash 2020-09-17 18:45:04 +02:00
Josh Brown
ebb1b8d76b fix(background-blur) refactor to improve performance 2020-09-17 18:25:06 +02:00
Saúl Ibarra Corretgé
42d559de93 deps: replace node-sass with sass
The former is no longer actively maintained.

Fixes: https://github.com/jitsi/jitsi-meet/issues/6427
2020-09-17 16:44:01 +02:00
Julian Vos
2838aefccc lang: fix Dutch dialog.kickTitle 2020-09-17 16:29:18 +02:00
33 changed files with 472 additions and 1294 deletions

View File

@@ -5,7 +5,7 @@ LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/
LIBFLAC_DIR = node_modules/libflacjs/dist/min/
OLM_DIR = node_modules/olm
RNNOISE_WASM_DIR = node_modules/rnnoise-wasm/dist/
NODE_SASS = ./node_modules/.bin/node-sass
NODE_SASS = ./node_modules/.bin/sass
NPM = npm
OUTPUT_DIR = .
STYLES_BUNDLE = css/all.bundle.css

View File

@@ -20,5 +20,5 @@
android.useAndroidX=true
android.enableJetifier=true
appVersion=20.4.0
sdkVersion=2.10.0
appVersion=20.5.0
sdkVersion=2.11.0

View File

@@ -14,9 +14,6 @@ var config = {
// Domain for authenticated users. Defaults to <domain>.
// authdomain: 'jitsi-meet.example.com',
// Jirecon recording component domain.
// jirecon: 'jirecon.jitsi-meet.example.com',
// Call control component (Jigasi).
// call_control: 'callcontrol.jitsi-meet.example.com',
@@ -67,6 +64,12 @@ var config = {
// adjusted to 2.5 Mbps. This takes a value between 0 and 1 which determines
// the probability for this to be enabled.
// capScreenshareBitrate: 1 // 0 to disable
// Enable callstats only for a percentage of users.
// This takes a value between 0 and 100. A hash value is calculated using the
// conference name and callstats is enabled if the hash value is less than the
// the threshold specified here.
// callStatsThreshold: 5 // enable callstats for 5% of the conferences.
},
// Disables ICE/UDP by filtering out local and remote UDP candidates in
@@ -635,8 +638,6 @@ var config = {
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold
autoRecord
autoRecordToken
debug
debugAudioLevels
deploymentInfo

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>20.4.0</string>
<string>20.5.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -66,10 +66,10 @@
<string>See your scheduled meetings in the app.</string>
<key>NSCameraUsageDescription</key>
<string>Participate in meetings with video.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSMicrophoneUsageDescription</key>
<string>Participate in meetings with voice.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Local network is used for establishing Peer-to-Peer connections.</string>
<key>NSUserActivityTypes</key>
<array>
<string>org.jitsi.JitsiMeet.ios.conference</string>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>20.4.0</string>
<string>20.5.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>20.4.0</string>
<string>20.5.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.10.0</string>
<string>2.11.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -212,7 +212,7 @@
"kickParticipantButton": "Verwijderen",
"kickParticipantDialog": "Weet u zeker dat u deze deelnemer wilt verwijderen?",
"kickParticipantTitle": "Deze deelnemer verwijderen?",
"kickTitle": "Oei! {{ParticipantDisplayName}} heeft u uit de vergadering verwijderd",
"kickTitle": "Oei! {{participantDisplayName}} heeft u uit de vergadering verwijderd",
"liveStreaming": "Livestreamen",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Niet mogelijk tijdens opnemen",
"liveStreamingDisabledForGuestTooltip": "Gasten kunnen geen livestream starten.",

View File

@@ -21,7 +21,11 @@ import {
import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox';
import { toggleE2EE } from '../../react/features/e2ee/actions';
import { invite } from '../../react/features/invite';
import { selectParticipantInLargeVideo } from '../../react/features/large-video/actions';
import {
captureLargeVideoScreenshot,
resizeLargeVideo,
selectParticipantInLargeVideo
} from '../../react/features/large-video/actions';
import { toggleLobbyMode } from '../../react/features/lobby/actions.web';
import { RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession } from '../../react/features/recording/functions';
@@ -119,6 +123,11 @@ function initCommands() {
'proxy-connection-event': event => {
APP.conference.onProxyConnectionEvent(event);
},
'resize-large-video': (width, height) => {
logger.debug('Resize large video command received');
sendAnalytics(createApiEvent('largevideo.resized'));
APP.store.dispatch(resizeLargeVideo(width, height));
},
'send-tones': (options = {}) => {
const { duration, tones, pause } = options;
@@ -334,6 +343,21 @@ function initCommands() {
const { name } = request;
switch (name) {
case 'capture-largevideo-screenshot' :
APP.store.dispatch(captureLargeVideoScreenshot())
.then(dataURL => {
let error;
if (!dataURL) {
error = new Error('No large video found!');
}
callback({
error,
dataURL
});
});
break;
case 'invite': {
const { invitees } = request;

View File

@@ -35,6 +35,7 @@ const commands = {
hangup: 'video-hangup',
muteEveryone: 'mute-everyone',
password: 'password',
resizeLargeVideo: 'resize-large-video',
sendEndpointTextMessage: 'send-endpoint-text-message',
sendTones: 'send-tones',
setLargeVideoParticipant: 'set-large-video-participant',
@@ -437,10 +438,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
const parsedWidth = parseSizeParam(width);
if (parsedHeight !== undefined) {
this._height = height;
this._frame.style.height = parsedHeight;
}
if (parsedWidth !== undefined) {
this._width = width;
this._frame.style.width = parsedWidth;
}
}
@@ -633,6 +636,18 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}
}
/**
* Captures the screenshot of the large video.
*
* @returns {dataURL} - Base64 encoded image data of the screenshot if large
* video is detected, an error otherwise.
*/
captureLargeVideoScreenshot() {
return this._transport.sendRequest({
name: 'capture-largevideo-screenshot'
});
}
/**
* Removes the listeners and removes the Jitsi Meet frame.
*
@@ -930,6 +945,19 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
eventList.forEach(event => this.removeEventListener(event));
}
/**
* Resizes the large video container as per the dimensions provided.
*
* @param {number} width - Width that needs to be applied on the large video container.
* @param {number} height - Height that needs to be applied on the large video container.
* @returns {void}
*/
resizeLargeVideo(width, height) {
if (width <= this._width && height <= this._height) {
this.executeCommand('resizeLargeVideo', width, height);
}
}
/**
* Passes an event along to the local conference participant to establish
* or update a direct peer connection. This is currently used for developing

View File

@@ -5,13 +5,6 @@
*/
const UIUtil = {
/**
* Returns the available video width.
*/
getAvailableVideoWidth() {
return window.innerWidth;
},
/**
* Escapes the given text.
*/

View File

@@ -15,13 +15,12 @@ import { VIDEO_TYPE } from '../../../react/features/base/media';
import { CHAT_SIZE } from '../../../react/features/chat';
import {
updateKnownLargeVideoResolution
} from '../../../react/features/large-video';
} from '../../../react/features/large-video/actions';
import { PresenceLabel } from '../../../react/features/presence-status';
/* eslint-enable no-unused-vars */
import UIEvents from '../../../service/UI/UIEvents';
import { createDeferred } from '../../util/helpers';
import AudioLevels from '../audio_levels/AudioLevels';
import UIUtil from '../util/UIUtil';
import { VideoContainer, VIDEO_CONTAINER_TYPE } from './VideoContainer';
@@ -323,20 +322,25 @@ export default class LargeVideoManager {
/**
* Update container size.
*/
updateContainerSize() {
let widthToUse = UIUtil.getAvailableVideoWidth();
updateContainerSize(width, height) {
let widthToUse = width ?? (this.width > 0 ? this.width : window.innerWidth);
const { isOpen } = APP.store.getState()['features/chat'];
if (isOpen) {
/**
* If chat state is open, we re-compute the container width
* by subtracting the default width of the chat.
*/
/**
* If chat state is open, we re-compute the container width by subtracting the default width of
* the chat. We re-compute the width again after the chat window is closed. This is needed when
* custom styling is configured on the large video container through the iFrame API.
*/
if (isOpen && !this.resizedForChat) {
widthToUse -= CHAT_SIZE;
this.resizedForChat = true;
} else if (this.resizedForChat) {
this.resizedForChat = false;
widthToUse += CHAT_SIZE;
}
this.width = widthToUse;
this.height = window.innerHeight;
this.height = height ?? (this.height > 0 ? this.height : window.innerHeight);
}
/**

View File

@@ -373,7 +373,6 @@ export default class RemoteVideo extends SmallVideo {
if (stream === this.videoStream) {
this.videoStream = null;
this.videoType = undefined;
}
this.updateView();
@@ -484,7 +483,6 @@ export default class RemoteVideo extends SmallVideo {
if (isVideo) {
this.videoStream = stream;
this.videoType = stream.videoType;
} else {
this.audioStream = stream;
}

View File

@@ -243,6 +243,10 @@ export default class SmallVideo {
* or hidden
*/
setScreenSharing(isScreenSharing) {
if (isScreenSharing === this.isScreenSharing) {
return;
}
this.isScreenSharing = isScreenSharing;
this.updateView();
this.updateStatusBar();

View File

@@ -489,7 +489,7 @@ const VideoLayout = {
onVideoTypeChanged(id, newVideoType) {
const remoteVideo = remoteVideos[id];
if (!remoteVideo || remoteVideo.videoType === newVideoType) {
if (!remoteVideo) {
return;
}

1083
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#43e7c853b834dc7ced0f81ee5f4b130444d85e95",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#a433ca2dbc8accf41df5a50b13b126ad2e7932bf",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
@@ -70,7 +70,7 @@
"react-linkify": "1.0.0-alpha",
"react-native": "github:jitsi/react-native#efd2aff5661d75a230e36406b698cfe0ee545be2",
"react-native-background-timer": "2.4.0",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#928a80e2ffef0d7e84936d7e7e0acc4f53ee8470",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#df48ecdc4e1e90c5352f803ddbab1fa7269b74a7",
"react-native-callstats": "3.61.0",
"react-native-collapsible": "1.5.1",
"react-native-default-preference": "1.4.2",
@@ -92,6 +92,7 @@
"redux-thunk": "2.2.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
"rtcstats": "github:jitsi/rtcstats#v6.2.0",
"stackblur-canvas": "2.3.0",
"styled-components": "3.4.9",
"util": "0.12.1",
"uuid": "3.1.0",
@@ -128,7 +129,7 @@
"imports-loader": "0.7.1",
"jetifier": "1.6.4",
"metro-react-native-babel-preset": "0.56.0",
"node-sass": "4.14.1",
"sass": "1.26.8",
"string-replace-loader": "2.1.1",
"style-loader": "0.19.0",
"unorm": "1.6.0",

View File

@@ -15,8 +15,6 @@ export default [
'abTesting',
'analytics.disabled',
'audioLevelsInterval',
'autoRecord',
'autoRecordToken',
'apiLogLevels',
'avgRtpStatsN',

View File

@@ -24,35 +24,19 @@ const _RIGHT_WATERMARK_STYLE = {
type Props = {
/**
* The user selected url used to navigate to on logo click.
* The link used to navigate to on logo click.
*/
_customLogoLink: string,
_logoLink: string,
/**
* The url of the user selected logo.
* The url for the logo.
*/
_customLogoUrl: string,
_logoUrl: string,
/**
* Whether or not the current user is logged in through a JWT.
* If the Jitsi watermark should be displayed or not.
*/
_isGuest: boolean,
/**
* Whether or not the current meeting is a vpaas one.
*/
_isVpaas: boolean,
/**
* Flag used to signal that the logo can be displayed.
* It becomes true after the user customization options are fetched.
*/
_readyToDisplayJitsiWatermark: boolean,
/**
* Returns true if welcome page is visible at the moment.
*/
_welcomePageIsVisible: boolean,
_showJitsiWatermark: boolean,
/**
* The default value for the Jitsi logo URL.
@@ -75,27 +59,11 @@ type State = {
*/
brandWatermarkLink: string,
/**
* The url to open when clicking the Jitsi watermark.
*/
jitsiWatermarkLink: string,
/**
* Whether or not the brand watermark should be displayed.
*/
showBrandWatermark: boolean,
/**
* Whether or not the Jitsi watermark should be displayed.
*/
showJitsiWatermark: boolean,
/**
* Whether or not the Jitsi watermark should be displayed for users not
* logged in through a JWT.
*/
showJitsiWatermarkForGuests: boolean,
/**
* Whether or not the show the "powered by Jitsi.org" link.
*/
@@ -117,29 +85,17 @@ class Watermarks extends Component<Props, State> {
super(props);
let showBrandWatermark;
let showJitsiWatermark;
let showJitsiWatermarkForGuests;
if (interfaceConfig.filmStripOnly) {
showBrandWatermark = false;
showJitsiWatermark = false;
showJitsiWatermarkForGuests = false;
} else {
showBrandWatermark = interfaceConfig.SHOW_BRAND_WATERMARK;
showJitsiWatermark = interfaceConfig.SHOW_JITSI_WATERMARK;
showJitsiWatermarkForGuests
= interfaceConfig.SHOW_WATERMARK_FOR_GUESTS;
}
this.state = {
brandWatermarkLink:
showBrandWatermark ? interfaceConfig.BRAND_WATERMARK_LINK : '',
jitsiWatermarkLink:
showJitsiWatermark || showJitsiWatermarkForGuests
? interfaceConfig.JITSI_WATERMARK_LINK : '',
showBrandWatermark,
showJitsiWatermark,
showJitsiWatermarkForGuests,
showPoweredBy: interfaceConfig.SHOW_POWERED_BY
};
}
@@ -166,55 +122,6 @@ class Watermarks extends Component<Props, State> {
);
}
/**
* Returns true if the watermark is ready to be displayed.
*
* @private
* @returns {boolean}
*/
_canDisplayJitsiWatermark() {
const {
showJitsiWatermark,
showJitsiWatermarkForGuests
} = this.state;
const {
_isGuest,
_readyToDisplayJitsiWatermark,
_welcomePageIsVisible
} = this.props;
return (_readyToDisplayJitsiWatermark
&& (showJitsiWatermark || (_isGuest && showJitsiWatermarkForGuests)))
|| _welcomePageIsVisible;
}
/**
* Returns the background image style.
*
* @private
* @returns {string}
*/
_getBackgroundImageStyle() {
const {
_customLogoUrl,
_isVpaas,
defaultJitsiLogoURL
} = this.props;
let style = 'none';
if (_isVpaas) {
if (_customLogoUrl) {
style = `url(${_customLogoUrl})`;
}
} else {
style = `url(${_customLogoUrl
|| defaultJitsiLogoURL
|| interfaceConfig.DEFAULT_LOGO_URL})`;
}
return style;
}
/**
* Renders a brand watermark if it is enabled.
*
@@ -254,33 +161,28 @@ class Watermarks extends Component<Props, State> {
* @returns {ReactElement|null}
*/
_renderJitsiWatermark() {
const {
_logoLink,
_logoUrl,
_showJitsiWatermark
} = this.props;
let reactElement = null;
if (this._canDisplayJitsiWatermark()) {
const backgroundImage = this._getBackgroundImageStyle();
const link = this.props._customLogoLink || this.state.jitsiWatermarkLink;
const additionalStyles = {};
if (backgroundImage === 'none') {
additionalStyles.height = 0;
additionalStyles.width = 0;
}
if (_showJitsiWatermark) {
const style = {
backgroundImage,
backgroundImage: `url(${_logoUrl})`,
maxWidth: 140,
maxHeight: 70,
...additionalStyles
maxHeight: 70
};
reactElement = (<div
className = 'watermark leftwatermark'
style = { style } />);
if (link) {
if (_logoLink) {
reactElement = (
<a
href = { link }
href = { _logoLink }
target = '_new'>
{ reactElement }
</a>
@@ -319,27 +221,52 @@ class Watermarks extends Component<Props, State> {
* Maps parts of Redux store to component prop types.
*
* @param {Object} state - Snapshot of Redux store.
* @param {Object} ownProps - Component's own props.
* @returns {Props}
*/
function _mapStateToProps(state) {
function _mapStateToProps(state, ownProps) {
const { isGuest } = state['features/base/jwt'];
const { customizationReady, logoClickUrl, logoImageUrl } = state['features/dynamic-branding'];
const { room } = state['features/base/conference'];
const {
customizationReady,
customizationFailed,
defaultBranding,
useDynamicBrandingData,
logoClickUrl,
logoImageUrl
} = state['features/dynamic-branding'];
const isValidRoom = state['features/base/conference'].room;
const {
DEFAULT_LOGO_URL,
JITSI_WATERMARK_LINK,
SHOW_JITSI_WATERMARK,
SHOW_JITSI_WATERMARK_FOR_GUESTS,
filmStripOnly
} = interfaceConfig;
let _showJitsiWatermark = (!filmStripOnly
&& (customizationReady && !customizationFailed)
&& (SHOW_JITSI_WATERMARK || (isGuest && SHOW_JITSI_WATERMARK_FOR_GUESTS)))
|| !isValidRoom;
let _logoUrl = logoImageUrl;
let _logoLink = logoClickUrl;
if (useDynamicBrandingData) {
if (isVpaasMeeting(state)) {
// don't show logo if request fails or no logo set for vpaas meetings
_showJitsiWatermark = !customizationFailed && Boolean(logoImageUrl);
} else if (defaultBranding) {
_logoUrl = DEFAULT_LOGO_URL;
_logoLink = JITSI_WATERMARK_LINK;
}
} else {
// When there is no custom branding data use defaults
_logoUrl = ownProps.defaultJitsiLogoURL || DEFAULT_LOGO_URL;
_logoLink = JITSI_WATERMARK_LINK;
}
return {
/**
* The indicator which determines whether the local participant is a
* guest in the conference.
*
* @private
* @type {boolean}
*/
_customLogoLink: logoClickUrl,
_customLogoUrl: logoImageUrl,
_isGuest: isGuest,
_isVpaas: isVpaasMeeting(state),
_readyToDisplayJitsiWatermark: customizationReady,
_welcomePageIsVisible: !room
_logoLink,
_logoUrl,
_showJitsiWatermark
};
}

View File

@@ -3,6 +3,11 @@
*/
export const SET_DYNAMIC_BRANDING_DATA = 'SET_DYNAMIC_BRANDING_DATA';
/**
* Action used to signal the customization failed.
*/
export const SET_DYNAMIC_BRANDING_FAILED = 'SET_DYNAMIC_BRANDING_FAILED';
/**
* Action used to signal the branding elements are ready to be displayed
*/

View File

@@ -4,7 +4,11 @@ import { getLogger } from 'jitsi-meet-logger';
import { doGetJSON } from '../base/util';
import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionTypes';
import {
SET_DYNAMIC_BRANDING_DATA,
SET_DYNAMIC_BRANDING_FAILED,
SET_DYNAMIC_BRANDING_READY
} from './actionTypes';
import { extractFqnFromPath } from './functions';
const logger = getLogger(__filename);
@@ -32,6 +36,8 @@ export function fetchCustomBrandingData() {
return dispatch(setDynamicBrandingData(res));
} catch (err) {
logger.error('Error fetching branding data', err);
return dispatch(setDynamicBrandingFailed());
}
}
@@ -63,3 +69,14 @@ function setDynamicBrandingReady() {
type: SET_DYNAMIC_BRANDING_READY
};
}
/**
* Action used to signal the branding request failed.
*
* @returns {Object}
*/
function setDynamicBrandingFailed() {
return {
type: SET_DYNAMIC_BRANDING_FAILED
};
}

View File

@@ -2,7 +2,11 @@
import { ReducerRegistry } from '../base/redux';
import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionTypes';
import {
SET_DYNAMIC_BRANDING_DATA,
SET_DYNAMIC_BRANDING_FAILED,
SET_DYNAMIC_BRANDING_READY
} from './actionTypes';
/**
* The name of the redux store/state property which is the root of the redux
@@ -11,12 +15,80 @@ import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionT
const STORE_NAME = 'features/dynamic-branding';
const DEFAULT_STATE = {
/**
* The custom background color for the LargeVideo.
*
* @public
* @type {string}
*/
backgroundColor: '',
/**
* The custom background image used on the LargeVideo.
*
* @public
* @type {string}
*/
backgroundImageUrl: '',
/**
* Flag indicating that the logo (JitsiWatermark) can be displayed.
* This is used in order to avoid image flickering.
*
* @public
* @type {boolean}
*/
customizationReady: false,
/**
* Flag indicating that the dynamic branding data request has failed.
* When the request fails there is no logo (JitsiWatermark) displayed.
*
* @public
* @type {boolean}
*/
customizationFailed: false,
/**
* Flag indicating that the dynamic branding has not been modified and should use
* the default options.
*
* @public
* @type {boolean}
*/
defaultBranding: true,
/**
* The custom invite domain.
*
* @public
* @type {string}
*/
inviteDomain: '',
/**
* The custom url used when the user clicks the logo.
*
* @public
* @type {string}
*/
logoClickUrl: '',
logoImageUrl: ''
/**
* The custom logo (JitisWatermark).
*
* @public
* @type {string}
*/
logoImageUrl: '',
/**
* Flag used to signal if the app should use a custom logo or not
*
* @public
* @type {boolean}
*/
useDynamicBrandingData: false
};
/**
@@ -25,15 +97,33 @@ const DEFAULT_STATE = {
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_DYNAMIC_BRANDING_DATA: {
const { backgroundColor, backgroundImageUrl, inviteDomain, logoClickUrl, logoImageUrl } = action.value;
const {
backgroundColor,
backgroundImageUrl,
defaultBranding,
inviteDomain,
logoClickUrl,
logoImageUrl
} = action.value;
return {
backgroundColor,
backgroundImageUrl,
defaultBranding,
inviteDomain,
logoClickUrl,
logoImageUrl,
customizationReady: true
customizationFailed: false,
customizationReady: true,
useDynamicBrandingData: true
};
}
case SET_DYNAMIC_BRANDING_FAILED: {
return {
...state,
customizationReady: true,
customizationFailed: true,
useDynamicBrandingData: true
};
}
case SET_DYNAMIC_BRANDING_READY:
@@ -41,7 +131,6 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
...state,
customizationReady: true
};
}
return state;

View File

@@ -9,6 +9,7 @@ import {
import { _handleParticipantError } from '../base/conference';
import { MEDIA_TYPE } from '../base/media';
import { getParticipants } from '../base/participants';
import { getTrackByMediaTypeAndParticipant } from '../base/tracks';
import { reportError } from '../base/util';
import { shouldDisplayTileView } from '../video-layout';
@@ -17,7 +18,56 @@ import {
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
} from './actionTypes';
declare var APP: Object;
/**
* Captures a screenshot of the video displayed on the large video.
*
* @returns {Function}
*/
export function captureLargeVideoScreenshot() {
return (dispatch: Dispatch<any>, getState: Function): Promise<Object> => {
const state = getState();
const largeVideo = state['features/large-video'];
if (!largeVideo) {
return Promise.resolve();
}
const tracks = state['features/base/tracks'];
const { jitsiTrack } = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, largeVideo.participantId);
const videoStream = jitsiTrack.getOriginalStream();
// Create a HTML canvas and draw video from the track on to the canvas.
const [ track ] = videoStream.getVideoTracks();
const { height, width } = track.getSettings() ?? track.getConstraints();
const canvasElement = document.createElement('canvas');
const ctx = canvasElement.getContext('2d');
const videoElement = document.createElement('video');
videoElement.height = parseInt(height, 10);
videoElement.width = parseInt(width, 10);
videoElement.autoplay = true;
videoElement.srcObject = videoStream;
canvasElement.height = videoElement.height;
canvasElement.width = videoElement.width;
// Wait for the video to load before drawing on to the canvas.
const promise = new Promise(resolve => {
videoElement.onloadeddata = () => resolve();
});
return promise.then(() => {
ctx.drawImage(videoElement, 0, 0, videoElement.width, videoElement.height);
const dataURL = canvasElement.toDataURL('image/png', 1.0);
// Cleanup.
ctx.clearRect(0, 0, videoElement.width, videoElement.height);
videoElement.srcObject = null;
canvasElement.remove();
videoElement.remove();
return Promise.resolve(dataURL);
});
};
}
/**
* Signals conference to select a participant.
@@ -50,7 +100,7 @@ export function selectParticipant() {
/**
* Action to select the participant to be displayed in LargeVideo based on the
* participant id provided. If a partcipant id is not provided, the LargeVideo
* participant id provided. If a participant id is not provided, the LargeVideo
* participant will be selected based on a variety of factors: If there is a
* dominant or pinned speaker, or if there are remote tracks, etc.
*

View File

@@ -0,0 +1,3 @@
// @flow
export * from './actions.any';

View File

@@ -0,0 +1,28 @@
// @flow
import type { Dispatch } from 'redux';
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
export * from './actions.any';
/**
* Resizes the large video container based on the dimensions provided.
*
* @param {number} width - Width that needs to be applied on the large video container.
* @param {number} height - Height that needs to be applied on the large video container.
* @returns {Function}
*/
export function resizeLargeVideo(width: number, height: number) {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const largeVideo = state['features/large-video'];
if (largeVideo) {
const largeVideoContainer = VideoLayout.getLargeVideo();
largeVideoContainer.updateContainerSize(width, height);
largeVideoContainer.resize();
}
};
}

View File

@@ -1,11 +1,11 @@
// @flow
import * as bodyPix from '@tensorflow-models/body-pix';
import * as StackBlur from 'stackblur-canvas';
import {
CLEAR_INTERVAL,
INTERVAL_TIMEOUT,
SET_INTERVAL,
CLEAR_TIMEOUT,
TIMEOUT_TICK,
SET_TIMEOUT,
timerWorkerScript
} from './TimerWorker';
@@ -17,6 +17,7 @@ import {
export default class JitsiStreamBlurEffect {
_bpModel: Object;
_inputVideoElement: HTMLVideoElement;
_inputVideoCanvasElement: HTMLCanvasElement;
_onMaskFrameTimer: Function;
_maskFrameTimerWorker: Worker;
_maskInProgress: boolean;
@@ -43,6 +44,7 @@ export default class JitsiStreamBlurEffect {
this._outputCanvasElement = document.createElement('canvas');
this._outputCanvasElement.getContext('2d');
this._inputVideoElement = document.createElement('video');
this._inputVideoCanvasElement = document.createElement('canvas');
}
/**
@@ -53,10 +55,8 @@ export default class JitsiStreamBlurEffect {
* @returns {void}
*/
async _onMaskFrameTimer(response: Object) {
if (response.data.id === INTERVAL_TIMEOUT) {
if (!this._maskInProgress) {
await this._renderMask();
}
if (response.data.id === TIMEOUT_TICK) {
await this._renderMask();
}
}
@@ -67,20 +67,53 @@ export default class JitsiStreamBlurEffect {
* @returns {void}
*/
async _renderMask() {
this._maskInProgress = true;
this._segmentationData = await this._bpModel.segmentPerson(this._inputVideoElement, {
internalResolution: 'medium', // resized to 0.5 times of the original resolution before inference
maxDetections: 1, // max. number of person poses to detect per image
segmentationThreshold: 0.7 // represents probability that a pixel belongs to a person
});
this._maskInProgress = false;
bodyPix.drawBokehEffect(
this._outputCanvasElement,
this._inputVideoElement,
this._segmentationData,
12, // Constant for background blur, integer values between 0-20
7 // Constant for edge blur, integer values between 0-20
if (!this._maskInProgress) {
this._maskInProgress = true;
this._bpModel.segmentPerson(this._inputVideoElement, {
internalResolution: 'low', // resized to 0.5 times of the original resolution before inference
maxDetections: 1, // max. number of person poses to detect per image
segmentationThreshold: 0.7, // represents probability that a pixel belongs to a person
flipHorizontal: false,
scoreThreshold: 0.2
}).then(data => {
this._segmentationData = data;
this._maskInProgress = false;
});
}
const inputCanvasCtx = this._inputVideoCanvasElement.getContext('2d');
inputCanvasCtx.drawImage(this._inputVideoElement, 0, 0);
const currentFrame = inputCanvasCtx.getImageData(
0,
0,
this._inputVideoCanvasElement.width,
this._inputVideoCanvasElement.height
);
if (this._segmentationData) {
const blurData = new ImageData(currentFrame.data.slice(), currentFrame.width, currentFrame.height);
StackBlur.imageDataRGB(blurData, 0, 0, currentFrame.width, currentFrame.height, 12);
for (let x = 0; x < this._outputCanvasElement.width; x++) {
for (let y = 0; y < this._outputCanvasElement.height; y++) {
const n = (y * this._outputCanvasElement.width) + x;
if (this._segmentationData.data[n] === 0) {
currentFrame.data[n * 4] = blurData.data[n * 4];
currentFrame.data[(n * 4) + 1] = blurData.data[(n * 4) + 1];
currentFrame.data[(n * 4) + 2] = blurData.data[(n * 4) + 2];
currentFrame.data[(n * 4) + 3] = blurData.data[(n * 4) + 3];
}
}
}
}
this._outputCanvasElement.getContext('2d').putImageData(currentFrame, 0, 0);
this._maskFrameTimerWorker.postMessage({
id: SET_TIMEOUT,
timeMs: 1000 / 30
});
}
/**
@@ -110,14 +143,16 @@ export default class JitsiStreamBlurEffect {
this._outputCanvasElement.width = parseInt(width, 10);
this._outputCanvasElement.height = parseInt(height, 10);
this._inputVideoCanvasElement.width = parseInt(width, 10);
this._inputVideoCanvasElement.height = parseInt(height, 10);
this._inputVideoElement.width = parseInt(width, 10);
this._inputVideoElement.height = parseInt(height, 10);
this._inputVideoElement.autoplay = true;
this._inputVideoElement.srcObject = stream;
this._inputVideoElement.onloadeddata = () => {
this._maskFrameTimerWorker.postMessage({
id: SET_INTERVAL,
timeMs: 1000 / parseInt(frameRate, 10)
id: SET_TIMEOUT,
timeMs: 1000 / 30
});
};
@@ -131,7 +166,7 @@ export default class JitsiStreamBlurEffect {
*/
stopEffect() {
this._maskFrameTimerWorker.postMessage({
id: CLEAR_INTERVAL
id: CLEAR_TIMEOUT
});
this._maskFrameTimerWorker.terminate();

View File

@@ -1,34 +1,34 @@
/**
* SET_INTERVAL constant is used to set interval and it is set in
* SET_TIMEOUT constant is used to set interval and it is set in
* the id property of the request.data property. timeMs property must
* also be set. request.data example:
*
* {
* id: SET_INTERVAL,
* id: SET_TIMEOUT,
* timeMs: 33
* }
*/
export const SET_INTERVAL = 1;
export const SET_TIMEOUT = 1;
/**
* CLEAR_INTERVAL constant is used to clear the interval and it is set in
* CLEAR_TIMEOUT constant is used to clear the interval and it is set in
* the id property of the request.data property.
*
* {
* id: CLEAR_INTERVAL
* id: CLEAR_TIMEOUT
* }
*/
export const CLEAR_INTERVAL = 2;
export const CLEAR_TIMEOUT = 2;
/**
* INTERVAL_TIMEOUT constant is used as response and it is set in the id property.
* TIMEOUT_TICK constant is used as response and it is set in the id property.
*
* {
* id: INTERVAL_TIMEOUT
* id: TIMEOUT_TICK
* }
*/
export const INTERVAL_TIMEOUT = 3;
export const TIMEOUT_TICK = 3;
/**
* The following code is needed as string to create a URL from a Blob.
@@ -40,15 +40,15 @@ const code = `
onmessage = function(request) {
switch (request.data.id) {
case ${SET_INTERVAL}: {
timer = setInterval(() => {
postMessage({ id: ${INTERVAL_TIMEOUT} });
case ${SET_TIMEOUT}: {
timer = setTimeout(() => {
postMessage({ id: ${TIMEOUT_TICK} });
}, request.data.timeMs);
break;
}
case ${CLEAR_INTERVAL}: {
case ${CLEAR_TIMEOUT}: {
if (timer) {
clearInterval(timer);
clearTimeout(timer);
}
break;
}

View File

@@ -5,7 +5,7 @@ import debounce from 'lodash/debounce';
import { pinParticipant, getPinnedParticipant } from '../base/participants';
import { StateListenerRegistry, equals } from '../base/redux';
import { isFollowMeActive } from '../follow-me';
import { selectParticipant } from '../large-video';
import { selectParticipant } from '../large-video/actions';
import { setParticipantsWithScreenShare } from './actions';

View File

@@ -1,3 +1,4 @@
local new_throttle = require "util.throttle".create;
local st = require "util.stanza";
local token_util = module:require "token/util".new(module);
@@ -10,12 +11,19 @@ if token_util == nil then
return;
end
-- configuration to limit number of outgoing calls
local LIMIT_OUTGOING_CALLS = module:get_option_number("max_number_outgoing_calls", -1);
-- The maximum number of simultaneous calls,
-- and also the maximum number of new calls per minute that a session is allowed to create.
local limit_outgoing_calls;
local function load_config()
limit_outgoing_calls = module:get_option_number("max_number_outgoing_calls", -1);
end
load_config();
-- Header names to use to push extra data extracted from token, if any
local OUT_INITIATOR_USER_ATTR_NAME = "X-outbound-call-initiator-user";
local OUT_INITIATOR_GROUP_ATTR_NAME = "X-outbound-call-initiator-group";
local OUTGOING_CALLS_THROTTLE_INTERVAL = 60; -- if max_number_outgoing_calls is enabled it will be
-- the max number of outgoing calls a user can try for a minute
-- filters rayo iq in case of requested from not jwt authenticated sessions
-- or if the session has features in user context and it doesn't mention
@@ -53,15 +61,21 @@ module:hook("pre-iq/full", function(event)
end
-- now lets check any limits if configured
if LIMIT_OUTGOING_CALLS > 0
and get_concurrent_outgoing_count(
session.jitsi_meet_context_user["id"],
session.jitsi_meet_context_group) >= LIMIT_OUTGOING_CALLS
then
module:log("warn",
"Filtering stanza dial, stanza:%s, outgoing calls limit reached", tostring(stanza));
session.send(st.error_reply(stanza, "cancel", "resource-constraint"));
return true;
if limit_outgoing_calls > 0 then
if not session.dial_out_throttle then
module:log("debug", "Enabling dial-out throttle session=%s.", session);
session.dial_out_throttle = new_throttle(limit_outgoing_calls, OUTGOING_CALLS_THROTTLE_INTERVAL);
end
if not session.dial_out_throttle:poll(1) -- we first check the throttle so we can mark one incoming dial for the balance
or get_concurrent_outgoing_count(session.jitsi_meet_context_user["id"], session.jitsi_meet_context_group)
>= limit_outgoing_calls
then
module:log("warn",
"Filtering stanza dial, stanza:%s, outgoing calls limit reached", tostring(stanza));
session.send(st.error_reply(stanza, "cancel", "resource-constraint"));
return true;
end
end
-- now lets insert token information if any
@@ -163,3 +177,4 @@ function get_concurrent_outgoing_count(context_user, context_group)
return count;
end
module:hook_global('config-reloaded', load_config);

View File

@@ -142,7 +142,8 @@ const config = {
// value that is a mock (/index.js).
__filename: true,
// Provide an empty 'fs' module.
// Provide some empty Node modules (required by olm).
crypto: 'empty',
fs: 'empty'
},
optimization: {
@@ -190,7 +191,7 @@ module.exports = [
entry: {
'app.bundle': './app.js'
},
performance: getPerformanceHints(4.5 * 1024 * 1024)
performance: getPerformanceHints(4 * 1024 * 1024)
}),
Object.assign({}, config, {
entry: {