PictureInPicture
Float a video in a small always-on-top window the user can move anywhere - outside the page, outside the browser - and keep watching while they do something else.
@inject Bit.Butil.PictureInPicture pipMDN reference
<video> can enter picture-in-picture, only one per document at a time,
and entering must come from a user gesture such as a click. Leaving does not, which is why
Exit can be called from anywhere.
IsSupported is the document-level question - false inside an iframe without the picture-in-picture permissions-policy allowance, even on an engine that implements the API. IsAvailableFor is the per-element one: it is a video, the document allows it, the element has not opted out with disablePictureInPicture, and its metadata has loaded. A video whose source is still loading answers false, so wait for loadedmetadata before offering the button.
@inject Bit.Butil.PictureInPicture pip
var supported = await pip.IsSupported();
var canFloat = await pip.IsAvailableFor(_video); // <video @ref="_video" />Request moves the element into the floating window and returns false when the browser refuses - no user gesture, no video track yet, or the user dismissed it. Exit brings it back. IsActive answers either 'is anything floating' or, with an element, 'is this the one'.
private ElementReference _video;
// from a click handler:
var entered = await pip.Request(_video);
var floating = await pip.IsActive(_video);
await pip.Exit();The user can close the floating window themselves at any time, and there is no other way to hear about it - so treat this, rather than a successful Request, as the source of truth for your button state. The floating window's size is only ever reported at the moment of entering; the platform exposes no way to read it back afterwards.
private ButilSubscription? _watch;
_watch = await pip.Subscribe(_video, change => InvokeAsync(() =>
{
_floating = change.Active;
_windowSize = change.Active ? $"{change.Width}x{change.Height}" : "";
StateHasChanged();
}));
// later:
await _watch.DisposeAsync();API reference
ValueTask<bool> IsSupported()ValueTask<bool> IsAvailableFor(ElementReference videoElement)ValueTask<bool> Request(ElementReference videoElement)ValueTask<bool> Exit()ValueTask<bool> IsActive() / IsActive(ElementReference videoElement)ValueTask<ButilSubscription> Subscribe(ElementReference videoElement, Action<PictureInPictureChange> handler)ValueTask DisposeAsync()record (bool Active, int Width, int Height)