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>&lt;video&gt;</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)