loading
Note:
One video, one window, one gesture Only a <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.

Support checks

IsSupported / IsAvailableFor

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.

C#
@inject Bit.Butil.PictureInPicture pip

var supported = await pip.IsSupported();
var canFloat = await pip.IsAvailableFor(_video);   // <video @ref="_video" />
Live sample
support check output
Results will appear here when you interact with the samples.

Float a video

Request / Exit / IsActive

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'.

C#
private ElementReference _video;

// from a click handler:
var entered = await pip.Request(_video);

var floating = await pip.IsActive(_video);

await pip.Exit();
Live sample
Status No video source yet.
picture-in-picture output
Results will appear here when you interact with the samples.

Follow the window

Subscribe

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.

C#
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();
Live sample
Floating no
subscription output
Results will appear here when you interact with the samples.
Warning:
Stop the camera when you're done This demo borrows your camera so there is something to float. The stream keeps the hardware (and its indicator light) on until it is stopped - leaving this page disposes it, but in your own app that is your job. See MediaDevices.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the document allows picture-in-picture. Returns default (false) during prerender/SSR instead of throwing.
IsAvailableFor
ValueTask<bool> IsAvailableFor(ElementReference videoElement)
True when this element could enter right now: a video, allowed, not opted out, and its metadata loaded.
Request
ValueTask<bool> Request(ElementReference videoElement)
Moves the element into the floating window. Requires a user gesture. False when refused or dismissed.
Exit
ValueTask<bool> Exit()
Closes the floating window. False when nothing was in picture-in-picture. No gesture needed.
IsActive
ValueTask<bool> IsActive() / IsActive(ElementReference videoElement)
Whether anything in the document is floating, or whether this specific element is the one.
Subscribe
ValueTask<ButilSubscription> Subscribe(ElementReference videoElement, Action<PictureInPictureChange> handler)
Watches an element entering and leaving, including when the user closes the window themselves.
DisposeAsync
ValueTask DisposeAsync()
On scope/circuit teardown, detaches any listener whose subscription was never disposed. The floating window is left alone - it is the user's.
PictureInPictureChange
record (bool Active, int Width, int Height)
Width and Height are the floating window's size while Active, otherwise 0.
An unhandled error has occurred. Reload 🗙