mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-18 18:59:23 +00:00
Use html audio element to call setsink id for reactions and call sounds.
This commit is contained in:
@@ -16,15 +16,18 @@ import { type RemoteAudioTrack } from "livekit-client";
|
|||||||
import { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
import { useTracks } from "@livekit/components-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 * as MediaDevicesContext from "./MediaDevicesContext";
|
||||||
import { MatrixAudioRenderer } from "./MatrixAudioRenderer";
|
import { MatrixAudioRenderer } from "./MatrixAudioRenderer";
|
||||||
import { mockTrack } from "../utils/test";
|
import { mockTrack } from "../utils/test";
|
||||||
|
|
||||||
export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubGlobal("AudioContext", TestAudioContextConstructor);
|
vi.stubGlobal("AudioContext", TestAudioContextConstructor);
|
||||||
|
vi.stubGlobal("Audio", TestAudioConstructor);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -87,12 +87,19 @@ export const testAudioContext = {
|
|||||||
createGain: vi.fn().mockReturnValue(gainNode),
|
createGain: vi.fn().mockReturnValue(gainNode),
|
||||||
createStereoPanner: vi.fn().mockReturnValue(panNode),
|
createStereoPanner: vi.fn().mockReturnValue(panNode),
|
||||||
close: vi.fn().mockResolvedValue(undefined),
|
close: vi.fn().mockResolvedValue(undefined),
|
||||||
|
createMediaStreamDestination: vi.fn().mockReturnValue({ stream: undefined }),
|
||||||
};
|
};
|
||||||
export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
|
export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
|
||||||
|
export const testAudioElement = {
|
||||||
|
setSinkId: vi.fn().mockResolvedValue(null),
|
||||||
|
};
|
||||||
|
export const TestAudioConstructor = vi.fn(() => testAudioElement);
|
||||||
|
|
||||||
let user: UserEvent;
|
let user: UserEvent;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubGlobal("AudioContext", TestAudioContextConstructor);
|
vi.stubGlobal("AudioContext", TestAudioContextConstructor);
|
||||||
|
vi.stubGlobal("Audio", TestAudioConstructor);
|
||||||
|
|
||||||
user = userEvent.setup();
|
user = userEvent.setup();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,6 +121,7 @@ test("will ignore sounds that are not registered", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("will use the correct device", () => {
|
test("will use the correct device", () => {
|
||||||
|
testAudioElement.setSinkId.mockClear();
|
||||||
render(
|
render(
|
||||||
<MediaDevicesContext.Provider
|
<MediaDevicesContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -134,7 +142,7 @@ test("will use the correct device", () => {
|
|||||||
</MediaDevicesContext.Provider>,
|
</MediaDevicesContext.Provider>,
|
||||||
);
|
);
|
||||||
expect(testAudioContext.createBufferSource).not.toHaveBeenCalled();
|
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 () => {
|
test("will use the correct volume level", async () => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "./livekit/MediaDevicesContext";
|
} from "./livekit/MediaDevicesContext";
|
||||||
import { type PrefetchedSounds } from "./soundUtils";
|
import { type PrefetchedSounds } from "./soundUtils";
|
||||||
import { useUrlParams } from "./UrlParams";
|
import { useUrlParams } from "./UrlParams";
|
||||||
|
import { useInitial } from "./useInitial";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a sound though a given AudioContext. Will take
|
* 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 [audioContext, setAudioContext] = useState<AudioContext>();
|
||||||
const [audioBuffers, setAudioBuffers] = useState<Record<S, AudioBuffer>>();
|
const [audioBuffers, setAudioBuffers] = useState<Record<S, AudioBuffer>>();
|
||||||
|
|
||||||
|
const htmlAudioElement = useInitial((): HTMLAudioElement => new Audio());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sounds = props.sounds;
|
const sounds = props.sounds;
|
||||||
if (!sounds) {
|
if (!sounds || !htmlAudioElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ctx = new AudioContext({
|
const ctx = new AudioContext({
|
||||||
// We want low latency for these effects.
|
// We want low latency for these effects.
|
||||||
latencyHint: props.latencyHint,
|
latencyHint: props.latencyHint,
|
||||||
});
|
});
|
||||||
|
htmlAudioElement.srcObject = ctx.createMediaStreamDestination().stream;
|
||||||
|
|
||||||
// We want to clone the content of our preloaded
|
// We want to clone the content of our preloaded
|
||||||
// sound buffers into this context. The context may
|
// sound buffers into this context. The context may
|
||||||
@@ -107,22 +111,16 @@ export function useAudioContext<S extends string>(
|
|||||||
});
|
});
|
||||||
setAudioContext(undefined);
|
setAudioContext(undefined);
|
||||||
};
|
};
|
||||||
}, [props.sounds, props.latencyHint]);
|
}, [props.sounds, props.latencyHint, htmlAudioElement]);
|
||||||
|
|
||||||
// Update the sink ID whenever we change devices.
|
// Update the sink ID whenever we change devices.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (!controlledAudioDevices && audioOutput.selectedId) {
|
||||||
audioContext &&
|
htmlAudioElement.setSinkId(audioOutput.selectedId).catch((ex) => {
|
||||||
"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) => {
|
|
||||||
logger.warn("Unable to change sink for audio context", 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();
|
const { pan: earpiecePan, volume: earpieceVolume } = useEarpieceAudioConfig();
|
||||||
|
|
||||||
// Don't return a function until we're ready.
|
// Don't return a function until we're ready.
|
||||||
|
|||||||
Reference in New Issue
Block a user