CameraView
Shiny.Maui.Controls.Camera is a cross-platform CameraView for .NET MAUI — live preview with zoom, torch, lens selection, photo & video capture, and a live effects pipeline — backed by AVFoundation (iOS / Mac Catalyst / macOS AppKit), CameraX (Android) and Media Capture (Windows). A matching Shiny.Blazor.Controls.Camera brings the same control to Blazor WebAssembly via getUserMedia.
Two pluggable pipelines are what set it apart from other camera controls, and they compose:
The frame-analysis pipeline — assign a single analyzer to the view and it streams frames to it off the UI thread. The analyzer draws styled bounding boxes continuously (via the built-in CameraOverlayView) but delivers a result only on a gated “scan trigger” — arm with Scan() / ScanCommand and the next confirmed detection fires once (an OnDetected handler returning true keeps scanning). The analyzer can be declared right in XAML (it’s the content property of CameraView), and an optional ScanWindow restricts detection to a region and draws an aim reticle. Ships with analyzers for barcode/QR scanning, face detection, motion detection, OCR, and structured documents — invoices (with order lines), receipts (line items + per-tax breakdown + totals), business cards (emails + typed phones + name/title/company), AAMVA driver’s licenses, Canadian-province-aware health cards, credit cards, and passports (MRZ). Each document is a strong record with nullable fields. When the document is free-form — or you’d rather not write parse rules — the AI document scanner detects that a document is present cheaply on every frame and sends just that one frame to a Microsoft.Extensions.AI IChatClient for structured extraction.
The effects pipeline — Effects is an ordered, live collection applied to the preview, captured stills and (on Apple) recorded video: eleven colour grades, five spatial GPU looks (comic, sketch, posterize, pixelate, blur), compositing draw effects for watermarks and face masks anchored to tracked facial landmarks, and slow post-capture transforms such as AI photo stylization. Filter is sugar over it — always applied first in the chain.
-
Frame Analyzers — barcode, face, motion, OCR, documents, custom
-
Effects & Filters — the four effect kinds, built-ins, per-platform coverage
-
Face Masks — Messenger-style masks anchored to landmarks
-
AI Document Scanner — detect a document, then let an
IChatClientread it -
AI Photo Stylizer — “redraw my photo as a comic” on the shutter
-
Blazor Usage — the WebAssembly specifics
-
-
No files change for this selection — pick a library above to see what it adds.
Features
Section titled “Features”Capture
Section titled “Capture”- Live preview with
AspectFill/AspectFitscaling, auto-starting and self-permissioning, with interruptions (phone call, another app taking the camera, thermal throttling) reported throughCameraErrorand recovered from automatically. - Lens & device selection —
Facing(Back / Front / External) or pin an exact device (multiple back lenses, USB webcams on macOS) viaGetAvailableCamerasAsync()+CameraId. - Zoom (clamped to the device’s reported
MinZoom..MaxZoom), optionally driven by pinch-to-zoom. - Torch / flashlight (
IsTorchOn, continuous) and still-capture flash (FlashMode— Off / On / Auto). - Photo capture → JPEG bytes with the effects chain baked in, plus
CaptureAndStopAsync()for a “scan then freeze” flow. - Video recording with optional audio, and session-level quality, bitrate and frame-rate control (
VideoQualityLowest…Highest, with fallback to the nearest rung the device supports). - Burn-in video overlays — composite a watermark, timestamp or telemetry into every encoded frame via
VideoRecordingOptions.Overlay, drawn withMicrosoft.Maui.Graphicsso one implementation covers every platform. - Recording and analyzing at the same time on every platform — a dash-cam app can read signs off its own live feed while it records.
- Ordered, live effects chain (
Effects) applied to the preview, captured photos and recorded video — mutate it while the camera runs and the change lands on the next frame. - Eleven colour grades — Mono, Noir, Sepia, Invert, Vivid, Cool, Warm, Fade, Chrome, Instant, Tonal (Apple Core Image, Android
RenderEffect, Blazor CSS), reachable asFilteror asCameraEffects.*. - Five spatial GPU looks — Comic, Sketch, Posterize, Pixelate, Blur — things no colour matrix can express.
- Four extension points —
IColorEffect(a colour matrix, honoured everywhere),INativeEffect(a per-backend GPU program),IDrawEffect(compositing over preview + stills + video),ICaptureEffect(slow post-capture work). - Face masks — anchor an image or custom drawing to tracked facial landmarks with smoothing (
FaceMaskEffect+FaceAnalyzer { DetectLandmarks = true }). - AI photo stylization —
AiPhotoStylizerover aMicrosoft.Extensions.AIIImageGenerator, soCapturePhotoAsyncreturns the regenerated image. - Honest per-platform coverage —
CameraView.GetEffectSupport(effect)returnsFull/ColorOnly/StillOnly/Unsupportedso a UI can grey out what would silently do nothing.
- Frame-analysis pipeline — a single pluggable
IFrameAnalyzerwith back-pressure (drop-on-busy); boxes draw every frame, results are delivered on a gated scan trigger (Scan()/OnDetected), optionally restricted to aScanWindowthat the overlay frames as a viewfinder. - Built-in analyzers — barcode/QR (native Vision / MLKit, restrictable to specific symbologies), face (with landmarks), motion (clustered into regions, debounced), OCR (with scan-window crop + upscale for small text), and structured documents.
- AI document scanner —
AiDocumentAnalyzer<T>detects presence natively every frame but calls the model at most once per document, straight into your record via MEAI structured output. - Drop-in overlay —
CameraOverlayViewlayered over the preview auto-subscribes, tracksScaleMode/aspect, and dims + reticles the scan window; box and reticle colours are tunable. - Modular analyzer packages — add only what you need:
.Camera.Barcode,.Camera.Face,.Camera.Motion,.Camera.Ocr,.Camera.Documents,.Camera.Ai.
AI Skill
Section titled “AI Skill”Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install plugins:
claude plugin install shiny-client@shinyclaude plugin install shiny-maui@shinyclaude plugin install controls@shinyclaude plugin install shiny-mediator@shinyclaude plugin install shiny-data@shinyclaude plugin install shiny-aspire@shinyclaude plugin install shiny-extensions@shinyStep 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skillsStep 2 — Install plugins:
copilot plugin install shiny-client@shinycopilot plugin install shiny-maui@shinycopilot plugin install controls@shinycopilot plugin install shiny-mediator@shinycopilot plugin install shiny-data@shinycopilot plugin install shiny-aspire@shinycopilot plugin install shiny-extensions@shinyInstallation
Section titled “Installation”.NET MAUI
Section titled “.NET MAUI”dotnet add package Shiny.Maui.Controls.Camera
# optional — add only the analyzers you needdotnet add package Shiny.Maui.Controls.Camera.Barcodedotnet add package Shiny.Maui.Controls.Camera.Facedotnet add package Shiny.Maui.Controls.Camera.Motiondotnet add package Shiny.Maui.Controls.Camera.Ocrdotnet add package Shiny.Maui.Controls.Camera.Documents # invoice / receipt / business card / licence / health card / credit card / passportdotnet add package Shiny.Maui.Controls.Camera.Ai # AI document scanner + AI photo stylizerRegister the handler alongside UseShinyControls():
builder .UseShinyControls() .UseShinyCamera();xmlns:cam="http://shiny.net/maui/camera"Add the platform permissions your app needs:
- iOS / Mac Catalyst / macOS —
NSCameraUsageDescription(andNSMicrophoneUsageDescriptionfor video with audio) inInfo.plist; the macOS camera entitlement when sandboxed. - Android —
<uses-permission android:name="android.permission.CAMERA" />(andRECORD_AUDIOfor video with audio). Minimum SDK 23 (CameraX requirement). - Windows — the
webcam(andmicrophone) capability inPackage.appxmanifest.
Blazor
Section titled “Blazor”dotnet add package Shiny.Blazor.Controls.Camera@using Shiny.Blazor.Controls.Camera@using Shiny.Controls.CameraSee Blazor Usage for the WebAssembly specifics.
Quick Start — MAUI
Section titled “Quick Start — MAUI”<cam:CameraView x:Name="Camera" Facing="Back" ScaleMode="AspectFill" Zoom="1" IsPinchToZoomEnabled="True" IsTorchOn="False" Filter="None" />// The preview auto-starts when the view is added (IsActive defaults true) and the control requests camera// permission itself — handle a denial (or any error) via CameraError. Toggle IsActive for lifecycle.this.Camera.CameraError += (_, e) => status = e.Message; // e.g. "Camera permission denied"
protected override void OnAppearing(){ base.OnAppearing(); this.Camera.IsActive = true; // resume (no-op on first show — it already auto-started)}
protected override void OnDisappearing(){ base.OnDisappearing(); this.Camera.IsActive = false; // release the camera while off-screen}
// Capture a still — the whole effects chain is baked into the JPEG, so preview and photo matchvar photo = await this.Camera.CapturePhotoAsync();await File.WriteAllBytesAsync(path, photo.Data);
// Or capture and stop the session in one step (handy from an analyzer's OnDetected — "scan then freeze")var frozen = await this.Camera.CaptureAndStopAsync();
// Flip the lensthis.Camera.Facing = this.Camera.Facing == CameraFacing.Back ? CameraFacing.Front : CameraFacing.Back;Pinch to zoom
Section titled “Pinch to zoom”Zoom is an ordinary bindable property — set it from code, bind it to a slider, bind it to a view model. Setting
IsPinchToZoomEnabled="True" adds a two-finger pinch on the preview as a second driver of that same property:
<cam:CameraView x:Name="Camera" Facing="Back" IsPinchToZoomEnabled="True" />
<!-- tracks the pinch live, because both write the same Zoom property --><Slider Minimum="{Binding MinZoom, Source={x:Reference Camera}}" Maximum="{Binding MaxZoom, Source={x:Reference Camera}}" Value="{Binding Zoom, Source={x:Reference Camera}, Mode=TwoWay}" />Because the gesture writes through Zoom, it inherits the same clamp everything else gets: the value can never
leave MinZoom..MaxZoom, the range the handler reports from the active device (AVCaptureDevice’s
MinAvailableVideoZoomFactor/VideoMaxZoomFactor capped at 10x on Apple platforms, CameraX’s ZoomState on
Android, ZoomControl.Min/Max on Windows). Pinching past either end simply pins there, and pinching back the
other way responds immediately.
The gesture is off by default so it can’t collide with an app that already handles touches over the preview — turn it on per view.
Video Recording
Section titled “Video Recording”VideoRecordingOptions.IncludeAudio defaults to true; the audio permission is requested only when audio is requested.
await this.Camera.StartVideoRecordingAsync(new VideoRecordingOptions { IncludeAudio = true });// ... later ...var video = await this.Camera.StopVideoRecordingAsync(); // CameraVideo { FilePath, Duration }Quality, bitrate and frame rate
Section titled “Quality, bitrate and frame rate”Recording quality is set on the control, not on VideoRecordingOptions:
<cam:CameraView x:Name="Camera" VideoQuality="High" VideoFrameRate="30" />this.Camera.VideoQuality = VideoQuality.Medium; // 720pthis.Camera.VideoFrameRate = 24; // null = platform defaultthis.Camera.VideoBitrate = 8_000_000; // null = platform default for the resolutionVideoQuality |
Target | Android (CameraX) | Apple (session preset) | Windows |
|---|---|---|---|---|
Lowest |
Device minimum | Quality.Lowest |
Low |
Qvga |
Low |
~480p | Quality.SD |
640x480 |
Vga |
Medium |
720p | Quality.HD |
1280x720 |
HD720p |
High (default) |
1080p | Quality.FHD |
1920x1080 |
HD1080p |
UltraHigh |
2160p (4K) | Quality.UHD |
3840x2160 |
Uhd2160p |
Highest |
Device maximum | Quality.Highest |
High |
Uhd2160p |
A device that cannot deliver the requested rung falls back to the nearest one it supports rather than failing to start — capture ladders vary enormously across hardware, and the front camera usually tops out below the back one.
Burning a detection into the recording. Analyzer geometry (
OverlayBox,RecognizedText.BoundingBox) is normalized upright coordinates andIVideoOverlayRendererdraws in encoded-frame pixel space, so boxing a detection in the saved file isbox.X * context.Width,box.Y * context.Height. That holds because the analyzed frame and the encoded frame share a field of view — on Apple platforms they are the same sample buffer, and on Android the handler binds a shared CameraXViewPortwhenever analysis and recording run together. Without it CameraX would size analysis 4:3 and the recorder 16:9 and the box would land off its subject.
Recording and analyzing at the same time is supported on every platform — a dash-cam-style app can read signs or plates off its own live feed while it records. On Android there is one cost:
Preview + VideoCapture + ImageAnalysisis a guaranteed CameraX combination at LIMITED hardware level, but a fourth use case needs LEVEL_3, soImageCaptureis dropped for the duration of a recording that has an enabled analyzer attached.CapturePhotoAsyncsays exactly that if you call it, and photo capture returns automatically when the recording stops. Outside a recording, an analyzer andImageCapturebind together as before.Effects in the recorded file are platform-dependent. On Apple the whole chain (colour, spatial and draw effects, including
Filter) is composited into the recording, so the file matches the preview. On Android only draw effects reach the file — the preview’sRenderEffectlives onPreviewView, not on theVideoCaptureuse case, so a colour or comic look is not recorded. On Windows and Blazor the recording is the raw feed. See Effects & Filters for the full coverage table.
Burn overlays into the recording
Section titled “Burn overlays into the recording”The CameraOverlayView only paints the on-screen preview — nothing it draws reaches the saved file. To composite a watermark, timestamp, telemetry or reticle into every recorded frame, set VideoRecordingOptions.Overlay to an IVideoOverlayRenderer. You draw with a Microsoft.Maui.Graphics.ICanvas, so the same drawing code works on every platform.
await this.Camera.StartVideoRecordingAsync(new VideoRecordingOptions{ IncludeAudio = true, Overlay = new DelegateVideoOverlay((canvas, frame, ctx) => { // frame = (0,0,Width,Height) in the encoded frame's pixel space; origin top-left canvas.FontColor = Colors.White; canvas.FontSize = Math.Max(24, frame.Height * 0.04f); canvas.DrawString(ctx.Elapsed.ToString(@"mm\:ss"), 20, 20, frame.Width, 60, HorizontalAlignment.Left, VerticalAlignment.Top); })});Two ship-ready implementations are included: DelegateVideoOverlay (an inline draw lambda, above) and DrawableVideoOverlay (wrap any existing IDrawable — e.g. a CameraOverlayDrawable — to burn the same boxes you show on-preview). VideoOverlayContext carries Elapsed, FrameIndex, Width, Height, and Facing.
DrawOverlayruns off the UI thread, once per encoded frame, on a capture/encoder thread. A renderer that reflects live UI state must read it through avolatilefield or an immutable snapshot — never touch UI objects from inside it. Draw in frame pixel space (ctx.Width/Height); front-camera frames are delivered already un-mirrored, so text renders the right way round.When
Overlayisnullthe fast native recorder is used unchanged (no performance or behavior change). Burn-in overlays are supported on iOS, Mac Catalyst, macOS, and Android; on WindowsStartVideoRecordingAsyncthrowsPlatformNotSupportedExceptionwhen an overlay is set — record without it, or use the on-previewCameraOverlayView.
Live Filters & Effects
Section titled “Live Filters & Effects”<cam:CameraView Filter="Noir" />Filter accepts None, Mono, Noir, Sepia, Invert, Vivid, Cool, Warm, Fade, Chrome, Instant, Tonal. Filtering is applied to the live preview (Apple CIFilter — the Fade/Chrome/Instant/Tonal set maps to the Core Image CIPhotoEffect* filters; Android RenderEffect color matrix on API 31+; Blazor CSS); on Windows it applies to captured stills only.
The same filter is baked into captured photos (CapturePhotoAsync) on every platform, so a still matches the preview. On Android below API 31 the live preview is unfiltered (no RenderEffect), but captured photos still come back filtered via a managed CPU pass.
Filter is sugar over the Effects chain — the chosen grade is materialized as the first effect, so setting both is well-defined rather than depending on assignment order:
this.Camera.Filter = CameraFilter.Noir; // applied firstthis.Camera.Effects.Add(CameraEffects.Comic); // then thisthis.Camera.Effects.Add(new DelegateDrawEffect("stamp", (canvas, frame, ctx) => { /* ... */ }));For anything beyond the eleven built-in colour grades — comic/sketch/blur looks, watermarks, face masks, AI stylization — and for the per-platform coverage table, see Effects & Filters.
Selecting a Camera
Section titled “Selecting a Camera”Facing picks by position, but you can enumerate every physical camera and pin an exact one — essential for phones with multiple back lenses or desktops with several webcams:
var cameras = await this.Camera.GetAvailableCamerasAsync();// CameraInfo { Id, Name, Facing, IsDefault }this.Camera.CameraId = cameras.First(c => c.Name.Contains("USB")).Id;Set CameraId back to null to fall back to Facing.
Properties
Section titled “Properties”| Property | Type | Default | Description |
|---|---|---|---|
Facing |
CameraFacing |
Back |
Back / Front / External lens position |
CameraId |
string? |
null |
Exact device id from GetAvailableCamerasAsync(); overrides Facing |
IsActive |
bool |
true |
Whether the session is running |
Zoom |
double |
1 |
Zoom factor, clamped to MinZoom..MaxZoom |
MinZoom / MaxZoom |
double |
1 |
Supported zoom range (reported by the handler) |
IsPinchToZoomEnabled |
bool |
false |
Two-finger pinch on the preview drives Zoom — see Pinch to zoom (MAUI only) |
IsTorchOn |
bool |
false |
Continuous flashlight/torch |
FlashMode |
CameraFlashMode |
Off |
Flash behaviour for still capture (Off/On/Auto) |
ScaleMode |
PreviewScaleMode |
AspectFill |
How the preview fills the view |
Filter |
CameraFilter |
None |
Color grade — sugar over Effects, always applied first in the chain |
Effects |
IList<ICameraEffect> |
empty | Ordered, live effects chain — colour, spatial, compositing and post-capture. Mutate while running; see Effects & Filters |
EffectChain |
CameraEffectChain (read-only) |
Empty |
Immutable snapshot of Filter + Effects that the handlers render against |
LastAnalyzerResult |
object? (read-only) |
null |
The active analyzer’s latest ungated result (updated every frame), handed to draw effects as ctx.AnalyzerResult |
ShowDetectionOverlay |
bool |
true |
Whether overlay boxes are surfaced for the overlay |
Analyzer |
IFrameAnalyzer? |
null |
The single frame analyzer (also the XAML content property); swap live or toggle it via its IsEnabled, set null for no analysis — see Frame Analyzers |
ScanWindow |
RectF? (read-only) |
null |
Mirrors the active analyzer’s ScanWindow (normalized 0..1; null = whole frame) |
VideoQuality |
VideoQuality |
High |
Target capture resolution for recording — see Quality, bitrate and frame rate |
VideoBitrate |
int? |
null |
Target video encoding bitrate in bits/sec; null lets the platform choose for the resolution |
VideoFrameRate |
int? |
null |
Target capture frame rate; null lets the platform choose. Clamped to what the device’s active format supports |
IsRecording |
bool (read-only) |
false |
Whether a recording is in progress |
Overlays |
IReadOnlyList<OverlayBox> |
empty | Latest aggregated overlay boxes (read-only) |
Methods & Events
Section titled “Methods & Events”| Member | Description |
|---|---|
Scan() / ScanCommand |
Arm the analyzer for one scan (the trigger) |
StopScanning() |
Disarm the analyzer |
RequestPermissionAsync() |
Request camera permission |
StartAsync() / StopAsync() |
Start / stop the session |
CapturePhotoAsync() |
Capture a still → CameraPhoto (Data / Width / Height, with the effects chain applied) |
CaptureAndStopAsync() |
Capture a still and stop the session, atomically → CameraPhoto |
StartVideoRecordingAsync() / StopVideoRecordingAsync() |
Record video → CameraVideo |
GetAvailableCamerasAsync() |
Enumerate cameras → IReadOnlyList<CameraInfo> |
CameraView.GetEffectSupport(effect) (static) |
How much of an effect this platform honours → Full / ColorOnly / StillOnly / Unsupported |
RebuildEffectChain() |
Re-snapshot the chain after mutating state inside an effect (the collection can’t observe that) |
MediaCaptured / VideoCaptured |
Raised after a photo / video is captured |
OverlaysChanged |
Raised with the latest aggregated overlay boxes (presentation only) |
CameraError |
Raised on a camera or pipeline error |
For results, arm with
Scan()/ScanCommand, then handle the analyzer’sOnDetected(Func<TArgs, Task<bool>>—return trueto keep scanning) or its typed event /Command(both gated by arming):BarcodesDetected,FacesDetected,MotionChanged,TextRecognized,DocumentDetected. Detection events that can hold several hits in one frame (barcode, face, motion) deliver an array.OverlaysChangedis only the styled boxes for drawing (never gated).
When to Use What
Section titled “When to Use What”Assign the matching analyzer to CameraView.Analyzer (one runs at a time). To offer several detectors, build them up front and swap the chosen one into Analyzer (e.g. from a picker) — see Frame Analyzers. Effects are additive by contrast: add as many as you like to Effects, in order.
Detecting something
Section titled “Detecting something”- Scan a barcode / QR →
BarcodeAnalyzer(Frame Analyzers). - Detect / box faces →
FaceAnalyzer. - Trigger on movement →
MotionAnalyzer. - Read raw text →
OcrAnalyzer(TextRecognized). - Parse an invoice (header + order lines) →
InvoiceAnalyzer. - Parse a receipt (line items, per-tax breakdown, subtotal/tip/total) →
ReceiptAnalyzer. - Scan a business card (name/title/company + emails/phones/website) →
BusinessCardAnalyzer. - Scan a driver’s license / health card →
DriversLicenseAnalyzer(deterministic AAMVA, incl. Canadian provinces + jurisdiction) or a Canadian-province-awareHealthCardAnalyzerfrom.Camera.Documents. - Scan a passport →
PassportAnalyzer(deterministic MRZ → number, names, nationality, DOB, expiry). - Read a credit card →
CreditCardAnalyzer(brand + number deterministic; name/expiry best-effort). - Parse a free-form or unknown document →
AiDocumentAnalyzer/AiDocumentAnalyzer<T>from.Camera.Ai— see AI Document Scanner. - Restrict scanning to a band → set the analyzer’s
ScanWindow(the overlay dims outside it and frames a reticle). - Just take a photo or record video → no analyzer required; use
CapturePhotoAsync/StartVideoRecordingAsync.
Changing how it looks
Section titled “Changing how it looks”- Apply a live colour grade → set
Filter(or add the matchingCameraEffects.*toEffects). - Apply a comic / sketch / posterize / pixelate / blur look → add
CameraEffects.Comic(etc.) toEffects— procedural, offline, live on the preview. - Ship your own look → add a
ColorEffect(a matrix, works everywhere) or anINativeEffectcarrying aNativeEffectDescriptor— see Effects & Filters. - Stick something on the user’s face →
FaceAnalyzer { DetectLandmarks = true }+ aFaceMaskEffect— see Face Masks. - Draw a watermark / timestamp across preview, photos and video → add a
DelegateDrawEffecttoEffects. - Burn something into the recorded file only →
VideoRecordingOptions.Overlay(see Burn overlays into the recording). - “Redraw my photo as a comic” → add an
AiPhotoStylizertoEffects— see AI Photo Stylizer. Pair it withCameraEffects.Comicfor a matching live viewfinder.


