Skip to content

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

Open
aviadlevy wants to merge 1 commit into
DonutWare:developfrom
aviadlevy:pip-support
Open

feat(pip): add Picture-in-Picture support for Android and iOS#995
aviadlevy wants to merge 1 commit into
DonutWare:developfrom
aviadlevy:pip-support

Conversation

@aviadlevy
Copy link
Copy Markdown

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.

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.
@aviadlevy
Copy link
Copy Markdown
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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant