mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-22 13:15:49 +00:00
Open Document Picture-in-Picture windows on behalf of apps
Browsers only allow documentPictureInPicture.requestWindow() from a
top-level document, and an app lives in an iframe, so an app calling it
gets NotAllowedError ("only allowed from a top-level browsing context").
The `document-picture-in-picture` token in the iframe's `allow` list does
nothing — it is not a policy feature the browser knows. Video PiP
(video.requestPictureInPicture) already works inside apps.
The GUI is the top-level document, so a new PictureInPictureService opens
the window for the app and fills it with an iframe of a page the app names,
which must come from the app's own origin (checked against the message's
origin, now carried on the IPC caller context). One window per app
instance; it closes with the app's window, and the app hears about a close
it didn't ask for. The window's opener is the GUI, so the page inside it
can reach its app's frame through parent.opener.frames and share objects
directly — a MediaStream included, which postMessage cannot carry (tracks
are not transferable between windows in Chromium).
puter.js gains puter.ui.requestPictureInPicture({ url, width, height,
onClose }) and puter.ui.exitPictureInPicture(), with docs.
This commit is contained in:
@@ -50,6 +50,8 @@ The UI API provides a comprehensive set of tools for creating rich user interfac
|
||||
### Additional UI Elements
|
||||
- **[`puter.ui.contextMenu()`](/UI/contextMenu/)** - Show a context menu at the cursor
|
||||
- **[`puter.ui.hideSpinner()`](/UI/hideSpinner/)** - Hide spinner
|
||||
- **[`puter.ui.requestPictureInPicture()`](/UI/requestPictureInPicture/)** - Float a page of the app in a picture-in-picture window
|
||||
- **[`puter.ui.exitPictureInPicture()`](/UI/exitPictureInPicture/)** - Close the app's picture-in-picture window
|
||||
- **[`puter.ui.showColorPicker()`](/UI/showColorPicker/)** - Show color picker
|
||||
- **[`puter.ui.showFontPicker()`](/UI/showFontPicker/)** - Show font picker
|
||||
- **[`puter.ui.showSpinner()`](/UI/showSpinner/)** - Show spinner
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: puter.ui.exitPictureInPicture()
|
||||
description: Closes the picture-in-picture window your app opened.
|
||||
platforms: [apps]
|
||||
---
|
||||
|
||||
Closes the picture-in-picture window opened with [`puter.ui.requestPictureInPicture()`](/UI/requestPictureInPicture/), if one is up. Its `onClose` callback does not run for this — you asked for the close.
|
||||
|
||||
## Syntax
|
||||
```js
|
||||
puter.ui.exitPictureInPicture()
|
||||
```
|
||||
|
||||
## Return value
|
||||
A `Promise` that resolves to `true` if there was a window to close, `false` otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<script src="https://js.puter.com/v2/"></script>
|
||||
<button id="exit">Exit picture-in-picture</button>
|
||||
<script>
|
||||
document.getElementById('exit').onclick = async () => {
|
||||
const wasOpen = await puter.ui.exitPictureInPicture();
|
||||
console.log(wasOpen ? 'closed' : 'nothing was open');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: puter.ui.requestPictureInPicture()
|
||||
description: Floats a page of your app in an always-on-top picture-in-picture window.
|
||||
platforms: [apps]
|
||||
---
|
||||
|
||||
Floats a page of your app in a picture-in-picture window: a small always-on-top window that stays in view while the user works in other windows or tabs.
|
||||
|
||||
Browsers only let a top-level page open a Document Picture-in-Picture window, and an app runs inside an iframe — so calling `documentPictureInPicture.requestWindow()` yourself fails with `NotAllowedError`. Puter opens the window on your app's behalf and loads the page you name in it.
|
||||
|
||||
The page must come from your app's own origin. Inside it, your app's main frame is one of `window.parent.opener.frames`: probe them in a `try`/`catch` (frames from other origins throw), and the two pages can share objects directly — a `MediaStream`, which `postMessage` cannot carry, included. `BroadcastChannel` works between them as well.
|
||||
|
||||
Call it from a user gesture such as a click; browsers refuse otherwise. One window per app: asking again replaces the one that is up.
|
||||
|
||||
## Syntax
|
||||
```js
|
||||
puter.ui.requestPictureInPicture(options)
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
#### `options.url` (String) (required)
|
||||
The page to show in the window. Resolved against your app's own page, and must be on the same origin.
|
||||
|
||||
#### `options.width` (Number) (optional)
|
||||
Window width in CSS pixels. The browser may clamp it.
|
||||
|
||||
#### `options.height` (Number) (optional)
|
||||
Window height in CSS pixels. The browser may clamp it.
|
||||
|
||||
#### `options.onClose` (Function) (optional)
|
||||
Runs when the window goes away other than through [`puter.ui.exitPictureInPicture()`](/UI/exitPictureInPicture/) — the user closing it, typically.
|
||||
|
||||
## Return value
|
||||
A `Promise` that resolves once the window is up. It rejects with an error named the way the DOM would name it:
|
||||
|
||||
- `NotSupportedError` — the browser has no Document Picture-in-Picture, or the code isn't running as an app on the Puter desktop.
|
||||
- `NotAllowedError` — not called from a user gesture.
|
||||
- `SecurityError` — `url` is not on your app's origin.
|
||||
- `TypeError` — `url` is not a URL.
|
||||
|
||||
## Examples
|
||||
|
||||
<strong class="example-title">Float a page from a button</strong>
|
||||
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<script src="https://js.puter.com/v2/"></script>
|
||||
<button id="pip">Picture-in-picture</button>
|
||||
<script>
|
||||
document.getElementById('pip').onclick = async () => {
|
||||
try {
|
||||
await puter.ui.requestPictureInPicture({
|
||||
url: '/pip.html',
|
||||
width: 400,
|
||||
height: 300,
|
||||
onClose: () => console.log('the user closed it'),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
<strong class="example-title">Reach the main frame from the floating page</strong>
|
||||
|
||||
```html
|
||||
<!-- pip.html -->
|
||||
<html>
|
||||
<body>
|
||||
<video id="v" autoplay muted playsinline></video>
|
||||
<script>
|
||||
// The desktop opened this window, so its opener is the desktop, and
|
||||
// your app's main frame is one of the desktop's frames — the only
|
||||
// one this page is allowed to read.
|
||||
const opener = window.parent.opener;
|
||||
for (let i = 0; i < opener.frames.length; i++) {
|
||||
try {
|
||||
const main = opener.frames[i];
|
||||
if (main.myAppStream) {
|
||||
document.getElementById('v').srcObject = main.myAppStream;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// a frame from another origin
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -896,6 +896,22 @@ let sidebar = [
|
||||
source: '/UI/setWindowY.md',
|
||||
path: '/UI/setWindowY',
|
||||
},
|
||||
{
|
||||
title: '<code>requestPictureInPicture()</code>',
|
||||
page_title: '<code>puter.ui.requestPictureInPicture()</code>',
|
||||
title_tag: 'puter.ui.requestPictureInPicture()',
|
||||
icon: '/assets/img/function.svg',
|
||||
source: '/UI/requestPictureInPicture.md',
|
||||
path: '/UI/requestPictureInPicture',
|
||||
},
|
||||
{
|
||||
title: '<code>exitPictureInPicture()</code>',
|
||||
page_title: '<code>puter.ui.exitPictureInPicture()</code>',
|
||||
title_tag: 'puter.ui.exitPictureInPicture()',
|
||||
icon: '/assets/img/function.svg',
|
||||
source: '/UI/exitPictureInPicture.md',
|
||||
path: '/UI/exitPictureInPicture',
|
||||
},
|
||||
{
|
||||
title: '<code>showColorPicker()</code>',
|
||||
page_title: '<code>puter.ui.showColorPicker()</code>',
|
||||
|
||||
Reference in New Issue
Block a user