From 9aeedb1a53f240c5d61f84d420596f91119d0bca Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Mon, 6 Jul 2026 12:56:27 +0000 Subject: [PATCH] feat(webrtc): add UI to select audio/video input and output devices --- web/src/components/DeviceSettingsModal.tsx | 118 +++++++++++++++++++++ web/src/components/VoiceControls.tsx | 14 +++ web/tsconfig.app.tsbuildinfo | 2 +- 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 web/src/components/DeviceSettingsModal.tsx diff --git a/web/src/components/DeviceSettingsModal.tsx b/web/src/components/DeviceSettingsModal.tsx new file mode 100644 index 0000000..c9d0496 --- /dev/null +++ b/web/src/components/DeviceSettingsModal.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react'; +import { useVoiceStore } from '../stores/voice.ts'; + +interface Props { + onClose: () => void; +} + +export function DeviceSettingsModal({ onClose }: Props) { + const room = useVoiceStore((s) => s._room); + const [audioInputs, setAudioInputs] = useState([]); + const [audioOutputs, setAudioOutputs] = useState([]); + const [videoInputs, setVideoInputs] = useState([]); + + const [activeAudioInput, setActiveAudioInput] = useState(''); + const [activeAudioOutput, setActiveAudioOutput] = useState(''); + const [activeVideoInput, setActiveVideoInput] = useState(''); + + useEffect(() => { + async function loadDevices() { + if (!room) return; + try { + // Request permissions first to ensure we get labels + await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); + } catch (e) { + // Ignore if denied or no devices + } + + const devices = await navigator.mediaDevices.enumerateDevices(); + setAudioInputs(devices.filter((d) => d.kind === 'audioinput')); + setAudioOutputs(devices.filter((d) => d.kind === 'audiooutput')); + setVideoInputs(devices.filter((d) => d.kind === 'videoinput')); + + // If room exposes getActiveDevice(kind) + if (typeof room.getActiveDevice === 'function') { + const ai = room.getActiveDevice('audioinput'); + const ao = room.getActiveDevice('audiooutput'); + const vi = room.getActiveDevice('videoinput'); + if (ai) setActiveAudioInput(ai); + if (ao) setActiveAudioOutput(ao); + if (vi) setActiveVideoInput(vi); + } + } + loadDevices(); + }, [room]); + + const handleDeviceChange = async (kind: MediaDeviceKind, deviceId: string) => { + if (!room) return; + try { + await room.switchActiveDevice(kind, deviceId); + if (kind === 'audioinput') setActiveAudioInput(deviceId); + if (kind === 'audiooutput') setActiveAudioOutput(deviceId); + if (kind === 'videoinput') setActiveVideoInput(deviceId); + } catch (e) { + console.error('Failed to switch device', e); + } + }; + + return ( +
+
+

Device Settings

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+
+
+ ); +} diff --git a/web/src/components/VoiceControls.tsx b/web/src/components/VoiceControls.tsx index eb45615..ad18b70 100644 --- a/web/src/components/VoiceControls.tsx +++ b/web/src/components/VoiceControls.tsx @@ -1,6 +1,9 @@ +import { useState } from 'react'; import { useVoiceStore } from '../stores/voice.ts'; +import { DeviceSettingsModal } from './DeviceSettingsModal.tsx'; export function VoiceControls() { + const [showDeviceSettings, setShowDeviceSettings] = useState(false); const isMuted = useVoiceStore((state) => state.isMuted); const isDeafened = useVoiceStore((state) => state.isDeafened); const isVideoOn = useVoiceStore((state) => state.isVideoOn); @@ -71,6 +74,13 @@ export function VoiceControls() { [ns{noiseSuppression ? '●' : '○'}]
+ + + {showDeviceSettings && ( + setShowDeviceSettings(false)} /> + )}
); } diff --git a/web/tsconfig.app.tsbuildinfo b/web/tsconfig.app.tsbuildinfo index b6e9ee2..386ce66 100644 --- a/web/tsconfig.app.tsbuildinfo +++ b/web/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AudioRenderers.tsx","./src/components/BotManager.tsx","./src/components/CalendarView.tsx","./src/components/ChannelList.tsx","./src/components/ChannelSettingsModal.tsx","./src/components/ChatArea.tsx","./src/components/CommandDropdown.tsx","./src/components/CommandManager.tsx","./src/components/ConnectionStatus.tsx","./src/components/ContextMenu.tsx","./src/components/ConversationList.tsx","./src/components/CreateChannelModal.tsx","./src/components/CreateServerModal.tsx","./src/components/DMChat.tsx","./src/components/DocsView.tsx","./src/components/EmojiPicker.tsx","./src/components/FeatureRequestsPanel.tsx","./src/components/ForgotPasswordPage.tsx","./src/components/FormatToolbar.tsx","./src/components/ForumView.tsx","./src/components/GiphyPicker.tsx","./src/components/InstallBanner.tsx","./src/components/InstallPrompt.tsx","./src/components/InviteModal.tsx","./src/components/JoinServer.tsx","./src/components/JoinServerModal.tsx","./src/components/Layout.tsx","./src/components/ListView.tsx","./src/components/LoginForm.tsx","./src/components/MemberContextMenu.tsx","./src/components/MemberList.tsx","./src/components/MemberRoleAssign.tsx","./src/components/MentionDropdown.tsx","./src/components/MentionPopup.tsx","./src/components/MessageSearch.tsx","./src/components/MobileDrawer.tsx","./src/components/MobileNav.tsx","./src/components/NewConversationModal.tsx","./src/components/NotificationPrompt.tsx","./src/components/PinnedMessages.tsx","./src/components/Poll.tsx","./src/components/ReactionBar.tsx","./src/components/ReplyBar.tsx","./src/components/ResetPasswordPage.tsx","./src/components/RoleManager.tsx","./src/components/ServerBar.tsx","./src/components/ServerSettingsModal.tsx","./src/components/SlashCommandPopup.tsx","./src/components/ThemeToggle.tsx","./src/components/ThreadListPanel.tsx","./src/components/ThreadPanel.tsx","./src/components/TypingIndicator.tsx","./src/components/UserProfileModal.tsx","./src/components/UserSettings.tsx","./src/components/VideoGrid.tsx","./src/components/VoiceChannel.tsx","./src/components/VoiceControls.tsx","./src/components/VoicePanel.tsx","./src/lib/api.ts","./src/lib/kaomojiData.ts","./src/lib/slashCommands.ts","./src/lib/usePermissions.ts","./src/stores/auth.ts","./src/stores/bot.ts","./src/stores/channel.ts","./src/stores/conversation.ts","./src/stores/featureRequest.ts","./src/stores/layout.ts","./src/stores/member.ts","./src/stores/message.ts","./src/stores/moderation.ts","./src/stores/notificationSettings.ts","./src/stores/permissions.ts","./src/stores/presence.ts","./src/stores/push.ts","./src/stores/readStates.ts","./src/stores/role.ts","./src/stores/server.ts","./src/stores/thread.ts","./src/stores/typing.ts","./src/stores/voice.ts","./src/stores/ws.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AudioRenderers.tsx","./src/components/BotManager.tsx","./src/components/CalendarView.tsx","./src/components/ChannelList.tsx","./src/components/ChannelSettingsModal.tsx","./src/components/ChatArea.tsx","./src/components/CommandDropdown.tsx","./src/components/CommandManager.tsx","./src/components/ConnectionStatus.tsx","./src/components/ContextMenu.tsx","./src/components/ConversationList.tsx","./src/components/CreateChannelModal.tsx","./src/components/CreateServerModal.tsx","./src/components/DMChat.tsx","./src/components/DeviceSettingsModal.tsx","./src/components/DocsView.tsx","./src/components/EmojiPicker.tsx","./src/components/FeatureRequestsPanel.tsx","./src/components/ForgotPasswordPage.tsx","./src/components/FormatToolbar.tsx","./src/components/ForumView.tsx","./src/components/GiphyPicker.tsx","./src/components/InstallBanner.tsx","./src/components/InstallPrompt.tsx","./src/components/InviteModal.tsx","./src/components/JoinServer.tsx","./src/components/JoinServerModal.tsx","./src/components/Layout.tsx","./src/components/ListView.tsx","./src/components/LoginForm.tsx","./src/components/MemberContextMenu.tsx","./src/components/MemberList.tsx","./src/components/MemberRoleAssign.tsx","./src/components/MentionDropdown.tsx","./src/components/MentionPopup.tsx","./src/components/MessageSearch.tsx","./src/components/MobileDrawer.tsx","./src/components/MobileNav.tsx","./src/components/NewConversationModal.tsx","./src/components/NotificationPrompt.tsx","./src/components/PinnedMessages.tsx","./src/components/Poll.tsx","./src/components/ReactionBar.tsx","./src/components/ReplyBar.tsx","./src/components/ResetPasswordPage.tsx","./src/components/RoleManager.tsx","./src/components/ServerBar.tsx","./src/components/ServerSettingsModal.tsx","./src/components/SlashCommandPopup.tsx","./src/components/ThemeToggle.tsx","./src/components/ThreadListPanel.tsx","./src/components/ThreadPanel.tsx","./src/components/TypingIndicator.tsx","./src/components/UserProfileModal.tsx","./src/components/UserSettings.tsx","./src/components/VideoGrid.tsx","./src/components/VoiceChannel.tsx","./src/components/VoiceControls.tsx","./src/components/VoicePanel.tsx","./src/lib/api.ts","./src/lib/kaomojiData.ts","./src/lib/slashCommands.ts","./src/lib/usePermissions.ts","./src/stores/auth.ts","./src/stores/bot.ts","./src/stores/channel.ts","./src/stores/conversation.ts","./src/stores/featureRequest.ts","./src/stores/layout.ts","./src/stores/member.ts","./src/stores/message.ts","./src/stores/moderation.ts","./src/stores/notificationSettings.ts","./src/stores/permissions.ts","./src/stores/presence.ts","./src/stores/push.ts","./src/stores/readStates.ts","./src/stores/role.ts","./src/stores/server.ts","./src/stores/thread.ts","./src/stores/typing.ts","./src/stores/voice.ts","./src/stores/ws.ts"],"version":"5.9.3"} \ No newline at end of file