Skip to content

Commit 89ab802

Browse files
CopilotjfversluisCopilot
authored
Add audio output device/port selection for Android and iOS/macOS (#191)
* Add audio output device/port selection for Android and iOS/macOS Adds the ability to control which audio output device or port is used for playback on both Android and iOS/macOS platforms. Android: - New AudioOutputDevice enum mapping to AudioDeviceType values - PreferredOutputDevice property on AudioPlayerOptions - SetPreferredOutputDevice method with API 28+ support - Extracted ConfigureAudioAttributes for reuse after player.Reset() - Reapply audio attributes + preferred device after Reset in SetSpeedInternal and SetSource - Fixed invalid Pause() call in Prepared state after speed change iOS/macCatalyst: - New AudioOutputPort enum (Default, Speaker) mapping to AVAudioSessionPortOverride values - PreferredOutputPort property on AudioPlayerOptions - SetPreferredOutputPort with session-wide override - Early-return for Default to avoid undoing other players' overrides - Static ref-counted Speaker override with lock for thread safety - Proper cleanup on Dispose (only clears when last player releases) - SetSource correctly manages override ownership across player recreation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review findings: restore docs, fix iOS category, add sample - Restore deleted Audio Focus & Interruption Handling documentation - Fix iOS: auto-upgrade session to PlayAndRecord when speaker override is requested (required for OverrideOutputAudioPort to take effect) - Fix Android: use existing audioManager field instead of new instance - Remove misleading [SupportedOSPlatform] attribute from enum (runtime check in SetPreferredOutputDevice handles API level gracefully) - Add 'Force Speaker Output' toggle to sample app MusicPlayerPage - Update README.md with output device selection example - Add Windows 'not supported' note and Mac Catalyst caveat in docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix audioManager init and sample app concurrency - Initialize audioManager unconditionally in all 3 Android constructors so PreferredOutputDevice works even when ManageAudioFocus is false - Add reentry guard to sample ReloadWithOutputDevice to prevent crash from rapid toggle of Force Speaker Output switch - Properly unsubscribe PlaybackEnded before disposing old player - Fix iOS docs: speaker override does not override wired/BT devices Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix XML docs: speaker override does not override wired/BT Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Windows output device selection support - Add AudioPlayerOptions.PreferredOutputDeviceName property (string) - Enumerate audio render devices via DeviceInformation.FindAllAsync - Match by partial name (case-insensitive), set MediaPlayer.AudioDevice - Update docs, README, and sample app with Windows examples - Cross-platform example now covers all three platforms Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix SetSource reapply and docs note for Windows - Reapply PreferredOutputDeviceName after SetSource on Windows - Fix docs note: options are customizable on all platforms now Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Gerald Versluis <gerald@verslu.is> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1e47b13 commit 89ab802

12 files changed

Lines changed: 615 additions & 18 deletions

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,28 @@ Now that you know how to use the `AudioManager` class, please refer to the follo
6969
* [Stream audio](docs/audio-streamer.md)
7070
* [Audio listeners](docs/audio-listeners.md)
7171

72+
### Output Device Selection
73+
74+
You can control which audio output device is used for playback. For example, force audio through the device speaker even when Bluetooth is connected:
75+
76+
```csharp
77+
var player = audioManager.CreatePlayer(
78+
await FileSystem.OpenAppPackageFileAsync("ukelele.mp3"),
79+
new AudioPlayerOptions
80+
{
81+
#if ANDROID
82+
PreferredOutputDevice = Plugin.Maui.Audio.AudioOutputDevice.Speaker
83+
#elif IOS || MACCATALYST
84+
Category = AVFoundation.AVAudioSessionCategory.PlayAndRecord,
85+
PreferredOutputPort = Plugin.Maui.Audio.AudioOutputPort.Speaker
86+
#elif WINDOWS
87+
PreferredOutputDeviceName = "Speakers"
88+
#endif
89+
});
90+
```
91+
92+
For more details, see the [Audio playback documentation](docs/audio-player.md#controlling-audio-output-deviceport).
93+
7294
## Supported Audio Formats
7395

7496
### Audio Playback

docs/audio-player.md

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class AudioPlayerViewModel
2626
When calling `CreatePlayer` it is possible to provide an optional parameter of type `AudioPlayerOptions`, this parameter makes it possible to customize the playback settings at the platform level.
2727

2828
> [!NOTE]
29-
> Currently you can only customize options for iOS, macOS and Android.
29+
> Currently you can customize options for iOS, macOS, Android, and Windows.
3030
3131
The following example shows how to configure your audio to blend in with existing audio being played on device on iOS and macOS:
3232

@@ -123,6 +123,101 @@ When `HandleAudioInterruptions` is set to `false`, the player will not automatic
123123
> [!NOTE]
124124
> By default, both `ManageAudioFocus` (Android) and `HandleAudioInterruptions` (iOS/macOS) are enabled (`true`). Your app will properly interact with system audio and other apps out of the box. The audio focus management is handled transparently - you can still control playback manually using `Play()`, `Pause()`, and `Stop()` methods. For backward compatibility, playback will continue even if audio focus cannot be acquired, though this is rare.
125125
126+
## Controlling Audio Output Device/Port
127+
128+
You can control which audio output device or port is used for playback on all platforms.
129+
130+
### Android - Output Device Selection
131+
132+
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.
133+
134+
```csharp
135+
audioManager.CreatePlayer(
136+
await FileSystem.OpenAppPackageFileAsync("ukelele.mp3"),
137+
new AudioPlayerOptions
138+
{
139+
#if ANDROID
140+
PreferredOutputDevice = Plugin.Maui.Audio.AudioOutputDevice.Speaker
141+
#endif
142+
});
143+
```
144+
145+
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.
146+
147+
Available output device options include:
148+
- `AudioOutputDevice.Default` - Use system default routing
149+
- `AudioOutputDevice.Speaker` - Built-in device speaker (loudspeaker)
150+
- `AudioOutputDevice.Earpiece` - Built-in earpiece (typically used for phone calls)
151+
- `AudioOutputDevice.WiredHeadset` - Wired headset or headphones with microphone
152+
- `AudioOutputDevice.WiredHeadphones` - Wired headphones without microphone
153+
- `AudioOutputDevice.BluetoothA2dp` - Bluetooth device with A2DP profile (e.g., Bluetooth headphones, car audio)
154+
- `AudioOutputDevice.BluetoothSco` - Bluetooth SCO device (typically used for phone calls)
155+
- `AudioOutputDevice.UsbDevice` - USB audio device
156+
- `AudioOutputDevice.UsbAccessory` - USB accessory
157+
- `AudioOutputDevice.AuxLine` - Auxiliary line connection (e.g., 3.5mm aux cable)
158+
159+
**Note:** The system treats this as a preference, not a guarantee. If the requested device is not available or connected, the system will fall back to its default routing behavior.
160+
161+
### iOS/macOS - Output Port Override
162+
163+
On iOS, you can override the audio output port to force audio to play through the built-in speaker instead of the earpiece when no external audio devices are connected. This is primarily useful for `PlayAndRecord` sessions where the default output is the earpiece.
164+
165+
> [!IMPORTANT]
166+
> The speaker override requires the audio session category to be set to `PlayAndRecord`. If your session uses the default `Playback` category, the plugin will automatically upgrade to `PlayAndRecord` when a speaker override is requested. Note that speaker override does **not** override wired headphones or Bluetooth — when those are connected, audio will route to them regardless of this setting. On Mac Catalyst, speaker override has no effect since Macs do not distinguish between speaker and earpiece routing.
167+
168+
```csharp
169+
audioManager.CreatePlayer(
170+
await FileSystem.OpenAppPackageFileAsync("ukelele.mp3"),
171+
new AudioPlayerOptions
172+
{
173+
#if IOS || MACCATALYST
174+
Category = AVFoundation.AVAudioSessionCategory.PlayAndRecord,
175+
PreferredOutputPort = Plugin.Maui.Audio.AudioOutputPort.Speaker
176+
#endif
177+
});
178+
```
179+
180+
Available output port options include:
181+
- `AudioOutputPort.Default` - Use system default routing
182+
- `AudioOutputPort.Speaker` - Force output to built-in speaker
183+
184+
**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` or all players using the override are disposed.
185+
186+
### Windows - Output Device Selection
187+
188+
On Windows, you can specify which audio output device should be used for playback by providing the device name (or a partial name match).
189+
190+
```csharp
191+
audioManager.CreatePlayer(
192+
await FileSystem.OpenAppPackageFileAsync("ukelele.mp3"),
193+
new AudioPlayerOptions
194+
{
195+
#if WINDOWS
196+
PreferredOutputDeviceName = "Speakers"
197+
#endif
198+
});
199+
```
200+
201+
The first audio render device whose name contains the specified value (case-insensitive) will be selected. If the specified device is not found, the system default audio device will be used.
202+
203+
### Cross-Platform Example
204+
205+
```csharp
206+
var options = new AudioPlayerOptions
207+
{
208+
#if ANDROID
209+
PreferredOutputDevice = Plugin.Maui.Audio.AudioOutputDevice.Speaker,
210+
#elif IOS || MACCATALYST
211+
Category = AVFoundation.AVAudioSessionCategory.PlayAndRecord,
212+
PreferredOutputPort = Plugin.Maui.Audio.AudioOutputPort.Speaker,
213+
#elif WINDOWS
214+
PreferredOutputDeviceName = "Speakers",
215+
#endif
216+
};
217+
var player = audioManager.CreatePlayer(await FileSystem.OpenAppPackageFileAsync("ukelele.mp3"), options);
218+
player.Play(); // Audio plays through device speaker on all platforms
219+
```
220+
126221
## AudioPlayer API
127222

128223
Once you have created an `AudioPlayer` you can interact with it in the following ways:

samples/Plugin.Maui.Audio.Sample/Pages/MusicPlayerPage.xaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@
108108
Maximum="{Binding MaximumSpeed}"
109109
Value="{Binding UserSpeed}"
110110
DragCompletedCommand="{Binding UpdateSpeedCommand}"/>
111+
112+
<HorizontalStackLayout HorizontalOptions="Center" Spacing="3">
113+
<Label Text="Force Speaker Output:" VerticalOptions="Center" />
114+
<Switch IsToggled="{Binding ForceSpeakerOutput}" VerticalOptions="Center" />
115+
</HorizontalStackLayout>
111116
</VerticalStackLayout>
112117
</ScrollView>
113118

samples/Plugin.Maui.Audio.Sample/ViewModels/MusicPlayerPageViewModel.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,87 @@ public void UpdateSpeed()
188188
public double MinimumSpeed => audioPlayer?.MinimumSpeed ?? 1;
189189
public double MaximumSpeed => audioPlayer?.MaximumSpeed ?? 1;
190190

191+
bool forceSpeakerOutput;
192+
public bool ForceSpeakerOutput
193+
{
194+
get => forceSpeakerOutput;
195+
set
196+
{
197+
if (forceSpeakerOutput == value)
198+
{
199+
return;
200+
}
201+
202+
forceSpeakerOutput = value;
203+
NotifyPropertyChanged();
204+
ReloadWithOutputDevice();
205+
}
206+
}
207+
208+
bool isReloadingOutputDevice;
209+
210+
async void ReloadWithOutputDevice()
211+
{
212+
if (musicItemViewModel is null || isReloadingOutputDevice)
213+
{
214+
return;
215+
}
216+
217+
isReloadingOutputDevice = true;
218+
219+
try
220+
{
221+
var wasPlaying = audioPlayer?.IsPlaying ?? false;
222+
var position = audioPlayer?.CurrentPosition ?? 0;
223+
224+
if (audioPlayer is not null)
225+
{
226+
audioPlayer.PlaybackEnded -= AudioPlayer_PlaybackEnded;
227+
audioPlayer.Dispose();
228+
audioPlayer = null;
229+
}
230+
231+
var options = new AudioPlayerOptions();
232+
233+
#if ANDROID
234+
options.PreferredOutputDevice = forceSpeakerOutput
235+
? AudioOutputDevice.Speaker
236+
: AudioOutputDevice.Default;
237+
#elif IOS || MACCATALYST
238+
if (forceSpeakerOutput)
239+
{
240+
options.Category = AVFoundation.AVAudioSessionCategory.PlayAndRecord;
241+
options.PreferredOutputPort = AudioOutputPort.Speaker;
242+
}
243+
#elif WINDOWS
244+
options.PreferredOutputDeviceName = forceSpeakerOutput ? "Speakers" : null;
245+
#endif
246+
247+
audioPlayer = audioManager.CreatePlayer(
248+
await FileSystem.OpenAppPackageFileAsync(musicItemViewModel.Filename),
249+
options);
250+
audioPlayer.PlaybackEnded += AudioPlayer_PlaybackEnded;
251+
252+
if (position > 0 && audioPlayer.CanSeek)
253+
{
254+
audioPlayer.Seek(position);
255+
}
256+
257+
if (wasPlaying)
258+
{
259+
audioPlayer.Play();
260+
}
261+
262+
NotifyPropertyChanged(nameof(HasAudioSource));
263+
NotifyPropertyChanged(nameof(Duration));
264+
NotifyPropertyChanged(nameof(IsPlaying));
265+
}
266+
finally
267+
{
268+
isReloadingOutputDevice = false;
269+
}
270+
}
271+
191272
public bool Loop
192273
{
193274
get => audioPlayer?.Loop ?? false;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Android.Media;
2+
3+
namespace Plugin.Maui.Audio;
4+
5+
/// <summary>
6+
/// Specifies the preferred audio output device type for Android.
7+
/// </summary>
8+
/// <remarks>
9+
/// This enum is used to specify which audio output device should be preferred for playback.
10+
/// Requires Android API 28 (Android 9.0 Pie) or higher for setting the preferred device.
11+
/// On older versions, this setting will be ignored and the system default routing will be used.
12+
/// </remarks>
13+
public enum AudioOutputDevice
14+
{
15+
/// <summary>
16+
/// Use the system default audio output device. No preferred device will be set.
17+
/// </summary>
18+
Default = 0,
19+
20+
/// <summary>
21+
/// Route audio to the built-in device speaker (typically the loudspeaker).
22+
/// Corresponds to <see cref="AudioDeviceType.BuiltinSpeaker"/>.
23+
/// </summary>
24+
Speaker = AudioDeviceType.BuiltinSpeaker,
25+
26+
/// <summary>
27+
/// Route audio to the built-in earpiece (typically used for phone calls).
28+
/// Corresponds to <see cref="AudioDeviceType.BuiltinEarpiece"/>.
29+
/// </summary>
30+
Earpiece = AudioDeviceType.BuiltinEarpiece,
31+
32+
/// <summary>
33+
/// Route audio to a wired headset or headphones.
34+
/// Corresponds to <see cref="AudioDeviceType.WiredHeadset"/>.
35+
/// </summary>
36+
WiredHeadset = AudioDeviceType.WiredHeadset,
37+
38+
/// <summary>
39+
/// Route audio to a wired headphone device.
40+
/// Corresponds to <see cref="AudioDeviceType.WiredHeadphones"/>.
41+
/// </summary>
42+
WiredHeadphones = AudioDeviceType.WiredHeadphones,
43+
44+
/// <summary>
45+
/// Route audio to a Bluetooth device with A2DP profile (e.g., Bluetooth headphones, car audio).
46+
/// Corresponds to <see cref="AudioDeviceType.BluetoothA2dp"/>.
47+
/// </summary>
48+
BluetoothA2dp = AudioDeviceType.BluetoothA2dp,
49+
50+
/// <summary>
51+
/// Route audio to a Bluetooth SCO (Synchronous Connection Oriented) device (typically used for phone calls).
52+
/// Corresponds to <see cref="AudioDeviceType.BluetoothSco"/>.
53+
/// </summary>
54+
BluetoothSco = AudioDeviceType.BluetoothSco,
55+
56+
/// <summary>
57+
/// Route audio to an auxiliary line connection (e.g., 3.5mm aux cable).
58+
/// Corresponds to <see cref="AudioDeviceType.AuxLine"/>.
59+
/// </summary>
60+
AuxLine = AudioDeviceType.AuxLine,
61+
62+
/// <summary>
63+
/// Route audio to a USB audio device.
64+
/// Corresponds to <see cref="AudioDeviceType.UsbDevice"/>.
65+
/// </summary>
66+
UsbDevice = AudioDeviceType.UsbDevice,
67+
68+
/// <summary>
69+
/// Route audio to a USB accessory.
70+
/// Corresponds to <see cref="AudioDeviceType.UsbAccessory"/>.
71+
/// </summary>
72+
UsbAccessory = AudioDeviceType.UsbAccessory,
73+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using AVFoundation;
2+
3+
namespace Plugin.Maui.Audio;
4+
5+
/// <summary>
6+
/// Specifies the preferred audio output port override for iOS/macOS.
7+
/// </summary>
8+
/// <remarks>
9+
/// This enum controls audio routing on iOS/macOS platforms using AVAudioSession.
10+
/// Unlike Android's device-specific routing, iOS uses a session-wide port override that affects all audio.
11+
/// </remarks>
12+
public enum AudioOutputPort : ulong
13+
{
14+
/// <summary>
15+
/// Use the default audio routing behavior. The system will route audio based on connected devices.
16+
/// Corresponds to <see cref="AVAudioSessionPortOverride.None"/>.
17+
/// </summary>
18+
Default = AVAudioSessionPortOverride.None,
19+
20+
/// <summary>
21+
/// Force audio output to the built-in speaker, overriding the default earpiece routing.
22+
/// This is primarily useful when using the PlayAndRecord category, where the default output is the earpiece.
23+
/// Note: This does not override wired headphones or Bluetooth devices — when those are connected, audio routes to them regardless.
24+
/// Corresponds to <see cref="AVAudioSessionPortOverride.Speaker"/>.
25+
/// </summary>
26+
Speaker = AVAudioSessionPortOverride.Speaker,
27+
}

0 commit comments

Comments
 (0)