Showing
1 changed file
with
141 additions
and
1 deletions
| 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 | } |
-
Please register or login to post a comment