Skip to content
Shiny.Net.HttpServer v1 - A lightweight feature rich HTTP Server - Tunnels, Websockets, AOT, ASPNET Featureset, & Works EVERYWHERE!Let me see!

Background Playback & Picture-in-Picture

Two separate features, because the platforms treat them separately: audio can keep running when your app is not on screen, but video stops rendering unless the user is in Picture-in-Picture. That is a platform rule, not a limitation of this control.

<media:MediaElement x:Name="Player"
Source="{Binding EpisodeUrl}"
EnableBackgroundPlayback="True"
AutoHideTransportBar="False"
ShowFullScreenButton="False" />
Player.Metadata = new MediaMetadata
{
Title = "Episode 42 — The One About Handlers",
Artist = "The Show",
Album = "Season 3",
ArtworkUri = "https://example.com/art.jpg"
};
An audio-only player with the transport bar pinned open and background playback enabled

Audio keeps playing with the app backgrounded or the device locked, and Metadata drives whatever transport UI the OS puts in front of the user:

Platform Surface
iOS / Mac Catalyst Lock screen and Control Center via MPNowPlayingInfoCenter, with buttons routed back through MPRemoteCommandCenter
Android The media notification, from a Media3 MediaSession hosted by a mediaPlayback foreground service
Windows The SMTC flyout
macOS AppKit The Now Playing widget
Blazor navigator.mediaSession
Linux GTK4 None yet — no MPRIS integration, so EnableBackgroundPlayback is a no-op

Assign a new MediaMetadata instance to update it; the control watches the property, not the object’s fields.

  • Sets the AVAudioSession category to Playback and activates it on Apple platforms.
  • Detaches the AVPlayerLayer from the player when iOS backgrounds the app, and reattaches on return. Without that, iOS suspends video decode and takes the audio down with it.
  • Sets ExoPlayer’s wake mode to WakeModeNetwork on Android, so the CPU and wifi don’t sleep with the screen — the “it works until I put the phone down” bug.
  • Starts and stops the foreground service with the toggle, and releases the MediaSession when background playback is switched off.
if (!await Player.TryEnterPictureInPictureAsync())
await this.DisplayAlertAsync("Picture in Picture", "Not available on this device", "OK");

TryEnterPictureInPictureAsync returns false — it never throws — where the platform, OS version, or the app’s manifest doesn’t allow it. PictureInPictureCommand.CanExecute is false there too, so a bound button disables itself, and ShowPictureInPictureButton hides the built-in one.

Platform Backing
iOS / Mac Catalyst AVPictureInPictureController over the player layer
Android 8+ Activity.EnterPictureInPictureMode
Blazor video.requestPictureInPicture()
Windows, macOS AppKit, Linux Not available

Android needs two things from your activity

Section titled “Android needs two things from your activity”
[Activity(
Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
LaunchMode = LaunchMode.SingleTop,
SupportsPictureInPicture = true,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode
| ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
public override void OnPictureInPictureModeChanged(bool isInPictureInPictureMode, Configuration? newConfig)
{
base.OnPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
AndroidMediaIntegration.NotifyPictureInPictureModeChanged(isInPictureInPictureMode);
}
}

SupportsPictureInPicture = true is mandatory — EnterPictureInPictureMode throws without it, and only the app can declare it.

Forwarding OnPictureInPictureModeChanged is optional but wanted: Android reports PiP transitions only to the activity, so without it PiP still works, but IsPictureInPictureActive and the PictureInPictureChanged event go stale when the user collapses the floating window.

There is no “leave PiP” call on Android — the user taps the expand affordance, or the app brings its own activity to the front. ExitPictureInPictureAsync() is a no-op there and works on iOS and Blazor.

Android rejects a PiP aspect ratio outside 1:2.39 … 2.39:1 with an exception rather than a return value, so the ratio taken from the video track is clamped into that range before the request.

Rather than branching on platform, read Capabilities:

if (Player.Capabilities.HasFlag(MediaPlaybackCapabilities.BackgroundAudio))
ShowBackgroundToggle();
if (Player.Capabilities.HasFlag(MediaPlaybackCapabilities.PictureInPicture))
ShowPipButton();

The built-in transport bar already does this for its own buttons.