DocumentPictureInPicture
An always-on-top window you can put arbitrary DOM in - custom controls, a live chart, a timer, a call roster - not just a video. The elements are moved, so a Blazor component keeps its state and its handlers.
@inject Bit.Butil.DocumentPictureInPicture documentPictureInPictureMDN reference
Chromium-only at the time of writing. Other engines have the video-element PictureInPicture, which floats a video and nothing else - a different API for a different job.
@inject Bit.Butil.DocumentPictureInPicture documentPictureInPicture
var isSupported = await documentPictureInPicture.IsSupported();The window opens empty and needs a user gesture. MoveElement takes a live element out of the page and into it - the counter below keeps counting while it floats, because it is the same DOM node and the same component. Everything is put back when the window closes, however it closes.
var window = await documentPictureInPicture.RequestWindow(
new DocumentPictureInPictureOptions { Width = 360, Height = 240 },
onClose: () => InvokeAsync(StateHasChanged));
await window!.MoveElement(panelElement); // <div @ref="panelElement"> ... </div>
// later - or when the user closes the window
await window.DisposeAsync();Ticks while it floats: 0
The window is a separate document and inherits no CSS - which is the first surprise everyone hits. CopyStyleSheets brings the page's styles over when the window opens; AddStyleSheet adds the rules that only apply while floating. The user can resize the window, so read the size rather than assume it.
await window.AddStyleSheet("body { margin: 0; font: 14px system-ui; background: #111; color: #eee }");
var size = await window.GetSize(); // the user may have resized it
var open = await window.IsOpen();
await window.Focus();Fires whenever a document picture-in-picture window opens, including one this component did not request. Since only one such window can exist at a time, this is how another part of the app learns that the one place for it is now taken.
var subscription = await documentPictureInPicture.SubscribeEnter(size =>
{
Console.WriteLine($"a floating window opened at {size.Width}x{size.Height}");
});
await subscription!.DisposeAsync();API reference
ValueTask<bool> IsSupported()ValueTask<DocumentPictureInPictureWindowHandle?> RequestWindow(DocumentPictureInPictureOptions? options = null, Action? onClose = null)ValueTask<ButilSubscription?> SubscribeEnter(Action<DocumentPictureInPictureSize> handler)Width, Height, DisallowReturnToOpener, PreferInitialWindowPlacement, CopyStyleSheetsint InitialWidth, int InitialHeightValueTask<bool> MoveElement(ElementReference element)ValueTask RestoreElements()ValueTask<bool> AddStyleSheet(string css)ValueTask<DocumentPictureInPictureSize?> GetSize()ValueTask<bool> IsOpen()ValueTask<bool> Focus()ValueTask DisposeAsync()