diff --git a/src/room/InCallView.jsx b/src/room/InCallView.jsx
index 8ecd5478..39cd5923 100644
--- a/src/room/InCallView.jsx
+++ b/src/room/InCallView.jsx
@@ -22,6 +22,7 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { useRageshakeRequestModal } from "../settings/rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { usePreventScroll } from "@react-aria/overlays";
+import { useMediaHandler } from "../settings/useMediaHandler";
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
@@ -51,6 +52,8 @@ export function InCallView({
usePreventScroll();
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
+ const { audioOutput } = useMediaHandler();
+
const items = useMemo(() => {
const participants = [];
@@ -159,6 +162,7 @@ export function InCallView({
item={item}
getAvatar={renderAvatar}
showName={items.length > 2 || item.focused}
+ audioOutputDevice={audioOutput}
{...rest}
/>
)}
diff --git a/src/room/LobbyView.jsx b/src/room/LobbyView.jsx
index fbeabcd6..f92fe274 100644
--- a/src/room/LobbyView.jsx
+++ b/src/room/LobbyView.jsx
@@ -14,6 +14,7 @@ import { useProfile } from "../profile/useProfile";
import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer";
import { useLocationNavigation } from "../useLocationNavigation";
+import { useMediaHandler } from "../settings/useMediaHandler";
export function LobbyView({
client,
@@ -31,7 +32,8 @@ export function LobbyView({
roomId,
}) {
const { stream } = useCallFeed(localCallFeed);
- const videoRef = useMediaStream(stream, true);
+ const { audioOutput } = useMediaHandler();
+ const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const avatarSize = (previewBounds.height - 66) / 2;
diff --git a/src/room/OverflowMenu.jsx b/src/room/OverflowMenu.jsx
index 3f760851..fd34bcd6 100644
--- a/src/room/OverflowMenu.jsx
+++ b/src/room/OverflowMenu.jsx
@@ -75,7 +75,6 @@ export function OverflowMenu({
{...settingsModalProps}
setShowInspector={setShowInspector}
showInspector={showInspector}
- client={client}
/>
)}
{inviteModalState.isOpen && (
diff --git a/src/room/RoomPage.jsx b/src/room/RoomPage.jsx
index f421e687..92e320e5 100644
--- a/src/room/RoomPage.jsx
+++ b/src/room/RoomPage.jsx
@@ -21,6 +21,7 @@ import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView";
+import { MediaHandlerProvider } from "../settings/useMediaHandler";
export function RoomPage() {
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
@@ -47,16 +48,18 @@ export function RoomPage() {
}
return (
-
- {(groupCall) => (
-
- )}
-
+
+
+ {(groupCall) => (
+
+ )}
+
+
);
}
diff --git a/src/settings/SettingsModal.jsx b/src/settings/SettingsModal.jsx
index 36c33c02..2945daca 100644
--- a/src/settings/SettingsModal.jsx
+++ b/src/settings/SettingsModal.jsx
@@ -13,12 +13,7 @@ import { Button } from "../button";
import { useDownloadDebugLog } from "./rageshake";
import { Body } from "../typography/Typography";
-export function SettingsModal({
- client,
- setShowInspector,
- showInspector,
- ...rest
-}) {
+export function SettingsModal({ setShowInspector, showInspector, ...rest }) {
const {
audioInput,
audioInputs,
@@ -26,7 +21,10 @@ export function SettingsModal({
videoInput,
videoInputs,
setVideoInput,
- } = useMediaHandler(client);
+ audioOutput,
+ audioOutputs,
+ setAudioOutput,
+ } = useMediaHandler();
const downloadDebugLog = useDownloadDebugLog();
@@ -56,6 +54,17 @@ export function SettingsModal({
- {label}
))}
+ {audioOutputs.length > 0 && (
+
+ {audioOutputs.map(({ deviceId, label }) => (
+ - {label}
+ ))}
+
+ )}
{
- const mediaHandler = client.getMediaHandler();
-
- return {
- audioInput: mediaHandler.audioInput,
- videoInput: mediaHandler.videoInput,
- audioInputs: [],
- videoInputs: [],
- };
- });
-
- useEffect(() => {
- const mediaHandler = client.getMediaHandler();
-
- function updateDevices() {
- navigator.mediaDevices.enumerateDevices().then((devices) => {
- const audioInputs = devices.filter(
- (device) => device.kind === "audioinput"
- );
- const videoInputs = devices.filter(
- (device) => device.kind === "videoinput"
- );
-
- setState(() => ({
- audioInput: mediaHandler.audioInput,
- videoInput: mediaHandler.videoInput,
- audioInputs,
- videoInputs,
- }));
- });
- }
-
- updateDevices();
-
- mediaHandler.on("local_streams_changed", updateDevices);
- navigator.mediaDevices.addEventListener("devicechange", updateDevices);
-
- return () => {
- mediaHandler.removeListener("local_streams_changed", updateDevices);
- navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
- };
- }, []);
-
- const setAudioInput = useCallback(
- (deviceId) => {
- setState((prevState) => ({ ...prevState, audioInput: deviceId }));
- client.getMediaHandler().setAudioInput(deviceId);
- },
- [client]
- );
-
- const setVideoInput = useCallback(
- (deviceId) => {
- setState((prevState) => ({ ...prevState, videoInput: deviceId }));
- client.getMediaHandler().setVideoInput(deviceId);
- },
- [client]
- );
-
- return {
- audioInput,
- audioInputs,
- setAudioInput,
- videoInput,
- videoInputs,
- setVideoInput,
- };
-}
diff --git a/src/settings/useMediaHandler.jsx b/src/settings/useMediaHandler.jsx
new file mode 100644
index 00000000..1b540736
--- /dev/null
+++ b/src/settings/useMediaHandler.jsx
@@ -0,0 +1,143 @@
+import React, {
+ useState,
+ useEffect,
+ useCallback,
+ useMemo,
+ useContext,
+ createContext,
+} from "react";
+
+const MediaHandlerContext = createContext();
+
+export function MediaHandlerProvider({ client, children }) {
+ const [
+ {
+ audioInput,
+ videoInput,
+ audioInputs,
+ videoInputs,
+ audioOutput,
+ audioOutputs,
+ },
+ setState,
+ ] = useState(() => {
+ const mediaHandler = client.getMediaHandler();
+
+ return {
+ audioInput: mediaHandler.audioInput,
+ videoInput: mediaHandler.videoInput,
+ audioOutput: undefined,
+ audioInputs: [],
+ videoInputs: [],
+ audioOutputs: [],
+ };
+ });
+
+ useEffect(() => {
+ const mediaHandler = client.getMediaHandler();
+
+ function updateDevices() {
+ navigator.mediaDevices.enumerateDevices().then((devices) => {
+ const audioInputs = devices.filter(
+ (device) => device.kind === "audioinput"
+ );
+ const videoInputs = devices.filter(
+ (device) => device.kind === "videoinput"
+ );
+ const audioOutputs = devices.filter(
+ (device) => device.kind === "audiooutput"
+ );
+
+ let audioOutput = undefined;
+
+ const audioOutputPreference = localStorage.getItem(
+ "matrix-audio-output"
+ );
+
+ if (
+ audioOutputPreference &&
+ audioOutputs.some(
+ (device) => device.deviceId === audioOutputPreference
+ )
+ ) {
+ audioOutput = audioOutputPreference;
+ }
+
+ setState({
+ audioInput: mediaHandler.audioInput,
+ videoInput: mediaHandler.videoInput,
+ audioOutput,
+ audioInputs,
+ audioOutputs,
+ videoInputs,
+ });
+ });
+ }
+
+ updateDevices();
+
+ mediaHandler.on("local_streams_changed", updateDevices);
+ navigator.mediaDevices.addEventListener("devicechange", updateDevices);
+
+ return () => {
+ mediaHandler.removeListener("local_streams_changed", updateDevices);
+ navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
+ };
+ }, [client]);
+
+ const setAudioInput = useCallback(
+ (deviceId) => {
+ setState((prevState) => ({ ...prevState, audioInput: deviceId }));
+ client.getMediaHandler().setAudioInput(deviceId);
+ },
+ [client]
+ );
+
+ const setVideoInput = useCallback(
+ (deviceId) => {
+ setState((prevState) => ({ ...prevState, videoInput: deviceId }));
+ client.getMediaHandler().setVideoInput(deviceId);
+ },
+ [client]
+ );
+
+ const setAudioOutput = useCallback((deviceId) => {
+ localStorage.setItem("matrix-audio-output", deviceId);
+ setState((prevState) => ({ ...prevState, audioOutput: deviceId }));
+ }, []);
+
+ const context = useMemo(
+ () => ({
+ audioInput,
+ audioInputs,
+ setAudioInput,
+ videoInput,
+ videoInputs,
+ setVideoInput,
+ audioOutput,
+ audioOutputs,
+ setAudioOutput,
+ }),
+ [
+ audioInput,
+ audioInputs,
+ setAudioInput,
+ videoInput,
+ videoInputs,
+ setVideoInput,
+ audioOutput,
+ audioOutputs,
+ setAudioOutput,
+ ]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useMediaHandler() {
+ return useContext(MediaHandlerContext);
+}