loading
Warning:
Chromium only, and the formats vary by device Firefox and Safari don't implement this. Even on Chromium the available symbologies come from the underlying platform, so the same browser version decodes a different set on Android than on desktop - which is what GetSupportedFormats is for.

Support and formats

IsSupported / GetSupportedFormats

Worth calling both before offering a scanner: asking for a format the device can't decode makes detection quietly return nothing rather than throwing, so a list you didn't check turns into a scanner that never finds anything.

C#
@inject Bit.Butil.BarcodeDetector barcodeDetector

if (await barcodeDetector.IsSupported())
{
    var formats = await barcodeDetector.GetSupportedFormats();
    // "qr_code", "ean_13", "code_128", "pdf417", ...
}
Live sample
support check output
Results will appear here when you interact with the samples.

Scan from the camera

StartScan

The usual shape: open a stream with MediaDevices, attach it to a video element, and point StartScan at that element. Scanning is a poll - there is no 'barcode appeared' event - and detection is much slower than a frame, so sampling a few times a second is both enough and cheaper than trying every frame. A tick that arrives while the previous detection is still running is skipped, so a slow device degrades to a lower rate rather than building a backlog.

Razor
@implements IAsyncDisposable
@inject Bit.Butil.MediaDevices mediaDevices
@inject Bit.Butil.BarcodeDetector barcodeDetector

<video @ref="_video" autoplay playsinline muted></video>
<p>@_lastValue</p>

@code {
    private string? _lastValue;
    private ElementReference _video;
    private MediaStreamHandle? _stream;
    private ButilSubscription? _scan;

    private async Task Start()
    {
        _stream = await mediaDevices.GetUserMedia(
            audio: false,
            video: true,
            videoConstraints: new { facingMode = "environment" });   // rear camera on a phone

        await _stream!.AttachTo(_video);

        _scan = await barcodeDetector.StartScan(
            _video,
            onDetected: codes => InvokeAsync(() =>
            {
                // fires repeatedly while a code stays in view - debounce on the value
                _lastValue = codes[0].RawValue;
                StateHasChanged();
            }),
            formats: ["qr_code"],
            intervalMs: 250);
    }

    // The camera light stays on until the stream is disposed, whatever the page does.
    public async ValueTask DisposeAsync()
    {
        if (_scan is not null) await _scan.DisposeAsync();
        if (_stream is not null) await _stream.DisposeAsync();
    }
}
Live sample
Status Not scanning.
scan output
Results will appear here when you interact with the samples.

Scan a single frame, or an image file

Detect / DetectImage

Detect reads one frame of a video, image or canvas element. DetectImage takes an encoded image's bytes - a PNG or JPEG, not raw pixels - which is what you want for a file the user picked or an image you fetched. The decoded bitmap is released as soon as the scan finishes; it holds uncompressed pixels, which would be a real leak in a loop.

Razor
@inject Bit.Butil.FileReader fileReader
@inject Bit.Butil.BarcodeDetector barcodeDetector

<video @ref="_video" autoplay playsinline muted></video>
<input @ref="inputElement" type="file" accept="image/*" />

@code {
    private ElementReference _video;
    private ElementReference inputElement;

    private async Task ScanOnce()
    {
        // one frame of whatever the video is showing right now:
        var codes = await barcodeDetector.Detect(_video, formats: ["qr_code"]);

        // or an image the user picked:
        var bytes = await fileReader.ReadAsByteArray(inputElement, index: 0);
        var found = await barcodeDetector.DetectImage(bytes, "image/png");

        foreach (var code in found)
        {
            // code.RawValue, code.Format, and code.X/Y/Width/Height in source pixels
        }
    }
}
Live sample
single-shot output
Results will appear here when you interact with the samples.
Note:
Nothing found is not an error Every method returns an empty array rather than throwing when there is nothing to find - and also when the video has no frame yet, or the image couldn't be decoded. That keeps a scan loop from needing a try/catch per tick, but it does mean "no results" and "something went wrong" look the same; check IsSupported up front rather than inferring it from silence.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes BarcodeDetector. Returns default (false) during prerender/SSR instead of throwing.
GetSupportedFormats
ValueTask<string[]> GetSupportedFormats()
The symbologies this device can decode. Empty when the API is unavailable. The list is the platform's, not the browser's.
Detect
ValueTask<DetectedBarcode[]> Detect(ElementReference element, string[]? formats = null)
Scans one frame of a video, image or canvas element. Empty when there is nothing to find, or no frame yet.
DetectImage
ValueTask<DetectedBarcode[]> DetectImage(byte[] imageBytes, string mimeType = "image/png", string[]? formats = null)
Scans an encoded image - a PNG or JPEG file's bytes, not raw pixels. The decoded bitmap is released afterwards.
StartScan
ValueTask<ButilSubscription?> StartScan(ElementReference element, Action<DetectedBarcode[]> onDetected, string[]? formats = null, int intervalMs = 250)
Polls a live element and calls back when it finds something. Null when the API is unavailable. Dispose the subscription to stop.
DisposeAsync
ValueTask DisposeAsync()
On scope/circuit teardown, stops any scan whose subscription was never disposed.
DetectedBarcode
RawValue, Format, X, Y, Width, Height
The decoded contents, the symbology, and the bounding box in the source element's pixel coordinates.
An unhandled error has occurred. Reload 🗙