riot-web/src/vector/jitsi/index.ts

128 lines
4.9 KiB
TypeScript
Raw Normal View History

Use a local widget wrapper for Jitsi calls Effectively fixes https://github.com/vector-im/riot-web/issues/11074 Effectively fixes https://github.com/vector-im/riot-web/issues/7112 Fixes https://github.com/vector-im/riot-web/issues/6930 Fixes Jitsi widgets not working for guests (https://github.com/vector-im/riot-web/issues/8933) Fixes https://github.com/vector-im/riot-web/issues/5048 Previously we were relying on an integration manager to be defined, functional, and alive in order to join Jitsi calls. This commit changes this so we aren't reliant on an integration manager for Jitsi calls at all, and gives people the option of choosing a Jitsi server via the config.json. This side is just the wrapper/shell: the logic is mostly in the react-sdk (to be linked via PRs). This layer simply has an HTML file exported that can be used to render a Jitsi widget, and the react-sdk constructs a URL to access it locally. This is similar to how the mobile apps handle Jitsi widgets: instead of iframing the widget URL directly into the app, they pull apart the widget information and natively render it. We're effectively doing the same here by parsing the widget options and using our local wrapper instead of whatever happens to be defined in the widget state event. Integration managers should still continue to offer a widget URL for Jitsi widgets as this is what the spec requires. A large part of this is based upon Dimension's handling of Jitsi and widgets in general: a license has been granted to allow Riot (and therefore the react-sdk) to use the code and be inspired by it.
2020-03-18 22:47:56 +01:00
/*
Copyright 2020 New Vector Ltd.
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.
*/
// We have to trick webpack into loading our CSS for us.
require("./index.scss");
import * as qs from 'querystring';
import { Capability, WidgetApi } from "matrix-react-sdk/src/widgets/WidgetApi";
import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import { loadConfig, preparePlatform } from "../initial-load";
// 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.
declare var JitsiMeetExternalAPI: any;
let inConference = false;
// Jitsi params
let jitsiDomain: string;
let conferenceId: string;
let displayName: string;
let avatarUrl: string;
let userId: string;
let widgetApi: WidgetApi;
(async function () {
try {
// The widget's options are encoded into the fragment to avoid leaking info to the server. The widget
// spec on the other hand requires the widgetId and parentUrl to show up in the regular query string.
const widgetQuery = qs.parse(window.location.hash.substring(1));
const query = Object.assign({}, qs.parse(window.location.search.substring(1)), widgetQuery);
const qsParam = (name: string, optional = false): string => {
if (!optional && (!query[name] || typeof (query[name]) !== 'string')) {
throw new Error(`Expected singular ${name} in query string`);
}
return <string>query[name];
};
// Set this up as early as possible because Riot will be hitting it almost immediately.
widgetApi = new WidgetApi(qsParam('parentUrl'), qsParam('widgetId'), [
Capability.AlwaysOnScreen,
]);
widgetApi.waitReady().then(async () => {
// Start off by ensuring we're not stuck on screen
await widgetApi.setAlwaysOnScreen(false);
});
// Bootstrap ourselves for loading the script and such
preparePlatform();
await loadConfig();
// Populate the Jitsi params now
jitsiDomain = qsParam('conferenceDomain', true) || SdkConfig.get()['jitsi']['preferredDomain'];
conferenceId = qsParam('conferenceId');
displayName = qsParam('displayName', true);
avatarUrl = qsParam('avatarUrl', true); // http not mxc
userId = qsParam('userId');
// Get the Jitsi Meet API loaded up as fast as possible, but ensure that the widget's postMessage
// receiver (WidgetApi) is up and running first.
const scriptTag = document.createElement("script");
scriptTag.src = SdkConfig.get()['jitsi']['externalApiUrl'];
document.body.appendChild(scriptTag);
// TODO: register widgetApi listeners for PTT controls
document.getElementById("joinButton").onclick = () => joinConference();
} catch (e) {
console.error("Error setting up Jitsi widget", e);
document.getElementById("jitsiContainer").innerText = "Failed to load Jitsi widget";
switchVisibleContainers();
}
})();
function switchVisibleContainers() {
inConference = !inConference;
document.getElementById("jitsiContainer").style.visibility = inConference ? 'unset' : 'hidden';
document.getElementById("joinButtonContainer").style.visibility = inConference ? 'hidden' : 'unset';
}
function joinConference() { // event handler bound in HTML
switchVisibleContainers();
// noinspection JSIgnoredPromiseFromCall
widgetApi.setAlwaysOnScreen(true); // ignored promise because we don't care if it works
const meetApi = new JitsiMeetExternalAPI(jitsiDomain, {
width: "100%",
height: "100%",
parentNode: document.querySelector("#jitsiContainer"),
roomName: conferenceId,
interfaceConfigOverwrite: {
SHOW_JITSI_WATERMARK: false,
SHOW_WATERMARK_FOR_GUESTS: false,
MAIN_TOOLBAR_BUTTONS: [],
VIDEO_LAYOUT_FIT: "height",
},
});
if (displayName) meetApi.executeCommand("displayName", displayName);
if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl);
if (userId) meetApi.executeCommand("email", userId);
meetApi.on("readyToClose", () => {
switchVisibleContainers();
// noinspection JSIgnoredPromiseFromCall
widgetApi.setAlwaysOnScreen(false); // ignored promise because we don't care if it works
document.getElementById("jitsiContainer").innerHTML = "";
});
}