Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions docs/audio-player.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,70 @@ audioManager.CreatePlayer(

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

## Audio Focus and Interruption Handling

The `AudioPlayer` automatically handles audio focus on Android and audio interruptions on iOS/macOS by default. This ensures proper behavior when your app interacts with other audio sources, such as phone calls, notifications, or other media apps.

### Android Audio Focus

On Android, the plugin automatically:
- **Requests audio focus** when you call `Play()`, notifying the system that your app wants to play audio
- **Abandons audio focus** when you call `Pause()` or `Stop()`, allowing other apps to take control
- **Responds to focus changes** from other apps:
- **Permanent loss**: Stops playback (e.g., user starts music in another app)
- **Temporary loss**: Pauses playback and resumes when focus returns (e.g., phone call)
- **Audio ducking**: Temporarily lowers volume to 20% while other audio plays (e.g., navigation prompts), then restores full volume

For more information, see the [Android Audio Focus documentation](https://developer.android.com/media/optimize/audio-focus).

#### Configuring Audio Focus (Android)

You can control audio focus behavior through the `AudioPlayerOptions`:

```csharp
var audioPlayer = audioManager.CreatePlayer(
await FileSystem.OpenAppPackageFileAsync("ukelele.mp3"),
new AudioPlayerOptions
{
#if ANDROID
ManageAudioFocus = false // Disable automatic audio focus management
#endif
});
```

When `ManageAudioFocus` is set to `false`, the player will not request or respond to audio focus changes, giving you full manual control.

### iOS/macOS Audio Interruptions

On iOS and macOS, the plugin automatically:
- **Registers for interruption notifications** when the player is created
- **Responds to interruptions**:
- **Interruption began**: Pauses playback (e.g., incoming phone call, alarm)
- **Interruption ended**: Resumes playback if the system indicates it should resume
- **Unregisters** interruption observers when the player is disposed

For more information, see the [iOS Audio Interruptions documentation](https://developer.apple.com/documentation/avfaudio/handling-audio-interruptions).

#### Configuring Interruption Handling (iOS/macOS)

You can control interruption handling behavior through the `AudioPlayerOptions`:

```csharp
var audioPlayer = audioManager.CreatePlayer(
await FileSystem.OpenAppPackageFileAsync("ukelele.mp3"),
new AudioPlayerOptions
{
#if IOS || MACCATALYST
HandleAudioInterruptions = false // Disable automatic interruption handling
#endif
});
```

When `HandleAudioInterruptions` is set to `false`, the player will not automatically pause or resume during interruptions, giving you full manual control.

> [!NOTE]
> 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.

## AudioPlayer API

Once you have created an `AudioPlayer` you can interact with it in the following ways:
Expand Down
186 changes: 186 additions & 0 deletions src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ partial class AudioPlayer : IAudioPlayer
MemoryStream? stream;
bool isDisposed = false;
AudioStopwatch stopwatch = new(TimeSpan.Zero, 1.0);
Android.Media.AudioManager? audioManager;
AudioFocusRequestClass? audioFocusRequest;
AudioFocusChangeListener? audioFocusChangeListener;
bool wasPlayingBeforeFocusLoss = false;
double volumeBeforeDucking = 0;
AudioPlayerOptions? audioPlayerOptions;

const double DuckingVolumeMultiplier = 0.2;

public double Duration => player.Duration <= -1 ? -1 : player.Duration / 1000.0;

Expand Down Expand Up @@ -184,6 +192,14 @@ void PrepareAudioSource()
internal AudioPlayer(AudioPlayerOptions audioPlayerOptions)
{
player = new MediaPlayer();
this.audioPlayerOptions = audioPlayerOptions;

// Initialize audio manager and focus listener only if audio focus management is enabled
if (audioPlayerOptions.ManageAudioFocus)
{
audioManager = (Android.Media.AudioManager?)Android.App.Application.Context.GetSystemService(Android.Content.Context.AudioService);
audioFocusChangeListener = new AudioFocusChangeListener(this);
}

if (OperatingSystem.IsAndroidVersionAtLeast(26))
{
Expand All @@ -195,6 +211,15 @@ internal AudioPlayer(AudioPlayerOptions audioPlayerOptions)
if (audioAttributes is not null)
{
player.SetAudioAttributes(audioAttributes);

// Build audio focus request for Android 26+ only if audio focus management is enabled
if (audioPlayerOptions.ManageAudioFocus && audioManager is not null && audioFocusChangeListener is not null)
{
audioFocusRequest = new AudioFocusRequestClass.Builder(AudioFocus.Gain)?
.SetAudioAttributes(audioAttributes)?
.SetOnAudioFocusChangeListener(audioFocusChangeListener)?
.Build();
}
}
}
else
Expand Down Expand Up @@ -259,7 +284,14 @@ internal AudioPlayer(Stream audioStream, AudioPlayerOptions audioPlayerOptions)
{
player = new MediaPlayer();
player.Completion += OnPlaybackEnded;
this.audioPlayerOptions = audioPlayerOptions;

// Initialize audio manager and focus listener only if audio focus management is enabled
if (audioPlayerOptions.ManageAudioFocus)
{
audioManager = (Android.Media.AudioManager?)Android.App.Application.Context.GetSystemService(Android.Content.Context.AudioService);
audioFocusChangeListener = new AudioFocusChangeListener(this);
}

if (OperatingSystem.IsAndroidVersionAtLeast(23))
{
Expand Down Expand Up @@ -294,6 +326,14 @@ internal AudioPlayer(string fileName, AudioPlayerOptions audioPlayerOptions)
player = new MediaPlayer();
player.Completion += OnPlaybackEnded;
player.Error += OnError;
this.audioPlayerOptions = audioPlayerOptions;

// Initialize audio manager and focus listener only if audio focus management is enabled
if (audioPlayerOptions.ManageAudioFocus)
{
audioManager = (Android.Media.AudioManager?)Android.App.Application.Context.GetSystemService(Android.Content.Context.AudioService);
audioFocusChangeListener = new AudioFocusChangeListener(this);
}

file = fileName;

Expand Down Expand Up @@ -330,6 +370,18 @@ public void Play()
stopwatch.Reset();
}

// Request audio focus before playing
if (!RequestAudioFocus())
{
System.Diagnostics.Trace.TraceWarning("Failed to request audio focus");
// Continue playing even if focus request fails for backward compatibility
}

PlayInternal();
}

void PlayInternal()
{
isPlaying = true;
player.Start();
stopwatch.Start();
Expand All @@ -343,6 +395,9 @@ public void Stop()
player.Pause();
}

// Abandon audio focus when stopping
AbandonAudioFocus();

Seek(0);

OnPlaybackEnded(player, EventArgs.Empty);
Expand All @@ -355,6 +410,14 @@ public void Pause()
return;
}

PauseInternal();

// Abandon audio focus when pausing
AbandonAudioFocus();
}

void PauseInternal()
{
isPlaying = false;
player.Pause();
stopwatch.Stop();
Expand Down Expand Up @@ -405,6 +468,109 @@ void OnError(object? sender, MediaPlayer.ErrorEventArgs e)
OnError(e);
}

bool RequestAudioFocus()
{
// Check if audio focus management is enabled
if (audioPlayerOptions?.ManageAudioFocus != true || audioManager is null)
{
return false;
}

AudioFocusRequest result;

if (OperatingSystem.IsAndroidVersionAtLeast(26) && audioFocusRequest is not null)
{
result = audioManager.RequestAudioFocus(audioFocusRequest);
}
else
{
// For API < 26, use deprecated method
#pragma warning disable CS0618 // Type or member is obsolete
result = audioManager.RequestAudioFocus(
audioFocusChangeListener,
Android.Media.Stream.Music,
AudioFocus.Gain);
#pragma warning restore CS0618 // Type or member is obsolete
}

return result == AudioFocusRequest.Granted;
}

void AbandonAudioFocus()
{
// Check if audio focus management is enabled
if (audioPlayerOptions?.ManageAudioFocus != true || audioManager is null)
{
return;
}

if (OperatingSystem.IsAndroidVersionAtLeast(26) && audioFocusRequest is not null)
{
audioManager.AbandonAudioFocusRequest(audioFocusRequest);
}
else
{
// For API < 26, use deprecated method
#pragma warning disable CS0618 // Type or member is obsolete
audioManager.AbandonAudioFocus(audioFocusChangeListener);
#pragma warning restore CS0618 // Type or member is obsolete
}
}

void HandleAudioFocusChange(AudioFocus focusChange)
{
switch (focusChange)
{
case AudioFocus.Loss:
// Permanent loss of audio focus - stop playback
// Reset state before Stop() to prevent incorrect state in any callbacks
wasPlayingBeforeFocusLoss = false;
volumeBeforeDucking = 0;
if (IsPlaying)
{
Stop();
}
break;

case AudioFocus.LossTransient:
// Temporary loss of audio focus - pause playback
if (IsPlaying)
{
wasPlayingBeforeFocusLoss = true;
// Don't abandon audio focus here since we want to resume later
PauseInternal();
}
break;

case AudioFocus.LossTransientCanDuck:
// Temporary loss of audio focus but can duck (lower volume)
// Lower the volume but continue playing
if (IsPlaying)
{
volumeBeforeDucking = Volume;
Volume = volumeBeforeDucking * DuckingVolumeMultiplier;
}
break;

case AudioFocus.Gain:
// Regained audio focus
if (wasPlayingBeforeFocusLoss)
{
// Resume playback if it was paused due to transient loss
// Use PlayInternal() since we already have audio focus
PlayInternal();
wasPlayingBeforeFocusLoss = false;
}
// Restore volume if it was ducked
if (volumeBeforeDucking > 0)
{
Volume = volumeBeforeDucking;
volumeBeforeDucking = 0;
}
break;
}
}

protected virtual void Dispose(bool disposing)
{
if (isDisposed)
Expand All @@ -414,6 +580,7 @@ protected virtual void Dispose(bool disposing)

if (disposing)
{
AbandonAudioFocus();
player.Completion -= OnPlaybackEnded;
player.Error -= OnError;
player.Reset();
Expand All @@ -422,8 +589,27 @@ protected virtual void Dispose(bool disposing)
DeleteFile(cachePath);
cachePath = string.Empty;
stream?.Dispose();
audioFocusRequest?.Dispose();
}

isDisposed = true;
}

/// <summary>
/// Listens for audio focus changes from the Android system and delegates handling to the parent AudioPlayer.
/// </summary>
class AudioFocusChangeListener : Java.Lang.Object, Android.Media.AudioManager.IOnAudioFocusChangeListener
{
readonly AudioPlayer audioPlayer;

public AudioFocusChangeListener(AudioPlayer player)
{
audioPlayer = player;
}

public void OnAudioFocusChange(AudioFocus focusChange)
{
audioPlayer.HandleAudioFocusChange(focusChange);
}
}
}
Loading