From fab1b79849f5a594c90a941d12cf9eddc7fc2255 Mon Sep 17 00:00:00 2001 From: Krushna Kanta Rout <129386740+krushnarout@users.noreply.github.com> Date: Sat, 27 Sep 2025 22:25:31 +0530 Subject: [PATCH 1/5] feat: select audio input --- .../widgets/desktop_recording_widget.dart | 250 +++++++++++++++--- app/macos/Runner/AudioManager.swift | 124 +++++++++ app/macos/Runner/MainFlutterWindow.swift | 15 ++ 3 files changed, 354 insertions(+), 35 deletions(-) diff --git a/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart b/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart index ed4442ba594..5c23fa4987f 100644 --- a/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart +++ b/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:omi/backend/schema/transcript_segment.dart'; import 'package:omi/providers/capture_provider.dart'; import 'package:omi/providers/connectivity_provider.dart'; @@ -12,6 +13,7 @@ import 'package:omi/utils/platform/platform_service.dart'; import 'package:omi/utils/responsive/responsive_helper.dart'; import 'package:provider/provider.dart'; import 'package:omi/ui/atoms/omi_icon_button.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class DesktopRecordingWidget extends StatefulWidget { final VoidCallback? onBack; @@ -33,6 +35,13 @@ class DesktopRecordingWidget extends StatefulWidget { class _DesktopRecordingWidgetState extends State { bool _isHovered = false; + List> _availableAudioDevices = []; + String? _selectedDeviceId; + // Add this GlobalKey to your state class +final GlobalKey _micKey = GlobalKey(); +// Add this OverlayEntry to your state class +OverlayEntry? _audioDeviceOverlayEntry; + Future _toggleRecording(BuildContext context, CaptureProvider provider) async { var recordingState = provider.recordingState; @@ -858,53 +867,224 @@ class _DesktopRecordingWidgetState extends State { ); } - Widget _buildAudioSourceStatus(CaptureProvider captureProvider) { - final micName = captureProvider.microphoneName; - final micLevel = captureProvider.microphoneLevel; - final systemLevel = captureProvider.systemAudioLevel; +Widget _buildAudioSourceStatus(CaptureProvider captureProvider) { + final micName = captureProvider.microphoneName; + final micLevel = captureProvider.microphoneLevel; + final systemLevel = captureProvider.systemAudioLevel; + + if (micName == null) { + return const SizedBox.shrink(); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildMicrophoneSection(micName, micLevel), + const SizedBox(width: 24), + Row( + children: [ + const Icon(Icons.volume_up_rounded, size: 16, color: ResponsiveHelper.textSecondary), + const SizedBox(width: 8), + const Text( + 'System', + style: TextStyle(fontSize: 13, color: ResponsiveHelper.textSecondary), + ), + const SizedBox(width: 8), + _buildAudioLevelBar(systemLevel, Colors.orange.shade600), + ], + ), + ], + ); +} + +Widget _buildMicrophoneSection(String micName, double micLevel) { + return GestureDetector( + key: _micKey, + onTap: _toggleAudioDeviceDropdown, + child: Tooltip( + message: micName, + child: Row( + children: [ + const Icon(Icons.mic_rounded, size: 16, color: ResponsiveHelper.textSecondary), + const SizedBox(width: 8), + const Text( + 'Mic', + style: TextStyle(fontSize: 13, color: ResponsiveHelper.textSecondary), + ), + const SizedBox(width: 4), + Icon( + _audioDeviceOverlayEntry != null ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, + size: 14, + color: ResponsiveHelper.textSecondary, + ), + const SizedBox(width: 8), + _buildAudioLevelBar(micLevel, ResponsiveHelper.purplePrimary), + ], + ), + ), + ); +} - if (micName == null) { - return const SizedBox.shrink(); +void _toggleAudioDeviceDropdown() { + if (_audioDeviceOverlayEntry != null) { + _removeAudioDeviceOverlay(); + } else { + if (_availableAudioDevices.isEmpty) { + _loadAvailableAudioDevices(); } + _audioDeviceOverlayEntry = _createAudioDeviceOverlayEntry(); + Overlay.of(context).insert(_audioDeviceOverlayEntry!); + setState(() {}); + } +} - return Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // Microphone - Tooltip( - message: micName ?? 'Default Microphone', +void _removeAudioDeviceOverlay() { + _audioDeviceOverlayEntry?.remove(); + _audioDeviceOverlayEntry = null; + setState(() {}); +} + +OverlayEntry _createAudioDeviceOverlayEntry() { + final renderBox = _micKey.currentContext!.findRenderObject() as RenderBox; + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + return OverlayEntry( + builder: (context) => Positioned( + top: offset.dy + size.height + 5, + left: offset.dx - 100 + (size.width / 2), + child: _buildFloatingAudioDeviceDropdown(), + ), + ); +} + +Future _selectAudioDevice(Map device) async { + setState(() { + _selectedDeviceId = device['id']; + }); + + _removeAudioDeviceOverlay(); + + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('selected_audio_device_id', device['id'] ?? ''); +} + +Widget _buildFloatingAudioDeviceDropdown() { + return Material( + elevation: 8, + color: const Color(0xFF2C2C2E), + child: Container( + constraints: const BoxConstraints(maxWidth: 280, minWidth: 220), + decoration: BoxDecoration( + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Text('Select Audio Input', + style: TextStyle( + fontSize: 13, + color: Colors.white70, + fontWeight: FontWeight.w600)), + ), + const SizedBox(height: 4), + if (_availableAudioDevices.isNotEmpty) + ..._availableAudioDevices.map(_buildFloatingAudioDeviceItem), + const SizedBox(height: 4), + ], + ), + ), + ); +} + +Widget _buildFloatingAudioDeviceItem(Map device) { + final isSelected = device['id'] == _selectedDeviceId; + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _selectAudioDevice(device), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Row( children: [ - const Icon(Icons.mic_rounded, size: 16, color: ResponsiveHelper.textSecondary), - const SizedBox(width: 8), - const Text( - 'Mic', - style: TextStyle(fontSize: 13, color: ResponsiveHelper.textSecondary), + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? ResponsiveHelper.purplePrimary : Colors.grey[500]!, + width: 2, + ), + color: Colors.transparent, + ), + child: isSelected + ? Center( + child: Container( + width: 10, + height: 10, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ResponsiveHelper.purplePrimary, + ), + ), + ) + : null, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + device['name'] ?? 'Unknown Device', + style: TextStyle( + fontSize: 14, + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, + ), ), - const SizedBox(width: 8), - _buildAudioLevelBar(micLevel, ResponsiveHelper.purplePrimary), ], ), ), - const SizedBox(width: 24), - // System Audio - Row( - children: [ - const Icon(Icons.volume_up_rounded, size: 16, color: ResponsiveHelper.textSecondary), - const SizedBox(width: 8), - const Text( - 'System', - style: TextStyle(fontSize: 13, color: ResponsiveHelper.textSecondary), - ), - const SizedBox(width: 8), - _buildAudioLevelBar(systemLevel, Colors.orange.shade600), - ], - ), - ], + ), ); } + Future _loadAvailableAudioDevices() async { + final result = await const MethodChannel('screenCapturePlatform') + .invokeMethod('getAvailableAudioDevices'); + + if (result is List && mounted) { + final devices = result.map((device) => { + 'id': device['id']?.toString() ?? '', + 'name': device['name']?.toString() ?? 'Unknown Device', + }).toList(); + + setState(() { + _availableAudioDevices = devices; + _selectedDeviceId ??= _findCurrentDeviceId(devices); + }); + } + } + + String? _findCurrentDeviceId(List> devices) { + if (devices.isEmpty) return null; + + final currentMicName = context.read().microphoneName; + if (currentMicName != null) { + final matchingDevice = devices.firstWhere( + (device) => device['name'] == currentMicName, + orElse: () => devices.first, + ); + return matchingDevice['id']; + } + return devices.first['id']; + } + Widget _buildCompactRecordingView( bool isInitializing, RecordingState recordingState, CaptureProvider captureProvider) { return Container( diff --git a/app/macos/Runner/AudioManager.swift b/app/macos/Runner/AudioManager.swift index 05d8e507131..358cfdcba23 100644 --- a/app/macos/Runner/AudioManager.swift +++ b/app/macos/Runner/AudioManager.swift @@ -890,6 +890,130 @@ class AudioManager: NSObject, SCStreamDelegate, SCStreamOutput { deviceListChangedListener = nil } + // MARK: - Audio Device Management + + func getAvailableAudioDevices() -> [[String: String]] { + var devices: [[String: String]] = [] + let deviceIDs = getAudioDeviceIDs() + + for deviceID in deviceIDs { + // Check if device has input streams + if hasInputStreams(deviceID: deviceID) { + if let deviceName = getDeviceName(for: deviceID) { + if !isAggregateOrVirtualDevice(deviceName: deviceName, deviceID: deviceID) { + let friendlyName = getFriendlyDeviceName(deviceName) + devices.append([ + "id": String(deviceID), + "name": friendlyName + ]) + } + } + } + } + + return devices + } + + private func isAggregateOrVirtualDevice(deviceName: String, deviceID: AudioDeviceID) -> Bool { + // Filter out aggregate devices + if deviceName.contains("CADefaultDeviceAggregate") || + deviceName.contains("Aggregate Device") || + deviceName.contains("Multi-Output Device") { + return true + } + + var address = AudioObjectPropertyAddress( + mSelector: kAudioDevicePropertyTransportType, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain + ) + + var transportType: UInt32 = 0 + var propertySize = UInt32(MemoryLayout.size) + + let status = AudioObjectGetPropertyData( + deviceID, + &address, + 0, + nil, + &propertySize, + &transportType + ) + + if status == noErr { + return transportType == kAudioDeviceTransportTypeVirtual || + transportType == kAudioDeviceTransportTypeAggregate + } + + return false + } + + private func getFriendlyDeviceName(_ deviceName: String) -> String { + if deviceName.contains("MacBook") && deviceName.contains("Microphone") { + return "MacBook Microphone" + } else if deviceName.contains("AirPods") { + return deviceName + } else if deviceName.contains("USB") { + return deviceName.replacingOccurrences(of: "USB ", with: "") + } else if deviceName.contains("Built-in") { + return "Built-in Microphone" + } + + return deviceName + } + + private func hasInputStreams(deviceID: AudioDeviceID) -> Bool { + var propertyAddress = AudioObjectPropertyAddress( + mSelector: kAudioDevicePropertyStreams, + mScope: kAudioDevicePropertyScopeInput, + mElement: kAudioObjectPropertyElementMain + ) + + var propertySize: UInt32 = 0 + let status = AudioObjectGetPropertyDataSize(deviceID, &propertyAddress, 0, nil, &propertySize) + + return status == noErr && propertySize > 0 + } + + func selectAudioDevice(deviceID: String) -> Bool { + guard let deviceIDInt = AudioDeviceID(deviceID) else { + print("ERROR: Invalid device ID: \(deviceID)") + return false + } + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: kAudioHardwarePropertyDefaultInputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain + ) + + var deviceIDToSet = deviceIDInt + let propertySize = UInt32(MemoryLayout.size) + + let status = AudioObjectSetPropertyData( + AudioObjectID(kAudioObjectSystemObject), + &propertyAddress, + 0, + nil, + propertySize, + &deviceIDToSet + ) + + if status == noErr { + print("DEBUG: Successfully set default input device to ID: \(deviceID)") + + self.currentInputDeviceID = deviceIDInt + if let deviceName = getDeviceName(for: deviceIDInt) { + self.currentInputDeviceName = deviceName + print("DEBUG: New input device name: \(deviceName)") + } + + return true + } else { + print("ERROR: Failed to set default input device: \(status)") + return false + } + } } // MARK: - Audio Manager Errors diff --git a/app/macos/Runner/MainFlutterWindow.swift b/app/macos/Runner/MainFlutterWindow.swift index ca7297af601..ba51a0f6b5b 100644 --- a/app/macos/Runner/MainFlutterWindow.swift +++ b/app/macos/Runner/MainFlutterWindow.swift @@ -252,6 +252,21 @@ class MainFlutterWindow: NSWindow, NSWindowDelegate { } } + case "getAvailableAudioDevices": + let devices = self.audioManager.getAvailableAudioDevices() + result(devices) + + case "selectAudioDevice": + guard let args = call.arguments as? [String: Any], + let deviceId = args["deviceId"] as? String else { + result(FlutterError(code: "INVALID_ARGUMENTS", + message: "Missing deviceId parameter", + details: nil)) + return + } + let success = self.audioManager.selectAudioDevice(deviceID: deviceId) + result(success) + default: result(FlutterMethodNotImplemented) } From dfa44a5ffd4115100dd1ff4e0ab5553d0b7b4246 Mon Sep 17 00:00:00 2001 From: Krushna Kanta Rout <129386740+krushnarout@users.noreply.github.com> Date: Sat, 27 Sep 2025 22:33:25 +0530 Subject: [PATCH 2/5] feat: make ui good --- .../conversations/widgets/desktop_recording_widget.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart b/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart index 5c23fa4987f..85ede6602ab 100644 --- a/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart +++ b/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart @@ -973,10 +973,13 @@ Future _selectAudioDevice(Map device) async { Widget _buildFloatingAudioDeviceDropdown() { return Material( elevation: 8, + borderRadius: BorderRadius.circular(12), color: const Color(0xFF2C2C2E), child: Container( constraints: const BoxConstraints(maxWidth: 280, minWidth: 220), decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withOpacity(0.1)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -990,6 +993,7 @@ Widget _buildFloatingAudioDeviceDropdown() { color: Colors.white70, fontWeight: FontWeight.w600)), ), + Divider(height: 1, color: Colors.white.withOpacity(0.1)), const SizedBox(height: 4), if (_availableAudioDevices.isNotEmpty) ..._availableAudioDevices.map(_buildFloatingAudioDeviceItem), @@ -1042,7 +1046,8 @@ Widget _buildFloatingAudioDeviceItem(Map device) { device['name'] ?? 'Unknown Device', style: TextStyle( fontSize: 14, - color: Colors.white, + color: isSelected ? Colors.white : Colors.white70, + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, ), overflow: TextOverflow.ellipsis, ), From b97824f686f3ba1db6532e16afc649a569003c6b Mon Sep 17 00:00:00 2001 From: Krushna Kanta Rout <129386740+krushnarout@users.noreply.github.com> Date: Sat, 27 Sep 2025 23:05:28 +0530 Subject: [PATCH 3/5] fix: show snackbar --- .../widgets/desktop_recording_widget.dart | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart b/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart index 85ede6602ab..cca2d8d3fdc 100644 --- a/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart +++ b/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart @@ -37,10 +37,8 @@ class _DesktopRecordingWidgetState extends State { bool _isHovered = false; List> _availableAudioDevices = []; String? _selectedDeviceId; - // Add this GlobalKey to your state class -final GlobalKey _micKey = GlobalKey(); -// Add this OverlayEntry to your state class -OverlayEntry? _audioDeviceOverlayEntry; + final GlobalKey _micKey = GlobalKey(); + OverlayEntry? _audioDeviceOverlayEntry; Future _toggleRecording(BuildContext context, CaptureProvider provider) async { @@ -968,6 +966,15 @@ Future _selectAudioDevice(Map device) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString('selected_audio_device_id', device['id'] ?? ''); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Audio input set to ${device['name']}'), + duration: const Duration(seconds: 2), + ), + ); + } } Widget _buildFloatingAudioDeviceDropdown() { @@ -996,7 +1003,15 @@ Widget _buildFloatingAudioDeviceDropdown() { Divider(height: 1, color: Colors.white.withOpacity(0.1)), const SizedBox(height: 4), if (_availableAudioDevices.isNotEmpty) - ..._availableAudioDevices.map(_buildFloatingAudioDeviceItem), + ..._availableAudioDevices.map(_buildFloatingAudioDeviceItem) + else + const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: Text('Loading devices...', + style: TextStyle( + fontSize: 12, + color: ResponsiveHelper.textSecondary)))), const SizedBox(height: 4), ], ), From f78f4c22916a690cb36af30f257362b6272ddc9d Mon Sep 17 00:00:00 2001 From: Krushna Kanta Rout <129386740+krushnarout@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:44:00 +0530 Subject: [PATCH 4/5] fix: frequent audio device switches work properly --- .../widgets/desktop_recording_widget.dart | 29 ++++++-- app/macos/Runner/AudioManager.swift | 67 +++++++++++++++++-- app/macos/Runner/MainFlutterWindow.swift | 14 ++-- 3 files changed, 88 insertions(+), 22 deletions(-) diff --git a/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart b/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart index cca2d8d3fdc..b6e28258797 100644 --- a/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart +++ b/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart @@ -967,13 +967,28 @@ Future _selectAudioDevice(Map device) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString('selected_audio_device_id', device['id'] ?? ''); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Audio input set to ${device['name']}'), - duration: const Duration(seconds: 2), - ), - ); + try { + final result = await const MethodChannel('screenCapturePlatform') + .invokeMethod('selectAudioDevice', {'deviceId': device['id']}); + + if (mounted && result == true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Audio input set to ${device['name']}'), + duration: const Duration(seconds: 2), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error switching audio device: $e'), + duration: const Duration(seconds: 3), + backgroundColor: Colors.red, + ), + ); + } } } diff --git a/app/macos/Runner/AudioManager.swift b/app/macos/Runner/AudioManager.swift index 358cfdcba23..2492667b0ee 100644 --- a/app/macos/Runner/AudioManager.swift +++ b/app/macos/Runner/AudioManager.swift @@ -139,7 +139,7 @@ class AudioManager: NSObject, SCStreamDelegate, SCStreamOutput { if let engine = audioEngine, engine.isRunning { engine.stop() - micNode?.removeTap(onBus: 0) + removeMicTap() } // Clean up resources. @@ -248,23 +248,54 @@ class AudioManager: NSObject, SCStreamDelegate, SCStreamOutput { } self.micNode = audioEngine?.inputNode - self.micNodeFormat = self.micNode!.outputFormat(forBus: 0) - self.micAudioConverter = AVAudioConverter(from: self.micNodeFormat!, to: outputFormat) + // Get the hardware input format + guard let micNode = self.micNode else { + throw AudioManagerError.audioFormatError("Could not get audio engine input node") + } + + let hardwareInputFormat = micNode.inputFormat(forBus: 0) + + // Validate the hardware input format + guard hardwareInputFormat.sampleRate > 0 && hardwareInputFormat.channelCount > 0 else { + throw AudioManagerError.audioFormatError("Invalid hardware input format: SR=\(hardwareInputFormat.sampleRate), CH=\(hardwareInputFormat.channelCount)") + } + + self.micAudioConverter = AVAudioConverter(from: hardwareInputFormat, to: outputFormat) guard self.micAudioConverter != nil else { - throw AudioManagerError.converterSetupError("Could not create main audio converter to Flutter format") + throw AudioManagerError.converterSetupError("Could not create audio converter from hardware input format to Flutter format") } self.micAudioConverter?.sampleRateConverterAlgorithm = AVSampleRateConverterAlgorithm_Mastering self.micAudioConverter?.sampleRateConverterQuality = .max self.micAudioConverter?.dither = true - print("DEBUG: Mic native format: \(self.micNodeFormat!))") - print("DEBUG: Flutter output format will be SR: \(Constants.flutterOutputSampleRate), CH: \(Constants.flutterOutputChannels)") + } + + private func removeMicTap() { + guard let micNode = micNode else { + return + } + + micNode.removeTap(onBus: 0) } private func installMicTap() { - micNode!.installTap(onBus: 0, bufferSize: Constants.micTapBufferSize, format: self.micNodeFormat!) { [weak self] (buffer, time) in + guard let micNode = micNode else { + print("ERROR: Cannot install mic tap - mic node is nil") + return + } + + removeMicTap() + + let hardwareInputFormat = micNode.inputFormat(forBus: 0) + + // Check if the format is valid for tap installation + guard hardwareInputFormat.sampleRate > 0 && hardwareInputFormat.channelCount > 0 else { + print("ERROR: Invalid hardware input format - cannot install tap") + return + } + micNode.installTap(onBus: 0, bufferSize: Constants.micTapBufferSize, format: hardwareInputFormat) { [weak self] (buffer, time) in guard let self = self, let finalConverter = self.micAudioConverter, let finalOutputFormat = self.outputAudioFormat else { return } @@ -1008,6 +1039,28 @@ class AudioManager: NSObject, SCStreamDelegate, SCStreamOutput { print("DEBUG: New input device name: \(deviceName)") } + // If currently recording, restart the audio engine to use the new device + if _isRecording { + print("DEBUG: Recording is active, restarting audio engine with new device") + Task { + do { + audioEngine?.stop() + removeMicTap() + audioEngine?.reset() + try await Task.sleep(nanoseconds: 100_000_000) + try audioEngine?.prepare() + try configureAudioFormatsAndConverter() + installMicTap() + try audioEngine?.start() + if isFlutterEngineActive { + self.screenCaptureChannel?.invokeMethod("microphoneDeviceChanged", arguments: nil) + } + } catch { + print("ERROR: Failed to restart audio engine with new device: \(error)") + } + } + } + return true } else { print("ERROR: Failed to set default input device: \(status)") diff --git a/app/macos/Runner/MainFlutterWindow.swift b/app/macos/Runner/MainFlutterWindow.swift index ba51a0f6b5b..af3cc2cce8f 100644 --- a/app/macos/Runner/MainFlutterWindow.swift +++ b/app/macos/Runner/MainFlutterWindow.swift @@ -257,15 +257,13 @@ class MainFlutterWindow: NSWindow, NSWindowDelegate { result(devices) case "selectAudioDevice": - guard let args = call.arguments as? [String: Any], - let deviceId = args["deviceId"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", - message: "Missing deviceId parameter", - details: nil)) - return + if let args = call.arguments as? [String: Any], + let deviceId = args["deviceId"] as? String { + let success = self.audioManager.selectAudioDevice(deviceID: deviceId) + result(success) + } else { + result(FlutterError(code: "INVALID_ARGUMENTS", message: "Device ID is required", details: nil)) } - let success = self.audioManager.selectAudioDevice(deviceID: deviceId) - result(success) default: result(FlutterMethodNotImplemented) From 9416c70dc235eb2186d4c240c7cb02419ef27a35 Mon Sep 17 00:00:00 2001 From: Krushna Kanta Rout <129386740+krushnarout@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:31:45 +0530 Subject: [PATCH 5/5] fix: close dropdown on outside click and fix device display issue --- .../widgets/desktop_recording_widget.dart | 64 +++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart b/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart index b6e28258797..f7b2d42ceef 100644 --- a/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart +++ b/app/lib/desktop/pages/conversations/widgets/desktop_recording_widget.dart @@ -40,6 +40,27 @@ class _DesktopRecordingWidgetState extends State { final GlobalKey _micKey = GlobalKey(); OverlayEntry? _audioDeviceOverlayEntry; + @override + void initState() { + super.initState(); + _loadSavedDeviceId(); + } + + Future _loadSavedDeviceId() async { + final prefs = await SharedPreferences.getInstance(); + final savedDeviceId = prefs.getString('selected_audio_device_id'); + if (savedDeviceId == null || savedDeviceId.isEmpty) return; + + _setSelectedDeviceId(savedDeviceId); + } + + void _setSelectedDeviceId(String deviceId) { + if (mounted) { + setState(() { + _selectedDeviceId = deviceId; + }); + } + } Future _toggleRecording(BuildContext context, CaptureProvider provider) async { var recordingState = provider.recordingState; @@ -944,19 +965,41 @@ void _removeAudioDeviceOverlay() { } OverlayEntry _createAudioDeviceOverlayEntry() { - final renderBox = _micKey.currentContext!.findRenderObject() as RenderBox; + final renderBox = _micKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox == null) { + return OverlayEntry(builder: (context) => const SizedBox.shrink()); + } + final size = renderBox.size; final offset = renderBox.localToGlobal(Offset.zero); return OverlayEntry( - builder: (context) => Positioned( - top: offset.dy + size.height + 5, - left: offset.dx - 100 + (size.width / 2), - child: _buildFloatingAudioDeviceDropdown(), + builder: (context) => GestureDetector( + onTap: _removeAudioDeviceOverlay, + behavior: HitTestBehavior.translucent, + child: Stack( + children: [ + _overlayBackground(), + Positioned( + top: offset.dy + size.height + 5, + left: offset.dx - 100 + (size.width / 2), + child: _overlayDropdown(), + ), + ], + ), ), ); } +Widget _overlayBackground() => Positioned.fill( + child: Container(color: Colors.transparent), +); + +Widget _overlayDropdown() => GestureDetector( + onTap: () {}, + child: _buildFloatingAudioDeviceDropdown(), +); + Future _selectAudioDevice(Map device) async { setState(() { _selectedDeviceId = device['id']; @@ -1089,6 +1132,15 @@ Widget _buildFloatingAudioDeviceItem(Map device) { ); } + String? _validateSelectedDevice(List> devices, String? currentId) { + if (currentId == null || currentId.isEmpty) { + return _findCurrentDeviceId(devices); + } + + final exists = devices.any((d) => d['id'] == currentId); + return exists ? currentId : _findCurrentDeviceId(devices); + } + Future _loadAvailableAudioDevices() async { final result = await const MethodChannel('screenCapturePlatform') .invokeMethod('getAvailableAudioDevices'); @@ -1101,7 +1153,7 @@ Widget _buildFloatingAudioDeviceItem(Map device) { setState(() { _availableAudioDevices = devices; - _selectedDeviceId ??= _findCurrentDeviceId(devices); + _selectedDeviceId = _validateSelectedDevice(devices, _selectedDeviceId); }); } }