Canvas
Getting a picture in and bytes out: draw a video frame or an image onto a canvas, then export it as a data URL or a byte[] - the screenshot and thumbnail path C# had no way to reach.
@inject Bit.Butil.Canvas canvasMDN reference
A 2D context is everywhere. OffscreenCanvas is what Capture uses when it is there, and Capture works either way. WebGL and WebGPU are probes rather than wrappers - Butil does not wrap either, but 'can this device do it at all' is often the question behind the feature.
@inject Bit.Butil.Canvas canvas
var supported = await canvas.IsSupported();
var offscreen = await canvas.IsOffscreenSupported();
var webgl = await canvas.IsWebGlSupported();
var webgpu = await canvas.IsWebGpuSupported();Source coordinates are in the source's own intrinsic pixels, not the size CSS is displaying it at - so a video laid out at 320px still yields its full frame. Note what GetSize reports: the pixel buffer and the CSS box are two different sizes, and the two disagreeing is the usual cause of a blurry canvas. Setting either dimension clears the canvas, even to the value it already had.
private ElementReference _canvas;
private ElementReference _image;
await canvas.SetSize(_canvas, 320, 160);
await canvas.DrawImage(_canvas, _image);
// or just a corner of it, in a corner of the canvas:
await canvas.DrawImage(_canvas, _image, new CanvasDrawOptions
{
SourceWidth = 100, SourceHeight = 50,
DestinationWidth = 160, DestinationHeight = 80
});A data URL is base64, so it is about a third larger than the bytes it carries and it all sits in a string - fine for a preview, wasteful for anything else. ToBytes is what you upload or hash. An unsupported MIME type silently falls back to PNG rather than failing, which is worth knowing when a WebP export comes back looking like a PNG.
var url = await canvas.ToDataUrl(_canvas); // <img src="...">
var png = await canvas.ToBytes(_canvas); // upload this
var jpeg = await canvas.ToBytes(_canvas, "image/jpeg", 0.7); // smaller, lossyCapture draws into a scratch canvas it discards afterwards, so nothing has to exist in your markup. Give one dimension and leave the other at 0 to keep the aspect ratio - a thumbnail is one number, not two. The source's intrinsic size is what gets captured, not the size CSS is showing it at.
// 120px wide, height follows the aspect ratio
var thumbnail = await canvas.Capture(_image, width: 120, type: "image/jpeg", quality: 0.7);
// or straight to something you can show
var url = await canvas.CaptureToDataUrl(_image, width: 120);The headline case: MediaDevices opens the stream, a video element shows it, and Capture takes a frame as bytes - the whole path from camera to upload, in C#. Needs a secure context and the camera permission, so this one prompts.
private ElementReference _video;
var stream = await mediaDevices.GetUserMedia(audio: false, video: true);
await stream!.AttachTo(_video);
// ... and later, a frame:
var photo = await canvas.Capture(_video, width: 640, type: "image/jpeg", quality: 0.85);null in that case rather than throwing, because
drawing someone else's picture is a normal thing to do. Serve the image same-origin, or with CORS
headers and crossorigin="anonymous" on the element.
API reference
ValueTask<bool> IsSupported()ValueTask<bool> ...()ValueTask<CanvasSize?> GetSize(ElementReference canvas)ValueTask<bool> SetSize(ElementReference canvas, int width, int height)ValueTask<bool> Clear(ElementReference canvas)ValueTask<bool> DrawImage(ElementReference canvas, ElementReference source, CanvasDrawOptions? options = null)ValueTask<string?> ToDataUrl(ElementReference canvas, string type = "image/png", double quality = 0.92)ValueTask<byte[]?> ToBytes(ElementReference canvas, string type = "image/png", double quality = 0.92)ValueTask<byte[]?> Capture(ElementReference source, int width = 0, int height = 0, string type = "image/png", double quality = 0.92)ValueTask<string?> CaptureToDataUrl(ElementReference source, int width = 0, int height = 0, string type = "image/png", double quality = 0.92)record (int Width, int Height, double CssWidth, double CssHeight, double DevicePixelRatio)class { double? SourceX, SourceY, SourceWidth, SourceHeight, DestinationX, DestinationY, DestinationWidth, DestinationHeight }