Picture in Picture Plugin for NativePHP Mobile#
Play video in your NativePHP app, let people keep watching in a small floating window while they use your app or any other, and send it to the TV.
See it in action#
- iPad (floating window, AirPlay): https://youtube.com/shorts/aMoHAZhFs6Y
- Android (Mini player, casting to a Google TV): https://youtube.com/shorts/Kvp1KyotEGY
Overview#
- A real floating window. The video shrinks into the corner and keeps playing while people use your app, check a message or open another app. They can move it, resize it, or bring it back full screen.
- It floats on its own. Leave the app while the video plays and it floats instead of stopping. Turn that off with
autoPip: false. - Send it to the TV. A cast button appears when a TV is on the same Wi-Fi: Chromecast and Google TV on Android, AirPlay on iOS. The video carries on from the same spot, and comes back to the phone where the TV left off.
- Control it from your app.
pause(),resume(),seek()andstop()keep working while the video floats or plays on the TV, so a lesson app can pause the video when a quiz starts. - Know where they are.
VideoStateChangedreports playing, paused, floating, back to full screen, on the TV, ended and closed, each with the position in seconds. Save it and pick up where they left off. - The phone's own player. Apple's player on iOS, with AirPlay to a TV and lock-screen controls. Google's Media3 player on Android, with lock-screen and floating-window controls. MP4 and HLS streams (
.m3u8), or a video file saved on the device.
Requirements: NativePHP Mobile 4.x, Laravel 11–13, PHP 8.3+, iOS 18+, Android 10+.
Installation#
composer require firsthand/nativephp-pip
Don't forget to register the plugin:
php artisan native:plugin:register firsthand/nativephp-pip
Then rebuild your app. No permissions are needed. The build hook turns on the iOS "audio" background mode, which Apple requires for a video to float.
Usage#
PHP (Super Native / Livewire / Blade)#
use Firsthand\Pip\Facades\Pip; Pip::play( 'https://example.com/lessons/knife-skills.m3u8', title: 'Knife Skills', subtitle: 'Lesson 3', startAt: 95, // seconds; pick up where they left off cast: true, // offer the TV (the default)); Pip::pause();Pip::seek(120);Pip::stop(); // also closes a floating window
Before showing a "watch while you browse" hint, check the device:
if (Pip::isSupported()) { /* this device can float video */ }
JavaScript (Vue / React / Inertia)#
import { play, onVideoStateChanged } from '@firsthand/nativephp-pip'; onVideoStateChanged(({ state, position }) => { if (state === 'closed') saveProgress(position); });await play('https://example.com/lessons/knife-skills.m3u8', { title: 'Knife Skills', startAt: 95 });
TypeScript types are included.
Events#
VideoStateChanged#
Firsthand\Pip\Events\VideoStateChanged, fired whenever the player changes.
| Property | Type | Description |
|---|---|---|
state |
string |
playing, paused, floating, full_screen (back from the floating window), casting (now on a TV), cast_ended (back on the phone), ended, closed or failed |
position |
float |
Seconds into the video |
duration |
float |
Length in seconds; 0 for a live stream |
message |
?string |
Why it failed |
use Firsthand\Pip\Events\VideoStateChanged;use Native\Mobile\Attributes\On; #[On(VideoStateChanged::class)] // #[OnNative(...)] in a Livewire web-view pagepublic function video(string $state, float $position): void { … }
Methods#
| Method | Returns |
|---|---|
play(source, title, subtitle, startAt, autoPip, loop, cast) |
bool; source is an https:// address or the full path of a video file on the device |
pause() / resume() / seek(seconds) / stop() |
bool |
status() |
['platform', 'supported', 'active', 'playing', 'floating', 'casting', 'position', 'duration'], or null outside the app |
isSupported() |
whether this device can float video |
isActive() |
whether a video is open, full screen or floating |
isCasting() |
whether it is playing on a TV |
A plain http:// address or a missing file throws InvalidArgumentException before anything opens, because phones refuse to stream plain http video. Outside a NativePHP app, calls return false or null, so pages still render.
Platform behavior#
- iOS: Apple's own player, with its picture-in-picture and AirPlay buttons. iPhone and iPad both float video. When the app is left while a full-screen video plays, iOS floats it; with
autoPip: falsethe video pauses instead. - Android: a full-screen player with a Mini player button. Android 12 and later float the video as the person goes home; Android 10 and 11 do the same through the leave-the-app signal. The person can switch picture in picture off for any app in Settings;
isSupported()then says no and the video plays full screen only. - Casting: Android sends the video to a Chromecast or Google TV through Google's own TV player, so there is nothing to register with Google. iOS uses AirPlay to an Apple TV or AirPlay TV. Only
https://videos can be cast (the TV fetches the video itself, so a file on the phone stays on the phone). Passcast: falseto hide the button. - Both: play/pause controls appear on the lock screen and in the floating window. Back (Android) or Done (iOS) closes the player, and stops the TV too.
Testing#
Script the bridge with NativePHP's testing suite:
use Firsthand\Pip\Events\VideoStateChanged;use Native\Mobile\Testing\Native; it('saves where they stopped watching', function () { Native::fakeBridge() ->respondTo('Pip.Status', ['platform' => 'ios', 'supported' => true]) ->respondTo('Pip.Play', ['started' => true]); Native::test(LessonScreen::class) ->tap('Watch') ->emitNative(VideoStateChanged::class, ['state' => 'closed', 'position' => 130]) ->assertSee('Saved at 2:10'); Native::fakeBridge()->assertCalled('Pip.Play', fn (array $p) => $p['title'] === 'Knife Skills');});
Troubleshooting#
| What you see | Why |
|---|---|
| The video doesn't float (iOS) | The app was built before the plugin was registered, so the "audio" background mode is missing. Rebuild |
| No cast button (Android) | No Chromecast or Google TV on the same Wi-Fi, the phone has no Google Play services, or the video is a file on the device |
| The video doesn't float (Android) | Picture in picture is switched off for the app: Settings → Apps → your app → Picture-in-picture |
| "must use https://" | Phones refuse plain http video; serve it over https |
| A native screen shows no text | Native screens need NativePHP's UI package, nativephp/mobile-ui |
License#
One purchase covers unlimited apps. See the LICENSE file in the package.