Stop reading URL parameters from inside the call path

The view models reached for getUrlParams() — and so window.location —
from deep inside the call path: CallViewModel, MediaDevices, Publisher,
LocalMember and the footer view model. An embedded Element Call has no URL
of its own, so these values have to arrive as arguments instead.

Add the relevant options to CallViewModelOptions, to the MediaDevices and
Publisher constructors, to createLocalMembership$ and enterRTCSession, and
to createCallFooterViewModel. The remaining React consumers read the
context added in the previous commit. AppViewModel now takes its audio
output options too, moving that URL read out to main.tsx, where the app
shell can act as the adapter.

The new CallViewModelOptions fields are optional, defaulting to what the
URL parameters resolve to outside widget mode; the MediaDevices and
Publisher arguments are required, so that every construction site has to
be explicit.

useTheme.test.ts mocked the UrlParams module with a factory, so it needed
updating to mock the hook rather than getUrlParams.

No functional change.
This commit is contained in:
Valere
2026-09-02 10:31:48 +02:00
parent bc6015eff2
commit 6e44332fd1
20 changed files with 176 additions and 65 deletions
+26 -5
View File
@@ -16,6 +16,7 @@ import {
} from "rxjs";
import { createMediaDeviceObserver } from "@livekit/components-core";
import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { type RTCCallIntent } from "matrix-js-sdk/lib/matrixrtc";
import {
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
@@ -25,7 +26,6 @@ import {
} from "../settings/settings";
import { type ObservableScope } from "./ObservableScope";
import { availableOutputDevices$ as controlledAvailableOutputDevices$ } from "../controls";
import { getUrlParams } from "../UrlParams";
import { platform } from "../Platform";
import { switchWhen } from "../utils/observable";
import { type Behavior, constant } from "./Behavior";
@@ -338,6 +338,22 @@ class VideoInput implements MediaDevice<DeviceLabel, SelectedDevice> {
}
}
/**
* How Element Call should manage audio output.
*/
export interface AudioOutputOptions {
/**
* Whether the list of output devices is controlled by the app hosting Element
* Call, through the global JS controls, rather than by the browser.
*/
controlledAudioDevices: boolean;
/**
* The kind of call being placed, which decides the initial output route when
* the host controls the devices.
*/
callIntent?: RTCCallIntent;
}
export class MediaDevices {
private readonly deviceNamesRequest$ = new Subject<void>();
/**
@@ -368,23 +384,28 @@ export class MediaDevices {
public readonly audioOutput: MediaDevice<
AudioOutputDeviceLabel,
SelectedAudioOutputDevice
> = getUrlParams().controlledAudioDevices
> = this.audioOutputOptions.controlledAudioDevices
? platform == "android"
? new AndroidControlledAudioOutput(
controlledAvailableOutputDevices$,
this.scope,
getUrlParams().callIntent,
this.audioOutputOptions.callIntent,
window.controls,
)
: new IOSControlledAudioOutput(
this.usingNames$,
this.scope,
getUrlParams().callIntent,
this.audioOutputOptions.callIntent,
)
: new AudioOutput(this.usingNames$, this.scope);
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
new VideoInput(this.usingNames$, this.scope);
public constructor(private readonly scope: ObservableScope) {}
// Note: both parameters are read by the field initializers above, which is
// safe because TypeScript assigns parameter properties before running them.
public constructor(
private readonly scope: ObservableScope,
private readonly audioOutputOptions: AudioOutputOptions,
) {}
}