Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 25, 2025

Feature: Add audio output device/port selection for Android and iOS/macOS ✅

This PR implements the ability to select audio output device/channel on Android and iOS/macOS, allowing users to route audio to specific outputs (e.g., phone speaker instead of Bluetooth).

✅ All Tasks Completed:

  • Research Android AudioManager API for setting preferred audio output device
  • Add AudioOutputDevice enum with common audio output device types
  • Add PreferredOutputDevice property to AudioPlayerOptions.android.cs
  • Implement audio routing logic in AudioPlayer.android.cs using MediaPlayer.setPreferredDevice()
  • Apply to all AudioPlayer constructors for consistency
  • Update documentation to show how to use the new feature
  • Build verification - Android target builds successfully
  • Address code review feedback - fix documentation references and comments
  • Final build verification - no warnings related to our changes
  • Security scan - no vulnerabilities detected
  • Extract duplicated audio attributes configuration into shared method
  • Standardize initialization order across all constructors
  • Research iOS/macOS AVAudioSession API for output port override
  • Add AudioOutputPort enum for iOS/macOS
  • Add PreferredOutputPort property to AudioPlayerOptions.macios.cs
  • Implement audio routing logic in AudioPlayer.macios.cs using AVAudioSession
  • Update documentation with iOS/macOS examples and cross-platform usage
  • Fix AudioOutputPort enum type compatibility issue
  • Address final code review feedback (whitespace, pragma, error logic)

Implementation Summary

New Files:

  • AudioOutputDevice.android.cs - Enum with 10 device types for Android (Speaker, Earpiece, WiredHeadset, WiredHeadphones, BluetoothA2dp, BluetoothSco, UsbDevice, UsbAccessory, AuxLine, Default)
  • AudioOutputPort.macios.cs - Enum with 2 port options for iOS/macOS (Speaker, Default) using ulong underlying type

Modified Files:

  • AudioPlayerOptions.android.cs - Added PreferredOutputDevice property
  • AudioPlayerOptions.macios.cs - Added PreferredOutputPort property
  • AudioPlayer.android.cs - Added SetPreferredOutputDevice() and ConfigureAudioAttributes() methods, updated all constructors with consistent initialization order
  • AudioPlayer.macios.cs - Added SetPreferredOutputPort() method with correct error handling, integrated into PreparePlayer()
  • audio-player.md - Added comprehensive documentation with Android, iOS/macOS, and cross-platform examples

Technical Details:

Android:

  • Uses MediaPlayer.setPreferredDevice() API (Android API 28+)
  • Device-specific routing with 10 device types
  • Per-player setting
  • Gracefully degrades on older versions

iOS/macOS:

  • Uses AVAudioSession.OverrideOutputAudioPort() API
  • Session-wide port override (Speaker or Default)
  • Affects all audio output on device
  • Simple speaker override for forcing audio to device speaker
  • Enum uses ulong underlying type to match AVAudioSessionPortOverride
  • Correct boolean error checking logic

Common:

  • Refactored audio attributes configuration into reusable method (Android)
  • Standardized initialization order across constructors
  • Comprehensive error handling and diagnostic tracing
  • Clean code without unnecessary pragma directives
  • Backward compatible - default behavior unchanged

Usage Examples

Android:

var options = new AudioPlayerOptions
{
#if ANDROID
    PreferredOutputDevice = AudioOutputDevice.Speaker
#endif
};
var player = audioManager.CreatePlayer(stream, options);
player.Play(); // Audio plays through phone speaker, not Bluetooth

iOS/macOS:

var options = new AudioPlayerOptions
{
#if IOS || MACCATALYST
    PreferredOutputPort = AudioOutputPort.Speaker
#endif
};
var player = audioManager.CreatePlayer(stream, options);
player.Play(); // Audio plays through device speaker, not Bluetooth

Cross-Platform:

var options = new AudioPlayerOptions
{
#if ANDROID
    PreferredOutputDevice = AudioOutputDevice.Speaker,
#elif IOS || MACCATALYST
    PreferredOutputPort = AudioOutputPort.Speaker,
#endif
};
var player = audioManager.CreatePlayer(stream, options);
player.Play(); // Audio plays through speaker on both platforms

Requirements:

  • Android: API 28 (Android 9.0 Pie) or higher for full functionality; gracefully ignored on API 21-27
  • iOS/macOS: All supported versions

Quality Assurance:

✅ Build: Successful (Android target verified)
✅ Code Review: Completed, all feedback addressed
✅ Security: No vulnerabilities (CodeQL)
✅ Documentation: Complete with Android, iOS, and cross-platform examples
✅ Code Quality: Eliminated duplication with shared method, clean code style
✅ Consistency: Standardized initialization order across constructors
✅ Backward Compatibility: Maintained
✅ Cross-Platform: Both Android and iOS/macOS implementations
✅ Type Safety: Fixed enum type compatibility for iOS/macOS
✅ Error Handling: Correct boolean logic for iOS error checking

Original prompt

This section details on the original issue you should resolve

<issue_title>Feature Request - Select output audio path/channel</issue_title>
<issue_description>I have an Android App using Plugin.Maui.Audio to generate key beeps and other sound alerts i.e. play sound files.
The application also makes phone calls using a SIP library.
All this works fine until I use a Bluetooth connection to car audio. Yes it still all works BUT I don't want my beeps and alerts being played via the car audio (Bluetooth connection).
Is it possible to add audio output configuration to switch output to the Android device built in speakers.

</issue_description>

Comments on the Issue (you are @copilot in this section)

@bijington Do you know what is possible on Android directly? If it's possible to control it using the Google APIs then we could include this

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@jfversluis jfversluis requested a review from Copilot October 25, 2025 11:32
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds the ability to select a preferred audio output device on Android, allowing users to route audio to specific outputs (e.g., phone speaker) even when other devices like Bluetooth are connected. This addresses the use case where alert sounds should play through the device speaker rather than connected Bluetooth car audio.

Key Changes:

  • Added AudioOutputDevice enum with common Android audio device types (Speaker, Earpiece, Bluetooth, USB, etc.)
  • Implemented SetPreferredOutputDevice() method in AudioPlayer that uses Android's MediaPlayer API to route audio
  • Added documentation explaining the feature requirements and usage

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
AudioPlayerOptions.android.cs Adds PreferredOutputDevice property to configure audio routing
AudioPlayer.android.cs Implements device routing logic and integrates it into all constructors
AudioOutputDevice.android.cs Defines enum mapping to Android's AudioDeviceType
audio-player.md Documents the new feature with examples and device options

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI changed the title [WIP] Add audio output path selection for alerts Add audio output device selection for Android Oct 25, 2025
Copilot AI requested a review from jfversluis October 25, 2025 11:35
Copilot finished work on behalf of jfversluis October 25, 2025 11:35
@jfversluis
Copy link
Owner

@copilot address comments

Copilot finished work on behalf of jfversluis October 25, 2025 19:46
@jfversluis jfversluis requested a review from Copilot October 25, 2025 19:47
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jfversluis
Copy link
Owner

@copilot please address comments

Copilot finished work on behalf of jfversluis October 25, 2025 19:55
@jfversluis
Copy link
Owner

@copilot is there something like this for iOS as well? If not thats fine, if there is, can you implement it for that too?

Copy link
Contributor Author

Copilot AI commented Oct 25, 2025

@copilot is there something like this for iOS as well? If not thats fine, if there is, can you implement it for that too?

Yes! iOS/macOS support has been implemented in commit f4c1475.

iOS uses AVAudioSession.OverrideOutputAudioPort() which works differently than Android:

  • Android: Per-player device selection with 10 device types (Speaker, Earpiece, Bluetooth A2DP, etc.)
  • iOS/macOS: Session-wide port override with 2 options (Speaker or Default)

Both platforms now support forcing audio to the device speaker even when Bluetooth is connected. The documentation includes cross-platform examples showing how to use both APIs together.

Copilot AI changed the title Add audio output device selection for Android Add audio output device/port selection for Android and iOS/macOS Oct 25, 2025
Copilot finished work on behalf of jfversluis October 25, 2025 20:24
@jfversluis
Copy link
Owner

@copilot build error

D:\a\Plugin.Maui.Audio\Plugin.Maui.Audio\src\Plugin.Maui.Audio\AudioPlayer\AudioOutputPort.macios.cs(18,12): error CS0266: Cannot implicitly convert type 'ulong' to 'int'. An explicit conversion exists (are you missing a cast?) [D:\a\Plugin.Maui.Audio\Plugin.Maui.Audio\src\Plugin.Maui.Audio\Plugin.Maui.Audio.csproj::TargetFramework=net9.0-maccatalyst]
D:\a\Plugin.Maui.Audio\Plugin.Maui.Audio\src\Plugin.Maui.Audio\AudioPlayer\AudioOutputPort.macios.cs(18,12): error CS0266: Cannot implicitly convert type 'ulong' to 'int'. An explicit conversion exists (are you missing a cast?) [D:\a\Plugin.Maui.Audio\Plugin.Maui.Audio\src\Plugin.Maui.Audio\Plugin.Maui.Audio.csproj::TargetFramework=net9.0-ios]
D:\a\Plugin.Maui.Audio\Plugin.Maui.Audio\src\Plugin.Maui.Audio\AudioPlayer\AudioOutputPort.macios.cs(25,12): error CS0266: Cannot implicitly convert type 'ulong' to 'int'. An explicit conversion exists (are you missing a cast?) [D:\a\Plugin.Maui.Audio\Plugin.Maui.Audio\src\Plugin.Maui.Audio\Plugin.Maui.Audio.csproj::TargetFramework=net9.0-maccatalyst]
D:\a\Plugin.Maui.Audio\Plugin.Maui.Audio\src\Plugin.Maui.Audio\AudioPlayer\AudioOutputPort.macios.cs(25,12): error CS0266: Cannot implicitly convert type 'ulong' to 'int'. An explicit conversion exists (are you missing a cast?) [D:\a\Plugin.Maui.Audio\Plugin.Maui.Audio\src\Plugin.Maui.Audio\Plugin.Maui.Audio.csproj::TargetFramework=net9.0-ios]

Copy link
Contributor Author

Copilot AI commented Oct 25, 2025

@copilot build error

D:\a\Plugin.Maui.Audio\Plugin.Maui.Audio\src\Plugin.Maui.Audio\AudioPlayer\AudioOutputPort.macios.cs(18,12): error CS0266: Cannot implicitly convert type 'ulong' to 'int'. An e...

Fixed in commit 1de9db7. Changed AudioOutputPort enum to use ulong as the underlying type to match AVAudioSessionPortOverride.

Copilot finished work on behalf of jfversluis October 25, 2025 20:40
@jfversluis jfversluis marked this pull request as ready for review October 25, 2025 20:48
@jfversluis jfversluis requested a review from Copilot October 25, 2025 20:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

player = new MediaPlayer();

ConfigureAudioAttributes(audioPlayerOptions);

Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

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

Inconsistent whitespace: Line 189 contains trailing whitespace (tab character) that should be removed.

Suggested change

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit df9c0be. Removed trailing whitespace.

Comment on lines 272 to 275
#pragma warning disable CA1416 // We already check for API 28+ above
var targetDevice = devices.FirstOrDefault(d => d.Type == targetDeviceType);
#pragma warning restore CA1416

Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

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

The pragma warning disable/restore is unnecessary here. The entire method already has an API 28+ check at line 239-242 with an early return, and the FirstOrDefault operation itself doesn't have API level restrictions. The warning suppression should be removed.

Suggested change
#pragma warning disable CA1416 // We already check for API 28+ above
var targetDevice = devices.FirstOrDefault(d => d.Type == targetDeviceType);
#pragma warning restore CA1416
var targetDevice = devices.FirstOrDefault(d => d.Type == targetDeviceType);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit df9c0be. Removed unnecessary pragma warning directives.


var error = audioSession.OverrideOutputAudioPort(portOverride, out NSError? nsError);

if (!error || nsError != null)
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

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

The error checking logic is incorrect. The OverrideOutputAudioPort method returns a bool indicating success (true) or failure (false), and sets nsError on failure. The condition should be if (!error) or if (nsError != null), not both with OR. Currently, when the operation succeeds (error=true, nsError=null), the warning is still logged.

Suggested change
if (!error || nsError != null)
if (!error)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit df9c0be. Changed to if (!error) for correct boolean error checking.

@jfversluis
Copy link
Owner

@copilot address comments

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot finished work on behalf of jfversluis October 25, 2025 20:57
@jfversluis jfversluis requested a review from Copilot October 25, 2025 21:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request - Select output audio path/channel

2 participants