How to Build a .NET Razor Camera Library and Scan QR Codes in Blazor
In our previous article, we mentioned how to create a Web Barcode Scanner using RazorBarcodeLibrary. The library is built on top of the Dynamsoft JavaScript Barcode SDK, which encapsulates the camera access and barcode reading logic in JavaScript. In fact, Dynamsoft also provides an independent JavaScript camera library, named Dynamsoft Camera Enhancer, for FREE use. In this article, we will demonstrate how to build a Razor class library that features camera access, utilizing the Dynamsoft JavaScript Camera SDK. You can combine this library with other image processing libraries, such as RazorBarcodeLibrary, to create a more powerful web camera application.
What you’ll build: A reusable Razor class library that wraps the Dynamsoft Camera Enhancer JavaScript SDK for webcam access, then integrate it with RazorBarcodeLibrary to build a real-time QR code scanner in a Blazor WebAssembly app.
Key Takeaways
- Dynamsoft Camera Enhancer provides a free JavaScript camera SDK that can be wrapped in a Razor class library for reuse across ASP.NET Core and Blazor projects.
- The
CameraEnhancerC# class uses JS interop to enumerate cameras, open/close streams, capture frames, and draw overlays — all from managed .NET code. - Captured canvas frames can be piped directly to Dynamsoft Barcode Reader (via RazorBarcodeLibrary) for real-time QR code and barcode scanning.
- This approach decouples camera access from barcode reading, letting you swap or extend image-processing logic independently.
Common Developer Questions
- How do I access a webcam from a Blazor WebAssembly app using C#?
- How do I scan QR codes from a live camera feed in ASP.NET Core Razor Pages?
- How do I use JavaScript interop in a Razor class library for camera and barcode reading?
This article is Part 1 in a 1-Part Series.
Demo: Real-Time QR Code Scanner with Razor Camera and Barcode Libraries
https://yushulx.me/Razor-Camera-Library/
Get the RazorCameraLibrary NuGet Package
https://www.nuget.org/packages/RazorCameraLibrary
Prerequisites
- .NET SDK
- Visual Studio 2022 or later
- A webcam connected to your development machine
- Get a 30-day free trial license for Dynamsoft Barcode Reader (required for the QR code scanning integration in Step 8)
Step 1: Create the Razor Class Library Project
- Create a new Razor class library project named
RazorCameraLibraryin Visual Studio. -
Download the JavaScript Camera Enhancer library via NPM:
npm i dynamsoft-camera-enhancer - Copy the
dce.jsfile from thenode_modules/dynamsoft-camera-enhancer/distfolder to thewwwrootfolder of your project. - Create a
cameraJsInterop.jsfile in thewwwrootfolder. This file will export JavaScript functions that can be invoked in C#.
Step 2: Load the JavaScript Camera SDK via JS Interop
-
In the
cameraJsInterop.jsfile, add the following code to load theDynamsoft Camera Enhancerlibrary:export function init() { return new Promise((resolve, reject) => { let script = document.createElement('script'); script.type = 'text/javascript'; script.src = '_content/RazorCameraLibrary/dce.js'; script.onload = async () => { resolve(); }; script.onerror = () => { reject(); }; document.head.appendChild(script); }); } -
Create a
CameraJsInterop.csfile in the project root folder and add the following C# code:using Microsoft.JSInterop; namespace RazorCameraLibrary { public class CameraJsInterop : IAsyncDisposable { private readonly Lazy<Task<IJSObjectReference>> moduleTask; public CameraJsInterop(IJSRuntime jsRuntime) { moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>( "import", "./_content/RazorCameraLibrary/cameraJsInterop.js").AsTask()); } public async ValueTask DisposeAsync() { if (moduleTask.IsValueCreated) { var module = await moduleTask.Value; await module.DisposeAsync(); } } public async Task LoadJS() { var module = await moduleTask.Value; await module.InvokeAsync<object>("init"); } } }As the
LoadJS()method is called, the JavaScript library will be loaded dynamically.
Step 3: Initialize the Camera Enhancer Instance
-
Add a
createCameraEnhancer()function to thecameraJsInterop.jsfile. This function creates a JavaScript camera object and returns it to the C# environment:export async function createCameraEnhancer() { if (!Dynamsoft) return; try { let cameraEnhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance(); return cameraEnhancer; } catch (ex) { console.error(ex); } return null; } -
In the
CameraJsInterop.csfile, add aCreateCameraEnhancer()function that invokes thecreateCameraEnhancer()function in JavaScript:public async Task<CameraEnhancer> CreateCameraEnhancer() { var module = await moduleTask.Value; IJSObjectReference jsObjectReference = await module.InvokeAsync<IJSObjectReference>("createCameraEnhancer"); CameraEnhancer cameraEnhancer = new CameraEnhancer(module, jsObjectReference); return cameraEnhancer; }The
CameraEnhancerclass is defined as follows:using Microsoft.JSInterop; using System.Text.Json; namespace RazorCameraLibrary { public class CameraEnhancer : IDisposable { private IJSObjectReference _module; private IJSObjectReference _jsObjectReference; private List<Camera> _cameras = new List<Camera>(); private DotNetObjectReference<CameraEnhancer> objRef; private bool _disposed = false; public int SourceWidth, SourceHeight; public CameraEnhancer(IJSObjectReference module, IJSObjectReference cameraEnhancer) { _module = module; _jsObjectReference = cameraEnhancer; objRef = DotNetObjectReference.Create(this); } } }We will add more functionalities to this class later.
Step 4: Enumerate All Available Cameras
-
Create a Camera class in C# to manage camera properties like
deviceIdandlabel. This class can be structured as follows:{ public string DeviceId { get; set; } = string.Empty; public string Label { get; set; } = string.Empty; } -
Add a
getCameras(cameraEnhancer)function to thecameraJsInterop.jsfile. This function retrieves a list of available cameras and returns it to the C# environment:export async function getCameras(cameraEnhancer) { if (!Dynamsoft) return; try { return await cameraEnhancer.getAllCameras(); } catch (ex) { console.error(ex); } } -
In the
CameraEnhancer.csfile, add aGetCameras()function that invokes thegetCameras()function in JavaScript. This function returns a list of Camera objects:public async Task<List<Camera>> GetCameras() { _cameras.Clear(); JsonElement? result = await _module.InvokeAsync<JsonElement>("getCameras", _jsObjectReference); if (result != null) { JsonElement element = result.Value; if (element.ValueKind == JsonValueKind.Array) { foreach (JsonElement item in element.EnumerateArray()) { Camera camera = new Camera(); if (item.TryGetProperty("deviceId", out JsonElement devideIdValue)) { string? value = devideIdValue.GetString(); if (value != null) { camera.DeviceId = value; } } if (item.TryGetProperty("label", out JsonElement labelValue)) { string? value = labelValue.GetString(); if (value != null) { camera.Label = value; } } _cameras.Add(camera); } } } return _cameras; }The
TryGetProperty()method is used to deserialize the JSON object returned from JavaScript.
Step 5: Open and Close a Camera Stream
Opening a camera takes two steps:
-
Setting an HTML Div element as the video container for displaying the camera feed.
JavaScript
export async function setVideoElement(cameraEnhancer, elementId) { if (!Dynamsoft) return; try { let element = document.getElementById(elementId); element.className = "dce-video-container"; await cameraEnhancer.setUIElement(element); } catch (ex) { console.error(ex); } }C#
public async Task SetVideoElement(string elementId) { await _module.InvokeVoidAsync("setVideoElement", _jsObjectReference, elementId); } -
Open a specific camera.
JavaScript
export async function openCamera(cameraEnhancer, cameraInfo, dotNetHelper, callback) { if (!Dynamsoft) return; try { await cameraEnhancer.selectCamera(cameraInfo); cameraEnhancer.on("played", function () { let resolution = cameraEnhancer.getResolution(); dotNetHelper.invokeMethodAsync(callback, resolution[0], resolution[1]); }); await cameraEnhancer.open(); } catch (ex) { console.error(ex); } }C#
public async Task OpenCamera(Camera camera) { await _module.InvokeVoidAsync("openCamera", _jsObjectReference, camera, objRef, "OnSizeChanged"); }
Closing a camera is a straightforward process. Here’s how you can implement it in both JavaScript and C#:
JavaScript
export async function closeCamera(cameraEnhancer) {
if (!Dynamsoft) return;
try {
await cameraEnhancer.close();
}
catch (ex) {
console.error(ex);
}
}
C#
public async Task CloseCamera()
{
await _module.InvokeVoidAsync("closeCamera", _jsObjectReference);
}
Step 6: Capture a Frame from the Camera Feed
In this section, we’ll implement a function to capture a frame from the camera feed. This function will return a JavaScript canvas object, which can be utilized for various image processing tasks. For instance, the canvas can be used to read barcodes from the captured image. This integration allows for versatile applications within ASP.NET and Blazor environments, where you might need to process or analyze the camera feed in real-time.
JavaScript
export function acquireCameraFrame(cameraEnhancer) {
if (!Dynamsoft) return;
try {
let img = cameraEnhancer.getFrame().toCanvas();
return img;
}
catch (ex) {
console.error(ex);
}
}
C#
public async Task<IJSObjectReference> AcquireCameraFrame()
{
IJSObjectReference jsObjectReference = await _module.InvokeAsync<IJSObjectReference>("acquireCameraFrame", _jsObjectReference);
return jsObjectReference;
}
Step 7: Draw Overlay Graphics on the Camera Feed
In many camera applications, an overlay is essential for displaying additional information or results directly on the camera feed. To facilitate this, we will create two key functions within our Razor class library:
-
DrawText(): This function will draw text onto the camera feed, which can be used to display scanning results or other relevant information.JavaScript
export function drawText(cameraEnhancer, text, x, y) { if (!Dynamsoft) return; try { let drawingLayers = cameraEnhancer.getDrawingLayers(); let drawingLayer; let drawingItems = new Array( new Dynamsoft.DCE.DrawingItem.DT_Text(text, x, y, 1), ) if (drawingLayers.length > 0) { drawingLayer = drawingLayers[0]; } else { drawingLayer = cameraEnhancer.createDrawingLayer(); } drawingLayer.addDrawingItems(drawingItems); } catch (ex) { console.error(ex); } }C#
public async Task DrawText(string text, int x, int y) { await _module.InvokeVoidAsync("drawText", _jsObjectReference, text, x, y); } -
DrawLine(): This function will be responsible for drawing lines on the camera feed. It can be used to highlight or outline specific areas of interest.JavaScript
export function drawLine(cameraEnhancer, x1, y1, x2, y2) { if (!Dynamsoft) return; try { let drawingLayers = cameraEnhancer.getDrawingLayers(); let drawingLayer; let drawingItems = new Array( new Dynamsoft.DCE.DrawingItem.DT_Line({ x: x1, y: y1 }, { x: x2, y: y2 }, 1) ) if (drawingLayers.length > 0) { drawingLayer = drawingLayers[0]; } else { drawingLayer = cameraEnhancer.createDrawingLayer(); } drawingLayer.addDrawingItems(drawingItems); } catch (ex) { console.error(ex); } }C#
public async Task DrawLine(int x1, int y1, int x2, int y2) { await _module.InvokeVoidAsync("drawLine", _jsObjectReference, x1, y1, x2, y2); }
Step 8: Build a Blazor WebAssembly QR Code Scanner
To test the Razor camera library, we’ll integrate it with the RazorBarcodeLibrary to create a QR code scanner in a Blazor WebAssembly app.
- Create a new Blazor WebAssembly app in Visual Studio.
- Install the
RazorCameraLibraryandRazorBarcodeLibraryNuGet packages. -
In the
Pages/Index.razorfile, add the following code for layout:@page "/" @inject IJSRuntime JSRuntime @using RazorCameraLibrary @using RazorBarcodeLibrary @using Camera = RazorCameraLibrary.Camera <PageTitle>Index</PageTitle> <div class="container"> <div> <button @onclick="GetCameras">Get Cameras</button> <select id="sources" @onchange="e => OnChange(e)"> @foreach (var camera in cameras) { <option value="@camera.DeviceId">@camera.Label</option> } </select> <button @onclick="Capture">@buttonText</button> </div> <div id="videoview"> <div id="videoContainer"></div> </div> </div> -
Initialize the camera enhancer object:
@code { private bool isLoading = false; private List<Camera> cameras = new List<Camera>(); private CameraJsInterop? cameraJsInterop; private CameraEnhancer? cameraEnhancer; private BarcodeReader? reader; private BarcodeJsInterop? barcodeJsInterop; private string selectedValue = string.Empty; private bool _isCapturing = false; private string buttonText = "Start"; protected override async Task OnInitializedAsync() { cameraJsInterop = new CameraJsInterop(JSRuntime); await cameraJsInterop.LoadJS(); cameraEnhancer = await cameraJsInterop.CreateCameraEnhancer(); await cameraEnhancer.SetVideoElement("videoContainer"); } } -
Get all available cameras and open the first one:
public async Task GetCameras() { if (cameraEnhancer == null) return; try { cameras = await cameraEnhancer.GetCameras(); if (cameras.Count >= 0) { selectedValue = cameras[0].DeviceId; await OpenCamera(); } } catch (Exception ex) { Console.WriteLine(ex.Message); } } public async Task OpenCamera() { if (cameraEnhancer == null) return; try { int selectedIndex = cameras.FindIndex(camera => camera.DeviceId == selectedValue); await cameraEnhancer.SetResolution(640, 480); await cameraEnhancer.OpenCamera(cameras[selectedIndex]); } catch (Exception ex) { Console.WriteLine(ex.Message); } } -
Add a button click event handler to trigger barcode scanning. Implement a loop to capture frames from the camera feed and read barcodes from the captured frames:
public async Task Capture() { if (barcodeJsInterop == null || reader == null) { isLoading = true; barcodeJsInterop = new BarcodeJsInterop(JSRuntime); await barcodeJsInterop.LoadJS(); reader = await barcodeJsInterop.CreateBarcodeReader(); isLoading = false; } if (cameraEnhancer == null) return; if (!_isCapturing) { buttonText = "Stop"; _isCapturing = true; _ = WorkLoop(); } else { buttonText = "Start"; _isCapturing = false; } } private async Task WorkLoop() { List<BarcodeResult> results = new List<BarcodeResult>(); if (barcodeJsInterop == null || cameraEnhancer == null || reader == null) return; while (_isCapturing) { try { IJSObjectReference canvas = await cameraEnhancer.AcquireCameraFrame(); results = await reader.DecodeCanvas(canvas); await cameraEnhancer.ClearOverlay(); for (int i = 0; i < results.Count; i++) { BarcodeResult result = results[i]; int minX = result.X1; int minY = result.Y1; await cameraEnhancer.DrawLine(result.X1, result.Y1, result.X2, result.Y2); minX = minX < result.X2 ? minX : result.X2; minY = minY < result.Y2 ? minY : result.Y2; await cameraEnhancer.DrawLine(result.X2, result.Y2, result.X3, result.Y3); minX = minX < result.X3 ? minX : result.X3; minY = minY < result.Y3 ? minY : result.Y3; await cameraEnhancer.DrawLine(result.X3, result.Y3, result.X4, result.Y4); minX = minX < result.X4 ? minX : result.X4; minY = minY < result.Y4 ? minY : result.Y4; await cameraEnhancer.DrawLine(result.X4, result.Y4, result.X1, result.Y1); await cameraEnhancer.DrawText(result.Text, minX, minY); } } catch (Exception ex) { Console.WriteLine(ex.Message); } } await cameraEnhancer.ClearOverlay(); } -
Run the Blazor QR code scanner app:

Common Issues and Edge Cases
- Camera permission denied in the browser: If
openCamera()throws aNotAllowedError, the user has blocked camera access. Ensure your page is served over HTTPS (orlocalhost), and prompt the user to grant permission in the browser’s site settings. - No cameras returned by
GetCameras(): This typically happens when the page is loaded over plain HTTP instead of HTTPS, or when the browser has nomediaDevicesAPI support. Verify thatnavigator.mediaDevices.enumerateDevices()is available in the browser console. - Blazor WebAssembly JS interop errors on first load: If the
dce.jsscript fails to load, check that the file is correctly placed underwwwrootand that the path_content/RazorCameraLibrary/dce.jsresolves. Clear the browser cache and rebuild the project if assets are stale.