Missed the action at the 2018 Chrome Dev Summit? Catch up with our playlist on the Google Chrome Developers channel on YouTube. Watch now.

Watch video using Picture-in-Picture

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 new Picture-in-Picture Web API, you can initiate 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 feature 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).

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.
});

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 feature 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;
}

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:

  • Picture-in-Picture will be supported in Chrome OS and Android O.
  • MediaStreams from MediaDevices.getUserMedia() will work with Picture-in-Picture.
  • Web developers will be able to add custom Picture-in-Picture controls.

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.

rss_feed Subscribe to our RSS or Atom feed and get the latest updates in your favorite feed reader!