2022-09-09 14:08:17 +08:00
|
|
|
/*
|
|
|
|
Copyright 2022 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
|
|
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
|
|
|
import { createRoomWidgetClient } from "matrix-js-sdk/src/matrix";
|
|
|
|
import { WidgetApi, MatrixCapabilities } from "matrix-widget-api";
|
|
|
|
|
|
|
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
|
|
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
|
|
|
import { LazyEventEmitter } from "./LazyEventEmitter";
|
2022-10-10 21:19:10 +08:00
|
|
|
import { getUrlParams } from "./UrlParams";
|
2023-07-10 20:36:07 +08:00
|
|
|
import { Config } from "./config/Config";
|
2022-09-09 14:08:17 +08:00
|
|
|
|
|
|
|
// Subset of the actions in matrix-react-sdk
|
|
|
|
export enum ElementWidgetActions {
|
|
|
|
JoinCall = "io.element.join",
|
|
|
|
HangupCall = "im.vector.hangup",
|
|
|
|
TileLayout = "io.element.tile_layout",
|
|
|
|
SpotlightLayout = "io.element.spotlight_layout",
|
2022-10-22 03:19:52 +08:00
|
|
|
|
|
|
|
// Element Call -> host requesting to start a screenshare
|
|
|
|
// (ie. expects a ScreenshareStart once the user has picked a source)
|
|
|
|
// Element Call -> host requesting to start a screenshare
|
|
|
|
// (ie. expects a ScreenshareStart once the user has picked a source)
|
|
|
|
// replies with { pending } where pending is true if the host has asked
|
|
|
|
// the user to choose a window and false if not (ie. if the host isn't
|
|
|
|
// running within Electron)
|
|
|
|
ScreenshareRequest = "io.element.screenshare_request",
|
|
|
|
// host -> Element Call telling EC to start screen sharing with
|
|
|
|
// the given source
|
|
|
|
ScreenshareStart = "io.element.screenshare_start",
|
|
|
|
// host -> Element Call telling EC to stop screen sharing, or that
|
|
|
|
// the user cancelled when selecting a source after a ScreenshareRequest
|
|
|
|
ScreenshareStop = "io.element.screenshare_stop",
|
2022-09-09 14:08:17 +08:00
|
|
|
}
|
|
|
|
|
2023-06-24 02:17:51 +08:00
|
|
|
export interface JoinCallData {
|
|
|
|
audioInput: string | null;
|
|
|
|
videoInput: string | null;
|
|
|
|
}
|
|
|
|
|
2022-10-22 03:19:52 +08:00
|
|
|
export interface ScreenshareStartData {
|
|
|
|
desktopCapturerSourceId: string;
|
|
|
|
}
|
|
|
|
|
2022-09-09 14:08:17 +08:00
|
|
|
interface WidgetHelpers {
|
|
|
|
api: WidgetApi;
|
|
|
|
lazyActions: LazyEventEmitter;
|
|
|
|
client: Promise<MatrixClient>;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A point of access to the widget API, if the app is running as a widget. This
|
|
|
|
* is declared and initialized on the top level because the widget messaging
|
|
|
|
* needs to be set up ASAP on load to ensure it doesn't miss any requests.
|
|
|
|
*/
|
|
|
|
export const widget: WidgetHelpers | null = (() => {
|
|
|
|
try {
|
|
|
|
const query = new URLSearchParams(window.location.search);
|
|
|
|
const widgetId = query.get("widgetId");
|
|
|
|
const parentUrl = query.get("parentUrl");
|
|
|
|
|
|
|
|
if (widgetId && parentUrl) {
|
|
|
|
const parentOrigin = new URL(parentUrl).origin;
|
|
|
|
logger.info("Widget API is available");
|
|
|
|
const api = new WidgetApi(widgetId, parentOrigin);
|
|
|
|
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
|
|
|
|
|
|
|
|
// Set up the lazy action emitter, but only for select actions that we
|
|
|
|
// intend for the app to handle
|
|
|
|
const lazyActions = new LazyEventEmitter();
|
|
|
|
[
|
|
|
|
ElementWidgetActions.JoinCall,
|
|
|
|
ElementWidgetActions.HangupCall,
|
|
|
|
ElementWidgetActions.TileLayout,
|
|
|
|
ElementWidgetActions.SpotlightLayout,
|
2022-10-22 03:19:52 +08:00
|
|
|
ElementWidgetActions.ScreenshareStart,
|
|
|
|
ElementWidgetActions.ScreenshareStop,
|
2022-09-09 14:08:17 +08:00
|
|
|
].forEach((action) => {
|
|
|
|
api.on(`action:${action}`, (ev: CustomEvent<IWidgetApiRequest>) => {
|
|
|
|
ev.preventDefault();
|
|
|
|
lazyActions.emit(action, ev);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// Now, initialize the matryoshka MatrixClient (so named because it routes
|
|
|
|
// all requests through the host client via the widget API)
|
|
|
|
// We need to do this now rather than later because it has capabilities to
|
|
|
|
// request, and is responsible for starting the transport (should it be?)
|
|
|
|
|
2023-06-06 03:52:05 +08:00
|
|
|
const {
|
|
|
|
roomId,
|
|
|
|
userId,
|
|
|
|
deviceId,
|
|
|
|
baseUrl,
|
|
|
|
e2eEnabled,
|
|
|
|
allowIceFallback,
|
|
|
|
} = getUrlParams();
|
2022-09-09 14:08:17 +08:00
|
|
|
if (!roomId) throw new Error("Room ID must be supplied");
|
|
|
|
if (!userId) throw new Error("User ID must be supplied");
|
|
|
|
if (!deviceId) throw new Error("Device ID must be supplied");
|
2022-10-17 21:30:22 +08:00
|
|
|
if (!baseUrl) throw new Error("Base URL must be supplied");
|
2022-09-09 14:08:17 +08:00
|
|
|
|
|
|
|
// These are all the event types the app uses
|
2022-10-25 00:30:30 +08:00
|
|
|
const sendRecvEvent = ["org.matrix.rageshake_request"];
|
2022-09-09 14:08:17 +08:00
|
|
|
const sendState = [
|
|
|
|
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
|
|
|
|
];
|
|
|
|
const receiveState = [
|
|
|
|
{ eventType: EventType.RoomMember },
|
|
|
|
{ eventType: EventType.GroupCallPrefix },
|
|
|
|
{ eventType: EventType.GroupCallMemberPrefix },
|
|
|
|
];
|
|
|
|
const sendRecvToDevice = [
|
|
|
|
EventType.CallInvite,
|
|
|
|
EventType.CallCandidates,
|
|
|
|
EventType.CallAnswer,
|
|
|
|
EventType.CallHangup,
|
|
|
|
EventType.CallReject,
|
|
|
|
EventType.CallSelectAnswer,
|
|
|
|
EventType.CallNegotiate,
|
|
|
|
EventType.CallSDPStreamMetadataChanged,
|
|
|
|
EventType.CallSDPStreamMetadataChangedPrefix,
|
|
|
|
EventType.CallReplaces,
|
|
|
|
];
|
|
|
|
|
|
|
|
const client = createRoomWidgetClient(
|
|
|
|
api,
|
|
|
|
{
|
2022-10-25 00:30:30 +08:00
|
|
|
sendEvent: sendRecvEvent,
|
|
|
|
receiveEvent: sendRecvEvent,
|
2022-09-09 14:08:17 +08:00
|
|
|
sendState,
|
|
|
|
receiveState,
|
|
|
|
sendToDevice: sendRecvToDevice,
|
|
|
|
receiveToDevice: sendRecvToDevice,
|
|
|
|
turnServers: true,
|
|
|
|
},
|
|
|
|
roomId,
|
|
|
|
{
|
2022-10-17 13:46:44 +08:00
|
|
|
baseUrl,
|
2022-09-09 14:08:17 +08:00
|
|
|
userId,
|
|
|
|
deviceId,
|
|
|
|
timelineSupport: true,
|
2023-03-01 21:30:25 +08:00
|
|
|
useE2eForGroupCall: e2eEnabled,
|
2023-06-06 03:52:05 +08:00
|
|
|
fallbackICEServerAllowed: allowIceFallback,
|
2023-07-10 20:36:07 +08:00
|
|
|
// XXX: The client expects the list of foci in its constructor, but we don't
|
|
|
|
// know this until we fetch the config file. However, we can't wait to construct
|
|
|
|
// the client object or we'll miss the 'capabilities' request from the host app.
|
|
|
|
// As of writing this, I have made the embedded widget client send the 'contentLoaded'
|
|
|
|
// message so that we can use the widget API in less racy mode, but we need to change
|
|
|
|
// element-web to use waitForIFrameLoad=false. Once that change has rolled out,
|
|
|
|
// we can just start the client after we've fetched the config.
|
|
|
|
foci: [],
|
2022-09-09 14:08:17 +08:00
|
|
|
}
|
|
|
|
);
|
2023-07-10 20:36:07 +08:00
|
|
|
|
2023-07-10 22:54:04 +08:00
|
|
|
const clientPromise = new Promise<MatrixClient>((resolve) => {
|
2023-07-10 20:36:07 +08:00
|
|
|
(async () => {
|
|
|
|
await Config.init();
|
|
|
|
const livekit = Config.get().livekit;
|
|
|
|
const focus = livekit?.livekit_service_url;
|
|
|
|
// Now we've fetched the config, be evil and use the getter to inject the focus
|
|
|
|
// into the client (see above XXX).
|
|
|
|
if (focus) {
|
|
|
|
client.getFoci().push({
|
|
|
|
livekitServiceUrl: livekit.livekit_service_url,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
await client.startClient();
|
|
|
|
resolve(client);
|
|
|
|
})();
|
|
|
|
});
|
2022-09-09 14:08:17 +08:00
|
|
|
|
|
|
|
return { api, lazyActions, client: clientPromise };
|
|
|
|
} else {
|
|
|
|
logger.info("No widget API available");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
logger.warn("Continuing without the widget API", e);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
})();
|