Skip to content
72 changes: 72 additions & 0 deletions docs/audio-player.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,78 @@ audioManager.CreatePlayer(

For more information, please refer to the Android documentation: https://developer.android.com/reference/android/media/AudioAttributes

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

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

#### 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/macOS
```


## AudioPlayer API

Once you have created an `AudioPlayer` you can interact with it in the following ways:
Expand Down
75 changes: 75 additions & 0 deletions src/Plugin.Maui.Audio/AudioPlayer/AudioOutputDevice.android.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Runtime.Versioning;
using Android.Media;

namespace Plugin.Maui.Audio;

/// <summary>
/// Specifies the preferred audio output device type for Android.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[SupportedOSPlatform("android23.0")]
public enum AudioOutputDevice
{
/// <summary>
/// Use the system default audio output device. No preferred device will be set.
/// </summary>
Default = 0,

/// <summary>
/// Route audio to the built-in device speaker (typically the loudspeaker).
/// Corresponds to <see cref="AudioDeviceType.BuiltinSpeaker"/>.
/// </summary>
Speaker = AudioDeviceType.BuiltinSpeaker,

/// <summary>
/// Route audio to the built-in earpiece (typically used for phone calls).
/// Corresponds to <see cref="AudioDeviceType.BuiltinEarpiece"/>.
/// </summary>
Earpiece = AudioDeviceType.BuiltinEarpiece,

/// <summary>
/// Route audio to a wired headset or headphones.
/// Corresponds to <see cref="AudioDeviceType.WiredHeadset"/>.
/// </summary>
WiredHeadset = AudioDeviceType.WiredHeadset,

/// <summary>
/// Route audio to a wired headphone device.
/// Corresponds to <see cref="AudioDeviceType.WiredHeadphones"/>.
/// </summary>
WiredHeadphones = AudioDeviceType.WiredHeadphones,

/// <summary>
/// Route audio to a Bluetooth device with A2DP profile (e.g., Bluetooth headphones, car audio).
/// Corresponds to <see cref="AudioDeviceType.BluetoothA2dp"/>.
/// </summary>
BluetoothA2dp = AudioDeviceType.BluetoothA2dp,

/// <summary>
/// Route audio to a Bluetooth SCO (Synchronous Connection Oriented) device (typically used for phone calls).
/// Corresponds to <see cref="AudioDeviceType.BluetoothSco"/>.
/// </summary>
BluetoothSco = AudioDeviceType.BluetoothSco,

/// <summary>
/// Route audio to an auxiliary line connection (e.g., 3.5mm aux cable).
/// Corresponds to <see cref="AudioDeviceType.AuxLine"/>.
/// </summary>
AuxLine = AudioDeviceType.AuxLine,

/// <summary>
/// Route audio to a USB audio device.
/// Corresponds to <see cref="AudioDeviceType.UsbDevice"/>.
/// </summary>
UsbDevice = AudioDeviceType.UsbDevice,

/// <summary>
/// Route audio to a USB accessory.
/// Corresponds to <see cref="AudioDeviceType.UsbAccessory"/>.
/// </summary>
UsbAccessory = AudioDeviceType.UsbAccessory,
}
26 changes: 26 additions & 0 deletions src/Plugin.Maui.Audio/AudioPlayer/AudioOutputPort.macios.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using AVFoundation;

namespace Plugin.Maui.Audio;

/// <summary>
/// Specifies the preferred audio output port override for iOS/macOS.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public enum AudioOutputPort : ulong
{
/// <summary>
/// Use the default audio routing behavior. The system will route audio based on connected devices.
/// Corresponds to <see cref="AVAudioSessionPortOverride.None"/>.
/// </summary>
Default = AVAudioSessionPortOverride.None,

/// <summary>
/// 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 <see cref="AVAudioSessionPortOverride.Speaker"/>.
/// </summary>
Speaker = AVAudioSessionPortOverride.Speaker,
}
80 changes: 76 additions & 4 deletions src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?
Expand Down Expand Up @@ -221,8 +231,60 @@ internal AudioPlayer(AudioPlayerOptions audioPlayerOptions)

player.SetAudioStreamType(streamType);
}

player.Completion += OnPlaybackEnded;
}

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;
var targetDevice = devices.FirstOrDefault(d => d.Type == targetDeviceType);

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)
Expand Down Expand Up @@ -258,8 +320,10 @@ 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))
{
Expand All @@ -284,20 +348,28 @@ internal AudioPlayer(Stream audioStream, AudioPlayerOptions audioPlayerOptions)
file = cachePath;
}


PrepareAudioSource();

// Set preferred output device if specified (API 28+)
SetPreferredOutputDevice(audioPlayerOptions.PreferredOutputDevice);
}


internal AudioPlayer(string fileName, AudioPlayerOptions audioPlayerOptions)
{
player = new MediaPlayer();

ConfigureAudioAttributes(audioPlayerOptions);

player.Completion += OnPlaybackEnded;
player.Error += OnError;

file = fileName;

PrepareAudioSource();

// Set preferred output device if specified (API 28+)
SetPreferredOutputDevice(audioPlayerOptions.PreferredOutputDevice);
}

static void DeleteFile(string path)
Expand Down
27 changes: 27 additions & 0 deletions src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ bool PreparePlayer()
{
ActiveSessionHelper.InitializeSession(audioPlayerOptions);

// Set preferred output port if specified
SetPreferredOutputPort(audioPlayerOptions.PreferredOutputPort);

player.FinishedPlaying += OnPlayerFinishedPlaying;
player.DecoderError += OnPlayerError;

Expand All @@ -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)
{
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);
Expand Down
29 changes: 29 additions & 0 deletions src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,33 @@ partial class AudioPlayerOptions : BaseOptions
/// If any other value is used, the default value of <see cref="Android.Media.Stream.System"/> is used.
/// </remarks>
public AudioUsageKind AudioUsageKind { get; set; } = AudioUsageKind.Unknown;

/// <summary>
/// Gets or sets the preferred audio output device for Android. Default value: <see cref="AudioOutputDevice.Default"/>.
/// </summary>
/// <remarks>
/// 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.
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// Example usage to force audio to phone speaker:
/// <code>
/// var options = new AudioPlayerOptions
/// {
/// PreferredOutputDevice = AudioOutputDevice.Speaker
/// };
/// var player = audioManager.CreatePlayer(stream, options);
/// </code>
/// </para>
/// </remarks>
#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
}
Loading