@management-and-computer-consultants/react-native-picture-in-picture
v1.0.0
Published
React Native Picture-in-Picture for keeping a session alive when the user presses Home. Android activity PiP + iOS AVKit sample-buffer PiP with a timer overlay.
Maintainers
Readme
react-native-picture-in-picture
React Native Picture-in-Picture for keeping a session alive when the user presses Home or switches apps (for example a voice recording).
npm: @management-and-computer-consultants/react-native-picture-in-picture
GitHub: Management-AND-Computer-Consultants/mcc-react-native-picture-in-picture
Native module: NativeModules.PictureInPicture
| Platform | Mechanism |
|----------|-----------|
| Android 8+ (API 26) | Activity PiP (enterPictureInPictureMode) |
| Android 12+ (API 31) | setAutoEnterEnabled(true) on the Home gesture |
| iOS 15+ | AVPictureInPictureController + hidden AVSampleBufferDisplayLayer |
The PiP window shows REC, a mm:ss countdown, and Playing / Paused. iOS PiP chrome play/pause is forwarded to JS.
Table of contents
- Install
- Architecture
- Package layout
- JS usage
- API
- Android host activity
- iOS host app
- Runtime flow
- Native method map
- Notes
1. Install
cd your-react-native-app
npm install @management-and-computer-consultants/react-native-picture-in-picturepackage.json:
"@management-and-computer-consultants/react-native-picture-in-picture": "^1.0.0"Then rebuild native (Metro reload is not enough):
npx react-native run-android
cd ios && pod install && cd .. && npx react-native run-iosThe package autolinks PictureInPicturePackage. Do not add it by hand in MainApplication unless autolinking is disabled.
Local checkout for edits: D:\CoDe\RNPlugin\react-native-picture-in-picture.
2. Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Host JS │
│ enablePictureInPicture() / disablePictureInPicture() │
│ updatePictureInPictureStatus(seconds, paused) │
│ subscribePictureInPicture({ onActiveChange, onPlayPause }) │
└───────────────────────────────┬─────────────────────────────────┘
│ NativeModules.PictureInPicture
│ events: PictureInPictureChange
│ PictureInPicturePlayPause (iOS)
┌───────────────────────────────▼─────────────────────────────────┐
│ Plugin (autolinked) │
│ android: PictureInPicturePackage / Module / Host │
│ ios: PictureInPicture.m (AVKit sample-buffer PiP) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Host app (required) │
│ Android MainActivity → PictureInPictureHost lifecycle hooks │
│ AndroidManifest supportsPictureInPicture │
│ iOS Info.plist UIBackgroundModes = audio │
└─────────────────────────────────────────────────────────────────┘PiP is opt-in. Call enable only while the session is active. Call disable on unmount, or Home on later screens will try to enter PiP.
This plugin does not intercept the lock button. Pair with a wake-state listener if you need an unlock warning.
3. Package layout
react-native-picture-in-picture/
├── src/index.ts
├── android/
│ ├── build.gradle
│ └── src/main/java/com/reactnativepictureinpicture/
│ ├── PictureInPicturePackage.kt
│ ├── PictureInPictureModule.kt
│ └── PictureInPictureHost.kt # call from MainActivity
├── ios/PictureInPicture.m
├── react-native.config.js
├── react-native-picture-in-picture.podspec
└── README.md4. JS usage
import {
disablePictureInPicture,
enablePictureInPicture,
subscribePictureInPicture,
updatePictureInPictureStatus,
} from '@management-and-computer-consultants/react-native-picture-in-picture';
useEffect(() => {
enablePictureInPicture();
const unsubscribe = subscribePictureInPicture({
onActiveChange: setIsPipActive,
onPlayPause: playing => {
// iOS PiP chrome — toggle pause/resume in your session
},
});
return () => {
unsubscribe();
disablePictureInPicture();
};
}, []);
useEffect(() => {
updatePictureInPictureStatus(remainingSeconds, isPaused);
}, [remainingSeconds, isPaused]);Default export:
import PictureInPicture from '@management-and-computer-consultants/react-native-picture-in-picture';
PictureInPicture.enable();
PictureInPicture.updateStatus(remainingSeconds, isPaused);Recording-session aliases (enableRecordingPip, subscribeRecordingPip, …) match older Eyes & Ears helpers.
5. API
| Export | Description |
|--------|-------------|
| enablePictureInPicture() | Allow Home / recents to enter PiP |
| disablePictureInPicture() | Stop auto-enter; iOS stops PiP |
| enterPictureInPicture() | Enter now if enabled |
| updatePictureInPictureStatus(seconds, paused) | Drive the overlay countdown |
| subscribePictureInPicture({ onActiveChange, onPlayPause }) | Returns unsubscribe |
| isPictureInPictureEnabled() | JS flag after enable / disable |
| isPictureInPictureActive() | Last native active event |
| isPictureInPictureSupported() | Promise<boolean> |
| isNativePictureInPictureActive() | Promise<boolean> |
Aliases: enableRecordingPip, disableRecordingPip, enterRecordingPip, updateRecordingPipStatus, subscribeRecordingPip, isRecordingPipEnabled, isRecordingPipActive.
Events:
{ active: boolean } // PictureInPictureChange
{ playing: boolean } // PictureInPicturePlayPause (iOS chrome only)6. Android host activity
Autolinking cannot override onUserLeaveHint / onPictureInPictureModeChanged. Wire the launch activity.
Manifest
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|density|fontScale|layoutDirection"configChanges must include screenSize / smallestScreenSize so entering PiP does not recreate the activity.
MainActivity
import android.content.res.Configuration
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.reactnativepictureinpicture.PictureInPictureHost
class MainActivity : ReactActivity() {
override fun getMainComponentName(): String = "YourApp"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
PictureInPictureHost.onUserLeaveHint(this)
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration,
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
PictureInPictureHost.onPictureInPictureModeChanged(this, isInPictureInPictureMode)
}
override fun onPictureInPictureRequested(): Boolean {
if (PictureInPictureHost.onPictureInPictureRequested(this)) {
return true
}
return super.onPictureInPictureRequested()
}
override fun onPause() {
super.onPause()
PictureInPictureHost.onPause(this)
}
override fun onResume() {
super.onResume()
PictureInPictureHost.onResume(this)
}
override fun createReactActivityDelegate(): ReactActivityDelegate =
PictureInPictureHost.createReactActivityDelegate(this, mainComponentName)
}Skipping React onPause while in PiP keeps JS (and a recorder) running.
If you previously had an in-app RecordingPipPackage, remove it from MainApplication so two modules do not fight.
7. iOS host app
pod installafter npm install (autolinksPictureInPicture.m).- In
Info.plist, keep audio in background modes (AVKit PiP expects it when audio is running):
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>No extra Xcode Compile Sources step when using CocoaPods autolinking.
8. Runtime flow
Session starts
enablePictureInPicture()
│
User presses Home / switches apps
│
├─ Android 12+ : system auto-enters PiP
├─ Android 8–11: onUserLeaveHint → enterPictureInPictureMode
└─ iOS: WillResignActive → startPictureInPicture
│
▼
PictureInPictureChange { active: true }
│
updatePictureInPictureStatus(...) every tick
│
User expands PiP / returns
│
PictureInPictureChange { active: false }
│
Session ends
disablePictureInPicture()9. Native method map
Same names on both platforms (NativeModules.PictureInPicture):
| JS | Android (PictureInPictureModule.kt) | iOS (PictureInPicture.m) |
|----|----------------------------------------|------------------------------|
| enable() | pipEnabled = true + ticker + applyParams | _pipEnabled = YES + setup + frame timer |
| disable() | pipEnabled = false + hide overlay | stop PiP + stop frame timer |
| enter() | enterIfEnabled(activity) | startPipIfPossible |
| updateStatus(n, paused) | deadline from elapsedRealtime | _deadline from NSDate |
| isSupported() | API 26 + feature flag | AVPictureInPictureController isPictureInPictureSupported |
| isActive() | isInPictureInPictureMode | isPictureInPictureActive |
Host-only Android (PictureInPictureHost): onUserLeaveHint, onPictureInPictureModeChanged, onPictureInPictureRequested, onPause, onResume, createReactActivityDelegate.
10. Notes
- Rebuild native after installing or changing this package. JS-only reload leaves
NativeModules.PictureInPicture === undefined. - Do not register
PictureInPicturePackageinMainApplicationunless autolinking is off. - Enable only for an active session. Leaving
pipEnabledtrue makes Home on later screens enter PiP. - Android 13+ skins may ignore auto-enter;
onUserLeaveHintcovers API 26–30. - PiP can be denied if the device has no
FEATURE_PICTURE_IN_PICTURE. - This is not a lock-screen API. Lock still backgrounds the app.
- iOS needs the hidden
AVSampleBufferDisplayLayer; AVKit will not start PiP from a normal React view.
Checklist
package.jsondepends on@management-and-computer-consultants/react-native-picture-in-picture.- Android:
supportsPictureInPicture+PictureInPictureHostinMainActivity. - iOS:
pod install+audioinUIBackgroundModes. - JS:
enableon session start,disableon unmount. subscribeforonActiveChange(and iOSonPlayPauseif needed).updatePictureInPictureStatuswhenever the countdown or pause flag changes.
