├── .gitignore ├── w3c.json ├── LICENSE.md ├── .github └── workflows │ └── build.yml ├── Makefile ├── CONTRIBUTING.md ├── security-privacy-questionnaire.md ├── svg ├── custom-video-player-1.svg ├── custom-video-player-2.svg ├── pomodoro-timers-1.svg ├── video-conferencing-1.svg └── video-conferencing-2.svg ├── README.md └── spec.bs /.gitignore: -------------------------------------------------------------------------------- 1 | /out/ 2 | /spec.html 3 | *.swp 4 | -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": [80485] 3 | , "contacts": ["cwilso"] 4 | , "repo-type": "cg-report" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All Reports in this Repository are licensed by Contributors 2 | under the 3 | [W3C Software and Document License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). 4 | 5 | Contributions to Specifications are made under the 6 | [W3C CLA](https://www.w3.org/community/about/agreements/cla/). 7 | 8 | Contributions to Test Suites are made under the 9 | [W3C 3-clause BSD License](https://www.w3.org/Consortium/Legal/2008/03-bsd-license.html) 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Build 16 | run: make ci 17 | - name: Deploy 18 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} 19 | uses: peaceiris/actions-gh-pages@v3 20 | with: 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | publish_dir: ./out 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | 3 | local: spec.bs 4 | bikeshed --die-on=warning spec spec.bs spec.html 5 | 6 | spec.html: spec.bs 7 | @ (HTTP_STATUS=$$(curl https://api.csswg.org/bikeshed/ \ 8 | --output spec.html \ 9 | --write-out "%{http_code}" \ 10 | --header "Accept: text/plain, text/html" \ 11 | -F die-on=warning \ 12 | -F file=@spec.bs) && \ 13 | [[ "$$HTTP_STATUS" -eq "200" ]]) || ( \ 14 | echo ""; cat spec.html; echo ""; \ 15 | rm -f spec.html; \ 16 | exit 22 \ 17 | ); 18 | 19 | remote: spec.html 20 | 21 | ci: spec.bs 22 | mkdir -p out 23 | make remote 24 | mv spec.html out/index.html 25 | 26 | clean: 27 | rm spec.html 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Web Platform Incubator Community Group 2 | 3 | This repository is being used for work in the W3C Web Platform Incubator Community Group, governed by the [W3C Community License 4 | Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To make substantive contributions, 5 | you must join the CG. 6 | 7 | If you are not the sole contributor to a contribution (pull request), please identify all 8 | contributors in the pull request comment. 9 | 10 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows: 11 | 12 | ``` 13 | +@github_username 14 | ``` 15 | 16 | If you added a contributor by mistake, you can remove them in a comment with: 17 | 18 | ``` 19 | -@github_username 20 | ``` 21 | 22 | If you are making a pull request on behalf of someone else but you had no part in designing the 23 | feature, you can remove yourself with the above syntax. 24 | -------------------------------------------------------------------------------- /security-privacy-questionnaire.md: -------------------------------------------------------------------------------- 1 | https://www.w3.org/TR/security-privacy-questionnaire/ 2 | 3 | ### 2.1 What information might this feature expose to Web sites or other parties, and for what purposes is that exposure necessary? 4 | 5 | None 6 | 7 | ### 2.2. Do features in your specification expose the minimum amount of information necessary to enable their intended uses? 8 | 9 | Yes 10 | 11 | ### 2.3. How do the features in your specification deal with personal information, personally-identifiable information (PII), or information derived from them? 12 | 13 | This feature does not deal with any PII 14 | 15 | ### 2.4. How do the features in your specification deal with sensitive information? 16 | 17 | N/A 18 | 19 | ### 2.5. Do the features in your specification introduce new state for an origin that persists across browsing sessions? 20 | 21 | No 22 | 23 | ### 2.6. Do the features in your specification expose information about the underlying platform to origins? 24 | 25 | No 26 | 27 | ### 2.7. Does this specification allow an origin to send data to the underlying platform? 28 | 29 | No 30 | 31 | ### 2.8. Do features in this specification enable access to device sensors? 32 | 33 | No 34 | 35 | ### 2.9. Do features in this specification enable new script execution/loading mechanisms? 36 | 37 | No 38 | 39 | ### 2.10. Do features in this specification allow an origin to access other devices? 40 | 41 | No 42 | 43 | ### 2.11. Do features in this specification allow an origin some measure of control over a user agent’s native UI? 44 | 45 | This feature allows the origin to open an always-on-top window. The origin can 46 | specify an initial width/height/aspect ratio of the window but cannot set a 47 | position. 48 | 49 | ### 2.12. What temporary identifiers do the features in this specification create or expose to the web? 50 | 51 | None 52 | 53 | ### 2.13. How does this specification distinguish between behavior in first-party and third-party contexts? 54 | 55 | No difference 56 | 57 | ### 2.14. How do the features in this specification work in the context of a browser’s Private Browsing or Incognito mode? 58 | 59 | No difference 60 | 61 | ### 2.15. Does this specification have both "Security Considerations" and "Privacy Considerations" sections? 62 | 63 | Yes 64 | 65 | ### 2.16. Do features in your specification enable origins to downgrade default security protections? 66 | 67 | No 68 | 69 | ### 2.17. How does your feature handle non-"fully active" documents? 70 | 71 | The Document Picture-in-Picture window closes when navigating away, so the 72 | window won't exist for a non-"fully active" document 73 | 74 | ### 2.18. What should this questionnaire have asked? 75 | 76 | N/A 77 | -------------------------------------------------------------------------------- /svg/custom-video-player-1.svg: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /svg/custom-video-player-2.svg: -------------------------------------------------------------------------------- 1 | 42 | -------------------------------------------------------------------------------- /svg/pomodoro-timers-1.svg: -------------------------------------------------------------------------------- 1 | 45 | -------------------------------------------------------------------------------- /svg/video-conferencing-1.svg: -------------------------------------------------------------------------------- 1 | 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Document Picture-in-Picture Explained 2 | 3 | 2023-10-26 4 | 5 | Spec: https://wicg.github.io/document-picture-in-picture/ 6 | 7 | ## What's all this then? 8 | 9 | There currently exists a Web API for putting an `HTMLVideoElement` into a 10 | Picture-in-Picture window (`HTMLVideoElement.requestPictureInPicture()`). This 11 | limits a website's ability to provide a custom picture-in-picture experience 12 | (PiP). We want to expand upon that functionality by giving websites the ability 13 | to open a picture-in-picture (i.e., always-on-top) window with a blank document 14 | that can be populated with arbitrary `HTMLElement`s instead of only a single 15 | `HTMLVideoElement`. 16 | 17 | This new window will be much like a blank same-origin window opened via the 18 | existing `window.open()` API, with some differences: 19 | 20 | - The PiP window will float on top of other windows. 21 | - The PiP window will never outlive the opening window. This means any 22 | navigations that change the opener to a new document (even same-origin 23 | navigations) will cause the PiP window to close, similar to the existing 24 | `HTMLVideoElement.requestPictureInPicture()` API. 25 | - The website cannot set the position of the PiP window. 26 | - The PiP window cannot be navigated (any `window.history` or `window.location` 27 | calls that change to a new document will close the PiP window). 28 | - The website can have only one PiP window open at a time, and the user agent 29 | may also restrict how many PiP windows can be open globally, similar to 30 | `HTMLVideoElement.requestPictureInPicture()` API. 31 | 32 | ### Goals 33 | 34 | - Allow a website to display arbitrary `HTMLElements` in an always-on-top 35 | window. 36 | - To be simple for web developers to use and understand. Note that while 37 | allowing websites to call `requestPictureInPicture()` on any element would be 38 | the simplest way, for reasons described below, this isn't feasible. 39 | 40 | ### Non-goals 41 | 42 | - This API is not attempting to handle placeholder content for elements that are 43 | moved out of the page (that is the responsibility of the website to handle). 44 | - Allowing websites to open always-on-top widgets that outlive the webpage (the 45 | PiP window will close when the webpage is closed). 46 | 47 | ### Use cases 48 | 49 | #### Custom video player 50 | 51 | While the existing Picture-in-Picture API for HTMLVideoElement allows a website 52 | to provide a Picture-in-Picture video experience, it is very limited in what 53 | inputs the window can take and the look-and-feel of those inputs. With a full 54 | Document in Picture-in-Picture, the website can provide custom controls and 55 | inputs (e.g. captions, playlists, time scrubber, liking/disliking videos, etc) 56 | to improve the user's PiP video experience. 57 | 58 |  59 |  60 | 61 | #### Video conferencing 62 | 63 | It is common for users to leave the tab during a video conferencing session for 64 | various reasons (e.g. presenting another tab to the call or multitasking) while 65 | still wishing to see the call, so it's a prime use case for Picture-in-Picture. 66 | As above, the current experience a video conferencing website can provide via 67 | the HTMLVideoElement PiP API is limited in style and input. With a full Document 68 | in Picture-in-Picture, the website can easily combine multiple video streams 69 | into a single PiP window and provide custom controls like sending a message, 70 | muting another user, raising a hand, etc. 71 | 72 |  73 |  74 | 75 | #### Pomodoro timers 76 | 77 | The Pomodoro technique is a time management method that uses a kitchen timer to 78 | break work into intervals, typically 25 minutes in length, separated by short 79 | breaks. Pomodoro timer apps on desktop and mobile can use the PiP feature to 80 | display the current timer permanently on the screen as a floating timer for 81 | timed focus management while sat at a desk or while on the go. 82 | 83 |  84 | 85 | ## Example code 86 | 87 | ### HTML 88 | 89 | ```html 90 |
91 |2 | Title: Document Picture-in-Picture Specification 3 | Repository: WICG/document-picture-in-picture 4 | Shortname: document-pip-spec 5 | Level: 1 6 | Status: CG-DRAFT 7 | Group: WICG 8 | URL: https://wicg.github.io/document-picture-in-picture/ 9 | Editor: Tommy Steimel, Google Inc., steimel@google.com 10 | Abstract: This specification enables web developers to populate an HTMLDocument 11 | Abstract: in an always-on-top window. 12 |13 | 14 |
15 | spec:dom; type:dfn; for:Document; text:mode 16 | spec:dom; type:dfn; text:origin 17 | spec:html; type:dfn; for:navigable; text:top-level traversable 18 | spec:html; type:dfn; for:Window; text:navigable 19 | spec:url; type:dfn; for:/; text:url 20 |21 | 22 | # Introduction # {#intro} 23 | 24 | This section is non-normative. 25 | 26 | There currently exists a Web API for putting an {{HTMLVideoElement}} into a 27 | Picture-in-Picture window (
requestPictureInPicture()). This limits
28 | a website's ability to provide a custom picture-in-picture experience (PiP). We
29 | want to expand upon that functionality by providing the website with a full
30 | {{Document}} on an always-on-top window.
31 |
32 | This new window will be much like a blank same-origin window opened via the
33 | existing open() method on {{Window}}, with some minor
34 | differences:
35 |
36 | - The PiP window will float on top of other windows.
37 | - The PiP window will never outlive the opening window.
38 | - The website cannot set the position of the PiP window.
39 | - The PiP window cannot be navigated (any `window.history` or `window.location`
40 | calls that change to a new document will close the PiP window).
41 |
42 | # Dependencies # {#dependencies}
43 |
44 | The IDL fragments in this specification must be interpreted as required for
45 | conforming IDL fragments, as described in the Web IDL specification. [[!WEBIDL]]
46 |
47 | # Security Considerations # {#security-considerations}
48 |
49 | ## Secure Context ## {#secure-context}
50 |
51 | The API is limited to [[SECURE-CONTEXTS]].
52 |
53 | ## Spoofing ## {#spoofing}
54 |
55 | It is required that the user agent provides enough UI on the
56 | {{DocumentPictureInPicture}} window to prevent malicious websites from abusing
57 | the ability to float on top of other windows to spoof other websites or system
58 | UI.
59 |
60 | ### Positioning ### {#positioning}
61 |
62 | The user agent must prevent the website from setting the position of the window
63 | in order to prevent the website from purposefully positioning the window in a
64 | location that may trick a user into thinking it is part of another page's UI. In
65 | particular, this means the {{Window/moveTo()}} and {{Window/moveBy()}} APIs must
66 | be disabled for document picture-in-picture windows.
67 |
68 | ### Origin Visibility ### {#origin-visibility}
69 |
70 | It is required that the user agent makes it clear to the user which origin is
71 | controlling the {{DocumentPictureInPicture}} window at all times to ensure that
72 | the user is aware of where the content is coming from. For example, the user
73 | agent may display the origin of the website in a titlebar on the window.
74 |
75 | ### Maximum size ### {#maximum-size}
76 |
77 | The user agent should restrict the maximum size of the document
78 | picture-in-picture window to prevent the website from covering the screen with
79 | an always-on-top window and locking the user in the picture-in-picture window.
80 | This also helps prevent spoofing the user's desktop.
81 |
82 | ## IFrames ## {#iframes}
83 |
84 | This API is only available on a top-level traversable. However, the
85 | {{DocumentPictureInPicture}} {{Window}} itself may contain {{HTMLIFrameElement}}s, even
86 | cross-origin
87 | {{HTMLIFrameElement}}s.
88 |
89 | # Privacy Considerations # {#privacy-considerations}
90 |
91 | ## Fingerprinting ## {#fingerprinting}
92 |
93 | When a PiP window is closed and then later re-opened, it can be useful for the
94 | user agent to re-use size and location of the previous PiP window to provide a
95 | smoother user experience. However, it is recommended that the user agent does
96 | not re-use size/location across different origins as this may provide malicious
97 | websites an avenue for fingerprinting a user.
98 |
99 | # API # {#api}
100 |
101 |
102 | [Exposed=Window]
103 | partial interface Window {
104 | [SameObject, SecureContext] readonly attribute DocumentPictureInPicture
105 | documentPictureInPicture;
106 | };
107 |
108 | [Exposed=Window, SecureContext]
109 | interface DocumentPictureInPicture : EventTarget {
110 | [NewObject] Promise<Window> requestWindow(
111 | optional DocumentPictureInPictureOptions options = {});
112 | readonly attribute Window window;
113 | attribute EventHandler onenter;
114 | };
115 |
116 | dictionary DocumentPictureInPictureOptions {
117 | [EnforceRange] unsigned long long width = 0;
118 | [EnforceRange] unsigned long long height = 0;
119 | boolean disallowReturnToOpener = false;
120 | boolean preferInitialWindowPlacement = false;
121 | };
122 |
123 | [Exposed=Window, SecureContext]
124 | interface DocumentPictureInPictureEvent : Event {
125 | constructor(DOMString type, DocumentPictureInPictureEventInit eventInitDict);
126 | [SameObject] readonly attribute Window window;
127 | };
128 |
129 | dictionary DocumentPictureInPictureEventInit : EventInit {
130 | required Window window;
131 | };
132 |
133 |
134 | 135 | A {{DocumentPictureInPicture}} object allows websites to create and open a new 136 | always-on-top {{Window}} as well as listen for events related to opening and 137 | closing that {{Window}}. 138 | 139 | Each {{Window}} object has an associated documentPictureInPicture API, 140 | which is a new {{DocumentPictureInPicture}} instance created alongside the {{Window}}. 141 | 142 |
null and is set as part of the
152 | requestWindow() method steps.
153 |
154 | null and |win|'s closed
159 | attribute is false, return |win|.
160 | 3. Return null.
161 |
162 | false, throw a
168 | "{{NotSupportedError}}" {{DOMException}}.
169 | 2. If this's relevant global object's navigable is not a
170 | top-level traversable, throw a "{{NotAllowedError}}"
171 | {{DOMException}}.
172 | 3. If this's relevant global object's navigable's
173 | Is Document Picture-in-Picture boolean is true, throw a
174 | "{{NotAllowedError}}" {{DOMException}}.
175 | 4. If this's relevant global object does not have
176 | transient activation, throw a "{{NotAllowedError}}"
177 | {{DOMException}}.
178 | 5. If |options|["{{DocumentPictureInPictureOptions/width}}"] exists and is greater than zero, but
179 | |options|["{{DocumentPictureInPictureOptions/height}}"] does not exist or is zero, throw a
180 | {{RangeError}}.
181 | 6. If |options|["{{DocumentPictureInPictureOptions/height}}"] exists and is greater than zero, but
182 | |options|["{{DocumentPictureInPictureOptions/width}}"] does not exist or is zero, throw a
183 | {{RangeError}}.
184 | 7. Consume user activation given this's relevant global object.
185 | 8. Let |win| be this's last-opened window. If |win| is not
186 | null and |win|'s closed attribute is
187 | false, then
188 | close
189 | |win|'s navigable.
190 | 9. Optionally, the user agent can close any existing picture-in-picture windows.
191 | 10. Set |pip traversable| to be the result of
192 | creating a new top-level traversable
193 | given this's relevant global object's navigable's
194 | active browsing context and "_blank".
195 |
196 | 197 | The resulting {{Document}}'s [=Document/URL=] will be `about:blank`, but its 198 | [=document base URL=] will fall back to be that of the initiator that called 199 | {{DocumentPictureInPicture/requestWindow()}}. Some browsers do not implement 200 | this fallback behavior for normal `about:blank` popups; see 201 | whatwg/html#421 for 202 | discussion. Implementers are advised to make sure this inheritance happens as 203 | specified for document picture-in-picture windows, to avoid further interop 204 | problems. 205 |
206 | 207 | 11. Set |pip traversable|'s active document's mode to 208 | this's relevant global object's associated Document's 209 | mode. 210 | 12. Set |pip traversable|'s Is Document Picture-in-Picture boolean to 211 |true.
212 | 13. If |options|["{{DocumentPictureInPictureOptions/width}}"] exists and is
213 | greater than zero:
214 | 1. Optionally, clamp or ignore |options|["{{DocumentPictureInPictureOptions/width}}"] if it is too large or too
215 | small in order to fit a user-friendly window size.
216 | 2. Optionally, size |pip traversable|'s active browsing context's
217 | window such that the distance between the left and right edges of the
218 | viewport are |options|["{{DocumentPictureInPictureOptions/width}}"]
219 | pixels.
220 | 14. If |options|["{{DocumentPictureInPictureOptions/height}}"] exists and is
221 | greater than zero:
222 | 1. Optionally, clamp or ignore |options|["{{DocumentPictureInPictureOptions/height}}"] if it is too large or too
223 | small in order to fit a user-friendly window size.
224 | 2. Optionally, size |pip traversable|'s active browsing context's
225 | window such that the distance between the top and bottom edges of the
226 | viewport are |options|["{{DocumentPictureInPictureOptions/height}}"]
227 | pixels.
228 |
229 | If |options|["{{DocumentPictureInPictureOptions/preferInitialWindowPlacement}}"]
230 | exists and is true, then the user agent may use this hint to prefer behavior
231 | that is similar that is similar to steps 13 and 14, rather than considering any
232 | previous position or size of any previously closed |pip traversable| window.
233 |
234 | 15. If |options|["{{DocumentPictureInPictureOptions/disallowReturnToOpener}}"] exists
235 | and is true, the user agent should not display UI affordances
236 | on the picture-in-picture window that allow the user to return to the
237 | opener window.
238 |
239 | 240 | For both video and document picture-in-picture, user agents often display a 241 | button for the user to return to the original page and close the 242 | picture-in-picture window. While this action makes sense in most cases 243 | (especially for a video picture-in-picture window that returns the video to the 244 | main document), it does not always make sense for document picture-in-picture 245 | windows. {{DocumentPictureInPictureOptions/disallowReturnToOpener}} is a hint to 246 | the user agent from the website as to whether that action makes sense for their 247 | particular document picture-in-picture experience. 248 |
249 | 250 | 15. Configure |pip traversable|'s active browsing context's window to 251 | float on top of other windows. 252 | 16. Set this's last-opened window to |pip traversable|'s active window. 253 | 17. Queue a global task on the 254 | DOM manipulation task source 255 | given this's relevant global object to fire an event 256 | named {{enter}} using {{DocumentPictureInPictureEvent}} on 257 | this with its {{DocumentPictureInPictureEvent/window}} attribute 258 | initialized to |pip traversable|'s active window. 259 | 18. Return |pip traversable|'s active window. 260 | 261 |264 | While the size of the window can be configured by the website, the initial 265 | position is left to the discretion of the user agent. 266 |
267 | 268 | 269 | 270 | 271 | : enter 272 | :: Fired on {{DocumentPictureInPicture}} when a PiP window is opened. 273 | 274 | # Concepts # {#concepts} 275 | 276 | ## Document Picture-in-Picture Support ## {#pip-support} 277 | 278 | Each user agent has a Document Picture-in-Picture Support boolean, 279 | whose value is implementation-defined (and might vary according to user 280 | preferences). 281 | 282 | ## DocumentPictureInPicture Window ## {#is-document-picture-in-picture-window} 283 | 284 | Each top-level traversable has an Is Document Picture-in-Picture 285 | boolean, whose value defaults tofalse, but can be set to
286 | true in the requestWindow() method steps.
287 |
288 | ## Closing a Document Picture-in-Picture window ## {#close-document-pip-window}
289 |
290 | 291 | Merge this into 292 | close 293 | once it has enough consensus. 294 |
295 | 296 | Modify step 2 of 297 | close, 298 | "If the result of checking if unloading is user-canceled for toUnload is true, 299 | then return." to be: 300 | 301 | 2. If traversable's Is Document Picture-in-Picture boolean is 302 | true, then skip this step. Otherwise, if the result of 303 | checking if unloading is user-canceled 304 | for toUnload is true, then return. 305 | 306 | ## Close any existing PiP windows ## {#close-existing-pip-windows} 307 | 308 | To close any existing picture-in-picture windows: 309 | 310 | 1. For each |top-level traversable| of the user agent's 311 | top-level traversable set: 312 | 1. If |top-level traversable|'s Is Document Picture-in-Picture 313 | boolean istrue, then
314 | close
315 | |top-level traversable|.
316 | 2. If |top-level traversable|'s active document's
317 | pictureInPictureElement
318 | is not null, run the
319 | exit Picture-in-Picture algorithm
320 | with |top-level traversable|'s active document.
321 | 3. For each |navigable| of |top-level traversable|'s active document's
322 | descendant navigables:
323 | 1. If |navigable|'s active document's
324 | pictureInPictureElement
325 | is not null, run the
326 | exit Picture-in-Picture algorithm
327 | with |navigable|'s active document.
328 |
329 | ## One PiP Window ## {#one-pip-window}
330 |
331 | Any top-level traversable must have at most one document
332 | picture-in-picture window open at a time. If a top-level traversable
333 | whose active window's documentPictureInPicture API's
334 | last-opened window is not null tries to open another
335 | document picture-in-picture window, the user agent must close the existing
336 | last-opened window as described in the requestWindow() method
337 | steps.
338 |
339 | However, whether only one window is allowed in Picture-in-Picture mode across
340 | all top-level traversables is left to the implementation and the platform.
341 | As such, what happens when there is a Picture-in-Picture request while there is
342 | a top-level traversable whose Is Document Picture-in-Picture
343 | boolean is true or whose active document's
344 | pictureInPictureElement
345 | is not null will be left as an implementation detail: the user
346 | agent could close any existing picture-in-picture windows or multiple
347 | Picture-in-Picture windows could be created.
348 |
349 | ## Closing the PiP window when either the original or PiP document is destroyed ## {#close-on-destroy}
350 |
351 | To close any associated Document Picture-in-Picture windows given a
352 | {{Document}} |document|:
353 |
354 | 1. Let |navigable| be |document|'s node navigable.
355 | 2. If |navigable| is not a top-level traversable, abort these steps.
356 | 3. If |navigable|'s Is Document Picture-in-Picture boolean is
357 | true, then
358 | close
359 | |navigable| and abort these steps.
360 | 4. Let |win| be |navigable|'s active window's
361 | documentPictureInPicture API's last-opened window.
362 | 5. If |win| is not null and |win|'s closed
363 | attribute is false, then
364 | close
365 | |win|'s navigable.
366 |
367 | 368 | Merge this into 369 | destroy 370 | once it has enough consensus. 371 |
372 | 373 | Add a step 10 to the end of 374 | destroy: 375 | 376 | 5. Close any associated Document Picture-in-Picture windows given |document|. 377 | 378 |379 | This ensures that when a page with an open Document Picture-in-Picture window is 380 | closed, then its PiP window is closed as well. 381 |
382 | 383 | ## Closing the PiP window when either the original or PiP document is navigated ## {#close-on-navigate} 384 | 385 |386 | Merge this into 387 | navigate 388 | once it has enough consensus. 389 |
390 | 391 | Modify step 16.3 of 392 | navigate, 393 | "Queue a global task on the navigation and traversal task source given navigable's active window to abort navigable's active document.", 394 | and also insert a step 16.4 immediately after it: 395 | 396 | 3. Queue a global task on the 397 | navigation and traversal task source 398 | given |navigable|'s active window to 399 | abort 400 | |navigable|'s active document and close any associated Document Picture-in-Picture windows 401 | given |navigable|'s active document. 402 | 4. If |navigable| is a top-level traversable whose 403 | Is Document Picture-in-Picture boolean istrue, then
404 | abort these steps.
405 |
406 | 407 | This ensures that when a page with an open Document Picture-in-Picture window is 408 | navigated, then its PiP window is closed as well. It also ensures that when the 409 | document in a Document Picture-in-Picture window is navigated, the Document 410 | Picture-in-Picture window is closed. 411 |
412 | 413 | ## Resizing the PiP window ## {#resizing-the-pip-window} 414 | 415 |416 | While programmatically resizing a document picture-in-picture window can be 417 | useful, the always-on-top nature of the window means an unrestricted ability 418 | to resize the window could be abused in annoying or intrusive way. To mitigate 419 | these concerns without completely preventing the use of window resize APIs, we 420 | will have those APIs consume a user gesture for document picture-in-picture 421 | windows. 422 |
423 | 424 |425 | Merge this into {{Window/resizeTo()}} once it has enough consensus. 426 |
427 | 428 | Add a new step to {{Window/resizeTo()}} after step 3, "If |target| is not an 429 | [=auxiliary browsing context=] that was created by a script (as opposed to by an 430 | action of the user), then return.": 431 | 432 | 4. If |target|'s top-level traversable's 433 | Is Document Picture-in-Picture boolean istrue, then:
434 | 1. If this's relevant global object does not have
435 | transient activation, throw a "{{NotAllowedError}}"
436 | {{DOMException}}.
437 | 2. Consume user activation given this's relevant global object.
438 |
439 | 440 | Merge this into {{Window/resizeBy()}} once it has enough consensus. 441 |
442 | 443 | Add a new step to {{Window/resizeBy()}} after step 3, "If |target| is not an 444 | [=auxiliary browsing context=] that was created by a script (as opposed to by an 445 | action of the user), then return.": 446 | 447 | 4. If |target|'s top-level traversable's 448 | Is Document Picture-in-Picture boolean istrue, then:
449 | 1. If this's relevant global object does not have
450 | transient activation, throw a "{{NotAllowedError}}"
451 | {{DOMException}}.
452 | 2. Consume user activation given this's relevant global object.
453 |
454 | ## Focusing the opener window ## {#focusing-the-opener-window}
455 |
456 | 457 | It can often be useful for the picture-in-picture window to be able to re-focus 458 | its opener tab, e.g. when the smaller form-factor of the window doesn't fit the 459 | experience the user needs. We modify the {{Window/focus()}} API to allow it to 460 | take system-level focus when a picture-in-picture window is focusing its 461 | opener. 462 |
463 | 464 |465 | Merge this into {{Window/focus()}} once it has enough consensus. 466 |
467 | 468 | Add a new step to {{Window/focus()}} after step 3, "Run the focusing steps with |current|.": 469 | 470 | 4. If |current| is a top-level traversable, then: 471 | 1. Let |pipWindow| be |current|'s active window's 472 | documentPictureInPicture API's last-opened window. 473 | 2. If |pipWindow| is notnull and |pipWindow|'s relevant global object
474 | has transient activation, then:
475 | 1. Consume user activation given |pipWindow|'s relevant global object.
476 | 2. Give |current| system focus.
477 |
478 | 479 | Giving system focus to the opener does not necessarily need to close the 480 | document picture-in-picture window. If the website wants to close the document 481 | picture-in-picture window after focusing, they can always do so using 482 | {{Window/close()}} on the document picture-in-picture window itself. 483 |
484 | 485 | ## CSS display-mode ## {#css-display-mode} 486 | 487 |488 | The CSS display mode media feature ''@media/display-mode/picture-in-picture'' lets web developers 489 | write specific CSS rules that are only applied when (part of the) the web app is shown in 490 | picture-in-picture mode. 491 |
492 | 493 | ## User activation propagation ## {#user-activation-propagation} 494 | 495 |496 | Due to the nature of document picture-in-picture windows, event handlers on 497 | buttons within the window often end up actually running in the opener's context. 498 | This can make it unergonomic for websites to call 499 | activation consuming APIs, 500 | since sometimes the document 501 | picture-in-picture window has transient activation while the opener does 502 | not. 503 | 504 | To make this easier, we will update the 505 | activation notification 506 | steps to also trigger user activation in the opener when triggering user 507 | activation in a document picture-in-picture window. Additionally, when user 508 | activation is triggered in the opener, we will activate same-origin frames 509 | insides the document picture-in-picture window, similar to how same-origin 510 | descendant frames are activated. 511 |
512 | 513 |514 | Merge this into 515 | activation notification 516 | steps once it has enough consensus. 517 |
518 | 519 | Add three new steps to 520 | activation notification 521 | after step 4, "Extend 522 | |windows| with the active window of each of |document|'s 523 | descendant navigables, filtered to include only those navigables 524 | whose active document's origin is same origin with 525 | |document|'s origin": 526 | 527 | 5. If |document|'s node navigable's top-level traversable's 528 | Is Document Picture-in-Picture boolean istrue, then
529 | extend |windows| with |document|'s node navigable's
530 | top-level traversable's active browsing context's
531 | opener browsing context's active window.
532 |
533 | 6. Let |document picture-in-picture window| be |document|'s node navigable's
534 | top-level traversable's active window's
535 | documentPictureInPicture API's last-opened window.
536 |
537 | 7. If |document picture-in-picture window| is not null then
538 | extend |windows| with the active window of each of
539 | |document picture-in-picture window|'s associated document's
540 | descendant navigables, filtered to include only those
541 | navigables whose active document's origin is
542 | same origin with |document picture-in-picture window|'s
543 | associated document's origin.
544 |
545 | 546 | Additionally, we need to make sure that this activation is properly consumed so 547 | it can't be used twice (once in the opener and once in the picture-in-picture 548 | window). We do this by adding steps to consume user activation which 549 | consume user activation from the opener when consuming a picture-in-picture 550 | window's user activation, and consuming an associated picture-in-picture 551 | window's user activation when consuming an opener's user activation. 552 |
553 | 554 |555 | Merge this into consume user activation steps once it has enough 556 | consensus. 557 |
558 | 559 | Add three new steps to consume user activation after step 3, "Let 560 | |navigables| be the inclusive descendant navigables of |top|'s 561 | active document.": 562 | 563 | 4. If |top|'s Is Document Picture-in-Picture boolean is 564 |true, then extend |navigables| with the
565 | inclusive descendant navigables of |top|'s
566 | active browsing context's opener browsing context's
567 | active document.
568 |
569 | 5. Let |document picture-in-picture window| be |top|'s active window's
570 | documentPictureInPicture API's last-opened window.
571 |
572 | 6. If |document picture-in-picture window| is not null then
573 | extend |navigables| with the inclusive descendant navigables
574 | of |document picture-in-picture window|'s associated document.
575 |
576 | # Examples # {#examples}
577 |
578 | This section is non-normative
579 |
580 | ## Extracting a video player into PiP ## {#example-video-player}
581 |
582 | ### HTML ### {#example-video-player-html}
583 |
584 | 585 | <body> 586 | <div id="player-container"> 587 | <div id="player"> 588 | <video id="video" src="foo.webm"></video> 589 | <!-- More player elements here. --> 590 | </div> 591 | </div> 592 | <input type="button" onclick="enterPiP();" value="Enter PiP" /> 593 | </body> 594 |595 | 596 | ### JavaScript ### {#example-video-player-js} 597 | 598 |
599 | // Handle to the picture-in-picture window.
600 | let pipWindow = null;
601 |
602 | function enterPiP() {
603 | const player = document.querySelector('#player');
604 |
605 | // Set the width/height so the window is properly sized to the video.
606 | const pipOptions = {
607 | width: player.clientWidth,
608 | height: player.clientHeight,
609 | };
610 |
611 | documentPictureInPicture.requestWindow(pipOptions).then((pipWin) => {
612 | pipWindow = pipWin;
613 |
614 | // Style remaining container to imply the player is in PiP.
615 | playerContainer.classList.add('pip-mode');
616 |
617 | // Add player to the PiP window.
618 | pipWindow.document.body.append(player);
619 |
620 | // Listen for the PiP closing event to put the video back.
621 | pipWindow.addEventListener('pagehide', onLeavePiP.bind(pipWindow), { once: true });
622 | });
623 | }
624 |
625 | // Called when the PiP window has closed.
626 | function onLeavePiP() {
627 | if (this !== pipWindow) {
628 | return;
629 | }
630 |
631 | // Remove PiP styling from the container.
632 | const playerContainer = document.querySelector('#player-container');
633 | playerContainer.classList.remove('pip-mode');
634 |
635 | // Add the player back to the main window.
636 | const player = pipWindow.document.querySelector('#player');
637 | playerContainer.append(player);
638 |
639 | pipWindow = null;
640 | }
641 |
642 |
643 | ## Accessing elements on the PiP Window ## {#example-access-elements}
644 |
645 |
646 | const video = pipWindow.document.querySelector('#video');
647 | video.loop = true;
648 |
649 |
650 | ## Listening to events on the PiP Window ## {#example-listen-events}
651 |
652 | As part of creating an improved picture-in-picture experience, websites will often want
653 | customize buttons and controls that need to respond to user input events such as clicks.
654 |
655 |
656 | const pipDocument = pipWindow.document;
657 | const video = pipDocument.querySelector('#video');
658 | const muteButton = pipDocument.document.createElement('button');
659 | muteButton.textContent = 'Toggle mute';
660 | muteButton.addEventListener('click', () => {
661 | video.muted = !video.muted;
662 | });
663 | pipDocument.body.append(muteButton);
664 |
665 |
666 | ## Exiting PiP ## {#example-exiting-pip}
667 |
668 | The website may want to close the {{DocumentPictureInPicture}} {{Window}}
669 | without the user explicitly clicking on the window's close button. They can do
670 | this by using the close() method on the {{Window}}
671 | object:
672 |
673 | 674 | // This will close the PiP window and trigger our existing onLeavePiP() 675 | // listener. 676 | pipWindow.close(); 677 |678 | 679 | ## Getting elements out of the PiP window when it closes ## {#example-elements-out-on-close} 680 | 681 | When the PiP window is closed for any reason (either because the website 682 | initiated it or the user closed it), the website will often want to get the 683 | elements back out of the PiP window. The website can perform this in an event 684 | handler for the {{Window/pagehide}} event on the 685 | {{Window}} object. This is shown in the 686 |
onLeavePiP() handler in
687 | video player example above and is copied
688 | below:
689 |
690 |
691 | // Called when the PiP window has closed.
692 | function onLeavePiP() {
693 | if (this !== pipWindow) {
694 | return;
695 | }
696 |
697 | // Remove PiP styling from the container.
698 | const playerContainer = document.querySelector('#player-container');
699 | playerContainer.classList.remove('pip-mode');
700 |
701 | // Add the player back to the main window.
702 | const player = pipWindow.document.querySelector('#player');
703 | playerContainer.append(player);
704 |
705 | pipWindow = null;
706 | }
707 |
708 |
709 | ## Programatically resize the PiP window ## {#example-programmatic-resize}
710 |
711 | The document picture-in-picture window supports the {{Window/resizeTo()}} and
712 | {{Window/resizeBy()}} APIs, but only with a user gesture on the PiP window:
713 |
714 |
715 | const expandButton = pipWindow.document.createElement('button');
716 | expandButton.textContent = 'Expand PiP Window';
717 | expandButton.addEventListener('click', () => {
718 | // Expand the PiP window's width by 20px and height by 30px.
719 | pipWindow.resizeBy(20, 30);
720 | });
721 | pipWindow.document.body.append(expandButton);
722 |
723 |
724 | ## Return to the opener tab ## {#example-return-to-tab}
725 |
726 | The {{Window/focus()}} API can be used to focus the opener tab from a
727 | picture-in-picture window (requiring a user gesture):
728 |
729 |
730 | const returnToTabButton = pipWindow.document.createElement('button');
731 | returnToTabButton.textContent = 'Return to opener tab';
732 | returnToTabButton.addEventListener('click', () => {
733 | window.focus();
734 | });
735 | pipWindow.document.body.append(returnToTabButton);
736 |
737 |
738 | ## CSS picture-in-picture display mode usage ## {#example-display-mode}
739 |
740 | The following example shows how to remove margins on the body element
741 | and reduce the font size of titles in PiP window to better fit the
742 | content in question inside the PiP window:
743 |
744 |
745 | @media all and (display-mode: picture-in-picture) {
746 | body {
747 | margin: 0;
748 | }
749 | h1 {
750 | font-size: 0.8em;
751 | }
752 | }
753 |
754 |
755 | ## Hide return-to-opener button ## {#example-hide-return-to-opener}
756 |
757 | While user agents often display a button on their video and document
758 | picture-in-picture windows to return to the opener and close the window,
759 | this button doesn't always make sense for some websites' document
760 | picture-in-picture experience. Use the
761 | {{DocumentPictureInPictureOptions/disallowReturnToOpener}} option to hide the
762 | button.
763 |
764 |
765 | await documentPictureInPicture.requestWindow({
766 | disallowReturnToOpener: true
767 | });
768 |
769 |
770 | ## Prefer initial window placement ## {#example-prefer-initial-window-placement}
771 |
772 | While a document picture-in-picture window is open, the user may manually
773 | resize or reposition it. If the document picture-in-picture window is closed,
774 | then reopened later, the user agent may use the previous position and size as
775 | a hint for where to place the new window rather than opening it in is original,
776 | default position.
777 |
778 | The site can provide a hint to the user agent that reusing the previous
779 | document picture-in-picture window position and size is not desirable
780 | by setting the {{DocumentPictureInPictureOptions/preferInitialWindowPlacement}}
781 | value to true. For example, if the site is requesting the new document
782 | picture-in-picture window for an unrelated activity from the previous one, then
783 | the site might provide this hint to the user agent. In response, the user
784 | agent may choose to use the default position, the default size, or the size
785 | hint provided by the site instead.
786 |
787 |
788 | await documentPictureInPicture.requestWindow({
789 | preferInitialWindowPlacement: true
790 | });
791 |
792 |
793 | # Acknowledgments # {#acknowledgments}
794 |
795 | Many thanks to Frank Liberato, Mark Foltz, Klaus Weidner, François Beaufort,
796 | Charlie Reis, Joe DeBlasio, Domenic Denicola, and Yiren Wang for their comments
797 | and contributions to this document and to the discussions that have informed it.
798 |
--------------------------------------------------------------------------------