Audio Output
The IAudioOutputDevices service tells you where the music is going — the built-in speaker, wired or USB headphones, a Bluetooth speaker, the car, HDMI or AirPlay — and raises an event when that route changes, such as when the user unplugs their headphones or a Bluetooth speaker connects.
It is registered by AddShinyMusic(), so resolve it from DI like any other service:
public class NowPlayingViewModel(IAudioOutputDevices outputs, IMusicPlayer player){ // ...}The Current Route
Section titled “The Current Route”Current returns the route music is playing through, or null when the platform reports none:
var current = outputs.Current;
if (current != null) Console.WriteLine($"Playing on {current.Name} ({current.Type})"); // e.g. "Playing on JBL Flip 5 (BluetoothA2dp)"Each route is an AudioOutputDevice:
| Property | Type | Description |
|---|---|---|
Id | string | Stable platform identifier (Apple port UID / Android AudioDeviceInfo.Id) |
Name | string | Human-friendly name, e.g. "JBL Flip 5" or "Speaker" |
Type | AudioOutputType | Normalized route type |
IsCurrent | bool | Whether this is the route in use right now |
AudioOutputType normalizes what each platform reports into one set of values:
Unknown, BuiltInSpeaker, BuiltInReceiver, WiredHeadphones, WiredHeadset, Bluetooth (hands-free HFP/SCO/LE), BluetoothA2dp (stereo — the normal music route), Usb, CarAudio, Hdmi, AirPlay.
Classifying the Route
Section titled “Classifying the Route”Rather than switching on every AudioOutputType value, use the helpers in AudioOutputExtensions. Each has an overload for both AudioOutputDevice and AudioOutputType:
| Method | true for | Use it for |
|---|---|---|
IsWired() | WiredHeadphones, WiredHeadset, Usb | ”is something plugged in” |
IsBluetooth() | Bluetooth, BluetoothA2dp | showing a Bluetooth badge |
IsBuiltIn() | BuiltInSpeaker, BuiltInReceiver | nothing is plugged in or paired |
IsHeadphones() | wired or Bluetooth headphones/headsets | ”audio is private to the user” |
IsExternalSystem() | CarAudio, Hdmi, AirPlay | playback has left the device |
var current = outputs.Current;
if (current?.IsBluetooth() == true) ShowBluetoothIcon();else if (current?.IsWired() == true) ShowWiredIcon();else ShowSpeakerIcon();Reacting to Route Changes
Section titled “Reacting to Route Changes”Changed fires whenever the active output route changes. The argument is the new Current value, which may be null:
outputs.Changed += (sender, device) =>{ // Classic behavior: the user yanked the headphones out and audio fell back // to the loudspeaker — pause rather than blast the room. if (device?.IsBuiltIn() == true) MainThread.BeginInvokeOnMainThread(_player.Pause);};Listing Routes
Section titled “Listing Routes”GetOutputs() returns the output routes the OS reports, with IsCurrent set on the active one:
foreach (var device in outputs.GetOutputs()) Console.WriteLine($"{device.Name} — {device.Type}{(device.IsCurrent ? " (current)" : "")}");What this contains differs by platform — see below. It is not a device picker: no platform here lets an app switch the route.
Platform Behavior
Section titled “Platform Behavior”Android
Section titled “Android”GetOutputs() returns every connected output from AudioManager.GetDevices(Outputs).
Android’s AudioDeviceInfo carries no “active” flag, so Current is a derived value: the connected outputs are ranked the way the platform’s own media routing policy does and the winner is reported.
| Priority | Route |
|---|---|
| 1 | Bluetooth A2DP |
| 2 | Bluetooth hands-free (SCO/LE) |
| 3 | Wired headset |
| 4 | Wired headphones |
| 5 | USB |
| 6 | Car audio (Automotive bus) |
| 7 | HDMI |
| 8 | Built-in speaker |
| 9 | Earpiece |
That’s a very good approximation for media playback — but it is derived, not a platform guarantee. Route changes come from an AudioDeviceCallback registered on the first Changed subscription.
iOS and Mac Catalyst
Section titled “iOS and Mac Catalyst”Current comes straight from AVAudioSession.CurrentRoute — the OS reports the active route directly, so nothing is inferred.
The OS exposes only the active route’s ports; discovering every reachable AirPlay or Bluetooth destination is owned by the system route picker (AVRoutePickerView / Control Center). So GetOutputs() returns the current route — usually a single entry — rather than a full list of destinations.
A mic-equipped wired headset reports as WiredHeadphones, because Apple only surfaces the headset microphone on the input side. Use IsWired() or IsHeadphones() instead of testing for WiredHeadset directly.
Route changes come from the AVAudioSession route-change notification, observed on the first Changed subscription.