How to Build a Razor Class Library for Web Camera Access
Tags: webdev, dotnet, csharp, blazor
Published: 2023-12-15 01:41:49

In our previous article, we mentioned how to create a Web Barcode Scanner using [RazorBarcodeLibrary](https://www.nuget.org/packages/RazorCameraLibrary). The library is built on top of the [Dynamsoft JavaScript Barcode SDK](https://www.dynamsoft.com/barcode-reader/overview/), 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.

## Demo: Integrating RazorBarcodeLibrary and RazorCameraLibrary
[https://yushulx.me/Razor-Camera-Library/](https://yushulx.me/Razor-Camera-Library/)

## NuGet Package
[https://www.nuget.org/packages/RazorCameraLibrary](https://www.nuget.org/packages/RazorCameraLibrary)

## Initiating a Razor Class Library Project for Camera Access
1. Create a new Razor class library project named `RazorCameraLibrary` in Visual Studio.
2. Download the [JavaScript Camera Enhancer library](https://www.npmjs.com/package/dynamsoft-camera-enhancer) via NPM:

    ```bash
    npm i dynamsoft-camera-enhancer
    ```
3. Copy the `dce.js` file from the `node_modules/dynamsoft-camera-enhancer/dist` folder to the `wwwroot` folder of your project.
4. Create a `cameraJsInterop.js` file in the `wwwroot` folder. This file will export JavaScript functions that can be invoked in C#.


## Loading the JavaScript Camera SDK
1. In the `cameraJsInterop.js` file, add the following code to load the `Dynamsoft Camera Enhancer` library:

    ```javascript
    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);
        });
    }
    ```

2. Create a `CameraJsInterop.cs` file in the project root folder and add the following C# code:

    ```csharp
    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.


## Initializing Dynamsoft Camera Enhancer
1. Add a `createCameraEnhancer()` function to the `cameraJsInterop.js` file. This function creates a JavaScript camera object and returns it to the C# environment:

    ```javascript
    export async function createCameraEnhancer() {
        if (!Dynamsoft) return;
    
        try {
            let cameraEnhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance();
            return cameraEnhancer;
        }
        catch (ex) {
            console.error(ex);
        }
        return null;
    }
    ```

2. In the `CameraJsInterop.cs` file, add a `CreateCameraEnhancer()` function that invokes the `createCameraEnhancer()` function in JavaScript:

    ```csharp
    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 `CameraEnhancer` class is defined as follows:

    ```csharp
    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.

## Getting All Available Cameras
1. Create a Camera class in C# to manage camera properties like `deviceId` and `label`. This class can be structured as follows:

    ```csharp
    {
        public string DeviceId { get; set; } = string.Empty;
        public string Label { get; set; } = string.Empty;
    }
    ```

2. Add a `getCameras(cameraEnhancer)` function to the `cameraJsInterop.js` file. This function retrieves a list of available cameras and returns it to the C# environment:

    ```javascript
    export async function getCameras(cameraEnhancer) {
        if (!Dynamsoft) return;
    
        try {
            return await cameraEnhancer.getAllCameras();
        }
        catch (ex) {
            console.error(ex);
        }
    }
    ```

3. In the `CameraEnhancer.cs` file, add a `GetCameras()` function that invokes the `getCameras()` function in JavaScript. This function returns a list of Camera objects:

    ```csharp
    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.


## Opening and Closing a Camera

Opening a camera takes two steps:
1. Setting an HTML Div element as the video container for displaying the camera feed.
    
    **JavaScript**

    ```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#**
    
    ```csharp
    public async Task SetVideoElement(string elementId)
    {
        await _module.InvokeVoidAsync("setVideoElement", _jsObjectReference, elementId);
    }
    ```
2. Open a specific camera.
    
    **JavaScript**

    ```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#**
    
    ```csharp
    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**

```javascript
export async function closeCamera(cameraEnhancer) {
    if (!Dynamsoft) return;

    try {
        await cameraEnhancer.close();
    }
    catch (ex) {
        console.error(ex);
    }
}
```

**C#**

```csharp
public async Task CloseCamera()
{
    await _module.InvokeVoidAsync("closeCamera", _jsObjectReference);
}
```

## Capturing a Camera Frame 
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**

```javascript
export function acquireCameraFrame(cameraEnhancer) {
    if (!Dynamsoft) return;

    try {
        let img = cameraEnhancer.getFrame().toCanvas();
        return img;
    }
    catch (ex) {
        console.error(ex);
    }
}
```

**C#**

```csharp
public async Task<IJSObjectReference> AcquireCameraFrame()
{
    IJSObjectReference jsObjectReference = await _module.InvokeAsync<IJSObjectReference>("acquireCameraFrame", _jsObjectReference);
    return jsObjectReference;
}
```

## Drawing Graphics over 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**

    ```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#**
    
    ```csharp
    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**

    ```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#**
    
    ```csharp
    public async Task DrawLine(int x1, int y1, int x2, int y2)
    {
        await _module.InvokeVoidAsync("drawLine", _jsObjectReference, x1, y1, x2, y2);
    }
    ```

## Testing the Razor Camera Library
To test the Razor camera library, we'll integrate it with the [RazorBarcodeLibrary](https://www.nuget.org/packages/RazorBarcodeLibrary) to create a QR code scanner in a Blazor WebAssembly app.

1. Create a new Blazor WebAssembly app in Visual Studio.
2. Install the `RazorCameraLibrary` and `RazorBarcodeLibrary` NuGet packages.
3. In the `Pages/Index.razor` file, add the following code for layout:
    
    ```html
    @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>
    ``` 

4. Initialize the camera enhancer object:
    
    ```csharp
    @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");
        }
    }

    ```

5. Get all available cameras and open the first one:
    
    ```csharp
    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);
        }
    }
    ```

6. 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:
    
    ```csharp
    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();
    }
    ```

7. Run the Blazor QR code scanner app:

    ![blazor barcode qrcode scanner](https://www.dynamsoft.com/codepool/img/2023/12/razor-camera-library-blazor-qr-scanner.png)
    
## Source Code
[https://github.com/yushulx/Razor-Camera-Library](https://github.com/yushulx/Razor-Camera-Library)