Skip to content

Output change#597

Merged
M4dhav merged 22 commits intoAOSSIE-Org:devfrom
Mayank4352:output_change
Jan 11, 2026
Merged

Output change#597
M4dhav merged 22 commits intoAOSSIE-Org:devfrom
Mayank4352:output_change

Conversation

@Mayank4352
Copy link
Contributor

@Mayank4352 Mayank4352 commented Nov 13, 2025

Description

Allow Users to Select Preferred Speaker

Fixes #567

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

Tested Locally using two devices for both rooms and calls

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

Maintainer Checklist

Summary by CodeRabbit

  • New Features

    • Choose and switch audio output devices during calls; refresh the device list on demand.
    • New audio settings button on call screens to open the device selector.
    • Localized UI strings added across multiple languages for the audio settings dialog.
  • Tests

    • Unit tests added covering device enumeration, selection, and device model behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Change Summary
Audio device core
lib/models/audio_device.dart, lib/utils/enums/audio_device_enum.dart
New AudioDevice model and AudioDeviceType enum with label→type mapping, equality, and display/icon metadata.
Controller
lib/controllers/audio_device_controller.dart
New AudioDeviceController (GetxController) that enumerates devices every 5s, exposes audioOutputDevices and selectedAudioOutput, provides enumerateDevices(), selectAudioOutput(), refreshDevices(), and lifecycle cleanup.
UI — Dialog & helper
lib/views/widgets/audio_selector_dialog.dart
New AudioDeviceSelectorDialog and showAudioDeviceSelector(context) bottom sheet listing devices, refresh/done actions, and selection flow wired to controller.
Screen integrations
lib/controllers/friend_call_screen.dart, lib/views/screens/live_chapter_screen.dart, lib/views/screens/room_screen.dart
Added audio settings button to footers (calls/live); minor leave-button refactor in live/room screens; friend call screen adds "Audio Options" button.
Controller lifecycle cleanup
lib/controllers/friend_calling_controller.dart, lib/controllers/single_room_controller.dart
Ensure AudioDeviceController is deleted/cleaned up on call end, leave, and room deletion paths.
Localization — ARB & getters
lib/l10n/app_en.arb, lib/l10n/app_hi.arb, lib/l10n/app_pa.arb, untranslated.txt, ...lib/l10n/app_localizations*.dart
Added localization keys audioOptions, audioOutput, selectPreferredSpeaker, noAudioOutputDevices, refresh, done across English and multiple regional localization files and corresponding getters.
Tests
test/controllers/audio_device_controller_test.dart
New unit tests covering controller initialization, filtering/enumeration, selection flows, getDeviceName, AudioDeviceType.fromLabel, and AudioDevice model behavior.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

translations

Poem

🐇 I hopped into code with a twitchy ear,
To choose the speaker I hold so dear.
From Bluetooth to phone, each tiny beep,
I pick my sound before drifting to sleep. 🎶

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Output change' is vague and generic, failing to convey meaningful information about the actual feature implementation. Consider revising the title to something more specific like 'Add audio output device selector for user-preferred speaker selection' to clearly reflect the feature being implemented.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR implements audio output device selection (#567) with enumeration, device filtering, selection UI, and localization support across multiple languages.
Out of Scope Changes check ✅ Passed All changes are focused on implementing the audio output device selector feature with supporting controllers, UI components, models, and localizations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

🎉 Welcome @Mayank4352!
Thank you for your pull request! Our team will review it soon. 🔍

  • Please ensure your PR follows the contribution guidelines. ✅
  • All automated tests should pass before merging. 🔄
  • If this PR fixes an issue, link it in the description. 🔗

We appreciate your contribution! 🚀

@Mayank4352
Copy link
Contributor Author

Hey @M4dhav, Kindly review this PR

@M4dhav M4dhav self-requested a review November 22, 2025 08:35
@M4dhav M4dhav added the enhancement New feature or request label Nov 22, 2025
Copy link
Contributor

@M4dhav M4dhav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix merge conflicts

@Mayank4352 Mayank4352 requested a review from M4dhav November 23, 2025 07:37
Copy link
Contributor

@M4dhav M4dhav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge conflicts came in again after merging #610

@Mayank4352 Mayank4352 requested a review from M4dhav November 23, 2025 14:11
@M4dhav
Copy link
Contributor

M4dhav commented Jan 4, 2026

Can you please explain the implementation a bit? What enumeration does, how audio devices are discovered, etc

@Mayank4352
Copy link
Contributor Author

->The enumerateDevices() uses webrtc.navigator.mediaDevices.enumerateDevices() which queries for all available media devices (microphones, cameras, speakers).
->The results are then filtered to only include audio output devices (isAudioOutput) - this removes microphones and cameras.
->If no device is currently selected, it automatically picks the first available output device
->The controller polls for devices every 5 seconds which scans for Bluetooth, headset etc.
->selectAudioOutput() uses webrtc.Helper.selectAudioOutput(deviceId) to route audio to the chosen device at the system level.
->getDeviceName() and getDeviceIcon() parse device labels and return user-friendly names and icons

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)

Comment on lines 186 to +215
@@ -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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there changes to the end call button?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +266 to +272
Widget _buildAudioSettingsButton() {
return FloatingActionButton(
onPressed: () => showAudioDeviceSelector(context),
backgroundColor: Theme.of(context).colorScheme.secondary,
child: const Icon(Icons.settings_voice),
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why FloatingActionButton and not ElevatedButton.icon like the others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used a FloatingActionButton initially while iterating on the layout, I'll switch it to an ElevatedButton.icon

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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???

Comment on lines 192 to +216
@@ -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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated changes to End Call button

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +296 to +302
Widget _buildAudioSettingsButton() {
return FloatingActionButton(
onPressed: () => showAudioDeviceSelector(context),
backgroundColor: Theme.of(context).colorScheme.onSecondary,
child: const Icon(Icons.volume_up),
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why FAB

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix it, used it while playing around with UI

Comment on lines +251 to +253
if (!Get.isRegistered<AudioDeviceController>()) {
Get.put(AudioDeviceController());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using GetX here, just instantiate an instance within the dialog, as you do not need dependency injection here

@Mayank4352 Mayank4352 requested a review from M4dhav January 7, 2026 09:43
Get.put(AudioDeviceController());
}
final controller = AudioDeviceController();
controller.refreshDevices();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refreshDevices is async so await this call

Comment on lines +253 to +258
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const AudioDeviceSelectorDialog(),
);
builder: (context) => AudioDeviceSelectorDialog(controller: controller),
).whenComplete(() => controller.dispose());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use GetX APIs to show the modal

IconButton(
icon: const Icon(Icons.close),
onPressed: () => Get.back(),
onPressed: () => Navigator.of(context).pop(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continue using Get.back

SizedBox(width: UiSizes.width_8),
ElevatedButton(
onPressed: () => Get.back(),
onPressed: () => Navigator.of(context).pop(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continue using Get.back

@Mayank4352 Mayank4352 requested a review from M4dhav January 7, 2026 14:06
@Mayank4352 Mayank4352 requested a review from M4dhav January 8, 2026 12:28
Copy link
Contributor

@M4dhav M4dhav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +250 to +257
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());
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saw a bug regarding, the selection dialog- it was not persisting the devices selected by user since the controller was getting disposed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines 73 to 75
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need of this function anymore

@Mayank4352 Mayank4352 requested a review from M4dhav January 9, 2026 09:24
Copy link
Contributor

@M4dhav M4dhav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AudioDeviceType is still not a class member for AudioDevice

@M4dhav
Copy link
Contributor

M4dhav commented Jan 10, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: set heroTag (and tooltip) for footer FABs

This 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 unique heroTag (or heroTag: 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 accessibility

Since the text label was removed, please add a Tooltip/Semantics label (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: set heroTag/tooltip for the new FAB (and align footer FAB tags)

Same multi-FAB concern as the room screen: assign a unique heroTag (or null) 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 selectedAudioOutput as expected.

lib/utils/enums/audio_device_enum.dart (1)

6-7: Improve icon differentiation for USB and unknown devices.

Both usb and unknown device 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 from AudioDeviceTypeIconData (avoid stringly-typed icons).

iconName + string switch is brittle; consider moving IconData mapping into AudioDeviceType (or a helper) and switching on the enum directly.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1aab2aa and a81a246.

📒 Files selected for processing (20)
  • lib/controllers/audio_device_controller.dart
  • lib/controllers/friend_call_screen.dart
  • lib/l10n/app_en.arb
  • lib/l10n/app_hi.arb
  • lib/l10n/app_localizations.dart
  • lib/l10n/app_localizations_bn.dart
  • lib/l10n/app_localizations_en.dart
  • lib/l10n/app_localizations_gu.dart
  • lib/l10n/app_localizations_hi.dart
  • lib/l10n/app_localizations_kn.dart
  • lib/l10n/app_localizations_mr.dart
  • lib/l10n/app_localizations_pa.dart
  • lib/l10n/app_pa.arb
  • lib/models/audio_device.dart
  • lib/utils/enums/audio_device_enum.dart
  • lib/views/screens/live_chapter_screen.dart
  • lib/views/screens/room_screen.dart
  • lib/views/widgets/audio_selector_dialog.dart
  • test/controllers/audio_device_controller_test.dart
  • untranslated.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.dart where 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/refresh are very generic keys—please ensure they don’t already exist elsewhere and that flutter 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 for done/refresh

Looks 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, ignoring label, kind, groupId, and deviceType. Two AudioDevice instances with the same deviceId but 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 fromLabel method uses sequential if-else checks that could classify devices ambiguously. For example, a device labeled "Bluetooth Headset" will match the bluetooth check (line 17) and return bluetoothAudio, never reaching the headset check (line 22).

Confirm whether the current prioritization is intentional:

  • bluetooth > earpiece > headset > speaker > usb

If 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, not headset
  • "USB Speaker" → currently returns speaker, not usb
  • "Wired Bluetooth Adapter" → currently returns bluetoothAudio, not headset
lib/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 / done are 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 supports Color.withValues(...).

If the repo’s Flutter SDK is older, withValues may not compile; withOpacity is the more widely-supported alternative.

Also applies to: 62-65, 189-197, 216-216

lib/controllers/audio_device_controller.dart (1)

4-4: Confirm flutter_webrtc supports enumerateDevices + Helper.selectAudioOutput on 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

Comment on lines +15 to +23
@override
void onInit() {
super.onInit();
enumerateDevices();
_deviceEnumerationTimer = Timer.periodic(
const Duration(seconds: 5),
(_) => enumerateDevices(),
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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

Comment on lines +46 to +54
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');
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "audio_device_controller.dart" -type f

Repository: AOSSIE-Org/Resonate

Length of output: 109


🏁 Script executed:

cat -n ./lib/controllers/audio_device_controller.dart | head -70

Repository: AOSSIE-Org/Resonate

Length of output: 2679


🏁 Script executed:

cat -n ./lib/controllers/audio_device_controller.dart | tail -30

Repository: 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.dart

Repository: 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.dart

Repository: 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.dart

Repository: AOSSIE-Org/Resonate

Length of output: 45


🏁 Script executed:

# Search for webrtc.Helper.selectAudioOutput usage elsewhere
rg "selectAudioOutput" --type dart

Repository: AOSSIE-Org/Resonate

Length of output: 665


🏁 Script executed:

cat -n ./test/controllers/audio_device_controller_test.dart

Repository: 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.

Suggested change
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.

Comment on lines +1357 to +1371
@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';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested 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';
@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.

Comment on lines +1346 to +1360
@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';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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).

Comment on lines +1353 to +1367
@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';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +1347 to +1361
@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';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +19 to +28
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),
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find where MediaDeviceInfo is defined or imported
rg -n "MediaDeviceInfo" lib/ --type=dart | head -20

Repository: 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.dart

Repository: 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 -5

Repository: 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/ -A1

Repository: 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.

Comment on lines +13 to +116
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),
),
],
),
],
),
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +248 to +257
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,
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "*.dart" | xargs grep -l "AudioDeviceController" | head -20

Repository: AOSSIE-Org/Resonate

Length of output: 233


🏁 Script executed:

fd "AudioDeviceController" --type f

Repository: AOSSIE-Org/Resonate

Length of output: 45


🏁 Script executed:

rg "class AudioDeviceController" --type dart

Repository: AOSSIE-Org/Resonate

Length of output: 160


🏁 Script executed:

cat -n lib/controllers/audio_device_controller.dart

Repository: AOSSIE-Org/Resonate

Length of output: 2699


🏁 Script executed:

sed -n '248,257p' lib/views/widgets/audio_selector_dialog.dart

Repository: 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.

Suggested change
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.

Comment on lines +18 to +21
enumerateDevices();
_deviceEnumerationTimer = Timer.periodic(
const Duration(seconds: 5),
(_) => enumerateDevices(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor to await these calls, as it is an async function

Comment on lines +48 to +52
selectedAudioOutput.value = device;
await webrtc.Helper.selectAudioOutput(device.deviceId);
log('Selected audio output: ${device.label}');
} catch (e) {
log('Error selecting audio output: $e');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this, like CodeRabbit suggested

),
_buildControlButton(
icon: Icons.settings_voice,
label: 'Audio Options',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use translated string

@M4dhav
Copy link
Contributor

M4dhav commented Jan 10, 2026

Also dispose the controller like previously requested

@Mayank4352 Mayank4352 requested a review from M4dhav January 11, 2026 09:00
@@ -18,7 +18,7 @@ class AudioDeviceController extends GetxController {
enumerateDevices();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await the first call as well

@Mayank4352 Mayank4352 requested a review from M4dhav January 11, 2026 12:04
@M4dhav
Copy link
Contributor

M4dhav commented Jan 11, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Fix switch case termination: missing break/return in case 'delete' when current user is removed.
As written, the updatedUserId == me.value.uid branch ends without break/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 return after 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 showAudioDeviceSelector call 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 duplicate AudioDeviceController cleanup 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 across leaveRoom(), 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

📥 Commits

Reviewing files that changed from the base of the PR and between a81a246 and 220d643.

📒 Files selected for processing (17)
  • lib/controllers/audio_device_controller.dart
  • lib/controllers/friend_call_screen.dart
  • lib/controllers/friend_calling_controller.dart
  • lib/controllers/single_room_controller.dart
  • lib/l10n/app_en.arb
  • lib/l10n/app_hi.arb
  • lib/l10n/app_localizations.dart
  • lib/l10n/app_localizations_bn.dart
  • lib/l10n/app_localizations_en.dart
  • lib/l10n/app_localizations_gu.dart
  • lib/l10n/app_localizations_hi.dart
  • lib/l10n/app_localizations_kn.dart
  • lib/l10n/app_localizations_mr.dart
  • lib/l10n/app_localizations_pa.dart
  • lib/l10n/app_pa.arb
  • test/controllers/audio_device_controller_test.dart
  • untranslated.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: true parameter 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 audioOutputDevices by 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 in AudioDeviceController.

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 selector

The 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

Copy link
Contributor

@M4dhav M4dhav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work, thank you for the contribution

@M4dhav M4dhav merged commit a7c89ee into AOSSIE-Org:dev Jan 11, 2026
2 checks passed
@github-actions
Copy link
Contributor

PR Closed - Thank You, @Mayank4352!

  • If this PR was merged: Congratulations! Your contribution is now part of the project. 🚀
  • If this PR was closed without merging: Don’t worry! You can always improve it and submit again. 💪

We appreciate your effort and look forward to more contributions from you! 🤝

@github-actions
Copy link
Contributor

🔔 Translation Check Notice

The following untranslated language codes were found in untranslated.txt:
Please translate these keys:

{
  bn: [
    audioOptions,
    audioOutput,
    selectPreferredSpeaker,
    noAudioOutputDevices,
    refresh,
    done
  ],

  gu: [
    appPreferences,
    transcriptionModel,
    transcriptionModelDescription,
    whisperModelTiny,
    whisperModelTinyDescription,
    whisperModelBase,
    whisperModelBaseDescription,
    whisperModelSmall,
    whisperModelSmallDescription,
    whisperModelMedium,
    whisperModelMediumDescription,
    whisperModelLargeV1,
    whisperModelLargeV1Description,
    whisperModelLargeV2,
    whisperModelLargeV2Description,
    modelDownloadInfo,
    hide,
    removeRoom,
    removeRoomFromList,
    removeRoomConfirmation,
    searchFailed,
    searchRooms,
    searchingRooms,
    clearSearch,
    searchError,
    searchRoomsError,
    searchUpcomingRoomsError,
    search,
    clear,
    audioOptions,
    failedToRemoveRoom,
    roomRemovedSuccessfully,
    audioOutput,
    selectPreferredSpeaker,
    noAudioOutputDevices,
    refresh,
    done,
    thisMessageWasDeleted,
    failedToDeleteMessage
  ],

  kn: [
    hide,
    removeRoom,
    removeRoomFromList,
    removeRoomConfirmation,
    searchFailed,
    searchRooms,
    searchingRooms,
    clearSearch,
    searchError,
    searchRoomsError,
    searchUpcomingRoomsError,
    search,
    clear,
    audioOptions,
    failedToRemoveRoom,
    roomRemovedSuccessfully,
    audioOutput,
    selectPreferredSpeaker,
    noAudioOutputDevices,
    refresh,
    done,
    deleteMessageTitle,
    deleteMessageContent,
    thisMessageWasDeleted,
    failedToDeleteMessage
  ],

  mr: [
    hide,
    removeRoom,
    removeRoomFromList,
    removeRoomConfirmation,
    searchFailed,
    searchRooms,
    searchingRooms,
    clearSearch,
    searchError,
    searchRoomsError,
    searchUpcomingRoomsError,
    search,
    clear,
    audioOptions,
    failedToRemoveRoom,
    roomRemovedSuccessfully,
    audioOutput,
    selectPreferredSpeaker,
    noAudioOutputDevices,
    refresh,
    done,
    thisMessageWasDeleted,
    failedToDeleteMessage
  ]
}

📣 Notifying maintainers per language:
Bengali (bn): @codealchemist007
Gujarati (gu): @4555jan
Kannada (kn): @Charan-BS
Marathi (mr): @4555jan

Automated post-merge notice by Translation Notifier

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Allow Users to Select Preferred Speaker and Microphone Options

2 participants