Navaron Bracke

reimplement ZXing barcode reader

  1 +import 'dart:async';
  2 +import 'dart:js_interop';
  3 +
  4 +import 'package:js/js.dart';
1 import 'package:mobile_scanner/src/enums/barcode_format.dart'; 5 import 'package:mobile_scanner/src/enums/barcode_format.dart';
  6 +import 'package:mobile_scanner/src/enums/camera_facing.dart';
  7 +import 'package:mobile_scanner/src/objects/barcode_capture.dart';
  8 +import 'package:mobile_scanner/src/objects/start_options.dart';
2 import 'package:mobile_scanner/src/web/barcode_reader.dart'; 9 import 'package:mobile_scanner/src/web/barcode_reader.dart';
  10 +import 'package:mobile_scanner/src/web/zxing/result.dart';
3 import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; 11 import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart';
  12 +import 'package:web/web.dart' as web;
4 13
5 /// A barcode reader implementation that uses the ZXing library. 14 /// A barcode reader implementation that uses the ZXing library.
6 final class ZXingBarcodeReader extends BarcodeReader { 15 final class ZXingBarcodeReader extends BarcodeReader {
@@ -10,10 +19,13 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -10,10 +19,13 @@ final class ZXingBarcodeReader extends BarcodeReader {
10 ZXingBrowserMultiFormatReader? _reader; 19 ZXingBrowserMultiFormatReader? _reader;
11 20
12 @override 21 @override
  22 + bool get isScanning => _reader?.stream != null;
  23 +
  24 + @override
13 String get scriptUrl => 'https://unpkg.com/@zxing/library@0.19.1'; 25 String get scriptUrl => 'https://unpkg.com/@zxing/library@0.19.1';
14 26
15 /// Get the barcode format from the ZXing library, for the given [format]. 27 /// Get the barcode format from the ZXing library, for the given [format].
16 - int getZXingBarcodeFormat(BarcodeFormat format) { 28 + static int getZXingBarcodeFormat(BarcodeFormat format) {
17 switch (format) { 29 switch (format) {
18 case BarcodeFormat.aztec: 30 case BarcodeFormat.aztec:
19 return 0; 31 return 0;
@@ -46,4 +58,132 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -46,4 +58,132 @@ final class ZXingBarcodeReader extends BarcodeReader {
46 return -1; 58 return -1;
47 } 59 }
48 } 60 }
  61 +
  62 + /// Prepare the [web.MediaStream] for the barcode reader video input.
  63 + ///
  64 + /// This method requests permission to use the camera.
  65 + Future<web.MediaStream?> _prepareMediaStream(
  66 + CameraFacing cameraDirection,
  67 + ) async {
  68 + if (web.window.navigator.mediaDevices.isUndefinedOrNull) {
  69 + return null;
  70 + }
  71 +
  72 + final capabilities = web.window.navigator.mediaDevices.getSupportedConstraints();
  73 +
  74 + final web.MediaStreamConstraints constraints;
  75 +
  76 + if (capabilities.isUndefinedOrNull || !capabilities.facingMode) {
  77 + constraints = web.MediaStreamConstraints(video: true.toJS);
  78 + } else {
  79 + final String facingMode = switch (cameraDirection) {
  80 + CameraFacing.back => 'environment',
  81 + CameraFacing.front => 'user',
  82 + };
  83 +
  84 + constraints = web.MediaStreamConstraints(
  85 + video: web.MediaTrackConstraintSet(
  86 + facingMode: facingMode.toJS,
  87 + ),
  88 + );
  89 + }
  90 +
  91 + final JSAny? mediaStream = await web.window.navigator.mediaDevices.getUserMedia(constraints).toDart;
  92 +
  93 + return mediaStream as web.MediaStream?;
  94 + }
  95 +
  96 + /// Prepare the video element for the barcode reader.
  97 + ///
  98 + /// The given [videoElement] is attached to the DOM, by attaching it to the [containerElement].
  99 + /// The camera video output is then attached to both the barcode reader (to detect barcodes),
  100 + /// and the video element (to display the camera output).
  101 + Future<void> _prepareVideoElement(
  102 + web.HTMLVideoElement videoElement, {
  103 + required CameraFacing cameraDirection,
  104 + required web.HTMLElement containerElement,
  105 + }) async {
  106 + // Attach the video element to the DOM, through its parent container.
  107 + containerElement.appendChild(videoElement);
  108 +
  109 + // Set up the camera output stream.
  110 + // This will request permission to use the camera.
  111 + final web.MediaStream? stream = await _prepareMediaStream(cameraDirection);
  112 +
  113 + if (stream != null) {
  114 + final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction(null, stream, videoElement) as JSPromise?;
  115 +
  116 + await result?.toDart;
  117 + }
  118 + }
  119 +
  120 + @override
  121 + Stream<BarcodeCapture> detectBarcodes() {
  122 + final controller = StreamController<BarcodeCapture>();
  123 +
  124 + controller.onListen = () {
  125 + _reader?.decodeContinuously.callAsFunction(
  126 + null,
  127 + _reader?.videoElement,
  128 + allowInterop((result, error) {
  129 + if (!controller.isClosed && result != null) {
  130 + final barcode = (result as Result).toBarcode;
  131 +
  132 + controller.add(
  133 + BarcodeCapture(
  134 + barcodes: [barcode],
  135 + ),
  136 + );
  137 + }
  138 + }).toJS,
  139 + );
  140 + };
  141 +
  142 + controller.onCancel = () async {
  143 + _reader?.stopContinuousDecode.callAsFunction();
  144 + _reader?.reset.callAsFunction();
  145 + await controller.close();
  146 + };
  147 +
  148 + return controller.stream;
  149 + }
  150 +
  151 + @override
  152 + Future<void> start(StartOptions options, {required web.HTMLElement containerElement}) async {
  153 + final int detectionTimeoutMs = options.detectionTimeoutMs;
  154 + final List<BarcodeFormat> formats = options.formats;
  155 +
  156 + if (formats.contains(BarcodeFormat.unknown)) {
  157 + formats.removeWhere((element) => element == BarcodeFormat.unknown);
  158 + }
  159 +
  160 + final Map<Object?, Object?>? hints;
  161 +
  162 + if (formats.isNotEmpty && !formats.contains(BarcodeFormat.all)) {
  163 + // Set the formats hint.
  164 + // See https://github.com/zxing-js/library/blob/master/src/core/DecodeHintType.ts#L45
  165 + hints = {
  166 + 2: formats.map(getZXingBarcodeFormat).toList(),
  167 + };
  168 + } else {
  169 + hints = null;
  170 + }
  171 +
  172 + _reader = ZXingBrowserMultiFormatReader(hints.jsify(), detectionTimeoutMs);
  173 +
  174 + final web.HTMLVideoElement videoElement = web.document.createElement('video') as web.HTMLVideoElement;
  175 +
  176 + await _prepareVideoElement(
  177 + videoElement,
  178 + cameraDirection: options.cameraDirection,
  179 + containerElement: containerElement,
  180 + );
  181 + }
  182 +
  183 + @override
  184 + Future<void> stop() async {
  185 + _reader?.stopContinuousDecode.callAsFunction();
  186 + _reader?.reset.callAsFunction();
  187 + _reader = null;
  188 + }
49 } 189 }