mirror of
https://github.com/vector-im/element-call.git
synced 2026-06-30 18:02:56 +00:00
WIP on audio route experiment
This commit is contained in:
@@ -196,6 +196,7 @@ export interface UrlConfiguration {
|
||||
* Element Call.
|
||||
*/
|
||||
controlledAudioDevices: boolean;
|
||||
audioInputOutputSelection: boolean;
|
||||
/**
|
||||
* Setting this flag skips the lobby and brings you in the call directly.
|
||||
* In the widget this can be combined with preload to pass the device settings
|
||||
@@ -372,6 +373,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||
allowIceFallback: true,
|
||||
perParticipantE2EE: true,
|
||||
controlledAudioDevices: platform === "desktop" ? false : true,
|
||||
audioInputOutputSelection: platform !== "ios",
|
||||
skipLobby: true,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: "notification",
|
||||
@@ -427,6 +429,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => {
|
||||
allowIceFallback: false,
|
||||
perParticipantE2EE: false,
|
||||
controlledAudioDevices: false,
|
||||
audioInputOutputSelection: true,
|
||||
skipLobby: false,
|
||||
returnToLobby: false,
|
||||
sendNotificationType: undefined,
|
||||
|
||||
60
src/button/AudioRouteButton.tsx
Normal file
60
src/button/AudioRouteButton.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Tooltip } from "@vector-im/compound-web";
|
||||
import {
|
||||
EarpieceIcon,
|
||||
HeadphonesSolidIcon,
|
||||
VolumeOnSolidIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import type { ComponentPropsWithoutRef, FC } from "react";
|
||||
import { RouteType } from "../controls.ts";
|
||||
|
||||
interface AudioRouteButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
size?: "md" | "lg";
|
||||
routeType: RouteType;
|
||||
}
|
||||
|
||||
export const AudioRouteButton: FC<AudioRouteButtonProps> = ({
|
||||
routeType,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
let label: string
|
||||
let icon;
|
||||
switch(routeType) {
|
||||
case RouteType.speaker:
|
||||
label = t("settings.devices.loudspeaker")
|
||||
icon = VolumeOnSolidIcon;
|
||||
break;
|
||||
case RouteType.phone:
|
||||
label = t("settings.devices.handset");
|
||||
icon = EarpieceIcon
|
||||
break;
|
||||
case RouteType.bluetooth:
|
||||
label = "bluetooth headset";
|
||||
icon = HeadphonesSolidIcon;
|
||||
break;
|
||||
case RouteType.wired:
|
||||
label = "headset";
|
||||
icon = HeadphonesSolidIcon;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label={label}>
|
||||
<Button
|
||||
iconOnly
|
||||
Icon={icon}
|
||||
{...props}
|
||||
kind={"primary"}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
MediaMuteAndSwitchButton,
|
||||
type MenuOptions,
|
||||
} from "./MediaMuteAndSwitchButton";
|
||||
import { type AudioRoute, RouteType } from "../controls.ts";
|
||||
|
||||
export interface AudioOutputSwitcher {
|
||||
targetOutput: string;
|
||||
@@ -90,6 +91,8 @@ export interface FooterProps {
|
||||
selectedVideo?: string;
|
||||
selectAudioDevice?: (deviceId: string) => void;
|
||||
selectVideoDevice?: (deviceId: string) => void;
|
||||
|
||||
nativeAudioRoute?: { targetOutput: AudioRoute , switch: () => void };
|
||||
}
|
||||
|
||||
export const CallFooter: FC<FooterProps> = ({
|
||||
@@ -112,6 +115,7 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
reactionIdentifier,
|
||||
reactionData,
|
||||
audioOutputSwitcher,
|
||||
nativeAudioRoute,
|
||||
hangup,
|
||||
debugTileLayout,
|
||||
tileStoreGeneration,
|
||||
@@ -228,15 +232,30 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
|
||||
// In this PR we just move the button to the bottom bar. We do not yet update its appearance
|
||||
const audioOutputButton = useMemo(() => {
|
||||
if (audioOutputSwitcher === undefined) return null;
|
||||
return (
|
||||
<LoudspeakerButton
|
||||
size={buttonSize}
|
||||
onClick={() => audioOutputSwitcher.switch()}
|
||||
loudspeakerModeEnabled={audioOutputSwitcher.targetOutput === "earpiece"}
|
||||
/>
|
||||
);
|
||||
}, [audioOutputSwitcher, buttonSize]);
|
||||
|
||||
if (nativeAudioRoute) {
|
||||
return (
|
||||
// TODO make a 4 state button to also include the headset option when supported by the OS
|
||||
<LoudspeakerButton
|
||||
size={buttonSize}
|
||||
onClick={() => nativeAudioRoute.switch()}
|
||||
loudspeakerModeEnabled={nativeAudioRoute.targetOutput.type === RouteType.speaker}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
if (audioOutputSwitcher === undefined) return null;
|
||||
return (
|
||||
<LoudspeakerButton
|
||||
size={buttonSize}
|
||||
onClick={() => audioOutputSwitcher.switch()}
|
||||
loudspeakerModeEnabled={
|
||||
audioOutputSwitcher.targetOutput === "earpiece"
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [audioOutputSwitcher, buttonSize, nativeAudioRoute]);
|
||||
|
||||
|
||||
if (audioOutputButton) buttons.push(audioOutputButton);
|
||||
|
||||
|
||||
@@ -8,6 +8,18 @@ Please see LICENSE in the repository root for full details.
|
||||
import { Subject } from "rxjs";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
export enum RouteType {
|
||||
speaker = "speaker",
|
||||
phone = "phone",
|
||||
bluetooth = "bluetooth",
|
||||
wired = "wired",
|
||||
}
|
||||
|
||||
export interface AudioRoute {
|
||||
type: RouteType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface Controls {
|
||||
canEnterPip(): boolean;
|
||||
enablePip(): void;
|
||||
@@ -31,6 +43,9 @@ export interface Controls {
|
||||
setOutputEnabled(enabled: boolean): void;
|
||||
/** @deprecated use showNativeAudioDevicePicker instead*/
|
||||
showNativeOutputDevicePicker?: () => void;
|
||||
|
||||
/** iOS native controlled device selection */
|
||||
onNativeRouteChanged(route: AudioRoute): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +108,8 @@ export const outputDevice$ = new Subject<string>();
|
||||
*/
|
||||
export const setAudioEnabled$ = new Subject<boolean>();
|
||||
|
||||
export const currentRoute$ = new Subject<AudioRoute | null>();
|
||||
|
||||
let playbackStartedEmitted = false;
|
||||
export const setPlaybackStarted = (): void => {
|
||||
if (!playbackStartedEmitted) {
|
||||
@@ -101,6 +118,10 @@ export const setPlaybackStarted = (): void => {
|
||||
}
|
||||
};
|
||||
|
||||
export const showNativeAudioDevicePicker = (): void => {
|
||||
window.controls.showNativeAudioDevicePicker?.();
|
||||
};
|
||||
|
||||
window.controls = {
|
||||
canEnterPip(): boolean {
|
||||
return setPipEnabled$.observed;
|
||||
@@ -156,6 +177,14 @@ window.controls = {
|
||||
setAudioEnabled$.next(enabled);
|
||||
},
|
||||
|
||||
onNativeRouteChanged(route: AudioRoute): void {
|
||||
logger.info(
|
||||
"[MediaDevices controls] onNativeRouteChanged called from native",
|
||||
route,
|
||||
);
|
||||
currentRoute$.next(route);
|
||||
},
|
||||
|
||||
// wrappers for the deprecated controls fields
|
||||
setOutputEnabled(enabled: boolean): void {
|
||||
this.setAudioEnabled(enabled);
|
||||
|
||||
@@ -31,7 +31,7 @@ window.setLKLogLevel = setLKLogLevel;
|
||||
initRageshake().catch((e) => {
|
||||
logger.error("Failed to initialize rageshake", e);
|
||||
});
|
||||
setLKLogLevel("info");
|
||||
setLKLogLevel("trace");
|
||||
setLKLogExtension((level, msg, context) => {
|
||||
// we pass a synthetic logger name of "livekit" to the rageshake to make it easier to read
|
||||
global.mx_rage_logger.log(level, "livekit", msg, context);
|
||||
|
||||
@@ -248,6 +248,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const showFooter = useBehavior(vm.showFooter$);
|
||||
const earpieceMode = useBehavior(vm.earpieceMode$);
|
||||
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
||||
const nativeAudioRoute = useBehavior(vm.nativeAudioRouteSwitcher$);
|
||||
const sharingScreen = useBehavior(vm.sharingScreen$);
|
||||
|
||||
const fatalCallError = useBehavior(vm.fatalError$);
|
||||
@@ -590,6 +591,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
reactionIdentifier={`${client.getUserId()}:${client.getDeviceId()}`}
|
||||
reactionData={supportsReactions ? vm : undefined}
|
||||
audioOutputSwitcher={audioOutputSwitcher ?? undefined}
|
||||
nativeAudioRoute = { nativeAudioRoute ?? undefined}
|
||||
// Only pass the openSettings function if the settings button is not in the app bar.
|
||||
// If there is no fn the button will be hidden in the footer.
|
||||
openSettings={settingsButtonInAppBar ? undefined : openSettings}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "livekit-client";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { map, startWith } from "rxjs";
|
||||
|
||||
import inCallStyles from "./InCallView.module.css";
|
||||
import styles from "./LobbyView.module.css";
|
||||
@@ -48,6 +49,14 @@ import { getValue } from "../utils/observable";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
import { CallFooter } from "../components/CallFooter";
|
||||
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
|
||||
import {
|
||||
type AudioRoute,
|
||||
currentRoute$,
|
||||
RouteType,
|
||||
showNativeAudioDevicePicker,
|
||||
} from "../controls.ts";
|
||||
import { ObservableScope } from "../state/ObservableScope.ts";
|
||||
import { startsWith } from "lodash-es";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
@@ -184,6 +193,39 @@ export const LobbyView: FC<Props> = ({
|
||||
|
||||
useTrackProcessorSync(videoTrack);
|
||||
|
||||
const [nativeAudioRoute, setNativeAudioRoute] = useState<{
|
||||
targetOutput: AudioRoute
|
||||
switch: () => void
|
||||
} | null>();
|
||||
useEffect(() => {
|
||||
const scope = new ObservableScope();
|
||||
const nativeAudioRouteSwitcher$ = scope.behavior<{
|
||||
targetOutput: AudioRoute;
|
||||
switch: () => void;
|
||||
} | null>(
|
||||
currentRoute$.pipe(
|
||||
map((route) => {
|
||||
return {
|
||||
targetOutput: route || { type: RouteType.speaker, label: "" },
|
||||
switch: (): void => {
|
||||
showNativeAudioDevicePicker?.();
|
||||
},
|
||||
};
|
||||
}),
|
||||
startWith(null),
|
||||
),
|
||||
);
|
||||
|
||||
nativeAudioRouteSwitcher$.subscribe((route) => {
|
||||
setNativeAudioRoute(route);
|
||||
});
|
||||
return (): void => {
|
||||
scope.end();
|
||||
};
|
||||
}, [setNativeAudioRoute]);
|
||||
|
||||
|
||||
|
||||
// TODO: Unify this component with InCallView, so we can get slick joining
|
||||
// animations and don't have to feel bad about reusing its CSS
|
||||
return (
|
||||
@@ -234,6 +276,7 @@ export const LobbyView: FC<Props> = ({
|
||||
toggleVideo={toggleVideo ?? undefined}
|
||||
openSettings={openSettings}
|
||||
hangup={!confineToRoom ? onLeaveClick : undefined}
|
||||
nativeAudioRoute={nativeAudioRoute ?? undefined}
|
||||
// Logo and header are connected. We will only show the logo in SPA with header.
|
||||
hideLogo={hideHeader}
|
||||
>
|
||||
|
||||
@@ -41,9 +41,9 @@ import {
|
||||
} from "rxjs";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
import {
|
||||
MembershipManagerEvent,
|
||||
type LivekitTransportConfig,
|
||||
type MatrixRTCSession,
|
||||
MembershipManagerEvent,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { type IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||
@@ -64,7 +64,14 @@ import {
|
||||
showReactions,
|
||||
} from "../../settings/settings";
|
||||
import { isFirefox, platform } from "../../Platform";
|
||||
import { setPipEnabled$ } from "../../controls";
|
||||
import {
|
||||
type AudioRoute,
|
||||
currentRoute$,
|
||||
RouteType,
|
||||
setPipEnabled$,
|
||||
showNativeAudioDevicePicker,
|
||||
showNativeAudioDevicePicker$,
|
||||
} from "../../controls";
|
||||
import { TileStore } from "../TileStore";
|
||||
import { gridLikeLayout } from "../GridLikeLayout";
|
||||
import { spotlightExpandedLayout } from "../SpotlightExpandedLayout";
|
||||
@@ -78,7 +85,7 @@ import {
|
||||
} from "../../reactions";
|
||||
import { shallowEquals } from "../../utils/array";
|
||||
import { type MediaDevices } from "../MediaDevices";
|
||||
import { constant, type Behavior } from "../Behavior";
|
||||
import { type Behavior, constant } from "../Behavior";
|
||||
import { E2eeType } from "../../e2ee/e2eeType";
|
||||
import { MatrixKeyProvider } from "../../e2ee/matrixKeyProvider";
|
||||
import { type MuteStates } from "../MuteStates";
|
||||
@@ -122,8 +129,8 @@ import {
|
||||
import {
|
||||
createMatrixLivekitMembers$,
|
||||
type LocalMatrixLivekitMember,
|
||||
type RemoteMatrixLivekitMember,
|
||||
type MatrixLivekitMember,
|
||||
type RemoteMatrixLivekitMember,
|
||||
} from "./remoteMembers/MatrixLivekitMembers.ts";
|
||||
import {
|
||||
type AutoLeaveReason,
|
||||
@@ -368,6 +375,14 @@ export interface CallViewModel {
|
||||
switch: () => void;
|
||||
} | null>;
|
||||
|
||||
/**
|
||||
* Use when in native controlled route mode
|
||||
*/
|
||||
nativeAudioRouteSwitcher$: Behavior<{
|
||||
targetOutput: AudioRoute;
|
||||
switch: () => void;
|
||||
} | null>;
|
||||
|
||||
/**
|
||||
* Whether the app is currently reconnecting to the LiveKit server and/or setting the matrix rtc room state.
|
||||
*/
|
||||
@@ -1435,6 +1450,23 @@ export function createCallViewModel$(
|
||||
),
|
||||
);
|
||||
|
||||
const nativeAudioRouteSwitcher$ = scope.behavior<{
|
||||
targetOutput: AudioRoute;
|
||||
switch: () => void;
|
||||
} | null>(
|
||||
currentRoute$.pipe(
|
||||
map((route) => {
|
||||
return {
|
||||
targetOutput: route || { type: RouteType.speaker, label: "" },
|
||||
switch: (): void => {
|
||||
showNativeAudioDevicePicker?.();
|
||||
},
|
||||
};
|
||||
}),
|
||||
startWith(null),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Emits an array of reactions that should be visible on the screen.
|
||||
*/
|
||||
@@ -1624,6 +1656,7 @@ export function createCallViewModel$(
|
||||
showFooter$: showFooter$,
|
||||
earpieceMode$: earpieceMode$,
|
||||
audioOutputSwitcher$: audioOutputSwitcher$,
|
||||
nativeAudioRouteSwitcher$: nativeAudioRouteSwitcher$,
|
||||
reconnecting$: localMembership.reconnecting$,
|
||||
livekitRoomItems$,
|
||||
connected$: localMembership.connected$,
|
||||
|
||||
@@ -64,7 +64,8 @@ export class Publisher {
|
||||
trackerProcessorState$: Behavior<ProcessorState>,
|
||||
private logger: Logger,
|
||||
) {
|
||||
const { controlledAudioDevices } = getUrlParams();
|
||||
const { controlledAudioDevices, audioInputOutputSelection } =
|
||||
getUrlParams();
|
||||
const room = connection.livekitRoom;
|
||||
|
||||
room.setE2EEEnabled(room.options.e2ee !== undefined)?.catch((e: Error) => {
|
||||
@@ -74,7 +75,12 @@ export class Publisher {
|
||||
// Setup track processor syncing (blur)
|
||||
this.observeTrackProcessors(this.scope, room, trackerProcessorState$);
|
||||
// Observe media device changes and update LiveKit active devices accordingly
|
||||
this.observeMediaDevices(this.scope, devices, controlledAudioDevices);
|
||||
this.observeMediaDevices(
|
||||
this.scope,
|
||||
devices,
|
||||
controlledAudioDevices,
|
||||
audioInputOutputSelection,
|
||||
);
|
||||
|
||||
this.workaroundRestartAudioInputTrackChrome(devices, this.scope);
|
||||
|
||||
@@ -304,7 +310,7 @@ export class Publisher {
|
||||
// the process of being restarted.
|
||||
activeMicTrack.mediaStreamTrack.readyState !== "ended"
|
||||
) {
|
||||
this.logger?.info(
|
||||
this.logger.info(
|
||||
"Restarting audio device track due to active media device changed (workaroundRestartAudioInputTrackChrome)",
|
||||
);
|
||||
// Restart the track, which will cause Livekit to do another
|
||||
@@ -326,14 +332,18 @@ export class Publisher {
|
||||
scope: ObservableScope,
|
||||
devices: MediaDevices,
|
||||
controlledAudioDevices: boolean,
|
||||
audioInputOutputSelection: boolean,
|
||||
): void {
|
||||
const lkRoom = this.connection.livekitRoom;
|
||||
const syncDevice = (
|
||||
kind: MediaDeviceKind,
|
||||
selected$: Observable<SelectedDevice | undefined>,
|
||||
): Subscription =>
|
||||
selected$.pipe(scope.bind()).subscribe((device) => {
|
||||
): Subscription => {
|
||||
return selected$.pipe(scope.bind()).subscribe((device) => {
|
||||
if (lkRoom.state != LivekitConnectionState.Connected) return;
|
||||
this.logger.info(
|
||||
`Selection change for kind: ${kind} selected is ${device?.id}`,
|
||||
);
|
||||
// if (this.connectionState$.value !== ConnectionState.Connected) return;
|
||||
this.logger.info(
|
||||
"[LivekitRoom] syncDevice room.getActiveDevice(kind) !== d.id :",
|
||||
@@ -355,10 +365,15 @@ export class Publisher {
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
syncDevice("audioinput", devices.audioInput.selected$);
|
||||
if (!controlledAudioDevices)
|
||||
syncDevice("audiooutput", devices.audioOutput.selected$);
|
||||
this.logger.debug(`Syncing initial devices with LiveKit, controlledAudioDevices: ${controlledAudioDevices}, audioInputOutputSelection: ${audioInputOutputSelection}`);
|
||||
if (audioInputOutputSelection) {
|
||||
syncDevice("audioinput", devices.audioInput.selected$);
|
||||
if (!controlledAudioDevices) {
|
||||
syncDevice("audiooutput", devices.audioOutput.selected$);
|
||||
}
|
||||
}
|
||||
syncDevice("videoinput", devices.videoInput.selected$);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export class IOSControlledAudioOutput implements MediaDevice<
|
||||
combineLatest(
|
||||
[controlledAvailableOutputDevices$.pipe(startWith([])), iosDeviceMenu$],
|
||||
(availableRaw, iosDeviceMenu) => {
|
||||
this.logger.debug("Available device update ", availableRaw);
|
||||
const available = new Map<string, AudioOutputDeviceLabel>(
|
||||
availableRaw.map(
|
||||
({ id, name, isEarpiece, isSpeaker /*,isExternalHeadset*/ }) => {
|
||||
|
||||
57
src/state/IOSNativeControlledAudioOutput.ts
Normal file
57
src/state/IOSNativeControlledAudioOutput.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2026 Element Corp.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
import { map, startWith } from "rxjs";
|
||||
|
||||
import { currentRoute$, type Controls } from "../controls.ts";
|
||||
import { type Behavior, constant } from "./Behavior.ts";
|
||||
import type { ObservableScope } from "./ObservableScope.ts";
|
||||
import {
|
||||
type AudioOutputDeviceLabel,
|
||||
type MediaDevice,
|
||||
type SelectedAudioOutputDevice,
|
||||
} from "./MediaDevices.ts";
|
||||
|
||||
/**
|
||||
* A special implementation of audio output that allows the hosting application
|
||||
* to have more control over the device selection process. This is used when the
|
||||
* `controlledAudioDevices` URL parameter is set, which is currently only true on mobile.
|
||||
*/
|
||||
export class IOSNativeControlledAudioOutput implements MediaDevice<
|
||||
AudioOutputDeviceLabel,
|
||||
SelectedAudioOutputDevice
|
||||
> {
|
||||
private logger = rootLogger.getChild(
|
||||
"[MediaDevices IOSNativeControlledAudioOutput]",
|
||||
);
|
||||
|
||||
public readonly available$: Behavior<Map<string, AudioOutputDeviceLabel>> =
|
||||
constant(new Map());
|
||||
|
||||
public select(id: string): void {
|
||||
this.controls.showNativeAudioDevicePicker?.();
|
||||
}
|
||||
|
||||
public readonly selected$ = this.scope.behavior(
|
||||
currentRoute$.pipe(
|
||||
startWith(null),
|
||||
map((route) => {
|
||||
if (!route) return undefined;
|
||||
this.logger.debug(`Selected ${route?.type}`);
|
||||
return {
|
||||
id: route.type,
|
||||
} as SelectedAudioOutputDevice;
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
public constructor(
|
||||
private readonly controls: Controls,
|
||||
private readonly scope: ObservableScope,
|
||||
) {}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import { platform } from "../Platform";
|
||||
import { switchWhen } from "../utils/observable";
|
||||
import { type Behavior, constant } from "./Behavior";
|
||||
import { AndroidControlledAudioOutput } from "./AndroidControlledAudioOutput.ts";
|
||||
import { IOSControlledAudioOutput } from "./IOSControlledAudioOutput.ts";
|
||||
import { IOSNativeControlledAudioOutput } from "./IOSNativeControlledAudioOutput.ts";
|
||||
|
||||
export type DeviceLabel =
|
||||
| { type: "name"; name: string }
|
||||
@@ -376,8 +376,9 @@ export class MediaDevices {
|
||||
getUrlParams().callIntent,
|
||||
window.controls,
|
||||
)
|
||||
: new IOSControlledAudioOutput(this.usingNames$, this.scope)
|
||||
: new AudioOutput(this.usingNames$, this.scope);
|
||||
: new IOSNativeControlledAudioOutput(window.controls, this.scope)
|
||||
: // : new IOSControlledAudioOutput(this.usingNames$, this.scope)
|
||||
new AudioOutput(this.usingNames$, this.scope);
|
||||
|
||||
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
||||
new VideoInput(this.usingNames$, this.scope);
|
||||
|
||||
Reference in New Issue
Block a user