mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-08 20:09:19 +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 setAudioEnabled$ = new Subject<boolean>();
|
||||||
|
|
||||||
|
export const earpieceModeToggle$ = new Subject<void>();
|
||||||
|
|
||||||
let playbackStartedEmitted = false;
|
let playbackStartedEmitted = false;
|
||||||
export const setPlaybackStarted = (): void => {
|
export const setPlaybackStarted = (): void => {
|
||||||
if (!playbackStartedEmitted) {
|
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,
|
startWith,
|
||||||
Subject,
|
Subject,
|
||||||
switchMap,
|
switchMap,
|
||||||
|
withLatestFrom,
|
||||||
type Observable,
|
type Observable,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { createMediaDeviceObserver } from "@livekit/components-core";
|
import { createMediaDeviceObserver } from "@livekit/components-core";
|
||||||
@@ -30,6 +31,7 @@ import { type ObservableScope } from "./ObservableScope";
|
|||||||
import {
|
import {
|
||||||
outputDevice$ as controlledOutputSelection$,
|
outputDevice$ as controlledOutputSelection$,
|
||||||
availableOutputDevices$ as controlledAvailableOutputDevices$,
|
availableOutputDevices$ as controlledAvailableOutputDevices$,
|
||||||
|
earpieceModeToggle$,
|
||||||
} from "../controls";
|
} from "../controls";
|
||||||
import { getUrlParams } from "../UrlParams";
|
import { getUrlParams } from "../UrlParams";
|
||||||
|
|
||||||
@@ -362,5 +364,31 @@ export class MediaDevices {
|
|||||||
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
public readonly videoInput: MediaDevice<DeviceLabel, SelectedDevice> =
|
||||||
new VideoInput(this.usingNames$, this.scope);
|
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