BarcodeDetector
Find QR codes and barcodes in a camera frame or an image, using the platform's own decoder - no JavaScript scanning library, and no canvas pixel-pushing.
@inject Bit.Butil.BarcodeDetector barcodeDetectorMDN reference
GetSupportedFormats is for.
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.
@inject Bit.Butil.BarcodeDetector barcodeDetector
if (await barcodeDetector.IsSupported())
{
var formats = await barcodeDetector.GetSupportedFormats();
// "qr_code", "ean_13", "code_128", "pdf417", ...
}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.
IAsyncDisposable
Bit.Butil.MediaDevices mediaDevices
Bit.Butil.BarcodeDetector barcodeDetector
<video @ref="_video" autoplay playsinline muted></video>
<p>_lastValue</p>
{
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();
}
}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.
Bit.Butil.FileReader fileReader
Bit.Butil.BarcodeDetector barcodeDetector
<video @ref="_video" autoplay playsinline muted></video>
<input @ref="inputElement" type="file" accept="image/*" />
{
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
}
}
}IsSupported up front rather than inferring it from silence.
API reference
ValueTask<bool> IsSupported()ValueTask<string[]> GetSupportedFormats()ValueTask<DetectedBarcode[]> Detect(ElementReference element, string[]? formats = null)ValueTask<DetectedBarcode[]> DetectImage(byte[] imageBytes, string mimeType = "image/png", string[]? formats = null)ValueTask<ButilSubscription?> StartScan(ElementReference element, Action<DetectedBarcode[]> onDetected, string[]? formats = null, int intervalMs = 250)ValueTask DisposeAsync()RawValue, Format, X, Y, Width, Height