BlazorBasics.Camera
1.2.3
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
<PackageReference Include="BlazorBasics.Camera" Version="1.2.3" />
<PackageVersion Include="BlazorBasics.Camera" Version="1.2.3" />
<PackageReference Include="BlazorBasics.Camera" />
paket add BlazorBasics.Camera --version 1.2.3
#r "nuget: BlazorBasics.Camera, 1.2.3"
#:package BlazorBasics.Camera@1.2.3
#addin nuget:?package=BlazorBasics.Camera&version=1.2.3
#tool nuget:?package=BlazorBasics.Camera&version=1.2.3
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 flipisOpen. - Imperative:
@refplusOpenAsync()/CloseAsync(), and never passIsOpen.
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. SetPrewarm="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,UseLensAsyncandDescribeCamerasAsync. Returning an empty list,falseand"[]"gives you exactly the previous behaviour (zoom only, no lens switching).
Coming from 1.1.x with your own
ICameraCapture? Three more —AttachPinchAsync,UpdatePinchConfigAsyncandDetachPinchAsync. Returningfalsefrom 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 throughICameraPinchListener.OnPinchZoomChanged.
License
Free — see LICENSE.md.
| Product | Versions 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. |
-
net10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.11)
-
net8.0
- Microsoft.AspNetCore.Components.Web (>= 8.0.30)
-
net9.0
- Microsoft.AspNetCore.Components.Web (>= 9.0.19)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
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.