Conversation
📝 WalkthroughWalkthroughAdds a new audio device selection feature: AudioDevice model and AudioDeviceType enum, an AudioDeviceController that periodically enumerates and selects audio outputs via WebRTC, an AudioDeviceSelectorDialog UI, integrations into call/live screens, new localization keys, and tests for the controller and model. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Dialog as AudioDeviceSelectorDialog
participant Controller as AudioDeviceController
participant WebRTC as MediaDevices / Helper
User->>Dialog: open selector (showAudioDeviceSelector)
Dialog->>Controller: create & call refreshDevices()
Controller->>WebRTC: enumerateDevices()
WebRTC-->>Controller: MediaDeviceInfo list
Controller->>Controller: filter audiooutput, set audioOutputDevices
Controller-->>Dialog: update device list
User->>Dialog: tap device
Dialog->>Controller: selectAudioOutput(device)
Controller->>WebRTC: Helper.selectAudioOutput(device)
WebRTC-->>Controller: success / error
Controller->>Controller: update selectedAudioOutput
Controller-->>Dialog: reflect selection
User->>Dialog: tap Done
Dialog-->>User: close
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
🎉 Welcome @Mayank4352!
We appreciate your contribution! 🚀 |
5c11f31 to
e5ff288
Compare
|
Hey @M4dhav, Kindly review this PR |
M4dhav
left a comment
There was a problem hiding this comment.
Please fix merge conflicts
|
Can you please explain the implementation a bit? What enumeration does, how audio devices are discovered, etc |
|
->The enumerateDevices() uses webrtc.navigator.mediaDevices.enumerateDevices() which queries for all available media devices (microphones, cameras, speakers). This is the main crux of the controller file, rest are the UI changes. I've made a simple data model that wraps WebRTC's MediaDeviceInfo which will Decouple the app from WebRTC's API, handles nullable fields (kind, groupId) with defaults and makes testing easier (we were able to create AudioDevice directly without WebRTC) |
| @@ -207,11 +208,11 @@ class LiveChapterScreenState extends State<LiveChapterScreen> { | |||
| ); | |||
| }, | |||
| style: ElevatedButton.styleFrom( | |||
| backgroundColor: const Color.fromARGB(255, 241, 108, 98), | |||
| backgroundColor: Colors.redAccent, | |||
| foregroundColor: Colors.white, | |||
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), | |||
| ), | |||
| icon: const Icon(Icons.exit_to_app), | |||
| label: Text(AppLocalizations.of(context)!.leaveButton), | |||
| child: const Icon(Icons.call_end, size: 24), | |||
There was a problem hiding this comment.
Why are there changes to the end call button?
There was a problem hiding this comment.
To make space the output change button as with the current size of end call button the ribbon was looking to cramped so i adjust it's size bit, also i saw that the color for the buttons was just a static color so i changed that to use the color from the theme
| Widget _buildAudioSettingsButton() { | ||
| return FloatingActionButton( | ||
| onPressed: () => showAudioDeviceSelector(context), | ||
| backgroundColor: Theme.of(context).colorScheme.secondary, | ||
| child: const Icon(Icons.settings_voice), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Why FloatingActionButton and not ElevatedButton.icon like the others?
There was a problem hiding this comment.
I used a FloatingActionButton initially while iterating on the layout, I'll switch it to an ElevatedButton.icon
There was a problem hiding this comment.
Hey, @M4dhav after reviewing it, i believe the other widgets also use FAB, do you still want me to make it ElevatedButton.icon or should i keep it the way it is currently???
| @@ -205,13 +207,13 @@ class RoomScreenState extends State<RoomScreen> { | |||
| ); | |||
| }, | |||
| style: ElevatedButton.styleFrom( | |||
| backgroundColor: const Color.fromARGB(255, 241, 108, 98), | |||
| backgroundColor: Colors.redAccent, | |||
| foregroundColor: Colors.white, | |||
| shape: RoundedRectangleBorder( | |||
| borderRadius: BorderRadius.circular(20), | |||
| ), | |||
| ), | |||
| icon: const Icon(Icons.exit_to_app), | |||
| label: Text(AppLocalizations.of(context)!.leaveButton), | |||
| child: const Icon(Icons.call_end, size: 24), | |||
There was a problem hiding this comment.
Unrelated changes to End Call button
There was a problem hiding this comment.
Those are needed for the space to not look cramped, i just made some cosmetic changes in the icon for it to look more like a end call button, initially it used static color, i replaced that with the theme one
| Widget _buildAudioSettingsButton() { | ||
| return FloatingActionButton( | ||
| onPressed: () => showAudioDeviceSelector(context), | ||
| backgroundColor: Theme.of(context).colorScheme.onSecondary, | ||
| child: const Icon(Icons.volume_up), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Will fix it, used it while playing around with UI
| if (!Get.isRegistered<AudioDeviceController>()) { | ||
| Get.put(AudioDeviceController()); | ||
| } |
There was a problem hiding this comment.
Instead of using GetX here, just instantiate an instance within the dialog, as you do not need dependency injection here
| Get.put(AudioDeviceController()); | ||
| } | ||
| final controller = AudioDeviceController(); | ||
| controller.refreshDevices(); |
There was a problem hiding this comment.
refreshDevices is async so await this call
| showModalBottomSheet( | ||
| context: context, | ||
| isScrollControlled: true, | ||
| backgroundColor: Colors.transparent, | ||
| builder: (context) => const AudioDeviceSelectorDialog(), | ||
| ); | ||
| builder: (context) => AudioDeviceSelectorDialog(controller: controller), | ||
| ).whenComplete(() => controller.dispose()); |
There was a problem hiding this comment.
use GetX APIs to show the modal
| IconButton( | ||
| icon: const Icon(Icons.close), | ||
| onPressed: () => Get.back(), | ||
| onPressed: () => Navigator.of(context).pop(), |
| SizedBox(width: UiSizes.width_8), | ||
| ElevatedButton( | ||
| onPressed: () => Get.back(), | ||
| onPressed: () => Navigator.of(context).pop(), |
M4dhav
left a comment
There was a problem hiding this comment.
Refactor to include the new enum as one of the members of the AudioDevice class
and good work on the refactor to make it an enum
| final controller = AudioDeviceController(); | ||
| final controller = Get.put(AudioDeviceController(), permanent: true); | ||
| await controller.refreshDevices(); | ||
|
|
||
| Get.bottomSheet( | ||
| AudioDeviceSelectorDialog(controller: controller), | ||
| isScrollControlled: true, | ||
| backgroundColor: Colors.transparent, | ||
| ).whenComplete(() => controller.dispose()); | ||
| ); |
There was a problem hiding this comment.
Saw a bug regarding, the selection dialog- it was not persisting the devices selected by user since the controller was getting disposed
There was a problem hiding this comment.
Since it is marked permanent, make sure to handle the disposal manually, i.e: Dispose this along with the rest of the room related controllers
| String getDeviceIcon(AudioDevice device) { | ||
| final label = device.label.toLowerCase(); | ||
| if (label.contains('earpiece') || label.contains('receiver')) { | ||
| return 'phone'; | ||
| } else if (label.contains('bluetooth')) { | ||
| return 'bluetooth_audio'; | ||
| } else if (label.contains('headset') || label.contains('headphone')) { | ||
| return 'headset'; | ||
| } else if (label.contains('speaker')) { | ||
| return 'speaker'; | ||
| } | ||
| return 'volume_up'; | ||
| return getDeviceType(device).iconName; | ||
| } |
There was a problem hiding this comment.
No need of this function anymore
M4dhav
left a comment
There was a problem hiding this comment.
AudioDeviceType is still not a class member for AudioDevice
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
lib/views/screens/room_screen.dart (2)
166-188: Potential Hero tag collision: setheroTag(and tooltip) for footer FABsThis screen already uses multiple
FloatingActionButtons in the same subtree; adding another increases the chance of a duplicate Hero tag crash during route transitions. Give the new FAB a uniqueheroTag(orheroTag: null) and add a tooltip for accessibility; ideally do the same for the other footer FABs too.Proposed fix (apply similarly to other footer FABs)
Widget _buildAudioSettingsButton() { return FloatingActionButton( - onPressed: () async => await showAudioDeviceSelector(context), - backgroundColor: Theme.of(context).colorScheme.onSecondary, - child: const Icon(Icons.volume_up), + heroTag: 'room_audio_settings_fab', + tooltip: AppLocalizations.of(context)!.audioOutput, + onPressed: () => showAudioDeviceSelector(context), + backgroundColor: Theme.of(context).colorScheme.onSecondary, + child: const Icon(Icons.volume_up), ); }Also applies to: 296-302
190-219: Leave button became icon-only: add tooltip/semantics to preserve accessibilitySince the text label was removed, please add a
Tooltip/Semanticslabel (and consider dynamic text for delete vs leave).Example
Widget _buildLeaveButton() { return GetBuilder<SingleRoomController>( init: SingleRoomController(appwriteRoom: widget.room), builder: (controller) { - return ElevatedButton( + final label = controller.appwriteRoom.isUserAdmin + ? AppLocalizations.of(context)!.delete + : AppLocalizations.of(context)!.leaveButton; + return Tooltip( + message: label, + child: Semantics( + label: label, + button: true, + child: ElevatedButton( onPressed: () async { await _deleteRoomDialog( controller.appwriteRoom.isUserAdmin ? AppLocalizations.of(context)!.delete : AppLocalizations.of(context)!.leave, () async { if (controller.appwriteRoom.isUserAdmin) { await controller.deleteRoom(); } else { await controller.leaveRoom(); } }, ); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ) ), child: const Icon(Icons.call_end, size: 24), - ); + ), + ), + ); }, ); }lib/views/screens/live_chapter_screen.dart (2)
158-182: Potential Hero tag collision + missing tooltip: setheroTag/tooltipfor the new FAB (and align footer FAB tags)Same multi-FAB concern as the room screen: assign a unique
heroTag(ornull) and add a tooltip; ideally make all footer FABs consistent.Proposed fix
Widget _buildAudioSettingsButton() { return FloatingActionButton( - onPressed: () async => await showAudioDeviceSelector(context), + heroTag: 'live_chapter_audio_settings_fab', + tooltip: AppLocalizations.of(context)!.audioOutput, + onPressed: () => showAudioDeviceSelector(context), backgroundColor: Theme.of(context).colorScheme.secondary, child: const Icon(Icons.settings_voice), ); }Also applies to: 266-272
184-217: Leave button became icon-only: add tooltip/semantics (delete vs leave)
🤖 Fix all issues with AI agents
In @lib/controllers/audio_device_controller.dart:
- Around line 46-54: In selectAudioOutput move the UI state update so it only
happens after the system call succeeds: call await
webrtc.Helper.selectAudioOutput(device.deviceId) first, then set
selectedAudioOutput.value = device; additionally capture the previous
selectedAudioOutput.value before attempting the switch and in the catch block
either leave it unchanged or explicitly revert to the captured previous device
and log the error (use the existing log call). Ensure you reference the
selectAudioOutput function, selectedAudioOutput.value, and
webrtc.Helper.selectAudioOutput when making the change.
In @lib/controllers/friend_call_screen.dart:
- Around line 132-140: Replace the hardcoded label 'Audio Options' passed into
_buildControlButton with a localized string: use AppLocalizations.of(context)!
to provide the text (either AppLocalizations.of(context)!.audioOutput if that
existing key fits, or add a new audioOptions key to the localization ARB files
and use AppLocalizations.of(context)!.audioOptions). Update the call site where
_buildControlButton is invoked with onPressed: () async => await
showAudioDeviceSelector(context) to pass the localized label and ensure the
heroTag ("audio-settings") and backgroundColor logic remain unchanged.
In @lib/l10n/app_localizations_bn.dart:
- Around line 1357-1371: Replace the English strings with Bengali translations
for the listed getters: update audioOutput to 'অডিও আউটপুট',
selectPreferredSpeaker to 'আপনার পছন্দসই স্পিকার নির্বাচন করুন',
noAudioOutputDevices to 'কোন অডিও আউটপুট ডিভাইস পাওয়া যায়নি', refresh to
'রিফ্রেশ' (or 'পুনরায় লোড' if preferred), and done to 'সম্পন্ন' by editing the
return values in the corresponding getters (audioOutput, selectPreferredSpeaker,
noAudioOutputDevices, refresh, done) so the Bengali localization file no longer
contains English strings.
In @lib/l10n/app_localizations_kn.dart:
- Around line 1353-1367: Replace the English strings for the five localization
getters with the provided Kannada translations: update the getter audioOutput to
'ಆಡಿಯೋ ಔಟ್ಪುಟ್', selectPreferredSpeaker to 'ನಿಮ್ಮ ಆದ್ಯತೆಯ ಸ್ಪೀಕರ್ ಆಯ್ಕೆಮಾಡಿ',
noAudioOutputDevices to 'ಯಾವುದೇ ಆಡಿಯೋ ಔಟ್ಪುಟ್ ಸಾಧನಗಳು ಪತ್ತೆಯಾಗಿಲ್ಲ', refresh to
'ರಿಫ್ರೆಶ್', and done to 'ಮುಗಿದಿದೆ' so the getters audioOutput,
selectPreferredSpeaker, noAudioOutputDevices, refresh, and done return the
correct Kannada text.
In @lib/l10n/app_localizations_mr.dart:
- Around line 1347-1361: The Marathi localization file has several getters still
returning English; update the getters audioOutput, selectPreferredSpeaker,
noAudioOutputDevices, refresh, and done in app_localizations_mr.dart to provide
proper Marathi translations (or explicitly call the English fallback/localized
lookup if you intend to fall back rather than embed English). Ensure each getter
returns the correct Marathi string for "Audio Output", "Select your preferred
speaker", "No audio output devices detected", "Refresh", and "Done"
respectively, and keep the getter names unchanged.
In @lib/models/audio_device.dart:
- Around line 19-28: In AudioDevice.fromMediaDeviceInfo, ensure deviceId and
label are null-safe by applying null coalescing: set deviceId to
deviceInfo.deviceId ?? '' and normalize label to a non-empty fallback (e.g.,
label != null && label.isNotEmpty ? label : 'Unknown Device'); this prevents
nulls propagating into the AudioDevice instance used by the equality operator
(operator ==) and hashCode and avoids runtime errors — update the factory
constructor (AudioDevice.fromMediaDeviceInfo) to apply these fallbacks
consistently along with the existing kind/groupId coalescing.
In @lib/views/widgets/audio_selector_dialog.dart:
- Around line 13-116: The Column uses mainAxisSize: MainAxisSize.min while
containing a Flexible widget (Flexible -> Obx -> SingleChildScrollView ->
_buildDeviceList), which can cause unbounded height assertions in bottom sheets;
fix by constraining the scroll area: either change the Column to mainAxisSize:
MainAxisSize.max and replace Flexible with Expanded, or keep Column.min and wrap
the Obx/SingleChildScrollView/_buildDeviceList chain in a
SizedBox/ConstrainedBox with an explicit maxHeight (e.g., fraction of
MediaQuery.of(context).size.height); update references to Column(mainAxisSize:
min), Flexible, Obx, SingleChildScrollView, and _buildDeviceList accordingly.
- Around line 248-257: The current showAudioDeviceSelector registers
AudioDeviceController as permanent which prevents its onClose() from running and
leaves its 5s polling timer alive; fix by removing permanent: true from
Get.put(AudioDeviceController()), await the Get.bottomSheet(...) call so it
completes when the sheet is dismissed, and then call
Get.delete<AudioDeviceController>() (or Get.delete(controller) using the
controller instance) to ensure the controller's onClose() runs and the timer is
cancelled; keep using AudioDeviceSelectorDialog(controller: controller) and
still call controller.refreshDevices() before showing the sheet.
🧹 Nitpick comments (3)
test/controllers/audio_device_controller_test.dart (1)
1-193: Good baseline coverage; consider testing the controller’s real enumeration/filter path (vs re-implementing it in the test)Right now the “filter only output devices” test manually does the filtering; it would be stronger to assert the controller’s own enumerate/devices API (with mocking/fakes) performs that filtering and updates
selectedAudioOutputas expected.lib/utils/enums/audio_device_enum.dart (1)
6-7: Improve icon differentiation for USB and unknown devices.Both
usbandunknowndevice types use the same'volume_up'icon, making them visually indistinguishable in the UI. Consider using a more specific icon for USB devices (e.g.,'usb') to improve user recognition.♻️ Suggested icon improvement
-usb('volume_up', 'USB Audio'), +usb('usb', 'USB Audio'),lib/views/widgets/audio_selector_dialog.dart (1)
159-184: Prefer mapping fromAudioDeviceType→IconData(avoid stringly-typed icons).
iconName+ string switch is brittle; consider movingIconDatamapping intoAudioDeviceType(or a helper) and switching on the enum directly.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
lib/controllers/audio_device_controller.dartlib/controllers/friend_call_screen.dartlib/l10n/app_en.arblib/l10n/app_hi.arblib/l10n/app_localizations.dartlib/l10n/app_localizations_bn.dartlib/l10n/app_localizations_en.dartlib/l10n/app_localizations_gu.dartlib/l10n/app_localizations_hi.dartlib/l10n/app_localizations_kn.dartlib/l10n/app_localizations_mr.dartlib/l10n/app_localizations_pa.dartlib/l10n/app_pa.arblib/models/audio_device.dartlib/utils/enums/audio_device_enum.dartlib/views/screens/live_chapter_screen.dartlib/views/screens/room_screen.dartlib/views/widgets/audio_selector_dialog.darttest/controllers/audio_device_controller_test.dartuntranslated.txt
🔇 Additional comments (15)
lib/l10n/app_localizations_pa.dart (1)
1309-1323: LGTM! Punjabi translations added correctly.The five new audio output localization getters are properly translated to Punjabi and follow the existing code structure. Good work on the translations.
lib/l10n/app_pa.arb (1)
1759-1778: LGTM! ARB entries properly structured.The five new localization entries follow the correct ARB format with proper descriptions and metadata. The Punjabi translations are consistent with the corresponding Dart file.
untranslated.txt (2)
2-8: Untranslated keys correctly tracked for Bengali locale.The five audio output keys listed here for the Bengali (bn) locale are consistent with the finding in
lib/l10n/app_localizations_bn.dartwhere English strings are used instead of Bengali translations.Once the Bengali translations are added in
app_localizations_bn.dart, remember to remove these entries from this section.
42-46: Untranslated keys tracked for Gujarati, Kannada, and Marathi.These entries correctly track that the five new audio output keys need translations for:
- Gujarati (gu) - lines 42-46
- Kannada (kn) - lines 67-71
- Marathi (mr) - lines 94-98
This is expected for a new feature. Consider adding these translations in a follow-up PR or coordinating with translators.
Also applies to: 67-71, 94-98
lib/l10n/app_hi.arb (1)
1761-1780: Localized strings added cleanly; please verify ARB validity + key uniqueness (done/refresh)The additions look fine, but
done/refreshare very generic keys—please ensure they don’t already exist elsewhere and thatflutter gen-l10n(or your l10n generation step) succeeds.lib/l10n/app_en.arb (1)
1757-1780: LGTM for new strings; please verify ARB formatting + no collisions fordone/refreshLooks consistent with the feature, but please double-check generation passes and these generic keys don’t conflict with existing ones.
lib/l10n/app_localizations_en.dart (1)
1346-1360: LGTM! English localization keys are well-defined.The new audio output localization strings are clear, concise, and follow the established pattern in the file.
lib/models/audio_device.dart (3)
4-17: LGTM! Well-structured immutable model.The AudioDevice model follows good practices with final fields and a clear constructor signature.
30-30: LGTM! Correct audio output check.The getter correctly identifies audio output devices using the WebRTC standard
'audiooutput'kind.
32-39: Verify that deviceId-only equality is intentional.The equality operator and hashCode only consider
deviceId, ignoringlabel,kind,groupId, anddeviceType. Two AudioDevice instances with the samedeviceIdbut different labels or types will be treated as equal. Confirm this is the intended behavior.If devices should be distinguished by more than just
deviceId, consider expanding the equality check:♻️ Alternative equality implementation
@override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is AudioDevice && other.deviceId == deviceId; + return other is AudioDevice && + other.deviceId == deviceId && + other.kind == kind && + other.deviceType == deviceType; } @override -int get hashCode => deviceId.hashCode; +int get hashCode => Object.hash(deviceId, kind, deviceType);lib/utils/enums/audio_device_enum.dart (1)
14-33: Verify device type prioritization for overlapping labels.The
fromLabelmethod uses sequential if-else checks that could classify devices ambiguously. For example, a device labeled "Bluetooth Headset" will match thebluetoothcheck (line 17) and returnbluetoothAudio, never reaching theheadsetcheck (line 22).Confirm whether the current prioritization is intentional:
bluetooth>earpiece>headset>speaker>usbIf devices should be classified by more specific attributes (e.g., connection type vs. form factor), consider refining the logic:
🔍 Example ambiguous cases
- "Bluetooth Headset" → currently returns
bluetoothAudio, notheadset- "USB Speaker" → currently returns
speaker, notusb- "Wired Bluetooth Adapter" → currently returns
bluetoothAudio, notheadsetlib/l10n/app_localizations_hi.dart (1)
1350-1364: LGTM! Hindi translations are properly implemented.The new audio output localization strings are correctly translated to Hindi and follow the established pattern in the file.
lib/l10n/app_localizations.dart (1)
2473-2502: Localization surface extension looks consistent.Docs + getters are wired cleanly for the new dialog strings. Only note:
refresh/doneare very generic keys—worth confirming there’s no semantic clash with other UI in the app as l10n grows.lib/views/widgets/audio_selector_dialog.dart (1)
38-41: Confirm Flutter version supportsColor.withValues(...).If the repo’s Flutter SDK is older,
withValuesmay not compile;withOpacityis the more widely-supported alternative.Also applies to: 62-65, 189-197, 216-216
lib/controllers/audio_device_controller.dart (1)
4-4: Confirmflutter_webrtcsupportsenumerateDevices+Helper.selectAudioOutputon all target platforms.These APIs can be platform-dependent (especially output routing); please verify behavior/availability for your supported platforms (Android/iOS/Web/Desktop) and gate accordingly if needed.
Also applies to: 31-33, 46-50, 56-67
| @override | ||
| void onInit() { | ||
| super.onInit(); | ||
| enumerateDevices(); | ||
| _deviceEnumerationTimer = Timer.periodic( | ||
| const Duration(seconds: 5), | ||
| (_) => enumerateDevices(), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Prevent overlapping polls + keep selection consistent with current device list.
Timer.periodic can re-enter enumerateDevices() while a prior call is still in-flight, and selectedAudioOutput can remain set to a device that no longer exists.
Proposed fix (in-flight guard + selection reconciliation)
class AudioDeviceController extends GetxController {
final RxList<AudioDevice> audioOutputDevices = <AudioDevice>[].obs;
final Rx<AudioDevice?> selectedAudioOutput = Rx<AudioDevice?>(null);
Timer? _deviceEnumerationTimer;
+ bool _isEnumerating = false;
@override
void onInit() {
super.onInit();
enumerateDevices();
_deviceEnumerationTimer = Timer.periodic(
const Duration(seconds: 5),
(_) => enumerateDevices(),
);
}
Future<void> enumerateDevices() async {
+ if (_isEnumerating) return;
+ _isEnumerating = true;
try {
final devices = await webrtc.navigator.mediaDevices.enumerateDevices();
final outputs = devices
.map((device) => AudioDevice.fromMediaDeviceInfo(device))
.where((d) => d.isAudioOutput)
.toList();
audioOutputDevices.value = outputs;
- selectedAudioOutput.value ??= outputs.firstOrNull;
+ final selectedId = selectedAudioOutput.value?.deviceId;
+ final stillPresent = selectedId != null &&
+ outputs.any((d) => d.deviceId == selectedId);
+ if (!stillPresent) {
+ selectedAudioOutput.value = outputs.firstOrNull;
+ }
log('Enumerated ${outputs.length} output devices');
} catch (e) {
log('Error enumerating devices: $e');
+ } finally {
+ _isEnumerating = false;
}
}Also applies to: 31-44
| Future<void> selectAudioOutput(AudioDevice device) async { | ||
| try { | ||
| selectedAudioOutput.value = device; | ||
| await webrtc.Helper.selectAudioOutput(device.deviceId); | ||
| log('Selected audio output: ${device.label}'); | ||
| } catch (e) { | ||
| log('Error selecting audio output: $e'); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "audio_device_controller.dart" -type fRepository: AOSSIE-Org/Resonate
Length of output: 109
🏁 Script executed:
cat -n ./lib/controllers/audio_device_controller.dart | head -70Repository: AOSSIE-Org/Resonate
Length of output: 2679
🏁 Script executed:
cat -n ./lib/controllers/audio_device_controller.dart | tail -30Repository: AOSSIE-Org/Resonate
Length of output: 1115
🏁 Script executed:
# Search for other device selection methods in the controller
rg "selectAudio|setAudio" ./lib/controllers/audio_device_controller.dartRepository: AOSSIE-Org/Resonate
Length of output: 185
🏁 Script executed:
# Search for all methods that set selectedAudio* values in this file
rg "selectedAudio" ./lib/controllers/audio_device_controller.dartRepository: AOSSIE-Org/Resonate
Length of output: 232
🏁 Script executed:
# Check if there's a selectAudioInput method with the same pattern
rg -A 10 "selectAudioInput|inputDevice" ./lib/controllers/audio_device_controller.dartRepository: AOSSIE-Org/Resonate
Length of output: 45
🏁 Script executed:
# Search for webrtc.Helper.selectAudioOutput usage elsewhere
rg "selectAudioOutput" --type dartRepository: AOSSIE-Org/Resonate
Length of output: 665
🏁 Script executed:
cat -n ./test/controllers/audio_device_controller_test.dartRepository: AOSSIE-Org/Resonate
Length of output: 6991
Move selectedAudioOutput.value = device after the system-level switch succeeds to prevent UI state from diverging if the operation fails.
Currently, selectedAudioOutput.value is set before awaiting webrtc.Helper.selectAudioOutput(). If the system call throws an exception, the UI shows the device as selected even though the actual switch failed. The error handler only logs the error without reverting the UI state.
Proposed fix
Future<void> selectAudioOutput(AudioDevice device) async {
try {
- selectedAudioOutput.value = device;
await webrtc.Helper.selectAudioOutput(device.deviceId);
+ selectedAudioOutput.value = device;
log('Selected audio output: ${device.label}');
} catch (e) {
log('Error selecting audio output: $e');
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Future<void> selectAudioOutput(AudioDevice device) async { | |
| try { | |
| selectedAudioOutput.value = device; | |
| await webrtc.Helper.selectAudioOutput(device.deviceId); | |
| log('Selected audio output: ${device.label}'); | |
| } catch (e) { | |
| log('Error selecting audio output: $e'); | |
| } | |
| } | |
| Future<void> selectAudioOutput(AudioDevice device) async { | |
| try { | |
| await webrtc.Helper.selectAudioOutput(device.deviceId); | |
| selectedAudioOutput.value = device; | |
| log('Selected audio output: ${device.label}'); | |
| } catch (e) { | |
| log('Error selecting audio output: $e'); | |
| } | |
| } |
🤖 Prompt for AI Agents
In @lib/controllers/audio_device_controller.dart around lines 46 - 54, In
selectAudioOutput move the UI state update so it only happens after the system
call succeeds: call await webrtc.Helper.selectAudioOutput(device.deviceId)
first, then set selectedAudioOutput.value = device; additionally capture the
previous selectedAudioOutput.value before attempting the switch and in the catch
block either leave it unchanged or explicitly revert to the captured previous
device and log the error (use the existing log call). Ensure you reference the
selectAudioOutput function, selectedAudioOutput.value, and
webrtc.Helper.selectAudioOutput when making the change.
| @override | ||
| String get audioOutput => 'Audio Output'; | ||
|
|
||
| @override | ||
| String get selectPreferredSpeaker => 'Select your preferred speaker'; | ||
|
|
||
| @override | ||
| String get noAudioOutputDevices => 'No audio output devices detected'; | ||
|
|
||
| @override | ||
| String get refresh => 'Refresh'; | ||
|
|
||
| @override | ||
| String get done => 'Done'; | ||
|
|
There was a problem hiding this comment.
English strings used instead of Bengali translations.
Lines 1357-1371 contain English strings instead of proper Bengali (বাংলা) translations. This defeats the purpose of localization for Bengali users.
The strings should be translated to Bengali:
- 'Audio Output' → should be in Bengali
- 'Select your preferred speaker' → should be in Bengali
- 'No audio output devices detected' → should be in Bengali
- 'Refresh' → should be in Bengali
- 'Done' → should be in Bengali
Note: These keys are correctly listed in untranslated.txt (lines 2-8), which confirms they need proper translation.
💡 Suggested Bengali translations
Here are suggested Bengali translations (please verify with a native speaker):
@override
-String get audioOutput => 'Audio Output';
+String get audioOutput => 'অডিও আউটপুট';
@override
-String get selectPreferredSpeaker => 'Select your preferred speaker';
+String get selectPreferredSpeaker => 'আপনার পছন্দের স্পিকার নির্বাচন করুন';
@override
-String get noAudioOutputDevices => 'No audio output devices detected';
+String get noAudioOutputDevices => 'কোনও অডিও আউটপুট ডিভাইস সনাক্ত করা যায়নি';
@override
-String get refresh => 'Refresh';
+String get refresh => 'রিফ্রেশ করুন';
@override
-String get done => 'Done';
+String get done => 'সম্পন্ন';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @override | |
| String get audioOutput => 'Audio Output'; | |
| @override | |
| String get selectPreferredSpeaker => 'Select your preferred speaker'; | |
| @override | |
| String get noAudioOutputDevices => 'No audio output devices detected'; | |
| @override | |
| String get refresh => 'Refresh'; | |
| @override | |
| String get done => 'Done'; | |
| @override | |
| String get audioOutput => 'অডিও আউটপুট'; | |
| @override | |
| String get selectPreferredSpeaker => 'আপনার পছন্দের স্পিকার নির্বাচন করুন'; | |
| @override | |
| String get noAudioOutputDevices => 'কোনও অডিও আউটপুট ডিভাইস সনাক্ত করা যায়নি'; | |
| @override | |
| String get refresh => 'রিফ্রেশ করুন'; | |
| @override | |
| String get done => 'সম্পন্ন'; |
🤖 Prompt for AI Agents
In @lib/l10n/app_localizations_bn.dart around lines 1357 - 1371, Replace the
English strings with Bengali translations for the listed getters: update
audioOutput to 'অডিও আউটপুট', selectPreferredSpeaker to 'আপনার পছন্দসই স্পিকার
নির্বাচন করুন', noAudioOutputDevices to 'কোন অডিও আউটপুট ডিভাইস পাওয়া যায়নি',
refresh to 'রিফ্রেশ' (or 'পুনরায় লোড' if preferred), and done to 'সম্পন্ন' by
editing the return values in the corresponding getters (audioOutput,
selectPreferredSpeaker, noAudioOutputDevices, refresh, done) so the Bengali
localization file no longer contains English strings.
| @override | ||
| String get audioOutput => 'Audio Output'; | ||
|
|
||
| @override | ||
| String get selectPreferredSpeaker => 'Select your preferred speaker'; | ||
|
|
||
| @override | ||
| String get noAudioOutputDevices => 'No audio output devices detected'; | ||
|
|
||
| @override | ||
| String get refresh => 'Refresh'; | ||
|
|
||
| @override | ||
| String get done => 'Done'; | ||
|
|
There was a problem hiding this comment.
Gujarati locale strings are left in English (likely placeholder).
These new getters return English text in the gu localization file; if this isn’t intentional, please provide Gujarati translations (or explicitly fall back to English via ARB/default locale rather than per-locale English strings).
| @override | ||
| String get audioOutput => 'Audio Output'; | ||
|
|
||
| @override | ||
| String get selectPreferredSpeaker => 'Select your preferred speaker'; | ||
|
|
||
| @override | ||
| String get noAudioOutputDevices => 'No audio output devices detected'; | ||
|
|
||
| @override | ||
| String get refresh => 'Refresh'; | ||
|
|
||
| @override | ||
| String get done => 'Done'; | ||
|
|
There was a problem hiding this comment.
Missing Kannada translations for new audio output keys.
The five new localization getters return English strings instead of Kannada translations, breaking the localization pattern established throughout the file. All other strings in this file are properly translated to Kannada.
Expected Kannada translations for:
audioOutput→ ಆಡಿಯೋ ಔಟ್ಪುಟ್selectPreferredSpeaker→ ನಿಮ್ಮ ಆದ್ಯತೆಯ ಸ್ಪೀಕರ್ ಆಯ್ಕೆಮಾಡಿnoAudioOutputDevices→ ಯಾವುದೇ ಆಡಿಯೋ ಔಟ್ಪುಟ್ ಸಾಧನಗಳು ಪತ್ತೆಯಾಗಿಲ್ಲrefresh→ ರಿಫ್ರೆಶ್done→ ಮುಗಿದಿದೆ
🤖 Prompt for AI Agents
In @lib/l10n/app_localizations_kn.dart around lines 1353 - 1367, Replace the
English strings for the five localization getters with the provided Kannada
translations: update the getter audioOutput to 'ಆಡಿಯೋ ಔಟ್ಪುಟ್',
selectPreferredSpeaker to 'ನಿಮ್ಮ ಆದ್ಯತೆಯ ಸ್ಪೀಕರ್ ಆಯ್ಕೆಮಾಡಿ',
noAudioOutputDevices to 'ಯಾವುದೇ ಆಡಿಯೋ ಔಟ್ಪುಟ್ ಸಾಧನಗಳು ಪತ್ತೆಯಾಗಿಲ್ಲ', refresh to
'ರಿಫ್ರೆಶ್', and done to 'ಮುಗಿದಿದೆ' so the getters audioOutput,
selectPreferredSpeaker, noAudioOutputDevices, refresh, and done return the
correct Kannada text.
| @override | ||
| String get audioOutput => 'Audio Output'; | ||
|
|
||
| @override | ||
| String get selectPreferredSpeaker => 'Select your preferred speaker'; | ||
|
|
||
| @override | ||
| String get noAudioOutputDevices => 'No audio output devices detected'; | ||
|
|
||
| @override | ||
| String get refresh => 'Refresh'; | ||
|
|
||
| @override | ||
| String get done => 'Done'; | ||
|
|
There was a problem hiding this comment.
Marathi locale strings are left in English (likely placeholder).
These new getters return English text in the mr localization file; please translate to Marathi (or intentionally fall back to en rather than embedding English in mr).
🤖 Prompt for AI Agents
In @lib/l10n/app_localizations_mr.dart around lines 1347 - 1361, The Marathi
localization file has several getters still returning English; update the
getters audioOutput, selectPreferredSpeaker, noAudioOutputDevices, refresh, and
done in app_localizations_mr.dart to provide proper Marathi translations (or
explicitly call the English fallback/localized lookup if you intend to fall back
rather than embed English). Ensure each getter returns the correct Marathi
string for "Audio Output", "Select your preferred speaker", "No audio output
devices detected", "Refresh", and "Done" respectively, and keep the getter names
unchanged.
| factory AudioDevice.fromMediaDeviceInfo(MediaDeviceInfo deviceInfo) { | ||
| final label = deviceInfo.label; | ||
| return AudioDevice( | ||
| deviceId: deviceInfo.deviceId, | ||
| label: label, | ||
| kind: deviceInfo.kind ?? '', | ||
| groupId: deviceInfo.groupId ?? '', | ||
| deviceType: AudioDeviceType.fromLabel(label), | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find where MediaDeviceInfo is defined or imported
rg -n "MediaDeviceInfo" lib/ --type=dart | head -20Repository: AOSSIE-Org/Resonate
Length of output: 282
🏁 Script executed:
# Check the AudioDevice class for equality implementation (around line 35)
cat -n lib/models/audio_device.dart | sed -n '30,40p'Repository: AOSSIE-Org/Resonate
Length of output: 405
🏁 Script executed:
# Search for MediaDeviceInfo definition/import across the codebase
fd . -type f -name "*.dart" | xargs rg -l "class MediaDeviceInfo|typedef MediaDeviceInfo"Repository: AOSSIE-Org/Resonate
Length of output: 235
🏁 Script executed:
# Check imports in audio_device.dart
head -30 lib/models/audio_device.dartRepository: AOSSIE-Org/Resonate
Length of output: 864
🏁 Script executed:
# Search for MediaDeviceInfo definition without type filter
rg -n "class MediaDeviceInfo|typedef MediaDeviceInfo" --type-list | head -5Repository: AOSSIE-Org/Resonate
Length of output: 452
🏁 Script executed:
# Search in web_audio or similar directories
find lib -type f -name "*.dart" -exec rg -l "MediaDeviceInfo" {} \;Repository: AOSSIE-Org/Resonate
Length of output: 136
🏁 Script executed:
# Check if there's a pubspec.yaml to see flutter_webrtc version
cat pubspec.yaml | grep -A2 "flutter_webrtc"Repository: AOSSIE-Org/Resonate
Length of output: 141
🏁 Script executed:
# Search for any other usage patterns of MediaDeviceInfo fields in the codebase
rg -n "deviceInfo\.(deviceId|label|kind|groupId)" lib/ -A1Repository: AOSSIE-Org/Resonate
Length of output: 531
🏁 Script executed:
# Check the audio_device_controller to understand how devices are processed
cat -n lib/controllers/audio_device_controller.dart | sed -n '30,45p'Repository: AOSSIE-Org/Resonate
Length of output: 695
🌐 Web query:
flutter_webrtc MediaDeviceInfo deviceId label nullable
💡 Result:
Short answer: Yes — deviceId and label can be null/empty depending on platform and permissions. The browser/WebRTC spec and MDN note labels (and deviceId visibility) are blank until getUserMedia permission is granted; Dart's web API exposes deviceId as nullable (String?). Flutter WebRTC docs likewise show you must call getUserMedia / request permission before enumerateDevices returns usable labels/IDs. Check for null/empty and request media permission first. [1][2][3][4]
References
- MDN: MediaDeviceInfo (label/deviceId visibility tied to permission). [1]
- flutter-webrtc docs (enumerateDevices / getUserMedia behavior). [2]
- dart:html MediaDeviceInfo.deviceId — String? (nullable). [3]
- webrtc_interface MediaDeviceInfo docs (implementation differences across platforms). [4]
🏁 Script executed:
# Check if there's any null safety configuration in the project
cat analysis_options.yaml 2>/dev/null || cat pubspec.yaml | grep -A10 "environment"Repository: AOSSIE-Org/Resonate
Length of output: 1580
Apply null coalescing consistently to all MediaDeviceInfo fields.
The factory method applies null coalescing to kind and groupId (lines 24–25) but not to deviceId and label (lines 22–23). According to the WebRTC specification and Dart's null-safety model, MediaDeviceInfo.deviceId and label can be null depending on platform and permissions—particularly before getUserMedia() permission is granted. Since deviceId is used in the equality operator (line 35) and hashCode (line 39), passing a null value will cause a runtime error.
Apply null coalescing to both fields:
deviceId: deviceInfo.deviceId ?? '',
label: label != null && label.isNotEmpty ? label : 'Unknown Device',🤖 Prompt for AI Agents
In @lib/models/audio_device.dart around lines 19 - 28, In
AudioDevice.fromMediaDeviceInfo, ensure deviceId and label are null-safe by
applying null coalescing: set deviceId to deviceInfo.deviceId ?? '' and
normalize label to a non-empty fallback (e.g., label != null && label.isNotEmpty
? label : 'Unknown Device'); this prevents nulls propagating into the
AudioDevice instance used by the equality operator (operator ==) and hashCode
and avoids runtime errors — update the factory constructor
(AudioDevice.fromMediaDeviceInfo) to apply these fallbacks consistently along
with the existing kind/groupId coalescing.
| Widget build(BuildContext context) { | ||
| return Container( | ||
| decoration: BoxDecoration( | ||
| color: Theme.of(context).colorScheme.surface, | ||
| borderRadius: BorderRadius.vertical( | ||
| top: Radius.circular(UiSizes.width_20), | ||
| ), | ||
| ), | ||
| padding: EdgeInsets.only( | ||
| left: UiSizes.width_20, | ||
| right: UiSizes.width_20, | ||
| top: UiSizes.height_8, | ||
| bottom: MediaQuery.of(context).viewInsets.bottom + UiSizes.height_20, | ||
| ), | ||
| child: Column( | ||
| mainAxisSize: MainAxisSize.min, | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| // Drag handle | ||
| Center( | ||
| child: Container( | ||
| width: UiSizes.width_40, | ||
| height: UiSizes.height_4, | ||
| margin: EdgeInsets.only(bottom: UiSizes.height_20), | ||
| decoration: BoxDecoration( | ||
| color: Theme.of( | ||
| context, | ||
| ).colorScheme.onSurface.withValues(alpha: 0.3), | ||
| borderRadius: BorderRadius.circular(UiSizes.width_2), | ||
| ), | ||
| ), | ||
| ), | ||
| Row( | ||
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||
| children: [ | ||
| Expanded( | ||
| child: Column( | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| Text( | ||
| AppLocalizations.of(context)!.audioOutput, | ||
| style: Theme.of(context).textTheme.titleLarge?.copyWith( | ||
| fontWeight: FontWeight.bold, | ||
| ), | ||
| ), | ||
| SizedBox(height: UiSizes.height_4), | ||
| Text( | ||
| AppLocalizations.of(context)!.selectPreferredSpeaker, | ||
| style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||
| color: Theme.of( | ||
| context, | ||
| ).colorScheme.onSurface.withValues(alpha: 0.6), | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| IconButton( | ||
| icon: const Icon(Icons.close), | ||
| onPressed: () => Get.back(), | ||
| ), | ||
| ], | ||
| ), | ||
| SizedBox(height: UiSizes.height_8), | ||
| const Divider(), | ||
| SizedBox(height: UiSizes.height_12), | ||
| Flexible( | ||
| child: Obx( | ||
| () => SingleChildScrollView( | ||
| child: _buildDeviceList( | ||
| context, | ||
| controller, | ||
| controller.audioOutputDevices, | ||
| controller.selectedAudioOutput.value, | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| SizedBox(height: UiSizes.height_12), | ||
| const Divider(), | ||
| SizedBox(height: UiSizes.height_8), | ||
| Row( | ||
| mainAxisAlignment: MainAxisAlignment.end, | ||
| children: [ | ||
| TextButton.icon( | ||
| onPressed: () => controller.refreshDevices(), | ||
| icon: Icon(Icons.refresh, size: UiSizes.size_18), | ||
| label: Text(AppLocalizations.of(context)!.refresh), | ||
| ), | ||
| SizedBox(width: UiSizes.width_8), | ||
| ElevatedButton( | ||
| onPressed: () => Get.back(), | ||
| style: ElevatedButton.styleFrom( | ||
| backgroundColor: Theme.of(context).colorScheme.primary, | ||
| foregroundColor: Theme.of(context).colorScheme.onPrimary, | ||
| ), | ||
| child: Text(AppLocalizations.of(context)!.done), | ||
| ), | ||
| ], | ||
| ), | ||
| ], | ||
| ), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Potential bottom-sheet layout assertion: Flexible inside Column(mainAxisSize: min).
This pattern can throw “incoming height constraints are unbounded” depending on how the bottom sheet constrains content. Prefer an explicit max height + Expanded, or make the column mainAxisSize: max.
Proposed fix (constrain height + use Expanded)
@override
Widget build(BuildContext context) {
- return Container(
+ final maxHeight = MediaQuery.of(context).size.height * 0.75;
+ return ConstrainedBox(
+ constraints: BoxConstraints(maxHeight: maxHeight),
+ child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(
top: Radius.circular(UiSizes.width_20),
),
),
padding: EdgeInsets.only(
left: UiSizes.width_20,
right: UiSizes.width_20,
top: UiSizes.height_8,
bottom: MediaQuery.of(context).viewInsets.bottom + UiSizes.height_20,
),
child: Column(
- mainAxisSize: MainAxisSize.min,
+ mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ...
- Flexible(
+ Expanded(
child: Obx(
() => SingleChildScrollView(
child: _buildDeviceList(
context,
controller,
controller.audioOutputDevices,
controller.selectedAudioOutput.value,
),
),
),
),
// ...
],
),
- ),
+ ),
);
}🤖 Prompt for AI Agents
In @lib/views/widgets/audio_selector_dialog.dart around lines 13 - 116, The
Column uses mainAxisSize: MainAxisSize.min while containing a Flexible widget
(Flexible -> Obx -> SingleChildScrollView -> _buildDeviceList), which can cause
unbounded height assertions in bottom sheets; fix by constraining the scroll
area: either change the Column to mainAxisSize: MainAxisSize.max and replace
Flexible with Expanded, or keep Column.min and wrap the
Obx/SingleChildScrollView/_buildDeviceList chain in a SizedBox/ConstrainedBox
with an explicit maxHeight (e.g., fraction of
MediaQuery.of(context).size.height); update references to Column(mainAxisSize:
min), Flexible, Obx, SingleChildScrollView, and _buildDeviceList accordingly.
| Future<void> showAudioDeviceSelector(BuildContext context) async { | ||
| final controller = Get.put(AudioDeviceController(), permanent: true); | ||
| await controller.refreshDevices(); | ||
|
|
||
| Get.bottomSheet( | ||
| AudioDeviceSelectorDialog(controller: controller), | ||
| isScrollControlled: true, | ||
| backgroundColor: Colors.transparent, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -type f -name "*.dart" | xargs grep -l "AudioDeviceController" | head -20Repository: AOSSIE-Org/Resonate
Length of output: 233
🏁 Script executed:
fd "AudioDeviceController" --type fRepository: AOSSIE-Org/Resonate
Length of output: 45
🏁 Script executed:
rg "class AudioDeviceController" --type dartRepository: AOSSIE-Org/Resonate
Length of output: 160
🏁 Script executed:
cat -n lib/controllers/audio_device_controller.dartRepository: AOSSIE-Org/Resonate
Length of output: 2699
🏁 Script executed:
sed -n '248,257p' lib/views/widgets/audio_selector_dialog.dartRepository: AOSSIE-Org/Resonate
Length of output: 391
Avoid Get.put(..., permanent: true) here — the periodic timer won't be cancelled.
The AudioDeviceController starts a 5-second polling timer in onInit() that is cancelled in onClose(). When registered as permanent, onClose() is never called unless explicitly deleted, leaving the timer running indefinitely and draining battery/memory after the sheet is dismissed. Remove permanent: true, await Get.bottomSheet(), and delete the controller when done.
Proposed fix
Future<void> showAudioDeviceSelector(BuildContext context) async {
- final controller = Get.put(AudioDeviceController(), permanent: true);
+ final controller = Get.put(AudioDeviceController());
await controller.refreshDevices();
- Get.bottomSheet(
+ await Get.bottomSheet(
AudioDeviceSelectorDialog(controller: controller),
isScrollControlled: true,
backgroundColor: Colors.transparent,
);
+
+ if (Get.isRegistered<AudioDeviceController>()) {
+ Get.delete<AudioDeviceController>();
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Future<void> showAudioDeviceSelector(BuildContext context) async { | |
| final controller = Get.put(AudioDeviceController(), permanent: true); | |
| await controller.refreshDevices(); | |
| Get.bottomSheet( | |
| AudioDeviceSelectorDialog(controller: controller), | |
| isScrollControlled: true, | |
| backgroundColor: Colors.transparent, | |
| ); | |
| } | |
| Future<void> showAudioDeviceSelector(BuildContext context) async { | |
| final controller = Get.put(AudioDeviceController()); | |
| await controller.refreshDevices(); | |
| await Get.bottomSheet( | |
| AudioDeviceSelectorDialog(controller: controller), | |
| isScrollControlled: true, | |
| backgroundColor: Colors.transparent, | |
| ); | |
| if (Get.isRegistered<AudioDeviceController>()) { | |
| Get.delete<AudioDeviceController>(); | |
| } | |
| } |
🤖 Prompt for AI Agents
In @lib/views/widgets/audio_selector_dialog.dart around lines 248 - 257, The
current showAudioDeviceSelector registers AudioDeviceController as permanent
which prevents its onClose() from running and leaves its 5s polling timer alive;
fix by removing permanent: true from Get.put(AudioDeviceController()), await the
Get.bottomSheet(...) call so it completes when the sheet is dismissed, and then
call Get.delete<AudioDeviceController>() (or Get.delete(controller) using the
controller instance) to ensure the controller's onClose() runs and the timer is
cancelled; keep using AudioDeviceSelectorDialog(controller: controller) and
still call controller.refreshDevices() before showing the sheet.
| enumerateDevices(); | ||
| _deviceEnumerationTimer = Timer.periodic( | ||
| const Duration(seconds: 5), | ||
| (_) => enumerateDevices(), |
There was a problem hiding this comment.
refactor to await these calls, as it is an async function
| selectedAudioOutput.value = device; | ||
| await webrtc.Helper.selectAudioOutput(device.deviceId); | ||
| log('Selected audio output: ${device.label}'); | ||
| } catch (e) { | ||
| log('Error selecting audio output: $e'); |
There was a problem hiding this comment.
move this, like CodeRabbit suggested
| ), | ||
| _buildControlButton( | ||
| icon: Icons.settings_voice, | ||
| label: 'Audio Options', |
|
Also dispose the controller like previously requested |
| @@ -18,7 +18,7 @@ class AudioDeviceController extends GetxController { | |||
| enumerateDevices(); | |||
There was a problem hiding this comment.
await the first call as well
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/controllers/single_room_controller.dart (1)
172-186: Fixswitchcase termination: missingbreak/returnincase 'delete'when current user is removed.
As written, theupdatedUserId == me.value.uidbranch ends withoutbreak/return/throw/continue, which is a Dart compile-time error (and even if it compiled, you likely want to stop processing after deleting the controller).Proposed fix (use
returnafter deleting controllers)if (updatedUserId == me.value.uid) { customSnackbar( AppLocalizations.of(Get.context!)!.alert, AppLocalizations.of(Get.context!)!.removedFromRoom, LogType.warning, ); Get.delete<AudioDeviceController>(force: true); await Get.delete<SingleRoomController>(); + return; } else { removeParticipantDataFromList(data.payload["uid"]); break; }
🤖 Fix all issues with AI agents
In @lib/controllers/single_room_controller.dart:
- Around line 91-97: The code calls Get.delete<AudioDeviceController>() in
multiple places (e.g., removeParticipantDataFromList, leaveRoom, deleteRoom, and
the realtime-delete handler) which triggers error logs if the controller isn't
registered; wrap each Get.delete<AudioDeviceController>(...) call with a guard
using Get.isRegistered<AudioDeviceController>() (or consolidate deletion into a
single cleanup helper/onClose method that first checks registration) so deletion
is only attempted when the controller exists and duplicate code is avoided.
In @lib/l10n/app_localizations_bn.dart:
- Around line 1004-1006: The getters currently return English; replace the
English values with Bengali translations for the audio-related localization
getters: update get audioOptions to "অডিও বিকল্প", get audioOutput to "অডিও
আউটপুট", get selectPreferredSpeaker to "প্রিয় ব্লুটুথ/স্পিকার নির্বাচন করুন"
(or "প্রিয় স্পিকার নির্বাচন করুন" if Bluetooth not applicable), get
noAudioOutputDevices to "কোনো অডিও আউটপুট ডিভাইস পাওয়া যায়নি", get refresh to
"রিফ্রেশ" and get done to "সম্পন্ন"; apply the same replacements for the
duplicate entries referenced at the other block (lines ~1360–1374) and ensure
the file remains UTF-8 encoded.
In @lib/l10n/app_localizations_kn.dart:
- Around line 1001-1003: The audio-related getters audioOptions, audioOutput,
selectPreferredSpeaker, noAudioOutputDevices, refresh, and done are still
returning English; update each of these getters in the bn, gu, kn and mr locale
implementations to provide the proper translated strings (matching
style/punctuation used in the hi/pa locales), replacing the English literals
with the correct translations for Bengali, Gujarati, Kannada and Marathi
respectively so the audio device selector is fully localized.
In @lib/l10n/app_localizations_mr.dart:
- Around line 997-999: Replace the six English strings in
lib/l10n/app_localizations_mr.dart with Marathi translations: update
audioOptions to "ऑडिओ पर्याय", audioOutput to "ऑडिओ आउटपुट",
selectPreferredSpeaker to "प्राधान्य स्पीकर निवडा", noAudioOutputDevices to
"कोणतीही ऑडिओ आउटपुट उपकरणे सापडली नाहीत", refresh to "ताजे करा", and done to
"ठीक आहे" so the Marathi locale returns proper localized text for those getters.
🧹 Nitpick comments (2)
lib/controllers/friend_call_screen.dart (1)
132-140: Consider adding error handling for the async operation.The
showAudioDeviceSelectorcall is awaited but lacks error handling. If the dialog encounters an error (e.g., device enumeration failure), the exception will propagate unhandled, potentially causing a poor user experience.🛡️ Proposed error handling
_buildControlButton( icon: Icons.settings_voice, label: AppLocalizations.of(context)!.audioOptions, - onPressed: () async => await showAudioDeviceSelector(context), + onPressed: () async { + try { + await showAudioDeviceSelector(context); + } catch (e) { + // Handle error gracefully, e.g., show a snackbar + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to open audio settings')), + ); + } + } + }, backgroundColor: _getControlButtonBackgroundColor( currentBrightness, ), heroTag: "audio-settings", ),lib/controllers/single_room_controller.dart (1)
219-225: Consolidate duplicateAudioDeviceControllercleanup into a shared helper method.
Get.delete<AudioDeviceController>(force: true)appears 4 times (lines 94, 180, 223, 232), creating drift risk if cleanup is accidentally omitted. Extract into a_cleanupAudioDeviceController()helper to apply uniformly acrossleaveRoom(),deleteRoom(),removeParticipantDataFromList(), and the realtime listener's delete case.Example refactor
class SingleRoomController extends GetxController { + void _cleanupAudioDeviceController() { + if (Get.isRegistered<AudioDeviceController>()) { + Get.delete<AudioDeviceController>(force: true); + } + } ... Future<void> leaveRoom() async { loadingDialog(Get.context!); await subscription?.close(); await RoomService.leaveRoom(roomId: appwriteRoom.id); - Get.delete<AudioDeviceController>(force: true); + _cleanupAudioDeviceController(); Get.delete<SingleRoomController>(); } Future<void> deleteRoom() async { try { isLoading.value = true; await RoomService.deleteRoom(roomId: appwriteRoom.id); await roomsController.getRooms(); - Get.delete<AudioDeviceController>(force: true); + _cleanupAudioDeviceController(); Get.delete<SingleRoomController>(); } catch (e) {
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
lib/controllers/audio_device_controller.dartlib/controllers/friend_call_screen.dartlib/controllers/friend_calling_controller.dartlib/controllers/single_room_controller.dartlib/l10n/app_en.arblib/l10n/app_hi.arblib/l10n/app_localizations.dartlib/l10n/app_localizations_bn.dartlib/l10n/app_localizations_en.dartlib/l10n/app_localizations_gu.dartlib/l10n/app_localizations_hi.dartlib/l10n/app_localizations_kn.dartlib/l10n/app_localizations_mr.dartlib/l10n/app_localizations_pa.dartlib/l10n/app_pa.arbtest/controllers/audio_device_controller_test.dartuntranslated.txt
🚧 Files skipped from review as they are similar to previous changes (7)
- lib/l10n/app_hi.arb
- lib/controllers/audio_device_controller.dart
- lib/l10n/app_localizations_gu.dart
- untranslated.txt
- lib/l10n/app_pa.arb
- lib/l10n/app_localizations_hi.dart
- lib/l10n/app_localizations_pa.dart
🔇 Additional comments (5)
lib/controllers/friend_calling_controller.dart (1)
11-11: LGTM! Consistent cleanup pattern for AudioDeviceController.The AudioDeviceController cleanup is properly integrated into all call termination paths (ended, declined, and status update), mirroring the existing LiveKitController cleanup pattern. The
force: trueparameter ensures graceful handling even if the controller wasn't initialized.Also applies to: 190-190, 223-223, 228-228
test/controllers/audio_device_controller_test.dart (1)
30-64: Verify enumeration logic is tested.This test manually populates
audioOutputDevicesby filtering mock devices rather than calling an enumeration method on the controller. While this validates the filtering logic, it may not fully exercise the actual device enumeration implementation inAudioDeviceController.Consider verifying that the controller's device enumeration method (if it exists) is tested either in this file or elsewhere. If the enumeration logic is complex or involves WebRTC calls, you might want to add an integration test or a separate unit test that specifically exercises that method.
Would you like me to help identify where the enumeration logic lives and suggest additional test coverage?
lib/l10n/app_localizations.dart (1)
1939-1943: LGTM! Well-documented localization additions.The new localization getters for audio device selection are properly documented and follow the existing patterns in the file. All six new strings have clear descriptions and example translations.
Also applies to: 2479-2507
lib/l10n/app_localizations_en.dart (1)
995-996: LGTM! Clean English localization implementation.The new localization strings are clear, user-friendly, and properly implemented. The English text appropriately describes the audio device selection functionality.
Also applies to: 1349-1362
lib/l10n/app_en.arb (1)
1327-1330: LGTM! Proper localization keys added for audio device selectorThe six new audio-related localization keys (audioOptions, audioOutput, selectPreferredSpeaker, noAudioOutputDevices, refresh, done) are correctly defined with:
- Clear, descriptive English values
- Proper @ metadata blocks with descriptions explaining each key's purpose
- Consistent formatting matching the existing ARB structure
These keys provide the foundation for the audio device selection feature across all locales.
Also applies to: 1765-1784
M4dhav
left a comment
There was a problem hiding this comment.
Good work, thank you for the contribution
|
✅ PR Closed - Thank You, @Mayank4352!
We appreciate your effort and look forward to more contributions from you! 🤝 |
|
🔔 Translation Check Notice The following untranslated language codes were found in 📣 Notifying maintainers per language: Automated post-merge notice by Translation Notifier |
Description
Allow Users to Select Preferred Speaker
Fixes #567
Type of change
Please delete options that are not relevant.
How Has This Been Tested?
Tested Locally using two devices for both rooms and calls
Checklist:
Maintainer Checklist
Summary by CodeRabbit
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.