Skip to content

feat(pip): add Picture-in-Picture support for Android and iOS#995

Merged
PartyDonut merged 5 commits into
DonutWare:developfrom
aviadlevy:pip-support
Jun 7, 2026
Merged

feat(pip): add Picture-in-Picture support for Android and iOS#995
PartyDonut merged 5 commits into
DonutWare:developfrom
aviadlevy:pip-support

Conversation

@aviadlevy

Copy link
Copy Markdown
Contributor

Adds Picture-in-Picture (PiP) support for the Flutter-rendered video players (libMPV / libMDK) on Android, with the iOS plumbing in place but untested. The standalone native ExoPlayer VideoPlayerActivity is intentionally out of scope.

Behavior

  • A PiP icon is now part of the player controls (bottom row, landscape only — see "Decisions" below). Tapping it enters PiP at any time.
  • A new "Auto Picture-in-Picture" toggle under Settings → Player controls whether the OS should also auto-enter PiP when the app is backgrounded from the player. Defaults to on.
  • While in PiP, the controls overlay is hidden so only the video surface (plus the subtitle widget, if any) is captured for the PiP window.
  • On player dispose, auto-enter is turned off so the user is never auto-PiP'd from unrelated screens.

Architecture

  • lib/wrappers/pip_manager.dart — new PipManager wrapper hides package:pip behind a narrow PipClient interface so it can be unit-tested without the native plugin. Exposes enable / enter / disable / dispose and a Stream<bool> for the current PiP state. 9 unit tests.
  • lib/providers/pip_provider.dartpipManagerProvider + pipStateProvider.
  • VideoPlayer integration listens to the auto-enter preference and re-applies it live if the user toggles the setting mid-playback.

Platform plumbing

  • pubspec.yaml: add pip: ^0.0.3 (only file in lib/ that imports package:pip is the wrapper).
  • android/app/src/main/AndroidManifest.xml: add android:supportsPictureInPicture="true" to MainActivity. configChanges already covers screenSize|smallestScreenSize|screenLayout.
  • ios/Runner/Info.plist: add audio to UIBackgroundModes (required for iOS PiP per package:pip docs).

Decisions taken during implementation

  1. Setting semantics split. Originally the single toggle gated both manual button visibility and auto-enter. Per UX feedback this was split: the toggle now controls only the OS auto-enter-on-background behavior; the manual PiP button is always available on Android/iOS.
  2. Button placement. Tried three positions: bottom-row always, top-bar after minimize button, top-bar before close-X. Landed on bottom row, after the more button, landscape-only — phone portrait was overflowing the row (RIGHT OVERFLOWED BY 46 PIXELS). Portrait users access PiP via the Home button + the auto-enter setting.
  3. Icon. Material Icons.picture_in_picture_alt_outlined (rather than an IconsaxPlusLinear equivalent) — matches the de-facto Flutter standard used by other video apps (frosty, mydia) and YouTube/Netflix; the universal "rectangle-with-corner-rectangle" glyph is more discoverable than a generic maximize icon.
  4. Aspect ratio hardcoded 16:9. PlayerState does not currently expose video width/height. Plumbing real ratios through every backend was deferred to keep the PR focused.
  5. Native player out of scope. The separate VideoPlayerActivity (ExoPlayer/Compose) already has supportsPictureInPicture in its manifest but no logic; wiring that needs a separate Kotlin-side change and would have doubled the PR's surface area.
  6. One-pref-toggle design rather than the three-state Never / Always / Only with headphones from the original discussion. Smaller surface, easier to maintain; can be expanded later if there's demand.

Caveats & known limitations

  • iOS untested. The Dart code is cross-platform and the iOS-side Info.plist change is in, but I do not have an iOS dev environment. The flow should work per package:pip docs but should be validated by someone with an iPhone before being treated as supported.
  • Aspect ratio is fixed at 16:9 as noted above. Non-16:9 videos will have black bars inside the PiP frame on Android.
  • Subtitle rendering inside the PiP frame depends on the backend. Flutter-rendered subtitles are captured; subtitles burned into the player's native surface inherit the surface and also appear. No special handling was added beyond keeping the subtitle widget visible during PiP.

Implementation notes

This PR was implemented with the help of an AI coding assistant (Claude Code). All decisions, UX trade-offs, and code review were human-driven; the AI handled mechanical edits and the wrapper/test scaffolding. Generated code follows the project's existing conventions (dart format --line-length 120, sparse comments, freezed for settings).

Issue Being Fixed

Picture-in-Picture has been requested multiple times across several discussions. This PR addresses the mobile (Android) portion of those asks.

Screenshots / Recordings

Settings — Auto PiP toggle Portrait player — no overflow In PiP window
Landscape player — PiP icon in bottom row

Tested On

  • Android — Pixel 9 Pro XL emulator (API 34) and Pixel 10 Pro XL hardware (Android 16, API 36). All four scenarios verified: manual button, auto-enter on Home, toggle off → no auto-PiP, close PiP via × → playback ends and resume position is reported to Jellyfin.
  • Android TV
  • iOS — code in place, not validated on device.
  • Linux — N/A (no PiP on desktop)
  • Windows — N/A
  • macOS — N/A
  • Web — N/A

Checklist

  • If a new package was added, did you ensure it works for all supported platforms? Is the package well maintained — pip ^0.0.3 is a 0.0.x release with limited maintenance history; flagging as a known trade-off. Cross-platform (Android/iOS) by design. Open to swapping for a more mature alternative if maintainers prefer.
  • Check that any changes are related to the issue at hand.

@aviadlevy

Copy link
Copy Markdown
Contributor Author

Found a small thing I missed - when minimized and then home is pressed it's not getting into pip, just exit.
Expected behavior - should be pip mode

I'll be able to tackle it on Sunday

@GustavoGarufi

Copy link
Copy Markdown

This looks like an awesome feature. I'm looking forward to seeing this live!

@aviadlevy

Copy link
Copy Markdown
Contributor Author

This looks like an awesome feature. I'm looking forward to seeing this live!

I'm using this PR version for a while now without issues, and it's really the thing I missed on the official version (watching mainly on the road with some context switch while watching)

@PartyDonut PartyDonut left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for having a go at implementing this, looks and works great. One small change request, after that we can merge it.

Comment thread lib/wrappers/pip_manager.dart
Comment thread lib/screens/video_player/video_player_controls.dart Outdated
aviadlevy added 4 commits June 7, 2026 14:34
Adds the long-requested PiP feature for the Flutter-side video players
(libMPV/libMDK). Native ExoPlayer activity is out of scope.

Behavior:
- The PiP icon button in the player controls is always visible on
  Android/iOS and lets users manually enter PiP at any time.
- A new 'Auto Picture-in-Picture' setting (Settings → Player) controls
  only whether the OS should also auto-enter PiP when the app is
  backgrounded from the player. Defaults to on.
- While in PiP, the controls overlay hides so only the video surface
  is captured for the PiP window.
- Lifecycle: on player dispose, auto-enter is turned off so the user
  is not auto-PiP'd when backgrounding from unrelated screens.

Architecture:
- New PipManager wrapper (lib/wrappers/pip_manager.dart) hides
  package:pip behind a narrow PipClient interface so it can be
  unit-tested without the native plugin. Exposes enable/enter/disable/
  dispose and a Stream<bool> for the current PiP state. 9 unit tests.
- Riverpod providers (lib/providers/pip_provider.dart): pipManagerProvider
  and pipStateProvider.
- VideoPlayer integration listens to the auto-enter preference and
  re-applies it live if the user toggles the setting mid-playback.

Platform plumbing:
- pubspec: add pip ^0.0.3.
- Android manifest: supportsPictureInPicture=true on MainActivity.
  configChanges already covers screenSize|smallestScreenSize|screenLayout.
- iOS Info.plist: add 'audio' to UIBackgroundModes (required for iOS
  PiP per package:pip docs).

Caveats:
- PiP aspect ratio is hardcoded 16:9. PlayerState does not currently
  expose video width/height; plumbing real ratios through every
  backend is deferred.
- The native player (separate VideoPlayerActivity) already has
  supportsPictureInPicture in its manifest but no logic — out of scope.

Closes DonutWare#494.
Move PiP setup/teardown out of the fullscreen VideoPlayer state into a
top-level PipLifecycleController driven by mediaPlaybackProvider.state.
Previously, minimizing the player ran disable() and the mini-player
never re-enabled auto-enter, so pressing home from mini-mode exited the
app instead of entering PiP.

While in PiP from minimized state, swap the app tree for a fullscreen
video + subtitles on black so Android captures just the video rather
than the home screen and mini-player chrome.

@PartyDonut PartyDonut left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me thanks for the quick fixes

@PartyDonut PartyDonut added the feature New feature or request label Jun 7, 2026
@PartyDonut PartyDonut merged commit d2dec73 into DonutWare:develop Jun 7, 2026
1 check passed
@aviadlevy

Copy link
Copy Markdown
Contributor Author

Looks good to me thanks for the quick fixes

Thank you for this amazing app! It replaced all my previous android & tv & desktop apps for Jellyfin!

When should we expect a new version?

@aviadlevy aviadlevy deleted the pip-support branch June 7, 2026 12:54
@PartyDonut

Copy link
Copy Markdown
Collaborator

Looks good to me thanks for the quick fixes

Thank you for this amazing app! It replaced all my previous android & tv & desktop apps for Jellyfin!

When should we expect a new version?

Thanks good to hear you like the application 😄

Release will be in the next few days probably, depends on how quickly I can fix the small remaining issues with the music player and offline playback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants