diff --git a/sdk/main.ts b/sdk/main.ts index 4a7926a16..6347d1421 100644 --- a/sdk/main.ts +++ b/sdk/main.ts @@ -46,7 +46,10 @@ import { // Can this be done in the tsconfig.json import { type TextStreamInfo } from "../node_modules/livekit-client/dist/src/room/types"; import { type Behavior, constant } from "../src/state/Behavior"; -import { createCallViewModel$ } from "../src/state/CallViewModel/CallViewModel"; +import { + callViewModelOptionsFromParams, + createCallViewModel$, +} from "../src/state/CallViewModel/CallViewModel"; import { ObservableScope } from "../src/state/ObservableScope"; import { getUrlParams } from "../src/UrlParams"; import { MuteStates } from "../src/state/MuteStates"; @@ -113,7 +116,8 @@ export async function createMatrixRTCSdk( logger.info("client created"); // url params - const { roomId, controlledAudioDevices, callIntent } = getUrlParams(); + const urlParams = getUrlParams(); + const { roomId, controlledAudioDevices, callIntent } = urlParams; if (roomId === null) throw Error("could not get roomId from url params"); const room = client.getRoom(roomId); if (room === null) throw Error("could not get room from client"); @@ -144,10 +148,9 @@ export async function createMatrixRTCSdk( mediaDevices, muteStates, { + ...callViewModelOptionsFromParams(urlParams), encryptionSystem: { kind: E2eeType.PER_PARTICIPANT }, hostBridge, - controlledAudioDevices, - callIntent, }, of({}), of({}), diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index ef1e1f988..97fb75438 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -41,6 +41,7 @@ import { type MatrixInfo } from "./VideoPreview"; import { InviteButton } from "../button/InviteButton"; import { type CallViewModel, + callViewModelOptionsFromParams, createCallViewModel$, } from "../state/CallViewModel/CallViewModel.ts"; import { Grid, type TileProps } from "../grid/Grid"; @@ -123,16 +124,8 @@ export const ActiveCall: FC = (props) => { rootLogger.info("START CALL VIEW SCOPE"); const scope = new ObservableScope(); const reactionsReader = new ReactionsReader(scope, props.rtcSession); - const { - autoLeaveWhenOthersLeft, - waitForCallPickup, - sendNotificationType, - controlledAudioDevices, - header, - showControls, - hideScreensharing, - callIntent, - } = urlParams; + const { autoLeaveWhenOthersLeft, waitForCallPickup, sendNotificationType } = + urlParams; const vm = createCallViewModel$( scope, @@ -141,17 +134,12 @@ export const ActiveCall: FC = (props) => { mediaDevices, props.muteStates, { + ...callViewModelOptionsFromParams(urlParams), encryptionSystem: props.e2eeSystem, hostBridge, autoLeaveWhenOthersLeft, waitForCallPickup: waitForCallPickup && sendNotificationType === "ring", matrixRTCMode$: matrixRTCModeSetting.value$, - controlledAudioDevices, - header, - showControls, - hideScreensharing, - sendNotificationType, - callIntent, }, reactionsReader.raisedHands$, reactionsReader.reactions$, diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index 7a27fd3e0..c7783d152 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -68,6 +68,8 @@ import { } from "./CallViewModelTestUtils.ts"; import { MatrixRTCMode } from "../../config/ConfigOptions.ts"; import { initializeWidget } from "../../widget.ts"; +import { computeUrlParams } from "../../UrlParams.ts"; +import { callViewModelOptionsFromParams } from "./CallViewModel.ts"; initializeWidget(); @@ -83,9 +85,6 @@ vi.mock("livekit-client/e2ee-worker?worker"); vi.mock("../e2ee/matrixKeyProvider"); -const getUrlParams = vi.hoisted(() => vi.fn(() => ({}))); -vi.mock("../UrlParams", () => ({ getUrlParams })); - const getPlatform = vi.hoisted(() => vi.fn(() => "desktop")); vi.mock("../../Platform", () => ({ get platform(): string { @@ -1701,3 +1700,42 @@ describe.each(modes)("CallViewModel (%s mode)", (mode) => { }); }); }); + +describe("callViewModelOptionsFromParams", () => { + // The defaults on CallViewModelOptions describe a standalone Element Call, so + // a widget caller that drops one of these gets standalone behaviour rather + // than an error. These check the whole chain from URL to options, which is + // where that went wrong for the SDK. + const widgetUrl = (extra: string): string => + `#?widgetId=id&parentUrl=${encodeURIComponent("http://parent")}&${extra}`; + + it("carries an explicitly requested notification type", () => { + const params = computeUrlParams("", widgetUrl("sendNotificationType=ring")); + expect(callViewModelOptionsFromParams(params).sendNotificationType).toBe( + "ring", + ); + }); + + it("carries the notification type an intent implies", () => { + const params = computeUrlParams("", widgetUrl("intent=start_call_dm")); + expect(callViewModelOptionsFromParams(params).sendNotificationType).toBe( + "ring", + ); + }); + + it("carries hideScreensharing", () => { + const params = computeUrlParams("", widgetUrl("hideScreensharing=true")); + expect(callViewModelOptionsFromParams(params).hideScreensharing).toBe(true); + }); + + it("carries controlledAudioDevices and the call intent", () => { + const params = computeUrlParams( + "", + widgetUrl("controlledAudioDevices=true&intent=start_call_voice"), + ); + expect(callViewModelOptionsFromParams(params)).toMatchObject({ + controlledAudioDevices: true, + callIntent: "audio", + }); + }); +}); diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index b40a45231..85ba27573 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -86,7 +86,7 @@ import { constant, type Behavior } from "../Behavior"; import { E2eeType } from "../../e2ee/e2eeType"; import { MatrixKeyProvider } from "../../e2ee/matrixKeyProvider"; import { type MuteStates } from "../MuteStates"; -import { HeaderStyle } from "../../UrlParams"; +import { HeaderStyle, type UrlParams } from "../../UrlParams"; import { type ProcessorState } from "../../livekit/TrackProcessorContext"; import { type HostBridge, nullHostBridge } from "../../HostBridge"; import { @@ -216,6 +216,40 @@ export interface CallViewModelOptions { toggleScreensharing?: () => void; } +/** + * The options {@link createCallViewModel$} takes from the parameters Element + * Call was started with. + * + * Callers share this rather than picking the fields out themselves. The + * defaults on {@link CallViewModelOptions} describe a standalone Element Call, + * so a widget or embedded caller that misses one does not get an error — it + * quietly gets standalone behaviour instead. + * + * Note `autoLeaveWhenOthersLeft` and `waitForCallPickup` are deliberately not + * here: unlike these, the view model never read them from the parameters + * itself, so they remain the caller's decision. + */ +export function callViewModelOptionsFromParams( + params: UrlParams, +): Pick< + CallViewModelOptions, + | "controlledAudioDevices" + | "header" + | "showControls" + | "hideScreensharing" + | "sendNotificationType" + | "callIntent" +> { + return { + controlledAudioDevices: params.controlledAudioDevices, + header: params.header, + showControls: params.showControls, + hideScreensharing: params.hideScreensharing, + sendNotificationType: params.sendNotificationType, + callIntent: params.callIntent, + }; +} + // Do not play any sounds if the participant count has exceeded this // number. export const MAX_PARTICIPANT_COUNT_FOR_SOUND = 8;