Simplify the Jitsi wrapper (#23158)

* Remove ClientReady and WidgetReady hacks from the Jitsi wrapper

By registering widget API listeners earlier and simply blocking the
execution of all actions until the Jitsi wrapper has finished setting
up, sending a WidgetReady action becomes no longer necessary.

The ClientReady action is likewise not necessary, because in practice
getting a ready event means the client and the widget have already been
happily talking back and forth for capability negotiation.

* Rename audioDevice/videoDevice to audioInput/videoInput

* Combine HangupCall and ForceHangupCall into one action

* Apply misc code review suggestions
This commit is contained in:
Robin 2022-08-30 15:13:37 -04:00 committed by GitHub
parent 26077db644
commit ed1ecde348
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 New Vector Ltd. Copyright 2020-2022 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,8 +18,11 @@ import { KJUR } from 'jsrsasign';
import { import {
IOpenIDCredentials, IOpenIDCredentials,
IWidgetApiRequest, IWidgetApiRequest,
IWidgetApiRequestData,
IWidgetApiResponseData,
VideoConferenceCapabilities, VideoConferenceCapabilities,
WidgetApi, WidgetApi,
WidgetApiAction,
} from "matrix-widget-api"; } from "matrix-widget-api";
import { ElementWidgetActions } from "matrix-react-sdk/src/stores/widgets/ElementWidgetActions"; import { ElementWidgetActions } from "matrix-react-sdk/src/stores/widgets/ElementWidgetActions";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -58,9 +61,7 @@ let widgetApi: WidgetApi;
let meetApi: any; // JitsiMeetExternalAPI let meetApi: any; // JitsiMeetExternalAPI
let skipOurWelcomeScreen = false; let skipOurWelcomeScreen = false;
const ack = (ev: CustomEvent<IWidgetApiRequest>) => widgetApi.transport.reply(ev.detail, {}); const setupCompleted = (async () => {
(async function() {
try { try {
// Queue a config.json lookup asap, so we can use it later on. We want this to be concurrent with // 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. // other setup work and therefore do not block.
@ -90,24 +91,88 @@ const ack = (ev: CustomEvent<IWidgetApiRequest>) => widgetApi.transport.reply(ev
} }
// Set this up as early as possible because Element will be hitting it almost immediately. // Set this up as early as possible because Element will be hitting it almost immediately.
let readyPromise: Promise<[void, void]>; let widgetApiReady: Promise<void>;
if (parentUrl && widgetId) { if (parentUrl && widgetId) {
const parentOrigin = new URL(qsParam('parentUrl')).origin; const parentOrigin = new URL(qsParam('parentUrl')).origin;
widgetApi = new WidgetApi(qsParam("widgetId"), parentOrigin); widgetApi = new WidgetApi(qsParam("widgetId"), parentOrigin);
widgetApiReady = new Promise<void>(resolve => widgetApi.once("ready", resolve));
widgetApi.requestCapabilities(VideoConferenceCapabilities); widgetApi.requestCapabilities(VideoConferenceCapabilities);
readyPromise = Promise.all([
new Promise<void>(resolve => {
widgetApi.once(`action:${ElementWidgetActions.ClientReady}`, ev => {
ev.preventDefault();
resolve();
widgetApi.transport.reply(ev.detail, {});
});
}),
new Promise<void>(resolve => {
widgetApi.once("ready", () => resolve());
}),
]);
widgetApi.start(); widgetApi.start();
const handleAction = (
action: WidgetApiAction,
handler: (request: IWidgetApiRequestData) => void,
): 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,
});
});
} else { } else {
logger.warn("No parent URL or no widget ID - assuming no widget API is available"); logger.warn("No parent URL or no widget ID - assuming no widget API is available");
} }
@ -136,7 +201,7 @@ const ack = (ev: CustomEvent<IWidgetApiRequest>) => widgetApi.transport.reply(ev
toggleConferenceVisibility(skipOurWelcomeScreen); toggleConferenceVisibility(skipOurWelcomeScreen);
if (widgetApi) { if (widgetApi) {
await readyPromise; await widgetApiReady;
// See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification // See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) { if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) {
@ -144,99 +209,6 @@ const ack = (ev: CustomEvent<IWidgetApiRequest>) => widgetApi.transport.reply(ev
openIdToken = await widgetApi.requestOpenIDConnectToken(); openIdToken = await widgetApi.requestOpenIDConnectToken();
logger.log("Got OpenID Connect token"); logger.log("Got OpenID Connect token");
} }
widgetApi.on(`action:${ElementWidgetActions.JoinCall}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
const { audioDevice, videoDevice } = ev.detail.data;
joinConference(audioDevice as string | null, videoDevice as string | null);
ack(ev);
},
);
widgetApi.on(`action:${ElementWidgetActions.HangupCall}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
meetApi?.executeCommand('hangup');
ack(ev);
},
);
widgetApi.on(`action:${ElementWidgetActions.ForceHangupCall}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
meetApi?.dispose();
notifyHangup();
meetApi = null;
closeConference();
ack(ev);
},
);
widgetApi.on(`action:${ElementWidgetActions.MuteAudio}`,
async (ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
if (meetApi && !await meetApi.isAudioMuted()) {
meetApi.executeCommand('toggleAudio');
}
ack(ev);
},
);
widgetApi.on(`action:${ElementWidgetActions.UnmuteAudio}`,
async (ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
if (meetApi && await meetApi.isAudioMuted()) {
meetApi.executeCommand('toggleAudio');
}
ack(ev);
},
);
widgetApi.on(`action:${ElementWidgetActions.MuteVideo}`,
async (ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
if (meetApi && !await meetApi.isVideoMuted()) {
meetApi.executeCommand('toggleVideo');
}
ack(ev);
},
);
widgetApi.on(`action:${ElementWidgetActions.UnmuteVideo}`,
async (ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
if (meetApi && await meetApi.isVideoMuted()) {
meetApi.executeCommand('toggleVideo');
}
ack(ev);
},
);
widgetApi.on(`action:${ElementWidgetActions.TileLayout}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
meetApi?.executeCommand('setTileView', true);
ack(ev);
},
);
widgetApi.on(`action:${ElementWidgetActions.SpotlightLayout}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
meetApi?.executeCommand('setTileView', false);
ack(ev);
},
);
widgetApi.on(`action:${ElementWidgetActions.StartLiveStream}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
if (meetApi) {
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: ev.detail.data.rtmpStreamKey,
youtubeStreamKey: ev.detail.data.rtmpStreamKey,
});
ack(ev);
} else {
widgetApi.transport.reply(ev.detail, { error: { message: "Conference not joined" } });
}
},
);
} }
// Now that everything should be set up, skip to the Jitsi splash screen if needed // Now that everything should be set up, skip to the Jitsi splash screen if needed
@ -245,13 +217,6 @@ const ack = (ev: CustomEvent<IWidgetApiRequest>) => widgetApi.transport.reply(ev
} }
enableJoinButton(); // always enable the button enableJoinButton(); // always enable the button
// Inform the client that we're ready to receive events
try {
await widgetApi?.transport.send(ElementWidgetActions.WidgetReady, {});
} catch (e) {
logger.error(e);
}
} catch (e) { } catch (e) {
logger.error("Error setting up Jitsi widget", e); logger.error("Error setting up Jitsi widget", e);
document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget"; document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget";
@ -346,11 +311,11 @@ function closeConference() {
} }
// event handler bound in HTML // event handler bound in HTML
// An audio device of undefined instructs Jitsi to start unmuted with whatever // An audio input of undefined instructs Jitsi to start unmuted with whatever
// audio device it can find, while a device of null instructs it to start muted, // audio input it can find, while an input of null instructs it to start muted,
// and a non-nullish device specifies the label of a specific device to use. // and a non-nullish input specifies the label of a specific device to use.
// Same for video devices. // Same for video inputs.
function joinConference(audioDevice?: string | null, videoDevice?: string | null) { function joinConference(audioInput?: string | null, videoInput?: string | null) {
let jwt; let jwt;
if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) { if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) {
if (!openIdToken?.access_token) { // eslint-disable-line camelcase if (!openIdToken?.access_token) { // eslint-disable-line camelcase
@ -376,8 +341,8 @@ function joinConference(audioDevice?: string | null, videoDevice?: string | null
parentNode: document.querySelector("#jitsiContainer"), parentNode: document.querySelector("#jitsiContainer"),
roomName: conferenceId, roomName: conferenceId,
devices: { devices: {
audioInput: audioDevice, audioInput,
videoInput: videoDevice, videoInput,
}, },
userInfo: { userInfo: {
displayName, displayName,
@ -392,8 +357,8 @@ function joinConference(audioDevice?: string | null, videoDevice?: string | null
configOverwrite: { configOverwrite: {
subject: roomName, subject: roomName,
startAudioOnly, startAudioOnly,
startWithAudioMuted: audioDevice === null, startWithAudioMuted: audioInput === null,
startWithVideoMuted: videoDevice === null, startWithVideoMuted: videoInput === null,
// Request some log levels for inclusion in rageshakes // Request some log levels for inclusion in rageshakes
// Ideally we would capture all possible log levels, but this can // Ideally we would capture all possible log levels, but this can
// cause Jitsi Meet to try to post various circular data structures // cause Jitsi Meet to try to post various circular data structures