DEV Community

Cover image for Step-by-Step Guide: Connect Blazor to Device Camera with JavaScript Interop and Face Recognition Logic
David Au Yeung
David Au Yeung

Posted on

Step-by-Step Guide: Connect Blazor to Device Camera with JavaScript Interop and Face Recognition Logic

Introduction

In my previous article, Step-by-Step Guide: Build a CRUD Blazor App with Entity Framework and PostgreSQL, we focused on data basics and CRUD flow.

In this article, we move to a practical AI patrol scenario: connect a Blazor web app to a device camera, send image frames to backend APIs, and display match alerts in real time. We will also explain how Blazor interacts with JavaScript, the face recognition logic behind the current demo, and the SQL DbStrategy design.

The project is now renamed to AIPatrolCamera.

This demo is built with .NET 10 and can be developed with VS Code.

Prerequisites

Before we begin, make sure you have:

  • .NET 10 SDK installed
  • VS Code + C# Dev Kit extension
  • A modern browser (Chrome/Edge recommended)
  • PostgreSQL (or use SQLite fallback from configuration)
  • Basic understanding of Blazor components and dependency injection

Step 1: Open and run AIPatrolCamera in VS Code

Open terminal in VS Code:

cd AIPatrolCamera
dotnet restore
dotnet run
Enter fullscreen mode Exit fullscreen mode

If startup succeeds, open the URL shown in the terminal and navigate to /patrol.

Step 2: Understand the camera page in Blazor

The page is implemented in Components/Pages/Patrol.razor.

Key UI elements:

  • <video id="video"> for live camera preview
  • <canvas id="canvas" class="d-none"> for snapshot capture
  • A start button that triggers patrol mode
  • An alert box shown when backend detects a match

Patrol page uses:

  • @rendermode InteractiveServer
  • IJSRuntime for JS interop
  • [JSInvokable] method ShowAlert(...) so JavaScript can call back into .NET

This is the core bridge between Blazor UI and browser camera APIs.

Detailed coding (Components/Pages/Patrol.razor)

@page "/patrol"
@rendermode InteractiveServer
@implements IAsyncDisposable

<PageTitle>Patrol</PageTitle>

<h1>AI Patrol Mode</h1>

<video id="video" autoplay playsinline class="patrol-video"></video>
<canvas id="canvas" class="d-none"></canvas>

@if (alertVisible)
{
    <div class="alert-box" role="alert" aria-live="assertive">
        <h2>⚠ MATCH FOUND</h2>
        <p><strong>Name:</strong> @alertName</p>
        <p><strong>Risk:</strong> @alertRisk</p>
        <p><strong>Action:</strong> @alertInstruction</p>
    </div>
}

<button class="btn btn-danger mt-3" @onclick="StartPatrolAsync">Start Patrol</button>

@code {
    private bool alertVisible;
    private string alertName = string.Empty;
    private string alertRisk = string.Empty;
    private string alertInstruction = string.Empty;
    private DotNetObjectReference<Patrol>? dotNetReference;
    private bool isInteractive;

    [Inject]
    private IJSRuntime JS { get; set; } = default!;

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            isInteractive = true;
        }
    }

    private async Task StartPatrolAsync()
    {
        dotNetReference ??= DotNetObjectReference.Create(this);
        await JS.InvokeVoidAsync("patrolCamera.start", dotNetReference);
    }

    [JSInvokable]
    public Task ShowAlert(string name, string riskLevel, string instruction)
    {
        alertName = name;
        alertRisk = riskLevel;
        alertInstruction = instruction;
        alertVisible = true;
        StateHasChanged();
        return Task.CompletedTask;
    }

    public async ValueTask DisposeAsync()
    {
        if (!isInteractive)
        {
            dotNetReference?.Dispose();
            return;
        }

        try
        {
            await JS.InvokeVoidAsync("patrolCamera.stop");
        }
        catch (JSDisconnectedException)
        {
        }

        dotNetReference?.Dispose();
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Understand Blazor ↔ JavaScript interop flow

In Patrol.razor, when user clicks Start Patrol, Blazor executes:

await JS.InvokeVoidAsync("patrolCamera.start", dotNetReference);
Enter fullscreen mode Exit fullscreen mode

This calls JavaScript function window.patrolCamera.start(...) in wwwroot/js/patrolCamera.js.

Inside JS:

  1. Request camera stream from browser:
   navigator.mediaDevices.getUserMedia({ video: true })
Enter fullscreen mode Exit fullscreen mode
  1. Bind stream to <video>
  2. Start timer (setInterval) every 3 seconds
  3. Draw current frame to canvas
  4. Convert frame to Base64 image with canvas.toDataURL("image/jpeg")
  5. POST to backend API /api/detect
  6. If match found, call C# method:
   dotNetHelper.invokeMethodAsync("ShowAlert", ...)
Enter fullscreen mode Exit fullscreen mode

In short:

  • Blazor calls JS to access browser-only capability (camera)
  • JS calls Blazor to update component state (alert rendering)

This two-way interop is the most important pattern when working with hardware features in web apps.

Detailed coding (wwwroot/js/patrolCamera.js)

window.patrolCamera = (() => {
    let dotNetHelper = null;
    let stream = null;
    let captureTimer = null;

    async function start(helper) {
        dotNetHelper = helper;

        const video = document.getElementById("video");
        if (!video) {
            return;
        }

        if (captureTimer) {
            clearInterval(captureTimer);
            captureTimer = null;
        }

        stream = await navigator.mediaDevices.getUserMedia({ video: true });
        video.srcObject = stream;

        captureTimer = setInterval(captureFrame, 3000);
    }

    async function captureFrame() {
        const video = document.getElementById("video");
        const canvas = document.getElementById("canvas");

        if (!video || !canvas || !dotNetHelper || video.videoWidth === 0 || video.videoHeight === 0) {
            return;
        }

        const context = canvas.getContext("2d");
        if (!context) {
            return;
        }

        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        context.drawImage(video, 0, 0);

        const image = canvas.toDataURL("image/jpeg");

        const response = await fetch("/api/detect", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ image })
        });

        if (!response.ok) {
            return;
        }

        const result = await response.json();
        if (result?.match) {
            await dotNetHelper.invokeMethodAsync(
                "ShowAlert",
                result.name ?? "Unknown",
                result.riskLevel ?? "Unknown",
                result.instruction ?? "Observe and Report");
        }
    }

    function stop() {
        if (captureTimer) {
            clearInterval(captureTimer);
            captureTimer = null;
        }

        if (stream) {
            for (const track of stream.getTracks()) {
                track.stop();
            }
            stream = null;
        }

        dotNetHelper = null;
    }

    return { start, stop };
})();
Enter fullscreen mode Exit fullscreen mode

Also ensure JS file is loaded in Components/App.razor:

<script src="@Assets["js/patrolCamera.js"]"></script>
<script src="@Assets["_framework/blazor.web.js"]"></script>
Enter fullscreen mode Exit fullscreen mode

Step 4: Explain camera permission and authority best practices

When your page calls getUserMedia, browser asks user permission to use camera.

Please remind readers:

  • Always request camera access only after explicit user action (for example clicking Start Patrol)

  • Explain clearly why camera is needed
  • Provide a visible stop action and actually stop tracks (track.stop())
  • Use HTTPS in non-local environments (camera APIs are restricted in insecure contexts)
  • Avoid capturing or sending frames when patrol is not active
  • Do not store sensitive data unless users have agreed

Good authority handling improves trust and keeps your app aligned with privacy expectations.

Step 5: Backend detection endpoint and current demo logic

Backend API is Controllers/DetectController.cs:

  • Accepts ImageRequest
  • Calls FaceRecognitionService.DetectAsync(...)
  • Returns DetectResponse

Current FaceRecognitionService is a demo implementation:

  • Decodes incoming Base64 image
  • Reads first person from database
  • Returns a simulated match response

So right now, it is a pipeline demo (camera → API → UI alert), not real AI face embedding comparison yet.

This is still useful because architecture and data flow are already valid.

Detailed coding (Controllers/DetectController.cs)

[ApiController]
[Route("api/[controller]")]
public class DetectController(FaceRecognitionService faceRecognitionService) : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> Detect([FromBody] ImageRequest request, CancellationToken cancellationToken)
    {
        if (request is null)
        {
            return BadRequest();
        }

        var result = await faceRecognitionService.DetectAsync(request.Image, cancellationToken);
        return Ok(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Detailed coding (Services/FaceRecognitionService.cs) - current demo behavior

public class FaceRecognitionService(AppDbContext dbContext)
{
    public async Task<DetectResponse> DetectAsync(string imageData, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(imageData))
        {
            return new DetectResponse { Match = false };
        }

        var imageBytes = TryDecodeBase64(imageData);
        if (imageBytes is null || imageBytes.Length == 0)
        {
            return new DetectResponse { Match = false };
        }

        var firstKnownPerson = await dbContext.People
            .AsNoTracking()
            .OrderBy(x => x.Id)
            .FirstOrDefaultAsync(cancellationToken);

        if (firstKnownPerson is null)
        {
            return new DetectResponse
            {
                Match = true,
                Name = "Test Person",
                RiskLevel = "Medium",
                Instruction = "Observe and Report"
            };
        }

        return new DetectResponse
        {
            Match = true,
            Name = firstKnownPerson.Name,
            RiskLevel = firstKnownPerson.RiskLevel,
            Instruction = "Observe and Report"
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: SQL DbStrategy design explained

AIPatrolCamera uses strategy pattern for DB provider selection:

  • IDbProviderStrategy
  • PostgresDbProviderStrategy
  • SqliteDbProviderStrategy
  • DbProviderStrategyResolver

Program.cs registers both strategies and resolves one by config key:

"Database": {
  "Provider": "Postgres"
}
Enter fullscreen mode Exit fullscreen mode

Why this DbStrategy is good:

  • No hardcoded provider in startup logic
  • Easy switching between PostgreSQL and SQLite
  • Good for local demo vs deployment environment
  • Keeps EF Core configuration clean and extensible

In this project, AppDbContext currently contains a People table with fields like:

  • Name
  • RiskLevel
  • FaceEmbedding (byte[] placeholder for model output)

This aligns with patrol use case and future AI model integration.

Detailed coding (Data/DbStrategy/*)

public interface IDbProviderStrategy
{
    string ProviderName { get; }
    void Configure(DbContextOptionsBuilder optionsBuilder, IConfiguration configuration);
}
Enter fullscreen mode Exit fullscreen mode
public class PostgresDbProviderStrategy : IDbProviderStrategy
{
    public string ProviderName => "Postgres";

    public void Configure(DbContextOptionsBuilder optionsBuilder, IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString("DefaultConnection")
            ?? throw new InvalidOperationException("ConnectionStrings:DefaultConnection is required for Postgres provider.");

        optionsBuilder.UseNpgsql(connectionString);
    }
}
Enter fullscreen mode Exit fullscreen mode
public class SqliteDbProviderStrategy : IDbProviderStrategy
{
    public string ProviderName => "Sqlite";

    public void Configure(DbContextOptionsBuilder optionsBuilder, IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString("SqliteConnection")
            ?? "Data Source=AIPatrolCamera.db";

        optionsBuilder.UseSqlite(connectionString);
    }
}
Enter fullscreen mode Exit fullscreen mode
public class DbProviderStrategyResolver(IEnumerable<IDbProviderStrategy> strategies, IConfiguration configuration)
{
    public IDbProviderStrategy Resolve()
    {
        var configuredProvider = configuration["Database:Provider"] ?? "Postgres";

        var strategy = strategies.FirstOrDefault(s =>
            string.Equals(s.ProviderName, configuredProvider, StringComparison.OrdinalIgnoreCase));

        return strategy ?? throw new InvalidOperationException($"Unsupported Database:Provider '{configuredProvider}'.");
    }
}
Enter fullscreen mode Exit fullscreen mode

And in Program.cs:

builder.Services.AddSingleton<IDbProviderStrategy, SqliteDbProviderStrategy>();
builder.Services.AddSingleton<IDbProviderStrategy, PostgresDbProviderStrategy>();
builder.Services.AddSingleton<DbProviderStrategyResolver>();
builder.Services.AddDbContext<AppDbContext>((serviceProvider, options) =>
{
    var strategyResolver = serviceProvider.GetRequiredService<DbProviderStrategyResolver>();
    var strategy = strategyResolver.Resolve();
    strategy.Configure(options, builder.Configuration);
});
Enter fullscreen mode Exit fullscreen mode

Step 7: Running the App

Run the app:

dotnet run
Enter fullscreen mode Exit fullscreen mode

What to do next - real image detection with ONNX in ML.NET

The next milestone is replacing demo logic in FaceRecognitionService with real model inference.

Target direction:

  1. Add ONNX face detection/recognition model to project
  2. Load model through ML.NET pipeline
  3. Preprocess camera frame (resize/normalize/tensor conversion)
  4. Run inference to get embedding or prediction scores
  5. Compare with stored embeddings in database
  6. Apply threshold for match decision
  7. Return confidence + risk-based instruction

The project already references Microsoft.ML.OnnxRuntime, so you have a good starting point.

When this part is done, patrol mode becomes true AI detection instead of static demo output.

Detailed coding skeleton for next step (ONNX + ML.NET direction)

Use this as a reference implementation structure for your next article:

public class FaceRecognitionService(AppDbContext dbContext)
{
    public async Task<DetectResponse> DetectAsync(string imageData, CancellationToken cancellationToken)
    {
        var imageBytes = DecodeImage(imageData);
        if (imageBytes.Length == 0)
        {
            return new DetectResponse { Match = false };
        }

        // 1) preprocess image -> tensor
        // 2) run ONNX inference -> embedding
        // 3) compare embedding with dbContext.People FaceEmbedding
        // 4) pick best score above threshold

        var threshold = 0.80f;
        var bestMatch = await dbContext.People.AsNoTracking().FirstOrDefaultAsync(cancellationToken);

        if (bestMatch is null)
        {
            return new DetectResponse { Match = false };
        }

        return new DetectResponse
        {
            Match = true,
            Name = bestMatch.Name,
            RiskLevel = bestMatch.RiskLevel,
            Instruction = "Observe and Report"
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

This keeps your readers focused on architecture first, then model details in the next deep-dive post.

Suggested mini roadmap for readers

If your readers want an implementation order, you can suggest:

  1. Stabilize current camera capture interval and error handling
  2. Add logging for detection latency and API response time
  3. Implement ONNX inference in FaceRecognitionService
  4. Add confidence threshold configuration in appsettings.json
  5. Improve data model for multiple embeddings per person
  6. Add audit table for detection events

This keeps learning progressive and practical.

Conclusion

In this article, we focused on three core ideas:

  • How Blazor connects to a device camera through JavaScript interop
  • How the current face recognition flow works end-to-end in AIPatrolCamera
  • How SQL DbStrategy keeps database provider selection clean and flexible

And most importantly, we prepared a clear next step: move from demo service logic to real ONNX-based image detection with ML.NET.

If you already finished the previous CRUD tutorial, this is a strong bridge from data-driven Blazor apps to AI-enabled real-world scenarios.

Top comments (0)