Compare commits

...
Author SHA1 Message Date
Timo 36860b6ca6 no useMediaDeviceNames 2025-06-05 15:21:35 +02:00
Timo 576abb2a8d fix lint 2025-06-05 15:18:36 +02:00
Timo 48a1240408 fix tests 2025-06-05 14:39:46 +02:00
Timo 36f678e6e9 mock createMediaStreamDestination 2025-06-05 14:39:46 +02:00
Timo aabdc2e894 Use html audio element to call setsink id for reactions and call sounds. 2025-06-05 14:39:46 +02:00
5 changed files with 27 additions and 37 deletions
+6 -3
View File
@@ -16,15 +16,18 @@ import { type RemoteAudioTrack } from "livekit-client";
import { type ReactNode } from "react";
import { useTracks } from "@livekit/components-react";
import { testAudioContext } from "../useAudioContext.test";
import {
TestAudioConstructor,
testAudioContext,
TestAudioContextConstructor,
} from "../useAudioContext.test";
import * as MediaDevicesContext from "./MediaDevicesContext";
import { MatrixAudioRenderer } from "./MatrixAudioRenderer";
import { mockTrack } from "../utils/test";
export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
beforeEach(() => {
vi.stubGlobal("AudioContext", TestAudioContextConstructor);
vi.stubGlobal("Audio", TestAudioConstructor);
});
afterEach(() => {
+3 -20
View File
@@ -124,7 +124,7 @@ function useMediaDeviceHandle(
usingNames: boolean,
): MediaDeviceHandle {
const hasRequestedPermissions = useRef(false);
const requestPermissions = usingNames || hasRequestedPermissions.current;
// const requestPermissions = usingNames || hasRequestedPermissions.current;
// Make sure we don't needlessly reset to a device observer without names,
// once permissions are already given
hasRequestedPermissions.current ||= usingNames;
@@ -140,7 +140,7 @@ function useMediaDeviceHandle(
createMediaDeviceObserver(
kind,
() => logger.error("Error creating MediaDeviceObserver"),
requestPermissions,
true,
).pipe(
startWith([]),
// This Observable emits new values whenever the browser fires a
@@ -151,7 +151,7 @@ function useMediaDeviceHandle(
// we call MediaDevices.getUserMedia. So, filter by deep equality.
distinctUntilChanged<MediaDeviceInfo[]>(deepCompare),
),
[kind, requestPermissions],
[kind],
);
const available = useObservableEagerState(
useMemo(
@@ -400,23 +400,6 @@ function useControlledOutput(): MediaDeviceHandle {
export const useMediaDevices = (): MediaDevices =>
useContext(MediaDevicesContext);
/**
* React hook that requests for the media devices context to be populated with
* real device names while this component is mounted. This is not done by
* default because it may involve requesting additional permissions from the
* user.
*/
export const useMediaDeviceNames = (
context: MediaDevices,
enabled = true,
): void =>
useEffect(() => {
if (enabled) {
context.startUsingDeviceNames();
return context.stopUsingDeviceNames;
}
}, [context, enabled]);
/**
* A convenience hook to get the audio node configuration for the earpiece.
* It will check the `useAsEarpiece` of the `audioOutput` device and return
-2
View File
@@ -19,7 +19,6 @@ import { ProfileSettingsTab } from "./ProfileSettingsTab";
import { FeedbackSettingsTab } from "./FeedbackSettingsTab";
import {
useMediaDevices,
useMediaDeviceNames,
iosDeviceMenu$,
} from "../livekit/MediaDevicesContext";
import { widget } from "../widget";
@@ -98,7 +97,6 @@ export const SettingsModal: FC<Props> = ({
};
const devices = useMediaDevices();
useMediaDeviceNames(devices, open);
const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting);
const [soundVolumeRaw, setSoundVolumeRaw] = useState(soundVolume);
const [showDeveloperSettingsTab] = useSetting(developerMode);
+9 -1
View File
@@ -87,12 +87,19 @@ export const testAudioContext = {
createGain: vi.fn().mockReturnValue(gainNode),
createStereoPanner: vi.fn().mockReturnValue(panNode),
close: vi.fn().mockResolvedValue(undefined),
createMediaStreamDestination: vi.fn().mockReturnValue({ stream: undefined }),
};
export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
export const testAudioElement = {
setSinkId: vi.fn().mockResolvedValue(null),
};
export const TestAudioConstructor = vi.fn(() => testAudioElement);
let user: UserEvent;
beforeEach(() => {
vi.stubGlobal("AudioContext", TestAudioContextConstructor);
vi.stubGlobal("Audio", TestAudioConstructor);
user = userEvent.setup();
});
@@ -114,6 +121,7 @@ test("will ignore sounds that are not registered", async () => {
});
test("will use the correct device", () => {
testAudioElement.setSinkId.mockClear();
render(
<MediaDevicesContext.Provider
value={{
@@ -134,7 +142,7 @@ test("will use the correct device", () => {
</MediaDevicesContext.Provider>,
);
expect(testAudioContext.createBufferSource).not.toHaveBeenCalled();
expect(testAudioContext.setSinkId).toHaveBeenCalledWith("chosen-device");
expect(testAudioElement.setSinkId).toHaveBeenCalledWith("chosen-device");
});
test("will use the correct volume level", async () => {
+9 -11
View File
@@ -18,6 +18,7 @@ import {
} from "./livekit/MediaDevicesContext";
import { type PrefetchedSounds } from "./soundUtils";
import { useUrlParams } from "./UrlParams";
import { useInitial } from "./useInitial";
/**
* Play a sound though a given AudioContext. Will take
@@ -76,15 +77,18 @@ export function useAudioContext<S extends string>(
const [audioContext, setAudioContext] = useState<AudioContext>();
const [audioBuffers, setAudioBuffers] = useState<Record<S, AudioBuffer>>();
const htmlAudioElement = useInitial((): HTMLAudioElement => new Audio());
useEffect(() => {
const sounds = props.sounds;
if (!sounds) {
if (!sounds || !htmlAudioElement) {
return;
}
const ctx = new AudioContext({
// We want low latency for these effects.
latencyHint: props.latencyHint,
});
htmlAudioElement.srcObject = ctx.createMediaStreamDestination().stream;
// We want to clone the content of our preloaded
// sound buffers into this context. The context may
@@ -107,22 +111,16 @@ export function useAudioContext<S extends string>(
});
setAudioContext(undefined);
};
}, [props.sounds, props.latencyHint]);
}, [props.sounds, props.latencyHint, htmlAudioElement]);
// Update the sink ID whenever we change devices.
useEffect(() => {
if (
audioContext &&
"setSinkId" in audioContext &&
!controlledAudioDevices
) {
// https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId
// @ts-expect-error - setSinkId doesn't exist yet in types, maybe because it's not supported everywhere.
audioContext.setSinkId(audioOutput.selectedId).catch((ex) => {
if (!controlledAudioDevices && audioOutput.selectedId) {
htmlAudioElement.setSinkId(audioOutput.selectedId).catch((ex) => {
logger.warn("Unable to change sink for audio context", ex);
});
}
}, [audioContext, audioOutput.selectedId, controlledAudioDevices]);
}, [audioOutput.selectedId, controlledAudioDevices, htmlAudioElement]);
const { pan: earpiecePan, volume: earpieceVolume } = useEarpieceAudioConfig();
// Don't return a function until we're ready.