From e8c6195445043f6216946f846633ebdc0e8e6a0b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 2 Mar 2026 18:57:17 +0000
Subject: [PATCH 1/4] Initial plan
From 3b2a1b98dcff4cab93b447593bd5971c7fb9cb08 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 2 Mar 2026 19:06:21 +0000
Subject: [PATCH 2/4] Add Picture in Picture (PiP) support with PipService,
Android implementation, and sample
Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com>
---
CHANGELOG.md | 3 +
.../PictureInPictureSamples.xaml | 42 ++++++++++++++
.../PictureInPictureSamples.xaml.cs | 18 ++++++
.../PictureInPictureSamplesViewModel.cs | 56 +++++++++++++++++++
.../Platforms/Android/MainActivity.cs | 12 +++-
.../Components/REGISTER_YOUR_SAMPLES_HERE.cs | 2 +
.../PictureInPicture/Android/PipService.cs | 43 ++++++++++++++
.../API/PictureInPicture/PipService.cs | 55 ++++++++++++++++++
.../API/PictureInPicture/dotnet/PipService.cs | 12 ++++
.../API/PictureInPicture/iOS/PipService.cs | 17 ++++++
10 files changed, 259 insertions(+), 1 deletion(-)
create mode 100644 src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamples.xaml
create mode 100644 src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamples.xaml.cs
create mode 100644 src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamplesViewModel.cs
create mode 100644 src/library/DIPS.Mobile.UI/API/PictureInPicture/Android/PipService.cs
create mode 100644 src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs
create mode 100644 src/library/DIPS.Mobile.UI/API/PictureInPicture/dotnet/PipService.cs
create mode 100644 src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 566fc8355..7277e9d0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## [55.3.0]
+- [PictureInPicture] Added `PipService` with `Enter()`, `IsSupported`, and `PipModeChanged` event for Android. Includes a sample in the Components app demonstrating usage.
+
## [55.2.2]
- [iOS26][Tip] Added more padding.
diff --git a/src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamples.xaml b/src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamples.xaml
new file mode 100644
index 000000000..646b3d651
--- /dev/null
+++ b/src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamples.xaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamples.xaml.cs b/src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamples.xaml.cs
new file mode 100644
index 000000000..22abe9cb0
--- /dev/null
+++ b/src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamples.xaml.cs
@@ -0,0 +1,18 @@
+namespace Components.ComponentsSamples.PictureInPicture;
+
+public partial class PictureInPictureSamples
+{
+ public PictureInPictureSamples()
+ {
+ InitializeComponent();
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ if (BindingContext is PictureInPictureSamplesViewModel vm)
+ {
+ vm.Unsubscribe();
+ }
+ }
+}
diff --git a/src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamplesViewModel.cs b/src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamplesViewModel.cs
new file mode 100644
index 000000000..48c7888ab
--- /dev/null
+++ b/src/app/Components/ComponentsSamples/PictureInPicture/PictureInPictureSamplesViewModel.cs
@@ -0,0 +1,56 @@
+using System.Windows.Input;
+using DIPS.Mobile.UI.API.PictureInPicture;
+using DIPS.Mobile.UI.MVVM;
+
+namespace Components.ComponentsSamples.PictureInPicture;
+
+public class PictureInPictureSamplesViewModel : ViewModel
+{
+ private string m_notes = string.Empty;
+ private string m_statusText = string.Empty;
+
+ public PictureInPictureSamplesViewModel()
+ {
+ EnterPipCommand = new Command(EnterPip);
+ PipService.PipModeChanged += OnPipModeChanged;
+ }
+
+ private void EnterPip()
+ {
+ PipService.Enter();
+ }
+
+ private void OnPipModeChanged(object? sender, bool isInPipMode)
+ {
+ StatusText = isInPipMode
+ ? "App is in Picture in Picture mode."
+ : "App returned from Picture in Picture mode.";
+ }
+
+ public void Unsubscribe()
+ {
+ PipService.PipModeChanged -= OnPipModeChanged;
+ }
+
+ public bool IsPipSupported => PipService.IsSupported;
+
+ public string Notes
+ {
+ get => m_notes;
+ set => RaiseWhenSet(ref m_notes, value);
+ }
+
+ public string StatusText
+ {
+ get => m_statusText;
+ set
+ {
+ RaiseWhenSet(ref m_statusText, value);
+ RaisePropertyChanged(nameof(HasStatus));
+ }
+ }
+
+ public bool HasStatus => !string.IsNullOrEmpty(StatusText);
+
+ public ICommand EnterPipCommand { get; }
+}
diff --git a/src/app/Components/Platforms/Android/MainActivity.cs b/src/app/Components/Platforms/Android/MainActivity.cs
index 3e16d5b75..0852acb87 100644
--- a/src/app/Components/Platforms/Android/MainActivity.cs
+++ b/src/app/Components/Platforms/Android/MainActivity.cs
@@ -1,13 +1,23 @@
using Android.App;
+using Android.Content;
using Android.Content.PM;
using Android.OS;
using AndroidX.Activity;
using AndroidX.Core.View;
+using DIPS.Mobile.UI.API.PictureInPicture;
namespace Components;
[Activity(Theme = "@style/DIPS.Mobile.UI.Style", MainLauncher = true,
ScreenOrientation = ScreenOrientation.Portrait,
+ SupportsPictureInPicture = true,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
-public class MainActivity : MauiAppCompatActivity;
\ No newline at end of file
+public class MainActivity : MauiAppCompatActivity
+{
+ public override void OnPictureInPictureModeChanged(bool isInPictureInPictureMode, Configuration newConfig)
+ {
+ base.OnPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
+ PipService.NotifyPipModeChanged(isInPictureInPictureMode);
+ }
+}
\ No newline at end of file
diff --git a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs
index 0a0ec44ee..d14c70507 100644
--- a/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs
+++ b/src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs
@@ -1,6 +1,7 @@
using Components.AccessibilitySamples;
using Components.AccessibilitySamples.VoiceOverSamples;
using Components.ComponentsSamples.Alerting;
+using Components.ComponentsSamples.PictureInPicture;
using Components.ComponentsSamples.AmplitudeView;
using Components.ComponentsSamples.BarcodeScanning;
using Components.ComponentsSamples.BottomSheets;
@@ -73,6 +74,7 @@ public static List RegisterSamples()
new(SampleType.Components, "Zoom Container", () => new PanZoomContainerSample()),
new(SampleType.Components, "Gallery", () => new GallerySample()),
new(SampleType.Components, "TIFF Viewer", () => new TiffViewerSample()),
+ new(SampleType.Components, "Picture in Picture", () => new PictureInPictureSamples()),
new(SampleType.Accessibility, "VoiceOver/TalkBack", () => new VoiceOverSamples()),
diff --git a/src/library/DIPS.Mobile.UI/API/PictureInPicture/Android/PipService.cs b/src/library/DIPS.Mobile.UI/API/PictureInPicture/Android/PipService.cs
new file mode 100644
index 000000000..cb143a52c
--- /dev/null
+++ b/src/library/DIPS.Mobile.UI/API/PictureInPicture/Android/PipService.cs
@@ -0,0 +1,43 @@
+using Android.Content.PM;
+using Android.OS;
+using Android.Util;
+
+namespace DIPS.Mobile.UI.API.PictureInPicture;
+
+public static partial class PipService
+{
+ public static partial bool IsSupported
+ {
+ get
+ {
+ if (Build.VERSION.SdkInt < BuildVersionCodes.O)
+ return false;
+
+ return Platform.AppContext.PackageManager
+ ?.HasSystemFeature(PackageManager.FeaturePictureInPicture) == true;
+ }
+ }
+
+ public static partial void Enter() => Enter(9, 16);
+
+ public static partial void Enter(int ratioWidth, int ratioHeight)
+ {
+ if (!IsSupported)
+ return;
+
+ var activity = Platform.CurrentActivity;
+ if (activity is null)
+ return;
+
+ var paramsBuilder = new Android.App.PictureInPictureParams.Builder();
+
+ if (Build.VERSION.SdkInt >= BuildVersionCodes.S)
+ {
+ paramsBuilder.SetSeamlessResizeEnabled(true);
+ }
+
+ paramsBuilder.SetAspectRatio(new Rational(ratioWidth, ratioHeight));
+
+ activity.EnterPictureInPictureMode(paramsBuilder.Build());
+ }
+}
diff --git a/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs b/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs
new file mode 100644
index 000000000..6f9a3bd30
--- /dev/null
+++ b/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs
@@ -0,0 +1,55 @@
+namespace DIPS.Mobile.UI.API.PictureInPicture;
+
+///
+/// Service for entering Picture in Picture (PiP) mode, allowing the app to be displayed
+/// in a small floating window while the user interacts with other apps or navigates away.
+///
+public static partial class PipService
+{
+ ///
+ /// Gets a value indicating whether Picture in Picture mode is supported on this device.
+ ///
+ public static partial bool IsSupported { get; }
+
+ ///
+ /// Enters Picture in Picture mode. The app will be displayed in a small floating window
+ /// with a default 9:16 (portrait) aspect ratio.
+ ///
+ ///
+ /// On Android, the activity must have android:supportsPictureInPicture="true" in the manifest,
+ /// or SupportsPictureInPicture = true in the [Activity] attribute.
+ /// On iOS, this feature is not currently supported for custom views.
+ ///
+ public static partial void Enter();
+
+ ///
+ /// Enters Picture in Picture mode with a specific aspect ratio for the PiP window.
+ ///
+ /// The width component of the desired aspect ratio.
+ /// The height component of the desired aspect ratio.
+ ///
+ /// On Android, the activity must have android:supportsPictureInPicture="true" in the manifest,
+ /// or SupportsPictureInPicture = true in the [Activity] attribute.
+ /// On iOS, this feature is not currently supported for custom views.
+ ///
+ public static partial void Enter(int ratioWidth, int ratioHeight);
+
+ ///
+ /// Raised when the app enters or exits Picture in Picture mode.
+ ///
+ ///
+ /// On Android, override OnPictureInPictureModeChanged in your MainActivity and call
+ /// for this event to fire.
+ ///
+ public static event EventHandler? PipModeChanged;
+
+ ///
+ /// Notifies the service that the PiP mode has changed.
+ /// Call this from your MainActivity.OnPictureInPictureModeChanged override on Android.
+ ///
+ /// true if the app entered PiP mode; false if it exited.
+ public static void NotifyPipModeChanged(bool isInPipMode)
+ {
+ PipModeChanged?.Invoke(null, isInPipMode);
+ }
+}
diff --git a/src/library/DIPS.Mobile.UI/API/PictureInPicture/dotnet/PipService.cs b/src/library/DIPS.Mobile.UI/API/PictureInPicture/dotnet/PipService.cs
new file mode 100644
index 000000000..e75124df5
--- /dev/null
+++ b/src/library/DIPS.Mobile.UI/API/PictureInPicture/dotnet/PipService.cs
@@ -0,0 +1,12 @@
+using DIPS.Mobile.UI.Exceptions;
+
+namespace DIPS.Mobile.UI.API.PictureInPicture;
+
+public static partial class PipService
+{
+ public static partial bool IsSupported => throw new Only_Here_For_UnitTests();
+
+ public static partial void Enter() => throw new Only_Here_For_UnitTests();
+
+ public static partial void Enter(int ratioWidth, int ratioHeight) => throw new Only_Here_For_UnitTests();
+}
diff --git a/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs b/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs
new file mode 100644
index 000000000..35880193f
--- /dev/null
+++ b/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs
@@ -0,0 +1,17 @@
+namespace DIPS.Mobile.UI.API.PictureInPicture;
+
+public static partial class PipService
+{
+ ///
+ /// Picture in Picture with custom views is not supported on iOS.
+ ///
+ public static partial bool IsSupported => false;
+
+ ///
+ /// Not implemented on iOS. Custom view PiP requires platform-specific integration with AVKit.
+ ///
+ public static partial void Enter() { }
+
+ ///
+ public static partial void Enter(int ratioWidth, int ratioHeight) { }
+}
From 0fc9495d96782802a6cc4480d0c3ba8e3a015ec2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 2 Mar 2026 19:23:10 +0000
Subject: [PATCH 3/4] Implement iOS PiP using AVPictureInPictureController with
AVSampleBufferDisplayLayer
Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com>
---
CHANGELOG.md | 2 +-
src/app/Components/Platforms/iOS/Info.plist | 4 +
.../API/PictureInPicture/PipService.cs | 8 +-
.../API/PictureInPicture/iOS/PipService.cs | 243 +++++++++++++++++-
4 files changed, 242 insertions(+), 15 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7277e9d0a..6f856883d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,5 @@
## [55.3.0]
-- [PictureInPicture] Added `PipService` with `Enter()`, `IsSupported`, and `PipModeChanged` event for Android. Includes a sample in the Components app demonstrating usage.
+- [PictureInPicture] Added `PipService` with `Enter()`, `Enter(ratioWidth, ratioHeight)`, `IsSupported`, and `PipModeChanged` event for Android and iOS. On iOS 15+, uses `AVPictureInPictureController` with `AVSampleBufferDisplayLayer` to display a snapshot of the current window. On Android, uses `EnterPictureInPictureMode`. Includes a sample in the Components app demonstrating usage.
## [55.2.2]
- [iOS26][Tip] Added more padding.
diff --git a/src/app/Components/Platforms/iOS/Info.plist b/src/app/Components/Platforms/iOS/Info.plist
index 5b2b155f1..58060e748 100644
--- a/src/app/Components/Platforms/iOS/Info.plist
+++ b/src/app/Components/Platforms/iOS/Info.plist
@@ -44,5 +44,9 @@
NSCameraUsageDescription
The app needs to use the camera to scan bar codes.
+ UIBackgroundModes
+
+ audio
+
diff --git a/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs b/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs
index 6f9a3bd30..c3229f293 100644
--- a/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs
+++ b/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs
@@ -18,7 +18,9 @@ public static partial class PipService
///
/// On Android, the activity must have android:supportsPictureInPicture="true" in the manifest,
/// or SupportsPictureInPicture = true in the [Activity] attribute.
- /// On iOS, this feature is not currently supported for custom views.
+ /// On iOS 15+, this captures a snapshot of the current window and displays it in a PiP window using
+ /// AVPictureInPictureController with AVSampleBufferDisplayLayer. Requires
+ /// UIBackgroundModes audio capability in the app's Info.plist.
///
public static partial void Enter();
@@ -30,7 +32,9 @@ public static partial class PipService
///
/// On Android, the activity must have android:supportsPictureInPicture="true" in the manifest,
/// or SupportsPictureInPicture = true in the [Activity] attribute.
- /// On iOS, this feature is not currently supported for custom views.
+ /// On iOS 15+, this captures a snapshot of the current window and displays it in a PiP window using
+ /// AVPictureInPictureController with AVSampleBufferDisplayLayer. Requires
+ /// UIBackgroundModes audio capability in the app's Info.plist.
///
public static partial void Enter(int ratioWidth, int ratioHeight);
diff --git a/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs b/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs
index 35880193f..c6ab2beb7 100644
--- a/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs
+++ b/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs
@@ -1,17 +1,236 @@
+using AVFoundation;
+using AVKit;
+using CoreGraphics;
+using CoreMedia;
+using CoreVideo;
+using DIPS.Mobile.UI.Internal.Logging;
+using Foundation;
+using UIKit;
+
namespace DIPS.Mobile.UI.API.PictureInPicture;
public static partial class PipService
{
- ///
- /// Picture in Picture with custom views is not supported on iOS.
- ///
- public static partial bool IsSupported => false;
-
- ///
- /// Not implemented on iOS. Custom view PiP requires platform-specific integration with AVKit.
- ///
- public static partial void Enter() { }
-
- ///
- public static partial void Enter(int ratioWidth, int ratioHeight) { }
+ private static AVPictureInPictureController? s_pipController;
+ private static PipSampleBufferView? s_pipView;
+ private static PipPlaybackDelegate? s_playbackDelegate;
+ private static PipControllerDelegate? s_controllerDelegate;
+
+ private static readonly CGColorSpace s_rgbColorSpace = CGColorSpace.CreateDeviceRGB();
+
+ public static partial bool IsSupported =>
+ AVPictureInPictureController.IsPictureInPictureSupported();
+
+ public static partial void Enter() => Enter(9, 16);
+
+ public static partial void Enter(int ratioWidth, int ratioHeight)
+ {
+ if (!IsSupported)
+ return;
+
+ var window = GetKeyWindow();
+ if (window is null)
+ return;
+
+ SetupAudioSession();
+
+ var windowWidth = window.Bounds.Width;
+ var windowHeight = windowWidth * ratioHeight / ratioWidth;
+ var frameSize = new CGSize(windowWidth, windowHeight);
+
+ s_pipView?.Dispose();
+ s_pipView = new PipSampleBufferView(frameSize);
+
+ s_playbackDelegate?.Dispose();
+ s_playbackDelegate = new PipPlaybackDelegate(window, frameSize, s_pipView.SampleBufferDisplayLayer);
+
+ s_controllerDelegate?.Dispose();
+ s_controllerDelegate = new PipControllerDelegate();
+
+ var contentSource = new AVPictureInPictureControllerContentSource(
+ s_pipView.SampleBufferDisplayLayer,
+ s_playbackDelegate);
+
+ s_pipController?.Dispose();
+ s_pipController = new AVPictureInPictureController(contentSource)
+ {
+ Delegate = s_controllerDelegate
+ };
+
+ s_playbackDelegate.PushCurrentFrame();
+
+ if (s_pipController.PictureInPicturePossible)
+ {
+ s_pipController.StartPictureInPicture();
+ }
+ }
+
+ private static UIWindow? GetKeyWindow()
+ {
+ foreach (var scene in UIApplication.SharedApplication.ConnectedScenes)
+ {
+ if (scene is UIWindowScene windowScene &&
+ windowScene.ActivationState == UISceneActivationState.ForegroundActive)
+ {
+ return windowScene.Windows.FirstOrDefault(w => w.IsKeyWindow);
+ }
+ }
+ return null;
+ }
+
+ private static void SetupAudioSession()
+ {
+ var audioSession = AVAudioSession.SharedInstance();
+ if (!audioSession.SetCategory(AVAudioSessionCategory.Playback,
+ AVAudioSessionCategoryOptions.MixWithOthers, out var categoryError))
+ {
+ DUILogService.LogError($"PiP: Failed to set AVAudioSession category: {categoryError?.LocalizedDescription}");
+ }
+
+ if (!audioSession.SetActive(true, out var activeError))
+ {
+ DUILogService.LogError($"PiP: Failed to activate AVAudioSession: {activeError?.LocalizedDescription}");
+ }
+ }
+
+ private sealed class PipSampleBufferView : UIView
+ {
+ public AVSampleBufferDisplayLayer SampleBufferDisplayLayer { get; }
+
+ public PipSampleBufferView(CGSize size) : base(new CGRect(CGPoint.Empty, size))
+ {
+ SampleBufferDisplayLayer = new AVSampleBufferDisplayLayer();
+ SampleBufferDisplayLayer.Frame = Bounds;
+ Layer.AddSublayer(SampleBufferDisplayLayer);
+ }
+ }
+
+ [Register("PipPlaybackDelegate")]
+ private sealed class PipPlaybackDelegate : NSObject, IAVPictureInPictureSampleBufferPlaybackDelegate
+ {
+ private readonly UIView m_sourceView;
+ private readonly CGSize m_frameSize;
+ private readonly AVSampleBufferDisplayLayer m_displayLayer;
+
+ public PipPlaybackDelegate(UIView sourceView, CGSize frameSize, AVSampleBufferDisplayLayer displayLayer)
+ {
+ m_sourceView = sourceView;
+ m_frameSize = frameSize;
+ m_displayLayer = displayLayer;
+ }
+
+ public void PushCurrentFrame()
+ {
+ var sampleBuffer = CreateSampleBuffer();
+ if (sampleBuffer is not null)
+ {
+ m_displayLayer.Enqueue(sampleBuffer);
+ sampleBuffer.Dispose();
+ }
+ }
+
+ ///
+ /// Static snapshot display — play/pause controls are not applicable.
+ ///
+ [Export("pictureInPictureController:setPlaying:")]
+ public void SetPlaying(AVPictureInPictureController pictureInPictureController, bool playing) { }
+
+ ///
+ /// Static snapshot display is always "paused" from PiP's perspective.
+ ///
+ [Export("pictureInPictureControllerIsPlaybackPaused:")]
+ public bool IsPlaybackPaused(AVPictureInPictureController pictureInPictureController) => true;
+
+ ///
+ /// Static snapshot display has no seekable time range — skip is a no-op.
+ ///
+ [Export("pictureInPictureController:skipByInterval:completionHandler:")]
+ public void SkipByInterval(AVPictureInPictureController pictureInPictureController, CMTime skipInterval,
+ Action completionHandler)
+ {
+ completionHandler();
+ }
+
+ ///
+ /// Returns an indefinite time range so the PiP controller does not show a playback position.
+ ///
+ [Export("pictureInPictureControllerTimeRangeForPlayback:")]
+ public CMTimeRange GetTimeRange(AVPictureInPictureController pictureInPictureController) =>
+ new CMTimeRange { Start = CMTime.Zero, Duration = CMTime.Indefinite };
+
+ [Export("pictureInPictureController:didTransitionToRenderSize:")]
+ public void DidTransitionToRenderSize(AVPictureInPictureController pictureInPictureController,
+ CMVideoDimensions newRenderSize) { }
+
+ private CMSampleBuffer? CreateSampleBuffer()
+ {
+ var width = (nint)m_frameSize.Width;
+ var height = (nint)m_frameSize.Height;
+
+ if (width <= 0 || height <= 0)
+ return null;
+
+ var pixelBufferAttrs = new CVPixelBufferAttributes
+ {
+ CGImageCompatibility = true,
+ CGBitmapContextCompatibility = true
+ };
+
+ var pixelBuffer = new CVPixelBuffer(width, height, CVPixelFormatType.CV32BGRA, pixelBufferAttrs);
+ pixelBuffer.Lock(CVOptionFlags.None);
+
+ try
+ {
+ using var bitmapContext = new CGBitmapContext(
+ pixelBuffer.BaseAddress,
+ width, height,
+ 8,
+ pixelBuffer.BytesPerRow,
+ s_rgbColorSpace,
+ CGBitmapFlags.ByteOrder32Little | CGBitmapFlags.NoneSkipFirst);
+
+ bitmapContext.TranslateCTM(0, height);
+ bitmapContext.ScaleCTM(1, -1);
+ m_sourceView.Layer.RenderInContext(bitmapContext);
+ }
+ finally
+ {
+ pixelBuffer.Unlock(CVOptionFlags.None);
+ }
+
+ var formatDescription = CMVideoFormatDescription.CreateForImageBuffer(pixelBuffer);
+ if (formatDescription is null)
+ {
+ pixelBuffer.Dispose();
+ return null;
+ }
+
+ var timing = new CMSampleTimingInfo
+ {
+ Duration = CMTime.Invalid,
+ PresentationTimeStamp = CMTime.Zero,
+ DecodeTimeStamp = CMTime.Invalid
+ };
+
+ CMSampleBuffer.CreateReadyWithImageBuffer(pixelBuffer, formatDescription, timing, out var sampleBuffer);
+ formatDescription.Dispose();
+ pixelBuffer.Dispose();
+
+ return sampleBuffer;
+ }
+ }
+
+ [Register("PipControllerDelegate")]
+ private sealed class PipControllerDelegate : AVPictureInPictureControllerDelegate
+ {
+ public override void DidStartPictureInPicture(AVPictureInPictureController pictureInPictureController)
+ {
+ NotifyPipModeChanged(true);
+ }
+
+ public override void DidStopPictureInPicture(AVPictureInPictureController pictureInPictureController)
+ {
+ NotifyPipModeChanged(false);
+ }
+ }
}
From 375652cbb7c828c6d59b19d050f4aa0860c9a649 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 2 Mar 2026 19:43:37 +0000
Subject: [PATCH 4/4] =?UTF-8?q?Remove=20AVAudioSession=20overhead=20from?=
=?UTF-8?q?=20iOS=20PiP=20=E2=80=94=20display-only,=20no=20audio=20needed?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com>
---
src/app/Components/Platforms/iOS/Info.plist | 4 ----
.../API/PictureInPicture/PipService.cs | 6 ++----
.../API/PictureInPicture/iOS/PipService.cs | 18 ------------------
3 files changed, 2 insertions(+), 26 deletions(-)
diff --git a/src/app/Components/Platforms/iOS/Info.plist b/src/app/Components/Platforms/iOS/Info.plist
index 58060e748..5b2b155f1 100644
--- a/src/app/Components/Platforms/iOS/Info.plist
+++ b/src/app/Components/Platforms/iOS/Info.plist
@@ -44,9 +44,5 @@
NSCameraUsageDescription
The app needs to use the camera to scan bar codes.
- UIBackgroundModes
-
- audio
-
diff --git a/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs b/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs
index c3229f293..bec226824 100644
--- a/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs
+++ b/src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs
@@ -19,8 +19,7 @@ public static partial class PipService
/// On Android, the activity must have android:supportsPictureInPicture="true" in the manifest,
/// or SupportsPictureInPicture = true in the [Activity] attribute.
/// On iOS 15+, this captures a snapshot of the current window and displays it in a PiP window using
- /// AVPictureInPictureController with AVSampleBufferDisplayLayer. Requires
- /// UIBackgroundModes audio capability in the app's Info.plist.
+ /// AVPictureInPictureController with AVSampleBufferDisplayLayer.
///
public static partial void Enter();
@@ -33,8 +32,7 @@ public static partial class PipService
/// On Android, the activity must have android:supportsPictureInPicture="true" in the manifest,
/// or SupportsPictureInPicture = true in the [Activity] attribute.
/// On iOS 15+, this captures a snapshot of the current window and displays it in a PiP window using
- /// AVPictureInPictureController with AVSampleBufferDisplayLayer. Requires
- /// UIBackgroundModes audio capability in the app's Info.plist.
+ /// AVPictureInPictureController with AVSampleBufferDisplayLayer.
///
public static partial void Enter(int ratioWidth, int ratioHeight);
diff --git a/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs b/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs
index c6ab2beb7..bfb77ba83 100644
--- a/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs
+++ b/src/library/DIPS.Mobile.UI/API/PictureInPicture/iOS/PipService.cs
@@ -3,7 +3,6 @@
using CoreGraphics;
using CoreMedia;
using CoreVideo;
-using DIPS.Mobile.UI.Internal.Logging;
using Foundation;
using UIKit;
@@ -32,8 +31,6 @@ public static partial void Enter(int ratioWidth, int ratioHeight)
if (window is null)
return;
- SetupAudioSession();
-
var windowWidth = window.Bounds.Width;
var windowHeight = windowWidth * ratioHeight / ratioWidth;
var frameSize = new CGSize(windowWidth, windowHeight);
@@ -78,21 +75,6 @@ public static partial void Enter(int ratioWidth, int ratioHeight)
return null;
}
- private static void SetupAudioSession()
- {
- var audioSession = AVAudioSession.SharedInstance();
- if (!audioSession.SetCategory(AVAudioSessionCategory.Playback,
- AVAudioSessionCategoryOptions.MixWithOthers, out var categoryError))
- {
- DUILogService.LogError($"PiP: Failed to set AVAudioSession category: {categoryError?.LocalizedDescription}");
- }
-
- if (!audioSession.SetActive(true, out var activeError))
- {
- DUILogService.LogError($"PiP: Failed to activate AVAudioSession: {activeError?.LocalizedDescription}");
- }
- }
-
private sealed class PipSampleBufferView : UIView
{
public AVSampleBufferDisplayLayer SampleBufferDisplayLayer { get; }