Watch video using Picture-in-Picture

François Beaufort
François Beaufort

Picture-in-Picture (PiP) allows users to watch videos in a floating window (always on top of other windows) so they can keep an eye on what they’re watching while interacting with other sites, or applications.

With the Picture-in-Picture Web API, you can start and control Picture-in-Picture for video elements on your website. Try it out on our official Picture-in-Picture sample.

Background

In September 2016, Safari added Picture-in-Picture support through a WebKit API in macOS Sierra. Six months later, Chrome automatically played Picture-in-Picture video on mobile with the release of Android O using a native Android API. Six months later, we announced our intent to build and standardize a Web API, feature compatible with Safari’s, that would allow web developers to create and control the full experience around Picture-in-Picture. And here we are!

Get into the code

Enter Picture-in-Picture

Let’s start simply with a video element and a way for the user to interact with it, such as a button element.

<video id="videoElement" src="https://example.com/file.mp4"></video>
<button id="pipButtonElement"></button>

Only request Picture-in-Picture in response to a user gesture, and never in the promise returned by videoElement.play(). This is because promises do not yet propagate user gestures. Instead, call requestPictureInPicture() in a click handler on pipButtonElement as shown below. It is your responsibility to handle what happens if a users clicks twice.

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  await videoElement.requestPictureInPicture();

  pipButtonElement.disabled = false;
});

When the promise resolves, Chrome shrinks the video into a small window that the user can move around and position over other windows.

You’re done. Great job! You can stop reading and go take your well-deserved vacation. Sadly, that is not always the case. The promise may reject for any of the following reasons:

  • Picture-in-Picture is not supported by the system.
  • Document is not allowed to use Picture-in-Picture due to a restrictive permissions policy.
  • Video metadata have not been loaded yet (videoElement.readyState === 0).
  • Video file is audio-only.
  • The new disablePictureInPicture attribute is present on the video element.
  • The call was not made in a user gesture event handler (e.g. a button click). Starting in Chrome 74, this is applicable only if there's not an element in Picture-in-Picture already.

The Feature support section below shows how to enable/disable a button based on these restrictions.

Let’s add a try...catch block to capture these potential errors and let the user know what’s going on.

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  try {
    await videoElement.requestPictureInPicture();
  } catch (error) {
    // TODO: Show error message to user.
  } finally {
    pipButtonElement.disabled = false;
  }
});

The video element behaves the same whether it is in Picture-in-Picture or not: events are fired and calling methods work. It reflects changes of state in the Picture-in-Picture window (such as play, pause, seek, etc.) and it is also possible to change state programmatically in JavaScript.

Exit Picture-in-Picture

Now, let's make our button toggle entering and exiting Picture-in-Picture. We first have to check if the read-only object document.pictureInPictureElement is our video element. If it isn’t, we send a request to enter Picture-in-Picture as above. Otherwise, we ask to leave by calling document.exitPictureInPicture(), which means the video will appear back in the original tab. Note that this method also returns a promise.

    ...
    try {
      if (videoElement !== document.pictureInPictureElement) {
        await videoElement.requestPictureInPicture();
      } else {
        await document.exitPictureInPicture();
      }
    }
    ...

Listen to Picture-in-Picture events

Operating systems usually restrict Picture-in-Picture to one window, so Chrome's implementation follows this pattern. This means users can only play one Picture-in-Picture video at a time. You should expect users to exit Picture-in-Picture even when you didn't ask for it.

The new enterpictureinpicture and leavepictureinpicture event handlers let us tailor the experience for users. It could be anything from browsing a catalog of videos, to surfacing a livestream chat.

videoElement.addEventListener('enterpictureinpicture', function (event) {
  // Video entered Picture-in-Picture.
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  // Video left Picture-in-Picture.
  // User may have played a Picture-in-Picture video from a different page.
});

Customize the Picture-in-Picture window

Chrome 74 supports play/pause, previous track and next track buttons in the Picture-in-Picture window you can control by using the Media Session API.

Media playback controls in a Picture-in-Picture window
Figure 1. Media playback controls in a Picture-in-Picture window

By default, a play/pause button is always shown in the Picture-in-Picture window unless the video is playing MediaStream objects (e.g. getUserMedia(), getDisplayMedia(), canvas.captureStream()) or the video has a MediaSource duration set to +Infinity (e.g. live feed). To make sure a play/pause button is always visible, set somesee Media Session action handlers for both "Play" and "Pause" media events as below.

// Show a play/pause button in the Picture-in-Picture window
navigator.mediaSession.setActionHandler('play', function () {
  // User clicked "Play" button.
});
navigator.mediaSession.setActionHandler('pause', function () {
  // User clicked "Pause" button.
});

Showing "Previous Track" and "Next track" window controls is similar. Setting Media Session action handlers for those will show them in the Picture-in-Picture window and you'll be able to handle these actions.

navigator.mediaSession.setActionHandler('previoustrack', function () {
  // User clicked "Previous Track" button.
});

navigator.mediaSession.setActionHandler('nexttrack', function () {
  // User clicked "Next Track" button.
});

To see this in action, try out the official Media Session sample.

Get the Picture-in-Picture window size

If you want to adjust the video quality when the video enters and leaves Picture-in-Picture, you need to know the Picture-in-Picture window size and be notified if a user manually resizes the window.

The example below shows how to get the width and height of the Picture-in-Picture window when it is created or resized.

let pipWindow;

videoElement.addEventListener('enterpictureinpicture', function (event) {
  pipWindow = event.pictureInPictureWindow;
  console.log(`> Window size is ${pipWindow.width}x${pipWindow.height}`);
  pipWindow.addEventListener('resize', onPipWindowResize);
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  pipWindow.removeEventListener('resize', onPipWindowResize);
});

function onPipWindowResize(event) {
  console.log(
    `> Window size changed to ${pipWindow.width}x${pipWindow.height}`
  );
  // TODO: Change video quality based on Picture-in-Picture window size.
}

I’d suggest not hooking directly to the resize event as each small change made to the Picture-in-Picture window size will fire a separate event that may cause performance issues if you’re doing an expensive operation at each resize. In other words, the resize operation will fire the events over and over again very rapidly. I’d recommend using common techniques such as throttling and debouncing to address this problem.

Feature support

The Picture-in-Picture Web API may not be supported, so you have to detect this to provide progressive enhancement. Even when it is supported, it may be turned off by the user or disabled by a permissions policy. Luckily, you can use the new boolean document.pictureInPictureEnabled to determine this.

if (!('pictureInPictureEnabled' in document)) {
  console.log('The Picture-in-Picture Web API is not available.');
} else if (!document.pictureInPictureEnabled) {
  console.log('The Picture-in-Picture Web API is disabled.');
}

Applied to a specific button element for a video, this is how you may want to handle your Picture-in-Picture button visibility.

if ('pictureInPictureEnabled' in document) {
  // Set button ability depending on whether Picture-in-Picture can be used.
  setPipButton();
  videoElement.addEventListener('loadedmetadata', setPipButton);
  videoElement.addEventListener('emptied', setPipButton);
} else {
  // Hide button if Picture-in-Picture is not supported.
  pipButtonElement.hidden = true;
}

function setPipButton() {
  pipButtonElement.disabled =
    videoElement.readyState === 0 ||
    !document.pictureInPictureEnabled ||
    videoElement.disablePictureInPicture;
}

MediaStream video support

Video playing MediaStream objects (e.g. getUserMedia(), getDisplayMedia(), canvas.captureStream()) also support Picture-in-Picture in Chrome 71. This means you can show a Picture-in-Picture window that contains user's webcam video stream, display video stream, or even a canvas element. Note that the video element doesn't have to be attached to the DOM to enter Picture-in-Picture as shown below.

Show user's webcam in Picture-in-Picture window

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

Show display in Picture-in-Picture window

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getDisplayMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

Show canvas element in Picture-in-Picture window

const canvas = document.createElement('canvas');
// Draw something to canvas.
canvas.getContext('2d').fillRect(0, 0, canvas.width, canvas.height);

const video = document.createElement('video');
video.muted = true;
video.srcObject = canvas.captureStream();
video.play();

// Later on, video.requestPictureInPicture();

Combining canvas.captureStream() with the Media Session API, you can for instance create an audio playlist window in Chrome 74. Check out the official Audio playlist sample.

Audio playlist in a Picture-in-Picture window
Figure 2. Audio playlist in a Picture-in-Picture window

Samples, demos, and codelabs

Check out our official Picture-in-Picture sample to try the Picture-in-Picture Web API.

Demos and codelabs will follow.

What’s next

First, check out the implementation status page to know which parts of the API are currently implemented in Chrome and other browsers.

Here's what you can expect to see in the near future:

Browser support

The Picture-in-Picture Web API is supported in Chrome, Edge, Opera, and Safari. See MDN for details.

Resources

Many thanks to Mounir Lamouri and Jennifer Apacible for their work on Picture-in-Picture, and help with this article. And a huge thanks to everyone involved in the standardization effort.