Compare commits

..

1 Commits

Author SHA1 Message Date
Hristo Terezov
e7cb719be0 feat(jibri-queue): JibriQueue support. 2020-08-18 12:57:48 -05:00
44 changed files with 784 additions and 938 deletions

View File

@@ -759,13 +759,7 @@ export default {
}
if (isPrejoinPageEnabled(APP.store.getState())) {
_connectionPromise = connect(roomName).then(c => {
// we want to initialize it early, in case of errors to be able
// to gather logs
APP.connection = c;
return c;
});
_connectionPromise = connect(roomName);
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(initialOptions);
const tracks = await tryCreateLocalTracks;
@@ -1212,6 +1206,10 @@ export default {
// end used by torture
getLogs() {
return room.getLogs();
},
/**
* Download logs, a function that can be called from console while
* debugging.
@@ -1220,7 +1218,7 @@ export default {
saveLogs(filename = 'meetlog.json') {
// this can be called from console and will not have reference to this
// that's why we reference the global var
const logs = APP.connection.getLogs();
const logs = APP.conference.getLogs();
const data = encodeURIComponent(JSON.stringify(logs, null, ' '));
const elem = document.createElement('a');

View File

@@ -82,7 +82,7 @@ function checkForAttachParametersAndConnect(id, password, connection) {
*/
function connect(id, password, roomName) {
const connectionConfig = Object.assign({}, config);
const { jwt } = APP.store.getState()['features/base/jwt'];
const { issuer, jwt } = APP.store.getState()['features/base/jwt'];
// Use Websocket URL for the web app if configured. Note that there is no 'isWeb' check, because there's assumption
// that this code executes only on web browsers/electron. This needs to be changed when mobile and web are unified.
@@ -94,7 +94,11 @@ function connect(id, password, roomName) {
// in future). It's included for the time being for Jitsi Meet and lib-jitsi-meet versions interoperability.
connectionConfig.serviceUrl = connectionConfig.bosh = serviceUrl;
const connection = new JitsiMeetJS.JitsiConnection(null, jwt, connectionConfig);
const connection
= new JitsiMeetJS.JitsiConnection(
null,
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
connectionConfig);
if (config.iAmRecorder) {
connection.addFeature(DISCO_JIBRI_FEATURE);
@@ -207,9 +211,10 @@ export function openConnection({ id, password, retry, roomName }) {
return connect(id, password, roomName).catch(err => {
if (retry) {
const { jwt } = APP.store.getState()['features/base/jwt'];
const { issuer, jwt } = APP.store.getState()['features/base/jwt'];
if (err === JitsiConnectionErrors.PASSWORD_REQUIRED && !jwt) {
if (err === JitsiConnectionErrors.PASSWORD_REQUIRED
&& (!jwt || issuer === 'anonymous')) {
return AuthHandler.requestAuth(roomName, connect);
}
}

74
css/_notifications.scss Normal file
View File

@@ -0,0 +1,74 @@
@include keyframes(exiting) {
from {
opacity: 1;
transform: translate(0, 0);
}
to {
opacity: 0;
transform: translate(-200px, 0);
}
}
.notificationsContainer {
display: flex;
position: absolute;
bottom: 0px;
flex-direction: column;
width: 400px;
left: 80px;
.topContainer {
padding-bottom: 16px;
// transition: height 0.4s ease-in-out;
.notification {
width: 400px;
z-index: 5;
&:nth-child(n+2) {
margin-top: 16px;
}
&.exiting {
animation-name: exiting;
animation-duration: 0.4s;
}
}
}
.bottomContainer {
margin-bottom: 64px;
position: relative;
height: 100%;
.notification {
width: 400px;
bottom: 0px;
&:nth-child(1) {
z-index: 5;
}
&:nth-child(n+2) {
transition: transform 0.4s ease-in-out;
z-index: 4;
position: absolute;
transform: translateY(100%) translateY(16px);
}
&:nth-child(n+4) {
display: none;
}
&.exiting {
animation-name: exiting;
animation-duration: 0.4s;
}
&.exiting+.notification {
transform: translateY(0);
}
}
}
}

View File

@@ -115,3 +115,19 @@
font-size: 12px;
}
}
.jibri-queue-info {
display: flex;
flex-flow: column;
.footer {
background: #a4b8a4D1;
border-radius: 3px;
font-weight: bold;
color: #5e6d7a;
padding: 5px;
margin-right: 30px;
margin-top: 5px;
}
}

View File

@@ -102,5 +102,6 @@ $flagsImagePath: "../images/";
@import 'premeeting-screens';
@import 'e2ee';
@import 'responsive';
@import 'notifications';
/* Modules END */

View File

@@ -45,10 +45,8 @@ server {
error_page 404 /static/404.html;
gzip on;
gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
gzip_types text/plain text/css application/javascript application/json;
gzip_vary on;
gzip_proxied no-cache no-store private expired auth;
gzip_min_length 512;
location = /config.js {
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
@@ -63,11 +61,6 @@ server {
{
add_header 'Access-Control-Allow-Origin' '*';
alias /usr/share/jitsi-meet/$1/$2;
# cache all versioned files
if ($arg_v) {
expires 1y;
}
}
# BOSH

View File

@@ -14,12 +14,6 @@ server {
ssi on;
}
gzip on;
gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
gzip_vary on;
gzip_proxied no-cache no-store private expired auth;
gzip_min_length 512;
# BOSH
location /http-bind {
proxy_pass http://localhost:5280/http-bind;

View File

@@ -28,12 +28,6 @@ server {
tcp_nodelay on;
}
gzip on;
gzip_types text/plain text/css application/javascript application/json image/x-icon application/octet-stream application/wasm;
gzip_vary on;
gzip_proxied no-cache no-store private expired auth;
gzip_min_length 512;
location ~ ^/([^/?&:'"]+)$ {
try_files $uri @root_path;
}

View File

@@ -102,7 +102,6 @@
"bandwidth": "Estimated bandwidth:",
"bitrate": "Bitrate:",
"bridgeCount": "Server count: ",
"codecs": "Codecs (A/V): ",
"connectedTo": "Connected to:",
"e2e_rtt": "E2E RTT:",
"framerate": "Frame rate:",
@@ -217,7 +216,9 @@
"kickParticipantDialog": "Are you sure you want to kick this participant?",
"kickParticipantTitle": "Kick this participant?",
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
"leaveJibriQueue": "Exit queue",
"liveStreaming": "Live Streaming",
"leaveJibriQueueWarning": "Are you sure you would like to exit the queue?",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Not possible while recording is active",
"liveStreamingDisabledForGuestTooltip": "Guests can't start live streaming.",
"liveStreamingDisabledTooltip": "Start live stream disabled.",
@@ -909,5 +910,22 @@
"passwordJoinButton": "Join",
"reject": "Reject",
"toggleLabel": "Enable lobby"
},
"jibriQueue": {
"recording": {
"title": "You have joined a recording queue!",
"time": "Estimated time for starting the recording: {{time}}",
"footer": "For unlimited recordings you should subscribe to 8x8 Meetings",
"left": "You have left the recording queue!"
},
"livestreaming": {
"title": "You have joined a live streaming queue!",
"time": "Estimated time for starting the live streaming: {{time}}",
"footer": "For unlimited live streaming you should subscribe to 8x8 Meetings",
"left": "You have left the live streaming queue!"
},
"position": "{{count}} more person is waiting in front of you.",
"position_plural": "{{count}} more people are waiting in front of you.",
"exit": "Exit queue"
}
}

View File

@@ -238,6 +238,7 @@ function initCommands() {
return;
}
const jibriQueueJID = state['features/base/config'].jibriQueueJID;
let recordingConfig;
if (mode === JitsiRecordingConstants.mode.FILE) {
@@ -251,7 +252,8 @@ function initCommands() {
'token': dropboxToken
}
}
})
}),
jibriQueueJID
};
} else {
recordingConfig = {
@@ -260,12 +262,14 @@ function initCommands() {
'file_recording_metadata': {
'share': shouldShare
}
})
}),
jibriQueueJID
};
}
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
recordingConfig = {
broadcastId: youtubeBroadcastID,
jibriQueueJID,
mode: JitsiRecordingConstants.mode.STREAM,
streamId: youtubeStreamKey
};
@@ -275,7 +279,9 @@ function initCommands() {
return;
}
conference.startRecording(recordingConfig);
conference.startRecording(recordingConfig).catch(() => {
// prevent unhandled promise rejection.
});
},
/**
@@ -302,8 +308,10 @@ function initCommands() {
const activeSession = getActiveSession(state, mode);
if (activeSession && activeSession.id) {
conference.stopRecording(activeSession.id);
if (activeSession && (activeSession.id || activeSession.queueID)) {
conference.stopRecording(activeSession.id, activeSession.queueID).catch(() => {
// prevent unhandled promise rejection.
});
} else {
logger.error('No recording or streaming session found');
}

16
package-lock.json generated
View File

@@ -982,9 +982,9 @@
}
},
"@atlaskit/portal": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@atlaskit/portal/-/portal-4.0.1.tgz",
"integrity": "sha512-dYe/YozUkFZ0NitZ2dfnHE/d8i1Tq+pcdlwXd3+PyhzvPbnPoseZpQ/jC+KAbxZ4wCzLwSF+SXapdAJSbwkLfg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/portal/-/portal-4.0.0.tgz",
"integrity": "sha512-FLvq90T2zt7bUOUkOb90xbB1JGOI77456euDwrz1d9NYVoe+kSQr4Xau7kQLUgmpzmH4Sd4BHOvp50JHJ0qezw==",
"requires": {
"@atlaskit/theme": "^10.0.0",
"exenv": "^1.2.2",
@@ -993,9 +993,9 @@
},
"dependencies": {
"@atlaskit/theme": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-10.0.1.tgz",
"integrity": "sha512-XUor9lYlX0yTRSxd/rvaL8i2gdm0PDbOV+KhuezpuGBaS0opzseRrCnEc+OMGmpWRYHjCRyEugp6FwSSquFb8w==",
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-10.0.2.tgz",
"integrity": "sha512-TfzWnISbO9R9BkBpu2PD1bvGku2LqFkzhcBiJUAjWcL2E5nIt9NZIlnnCZ4A94510Lnd4B/orcghh5iid5R/LA==",
"requires": {
"exenv": "^1.2.2",
"prop-types": "^15.5.10",
@@ -17945,8 +17945,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#15dcc57424cc937290e1963b8eb402c1fcf48ccb",
"from": "github:jitsi/lib-jitsi-meet#15dcc57424cc937290e1963b8eb402c1fcf48ccb",
"version": "github:jitsi/lib-jitsi-meet#f74cd0abe9c696a9c3ca7dbb9ca170e6e84d6756",
"from": "github:jitsi/lib-jitsi-meet#f74cd0abe9c696a9c3ca7dbb9ca170e6e84d6756",
"requires": {
"@jitsi/js-utils": "1.0.0",
"@jitsi/sdp-interop": "1.0.3",

View File

@@ -31,6 +31,7 @@
"@atlaskit/tabs": "8.0.11",
"@atlaskit/theme": "7.0.2",
"@atlaskit/toggle": "5.0.14",
"@atlaskit/portal": "4.0.0",
"@atlaskit/tooltip": "12.1.13",
"@jitsi/js-utils": "1.0.1",
"@microsoft/microsoft-graph-client": "1.1.0",
@@ -56,7 +57,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#15dcc57424cc937290e1963b8eb402c1fcf48ccb",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f74cd0abe9c696a9c3ca7dbb9ca170e6e84d6756",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",

View File

@@ -43,8 +43,8 @@ export const SET_CONFIG = 'SET_CONFIG';
* and the passed object.
*
* {
* type: UPDATE_CONFIG,
* type: _UPDATE_CONFIG,
* config: Object
* }
*/
export const UPDATE_CONFIG = 'UPDATE_CONFIG';
export const _UPDATE_CONFIG = '_UPDATE_CONFIG';

View File

@@ -6,24 +6,10 @@ import type { Dispatch } from 'redux';
import { addKnownDomains } from '../known-domains';
import { parseURIString } from '../util';
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG, UPDATE_CONFIG } from './actionTypes';
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
import { _CONFIG_STORE_PREFIX } from './constants';
import { setConfigFromURLParams } from './functions';
/**
* Updates the config with new options.
*
* @param {Object} config - The new options (to add).
* @returns {Function}
*/
export function updateConfig(config: Object) {
return {
type: UPDATE_CONFIG,
config
};
}
/**
* Signals that the configuration (commonly known in Jitsi Meet as config.js)
* for a specific locationURL will be loaded now.

View File

@@ -8,8 +8,7 @@ import { addKnownDomains } from '../known-domains';
import { MiddlewareRegistry } from '../redux';
import { parseURIString } from '../util';
import { SET_CONFIG } from './actionTypes';
import { updateConfig } from './actions';
import { _UPDATE_CONFIG, SET_CONFIG } from './actionTypes';
import { _CONFIG_STORE_PREFIX } from './constants';
/**
@@ -115,7 +114,10 @@ function _setConfig({ dispatch, getState }, next, action) {
config.resolution = resolutionFlag;
}
dispatch(updateConfig(config));
dispatch({
type: _UPDATE_CONFIG,
config
});
// FIXME On Web we rely on the global 'config' variable which gets altered
// multiple times, before it makes it to the reducer. At some point it may

View File

@@ -4,7 +4,7 @@ import _ from 'lodash';
import { equals, ReducerRegistry, set } from '../redux';
import { UPDATE_CONFIG, CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
import { _UPDATE_CONFIG, CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
import { _cleanupConfig } from './functions';
/**
@@ -50,7 +50,7 @@ const INITIAL_RN_STATE = {
ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
switch (action.type) {
case UPDATE_CONFIG:
case _UPDATE_CONFIG:
return _updateConfig(state, action);
case CONFIG_WILL_LOAD:

View File

@@ -80,8 +80,12 @@ export function connect(id: ?string, password: ?string) {
const state = getState();
const options = _constructOptions(state);
const { locationURL } = state['features/base/connection'];
const { jwt } = state['features/base/jwt'];
const connection = new JitsiMeetJS.JitsiConnection(options.appId, jwt, options);
const { issuer, jwt } = state['features/base/jwt'];
const connection
= new JitsiMeetJS.JitsiConnection(
options.appId,
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
options);
connection[JITSI_CONNECTION_URL_KEY] = locationURL;

View File

@@ -1,5 +0,0 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/jwt');

View File

@@ -13,7 +13,6 @@ import { MiddlewareRegistry } from '../redux';
import { SET_JWT } from './actionTypes';
import { setJWT } from './actions';
import { parseJWTFromURLParams } from './functions';
import logger from './logger';
declare var APP: Object;
@@ -134,13 +133,7 @@ function _setJWT(store, next, action) {
action.isGuest = !enableUserRolesBasedOnToken;
let jwtPayload;
try {
jwtPayload = jwtDecode(jwt);
} catch (e) {
logger.error(e);
}
const jwtPayload = jwtDecode(jwt);
if (jwtPayload) {
const { context, iss } = jwtPayload;

View File

@@ -340,7 +340,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
bandwidth,
bitrate,
bridgeCount,
codec,
e2eRtt,
framerate,
maxEnabledResolution,
@@ -356,7 +355,6 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
bandwidth = { bandwidth }
bitrate = { bitrate }
bridgeCount = { bridgeCount }
codec = { codec }
connectionSummary = { this._getConnectionStatusTip() }
e2eRtt = { e2eRtt }
framerate = { framerate }

View File

@@ -122,7 +122,6 @@ const statsEmitter = {
_onStatsUpdated(localUserId: string, stats: Object) {
const allUserFramerates = stats.framerate || {};
const allUserResolutions = stats.resolution || {};
const allUserCodecs = stats.codec || {};
// FIXME resolution and framerate are maps keyed off of user ids with
// stat values. Receivers of stats expect resolution and framerate to
@@ -130,8 +129,7 @@ const statsEmitter = {
// stats objects.
const modifiedLocalStats = Object.assign({}, stats, {
framerate: allUserFramerates[localUserId],
resolution: allUserResolutions[localUserId],
codec: allUserCodecs[localUserId]
resolution: allUserResolutions[localUserId]
});
this._emitStatsUpdate(localUserId, modifiedLocalStats);
@@ -140,9 +138,8 @@ const statsEmitter = {
// and update remote user stats as needed.
const framerateUserIds = Object.keys(allUserFramerates);
const resolutionUserIds = Object.keys(allUserResolutions);
const codecUserIds = Object.keys(allUserCodecs);
_.union(framerateUserIds, resolutionUserIds, codecUserIds)
_.union(framerateUserIds, resolutionUserIds)
.filter(id => id !== localUserId)
.forEach(id => {
const remoteUserStats = {};
@@ -159,12 +156,6 @@ const statsEmitter = {
remoteUserStats.resolution = resolution;
}
const codec = allUserCodecs[id];
if (codec) {
remoteUserStats.codec = codec;
}
this._emitStatsUpdate(id, remoteUserStats);
});
}

View File

@@ -34,11 +34,6 @@ type Props = {
*/
bridgeCount: number,
/**
* Audio/video codecs in use for the connection.
*/
codec: Object,
/**
* A message describing the connection quality.
*/
@@ -224,45 +219,6 @@ class ConnectionStatsTable extends Component<Props> {
);
}
/**
* Creates a a table row as a ReactElement for displaying codec, if present.
* This will typically be something like "Codecs (A/V): opus, vp8".
*
* @private
* @returns {ReactElement}
*/
_renderCodecs() {
const { codec, t } = this.props;
if (!codec) {
return;
}
let codecString;
// Only report one codec, in case there are multiple for a user.
Object.keys(codec || {})
.forEach(ssrc => {
const { audio, video } = codec[ssrc];
codecString = `${audio}, ${video}`;
});
if (!codecString) {
codecString = 'N/A';
}
return (
<tr>
<td>
<span>{ t('connectionindicator.codecs') }</span>
</td>
<td>{ codecString }</td>
</tr>
);
}
/**
* Creates a table row as a ReactElement for displaying a summary message
* about the current connection status.
@@ -496,7 +452,6 @@ class ConnectionStatsTable extends Component<Props> {
{ isRemoteVideo ? this._renderRegion() : null }
{ this._renderResolution() }
{ this._renderFrameRate() }
{ this._renderCodecs() }
{ isRemoteVideo ? null : this._renderBridgeCount() }
</tbody>
</table>

View File

@@ -1,7 +1,8 @@
// @flow
import { FlagGroup } from '@atlaskit/flag';
import Portal from '@atlaskit/portal';
import React from 'react';
import { Transition, TransitionGroup } from 'react-transition-group';
import { connect } from '../../../base/redux';
import AbstractNotificationsContainer, {
@@ -27,6 +28,16 @@ type Props = AbstractProps & {
* @extends {Component}
*/
class NotificationsContainer extends AbstractNotificationsContainer<Props> {
/**
* Creates new NotificationContainer instance.
*
* @param {Props} props - The props of the react component.
*/
constructor(props: Props) {
super(props);
this._renderNotification = this._renderNotification.bind(this);
}
/**
* Implements React's {@link Component#render()}.
@@ -40,39 +51,92 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
}
return (
<FlagGroup onDismissed = { this._onDismissed }>
{ this._renderFlags() }
</FlagGroup>
<Portal zIndex = { 600 }>
<div className = 'notificationsContainer'>
{ this._renderTopNotificationsContainer() }
{ this._renderBottomNotificationsContainer() }
</div>
</Portal>
);
}
_onDismissed: number => void;
/**
* Renders notifications to display as ReactElements. An empty array will
* be returned if notifications are disabled.
* Renders the bottom notification container.
*
* @private
* @returns {ReactElement[]}
* @returns {ReactElement}
*/
_renderFlags() {
_renderBottomNotificationsContainer() {
const { _notifications } = this.props;
return _notifications.map(notification => {
const { props, uid } = notification;
return (
<TransitionGroup className = 'bottomContainer'>
{
_notifications.filter(n => n.props.position !== 'top').map((notification, index) => {
const { props, uid } = notification;
// The id attribute is necessary as {@code FlagGroup} looks for
// either id or key to set a key on notifications, but accessing
// props.key will cause React to print an error.
return (
<Notification
{ ...props }
id = { uid }
key = { uid }
uid = { uid } />
return this._renderNotification({
...props,
isDismissAllowed: index > 0 ? false : props.isDismissAllowed
}, uid);
})
}
</TransitionGroup>
);
}
);
});
_renderNotification: (string, number) => Function;
/**
* Renders a notification.
*
* @param {Object} props - The props for the Notification component.
* @param {string} uid - A unique ID for the notification.
* @returns {Function} - Returns a transition function for the Transition component.
*/
_renderNotification(props, uid) {
return (
<Transition
key = { uid }
timeout = { 400 }>
{
transitionState => (
<div className = { `notification ${transitionState}` }>
<Notification
{ ...props }
id = { uid }
key = { uid }
onDismissed = { this._onDismissed }
uid = { uid } />
</div>
)
}
</Transition>
);
}
/**
* Renders the top notifications container.
*
* @private
* @returns {ReactElement}
*/
_renderTopNotificationsContainer() {
const { _notifications } = this.props;
return (
<TransitionGroup className = 'topContainer'>
{
_notifications.filter(n => n.props.position === 'top').map(notification => {
const { props, uid } = notification;
return this._renderNotification(props, uid);
})
}
</TransitionGroup>
);
}
}

View File

@@ -201,13 +201,11 @@ export function initPrejoin(tracks: Object[], errors: Object) {
/**
* Action used to start the conference.
*
* @param {Object} options - The config options that override the default ones (if any).
* @returns {Function}
*/
export function joinConference(options?: Object) {
export function joinConference() {
return {
type: PREJOIN_START_CONFERENCE,
options
type: PREJOIN_START_CONFERENCE
};
}
@@ -224,10 +222,7 @@ export function joinConferenceWithoutAudio() {
if (audioTrack) {
await dispatch(replaceLocalTrack(audioTrack, null));
}
dispatch(joinConference({
startSilent: true
}));
dispatch(joinConference());
};
}

View File

@@ -1,6 +1,5 @@
// @flow
import { updateConfig } from '../base/config';
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media';
import { MiddlewareRegistry } from '../base/redux';
import { updateSettings } from '../base/settings';
@@ -25,9 +24,6 @@ MiddlewareRegistry.register(store => next => async action => {
const state = getState();
const { userSelectedSkipPrejoin } = state['features/prejoin'];
const localVideoTrack = getLocalVideoTrack(state['features/base/tracks']);
const { options } = action;
options && store.dispatch(updateConfig(options));
userSelectedSkipPrejoin && dispatch(updateSettings({
userSelectedSkipPrejoin

View File

@@ -46,3 +46,17 @@ export const SET_PENDING_RECORDING_NOTIFICATION_UID
* }
*/
export const SET_STREAM_KEY = 'SET_STREAM_KEY';
/**
* The type of Redux action which sets the waiting in queue recording notification UID to
* use it for when hiding the notification is necessary, or unsets it when
* undefined (or no param) is passed.
*
* {
* type: SET_WAITING_IN_RECORDING_NOTIFICATION_UID,
* streamType: string,
* uid: ?number
* }
* @public
*/
export const SET_WAITING_IN_RECORDING_NOTIFICATION_UID = 'SET_WAITING_IN_RECORDING_NOTIFICATION_UID';

View File

@@ -1,8 +1,12 @@
// @flow
import React from 'react';
import { openDialog } from '../base/dialog';
import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import {
NOTIFICATION_TIMEOUT,
NOTIFICATION_TYPE,
hideNotification,
showErrorNotification,
showNotification
@@ -12,8 +16,10 @@ import {
CLEAR_RECORDING_SESSIONS,
RECORDING_SESSION_UPDATED,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_STREAM_KEY
SET_STREAM_KEY,
SET_WAITING_IN_RECORDING_NOTIFICATION_UID
} from './actionTypes';
import { QueueInfo, StopLiveStreamDialog, StopRecordingDialog } from './components';
/**
* Clears the data of every recording sessions.
@@ -50,6 +56,25 @@ export function hidePendingRecordingNotification(streamType: string) {
};
}
/**
* Signals that the waiting in queue recording notification should be removed from the screen.
*
* @param {string} streamType - The type of the stream ({@code 'file'} or
* {@code 'stream'}).
* @returns {Function}
*/
export function hideWaitingInQueueRecordingNotification(streamType: string) {
return (dispatch: Function, getState: Function) => {
const { waitingInQueueNotificationUids } = getState()['features/recording'];
const waitingInQueueNotificationUid = waitingInQueueNotificationUids[streamType];
if (waitingInQueueNotificationUid) {
dispatch(hideNotification(waitingInQueueNotificationUid));
dispatch(_setWaitingInQueueRecordingNotificationUid(undefined, streamType));
}
};
}
/**
* Sets the stream key last used by the user for later reuse.
*
@@ -97,6 +122,22 @@ export function showPendingRecordingNotification(streamType: string) {
};
}
/**
* Signals that the jibri queue has been left and notification should be shown on the
* screen.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @returns {showNotification}
*/
export function showQueueLeftRecordingNotification(streamType: string) {
const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
return showNotification({
titleKey: `jibriQueue.${isLiveStreaming ? 'livestreaming' : 'recording'}.left`
}, NOTIFICATION_TIMEOUT);
}
/**
* Signals that the recording error notification should be shown.
*
@@ -175,6 +216,15 @@ export function updateRecordingSessionData(session: Object) {
= status === JitsiRecordingConstants.status.ON
? Date.now() / 1000
: undefined;
const queueID = session.getQueueID();
let queueEstimatedTimeOfStart, queuePosition;
if (status === JitsiRecordingConstants.status.WAITING_IN_QUEUE) {
const { position, estimatedTimeLeft } = session.getQueueMetrics();
queuePosition = position;
queueEstimatedTimeOfStart = (new Date()).getTime() + (estimatedTimeLeft * 1000);
}
return {
type: RECORDING_SESSION_UPDATED,
@@ -186,7 +236,10 @@ export function updateRecordingSessionData(session: Object) {
mode: session.getMode(),
status,
terminator: session.getTerminator(),
timestamp
timestamp,
queueID,
queuePosition,
queueEstimatedTimeOfStart
}
};
}
@@ -212,3 +265,57 @@ function _setPendingRecordingNotificationUid(uid: ?number, streamType: string) {
uid
};
}
/**
* Sets UID of the the pending streaming notification to use it when hiding
* the notification is necessary, or unsets it when undefined (or no param) is
* passed.
*
* @param {?number} uid - The UID of the notification.
* @param {string} streamType - The type of the stream ({@code file} or {@code stream}).
* @returns {{
* type: SET_PENDING_RECORDING_NOTIFICATION_UID,
* streamType: string,
* uid: number
* }}
*/
function _setWaitingInQueueRecordingNotificationUid(uid: ?number, streamType: string) {
return {
type: SET_WAITING_IN_RECORDING_NOTIFICATION_UID,
streamType,
uid
};
}
/**
* Signals that the recording queue notification should be shown on the screen.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @returns {Function}
*/
export function showWaitingInQueueRecordingNotification(streamType: string) {
return (dispatch: Function) => {
const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
const showNotificationAction = showNotification({
appearance: NOTIFICATION_TYPE.INFO,
customActionNameKey: 'jibriQueue.exit',
customActionHandler: () => {
if (isLiveStreaming) {
dispatch(openDialog(StopLiveStreamDialog));
} else {
dispatch(openDialog(StopRecordingDialog));
}
return false;
},
position: 'top',
titleKey: `jibriQueue.${isLiveStreaming ? 'livestreaming' : 'recording'}.title`,
description: <QueueInfo />
});
dispatch(showNotificationAction);
dispatch(_setWaitingInQueueRecordingNotificationUid(
showNotificationAction.uid, streamType));
};
}

View File

@@ -217,6 +217,8 @@ export default class AbstractStartLiveStreamDialog<P: Props>
broadcastId: selectedBroadcastID,
mode: JitsiRecordingConstants.mode.STREAM,
streamId: key
}).catch(() => {
// prevent unhandled promise rejection.
});
return true;

View File

@@ -65,7 +65,9 @@ export default class AbstractStopLiveStreamDialog extends Component<Props> {
const { _session } = this.props;
if (_session) {
this.props._conference.stopRecording(_session.id);
this.props._conference.stopRecording(_session.id, _session.queueID).catch(() => {
// prevent unhandled promise rejection.
});
}
return true;

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { connect } from '../../../../base/redux';
import AbstractStopLiveStreamDialog, {
_mapStateToProps
@@ -24,13 +25,17 @@ class StopLiveStreamDialog extends AbstractStopLiveStreamDialog {
* @returns {ReactElement}
*/
render() {
const { _session = {}, t } = this.props;
const isInQueue = _session.status === JitsiRecordingConstants.status.WAITING_IN_QUEUE;
return (
<Dialog
okKey = 'dialog.stopLiveStreaming'
okKey = { isInQueue ? 'dialog.leaveJibriQueue' : 'dialog.stopLiveStreaming' }
onSubmit = { this._onSubmit }
titleKey = 'dialog.liveStreaming'
width = 'small'>
{ this.props.t('dialog.stopStreamingWarning') }
{ t(isInQueue ? 'dialog.leaveJibriQueueWarning' : 'dialog.stopStreamingWarning') }
</Dialog>
);
}

View File

@@ -280,6 +280,8 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
_conference.startRecording({
mode: JitsiRecordingConstants.mode.FILE,
appData
}).catch(() => {
// prevent unhandled promise rejection.
});
if (_autoCaptionOnRecord) {

View File

@@ -65,7 +65,9 @@ export default class AbstractStopRecordingDialog<P: Props>
const { _fileRecordingSession } = this.props;
if (_fileRecordingSession) {
this.props._conference.stopRecording(_fileRecordingSession.id);
this.props._conference.stopRecording(_fileRecordingSession.id, _fileRecordingSession.queueID).catch(() => {
// prevent unhandled promise rejection.
});
}
return true;

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { connect } from '../../../../base/redux';
import AbstractStopRecordingDialog, {
type Props,
@@ -24,15 +25,17 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { _fileRecordingSession = {}, t } = this.props;
const isInQueue = _fileRecordingSession.status === JitsiRecordingConstants.status.WAITING_IN_QUEUE;
return (
<Dialog
okKey = 'dialog.confirm'
okKey = { isInQueue ? 'dialog.leaveJibriQueue' : 'dialog.confirm' }
onSubmit = { this._onSubmit }
titleKey = 'dialog.recording'
width = 'small'>
{ t('dialog.stopRecordingWarning') }
{ t(isInQueue ? 'dialog.leaveJibriQueueWarning' : 'dialog.stopRecordingWarning') }
</Dialog>
);
}

View File

@@ -0,0 +1,230 @@
// @flow
import React, { Component } from 'react';
import { getLocalizedDurationFormatter, translate } from '../../../base/i18n';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { connect } from '../../../base/redux';
import { getActiveSession } from '../../functions';
type Props = {
/**
* The current position of the participant in the queue.
*/
_position: ?string,
/**
* The recording mode.
*/
_mode: string,
/**
* The ID of the queue.
*/
_queueID: string,
/**
* The time when the recording is expected to start.
*/
_estimatedTimeOfStart: number,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link QueueInfo}.
*/
type State = {
/**
* The current value of the timer for estimated time left.
*/
timerValue: ?string
};
/**
* Implements a React {@link Component} which displays the current state of the Jibri Queue.
*
* @extends {Component}
*/
class QueueInfo extends Component<Props, State> {
/**
* Handle for setInterval timer.
*/
_interval: IntervalID;
/**
* Initializes a new {@code QueueInfo} instance.
*
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
timerValue: undefined
};
}
/**
* Stops the timer when component will be unmounted.
*
* @inheritdoc
*/
componentWillUnmount() {
this._stopTimer();
}
/**
* Starts the timer when component will be mounted.
*
* @inheritdoc
*/
componentDidMount() {
if (typeof this.props._estimatedTimeOfStart !== 'undefined') {
this._startTimer();
}
}
/**
* Implements React Component's componentDidUpdate.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (this.props._estimatedTimeOfStart !== prevProps._estimatedTimeOfStart) {
this._stopTimer(false);
this._startTimer();
}
}
/**
* Sets the current state values that will be used to render the timer.
*
* @param {number} refValueUTC - The initial UTC timestamp value.
* @param {number} currentValueUTC - The current UTC timestamp value.
*
* @returns {void}
*/
_setStateFromUTC(refValueUTC, currentValueUTC) {
if (!refValueUTC || !currentValueUTC) {
return;
}
const timerMsValue = currentValueUTC > refValueUTC ? 0 : refValueUTC - currentValueUTC;
const localizedTime = getLocalizedDurationFormatter(timerMsValue);
this.setState({
timerValue: localizedTime
});
}
/**
* Starts the timer.
*
* @returns {void}
*/
_startTimer() {
const { _estimatedTimeOfStart } = this.props;
if (!this._interval && typeof _estimatedTimeOfStart !== 'undefined') {
this._setStateFromUTC(_estimatedTimeOfStart, (new Date()).getTime());
this._interval = setInterval(() => {
this._setStateFromUTC(_estimatedTimeOfStart, (new Date()).getTime());
}, 1000);
}
}
/**
* Stops the timer.
*
* @param {boolean} [clearState] - If true, the timer value in the state will be cleared.
* @returns {void}
*/
_stopTimer(clearState = true) {
if (this._interval) {
clearInterval(this._interval);
delete this._interval;
}
if (clearState) {
this.setState({
timerValue: undefined
});
}
}
/**
* Implements React {@code Component}'s render.
*
* @inheritdoc
*/
render() {
const { _estimatedTimeOfStart, _mode, _position = 0, t } = this.props;
const { STREAM } = JitsiRecordingConstants.mode;
const timeTextKey = `jibriQueue.${_mode === STREAM ? 'livestreaming' : 'recording'}.time`;
const { timerValue } = this.state;
const footerText = t(`jibriQueue.${_mode === STREAM ? 'livestreaming' : 'recording'}.footer`);
const showFooter = typeof footerText === 'string' && footerText.length > 0;
return (
<div className = 'jibri-queue-info'>
<span className = 'position'>
{ t('jibriQueue.position', { count: _position }) }
</span>
{
typeof _estimatedTimeOfStart === 'undefined' || timerValue === 'undefined'
? null : <span className = 'time'>
{ t(timeTextKey, { time: timerValue }) }
</span>
}
{
showFooter ? <div className = 'footer'>{ footerText }</div> : null
}
</div>
);
}
}
/**
* Maps (parts of) the Redux state to the associated
* {@code AbstractRecordingLabel}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _estimatedTimeOfStart: number,
* _mode: string,
* _position: string,
* _queueID: string,
* t: Function
* }}
*/
export function _mapStateToProps(state: Object) {
const session = getActiveSession(state);
if (!session) {
return {};
}
const { id, mode, queueEstimatedTimeOfStart, queueID, queuePosition } = session;
return {
_sessionID: id,
_mode: mode,
_queueID: queueID,
_position: queuePosition,
_estimatedTimeOfStart: queueEstimatedTimeOfStart
};
}
export default translate(connect(_mapStateToProps)(QueueInfo));

View File

@@ -2,3 +2,4 @@
export { default as RecordingLabel } from './RecordingLabel';
export { default as RecordingLimitNotificationDescription } from './RecordingLimitNotificationDescription';
export { default as QueueInfo } from './QueueInfo';

View File

@@ -9,16 +9,17 @@ import { RECORDING_STATUS_PRIORITIES } from './constants';
* passed in mode.
*
* @param {Object} state - The redux state to search in.
* @param {string} mode - Find an active recording session of the given mode.
* @param {string|undefined} mode - Find an active recording session of the given mode.
* @returns {Object|undefined}
*/
export function getActiveSession(state: Object, mode: string) {
export function getActiveSession(state: Object, mode: ?string) {
const { sessionDatas } = state['features/recording'];
const { status: statusConstants } = JitsiRecordingConstants;
return sessionDatas.find(sessionData => sessionData.mode === mode
return sessionDatas.find(sessionData => (typeof mode === 'undefined' || sessionData.mode === mode)
&& (sessionData.status === statusConstants.ON
|| sessionData.status === statusConstants.PENDING));
|| sessionData.status === statusConstants.PENDING
|| sessionData.status === statusConstants.WAITING_IN_QUEUE));
}
/**
@@ -37,6 +38,8 @@ export function getRecordingDurationEstimation(size: ?number) {
* Searches in the passed in redux state for a recording session that matches
* the passed in recording session ID.
*
* NOTE: The sessoins in WAITING_IN_QUEUE status don't have ID yet.
*
* @param {Object} state - The redux state to search in.
* @param {string} id - The ID of the recording session to find.
* @returns {Object|undefined}
@@ -51,6 +54,8 @@ export function getSessionById(state: Object, id: string) {
* there is a session with the status OFF and one with PENDING, then the PENDING
* one will be shown, because that is likely more important for the user to see.
*
* NOTE: For all "queue" statuses the function returns undefined because we don't want to show label.
*
* @param {Object} state - The redux state to search in.
* @param {string} mode - The recording mode to get status for.
* @returns {string|undefined}

View File

@@ -24,11 +24,14 @@ import { RECORDING_SESSION_UPDATED } from './actionTypes';
import {
clearRecordingSessions,
hidePendingRecordingNotification,
hideWaitingInQueueRecordingNotification,
showPendingRecordingNotification,
showQueueLeftRecordingNotification,
showRecordingError,
showRecordingLimitNotification,
showStartedRecordingNotification,
showStoppedRecordingNotification,
showWaitingInQueueRecordingNotification,
updateRecordingSessionData
} from './actions';
import {
@@ -110,15 +113,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
conference.on(
JitsiConferenceEvents.RECORDER_STATE_CHANGED,
recorderSession => {
if (recorderSession) {
recorderSession.getID()
&& dispatch(
if (recorderSession.getID() || recorderSession.getQueueID()) {
dispatch(
updateRecordingSessionData(recorderSession));
}
recorderSession.getError()
&& _showRecordingErrorNotification(
if (recorderSession.getError()) {
_showRecordingErrorNotification(
recorderSession, dispatch);
}
}
return;
@@ -142,75 +146,91 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
break;
}
const updatedSessionData
= getSessionById(getState(), action.sessionData.id);
const { initiator, mode, terminator } = updatedSessionData;
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
const updatedSessionData = getSessionById(getState(), action.sessionData.id);
const { initiator, mode, status: newStatus, terminator } = updatedSessionData;
const { PENDING, OFF, ON, WAITING_IN_QUEUE, QUEUE_LEFT } = JitsiRecordingConstants.status;
if (updatedSessionData.status === PENDING
&& (!oldSessionData || oldSessionData.status !== PENDING)) {
dispatch(showPendingRecordingNotification(mode));
} else if (updatedSessionData.status !== PENDING) {
if (oldSessionData && oldSessionData.status === newStatus) {
return result;
}
if (newStatus !== WAITING_IN_QUEUE) {
dispatch(hideWaitingInQueueRecordingNotification(mode));
}
if (newStatus !== PENDING) {
dispatch(hidePendingRecordingNotification(mode));
}
if (updatedSessionData.status === ON
&& (!oldSessionData || oldSessionData.status !== ON)) {
if (initiator) {
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
switch (newStatus) {
case WAITING_IN_QUEUE:
dispatch(showWaitingInQueueRecordingNotification(mode));
break;
case QUEUE_LEFT:
dispatch(showQueueLeftRecordingNotification(mode));
break;
case PENDING:
dispatch(showPendingRecordingNotification(mode));
break;
case ON: {
if (initiator) {
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
} else if (typeof recordingLimit === 'object') {
// Show notification with additional information to the initiator.
dispatch(showRecordingLimitNotification(mode));
}
sendAnalytics(createRecordingEvent('start', mode));
if (disableRecordAudioNotification) {
break;
}
let soundID;
if (mode === JitsiRecordingConstants.mode.FILE) {
soundID = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundID = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundID) {
dispatch(playSound(soundID));
}
} else if (updatedSessionData.status === OFF
&& (!oldSessionData || oldSessionData.status !== OFF)) {
dispatch(showStoppedRecordingNotification(
mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
let duration = 0, soundOff, soundOn;
if (oldSessionData && oldSessionData.timestamp) {
duration
= (Date.now() / 1000) - oldSessionData.timestamp;
}
sendAnalytics(createRecordingEvent('stop', mode, duration));
if (disableRecordAudioNotification) {
break;
}
if (mode === JitsiRecordingConstants.mode.FILE) {
soundOff = RECORDING_OFF_SOUND_ID;
soundOn = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundOff = LIVE_STREAMING_OFF_SOUND_ID;
soundOn = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundOff && soundOn) {
dispatch(stopSound(soundOn));
dispatch(playSound(soundOff));
}
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
} else if (typeof recordingLimit === 'object') {
// Show notification with additional information to the initiator.
dispatch(showRecordingLimitNotification(mode));
}
sendAnalytics(createRecordingEvent('start', mode));
if (disableRecordAudioNotification) {
return result;
}
let soundID;
if (mode === JitsiRecordingConstants.mode.FILE) {
soundID = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundID = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundID) {
dispatch(playSound(soundID));
}
break;
}
case OFF: {
dispatch(showStoppedRecordingNotification(
mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
let duration = 0, soundOff, soundOn;
if (oldSessionData && oldSessionData.timestamp) {
duration
= (Date.now() / 1000) - oldSessionData.timestamp;
}
sendAnalytics(createRecordingEvent('stop', mode, duration));
if (disableRecordAudioNotification) {
return result;
}
if (mode === JitsiRecordingConstants.mode.FILE) {
soundOff = RECORDING_OFF_SOUND_ID;
soundOn = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundOff = LIVE_STREAMING_OFF_SOUND_ID;
soundOn = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundOff && soundOn) {
dispatch(stopSound(soundOn));
dispatch(playSound(soundOff));
}
break;
}
}
break;

View File

@@ -4,12 +4,14 @@ import {
CLEAR_RECORDING_SESSIONS,
RECORDING_SESSION_UPDATED,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_STREAM_KEY
SET_STREAM_KEY,
SET_WAITING_IN_RECORDING_NOTIFICATION_UID
} from './actionTypes';
const DEFAULT_STATE = {
pendingNotificationUids: {},
sessionDatas: []
sessionDatas: [],
waitingInQueueNotificationUids: {}
};
/**
@@ -56,6 +58,20 @@ ReducerRegistry.register(STORE_NAME,
streamKey: action.streamKey
};
case SET_WAITING_IN_RECORDING_NOTIFICATION_UID: {
const waitingInQueueNotificationUids = {
...state.waitingInQueueNotificationUids
};
waitingInQueueNotificationUids[action.streamType] = action.uid;
return {
...state,
waitingInQueueNotificationUids
};
}
default:
return state;
}
@@ -71,12 +87,12 @@ ReducerRegistry.register(STORE_NAME,
*/
function _updateSessionDatas(sessionDatas, newSessionData) {
const hasExistingSessionData = sessionDatas.find(
sessionData => sessionData.id === newSessionData.id);
sessionData => sessionData.id === newSessionData.id || sessionData.queueID === newSessionData.queueID);
let newSessionDatas;
if (hasExistingSessionData) {
newSessionDatas = sessionDatas.map(sessionData => {
if (sessionData.id === newSessionData.id) {
if (sessionData.id === newSessionData.id || sessionData.queueID === newSessionData.queueID) {
return {
...newSessionData
};

View File

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

View File

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

View File

@@ -132,7 +132,7 @@ function filter_stanza(stanza)
-- check is an owner, only owners can receive the presence
local room = main_muc_service.get_room_from_jid(jid_bare(node .. '@' .. main_muc_component_config));
if not room or room.get_affiliation(room, stanza.attr.to) == 'owner' then
if room.get_affiliation(room, stanza.attr.to) == 'owner' then
return stanza;
end
@@ -159,11 +159,6 @@ function attach_lobby_room(room)
local lobby_room_jid = node .. '@' .. lobby_muc_component_config;
if not lobby_muc_service.get_room_from_jid(lobby_room_jid) then
local new_room = lobby_muc_service.create_room(lobby_room_jid);
-- set persistent the lobby room to avoid it to be destroyed
-- there are cases like when selecting new moderator after the current one leaves
-- which can leave the room with no occupants and it will be destroyed and we want to
-- avoid lobby destroy while it is enabled
new_room:set_persistent(true);
module:log("debug","Lobby room jid = %s created",lobby_room_jid);
new_room.main_room = room;
room._data.lobbyroom = new_room;
@@ -173,18 +168,6 @@ function attach_lobby_room(room)
return false
end
-- destroys lobby room for the supplied main room
function destroy_lobby_room(room, newjid, message)
if not message then
message = 'Lobby room closed.';
end
if room and room._data.lobbyroom then
room._data.lobbyroom:set_persistent(false);
room._data.lobbyroom:destroy(newjid, message);
room._data.lobbyroom = nil;
end
end
-- process a host module directly if loaded or hooks to wait for its load
function process_host_module(name, callback)
local function process_host(host)
@@ -297,14 +280,16 @@ process_host_module(main_muc_component_config, function(host_module, host)
notify_lobby_enabled(room, actor, true);
end
elseif room._data.lobbyroom then
destroy_lobby_room(room, room.jid);
room._data.lobbyroom:destroy(room.jid, 'Lobby room closed.');
room._data.lobbyroom = nil;
notify_lobby_enabled(room, actor, false);
end
end);
host_module:hook('muc-room-destroyed',function(event)
local room = event.room;
if room._data.lobbyroom then
destroy_lobby_room(room, nil);
room._data.lobbyroom:destroy(nil, 'Lobby room closed.');
room._data.lobbyroom = nil;
end
end);
host_module:hook('muc-disco#info', function (event)
@@ -414,12 +399,7 @@ function handle_create_lobby(event)
attach_lobby_room(room)
end
function handle_destroy_lobby(event)
destroy_lobby_room(event.room, event.newjid, event.message);
end
module:hook_global('bosh-session', update_session);
module:hook_global('websocket-session', update_session);
module:hook_global('config-reloaded', load_config);
module:hook_global('create-lobby-room', handle_create_lobby);
module:hook_global('destroy-lobby-room', handle_destroy_lobby);

View File

@@ -93,8 +93,6 @@ function Util.new(module)
--array of accepted audiences: by default only includes our appId
self.acceptedAudiences = module:get_option_array('asap_accepted_audiences',{'*'})
self.requireRoomClaim = module:get_option_boolean('asap_require_room_claim', true);
if self.asapKeyServer and not have_async then
module:log("error", "requires a version of Prosody with util.async");
return nil;
@@ -104,23 +102,7 @@ function Util.new(module)
end
function Util:set_asap_key_server(asapKeyServer)
self.asapKeyServer = asapKeyServer;
end
function Util:set_asap_accepted_issuers(acceptedIssuers)
self.acceptedIssuers = acceptedIssuers;
end
function Util:set_asap_accepted_audiences(acceptedAudiences)
self.acceptedAudiences = acceptedAudiences;
end
function Util:set_asap_require_room_claim(checkRoom)
self.requireRoomClaim = checkRoom;
end
function Util:clear_asap_cache()
self.cache = require"util.cache".new(cacheSize);
self.asapKeyServer = asapKeyServer
end
--- Returns the public key by keyID
@@ -132,41 +114,18 @@ function Util:get_public_key(keyId)
-- If the key is not found in the cache.
module:log("debug", "Cache miss for key: "..keyId);
local code;
local timeout_occurred;
local wait, done = async.waiter();
local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
local function cb(content_, code_, response_, request_)
if timeout_occurred == nil then
content, code = content_, code_;
if code == 200 or code == 204 then
self.cache:set(keyId, content);
else
module:log("warn", "Error on public key request: Code %s, Content %s",
code_, content_);
end
done();
content, code = content_, code_;
if code == 200 or code == 204 then
self.cache:set(keyId, content);
else
module:log("warn", "public key reply delivered after timeout from: %s",keyurl);
module:log("warn", "Error on public key request: Code %s, Content %s",
code_, content_);
end
done();
end
-- TODO: Is the done() call racey? Can we cancel this if the request
-- succeedes?
local function cancel()
-- TODO: This check is racey. Not likely to be a problem, but we should
-- still stick a mutex on content / code at some point.
if code == nil then
timeout_occurred = true;
module:log("warn", "Timeout %s seconds fetching public key from: %s",http_timeout,keyurl);
if http.destroy_request ~= nil then
http.destroy_request(request);
end
done();
end
end
local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
module:log("debug", "Fetching public key from: "..keyurl);
-- We hash the key ID to work around some legacy behavior and make
@@ -177,6 +136,19 @@ function Util:get_public_key(keyId)
method = "GET"
}, cb);
-- TODO: Is the done() call racey? Can we cancel this if the request
-- succeedes?
local function cancel()
-- TODO: This check is racey. Not likely to be a problem, but we should
-- still stick a mutex on content / code at some point.
if code == nil then
-- no longer present in prosody 0.11, so check before calling
if http.destroy_request ~= nil then
http.destroy_request(request);
end
done();
end
end
timer.add_task(http_timeout, cancel);
wait();
@@ -197,10 +169,6 @@ end
-- @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);
for i, iss in ipairs(acceptedIssuers) do
if issClaim == iss then
--claim matches an accepted issuer so return success
@@ -215,7 +183,6 @@ end
-- @param 'aud' 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);
for i, aud in ipairs(self.acceptedAudiences) do
if aud == '*' then
--* indicates to accept any audience in the claims so return success
@@ -256,11 +223,9 @@ function Util:verify_token(token, secret, acceptedIssuers)
return nil, issCheckErr;
end
if self.requireRoomClaim then
local roomClaim = claims["room"];
if roomClaim == nil then
return nil, "'room' claim is missing";
end
local roomClaim = claims["room"];
if roomClaim == nil then
return nil, "'room' claim is missing";
end
local audClaim = claims["aud"];

View File

@@ -18,20 +18,14 @@ local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
local target_subdomain_pattern
= "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
-- Utility function to split room JID to include room name and subdomain
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);
return node, host, resource, target_subdomain
end
--- Utility function to check and convert a room JID from
-- virtual room1@muc.foo.example.com to real [foo]room1@muc.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);
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;
@@ -44,23 +38,6 @@ local function room_jid_match_rewrite(room_jid)
return room_jid
end
local function internal_room_jid_match_rewrite(room_jid)
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);
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
--- Finds and returns room by its jid
-- @param room_jid the room jid to search in the muc component
@@ -214,7 +191,5 @@ return {
get_room_from_jid = get_room_from_jid;
async_handler_wrapper = async_handler_wrapper;
room_jid_match_rewrite = room_jid_match_rewrite;
room_jid_split_subdomain = room_jid_split_subdomain;
internal_room_jid_match_rewrite = internal_room_jid_match_rewrite;
update_presence_identity = update_presence_identity;
};

View File

@@ -34,8 +34,8 @@
<!--#include virtual="/title.html" -->
<script><!--#include virtual="/config.js" --></script>
<script><!--#include virtual="/interface_config.js" --></script>
<script src="libs/lib-jitsi-meet.min.js"></script>
<script src="libs/app.bundle.min.js"></script>
<script src="libs/lib-jitsi-meet.min.js?v=139"></script>
<script src="libs/app.bundle.min.js?v=139"></script>
</head>
<body>
<div id="react"></div>