mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-12 00:40:18 +00:00
Compare commits
10 Commits
4510
...
react-thum
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d1ed86e49 | ||
|
|
88c02fb658 | ||
|
|
0f64c66f91 | ||
|
|
9f65ae52f1 | ||
|
|
a242e86b23 | ||
|
|
4211db0893 | ||
|
|
9a35026d6a | ||
|
|
9742e90bb5 | ||
|
|
2a01d3550c | ||
|
|
efce5a831b |
4
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
4
.github/ISSUE_TEMPLATE/2-feature-request.md
vendored
@@ -1,7 +1,9 @@
|
||||
---
|
||||
name: "Feature request"
|
||||
about: Suggest an idea for this project
|
||||
|
||||
title: ''
|
||||
labels: 'feature-request'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}%`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
12
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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: {
|
||||
74
react/features/base/conference/middleware.native.js
Normal file
74
react/features/base/conference/middleware.native.js
Normal 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);
|
||||
});
|
||||
}
|
||||
23
react/features/base/conference/middleware.web.js
Normal file
23
react/features/base/conference/middleware.web.js
Normal 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);
|
||||
});
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -91,7 +91,6 @@ export default [
|
||||
'disableSimulcast',
|
||||
'disableSuspendVideo',
|
||||
'disableThirdPartyRequests',
|
||||
'displayJids',
|
||||
'doNotStoreRoom',
|
||||
'e2eping',
|
||||
'enableDisplayNameInStats',
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
567
react/features/filmstrip/components/web/Thumbnail.js
Normal file
567
react/features/filmstrip/components/web/Thumbnail.js
Normal 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);
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
15
react/features/mobile/picture-in-picture/functions.js
Normal file
15
react/features/mobile/picture-in-picture/functions.js
Normal 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);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
export * from './functions';
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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 } />
|
||||
|
||||
@@ -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));
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user