2020-03-18 22:47:56 +01:00
|
|
|
/*
|
2022-08-30 21:13:37 +02:00
|
|
|
Copyright 2020-2022 New Vector Ltd.
|
2020-03-18 22:47:56 +01:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2021-06-30 14:28:31 +02:00
|
|
|
import { KJUR } from 'jsrsasign';
|
2020-09-05 05:55:50 +02:00
|
|
|
import {
|
2020-09-24 21:25:59 +02:00
|
|
|
IOpenIDCredentials,
|
2020-09-05 05:55:50 +02:00
|
|
|
IWidgetApiRequest,
|
2022-08-30 21:13:37 +02:00
|
|
|
IWidgetApiRequestData,
|
|
|
|
IWidgetApiResponseData,
|
2020-09-05 05:55:50 +02:00
|
|
|
VideoConferenceCapabilities,
|
2020-10-01 04:09:42 +02:00
|
|
|
WidgetApi,
|
2022-08-30 21:13:37 +02:00
|
|
|
WidgetApiAction,
|
2020-09-05 05:55:50 +02:00
|
|
|
} from "matrix-widget-api";
|
2020-10-01 04:09:42 +02:00
|
|
|
import { ElementWidgetActions } from "matrix-react-sdk/src/stores/widgets/ElementWidgetActions";
|
2021-10-15 16:56:22 +02:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2022-03-18 17:12:44 +01:00
|
|
|
import { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions";
|
|
|
|
import { SnakedObject } from "matrix-react-sdk/src/utils/SnakedObject";
|
2021-10-15 16:56:22 +02:00
|
|
|
|
2022-02-28 19:02:03 +01:00
|
|
|
import { getVectorConfig } from "../getconfig";
|
|
|
|
|
2021-12-09 23:57:46 +01:00
|
|
|
// We have to trick webpack into loading our CSS for us.
|
2022-07-15 15:53:21 +02:00
|
|
|
require("./index.pcss");
|
2021-12-09 23:57:46 +01:00
|
|
|
|
2020-09-08 10:44:11 +02:00
|
|
|
const JITSI_OPENIDTOKEN_JWT_AUTH = 'openidtoken-jwt';
|
2020-03-18 22:47:56 +01:00
|
|
|
|
|
|
|
// Dev note: we use raw JS without many dependencies to reduce bundle size.
|
|
|
|
// We do not need all of React to render a Jitsi conference.
|
|
|
|
|
2020-07-21 12:30:28 +02:00
|
|
|
declare let JitsiMeetExternalAPI: any;
|
2020-03-18 22:47:56 +01:00
|
|
|
|
|
|
|
let inConference = false;
|
|
|
|
|
|
|
|
// Jitsi params
|
|
|
|
let jitsiDomain: string;
|
|
|
|
let conferenceId: string;
|
|
|
|
let displayName: string;
|
|
|
|
let avatarUrl: string;
|
|
|
|
let userId: string;
|
2020-09-04 12:14:52 +02:00
|
|
|
let jitsiAuth: string;
|
|
|
|
let roomId: string;
|
2020-09-24 21:25:59 +02:00
|
|
|
let openIdToken: IOpenIDCredentials;
|
2021-02-15 16:54:37 +01:00
|
|
|
let roomName: string;
|
2021-12-17 09:54:57 +01:00
|
|
|
let startAudioOnly: boolean;
|
2022-04-01 17:28:47 +02:00
|
|
|
let isVideoChannel: boolean;
|
2022-07-12 14:23:53 +02:00
|
|
|
let supportsScreensharing: boolean;
|
2020-03-18 22:47:56 +01:00
|
|
|
|
|
|
|
let widgetApi: WidgetApi;
|
2020-09-16 22:39:40 +02:00
|
|
|
let meetApi: any; // JitsiMeetExternalAPI
|
2022-02-28 19:02:03 +01:00
|
|
|
let skipOurWelcomeScreen = false;
|
2020-03-18 22:47:56 +01:00
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
const setupCompleted = (async (): Promise<string | void> => {
|
2020-03-18 22:47:56 +01:00
|
|
|
try {
|
2022-02-28 19:02:03 +01:00
|
|
|
// Queue a config.json lookup asap, so we can use it later on. We want this to be concurrent with
|
|
|
|
// other setup work and therefore do not block.
|
2022-10-24 14:48:32 +02:00
|
|
|
const configPromise = getVectorConfig();
|
2022-02-28 19:02:03 +01:00
|
|
|
|
2021-07-16 20:37:48 +02:00
|
|
|
// The widget's options are encoded into the fragment to avoid leaking info to the server.
|
|
|
|
const widgetQuery = new URLSearchParams(window.location.hash.substring(1));
|
|
|
|
// The widget spec on the other hand requires the widgetId and parentUrl to show up in the regular query string.
|
|
|
|
const realQuery = new URLSearchParams(window.location.search.substring(1));
|
2020-03-18 22:47:56 +01:00
|
|
|
const qsParam = (name: string, optional = false): string => {
|
2021-07-16 20:37:48 +02:00
|
|
|
const vals = widgetQuery.has(name) ? widgetQuery.getAll(name) : realQuery.getAll(name);
|
|
|
|
if (!optional && vals.length !== 1) {
|
2020-03-18 22:47:56 +01:00
|
|
|
throw new Error(`Expected singular ${name} in query string`);
|
|
|
|
}
|
2022-05-16 17:47:12 +02:00
|
|
|
return vals[0];
|
2020-03-18 22:47:56 +01:00
|
|
|
};
|
|
|
|
|
2020-04-01 12:08:53 +02:00
|
|
|
// If we have these params, expect a widget API to be available (ie. to be in an iframe
|
|
|
|
// inside a matrix client). Otherwise, assume we're on our own, eg. have been popped
|
|
|
|
// out into a browser.
|
|
|
|
const parentUrl = qsParam('parentUrl', true);
|
|
|
|
const widgetId = qsParam('widgetId', true);
|
2020-10-19 19:51:16 +02:00
|
|
|
const theme = qsParam('theme', true);
|
|
|
|
|
|
|
|
if (theme) {
|
2020-10-27 12:13:55 +01:00
|
|
|
document.body.classList.add(`theme-${theme.replace(" ", "_")}`);
|
2020-10-19 19:51:16 +02:00
|
|
|
}
|
2020-04-01 12:08:53 +02:00
|
|
|
|
2020-07-13 18:32:17 +02:00
|
|
|
// Set this up as early as possible because Element will be hitting it almost immediately.
|
2022-08-30 21:13:37 +02:00
|
|
|
let widgetApiReady: Promise<void>;
|
2020-04-01 12:08:53 +02:00
|
|
|
if (parentUrl && widgetId) {
|
2020-09-05 05:55:50 +02:00
|
|
|
const parentOrigin = new URL(qsParam('parentUrl')).origin;
|
|
|
|
widgetApi = new WidgetApi(qsParam("widgetId"), parentOrigin);
|
2022-08-30 21:13:37 +02:00
|
|
|
|
|
|
|
widgetApiReady = new Promise<void>(resolve => widgetApi.once("ready", resolve));
|
2020-09-05 05:55:50 +02:00
|
|
|
widgetApi.requestCapabilities(VideoConferenceCapabilities);
|
|
|
|
widgetApi.start();
|
2022-08-30 21:13:37 +02:00
|
|
|
|
|
|
|
const handleAction = (
|
|
|
|
action: WidgetApiAction,
|
2022-11-04 16:30:25 +01:00
|
|
|
handler: (request: IWidgetApiRequestData) => Promise<void>,
|
2022-08-30 21:13:37 +02:00
|
|
|
): void => {
|
|
|
|
widgetApi.on(`action:${action}`, async (ev: CustomEvent<IWidgetApiRequest>) => {
|
|
|
|
ev.preventDefault();
|
|
|
|
await setupCompleted;
|
|
|
|
|
|
|
|
let response: IWidgetApiResponseData;
|
|
|
|
try {
|
|
|
|
await handler(ev.detail.data);
|
|
|
|
response = {};
|
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof Error) {
|
|
|
|
response = { error: { message: e.message } };
|
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await widgetApi.transport.reply(ev.detail, response);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
handleAction(ElementWidgetActions.JoinCall, async ({ audioInput, videoInput }) => {
|
|
|
|
joinConference(audioInput as string | null, videoInput as string | null);
|
|
|
|
});
|
|
|
|
handleAction(ElementWidgetActions.HangupCall, async ({ force }) => {
|
|
|
|
if (force === true) {
|
|
|
|
meetApi?.dispose();
|
|
|
|
notifyHangup();
|
|
|
|
meetApi = null;
|
|
|
|
closeConference();
|
|
|
|
} else {
|
|
|
|
meetApi?.executeCommand('hangup');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
handleAction(ElementWidgetActions.MuteAudio, async () => {
|
|
|
|
if (meetApi && !await meetApi.isAudioMuted()) {
|
|
|
|
meetApi.executeCommand('toggleAudio');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
handleAction(ElementWidgetActions.UnmuteAudio, async () => {
|
|
|
|
if (meetApi && await meetApi.isAudioMuted()) {
|
|
|
|
meetApi.executeCommand('toggleAudio');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
handleAction(ElementWidgetActions.MuteVideo, async () => {
|
|
|
|
if (meetApi && !await meetApi.isVideoMuted()) {
|
|
|
|
meetApi.executeCommand('toggleVideo');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
handleAction(ElementWidgetActions.UnmuteVideo, async () => {
|
|
|
|
if (meetApi && await meetApi.isVideoMuted()) {
|
|
|
|
meetApi.executeCommand('toggleVideo');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
handleAction(ElementWidgetActions.TileLayout, async () => {
|
|
|
|
meetApi?.executeCommand('setTileView', true);
|
|
|
|
});
|
|
|
|
handleAction(ElementWidgetActions.SpotlightLayout, async () => {
|
|
|
|
meetApi?.executeCommand('setTileView', false);
|
|
|
|
});
|
|
|
|
handleAction(ElementWidgetActions.StartLiveStream, async ({ rtmpStreamKey }) => {
|
|
|
|
if (!meetApi) throw new Error("Conference not joined");
|
|
|
|
meetApi.executeCommand('startRecording', {
|
|
|
|
mode: 'stream',
|
|
|
|
// this looks like it should be rtmpStreamKey but we may be on too old
|
|
|
|
// a version of jitsi meet
|
|
|
|
//rtmpStreamKey,
|
|
|
|
youtubeStreamKey: rtmpStreamKey,
|
|
|
|
});
|
|
|
|
});
|
2020-09-05 05:55:50 +02:00
|
|
|
} else {
|
2021-10-15 17:00:43 +02:00
|
|
|
logger.warn("No parent URL or no widget ID - assuming no widget API is available");
|
2020-04-01 12:08:53 +02:00
|
|
|
}
|
2020-03-24 16:14:59 +01:00
|
|
|
|
2020-03-18 22:47:56 +01:00
|
|
|
// Populate the Jitsi params now
|
2020-03-24 16:54:15 +01:00
|
|
|
jitsiDomain = qsParam('conferenceDomain');
|
2020-03-18 22:47:56 +01:00
|
|
|
conferenceId = qsParam('conferenceId');
|
|
|
|
displayName = qsParam('displayName', true);
|
|
|
|
avatarUrl = qsParam('avatarUrl', true); // http not mxc
|
|
|
|
userId = qsParam('userId');
|
2020-09-04 12:14:52 +02:00
|
|
|
jitsiAuth = qsParam('auth', true);
|
|
|
|
roomId = qsParam('roomId', true);
|
2021-02-15 16:54:37 +01:00
|
|
|
roomName = qsParam('roomName', true);
|
2021-12-17 09:54:57 +01:00
|
|
|
startAudioOnly = qsParam('isAudioOnly', true) === "true";
|
2022-04-01 17:28:47 +02:00
|
|
|
isVideoChannel = qsParam('isVideoChannel', true) === "true";
|
2022-07-12 14:23:53 +02:00
|
|
|
supportsScreensharing = qsParam('supportsScreensharing', true) === "true";
|
2020-03-18 22:47:56 +01:00
|
|
|
|
2022-02-28 19:02:03 +01:00
|
|
|
// We've reached the point where we have to wait for the config, so do that then parse it.
|
2022-03-18 17:12:44 +01:00
|
|
|
const instanceConfig = new SnakedObject<IConfigOptions>((await configPromise) ?? <IConfigOptions>{});
|
|
|
|
const jitsiConfig = instanceConfig.get("jitsi_widget") ?? {};
|
|
|
|
skipOurWelcomeScreen = (new SnakedObject<IConfigOptions["jitsi_widget"]>(jitsiConfig))
|
2022-04-20 17:03:28 +02:00
|
|
|
.get("skip_built_in_welcome_screen") ?? false;
|
2022-02-28 19:02:03 +01:00
|
|
|
|
2022-04-05 17:32:26 +02:00
|
|
|
// Either reveal the prejoin screen, or skip straight to Jitsi depending on the config.
|
2022-02-28 19:02:03 +01:00
|
|
|
// We don't set up the call yet though as this might lead to failure without the widget API.
|
2022-04-05 17:32:26 +02:00
|
|
|
toggleConferenceVisibility(skipOurWelcomeScreen);
|
2022-02-28 19:02:03 +01:00
|
|
|
|
2020-04-01 12:08:53 +02:00
|
|
|
if (widgetApi) {
|
2022-08-30 21:13:37 +02:00
|
|
|
await widgetApiReady;
|
2020-03-24 16:54:15 +01:00
|
|
|
|
2020-09-07 18:25:44 +02:00
|
|
|
// See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
2020-09-08 10:44:11 +02:00
|
|
|
if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) {
|
2020-09-08 11:50:53 +02:00
|
|
|
// Request credentials, give callback to continue when received
|
2020-09-24 21:25:59 +02:00
|
|
|
openIdToken = await widgetApi.requestOpenIDConnectToken();
|
2021-10-15 16:59:13 +02:00
|
|
|
logger.log("Got OpenID Connect token");
|
2020-09-07 17:10:19 +02:00
|
|
|
}
|
|
|
|
}
|
2020-10-01 17:59:13 +02:00
|
|
|
|
2022-02-28 19:02:03 +01:00
|
|
|
// Now that everything should be set up, skip to the Jitsi splash screen if needed
|
|
|
|
if (skipOurWelcomeScreen) {
|
|
|
|
skipToJitsiSplashScreen();
|
|
|
|
}
|
|
|
|
|
2020-10-01 17:59:13 +02:00
|
|
|
enableJoinButton(); // always enable the button
|
2020-03-18 22:47:56 +01:00
|
|
|
} catch (e) {
|
2021-10-15 16:56:22 +02:00
|
|
|
logger.error("Error setting up Jitsi widget", e);
|
2020-09-07 18:51:16 +02:00
|
|
|
document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget";
|
2020-03-18 22:47:56 +01:00
|
|
|
}
|
|
|
|
})();
|
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
function enableJoinButton(): void {
|
|
|
|
document.getElementById("joinButton").onclick = (): void => joinConference();
|
2020-09-07 17:10:19 +02:00
|
|
|
}
|
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
function switchVisibleContainers(): void {
|
2020-03-18 22:47:56 +01:00
|
|
|
inConference = !inConference;
|
2022-02-28 19:02:03 +01:00
|
|
|
|
|
|
|
// Our welcome screen is managed by other code, so just don't switch to it ever
|
|
|
|
// if we're not supposed to.
|
|
|
|
if (!skipOurWelcomeScreen) {
|
|
|
|
toggleConferenceVisibility(inConference);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
function toggleConferenceVisibility(inConference: boolean): void {
|
2020-03-18 22:47:56 +01:00
|
|
|
document.getElementById("jitsiContainer").style.visibility = inConference ? 'unset' : 'hidden';
|
2022-05-09 16:22:45 +02:00
|
|
|
// Video rooms have a separate UI for joining, so they should never show our join button
|
|
|
|
document.getElementById("joinButtonContainer").style.visibility =
|
|
|
|
(inConference || isVideoChannel) ? 'hidden' : 'unset';
|
2020-03-18 22:47:56 +01:00
|
|
|
}
|
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
function skipToJitsiSplashScreen(): void {
|
2022-02-28 19:02:03 +01:00
|
|
|
// really just a function alias for self-documenting code
|
|
|
|
joinConference();
|
|
|
|
}
|
|
|
|
|
2020-09-04 12:14:52 +02:00
|
|
|
/**
|
|
|
|
* Create a JWT token fot jitsi openidtoken-jwt auth
|
|
|
|
*
|
2020-09-07 18:25:44 +02:00
|
|
|
* See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
2020-09-04 12:14:52 +02:00
|
|
|
*/
|
2022-11-23 17:24:36 +01:00
|
|
|
function createJWTToken(): string {
|
2020-09-04 12:14:52 +02:00
|
|
|
// Header
|
2021-06-30 14:28:31 +02:00
|
|
|
const header = { alg: 'HS256', typ: 'JWT' };
|
2020-09-04 12:14:52 +02:00
|
|
|
// Payload
|
|
|
|
const payload = {
|
2020-09-07 18:23:36 +02:00
|
|
|
// As per Jitsi token auth, `iss` needs to be set to something agreed between
|
|
|
|
// JWT generating side and Prosody config. Since we have no configuration for
|
|
|
|
// the widgets, we can't set one anywhere. Using the Jitsi domain here probably makes sense.
|
|
|
|
iss: jitsiDomain,
|
2020-09-04 12:14:52 +02:00
|
|
|
sub: jitsiDomain,
|
|
|
|
aud: `https://${jitsiDomain}`,
|
|
|
|
room: "*",
|
|
|
|
context: {
|
|
|
|
matrix: {
|
2020-09-24 21:25:59 +02:00
|
|
|
token: openIdToken.access_token,
|
2020-09-04 12:14:52 +02:00
|
|
|
room_id: roomId,
|
2021-01-19 18:23:23 +01:00
|
|
|
server_name: openIdToken.matrix_server_name,
|
2020-09-04 12:14:52 +02:00
|
|
|
},
|
|
|
|
user: {
|
|
|
|
avatar: avatarUrl,
|
|
|
|
name: displayName,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
// Sign JWT
|
|
|
|
// The secret string here is irrelevant, we're only using the JWT
|
|
|
|
// to transport data to Prosody in the Jitsi stack.
|
|
|
|
return KJUR.jws.JWS.sign(
|
2020-09-07 17:10:19 +02:00
|
|
|
'HS256',
|
2020-09-04 12:14:52 +02:00
|
|
|
JSON.stringify(header),
|
|
|
|
JSON.stringify(payload),
|
2020-09-07 17:10:19 +02:00
|
|
|
'notused',
|
2020-09-04 12:14:52 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
async function notifyHangup(errorMessage?: string): Promise<void> {
|
2022-04-06 19:08:58 +02:00
|
|
|
if (widgetApi) {
|
|
|
|
// We send the hangup event before setAlwaysOnScreen, because the latter
|
|
|
|
// can cause the receiving side to instantly stop listening.
|
|
|
|
try {
|
2022-05-26 16:03:55 +02:00
|
|
|
await widgetApi.transport.send(ElementWidgetActions.HangupCall, { errorMessage });
|
2022-04-06 19:08:58 +02:00
|
|
|
} finally {
|
|
|
|
await widgetApi.setAlwaysOnScreen(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
function closeConference(): void {
|
2022-05-05 11:51:00 +02:00
|
|
|
switchVisibleContainers();
|
|
|
|
document.getElementById("jitsiContainer").innerHTML = "";
|
|
|
|
|
|
|
|
if (skipOurWelcomeScreen) {
|
|
|
|
skipToJitsiSplashScreen();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-20 17:03:28 +02:00
|
|
|
// event handler bound in HTML
|
2022-08-30 21:13:37 +02:00
|
|
|
// An audio input of undefined instructs Jitsi to start unmuted with whatever
|
|
|
|
// audio input it can find, while an input of null instructs it to start muted,
|
|
|
|
// and a non-nullish input specifies the label of a specific device to use.
|
|
|
|
// Same for video inputs.
|
2022-11-23 17:24:36 +01:00
|
|
|
function joinConference(audioInput?: string | null, videoInput?: string | null): void {
|
2020-09-08 11:50:53 +02:00
|
|
|
let jwt;
|
|
|
|
if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) {
|
2020-10-01 04:51:31 +02:00
|
|
|
if (!openIdToken?.access_token) { // eslint-disable-line camelcase
|
2020-09-08 11:50:53 +02:00
|
|
|
// We've failing to get a token, don't try to init conference
|
2021-10-15 17:00:43 +02:00
|
|
|
logger.warn('Expected to have an OpenID credential, cannot initialize widget.');
|
2020-09-08 11:50:53 +02:00
|
|
|
document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
jwt = createJWTToken();
|
|
|
|
}
|
|
|
|
|
2020-03-18 22:47:56 +01:00
|
|
|
switchVisibleContainers();
|
|
|
|
|
2021-10-15 17:00:43 +02:00
|
|
|
logger.warn(
|
2020-03-31 20:19:17 +02:00
|
|
|
"[Jitsi Widget] The next few errors about failing to parse URL parameters are fine if " +
|
|
|
|
"they mention 'external_api' or 'jitsi' in the stack. They're just Jitsi Meet trying to parse " +
|
|
|
|
"our fragment values and not recognizing the options.",
|
|
|
|
);
|
2022-04-01 17:28:47 +02:00
|
|
|
|
2020-09-04 12:14:52 +02:00
|
|
|
const options = {
|
2020-03-18 22:47:56 +01:00
|
|
|
width: "100%",
|
|
|
|
height: "100%",
|
|
|
|
parentNode: document.querySelector("#jitsiContainer"),
|
|
|
|
roomName: conferenceId,
|
2022-04-20 17:03:28 +02:00
|
|
|
devices: {
|
2022-08-30 21:13:37 +02:00
|
|
|
audioInput,
|
|
|
|
videoInput,
|
2022-04-20 17:03:28 +02:00
|
|
|
},
|
2022-04-30 16:23:36 +02:00
|
|
|
userInfo: {
|
|
|
|
displayName,
|
|
|
|
email: userId,
|
|
|
|
},
|
2020-03-18 22:47:56 +01:00
|
|
|
interfaceConfigOverwrite: {
|
|
|
|
SHOW_JITSI_WATERMARK: false,
|
|
|
|
SHOW_WATERMARK_FOR_GUESTS: false,
|
|
|
|
MAIN_TOOLBAR_BUTTONS: [],
|
|
|
|
VIDEO_LAYOUT_FIT: "height",
|
|
|
|
},
|
2021-12-17 09:54:57 +01:00
|
|
|
configOverwrite: {
|
2022-04-30 16:23:36 +02:00
|
|
|
subject: roomName,
|
2021-12-17 09:54:57 +01:00
|
|
|
startAudioOnly,
|
2022-08-30 21:13:37 +02:00
|
|
|
startWithAudioMuted: audioInput === null,
|
|
|
|
startWithVideoMuted: videoInput === null,
|
2022-05-25 23:50:27 +02:00
|
|
|
// Request some log levels for inclusion in rageshakes
|
|
|
|
// Ideally we would capture all possible log levels, but this can
|
|
|
|
// cause Jitsi Meet to try to post various circular data structures
|
|
|
|
// back over the iframe API, and therefore end up crashing
|
|
|
|
// https://github.com/jitsi/jitsi-meet/issues/11585
|
|
|
|
apiLogLevels: ["warn", "error"],
|
2022-04-01 17:28:47 +02:00
|
|
|
} as any,
|
2020-09-08 11:50:53 +02:00
|
|
|
jwt: jwt,
|
2020-09-04 12:14:52 +02:00
|
|
|
};
|
2020-09-08 11:50:53 +02:00
|
|
|
|
2022-04-01 17:28:47 +02:00
|
|
|
// Video channel widgets need some more tailored config options
|
|
|
|
if (isVideoChannel) {
|
2022-04-20 17:03:28 +02:00
|
|
|
// Ensure that we skip Jitsi Meet's native prejoin screen, for
|
|
|
|
// deployments that have it enabled
|
|
|
|
options.configOverwrite.prejoinConfig = { enabled: false };
|
2022-04-01 17:28:47 +02:00
|
|
|
// Use a simplified set of toolbar buttons
|
2022-07-12 14:23:53 +02:00
|
|
|
options.configOverwrite.toolbarButtons = ["microphone", "camera", "tileview", "hangup"];
|
|
|
|
// Note: We can hide the screenshare button in video rooms but not in
|
|
|
|
// normal conference calls, since in video rooms we control exactly what
|
|
|
|
// set of controls appear, but in normal calls we need to leave that up
|
|
|
|
// to the deployment's configuration.
|
|
|
|
// https://github.com/vector-im/element-web/issues/4880#issuecomment-940002464
|
|
|
|
if (supportsScreensharing) options.configOverwrite.toolbarButtons.splice(2, 0, "desktop");
|
2022-04-01 17:28:47 +02:00
|
|
|
// Hide all top bar elements
|
|
|
|
options.configOverwrite.conferenceInfo = { autoHide: [] };
|
2022-07-11 16:13:23 +02:00
|
|
|
// Remove the ability to hide your own tile, since we're hiding the
|
|
|
|
// settings button which would be the only way to get it back
|
|
|
|
options.configOverwrite.disableSelfViewSettings = true;
|
2022-04-01 17:28:47 +02:00
|
|
|
}
|
|
|
|
|
2020-09-16 22:39:40 +02:00
|
|
|
meetApi = new JitsiMeetExternalAPI(jitsiDomain, options);
|
2020-03-18 22:47:56 +01:00
|
|
|
|
2022-01-20 09:51:14 +01:00
|
|
|
// fires once when user joins the conference
|
|
|
|
// (regardless of video on or off)
|
2022-06-27 19:41:33 +02:00
|
|
|
meetApi.on("videoConferenceJoined", onVideoConferenceJoined);
|
|
|
|
meetApi.on("videoConferenceLeft", onVideoConferenceLeft);
|
|
|
|
meetApi.on("readyToClose", closeConference);
|
|
|
|
meetApi.on("errorOccurred", onErrorOccurred);
|
|
|
|
meetApi.on("audioMuteStatusChanged", onAudioMuteStatusChanged);
|
|
|
|
meetApi.on("videoMuteStatusChanged", onVideoMuteStatusChanged);
|
2022-04-01 17:28:47 +02:00
|
|
|
|
2022-06-27 19:41:33 +02:00
|
|
|
["videoConferenceJoined", "participantJoined", "participantLeft"].forEach(event => {
|
|
|
|
meetApi.on(event, updateParticipants);
|
2022-01-20 09:51:14 +01:00
|
|
|
});
|
|
|
|
|
2022-06-27 19:41:33 +02:00
|
|
|
// Patch logs into rageshakes
|
|
|
|
meetApi.on("log", onLog);
|
|
|
|
}
|
2020-03-18 22:47:56 +01:00
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
const onVideoConferenceJoined = (): void => {
|
2022-06-27 19:41:33 +02:00
|
|
|
// Although we set our displayName with the userInfo option above, that
|
|
|
|
// option has a bug where it causes the name to be the HTML encoding of
|
|
|
|
// what was actually intended. So, we use the displayName command to at
|
|
|
|
// least ensure that the name is correct after entering the meeting.
|
|
|
|
// https://github.com/jitsi/jitsi-meet/issues/11664
|
|
|
|
// We can't just use these commands immediately after creating the
|
|
|
|
// iframe, because there's *another* bug where they can crash Jitsi by
|
|
|
|
// racing with its startup process.
|
|
|
|
if (displayName) meetApi.executeCommand("displayName", displayName);
|
|
|
|
// This doesn't have a userInfo equivalent, so has to be set via commands
|
|
|
|
if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl);
|
2022-03-22 23:14:27 +01:00
|
|
|
|
2022-06-27 19:41:33 +02:00
|
|
|
if (widgetApi) {
|
|
|
|
// ignored promise because we don't care if it works
|
|
|
|
// noinspection JSIgnoredPromiseFromCall
|
|
|
|
widgetApi.setAlwaysOnScreen(true);
|
|
|
|
widgetApi.transport.send(ElementWidgetActions.JoinCall, {});
|
|
|
|
}
|
2022-04-06 19:08:58 +02:00
|
|
|
|
2022-06-27 19:41:33 +02:00
|
|
|
// Video rooms should start in tile mode
|
|
|
|
if (isVideoChannel) meetApi.executeCommand("setTileView", true);
|
|
|
|
};
|
2022-03-22 23:14:27 +01:00
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
const onVideoConferenceLeft = (): void => {
|
2022-06-27 19:41:33 +02:00
|
|
|
notifyHangup();
|
|
|
|
meetApi = null;
|
|
|
|
};
|
2022-03-22 23:14:27 +01:00
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
const onErrorOccurred = ({ error }): void => {
|
2022-06-27 19:41:33 +02:00
|
|
|
if (error.isFatal) {
|
|
|
|
// We got disconnected. Since Jitsi Meet might send us back to the
|
|
|
|
// prejoin screen, we're forced to act as if we hung up entirely.
|
|
|
|
notifyHangup(error.message);
|
|
|
|
meetApi = null;
|
|
|
|
closeConference();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
const onAudioMuteStatusChanged = ({ muted }): void => {
|
2022-06-27 19:41:33 +02:00
|
|
|
const action = muted ? ElementWidgetActions.MuteAudio : ElementWidgetActions.UnmuteAudio;
|
|
|
|
widgetApi?.transport.send(action, {});
|
|
|
|
};
|
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
const onVideoMuteStatusChanged = ({ muted }): void => {
|
2022-06-27 19:41:33 +02:00
|
|
|
if (muted) {
|
|
|
|
// Jitsi Meet always sends a "video muted" event directly before
|
|
|
|
// hanging up, which we need to ignore by padding the timeout here,
|
|
|
|
// otherwise the React SDK will mistakenly think the user turned off
|
|
|
|
// their video by hand
|
|
|
|
setTimeout(() => {
|
|
|
|
if (meetApi) widgetApi?.transport.send(ElementWidgetActions.MuteVideo, {});
|
|
|
|
}, 200);
|
|
|
|
} else {
|
|
|
|
widgetApi?.transport.send(ElementWidgetActions.UnmuteVideo, {});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
const updateParticipants = (): void => {
|
2022-06-27 19:41:33 +02:00
|
|
|
widgetApi?.transport.send(ElementWidgetActions.CallParticipants, {
|
|
|
|
participants: meetApi.getParticipantsInfo(),
|
2022-03-22 23:14:27 +01:00
|
|
|
});
|
2022-06-27 19:41:33 +02:00
|
|
|
};
|
2022-05-19 10:24:39 +02:00
|
|
|
|
2022-11-23 17:24:36 +01:00
|
|
|
const onLog = ({ logLevel, args }): void =>
|
2022-06-27 19:41:33 +02:00
|
|
|
(parent as unknown as typeof global).mx_rage_logger?.log(logLevel, ...args);
|