From 1a68888019c2df970edce5184ec615b9aaedc377 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Oct 2025 11:16:34 +0000
Subject: [PATCH 1/9] Initial plan
From 0fc6eb04cb05bb206593d2185a5aa18c978db9a6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Oct 2025 11:28:10 +0000
Subject: [PATCH 2/9] Add audio output device selection for Android
Co-authored-by: jfversluis <939291+jfversluis@users.noreply.github.com>
---
docs/audio-player.md | 32 ++++
.../AudioPlayer/AudioOutputDevice.android.cs | 75 ++++++++++
.../AudioPlayer/AudioPlayer.android.cs | 139 +++++++++++++++++-
.../AudioPlayer/AudioPlayerOptions.android.cs | 29 ++++
4 files changed, 274 insertions(+), 1 deletion(-)
create mode 100644 src/Plugin.Maui.Audio/AudioPlayer/AudioOutputDevice.android.cs
diff --git a/docs/audio-player.md b/docs/audio-player.md
index 8a1f592..19b23e1 100644
--- a/docs/audio-player.md
+++ b/docs/audio-player.md
@@ -59,6 +59,38 @@ audioManager.CreatePlayer(
For more information, please refer to the Android documentation: https://developer.android.com/reference/android/media/AudioAttributes
+### Controlling Audio Output Device (Android)
+
+On Android, you can specify which audio output device should be used for playback. This is useful when you want to ensure audio plays through a specific output, such as the device speaker, even when other outputs like Bluetooth are connected.
+
+```csharp
+audioManager.CreatePlayer(
+ await FileSystem.OpenAppPackageFileAsync("beep.wav"),
+ new AudioPlayerOptions
+ {
+#if ANDROID
+ PreferredOutputDevice = Plugin.Maui.Audio.AudioOutputDevice.Speaker
+#endif
+ });
+```
+
+This feature requires Android API 28 (Android 9.0 Pie) or higher. On older versions, the setting will be ignored and the system default routing will be used.
+
+Available output device options include:
+- `AudioOutputDevice.Default` - Use system default routing
+- `AudioOutputDevice.Speaker` - Built-in device speaker (loudspeaker)
+- `AudioOutputDevice.Earpiece` - Built-in earpiece (typically used for phone calls)
+- `AudioOutputDevice.WiredHeadset` - Wired headset or headphones with microphone
+- `AudioOutputDevice.WiredHeadphones` - Wired headphones without microphone
+- `AudioOutputDevice.BluetoothA2dp` - Bluetooth device with A2DP profile (e.g., Bluetooth headphones, car audio)
+- `AudioOutputDevice.BluetoothSco` - Bluetooth SCO device (typically used for phone calls)
+- `AudioOutputDevice.UsbDevice` - USB audio device
+- `AudioOutputDevice.UsbAccessory` - USB accessory
+- `AudioOutputDevice.AuxLine` - Auxiliary line connection (e.g., 3.5mm aux cable)
+
+**Note:** The system may override this preference based on user actions or system policies. If the requested device is not available or connected, the system will fall back to its default routing behavior.
+
+
## AudioPlayer API
Once you have created an `AudioPlayer` you can interact with it in the following ways:
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputDevice.android.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputDevice.android.cs
new file mode 100644
index 0000000..5f5111a
--- /dev/null
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputDevice.android.cs
@@ -0,0 +1,75 @@
+using System.Runtime.Versioning;
+using Android.Media;
+
+namespace Plugin.Maui.Audio;
+
+///
+/// Specifies the preferred audio output device type for Android.
+///
+///
+/// This enum is used to specify which audio output device should be preferred for playback.
+/// Requires Android API 28 (Android 9.0 Pie) or higher for setting the preferred device.
+/// On older versions, this setting will be ignored and the system default routing will be used.
+///
+[SupportedOSPlatform("android23.0")]
+public enum AudioOutputDevice
+{
+ ///
+ /// Use the system default audio output device. No preferred device will be set.
+ ///
+ Default = 0,
+
+ ///
+ /// Route audio to the built-in device speaker (typically the loudspeaker).
+ /// Corresponds to .
+ ///
+ Speaker = AudioDeviceType.BuiltinSpeaker,
+
+ ///
+ /// Route audio to the built-in earpiece (typically used for phone calls).
+ /// Corresponds to .
+ ///
+ Earpiece = AudioDeviceType.BuiltinEarpiece,
+
+ ///
+ /// Route audio to a wired headset or headphones.
+ /// Corresponds to .
+ ///
+ WiredHeadset = AudioDeviceType.WiredHeadset,
+
+ ///
+ /// Route audio to a wired headphone device.
+ /// Corresponds to .
+ ///
+ WiredHeadphones = AudioDeviceType.WiredHeadphones,
+
+ ///
+ /// Route audio to a Bluetooth device with A2DP profile (e.g., Bluetooth headphones, car audio).
+ /// Corresponds to .
+ ///
+ BluetoothA2dp = AudioDeviceType.BluetoothA2dp,
+
+ ///
+ /// Route audio to a Bluetooth SCO (Synchronous Connection Oriented) device (typically used for phone calls).
+ /// Corresponds to .
+ ///
+ BluetoothSco = AudioDeviceType.BluetoothSco,
+
+ ///
+ /// Route audio to an auxiliary line connection (e.g., 3.5mm aux cable).
+ /// Corresponds to .
+ ///
+ AuxLine = AudioDeviceType.AuxLine,
+
+ ///
+ /// Route audio to a USB audio device.
+ /// Corresponds to .
+ ///
+ UsbDevice = AudioDeviceType.UsbDevice,
+
+ ///
+ /// Route audio to a USB accessory.
+ /// Corresponds to .
+ ///
+ UsbAccessory = AudioDeviceType.UsbAccessory,
+}
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
index 0fafc9f..b4fece2 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
@@ -223,6 +223,65 @@ internal AudioPlayer(AudioPlayerOptions audioPlayerOptions)
}
player.Completion += OnPlaybackEnded;
+
+ // Set preferred output device if specified (API 23+)
+ SetPreferredOutputDevice(audioPlayerOptions.PreferredOutputDevice);
+ }
+
+ void SetPreferredOutputDevice(AudioOutputDevice preferredDevice)
+ {
+ // setPreferredDevice is only available on API 28 and above
+ if (!OperatingSystem.IsAndroidVersionAtLeast(28))
+ {
+ return;
+ }
+
+ // If Default is specified, don't set any preferred device
+ if (preferredDevice == AudioOutputDevice.Default)
+ {
+ return;
+ }
+
+ try
+ {
+ var context = Android.App.Application.Context;
+ var audioManager = context?.GetSystemService(Android.Content.Context.AudioService) as Android.Media.AudioManager;
+
+ if (audioManager is null)
+ {
+ System.Diagnostics.Trace.TraceWarning("Unable to get AudioManager service.");
+ return;
+ }
+
+ // Get all output audio devices (API 23+)
+ var devices = audioManager.GetDevices(GetDevicesTargets.Outputs);
+
+ if (devices is null || devices.Length == 0)
+ {
+ System.Diagnostics.Trace.TraceWarning("No output audio devices found.");
+ return;
+ }
+
+ // Find the first device matching the preferred type
+ var targetDeviceType = (AudioDeviceType)preferredDevice;
+#pragma warning disable CA1416 // We already check for API 28+ above
+ var targetDevice = devices.FirstOrDefault(d => d.Type == targetDeviceType);
+#pragma warning restore CA1416
+
+ if (targetDevice is not null)
+ {
+ player.SetPreferredDevice(targetDevice);
+ System.Diagnostics.Trace.TraceInformation($"Preferred audio output device set to: {targetDeviceType}");
+ }
+ else
+ {
+ System.Diagnostics.Trace.TraceWarning($"Requested audio output device type {targetDeviceType} not found or not available.");
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Trace.TraceError($"Error setting preferred audio output device: {ex.Message}");
+ }
}
public void SetSource(Stream audioStream)
@@ -260,6 +319,42 @@ internal AudioPlayer(Stream audioStream, AudioPlayerOptions audioPlayerOptions)
player = new MediaPlayer();
player.Completion += OnPlaybackEnded;
+ if (OperatingSystem.IsAndroidVersionAtLeast(26))
+ {
+ var audioAttributes = new AudioAttributes.Builder()?
+ .SetContentType(audioPlayerOptions.AudioContentType)?
+ .SetUsage(audioPlayerOptions.AudioUsageKind)?
+ .Build();
+
+ if (audioAttributes is not null)
+ {
+ player.SetAudioAttributes(audioAttributes);
+ }
+ }
+ else
+ {
+ Android.Media.Stream streamType = Android.Media.Stream.System;
+
+ switch (audioPlayerOptions.AudioUsageKind)
+ {
+ case AudioUsageKind.Media:
+ streamType = Android.Media.Stream.Music;
+ break;
+ case AudioUsageKind.Alarm:
+ streamType = Android.Media.Stream.Alarm;
+ break;
+ case AudioUsageKind.Notification:
+ streamType = Android.Media.Stream.Notification;
+ break;
+ case AudioUsageKind.VoiceCommunication:
+ streamType = Android.Media.Stream.VoiceCall;
+ break;
+ case AudioUsageKind.Unknown:
+ break;
+ }
+
+ player.SetAudioStreamType(streamType);
+ }
if (OperatingSystem.IsAndroidVersionAtLeast(23))
{
@@ -284,8 +379,10 @@ internal AudioPlayer(Stream audioStream, AudioPlayerOptions audioPlayerOptions)
file = cachePath;
}
-
PrepareAudioSource();
+
+ // Set preferred output device if specified (API 23+)
+ SetPreferredOutputDevice(audioPlayerOptions.PreferredOutputDevice);
}
@@ -295,9 +392,49 @@ internal AudioPlayer(string fileName, AudioPlayerOptions audioPlayerOptions)
player.Completion += OnPlaybackEnded;
player.Error += OnError;
+ if (OperatingSystem.IsAndroidVersionAtLeast(26))
+ {
+ var audioAttributes = new AudioAttributes.Builder()?
+ .SetContentType(audioPlayerOptions.AudioContentType)?
+ .SetUsage(audioPlayerOptions.AudioUsageKind)?
+ .Build();
+
+ if (audioAttributes is not null)
+ {
+ player.SetAudioAttributes(audioAttributes);
+ }
+ }
+ else
+ {
+ Android.Media.Stream streamType = Android.Media.Stream.System;
+
+ switch (audioPlayerOptions.AudioUsageKind)
+ {
+ case AudioUsageKind.Media:
+ streamType = Android.Media.Stream.Music;
+ break;
+ case AudioUsageKind.Alarm:
+ streamType = Android.Media.Stream.Alarm;
+ break;
+ case AudioUsageKind.Notification:
+ streamType = Android.Media.Stream.Notification;
+ break;
+ case AudioUsageKind.VoiceCommunication:
+ streamType = Android.Media.Stream.VoiceCall;
+ break;
+ case AudioUsageKind.Unknown:
+ break;
+ }
+
+ player.SetAudioStreamType(streamType);
+ }
+
file = fileName;
PrepareAudioSource();
+
+ // Set preferred output device if specified (API 23+)
+ SetPreferredOutputDevice(audioPlayerOptions.PreferredOutputDevice);
}
static void DeleteFile(string path)
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.android.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.android.cs
index 8a3aadc..e420a55 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.android.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.android.cs
@@ -28,4 +28,33 @@ partial class AudioPlayerOptions : BaseOptions
/// If any other value is used, the default value of is used.
///
public AudioUsageKind AudioUsageKind { get; set; } = AudioUsageKind.Unknown;
+
+ ///
+ /// Gets or sets the preferred audio output device for Android. Default value: .
+ ///
+ ///
+ /// This property allows you to control which audio output device is used for playback.
+ /// For example, you can force audio to play through the device speaker even when Bluetooth is connected.
+ ///
+ /// This feature requires Android API 28 (Android 9.0 Pie) or higher.
+ /// On older versions, this setting will be ignored and the system default routing will be used.
+ ///
+ ///
+ /// Note: The system may override this preference based on user actions or system policies.
+ /// For example, if the user is in a phone call, the system may route audio differently.
+ ///
+ ///
+ /// Example usage to force audio to phone speaker:
+ ///
+ /// var options = new AudioPlayerOptions
+ /// {
+ /// PreferredOutputDevice = AudioOutputDevice.Speaker
+ /// };
+ /// var player = audioManager.CreatePlayer(stream, options);
+ ///
+ ///
+ ///
+#pragma warning disable CA1416 // This enum is only used on API 23+ contexts, initialized as default on all API levels
+ public AudioOutputDevice PreferredOutputDevice { get; set; } = AudioOutputDevice.Default;
+#pragma warning restore CA1416
}
From b5a14f302c57a926fe5c01f9d9ea288b9a529c71 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Oct 2025 11:31:15 +0000
Subject: [PATCH 3/9] Fix documentation references and API level comments
Co-authored-by: jfversluis <939291+jfversluis@users.noreply.github.com>
---
.../AudioPlayer/AudioOutputDevice.android.cs | 12 ++++++------
.../AudioPlayer/AudioPlayer.android.cs | 6 +++---
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputDevice.android.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputDevice.android.cs
index 5f5111a..bedc714 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputDevice.android.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputDevice.android.cs
@@ -21,25 +21,25 @@ public enum AudioOutputDevice
///
/// Route audio to the built-in device speaker (typically the loudspeaker).
- /// Corresponds to .
+ /// Corresponds to .
///
Speaker = AudioDeviceType.BuiltinSpeaker,
///
/// Route audio to the built-in earpiece (typically used for phone calls).
- /// Corresponds to .
+ /// Corresponds to .
///
Earpiece = AudioDeviceType.BuiltinEarpiece,
///
/// Route audio to a wired headset or headphones.
- /// Corresponds to .
+ /// Corresponds to .
///
WiredHeadset = AudioDeviceType.WiredHeadset,
///
/// Route audio to a wired headphone device.
- /// Corresponds to .
+ /// Corresponds to .
///
WiredHeadphones = AudioDeviceType.WiredHeadphones,
@@ -63,13 +63,13 @@ public enum AudioOutputDevice
///
/// Route audio to a USB audio device.
- /// Corresponds to .
+ /// Corresponds to .
///
UsbDevice = AudioDeviceType.UsbDevice,
///
/// Route audio to a USB accessory.
- /// Corresponds to .
+ /// Corresponds to .
///
UsbAccessory = AudioDeviceType.UsbAccessory,
}
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
index b4fece2..f616440 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
@@ -224,7 +224,7 @@ internal AudioPlayer(AudioPlayerOptions audioPlayerOptions)
player.Completion += OnPlaybackEnded;
- // Set preferred output device if specified (API 23+)
+ // Set preferred output device if specified (API 28+)
SetPreferredOutputDevice(audioPlayerOptions.PreferredOutputDevice);
}
@@ -381,7 +381,7 @@ internal AudioPlayer(Stream audioStream, AudioPlayerOptions audioPlayerOptions)
PrepareAudioSource();
- // Set preferred output device if specified (API 23+)
+ // Set preferred output device if specified (API 28+)
SetPreferredOutputDevice(audioPlayerOptions.PreferredOutputDevice);
}
@@ -433,7 +433,7 @@ internal AudioPlayer(string fileName, AudioPlayerOptions audioPlayerOptions)
PrepareAudioSource();
- // Set preferred output device if specified (API 23+)
+ // Set preferred output device if specified (API 28+)
SetPreferredOutputDevice(audioPlayerOptions.PreferredOutputDevice);
}
From e3b0ba94a50a727e7153caba81574e550ff105dd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Oct 2025 19:45:23 +0000
Subject: [PATCH 4/9] Extract audio attributes configuration into shared method
Co-authored-by: jfversluis <939291+jfversluis@users.noreply.github.com>
---
.../AudioPlayer/AudioPlayer.android.cs | 89 +++----------------
1 file changed, 12 insertions(+), 77 deletions(-)
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
index f616440..0460ac0 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
@@ -185,6 +185,16 @@ internal AudioPlayer(AudioPlayerOptions audioPlayerOptions)
{
player = new MediaPlayer();
+ ConfigureAudioAttributes(audioPlayerOptions);
+
+ player.Completion += OnPlaybackEnded;
+
+ // Set preferred output device if specified (API 28+)
+ SetPreferredOutputDevice(audioPlayerOptions.PreferredOutputDevice);
+ }
+
+ void ConfigureAudioAttributes(AudioPlayerOptions audioPlayerOptions)
+ {
if (OperatingSystem.IsAndroidVersionAtLeast(26))
{
var audioAttributes = new AudioAttributes.Builder()?
@@ -221,11 +231,6 @@ internal AudioPlayer(AudioPlayerOptions audioPlayerOptions)
player.SetAudioStreamType(streamType);
}
-
- player.Completion += OnPlaybackEnded;
-
- // Set preferred output device if specified (API 28+)
- SetPreferredOutputDevice(audioPlayerOptions.PreferredOutputDevice);
}
void SetPreferredOutputDevice(AudioOutputDevice preferredDevice)
@@ -319,42 +324,7 @@ internal AudioPlayer(Stream audioStream, AudioPlayerOptions audioPlayerOptions)
player = new MediaPlayer();
player.Completion += OnPlaybackEnded;
- if (OperatingSystem.IsAndroidVersionAtLeast(26))
- {
- var audioAttributes = new AudioAttributes.Builder()?
- .SetContentType(audioPlayerOptions.AudioContentType)?
- .SetUsage(audioPlayerOptions.AudioUsageKind)?
- .Build();
-
- if (audioAttributes is not null)
- {
- player.SetAudioAttributes(audioAttributes);
- }
- }
- else
- {
- Android.Media.Stream streamType = Android.Media.Stream.System;
-
- switch (audioPlayerOptions.AudioUsageKind)
- {
- case AudioUsageKind.Media:
- streamType = Android.Media.Stream.Music;
- break;
- case AudioUsageKind.Alarm:
- streamType = Android.Media.Stream.Alarm;
- break;
- case AudioUsageKind.Notification:
- streamType = Android.Media.Stream.Notification;
- break;
- case AudioUsageKind.VoiceCommunication:
- streamType = Android.Media.Stream.VoiceCall;
- break;
- case AudioUsageKind.Unknown:
- break;
- }
-
- player.SetAudioStreamType(streamType);
- }
+ ConfigureAudioAttributes(audioPlayerOptions);
if (OperatingSystem.IsAndroidVersionAtLeast(23))
{
@@ -392,42 +362,7 @@ internal AudioPlayer(string fileName, AudioPlayerOptions audioPlayerOptions)
player.Completion += OnPlaybackEnded;
player.Error += OnError;
- if (OperatingSystem.IsAndroidVersionAtLeast(26))
- {
- var audioAttributes = new AudioAttributes.Builder()?
- .SetContentType(audioPlayerOptions.AudioContentType)?
- .SetUsage(audioPlayerOptions.AudioUsageKind)?
- .Build();
-
- if (audioAttributes is not null)
- {
- player.SetAudioAttributes(audioAttributes);
- }
- }
- else
- {
- Android.Media.Stream streamType = Android.Media.Stream.System;
-
- switch (audioPlayerOptions.AudioUsageKind)
- {
- case AudioUsageKind.Media:
- streamType = Android.Media.Stream.Music;
- break;
- case AudioUsageKind.Alarm:
- streamType = Android.Media.Stream.Alarm;
- break;
- case AudioUsageKind.Notification:
- streamType = Android.Media.Stream.Notification;
- break;
- case AudioUsageKind.VoiceCommunication:
- streamType = Android.Media.Stream.VoiceCall;
- break;
- case AudioUsageKind.Unknown:
- break;
- }
-
- player.SetAudioStreamType(streamType);
- }
+ ConfigureAudioAttributes(audioPlayerOptions);
file = fileName;
From 9842109b009e67939338bcb7f058bcccf6c168ff Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Oct 2025 19:54:37 +0000
Subject: [PATCH 5/9] Standardize initialization order across all constructors
Co-authored-by: jfversluis <939291+jfversluis@users.noreply.github.com>
---
src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
index 0460ac0..ad60a91 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
@@ -322,10 +322,11 @@ public void SetSource(Stream audioStream)
internal AudioPlayer(Stream audioStream, AudioPlayerOptions audioPlayerOptions)
{
player = new MediaPlayer();
- player.Completion += OnPlaybackEnded;
ConfigureAudioAttributes(audioPlayerOptions);
+ player.Completion += OnPlaybackEnded;
+
if (OperatingSystem.IsAndroidVersionAtLeast(23))
{
using var memoryStream = new MemoryStream();
@@ -359,11 +360,12 @@ internal AudioPlayer(Stream audioStream, AudioPlayerOptions audioPlayerOptions)
internal AudioPlayer(string fileName, AudioPlayerOptions audioPlayerOptions)
{
player = new MediaPlayer();
- player.Completion += OnPlaybackEnded;
- player.Error += OnError;
ConfigureAudioAttributes(audioPlayerOptions);
+ player.Completion += OnPlaybackEnded;
+ player.Error += OnError;
+
file = fileName;
PrepareAudioSource();
From f4c1475518d2c78821e693ee2da8390daff81dac Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Oct 2025 20:23:05 +0000
Subject: [PATCH 6/9] Add iOS/macOS audio output port override support
Co-authored-by: jfversluis <939291+jfversluis@users.noreply.github.com>
---
docs/audio-player.md | 42 ++++++++++++++++-
.../AudioPlayer/AudioOutputPort.macios.cs | 26 +++++++++++
.../AudioPlayer/AudioPlayer.macios.cs | 27 +++++++++++
.../AudioPlayer/AudioPlayerOptions.macios.cs | 46 +++++++++++++++----
4 files changed, 132 insertions(+), 9 deletions(-)
create mode 100644 src/Plugin.Maui.Audio/AudioPlayer/AudioOutputPort.macios.cs
diff --git a/docs/audio-player.md b/docs/audio-player.md
index 19b23e1..62fc96c 100644
--- a/docs/audio-player.md
+++ b/docs/audio-player.md
@@ -59,7 +59,11 @@ audioManager.CreatePlayer(
For more information, please refer to the Android documentation: https://developer.android.com/reference/android/media/AudioAttributes
-### Controlling Audio Output Device (Android)
+### Controlling Audio Output Device/Port
+
+You can control which audio output device or port is used for playback on both Android and iOS/macOS platforms.
+
+#### Android - Output Device Selection
On Android, you can specify which audio output device should be used for playback. This is useful when you want to ensure audio plays through a specific output, such as the device speaker, even when other outputs like Bluetooth are connected.
@@ -90,6 +94,42 @@ Available output device options include:
**Note:** The system may override this preference based on user actions or system policies. If the requested device is not available or connected, the system will fall back to its default routing behavior.
+#### iOS/macOS - Output Port Override
+
+On iOS and macOS, you can override the audio output port to force audio to play through the built-in speaker, even when headphones or Bluetooth devices are connected.
+
+```csharp
+audioManager.CreatePlayer(
+ await FileSystem.OpenAppPackageFileAsync("beep.wav"),
+ new AudioPlayerOptions
+ {
+#if IOS || MACCATALYST
+ PreferredOutputPort = Plugin.Maui.Audio.AudioOutputPort.Speaker
+#endif
+ });
+```
+
+Available output port options include:
+- `AudioOutputPort.Default` - Use system default routing
+- `AudioOutputPort.Speaker` - Force output to built-in speaker
+
+**Note:** Unlike Android's per-player device selection, iOS uses a session-wide port override that affects all audio output on the device. The override remains in effect until explicitly changed back to `AudioOutputPort.Default`.
+
+#### Cross-Platform Example
+
+```csharp
+var options = new AudioPlayerOptions
+{
+#if ANDROID
+ PreferredOutputDevice = Plugin.Maui.Audio.AudioOutputDevice.Speaker,
+#elif IOS || MACCATALYST
+ PreferredOutputPort = Plugin.Maui.Audio.AudioOutputPort.Speaker,
+#endif
+};
+var player = audioManager.CreatePlayer(await FileSystem.OpenAppPackageFileAsync("beep.wav"), options);
+player.Play(); // Audio plays through device speaker on both Android and iOS
+```
+
## AudioPlayer API
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputPort.macios.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputPort.macios.cs
new file mode 100644
index 0000000..1d54eff
--- /dev/null
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputPort.macios.cs
@@ -0,0 +1,26 @@
+using AVFoundation;
+
+namespace Plugin.Maui.Audio;
+
+///
+/// Specifies the preferred audio output port override for iOS/macOS.
+///
+///
+/// This enum controls audio routing on iOS/macOS platforms using AVAudioSession.
+/// Unlike Android's device-specific routing, iOS uses a session-wide port override that affects all audio.
+///
+public enum AudioOutputPort
+{
+ ///
+ /// Use the default audio routing behavior. The system will route audio based on connected devices.
+ /// Corresponds to .
+ ///
+ Default = AVAudioSessionPortOverride.None,
+
+ ///
+ /// Force audio output to the built-in speaker, overriding the default routing.
+ /// Use this to ensure audio plays through the device speaker even when headphones or Bluetooth devices are connected.
+ /// Corresponds to .
+ ///
+ Speaker = AVAudioSessionPortOverride.Speaker,
+}
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs
index 1b3c755..739e442 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs
@@ -233,6 +233,9 @@ bool PreparePlayer()
{
ActiveSessionHelper.InitializeSession(audioPlayerOptions);
+ // Set preferred output port if specified
+ SetPreferredOutputPort(audioPlayerOptions.PreferredOutputPort);
+
player.FinishedPlaying += OnPlayerFinishedPlaying;
player.DecoderError += OnPlayerError;
@@ -242,6 +245,30 @@ bool PreparePlayer()
return true;
}
+ void SetPreferredOutputPort(AudioOutputPort preferredPort)
+ {
+ try
+ {
+ var audioSession = AVAudioSession.SharedInstance();
+ var portOverride = (AVAudioSessionPortOverride)preferredPort;
+
+ var error = audioSession.OverrideOutputAudioPort(portOverride, out NSError? nsError);
+
+ if (!error || nsError != null)
+ {
+ System.Diagnostics.Trace.TraceWarning($"Failed to set preferred output port: {nsError?.LocalizedDescription ?? "Unknown error"}");
+ }
+ else
+ {
+ System.Diagnostics.Trace.TraceInformation($"Preferred audio output port set to: {preferredPort}");
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Trace.TraceError($"Error setting preferred audio output port: {ex.Message}");
+ }
+ }
+
void OnPlayerError(object? sender, AVErrorEventArgs e)
{
OnError(e);
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.macios.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.macios.cs
index d6dffc1..aa2dd61 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.macios.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.macios.cs
@@ -4,11 +4,41 @@ namespace Plugin.Maui.Audio;
partial class AudioPlayerOptions
{
- ///
- /// Initializes a new instance of the class with default settings for macOS/iOS.
- ///
- public AudioPlayerOptions()
- {
- Category = AVAudioSessionCategory.Playback;
- }
-}
\ No newline at end of file
+ ///
+ /// Initializes a new instance of the class with default settings for macOS/iOS.
+ ///
+ public AudioPlayerOptions()
+ {
+ Category = AVAudioSessionCategory.Playback;
+ }
+
+ ///
+ /// Gets or sets the preferred audio output port override for iOS/macOS. Default value: .
+ ///
+ ///
+ /// This property allows you to override the audio output routing on iOS/macOS.
+ /// For example, you can force audio to play through the device speaker even when Bluetooth is connected.
+ ///
+ /// Note: This is a session-wide setting that affects all audio output on the device, not just this player.
+ /// The override remains in effect until explicitly changed back to .
+ ///
+ ///
+ /// Unlike Android's device-specific routing, iOS only supports two options:
+ ///
+ /// - - Use system default routing
+ /// - - Force output to built-in speaker
+ ///
+ ///
+ ///
+ /// Example usage to force audio to phone speaker:
+ ///
+ /// var options = new AudioPlayerOptions
+ /// {
+ /// PreferredOutputPort = AudioOutputPort.Speaker
+ /// };
+ /// var player = audioManager.CreatePlayer(stream, options);
+ ///
+ ///
+ ///
+ public AudioOutputPort PreferredOutputPort { get; set; } = AudioOutputPort.Default;
+}
From 1de9db7ddf161cd496729c42bef7eed763502a36 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Oct 2025 20:39:19 +0000
Subject: [PATCH 7/9] Fix AudioOutputPort enum type to ulong for iOS/macOS
Co-authored-by: jfversluis <939291+jfversluis@users.noreply.github.com>
---
src/Plugin.Maui.Audio/AudioPlayer/AudioOutputPort.macios.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputPort.macios.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputPort.macios.cs
index 1d54eff..dd45d4b 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputPort.macios.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioOutputPort.macios.cs
@@ -9,7 +9,7 @@ namespace Plugin.Maui.Audio;
/// This enum controls audio routing on iOS/macOS platforms using AVAudioSession.
/// Unlike Android's device-specific routing, iOS uses a session-wide port override that affects all audio.
///
-public enum AudioOutputPort
+public enum AudioOutputPort : ulong
{
///
/// Use the default audio routing behavior. The system will route audio based on connected devices.
From df9c0be2045704f2ee5414ec88ddadc482cbe01a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 25 Oct 2025 20:56:41 +0000
Subject: [PATCH 8/9] Address code review feedback - fix whitespace, remove
unnecessary pragma, fix error check
Co-authored-by: jfversluis <939291+jfversluis@users.noreply.github.com>
---
src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs | 4 +---
src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs | 2 +-
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
index ad60a91..df9e4ff 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
@@ -186,7 +186,7 @@ internal AudioPlayer(AudioPlayerOptions audioPlayerOptions)
player = new MediaPlayer();
ConfigureAudioAttributes(audioPlayerOptions);
-
+
player.Completion += OnPlaybackEnded;
// Set preferred output device if specified (API 28+)
@@ -269,9 +269,7 @@ void SetPreferredOutputDevice(AudioOutputDevice preferredDevice)
// Find the first device matching the preferred type
var targetDeviceType = (AudioDeviceType)preferredDevice;
-#pragma warning disable CA1416 // We already check for API 28+ above
var targetDevice = devices.FirstOrDefault(d => d.Type == targetDeviceType);
-#pragma warning restore CA1416
if (targetDevice is not null)
{
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs
index 739e442..14872d1 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs
@@ -254,7 +254,7 @@ void SetPreferredOutputPort(AudioOutputPort preferredPort)
var error = audioSession.OverrideOutputAudioPort(portOverride, out NSError? nsError);
- if (!error || nsError != null)
+ if (!error)
{
System.Diagnostics.Trace.TraceWarning($"Failed to set preferred output port: {nsError?.LocalizedDescription ?? "Unknown error"}");
}
From dc3f30768cb9f27d2373956c3051ef5dd09d27e9 Mon Sep 17 00:00:00 2001
From: Gerald Versluis
Date: Sat, 25 Oct 2025 23:10:24 +0200
Subject: [PATCH 9/9] Update docs/audio-player.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
docs/audio-player.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/audio-player.md b/docs/audio-player.md
index 62fc96c..9ca9398 100644
--- a/docs/audio-player.md
+++ b/docs/audio-player.md
@@ -127,7 +127,7 @@ var options = new AudioPlayerOptions
#endif
};
var player = audioManager.CreatePlayer(await FileSystem.OpenAppPackageFileAsync("beep.wav"), options);
-player.Play(); // Audio plays through device speaker on both Android and iOS
+player.Play(); // Audio plays through device speaker on both Android and iOS/macOS
```