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); +}