Skip to content
Document DB 13 - MCP Server, REST API, Field Level Encryption, Transactional Outbox, & More!SHOW ME!!

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 pipelineEffects 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.

Frameworks
.NET MAUI
Blazor

No files change for this selection — pick a library above to see what it adds.

  • Live preview with AspectFill / AspectFit scaling, auto-starting and self-permissioning, with interruptions (phone call, another app taking the camera, thermal throttling) reported through CameraError and recovered from automatically.
  • Lens & device selectionFacing (Back / Front / External) or pin an exact device (multiple back lenses, USB webcams on macOS) via GetAvailableCamerasAsync() + 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 (VideoQuality LowestHighest, 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 with Microsoft.Maui.Graphics so 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 as Filter or as CameraEffects.*.
  • Five spatial GPU looks — Comic, Sketch, Posterize, Pixelate, Blur — things no colour matrix can express.
  • Four extension pointsIColorEffect (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 stylizationAiPhotoStylizer over a Microsoft.Extensions.AI IImageGenerator, so CapturePhotoAsync returns the regenerated image.
  • Honest per-platform coverageCameraView.GetEffectSupport(effect) returns Full / ColorOnly / StillOnly / Unsupported so a UI can grey out what would silently do nothing.
  • Frame-analysis pipeline — a single pluggable IFrameAnalyzer with back-pressure (drop-on-busy); boxes draw every frame, results are delivered on a gated scan trigger (Scan() / OnDetected), optionally restricted to a ScanWindow that 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 scannerAiDocumentAnalyzer<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 overlayCameraOverlayView layered over the preview auto-subscribes, tracks ScaleMode/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.
claude plugin marketplace add shinyorg/skills
claude plugin install shiny-client@shiny
BLE, GPS, Jobs, Notifications, Push, HTTP Transfers, OBD, Music, Health, DataSync — iOS, Android, Windows, MacOS, Linux, Web
claude plugin install shiny-maui@shiny
Shell, Contact Store
claude plugin install controls@shiny
TableView, BottomSheet, PillView, ImageViewer, Scheduler, Markdown, Mermaid Diagrams — MAUI and Blazor
claude plugin install shiny-mediator@shiny
Mediator/CQRS with middleware and source generators
claude plugin install shiny-data@shiny
DocumentDB and Spatial data libraries
claude plugin install shiny-aspire@shiny
Orleans and Gluetun Aspire integrations
claude plugin install shiny-extensions@shiny
DI, Stores, Reflector, Localization, Hosting modules
copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny-client@shiny
BLE, GPS, Jobs, Notifications, Push, HTTP Transfers, OBD, Music, Health, DataSync — iOS, Android, Windows, MacOS, Linux, Web
copilot plugin install shiny-maui@shiny
Shell, Contact Store
copilot plugin install controls@shiny
TableView, BottomSheet, PillView, ImageViewer, Scheduler, Markdown, Mermaid Diagrams — MAUI and Blazor
copilot plugin install shiny-mediator@shiny
Mediator/CQRS with middleware and source generators
copilot plugin install shiny-data@shiny
DocumentDB and Spatial data libraries
copilot plugin install shiny-aspire@shiny
Orleans and Gluetun Aspire integrations
copilot plugin install shiny-extensions@shiny
DI, Stores, Reflector, Localization, Hosting modules
View Skills Repository
Terminal window
dotnet add package Shiny.Maui.Controls.Camera
# optional — add only the analyzers you need
dotnet add package Shiny.Maui.Controls.Camera.Barcode
dotnet add package Shiny.Maui.Controls.Camera.Face
dotnet add package Shiny.Maui.Controls.Camera.Motion
dotnet add package Shiny.Maui.Controls.Camera.Ocr
dotnet add package Shiny.Maui.Controls.Camera.Documents # invoice / receipt / business card / licence / health card / credit card / passport
dotnet add package Shiny.Maui.Controls.Camera.Ai # AI document scanner + AI photo stylizer

Register the handler alongside UseShinyControls():

builder
.UseShinyControls()
.UseShinyCamera();
xmlns:cam="http://shiny.net/maui/camera"

Add the platform permissions your app needs:

  • iOS / Mac Catalyst / macOSNSCameraUsageDescription (and NSMicrophoneUsageDescription for video with audio) in Info.plist; the macOS camera entitlement when sandboxed.
  • Android<uses-permission android:name="android.permission.CAMERA" /> (and RECORD_AUDIO for video with audio). Minimum SDK 23 (CameraX requirement).
  • Windows — the webcam (and microphone) capability in Package.appxmanifest.
Terminal window
dotnet add package Shiny.Blazor.Controls.Camera
@using Shiny.Blazor.Controls.Camera
@using Shiny.Controls.Camera

See Blazor Usage for the WebAssembly specifics.

<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 match
var 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 lens
this.Camera.Facing = this.Camera.Facing == CameraFacing.Back
? CameraFacing.Front
: CameraFacing.Back;

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.

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 }

Recording quality is set on the control, not on VideoRecordingOptions:

<cam:CameraView x:Name="Camera"
VideoQuality="High"
VideoFrameRate="30" />
this.Camera.VideoQuality = VideoQuality.Medium; // 720p
this.Camera.VideoFrameRate = 24; // null = platform default
this.Camera.VideoBitrate = 8_000_000; // null = platform default for the resolution
VideoQuality 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 and IVideoOverlayRenderer draws in encoded-frame pixel space, so boxing a detection in the saved file is box.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 CameraX ViewPort whenever 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 + ImageAnalysis is a guaranteed CameraX combination at LIMITED hardware level, but a fourth use case needs LEVEL_3, so ImageCapture is dropped for the duration of a recording that has an enabled analyzer attached. CapturePhotoAsync says exactly that if you call it, and photo capture returns automatically when the recording stops. Outside a recording, an analyzer and ImageCapture bind 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’s RenderEffect lives on PreviewView, not on the VideoCapture use 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.

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.

DrawOverlay runs off the UI thread, once per encoded frame, on a capture/encoder thread. A renderer that reflects live UI state must read it through a volatile field 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 Overlay is null the fast native recorder is used unchanged (no performance or behavior change). Burn-in overlays are supported on iOS, Mac Catalyst, macOS, and Android; on Windows StartVideoRecordingAsync throws PlatformNotSupportedException when an overlay is set — record without it, or use the on-preview CameraOverlayView.

<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 first
this.Camera.Effects.Add(CameraEffects.Comic); // then this
this.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.

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.

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)
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’s OnDetected (Func<TArgs, Task<bool>>return true to 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. OverlaysChanged is only the styled boxes for drawing (never gated).

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.

  • Scan a barcode / QRBarcodeAnalyzer (Frame Analyzers).
  • Detect / box facesFaceAnalyzer.
  • Trigger on movementMotionAnalyzer.
  • Read raw textOcrAnalyzer (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 cardDriversLicenseAnalyzer (deterministic AAMVA, incl. Canadian provinces + jurisdiction) or a Canadian-province-aware HealthCardAnalyzer from .Camera.Documents.
  • Scan a passportPassportAnalyzer (deterministic MRZ → number, names, nationality, DOB, expiry).
  • Read a credit cardCreditCardAnalyzer (brand + number deterministic; name/expiry best-effort).
  • Parse a free-form or unknown documentAiDocumentAnalyzer / 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.
  • Apply a live colour grade → set Filter (or add the matching CameraEffects.* to Effects).
  • Apply a comic / sketch / posterize / pixelate / blur look → add CameraEffects.Comic (etc.) to Effects — procedural, offline, live on the preview.
  • Ship your own look → add a ColorEffect (a matrix, works everywhere) or an INativeEffect carrying a NativeEffectDescriptor — see Effects & Filters.
  • Stick something on the user’s faceFaceAnalyzer { DetectLandmarks = true } + a FaceMaskEffect — see Face Masks.
  • Draw a watermark / timestamp across preview, photos and video → add a DelegateDrawEffect to Effects.
  • Burn something into the recorded file onlyVideoRecordingOptions.Overlay (see Burn overlays into the recording).
  • “Redraw my photo as a comic” → add an AiPhotoStylizer to Effects — see AI Photo Stylizer. Pair it with CameraEffects.Comic for a matching live viewfinder.