diff --git a/docs/audio-player.md b/docs/audio-player.md
index 8a1f592..167c015 100644
--- a/docs/audio-player.md
+++ b/docs/audio-player.md
@@ -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:
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
index 0fafc9f..ffb97bd 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.android.cs
@@ -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;
@@ -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))
{
@@ -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
@@ -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))
{
@@ -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;
@@ -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();
@@ -343,6 +395,9 @@ public void Stop()
player.Pause();
}
+ // Abandon audio focus when stopping
+ AbandonAudioFocus();
+
Seek(0);
OnPlaybackEnded(player, EventArgs.Empty);
@@ -355,6 +410,14 @@ public void Pause()
return;
}
+ PauseInternal();
+
+ // Abandon audio focus when pausing
+ AbandonAudioFocus();
+ }
+
+ void PauseInternal()
+ {
isPlaying = false;
player.Pause();
stopwatch.Stop();
@@ -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)
@@ -414,6 +580,7 @@ protected virtual void Dispose(bool disposing)
if (disposing)
{
+ AbandonAudioFocus();
player.Completion -= OnPlaybackEnded;
player.Error -= OnError;
player.Reset();
@@ -422,8 +589,27 @@ protected virtual void Dispose(bool disposing)
DeleteFile(cachePath);
cachePath = string.Empty;
stream?.Dispose();
+ audioFocusRequest?.Dispose();
}
isDisposed = true;
}
+
+ ///
+ /// Listens for audio focus changes from the Android system and delegates handling to the parent AudioPlayer.
+ ///
+ 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);
+ }
+ }
}
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs
index 1b3c755..b05d12d 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayer.macios.cs
@@ -11,6 +11,8 @@ partial class AudioPlayer : IAudioPlayer
AVAudioPlayer player;
readonly AudioPlayerOptions audioPlayerOptions;
bool isDisposed;
+ NSObject? interruptionObserver;
+ bool wasPlayingBeforeInterruption = false;
///
/// Gets the current position of audio playback in seconds.
@@ -131,6 +133,7 @@ public void SetSource(Stream audioStream)
{
player.FinishedPlaying -= OnPlayerFinishedPlaying;
player.DecoderError -= OnPlayerError;
+ UnregisterFromAudioInterruptions();
ActiveSessionHelper.FinishSession(audioPlayerOptions);
Stop();
player.Dispose();
@@ -179,6 +182,7 @@ protected virtual void Dispose(bool disposing)
if (disposing)
{
+ UnregisterFromAudioInterruptions();
ActiveSessionHelper.FinishSession(audioPlayerOptions);
Stop();
@@ -236,12 +240,86 @@ bool PreparePlayer()
player.FinishedPlaying += OnPlayerFinishedPlaying;
player.DecoderError += OnPlayerError;
+ // Subscribe to audio session interruptions
+ RegisterForAudioInterruptions();
+
player.EnableRate = true;
player.PrepareToPlay();
return true;
}
+ void RegisterForAudioInterruptions()
+ {
+ // Only register if interruption handling is enabled
+ if (audioPlayerOptions.HandleAudioInterruptions)
+ {
+ // Register for AVAudioSession interruption notifications
+ interruptionObserver = NSNotificationCenter.DefaultCenter.AddObserver(
+ AVAudioSession.InterruptionNotification,
+ HandleAudioSessionInterruption);
+ }
+ }
+
+ void UnregisterFromAudioInterruptions()
+ {
+ if (interruptionObserver is not null)
+ {
+ NSNotificationCenter.DefaultCenter.RemoveObserver(interruptionObserver);
+ interruptionObserver = null;
+ }
+ }
+
+ void HandleAudioSessionInterruption(NSNotification notification)
+ {
+ var interruptionType = GetInterruptionType(notification);
+
+ if (interruptionType == AVAudioSessionInterruptionType.Began)
+ {
+ // Audio session was interrupted (phone call, alarm, etc.)
+ if (player.Playing)
+ {
+ wasPlayingBeforeInterruption = true;
+ player.Pause();
+ }
+ }
+ else if (interruptionType == AVAudioSessionInterruptionType.Ended)
+ {
+ // Audio session interruption ended
+ var interruptionOptions = GetInterruptionOptions(notification);
+
+ // Check if we should resume playback
+ if (interruptionOptions.HasFlag(AVAudioSessionInterruptionOptions.ShouldResume) && wasPlayingBeforeInterruption)
+ {
+ wasPlayingBeforeInterruption = false;
+ Play();
+ }
+ }
+ }
+
+ ///
+ /// Retrieves the interruption type from an AVAudioSession interruption notification.
+ ///
+ /// The notification containing interruption information.
+ /// The interruption type, or Began if it cannot be determined (safer default).
+ AVAudioSessionInterruptionType GetInterruptionType(NSNotification notification)
+ {
+ var typeValue = notification.UserInfo?["AVAudioSessionInterruptionTypeKey"] as NSNumber;
+ // Default to Began if type cannot be determined - safer to assume interruption started
+ return typeValue != null ? (AVAudioSessionInterruptionType)(int)typeValue : AVAudioSessionInterruptionType.Began;
+ }
+
+ ///
+ /// Retrieves the interruption options from an AVAudioSession interruption notification.
+ ///
+ /// The notification containing interruption information.
+ /// The interruption options, or 0 if none are specified.
+ AVAudioSessionInterruptionOptions GetInterruptionOptions(NSNotification notification)
+ {
+ var optionsValue = notification.UserInfo?["AVAudioSessionInterruptionOptionKey"] as NSNumber;
+ return optionsValue != null ? (AVAudioSessionInterruptionOptions)(int)optionsValue : 0;
+ }
+
void OnPlayerError(object? sender, AVErrorEventArgs e)
{
OnError(e);
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.android.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.android.cs
index 8a3aadc..a7d9790 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.android.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.android.cs
@@ -28,4 +28,15 @@ 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 whether audio focus should be automatically managed. Default value: .
+ ///
+ ///
+ /// When enabled (default), the player will automatically request audio focus when playing and abandon it when paused or stopped.
+ /// This ensures proper interaction with other audio sources like phone calls and other apps.
+ /// When disabled, the player will not request or abandon audio focus, giving you full control over audio focus management.
+ /// See https://developer.android.com/media/optimize/audio-focus for more information.
+ ///
+ public bool ManageAudioFocus { get; set; } = true;
}
diff --git a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.macios.cs b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.macios.cs
index d6dffc1..e296a3e 100644
--- a/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.macios.cs
+++ b/src/Plugin.Maui.Audio/AudioPlayer/AudioPlayerOptions.macios.cs
@@ -11,4 +11,14 @@ public AudioPlayerOptions()
{
Category = AVAudioSessionCategory.Playback;
}
+
+ ///
+ /// Gets or sets whether audio interruptions should be automatically handled. Default value: .
+ ///
+ ///
+ /// When enabled (default), the player will automatically pause when interrupted (e.g., phone calls) and resume when appropriate.
+ /// When disabled, the player will not respond to audio interruptions, giving you full control over interruption handling.
+ /// See https://developer.apple.com/documentation/avfaudio/handling-audio-interruptions for more information.
+ ///
+ public bool HandleAudioInterruptions { get; set; } = true;
}
\ No newline at end of file