Picture-in-Picture
Float a video into an always-on-top miniplayer the user keeps visible while they scroll or switch tabs, via IPictureInPicture (the Picture-in-Picture API). WASM-only: requestPictureInPicture needs a live user gesture. This demo synthesizes its video from an animated canvas (sibling scoped JS), so it needs no shipped media file.
RequestAsync(ElementRef) sends that <video> to the miniplayer; ExitAsync brings it back. Gate on IsSupportedAsync and wrap in try/catch — a request without activation rejects.
using Microsoft.JSInterop;
using Rask.Core;
using Rask.Site;
using Rask.Wasm.Browser;
namespace Rask.Site.Features;
/// <summary>
/// <see cref="IPictureInPicture" /> — float a <c><video></c> into an always-on-top miniplayer.
/// The video is synthesized from an animated canvas by the sibling scoped JS
/// (<c>PictureInPictureDemo.js</c>) so the demo needs no shipped video file.
/// </summary>
public sealed partial class PictureInPictureDemo(IPictureInPicture pip, IJSRuntime js) : Component
{
private readonly ElementRef _video = ElementRef.New();
private string _status = "(idle)";
protected override async Task OnRenderedAsync(bool firstRender)
{
if (!firstRender)
{
return;
}
try
{
await js.InvokeVoidAsync("Rask.PictureInPictureDemo.start", _video);
_status = await pip.IsSupportedAsync()
? "Playing — click \"Open miniplayer\""
: "Picture-in-Picture not supported in this browser";
}
catch (Exception ex)
{
_status = "Setup failed: " + ex.Message;
}
StateHasChanged();
}
protected override Component? Render() =>
Div.Class($"{Tw.Card} shadow-sm border-0")[
Div.Class(Tw.CardBody)[
Video
.Ref(_video)
.Width(320)
.Height(180)
.Muted(true)
.PlaysInline(true)
.Controls(true)
.Class("rounded border mb-2 bg-slate-900"),
Div.Class("flex gap-2 flex-wrap mb-2")[
Button.Class(Tw.BtnPrimary).Id("pip-enter").OnClickAsync(Enter)[
"Open miniplayer"],
Button.Class(Tw.BtnOutlineDanger).Id("pip-exit").OnClickAsync(Exit)["Exit"]
],
Div.Class("text-sm text-ui-muted")["Status: ", Code.Id("pip-status")[_status]]
]
];
private async Task Enter()
{
try
{
await pip.RequestAsync(_video);
_status = "In the miniplayer — drag it anywhere, then Exit to bring it back";
}
catch (Exception ex)
{
_status = "Failed: " + ex.Message;
}
}
private async Task Exit()
{
await pip.ExitAsync();
_status = await pip.IsActiveAsync() ? "Still in miniplayer" : "Back in the page";
}
}
Live result
Status:
(idle)