BlazorBasics.Camera 1.2.3

There is a newer version of this package available.
See the version list below for details.
dotnet add package BlazorBasics.Camera --version 1.2.3
                    
NuGet\Install-Package BlazorBasics.Camera -Version 1.2.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="BlazorBasics.Camera" Version="1.2.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BlazorBasics.Camera" Version="1.2.3" />
                    
Directory.Packages.props
<PackageReference Include="BlazorBasics.Camera" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add BlazorBasics.Camera --version 1.2.3
                    
#r "nuget: BlazorBasics.Camera, 1.2.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package BlazorBasics.Camera@1.2.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=BlazorBasics.Camera&version=1.2.3
                    
Install as a Cake Addin
#tool nuget:?package=BlazorBasics.Camera&version=1.2.3
                    
Install as a Cake Tool

Nuget Nuget

BlazorBasics.Camera

A Blazor camera component built on getUserMedia. Full screen viewfinder, front/back switch, torch, optional zoom and quality presets, JPEG capture.

It has no opinion about what you do with the photo. It streams, it captures, it hands you a CameraPhoto and it tells you when something went wrong. Uploading, storing, counting, geotagging, localizing and gating are the application's business, so each one is a parameter or a callback.

Only dependency: Microsoft.AspNetCore.Components.Web. No icon font, no localization stack, no CSS framework.

Install

dotnet add package BlazorBasics.Camera
// Program.cs
builder.Services.AddBlazorCamera();

The bare minimum

@page "/"

<button @onclick="() => camera!.OpenAsync()">Open camera</button>

<CameraView @ref="camera" OnPhotoCaptured="OnPhoto" />

@code {
    private CameraView? camera;

    private Task OnPhoto(CameraPhoto photo)
    {
        // photo.Bytes / photo.OpenStreamAsync() / photo.TakenAt
        return Task.CompletedTask;
    }
}

That is a working camera: prewarmed stream, shutter, close, front/back switch. No zoom bar and no quality bar are rendered, because you passed no presets.

Opening and closing

Two ways, pick one:

  • Bound: <CameraView @bind-IsOpen="isOpen" /> and flip isOpen.
  • Imperative: @ref plus OpenAsync() / CloseAsync(), and never pass IsOpen.

Do not pass IsOpen one-way: a parent re-render would close a camera you opened imperatively.

IsReadyChanged fires when the stream becomes usable, so your own button can stay disabled until there is something to show.

Zoom and quality presets

Both bars are data driven and both disappear when there is no data:

<CameraView ZoomOptions="zooms" QualityOptions="qualities" @bind-Quality="quality" ... />

@code {
    // Only the levels the camera can actually reach are shown, and the bar stays hidden unless at
    // least two survive — a single choice is not a choice. 0.6× needs an ultra-wide lens.
    private readonly CameraZoomOption[] zooms = [new(0.6), new(1.0), new(1.5), new(2.0)];

    // Labels come from you, already localized. Disabled is yours too (e.g. low on storage).
    private readonly CameraQualityOption[] qualities =
    [
        new("Low", 0.5), new("Med", 0.75), new("Std", 0.93), new("Max", 1.0)
    ];

    private double quality = 0.93;
}

CameraZoomOption formats its own label from the factor ("0.6×", "2×") unless you give one.

Pinch to zoom

Two fingers over the viewfinder move the zoom continuously, from the minimum of the lens that is streaming up to the maximum the device can actually do, and a floating pill shows where it is ("1×", "2.3×", "6×") for as long as the gesture lasts plus a moment after. The presets stay exactly as they were, as shortcuts; a pill lights up only while the zoom sits on its level.

It is on by default and needs nothing from you — not even ZoomOptions, so a camera with no bar at all still zooms. EnablePinchZoom="false" turns it off.

<CameraView EnablePinchZoom="true" MaxDigitalZoom="8" ... />

The gesture is resolved in the browser and only reports the factor it arrives at: moving the zoom through .NET on every finger movement is what would make it feel sticky in WebAssembly. What the camera cannot do natively is cropped — MaxDigitalZoom is how far that crop may go (the ceiling of the gesture is the larger of that and the device's real maximum), which is also what gives a laptop webcam or an emulator a working gesture. Native zoom always goes first and only the remainder is cropped, so nothing is thrown away where the camera does have zoom of its own.

The gesture never changes lens. Reaching below 1× means reopening the stream on another camera (see below), which with two fingers on the screen would freeze the preview and lose the pinch — so the gesture stops at the minimum of the active lens, and going wider stays a preset.

Below 1×: the ultra-wide lens is a different camera

A "0.6×" preset is not a zoom. On Android every lens is its own videoinput and the main rear camera reports a minimum zoom of exactly 1, so zooming can never reach it — which is why sub-1× presets used to appear on iOS (Safari fuses the rear lenses into one virtual camera whose zoom starts at 0.5) and silently vanish on Android.

The camera now enumerates the lenses and, when a preset below 1× is picked and zoom cannot honour it, reopens the stream on the ultra-wide lens; going back to 1× or above returns to the main one. It is automatic — the only thing you do is offer the preset. Set EnableLensSwitching="false" to keep the stream on the device it started with.

Identifying the ultra-wide is best effort, because no browser exposes field of view: a descriptive label is used when there is one (Back Ultra Wide Camera on iOS), otherwise the Camera2 ordering on Android (camera2 0 is the main rear camera and the first extra rear camera is the ultra-wide). When neither says anything, nothing is guessed and the preset is simply not offered.

To see what a particular device reports, call the diagnostic dump — it prints a table of every camera with its capabilities to the console:

@inject ICameraCapture Camera
...
string report = await Camera.DescribeCamerasAsync();

Vetoing a capture

The host decides whether a photo may be taken at all — no storage left, quota exceeded, subscription expired:

<CameraView OnBeforeCapture="ctx => { if (NoRoomLeft) { ctx.Cancel = true; ctx.Reason = "quota"; } }" ... />

Errors

The camera never renders a message. It reports and you decide:

<CameraView OnError="ShowToast" ... />

CameraError.Kind is one of Initialization, Start, Stop, Busy, Recovered, Unavailable, Capture, Switch, Torch, Zoom. Busy means the stream could not start (permission denied, another app holds the camera, insecure context, no device) — the component then polls in the background, and reports Recovered if it comes back on its own or Unavailable once it gives up. Tune it with RetryAttempts and RetryDelayMs.

Customization

Slot Replaces
ShutterContent the shutter's inner disc
CloseContent the close icon
SwitchContent the front/back icon
TorchContent the torch icon (bool = armed)
LoadingContent the spinner shown while starting
OverlayTop free slot, centered at the top
OverlayBottom free slot, centered above the zoom bar

ShowSwitchCamera, ShowTorch, ShowCloseButton and CanCapture hide controls; Texts (a CameraTexts record) carries every label the camera renders, so a localized app builds one from its own resources.

Naming captures

The camera does not name photos. CameraPhoto gives you Bytes, Quality, ContentType and TakenAt, and you build the name — because only you know whether it becomes a download prompt, a blob key or a primary key in an upload queue:

Make it unique, not just timestamped. Captures are pipelined, so two shots in a burst can share the same millisecond — if that name is a key anywhere, the second photo overwrites the first. This is not hypothetical: it is why the camera stopped suggesting a name at all in 2.0.

If you have no opinion, BuildFileName() gives you one that is safe — photo_yyyyMMddHHmmssfff_{12 hex}.jpg:

private Task OnPhoto(CameraPhoto photo)
{
    var name = photo.BuildFileName();      // photo_20260815143012487_9f2c41ab77de.jpg
    ...
}

It is a helper, not a default: you ask for it. The timestamp keeps names sortable and records when the shot was actually taken (an upload pipeline stamps its own arrival time, which for a photo queued offline can be hours later), and the 48 random bits next to it put a collision at around 1 in 10^14 — so a full 32-character GUID would buy nothing and cost 20 characters, which matters on Android where each path component is capped at 255 bytes.

CameraPhotoNaming.BuildFileName(takenAt) is the same thing without a CameraPhoto in hand, and both take an extension argument if you are not writing .jpg.

Theming goes through CSS custom properties on the overlay root, so no ::deep is needed:

.my-camera {
    --bbcam-accent: #2563eb;      /* shutter disc */
    --bbcam-shutter-size: 84px;
    --bbcam-control-size: 56px;
    --bbcam-bg: #000;
    --bbcam-control-bg: rgba(255, 255, 255, .14);
    --bbcam-control-border: rgba(255, 255, 255, .55);
    --bbcam-control-color: #fff;
    --bbcam-pill-bg: rgba(0, 0, 0, .28);
    --bbcam-pill-border: rgba(255, 255, 255, .18);
    --bbcam-z-index: 1400;
}
<CameraView Class="my-camera" ... />

Style does the same thing inline, which is handy when the values come from the host's own design tokens rather than from a stylesheet:

<CameraView Style="--bbcam-accent: #e53935; --bbcam-shutter-size: 98px;" ... />

Notes from the field

  • Torch is pulsed, not held. Keeping it on blinds the preview and drains the battery, so it is switched on around the capture and off again — what a phone's camera app does.
  • No torch? The screen flashes. Front cameras and torchless devices get a white overlay pulse instead, which is enough to light a face at arm's length.
  • The front preview is mirrored, the file is not. The capture undoes the flip.
  • The overlay is always in the DOM, only hidden. The stream is prewarmed behind it so opening is instant; remounting a <video> would throw that away. Set Prewarm="false" to only touch the device when the user asks.

Advanced

ICameraCapture is the whole browser surface (start, capture, switch, torch, zoom, lenses, body lock). Register your own implementation instead of calling AddBlazorCamera() to fake the camera in tests or back it with something else.

Coming from 1.0.x with your own ICameraCapture? The interface grew three members — GetLensesAsync, UseLensAsync and DescribeCamerasAsync. Returning an empty list, false and "[]" gives you exactly the previous behaviour (zoom only, no lens switching).

Coming from 1.1.x with your own ICameraCapture? Three more — AttachPinchAsync, UpdatePinchConfigAsync and DetachPinchAsync. Returning false from the first and doing nothing in the other two leaves the camera with presets only, exactly as before: the component asks for the gesture, the backend decides whether it can serve it. A backend that does serve it reports where the gesture lands through ICameraPinchListener.OnPinchZoomChanged.

License

Free — see LICENSE.md.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.4.6 105 9/25/2026
1.4.5 96 9/23/2026
1.3.4 112 9/20/2026
1.2.3 148 9/8/2026
1.1.2 191 8/22/2026
1.1.1 151 8/15/2026
1.0.0 221 8/3/2026

Pinch to zoom: two fingers over the viewfinder now move the zoom continuously, from the minimum of the active lens up to the maximum the device can really do, with a floating indicator of the current factor. The presets stay as shortcuts. Native zoom is used first and only the remainder is cropped, so going past the camera's own maximum no longer throws away the zoom it could do. Custom ICameraCapture implementations gain three members (AttachPinchAsync, UpdatePinchConfigAsync, DetachPinchAsync); returning false from the first keeps the previous behaviour.