mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-19 06:20:25 +00:00
Add a global control for toggling earpiece mode
This will be used by Element X to show an earpiece toggle button in the header.
This commit is contained in:
@@ -61,6 +61,8 @@ export const outputDevice$ = new Subject<string | undefined>();
|
||||
*/
|
||||
export const setAudioEnabled$ = new Subject<boolean>();
|
||||
|
||||
export const earpieceModeToggle$ = new Subject<void>();
|
||||
|
||||
let playbackStartedEmitted = false;
|
||||
export const setPlaybackStarted = (): void => {
|
||||
if (!playbackStartedEmitted) {
|
||||
|
||||
50
src/state/MediaDevices.test.ts
Normal file
50
src/state/MediaDevices.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { vi, test, onTestFinished } from "vitest";
|
||||
import { createMediaDeviceObserver } from "@livekit/components-core";
|
||||
import { map, of } from "rxjs";
|
||||
|
||||
import { withTestScheduler } from "../utils/test";
|
||||
import { MediaDevices } from "./MediaDevices";
|
||||
import { ObservableScope } from "./ObservableScope";
|
||||
|
||||
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
||||
vi.mock("../UrlParams", () => ({ getUrlParams }));
|
||||
|
||||
vi.mock("@livekit/components-core");
|
||||
|
||||
test("audio output changes when toggling earpiece mode", () => {
|
||||
withTestScheduler(({ schedule, expectObservable }) => {
|
||||
getUrlParams.mockReturnValue({ controlledAudioDevices: true });
|
||||
vi.mocked(createMediaDeviceObserver).mockReturnValue(of([]));
|
||||
|
||||
const scope = new ObservableScope();
|
||||
onTestFinished(() => scope.end());
|
||||
const devices = new MediaDevices(scope);
|
||||
|
||||
window.controls.setAvailableAudioDevices([
|
||||
{ id: "speaker", name: "Speaker", isSpeaker: true },
|
||||
{ id: "earpiece", name: "Earpiece", isEarpiece: true },
|
||||
]);
|
||||
|
||||
const toggleInputMarbles = " -aa";
|
||||
const expectedEarpieceModeMarbles = " nyn";
|
||||
const expectedSelectedOutputMarbles = "ses";
|
||||
|
||||
schedule(toggleInputMarbles, {
|
||||
a: () => window.controls.toggleEarpieceMode(),
|
||||
});
|
||||
expectObservable(devices.earpieceMode$).toBe(expectedEarpieceModeMarbles, {
|
||||
n: false,
|
||||
y: true,
|
||||
});
|
||||
expectObservable(
|
||||
devices.audioOutput.selected$.pipe(map((d) => d?.id)),
|
||||
).toBe(expectedSelectedOutputMarbles, { s: "speaker", e: "earpiece" });
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
startWith,
|
||||
Subject,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
type Observable,
|
||||
} from "rxjs";
|
||||
import { createMediaDeviceObserver } from "@livekit/components-core";
|
||||
@@ -30,6 +31,7 @@ import { type ObservableScope } from "./ObservableScope";
|
||||
import {
|
||||
outputDevice$ as controlledOutputSelection$,
|
||||
availableOutputDevices$ as controlledAvailableOutputDevices$,
|
||||
earpieceModeToggle$,
|
||||
} from "../controls";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
|
||||
@@ -362,5 +364,31 @@ export class MediaDevices {
|
||||
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
||||
new VideoInput(this.usingNames$, this.scope);
|
||||
|
||||
public constructor(private readonly scope: ObservableScope) {}
|
||||
/**
|
||||
* Whether audio is currently being output through the earpiece.
|
||||
*/
|
||||
public readonly earpieceMode$: Observable<boolean> = combineLatest(
|
||||
[this.audioOutput.available$, this.audioOutput.selected$],
|
||||
(available, selected) =>
|
||||
selected !== undefined && available.get(selected.id)?.type === "earpiece",
|
||||
).pipe(this.scope.state());
|
||||
|
||||
public constructor(private readonly scope: ObservableScope) {
|
||||
earpieceModeToggle$
|
||||
.pipe(
|
||||
withLatestFrom(
|
||||
this.audioOutput.available$,
|
||||
this.earpieceMode$,
|
||||
(_toggle, available, earpieceMode) =>
|
||||
// Determine the new device ID to switch to
|
||||
[...available].find(
|
||||
([, d]) => (d.type === "earpiece") !== earpieceMode,
|
||||
)?.[0],
|
||||
),
|
||||
this.scope.bind(),
|
||||
)
|
||||
.subscribe((newSelection) => {
|
||||
if (newSelection !== undefined) this.audioOutput.select(newSelection);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user