Skip to content

GuidePlayback

Use picture-in-picture

Pop the video into a floating window, with availability detection and state that tracks every entry path.

Add a PiPButton. It requests and exits picture-in-picture, reflects the current state, and stays hidden until picture-in-picture is available.

import { Container, createPlayer, PiPButton } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';

const { Player } = createPlayer({ features: videoFeatures });

export default function BasicUsage() {
  return (
    <Player>
      <Container className="media-container">
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
        <PiPButton
          className="media-pip-button"
          render={(props, state) => <button {...props}>{state.pip ? 'Exit PiP' : 'Enter PiP'}</button>}
        />
      </Container>
    </Player>
  );
}

How it works

The picture-in-picture feature puts three things in player state.

isPictureInPicture is true while the video is in a floating window. It tracks every way in and out — your button, the native controls, or a control the browser adds on its own.

pictureInPictureAvailability starts as 'unavailable'. It becomes 'available' when the media supports picture-in-picture and has loaded metadata, or 'unsupported' when the browser or media has no supported route. Loading a new source returns it to 'unavailable' until metadata loads.

requestPictureInPicture() and exitPictureInPicture() change the presentation. Handle rejected promises when the browser blocks a request. An entry request before metadata loads rejects with InvalidStateError. Without a supported API, the request resolves without opening a window.

Picture-in-picture and fullscreen are exclusive: requesting one exits the other first.

Availability and constraints

If a Safari standalone web app exposes the standard Picture-in-Picture API, Video.js still reports picture-in-picture as 'unsupported'. The built-in button hides in that case. Check pictureInPictureAvailability in installed web apps as well as browser tabs.

  • Show picture-in-picture controls only when pictureInPictureAvailability is 'available'. PiPButton handles this for you.
  • Requests must come from a user gesture; browsers reject requests outside one.
  • Firefox 153 (July 2026) added the programmatic picture-in-picture API on desktop. On earlier desktop versions, including Firefox ESR 140, pictureInPictureAvailability is 'unsupported', even though Firefox shows users its own picture-in-picture toggle. Check pictureInPictureAvailability rather than inferring support from the browser version.
  • On iOS, picture-in-picture is supported on iPhone and iPad through the WebKit presentation mode; the player handles this path for you.
  • The browser owns the floating window: its size, position, and built-in controls aren’t yours to style.
  • The user can close the window or return the video at any time. React to isPictureInPicture instead of assuming your button is the only entry and exit.

Common variations

Open picture-in-picture from your own button

Call requestPictureInPicture() directly in your button’s click handler so the browser treats the request as a user gesture. The action exits fullscreen first, then uses the standard Picture-in-Picture API or Safari’s presentation mode:

import { Container, createPlayer, selectPiP } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';

const { Player, usePlayer } = createPlayer({ features: videoFeatures });

function WatchElsewhereButton() {
  const pip = usePlayer(selectPiP);
  if (pip?.pictureInPictureAvailability !== 'available') return null;

  return (
    <button
      type="button"
      onClick={() => {
        pip.requestPictureInPicture().catch(() => {
          // Show your app's request-failed message.
        });
      }}
    >
      Keep watching while you browse
    </button>
  );
}

export default function App() {
  return (
    <Player>
      <Container>
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsInline />
        <WatchElsewhereButton />
      </Container>
    </Player>
  );
}

Troubleshooting

The picture-in-picture button doesn’t render

Check pictureInPictureAvailability. It is 'unavailable' before metadata loads, and 'unsupported' when the browser or media has no supported picture-in-picture route. Some embeds cannot enter picture-in-picture even in a browser that supports it.

The request rejects

The request ran outside a user gesture, or the browser refused it. Handle the rejection. A request before metadata loads rejects with InvalidStateError; check pictureInPictureAvailability before offering your button.

State is wrong after the user closes the floating window

Read isPictureInPicture from player state instead of tracking entry and exit yourself; it stays in sync with every path in and out of picture-in-picture.

Components

API

Guides