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; }