Add audio output setting when available

This commit is contained in:
Robert Long 2022-02-22 18:32:51 -08:00
parent 81a763f17f
commit 2c3ebd4c03
7 changed files with 180 additions and 92 deletions

View File

@ -22,6 +22,7 @@ import { UserMenuContainer } from "../UserMenuContainer";
import { useRageshakeRequestModal } from "../settings/rageshake"; import { useRageshakeRequestModal } from "../settings/rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal"; import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { usePreventScroll } from "@react-aria/overlays"; import { usePreventScroll } from "@react-aria/overlays";
import { useMediaHandler } from "../settings/useMediaHandler";
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices; const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
// There is currently a bug in Safari our our code with cloning and sending MediaStreams // There is currently a bug in Safari our our code with cloning and sending MediaStreams
@ -51,6 +52,8 @@ export function InCallView({
usePreventScroll(); usePreventScroll();
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0); const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
const { audioOutput } = useMediaHandler();
const items = useMemo(() => { const items = useMemo(() => {
const participants = []; const participants = [];
@ -159,6 +162,7 @@ export function InCallView({
item={item} item={item}
getAvatar={renderAvatar} getAvatar={renderAvatar}
showName={items.length > 2 || item.focused} showName={items.length > 2 || item.focused}
audioOutputDevice={audioOutput}
{...rest} {...rest}
/> />
)} )}

View File

@ -14,6 +14,7 @@ import { useProfile } from "../profile/useProfile";
import useMeasure from "react-use-measure"; import useMeasure from "react-use-measure";
import { ResizeObserver } from "@juggle/resize-observer"; import { ResizeObserver } from "@juggle/resize-observer";
import { useLocationNavigation } from "../useLocationNavigation"; import { useLocationNavigation } from "../useLocationNavigation";
import { useMediaHandler } from "../settings/useMediaHandler";
export function LobbyView({ export function LobbyView({
client, client,
@ -31,7 +32,8 @@ export function LobbyView({
roomId, roomId,
}) { }) {
const { stream } = useCallFeed(localCallFeed); const { stream } = useCallFeed(localCallFeed);
const videoRef = useMediaStream(stream, true); const { audioOutput } = useMediaHandler();
const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client); const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver }); const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
const avatarSize = (previewBounds.height - 66) / 2; const avatarSize = (previewBounds.height - 66) / 2;

View File

@ -75,7 +75,6 @@ export function OverflowMenu({
{...settingsModalProps} {...settingsModalProps}
setShowInspector={setShowInspector} setShowInspector={setShowInspector}
showInspector={showInspector} showInspector={showInspector}
client={client}
/> />
)} )}
{inviteModalState.isOpen && ( {inviteModalState.isOpen && (

View File

@ -21,6 +21,7 @@ import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView"; import { RoomAuthView } from "./RoomAuthView";
import { GroupCallLoader } from "./GroupCallLoader"; import { GroupCallLoader } from "./GroupCallLoader";
import { GroupCallView } from "./GroupCallView"; import { GroupCallView } from "./GroupCallView";
import { MediaHandlerProvider } from "../settings/useMediaHandler";
export function RoomPage() { export function RoomPage() {
const { loading, isAuthenticated, error, client, isPasswordlessUser } = const { loading, isAuthenticated, error, client, isPasswordlessUser } =
@ -47,16 +48,18 @@ export function RoomPage() {
} }
return ( return (
<GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}> <MediaHandlerProvider client={client}>
{(groupCall) => ( <GroupCallLoader client={client} roomId={roomId} viaServers={viaServers}>
<GroupCallView {(groupCall) => (
client={client} <GroupCallView
roomId={roomId} client={client}
groupCall={groupCall} roomId={roomId}
isPasswordlessUser={isPasswordlessUser} groupCall={groupCall}
simpleGrid={simpleGrid} isPasswordlessUser={isPasswordlessUser}
/> simpleGrid={simpleGrid}
)} />
</GroupCallLoader> )}
</GroupCallLoader>
</MediaHandlerProvider>
); );
} }

View File

@ -13,12 +13,7 @@ import { Button } from "../button";
import { useDownloadDebugLog } from "./rageshake"; import { useDownloadDebugLog } from "./rageshake";
import { Body } from "../typography/Typography"; import { Body } from "../typography/Typography";
export function SettingsModal({ export function SettingsModal({ setShowInspector, showInspector, ...rest }) {
client,
setShowInspector,
showInspector,
...rest
}) {
const { const {
audioInput, audioInput,
audioInputs, audioInputs,
@ -26,7 +21,10 @@ export function SettingsModal({
videoInput, videoInput,
videoInputs, videoInputs,
setVideoInput, setVideoInput,
} = useMediaHandler(client); audioOutput,
audioOutputs,
setAudioOutput,
} = useMediaHandler();
const downloadDebugLog = useDownloadDebugLog(); const downloadDebugLog = useDownloadDebugLog();
@ -56,6 +54,17 @@ export function SettingsModal({
<Item key={deviceId}>{label}</Item> <Item key={deviceId}>{label}</Item>
))} ))}
</SelectInput> </SelectInput>
{audioOutputs.length > 0 && (
<SelectInput
label="Speaker"
selectedKey={audioOutput}
onSelectionChange={setAudioOutput}
>
{audioOutputs.map(({ deviceId, label }) => (
<Item key={deviceId}>{label}</Item>
))}
</SelectInput>
)}
</TabItem> </TabItem>
<TabItem <TabItem
title={ title={

View File

@ -1,72 +0,0 @@
import { useState, useEffect, useCallback } from "react";
export function useMediaHandler(client) {
const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
useState(() => {
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,
};
}

View File

@ -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 (
<MediaHandlerContext.Provider value={context}>
{children}
</MediaHandlerContext.Provider>
);
}
export function useMediaHandler() {
return useContext(MediaHandlerContext);
}