1. Introduction
This section is non-normative.
There currently exists a Web API for putting an HTMLVideoElement
into a
Picture-in-Picture window (requestPictureInPicture()
). This limits
a website’s ability to provide a custom picture-in-picture experience (PiP). We
want to expand upon that functionality by providing the website with a full Document
on an always-on-top window.
This new window will be much like a blank same-origin window opened via the
existing open() method on Window
, with some minor
differences:
-
The PiP window will float on top of other windows.
-
The PiP window will never outlive the opening window.
-
The website cannot set the position of the PiP window.
-
The PiP window cannot be navigated (any `window.history` or `window.location` calls that change to a new document will close the PiP window).
2. Dependencies
The IDL fragments in this specification must be interpreted as required for conforming IDL fragments, as described in the Web IDL specification. [WEBIDL]
3. Security Considerations
3.1. Secure Context
The API is limited to [SECURE-CONTEXTS].
3.2. Spoofing
It is required that the user agent provides enough UI on the DocumentPictureInPicture
window to prevent malicious websites from abusing
the ability to float on top of other windows to spoof other websites or system
UI.
3.2.1. Positioning
The user agent must prevent the website from setting the position of the window
in order to prevent the website from purposefully positioning the window in a
location that may trick a user into thinking it is part of another page’s UI. In
particular, this means the moveTo()
and moveBy()
APIs must
be disabled for document picture-in-picture windows.
3.2.2. Origin Visibility
It is required that the user agent makes it clear to the user which origin is
controlling the DocumentPictureInPicture
window at all times to ensure that
the user is aware of where the content is coming from. For example, the user
agent may display the origin of the website in a titlebar on the window.
3.3. IFrames
This API is only available on a top-level traversable. However, the DocumentPictureInPicture
Window
itself may contain HTMLIFrameElement
s, even cross-origin HTMLIFrameElement
s.
4. Privacy Considerations
4.1. Fingerprinting
When a PiP window is closed and then later re-opened, it can be useful for the user agent to re-use size and location of the previous PiP window to provide a smoother user experience. However, it is recommended that the user agent does not re-use size/location across different origins as this may provide malicious websites an avenue for fingerprinting a user.
5. API
[Exposed =Window ]partial interface Window { [SameObject ,SecureContext ]readonly attribute DocumentPictureInPicture documentPictureInPicture ; }; [Exposed =Window ,SecureContext ]interface :
DocumentPictureInPicture EventTarget { [NewObject ]Promise <Window >requestWindow (optional DocumentPictureInPictureOptions = {});
options readonly attribute Window window ;attribute EventHandler ; };
onenter dictionary { [
DocumentPictureInPictureOptions EnforceRange ]unsigned long long = 0; [
width EnforceRange ]unsigned long long = 0; }; [
height Exposed =Window ,SecureContext ]interface :
DocumentPictureInPictureEvent Event {(
constructor DOMString ,
type DocumentPictureInPictureEventInit ); [
eventInitDict SameObject ]readonly attribute Window ; };
window dictionary :
DocumentPictureInPictureEventInit EventInit {required Window ; };
window
A DocumentPictureInPicture
object allows websites to create and open a new
always-on-top Window
as well as listen for events related to opening and
closing that Window
.
Each Window
object has an associated documentPictureInPicture API,
which is a new DocumentPictureInPicture
instance created alongside the Window
.
documentPictureInPicture
getter steps are:
-
Return this’s documentPictureInPicture API.
Each DocumentPictureInPicture
object has an associated last-opened window which is a Window
object that is initially null
and is set as part of the requestWindow() method steps.
window
getter steps are:
-
Let win be this’s last-opened window.
-
If win is not
null
and win’s closed attribute isfalse
, return win. -
Return
null
.
requestWindow(options)
method steps are:
-
If Document Picture-in-Picture support is
false
, throw a "NotSupportedError
"DOMException
. -
If this’s relevant global object’s navigable is not a top-level traversable, throw a "
NotAllowedError
"DOMException
. -
If this’s relevant global object’s navigable’s Is Document Picture-in-Picture boolean is
true
, throw a "NotAllowedError
"DOMException
. -
If this’s relevant global object does not have transient activation, throw a "
NotAllowedError
"DOMException
. -
If options["
width
"] exists and is greater than zero, but options["height
"] does not exist or is zero, throw aRangeError
. -
If options["
height
"] exists and is greater than zero, but options["width
"] does not exist or is zero, throw aRangeError
. -
Consume user activation given this’s relevant global object.
-
Let win be this’s last-opened window. If win is not
null
and win’s closed attribute isfalse
, then close win’s navigable. -
Optionally, the user agent can close any existing picture-in-picture windows.
-
Set pip traversable to be the result of creating a new top-level traversable given this’s relevant global object’s navigable’s active browsing context and "
_blank
".
The resulting Document
's URL will be `about:blank`, but its document base URL will fall back to be that of the initiator that called requestWindow()
. Some browsers do not implement
this fallback behavior for normal `about:blank` popups; see whatwg/html#421 for
discussion. Implementers are advised to make sure this inheritance happens as
specified for document picture-in-picture windows, to avoid further interop
problems.
-
Set pip traversable’s Is Document Picture-in-Picture boolean to
true
. -
If options["
width
"] exists and is greater than zero:-
Optionally, clamp or ignore options["
width
"] if it is too large or too small in order to fit a user-friendly window size. -
Optionally, size pip traversable’s active browsing context’s window such that the distance between the left and right edges of the viewport are options["
width
"] pixels.
-
-
If options["
height
"] exists and is greater than zero:-
Optionally, clamp or ignore options["
height
"] if it is too large or too small in order to fit a user-friendly window size. -
Optionally, size pip traversable’s active browsing context’s window such that the distance between the top and bottom edges of the viewport are options["
height
"] pixels.
-
-
Configure pip traversable’s active browsing context’s window to float on top of other windows.
-
Set this’s last-opened window to pip traversable’s active window.
-
Queue a global task on the DOM manipulation task source given this’s relevant global object to fire an event named
enter
usingDocumentPictureInPictureEvent
on this with itswindow
attribute initialized to pip traversable’s active window. -
Return pip traversable’s active window.
While the size of the window can be configured by the website, the initial position is left to the discretion of the user agent.
enter
-
Fired on
DocumentPictureInPicture
when a PiP window is opened.
6. Concepts
6.1. Document Picture-in-Picture Support
Each user agent has a Document Picture-in-Picture Support boolean, whose value is implementation-defined (and might vary according to user preferences).
6.2. DocumentPictureInPicture Window
Each top-level traversable has an Is Document Picture-in-Picture boolean, whose value defaults to false
, but can be set to true
in the requestWindow() method steps.
6.3. Closing a Document Picture-in-Picture window
Merge this into close once it has enough consensus.
Modify step 2 of close, "If the result of checking if unloading is user-canceled for toUnload is true, then return." to be:
-
If traversable’s Is Document Picture-in-Picture boolean is true, then skip this step. Otherwise, if the result of checking if unloading is user-canceled for toUnload is true, then return.
6.4. Close any existing PiP windows
To close any existing picture-in-picture windows:
-
For each top-level traversable of the user agent’s top-level traversable set:
-
If top-level traversable’s Is Document Picture-in-Picture boolean is
true
, then close top-level traversable. -
If top-level traversable’s active document’s pictureInPictureElement is not
null
, run the exit Picture-in-Picture algorithm with top-level traversable’s active document. -
For each navigable of top-level traversable’s active document’s descendant navigables:
-
If navigable’s active document’s pictureInPictureElement is not
null
, run the exit Picture-in-Picture algorithm with navigable’s active document.
-
-
6.5. One PiP Window
Any top-level traversable must have at most one document
picture-in-picture window open at a time. If a top-level traversable whose active window’s documentPictureInPicture API’s last-opened window is not null
tries to open another
document picture-in-picture window, the user agent must close the existing last-opened window as described in the requestWindow() method
steps.
However, whether only one window is allowed in Picture-in-Picture mode across
all top-level traversables is left to the implementation and the platform.
As such, what happens when there is a Picture-in-Picture request while there is
a top-level traversable whose Is Document Picture-in-Picture boolean is true
or whose active document’s pictureInPictureElement is not null
will be left as an implementation detail: the user
agent could close any existing picture-in-picture windows or multiple
Picture-in-Picture windows could be created.
6.6. Closing the PiP window when either the original or PiP document is destroyed
To close any associated Document Picture-in-Picture windows given a Document
document:
-
Let navigable be document’s node navigable.
-
If navigable is not a top-level traversable, abort these steps.
-
If navigable’s Is Document Picture-in-Picture boolean is
true
, then close navigable and abort these steps. -
Let win be navigable’s active window’s documentPictureInPicture API’s last-opened window.
-
If win is not
null
and win’s closed attribute isfalse
, then close win’s navigable.
Merge this into destroy once it has enough consensus.
Add a step 10 to the end of destroy:
-
Close any associated Document Picture-in-Picture windows given document.
This ensures that when a page with an open Document Picture-in-Picture window is closed, then its PiP window is closed as well.
6.7. Closing the PiP window when either the original or PiP document is navigated
Merge this into navigate once it has enough consensus.
Modify step 16.3 of navigate, "Queue a global task on the navigation and traversal task source given navigable’s active window to abort navigable’s active document.", and also insert a step 16.4 immediately after it:
-
Queue a global task on the navigation and traversal task source given navigable’s active window to abort navigable’s active document and close any associated Document Picture-in-Picture windows given navigable’s active document.
-
If navigable is a top-level traversable whose Is Document Picture-in-Picture boolean is
true
, then abort these steps.
This ensures that when a page with an open Document Picture-in-Picture window is navigated, then its PiP window is closed as well. It also ensures that when the document in a Document Picture-in-Picture window is navigated, the Document Picture-in-Picture window is closed.
7. Examples
This section is non-normative
7.1. Extracting a video player into PiP
7.1.1. HTML
< body > < div id = "player-container" > < div id = "player" > < video id = "video" src = "foo.webm" ></ video > <!-- More player elements here. --> </ div > </ div > < input type = "button" onclick = "enterPiP();" value = "Enter PiP" /> </ body >
7.1.2. JavaScript
// Handle to the picture-in-picture window. let pipWindow= null ; function enterPiP() { const player= document. querySelector( '#player' ); // Set the width/height so the window is properly sized to the video. const pipOptions= { width: player. clientWidth, height: player. clientHeight, }; documentPictureInPicture. requestWindow( pipOptions). then(( pipWin) => { pipWindow= pipWin; // Style remaining container to imply the player is in PiP. playerContainer. classList. add( 'pip-mode' ); // Add player to the PiP window. pipWindow. document. body. append( player); // Listen for the PiP closing event to put the video back. pipWindow. addEventListener( 'pagehide' , onLeavePiP. bind( pipWindow), { once: true }); }); } // Called when the PiP window has closed. function onLeavePiP() { if ( this !== pipWindow) { return ; } // Remove PiP styling from the container. const playerContainer= document. querySelector( '#player-container' ); playerContainer. classList. remove( 'pip-mode' ); // Add the player back to the main window. const player= pipWindow. document. querySelector( '#player' ); playerContainer. append( player); pipWindow= null ; }
7.2. Accessing elements on the PiP Window
const video= pipWindow. document. querySelector( '#video' ); video. loop= true ;
7.3. Listening to events on the PiP Window
As part of creating an improved picture-in-picture experience, websites will often want customize buttons and controls that need to respond to user input events such as clicks.
const pipDocument= pipWindow. document; const video= pipDocument. querySelector( '#video' ); const muteButton= pipDocument. document. createElement( 'button' ); muteButton. textContent= 'Toggle mute' ; muteButton. addEventListener( 'click' , () => { video. muted= ! video. muted; }); pipDocument. body. append( muteButton);
7.4. Exiting PiP
The website may want to close the DocumentPictureInPicture
Window
without the user explicitly clicking on the window’s close button. They can do
this by using the close() method on the Window
object:
// This will close the PiP window and trigger our existing onLeavePiP() // listener. pipWindow. close();
7.5. Getting elements out of the PiP window when it closes
When the PiP window is closed for any reason (either because the website
initiated it or the user closed it), the website will often want to get the
elements back out of the PiP window. The website can perform this in an event
handler for the pagehide
event on the Window
object. This is shown in the onLeavePiP()
handler in video player example above and is copied
below:
// Called when the PiP window has closed. function onLeavePiP() { if ( this !== pipWindow) { return ; } // Remove PiP styling from the container. const playerContainer= document. querySelector( '#player-container' ); playerContainer. classList. remove( 'pip-mode' ); // Add the player back to the main window. const player= pipWindow. document. querySelector( '#player' ); playerContainer. append( player); pipWindow= null ; }
8. Acknowledgments
Many thanks to Frank Liberato, Mark Foltz, Klaus Weidner, François Beaufort, Charlie Reis, Joe DeBlasio, Domenic Denicola, and Yiren Wang for their comments and contributions to this document and to the discussions that have informed it.