Compare commits

...

10 Commits

Author SHA1 Message Date
Hristo Terezov
4d1ed86e49 ref(Thumbnail): Create React component. 2020-11-12 11:10:04 -06:00
Avram Tudor
88c02fb658 Merge pull request #8039 from jitsi/tavram/fix-double-slash
fix(jaas) fix double slash for branded invite urls
2020-11-05 16:04:59 +02:00
Tudor-Ovidiu Avram
0f64c66f91 fix(jaas) fix double slash for branded invite urls 2020-11-05 15:43:53 +02:00
Дамян Минков
9f65ae52f1 fix: Prosody modules - drop unused and duplicate code and drop chatty debug statements (#8027)
* chore: Updates mod_smacks.lua version to latest.

https://hg.prosody.im/prosody-modules/file/db75772afb28/mod_smacks/mod_smacks.lua

* Drop unused modules.

* Update docs.

* Move utility functions away from domain mapper.

* Remove some chatty debug log messages.

* Drops not needed patch for mod_websocket.
2020-11-04 08:25:03 -06:00
Vlad Piersec
a242e86b23 chore(deps) amplitude 7.3.1 2020-11-04 16:18:06 +02:00
Saúl Ibarra Corretgé
4211db0893 fix(rn, screen-sharing) don't render own screen-share in large view 2020-11-04 14:45:16 +01:00
Titus-Andrei Moldovan
9a35026d6a feat(android) add screen-sharing support
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
Co-authored-by: zycwind <391321232@qq.com>
2020-11-04 14:45:16 +01:00
slauth
9742e90bb5 allow wildcard in token issuer verification 2020-11-03 10:45:47 -06:00
Saúl Ibarra Corretgé
2a01d3550c fix(conference) remove no longer needed code
There is no need for setting the availability of desktop sharing anymore. It can
now be detected on the spot.

The reson for the previous code was that way back when browser extensions were
needed, it was possible to start a conference without desktopo sharing support
and get it afterwards. This is no longer the case.
2020-11-03 16:15:01 +01:00
rugk
efce5a831b feat(misc) automatically assign feature-request tag to issues
…if they are reported as a feature request, of course.
2020-11-03 09:41:20 +01:00
51 changed files with 1072 additions and 1608 deletions

View File

@@ -1,7 +1,9 @@
---
name: "Feature request"
about: Suggest an idea for this project
title: ''
labels: 'feature-request'
assignees: ''
---
<!--

View File

@@ -2,10 +2,3 @@
* Notifies interested parties that hangup procedure will start.
*/
export const BEFORE_HANGUP = 'conference.before_hangup';
/**
* Notifies interested parties that desktop sharing enable/disable state is
* changed.
*/
export const DESKTOP_SHARING_ENABLED_CHANGED
= 'conference.desktop_sharing_enabled_changed';

View File

@@ -12,7 +12,7 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-feature
android:glEsVersion="0x00020000"
@@ -34,8 +34,7 @@
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="adjustResize">
</activity>
android:windowSoftInputMode="adjustResize"></activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service
@@ -46,7 +45,9 @@
</intent-filter>
</service>
<service android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService" />
<service
android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService"
android:foregroundServiceType="mediaProjection" />
</application>
</manifest>

View File

@@ -43,6 +43,7 @@ class PictureInPictureModule extends ReactContextBaseJavaModule {
private static final String TAG = NAME;
private static boolean isSupported;
private boolean isDisabled;
public PictureInPictureModule(ReactApplicationContext reactContext) {
super(reactContext);
@@ -83,6 +84,10 @@ class PictureInPictureModule extends ReactContextBaseJavaModule {
*/
@TargetApi(Build.VERSION_CODES.O)
public void enterPictureInPicture() {
if (isDisabled) {
return;
}
if (!isSupported) {
throw new IllegalStateException("Picture-in-Picture not supported");
}
@@ -126,6 +131,11 @@ class PictureInPictureModule extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void setPictureInPictureDisabled(Boolean disabled) {
this.isDisabled = disabled;
}
public boolean isPictureInPictureSupported() {
return isSupported;
}

View File

@@ -41,8 +41,7 @@ import {
lockStateChanged,
onStartMutedPolicyChanged,
p2pStatusChanged,
sendLocalParticipant,
setDesktopSharingEnabled
sendLocalParticipant
} from './react/features/base/conference';
import {
checkAndNotifyForNewDevice,
@@ -441,17 +440,8 @@ export default {
* the tracks won't exist).
*/
_localTracksInitialized: false,
isSharingScreen: false,
/**
* Indicates if the desktop sharing functionality has been enabled.
* It takes into consideration the status returned by
* {@link JitsiMeetJS.isDesktopSharingEnabled()}. The latter can be false
* either if the desktop sharing is not supported by the current browser
* or if it was disabled through lib-jitsi-meet specific options (check
* config.js for listed options).
*/
isDesktopSharingEnabled: false,
isSharingScreen: false,
/**
* The local audio track (if any).
@@ -679,14 +669,6 @@ export default {
con.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED, _connectionFailedHandler);
APP.connection = connection = con;
// Desktop sharing related stuff:
this.isDesktopSharingEnabled
= JitsiMeetJS.isDesktopSharingEnabled();
eventEmitter.emit(JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED, this.isDesktopSharingEnabled);
APP.store.dispatch(
setDesktopSharingEnabled(this.isDesktopSharingEnabled));
this._createRoom(tracks);
APP.remoteControl.init();
@@ -1532,9 +1514,8 @@ export default {
if (this.videoSwitchInProgress) {
return Promise.reject('Switch in progress.');
}
if (!this.isDesktopSharingEnabled) {
return Promise.reject(
'Cannot toggle screen sharing: not supported.');
if (!JitsiMeetJS.isDesktopSharingEnabled()) {
return Promise.reject('Cannot toggle screen sharing: not supported.');
}
if (this.isAudioOnly()) {
@@ -2011,7 +1992,6 @@ export default {
formattedDisplayName
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
});
APP.UI.changeDisplayName(id, formattedDisplayName);
}
);
room.on(
@@ -2074,10 +2054,7 @@ export default {
});
room.on(JitsiConferenceEvents.KICKED, participant => {
APP.UI.hideStats();
APP.store.dispatch(kickedOut(room, participant));
// FIXME close
});
room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
@@ -2410,11 +2387,6 @@ export default {
APP.keyboardshortcut.init();
APP.store.dispatch(conferenceJoined(room));
const displayName
= APP.store.getState()['features/base/settings'].displayName;
APP.UI.changeDisplayName('localVideoContainer', displayName);
},
/**
@@ -2912,10 +2884,6 @@ export default {
APP.store.dispatch(updateSettings({
displayName: formattedNickname
}));
if (room) {
APP.UI.changeDisplayName(id, formattedNickname);
}
},
/**

View File

@@ -293,8 +293,8 @@ PODS:
- React
- react-native-splash-screen (3.2.0):
- React
- react-native-webrtc (1.84.0):
- React
- react-native-webrtc (1.84.1):
- React-Core
- react-native-webview (10.9.0):
- React
- React-RCTActionSheet (0.61.5-jitsi.2):
@@ -562,7 +562,7 @@ SPEC CHECKSUMS:
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-webrtc: 9268ae9a2bc9730796b0968d012327e92c392adf
react-native-webrtc: edd689b0d5a462d7a6f6f52bca3f9414fc0ee11c
react-native-webview: 6ee7868ca8eba635dbf7963986d1ab7959da0391
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6

View File

@@ -2,7 +2,6 @@
import Logger from 'jitsi-meet-logger';
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
import {
createApiEvent,
sendAnalytics
@@ -14,7 +13,7 @@ import {
setSubject
} from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import { pinParticipant } from '../../react/features/base/participants';
import {
processExternalDeviceRequest
@@ -46,14 +45,6 @@ declare var APP: Object;
*/
let commands = {};
/**
* The state of screen sharing(started/stopped) before the screen sharing is
* enabled and initialized.
* NOTE: This flag help us to cache the state and use it if toggle-share-screen
* was received before the initialization.
*/
let initialScreenSharingState = false;
/**
* The transport instance used for communication with external apps.
*
@@ -430,19 +421,6 @@ function initCommands() {
});
}
/**
* Listens for desktop/screen sharing enabled events and toggles the screen
* sharing if needed.
*
* @param {boolean} enabled - Current screen sharing enabled status.
* @returns {void}
*/
function onDesktopSharingEnabledChanged(enabled = false) {
if (enabled && initialScreenSharingState) {
toggleScreenSharing();
}
}
/**
* Check whether the API should be enabled or not.
*
@@ -470,12 +448,10 @@ function shouldBeEnabled() {
* @returns {void}
*/
function toggleScreenSharing(enable) {
if (APP.conference.isDesktopSharingEnabled) {
// eslint-disable-next-line no-empty-function
APP.conference.toggleScreenSharing(enable).catch(() => {});
} else {
initialScreenSharingState = !initialScreenSharingState;
if (JitsiMeetJS.isDesktopSharingEnabled()) {
APP.conference.toggleScreenSharing(enable).catch(() => {
logger.warn('Failed to toggle screen-sharing');
});
}
}
@@ -508,10 +484,6 @@ class API {
*/
this._enabled = true;
APP.conference.addListener(
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
onDesktopSharingEnabledChanged);
initCommands();
}
@@ -1058,9 +1030,6 @@ class API {
dispose() {
if (this._enabled) {
this._enabled = false;
APP.conference.removeListener(
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
onDesktopSharingEnabledChanged);
}
}
}

View File

@@ -7,7 +7,6 @@ import EventEmitter from 'events';
import Logger from 'jitsi-meet-logger';
import { isMobileBrowser } from '../../react/features/base/environment/utils';
import { getLocalParticipant } from '../../react/features/base/participants';
import { toggleChat } from '../../react/features/chat';
import { setDocumentUrl } from '../../react/features/etherpad';
import { setFilmstripVisible } from '../../react/features/filmstrip';
@@ -99,29 +98,11 @@ UI.notifyReservationError = function(code, msg) {
});
};
/**
* Change nickname for the user.
* @param {string} id user id
* @param {string} displayName new nickname
*/
UI.changeDisplayName = function(id, displayName) {
VideoLayout.onDisplayNameChanged(id, displayName);
};
/**
* Initialize conference UI.
*/
UI.initConference = function() {
const { getState } = APP.store;
const { id, name } = getLocalParticipant(getState);
UI.showToolbar();
const displayName = config.displayJids ? id : name;
if (displayName) {
UI.changeDisplayName('localVideoContainer', displayName);
}
};
/**
@@ -247,19 +228,12 @@ UI.getSharedDocumentManager = () => etherpadManager;
* @param {JitsiParticipant} user
*/
UI.addUser = function(user) {
const id = user.getId();
const displayName = user.getDisplayName();
const status = user.getStatus();
if (status) {
// FIXME: move updateUserStatus in participantPresenceChanged action
UI.updateUserStatus(user, status);
}
// set initial display name
if (displayName) {
UI.changeDisplayName(id, displayName);
}
};
/**
@@ -494,14 +468,6 @@ UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
*/
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
/**
* Hide connection quality statistics from UI.
*/
UI.hideStats = function() {
VideoLayout.hideStats();
};
UI.notifyTokenAuthFailed = function() {
messageHandler.showError({
descriptionKey: 'dialog.tokenAuthFailed',

View File

@@ -1,10 +1,15 @@
/* global $ */
/* global $, APP */
import Logger from 'jitsi-meet-logger';
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
import SmallVideo from '../videolayout/SmallVideo';
const logger = Logger.getLogger(__filename);
/* eslint-enable no-unused-vars */
/**
*
@@ -24,17 +29,12 @@ export default class SharedVideoThumb extends SmallVideo {
this.videoSpanId = 'sharedVideoContainer';
this.container = this.createContainer(this.videoSpanId);
this.$container = $(this.container);
this.renderThumbnail();
this._setThumbnailSize();
this.bindHoverHandler();
this.updateDisplayName();
this.container.onclick = this._onContainerClick;
}
/**
*
*/
initializeAvatar() {} // eslint-disable-line no-empty-function
/**
*
* @param {*} spanId
@@ -45,18 +45,6 @@ export default class SharedVideoThumb extends SmallVideo {
container.id = spanId;
container.className = 'videocontainer';
// add the avatar
const avatar = document.createElement('img');
avatar.className = 'sharedVideoAvatar';
avatar.src = `https://img.youtube.com/vi/${this.url}/0.jpg`;
container.appendChild(avatar);
const displayNameContainer = document.createElement('div');
displayNameContainer.className = 'displayNameContainer';
container.appendChild(displayNameContainer);
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
@@ -68,21 +56,14 @@ export default class SharedVideoThumb extends SmallVideo {
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
* Renders the thumbnail.
*/
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
}

View File

@@ -37,7 +37,6 @@ const Filmstrip = {
*/
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
const thumbs = this._getThumbs(!forceUpdate);
const avatarSize = height / 2;
if (thumbs.localThumb) {
thumbs.localThumb.css({
@@ -58,11 +57,6 @@ const Filmstrip = {
width: `${width}px`
});
}
$('.avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
},
/**
@@ -77,7 +71,6 @@ const Filmstrip = {
if (thumbs.localThumb) {
const { height, width } = local;
const avatarSize = height / 2;
thumbs.localThumb.css({
height: `${height}px`,
@@ -85,15 +78,10 @@ const Filmstrip = {
'min-width': `${width}px`,
width: `${width}px`
});
$('#localVideoContainer > .avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
if (thumbs.remoteThumbs) {
const { height, width } = remote;
const avatarSize = height / 2;
thumbs.remoteThumbs.css({
height: `${height}px`,
@@ -101,10 +89,6 @@ const Filmstrip = {
'min-width': `${width}px`,
width: `${width}px`
});
$('#filmstripRemoteVideosContainer > span > .avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
},
@@ -126,10 +110,6 @@ const Filmstrip = {
'min-width': '',
'min-height': ''
});
$('#localVideoContainer > .avatar-container').css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
}
if (thumbs.remoteThumbs) {
@@ -142,10 +122,6 @@ const Filmstrip = {
'min-width': '',
'min-height': ''
});
$('#filmstripRemoteVideosContainer > span > .avatar-container').css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
}
},

View File

@@ -1,23 +1,23 @@
/* global $, config, interfaceConfig, APP */
/* global $, config, APP */
import Logger from 'jitsi-meet-logger';
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
import { VideoTrack } from '../../../react/features/base/media';
import { updateSettings } from '../../../react/features/base/settings';
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
import UIEvents from '../../../service/UI/UIEvents';
import SmallVideo from './SmallVideo';
const logger = Logger.getLogger(__filename);
/**
*
*/
@@ -37,6 +37,7 @@ export default class LocalVideo extends SmallVideo {
this.isLocal = true;
this._setThumbnailSize();
this.updateDOMLocation();
this.renderThumbnail();
this.localVideoId = null;
this.bindHoverHandler();
@@ -44,7 +45,6 @@ export default class LocalVideo extends SmallVideo {
this._buildContextMenu();
}
this.emitter = emitter;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center';
Object.defineProperty(this, 'id', {
get() {
@@ -53,18 +53,6 @@ export default class LocalVideo extends SmallVideo {
});
this.initBrowserSpecificProperties();
// Set default display name.
this.updateDisplayName();
// Initialize the avatar display with an avatar url selected from the redux
// state. Redux stores the local user with a hardcoded participant id of
// 'local' if no id has been assigned yet.
this.initializeAvatar();
this.addAudioLevelIndicator();
this.updateIndicators();
this.updateStatusBar();
this.container.onclick = this._onContainerClick;
}
@@ -77,38 +65,19 @@ export default class LocalVideo extends SmallVideo {
containerSpan.classList.add('videocontainer');
containerSpan.id = this.videoSpanId;
containerSpan.innerHTML = `
<div class = 'videocontainer__background'></div>
<span id = 'localVideoWrapper'></span>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>`;
return containerSpan;
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
* Renders the thumbnail.
*/
updateDisplayName() {
if (!this.container) {
logger.warn(
`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
}
this._renderDisplayName({
allowEditing: APP.store.getState()['features/base/jwt'].isGuest,
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
elementID: 'localDisplayName',
participantID: this.id
});
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
/**
@@ -118,7 +87,6 @@ export default class LocalVideo extends SmallVideo {
changeVideo(stream) {
this.videoStream = stream;
this.localVideoId = `localVideo_${stream.getId()}`;
this._updateVideoElement();
// eslint-disable-next-line eqeqeq
const isVideo = stream.videoType != 'desktop';
@@ -128,17 +96,6 @@ export default class LocalVideo extends SmallVideo {
this.setFlipX(isVideo ? settings.localFlipX : false);
const endedHandler = () => {
const localVideoContainer
= document.getElementById('localVideoWrapper');
// Only remove if there is no video and not a transition state.
// Previous non-react logic created a new video element with each track
// removal whereas react reuses the video component so it could be the
// stream ended but a new one is being used.
if (localVideoContainer && this.videoStream.isEnded()) {
ReactDOM.unmountComponentAtNode(localVideoContainer);
}
this._notifyOfStreamEnded();
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};
@@ -254,35 +211,5 @@ export default class LocalVideo extends SmallVideo {
: document.getElementById('filmstripLocalVideoThumbnail');
appendTarget && appendTarget.appendChild(this.container);
this._updateVideoElement();
}
/**
* Renders the React Element for displaying video in {@code LocalVideo}.
*
*/
_updateVideoElement() {
const localVideoContainer = document.getElementById('localVideoWrapper');
const videoTrack
= getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = 'localVideo_container'
videoTrack = { videoTrack } />
</Provider>,
localVideoContainer
);
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay. Also, set the playsinline attribute on the
// video element so that local video doesn't open in full screen by default
// in Safari browser on iOS.
const video = this.container.querySelector('video');
video && video.setAttribute('playsinline', 'true');
video && !config.testing?.noAutoPlayVideo && video.play();
}
}

View File

@@ -19,6 +19,7 @@ import {
pinParticipant
} from '../../../react/features/base/participants';
import { isRemoteTrackMuted } from '../../../react/features/base/tracks';
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
import { PresenceLabel } from '../../../react/features/presence-status';
import {
REMOTE_CONTROL_MENU_STATES,
@@ -42,16 +43,6 @@ function createContainer(spanId) {
container.id = spanId;
container.className = 'videocontainer';
container.innerHTML = `
<div class = 'videocontainer__background'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>
<div class ='presence-label-container'></div>
<span class = 'remotevideomenu'></span>`;
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
@@ -82,10 +73,7 @@ export default class RemoteVideo extends SmallVideo {
this._audioStreamElement = null;
this._supportsRemoteControl = false;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
this.addRemoteVideoContainer();
this.updateIndicators();
this.updateDisplayName();
this.bindHoverHandler();
this.flipX = false;
this.isLocal = false;
@@ -117,17 +105,26 @@ export default class RemoteVideo extends SmallVideo {
addRemoteVideoContainer() {
this.container = createContainer(this.videoSpanId);
this.$container = $(this.container);
this.initializeAvatar();
this.renderThumbnail();
this._setThumbnailSize();
this.initBrowserSpecificProperties();
this.updateRemoteVideoMenu();
this.updateStatusBar();
this.addAudioLevelIndicator();
this.addPresenceLabel();
return this.container;
}
/**
* Renders the thumbnail.
*/
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
/**
* Generates the popup menu content.
*
@@ -357,9 +354,9 @@ export default class RemoteVideo extends SmallVideo {
* Removes RemoteVideo from the page.
*/
remove() {
super.remove();
this.removePresenceLabel();
this.removeRemoteVideoMenu();
ReactDOM.unmountComponentAtNode(this.container);
super.remove();
}
/**
@@ -432,24 +429,6 @@ export default class RemoteVideo extends SmallVideo {
}
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId} does not exist`);
return;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
}
/**
* Removes remote video menu element from video element identified by
* given <tt>videoElementId</tt>.
@@ -464,39 +443,4 @@ export default class RemoteVideo extends SmallVideo {
menuSpan.remove();
}
}
/**
* Mounts the {@code PresenceLabel} for displaying the participant's current
* presence status.
*
* @return {void}
*/
addPresenceLabel() {
const presenceLabelContainer = this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<PresenceLabel
participantID = { this.id }
className = 'presence-label' />
</I18nextProvider>
</Provider>,
presenceLabelContainer);
}
}
/**
* Unmounts the {@code PresenceLabel} component.
*
* @return {void}
*/
removePresenceLabel() {
const presenceLabelContainer = this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
}
}
}

View File

@@ -95,30 +95,6 @@ export default class SmallVideo {
this.videoIsHovered = false;
this.videoType = undefined;
/**
* Whether or not the connection indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showConnectionIndicator = !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
/**
* Whether or not the dominant speaker indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showDominantSpeaker = false;
/**
* Whether or not the raised hand indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showRaisedHand = false;
// Bind event handlers so they are only bound once for every instance.
this.updateView = this.updateView.bind(this);
@@ -179,103 +155,22 @@ export default class SmallVideo {
this.$container.hover(
() => {
this.videoIsHovered = true;
this.renderThumbnail(true);
this.updateView();
this.updateIndicators();
},
() => {
this.videoIsHovered = false;
this.renderThumbnail(false);
this.updateView();
this.updateIndicators();
}
);
}
/**
* Unmounts the ConnectionIndicator component.
* @returns {void}
*/
removeConnectionIndicator() {
this._showConnectionIndicator = false;
this.updateIndicators();
}
/**
* Create or updates the ReactElement for displaying status indicators about
* audio mute, video mute, and moderator status.
*
* @returns {void}
* Renders the thumbnail.
*/
updateStatusBar() {
const statusBarContainer = this.container.querySelector('.videocontainer__toolbar');
if (!statusBarContainer) {
return;
}
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<StatusIndicators
participantID = { this.id } />
</I18nextProvider>
</Provider>,
statusBarContainer);
}
/**
* Adds the element indicating the audio level of the participant.
*
* @returns {void}
*/
addAudioLevelIndicator() {
let audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
return;
}
audioLevelContainer = document.createElement('span');
audioLevelContainer.className = 'audioindicator-container';
this.container.appendChild(audioLevelContainer);
this.updateAudioLevelIndicator();
}
/**
* Removes the element indicating the audio level of the participant.
*
* @returns {void}
*/
removeAudioLevelIndicator() {
const audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
ReactDOM.unmountComponentAtNode(audioLevelContainer);
}
}
/**
* Updates the audio level for this small video.
*
* @param lvl the new audio level to set
* @returns {void}
*/
updateAudioLevelIndicator(lvl = 0) {
const audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
ReactDOM.render(<AudioLevelIndicator audioLevel = { lvl }/>, audioLevelContainer);
}
}
/**
* Queries the component's DOM for the element that should be the parent to the
* AudioLevelIndicator.
*
* @returns {HTMLElement} The DOM element that holds the AudioLevelIndicator.
*/
_getAudioLevelContainer() {
return this.container.querySelector('.audioindicator-container');
renderThumbnail() {
// Should be implemented by in subclasses.
}
/**
@@ -292,62 +187,6 @@ export default class SmallVideo {
return $($(this.container).find('video')[0]);
}
/**
* Selects the HTML image element which displays user's avatar.
*
* @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
* element which displays the user's avatar.
*/
$avatar() {
return this.$container.find('.avatar-container');
}
/**
* Returns the display name element, which appears on the video thumbnail.
*
* @return {jQuery} a jQuery selector pointing to the display name element of
* the video thumbnail
*/
$displayName() {
return this.$container.find('.displayNameContainer');
}
/**
* Creates or updates the participant's display name that is shown over the
* video preview.
*
* @param {Object} props - The React {@code Component} props to pass into the
* {@code DisplayName} component.
* @returns {void}
*/
_renderDisplayName(props) {
const displayNameContainer = this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<DisplayName { ...props } />
</I18nextProvider>
</Provider>,
displayNameContainer);
}
}
/**
* Removes the component responsible for showing the participant's display name,
* if its container is present.
*
* @returns {void}
*/
removeDisplayName() {
const displayNameContainer = this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.unmountComponentAtNode(displayNameContainer);
}
}
/**
* Enables / disables the css responsible for focusing/pinning a video
* thumbnail.
@@ -516,43 +355,6 @@ export default class SmallVideo {
}
}
/**
* Updates the react component displaying the avatar with the passed in avatar
* url.
*
* @returns {void}
*/
initializeAvatar() {
const thumbnail = this.$avatar().get(0);
if (thumbnail) {
// Maybe add a special case for local participant, as on init of
// LocalVideo.js the id is set to "local" but will get updated later.
ReactDOM.render(
<Provider store = { APP.store }>
<AvatarDisplay
className = 'userAvatar'
participantId = { this.id } />
</Provider>,
thumbnail
);
}
}
/**
* Unmounts any attached react components (particular the avatar image) from
* the avatar container.
*
* @returns {void}
*/
removeAvatar() {
const thumbnail = this.$avatar().get(0);
if (thumbnail) {
ReactDOM.unmountComponentAtNode(thumbnail);
}
}
/**
* Shows or hides the dominant speaker indicator.
* @param show whether to show or hide.
@@ -569,30 +371,8 @@ export default class SmallVideo {
return;
}
if (this._showDominantSpeaker === show) {
return;
}
this._showDominantSpeaker = show;
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
this.updateIndicators();
this.updateView();
}
/**
* Shows or hides the raised hand indicator.
* @param show whether to show or hide.
*/
showRaisedHandIndicator(show) {
if (!this.container) {
logger.warn(`Unable to raised hand indication - ${
this.videoSpanId} does not exist`);
return;
}
this._showRaisedHand = show;
this.updateIndicators();
this.$container.toggleClass('active-speaker', show);
}
/**
@@ -623,19 +403,7 @@ export default class SmallVideo {
*/
remove() {
logger.log('Remove thumbnail', this.id);
this.removeAudioLevelIndicator();
const toolbarContainer
= this.container.querySelector('.videocontainer__toolbar');
if (toolbarContainer) {
ReactDOM.unmountComponentAtNode(toolbarContainer);
}
this.removeConnectionIndicator();
this.removeDisplayName();
this.removeAvatar();
this._unmountIndicators();
this._unmountThumbnail();
// Remove whole container
if (this.container.parentNode) {
@@ -650,76 +418,9 @@ export default class SmallVideo {
* @returns {void}
*/
rerender() {
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
* state to display. Will create the React element if not already created.
*
* @private
* @returns {void}
*/
updateIndicators() {
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
if (!indicatorToolbar) {
return;
}
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
const iconSize = NORMAL;
const showConnectionIndicator = this.videoIsHovered || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
const state = APP.store.getState();
const currentLayout = getCurrentLayout(state);
const participantCount = getParticipantCount(state);
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';
}
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<div>
<AtlasKitThemeProvider mode = 'dark'>
{ this._showConnectionIndicator
? <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
iconSize = { iconSize }
isLocalVideo = { this.isLocal }
enableStatsDisplay = { !interfaceConfig.filmStripOnly }
participantId = { this.id }
statsPopoverPosition = { statsPopoverPosition } />
: null }
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { this.id }
tooltipPosition = { tooltipPosition } />
{ this._showDominantSpeaker && participantCount > 2
? <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
: null }
</AtlasKitThemeProvider>
</div>
</I18nextProvider>
</Provider>,
indicatorToolbar
);
}
/**
* Callback invoked when the thumbnail is clicked and potentially trigger
* pinning of the participant.
@@ -777,18 +478,10 @@ export default class SmallVideo {
}
/**
* Removes the React element responsible for showing connection status, dominant
* speaker, and raised hand icons.
*
* @private
* @returns {void}
* Unmounts the thumbnail.
*/
_unmountIndicators() {
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
if (indicatorToolbar) {
ReactDOM.unmountComponentAtNode(indicatorToolbar);
}
_unmountThumbnail() {
ReactDOM.unmountComponentAtNode(this.container);
}
/**
@@ -802,10 +495,6 @@ export default class SmallVideo {
switch (layout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
this.$container.css('padding-top', `${heightToWidthPercent}%`);
this.$avatar().css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
@@ -815,7 +504,6 @@ export default class SmallVideo {
if (typeof size !== 'undefined') {
const { height, width } = size;
const avatarSize = height / 2;
this.$container.css({
height: `${height}px`,
@@ -823,10 +511,6 @@ export default class SmallVideo {
'min-width': `${width}px`,
width: `${width}px`
});
this.$avatar().css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
break;
}
@@ -836,7 +520,6 @@ export default class SmallVideo {
if (typeof thumbnailSize !== 'undefined') {
const { height, width } = thumbnailSize;
const avatarSize = height / 2;
this.$container.css({
height: `${height}px`,
@@ -844,10 +527,6 @@ export default class SmallVideo {
'min-width': `${width}px`,
width: `${width}px`
});
this.$avatar().css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
break;
}

View File

@@ -116,12 +116,6 @@ const VideoLayout = {
* @param lvl the new audio level to update to
*/
setAudioLevel(id, lvl) {
const smallVideo = this.getSmallVideo(id);
if (smallVideo) {
smallVideo.updateAudioLevelIndicator(lvl);
}
if (largeVideo && id === largeVideo.id) {
largeVideo.updateLargeVideoAudioLevel(lvl);
}
@@ -137,19 +131,6 @@ const VideoLayout = {
this._updateLargeVideoIfDisplayed(localId);
},
/**
* Get's the localID of the conference and set it to the local video
* (small one). This needs to be called as early as possible, when muc is
* actually joined. Otherwise events can come with information like email
* and setting them assume the id is already set.
*/
mucJoined() {
// FIXME: replace this call with a generic update call once SmallVideo
// only contains a ReactElement. Then remove this call once the
// Filmstrip is fully in React.
localVideoThumbnail.updateIndicators();
},
/**
* Shows/hides local video.
* @param {boolean} true to make the local video visible, false - otherwise
@@ -345,22 +326,6 @@ const VideoLayout = {
this._updateLargeVideoIfDisplayed(id, true);
},
/**
* Display name changed.
*/
onDisplayNameChanged(id) {
if (id === 'localVideoContainer'
|| APP.conference.isLocalId(id)) {
localVideoThumbnail.updateDisplayName();
} else {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.updateDisplayName();
}
}
},
/**
* On dominant speaker changed event.
*
@@ -427,20 +392,6 @@ const VideoLayout = {
}
},
/**
* Hides all the indicators
*/
hideStats() {
for (const video in remoteVideos) { // eslint-disable-line guard-for-in
const remoteVideo = remoteVideos[video];
if (remoteVideo) {
remoteVideo.removeConnectionIndicator();
}
}
localVideoThumbnail.removeConnectionIndicator();
},
removeParticipantContainer(id) {
// Unlock large video
if (this.getPinnedId() === id) {
@@ -491,15 +442,6 @@ const VideoLayout = {
},
changeUserAvatar(id, avatarUrl) {
const smallVideo = VideoLayout.getSmallVideo(id);
if (smallVideo) {
smallVideo.initializeAvatar();
} else {
logger.warn(
`Missed avatar update - no small video yet for ${id}`
);
}
if (this.isCurrentlyOnLarge(id)) {
largeVideo.updateAvatar(avatarUrl);
}

View File

@@ -3,6 +3,7 @@
import EventEmitter from 'events';
import { getLogger } from 'jitsi-meet-logger';
import JitsiMeetJS from '../../react/features/base/lib-jitsi-meet';
import { DISCO_REMOTE_CONTROL_FEATURE }
from '../../service/remotecontrol/Constants';
import * as RemoteControlEvents
@@ -68,9 +69,7 @@ class RemoteControl extends EventEmitter {
* @returns {void}
*/
init() {
if (config.disableRemoteControl
|| this._initialized
|| !APP.conference.isDesktopSharingEnabled) {
if (config.disableRemoteControl || this._initialized || !JitsiMeetJS.isDesktopSharingEnabled()) {
return;
}
logger.log('Initializing remote control.');

12
package-lock.json generated
View File

@@ -4922,9 +4922,9 @@
"dev": true
},
"amplitude-js": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-7.1.1.tgz",
"integrity": "sha512-grEQf0p4V/q4aIcGYdGEJ6EquBXu91R/RorsYTQvh9O6sxjpwHf5vSDICQJq7twEElBrSHoSF77GUvC9ZTBj4A==",
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-7.3.1.tgz",
"integrity": "sha512-dsJU9MdtDDAOtKnbHrJuVBgsL5UGxD1P2B7doGdAQ1hxxT/5mFrmJTFzi1tKe+2ir3QtcRa9B0qvH8TMsGw22A==",
"requires": {
"@amplitude/ua-parser-js": "0.7.24",
"blueimp-md5": "^2.10.0",
@@ -14265,9 +14265,9 @@
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
},
"react-native-webrtc": {
"version": "1.84.0",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.84.0.tgz",
"integrity": "sha512-xPOFbrcehuBzLnFy3keCM2HyMsyCVDQjQNAn8SIHKH/PA8Q7kZ4spuytc2E1hBTr7zH/vQ2Px+DWqu7on12jag==",
"version": "1.84.1",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.84.1.tgz",
"integrity": "sha512-ewZBgKE+YhLaivo9Wh6aiaEp8ZRvFMqblrkDl1nptQiNNH6CungoAzSOxGDnHWAxepRfiUrW5qnADrsYKmaNeQ==",
"requires": {
"base64-js": "^1.1.2",
"event-target-shim": "^1.0.5",

View File

@@ -40,7 +40,7 @@
"@svgr/webpack": "4.3.2",
"@tensorflow-models/body-pix": "2.0.4",
"@tensorflow/tfjs": "1.5.1",
"amplitude-js": "7.1.1",
"amplitude-js": "7.3.1",
"base64-js": "1.3.1",
"bc-css-flags": "3.0.0",
"dropbox": "4.0.9",
@@ -84,7 +84,7 @@
"react-native-svg-transformer": "0.14.3",
"react-native-url-polyfill": "1.2.0",
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "1.84.0",
"react-native-webrtc": "1.84.1",
"react-native-webview": "10.9.0",
"react-native-youtube-iframe": "1.2.3",
"react-redux": "7.1.0",
@@ -92,7 +92,7 @@
"react-transition-group": "2.4.0",
"redux": "4.0.4",
"redux-thunk": "2.2.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
"rtcstats": "github:jitsi/rtcstats#v6.2.0",
"stackblur-canvas": "2.3.0",
"styled-components": "3.4.9",

View File

@@ -140,18 +140,6 @@ export const P2P_STATUS_CHANGED = 'P2P_STATUS_CHANGED';
*/
export const SEND_TONES = 'SEND_TONES';
/**
* The type of (redux) action which sets the desktop sharing enabled flag for
* the current conference.
*
* {
* type: SET_DESKTOP_SHARING_ENABLED,
* desktopSharingEnabled: boolean
* }
*/
export const SET_DESKTOP_SHARING_ENABLED
= 'SET_DESKTOP_SHARING_ENABLED';
/**
* The type of (redux) action which updates the current known status of the
* Follow Me feature.

View File

@@ -43,7 +43,6 @@ import {
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SEND_TONES,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_PASSWORD,
SET_PASSWORD_FAILED,
@@ -573,22 +572,6 @@ export function sendTones(tones: string, duration: number, pause: number) {
};
}
/**
* Sets the flag for indicating if desktop sharing is enabled.
*
* @param {boolean} desktopSharingEnabled - True if desktop sharing is enabled.
* @returns {{
* type: SET_DESKTOP_SHARING_ENABLED,
* desktopSharingEnabled: boolean
* }}
*/
export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
return {
type: SET_DESKTOP_SHARING_ENABLED,
desktopSharingEnabled
};
}
/**
* Enables or disables the Follow Me feature.
*

View File

@@ -128,9 +128,6 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
titleKey: 'dialog.sessTerminated'
}));
if (typeof APP !== 'undefined') {
APP.UI.hideStats();
}
break;
}
case JitsiConferenceErrors.CONNECTION_ERROR: {

View File

@@ -0,0 +1,74 @@
// @flow
import { setPictureInPictureDisabled } from '../../mobile/picture-in-picture/functions';
import { setAudioOnly } from '../audio-only';
import JitsiMeetJS from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes';
import { destroyLocalDesktopTrackIfExists, replaceLocalTrack } from '../tracks/actions';
import { getLocalVideoTrack, isLocalVideoTrackDesktop } from '../tracks/functions';
import './middleware.any';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case TOGGLE_SCREENSHARING: {
_toggleScreenSharing(store);
break;
}
}
return next(action);
});
/**
* Toggles screen sharing.
*
* @private
* @param {Store} store - The redux.
* @returns {void}
*/
function _toggleScreenSharing(store) {
const { dispatch, getState } = store;
const state = getState();
const isSharing = isLocalVideoTrackDesktop(state);
if (isSharing) {
dispatch(destroyLocalDesktopTrackIfExists());
} else {
_startScreenSharing(dispatch, state);
}
}
/**
* Creates desktop track and replaces the local one.
*
* @private
* @param {Dispatch} dispatch - The redux {@code dispatch} function.
* @param {Object} state - The redux state.
* @returns {void}
*/
function _startScreenSharing(dispatch, state) {
setPictureInPictureDisabled(true);
JitsiMeetJS.createLocalTracks({ devices: [ 'desktop' ] })
.then(tracks => {
const track = tracks[0];
const currentLocalTrack = getLocalVideoTrack(state['features/base/tracks']);
const currentJitsiTrack = currentLocalTrack && currentLocalTrack.jitsiTrack;
dispatch(replaceLocalTrack(currentJitsiTrack, track));
const { enabled: audioOnly } = state['features/base/audio-only'];
if (audioOnly) {
dispatch(setAudioOnly(false));
}
})
.catch(error => {
console.log('ERROR creating ScreeSharing stream ', error);
setPictureInPictureDisabled(false);
});
}

View File

@@ -0,0 +1,23 @@
// @flow
import UIEvents from '../../../../service/UI/UIEvents';
import { MiddlewareRegistry } from '../redux';
import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes';
import './middleware.any';
declare var APP: Object;
MiddlewareRegistry.register((/* store */) => next => action => {
switch (action.type) {
case TOGGLE_SCREENSHARING: {
if (typeof APP === 'object') {
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
}
break;
}
}
return next(action);
});

View File

@@ -16,7 +16,6 @@ import {
CONFERENCE_WILL_LEAVE,
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_PASSWORD,
SET_PENDING_SUBJECT_CHANGE,
@@ -76,9 +75,6 @@ ReducerRegistry.register(
case P2P_STATUS_CHANGED:
return _p2pStatusChanged(state, action);
case SET_DESKTOP_SHARING_ENABLED:
return _setDesktopSharingEnabled(state, action);
case SET_FOLLOW_ME:
return set(state, 'followMeEnabled', action.enabled);
@@ -343,21 +339,6 @@ function _p2pStatusChanged(state, action) {
return set(state, 'p2p', action.p2p);
}
/**
* Reduces a specific Redux action SET_DESKTOP_SHARING_ENABLED of the feature
* base/conference.
*
* @param {Object} state - The Redux state of the feature base/conference.
* @param {Action} action - The Redux action SET_DESKTOP_SHARING_ENABLED to
* reduce.
* @private
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _setDesktopSharingEnabled(state, action) {
return set(state, 'desktopSharingEnabled', action.desktopSharingEnabled);
}
/**
* Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
*

View File

@@ -91,7 +91,6 @@ export default [
'disableSimulcast',
'disableSuspendVideo',
'disableThirdPartyRequests',
'displayJids',
'doNotStoreRoom',
'e2eping',
'enableDisplayNameInStats',

View File

@@ -59,7 +59,7 @@ export function getInviteURL(stateOrGetState: Function | Object): string {
if (inviteDomain) {
const meetingId
= state['features/base/config'].brandingRoomAlias || urlWithoutParams.pathname;
= state['features/base/config'].brandingRoomAlias || urlWithoutParams.pathname.replace('/', '');
return `${inviteDomain}/${meetingId}`;
}

View File

@@ -16,6 +16,7 @@ import {
getParticipantCount
} from '../participants/functions';
import { MiddlewareRegistry } from '../redux';
import { isLocalVideoTrackDesktop } from '../tracks/functions';
import { limitLastN } from './functions';
import logger from './logger';
@@ -78,7 +79,7 @@ function _updateLastN({ getState }) {
}
if (typeof appState !== 'undefined' && appState !== 'active') {
lastN = 0;
lastN = isLocalVideoTrackDesktop(state) ? 1 : 0;
} else if (audioOnly) {
const { screenShares, tileViewEnabled } = state['features/video-layout'];
const largeVideoParticipantId = state['features/large-video'].participantId;

View File

@@ -99,6 +99,13 @@ class Video extends Component<Props> {
}
this._attachTrack(this.props.videoTrack);
if (this._videoElement && this.props.autoPlay) {
// 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.
this._videoElement.play();
}
}
/**
@@ -142,6 +149,8 @@ class Video extends Component<Props> {
* @returns {ReactElement}
*/
render() {
// NOTE: Maybe we should render null if we don't have video track or if the video track has ended.
return (
<video
autoPlay = { this.props.autoPlay }

View File

@@ -13,7 +13,7 @@ import { isRoomValid, SET_ROOM } from '../conference';
import JitsiMeetJS from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { getPropertyValue } from '../settings';
import { setTrackMuted, TRACK_ADDED } from '../tracks';
import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks';
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
import {
@@ -73,13 +73,15 @@ MiddlewareRegistry.register(store => next => action => {
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _appStateChanged({ dispatch }, next, action) {
const { appState } = action;
const mute = appState !== 'active'; // Note that 'background' and 'inactive' are treated equal.
function _appStateChanged({ dispatch, getState }, next, action) {
if (navigator.product === 'ReactNative') {
const { appState } = action;
const mute = appState !== 'active' && !isLocalVideoTrackDesktop(getState());
sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
}
return next(action);
}

View File

@@ -9,7 +9,8 @@ import {
MEDIA_TYPE,
setAudioMuted,
setVideoMuted,
VIDEO_MUTISM_AUTHORITY
VIDEO_MUTISM_AUTHORITY,
VIDEO_TYPE
} from '../media';
import { getLocalParticipant } from '../participants';
@@ -24,7 +25,13 @@ import {
TRACK_UPDATED,
TRACK_WILL_CREATE
} from './actionTypes';
import { createLocalTracksF, getLocalTrack, getLocalTracks, getTrackByJitsiTrack } from './functions';
import {
createLocalTracksF,
getLocalTrack,
getLocalTracks,
getLocalVideoTrack,
getTrackByJitsiTrack
} from './functions';
import logger from './logger';
/**
@@ -40,6 +47,8 @@ export function createDesiredLocalTracks(...desiredTypes) {
return (dispatch, getState) => {
const state = getState();
dispatch(destroyLocalDesktopTrackIfExists());
if (desiredTypes.length === 0) {
const { audio, video } = state['features/base/media'];
@@ -663,6 +672,22 @@ function _trackCreateCanceled(mediaType) {
};
}
/**
* If thee local track if of type Desktop, it calls _disposeAndRemoveTracks) on it.
*
* @returns {Function}
*/
export function destroyLocalDesktopTrackIfExists() {
return (dispatch, getState) => {
const videoTrack = getLocalVideoTrack(getState()['features/base/tracks']);
const isDesktopTrack = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
if (isDesktopTrack) {
dispatch(_disposeAndRemoveTracks([ videoTrack.jitsiTrack ]));
}
};
}
/**
* Sets UID of the displayed no data from source notification. Used to track
* if the notification was previously displayed in this context.

View File

@@ -1,7 +1,7 @@
/* global APP */
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { MEDIA_TYPE, setAudioMuted } from '../media';
import { MEDIA_TYPE, VIDEO_TYPE, setAudioMuted } from '../media';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId
@@ -383,6 +383,19 @@ export function isLocalTrackMuted(tracks, mediaType) {
return !track || track.muted;
}
/**
* Checks if the local video track is of type DESKtOP.
*
* @param {Object} state - The redux state.
* @returns {boolean}
*/
export function isLocalVideoTrackDesktop(state) {
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
return videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
}
/**
* Returns true if the remote track of the given media type and the given
* participant is muted, false otherwise.

View File

@@ -0,0 +1,567 @@
// @flow
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React, { Component } from 'react';
import { AudioLevelIndicator } from '../../../audio-level-indicator';
import { Avatar } from '../../../base/avatar';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
import {
getLocalParticipant,
getParticipantById,
getParticipantCount
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { getLocalAudioTrack, getLocalVideoTrack, getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import { ConnectionIndicator } from '../../../connection-indicator';
import { DisplayName } from '../../../display-name';
import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
import { PresenceLabel } from '../../../presence-status';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
const JitsiTrackEvents = JitsiMeetJS.events.track;
declare var interfaceConfig: Object;
type State = {
audioLevel: number
};
/**
* The type of the React {@code Component} props of {@link Thumbnail}.
*/
type Props = {
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
_height: number,
_heightToWidthPercent: number,
/**
* The video track that will be displayed in the thumbnail.
*/
_videoTrack: ?Object,
/**
* The audio track related to the participant.
*/
_audioTrack: ?Object,
_width: number,
/**
* The ID of the participant related to the thumbnaul.
*/
_participant: Object,
_defaultLocalDisplayName: string,
_isGuest: boolean,
_participantCount: number,
_isFilmstripOnly: boolean,
_connectionIndicatorDisabled: boolean,
_connectionIndicatorAutoHideEnabled: boolean,
_isDominantSpeakerDisabled: boolean,
participantID: ?string,
isHovered: ?boolean,
dispatch: Function
};
/**
* TODO.
*
* @returns {number}
*/
function _getIndicatorsIconSize() {
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
return NORMAL;
}
/**
* Implements a thumbnail.
*
* @extends Component
*/
class Thumbnail extends Component<Props, State> {
/**
* Initializes a new Thumbnail instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
audioLevel: 0
};
this._updateAudioLevel = this._updateAudioLevel.bind(this);
}
/**
* Starts listening for audio level updates after the initial render.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._listenForAudioUpdates();
}
/**
* Stops listening for audio level updates on the old track and starts
* listening instead on the new track.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(prevProps: Props) {
if (prevProps._audioTrack !== this.props._audioTrack) {
this._stopListeningForAudioUpdates(prevProps._audioTrack);
this._listenForAudioUpdates();
this._updateAudioLevel(0);
}
}
/**
* Unsubscribe from audio level updates.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._stopListeningForAudioUpdates(this.props._audioTrack);
}
/**
* Starts listening for audio level updates from the library.
*
* @private
* @returns {void}
*/
_listenForAudioUpdates() {
const { _audioTrack } = this.props;
if (_audioTrack) {
const { jitsiTrack } = _audioTrack;
jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
}
}
/**
* Stops listening to further updates from the passed track.
*
* @param {Object} audioTrack - The track.
* @private
* @returns {void}
*/
_stopListeningForAudioUpdates(audioTrack) {
if (audioTrack) {
const { jitsiTrack } = audioTrack;
jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
}
}
_updateAudioLevel: (number) => void;
/**
* Updates the internal state of the last know audio level. The level should
* be between 0 and 1, as the level will be used as a percentage out of 1.
*
* @param {number} audioLevel - The new audio level for the track.
* @private
* @returns {void}
*/
_updateAudioLevel(audioLevel) {
this.setState({
audioLevel
});
}
/**
* Returns an object with the styles for the video container and the avatar container.
*
* @returns {Object} - The styles for the video container and the avatar container.
*/
_getStyles(): Object {
const { _height, _heightToWidthPercent, _currentLayout } = this.props;
let styles;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const avatarSize = _height / 2;
styles = {
avatarContainer: {
height: `${avatarSize}px`,
width: `${avatarSize}px`
}
};
break;
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
styles = {
avatarContainer: {
height: '50%',
width: `${_heightToWidthPercent / 2}%`
}
};
break;
}
}
return styles;
}
/**
* Renders a fake participant (youtube video) thumbnail.
*
* @param {string} id - The id of the participant.
* @returns {ReactElement}
*/
_renderFakeParticipant(id) {
return (
<>
<img
className = 'sharedVideoAvatar'
src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
<div className = 'displayNameContainer'>
<DisplayName
elementID = 'sharedVideoContainer_name'
participantID = { id } />
</div>
</>
);
}
/**
* Renders the local participant's thumbnail.
*
* @param {string} id - The ID of the participant.
* @returns {ReactElement}
*/
_renderLocalParticipant(id) {
const styles = this._getStyles();
const {
_participant,
_participantCount,
_videoTrack,
_defaultLocalDisplayName,
_isGuest,
_isFilmstripOnly,
_isDominantSpeakerDisabled,
_connectionIndicatorDisabled,
_connectionIndicatorAutoHideEnabled,
_currentLayout
} = this.props;
const { audioLevel = 0 } = this.state;
const iconSize = _getIndicatorsIconSize();
const showConnectionIndicator = this.props.isHovered || !_connectionIndicatorAutoHideEnabled;
const { dominantSpeaker = false } = _participant;
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
let statsPopoverPosition, tooltipPosition;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
statsPopoverPosition = 'right top';
tooltipPosition = 'right';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
statsPopoverPosition = 'left top';
tooltipPosition = 'left';
break;
default:
statsPopoverPosition = 'top center';
tooltipPosition = 'top';
}
return (
<>
<div className = 'videocontainer__background' />
<span id = 'localVideoWrapper'>
<VideoTrack
id = 'localVideo_container'
videoTrack = { _videoTrack } />
</span>
<div className = 'videocontainer__toolbar'>
<StatusIndicators participantID = { id } />
</div>
<div className = 'videocontainer__toptoolbar'>
<div>
<AtlasKitThemeProvider mode = 'dark'>
{ _connectionIndicatorDisabled
? null
: <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { !_isFilmstripOnly }
iconSize = { iconSize }
isLocalVideo = { true }
participantId = { id }
statsPopoverPosition = { statsPopoverPosition } />
}
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { id }
tooltipPosition = { tooltipPosition } />
{ showDominantSpeaker && _participantCount > 2
? <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
: null }
</AtlasKitThemeProvider>
</div>
</div>
<div className = 'videocontainer__hoverOverlay' />
<div className = 'displayNameContainer'>
<DisplayName
allowEditing = { _isGuest }
displayNameSuffix = { _defaultLocalDisplayName }
elementID = 'localDisplayName'
participantID = { _participant?.id } />
</div>
<div
className = 'avatar-container'
style = { styles.avatarContainer }>
<Avatar
className = 'userAvatar'
participantId = { id } />
</div>
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
</>
);
}
/**
* Renders a remote participant's 'thumbnail.
*
* @param {string} id - The id of the participant.
* @returns {ReactElement}
*/
_renderRemoteParticipant(id) {
const styles = this._getStyles();
const {
_participant,
_participantCount,
_isFilmstripOnly,
_currentLayout,
_connectionIndicatorDisabled,
_connectionIndicatorAutoHideEnabled,
_isDominantSpeakerDisabled
} = this.props;
const { audioLevel = 0 } = this.state;
const showConnectionIndicator = this.props.isHovered || !_connectionIndicatorAutoHideEnabled;
const { dominantSpeaker = false } = _participant;
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
const iconSize = _getIndicatorsIconSize();
let remoteMenuPosition, statsPopoverPosition, tooltipPosition;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
statsPopoverPosition = 'right top';
tooltipPosition = 'right';
remoteMenuPosition = 'left top';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
statsPopoverPosition = 'left bottom';
tooltipPosition = 'left';
remoteMenuPosition = 'left bottom';
break;
default:
statsPopoverPosition = 'top center';
tooltipPosition = 'top';
remoteMenuPosition = 'top center';
}
return (
<>
<div className = 'videocontainer__background' />
<div className = 'videocontainer__toptoolbar'>
<div>
<AtlasKitThemeProvider mode = 'dark'>
{ _connectionIndicatorDisabled
? null
: <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { !_isFilmstripOnly }
iconSize = { iconSize }
isLocalVideo = { false }
participantId = { id }
statsPopoverPosition = { statsPopoverPosition } />
}
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { id }
tooltipPosition = { tooltipPosition } />
{ showDominantSpeaker && _participantCount > 2
? <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
: null }
</AtlasKitThemeProvider>
</div>
</div>
<div className = 'videocontainer__toolbar'>
<StatusIndicators participantID = { id } />
</div>
<div className = 'videocontainer__hoverOverlay' />
<div className = 'displayNameContainer'>
<DisplayName
elementID = { `participant_${id}_name` }
participantID = { id } />
</div>
<div
className = 'avatar-container'
style = { styles.avatarContainer }>
<Avatar
className = 'userAvatar'
participantId = { id } />
</div>
<div className = 'presence-label-container'>
<PresenceLabel
className = 'presence-label'
participantID = { id } />
</div>
<span className = 'remotevideomenu'>
{/* <AtlasKitThemeProvider mode = 'dark'>
<RemoteVideoMenuTriggerButton
initialVolumeValue = { initialVolumeValue }
menuPosition = { remoteMenuPosition }
onMenuDisplay = {this._onRemoteVideoMenuDisplay.bind(this)}
onRemoteControlToggle = { onRemoteControlToggle }
onVolumeChange = { onVolumeChange }
participantID = { participantID }
remoteControlState = { remoteControlState } />
</AtlasKitThemeProvider> */}
</span>
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
</>
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _participant } = this.props;
if (!_participant) {
return null;
}
const { id, isFakeParticipant, local = false } = _participant;
if (local) {
return this._renderLocalParticipant(id);
}
if (isFakeParticipant) {
return this._renderFakeParticipant(id);
}
return this._renderRemoteParticipant(id);
}
}
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {{
* _videoTrack: Object
* }}
*/
function _mapStateToProps(state, ownProps): Object {
const { participantID } = ownProps;
// Only the local participant won't have id for the time when the conference is not yet joined.
const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
const isLocal = participant?.local ?? true;
const _videoTrack = isLocal
? getLocalVideoTrack(state['features/base/tracks'])
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
const _audioTrack = isLocal
? getLocalAudioTrack(state['features/base/tracks'])
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
const _currentLayout = getCurrentLayout(state);
let size = {};
const _isGuest = state['features/base/jwt'].isGuest;
switch (_currentLayout) {
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const {
horizontalViewDimensions = {
local: {},
remote: {}
}
} = state['features/filmstrip'];
const { local, remote } = horizontalViewDimensions;
const { width, height } = isLocal ? local : remote;
size = {
_width: width,
_height: height
};
break;
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
size = {
_heightToWidthPercent: isLocal
? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
: 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
};
break;
case LAYOUTS.TILE_VIEW: {
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
size = {
_width: width,
_height: height
};
break;
}
}
return {
_isGuest,
_videoTrack,
_audioTrack,
_currentLayout,
_participant: participant,
_participantCount: getParticipantCount(state),
_isFilmstripOnly: interfaceConfig.filmStripOnly,
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
_connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
...size
};
}
export default connect(_mapStateToProps)(Thumbnail);

View File

@@ -3,9 +3,10 @@
import React, { PureComponent } from 'react';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { ParticipantView } from '../../base/participants';
import { ParticipantView, getParticipantById } from '../../base/participants';
import { connect } from '../../base/redux';
import { StyleType } from '../../base/styles';
import { isLocalVideoTrackDesktop } from '../../base/tracks/functions';
import { AVATAR_SIZE } from './styles';
@@ -14,6 +15,11 @@ import { AVATAR_SIZE } from './styles';
*/
type Props = {
/**
* Whether video should be disabled.
*/
_disableVideo: boolean,
/**
* Application's viewport height.
*/
@@ -112,6 +118,7 @@ class LargeVideo extends PureComponent<Props, State> {
useConnectivityInfoLabel
} = this.state;
const {
_disableVideo,
_participantId,
_styles,
onClick
@@ -120,6 +127,7 @@ class LargeVideo extends PureComponent<Props, State> {
return (
<ParticipantView
avatarSize = { avatarSize }
disableVideo = { _disableVideo }
onPress = { onClick }
participantId = { _participantId }
style = { _styles.largeVideo }
@@ -139,11 +147,19 @@ class LargeVideo extends PureComponent<Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state) {
const { participantId } = state['features/large-video'];
const participant = getParticipantById(state, participantId);
const { clientHeight: height, clientWidth: width } = state['features/base/responsive-ui'];
let disableVideo = false;
if (participant?.local) {
disableVideo = isLocalVideoTrackDesktop(state);
}
return {
_disableVideo: disableVideo,
_height: height,
_participantId: state['features/large-video'].participantId,
_participantId: participantId,
_styles: ColorSchemeRegistry.get(state, 'LargeVideo'),
_width: width
};

View File

@@ -7,6 +7,7 @@ import { translate } from '../../../base/i18n';
import { IconMenuDown } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions';
import { enterPictureInPicture } from '../actions';
type Props = AbstractButtonProps & {
@@ -63,7 +64,7 @@ class PictureInPictureButton extends AbstractButton<Props, *> {
*/
function _mapStateToProps(state): Object {
const flag = Boolean(getFeatureFlag(state, PIP_ENABLED));
let enabled = flag;
let enabled = flag && !isLocalVideoTrackDesktop(state);
// Override flag for Android, since it might be unsupported.
if (Platform.OS === 'android' && !NativeModules.PictureInPicture.SUPPORTED) {

View File

@@ -0,0 +1,15 @@
// @flow
import { NativeModules } from 'react-native';
/**
* Enabled/Disables the PictureInPicture mode in PiP native module.
*
* @param {boolean} disabled - Whether the PiP mode should be disabled.
* @returns {void}
*/
export function setPictureInPictureDisabled(disabled: boolean) {
const { PictureInPicture } = NativeModules;
PictureInPicture.setPictureInPictureDisabled(disabled);
}

View File

@@ -1,3 +1,4 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './functions';

View File

@@ -9,7 +9,6 @@ import {
sendAnalytics
} from '../../analytics';
import { setAudioOnly } from '../../base/audio-only';
import { hasAvailableDevices } from '../../base/devices';
import { translate } from '../../base/i18n';
import {
VIDEO_MUTISM_AUTHORITY,
@@ -19,6 +18,7 @@ import { connect } from '../../base/redux';
import { AbstractVideoMuteButton } from '../../base/toolbox/components';
import type { AbstractButtonProps } from '../../base/toolbox/components';
import { getLocalVideoType, isLocalCameraTrackMuted } from '../../base/tracks';
import { isVideoMuteButtonDisabled } from '../functions';
declare var APP: Object;
@@ -190,7 +190,7 @@ function _mapStateToProps(state): Object {
return {
_audioOnly: Boolean(audioOnly),
_videoDisabled: !hasAvailableDevices(state, 'videoInput'),
_videoDisabled: isVideoMuteButtonDisabled(state),
_videoMediaType: getLocalVideoType(tracks),
_videoMuted: isLocalCameraTrackMuted(tracks)
};

View File

@@ -23,6 +23,7 @@ import HelpButton from '../HelpButton';
import AudioOnlyButton from './AudioOnlyButton';
import MoreOptionsButton from './MoreOptionsButton';
import RaiseHandButton from './RaiseHandButton';
import ScreenSharingButton from './ScreenSharingButton.js';
import ToggleCameraButton from './ToggleCameraButton';
import styles from './styles';
@@ -131,6 +132,7 @@ class OverflowMenu extends PureComponent<Props, State> {
<AudioOnlyButton { ...buttonProps } />
<RaiseHandButton { ...buttonProps } />
<LobbyModeButton { ...buttonProps } />
<ScreenSharingButton { ...buttonProps } />
<MoreOptionsButton { ...moreOptionsButtonProps } />
<Collapsible collapsed = { !showMore }>
<ToggleCameraButton { ...buttonProps } />

View File

@@ -0,0 +1,77 @@
// @flow
import { Platform } from 'react-native';
import { translate } from '../../../base/i18n';
import { IconShareDesktop } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { toggleScreensharing, isLocalVideoTrackDesktop } from '../../../base/tracks';
/**
* The type of the React {@code Component} props of {@link ScreenSharingButton}.
*/
type Props = AbstractButtonProps & {
/**
* Whether video is currently muted or not.
*/
_screensharing: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
/**
* An implementation of a button for toggling screen sharing.
*/
class ScreenSharingButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
icon = IconShareDesktop;
label = 'toolbar.startScreenSharing';
toggledLabel = 'toolbar.stopScreenSharing';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
_handleClick() {
this.props.dispatch(toggleScreensharing());
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props._screensharing;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _disabled: boolean,
* _screensharing: boolean
* }}
*/
function _mapStateToProps(state): Object {
return {
_screensharing: isLocalVideoTrackDesktop(state),
visible: Platform.OS === 'android'
};
}
export default translate(connect(_mapStateToProps)(ScreenSharingButton));

View File

@@ -25,6 +25,7 @@ import {
IconShareDesktop,
IconShareVideo
} from '../../../base/icons';
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
import {
getLocalParticipant,
getParticipants,
@@ -1403,7 +1404,7 @@ class Toolbox extends Component<Props, State> {
*/
function _mapStateToProps(state) {
const { conference, locked } = state['features/base/conference'];
let { desktopSharingEnabled } = state['features/base/conference'];
let desktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled();
const {
callStatsID,
enableFeaturesBasedOnToken

View File

@@ -1,7 +1,9 @@
// @flow
import { hasAvailableDevices } from '../base/devices';
import { TOOLBOX_ALWAYS_VISIBLE, getFeatureFlag } from '../base/flags';
import { toState } from '../base/redux';
import { isLocalVideoTrackDesktop } from '../base/tracks';
/**
* Returns true if the toolbox is visible.
@@ -18,3 +20,13 @@ export function isToolboxVisible(stateful: Object | Function) {
return enabled && (alwaysVisible || visible || participantCount === 1 || flag);
}
/**
* Indicates if the video mute button is disabled or not.
*
* @param {string} state - The state from the Redux store.
* @returns {boolean}
*/
export function isVideoMuteButtonDisabled(state: Object) {
return !hasAvailableDevices(state, 'videoInput') || isLocalVideoTrackDesktop(state);
}

View File

@@ -77,3 +77,13 @@ export function isAudioSettingsButtonDisabled(state: Object) {
export function isVideoSettingsButtonDisabled(state: Object) {
return !hasAvailableDevices(state, 'videoInput');
}
/**
* Indicates if the video mute button is disabled or not.
*
* @param {string} state - The state from the Redux store.
* @returns {boolean}
*/
export function isVideoMuteButtonDisabled(state: Object) {
return !hasAvailableDevices(state, 'videoInput');
}

View File

@@ -1,7 +1,7 @@
// @flow
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout.js';
import { CONFERENCE_JOINED, CONFERENCE_WILL_LEAVE } from '../base/conference';
import { CONFERENCE_WILL_LEAVE } from '../base/conference';
import {
DOMINANT_SPEAKER_CHANGED,
PARTICIPANT_JOINED,
@@ -33,10 +33,6 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case CONFERENCE_JOINED:
VideoLayout.mucJoined();
break;
case CONFERENCE_WILL_LEAVE:
VideoLayout.reset();
break;

View File

@@ -19,6 +19,7 @@ import { connect } from '../../base/redux';
import { ColorPalette } from '../../base/styles';
import {
createDesiredLocalTracks,
destroyLocalDesktopTrackIfExists,
destroyLocalTracks
} from '../../base/tracks';
import { HelpView } from '../../help';
@@ -81,6 +82,8 @@ class WelcomePage extends AbstractWelcomePage {
if (this.props._settings.startAudioOnly) {
dispatch(destroyLocalTracks());
} else {
dispatch(destroyLocalDesktopTrackIfExists());
// Make sure we don't request the permission for the camera from
// the start. We will, however, create a video track iff the user
// already granted the permission.

View File

@@ -1,5 +0,0 @@
local jibri_queue_component
= module:get_option_string(
"jibri_queue_component", "jibriqueue"..module.host);
module:add_identity("component", "jibri-queue", jibri_queue_component);

View File

@@ -1,559 +0,0 @@
local st = require "util.stanza";
local jid = require "util.jid";
local http = require "net.http";
local json = require "cjson";
local inspect = require('inspect');
local socket = require "socket";
local uuid_gen = require "util.uuid".generate;
local jwt = require "luajwtjitsi";
local it = require "util.iterators";
local neturl = require "net.url";
local parse = neturl.parseQuery;
local get_room_from_jid = module:require "util".get_room_from_jid;
local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
local is_healthcheck_room = module:require "util".is_healthcheck_room;
local room_jid_split_subdomain = module:require "util".room_jid_split_subdomain;
local internal_room_jid_match_rewrite = module:require "util".internal_room_jid_match_rewrite;
local async_handler_wrapper = module:require "util".async_handler_wrapper;
-- this basically strips the domain from the conference.domain address
local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
if parentHostName == nil then
log("error", "Failed to start - unable to get parent hostname");
return;
end
local parentCtx = module:context(parentHostName);
if parentCtx == nil then
log("error",
"Failed to start - unable to get parent context for host: %s",
tostring(parentHostName));
return;
end
local token_util = module:require "token/util".new(parentCtx);
local ASAPKeyServer;
local ASAPKeyPath;
local ASAPKeyId;
local ASAPIssuer;
local ASAPAudience;
local ASAPAcceptedIssuers;
local ASAPAcceptedAudiences;
local ASAPTTL;
local ASAPTTL_THRESHOLD;
local ASAPKey;
local JibriRegion;
local disableTokenVerification;
local muc_component_host;
local external_api_url;
local jwtKeyCacheSize;
local jwtKeyCache;
local function load_config()
ASAPKeyServer = module:get_option_string("asap_key_server");
if ASAPKeyServer then
module:log("debug", "ASAP Public Key URL %s", ASAPKeyServer);
token_util:set_asap_key_server(ASAPKeyServer);
end
ASAPKeyPath
= module:get_option_string("asap_key_path", '/etc/prosody/certs/asap.key');
ASAPKeyId
= module:get_option_string("asap_key_id", 'jitsi');
ASAPIssuer
= module:get_option_string("asap_issuer", 'jitsi');
ASAPAudience
= module:get_option_string("asap_audience", 'jibri-queue');
ASAPAcceptedIssuers
= module:get_option_array('asap_accepted_issuers',{'jibri-queue'});
module:log("debug", "ASAP Accepted Issuers %s", ASAPAcceptedIssuers);
token_util:set_asap_accepted_issuers(ASAPAcceptedIssuers);
ASAPAcceptedAudiences
= module:get_option_array('asap_accepted_audiences',{'*'});
module:log("debug", "ASAP Accepted Audiences %s", ASAPAcceptedAudiences);
token_util:set_asap_accepted_audiences(ASAPAcceptedAudiences);
-- do not require room to be set on tokens for jibri queue
token_util:set_asap_require_room_claim(false);
ASAPTTL
= module:get_option_number("asap_ttl", 3600);
ASAPTTL_THRESHOLD
= module:get_option_number("asap_ttl_threshold", 600);
queueServiceURL
= module:get_option_string("jibri_queue_url");
JibriRegion
= module:get_option_string("jibri_region", 'default');
-- option to enable/disable token verifications
disableTokenVerification
= module:get_option_boolean("disable_jibri_queue_token_verification", false);
muc_component_host
= module:get_option_string("muc_component");
external_api_url = module:get_option_string("external_api_url",tostring(parentHostName));
module:log("debug", "External advertised API URL", external_api_url);
-- TODO: Figure out a less arbitrary default cache size.
jwtKeyCacheSize
= module:get_option_number("jwt_pubkey_cache_size", 128);
jwtKeyCache = require"util.cache".new(jwtKeyCacheSize);
if queueServiceURL == nil then
log("error", "No jibri_queue_url specified. No service to contact!");
return;
end
if muc_component_host == nil then
log("error", "No muc_component specified. No muc to operate on for jibri queue!");
return;
end
-- Read ASAP key once on module startup
local f = io.open(ASAPKeyPath, "r");
if f then
ASAPKey = f:read("*all");
f:close();
if not ASAPKey then
module:log("warn", "No ASAP Key read from %s, disabling jibri queue component plugin", ASAPKeyPath);
return
end
else
module:log("warn", "Error reading ASAP Key %s, disabling jibri queue component plugin", ASAPKeyPath);
return
end
return true;
end
local function reload_config()
module:log("info", "Reloading configuration for jibri queue component");
local config_success = load_config();
-- clear ASAP public key cache on config reload
token_util:clear_asap_cache();
if not config_success then
log("error", "Unsuccessful reconfiguration, jibri queue component may misbehave");
end
end
local config_success = load_config();
if not config_success then
log("error", "Unsuccessful configuration step, jibri queue component disabled")
return;
end
local http_headers = {
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")",
["Content-Type"] = "application/json"
};
-- we use async to detect Prosody 0.10 and earlier
local have_async = pcall(require, "util.async");
if not have_async then
module:log("warn", "conference duration will not work with Prosody version 0.10 or less.");
return;
end
log("info", "Starting jibri queue handling for %s", muc_component_host);
local function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
local function generateToken(audience)
audience = audience or ASAPAudience
local t = os.time()
local err
local exp_key = 'asap_exp.'..audience
local token_key = 'asap_token.'..audience
local exp = jwtKeyCache:get(exp_key)
local token = jwtKeyCache:get(token_key)
--if we find a token and it isn't too far from expiry, then use it
if token ~= nil and exp ~= nil then
exp = tonumber(exp)
if (exp - t) > ASAPTTL_THRESHOLD then
return token
end
end
--expiry is the current time plus TTL
exp = t + ASAPTTL
local payload = {
iss = ASAPIssuer,
aud = audience,
nbf = t,
exp = exp,
}
-- encode
local alg = "RS256"
token, err = jwt.encode(payload, ASAPKey, alg, {kid = ASAPKeyId})
if not err then
token = 'Bearer '..token
jwtKeyCache:set(exp_key,exp)
jwtKeyCache:set(token_key,token)
return token
else
return ''
end
end
local function sendIq(participant,action,requestId,time,position,token)
local iqId = uuid_gen();
local from = module:get_host();
local outStanza = st.iq({type = 'set', from = from, to = participant, id = iqId}):tag("jibri-queue",
{ xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId, action = action });
if token then
outStanza:tag("token"):text(token):up()
end
if time then
outStanza:tag("time"):text(tostring(time)):up()
end
if position then
outStanza:tag("position"):text(tostring(position)):up()
end
module:send(outStanza);
end
local function cb(content_, code_, response_, request_)
if code_ == 200 or code_ == 204 then
module:log("debug", "URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s",
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
else
module:log("warn", "URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s",
code_, content_, inspect(request_), inspect(response_));
end
end
local function sendEvent(type,room_address,participant,requestId,replyIq,replyError)
local event_ts = round(socket.gettime()*1000);
local node, host, resource, target_subdomain = room_jid_split_subdomain(room_address);
local room_param = '';
if target_subdomain then
room_param = target_subdomain..'/'..node;
else
room_param = node;
end
local out_event = {
["conference"] = room_address,
["roomParam"] = room_param,
["eventType"] = type,
["participant"] = participant,
["externalApiUrl"] = external_api_url.."/jibriqueue/update",
["requestId"] = requestId,
["region"] = JibriRegion,
}
module:log("debug","Sending event %s",inspect(out_event));
local headers = http_headers or {}
headers['Authorization'] = generateToken()
module:log("debug","Sending headers %s",inspect(headers));
local requestURL = queueServiceURL.."/job/recording"
if type=="LeaveQueue" then
requestURL = requestURL .."/cancel"
end
local request = http.request(requestURL, {
headers = headers,
method = "POST",
body = json.encode(out_event)
}, function (content_, code_, response_, request_)
if code_ == 200 or code_ == 204 then
module:log("debug", "URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s",
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
if (replyIq) then
module:log("debug", "sending reply IQ %s",inspect(replyIq));
module:send(replyIq);
end
else
module:log("warn", "URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s",
code_, content_, inspect(request_), inspect(response_));
if (replyError) then
module:log("warn", "sending reply error IQ %s",inspect(replyError));
module:send(replyError);
end
end
end);
end
function clearRoomQueueByOccupant(room, occupant)
room.jibriQueue[occupant.jid] = nil;
end
function addRoomQueueByOccupant(room, occupant, requestId)
room.jibriQueue[occupant.jid] = requestId;
end
-- receives iq from client currently connected to the room
function on_iq(event)
local requestId;
-- Check the type of the incoming stanza to avoid loops:
if event.stanza.attr.type == "error" then
return; -- We do not want to reply to these, so leave.
end
if event.stanza.attr.to == module:get_host() then
if event.stanza.attr.type == "set" then
local reply = st.reply(event.stanza);
local replyError = st.error_reply(event.stanza,'cancel','internal-server-error',"Queue Server Error");
local jibriQueue
= event.stanza:get_child('jibri-queue', 'http://jitsi.org/protocol/jibri-queue');
if jibriQueue then
module:log("debug", "Received Jibri Queue Request: %s ",inspect(jibriQueue));
local roomAddress = jibriQueue.attr.room;
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
if not room then
module:log("warn", "No room found %s", roomAddress);
return false;
end
local from = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(from);
if not occupant then
module:log("warn", "No occupant %s found for %s", from, roomAddress);
return false;
end
local action = jibriQueue.attr.action;
if action == 'join' then
-- join action, so send event out
requestId = uuid_gen();
module:log("debug","Received join queue request for jid %s occupant %s requestId %s",roomAddress,occupant.jid,requestId);
-- now handle new jibri queue message
addRoomQueueByOccupant(room, occupant, requestId);
reply:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
replyError:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
module:log("debug","Sending JoinQueue event for jid %s occupant %s reply %s",roomAddress,occupant.jid,inspect(reply));
sendEvent('JoinQueue',roomAddress,occupant.jid,requestId,reply,replyError);
end
if action == 'leave' then
requestId = jibriQueue.attr.requestId;
module:log("debug","Received leave queue request for jid %s occupant %s requestId %s",roomAddress,occupant.jid,requestId);
-- TODO: check that requestId is the same as cached value
clearRoomQueueByOccupant(room, occupant);
reply:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
replyError:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
module:log("debug","Sending LeaveQueue event for jid %s occupant %s reply %s",roomAddress,occupant.jid,inspect(reply));
sendEvent('LeaveQueue',roomAddress,occupant.jid,requestId,reply,replyError);
end
else
module:log("warn","Jibri Queue Stanza missing child %s",inspect(event.stanza))
end
end
end
return true
end
-- create recorder queue cache for the room
function room_created(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
room.jibriQueue = {};
end
-- Conference ended, clear all queue cache jids
function room_destroyed(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
for jid, x in pairs(room.jibriQueue) do
if x then
sendEvent('LeaveQueue',internal_room_jid_match_rewrite(room.jid),jid,x);
end
end
end
-- Occupant left remove it from the queue if it joined the queue
function occupant_leaving(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
local occupant = event.occupant;
local requestId = room.jibriQueue[occupant.jid];
-- check if user has cached queue request
if requestId then
-- remove occupant from queue cache, signal backend
room.jibriQueue[occupant.jid] = nil;
sendEvent('LeaveQueue',internal_room_jid_match_rewrite(room.jid),occupant.jid,requestId);
end
end
module:hook("iq/host", on_iq);
-- executed on every host added internally in prosody, including components
function process_host(host)
if host == muc_component_host then -- the conference muc component
module:log("debug","Hook to muc events on %s", host);
local muc_module = module:context(host);
muc_module:hook("muc-room-created", room_created, -1);
-- muc_module:hook("muc-occupant-joined", occupant_joined, -1);
muc_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
muc_module:hook("muc-room-destroyed", room_destroyed, -1);
end
end
if prosody.hosts[muc_component_host] == nil then
module:log("debug","No muc component found, will listen for it: %s", muc_component_host)
-- when a host or component is added
prosody.events.add_handler("host-activated", process_host);
else
process_host(muc_component_host);
end
module:log("info", "Loading jibri_queue_component");
--- Verifies room name, domain name with the values in the token
-- @param token the token we received
-- @param room_name the room name
-- @param group name of the group (optional)
-- @param session the session to use for storing token specific fields
-- @return true if values are ok or false otherwise
function verify_token(token, room_jid, session)
if disableTokenVerification then
return true;
end
-- if not disableTokenVerification and we do not have token
-- stop here, cause the main virtual host can have guest access enabled
-- (allowEmptyToken = true) and we will allow access to rooms info without
-- a token
if token == nil then
log("warn", "no token provided");
return false;
end
session.auth_token = token;
local verified, reason, message = token_util:process_and_verify_token(session);
if not verified then
log("warn", "not a valid token %s: %s", tostring(reason), tostring(message));
log("debug", "invalid token %s", token);
return false;
end
return true;
end
--- Handles request for updating jibri queue status
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
function handle_update_jibri_queue(event)
local body = json.decode(event.request.body);
module:log("debug","Update Jibri Queue Event Received: %s",inspect(body));
local token = event.request.headers["authorization"];
if not token then
token = ''
else
local prefixStart, prefixEnd = token:find("Bearer ");
if prefixStart ~= 1 then
module:log("error", "REST event: Invalid authorization header format. The header must start with the string 'Bearer '");
return { status_code = 403; };
end
token = token:sub(prefixEnd + 1);
end
local user_jid = body["participant"];
local roomAddress = body["conference"];
local userJWT = body["token"];
local action = body["action"];
local time = body["time"];
local position = body["position"];
local requestId = body["requestId"];
if not action then
if userJWT then
action = 'token';
else
action = 'info';
end
end
local room_jid = room_jid_match_rewrite(roomAddress);
if not verify_token(token, room_jid, {}) then
log("error", "REST event: Invalid token for room %s to route action %s for requestId %s", roomAddress, action, requestId);
return { status_code = 403; };
end
local room = get_room_from_jid(room_jid);
if (not room) then
log("error", "REST event: no room found %s to route action %s for requestId %s", roomAddress, action, requestId);
return { status_code = 404; };
end
local occupant = room:get_occupant_by_real_jid(user_jid);
if not occupant then
log("warn", "REST event: No occupant %s found for %s to route action %s for requestId %s", user_jid, roomAddress, action, requestId);
return { status_code = 404; };
end
if not room.jibriQueue[occupant.jid] then
log("warn", "REST event: No queue request found for occupant %s in conference %s to route action %s for requestId %s",occupant.jid,room.jid, action, requestId)
return { status_code = 404; };
end
if not requestId then
requestId = room.jibriQueue[occupant.jid];
end
if action == 'token' and userJWT then
log("debug", "REST event: Token received for occupant %s in conference %s requestId %s, clearing room queue");
clearRoomQueueByOccupant(room, occupant);
end
log("debug", "REST event: Sending update for occupant %s in conference %s to route action %s for requestId %s",occupant.jid,room.jid, action, requestId);
sendIq(occupant.jid,action,requestId,time,position,userJWT);
return { status_code = 200; };
end
module:depends("http");
module:provides("http", {
default_path = "/";
name = "jibriqueue";
route = {
["POST /jibriqueue/update"] = function (event) return async_handler_wrapper(event,handle_update_jibri_queue) end;
};
});
module:hook_global('config-reloaded', reload_config);

View File

@@ -7,99 +7,32 @@ local jid = require "util.jid";
local filters = require "util.filters";
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
if not muc_domain_base then
module:log("warn", "No 'muc_mapper_domain_base' option set, disabling muc_mapper plugin inactive");
return
end
-- The "real" MUC domain that we are proxying to
local muc_domain = module:get_option_string("muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
local escaped_muc_domain_base = muc_domain_base:gsub("%p", "%%%1");
local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
-- The pattern used to extract the target subdomain (e.g. extract 'foo' from 'foo.muc.example.com')
local target_subdomain_pattern = "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
-- table to store all incoming iqs without roomname in it, like discoinfo to the muc compoent
local roomless_iqs = {};
if not muc_domain then
module:log("warn", "No 'muc_mapper_domain' option set, disabling muc_mapper plugin inactive");
return
end
-- Utility function to check and convert a room JID from virtual room1@muc.foo.example.com to real [foo]room1@muc.example.com
local function match_rewrite_to_jid(room_jid, stanza)
local node, host, resource = jid.split(room_jid);
local target_subdomain = host and host:match(target_subdomain_pattern);
if not target_subdomain then
module:log("debug", "No need to rewrite out 'to' %s", room_jid);
return room_jid;
end
-- Ok, rewrite room_jid address to new format
local new_node, new_host, new_resource;
if node then
new_node, new_host, new_resource = "["..target_subdomain.."]"..node, muc_domain, resource;
else
module:log("debug", "No room name provided so rewriting only host 'to' %s", room_jid);
new_host, new_resource = muc_domain, resource;
if (stanza.attr and stanza.attr.id) then
roomless_iqs[stanza.attr.id] = stanza.attr.to;
end
end
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
-- Utility function to check and convert a room JID from real [foo]room1@muc.example.com to virtual room1@muc.foo.example.com
local function match_rewrite_from_jid(room_jid, stanza)
local node, host, resource = jid.split(room_jid);
if host ~= muc_domain or not node then
module:log("debug", "No need to rewrite %s (not from the MUC host) %s, %s", room_jid, stanza.attr.id, roomless_iqs[stanza.attr.id]);
if (stanza.attr and stanza.attr.id and roomless_iqs[stanza.attr.id]) then
local result = roomless_iqs[stanza.attr.id];
roomless_iqs[stanza.attr.id] = nil;
return result;
end
return room_jid;
end
local target_subdomain, target_node = node:match("^%[([^%]]+)%](.+)$");
if not (target_node and target_subdomain) then
module:log("debug", "Not rewriting... unexpected node format: %s", node);
return room_jid;
end
-- Ok, rewrite room_jid address to pretty format
local new_node, new_host, new_resource = target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource;
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
local util = module:require "util";
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
-- We must filter stanzas in order to hook in to all incoming and outgoing messaging which skips the stanza routers
function filter_stanza(stanza)
if stanza.name == "message" or stanza.name == "iq" or stanza.name == "presence" then
module:log("debug", "Filtering stanza type %s to %s from %s",stanza.name,stanza.attr.to,stanza.attr.from);
-- module:log("debug", "Filtering stanza type %s to %s from %s",stanza.name,stanza.attr.to,stanza.attr.from);
if stanza.name == "iq" then
local conf = stanza:get_child('conference')
if conf then
module:log("debug", "Filtering stanza conference %s to %s from %s",conf.attr.room,stanza.attr.to,stanza.attr.from);
conf.attr.room = match_rewrite_to_jid(conf.attr.room, stanza)
-- module:log("debug", "Filtering stanza conference %s to %s from %s",conf.attr.room,stanza.attr.to,stanza.attr.from);
conf.attr.room = room_jid_match_rewrite(conf.attr.room, stanza)
end
end
if stanza.attr.to then
stanza.attr.to = match_rewrite_to_jid(stanza.attr.to, stanza)
stanza.attr.to = room_jid_match_rewrite(stanza.attr.to, stanza)
end
if stanza.attr.from then
stanza.attr.from = match_rewrite_from_jid(stanza.attr.from, stanza)
stanza.attr.from = internal_room_jid_match_rewrite(stanza.attr.from, stanza)
end
end
return stanza;
@@ -107,7 +40,6 @@ end
function filter_session(session)
module:log("warn", "Session filters applied");
-- filters.add_filter(session, "stanzas/in", filter_stanza_in);
filters.add_filter(session, "stanzas/out", filter_stanza);
end
@@ -128,14 +60,14 @@ end
local function outgoing_stanza_rewriter(event)
local stanza = event.stanza;
if stanza.attr.to then
stanza.attr.to = match_rewrite_to_jid(stanza.attr.to, stanza)
stanza.attr.to = room_jid_match_rewrite(stanza.attr.to, stanza)
end
end
local function incoming_stanza_rewriter(event)
local stanza = event.stanza;
if stanza.attr.from then
stanza.attr.from = match_rewrite_from_jid(stanza.attr.from, stanza)
stanza.attr.from = internal_room_jid_match_rewrite(stanza.attr.from, stanza)
end
end

View File

@@ -17,7 +17,7 @@ local cache = dep.softreq("util.cache"); -- only available in prosody 0.10+
local uuid_generate = require "util.uuid".generate;
local jid = require "util.jid";
local t_insert, t_remove = table.insert, table.remove;
local t_remove = table.remove;
local math_min = math.min;
local math_max = math.max;
local os_time = os.time;
@@ -26,6 +26,7 @@ local add_filter = require "util.filters".add_filter;
local timer = require "util.timer";
local datetime = require "util.datetime";
local xmlns_mam2 = "urn:xmpp:mam:2";
local xmlns_sm2 = "urn:xmpp:sm:2";
local xmlns_sm3 = "urn:xmpp:sm:3";
local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas";
@@ -34,11 +35,11 @@ local xmlns_delay = "urn:xmpp:delay";
local sm2_attr = { xmlns = xmlns_sm2 };
local sm3_attr = { xmlns = xmlns_sm3 };
local resume_timeout = module:get_option_number("smacks_hibernation_time", 300);
local resume_timeout = module:get_option_number("smacks_hibernation_time", 600);
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", false);
local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false);
local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0);
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 60);
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 30);
local max_hibernated_sessions = module:get_option_number("smacks_max_hibernated_sessions", 10);
local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10);
local core_process_stanza = prosody.core_process_stanza;
@@ -200,8 +201,15 @@ local function request_ack_if_needed(session, force, reason)
end
local function outgoing_stanza_filter(stanza, session)
local is_stanza = stanza.attr and not stanza.attr.xmlns and not stanza.name:find":";
if is_stanza and not stanza._cached then -- Stanza in default stream namespace
-- XXX: Normally you wouldn't have to check the xmlns for a stanza as it's
-- supposed to be nil.
-- However, when using mod_smacks with mod_websocket, then mod_websocket's
-- stanzas/out filter can get called before this one and adds the xmlns.
local is_stanza = stanza.attr and
(not stanza.attr.xmlns or stanza.attr.xmlns == 'jabber:client')
and not stanza.name:find":";
if is_stanza and not stanza._cached then
local queue = session.outgoing_stanza_queue;
local cached_stanza = st.clone(stanza);
cached_stanza._cached = true;
@@ -400,12 +408,14 @@ local function handle_unacked_stanzas(session)
session.outgoing_stanza_queue = {};
for i=1,#queue do
if not module:fire_event("delivery/failure", { session = session, stanza = queue[i] }) then
local reply = st.reply(queue[i]);
if reply.attr.to ~= session.full_jid then
reply.attr.type = "error";
reply:tag("error", error_attr)
:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
core_process_stanza(session, reply);
if queue[i].attr.type ~= "error" then
local reply = st.reply(queue[i]);
if reply.attr.to ~= session.full_jid then
reply.attr.type = "error";
reply:tag("error", error_attr)
:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
core_process_stanza(session, reply);
end
end
end
end
@@ -413,37 +423,46 @@ local function handle_unacked_stanzas(session)
end
-- don't send delivery errors for messages which will be delivered by mam later on
-- check if stanza was archived --> this will allow us to send back errors for stanzas not archived
-- because the user configured the server to do so ("no-archive"-setting for one special contact for example)
local function get_stanza_id(stanza, by_jid)
for tag in stanza:childtags("stanza-id", "urn:xmpp:sid:0") do
if tag.attr.by == by_jid then
return tag.attr.id;
end
end
return nil;
end
module:hook("delivery/failure", function(event)
local session, stanza = event.session, event.stanza;
-- Only deal with authenticated (c2s) sessions
if session.username then
if stanza.name == "message" and stanza.attr.xmlns == nil and
( stanza.attr.type == "chat" or ( stanza.attr.type or "normal" ) == "normal" ) then
-- don't store messages in offline store if they are mam results
local mam_result = stanza:get_child("result", xmlns_mam2);
if mam_result ~= nil then
return true; -- stanza already "handled", don't send an error and don't add it to offline storage
end
-- do nothing here for normal messages and don't send out "message delivery errors",
-- because messages are already in MAM at this point (no need to frighten users)
if session.mam_requested and stanza._was_archived then
local stanza_id = get_stanza_id(stanza, jid.bare(session.full_jid));
if session.mam_requested and stanza_id ~= nil then
session.log("debug", "mod_smacks delivery/failure returning true for mam-handled stanza: mam-archive-id=%s", tostring(stanza_id));
return true; -- stanza handled, don't send an error
end
-- store message in offline store, if this client does not use mam *and* was the last client online
local sessions = prosody.hosts[module.host].sessions[session.username] and
prosody.hosts[module.host].sessions[session.username].sessions or nil;
if sessions and next(sessions) == session.resource and next(sessions, session.resource) == nil then
module:fire_event("message/offline/handle", { origin = session, stanza = stanza } );
return true; -- stanza handled, don't send an error
local ok = module:fire_event("message/offline/handle", { origin = session, stanza = stanza } );
session.log("debug", "mod_smacks delivery/failuere returning %s for offline-handled stanza", tostring(ok));
return ok; -- if stanza was handled, don't send an error
end
end
end
end);
-- mark stanzas as archived --> this will allow us to send back errors for stanzas not archived
-- because the user configured the server to do so ("no-archive"-setting for one special contact for example)
module:hook("archive-message-added", function(event)
local session, stanza, for_user, stanza_id = event.origin, event.stanza, event.for_user, event.id;
if session then session.log("debug", "Marking stanza as archived, archive_id: %s, stanza: %s", tostring(stanza_id), tostring(stanza:top_tag())); end
if not session then module:log("debug", "Marking stanza as archived in unknown session, archive_id: %s, stanza: %s", tostring(stanza_id), tostring(stanza:top_tag())); end
stanza._was_archived = true;
end);
module:hook("pre-resource-unbind", function (event)
local session, err = event.session, event.error;
if session.smacks then

View File

@@ -93,8 +93,7 @@ end
-- saves start time if it is new dominat speaker
-- or calculates and accumulates time of speaking
function SpeakerStats:setDominantSpeaker(isNowDominantSpeaker)
log("debug",
"set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
-- log("debug", "set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
if not self:isDominantSpeaker() and isNowDominantSpeaker then
self._dominantSpeakerStart = socket.gettime()*1000;

View File

@@ -1,101 +0,0 @@
--- mod_websocket.lua
+++ mod_websocket.lua
@@ -163,34 +163,34 @@ function handle_request(event)
return 403;
end
- local function websocket_close(code, message)
+ local function websocket_close(conn, code, message)
conn:write(build_close(code, message));
conn:close();
end
local dataBuffer;
- local function handle_frame(frame)
+ local function handle_frame(conn, frame)
local opcode = frame.opcode;
local length = frame.length;
module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
-- Error cases
if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
- websocket_close(1002, "Reserved bits not zero");
+ websocket_close(conn, 1002, "Reserved bits not zero");
return false;
end
if opcode == 0x8 then -- close frame
if length == 1 then
- websocket_close(1002, "Close frame with payload, but too short for status code");
+ websocket_close(conn, 1002, "Close frame with payload, but too short for status code");
return false;
elseif length >= 2 then
local status_code = parse_close(frame.data)
if status_code < 1000 then
- websocket_close(1002, "Closed with invalid status code");
+ websocket_close(conn, 1002, "Closed with invalid status code");
return false;
elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
- websocket_close(1002, "Closed with reserved status code");
+ websocket_close(conn, 1002, "Closed with reserved status code");
return false;
end
end
@@ -198,28 +198,28 @@ function handle_request(event)
if opcode >= 0x8 then
if length > 125 then -- Control frame with too much payload
- websocket_close(1002, "Payload too large");
+ websocket_close(conn, 1002, "Payload too large");
return false;
end
if not frame.FIN then -- Fragmented control frame
- websocket_close(1002, "Fragmented control frame");
+ websocket_close(conn, 1002, "Fragmented control frame");
return false;
end
end
if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
- websocket_close(1002, "Reserved opcode");
+ websocket_close(conn, 1002, "Reserved opcode");
return false;
end
if opcode == 0x0 and not dataBuffer then
- websocket_close(1002, "Unexpected continuation frame");
+ websocket_close(conn, 1002, "Unexpected continuation frame");
return false;
end
if (opcode == 0x1 or opcode == 0x2) and dataBuffer then
- websocket_close(1002, "Continuation frame expected");
+ websocket_close(conn, 1002, "Continuation frame expected");
return false;
end
@@ -229,11 +229,11 @@ function handle_request(event)
elseif opcode == 0x1 then -- Text frame
dataBuffer = {frame.data};
elseif opcode == 0x2 then -- Binary frame
- websocket_close(1003, "Only text frames are supported");
+ websocket_close(conn, 1003, "Only text frames are supported");
return;
elseif opcode == 0x8 then -- Close request
- websocket_close(1000, "Goodbye");
- return;
+ websocket_close(conn, 1000, "Goodbye");
+ return "";
elseif opcode == 0x9 then -- Ping frame
frame.opcode = 0xA;
conn:write(build_frame(frame));
@@ -276,7 +276,7 @@ function handle_request(event)
while frame do
frameBuffer = frameBuffer:sub(length + 1);
- local result = handle_frame(frame);
+ local result = handle_frame(session.conn, frame);
if not result then return; end
cache[#cache+1] = filter_open_close(result);
frame, length = parse_frame(frameBuffer);

View File

@@ -142,40 +142,44 @@ function Util:get_public_key(keyId)
end
--- Verifies issuer part of token
-- @param 'iss' claim from the token to verify
-- @param 'issClaim' claim from the token to verify
-- @param 'acceptedIssuers' list of issuers to check
-- @return nil and error string or true for accepted claim
function Util:verify_issuer(issClaim, acceptedIssuers)
if not acceptedIssuers then
acceptedIssuers = self.acceptedIssuers
end
module:log("debug","verify_issuer claim: %s against accepted: %s",issClaim, acceptedIssuers);
module:log("debug", "verify_issuer claim: %s against accepted: %s", issClaim, acceptedIssuers);
for i, iss in ipairs(acceptedIssuers) do
if iss == '*' then
-- "*" indicates to accept any issuer in the claims so return success
return true;
end
if issClaim == iss then
--claim matches an accepted issuer so return success
-- claim matches an accepted issuer so return success
return true;
end
end
--if issClaim not found in acceptedIssuers, fail claim
-- if issClaim not found in acceptedIssuers, fail claim
return nil, "Invalid issuer ('iss' claim)";
end
--- Verifies audience part of token
-- @param 'aud' claim from the token to verify
-- @param 'audClaim' claim from the token to verify
-- @return nil and error string or true for accepted claim
function Util:verify_audience(audClaim)
module:log("debug","verify_audience claim: %s against accepted: %s",audClaim, self.acceptedAudiences);
module:log("debug", "verify_audience claim: %s against accepted: %s", audClaim, self.acceptedAudiences);
for i, aud in ipairs(self.acceptedAudiences) do
if aud == '*' then
--* indicates to accept any audience in the claims so return success
-- "*" indicates to accept any audience in the claims so return success
return true;
end
if audClaim == aud then
--claim matches an accepted audience so return success
-- claim matches an accepted audience so return success
return true;
end
end
--if issClaim not found in acceptedIssuers, fail claim
-- if audClaim not found in acceptedAudiences, fail claim
return nil, "Invalid audience ('aud' claim)";
end

View File

@@ -22,11 +22,15 @@ local muc_domain = module:get_option_string(
local escaped_muc_domain_base = muc_domain_base:gsub("%p", "%%%1");
local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
-- The pattern used to extract the target subdomain
-- (e.g. extract 'foo' from 'foo.muc.example.com')
-- (e.g. extract 'foo' from 'conference.foo.example.com')
local target_subdomain_pattern
= "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
-- table to store all incoming iqs without roomname in it, like discoinfo to the muc compoent
local roomless_iqs = {};
-- Utility function to split room JID to include room name and subdomain
-- (e.g. from room1@conference.foo.example.com/res returns (room1, example.com, res, foo))
local function room_jid_split_subdomain(room_jid)
local node, host, resource = jid.split(room_jid);
local target_subdomain = host and host:match(target_subdomain_pattern);
@@ -34,39 +38,57 @@ local function room_jid_split_subdomain(room_jid)
end
--- Utility function to check and convert a room JID from
-- virtual room1@muc.foo.example.com to real [foo]room1@muc.example.com
--- virtual room1@conference.foo.example.com to real [foo]room1@conference.example.com
-- @param room_jid the room jid to match and rewrite if needed
-- @return returns room jid [foo]room1@muc.example.com when it has subdomain
-- otherwise room1@muc.example.com(the room_jid value untouched)
local function room_jid_match_rewrite(room_jid)
local node, host, resource, target_subdomain = room_jid_split_subdomain(room_jid);
-- @param stanza the stanza
-- @return returns room jid [foo]room1@conference.example.com when it has subdomain
-- otherwise room1@conference.example.com(the room_jid value untouched)
local function room_jid_match_rewrite(room_jid, stanza)
local node, _, resource, target_subdomain = room_jid_split_subdomain(room_jid);
if not target_subdomain then
module:log("debug", "No need to rewrite out 'to' %s", room_jid);
-- module:log("debug", "No need to rewrite out 'to' %s", room_jid);
return room_jid;
end
-- Ok, rewrite room_jid address to new format
local new_node, new_host, new_resource
= "["..target_subdomain.."]"..node, muc_domain, resource;
local new_node, new_host, new_resource;
if node then
new_node, new_host, new_resource = "["..target_subdomain.."]"..node, muc_domain, resource;
else
-- module:log("debug", "No room name provided so rewriting only host 'to' %s", room_jid);
new_host, new_resource = muc_domain, resource;
if (stanza and stanza.attr and stanza.attr.id) then
roomless_iqs[stanza.attr.id] = stanza.attr.to;
end
end
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
-- module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
local function internal_room_jid_match_rewrite(room_jid)
-- Utility function to check and convert a room JID from real [foo]room1@muc.example.com to virtual room1@muc.foo.example.com
local function internal_room_jid_match_rewrite(room_jid, stanza)
local node, host, resource = jid.split(room_jid);
if host ~= muc_domain or not node then
module:log("debug", "No need to rewrite %s (not from the MUC host)", room_jid);
-- module:log("debug", "No need to rewrite %s (not from the MUC host)", room_jid);
if (stanza and stanza.attr and stanza.attr.id and roomless_iqs[stanza.attr.id]) then
local result = roomless_iqs[stanza.attr.id];
roomless_iqs[stanza.attr.id] = nil;
return result;
end
return room_jid;
end
local target_subdomain, target_node = node:match("^%[([^%]]+)%](.+)$");
if not (target_node and target_subdomain) then
module:log("debug", "Not rewriting... unexpected node format: %s", node);
-- module:log("debug", "Not rewriting... unexpected node format: %s", node);
return room_jid;
end
-- Ok, rewrite room_jid address to pretty format
local new_node, new_host, new_resource = target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource;
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
-- module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
@@ -216,8 +238,8 @@ function is_healthcheck_room(room_jid)
return false;
end
-- Utility function to make an http get request and
-- retry @param retry number of times
--- Utility function to make an http get request and
--- retry @param retry number of times
-- @param url endpoint to be called
-- @param retry nr of retries, if retry is
-- nil there will be no retries